Skip to content

TrailView ground plan, step 3: the mesh on level-17 terrain nodes (design first) #3196

Open

Description

@timohueser

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:124 and :219); the mesh's grid is MESH_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, steps is 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

  1. 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).
  2. The strip meets the terrain vertex for vertex at each level, at morph 0 and 1.
  3. The interior of a cell's mesh does not depend on the terrain level; only the strip or the floor does.
  4. No terrain vertex snaps where the mesh draws.
  5. 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=plan that 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.html ran 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

  1. timohueser commented on Nov 7, 2026

    @timohueser
    OwnerAuthor

    Design: the mesh on level-17 terrain nodes

    Prototype on origin/ground-plan f518fd8, 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

    • Not reproduced on the fixture: 0 of 2,402 walk and drive frames lack level-18 nodes (trace/trace16-gp-full.json).
    • The measure/ frames on :8710 did not draw level 16 in place of 18. They lost the near field at every level: a bad
      v17 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 and view.z) or covers()
      (no drawn page at any level) can drop near nodes of all levels. Not traced further: :8710 was out of bounds for me.
    • So the finding does not change how often the level-17 mode matters: only the quality step makes finest 17, and
      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

    • (b) floors: no. A half with all corners on ∂R has no floored vertex, so floors keep the fault. The floor needs the
      drawn mesh heights, which only the GPU makes (corners.wgsl): one more GPU pass per cell and level, and 4,225 floats
      per 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.
    • (a) a second strip: no. A level-17 strip that keeps off D needs R17 = R in whole level-16 cells plus one ring:
      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.
    • (a′): yes. The fan of a strip cell follows the terrain's diagonal: three triangles tile each half. Over a level-17
      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).
    • A tie in place of the left-out halves (the strip draws them too, tie=1) fails: where two parts of R touch, a half
      holds a fixed point, so it is no copy of the terrain's triangle (t-tertiary-top: 2,075 px).

    4. Algorithm

    • Crate: fan of strip cell (i, j), points cXY and mXY in level-18 grid steps: [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: those
      runs last; head word 9 = their count; MAGIC 0x4f424d03, HEAD 10.
    • Viewer: mesh level M = min(finest, 18), none below 17; the cells, corners and bytes are the same at 18 and 17.
    • terrain.wgsl: a vertex of a node of level M sinks by the same bit (a level-17 vertex is an even level-18 grid
      point) when it lies nearer than S, and takes no own snap there.
    • piece.wgsl: at finest 17 a terrain vertex of the strip is grid_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

    1. No terrain pixel in front of the mesh in R, 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).
    2. The strip meets the terrain vertex for vertex: at 18 for every morph (no_strip_triangle_turns_over…); at 17 each
      half 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).
    3. The interior does not depend on the level: only the level-18 runs and the terrain's sinking level change.
    4. No terrain vertex snaps where the mesh draws whole: plan_near holds at 17 and 18.
    5. Pure function: no new input; 256 of 256 fixture cells give the native digest in WASM (check 7).

    6. Range and hysteresis

    • S = morphStart(18) = range / 2^18.3 (389 m at factor 4.6, 47° N). The terrain sinks nearer than S at 18 and 17.
      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).
    • Rule: S + TRIANGLE_M < morphStart(17) = 2 S, true at every factor. So level 17 never morphs where the mesh draws
      whole, and the strip needs no level-17 morph. The range is the same at 18 and 17.
    • Step 6 keeps the rule (S ≤ morphStart(17) − TRIANGLE_M). At finest 18 past range / 2^18 one cell then lies over
      both levels: the level-18 runs and the strip vertex's level must go by level-18 node (the epic's piece).
    • 18 ↔ 17 has no hysteresis of its own: nothing builds again, and the quality rises only after CALM = 120 calm
      frames with both pools under 60 %. 17 ↔ 16: off in the first frame below 17, on after CALM frames in a row at 17
      or more. The cells stay wanted and built while off (today meshCells drops them below 18, and a return builds again).

    7. Cost (GP0's method: walk / drive, all passes per frame, ms)

    develop ground-plan prototype
    finest 18 3.89 / 3.60 4.11 / 3.94 (two runs, ± 0.03) 4.21 / 3.98
    finest 17 3.62 / 3.43 3.60 / 3.33 (no mesh) 3.93 / 3.70
    Triangles in view at 17: terrain 762 k / 565 k (18: 1,016 k / 754 k), pieces 436 k / 216 k (18: 438 k / 217 k).
    At 17 the mesh costs 0.33–0.37 ms; at 18 the prototype is 0.04–0.10 ms over ground-plan (not traced). hold=1
    draws the held terrain (10.7 k px show through cuts deeper than 0.5 m in v17-top) and adds 0.00–0.01 ms.

    8. Build plan and gates

    1. Crate PR: fan, halves, level-18 runs, bytes, the strip test. 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).
    2. Viewer PR: mesh level, sinking and no snap at 17, the strip vertex at 17, the fade in metres, meshCells at 17, the
      hysteresis; finest=16..18 and debug=owned (additive only in that view; the fault test by the strip's rule) as
      debug options. Gates: vitest (mesh level, hysteresis), npm run check, npm run check:wgsl; debug=owned = 0
      in 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).
  2. added a commit that references this issue on Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions