gpu.js grows a fourth compute backend: kernels now compile to WebAssembly — SIMD-vectorized, optionally threaded, running everywhere wasm runs, with no GPU and no headless-gl build. It slots into the automatic fallback chain one step above cpu, so kernels that used to land on plain JavaScript now land on something 2–20× faster without a code change. No breaking changes.
The WebAssembly backend
new GPU({ mode: 'webasm' }), or automatically whenever the GL backends are unavailable. The kernel translates to a hand-assembled wasm module — no Emscripten, no runtime dependency — with the same typing rules as the GL/WebGPU backends and Math.random as the same seeded PCG stream as WebGPU.
Three execution tiers, chosen automatically:
- Scalar wasm: the baseline, roughly 2× plain JavaScript on arithmetic-dense kernels.
- SIMD: four cells per step through wasm's 128-bit vectors, wherever the platform validates SIMD (all modern browsers and Node). Divergent control flow vectorizes through mask predication rather than bailing out — both sides of a branch execute blended, which still prices out ahead on real workloads.
runandrun_simdare bit-identical per cell, pinned by tests. - Threaded: under the async contract (
asyncMode: true), outputs of 4096+ cells split across a worker pool over one shared memory — each worker running the SIMD path. Pool size followshardwareConcurrency(cap it withpoolSize). NoAtomics.wait, no main-thread blocking; withoutSharedArrayBuffer(no cross-origin isolation) the async contract still holds, minus the threads.
Measured in plain Node (Apple M1 Max):
| Workload | cpu backend | headlessgl | webasm scalar | webasm SIMD | webasm threaded+SIMD |
|---|---|---|---|---|---|
| matmul 512×512 | 331.8 ms | 58.6 ms (5.7×) | 159.4 ms (2.1×) | 84.6 ms (3.9×) | 16.2 ms (20.5×) |
| 4M-element map | 6.9 ms | 15.9 ms (0.4×) | 12.1 ms (0.6×) | 7.3 ms (0.9×) | 3.5 ms (2.0×) |
| divergent piecewise, 1M cells | 6.3 ms | 4.0 ms (1.6×) | 6.5 ms (1.0×) | 4.7 ms (1.3×) | 2.9 ms (2.2×) |
On transfer-bound work the threaded tier beats not only cpu but headless WebGL — wasm shares memory with JavaScript, and a GL backend pays upload/readback on every call.
pipeline: true is accepted the way the cpu backend accepts it: results are plain typed arrays (fresh per call) that chain straight into downstream kernels. Graphical mode, kernel maps, and texture/image arguments degrade to cpu — and degradations are no longer silent anywhere: the console warning names the reason, and kernel.kernel.fallbackReason carries it queryably. Verified on real devices: iOS 16/17, Android 13, including scalar fallback on Safari 16.1 (no wasm SIMD) and threadless async on non-isolated pages.
Control flow now matches plain JavaScript everywhere
The webasm test suite pins every backend against plain-JS references, and that honesty audit found long-standing divergences in the established backends — all fixed:
- cpu (#865): an early
returninside a loop no longer falls through and lets later statements overwrite the result;do…whilekeeps its exact JS semantics (continuejumps to the test); assigning to a scalar argument stays per-cell instead of leaking into every later cell. - WebGL/WebGL2 (#867): the same
do…whilecontinuebug, fixed by rotating the loop emulation so the exit test sits where JavaScript puts it — the fix holds insideswitchlowerings and unbraced bodies; assigning to a scalar argument (a GLSL uniform) now works through a per-invocation shadow local, for Number, Integer and Boolean arguments alike. - All GL backends (#860):
for (i = 0, j = 1; …)— an expression init rather than a declaration — was a TypeError; it now hoists and runs. A loop counter assigned outside its header (an inner loop reusing the counter) is legal JavaScript and now computes JS-exact results on every backend, retiring a 2016-era "not assignable here" throw (#31). - WebGL/WebGL2 (#864):
return 1e30emitted1e+30.0, invalid GLSL. Integer-valued literals of 1e21 and beyond — ray-marcher sentinels — now compile, from kernel bodies andsetConstantsalike.
Reliability
- Wasm memory lifetime is bounded (#870): each size signature instantiates over its own
WebAssembly.Memory, which is invisible to JS heap accounting; the per-kernel module cache is now LRU-bounded (moduleCacheLimit, default 8), evicted and destroyed entries are scrubbed down to the worker-side instantiations, and a worker that dies is terminated, not just retired — an uncaught worker exception does not kill the thread, and the zombie pinned every memory it held. - A GL texture handed to an already-built webasm kernel switches kernels and degrades cleanly instead of crashing; the fallback cpu kernel can itself switch on later argument-type changes.
- Where the backend runs, it is right: the gpu.rocks 30-workload suite cross-checks every result against plain-JS oracles — zero wrong results. Two workloads that measured 20–32× slower than plain JS turned out to be pricing the gather-for-scatter algorithm rewrite itself, not the compiler: the cpu backend runs within 2% of hand-written JavaScript of the same transposed algorithm (#869), now documented in the README.
Installing
npm install gpu.js@2.22.0<script src="https://unpkg.com/gpu.js@2.22.0/dist/gpu-browser.min.js"></script>