Experiments

Hand-built experiments that start from a bakeoff world. First: one of them rebuilt at the same visual bar inside a real game frame budget. The bakeoff worlds are one-shot artifacts and stay that way — these are what happens when one of them is taken apart and rebuilt on purpose.

Neon City, game-ready

Neon City, game-ready

One bakeoff world rebuilt by hand at the same visual bar, this time inside a real game frame budget. Walk it, or fly one of its air cars — and watch the frame-time readout.

GPU / frame
2.72ms 11.4
draw calls
118 149
triangles
824,448 1,462,730

Struck-through: the one-shot bakeoff world it rebuilds. 4.2× less GPU time for the same street.

Neon City, game-ready — how it was built

Why this exists

Every world in the bakeoff is a one-shot artifact: a model received a brief and had a single autonomous attempt, with no profiler, no device to test on, and no second pass. That is the contest, and it is a fair test of what a model produces cold.

It is not a fair test of what the idea can cost. This is Opus 5's Neon Rain rebuilt by hand at the same visual bar — same street, same rain, same signage, same wet road — with a frame budget as a hard constraint and a measurement harness built alongside it. The interesting result is not that the rebuild is faster. It is which things turned out to be expensive, because almost none of them were the things you would guess.

What a frame actually costs

A 60 fps frame is 16.7 ms, and that is the budget for everything — player controls, physics, AI, UI, audio, and the world. An environment is meant to be a fraction of it.

the same street, rendered byGPU timeleft for the rest of the game
the one-shot bakeoff world11.4 ms5.3 ms
this rebuild2.72 ms14.0 ms

That is the whole point of the number. 4.2× is a ratio; 14.0 ms instead of 5.3 ms is the difference between an environment you can build a game on top of and one that has already spent your frame.

The finding: it was never the polygons

The obvious plan is to cut geometry. That plan was wrong, and the way it was found to be wrong is by ablating the original by cause — removing exactly one mechanism at a time and re-measuring — rather than by profiling it by render pass.

removed from the one-shot worldGPU time saved
its 16 × PointLight4.90 ms
post-processing (4 passes)2.70 ms
the planar Reflector2.50 ms
all 425,000 triangles of towers, under lean lighting0.10 ms
all 29,000 rain instances0.00 ms

The build's own ablation of the reference, control 11.30 ms. Measured during development, not re-run for this page.

Deleting every building in the city saves a tenth of a millisecond. Geometry on this GPU costs roughly 0.024 ms per 100,000 triangles. The towers were never expensive because of their vertices — they were the surface area over which MeshStandardMaterial evaluated a twenty-light loop, once per fragment. Apple's hidden-surface removal cannot help with that: these are visible pixels, shaded once, just very expensively.

What replaced them

One hand-written analytic material instead of MeshStandardMaterial plus twenty lights. The lighting is evaluated as a closed-form function of position and normal rather than as a loop over light sources, which removes the 4.90 ms above and, incidentally, the reason the tower count mattered at all.

The tone map, colour grade and vignette folded into every material. The original spends a full-screen pass on them; here they run at the end of each material's fragment shader, so the frame needs no post-processing chain to look finished. Worth about 2.70 ms — and it is why the neon still glows with post switched off, since the bloom is carried by additive glow cards baked into the sign atlas rather than by a blur pass.

Interior mapping, which is the one that is free and does not look it. Each lit window ray-traces a virtual room box in the fragment shader — ceiling, back wall, side walls, floor, furniture silhouettes — so the depth behind the glass is real and parallaxes correctly as you walk past. No extra triangles, no extra draw calls, +0.03 ms. The original painted a flat lit-window grid. Walk close to a facade and look along it: the rooms turn.

The sign atlas is drawn rather than downloaded, which is the same mistake as the towers in a different currency. The build shipped signs.png — 1.01 MB gzipped, 43% of the entire download — so that a phone would not have to run the ~30 canvas operations that generate it. Measured, those operations cost 0.9 ms on a desktop and 2.7 ms at 4× CPU throttle. A megabyte to save three milliseconds. Generating it at load instead, together with the geometry change below, takes the payload from 1.49 MB to 0.46 MB. ?atlas=baked restores the download, which is the right trade for a game that wants pixel-identical output across devices — the generated atlas is bit-identical to the PNG on the same canvas rasteriser, but Chromium's Metal-backed 2D canvas antialiases strokes differently, so "deterministic" here means per rasteriser, not per machine.

