How GPU.js Maps Thread Coordinates to WebGL Textures
GPU.js executes general-purpose parallel computations in the browser
by compiling JavaScript into GLSL fragment shaders, using WebGL textures
as memory buffers. Because WebGL historically processes 2D images rather
than raw memory arrays, GPU.js must bridge user-defined logical thread
coordinates (1D, 2D, or 3D) and the underlying physical 2D texture
layout. It accomplishes this through a deterministic index linearization
process, mapping hardware-driven pixel positions
(gl_FragCoord) back to kernel thread indices, and
converting requested data reads into normalized UV texture
coordinates.
The Dimension Mismatch Problem
When you define a kernel in GPU.js, you specify output dimensions
such as [width], [width, height], or
[width, height, depth]. WebGL compute limitations require
output targets to be 2D framebuffers bound to 2D textures. Furthermore,
WebGL limits the maximum texture width and height (typically 4096 or
16384 pixels). If a 1D thread array exceeds this limit, or when 3D
thread spaces are declared, GPU.js cannot map logical axes directly to
raw texture dimensions. Instead, it must pack the logical domain into an
optimal 2D grid that conforms to hardware constraints.
Output Mapping: Reconstructing Thread Coordinates
During kernel execution, GPU.js draws a full-screen quad over the target framebuffer. Each fragment corresponds to a single execution thread. Within the compiled GLSL fragment shader, GPU.js determines which thread is currently executing by converting the pixel coordinate into a global index:
Pixel to Linear Index: The fragment shader uses the built-in variable
gl_FragCoord.xy. GPU.js flattens this 2D physical position into a 1D linear thread index: \[\text{index} = \lfloor \text{gl\_FragCoord.y} \rfloor \times \text{textureWidth} + \lfloor \text{gl\_FragCoord.x} \rfloor\]Linear Index to Logical Threads: Using integer arithmetic operations in GLSL, GPU.js reconstructs the user-facing
this.thread.x,this.thread.y, andthis.thread.zvalues:- 1D Kernels:
this.thread.x = index - 2D Kernels:
this.thread.x = mod(index, logicalWidth)this.thread.y = floor(index / logicalWidth)
- 3D Kernels:
this.thread.x = mod(index, logicalWidth)this.thread.y = mod(floor(index / logicalWidth), logicalHeight)this.thread.z = floor(index / (logicalWidth * logicalHeight))
- 1D Kernels:
Any threads whose linearized index falls outside the total logical
thread count execute an early discard instruction in the
shader to avoid calculating out-of-bounds padding values.
Input Mapping: Translating Thread Reads to UV Coordinates
When a kernel reads an input array or accesses data relative to thread coordinates, the process operates in reverse. Input arrays are pre-encoded into 2D WebGL textures (often packed into 32-bit floats or split across 8-bit RGBA channels).
To read an element from memory at a given logical index:
- Linearization: GPU.js calculates the 1D offset of the requested element using the target data's declared dimensions.
- 2D Physical Location: The 1D offset is converted into physical integer coordinates \((X_{\text{tex}}, Y_{\text{tex}})\) based on the input texture's allocated width: \[X_{\text{tex}} = \text{mod}(\text{index}, \text{inputTextureWidth})\] \[Y_{\text{tex}} = \lfloor \text{index} / \text{inputTextureWidth} \rfloor\]
- Normalized UV Coordinates: Because WebGL texture
lookups via
texture2D()expect normalized coordinates between0.0and1.0, GPU.js offsets the integer by0.5(targeting the center of the texel) and divides by the texture dimensions: \[U = \frac{X_{\text{tex}} + 0.5}{\text{inputTextureWidth}}\] \[V = \frac{Y_{\text{tex}} + 0.5}{\text{inputTextureHeight}}\]
Texel Centering and Precision
WebGL textures use nearest-neighbor interpolation during compute
passes. If a coordinate aligns precisely on a texel boundary,
floating-point rounding errors can cause the shader to read the
neighboring texel. By injecting the + 0.5 half-pixel offset
into the coordinate calculation, GPU.js guarantees that the texture
sampler always strikes the exact center of the target memory cell,
preventing computational drift across parallel operations.