제어 흐름
이 페이지에서

Control flow: 익숙한 흐름, 명시적인 GPU 실행

TypeShade는 TypeScript의 조건문과 반복문 표면을 활용하지만, 코드는 GPU에서 실행됩니다. 따라서 JavaScript runtime의 동적 동작이 아니라 컴파일 가능한 계산을 기준으로 제어 흐름을 이해해야 합니다.

1. TypeScript의 흐름에서 출발하기

if/else와 for 같은 구문은 익숙하지만, TypeShade에서는 각 조건식과 반복식이 GPU 코드로 낮아질 수 있어야 합니다. 문법이 같다는 것이 JavaScript runtime semantics 전체를 가져온다는 뜻은 아닙니다.

2. 조건 분기

if/else는 계산 경로를 표현합니다. 분기 안에서도 TypeShade가 이해하는 값과 resource만 사용합니다.

@compute([64, 1, 1])
export function paint(@builtin("global_invocation_id") gid: vec3u) {
if (camera.pos.x > 0) {
pixels[gid.x] = 1
} else {
pixels[gid.x] = 0
}
}

3. 반복

for 루프와 while 루프, 배열을 도는 for...of를 씁니다. for 루프는 유니폼 필드나 배열의 길이처럼 실행 중에야 정해지는 값까지 셀 수 있습니다. 배열의 map, forEach, some, every, reduce도 저마다 카운트 루프로 컴파일되며, map은 길이가 정해진 배열에 씁니다.

for (let i = 0; i < 4; i++) {
pixels[i] = pixels[i] + camera.pos.x
}

4. 커널로 도는 루프

진입점이 아니면서 크기가 없는 배열 array<T>를 받는 내보낸 함수는 커널 함수입니다(규칙 8.22). 그 배열은 호출한 쪽의 것이며, 함수는 그 자리에서 읽고 씁니다. 본문 최상위의 for는 저마다 컴파일러가 GPU에서 반복 하나당 호출 하나로 돌릴 수 있는 루프이고, 호스트 파일은 다른 가져오기처럼 이 함수를 await로 호출합니다. @compute나 바인딩, global_invocation_id는 쓰지 않습니다.

export function render(k: vec4, size: u32, out: array<f32>) {
for (let i: u32 = 0; i < size * size; i++) {
const p = vec2(f32(i % size), f32(i / size)) / f32(size)
out[i] = k.x * sin(p.x * k.y) + k.z * cos(p.y * k.w)
}
}

컴파일러는 어떤 반복도 다른 반복이 건드리는 것을 건드리지 않는다고 증명할 때 루프를 GPU에서 돌립니다. 루프는 상수만큼 나아가는 카운트 for나 for...of여야 하고, 안에서 반환하거나 빠져나가는 곳이 없어야 합니다. 쓰기는 루프 안에서 선언한 이름, 루프 자신의 인덱스로 만든 자리의 바깥 배열이나 텍스처, 아무 인덱스에나 합쳐 넣는 정수 배열, 또는 +=, *=, min, max, &, |, ^로 합치는 변수에만 합니다. 합치는 변수는 GPU와 CPU가 같은 트리 순서로 접습니다. 루프는 자신이 쓰는 배열을 쓰는 자리에서만 읽고 합치는 변수는 읽지 않으며, 모듈 변수나 바인딩을 쓰는 함수를 부르지 않고, 배리어와 워크그룹 메모리, console 호출이 없어야 합니다.

증명이 받아들이지 않은 루프는 CPU에서 돌며, 어느 쪽이든 프로그램은 올바르게 동작합니다. 진입점이나 헬퍼, 프래그먼트 셰이더 안의 루프는 증명 대상이 아니며, 지금처럼 호출마다 도는 코드로 남습니다. 컴파일러는 거부한 루프에 경고 TS8070을 붙입니다. 첫 문장은 해당 줄과 코드에 쓴 이름을 짚고, 둘째 문장은 고치는 방법을 알려 줍니다. 고정된 컴파일러에서 경고는 다음과 같습니다.

규칙경고
R1This loop runs on the CPU because it is a while loop, whose trip count is known only when it ends. A for loop over a count runs on the GPU.
R1This loop runs on the CPU because "stride /= 2" does not step through a range of indices. Step by adding a constant.
R2This loop runs on the CPU because line 9 returns from inside it, so whether an iteration runs depends on the ones before it. Record the result in an array and read it after the loop.
R3This loop runs on the CPU because line 49 writes "nearest", which the next iteration reads. Declare it inside the loop, or combine it with one of += *= min max & | ^.
R3This loop runs on the CPU because line 4 writes "b[idx[i]]", an element two iterations can share. Write at an index made from "i".
R4This loop runs on the CPU because line 9 reads "out[i - 1]", which another iteration writes. Read from an array the loop does not write.
R5This loop runs on the CPU because line 7 calls "tally", which writes "calls". Return the value from "tally" and combine it in the loop instead.
R6This loop runs on the CPU because line 5 calls console.log, whose lines would print in another order on the GPU. Log after the loop.

5. JavaScript와의 경계

  • filter, find처럼 길이를 바꾸거나 원소를 찾는 배열 메서드는 루프로 씁니다.
  • 일반 runtime 객체에 의존하지 않습니다.
  • 조건과 반복은 GPU에서 계산 가능한 값과 범위로 제한합니다.
  • TypeScript에서 유효한 제어 흐름이라고 해서 TypeShade shader semantics에서도 자동으로 유효한 것은 아닙니다.

다음: GPU types

이 페이지 편집 문제 보고