A page target: -o file.html builds the runtime as WebAssembly, a worker a core, and a Window on its canvas - #866
AdrielSantana wants to merge 1 commit into
Conversation
9f94119 to
e948bfe
Compare
|
Rebased onto 2.0.22 (94ee9ba) and squashed to one commit. The corpus now grows in place on the cores, which wasm32 cannot do, so there it is one memalign'd block of 1 GiB, asked for no place and never grown; Written by Claude (Anthropic) with Adriel Santana driving. |
|
The WebGPU follow-up, a WGSL lane emitted from the same segments, is #920. |
e948bfe to
e7ca129
Compare
|
Rebased onto 2.0.23 (fd7d9e6): no conflicts, |
|
Ready for review. What it costs the capped files, against main today: If a target the team does not run is more than the repo should carry, the branch offered in #891 works for me: create a |
|
A correction to my comment above: Rebased onto 2.0.24 (e52cda4): no conflicts, and the gate is 45 / 46. The one failure is the cap: Checked on Bendcraft with the rebased tree: the page it builds is byte-identical to the one 2.0.23 built, 30 fps on ten workers. |
e7ca129 to
b20ce2f
Compare
…er a core, and a Window draws on its canvas (Emscripten) The page shows its frames per second and picks its thread count from a selector, which reloads with ?threads=N since the runtime sizes its pool at start; it survives a hidden tab and a button released off the canvas. Every host passes a segment the Env's two words: a tail call on wasm pops the frame a struct is passed through, so -O2 and up read a stale Env, and the compute segments compile to the same arm64 instructions either way. window_k and window_host, in the window_open effect that every window effect follows, fill a frame on the host for Linux and the page. On wasm32 a map is a commit, so the corpus is one memalign'd block of 1 GiB, asked for no place and never grown, and a stack is 16 MiB; mmap would memset what is zero already. Rebased onto 2.0.22 as one commit.
b20ce2f to
0bafcc5
Compare
|
Rebased onto 2.0.25 (0bafcc5). Same eight files; the gate is 45/46: One line in the rebase is new, in |
Live: https://adrielsantana.github.io/bendcraft/ (a first-person voxel game, this target on ten workers) and https://adrielsantana.github.io/metal-bending/ (Mandelbrot, Pong, the triangle, a raycaster,
demos/app_ray_tracer_3d), served withcoi-serviceworkerfor the headers.What
bend file.bend -o file.htmlbuilds the program's C, unchanged, with Emscripten intofile.html,file.jsandfile.wasm: the same runtime as WebAssembly, a Web Worker per core,!on the cores. AWindowdraws on the page's canvas and reads its keyboard and mouse; frames are paced by the display (requestAnimationFrame), as the Mac's display sync.The JavaScript target runs on one core, and so does the playground in #859 (it runs the JS target in a Worker). The C runtime already compiles as C11 with pthreads and 32-bit atomics, so the browser gap was the target, not the language: with
-pthread -mtail-callthe segment machine'smusttailcalls become wasm tail calls, the pool's mutex/condvar/atomics becomeSharedArrayBuffer+Atomics, andpreserve_noneis ignored (wasm has no callee-saved registers).Measured
Apple M5 (10 cores), Chrome 153, Emscripten 6.0.9, Node 24. A windowless 512×512 Mandelbrot, 50 iterations, quadtree built with parallel calls, summed to a checksum;
IO.now()around the computation only, five frames with varying input, median. Every cell has the same checksum.With a window, frames drawn in 5 s (300 display ticks), headless Chrome; 288 is the 60 Hz ceiling:
demos/app_pong_game_2dThe live pages, ten workers, frames per second the page counts, with the same program's native frame on Metal where it was measured (Bend 2.0.23):
demos/app_ray_tracer_3d, 1024×768Bendcraft's frames are its native build's: the bench's 30 checksums are the same from this tree and from the shipped 2.0.23. The first frame costs ~70 ms extra while the Workers start.
Try it
brew install emscripten # or emsdk; 3.1.35+ for tail calls bun bend2/main.ts demos/app_pong_game_2d/main.bend -o pong/pong.htmlThreads need cross-origin isolation, so the page must be served with two headers (
file://cannot work):Then open
http://127.0.0.1:8000/pong.html(the selector under the canvas, or?threads=1, to compare). On GitHub Pages, which sets no headers, thecoi-serviceworkertrick works.Changes
bend2/main.ts:cli_build_web(theemccline) andPAGE, the page it writes: the canvas, a thread-count selector (it reloads with?threads=N, clamped to the cores, since the runtime sizes its pool at start), the frames per second the program delivered, a line per print; help text. 8085 ttok of 10000.bend2/comp.ts: a segment takes theEnv's two words instead of the struct, on every host (WL_SIG/WL_ALL,WL_OPENrebuildse): wasm passes a struct by a pointer into the caller's frame, and areturn_callpops that frame, so from-O2up a segment read a staleEnv(heap_alloc_misstrapped on an unaligned atomic withe.mem= garbage;-O1only worked by luck). On arm64 and x86-64 a two-pointer struct already travels in two registers, and the benchmark's compute segments compile to the same instructions either way, so the signature is one for all rather than a wasm#ifdef. Also the runtime's sizes for wasm32 (STACK_LEN,CORPUS_LEN,CORPUS_MIN: wasm commits what it maps, the Loc space is 4 GiB, and the corpus, which grows in place on the cores, is onememalign'd block of 1 GiB there, asked for no place and never grown);pool_tryasmemalignon wasm, since Emscripten'smmapismemalignplus amemsetof memory that is zero already (140–230 ms per page load for the 1 GiB corpus, measured). 64972 ttok of 65000.bend2/effs/window_{open,frame,close,set_title}.c: an#elif defined(__EMSCRIPTEN__)branch each, between the Linux one and the stub, withBendWinunder the X11's#ifndef BendWinguard. The page's side is twoEM_JSfunctions inwindow_open.c(the canvas and its listeners; a frame's blit and event drain on the next animation frame, or a 16 ms timer while the tab is hidden), called on the main thread throughMAIN_THREAD_EM_ASM; the program's thread waits on a futex the page signals. Key codes, buttons and the five-word events are the Mac's, as on Linux;mouseupis heard on the window, so a button released off the canvas still comes up, and a button past the middle one is dropped.window_k(the quadtree's depth) andwindow_host(the pixel walk) live inwindow_open.cand the Mac, Linux and wasm frames share them; the window's size check is inwindow_open_run, out of the three platforms. 3620 / 3981 / 378 / 419 ttok of 4000.guide/GUIDE.md,README.md: the target, the headers, the Emscripten need.bun gates/repo.ts: PASS 46 / 46 on 2.0.23. No new files; nothing outside the allow list.Since it was written
The PR was written against 2.0.16 and rebased through 2.0.22 and 2.0.23 (one commit, squashed). What the runtime's own moves changed here: 2.0.22's corpus grows in place on the cores, which wasm32 cannot do, so there the corpus is one block asked for no place and never grown;
window_kandwindow_hostmoved fromcomp.tsintowindow_open.c, which every window effect follows, to keepcomp.tsunder its cap; the two-wordEnvheld. What did not change: the page, the workers, the canvasWindow, the numbers.What a next step keeps
coi-serviceworkeron a static host, the canvasWindowwith the Mac's key codes. A WGSL lane (A WGSL lane:-o x.htmlwith its!on WebGPU #920) would keep all of it and add a device path the way the Mac has one: the page dispatches the!'s frontier on WebGPU instead of the pool. The reasons a port of the Metal runtime cannot be that lane, and a runtime built for WebGPU with its measurements (0.3 ms for the Mandelbrot frame, 1.8 for the raycaster's), are in A WGSL lane:-o x.htmlwith its!on WebGPU #920.window_hostcould fork, as the Linux frame could.__EMSCRIPTEN__.Limits, stated
-sINITIAL_MEMORY), no growth, so theHEAP*views the page's JS reads stay valid; corpus 1 GiB, halved down to 256 MiB if the allocation fails; 16 MiB stack per thread (native reserves 2 GiB lazily; wasm cannot). iOS Safari gives a page far less than 2 GiB; untested there.sigaltstack/SIGSEGVare stubs, so a host recursion past 16 MiB corrupts memory instead of failing. AWL_ROOM-style check on the host under__EMSCRIPTEN__is the follow-up.window_hostwalks the quadtree on the program's thread, single-threaded (~3 ms at 512²), as Linux without CUDA; the Mac does it on Metal. It could fork.#bend); the page keeps at most 1024 events for a frame that has not come.ENOTSUP), files hit Emscripten's in-memory FS (Bendcraft's save lasts until the tab closes), sockets are untested. AudioWorklet, OPFS and WebSocket are the paths.navigator.hardwareConcurrencyat 8; Firefox and Safari 18.2+ have the needed wasm features (threads, tail calls) but only Chrome was measured.!runs on the cores; the GPU in the browser is A WGSL lane:-o x.htmlwith its!on WebGPU #920.How this was made
Written by Claude (Anthropic) with Adriel Santana driving, as this repo's runtime was; every number above was measured on his M5. The diff had a review pass (bugs, reuse, simplification, efficiency, depth of each change) before the squash.