GEO-RUNTIME / SPRINT 0THE INSTRUMENT3 SEPT 202612 / 12 CHECKS
Rust writes vertices into a static arena. JavaScript takes a window onto that arena — not a copy of it — and hands the window straight to WebGL2. Sprint 0's job was to prove that path end to end and then build something honest enough to measure it. The bridge turned out to be the easy half.
The sprint's exit condition was written before any of it existed: change one constant in the Rust source, rebuild, and the chart moves without anyone touching the harness. hello::RES is that constant. It went 48 → 72 → 48, and the harness file's checksum never changed.
| RES | vertices | build | cpu frame | raster | harness |
|---|---|---|---|---|---|
| 48 | 56,497 | 1.212 ms | 2.46 ms | 215 ms | untouched |
| 72 | 126,217 | 2.615 ms | 6.81 ms | 392 ms | untouched |
| 48 | 56,497 | 1.212 ms | 2.46 ms | 215 ms | untouched |
+123% vertices for +50% resolution — the grid is RES², so that is the number it should be. The harness never receives RES as an argument: it calls hello_res() and reads the value out of the compiled WebAssembly. It is not told what it was built from. It asks.
The split is taken inside the frame, not inferred from separately benched pieces — so the parts sum to the whole by construction, and a non-zero residual later means a phase actually went missing. The view costs 0.003 ms: that line is the zero-copy claim, and it is the whole reason the two big bands are build and upload rather than build and copy.
The detached-view footgun is closed by reproduction, not by assertion. The plan reserved a day for “handle WASM memory growth detaching the view.” Instead the harness grows memory on purpose, watches a live Float32Array drop to zero bytes, and proves the bridge notices, alarms, and recovers. A hazard you can only assert is a hazard you will meet again.
None of these produced a wrong-looking number. Each produced a clean, precise, plausible one — for work that had not happened yet. A benchmark's failure mode is not noise. It is confidence.
gl.finish() is not a synchronisation pointWebGL commands cross into the GPU process asynchronously and finish() returns before the rasteriser has run. A draw phase timed with finish() around it reports a free draw forever, and charts beautifully doing it. Only a readPixels forces completion. It is now on no measured path in the repo.
Timing the draw call at 25 × 8 queued 200 full-scene draws the CPU never waited for — free to submit, ~100 ms each to rasterise. The GPU ran twenty seconds behind, and the symptoms all looked like other bugs: the timer query answered once then returned n/a for every size after, and the CPU residual blew out to 66% as backpressure reached unrelated phases.
The fix is a 1×1 readPixels drain after any bench that draws, and keeping drawing benches small. Cheap phases can be batched hard; phases with a queue behind them cannot.
performance.now() is clamped to 0.1 ms. Four phases benched separately against one frame benched whole disagreed by 29% — and that residual reads exactly like an unaccounted phase, which is an hour of chasing something that is not there.
The fix is structural rather than statistical: take the split inside the frame. Each frame times its own four phases and forty are accumulated. The parts now sum to the whole by construction, the residual is 0.000%, and the isolated benches survive as a cross-check whose disagreement is reported rather than hidden.
First render: 0% pixel coverage, every other check green. The arena emits quads as (a,c,b)(b,c,d) — byte-identical to the engine's own indexer, which BENCH-002 validated index-for-index — and that winding puts the front face on the clockwise side. Default front-face plus backface culling equals an invisible mesh.
Fixed in the renderer, not the data: the index buffer's exact match with the engine is worth more than a cull setting. And culling stays on deliberately — with it off, an inside-out surface looks fine forever; with it on, the coverage check catches it the same day.
| Clock | What it sees | Portable? |
|---|---|---|
| CPU | build · view · upload · submit — the part the engine owns | yes, as ratios |
| RASTER | the GPU's own clock, honest and linear in triangles — but software rasterised here | regression signal only |
The bench container has no GPU. SwiftShader rasterises on the CPU, so 215 ms at 110k triangles is a fact about this container and not a claim about anybody's machine. It is charted on its own axis with that written on it, because a raster band mixed into a 2.46 ms CPU stack would flatten the thing the engine actually controls into a hairline.
.geo binary, and the .geocast → .geo compiler does not exist yet.bound_ops() states them honestly, but a ceiling someone typed is not a ceiling derived from a tree — Sprint 1 owes the derived form and the fuzz pass that proves no input beats it.gcLeaf has never run through this path. The test character has no plates, so hair and hems are untested in both the old measurement and the new engine.