GPU.js 3D Thread ID Calculation Explained

gpu.js enables JavaScript functions to execute across parallel hardware by transpiling JavaScript kernels into WebGL shader code. Because standard WebGL relies on a 2D rendering pipeline rather than native 3D compute dispatches, gpu.js handles arbitrary three-dimensional volumes by linearizing the 3D execution space into a 2D texture, then deriving individual this.thread.x, this.thread.y, and this.thread.z coordinates inside the compiled GLSL fragment shader using arithmetic deconstruction.

Mapping 3D Volumes to 2D Surfaces

When a kernel is declared with a 3D output dimension like [width, height, depth] (or \([D_x, D_y, D_z]\)), gpu.js determines the total number of required execution units:

\[\text{Total Threads} = D_x \times D_y \times D_z\]

Standard WebGL fragment shaders execute over a two-dimensional grid of pixels (texels). To accommodate the 3D volume, gpu.js provisions a 2D offscreen rendering target whose dimensions \((\text{texWidth}, \text{texHeight})\) are sized to hold all threads up to the hardware’s maximum texture size limit.

Deriving the Linear Index from gl_FragCoord

Each execution unit runs as an isolated fragment shader instance. The GPU provides the current fragment's 2D screen-space coordinate via the built-in GLSL variable gl_FragCoord.

gpu.js computes an absolute, one-dimensional execution index from this 2D coordinate:

int x = int(floor(gl_FragCoord.x));
int y = int(floor(gl_FragCoord.y));
int linearIndex = y * texWidth + x;

This single integer value represents the unique thread rank within the flattened sequence of all threads in the allocated 3D volume.

Deconstructing the Linear Index into 3D Coordinates

Once the linearized index is established, the compiled shader converts it back into 3D volume coordinates according to the user-defined dimensions \([D_x, D_y, D_z]\). gpu.js performs modulo arithmetic and division inside the shader to recreate the three spatial axes:

In generated GLSL, these calculations are implemented either with integer arithmetic (in WebGL 2 mode) or through high-precision floating-point approximations using mod() and floor() (in WebGL 1 mode) to prevent truncation errors.

Handling Non-Uniform and Arbitrary Dimensions

Because the bounding 2D texture might allocate more total pixels than the exact volume requirement (\(D_x \times D_y \times D_z\)), gpu.js injects a boundary guard at the start of the shader. If linearIndex >= Total Threads, the fragment is immediately discarded:

if (linearIndex >= totalThreads) {
    discard;
}

This boundary check ensures that arbitrary, non-power-of-two, or asymmetric volumes (such as \(17 \times 5 \times 101\)) evaluate strictly within valid index ranges without out-of-bounds memory accesses or erroneous calculations.