BEND IN THE BROWSER

Bend programs running on every core of your machine, inside a web page: the same C runtime a native binary uses, compiled to WebAssembly. And, further down, three of them with the ! on the GPU through WebGPU.

Each demo below is a .bend file built with bend file.bend -o file.html from bendlang/bend#866: Emscripten compiles Bend's C runtime with -pthread -mtail-call, so the fork-join scheduler runs on a Web Worker per core and the segment machine's tail calls become wasm tail calls. A Window draws on a canvas, paced by the display; the line under it counts the frames the Bend program delivered, and its selector picks how many cores to run on (the page reloads), so you can see what the cores buy.

Demos

Mandelbrot — 512×512, the whole quadtree recomputed every frame with parallel calls. 60 fps on 10 cores, ~29 fps on one. Also on WebGPU: the selector under the canvas switches. Bendcraft — an endless first-person voxel world you can edit, 512×512: seeded noise terrain, sun shadows, pixel-art textures, ambient occlusion per vertex, fog. Now its own project, AdrielSantana/bendcraft, with its own page; the copy here is the 20 Sep state. WASD walk · space jumps · drag the mouse to look (or arrows) · click breaks · right click places (or J/L) · Esc quits. About 35 fps on 10 cores, 8 on one; the Metal build renders it in 3–7 ms, untouched or after 300 edits. Since Bend 2.0.22 the world is one array shared through the fork: a ring of 128×128 columns around you, refilled a row at a time as you walk, with your edits kept in a map so they are there when you come back. Voxel raycaster — 512×512 rays, DDA through a voxel grid, orbiting camera. 24 fps on 10 cores; the GPU version does 60 on Metal. Also on WebGPU, from the selector. Ray tracer — the upstream demo as is: 1024×768, ray-marched spheres on a checkered plane, hard shadows, its own fps in the corner. W/A/S/D fly · Q/E up/down · arrows turn/look · Esc quits. 19 fps on 10 cores, 4 on one; the Metal build does 60. Also on WebGPU, from the selector. Ray tracer, 512×384 — the same program with the camera scaled to a quarter of the pixels (source). 60 fps on 10 cores, 14 on one. Also on WebGPU, from the selector. Pong — the upstream demo. W/S and Up/Down move the paddles, Esc quits. 60 fps. Triangle — the upstream demo the web playground runs on one core. 60 fps.

Needs a browser with SharedArrayBuffer and wasm tail calls: Chrome 112+, Firefox 121+, Safari 18.2+. Measured on Chrome 153 on an Apple M5 (10 cores). GitHub Pages sends no COOP/COEP headers, so a small service worker (coi-serviceworker) adds them and reloads the page once on your first visit.

On the GPU

Three of the demos again, this time with the ! call running on the GPU through WebGPU; each of them has a selector under the canvas that switches between the CPU threads and WebGPU on the same demo. This is not a port of the Metal runtime (WGSL has no 64-bit integers, no untyped atomics, no device-scope fence, no forward-progress guarantee): it is a runtime written for the browser around what it does have. Terms are pairs of 32-bit words, a task is a node on the heap, a fork pushes its four children onto the next queue, and every handoff between lanes crosses a dispatch boundary: one indirect dispatch per fork level, one that works each subtree sequentially on a lane (the runtime's own sequential path, continuations on an explicit stack), one per join level, and one that walks the quadtree into pixels. Fifteen to twenty-one dispatches a frame in one command buffer, nothing read back. The scheduler, the heap and the quadtree are generic; the per-pixel code of each demo was translated from its Bend source by hand, because the compiler has no WGSL target yet.

Mandelbrot on WebGPU — the same 512×512 quadtree every frame. 0.31 ms a frame. Voxel raycaster on WebGPU — 512×512, the DDA and the 2×2 collapse as written. 1.8 ms a frame; the CPU page does 24 fps. Ray tracer on WebGPU, 1024×768 — the upstream demo at full size. W/A/S/D fly · Q/E up/down · arrows turn/look. 2.4 ms a frame; the CPU page does 19 fps.
ms per frameWebAssembly, 10 threadsWebGPU
Mandelbrot 512×5124–50.31
Voxel raycaster 512×51242 (24 fps)1.8
Ray tracer 512×384≤ 16 (60 fps, capped)0.7
Ray tracer 1024×76853 (19 fps)2.4

Bendcraft was on this list until 20 Sep as a port written by hand in WGSL, a measurement of what its world read cost: 1.3 ms a frame against 15 on Metal with 300 blocks placed. Bend 2.0.22 shares one array through the fork, the Metal build costs 4 ms in both worlds, and the game is written once, in Bend, so the port is gone.

GPU time of the compute pass from timestamp queries, median of 100 frames submitted back to back, Chrome 153 on the M5's Metal. The pages themselves show 60 fps because the display caps them, and between frames the GPU clocks down, so the per-frame time printed in that loop is a few times the table's. Each image is checked against a JavaScript rendering of the same pixel function: the Mandelbrot differs on 10 of 262144 pixels by one escape step (a fused multiply-add), the raycaster on 2 (a tie between two faces), the ray tracer on none. Needs WebGPU: Chrome 113+, Safari 26+, recent Firefox.

Numbers

A windowless 512×512 Mandelbrot, 50 iterations, summed to a checksum; time around the computation only, median of five frames. Same checksum in every cell.

ms per frame1 thread4 threads10 threads
JavaScript target (bun), what the web had530
WebAssembly in Chrome267–84–5
Native binary (clang)2266

So the browser runs at ~1.2x the native binary and scales with the cores the same way: 20x over the JavaScript target on one core, ~100x on ten. The GPU section above adds another 15–25x over ten cores on the heavy demos; the pull request's WebGPU section, written before it, lists the reasons a port of the Metal runtime is not possible, which is why the browser runtime is a new one.

Source

The compiler change: bendlang/bend#866 (eight files, no new ones). The Bend programs: bendlang/bend/demos for the ray tracer, Pong and Triangle, AdrielSantana/metal-bending for the Mandelbrot, the raycaster and Bendcraft. This page is the gh-pages branch of that repo.

Built by Claude (Anthropic) with Adriel Santana, September 2026.