Repository navigation
Description
Part of #3096, plan after the 2026-11-07 review, step 3. Size: L (design S, then the build).
Depends on: nothing for the design. Step 6 (#3199) builds on it. Takes over the level-17 mode of GP9 (#3103), which was designed but not built.
Problem
The mesh draws only over level-18 terrain nodes:
- Pieces draw only when
rule.finest >= 18(plan/pieces.ts:124and:219); the mesh's grid isMESH_LEVEL = 18(host/obc-ground-plan/src/mesh/params.rs). - The terrain lowers its finest level with the quality:
FINEST - Math.max(0, this.quality - steps)(terrain/terrain.ts,rule()). At the usual factor of 4.6,stepsis 0, so the first quality step removes level 18. That is every phone and most screens. - Then no mesh draws, and the far look draws the near ground: saw-tooth wall faces, smeared shoulders, no kerbs (the loading review, state 2).
- The mesh hides the terrain with sunk points, and that holds only over nodes of
MESH_LEVEL(mesh/mod.rs, "Sunk points"): on a coarser node, a terrain triangle over three strip corners has no sunk corner and can show through.
Design (decide in this issue before code)
The mesh interior stays one CDT per cell for both levels. What changes is how the terrain gives way over a level-17 node. Two candidates:
- (a) A strip per level. The cell also gets a level-17 region R17 (R grown to whole level-16 cells) and a level-17 strip on ∂R17 made of level-17 grid vertices. A level-17 grid point is sunk when every level-16 cell that touches it is in R17. Over a level-17 node the pieces draw the interior and the level-17 strip; over a level-18 node, as today.
- (b) Floors as in the epic's section 7. A terrain vertex inside the owned region takes
min(its height, its floor), with the floor from the drawn mesh, at any level. One texture read per vertex; the floor needs a grid per node and its build.
Choose by: no terrain pixel in front of the mesh at either level; the cost per cell and per frame on the phone; the strip's fit at morph 0 and 1. The issue records the choice, the algorithm and the invariants. Then the build.
Also in the design: the mesh's range over level-17 nodes stops before level 17 starts to morph (GP9's rule R + TRIANGLE_M < morphStart(17)); the switch between finest 18 and finest 17 has a hysteresis.
Invariants
- Inside the owned region no terrain pixel draws in front of the mesh, over level-18 and level-17 nodes and at the border between them (
debug=owned: cracks = 0, inside = 0). - The strip meets the terrain vertex for vertex at each level, at morph 0 and 1.
- The interior of a cell's mesh does not depend on the terrain level; only the strip or the floor does.
- No terrain vertex snaps where the mesh draws.
- The crate stays a pure function: the same bytes natively and in WASM.
Done when
- Design: the chosen candidate, its algorithm and invariants in this issue, with a probe build behind
ground=planthat forces finest 17. - The owner runs the probe on the iPhone: v17 and m1, the frame time, and a look at the near ground. No phone numbers before
plan-timing.htmlran there (TrailView ground plan: measurements before the far-cell work (coarse records, stamp GPU time, iPhone, city caps) #3201). debug=owned= 0 in the walk and drive flights at finest 18 and at finest 17.- Time to a clean frame, cold and warm, at v17 and m1: develop | ground-plan | this branch, at finest 18 and 17.
- The frame time with GP0's method (walk and drive), at finest 18 and 17; triangles in view.
- Renders of the owner's views, develop | new, at finest 17 (top and eye height).
obc test -p obc-ground-plan, clippy,cargo fmt --all, vitest,npm run check,npm run check:wgsl, the WASM size guard.
Risks
- (a) adds strip triangles and cell bytes; (b) adds a floor build that GP7's redesign removed.
- A level-17 node reaches far past the level-18 plateau; the range rule decides how much of it the mesh covers.
- A phone GPU is several times slower than the GTX.
Order
The design runs now, in parallel with steps 1 and 2. The build lands before step 6.
Activity
timohueser commentedon Nov 7, 2026
Design: the mesh on level-17 terrain nodes
Prototype on
origin/ground-planf518fd8, not pushed:step3/prototype.patch(probe code marked PROBE). GTX 1660,Engelberg fixture, 2026-11-07. Evidence:
step3/runs/. Renders:step3/renders/.1. The level-16 finding
trace/trace16-gp-full.json).measure/frames on :8710 did not draw level 16 in place of 18. They lost the near field at every level: a badv17 load draws 101 nodes (levels 11–16: 4, 28, 19, 25, 17, 8), a good one 201 (… 16: 29, 17: 28, 18: 37). The rule
kept finest 18, so the pieces drew. Only
visible()(frustum test with the page heights andview.z) orcovers()(no drawn page at any level) can drop near nodes of all levels. Not traced further: :8710 was out of bounds for me.
steps= 0 on every usual screen. A separate issue traces the drop with the :8710 data.2. The fault
Over a level-17 node a terrain vertex of the strip slides to its even point (morph 1). A level-17 terrain triangle in R
draws when its three corners lie on ∂R. Today's fan also covers such a triangle: at each north-west convex corner of R
(the terrain's diagonal runs north-east to south-west) and where two parts of R touch at a corner. Today's fan at
finest 17,
debug=owned: terrain over the mesh in 39 of 50 top views, up to 10,275 px (renders/owned-*.jpg).3. Choice: (a′) one strip, cut along the level-17 diagonal
drawn mesh heights, which only the GPU makes (
corners.wgsl): one more GPU pass per cell and level, and 4,225 floatsper node. The epic's floor (lowest mesh height − 5 cm) is not valid by construction: a terrain triangle with two
∂R corners rises over a strip that falls to a cut slope near ∂R. Only an infinitely low floor is safe: the sunk point.
Held, not dropped, terrain (stand-in
hold=1) costs 0.00–0.01 ms to draw: the cost of (b) is its build and memory.1.66 × R. At level 17 it draws ground fill and the second strip: 1.85 M triangles in place of 0.70 M strip triangles
on the 256 fixture cells (+33 % drawn at 17, +53 % stored). It buys a level-17 morph that the range never uses.
node the strip leaves out each half whose three corners lie on ∂R, as it leaves out an ear at level 18. Those
triangles go last, in runs of their own that draw only over level-18 nodes. Fixture: the same 3,480,579 triangles,
25,242 (0.7 %) in level-18 runs, bytes +0.005 %, WASM +2.5 kB (the guard passes at 100 % of its raw budget), mesh
build 3.89 → 3.96 ms median per cell (inside noise: other jobs ran).
tie=1) fails: where two parts of R touch, a halfholds a fixed point, so it is no copy of the terrain's triangle (t-tertiary-top: 2,075 px).
4. Algorithm
[c00 m01 m10] [m10 m01 c20] [m01 c02 c20] | [c20 c02 m21] [m21 c02 m12] [m21 m12 c22]; ears 0 and 5 as today.halves(i, j)=[c00 c02 c20],[c20 c02 c22]. A triangle of a half with all corners on ∂R goes to the level-18 list. Bytes: thoseruns last; head word 9 = their count;
MAGIC0x4f424d03,HEAD10.terrain.wgsl: a vertex of a node of level M sinks by the same bit (a level-17 vertex is an even level-18 gridpoint) when it lies nearer than S, and takes no own snap there.
piece.wgsl: at finest 17 a terrain vertex of the strip isgrid_vertex_by(even, 17), the terrain's own vertex.At M = 17 the draw skips the level-18 runs.
pieces cut: the fade in metres (the lod is clamped at the finest level): whole to S + TRIANGLE_M, gone at range / 2^18.5. Invariants and proof
debug=owned: top views (50, mesh green, terrain in R blue, additive):0 px and 0 holes at 18 and at 17. Walk and drive, every 10th frame: blue marks terrain at a point that the strip's
rule gives the mesh, a fault from any view: ≤ 1 px in 17 of 364 frames, at 18 as at 17 (single pixels: probably
the rounding of the probe's own test at triangle edges).
The additive count is no gate at eye height: nearer terrain hides farther mesh (median 41–217 px, new and old fan).
no_strip_triangle_turns_over…); at 17 eachhalf is drawn once, by the strip or by the terrain, and no fixed point stands on the side of a terrain half
(
each_strip_cell_is_drawn_once…: 400 shapes of R, with convex corners and corner contacts).plan_nearholds at 17 and 18.6. Range and hysteresis
TRIANGLE_M = 2 level-17 cells (6.5 m): the reach of a level-17 triangle past a sunk corner. The mesh fades from
S + TRIANGLE_M to range / 2^18 (479 m).
whole, and the strip needs no level-17 morph. The range is the same at 18 and 17.
both levels: the level-18 runs and the strip vertex's level must go by level-18 node (the epic's piece).
CALM= 120 calmframes with both pools under 60 %. 17 ↔ 16: off in the first frame below 17, on after
CALMframes in a row at 17or more. The cells stay wanted and built while off (today
meshCellsdrops them below 18, and a return builds again).7. Cost (GP0's method: walk / drive, all passes per frame, ms)
hold=18. Build plan and gates
obc test -p obc-ground-plan(105 pass),clippy,
cargo fmt --all, check 7, the WASM size guard (budget full: raise it or cut).meshCellsat 17, thehysteresis;
finest=16..18anddebug=owned(additive only in that view; the fault test by the strip's rule) asdebug options. Gates: vitest (mesh level, hysteresis),
npm run check,npm run check:wgsl;debug=owned= 0in the 50 top views and in the walk and drive at 18 and 17; frame time against develop at 18 and 17; time to a clean
frame at v17 and m1, cold and warm, at 18 and 17; renders develop | new at 17; the iPhone probe (
IPHONE-PROBE.md).Merge pull request #3207 from timohueser/claude/gp-mesh-level17