TypeGPU

Keonwoo Kim·2024년 9월 15일
0

TypeGPU Logo

얼마 전에 Software Mansion에서 TypeGPU를 발표했고, 평소에 WebGL에 이런 비슷한 게 있었다면 편했겠다고 생각했던 터라 관심이 생겼다.

0. Device

GPUDevice를 요청하려면 다음과 같이 navigator.gpu.requestAdapterGPUAdapter를 만든 후에 adapter.requestDevice로 얻을 수 있다.

if (!navigator.gpu) {
  throw Error("WebGPU is not supported on this environment.");
}

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
  throw Error("Failed to request a GPU adapter.");
}

// WebGPU에서 사용하는 logical GPU device
let device: GPUDevice = await adapter.requestDevice();
device.lost.then(() => {
  console.error("Connection to the GPU device has been lost.");
  // 재연결 혹은 어플리케이션 크래시
});

1. 데이터 구조 정의

JavaScript에 Object가 있다면 low-level 언어들에는 보통 struct가 있다. TypeGPU를 이용하면 데이터 버퍼들을 쉽게 만들고 전달할 수 있다.

import { u32, vec3f, struct } from 'typegpu/data';
import tgpu from 'typegpu';

declare const device: GPUDevice; // 위에서 얻은 WebGPU device

// struct 만들기
const PlayerInfo = struct({
  position: vec3f,
  health: u32,
});
// ^? PlayerInfo: TgpuStruct<{ position: Vec3f; health: U32; }>

const buffer = tgpu
  // PlayerInfo struct 타입의 데이터 버퍼를 생성
  .createBuffer(PlayerInfo, {
    position: vec3f(1.1, 2.0, 0.0),
    health: 100,
  })
  // ^? TgpuBuffer<typeof PlayerInfo>

  // Unmanaged 버퍼로 만들어서 read/write operations에 쓸 수 있도록 함
  .$device(device);
  // ^? buffer: TgpuBuffer<typeof PlayerInfo> & Unmanaged

// 참고:
// interface Unmanaged {
//   readonly device: GPUDevice;
//   readonly buffer: GPUBuffer;
// }

// Raw WebGPU buffer
const gpuBuffer = buffer.buffer;
//    ^? gpuBuffer: GPUBuffer

// GPU에서 값을 불러오기
const value = await tgpu.read(buffer);
//    ^? value: Parsed<typeof PlayerInfo> = { position: vec3f, health: number }

1.1. Alignments

Alignment는 기본적으로 정의된 필드 중 가장 byteAlignment 값이 큰 것을 따라가지만, 다음과 같이 override도 가능하다.

import { struct, vec3u, vec3f, vec4f, bool, align, size } from 'typegpu/data';

const Boid = struct({
  position: align(32, vec3u),
  velocity: vec3f,
  color: vec4f,
  isActive: size(8, bool),
});

1.2. Array

import { arrayOf, f32 } from 'typegpu/data';

const ExampleArray = arrayOf(f32, 4);

2. Usage flags

WebGL 셰이더 코드를 짜 봤다면 uniform이 무엇인지는 알고 있을 것이다. vertex/fragment 셰이더에서 참조할 수 있는, 모든 vertex/pixel에 대해 고정된 변수이다.

또 WebGPU가 WebGL과 달라진 점 중 가장 큰 것은 vertex/fragment shader 외에 compute shader를 지원한다는 점이다. 즉 (메인 thread나 worker thread에서 돌려서) CPU에서는 오래 걸릴 작업을 GPU로 보내 빠르게 계산할 수 있다. Compute shader에서 참조할 수 있는 데이터를 넣어두는 데이터 버퍼가 필요할텐데, 이는 storage라고 한다.

TypeGPU에서는 TgpuBuffer의 메소드 .$usage(...flags: (tgpu.Uniform | tgpu.Storage | tgpu.Vertex)[])를 사용하여 mark할 수 있다.

