얼마 전에 Software Mansion에서 TypeGPU를 발표했고, 평소에 WebGL에 이런 비슷한 게 있었다면 편했겠다고 생각했던 터라 관심이 생겼다.
GPUDevice
를 요청하려면 다음과 같이 navigator.gpu.requestAdapter
로 GPUAdapter
를 만든 후에 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.");
// 재연결 혹은 어플리케이션 크래시
});
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 }
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),
});
import { arrayOf, f32 } from 'typegpu/data';
const ExampleArray = arrayOf(f32, 4);
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[])
를 사용할 수도 있다.
WebGPU에서 다루는 메모리는 크게 둘로 나눌 수 있다.
예를 들어 GPUTexture는 언제나 GPU에서 접근 가능한 메모리 상에 있다.
일부 기기 (특히 모바일 등)에서는 위 두 메모리가 통합되어 있는 경우도 있지만, 외장 그래픽스 카드를 가진 데스크탑 같은 경우에는 나뉘어 있을 것이다. WebGPU는 최대한 일반적인 경우를 생각하기 때문에, 이 경우에서는 나뉘어졌다고 가정해도 무방하다.
즉, JavaScript 상에서 데이터 버퍼를 만들어 GPU 메모리 상에 올리려면 다음과 같은 과정을 거쳐야 한다.
GPUBufferUsage.MAP_WRITE
flag가 필요)copyBufferToBuffer()
나 copyBufferToTexture()
를 통해 GPU 메모리 위에 복사한다. (이를 위해 버퍼에 GPUBufferUsage.COPY_SRC
flag가 필요)이 복잡한 과정을 간단하게 해주는 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,
});
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);