CPU와 GPU
TypeShade 파일은 편집기에게는 TypeScript이고 컴파일러에게는 GPU 프로그램입니다. 언어 가이드가 적어 둔 제약은 취향의 문제가 아닙니다. 하나하나가 하드웨어의 동작에서 나오므로, 이 페이지는 사실을 먼저 적고 규칙을 그다음에 적습니다.
인보케이션
TypeScript 함수는 호출할 때 한 번 실행됩니다. 진입점은 다릅니다. GPU가 부르며, 한 번 그릴 때 버텍스마다 한 번, 프리미티브가 덮는 프래그먼트마다 한 번, 디스패치의 작업 항목마다 한 번 실행됩니다. 이렇게 한 번 실행되는 단위를 인보케이션이라고 부릅니다. 인보케이션은 서로 나란히 돌아가며 서로의 값을 읽지 못합니다. 자기가 몇 번째인지 알려 주는 것도 함수 바깥에는 없습니다.
@compute([64, 1, 1])export function paint(@builtin("global_invocation_id") gid: vec3u) { pixels[gid.x] = camera.pos.x}여기에서 규칙이 나옵니다. 스테이지 데코레이터가 셋 가운데 어느 쪽이 부르는지 밝히고, @builtin(...) 매개변수가 인보케이션에게 자기 자리를 알려 줍니다. 데코레이터는 셰이더 스테이지가, 매개변수는 함수가 적어 두었습니다.
메모리
인보케이션은 레지스터와, 호스트가 그리기 전에 바인딩해 둔 버퍼와 텍스처 위에서 움직입니다. 그 아래에 힙이 없으므로 셰이더는 메모리를 할당할 곳도, 길이가 늘어나는 배열도, 문자열을 만들 방법도 없습니다. TypeShade 파일의 클래스는 GPU 구조체의 바이트 배치를 설명하고, 그 바이트는 호스트가 채웁니다.
class Camera { view: mat4 pos: vec3}
declare const camera: uniform<Camera>여기에서 규칙이 나옵니다. new는 정체성을 가진 객체가 아니라 값을 만들고, 클래스 필드의 순서가 호스트가 쓰는 바이트의 레이아웃이 되며, 리소스는 모두 declare로 들어옵니다. 구조체 작성 인터페이스는 타입이, 선언은 리소스가 적어 두었습니다.
호출
셰이더에는 돌아갈 호출 스택이 없고, 호출 그래프는 드라이버가 보기 전에 평탄화됩니다. 자기 자신을 직접 부르거나 다른 함수를 거쳐 부르는 함수는 평탄화할 대상을 남기지 못합니다.
여기에서 규칙이 나옵니다. 재귀는 컴파일러가 거부하고, 헬퍼 함수는 컴파일러가 끝까지 따라갈 수 있는 평범한 함수입니다. 호출이 무엇일 수 있는지는 함수가 적어 두었습니다.
루프
한 스테이지의 인보케이션들은 루프를 함께 지나가고, 먼저 빠져나온 쪽은 나머지를 기다립니다. 횟수를 세는 루프는 본문이 바꾸지 않는 상한과 카운터를 비교하며 그 상한 쪽으로 나아가고, 컴파일러는 바로 이 점을 검사합니다. 상한 자체는 프로그램이 실행 중에 알게 되는 값이어도 됩니다.
for (let i = 0; i < 4; i++) { pixels[i] = pixels[i] + camera.pos.x}여기에서 규칙이 나옵니다. for는 본문이 움직이지 않는 상한까지 세고, while은 조건이나 break가 정한 곳에서 끝납니다. 어떤 조건과 반복이 컴파일되는지는 제어 흐름이 적어 두었습니다.
값 타입
GPU 레지스터의 폭과 배치는 셰이더를 컴파일할 때 정해집니다. 그래서 변수는 선언된 자리부터 스코프 끝까지 값 타입 하나를 담습니다. 값 타입 두 개를 합친 유니온도 없고, 실행 중에 둘 사이를 고를 방법도 없습니다.
let value: f32 = 1let index: u32 = 0let direction: vec3 = vec3(0, 1, 0)여기에서 규칙이 나옵니다. 모든 값은 f32, u32, vec4처럼 적어 둔 GPU 타입을 가집니다. 타입 작성 인터페이스는 타입이, 값 자체는 GPU 타입이 적어 두었습니다.
사실과 규칙
위의 사실들과 거기에서 나오는 규칙, 그리고 그 규칙을 적어 둔 언어 가이드의 페이지입니다.
| GPU가 하는 일 | TypeShade가 요구하는 것 | 적어 둔 곳 |
|---|---|---|
| 버텍스, 프래그먼트, 작업 항목마다 진입점을 한 번씩 부릅니다 | 진입점에 스테이지 데코레이터를 달고, builtin 입력마다 매개변수를 둡니다 | 셰이더 스테이지 |
| 인보케이션에 레지스터와 바인딩된 리소스를 주고, 그 아래에 힙은 없습니다 | 늘어나는 배열도 문자열도 없습니다. new는 값을 만들고, 클래스는 레이아웃이며 리소스는 declare입니다 | 리소스 |
| 호출 스택 없이 실행합니다 | 컴파일러가 평탄화할 수 있는 호출 그래프를 요구하므로 재귀가 없습니다 | 함수 |
| 한 스테이지의 인보케이션들을 루프에 함께 통과시킵니다 | 컴파일러가 읽을 수 있는 루프 상한을 요구합니다 | 제어 흐름 |
| 레지스터 하나에 폭이 정해진 값을 담습니다 | 변수마다 값 타입 하나를 적어 둡니다 | GPU 타입 |
더 읽을 자료
- WGSL 명세에 이 사실들이 나온 실행 모델이 적혀 있습니다. 인보케이션이 무엇인지, 무엇을 담을 수 있는지도 그곳에 있습니다.
- MDN WebGPU API는 같은 모델을 브라우저 쪽에서 JavaScript 개발자를 위해 풀어 놓은 문서입니다.
이 경로의 다음 글은 파이프라인입니다. 스테이지마다 무엇을 건네받고 무엇을 만들어 내는지 다룹니다.