{"slug": "it-compiled-first-try-then-nothing-worked", "title": "It compiled first try, then nothing worked", "summary": "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.", "body_md": "# [It compiled first try, then nothing worked](https://jva.lol/weblog/it-compiled-first-try/)\n\nI asked AI to draft this for me. I've edited it. Any surviving AI smells are my oversight.\n\nEverything [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.\n\nSo I pointed cargo at `wasm32-unknown-unknown` expecting a week of work.\n\nIt 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.\n\nThat 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.\n\n## Then it didn’t run\n[#](#then-it-didnt-run)\n\nThree things compiled and panicked.\n\n`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.\n\nThe 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.\n\nA 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.\n\n## And one that ran and drew nothing\n[#](#and-one-that-ran-and-drew-nothing)\n\nA 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.\n\nnaga 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.\n\n`textureSampleCompareLevel` is the same sample with the level pinned. A shadow map has one level, so no picture anywhere changes.\n\n## The pixel\n[#](#the-pixel)\n\nThe last one I only found because I recorded it.\n\nA 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.\n\nA 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.\n\nIt drew correctly on the machine it was written on the whole time.\n\nTwo 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.\n\n## Where it got to\n[#](#where-it-got-to)\n\nripple 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.\n\nThe wasm is 7 MB release, 3.6 MB gzipped, before `wasm-opt` has seen it.", "url": "https://wpnews.pro/news/it-compiled-first-try-then-nothing-worked", "canonical_source": "https://jva.lol/weblog/it-compiled-first-try/", "published_at": "2026-11-10 02:29:31+00:00", "updated_at": "2026-11-10 02:50:42.281559+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-tools", "generative-ai"], "entities": ["blitzkit", "jva.lol", "WebAssembly", "WebGPU", "winit", "naga", "web-time", "ripple"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/it-compiled-first-try-then-nothing-worked", "markdown": "https://wpnews.pro/news/it-compiled-first-try-then-nothing-worked.md", "text": "https://wpnews.pro/news/it-compiled-first-try-then-nothing-worked.txt", "jsonld": "https://wpnews.pro/news/it-compiled-first-try-then-nothing-worked.jsonld"}}