Skip to content

perf(maps): 42% off the walk frame — cache survival and culling before projection - #45

Merged
apresmoi merged 1 commit into
mainfrom
perf/maps-walk
Sep 11, 2026
Merged

apresmoi merged 1 commit into
mainfrom
perf/maps-walk

Conversation

@apresmoi

Copy link
Copy Markdown
Owner

First-person walk mode ran at 30 fps in a dense city. It now runs at 60. Renderer main-thread time per frame 36.2 ms → 21.0 ms, a 41.8% reduction; frame p50 33.4 → 16.7 ms; dropped frames 260 of 274 → 60 of 274.

Every cell is byte-identical. Two fidelity digests held at all eleven checkpoints: the existing globe digest, and a new walk-scene digest over nine settled street-level poses carrying buildings, shadows, the sky dome and the perspective camera. The shipped digest alone is blind to all of this — not one of its waypoints has a building in it, which is why the new one exists.

The frame was not where we thought. A Chrome trace of the real interaction (new harness at bench/maps-trace/) put base-raster at 51% of a walk frame rather than the 99% the orbit bench had measured, with 11.9 ms sitting outside the render entirely and therefore invisible to bench/maps-render. Style, layout, paint and compositor together are 1.51 ms — the DOM is not a limiting factor.

The structural change. The wall cull expressed its per-frame verdict by handing glyphcss a fresh 45,726-polygon array, destroying four identity-keyed caches — the cross-frame shade cache, the texture-sampler walk, the cull-chunk map and its normal cones — in order to communicate a difference of one to seven polygons. Skipping the write when the verdict is unchanged was measured first and recovers almost nothing: the survivor set genuinely moves on 251 of 263 walking frames. The fix is the shape, not the frequency — the cull now writes Polygon.hidden in place, and the shadow map honours it on both its passes, the depth raster and the fitted light volume.

The rest is culling before projection rather than after. A cull run wholly behind the near plane is provably invisible and is now rejected whole, where before 75.9% of runs were accepted on a NaN corner and their triangles projected one at a time. The globe horizon is resolved on the walker so projection is paid for only by what it admits. Two exact pre-rejects run ahead of the haversine, and a Lipschitz bound on a cached distance rules out 85% of walls with a single compare. Smaller: the shadow map builds into scratch instead of allocating 512 KB and 166,200 tuples per frame, the stroke stamp hoists three closures per run to per stamp, and the per-render mesh flattening is memoized.

Twelve changes, each measured on its own against a re-measured baseline, medians of four to five headed runs. Five things that did not pay are recorded with their numbers, including two that looked certain on paper.

Six new test files; every load-bearing line mutation-checked, including both halves of the shadow hidden contract separately, since each catches only its own mutant.

Two correctness defects the trace exposed are reported and deliberately not fixed here, in docs/design/performance.md: in orbit at city zoom the camera targets the datum while terrain stands at elevation × exaggeration, so the real 408 m Zürich ground projects to row −336 of a 63-row grid and 108,101 submitted polygons draw ten triangles; and getMaxTilt() answers 85 at every tilt while camera.rotX reaches 127.6, blanking all 8,820 cells at tilt 80. Both are camera-model changes that move the picture and reopen centerForCamera's inversion and every pose pinned in widget.tiltPivot.test.ts — they belong in their own change, and the right tilt ceiling cannot be settled while the first stands.

@apresmoi
apresmoi merged commit d62870a into main Sep 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant