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:

  1. 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\]

  2. Linear Index to Logical Threads: Using integer arithmetic operations in GLSL, GPU.js reconstructs the user-facing this.thread.x, this.thread.y, and this.thread.z values:

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

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:

  1. Linearization: GPU.js calculates the 1D offset of the requested element using the target data's declared dimensions.
  2. 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\]
  3. Normalized UV Coordinates: Because WebGL texture lookups via texture2D() expect normalized coordinates between 0.0 and 1.0, GPU.js offsets the integer by 0.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.