GEO-RUNTIME / SPRINT 0THE INSTRUMENT3 SEPT 202612 / 12 CHECKS

The scoreboard exists
before the engine does.

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.

A cyan wave surface rendered by WebGL2 from geometry built in Rust, showing smooth shading across two axes of curvature.
31,609 vertices, 62,208 triangles, built in WebAssembly and drawn without ever being copied. The surface carries analytic normals, so shading is the test: a wrong normal goes flat and you can see it, rather than reading correct in a log. This is not GeoV content — Sprint 0 deliberately proves the pipe with nothing of the engine in it.
Kernel23,971bytes · 0 imports
CPU frame · 56k verts2.46ms · build→submit
The copynonethe view is the arena
Checks12 / 12incl. it actually drew
The done-when

One constant, and the chart moves on its own

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.

RESverticesbuildcpu framerasterharness
4856,4971.212 ms2.46 ms215 msuntouched
72126,2172.615 ms6.81 ms392 msuntouched
4856,4971.212 ms2.46 ms215 msuntouched

+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.

Where the frame goes

Four phases, and they sum to the whole

01.0 ms 2.0 ms3.0 ms 2.46 ms 2.4k4.8k9.5k 18.9k37.7k56.5k VERTICES · CPU ONLY build 1.212 view 0.003 upload 1.210 submit 0.035 residual 0.000%

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.

What passed

Twelve checks, and none of them are “it compiled”

✓It actually drewpixel coverage measured, not assumed
✓Every build stayed inside its own stated boundbound_hello() before, vertex count after
✓The arena never overflowedoverflow_count() == 0
✓Linear memory never grew247 pages, start to finish
✓No view detached during the sweepbuffer identity never changed
✓The footgun is real, and was reproduceda live view went to 0 bytes on purpose
✓And the bridge caught italarm raised 1×, view re-derived intact
✓The four phases account for the frameresidual 0.000%, 40 frames per point
✓In-frame agrees with the isolated benchratio 0.90–1.37×, the clock is the spread
✓The raster clock answeredEXT_disjoint_timer_query_webgl2
✓Raster scales with triangles16 → 215 ms, linear
✓No page errorszero, including the favicon

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.

What it cost

Three ways to measure nothing convincingly

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.

1gl.finish() is not a synchronisation point
draw alone
0.015 ms
draw + gl.finish()
0.010 ms
draw + gl.flush()
0.005 ms
draw + readPixels
215 ms  ← the actual work

WebGL 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.

2Benchmarking submission starves everything after it

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.

3A 100 µs clock makes quantisation look like a missing phase

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.

4And the visual one — culling ate the entire surface

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.

Honesty

Two clocks, and only one of them travels

ClockWhat it seesPortable?
CPUbuild · view · upload · submit — the part the engine ownsyes, as ratios
RASTERthe GPU's own clock, honest and linear in triangles — but software rasterised hereregression 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.

Not done

What Sprint 1 inherits, and what it still owes

  • Nothing is posed. Sprint 1's exit is the dummy at three angles from the .geo binary, and the .geocast → .geo compiler does not exist yet.
  • The bounds are still hand-typed constants. 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.
  • The absolute milliseconds do not travel. Ratios do. Every number here is from one software-rasterised container.
  • Route B is still on the table. A typed arena inside the existing app is an afternoon, produces bit-identical output, and is 3.5–4× faster. It was never refused — and it does not compete with the rebuild for a single hour.