Skip to main content

mapv10 Viewer-Side Audit — Wave 1 Agent B

Generated: 2026-05-12
Audit scope: TypeScript/JS viewer files only. Generator code excluded.


Governance lines relied on

  • CLAUDE.md:26"Never use billboards or 2D primitives in a 3D scene where a real mesh is the correct primitive"
  • CLAUDE.md:29"Never bbox-quad a complex shape"
  • CLAUDE.md:30"When you patch the same subsystem 3+ times, the original design is wrong"
  • .claude/rules/orchestrator-mode.md:53"Not spawn further sub-agents. Forbidden tools: Agent, Task, spawn_task, ScheduleWakeup, CronCreate, and any other scheduling tool."
  • .claude/rules/mapv10.md:30"Missing required generated artifacts must fail loudly."
  • .claude/rules/mapv10.md:34"The target is 3D terrain with real water/border/overlay geometry or stable generated data products, not a flat 2D political image."

Files relevant to the wave

PathLOCRole
examples/map/mapv10/viewer/src/renderer/lod/VisibilitySet.ts884Pure-function tile selector: camera snapshot, SSE computation, primary/underlay/preload sets
examples/map/mapv10/viewer/src/renderer/lod/RenderResolver.ts590Stateful resolver: maps VisibilitySet to RenderCommit; ancestor-walk, kicker fallback, coverage-hole tracking
examples/map/mapv10/viewer/src/renderer/lod/TileStreamingPlanner.ts402Imperative planner: turns VisibilitySet into cache fan-out + scheduler reconciliation; runtime-tile pin/unpin
examples/map/mapv10/viewer/src/renderer/Mapv10ThreeRenderer.ts7541Main renderer class: mount/loadRun/tickFrame; terrain, water, route, label scene management; zoom-band logic
examples/map/mapv10/viewer/src/renderer/MaterialFade.ts2505All shader material factory functions; unified terrain shader (11 samplers + mode dispatch); fade constants
examples/map/mapv10/viewer/src/renderer/LabelTextureCache.ts120Memoized canvas-texture builder keyed by label text string
examples/map/mapv10/viewer/src/scenarios/mapv10_scenarios.json98825 regression scenarios with camera, mode, layer, and gate definitions
examples/map/mapv10/viewer/scripts/scenario-gates.mjs488Gate evaluator: dispatches on gate.type; returns GateFailure[]

Section 1 — VisibilitySet.ts (884 lines)

Exports / public surface

  • TERRAIN_GEOMETRIC_ERROR_TARGET_PX (20) — 1.5 px geometric SSE threshold
  • TERRAIN_RASTER_SAMPLE_ERROR_TARGET_PX (34) — 6.0 px raster sample SSE threshold
  • VIEW_FOOTPRINT_TILE_MARGIN (42) — 0.6 tile expansion margin
  • FULL_COVERAGE_PRIMARY_TILE_LIMIT (50) — 64 tiles; levels at or below this count render whole-level
  • PRIMARY_LOD_VIEWPORT_FRACTION (53) — 0.45 fraction of viewport height for focal disk
  • SCREEN_AREA_WEIGHT, DEPTH_PENALTY_PER_LEVEL, FOREGROUND_BIAS (56–58)
  • FOVEATION_BIAS (100), FOVEATION_RADIUS_NDC (101)
  • buildCameraSnapshot (268) — constructs immutable CameraSnapshot from live THREE.PerspectiveCamera
  • selectVisibilitySet (328) — pure function; returns VisibilitySet including primary, underlay, preload, SSE map, signature
  • computeScreenSpaceError (503), computeTerrainRasterSampleScreenSpaceError (524), computeTileTerrainLodScreenSpaceError (543) — exported SSE utilities

Function inventory

  • buildCameraSnapshot (268) — builds immutable per-frame camera + view-projection snapshot
  • selectVisibilitySet (328) — pure tile-selector; calls pickPrimaryZ, collectPrimary, collectUnderlay, optional preload
  • computeScreenSpaceError (503) — generic geometric SSE in pixels from error-metres + camera params
  • computeTerrainRasterSampleScreenSpaceError (524) — raster-sample SSE from tile.sourceWindow metadata
  • computeTileTerrainLodScreenSpaceError (543) — combined {geometric, rasterSample, max} for one tile
  • pickPrimaryZ (476) — internal; walks sortedLevels coarse-to-fine and returns first z meeting BOTH SSE targets
  • collectPrimary (570) — internal; gathers tiles at primaryZ within focal footprint bounds
  • collectUnderlay (606) — internal; emits exactly primaryZ-1 parent set (Wave 2 contract)
  • populatePriorityAndSse (658) — internal; fills priorityByKey and ssePixelsByKey for one slot
  • foveationFactor (725) — internal; projects tile centre to NDC, returns linear proximity to pointer
  • distanceToTileFootprintKm (749) — internal; 3-D distance from camera to closest point of tile rectangle
  • buildSignature (762) — internal; deterministic string from sorted primary+underlay keys + primaryZ
  • projectControlsTargetToWorld (768) — internal; maps scene target to world-km, clamped to bounds
  • primaryFootprintBounds (789) — internal; computes km-space bounding rect for tile collection
  • focalRadiusKm (819) — internal; half-projected-diagonal capped by world extent
  • clampBounds, tileIntersectsBounds, pointInBounds, distance2D, clamp — small geometric utilities
  • toActiveTileSlot (868), compareActiveTileSlots (877), tileKeyOf (881) — slot helpers

