Repository navigation
Description
Rewritten to the one road model (#3096); the previous body is in this issue's edit history.
Status: approved
Part of #3096. One pull request into ground-plan.
Depends on: #3101 (GP7), #3102 (GP8, after its entry gate). The question-4 views with heights.legs = wall need the leg split (GP3 follow-up, #3132).
TL;DR
Goal: With ground=plan, the viewer draws GP7's pieces near the eye before the terrain, holds the terrain under them by its floors, and closes the mesh against the terrain at the strip, so that no crack shows anywhere and no terrain pixel shows inside the owned region within the floors' range, at level 18 and at level 17. Past R_eff the terrain shows GP8's far look of the same records. The snap moves terrain vertices onto the model's core outline only past the floors. The far look stops where pieces draw. A GPU count proves the ownership over the walk and drive flights, and the whole road share of a frame, pieces and the lean pipeline included, is no larger than develop's, low tier first.
Problem: GP7 builds pieces and floors, and GP8 paints and shapes every page from the records, but nothing draws the pieces. The terrain has no floor, no draw takes a piece, the range is not fixed in the LOD metric, nothing says what draws while a cell is not built yet, and no count finds a crack at eye height. Without GP9, GP8's far look paints at every distance, its snap moves every terrain vertex near a road, including the vertices that the floors and the strip must keep on the grid, and the road terms of the look cost every detail fragment near the eye where the pieces draw the roads anyway.
Decision: The ownership stays: the floor slot per node, plan/pieces.ts with its pools, R_eff, the strip through the terrain's own grid_vertex, @invariant clip positions, debug=owned. Three parts change: (1) past R_eff the terrain shows GP8's far look (B); (2) the snap factor is smoothstep(floor_range, plan_range, d) (1 at R = 0), the same in vertex_main and piece_place, and no strip vertex has a snap word; (3) the far look's paint and ground terms stop where pieces draw, by the test of today's mask paint with surface.plan_range in place of the corridors' held, and terrain nodes wholly inside the floors' range draw with WAYS = false (the lean pipeline) when the shots prove that nothing changes. At R = 0 no piece draws, the worker builds only the records, and B is the look at every distance. The frame gate is the owner's: no slower than develop, with the pieces, their casters and the lean pipeline, by GP8's clock rule.
Rejected:
- A test per node for "level 17 without a morph" (
rule.morphs): a drawn level-17 node starts at 480 m and reaches 294 m farther, past the 779 m where level 17 starts to morph, so most level-17 nodes in range would draw no piece. - Each cell draws as soon as it is built: where a built cell meets an unbuilt one inside the owned region, a floored node meets an unfloored one, and the terrain cracks at their border.
- The near or far range per piece by distance: near and far share vertices only on locked lines (GP7, section 3), not on piece borders, and GP7 measures a level-18 node's floors against the near range only.
- A node that draws a coarser page keeps its pieces: the strip's terrain vertices then take the coarser page's heights, and nothing bounds the difference to the 5 cm under GP7's floors.
- Floors in a texture array: 65 × 65 grids do not tile its 2048-texel layers. A storage buffer reads by
vertex_indexdirectly. - An atomic count in the fragment stage: a storage write makes the depth test run late, so it counts hidden fragments.
- One discarding pipeline for all pieces: a discard turns off early depth writes, and on Apple GPUs the hidden-surface removal of the pipeline, which would then draw all the owned ground near the eye.
- A fragment stage of its own for pieces: it would copy
corridor_fragment. Two override constants share it. - A per-fragment model look near the eye for R = 0 (variant A): several ms per frame.
- The page cache near the eye for R = 0: about 2 ms more than the far-only variant (
PROPOSAL-shapes.mdsection 8). - No snap on the new path (the previous GP8): the edge lines wobble far from the eye (owner: a showstopper, TrailView ground plan GP8: the far stamp, one stamp function in every page on the new path, no snap #3102).
Systems: trailview
Contracts: none
Size: L (the terrain's vertex stage, its layouts and the Surface uniform; a new draw module with pools, floor slots and the range rule; GP5's plan client gains the payload; a new debug view with a backstop; the count in three test scripts; the snap factor, the paint's stop and the lean terrain pipeline; about 800 to 1,100 changed lines)
Facts
Line numbers from develop 4f8b8b255; GP5 and GP8 can shift them. Paths under builder/web/src/lib/trailview/ unless they start with tests/.
- The terrain draws one instanced 64 × 64 grid per node. The instance is
[x, y, level, 0], 16 bytes (terrain/terrain.ts:882-884); the WGSLNodehasunused: u32(terrain/terrain.wgsl:25). vertex_mainmakes each vertex withgrid_vertex(node.origin + grid * cell, node.level, true);vertex_indexisj × 65 + i; it computes the clip position inline (terrain/terrain.wgsl:57-64). No WGSL file uses@invariant.vertex_placeslides an odd vertex to the even one bymorph = level − lod;place_heightat morph ≥ 1 returns the coarser level's height at the even vertex (terrain/drawn-mesh.wgsl:65-89).even = u − ((anchor + u) & (1 << (24 − level)))(:72).MORPH = 0.3(terrain/lod.ts:10). A level-k vertex morphs wherelod < k, pastrange / 2^(k + 0.3)(terrain/lod.ts:78-90,terrain/drawn-height.wgsl:202-206). At the least factor 4.6, level 17 starts to morph at 779 m at 47° N and at 571 m at 60° N: lengths scale with the metres per unit, which fall with the cosine of the latitude.- A level-17 node splits where its nearest point is nearer than
range / 2^18(480 m at 47° N), so a drawn level-17 node lies at least 480 m off; its far corner lies up to 294 m farther (terrain/lod.ts:86-87). distancesandlod()include the eye's height above the ground (terrain/lod.ts:65-72,terrain/drawn-height.wgsl:203).selectculls while it walks the quadtree: a culled node takes its subtree (terrain/lod.ts:104-127).pages.drawing(node)gives the node of the finest page that draws the node; it is coarser while the node's own page is on its way (terrain/pages.ts:175-178,terrain/terrain.ts:438).- Group 0 of the terrain holds the frame,
nodesandholes(terrain/terrain.ts:901). Today the vertex stage reads 6 storage buffers in groups 0 to 3 (gpu/layouts.ts); the WebGPU default limit is 8. Surfaceis 416 bytes;levelsis its last field (terrain/terrain.ts:62,terrain/drawn-height.wgsl:9-25).- Detailed nodes draw with cull mode none, far nodes with back; depth
greater, reversed Z,depth32float, near plane 0.1 m (terrain/terrain.ts:932-945,resolve/targets.ts:3,frame/camera.ts:13). - Only the portal pipeline discards: a discard turns off the hidden-surface removal of tile-based GPUs such as Apple's (
terrain/terrain.wgsl:33-36). Terrain.draw: structures, corridors, opaque water, the terrain nodes (holed, detailed, far), then ribbons (terrain/terrain.ts:579-601).- The selection culls a node by its page's height range ± 50 m (
terrain/terrain.ts:52,:799-804). A corridor piece culls by its own box: its node's square grown byover, betweenlowandhigh(:806-810). - Quality falls one step in a frame with a shortage and rises after
CALM = 120calm frames with room (terrain/terrain.ts:49,:451-457);finest = FINEST − max(0, quality − steps)(:792-797). - Corridors:
RANGE_M600, pools of 27 and 20 MiB, 256 draws. They draw before the terrain and cast into the shadow map (roads/roads.ts:18-34,:180-201). Meshes build only after the eye field drew (terrain/terrain.ts:448). Poolhas fixed buffers; a mesh that finds no room drops, the last of the frame's list first (structures/pool.ts:4-60).corridor_fragmentdiscards the paint's dither share, the detail fade and every all-ground pixel (roads/corridor.wgsl:214-220,:235-237). The corridor vertex moves toward the eye by 6 cm plus 0.3 % (:113-116). A debug view clips corridors away (:146-148). The pipeline setsBAND = 1, which turns off the road paint inground_colour(roads/roads.ts:293,ground/look.wgsl:40,:673).- Casters draw with
CASTER_PRIMITIVE(cull none) andCASTER_DEPTH(light/shadow.ts:53-54). debug=cracksclears the scene to red and skips the sky (engine.ts:617-632).flight.mjs --cracks Ncounts exact red pixels of a screenshot every Nth frame, and hides#hudonly for--cracks(tests/browser/trailview/flight.mjs:99-102,:124,harness.js:133-144); such a flight must look down on known terrain (README.md:103-104).- A debug view's resolve copies the scene colour unchanged, so channels of 0 and 1 reach the PNG as 0 and 255 (
resolve/resolve.wgsl:116-118). DEBUG_VIEWSorder is the shader'sdebugindex (view.ts:9); the terrain fragment picks its branch atterrain/terrain.wgsl:121.Varyingsuses locations 0 to 5 interrain/unknown.wgslandterrain/unknown-clip.wgsl; onlyterrain/terrain.wgslincludes them.waitForIdle()waits whileterrain.roadsLoading(engine.ts:330).shoot.mjsprintsredPixelsfor adebug=cracksview (tests/browser/trailview/shoot.mjs:62).- The TrailView README has 794 words of its 800-word cap (
obc prose). - GP8 writes the snap words from the model's core distance (paved area ∪ paved shoulder, shoulder, verge), and its far look (B) reads B's paint texture, the feature mask and the move-and-share texture (GP8, sections 1 and 2).
- The snap moves a mesh vertex onto an edge within
SNAP_TEXELS= 3 texels of its level inside,SNAP_OUT_TEXELS= 3.75 outside (terrain/stamp.wgsl:59-62): 3.05 m at level 17, 1.52 m at level 18 outside. - GP7's inner border lies in terrain faces, a level-17 cell or more from other faces (GP7, section 3).
- Today's paint draws where the corridors do not:
paint_shareis 1 pastmasks.held(terrain/features.wgsl:191-194). - The road terms of the look cost every detail fragment: B with no paint against B is the most that a lean terrain pipeline inside the mesh range can save, about a third of B's share (
shapes/budget/). - B with fixed ground terms and the one-walk stamp, past 560 m, before pieces: a little faster than develop's whole road cost on both flights (
farb/cost/INDEX.md). The pieces replace the corridors. - B's quality nearer than 560 m is not measured since GP8's fix of the ground terms; before it, B near the eye showed the stair faults of the 8-bit mask (
cost/page/page-vs-fragment.png). - A stamp job with heights can spike on dense near pages (p95 about +1.7 ms per job,
heights/round5/). - Evidence paths are on timosPC under
/media/timo/OSM1/tmp/ground-plan/one-model/.
Design
1. Constants
In plan/pieces.ts unless the row says otherwise. GP9 adds no rule-table field: these are viewer budgets, not rules of the ground. GP5's constants of Plan stay; section 4 names the one that changes.
| Name | Value | Why |
|---|---|---|
RANGE_M |
600 m, LOD metric | the epic's range (section 8), as today's corridors |
FADE_M |
40 m | the epic's fade band, where the floor is off; also the margin before level 17 starts to morph. Both must exceed TRIANGLE_M |
TRIANGLE_M |
10 m | the reach of a terrain triangle with its morph: a level-17 diagonal is 4.6 m, and a level-18 vertex slides by at most 1.63 m |
VERTEX_BYTES |
24 MiB | GPU pool of piece vertices; with the floors, today's 27 MiB |
INDEX_BYTES |
20 MiB | GPU pool of piece indices, as today |
FLOOR_SLOTS |
160 | floor grids of 4225 f32 (16,900 bytes), 2.6 MiB. All round at 47° N, at most about 120 owned nodes lie within 600 m (108 level-18 nodes of the 27 split level-17 nodes, and about 12 level-17 nodes from 480 to 600 m); a third more holds a turn |
FLOOR_NONE |
1e30 m | a vertex without a floor; a shader never sees NaN |
DRAWS |
256 | piece parts per frame at most; within 600 m all round at most 156 nodes hold pieces |
BACKSTOP_M |
20 m | in terrain/terrain.wgsl: the backstop's depth under the floored terrain |
| return frames | CALM (120) |
exported from terrain/terrain.ts (section 10) |
2. What GP9 takes from GP5, GP7 and GP8
From GP7, through GP5's worker, per CellKey (a level-16 node) once built with MeshOptions { near }:
Piece[]: per level-18 node of the cell that the owned region touches, one or more parts: the node,origin(the north-west corner of the node, whole world units),part,low_m,high_m,over(the world units that its vertices reach past the node's square), the vertices (GP7's 20-byte vertex, the position in 1/16 world unit aboutorigin), 16-bit indices, and the rangesnear,far,stripandcasters(each{first, count}). Withnear: false, thenearrange is empty.Floors: per level-18 and level-17 node of the cell that the owned region touches: 4225 heights in the terrain's vertex order (j × 65 + i, i east and j south from the node's north-west corner), or none per vertex. GP9 decodes GP7's codes to f32 metres and uploads none asFLOOR_NONE.
GP9 relies on these properties of GP7's output (GP7's checks prove them): (a) every terrain vertex of the strip (on ∂R and on the middle ring) carries the terrain-vertex flag, and its position is its exact level-18 grid point; every other vertex keeps its stored position and height; (b) every face turns counter-clockwise from above, and a vertical face turns its front to its lower side; (c) the near and far ranges hold the same vertices on the locked lines (cell borders and the inner border), so two cells that draw different ranges meet vertex for vertex; (d) the floor lies at least 5 cm under every mesh triangle that can draw over it, by GP7's draw rule (GP7, section 9), the strip at morph 0 and 1 included; (e) ground faces carry MAT_GROUND in the look word; (f) each node with a piece has a level-18 floor grid, and its level-17 parent a level-17 grid. GP9 checks (f) when a cell arrives; a cell that breaks it gets GP5's failed record.
From GP8: under the switch, every page holds the model's heights, feature mask, move-and-share texture and B's paint (and the far layers on the high tier); no corridor draws; the snap pass writes the snap words from the core distance. If Roads still runs under the switch, GP9 makes Terrain build no Roads then. GP8's WAYS override, --toggle ways, --lean and its clock rule are GP9's test tools.
From GP5: the switch flag, which Terrain takes at construction, and class Plan (plan/plan.ts) with its wanted cells, records, dispatch, loading and stats.ready (GP5, section 6).
3. The range R
planRange(rule, on) = on ? min(RANGE_M, morphStart(rule, 17) − FADE_M) : 0, with morphStart(rule, level) = rule.range / 2^(level + MORPH), in the LOD metric (the distance includes the eye's height). At factor 4.6, R is 600 m at 47° N and 531 m at 60° N.
Invariant: every vertex of a triangle with a pixel within R lies within R + TRIANGLE_M < morphStart(17), so level 17 does not morph there. So over a level-17 node the strip, made of level-17 grid vertices (section 8), is exactly the terrain's level-17 cell row.
Decision: the epic's "level 17 without a morph" holds per point, by R. The node test is drawable(node) (section 6, step 5).
4. Cells (GP5's Plan, changed)
GP9 keeps GP5's Plan: the wanted cells (within the build range by distance, then the ahead band of AHEAD_M within AHEAD_COS), the records in priority order within KEPT_BYTES, QUEUED, KEEP_M, failed, empty and loading. So the cells inside R always come first in the requests and in the budget. GP9 changes these rules:
updategainsrangeandfull.range = rule.finest ≥ 17 ? planRange(rule, true) : 0replacesBUILD_M. It is the range as if the pieces were on, so a return after the hysteresis finds the cells built. Withrange = 0no cell is wanted, the ahead band too, and the records stay until they passKEEP_M.- The build message gains
near: rule.finest ≥ 18, which goes toMeshOptions. Abuiltrecord keeps itsnearflag, its pieces and its floors; their bytes count inKEPT_BYTES. - While
rule.finest ≥ 18, a wanted record withoutnearcounts as missing for the dispatch. It stays until its new build arrives, as GP5 does for a debug mesh. - A record that fails check (f) is
failed. fullis true whenR > 0andR_eff = Rin the last frame.stats.readyends at the first frame in which every wanted cell withinrangehas a record andfullholds. So after a jump it is the time to a complete mesh.Terrain.roadsLoadingreturnsroads.loading || plan.loading, sowaitForIdle()waits for the cells withinrange.
A cell is built in a frame when its record is empty, or built and passing (f), and, if one of its candidate nodes (section 6) is level 18, built with near.
5. Pools and floor slots (plan/pieces.ts)
- The vertex and index pools are one
PoolofVERTEX_BYTESandINDEX_BYTES. Each frame the pool takes the piece parts of built cells whose node's nearest LOD distance is underR, ordered by that distance alone, so a turn never reorders the pool. Under pressure the farthest parts drop first. - The floor buffer holds
FLOOR_SLOTS × 4225f32 (read-only storage, vertex stage). A slot holds one floor grid: a node and its level. A candidate node that needs a floor (section 6, step 5) keeps its slot, or takes the free slot that has gone longest without use. A slot is free when no candidate node of this frame uses it.write()uploads the grid of a newly taken slot (16,900 bytes). attach()makes the pools and the floor buffer on a new device; every piece and floor then uploads again from the records.
6. The effective range R_eff (each frame, after the selection)
The candidates are the leaves of rule.split within R, without the frustum cull: from each level-16 node with nearest distance under R, split while rule.split says so, and keep each leaf with nearest distance d < R. So no step below depends on the view direction.
R = planRange(rule, on)(section 10 setson).R_eff = R.- For each wanted cell with nearest distance
d < Rthat is not built (section 4):R_eff = min(R_eff, d). - For each entry of the pool's list (section 5) that the pool dropped, or that comes after the first
DRAWSentries:R_eff = min(R_eff, d). - Take the candidates in order of
d, and stop at the first withd ≥ R_eff. For each: ifdrawable(node)is false,R_eff = dand stop. Ifd < R_eff − FADE_Mand its cell holds a floor grid for this node and level, it needs a slot; if no slot is free,R_eff = dand stop.drawable(node)is true when the node's level is 17 or 18 andpages.drawing(node)is the node itself (its own page is resident). GP10 adds its conditions to this predicate only, so every refusal cutsR_eff. floorRange = max(0, R_eff − FADE_M).- The word of each drawn terrain node: its slot + 1 if it is a candidate with a slot from step 5 and
d < floorRange, else 0. - The draws: for each candidate with
d < R_eff, the pieces of its square (level 18: its own; level 17: those of its four children), each part with thenearrange if its cell has a level-18 candidate, else thefarrange, then itsstrip. The instance is[the part's origin x, origin y, the candidate's level, 0](sint32x4, whole world units). Over a level-17 candidate, each child's part takes its own origin. A part draws when its box meets the frustum: its node's square grown byover, betweenlow_mandhigh_m, by the corridors'inFrustum. It draws in thepiecespipeline when its box lies withinfloorRange(its node's far LOD distance plusoverin metres is underfloorRange), else inpieces cut(section 8). changedis true whenR_eff, the draws or the words differ from the last frame;Terrain.newContenttakes it.
Steps 2 to 7 are one pure function, effectiveRange(R, candidates, cells, pool, slots, drawable), so pieces.test.ts tests it without a GPU.
Invariant I1: every candidate within R_eff is drawable, lies in a cell built with the range that it draws, has its parts in the pool and among the first DRAWS, and has its floor in a slot if it is nearer than floorRange. So a pixel within R_eff shows a piece or plain terrain, never a sunk floor without its piece. A node border inside R_eff joins two built cells. Next to a node past R_eff, the shared border lies past R_eff, so its vertices are not floored on either side.
Invariant I2: R_eff, the slots and the words do not depend on the view direction. Only the frustum cull of step 8 does.
This is the defined fallback for every case that cannot draw: a cell not built yet, a failed cell, a full pool, too many draws, too few slots, a page on its way. The mesh ends at R_eff, and past it the terrain shows GP8's far look of the ways. No face covers the case.
7. Floors in the terrain (terrain/terrain.wgsl)
- Override
PLAN: bool(the switch) on every mesh pipeline.Node.unusedbecomesfloor: the slot + 1, or 0. - Group 0 binding 3:
floors: array<f32>, read-only storage, vertex stage: 7 of the 8 storage buffers. Surfacegainsplan_range: f32andfloor_range: f32afterlevels: 432 bytes.Terrain.writefills them withR_effandfloorRange.vertex_main:grid_vertex(u, level, snap)with the snap factorsnap = 1without the switch, and with itsnap = select(smoothstep(floor_range, plan_range, d), 1.0, plan_range <= floor_range), d the LOD distance of the vertex's grid point before the morph. At R = 0 (plan_range=floor_range= 0) the smoothstep is not defined, and the select gives 1. So no vertex withinfloor_rangemoves, and the snap is full pastR_eff. Then, ifPLAN,node.floor ≠ 0, the height is known, andlength(vec3f(lod_space(vec2f(u)), surface.above)) < surface.floor_range:f = floors[(node.floor − 1) × 4225 + vertex_index]; iff < 1e29, the height becomesmin(height, f)andfloored = 1.- The test reads the grid point
ubefore the morph, so a vertex keeps its floor through its morph; the floor holds at morph 0 and 1 (GP7, property d). - The varying
floored(location 6, in bothVaryingsvariants) is read only by theownedview. - The holed pipeline shares
vertex_main, so the floor holds at tunnel portals too. There is no discard, no clip distance and no cover pass, and the terrain stays one instanced draw (the epic).
8. The pieces' draw (plan/piece.wgsl, Pieces.draw)
-
plan/piece.wgslincludes../terrain/drawn-mesh.wgsland../roads/corridor.wgsl, and addsPieceIn(GP7's vertex and the instance),piece_place,piece_vertexandpiece_caster. -
piece_place:g = origin + position / 16, withoriginfrom the instance. For a terrain vertex,gis its level-18 grid point in whole world units (the division is exact). Over a level-18 node it takesgrid_vertex(g, 18u, snap); over a level-17 node,grid_vertex(even, 17u, snap)witheven = g − ((anchor + g) & vec2i(64));snapis the factor of section 7. If that height is unknown, the vertex keeps its stored height, so the strip still closes against the interior. Any other vertex takes its stored position and height. -
So a terrain vertex of the strip is the terrain's own vertex of the node's level at that grid point, made by the same function with the same integer input. Over a level-18 node it slides and morphs as the terrain's does; over a level-17 node it is the level-17 grid vertex, which is the strip "at morph 1". Where a level-18 node meets a level-17 node, the level-18 side has morph 1 (CDLOD), so both give the same vertex.
-
Same clip position:
terrain/drawn-mesh.wgslgainsrender_point(s: vec2f, z: f32, curvature: f32) -> vec3f:p = vec3f(to_render(s), z), thenp.z −= curvature × dot(p.xy, p.xy).vertex_mainandpiece_vertexboth call it, withz = h − frame.eye, and both writeclip = frame.view_proj * vec4f(p, 1.0).@invariantgoes on@builtin(position)in bothVaryingsvariants and inCorridorOut; the backstop usesvertex_main. Invariant: a terrain vertex of the strip and the terrain's vertex at the same grid point have bit-equal clip positions, so ∂R has no pixel crack, and a strip triangle that equals a terrain triangle wins thegreaterdepth test by its draw order (GP7, section 6). -
piece_vertexhas no bias. It writesCorridorOutfrom GP7's look word, across and along. In a debug view other thanoffandownedit clips away, as corridors do, so those views show the terrain as it draws. -
corridor_fragmentgains the overridesPIECE: boolandCUT: bool(both false for corridors). WithPIECE: it has no paint dither and no detail fade; an all-ground pixel drawscorridor_groundinstead of a discard (no terrain stands under it to draw it); inownedit returns a lit blue-grey. WithPIECEandCUT: it discards a pixel whose LOD distance is at leastsurface.plan_range. WithPIECEand notCUT, the shader has no discard, as the terrain's pipelines withoutHOLES. -
Two piece pipelines,
pieces(CUT = 0) andpieces cut(CUT = 1): fragmentcorridor_fragmentwithBAND = 1(no road paint on the mesh's ground faces) andPIECE = 1; triangle list, cull back; depthgreaterwith write. Only the parts whose box reaches pastfloorRangedraw inpieces cut: the nodes that cross the lastFADE_Mof the range. GP10's dither goes intopieces cutonly. -
Pieces.drawruns inTerrain.drawwhere corridors draw today, before the opaque water and the terrain: firstpieces, thenpieces cut, each nearest first. The early depth test then rejects the terrain under the pieces. -
Snap at the strip:
piece_placepasses the same snap factor togrid_vertexfor a terrain vertex of the strip, so a strip vertex and the terrain's vertex at the same grid point stay bit-equal. Withinfloor_rangethe factor is 0. Past it, the strip's snap words are 0 by construction: GP7's inner border lies a level-17 cell (3.25 m) or more from every non-terrain face, and every slope band reaches at leastfeather_min_m(1 m) past the core, so a strip vertex lies at least 4.25 m from the core outline, past the snap's reach (3.05 m at level 17, 1.52 m at level 18). The count of strip vertices with a snap word ≠ 0 proves it (target 0).
9. Casters
piece_caster takes the same piece_place and the sun's frame, with CASTER_PRIMITIVE and CASTER_DEPTH. It draws the casters range of every part in the pool whose node lies nearer than R_eff, in view or not, in both cascades (Terrain.cast). Pieces.arrived is the distance of the nearest part whose casters draw for the first time in this frame; Terrain.reshaped takes it as it takes roads.arrived.
10. Lower quality and the hysteresis
onbecomes false in the first frame withrule.finest < 17. It becomes true afterCALMframes in a row withrule.finest ≥ 17. At the start it is true.- With finest 17, every node within 959 m is level 17: pieces draw their
farrange over level-17 nodes, the strip is made of level-17 grid vertices, and the level-17 floors hold the terrain. Cells build withnear: false. - From finest 17 back to 18, a cell built without
nearis not built in a frame in which it has a level-18 candidate (section 4), soR_effstops before it until its new build arrives. - With finest 16,
R = 0: no piece and no floor. The worker builds only the records of each cell (GP3's candidates and GP3R's encoders; no overlay, no heights on the CPU, no mesh), and B is the look at every distance (GP8). The snap is full everywhere. This is the phone's lowest step; its build time is measured (Verification), and the owner accepts B near the eye or asks for more (question 1). - Why
CALM: the quality rises afterCALMcalm frames and can fall again a few frames later. The pieces then waitCALMframes more, so a quick fall never shows them. - The address parameter
finest(16 to 18, default 18) caps the rule's finest level (terrain/terrain.ts:796), so shots and counts show the phone's path on the GTX box.
11. What GP9 gives the other items
- GP10:
surface.plan_rangeandsurface.floor_range, the predicatedrawableand invariants I1 and I2. "Exactly where pieces draw" is then the test per pixellength(vec3f(s, surface.above)) < surface.plan_rangeover the candidates. GP10 adds the dither over the lastFADE_Minpieces cut; until then the mesh ends with a hard edge atR_eff. - GP11: the piece pipelines.
piece_vertexgivesCorridorOut; GP11 replaces the look (normals, ground faces and the strip with the terrain's own normal and facet). - GP12 to GP17: the
castersrange, the pools and the draw order. - GP8 and GP10: the snap factor;
surface.plan_rangeas the far look's stop.
12. The ownership count: debug=owned
DEBUG_VIEWSgainsowned(index 11;OWNED_VIEWinterrain/terrain.wgslandroads/corridor.wgsl).- Engine: in
ownedthe scene clears to black, and the sky, the instances and the houses do not draw.Terrain.drawdraws no structures and no water inowned. - In
owned, all detailed nodes draw with the plain mesh pipeline (no portal holes: an open portal is no crack). Then the backstop pipeline (overrideBACKSTOP, cull none) draws every drawn node again, with its floor,BACKSTOP_Mlower, in magenta (1, 0, 1). - The terrain fragment in
owned: green (0, 1, 0) wherefloored > 0and the pixel's LOD distance is undersurface.floor_range − TRIANGLE_M, else lit grey as incracks. Why: outside R no vertex is floored, sofloored > 0marks every terrain triangle with a corner inside R, the outer sub-cells of the strip included. WithinfloorRange − TRIANGLE_M, each such triangle has all its corners withinfloorRange, so each corner off ∂R is floored, and a green pixel is terrain that rose through the mesh. The band from there toR_effholds triangles with unfloored corners by design; it is GP10's fade. Pieces: lit blue-grey. - Why the backstop finds every crack: the eye stands above the drawn surface, inside the convex box of known heights. The drawn surface (pieces with their vertical faces, and the terrain) is continuous where it has no crack, and the backstop lies under it everywhere. A ray to a backstop point under the surface crosses the surface first, unless it passes through a crack. So a magenta pixel is a see-through crack at any pitch and at any distance, with or without sky in the view.
- Counts: magenta pixels are
cracks, green pixels areinside. The target of each is 0. tests/browser/trailview/harness.js:colourPixels(page, colours)counts exact colours in one screenshot;redPixelsbecomes its red case.flight.mjs --owned Nloads each flight withdebug=owned, hides#hudas--cracksdoes, and printsowned: {cracks: [...], inside: [...]}every Nth frame. It excludes--cracks.shoot.mjshides#hudand printsownedfor a view withdebug=owned, and theplanstats for every view.- If neither GP5 nor GP8 added it, both scripts gain
--query Q, a string appended to every view, for&ground=plan. - Limits: the eye must lie inside the box of known heights (true for the fixture's flights and views); count by day, as the flights do, because night light pools tint pixels.
13. Edge cases
- Empty cell (no way or water near): an
emptyrecord, no piece and no floor; no request if no feature tile covers it (GP5). - Cell at the edge of the data: the terrain drops each triangle with an unknown vertex; a strip vertex with an unknown drawn height keeps its stored height, so the strip closes against the interior, and no terrain stands beside it to crack against.
- Eye high above the ground: every LOD distance exceeds
R, so no candidate, no floor, no request. - Finest 17 and finest under 17: section 10.
- Level-18 node in its morph band (390 to 480 m): the strip's outer vertices slide and morph with the terrain's, through the same code.
- Level-17 node that morphs past
R: its pixels pastRdiscard inpieces cut; its vertices withinR + TRIANGLE_Mdo not morph. - Node that draws a coarser page:
drawableis false, soR_effstops before it until its own page arrives, and the far look and the snap act there (they read the coarser page's mask). The walk's range gate measures how often this happens. - Full pools, too many draws, too few slots, full budget, failed cell:
R_effshrinks (section 6);statscount each case. - A jump: every cell is new;
R_effgrows from 0 as cells arrive, nearest first. - Device loss: section 5.
- Two cells: a cell border inside
R_effjoins built cells (I1), whose meshes meet vertex for vertex on the locked border, also where one drawsnearand the otherfar(GP7, property c). - Owned region at the border of a cell past
R: every point of that border lies pastR_eff; its vertices are not floored. - Mesh heights outside the node's page range: each part culls by its own box (section 6, step 8); the terrain node keeps its page box.
- Steep ground and deep cuts: a floor can lie metres under the terrain; the backstop lies
BACKSTOP_Munder the floored terrain, so it stays under the mesh. - A cell switches from
fartonearwhen its first node splits into level 18: the whole cell at once, by GP7's rule, which its floors assume. - Eye under a piece (a deep fill, while the probe still reads the pages): cull back then shows the floored terrain from under the mesh. GP10 measures the readers and the probe.
- Without the switch: section 14.
- A node that crosses
R_eff: its terrain draws the far look per pixel pastplan_range(section 15) and nothing nearer. - A page whose blocks of the model's records have not arrived (GP8, section 3; not GP5's cell records): the coarser page draws, and the far look shows no way there until the blocks arrive and the page is stamped again. Inside
R_effevery cell is built, so its block exists. The wait is part ofplan.ready.
14. The old path
Without the switch, PLAN = 0, BACKSTOP = 0, PIECE = 0 and CUT = 0 fold every new branch away, the node words stay 0, plan_range and floor_range stay 0, and binding 3 holds a 16-byte buffer. Pieces and the plan do not exist. render_point and @invariant change no value, so shots without the switch are byte-equal to the merge base's. WAYS and the snap factor fold away without the switch (the factor is 1).
15. The far look stops where pieces draw
- Paint and ground terms: on the new path,
paint_sharereadssurface.plan_rangein place ofmasks.held: B paints and shapes the look only where a pixel's LOD distance is at leastplan_range. That is the test of today's mask paint, with the pieces in the corridors' place. GP10 replaces the hard edge with the dither of its fade band. - The lean terrain pipeline: a terrain node whose far LOD distance is under
floor_rangedraws withWAYS = false. Its pixels lie under pieces, or on plain terrain outside the owned region, where no way has a road term: outside every non-terrain face the height is the plain terrain, GP7's owned region holds every slope band, and GP8's shaped zone ends at the slope layer (GP8 section 2; the class form with its fixed 10 m reach would reach past a narrow slope band and fail this test). Proof: shots of every view of Pictures with and without the lean pipeline differ in no pixel. If a pixel differs, a way is missing from the mesh: the PR names it, and the lean pipeline stays off until it is fixed.
Plan
terrain/lod.ts: exportmorphStart(rule: Rule, level: number): number=rule.range / 2 ** (level + MORPH).terrain/terrain.ts: exportCALM. Add thefinestcap torule().plan/plan.ts(GP5'sPlan):rangein place ofBUILD_M,nearin the build message and the record, the payload inKEPT_BYTES, check (f), thenearrule of the dispatch,fulland the end ofstats.ready(section 4);cell(key)returns the record orundefined.plan/pieces.ts(new): the constants of section 1;planRange(rule, on),candidates(rule, eye, range)andeffectiveRange(...)(pure); classPieceswithattach(gpu, frames),update(eye, rule, drawn: Node[], plan, pages, frustum): { range, floorRange, words: Uint32Array, changed },write(),draw(pass, surface, ground, features),cast(pass, surface),arrived,floors: GPUBuffer,stats: { range, target, cells, pieces, vertices, triangles, floors, draws, cut, dropped, short, waiting, failed }. Its pipelines:piecesandpieces cut(layout: the corridors' frame group, surface, ground, features; GP7's vertex buffer and thesint32x4instance) andpiece casters(frame, surface).plan/piece.wgsl(new): section 8.terrain/drawn-mesh.wgsl:render_point.roads/corridor.wgsl: the overridesPIECEandCUT,OWNED_VIEWincorridor_fragment,@invariantinCorridorOut(section 8).terrain/drawn-height.wgsl:plan_rangeandfloor_rangeinSurface.terrain/unknown.wgsl,terrain/unknown-clip.wgsl:@invariantonclip, and@location(6) floored: f32inVaryings(andFragment).terrain/terrain.wgsl:Node.floor, binding 3, the overridesPLANandBACKSTOP,BACKSTOP_M,OWNED_VIEW,render_pointand the floor invertex_main, the backstop andownedbranches infragment_main.terrain/terrain.ts:SURFACE_BYTES = 432and the two fields; binding 3 in the mesh layout;meshPipeline(gpu, detail, holes, backstop)withPLANfrom the switch (cache the pipelines per switch value); under the switch makePieces, givePlan.updateitsrangeandfull, callPieces.updateafter the drawn nodes are sorted, write the node words, draw pieces before the opaque water, cast them, addplantoTerrainStats,roadsLoading,reshaped,failedandnewContent; theowneddraw with the backstop.view.ts:ownedinDEBUG_VIEWS; thefinestparameter (16 to 18).engine.ts: passfinesttoTerrain; inowned, clear black and skip the sky, the instances and the houses.tests/browser/trailview/harness.js,flight.mjs,shoot.mjs: section 12.tests/browser/trailview/smoke.test.js: one@gputest (Verification).plan/pieces.test.ts(new),plan/plan.test.tsandview.test.ts: Verification.README.mdof TrailView:ownedin thedebugrow, afinestrow,--owned Nin the flight line; shorten the--crackssentence so that the file stays within 800 words.
Additions to the steps above:
- Step 4 (
plan/pieces.ts): at R = 0, the plan asks the worker for the records only. - Step 5 (
plan/piece.wgsl): the snap factor inpiece_place; the count of strip vertices with a snap word. - Step 10 (
terrain/terrain.wgsl): the snap factor invertex_main;paint_sharewithsurface.plan_rangeon the new path. - Step 11 (
terrain/terrain.ts): the lean pipeline (WAYS = false) for nodes insidefloor_range, cached per switch value. - Step 13:
flight.mjs --toggle waysskips the pieces and their casters on odd frames (GP8's toggle).
Files
builder/web/src/lib/trailview/plan/pieces.ts: create: candidates, pools, floor slots,R,R_eff, draws, culling, casters, hysteresis, stats.builder/web/src/lib/trailview/plan/piece.wgsl: create:piece_place,piece_vertex,piece_caster.builder/web/src/lib/trailview/plan/pieces.test.ts: create: the range, the candidates,effectiveRangewith the node words, the hysteresis.builder/web/src/lib/trailview/plan/plan.ts,plan/plan.test.ts: change:range,near, payload, check (f),full,ready.builder/web/src/lib/trailview/plan/plan-worker.ts: change:neartoMeshOptions, the pieces and floors in the result (if GP7 did not add them).builder/web/src/lib/trailview/roads/corridor.wgsl: change:PIECE,CUT,OWNED_VIEW,@invariant.builder/web/src/lib/trailview/terrain/terrain.wgsl: change: floor,render_point, overrides, backstop,owned.builder/web/src/lib/trailview/terrain/unknown.wgsl,terrain/unknown-clip.wgsl: change:@invariant,floored.builder/web/src/lib/trailview/terrain/drawn-mesh.wgsl: change:render_point.builder/web/src/lib/trailview/terrain/drawn-height.wgsl: change: twoSurfacefields.builder/web/src/lib/trailview/terrain/terrain.ts: change: section 7 to 12 wiring,CALMexport,finestcap.builder/web/src/lib/trailview/terrain/lod.ts: change:morphStart.builder/web/src/lib/trailview/view.ts,view.test.ts: change:owned,finest.builder/web/src/lib/trailview/engine.ts: change:finest, theownedpass.builder/web/tests/browser/trailview/harness.js,flight.mjs,shoot.mjs: change: the count,--owned, the HUD,--queryif absent,planstats.builder/web/tests/browser/trailview/smoke.test.js: change: one@gputest.builder/web/src/lib/trailview/README.md: change: the address and flight lines. Public docs (docs/content/) do not change.builder/web/src/lib/trailview/ground/look.wgsl: no change (theWAYSpipeline is GP8's; GP9 uses it).terrain/features.wgsl: change:paint_share.builder/web/src/lib/trailview/plan/plan-worker.ts: change: the records-only build at R = 0.
Verification
- Checks:
npx vitest run src/lib/trailview(inbuilder/web).pieces.test.tsasserts:planRangeequalsmin(600, morphStart(17) − 40)for factors 4.6 and 8 with the eye at 1.6 m and at 300 m, is 0 when off, andrule.lodis at least 17 at every distance up toR + TRIANGLE_M;candidatesequalsselect's leaves withinRwith no cull, for any view;effectiveRangegivesRwhen all is built, and the distance of an unbuilt cell, of a cell built withoutnearthat has a level-18 candidate, of a dropped part, of the first part pastDRAWS, of a node that is not drawable and of a node without a free slot, and the word 0 for each node at or pastfloorRange; the hysteresis stays off forCALM − 1frames at finest 17 after a frame at 16, and turns on at the next.plan.test.tsasserts thatrange = 0wants no cell, and that a record withoutnearcounts as missing at finest 18 and as present at finest 17.view.test.tsasserts the round trip offinest=17anddebug=owned.npm run checkandnpm run check:wgsl: every new and changed WGSL file composes with its includes.flock /tmp/trailview-gpu.lock npm run test:trailview(inbuilder/web/tests/browser, on the GTX box): the new@gputest opensVILLAGEand the walk's start view with&ground=plan&debug=ownedand assertsterrain.plan.pieces > 0,cracks = 0andinside = 0. CI skips@gpu, so the pull request reports this local run.obc test affected --base origin/ground-planandobc prose --check(the README stays within its cap).- The count of strip vertices with a snap word ≠ 0: 0 in every view and flight frame. The lean pipeline's pixel test (section 15).
- Measurements (GTX 1660 SUPER,
flock /tmp/trailview-gpu.lock, frombuilder/web/tests/browser):- Frame gate, low tier first:
node trailview/flight.mjs --gpu --toggle ways, walk and valley-drive, 2560 × 1600, by GP8's clock rule (develop, new, develop, new in one session; the median clock per run; shares scaled to the session's median clock). Runs: develop (the old path of the same build, which is pixel-equal to develop) and&ground=plan&tier=low. The road share = terrain (on − off) + shadow and far-shadow (on − off) + the stamp per frame. Gate: on both flights, the new path's scaled road share is at most develop's scaled share plus the spread (owner: "about as fast as develop, ideally a little faster"). Then the high tier, the same runs and the same gate; if only the far layers break it, the owner decides with GP8's number. Report the parts: pieces, their casters, B pastR_eff, the stamp, and the saving of the lean pipeline; the p95 shares and the stamp's p95 per frame (question 3). - R = 0: the same toggle with
&ground=plan&finest=16, against develop atfinest=16. Gate: the same rule. - Discard cost: one walk run with the switch and every part in
pieces cut(a local change, not committed) against the run above. Report theterrainpass difference; it is the cost that the second pipeline saves on the GTX. - Old path pixel-equal: on the merge base of
ground-planand on this branch, each built withnpm run pretest:trailview:flock /tmp/trailview-gpu.lock node trailview/shoot.mjs --gpu --views FILE --width 1920 --height 960 --out DIRfor the views of Pictures without the switch;cmpeach pair of PNGs. Gate: every pair is byte-equal. If two shots of the base differ for a view, shoot the base twice; that view's gate is the base-to-base difference. - Count:
node trailview/flight.mjs --gpu --query '&ground=plan' --owned 10, and again with'&ground=plan&finest=17'. Gate: everycracksandinsidevalue is 0 in the walk and valley-drive flights. - Pools: the largest pool use and
dropped,short,failedand the draws over the flights and the views below. Gate:dropped = short = failed = 0, draws at mostDRAWS, and vertex pool, floors and index pool together within today's 47 MiB. - Range: the share of frames with
range < targetafter the first full range, with the cause (waitingfor a page, an unbuilt cell, a pool). Gate: 0 in the walk flight; report the valley drive, whose flight moves at about 245 m/s. - Jump:
plan.readythatshoot.mjsprints per view of the picture list below. Gate: the largest is at most 2,000 ms (the epic's budget), or the owner accepts the measured times. - R = 0 build: the records-only build per cell, native and WASM, and the fill after a jump at
finest=16on GP5's timing page. - Phone (the owner runs it; the PR gives the link and the steps): GP5's timing page at
finest=16andfinest=17, GP8's pools on the low tier (records, paint, move and share) against the phone's memory, and one far view. The PR proposes the low tier'sRECORDS_MBfrom it (GP8 question 3).
- Frame gate, low tier first:
- Pictures (
node trailview/shoot.mjs --gpu --width 1920 --height 960, with and without--query '&ground=plan', on one slider page):- At eye height and oblique (
road-views.json):v01-stairstov17-sharp-corners,i07-hairpin-eye,d-hairpin-15m,hp-15m,side-eye,b45-bridge-junction. - From above (
road-views-top.jsonof GP0): the-topviews of v01 to v17,i07-hairpin,d-hairpin,hpandb45-bridge-junction. - The same views with
--query '&ground=plan&debug=owned': the owned region in blue-grey, no magenta, no green;shoot.mjsprints the counts. - The phone's path,
'&ground=plan&finest=17':v06-road-path-broken,v12-good-town-junctions,v17-sharp-corners,i07-hairpin-eyeand their-topviews. - Finest 16 (R = 0) at
v06-road-path-broken,v12-good-town-junctions,v17-sharp-corners,i07-hairpin-eye,main-road-15mand their-topviews: B as the near look, no piece, no hole. The owner judges whether B near the eye is good enough for the lowest quality step. - The end of the range:
valley-eyeandmain-road-eyewith the switch: the mesh ends atR_effwith no gap (a hard edge until GP10). - The owner looks for: no tear, no crack, no sliver and no terrain through the mesh, at 2×, from above and at eye height; no step or seam in the geometry where the mesh meets the terrain at the strip; the same ground at level 17 as at level 18, with coarser terrain only outside the owned region. The look on the mesh is today's corridor look with facet normals; GP11 changes it.
main-road-15m,valley-eye,t-tertiary-eye: the snap acts only past the floors; no wobble of the far edge lines, and no seam where it starts.- v17, i07 and d-hairpin on the near mesh with GP6's
heights.legs=rampand =wall(W with the leg split of TrailView ground plan GP3 follow-ups: open decisions and review items #3132): the views for the owner's question 4.
- At eye height and oblique (
Done when
- The pull request into
ground-planhas merged with green CI and resolved review findings. - The checks pass, the
@gputest included (local run reported). cracks = 0andinside = 0in the walk and valley-drive flights at level 18 and at level 17, and in every view of the picture list; no strip vertex has a snap word.- The frame gate holds on both flights, low tier and high tier, or the owner decides on the measured numbers; the R = 0 gate likewise.
- The pool gate and the walk's range gate hold, and the discard cost is reported; the largest
plan.readyis at most 2,000 ms, or the owner accepted the measured times. - The phone's link and steps are in the PR.
- Shots without the switch are byte-equal to the merge base's.
- The slider page holds every picture of the list, the implementer looked at each one, and the owner has it for checkpoint 7, with the question-4 views.
Out of scope
- The plan layer, the readers under
drawn_height,plain_heightandstand_height, the mask reads, the probe of the eye, plants off water, the dither over the lastFADE_M, the terrain in the fade band and the look of the handover to the far look: GP10. - The look on the mesh: smooth normals, the strip and terrain faces with the terrain's own normal and facet (a faint shading seam at the strip can show until then), markings in junctions, kerbs, walls, steep faces: GP11.
- Deck plans, piers, abutments and portals (bridges near the eye show what
structures/bridges.tsdraws): GP12. Rails, sleepers and treads: GP13. Water beds and surfaces: GP14. Water crossings: GP15. Guardrails, posts and railings: GP16. Lamps and point objects: GP17. - The pieces, the strip and the floors themselves: GP7. The worker, the WASM bridge and the rest of
Plan: GP5. The far look, its paint and its snap words: GP8. - Deleting
debug=cracks, the corridors and the old stamp: GP19. - Heights and the leg rules: GP6. The leg split: GP3 follow-ups (TrailView ground plan GP3 follow-ups: open decisions and review items #3132).
Questions for the owner
- R = 0 (the lowest quality step): B at every distance is the near look; nothing else fits the budget. Recommendation: accept if the finest-16 shots show no stair, tooth or blob near the eye; else GP8 gets a near rule for B.
- Question 4 of the round: the near-mesh views of v17, i07 and d-hairpin with R and with W (legs split) are the views that the owner asked for. GP6 then deletes the other option.
- The stamp's p95: a stamp job with heights can spike on dense near pages. Recommendation: report the p95 shares; gate the medians only (owner: "about as fast as develop").
Decided (kept from the previous body)
Owner decision: every recommendation of the notes below is accepted.
Epic note: section 8 says a piece draws over "a node that draws at level 17 without a morph". GP9 reads this per point: the range keeps every drawn point of level 17 out of its morph. A test per node would take the pieces off most level-17 nodes past about 500 m.
Epic note: section 8 gives one range, but not what draws while a cell is not built or a pool is full. GP9 cuts the range to the nearest node that cannot draw, so the mesh always covers a full disk of nodes that can draw. GP10's readers then need only a distance test.
Epic note: section 8 says the floor is "one texture read per vertex". GP9 reads it from a storage buffer by vertex_index: one read per vertex, as the epic wants, but 65 × 65 grids do not tile texture layers.
Epic note: section 14, check 8 says "no terrain pixel inside the owned region". GP9 proves it within floorRange − TRIANGLE_M and proves no crack everywhere. In the last FADE_M the floor is off by the epic's section 8, so the terrain can show there; GP10 owns that band.
What changed from the previous body, and why
- Past
R_eff: GP8's far look of the records, not the previous far stamp. Reason: one road model. - The snap: on the new path past the floors, onto the model's core outline (owner-approved in TrailView ground plan GP8: the far stamp, one stamp function in every page on the new path, no snap #3102; interface comment 6639068685); no strip vertex snaps, by a stated bound.
- The far look stops where pieces draw (
paint_sharewithplan_range), and the lean terrain pipeline inside the floors' range, proven pixel-equal. Reason: the road terms cost every detail fragment. - R = 0: the worker builds only the records; B is the near look; the owner judges it.
- Frame gate: the owner's whole-road rule against develop with the fair toggle and the clock rule, low tier first, in place of "the same sum with and without the switch".
- Phone: GP9 owns the phone's memory and records run.
- Kept: floors, pools, strip, range,
R_eff, casters, hysteresis,debug=owned, the old path.
Generated by Claude Code
Activity
timohueser commentedon Nov 3, 2026
Deviation from GP0 (PR #3118), for this sub-issue's frame measurements: the viewer has no GPU timer of its own for corridors, band or water; all draw inside the one
terrainpass, and the viewer has no switch for "ways off". GP0 measured ways-off with a scratch copy offlight.mjsthat skips the corridors' draw and cast (see the baseline comment on #3096). The 0.8 ms gate compares theterrain+stamppasses with and without ways. Alternate the runs and correct for clock drift (the lighter frame drops the GPU clock by 2–7 %). If this sub-issue needs per-pass numbers for the new path, add its own timestamps or a switch.timohueser commentedon Nov 3, 2026
Open owner question from GP0 (baseline comment on #3096): develop's water draw near the eye costs 0.125 ms (walk) and 0.073 ms (drive) on the GTX, measured with the alternate-frame toggle. The epic's frame budget charges ways, water and ownership on the new side but compares them only with develop's corridors, band and stamp (0.27 ms walk, 0.42 ms drive). Until the owner decides, report both: the reference without and with develop's water.
timohueser commentedon Nov 4, 2026
Interface change from GP8 (owner approved, see #3102):
smoothstep(floor_range, plan_range, distance). GP9'svertex_mainandpiece_placepass the same snap flag, so terrain and strip vertices agree bit for bit. GP9 addsfloor_rangeandplan_rangetoSurfaceand counts strip vertices with a snap word ≠ 0 (target 0). I6 now reads: "No vertex withinfloor_rangemoves. No strip vertex moves."r(≤ 9 m) past an arm's own footprint, inside the corner's fan; GP6's owned region (I1) must cover this.acrosscode is finer (an asinh scale) on the new path only.Generated by Claude Code
timohueser commentedon Nov 5, 2026
DRAFT for review: a new body of GP9 for the one road model. This comment does not change the issue body. An independent reviewer reads it before the owner does.
Sources: the round's proposal (#3096 (comment), "GP9 changes in three parts"), its review (#3096 (comment)), the owner's decisions of 2026-11-05 (#3096 (comment)), the interface change from GP8 on this issue (#3103 (comment)), the GP7 and GP8 drafts, and on timosPC
/media/timo/OSM1/tmp/ground-plan/one-model/(shapes/budget/,shapes/bquality/,farb/,PROPOSAL-shapes.mdsection 7).How to read it. The floors, the pools, the strip draw, the range,
R_effand the crack count stay as approved. A section marked unchanged keeps the approved text word for word; the body edit copies it. The new text covers the three parts of the proposal (the far look pastR_eff, the snap past the floors, the far look's stop where pieces draw), the near look at R = 0, and the frame gate.TrailView ground plan GP9: ownership in the viewer: pieces near, the far look of the records past them, floors, the strip, the snap, crack count
Status: draft (one road model)
Part of #3096. One pull request into ground-plan.
Depends on: #3101 (GP7), #3102 (GP8, after its entry gate)
TL;DR
Goal: With
ground=plan, the viewer draws GP7's pieces near the eye before the terrain, holds the terrain under them by its floors, and closes the mesh against the terrain at the strip, so that no crack shows anywhere and no terrain pixel shows inside the owned region within the floors' range, at level 18 and at level 17. PastR_effthe terrain shows GP8's far look of the same records. The snap moves terrain vertices onto the model's core outline only past the floors. The far look stops where pieces draw. A GPU count proves the ownership over the walk and drive flights, and the whole road share of a frame, pieces included, is no larger than develop's.Problem: GP7 builds pieces and floors, and GP8 paints and shapes every page from the records, but nothing draws the pieces. Without GP9, GP8's far look paints at every distance, its snap moves every terrain vertex near a road, including the vertices that the floors and the strip must keep on the grid, and the road terms of the look cost every detail fragment near the eye where the pieces draw the roads anyway.
Decision: The approved ownership stays: the floor slot per node,
plan/pieces.tswith its pools,R_eff, the strip through the terrain's owngrid_vertex,@invariantclip positions,debug=owned. Three parts change: (1) pastR_effthe terrain shows GP8's far look (B); (2) the snap factor issmoothstep(floor_range, plan_range, d), the same invertex_mainandpiece_place, and no strip vertex has a snap word; (3) the far look's paint and ground terms stop where pieces draw, by the test of today's mask paint withsurface.plan_rangein place of the corridors'held, and terrain nodes wholly inside the floors' range draw withWAYS = falseif the shots prove that nothing changes. At R = 0 no piece draws and B is the look at every distance. The frame gate is the owner's: no slower than develop, with the pieces and their casters, by GP0's alternate-frame method against the flight with no ways.Rejected: unchanged (the approved list), plus:
PROPOSAL-shapes.mdsection 8).Systems: trailview
Contracts: none
Size: L (as approved, plus the snap factor, the paint's stop and the lean terrain pipeline; about 800 to 1,100 changed lines)
Facts
Unchanged (approved Facts), plus:
paint_shareis 1 pastmasks.held(terrain/features.wgsl:191-194).shapes/budget/, "(b) − ma32"). That is the most that a lean terrain pipeline inside the mesh range can save.shapes/budget/). The pieces replace the corridors.farb/item 1); before it, B near the eye showed the stair faults of the 8-bit mask (cost/page/page-vs-fragment.png).Design
1. Constants
Unchanged.
2. What GP9 takes from GP5, GP7 and GP8
Unchanged for GP5 and GP7. From GP8: under the switch, every page holds the model's heights, feature mask, moved byte and B's paint (and the far layers on the high tier); the corridors do not draw; the snap pass writes the snap words from the core distance. GP8's
WAYSoverride and--toggle waysare GP9's test tools.3. The range R
Unchanged.
4. Cells
Unchanged.
5. Pools and floor slots
Unchanged.
6. The effective range R_eff
Unchanged.
7. Floors and the snap in the terrain
Unchanged, with one change:
vertex_maincallsgrid_vertex(u, level, snap)withsnap = PLAN ? smoothstep(floor_range, plan_range, d) : 1, d the LOD distance of the vertex's grid point before the morph. With R = 0 (plan_range=floor_range= 0) the factor is 1 everywhere. So no vertex withinfloor_rangemoves, and the snap is full pastR_eff.8. The pieces' draw
Unchanged, with one change:
piece_placepasses the same snap factor togrid_vertexfor a terrain vertex of the strip, so a strip vertex and the terrain's vertex at the same grid point stay bit-equal. The strip lies at least one level-17 cell outside every non-terrain face, past the core outline's snap reach, so its snap words are 0 by construction; the count of strip vertices with a snap word ≠ 0 proves it (target 0).9. Casters
Unchanged.
10. Lower quality and the hysteresis
Unchanged, with one change for finest 16 (R = 0): no piece and no floor; B is the look at every distance (GP8), and the snap is full everywhere. B nearer than 560 m is the near look of that step. Its quality and cost are measured (Verification) and the owner accepts it or asks for more.
11. What GP9 gives the other items
Unchanged, plus: the snap factor;
surface.plan_rangeas the far look's stop.12. The ownership count:
debug=ownedUnchanged.
13. Edge cases
Unchanged, with these lines changed:
drawableis false, soR_effstops before it, and the far look and the snap act there (they read the coarser page's mask).R_eff: its terrain draws the far look per pixel pastplan_range(section 15) and nothing nearer.R_effevery cell is built, so its block exists.14. The old path
Unchanged, plus:
WAYSand the snap factor fold away without the switch (the factor is 1).15. The far look stops where pieces draw (new)
paint_sharereadssurface.plan_rangein place ofmasks.held: B paints and shapes the look only where a pixel's LOD distance is at leastplan_range. That is the test of today's mask paint, with the pieces in the corridors' place. GP10 replaces the hard edge with the dither of its fade band.floor_rangedraws withWAYS = false. Its pixels lie under pieces, or on plain terrain outside the owned region, where no way has a road term (outside every non-terrain face the height is the plain terrain, and GP7's owned region holds every slope band). Proof: shots of every view of Pictures with and without the lean pipeline differ in no pixel. If a pixel differs, a way is missing from the mesh: the PR names it, and the lean pipeline stays off until it is fixed. Expected saving: at most 0.33 / 0.26 ms.Plan
Unchanged (steps 1 to 16), plus:
terrain/terrain.wgsl): the snap factor invertex_main;paint_sharewithsurface.plan_rangeon the new path.terrain/terrain.ts): the lean pipeline (WAYS = false) for nodes insidefloor_range, cached per switch value.plan/piece.wgsl): the snap factor inpiece_place; the count of strip vertices with a snap word.flight.mjs --toggle waysskips the pieces and their casters on odd frames (GP8's toggle).Files
Unchanged, plus
ground/look.wgsl(theWAYSpipeline is GP8's; GP9 uses it) andterrain/features.wgsl(paint_share).Verification
npm run check,npm run check:wgsl, the@gputest,obc test affected,obc prose --check), plus the count of strip vertices with a snap word ≠ 0 (0 in every view and flight frame) and the lean pipeline's pixel test (section 15).flight.mjs --gpu --toggle ways, walk and valley-drive, 2560 × 1600, GTX 1660 SUPER, two runs each, one session,flock /tmp/trailview-gpu.lock, the clock printed per run. Runs: develop (the old path of the same build, which is pixel-equal to develop), and&ground=planon the high and the low tier. The road share = terrain (on − off) + shadow and far-shadow (on − off) + the stamp per frame. Gate: on both flights and both tiers, the new path's road share is at most develop's, within the run-to-run spread (owner: "about as fast as develop, ideally a little faster"). Report the parts: pieces, their casters, B pastR_eff, the stamp, and the saving of the lean pipeline. Also report the p95 shares and the stamp's p95 per frame (a stamp job with heights can spike by up to 1.7 ms on dense near pages).&ground=plan&finest=16, against develop atfinest=16. Gate: the same rule. Pictures below.--owned 10at finest 18 and 17, pools, range, jump), without the approved frame gate.debug=owned, finest 17 and 16, the end of the range), plus:main-road-15m,valley-eye,t-tertiary-eye: the snap acts only past the floors; no wobble of the far edge lines, and no seam where it starts;v06-road-path-broken,v12-good-town-junctions,v17-sharp-corners,i07-hairpin-eye,main-road-15mand their-topviews: B as the near look. The owner judges whether B near the eye is good enough for the lowest quality step;heights.legs=rampand =wall(W with GP3's leg split): the views for the owner's question 4.Done when
ground-planhas merged with green CI and resolved review findings.@gputest included (local run reported).cracks = 0andinside = 0in the walk and valley-drive flights at level 18 and at level 17, and in every view of the picture list; no strip vertex has a snap word.plan.readyis at most 2,000 ms, or the owner accepted the measured times.Out of scope
Unchanged, with: the far look itself and its snap words: GP8; heights and the leg rules: GP6.
Questions for the owner
What changed from the approved body, and why
R_eff: GP8's far look of the records, not the approved far stamp. Reason: one road model.paint_sharewithplan_range), and the lean terrain pipeline inside the floors' range, proven pixel-equal. Reason: the road terms cost every detail fragment.R_eff, casters, hysteresis,debug=owned, the old path.Generated by Claude Code
timohueser commentedon Nov 6, 2026
Deviations of the live viewer (branch
claude/gp9-live, draft PR intoground-plan) from this body. The orchestrator asked for a live viewer on GP7-C's 2D mesh and GP8's far look within a day; the body above predates both. These are the differences.plan/cells.wgsl). A vertex sinks only where a mesh draws its cell, on the level-18 plateau of the LOD, and where every cell that holds the point has a mesh.R_eff, candidates or pools. A cell's mesh draws when the cell holds a level-18 node and its corners hold their heights. Over the morph band of level 18 (the plateau's end to level 17) the mesh fades out by a dither, and the terrain there neither sinks nor loses its snap; past it, B draws. So GP10's fade band is in place, without GP10's readers.grid_vertex_by, so the strip's terrain vertices stay bit-equal (@invarianton both).height_at, the plain terrain from the stored pages; the terrain rule takes the drawn page), the paved faces' normals (central differences of the function) and the look words, at most 8,192 corners a frame. Ground faces of the mesh take the pages' normal and B's ground terms per pixel, so they shade as the terrain beside them.Planbuilds only forplan=faces. GP7-C's worker-side rim wait is not used.debug=ownedwith its backstop and green count. In its place,debug=crackswithground=plandraws the meshes grey and the sunk terrain red: a red pixel is a crack or a hole in a mesh. The far look's paint still runs under the meshes (noplan_rangestop, no lean pipeline).Generated by Claude Code
timohueser commentedon Nov 6, 2026
GP9 landed in
ground-planwith PR #3164 (merge cacad6d), after the review, the fixes and the delta review. The live viewer draws the GP7-C pieces near the eye and GP8's far look B past them, on one model. Frame against develop: +0.2 ms walk, +0.36 ms drive (mean). Corner pass worst frame 3.3 ms. A full mesh is ready in 2.4–2.8 s. The remaining look faults are in the follow-up issue.Generated by Claude Code