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
| Path | LOC | Role |
|---|---|---|
examples/map/mapv10/viewer/src/renderer/lod/VisibilitySet.ts | 884 | Pure-function tile selector: camera snapshot, SSE computation, primary/underlay/preload sets |
examples/map/mapv10/viewer/src/renderer/lod/RenderResolver.ts | 590 | Stateful resolver: maps VisibilitySet to RenderCommit; ancestor-walk, kicker fallback, coverage-hole tracking |
examples/map/mapv10/viewer/src/renderer/lod/TileStreamingPlanner.ts | 402 | Imperative planner: turns VisibilitySet into cache fan-out + scheduler reconciliation; runtime-tile pin/unpin |
examples/map/mapv10/viewer/src/renderer/Mapv10ThreeRenderer.ts | 7541 | Main renderer class: mount/loadRun/tickFrame; terrain, water, route, label scene management; zoom-band logic |
examples/map/mapv10/viewer/src/renderer/MaterialFade.ts | 2505 | All shader material factory functions; unified terrain shader (11 samplers + mode dispatch); fade constants |
examples/map/mapv10/viewer/src/renderer/LabelTextureCache.ts | 120 | Memoized canvas-texture builder keyed by label text string |
examples/map/mapv10/viewer/src/scenarios/mapv10_scenarios.json | 988 | 25 regression scenarios with camera, mode, layer, and gate definitions |
examples/map/mapv10/viewer/scripts/scenario-gates.mjs | 488 | Gate 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 thresholdTERRAIN_RASTER_SAMPLE_ERROR_TARGET_PX(34) — 6.0 px raster sample SSE thresholdVIEW_FOOTPRINT_TILE_MARGIN(42) — 0.6 tile expansion marginFULL_COVERAGE_PRIMARY_TILE_LIMIT(50) — 64 tiles; levels at or below this count render whole-levelPRIMARY_LOD_VIEWPORT_FRACTION(53) — 0.45 fraction of viewport height for focal diskSCREEN_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.PerspectiveCameraselectVisibilitySet(328) — pure function; returns VisibilitySet including primary, underlay, preload, SSE map, signaturecomputeScreenSpaceError(503),computeTerrainRasterSampleScreenSpaceError(524),computeTileTerrainLodScreenSpaceError(543) — exported SSE utilities
Function inventory
buildCameraSnapshot(268) — builds immutable per-frame camera + view-projection snapshotselectVisibilitySet(328) — pure tile-selector; calls pickPrimaryZ, collectPrimary, collectUnderlay, optional preloadcomputeScreenSpaceError(503) — generic geometric SSE in pixels from error-metres + camera paramscomputeTerrainRasterSampleScreenSpaceError(524) — raster-sample SSE from tile.sourceWindow metadatacomputeTileTerrainLodScreenSpaceError(543) — combined {geometric, rasterSample, max} for one tilepickPrimaryZ(476) — internal; walks sortedLevels coarse-to-fine and returns first z meeting BOTH SSE targetscollectPrimary(570) — internal; gathers tiles at primaryZ within focal footprint boundscollectUnderlay(606) — internal; emits exactly primaryZ-1 parent set (Wave 2 contract)populatePriorityAndSse(658) — internal; fills priorityByKey and ssePixelsByKey for one slotfoveationFactor(725) — internal; projects tile centre to NDC, returns linear proximity to pointerdistanceToTileFootprintKm(749) — internal; 3-D distance from camera to closest point of tile rectanglebuildSignature(762) — internal; deterministic string from sorted primary+underlay keys + primaryZprojectControlsTargetToWorld(768) — internal; maps scene target to world-km, clamped to boundsprimaryFootprintBounds(789) — internal; computes km-space bounding rect for tile collectionfocalRadiusKm(819) — internal; half-projected-diagonal capped by world extentclampBounds,tileIntersectsBounds,pointInBounds,distance2D,clamp— small geometric utilitiestoActiveTileSlot(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 firstzwhereerrorPx.geometric <= 1.5ANDerrorPx.rasterSample <= 6.0. At ~60–100 km altitude the raster-SSE test dominates becausecomputeTerrainRasterSampleScreenSpaceError(524) usestile.sourceWindow.sampleStepto compute the sample footprint. For the continent-lod6 fixture theprimaryRasterSsePxgate 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 whencounts.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 capMAX_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'suTexInnerRegionUV remapping (inner-padding contract fromMaterialFade.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 onlyREQUIRED_SIDECARS(39–46) — tuple:materialWeights,materialWeightsB,biome,waterMask,borderSdf,normalRoughnessRenderResolverclass (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-holeRenderResolver.notifyRendered(308) — updatespreviousRenderedSlotsfrom the just-drawn commit; called by renderer after every frameRenderResolver.inspectPreviousRenderedKeys(334) — read-only diagnostic; used by testsRenderResolver.dispose(339) — clears state; throws on subsequentresolvecallsRenderResolver.incrementFallbackDuration(345) — private; accumulates per-primary fallback frame counterfindStructuralAncestorWithin(379) — walks tile quad-tree parent chain at mostmaxDistancesteps; returns first fully-resident ancestor fromunderlayByKeypickKickedFallback(419) — scanspreviousRenderedSlots; returns a bounds-containing previous tile within z-distance capisFullyResident(471) — discriminated union; AND of CPU raster payload + GPU texture readiness viaTextureResidencyViewsidecarsResident(493),sidecarPresent(506) — helpers for residency checkssummarizeFallbackSlots(532) — aggregates slot list intoRenderFallbackMetricsboundsContains(552) — EPSILON-tolerant containment check for kicker validationbuildCommitSignature(566) — terrain + runtime + coverage-holes → string signature
Visual-foundation findings
-
Slab mechanism:
pickKickedFallback(RenderResolver.ts:419–451) allows previous-rendered tiles frompreviousRenderedSlotsthat are at mostMAX_ANCESTOR_Z_DISTANCE = 1coarser than the primary. HoweverpreviousRenderedSlotsis keyed bymeshKey, NOT by primary tile key. ThenotifyRenderedpath (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. TherenderedMeshKeysde-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-holeslots (RenderResolver.ts:251–258) show the renderer's clear color0x14110d(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
TileStreamingPlannerclass (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/unpinTileStreamingPlanner.dispose(259) — unpins all pinned keys; marks disposeduniqueSorted(270),slotByTerrainKey(274),slotByTileKey(282) — key-index helpersbandedPriorityMap(290),deriveBandedPriorityMap(304),lookupRuntimeBoost(320) — priority-inheritance helpersselectRouteKeysForStreaming(332) — limits pending/missing route keys to capsexcludeOversizedRouteAggregates(364) — filters aggregate keys exceeding 1 MBdefaultCurrent(383),appendDesired(387) — scheduler-entry helpers
Visual-foundation findings
-
Preload: terrain + runtime only, no aux (TileStreamingPlanner.ts:130–138): predicted-preload tiles fill
preloadTerrainKeysandpreloadRuntimeKeysbutpreloadRuntimeKeysis never populated —preloadRuntimeKeysis 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
primaryRuntimeKeysonly (TileStreamingPlanner.ts:143–145): water mesh keys are derived fromprimaryRuntimeKeys(only tiles whose mesh is already loaded), not from the fullallCurrentSlotsset. A newly-arrived primary tile whose mesh just loaded this frame contributes its tile key toprimaryRuntimeKeysonly ifterrainTileCache.hasLoaded(slot.terrainMeshKey)returns true at thisapply()call. If the terrain cache loads and the runtime cache is still pending on the same frame,renderableCurrentSlotsis empty and no water request is issued.
Section 4 — Mapv10ThreeRenderer.ts (7541 lines)
Exports / public surface
Mapv10ThreeRendererclass (416) — implementsMapv10Renderer; all public API is on this class
Key function inventory (partial — major methods only)
mount(779) — attaches to canvas; creates WebGLRenderer, OrbitControls, sampler probe, lightingloadRun(849) — async; callsprepareStrategicMapResources,prepareTerrainStreaming,buildGeneratedLayerssetMapMode(952) — writesuModeIduniform; triggers LOD/visual config and color recomputesetLayerState(975) — controls terrain/water/borders/routes/labels/markers/wireframe visibilityframeSelection/frameWorld(987–1025) — heuristic camera framingsetCameraPose(1274) — deterministic pose setter for scenarioswaitForTilesSettled(1324) — async; pollscurrentViewResidency()getTerrainSelectionProbe(1570) — probe: primaryZ, role-split SSE telemetry, ancestor distancegetDebugState(1723) — large diagnostic aggregate: label texts, sidecar readiness, coverage holes, etc.zoomBandForDistance(6124) — classifies distance to target as continent / realm / province / locationselectLabelCandidates(4184) — label layout; zoom-band filter; text-dedup viaseenTextsprepareTerrainStreaming(implicit ~2580) — builds caches, planner, resolverapplyTerrainRenderCommit(2747) — paces terrain node creation byTERRAIN_NODE_CREATES_PER_FRAME
Numeric constants with visual-LOD relevance
| Constant | File:line | Value | Purpose |
|---|---|---|---|
TERRAIN_GEOMETRIC_ERROR_TARGET_PX | VisibilitySet.ts:20 | 1.5 | LOD selection geometric threshold |
TERRAIN_RASTER_SAMPLE_ERROR_TARGET_PX | VisibilitySet.ts:34 | 6.0 | LOD selection raster threshold |
FULL_COVERAGE_PRIMARY_TILE_LIMIT | VisibilitySet.ts:50 | 64 | Levels with ≤ 64 tiles render whole-level |
VIEW_FOOTPRINT_TILE_MARGIN | VisibilitySet.ts:42 | 0.6 | Tile footprint expansion (single margin replaces enter/retire pair) |
MAX_ANCESTOR_Z_DISTANCE | RenderResolver.ts:27 | 1 | Parent-walk depth cap |
LOD_FADE_MS | MaterialFade.ts:19 | 320 | Fade duration for non-terrain surfaces |
TERRAIN_UNDERLAY_OFFSET_KM | Mapv10ThreeRenderer.ts:185 | -0.16 | Depth offset for underlay terrain meshes |
CAMERA_COHERENCE_SELECTION_FRAMES | Mapv10ThreeRenderer.ts:201 | 2 | Frames to hold selection after camera write |
controls.minDistance | Mapv10ThreeRenderer.ts:827 | 35 (initial); then max(18, maxExtent * 0.025) | Interactive floor |
controls.maxDistance | Mapv10ThreeRenderer.ts:828 | 260 (initial); then max(260, maxExtent * 1.8) | Interactive ceiling |
| Continent zoom threshold | Mapv10ThreeRenderer.ts:6126 | max(130, extent * 0.95) | Camera-to-target distance where band is "continent" |
| Realm zoom threshold | Mapv10ThreeRenderer.ts:6127 | max(85, extent * 0.6) | Realm/province band boundary |
| Province zoom threshold | Mapv10ThreeRenderer.ts:6128 | max(50, extent * 0.32) | Province/location band boundary |
| Water mesh vertical offset | Mapv10ThreeRenderer.ts:2646 | 0.12 km | Water meshes sit 0.12 km above sea level |
| Route mesh vertical offset | Mapv10ThreeRenderer.ts:2656 | 0.28 km | Route 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 (waterMeshCache → MeshAssetCache 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:
pickKickedFallbackreturns 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.pickingDebuggroup 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 = 0collapses the roughness term toroughnessFactor = roughnessand the splat branch falls back tobaseColor = 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 fadesFADE_REMOVAL_EPSILON(22) — 0.001; eviction floor for fading actorsTERRAIN_MODE_*constants (221–226) — mode IDs 0–5 for the unified shadercomputeFadeProgress(35) — pure function; appear/dismiss linear ramp- All
createFadeable*Materialfactory functions andapply*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 appearinjectTileOpacityUniform(693) — patches vertex+fragment shader withuTileOpacityuniforminjectUnifiedTerrainVertexShader(713) — addsvTerrainSceneYvarying after<begin_vertex>injectUnifiedTerrainShader(735) — replaces<color_fragment>with 11-sampler mode dispatchinjectRoughnessMapUniform(756) — replaces<roughnessmap_fragment>with Toksvig α²-space combinecreateFromShaderLib(787) — master factory: ShaderLib-derived ShaderMaterial with per-mesh uTileOpacitycreateFadeableTerrainMaterial(implicit) — calls createFromShaderLib withunifiedTerrain: true, roughnessMap: truecreateFadeableOceanMaterial,createFadeableLakeMaterial,createFadeableRiverMaterial— separate water-surface factoriessetTerrainMode,setBorderSdfOpacity,setHoveredLocationId,setRouteVisualStyle— uniform settersapplyTileSplatMap,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.001ANDseaMask > 0) receiveterrainAlpha = mix(terrainAlpha, seaAlpha, seaMask)whereseaAlpha = 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 atverticalOffsetKm: 0.12and 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. -
uRoughnessMapPresentfallback (MaterialFade.ts:675–681): when no Toksvig sidecar is bound, the gateuRoughnessMapPresent = 0collapses toroughnessFactor = 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
LabelTextureCacheclass (34) —acquire(text),acquireWithStatus(text),release(text),size(),setSamplerProbe(probe),dispose()LabelTextureEntryinterface (23)
Function inventory
acquire(42) — thin wrapper overacquireWithStatusacquireWithStatus(46) — returns existing (refcount++) or callsbuildEntry; reportscreated: booleanrelease(57) — decrements refcount; disposes texture and removes entry when refcount reaches 0size(70) — returnsentries.sizedispose(75) — disposes all textures; clears mapbuildEntry(82) — creates canvas, measures text, builds CanvasTexture (SRGB); asserts sampler budgetnextPowerOfTwo(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 oneTHREE.CanvasTexture. This is architecturally correct for GPU memory but means therenderedLabelTextsarray used by scenario gates (Mapv10ThreeRenderer.ts:1831–1836) will show a repeated text if two sprites with the same text are active simultaneously — which theseenTextsguard inselectLabelCandidatesprevents 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 theVectorSceneNodeis disposed andrelease()is called. -
Sampler-probe assertion (LabelTextureCache.ts:94–97):
assertTextureFitsBudgetthrows 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
| ID | Camera altitude / mode | Key gates |
|---|---|---|
mapv10_continent_political_topdown | 5000 km, political | primaryGeometricSsePx ≤ 1.5, primaryRasterSsePx ≤ 6 |
mapv10_continent_political_oblique | 1500 km, political | primaryRasterSsePx ≤ 10 |
mapv10_continent_geography_topdown | 5000 km, geography | primaryGeometricSsePx ≤ 1.5, primaryRasterSsePx ≤ 6 |
mapv10_continent_geography_oblique | 1200 km, geography | primaryRasterSsePx ≤ 10 |
mapv10_continent_height_topdown | 5000 km, height | primaryGeometricSsePx ≤ 1.5, primaryRasterSsePx ≤ 6 |
mapv10_continent_routes_topdown | 5000 km, routes | primaryGeometricSsePx ≤ 1.5, primaryRasterSsePx ≤ 6 |
mapv10_continent_slope_topdown | 5000 km, slope | primaryGeometricSsePx ≤ 1.5, primaryRasterSsePx ≤ 6 |
mapv10_continent_normals_topdown | 5000 km, normal | primaryGeometricSsePx ≤ 1.5, primaryRasterSsePx ≤ 6 |
mapv10_realm_political_topdown | 1200 km, political | forbidZ0UnderlayAtPrimaryZ, ancestorZDistanceMax ≤ 1, primaryRasterSsePx ≤ 11 |
mapv10_province_political_oblique | 400 km, political | forbidZ0UnderlayAtPrimaryZ, ancestorZDistanceMax ≤ 1, primaryRasterSsePx ≤ 11 |
mapv10_location_political_topdown | 200 km, political | forbidZ0UnderlayAtPrimaryZ, ancestorZDistanceMax ≤ 1, primaryRasterSsePx ≤ 7 |
mapv10_continent_geography_oblique_low | 70 km clearance, geography | forbidZ0Underlay, ancestorZDistance ≤ 1, primaryRasterSsePx ≤ 20 |
mapv10_location_geography_overlays_oblique_z4 | 100 km, geography+overlays | sidecarReady, labelTextUniqueness ≥ 0.95, labelMaxDuplicates ≤ 1, primaryRasterSsePx ≤ 25 |
mapv10_location_geography_overlays_oblique_z5 | 55 km, geography+overlays | sidecarReady, labelUniqueness, primaryRasterSsePx ≤ 30 |
mapv10_location_geography_clean_oblique_z5 | 55 km, geography | sidecarReady, primaryRasterSsePx ≤ 30 |
mapv10_location_geography_clean_lowland_oblique_z5 | 55 km, geography | sidecarReady, primaryRasterSsePx ≤ 30 |
mapv10_zoom_continent_to_location_clean_lowland | zoom trace, geography | tilesSettled, sidecarReady (no raster SSE gate on zoom trace itself) |
mapv10_continent_to_location_slow_network | zoom trace + 75ms delay, geography | tilesSettled, sidecarReady, omittedSlotCountMax=0 (in performanceBudget.fail) |
mapv10_zoom_realm_to_location_overlays_z5 | zoom trace, geography+overlays | sidecarReady, labelUniqueness |
mapv10_location_routes_oblique_z5 | 55 km, routes+overlays | sidecarReady, labelUniqueness, primaryRasterSsePx ≤ 30 |
mapv10_scale_ruler_continent | 600 km, geography | tilesSettled only (no SSE gate) |
mapv10_scale_ruler_realm | 80 km, geography | forbidZ0Underlay, ancestorZDistance ≤ 1, primaryRasterSsePx ≤ 20 |
mapv10_scale_ruler_location | 60 km, geography | sidecarReady, primaryRasterSsePx ≤ 30 |
mapv10_lod_transition_z4_z5_seam | 85 km, geography | forbidZ0UnderlayAtPrimaryZ, ancestorZDistance ≤ 1, primaryRasterSsePx ≤ 25 |
mapv10_location_geography_wireframe_z5 | 55 km, wireframe | forbidZ0Underlay, ancestorZDistance ≤ 1, sidecarReady, primaryRasterSsePx ≤ 30 |
mapv10_location_height_z5 | 55 km, height | sidecarReady, primaryRasterSsePx ≤ 30 |
mapv10_location_label_uniqueness_z5 | 55 km, geography+labels | labelUniqueness ≥ 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_seam—forbidZ0UnderlayAtPrimaryZfires because resolver still uses z0 underlaymapv10_location_label_uniqueness_z5—labelTextUniquenessfails 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.
forbidZ0UnderlayAtPrimaryZcatches 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 = falseis set correctly.
Risks of fabricated source / invented syntax / silent fallback
uRoughnessMapPresent = 0silent 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:30states "Missing required generated artifacts must fail loudly" — but this is a within-frame race, not a missing artifact.splat texture not yet boundflat-grass fallback (MaterialFade.ts:455–463): shader comment explicitly calls this a "race-frame" wheresumWeights < 1e-3. Same category as above — intentional but silent.- Preload runtime keys never populated (TileStreamingPlanner.ts:133):
preloadRuntimeKeysis declared asstring[]but left empty. Code on line 138 includes it in the combined set (uniqueSorted([...currentRuntimeKeys, ...preloadRuntimeKeys])), which resolves correctly tocurrentRuntimeKeys. 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
.secsfiles are involved in the viewer; no risk of fabricated SECS source syntax here.
Open questions the implement step will need to answer
-
Wave 3b LOD ladder / camera decision (
docs/wave-3b-lod-ladder-camera-decision.mdis referenced in 8+ scenario notes but was not in the audit scope): close-zoom scenarios gateprimaryRasterSsePxat 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 (raiseminDistance), or in the raster-sample SSE formula? -
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.
-
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?
-
previousRenderedSlotsretention duration:notifyRendered(RenderResolver.ts:308–332) replaces the entire map each frame. If a frame renders zero terrain slots (empty commit),next.size > 0is 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? -
Label sprite material sharing:
ensureLabelNode(Mapv10ThreeRenderer.ts:4315) createsnew THREE.Sprite(this.materials.label)— all label sprites share oneSpriteMaterial. TheonBeforeRendercallback (4330–4335) writesmaterial.map = entry.textureimmediately before each draw. Becausethis.materials.labelis a shared instance, the last sprite to executeonBeforeRenderin 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? -
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.sampleStepper-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 thenotifyRenderedempty-commit retention case — the unit tests inrenderer/__tests__/should cover this path but were not in the wave brief. - Confirm whether
THREE.Spriteshared-materialonBeforeRendermap-swap is safe by inspecting Three.js source or the renderer's__tests__coverage. - Check
FBOPool.tsandSelectionMask.tsfor any rectangular debug geometry or billboard-style placeholders that could produce visible slabs in debug mode.