Visual-foundation findings

  • Q1 / z5 threshold: pickPrimaryZ (VisibilitySet.ts:487–500) walks sortedLevels coarse-to-fine and returns the first z where errorPx.geometric <= 1.5 AND errorPx.rasterSample <= 6.0. At ~60–100 km altitude the raster-SSE test dominates because computeTerrainRasterSampleScreenSpaceError (524) uses tile.sourceWindow.sampleStep to compute the sample footprint. For the continent-lod6 fixture the primaryRasterSsePx gate values in scenarios confirm that at 55–85 km the resolver reaches z5 but the raster SSE is 20–30 px — well above the 6 px target. The gap is the known open Wave 3b architectural issue.

  • Slab risk from FULL_COVERAGE_PRIMARY_TILE_LIMIT: collectPrimary (584) returns ALL tiles at a zoom level when counts.x * counts.y <= 64. At coarse LODs (z0, z1) this means a full-level quad is always rendered and may remain visible behind a refinement frontier if the resolver places the parent in the underlay set. The underlay cap MAX_ANCESTOR_Z_DISTANCE = 1 (RenderResolver.ts:27) limits this to one parent level, but a whole-level z0 quad is still a rectangular footprint potentially covering areas where z3+ tiles are primary.

  • No edge-stitch or skirt: Neither collectPrimary, collectUnderlay, nor the geometry-decoding path imports any seam-stitching or skirt-mesh mechanism. Tile adjacency is handled by the shader's uTexInnerRegion UV remapping (inner-padding contract from MaterialFade.ts:211), which prevents bilinear bleed at sidecar tile boundaries but does not address geometry-level height discontinuities at tile edges.

  • Preload set excluded from signature (VisibilitySet.ts:459–461): preload churn is intentionally excluded from the visibility signature. This means a predicted-preload set arriving while the camera is static produces no render rebuild — correct design, but means the verifier cannot confirm preload tiles are healthy from the signature alone.


Section 2 — RenderResolver.ts (590 lines)

Exports / public surface

  • MAX_ANCESTOR_Z_DISTANCE (27) — 1; structural-parent walk bounded to immediate parent only
  • REQUIRED_SIDECARS (39–46) — tuple: materialWeights, materialWeightsB, biome, waterMask, borderSdf, normalRoughness
  • RenderResolver class (152) — stateful; resolve(plan, caches), notifyRendered(commit), inspectPreviousRenderedKeys(), dispose()
  • Type exports: TerrainRenderAsset, RenderFallbackSource, RenderFallbackSlot, RenderFallbackMetrics, RenderCommit, SceneCacheView

Function inventory

  • RenderResolver.resolve (157) — core: iterates primary slots; attempts direct residency → structural-parent ancestor → previous-rendered kicker → coverage-hole
  • RenderResolver.notifyRendered (308) — updates previousRenderedSlots from the just-drawn commit; called by renderer after every frame
  • RenderResolver.inspectPreviousRenderedKeys (334) — read-only diagnostic; used by tests
  • RenderResolver.dispose (339) — clears state; throws on subsequent resolve calls
  • RenderResolver.incrementFallbackDuration (345) — private; accumulates per-primary fallback frame counter
  • findStructuralAncestorWithin (379) — walks tile quad-tree parent chain at most maxDistance steps; returns first fully-resident ancestor from underlayByKey
  • pickKickedFallback (419) — scans previousRenderedSlots; returns a bounds-containing previous tile within z-distance cap
  • isFullyResident (471) — discriminated union; AND of CPU raster payload + GPU texture readiness via TextureResidencyView
  • sidecarsResident (493), sidecarPresent (506) — helpers for residency checks
  • summarizeFallbackSlots (532) — aggregates slot list into RenderFallbackMetrics
  • boundsContains (552) — EPSILON-tolerant containment check for kicker validation
  • buildCommitSignature (566) — terrain + runtime + coverage-holes → string signature

Visual-foundation findings

  • Slab mechanism: pickKickedFallback (RenderResolver.ts:419–451) allows previous-rendered tiles from previousRenderedSlots that are at most MAX_ANCESTOR_Z_DISTANCE = 1 coarser than the primary. However previousRenderedSlots is keyed by meshKey, NOT by primary tile key. The notifyRendered path (308–332) overwrites the entire map each frame. If a primary at z5 has no direct residency and no underlay (planner did not emit its z4 parent into the underlay set), the kicker walk can return a z4 tile that covers the same bounds. That z4 tile is a full z4 rectangle — a "slab" one LOD coarser than the primary frontier. The renderedMeshKeys de-duplication (RenderResolver.ts:211, 219, 232) prevents the same mesh drawing twice, but does not prevent an ancestor-level rectangle from covering child-level primary slots while waiting for residency.

  • Coverage-hole = clear color: coverage-hole slots (RenderResolver.ts:251–258) show the renderer's clear color 0x14110d (Mapv10ThreeRenderer.ts:797). This is visible as dark brown patches where primary tiles have neither mesh nor ancestor/kicker. Not a slab, but a different visibility artifact.

  • Underlay limited to underlayByKey (RenderResolver.ts:176–179): the structural-parent walk consults only the planner-emitted underlay set. If the planner did not emit a parent tile (e.g. because it fell outside the focal footprint bounds), the resolver returns null for that step even if the tile is in the cache — intentional Wave 2 design, but means the structural parent walk CAN fail when the camera pans and the underlay footprint lags by one frame.


Section 3 — TileStreamingPlanner.ts (402 lines)

Exports / public surface

  • TileStreamingPlanner class (102) — apply(set, auxiliaryLayers), dispose()
  • MAX_MISSING_ROUTE_ASSETS_PER_FRAME (84) — 8
  • Interfaces: TerrainCacheLike, RuntimeCacheLike, MeshAssetCacheLike, SchedulerLike, PlannedSelection, AuxiliaryLayerState

Function inventory

  • TileStreamingPlanner.apply (116) — main method; fans out VisibilitySet to four caches; manages runtime-tile pin/unpin
  • TileStreamingPlanner.dispose (259) — unpins all pinned keys; marks disposed
  • uniqueSorted (270), slotByTerrainKey (274), slotByTileKey (282) — key-index helpers
  • bandedPriorityMap (290), deriveBandedPriorityMap (304), lookupRuntimeBoost (320) — priority-inheritance helpers
  • selectRouteKeysForStreaming (332) — limits pending/missing route keys to caps
  • excludeOversizedRouteAggregates (364) — filters aggregate keys exceeding 1 MB
  • defaultCurrent (383), appendDesired (387) — scheduler-entry helpers

Visual-foundation findings

  • Preload: terrain + runtime only, no aux (TileStreamingPlanner.ts:130–138): predicted-preload tiles fill preloadTerrainKeys and preloadRuntimeKeys but preloadRuntimeKeys is never populated — preloadRuntimeKeys is declared but stays empty on line 133. As a result preload slots stage terrain geometry only, not runtime sidecars. When the camera arrives at the preloaded position, the tile may be mesh-resident but sidecar-not-resident, routing through the ancestor/kicker/coverage-hole paths for at least one additional frame.

  • Aux: water uses primaryRuntimeKeys only (TileStreamingPlanner.ts:143–145): water mesh keys are derived from primaryRuntimeKeys (only tiles whose mesh is already loaded), not from the full allCurrentSlots set. A newly-arrived primary tile whose mesh just loaded this frame contributes its tile key to primaryRuntimeKeys only if terrainTileCache.hasLoaded(slot.terrainMeshKey) returns true at this apply() call. If the terrain cache loads and the runtime cache is still pending on the same frame, renderableCurrentSlots is empty and no water request is issued.


Section 4 — Mapv10ThreeRenderer.ts (7541 lines)

Exports / public surface

  • Mapv10ThreeRenderer class (416) — implements Mapv10Renderer; all public API is on this class

Key function inventory (partial — major methods only)

  • mount (779) — attaches to canvas; creates WebGLRenderer, OrbitControls, sampler probe, lighting
  • loadRun (849) — async; calls prepareStrategicMapResources, prepareTerrainStreaming, buildGeneratedLayers
  • setMapMode (952) — writes uModeId uniform; triggers LOD/visual config and color recompute
  • setLayerState (975) — controls terrain/water/borders/routes/labels/markers/wireframe visibility
  • frameSelection / frameWorld (987–1025) — heuristic camera framing
  • setCameraPose (1274) — deterministic pose setter for scenarios
  • waitForTilesSettled (1324) — async; polls currentViewResidency()
  • getTerrainSelectionProbe (1570) — probe: primaryZ, role-split SSE telemetry, ancestor distance
  • getDebugState (1723) — large diagnostic aggregate: label texts, sidecar readiness, coverage holes, etc.
  • zoomBandForDistance (6124) — classifies distance to target as continent / realm / province / location
  • selectLabelCandidates (4184) — label layout; zoom-band filter; text-dedup via seenTexts
  • prepareTerrainStreaming (implicit ~2580) — builds caches, planner, resolver
  • applyTerrainRenderCommit (2747) — paces terrain node creation by TERRAIN_NODE_CREATES_PER_FRAME

Numeric constants with visual-LOD relevance

ConstantFile:lineValuePurpose
TERRAIN_GEOMETRIC_ERROR_TARGET_PXVisibilitySet.ts:201.5LOD selection geometric threshold
TERRAIN_RASTER_SAMPLE_ERROR_TARGET_PXVisibilitySet.ts:346.0LOD selection raster threshold
FULL_COVERAGE_PRIMARY_TILE_LIMITVisibilitySet.ts:5064Levels with ≤ 64 tiles render whole-level
VIEW_FOOTPRINT_TILE_MARGINVisibilitySet.ts:420.6Tile footprint expansion (single margin replaces enter/retire pair)
MAX_ANCESTOR_Z_DISTANCERenderResolver.ts:271Parent-walk depth cap
LOD_FADE_MSMaterialFade.ts:19320Fade duration for non-terrain surfaces
TERRAIN_UNDERLAY_OFFSET_KMMapv10ThreeRenderer.ts:185-0.16Depth offset for underlay terrain meshes
CAMERA_COHERENCE_SELECTION_FRAMESMapv10ThreeRenderer.ts:2012Frames to hold selection after camera write
controls.minDistanceMapv10ThreeRenderer.ts:82735 (initial); then max(18, maxExtent * 0.025)Interactive floor
controls.maxDistanceMapv10ThreeRenderer.ts:828260 (initial); then max(260, maxExtent * 1.8)Interactive ceiling
Continent zoom thresholdMapv10ThreeRenderer.ts:6126max(130, extent * 0.95)Camera-to-target distance where band is "continent"
Realm zoom thresholdMapv10ThreeRenderer.ts:6127max(85, extent * 0.6)Realm/province band boundary
Province zoom thresholdMapv10ThreeRenderer.ts:6128max(50, extent * 0.32)Province/location band boundary
Water mesh vertical offsetMapv10ThreeRenderer.ts:26460.12 kmWater meshes sit 0.12 km above sea level
Route mesh vertical offsetMapv10ThreeRenderer.ts:26560.28 kmRoute ribbons sit 0.28 km above terrain

Visual-foundation findings (Q1–Q6 answers embedded)

Q1. z5 close zoom threshold: pickPrimaryZ (VisibilitySet.ts:487) walks levels coarse-to-fine; z5 is selected when BOTH geometric <= 1.5 px AND rasterSample <= 6.0 px. At ~60–100 km altitude on the continent-lod6 fixture the scenarios confirm the raster SSE is 20–30 px even at z5 (scenario gates use max: 30). The z5 raster sample size is tile.sourceWindow.sampleStep samples across the tile extent — a z5 tile is 75 km wide / ~32 samples = ~2.3 km/sample, which at 80 km altitude and 45° FOV projects to ~10+ px. This is the unresolved Wave 3b gap.

Q4. Political/route layer zoom gating: setLayerState (975) accepts a MapLayerState from the caller and stores it. Layer visibility is applied directly — there is no renderer-side zoom-band gate on political or route layers. However, selectLabelCandidates (4184–4249) does apply a zoom-band filter via ZOOM_ORDER[label.minZoomBand] and ZOOM_ORDER[label.maxZoomBand] against the current zoomBandForDistance result. Labels are zoom-gated; route/political terrain visibility is caller-controlled only.

Q5. Water / coastline path: Water is rendered as separate mesh objects (waterMeshCacheMeshAssetCache with verticalOffsetKm: 0.12). The unified terrain shader handles inland water (lake/river masks) via the uWaterMask sidecar — the fragment-level waterMask * waterBlend (MaterialFade.ts:483–488) composites inland water colour directly onto the terrain fragment without a separate draw. Ocean is both shader-driven (sea tone for fragments where global id = 0) AND separate generated sea-surface meshes that sit at 0.12 km above terrain. Z-fighting risk: the separate water meshes at 0.12 km must clear the terrain mesh's displaced surface in the coastline transition zone. The terrain fragment shader's seaAlpha path (MaterialFade.ts:486–487) partially fades sea-extent terrain fragments, but the actual coastline boundary is where the sea mesh edge meets the terrain mesh. No explicit z-fighting mitigation (depth offset, stencil, or polygon offset) was found in the audit files. The water mesh uses emitNormals: true and is a real 3D mesh, not a billboard — consistent with the CLAUDE.md tenet.

Q2. Rectangular footprint paths:

  • Underlay from FULL_COVERAGE_PRIMARY_TILE_LIMIT: when a level has ≤ 64 tiles, the entire level is collected, resulting in a rectangular tile or set of tiles that may remain visible behind finer primary tiles during the fade window (LOD_FADE_MS = 320ms).
  • Kicker fallback: pickKickedFallback returns a previous-rendered tile whose bounds contain the primary's bounds. If the previous tile is one level coarser, it covers a 2×2 child-footprint rectangle.
  • Debug helpers: renderPasses.pickingDebug group exists (Mapv10ThreeRenderer.ts:364, 808) but no contents were traced to permanent billboard-style primitives.
  • TERRAIN_UNDERLAY_OFFSET_KM = -0.16 (Mapv10ThreeRenderer.ts:185): underlay tiles are drawn 0.16 km below sea level to avoid z-fighting with the primary surface. An underlay tile that is one LOD coarser than the primary frontier is a full rectangular coarse mesh drawn below and behind the primary — visible at the tile edges where the primary does not yet cover (e.g. coverage holes).
  • Shader placeholder race: when a terrain mesh draws before its sidecar textures are ready, uRoughnessMapPresent = 0 collapses the roughness term to roughnessFactor = roughness and the splat branch falls back to baseColor = uColorGrass (MaterialFade.ts:461–463). This produces a flat-grass-coloured rectangular tile for one frame — not a persistent slab, but a single-frame visual artifact.

Q3. Label deduplication: LabelTextureCache (34) keys its entries map by text string — two labels with identical text SHARE the same GPU texture. Each label still owns its own THREE.Sprite with its own SpriteMaterial so per-sprite opacity is independent. The visible-label deduplication is in selectLabelCandidates (4214–4219) via a seenTexts Set: higher-priority anchors win; same-text lower-priority anchors are dropped. This is a defense-in-depth guard added in Wave 4 to catch generator regressions (comment at 4203–4212). The LabelTextureCache itself has no entity-id key — it is keyed by text only. Two entities with different IDs but the same name share one texture, which is correct (saves GPU memory) but means renaming one entity would need to release and re-acquire the cached texture.


Section 5 — MaterialFade.ts (2505 lines)

Exports / public surface

  • LOD_FADE_MS (19) — 320 ms; duration for non-terrain fades
  • FADE_REMOVAL_EPSILON (22) — 0.001; eviction floor for fading actors
  • TERRAIN_MODE_* constants (221–226) — mode IDs 0–5 for the unified shader
  • computeFadeProgress (35) — pure function; appear/dismiss linear ramp
  • All createFadeable*Material factory functions and apply* uniform-setter functions
  • Shader-string constants: UNIFIED_FRAGMENT_REPLACEMENT, ROUGHNESS_MAP_FRAGMENT_REPLACEMENT, etc.

Function inventory (selected)

  • computeFadeProgress (35) — pure linear ramp [0,1]; dismiss overrides appear
  • injectTileOpacityUniform (693) — patches vertex+fragment shader with uTileOpacity uniform
  • injectUnifiedTerrainVertexShader (713) — adds vTerrainSceneY varying after <begin_vertex>
  • injectUnifiedTerrainShader (735) — replaces <color_fragment> with 11-sampler mode dispatch
  • injectRoughnessMapUniform (756) — replaces <roughnessmap_fragment> with Toksvig α²-space combine
  • createFromShaderLib (787) — master factory: ShaderLib-derived ShaderMaterial with per-mesh uTileOpacity
  • createFadeableTerrainMaterial (implicit) — calls createFromShaderLib with unifiedTerrain: true, roughnessMap: true
  • createFadeableOceanMaterial, createFadeableLakeMaterial, createFadeableRiverMaterial — separate water-surface factories
  • setTerrainMode, setBorderSdfOpacity, setHoveredLocationId, setRouteVisualStyle — uniform setters
  • applyTileSplatMap, applyTileBorderSdf, applyTileWaterMask, applyGlobalIdMap — per-tile/per-run binding helpers

Visual-foundation findings

  • Terrain fade is disabled by design: LOD_FADE_MS's docstring (MaterialFade.ts:12–18) explicitly states terrain coverage is not alpha-crossfaded because "alpha-crossfading two terrain LOD rasters blends different splat/water/SDF masks and produces temporal shoreline shimmer." Instead the resolver's kicker/ancestor pattern keeps the coarser tile opaque until the finer tile replaces it without a blend. This means: a stuck fade (FADE_REMOVAL_EPSILON = 0.001) on a non-terrain surface (auxiliary batch, label) could leave it at a near-zero opacity "ghost" that persists until eviction. The terrain slab itself will always appear at full opacity — the fade concern is on water/route batch surfaces.

  • Unified shader water composition (MaterialFade.ts:479–489): sea fragments (waterMask > 0.001 AND seaMask > 0) receive terrainAlpha = mix(terrainAlpha, seaAlpha, seaMask) where seaAlpha = mix(0.035, 0.34 or 0.48, seaNearFactor). This makes terrain fragments partially transparent where open sea sits, revealing the scene clear-color. The generated sea-mesh sits at verticalOffsetKm: 0.12 and draws on top. If the sea mesh is missing or culled for a tile, the transparent sea-fragment terrain + clear-color background is the only visual — the sea mesh being absent would show as darker/empty ocean with no depth.

  • uRoughnessMapPresent fallback (MaterialFade.ts:675–681): when no Toksvig sidecar is bound, the gate uRoughnessMapPresent = 0 collapses to roughnessFactor = roughness. This is an intentional silent fallback for the first draw of a newly-active mesh before the texture is ready. The docstring notes this as a "race-frame" placeholder. This is a known single-frame silent default.


Section 6 — LabelTextureCache.ts (120 lines)

Exports / public surface

  • LabelTextureCache class (34) — acquire(text), acquireWithStatus(text), release(text), size(), setSamplerProbe(probe), dispose()
  • LabelTextureEntry interface (23)

Function inventory

  • acquire (42) — thin wrapper over acquireWithStatus
  • acquireWithStatus (46) — returns existing (refcount++) or calls buildEntry; reports created: boolean
  • release (57) — decrements refcount; disposes texture and removes entry when refcount reaches 0
  • size (70) — returns entries.size
  • dispose (75) — disposes all textures; clears map
  • buildEntry (82) — creates canvas, measures text, builds CanvasTexture (SRGB); asserts sampler budget
  • nextPowerOfTwo (118) — canvas-width rounding helper

Visual-foundation findings

  • Keyed by string, not entity ID (LabelTextureCache.ts:34, 46–54): the map key is text. Two different entities with the same location name share one THREE.CanvasTexture. This is architecturally correct for GPU memory but means the renderedLabelTexts array used by scenario gates (Mapv10ThreeRenderer.ts:1831–1836) will show a repeated text if two sprites with the same text are active simultaneously — which the seenTexts guard in selectLabelCandidates prevents at the selection layer. The cache is not the deduplication site; the label-selection layer is.

  • No zoom-band gating in the cache itself: LabelTextureCache has no concept of zoom band or LOD. Visible-set culling happens entirely upstream in selectLabelCandidates. A texture acquired for a label that later becomes invisible at the wrong zoom level is retained in the cache until the VectorSceneNode is disposed and release() is called.

  • Sampler-probe assertion (LabelTextureCache.ts:94–97): assertTextureFitsBudget throws if the GPU cannot host the texture. This is loud failure, consistent with the mapv10.md governance.


Section 7 — Scenarios JSON (25 scenarios) and scenario-gates.mjs

Scenario inventory

