The WebGPU backend finishes what 2.20.0 started — graphical mode and Math.random land, the deferred list shrinks to three — and kernels survive their bundler. One breaking change, and it is the correction of a 2.20.0 mistake, explained below.
Breaking: getPixels() returns a Promise under the async contract
We made a mistake in 2.20.0. The async contract's rule is that every result-bearing call returns a Promise uniformly, so the same code runs on every backend — that is the entire point of mode: 'async'. getPixels() should have followed that rule from the day the contract shipped, and it did not: it kept returning raw bytes on the GL and CPU backends even on an asyncMode kernel. The moment WebGPU graphical mode existed, the inconsistency became a portability bug: WebGPU readback is genuinely asynchronous, so its getPixels() must return a Promise, and the same kernel code behaved differently by backend.
2.21.0 corrects it: under the async contract (asyncMode: true or mode: 'async'), getPixels() returns a Promise on every backend — resolved immediately on GL and CPU, genuinely asynchronous on WebGPU. await kernel.getPixels() is the portable form, and the v3 migration guide now says so (step 5).
Who is affected: only code that calls getPixels() on an asyncMode kernel — a combination introduced in 2.20.0. Add await. Kernels outside the async contract are byte-for-byte unchanged.
WebGPU: graphical mode
graphical: true works on mode: 'webgpu'. The kernel stays a compute pass writing RGBA through this.color(...); a fixed fullscreen-triangle render pass presents it to the kernel's canvas. Orientation matches the GL backends (thread.y = 0 is the bottom row). Presentation needs no readback, so an un-awaited kernel() per animation frame works; getPixels() returns a Promise, as above.
Under mode: 'async', a graphical kernel binds to its backend at creation — a canvas is permanently committed to its first context type, so the backend must be decided before kernel.canvas is ever exposed. await GPU.isWebGPUAvailable() before createKernel to guarantee the probe has settled; either way the canvas never changes identity.
WebGPU: Math.random()
Implemented as PCG in integer WGSL — and therefore better than the GL implementation, not just equal to it: with randomSeed, the stream is bit-exact across runs and across drivers, a promise the GL backends' float-hash generator cannot make. Unseeded kernels draw differently every run, as on GL. Verified on real hardware: 4096 draws, mean 0.4997, full per-thread decorrelation, seeded streams byte-identical across repeat runs.
Kernels survive their bundler
Three fixes for code that works in development and breaks minified:
- esbuild/terser statement folding compiles everywhere —
c && (x = 1), comma chains hanging off areturn, ternaries of assignments, comma for-headers: a de-minification pass unfolds them back into statements before translation, on every backend. Ten fixtures pinned exactly as esbuild emits them. addFunctionsurvives name-stripping (#863):settings.namenow works for function sources,Function.prototype.namecovers inferred names, and object-formargumentTypeswhose keys match no parameter (the minifier renamed them) throw with a pointer to the position-based array form instead of silently computing wrong values.- WGSL reserved words can be helper names (#861): every user function is name-mangled unconditionally —
filter,get,set,type,selfand sixty more now work asaddFunctionnames on WebGPU.
Correctness fixes
- One kernel's arguments no longer clobber another's array constants (#862) — texture units are context state, but each kernel counted its own from zero; constants were bound once and silently read the other kernel's data from the second call on. Constants now rebind per run. This one was invisible without cross-checking results, and warmed-up harnesses only ever saw the corrupted state.
- A kernel accepts an
Arrayafter aFloat32Arrayand vice versa (#857) — same shape, same upload; constructor identity was never load-bearing. - A kernel local may shadow a constant's name (#858) —
const n = this.constants.nno longer dies on the CPU backend. - One banner in the minified bundles, not two — the minify step prepended a banner terser had already preserved.
Installing
npm install gpu.js@2.21.0<script src="https://unpkg.com/gpu.js@2.21.0/dist/gpu-browser.min.js"></script>