wgpu.js
v0.1.0
Published
WebGPU compute in pure JS, for Node and Browser!
Maintainers
Readme
wgpu.js
WebGPU compute from plain JavaScript functions.
Write a function that computes one output element, and wgpu.js compiles it to
WGSL, runs it across every element in parallel, and hands back a Float32Array.
It is gpu.js in spirit, targeting WebGPU
instead of WebGL.
import { GPU } from "wgpu.js";
const gpu = await GPU.create();
const add = gpu.createKernel(
function (a, b) {
return a[this.thread.x] + b[this.thread.x];
},
{ output: [1_000_000] },
);
const result = await add(left, right); // Float32Array(1000000)Status: MVP, browser only. The API will change. Node support is not built yet — it needs a decision on which WebGPU implementation to bind to. You can already use wgpu.js anywhere
navigator.gpuexists, and you can hand it a device you created yourself viaGPU.create({ device }).
Demos
Live at https://playforge-coding.github.io/wgpu.js/ (needs a browser with WebGPU):
- Benchmarks — vector add, matrix multiply against a CPU reference, mandelbrot, kernel chaining
- Game of Life — one kernel per generation, with the grid held on the GPU in between
- Self test — the numerical end-to-end suite, run against your own GPU
Install
npm install wgpu.jsWhy this is not a drop-in gpu.js replacement
- Kernel calls are async. WebGPU reads results back asynchronously, so
await kernel(...)instead ofkernel(...). - Results are flat by default. A
[width, height]kernel returns a singleFloat32Arrayin row-major order. PassreturnNested: trueif you want gpu.js-style rows. - Graphics output is not implemented. There is no
graphical: truemode; a kernel produces numbers, and you decide what to do with them.
Writing kernels
Inside a kernel you get:
| Expression | Meaning |
| ------------------------------- | ------------------------------------------------ |
| this.thread.x, .y, .z | Index of the element this invocation computes |
| this.output.x, .y, .z | The kernel's output size |
| this.constants.NAME | A value from the constants option |
| a[i], a[y][x], a[z][y][x] | Array arguments, indexed by their dimensionality |
| a.length | The outermost dimension of an array argument |
The return value becomes the output element for that thread.
Supported JavaScript
const / let / var, assignment and compound assignment, ++ / --,
arithmetic and comparison operators, && / || / !, the ternary operator,
if / else, for, while, do..while, break, continue, early return,
and most of Math (sqrt, pow, min, max, sin, log10, cbrt, hypot,
the Math.PI family, and so on).
Anything else is a compile-time error with the offending line quoted, rather than silently wrong output.
Not supported yet
Helper functions, arrays declared inside a kernel, bitwise operators, strings,
objects, closures over outer scope, Math.random, and integer types. Every value
is a 32-bit float.
Arguments
An argument can be:
| You pass | The kernel sees |
| ------------------------------------------------------- | ---------------------------------------- |
| number / boolean | a scalar (s) |
| Float32Array, Int32Array, Uint32Array, number[] | a 1D array (a[i]) |
| number[][] / Float32Array[] | a 2D array (a[y][x]) |
| number[][][] | a 3D array (a[z][y][x]) |
| { data, size: [w, h] } | a flat buffer treated as 2D or 3D |
| the result of kernel.pipe(...) | a 1D/2D/3D array that never left the GPU |
A kernel compiles lazily, once per distinct argument shape, and caches the pipeline. Changing argument values, the output shape, or constant values costs nothing; changing dimensionality compiles a second variant.
Chaining kernels
kernel.pipe(...) runs the kernel but leaves the result in GPU memory, so it can
feed the next kernel without a round trip:
const doubled = await double.pipe(input);
const result = await addOne(doubled); // input never came back to the CPU
doubled.destroy();API
GPU.isSupported()
true when navigator.gpu exists.
await GPU.create(options?)
Requests an adapter and device. Options: device, powerPreference,
requiredFeatures, requiredLimits, label.
gpu.createKernel(fn, settings?)
Settings:
| Option | Default | Meaning |
| --------------- | -------- | ----------------------------------------------------------------------------- |
| output | — | Output shape, [x], [x, y] or [x, y, z]. Required before the first call. |
| constants | {} | Numbers exposed as this.constants.* |
| returnNested | false | Reshape 2D/3D results into arrays of row views |
| name | "main" | Entry point name, used in errors and debug output |
| workgroupSize | 64 | Threads per workgroup |
| debug | false | Log the generated WGSL when a variant compiles |
The returned kernel is callable and also has pipe(), setOutput(),
setConstants(), compile(), destroy(), and a wgsl property holding the
most recently generated shader.
gpu.destroy()
Destroys every kernel created by this instance and then the device.
transpile(input)
The JS → WGSL translator on its own, with no GPU involved. Useful for tests, tooling, or generating shaders ahead of time.
Notes and limits
- Every value is
f32. Indices are computed in floats too, except when you index directly bythis.thread.*, which uses the integer thread id — so arrays larger than 2²⁴ elements are safe as long as you index them by thread. - 1D outputs larger than the per-dimension workgroup limit are dispatched across a 2D grid automatically; the self test covers 5,000,000 threads.
- A kernel serialises its own calls, so
Promise.all([k(a), k(b)])is safe but will not overlap. Use separate kernels for real concurrency. Math.roundis emitted asfloor(x + 0.5)to match JavaScript, which differs from WGSL's round-half-to-even.
Development
vp install
vp test # unit tests for the transpiler (no GPU needed)
vp check # format, lint, type check
vpr demo # dev server on /examples/index.html
vpr life # dev server on /examples/life.html
vpr selftest # dev server on /examples/selftest.htmlThe pages are:
examples/index.html— vector add, matrix multiply vs CPU, mandelbrot, kernel chainingexamples/life.html— Conway's Game of Life, one kernel per generation, with the grid held on the GPU between generationsexamples/selftest.html— numerical end-to-end checks against a real device, also readable headlessly fromglobalThis.__selftest
vp test runs everywhere; the pages above are how the GPU path gets verified,
since Node has no WebGPU.
Releasing
.github/workflows/publish.yml publishes to npm with trusted
publishing — OIDC, so no npm token
is stored anywhere. It runs on a v* tag, gates on vp check and vp test,
refuses to publish if the tag and package.json version disagree or the version
already exists, checks the tarball contents, and attaches build provenance.
To cut a release:
vpr release # bumpp: bumps the version, commits, tags and pushesPrereleases (0.2.0-beta.1) go to the next dist-tag automatically, so they
never take over latest.
One-time setup
- Publish
0.1.0manually with a token. npm cannot configure a trusted publisher for a package that does not exist yet (npm/cli#8544), so the first version has to go up the old way. Revoke the token afterwards. - On npmjs.com, go to the package → Settings → Trusted Publisher and enter
the GitHub org/user, the repository, and the workflow filename
publish.yml, then selectnpm publishunder allowed actions. - Every later release publishes with no credentials at all.
Two things to know if you change any of this:
- The workflow filename is part of the npm configuration. Renaming
publish.ymlbreaks publishing until npmjs.com is updated to match. - Trusted publishing does not work on self-hosted runners, and needs
id-token: writeplus Node ≥ 22.14. Publishing goes throughpnpm publishrather thannpm publish, becausedevEnginespins pnpm and npm refuses to run in that case; pnpm has supported OIDC properly since 11.1.3.
For an approval gate before a release goes out, add an environment: to the
publish job and set the matching environment name on npmjs.com.
Publishing the demos
.github/workflows/deploy-demos.yml runs vp check and vp test, builds the
demo site, and publishes it to GitHub Pages on every push to main. Pull
requests build but do not deploy.
It needs Settings → Pages → Source = GitHub Actions enabled once on the repository. The base path is derived from the repository name, so forks deploy to their own URL without editing anything.
To build the site locally:
vpr build:demos # -> dist-demos/
vp preview examplesNote that the deployed site is minified, and kernels reach wgpu.js through
Function.prototype.toString. The transpiler handles the statement shapes
minifiers produce (comma sequences, a && (x = 1), and conditionals used as
statements) — see the minified source tests.
License
BSD-3-Clause