IDCamera altitude / modeKey gates
mapv10_continent_political_topdown5000 km, politicalprimaryGeometricSsePx ≤ 1.5, primaryRasterSsePx ≤ 6
mapv10_continent_political_oblique1500 km, politicalprimaryRasterSsePx ≤ 10
mapv10_continent_geography_topdown5000 km, geographyprimaryGeometricSsePx ≤ 1.5, primaryRasterSsePx ≤ 6
mapv10_continent_geography_oblique1200 km, geographyprimaryRasterSsePx ≤ 10
mapv10_continent_height_topdown5000 km, heightprimaryGeometricSsePx ≤ 1.5, primaryRasterSsePx ≤ 6
mapv10_continent_routes_topdown5000 km, routesprimaryGeometricSsePx ≤ 1.5, primaryRasterSsePx ≤ 6
mapv10_continent_slope_topdown5000 km, slopeprimaryGeometricSsePx ≤ 1.5, primaryRasterSsePx ≤ 6
mapv10_continent_normals_topdown5000 km, normalprimaryGeometricSsePx ≤ 1.5, primaryRasterSsePx ≤ 6
mapv10_realm_political_topdown1200 km, politicalforbidZ0UnderlayAtPrimaryZ, ancestorZDistanceMax ≤ 1, primaryRasterSsePx ≤ 11
mapv10_province_political_oblique400 km, politicalforbidZ0UnderlayAtPrimaryZ, ancestorZDistanceMax ≤ 1, primaryRasterSsePx ≤ 11
mapv10_location_political_topdown200 km, politicalforbidZ0UnderlayAtPrimaryZ, ancestorZDistanceMax ≤ 1, primaryRasterSsePx ≤ 7
mapv10_continent_geography_oblique_low70 km clearance, geographyforbidZ0Underlay, ancestorZDistance ≤ 1, primaryRasterSsePx ≤ 20
mapv10_location_geography_overlays_oblique_z4100 km, geography+overlayssidecarReady, labelTextUniqueness ≥ 0.95, labelMaxDuplicates ≤ 1, primaryRasterSsePx ≤ 25
mapv10_location_geography_overlays_oblique_z555 km, geography+overlayssidecarReady, labelUniqueness, primaryRasterSsePx ≤ 30
mapv10_location_geography_clean_oblique_z555 km, geographysidecarReady, primaryRasterSsePx ≤ 30
mapv10_location_geography_clean_lowland_oblique_z555 km, geographysidecarReady, primaryRasterSsePx ≤ 30
mapv10_zoom_continent_to_location_clean_lowlandzoom trace, geographytilesSettled, sidecarReady (no raster SSE gate on zoom trace itself)
mapv10_continent_to_location_slow_networkzoom trace + 75ms delay, geographytilesSettled, sidecarReady, omittedSlotCountMax=0 (in performanceBudget.fail)
mapv10_zoom_realm_to_location_overlays_z5zoom trace, geography+overlayssidecarReady, labelUniqueness
mapv10_location_routes_oblique_z555 km, routes+overlayssidecarReady, labelUniqueness, primaryRasterSsePx ≤ 30
mapv10_scale_ruler_continent600 km, geographytilesSettled only (no SSE gate)
mapv10_scale_ruler_realm80 km, geographyforbidZ0Underlay, ancestorZDistance ≤ 1, primaryRasterSsePx ≤ 20
mapv10_scale_ruler_location60 km, geographysidecarReady, primaryRasterSsePx ≤ 30
mapv10_lod_transition_z4_z5_seam85 km, geographyforbidZ0UnderlayAtPrimaryZ, ancestorZDistance ≤ 1, primaryRasterSsePx ≤ 25
mapv10_location_geography_wireframe_z555 km, wireframeforbidZ0Underlay, ancestorZDistance ≤ 1, sidecarReady, primaryRasterSsePx ≤ 30
mapv10_location_height_z555 km, heightsidecarReady, primaryRasterSsePx ≤ 30
mapv10_location_label_uniqueness_z555 km, geography+labelslabelUniqueness ≥ 0.95, labelMaxDuplicates ≤ 1, labelTextMinDistinctCount ≥ 12, primaryRasterSsePx ≤ 30

Gate types present in scenario-gates.mjs

tilesSettled, noPageCrash, noNewReadyErrors, primaryGeometricSsePx, primaryRasterSsePx, underlayGeometricSsePx, underlayRasterSsePx, underlayTileCount, primaryTileCountMin, forbidZ0UnderlayAtPrimaryZ, ancestorZDistanceMax, sidecarReadyForActiveMeshes, labelTextUniqueness, labelTextMinDistinctCount, labelMaxDuplicatesForText, coverageHoleMax, noPlaceholderTextureBound

Q6 — Scenario gate status

The scenario JSON and gate script do not contain a baseline run reference or pass/fail history. There is no "last run at commit X" record. Two scenarios are self-annotated as expected-FAIL on master:

  • mapv10_lod_transition_z4_z5_seamforbidZ0UnderlayAtPrimaryZ fires because resolver still uses z0 underlay
  • mapv10_location_label_uniqueness_z5labelTextUniqueness fails because generator's 120-name pool re-issues text

The close-zoom scenarios (z5 at 55 km) all carry primaryRasterSsePx ≤ 30 which is documented in scenario notes as exceeding the 6 px architectural target "by design pending Wave 3b". The gate bound of 30 is a regression floor, not a pass against the target.

