● S3 WebGPU wasm32

Horde

A crowd that never stops arriving, a gun that aims itself, and five minutes you are not going to get — the same apps/horde that builds a native binary, running here.

horde — crcbl
Starting…
Click the canvas first. A canvas has to hold keyboard focus to receive keys, and browsers will not start audio until you have interacted with the page. Clicking away pauses the game and lets go of whatever key was held — no platform sends the release for a key that was down when focus left.
Touch reaches the engine. A contact arrives as an ordinary pointer, and the first one is also the gesture a browser waits for before it will start audio. Whether this demo can be played with one is in its controls below — not all of them can. Switching tabs pauses the game and lets go of whatever was held: no platform sends the release for an input that was down when focus left.

controls

Move, and pick. The shooting is not your problem.

why a fourth game

To find out what one tick costs per live body.

Breakout spawns its world once. Flappy runs a treadmill. Asteroids churns hard and holds fifty things. This one holds a thousand and wants ten thousand, and every one of them steers, asks the broadphase what it is standing next to, and is drawn. It is the sample that exists to be measured rather than played: a benchmark in a game costume.

The measurement is written down in the sample's plan, with the conditions each number was taken under. The short version, on a Radeon RX 7900 XTX: the render side carries ten thousand with room to spare — a frame costs about a tenth of a millisecond of CPU whether the field holds one enemy or ten thousand — and the simulation is what breaks, because separation is one broadphase query per body per tick. It is spread over a job pool now — steering goes through par_for, and web/run-horde-threads-e2e.sh asserts a chunk really ran on a Web Worker — but not on this page: GitHub Pages cannot send the COOP/COEP headers a shared memory needs, so the published build has no workers to announce and every chunk runs inline. That degradation is itself one of the gate's red checks.

what is actually running

The native sample, with four differences the browser forces.

Start-up is polled across several frames, because requestDevice is a promise and the main thread cannot block on it. The frame loop is driven by requestAnimationFrame out here rather than by a loop inside the engine. The clock is the browser's, because wasm32-unknown-unknown has no Instant. And your longest run lives in the Origin Private File System instead of ~/.config.

Everything else — the ECS, the fixed-tick server, the physics, the render graph, the spatial audio — is the same code the native build runs. The whole crowd is one draw call: the player, all three enemy kinds and the experience gems share a single sprite sheet at a single frame size, so the number of batches a frame costs does not move when the field grows. Press F3 and the panel will tell you how many of the field survived the view cull and how many draws it took.

Open the console for the engine's log. The wasm module imports nothing, not even console.log, so log lines are queued inside wasm and drained by the page each frame. crcbl.audio(), crcbl.saves() and crcbl.logLevel(4) are there too.