// TgpuBuffer<U32> & Uniform & Storage & Unmanaged
const buffer = tgpu.createBuffer(u32)
  .$usage(tpu.Uniform, tgpu.Storage);
  .$device(device)

이 이외에 다른 usage flag를 쓰고 싶다면 .$addFlags(...flags: number[])를 사용할 수도 있다.

3. GPU 메모리 위에 데이터 버퍼를 쓰기

WebGPU에서 다루는 메모리는 크게 둘로 나눌 수 있다.

  • GPU에서 접근 가능한 메모리
  • CPU에서 접근 가능한 메모리 (GPU로 효율적으로 복사 가능)

예를 들어 GPUTexture는 언제나 GPU에서 접근 가능한 메모리 상에 있다.

일부 기기 (특히 모바일 등)에서는 위 두 메모리가 통합되어 있는 경우도 있지만, 외장 그래픽스 카드를 가진 데스크탑 같은 경우에는 나뉘어 있을 것이다. WebGPU는 최대한 일반적인 경우를 생각하기 때문에, 이 경우에서는 나뉘어졌다고 가정해도 무방하다.

즉, JavaScript 상에서 데이터 버퍼를 만들어 GPU 메모리 상에 올리려면 다음과 같은 과정을 거쳐야 한다.

  1. CPU에서 접근 가능한 메모리 (GPU로 효율적으로 복사 가능) 위에 임시 버퍼를 만든다.
  2. 해당 버퍼를 mapped buffer 버퍼로 만들어 JavaScript의 ArrayBuffer 인터페이스를 통해 쓸 수 있게 한다. (이를 위해 버퍼에 GPUBufferUsage.MAP_WRITE flag가 필요)
  3. 임시 버퍼를 destroy하기 위해 버퍼를 unmap한다.
  4. copyBufferToBuffer()copyBufferToTexture()를 통해 GPU 메모리 위에 복사한다. (이를 위해 버퍼에 GPUBufferUsage.COPY_SRC flag가 필요)
  5. 복사 후에 임시 버퍼를 destroy한다.

이 복잡한 과정을 간단하게 해주는 WebGPU helper 함수가 GPUQueue.writeBuffer(buf: ArrayBuffer)이다.

TypeGPU에서는 비슷한 기능을 tgpu.write 함수가 담당한다. 이 함수는 mapped buffer의 경우에는 unmap 없이 해당 buffer에 그대로 쓰고, unmapped buffer의 경우에는 writeBuffer를 이용하여 쓴다. TypeGPU로 GPUBuffer를 생성하면 GPUBufferUsage.COPY_DST 등의 map, copy와 관련된 usage flag는 자동으로 관리해 주기 때문에 넣지 않아도 된다.

const buffer = tgpu.createBuffer(PlayerInfo).$device(device);

// write(TgpuBuffer<typeof PlayerInfo>, { position: vec3f, health: number }): void
tgpu.write(buffer, {
  position: vec3f(1.0, 2.0, 3.0),
  health: 100,
});

4. 버퍼 내의 데이터 읽기

Unmanaged buffer 안의 데이터를 tgpu.read(buffer: TgpuBuffer & Unmanaged): Promise<...>로 읽어올 수 있다.

import { arrayOf, u32 } from 'typegpu/data';

const buffer = tgpu.createBuffer(arrayOf(u32, 10)).$device(device);

// data: number[]
const data = await tgpu.read(buffer);
  • 주어진 버퍼가 mapped buffer이면, unmap 하지 않고 읽기만 한다.
  • 주어진 버퍼가 unmapped buffer이면, map하고 데이터를 읽은 후에 다시 unmap한다.
    • 만약 map하는 데에 실패했으면, 임시 버퍼를 하나 만들어 그 버퍼에 데이터를 불러온 뒤 read가 끝난 후에 임시 버퍼를 버린다.

0개의 댓글