It compiled first try, then nothing worked
I asked AI to draft this for me. I've edited it. Any surviving AI smells are my oversight.
Everything blitzkit has ever drawn, you had to clone a repo and build it to see. The site has pictures, and the pictures became clips, but a clip of a physics engine is still somebody else playing.
So I pointed cargo at wasm32-unknown-unknown expecting a week of work.
It built. The engine and the arcade both, with no source change at all. One feature flag on getrandom, which two crates in the tree pull in and which refuses to build for a browser until you tell it where randomness comes from. That was the whole diff.
That is not because I planned ahead. The engine reads no files and spawns no threads, because everything it draws is worked out rather than loaded: the carpet, the wood grain, the stars, the water, the sounds. A renderer that opened textures off disk would have had a second job here. There are no compute shaders and no storage buffers either, so nothing in it is outside what a browser offers.
Then it didn’t run #
Three things compiled and panicked.
std::time::Instant is not a clock in a browser. It compiles, then panics the first time anything asks it the time, which here is once a frame, forever. web-time is a drop-in with the same name.
The event loop cannot be run. A browser gives a page one thread and takes it back between frames, so run_app, which never returns, hangs the tab before anything draws. spawn_app hands the loop to the browser’s own and returns.
A graphics device cannot be waited for. Asking for one is asynchronous everywhere, and only here does that matter: with one thread, blocking on the answer blocks the thing that would deliver it. pollster::block_on is correct on a machine and a deadlock in a page.
And one that ran and drew nothing #
A shadow may not work out its own mip level. All three shadow lookups used textureSampleCompare, which derives its level from neighboring pixels and so has to be reached by every lane in a group together. None of the three are, because a shadow is looked up inside if lambert <= 0.0, and whether a surface faces a light is about as non-uniform as a value gets.
naga lets that through. The browser does not. All three came back as validation errors, which invalidated the shader, which invalidated the mesh pipeline and both prepasses, so the page drew a black canvas and said nothing I could act on.
textureSampleCompareLevel is the same sample with the level pinned. A shadow map has one level, so no picture anywhere changes.
The pixel #
The last one I only found because I recorded it.
A canvas is not given a size by anything. A window on a machine has one before it has a surface. A canvas has whatever winit decided, which is one pixel square, brought up to the laid-out size by an observer in its own time.
A surface one pixel across is not a small picture. It is one pixel of the scene, stretched by the browser across however wide the page lays the canvas out. My recording is flat teal in one frame and flat black in the next. That is the single pixel watching the water move.
It drew correctly on the machine it was written on the whole time.
Two fixes did nothing: with_inner_size at creation doesn’t take, and reading window.inner_size() when the renderer arrives doesn’t help, because the machine that lost the race hasn’t learned a size to report. What works is asking the canvas every frame how wide it is laid out, times the pixel ratio, and acting when the answer changes. Two integers compared per frame.
Where it got to #
ripple runs in a browser. WebGPU device, four samples a pixel, keyboard, drag on the camera, 120 frames a second in a debug build. Dropping balls into the pool works and the water answers them.
The wasm is 7 MB release, 3.6 MB gzipped, before wasm-opt has seen it.