City geometry is generated rather than downloaded, by the same argument as the atlas. Baking shipped a megabyte of city-*.bin so a phone would not pay 250–520 ms of CPU at load; generating by default drops the page to 0.46 MB over the wire, with the baked path kept behind ?assets=1 for the game case that wants a fixed asset. Generation used to freeze the main thread — ~1039 ms of stall at 6× CPU throttle on desktop — so it now runs in a Worker and the main-thread stall falls to ~13 ms (5 ms unthrottled). Both phones in the device re-test report the Worker path; ?worker=0 is the main-thread freeze, kept for comparison. When the baked .bin is served, Cloudflare would leave application/octet-stream raw at 4.75 MB, so it is declared as application/json purely so brotli kicks in (~722 KB).

The wet road was rebuilt twice, and the failure is more instructive than the fix. The first version sampled a cubemap reflection probe baked once at load — one texture fetch per road pixel, effectively zero per-frame cost, and by ordinary game-engine reasoning the right call for a rough surface at grazing angles. It was wrong for a reason no amount of tuning could reach: a cubemap has no position. It answers "what lies in direction R", so a noodle stall twenty metres away reflects at the bearing it has from the probe, not from you — which puts its reflection somewhere other than beneath it, or nowhere. Five rounds of increasingly specific workarounds were all attempts to hand-place sources that a real mirror places for free. It now renders the scene once more from a camera mirrored in the road plane and samples that projectively. The mechanism was the bug.

Measured

one-shot worldthis rebuild
GPU time per frame11.4 ms2.72 ms
draw calls149118
triangles1,462,730824,448
download, gzipped0.46 MB

Apple M3 Pro, headless Chromium, ANGLE Metal backend, 1920x1080, vsync and frame cap disabled. GPU time is the minimum EXT_disjoint_timer_query_webgl2 sample; three runs each, agreeing within 2%; draw calls counted at the GL level, so the reflection pass cannot hide inside them. Both pages measured minutes apart in one session on 2026-07-27, against the exact build served here. Download is all 33 requests this page makes, gzipped and summed — Cloudflare prefers brotli, so it is an upper bound; the one-shot world is a single file and not usefully comparable. Caveat: the same pair measured on a busy machine read 2.93 and 12.06 — absolutes shift with contention, the ratio barely moves.

Flying-car mode

A second mode on the same page. The player takes over air-car instance 0 of the existing traffic InstancedMesh — zero extra body draws, and the wet-road reflection is free and correct because traffic was never excluded from the mirrored pass. Motion adds one GL draw for twin world-space drift trails and a tail-bar ember (one buffer, one material), for a measured 119 draws against walk mode's 118.

GPU non-regression against walking, same session on M3 Pro / ANGLE Metal at 1920×1080: walking ~3.0 ms, flying ~2.9 ms — inside run-to-run spread, so no measurable difference, not "flying is faster". The walk headline on the card stays the quieter-session 2.72 ms figure above. Loading the flight module and leaving it idle also does not move walking-mode GPU time outside measurement noise.

Audio is ~30 procedural Web Audio nodes — no audio files; a 0.5 s silent WAV is generated at init only to promote iOS to a playback session that survives the silent switch. Mute is remembered across reloads. Tilt steering is available on phones with a sensor (TILT in the HUD); landscape orientation is fixed. The iOS audio session and tilt were verified on hardware. Flight frame times on phones are unmeasured.

This is not a fair model comparison either — it is hand-built on top of the rebuild.

What this does not claim

Reading the number in the corner

The readout is wall-clock time between animation frames, not GPU time — EXT_disjoint_timer_query_webgl2 does not exist in mobile Safari, so there is no honest way to report GPU cost on a phone from inside the page. It is capped by your display's refresh rate, which means ≈16.7 ms is the floor, not the cost: it tells you the frame is being hit, not by how much. p95 is the number to watch — it catches the hitches a median hides.

?perf=0 hides it. ?post=0 drops the bloom chain, ?q=low selects the headroom tier, ?refl=off removes the road reflection, ?refl=probe restores the cheap cubemap so you can see the near-field failure described above, ?worker=0 freezes generation onto the main thread, and ?mode=fly starts in the air car. The full parameter list is in the build's README.

Controls

Walk. W A S D to move, mouse to look, Shift to run, click to capture the cursor. On a phone, a joystick appears bottom-left and dragging anywhere else looks around.

Fly. Press F or the HUD's Fly button (or open with ?mode=fly). W A S D throttle and steer, Space / Shift altitude, C cockpit, M mute. On a phone: throttle and altitude sticks, optional TILT steering, mute in the HUD.

Walk it ↗