It compiled first try, then nothing worked Blitzkit developer jva.lol ported the ripple physics engine and arcade to WebAssembly for the browser, compiling both to wasm32-unknown-unknown with a single source change — one feature flag on getrandom — but the build then panicked or failed at runtime in four places. std::time::Instant panics in a browser and was replaced with web-time, run_app hangs the tab and was swapped for spawn_app, pollster::block_on deadlocks on a page, three shadow lookups using textureSampleCompare failed WebGPU validation and were changed to textureSampleCompareLevel, and a one-pixel canvas produced a flat teal and black recording until the code began reading the canvas's laid-out size times the pixel ratio every frame. The port now runs in a browser on a WebGPU device at four samples per pixel and 120 frames per second in a debug build, with a 7 MB wasm binary. It compiled first try, then nothing worked https://jva.lol/weblog/it-compiled-first-try/ I asked AI to draft this for me. I've edited it. Any surviving AI smells are my oversight. Everything blitzkit https://blitzkit.jva.lol 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 then-it-didnt-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 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-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 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.