Control flow
On this page

Control flow: familiar syntax, explicit GPU execution

TypeShade uses familiar TypeScript conditional and loop syntax, but the code executes on the GPU. Understand control flow in terms of compilable computation, not the full dynamic behavior of the JavaScript runtime.

1. Start from the TypeScript control-flow model

Syntax such as if/else and for is familiar, but every condition and loop in TypeShade must be lowerable to GPU code. Matching syntax does not imply that all JavaScript runtime semantics are available.

2. Branching

Use if/else to express calculation paths. Inside a branch, keep values and resources within the TypeShade GPU model.

@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. Loops

Write a for loop, a while loop, or for...of over an array. A for loop may count to a value known only at run time, such as a uniform field or the length of an array. An array's map, forEach, some, every and reduce compile too, each to a counted loop, and map takes an array with a fixed length.

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

4. A loop that runs as a kernel

An exported function that is not an entry point and takes an array with no size, array<T>, is a kernel function (Rule 8.22). Its array is the caller's, read and written in place. Each for at the top of its body is a loop the compiler may run on the GPU, one invocation per iteration, and a host file awaits the function like any import. No @compute, binding or global_invocation_id is written.

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)
}
}

The compiler runs a loop on the GPU when it proves that no iteration touches what another one does. The loop is a counted for, or a for...of, that steps by a constant and that nothing returns from or breaks out of. Each write lands on a name declared inside the loop, on an outer array or a texture at an index made from the loop's own, on an integer array the loop combines into at any index, or on a variable the loop combines with +=, *=, min, max, &, | or ^, which the GPU and the CPU fold in one tree order. The loop reads an array it writes only where it writes it and never reads a variable it combines, calls no function that writes a module variable or a binding, and has no barrier, no workgroup memory and no console call.

A loop the proof refuses runs on the CPU, and the program is correct either way. The proof never looks at a loop in an entry, a helper or a fragment shader, which stays per-invocation code. The compiler marks a refused loop with the warning TS8070: its first sentence names the line and the names in your code, and its second gives the remedy. At the pinned compiler the warnings read:

RuleWarning
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. Where JavaScript control flow stops

  • Write filter, find and the other array methods that change a length or search for an element as a loop.
  • Do not rely on general runtime objects.
  • Keep conditions and loop ranges based on GPU-compilable values.
  • A control-flow pattern valid in TypeScript is not automatically valid under TypeShade shader semantics.

Next: GPU types

Edit this page Report a problem