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:
- X Coordinate: Derived using the modulus of the width. \[\text{thread.x} = \text{linearIndex} \pmod{D_x}\]
- Y Coordinate: Derived by shifting out the X component, then taking the modulus of the height. \[\text{thread.y} = \left\lfloor \frac{\text{linearIndex}}{D_x} \right\rfloor \pmod{D_y}\]
- Z Coordinate: Derived by shifting out both the X and Y components. \[\text{thread.z} = \left\lfloor \frac{\text{linearIndex}}{D_x \times D_y} \right\rfloor\]
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.