Coverage gaps

  • Slab / underlay-rectangle: No gate directly detects a visible rectangular ancestor tile behind a primary frontier. forbidZ0UnderlayAtPrimaryZ catches only z=0 catch-all collapse. A z4 underlay behind z5 primary (which is the correct fallback) is not gated; whether that z4 tile's rectangle is visually distracting is untested.
  • Water/coastline Z-fighting: No scenario validates that the sea mesh and terrain-sea-fade produce a visually clean coastline. No gate checks sea-mesh presence per active tile.
  • Coastline continuity: No scenario checks that sea-tone areas are covered by the sea mesh (as opposed to just the shader's sea-tone branch).
  • Political layer zoom gating: All political scenarios are at continent/realm/province altitude. No scenario tests that the political layer remains off at continent zoom when layers.borders = false is set correctly.

Risks of fabricated source / invented syntax / silent fallback

  • uRoughnessMapPresent = 0 silent default (MaterialFade.ts:675–681): the first draw of a freshly-active terrain mesh renders with no Toksvig roughness for one frame. This is documented as an intentional race-frame accommodation, not a persistent fallback. However it silently alters the rendered surface quality for that single frame. The governance doc .claude/rules/mapv10.md:30 states "Missing required generated artifacts must fail loudly" — but this is a within-frame race, not a missing artifact.
  • splat texture not yet bound flat-grass fallback (MaterialFade.ts:455–463): shader comment explicitly calls this a "race-frame" where sumWeights < 1e-3. Same category as above — intentional but silent.
  • Preload runtime keys never populated (TileStreamingPlanner.ts:133): preloadRuntimeKeys is declared as string[] but left empty. Code on line 138 includes it in the combined set (uniqueSorted([...currentRuntimeKeys, ...preloadRuntimeKeys])), which resolves correctly to currentRuntimeKeys. Not a fabrication risk, but the dead variable is a maintenance hazard — future changes may incorrectly populate it and change semantics without realising the intended scope.
  • No .secs files are involved in the viewer; no risk of fabricated SECS source syntax here.

Open questions the implement step will need to answer

  1. Wave 3b LOD ladder / camera decision (docs/wave-3b-lod-ladder-camera-decision.md is referenced in 8+ scenario notes but was not in the audit scope): close-zoom scenarios gate primaryRasterSsePx at 20–30 px while the documented target is 6 px. The implementer needs this decision document before designing any LOD fix — is the fix in the tile pyramid (finer z6 level), in the camera envelope (raise minDistance), or in the raster-sample SSE formula?

  2. Coastline Z-fighting mitigation: water mesh at 0.12 km above sea level vs. terrain mesh at variable displacement — is there a depth offset or polygon offset applied to the water mesh? No evidence found in the audited files for explicit z-fighting protection beyond the vertical offset.

  3. Sea-mesh coverage vs. shader sea-tone: at what camera distance does the shader's sea-tone branch become the only visible water path (when sea meshes are culled or unloaded)? Is there a defined LOD hand-off?

  4. previousRenderedSlots retention duration: notifyRendered (RenderResolver.ts:308–332) replaces the entire map each frame. If a frame renders zero terrain slots (empty commit), next.size > 0 is false and the map is NOT updated — stale previous slots are retained. Is this the intended behavior (yes — it acts as a cache), or should a zero-slot frame clear the kicker set?

  5. Label sprite material sharing: ensureLabelNode (Mapv10ThreeRenderer.ts:4315) creates new THREE.Sprite(this.materials.label) — all label sprites share one SpriteMaterial. The onBeforeRender callback (4330–4335) writes material.map = entry.texture immediately before each draw. Because this.materials.label is a shared instance, the last sprite to execute onBeforeRender in the render loop determines the texture for any sprite whose draw call happens after the overwrite but before the GPU flush. Is this safe in Three.js's render order, or can sprites with different textures collide within a single renderPass call?

  6. TERRAIN_UNDERLAY_OFFSET_KM = -0.16: underlay tiles drawn below sea level may poke through sea-mesh geometry in areas where the sea floor is above this offset. Is there a minimum-depth constraint or is this value expected to always be below generated sea meshes?


Deferred work (no scheduling tools — text only)

  • Cross-check docs/wave-3b-lod-ladder-camera-decision.md (referenced in scenarios but not in audit scope) for architectural constraints on the SSE-gap fix.
  • Audit the generator-side tile pyramid metadata (sourceWindow.sampleStep per-tile values) to confirm actual z5 sample density against the 6 px target at 60–100 km — the viewer's raster-SSE formula is only as accurate as the metadata.
  • Verify that pickKickedFallback (RenderResolver.ts:419) interacts correctly with the notifyRendered empty-commit retention case — the unit tests in renderer/__tests__/ should cover this path but were not in the wave brief.
  • Confirm whether THREE.Sprite shared-material onBeforeRender map-swap is safe by inspecting Three.js source or the renderer's __tests__ coverage.
  • Check FBOPool.ts and SelectionMask.ts for any rectangular debug geometry or billboard-style placeholders that could produce visible slabs in debug mode.