How gpu.js Converts JavaScript to WebGL Shaders
This article explains the internal transpilation pipeline of gpu.js, detailing how standard JavaScript functions are converted into high-performance WebGL shader code. By parsing JavaScript into an Abstract Syntax Tree, inferring data types, mapping built-in functions, and injecting memory-management boilerplate, gpu.js bridges the gap between dynamic browser code and statically typed GLSL (OpenGL Shading Language) execution.
1. Function Stringification and AST Parsing
The process begins when a standard JavaScript function is passed into
the gpu.createKernel() method. Because JavaScript engines
execute functions natively rather than exposing raw GLSL, gpu.js must
deconstruct the source code:
- Stringification: The library calls
.toString()on the supplied JavaScript kernel function to retrieve its raw text representation. - Lexical Analysis and Parsing: Using a JavaScript parser (historically Acorn or a modified internal variant), gpu.js tokenizes the source code and constructs an Abstract Syntax Tree (AST). The AST represents the syntactic structure of the function, identifying variable declarations, assignments, conditionals, iterations, and return statements as distinct tree nodes.
2. Static Type Inference
GLSL requires strict, static typing, whereas JavaScript is dynamically typed. To generate valid shader code, gpu.js performs a static analysis pass over the AST:
- Argument Inference: When input parameters (such as arrays, matrices, or numbers) are passed into the kernel, gpu.js inspects their runtime types before shader compilation.
- Variable Propagation: The compiler walks through
the AST to determine the types of local variables based on the literals
assigned to them or the results of operations. For example, a numeric
literal like
0or1.5is tracked as a float (since GLSL handles floating-point math differently than integers). If a variable changes types or uses unsupported dynamic structures (like objects), the compiler throws an error.
3. Syntax and Math Mapping
Once the AST nodes are typed, gpu.js traverses the tree and translates JavaScript syntax into corresponding GLSL language constructs:
- Thread Indexing: The dynamic context variable
this.thread.x,this.thread.y, andthis.thread.zis mapped directly to GLSL calculations derived from fragment coordinates (gl_FragCoord). - Math Library Conversion: Standard
Mathmethods are mapped directly to native GLSL hardware intrinsics. For instance,Math.sin(x)translates tosin(x),Math.floor(x)translates tofloor(x), andMath.pow(x, y)becomespow(x, y). - Control Flow: Standard
if-elsebranches and standard countingforloops are rewritten into C-style GLSL equivalents. Loops must generally have static or predictable bounds so the GPU can manage execution paths.
4. Memory Layout and Texture Mapping
GPUs process large data parallelly by drawing to screen pixels or off-screen framebuffers. gpu.js handles data input and output via WebGL textures:
- Input Arrays to 2D Textures: Large 1D or 2D JavaScript numeric arrays are converted into WebGL textures, where pixel values (RGBA channels) store the numeric data.
- Array Indexing to Texture Lookups: Inside the
generated GLSL, array access like
A[y][x]is translated into a texture lookup function (texture2D) that calculates the normalized UV coordinates needed to sample the correct pixel from the data texture.
5. GLSL Assembly and WebGL Compilation
After transforming the core logic, gpu.js wraps the translated statements into a complete GLSL Fragment Shader template:
- Header Injection: Precision specifiers (e.g.,
precision highp float;) and uniform declarations for kernel arguments are injected at the top. - Main Function Insertion: The transformed kernel
body is placed inside
void main() { ... }. - Output Encoding: The kernel’s return value is
packed into the
gl_FragColorvector, encoding numerical results across the four color channels (RGBA) if floating-point textures are unsupported, or directly bound if supported.
Finally, this assembled GLSL string is passed to the browser's WebGL
context via gl.shaderSource() and compiled with
gl.compileShader(). The resulting WebGL program executes in
parallel across the GPU cores, returning the computed results directly
back to JavaScript.