Skip to main content

mapv10 Viewer

The mapv10 viewer renders generated continent artifacts from a manifest URL into a 3D strategic map. It is a Vite + plain TypeScript + Three.js application; the renderer core is decoupled from any UI framework. For the broader workspace context, see architecture.md § Viewer And Renderer and § Renderer/UI Boundary. For the wave protocol that governs every code change here, see wave-protocol.md.

Stack and Build

  • Three.js ^0.181.2 (viewer/package.json).

  • Vite ^7.1.12 (viewer/package.json).

  • TypeScript ^5.9.3 (viewer/package.json); tsconfig.json targets ES2022.

  • Plain HTTP. The dev server listens on port 5443 and the preview server on port 4279 (viewer/vite.config.ts server and preview config). Both are host: "0.0.0.0" with strictPort: false so an occupied port falls through to the next free one rather than failing the boot.

  • @vitejs/plugin-basic-ssl is a dev dependency but the active config does not install it. HTTPS is not part of the viewer's current contract.

  • The Three.js bundle is pinned to its own chunk via manualChunks in viewer/vite.config.ts. Three.js is around 400 kB minified and changes rarely between iterations; isolating it lets the browser cache absorb its weight while only the small main bundle re-downloads on viewer-side edits.

  • Generated artifacts are served by an in-process middleware mounted on both the dev (configureServer) and preview (configurePreviewServer) Vite servers. The mount lives at viewer/server/mapv10ArtifactMiddleware.ts:

    • MAPV10_ARTIFACT_PREFIX = "/mapv10/runs"
    • DEFAULT_ARTIFACT_RUN_ID = "continent-lod6"
    • DEFAULT_ARTIFACT_MANIFEST_URL = "/mapv10/runs/continent-lod6/manifest.json"

    The middleware serves typed JSON and binary assets out of viewer/public/ rather than letting Vite fall through to the SPA index. Per-request byte ranges and content types are enforced so a corrupt artifact surfaces as a load error instead of a silent zero-byte read.

  • Generated artifact run directories under viewer/public/ are excluded from Vite's watcher in viewer/vite.config.ts. The middleware still serves them normally, but Vite must not treat the full continent fixture as source input: z0-z7 tile artifacts create hundreds of thousands of files and can exhaust Node's heap during browser verifier startup if watched.

npm scripts

The full set of scripts in viewer/package.json:

ScriptRole
devRun the dev server on port 5443 with the artifact middleware.
buildtsc --noEmit then vite build; produces a static bundle in dist/.
typechecktsc --noEmit only; no bundle output.
testRun the Vitest suite once.
test:watchRun Vitest in watch mode.
scenariosRun scripted-camera scenarios via scripts/run-scenarios.mjs.
fixture:continentBootstrap the continent fixture into public/continent-lod6.
fixture:continent:validateValidate-only run of the fixture bootstrap (no regeneration).
validate:valenar-exportValidate a Valenar export against the run-root structure.
verify:browserRun the Playwright verifier against the dev URL.
verify:browser:continentRun the m10-lod6-continuity-proof verifier scenario.
verify:browser:baseline:updateCapture a fresh visual baseline.
verify:browser:baselineCompare against the captured baseline.
verify:browser:allChain verify:browser then verify:browser:continent.

Renderer Architecture

Everything visible originates in a single class, Mapv10ThreeRenderer, defined in viewer/src/renderer/Mapv10ThreeRenderer.ts. The class owns the THREE.WebGLRenderer, the scene-graph, all caches, the request scheduler, the worker pool, the camera, the controls, the keyboard/pointer state, and the debug-state surface. Lifecycle:

mount → loadRun → tickFrame (rAF) → dispose.

mount(canvas: HTMLCanvasElement)

Sequence at the referenced source location:

  1. Create the THREE.WebGLRenderer against the supplied canvas.
  2. Probe the GL sampler budget (SamplerProbe.ts) and assert that the unified terrain shader's binding count fits.
  3. Set the scene background colour and the THREE.Fog near/far envelope at Mapv10ThreeRenderer.ts.
  4. Set the camera default pose (position(0, 96, 120), target(0, 0, 0)).
  5. Name the seven scene-graph groups and add them to the scene (Mapv10ThreeRenderer.ts).
  6. Construct the OrbitControls against the canvas with the configuration detailed in Camera and Controls.
  7. Install keyboard navigation (installKeyboardNavigation), pointer tracking (installPointerTracking), and the PerformanceObserver long-task hook.
  8. Sync the terrain visual config to the shared shader uniforms.
  9. Install the directional + ambient lights (installLights).
  10. Mark the first frame dirty and schedule the rAF.

loadRun(manifestUrl: string)

Defined at Mapv10ThreeRenderer.ts. The phases match the load-progress events emitted to UI handlers (Mapv10ThreeRenderer.ts):

  1. manifest: download and parse the run manifest via loadMapv10Run. This fetches the run envelope, product manifests, the generated tile-coordinate-index.json, and the small z-row shard indexes for raster/vector/semantic/mesh families. It does not fetch the family shard payloads or tile binaries until streaming requests them.
  2. renderer 1/4: prepareStrategicMapResources — semantic maps, color LUTs, continent-wide ID raster.
  3. renderer 2/4: prepareTerrainStreaming — tile caches, the TileRequestScheduler, the DecoderWorkerPool, and the planner/resolver.
  4. renderer 3/4: frameRun — camera framed to the run's world bounds.
  5. renderer 4/4: buildGeneratedLayers — routes, labels, water, markers.
  6. initial-residency: awaitInitialResidency blocks the promise until the first VisibilitySet for the framed camera reports auxiliaryReady === true.
  7. ready: run-loaded event emitted with the run id.

A throw at any phase emits tile-load-state failed=1 and an error event, then re-throws so the caller (the shell) can surface a fatal status.

tickFrame(now: number)

Defined at Mapv10ThreeRenderer.ts. One frame's body, in order:

  1. Update controls (OrbitControls damping). Update keyboard navigation integration via applyKeyboardNavigation.
  2. Update DESP smoothing for the predictive-preload snapshot.
  3. If the frame is dirty (camera, layer state, mode, run, or input changed), compute a fresh VisibilitySet from the current CameraSnapshot.
  4. Run the TileStreamingPlanner.apply pass. The planner reconciles the scheduler against the desired set, fans out cache requests, and returns the PlannedSelection (terrain keys, runtime keys, auxiliary keys).
  5. Drive the RenderResolver to produce a RenderCommit (the final per-frame list of terrain mesh slots, including ancestor and previous-frame fallbacks).
  6. Run the spatial-LRU cache maintain pass against the live view centre.
  7. Update label visibility (zoom band filter, collision avoidance, fade ramp).
  8. Step every active fade animation (terrain tiles, auxiliary batches, labels).
  9. Drain the per-frame FrameBudget (texture creates, geometry merges, mesh disposes) within FRAME_BUDGET_TOTAL_MS = 4.
  10. Render via WebGLRenderer.render(scene, camera).
  11. Emit a camera-changed event if the camera state diverged from the last emit (debounced).
  12. Sample the frame time into the rolling p50/p95/p99 buffer.
  13. Decide whether to re-queue another rAF (see below).

Scene-graph passes

Seven THREE.Groups named and added by Mapv10ThreeRenderer.mount:

Group namePurpose
mapv10-terrain-passPer-tile terrain meshes (the unified terrain ShaderMaterial).
mapv10-water-passPer-water-body batched lake / ocean / river meshes.
mapv10-route-passRoute ribbons and crossings.
mapv10-overlay-passPer-tile overlay meshes (state-field overlay material).
mapv10-marker-passSample-point markers.
mapv10-label-passPer-label sprites driven by LabelTextureCache.
mapv10-picking-debug-passOptional picking-overlay diagnostics; off by default.

The pass groups are added in this order so the GPU draw order matches the required compositing: terrain underneath, water on top of terrain, routes on top of water, overlay on top of routes, markers and labels above overlays, debug last.

On-demand frame loop

requestAnimationFrame is not driven on a free-running clock. Three primitives control re-queueing (Mapv10ThreeRenderer.ts):

  • markFrameDirty() sets frameDirty = true, flags the label-layout dirty bit, and calls requestNextFrame().

  • requestNextFrame() is idempotent. If a frame is already pending it returns immediately. Otherwise it sets framePending = true and schedules the rAF body, which clears the bit before invoking tickFrame.

  • scheduleNextFrameIfNeeded() runs at the end of every tickFrame. It re-queues another frame if any of these conditions hold (Mapv10ThreeRenderer.ts):

    1. frameDirty is set.
    2. controlsDampingActive() is true (OrbitControls is bleeding off momentum).
    3. At least one keyboard navigation key is held.
    4. The request scheduler has in-flight or pending tile requests.
    5. The frame budget has pending work units.
    6. A pending terrain render commit is queued, or terrain texture/disposal work is deferred, or auxiliary disposal work is deferred.
    7. Any actor is mid-fade (fadesActive() checks terrain tiles, auxiliary batches, and labels).
    8. The initial-residency awaiter is still unresolved.

When none of these hold the loop parks. CPU and GPU usage drop to zero until external input, a cache callback, or a resize fires markFrameDirty() again.

Materials and Shading

Every fadeable terrain surface uses a single THREE.ShaderMaterial returned by createFadeableTerrainMaterial (viewer/src/renderer/MaterialFade.ts). One GL program serves every map mode; mode dispatch happens in the fragment shader through an int uModeId uniform. Switching map modes is a single uniform write — not a program recompile, not a material swap.

Sampler bindings

The unified terrain shader binds twelve samplers, every one declared in MaterialFade.ts:

#UniformFormatFilterPurpose
1uMaterialWeightsARGBA8linearPer-tile splat-map weights, primary band: grass, rock, forest, snow-or-wetland (MaterialFade.ts).
2uMaterialWeightsBRGBA8linearPer-tile splat-map weights, secondary band: sand/coast, bare earth, ice/glacier, riverbank mud (MaterialFade.ts).
3uBiomeMapR8nearestPer-tile biome enum used to switch the snow-vs-wetland palette (MaterialFade.ts).
4uWaterMaskR8linearPer-tile dry/sea/lake/river mask (MaterialFade.ts).
5uRoughnessMapR8linearPer-tile Toksvig sidecar; replaces the Three.js <roughnessmap_fragment> chunk (MaterialFade.ts).
6uGlobalIdMapRGBA8nearestContinent-wide RGB-packed locationId raster sampled by world-space UV (MaterialFade.ts).
7uColorMapLutRGBA8nearestdim×dim per-locationId LUT authored from provinceColorSeeds.json (MaterialFade.ts).
8uBorderSdfRGBA8linearPer-tile JFA SDF + nearest-id; the fragment shader smoothsteps the distance channel for crisp borders (MaterialFade.ts).
9uStateFieldRGBA8linearWorld-space overlay RT. The producer transport is a future wave; the binding ensures the texture() read has a valid GL texture even before the producer exists (MaterialFade.ts).
10uSelectionMaskR8nearestPer-locationId selection state (MaterialFade.ts).
11uTerrainMicroReliefNormalMapRGBA8linearPer-tile generated closeDetailNormal product sampled in tile UV space and faded by semantic band (MaterialFade.ts).
12uInfluenceMaskCorruptionR8linearPer-tile generated corruption intensity sliced from influenceMask.corruption, driving the organic world-space influence blend (MaterialFade.ts).

Mode dispatch

The seven terrain-mode constants are exported from MaterialFade.ts:

ModeConstantValue
GeographyTERRAIN_MODE_GEOGRAPHY0
HeightTERRAIN_MODE_HEIGHT1
SlopeTERRAIN_MODE_SLOPE2
NormalTERRAIN_MODE_NORMAL3
PoliticalTERRAIN_MODE_POLITICAL4
RoutesTERRAIN_MODE_ROUTES5
InfluenceTERRAIN_MODE_INFLUENCE6

The fragment program switches on uModeId to select the colour-derivation path. Selection / hover composition (uSelectionMask, uHoveredId, uBorderSdf, uStateField) runs in every mode. Influence mode keeps the effective material blend visible and emphasizes generated corruption territory from uInfluenceMaskCorruption.

Per-tile fade

LOD_FADE_MS = 320 (MaterialFade.ts). A new tile entering the active commit ramps uTileOpacity from 0 to 1 over 320 ms; a tile leaving ramps back to 0. The ramp value is computed by computeFadeProgress (MaterialFade.ts), a pure function with no side effects.

Per-mesh uniform delivery is the architecturally interesting part. A shared THREE.ShaderMaterial only re-uploads its uniforms once per material activation in WebGLRenderer.setProgram. Writing uTileOpacity from mesh.onBeforeRender is therefore subject to last-write-wins across meshes sharing the material. The viewer works around this by setting material.uniformsNeedUpdate = true on every per-mesh write so the renderer's per-mesh refresh path takes effect (Mapv10ThreeRenderer.ts documents the shape; the onBeforeRender hook in applyFadeOpacity performs the write).

Terrain meshes themselves do not alpha-fade. Cross-fading two terrain LOD rasters blends different splat / water / SDF masks and produces a temporal shoreline shimmer that is much worse than a hard pop. Coverage is controlled by the resolver's previous-frame fallback ("kicking") plus opaque child commits. The 320 ms fade window is reused unchanged for auxiliary batches and labels, where the visual gain outweighs the fade artefact.

Other material factories

All defined in MaterialFade.ts:

FactorySymbolPurpose
createFadeableTerrainMaterialfunction exportUnified terrain shader.
createFadeableOverlayMaterialfunction exportPer-tile overlay layer.
createFadeableLakeMaterialfunction exportLake water surface.
createFadeableOceanMaterialfunction exportOcean water surface.
createFadeableRiverMaterialfunction exportRiver ribbon material.
createFadeableBorderMaterialfunction exportBorder-line material (used only by debug overlays now that borders render in the terrain shader).
createFadeableRouteMaterialfunction exportRoute ribbon material.
createFadeableCrossingMaterialfunction exportCrossing-marker material.
createFadeableMarkerMaterialfunction exportSample-point marker material.
createFadeableFootprintMaterialfunction exportSelection-footprint material.

Lighting

installLights() at Mapv10ThreeRenderer.ts adds three lights to the scene:

  • One THREE.AmbientLight(0xf0e8d7, 1.0) — warm hemispheric fill.
  • One THREE.DirectionalLight(0xfff2d0, 1.0) — key sun, cool-warm direction driven by terrainSunDirection from TerrainVisualConfig.
  • One THREE.DirectionalLight(0x9cc7b8, 1.0) — fill light from the opposite hemisphere.

There is no IBL and no shadow-map pass.

Atmosphere

Two independent layers:

  • Scene-level THREE.Fog(aerialPerspectiveColorHex, 190, 470) set in Mapv10ThreeRenderer.ts. Three.js uses the linear fog ramp through the standard <fog_fragment> chunk for materials that opt in.
  • A per-fragment exponential haze in the unified terrain shader. The two layers composite independently so the strategic-map look does not depend on Three.js fog being lit consistently across all materials.

Micro-relief

uTerrainMicroReliefNormalMap is built in MaterialFade.ts at the referenced source location by createMicroReliefNormalTexture(). It is sampled in world-UV space (so the detail does not "swim" with mesh seams) and distance-faded — at continent zoom the relief contribution is forced to identity so the close-up texture does not bleed into a coarse strategic view.

Toksvig roughness composition

The per-tile R8 sidecar uRoughnessMap carries a Toksvig-method roughness authored offline. The shader composes it in α²-space against the per-class authored roughness values, then routes the result through the standard roughness chunk. This replaces the Three.js <roughnessmap_fragment> chunk verbatim so the lit look matches MeshStandardMaterial.

Physical LOD Selection

Physical LOD controls generated product residency only: terrain mesh tiles, raster sidecars, semantic ID tiles, vector tiles, auxiliary mesh sidecars, preloads, cache priority, and resolver fallback. It is not the semantic zoom policy for labels, routes, borders, markers, water visibility, hover, selection, or influence overlays.

Terrain LOD selection is the Cesium 3D Tiles screen-space-error formula:

sse_pixels = (geometricErrorKm * viewportHeight) / (2 * tan(fov/2) * distanceToCameraKm)

The selector walks the LOD pyramid coarse-to-fine and picks the first level whose measured tile geometric error projects below the target.

Dual screen-space-error targets

mapv10's "terrain" is a coupled product (mesh displacement plus per-tile water masks, splat weights, roughness, close-detail normals, and border SDFs). Geometry SSE alone is not enough — the coarse continent root has acceptable geometric error at realm scale, but its 18.75 km raster sample projects to many pixels and crawls when the camera moves. Two thresholds therefore drive selection (viewer/src/renderer/lod/VisibilitySet.ts terrain SSE constants):

  • TERRAIN_GEOMETRIC_ERROR_TARGET_PX = 1.5 — geometry SSE target.
  • TERRAIN_RASTER_SAMPLE_ERROR_TARGET_PX = 6.0 — generated raster/detail sample footprint target. z6/z7 report their generated rasterSampleSpacingKm from closeDetailScale = 8, so close views are judged against the detail product rather than merely smaller tile windows.

Semantic Display Policy

The generated semanticDisplayPolicy product maps camera distance and map mode to the first-class semantic bands continent, realm, province, location, and close. Mapv10ThreeRenderer consumes this policy through SemanticDisplayPolicy.ts before applying layer visibility, label eligibility, max label counts, water/route planner fan-out, and influence requirements. lodBand remains useful debug metadata for product residency, but it must not be the only source of display visibility.

The policy also carries influence requirements. corruption rendering requires the generated influenceMask.corruption product before the viewer may render, and the manifest loader validates that requirement against influenceTypes.json, influenceSources.json, influenceRules.json, and the derived effective products. Missing required influence truth is a load-time failure.

Full-coverage limit

FULL_COVERAGE_PRIMARY_TILE_LIMIT = 64 (VisibilitySet.ts). When the selected level contains 64 or fewer tiles, render the entire level rather than a target-centred subset. Coarse strategic LODs are cheap enough to keep whole, and doing so prevents camera pans at constant distance from swapping a visible z2 footprint against the z0 context surface.

Focal-footprint pruning

Inside the focal disk:

  • PRIMARY_LOD_VIEWPORT_FRACTION = 0.45 — fraction of viewport height treated as the focal disk (VisibilitySet.ts).
  • VIEW_FOOTPRINT_TILE_MARGIN = 0.6 — margin used to expand the focal-pixel rectangle when collecting primary tiles (VisibilitySet.ts). One value replaces the prior enter/retire pair; the RenderResolver's previous-frame set absorbs the visual transition without a second threshold.

Within-band priority

Tiles inside the same band (Urgent / Current / Preload) are ordered by a continuous priority scalar (VisibilitySet.ts):

priority = SCREEN_AREA_WEIGHT * screenAreaPx
- DEPTH_PENALTY_PER_LEVEL * (primaryZ - tile.z)
+ FOREGROUND_BIAS * foreground
+ FOVEATION_BIAS * foveation

With:

  • SCREEN_AREA_WEIGHT = 1.0
  • DEPTH_PENALTY_PER_LEVEL = 250
  • FOREGROUND_BIAS = 200
  • FOVEATION_BIAS = 100

Foveation

The pointer-weighted bias takes the cursor's NDC position from the last mousemove (or screen centre when the pointer has not moved or has left the canvas). Falloff is linear with radius FOVEATION_RADIUS_NDC = 0.4 (VisibilitySet.ts). Tiles outside the radius get zero bonus. The coefficient (100) is intentionally smaller than FOREGROUND_BIAS (200) so foveation acts as a tie-breaker between similarly-framed tiles, not as a primary priority axis.

Three-stage flow

A frame's selection runs through three modules:

  1. VisibilitySet (viewer/src/renderer/lod/VisibilitySet.ts) — pure function over (camera, run, metrics, predictedSnapshot?) returns the desired primary[], underlay[], preload[] slot lists, the per-key band-tagged priority map, and a canonical signature string for cheap equality.
  2. TileStreamingPlanner.apply (viewer/src/renderer/lod/TileStreamingPlanner.ts) — turns the desired set into scheduler reconcile entries plus cache request fan-out across TerrainTileCache, RuntimeTileCache, the water mesh cache, and the route mesh cache.
  3. RenderResolver.resolve (viewer/src/renderer/lod/RenderResolver.ts) — produces the final RenderCommit: which tile slots draw this frame, with what fallback metadata. Holds the only legitimate cross-frame state in the new selection model.

Ancestor-fallback "kicking"

When a primary tile's mesh has not yet streamed in, the resolver "kicks" — it keeps the previous-frame rendered tile visible until the replacement lands. The previous-frame slot is held in RenderResolver.previousRenderedSlots (RenderResolver.ts). Without this, deeper-LOD refinements would produce a visible hole during the streaming window.

Ancestor selection is a structural-parent walk, not bounds containment. The resolver climbs the tile pyramid one parent at a time using the recorded parentId chain in the run's tile pyramid, and at each step asks isFullyResident (RenderResolver.ts) whether the candidate has both CPU and GPU residency (typed-array data decoded into the tile cache AND the GPU texture/mesh upload completed). A tile that contains the primary in 2D bounds but is not resident is never promoted as a fallback. The walk stops at MAX_ANCESTOR_Z_DISTANCE = 1 (RenderResolver.ts): the resolver refuses to promote any ancestor more than one z step coarser than the primary. When no qualifying ancestor exists within that cap, the resolver falls back to the previous-rendered slot if one is fully resident; otherwise the slot is recorded as omitted (omittedSlotCountMax gate, scenarioTypes.ts) and the primary is left empty for that frame rather than substituting fabricated or stale data.

For the accepted and now implemented z6/z7 physical LOD ladder tied to raster SSE target coverage, see wave-3b-lod-ladder-camera-decision.md.

DESP predictive preload

PRELOAD_LOOKAHEAD_MS = 250 and DESP_ALPHA = 0.5 (TilePriority.ts DESP preload constants). The renderer applies double-exponential smoothing to the camera position over recent frames, projects the smoothed velocity 250 ms forward, and constructs a predicted CameraSnapshot. The visibility set diff (predicted minus current) becomes the Preload band. Three safeguards suppress speculation when it would burn bandwidth without a payoff:

  • A velocity floor (PRELOAD_VELOCITY_FLOOR_TILES_PER_LOOKAHEAD = 0.5, TilePriority.ts) — a settling camera generates no preloads.
  • A predicted/current overlap threshold (PREDICTED_OVERLAP_SKIP_RATIO = 0.7, TilePriority.ts) — if 70 % of the predicted set is already in the current set, the preload band is empty for this frame.
  • A direction-change commitment window (PRELOAD_COMMITMENT_WINDOW_MS = 100, TilePriority.ts) — preload requests in flight for less than 100 ms are not cancelled even when they leave the predicted set.

Streaming and Scheduler

The TileRequestScheduler is constructed at Mapv10ThreeRenderer.ts with MAX_IN_FLIGHT_TILE_REQUESTS = 24 (Mapv10ThreeRenderer.ts). Per-host concurrency is capped at DEFAULT_MAX_PER_HOST = 8 (viewer/src/renderer/TileCache.ts). Within each host, the preload band reserves a fraction of the slots: floor(PRELOAD_HOST_CONCURRENCY_FRACTION * 8) = floor(0.3 * 8) = 2 (TileCache.ts).

Heap ordering

The scheduler heap is a binary max-heap over the lex tuple (band, withinBandPriority, enqueuedAtFrame). compareBandedPriority (TilePriority.ts) is the single-line comparator: smaller band wins, then larger within-band priority wins. Frame number breaks priority ties so older requests do not starve.

Bands

TileBand is a categorical enum (TilePriority.ts):

  • Urgent (0) — a coarse ancestor is missing while a descendant renders in its place.
  • Current (1) — in the current-frame visibility set.
  • Preload (2) — in the predicted-frame set only.

Bands are categorical, not multiplicative weights. A high-priority Preload tile never preempts a low-priority Current tile.

Reconcile flow

Each TileStreamingPlanner.apply call hands the scheduler a fresh Iterable<ReconcileEntry>. The scheduler:

  1. Cancels in-flight requests that no longer appear in the desired set, with exceptions: a Preload request inside PRELOAD_COMMITMENT_WINDOW_MS = 100 is held; a request that has been absent from BOTH the current and predicted sets for PRELOAD_STALE_FRAMES = 3 frames is aborted.
  2. Refreshes priorities on requests that survived reconcile.
  3. Enqueues new desired keys onto the heap.
  4. Pumps the heap up to maxInFlight - inFlight times subject to per-host limits.

Preload pin and stale eviction

After a Preload-band request completes, the cache pins it as non-evictable for PRELOAD_CACHE_PIN_MS = 250 ms (TilePriority.ts). This stops the "preload, then evict before use" loop when the camera arrives at the predicted location.

Coalescing

Re-enqueue of a key already in the heap updates its priority in place. The heap re-sifts the entry without producing a duplicate request.

No retry

A transient HTTP error permanently fails the tile in this version. See Known Limitations.

Cache Architecture

Three caches carry the streaming payload, each owned by Mapv10ThreeRenderer:

CacheFamilyWhat it stores
TerrainTileCache (viewer/src/renderer/TileCache.ts)terrainPer-tile mesh assets and per-tile sidecars.
RuntimeTileCache (viewer/src/renderer/RuntimeTileCache.ts)raster + semanticPer-tile decoded raster sidecars (base/effective splat, biome, water, influence mask, roughness, close-detail normal, ID, border SDF) and semantic data.
MeshAssetCache (viewer/src/renderer/TileCache.ts)vectorAuxiliary meshes (water, route ribbons) keyed independently of terrain tiles.

A single CacheCoordinator (viewer/src/renderer/RuntimeTileCache.ts, Mapv10ThreeRenderer.ts) wires them together. Each cache reports its byte usage and family classification into the spatial-LRU evictor.

RuntimeTileCache consumes sharded tile family manifests. It owns ShardedTileManifestStore instances for raster, vector, and semantic families; each store resolves a requested tile key to the generated z/y row shard, fetches and byte-checks that shard once, attaches the tile coordinate from tile-coordinate-index.json, then lets the worker pool fetch the actual raster/vector payloads. This keeps boot readiness bounded to manifest indexes and visible tile shards rather than the full continent raster manifest.

Family floors

Family floors are SOFT — they bias the eviction order rather than excluding entries from the candidate pool. From viewer/src/config/CacheConfig.ts:

FamilyDefault floor (MiB)
terrain64
raster32
vector8
semantic8

When a family is below its floor, its entries get cacheFloorProtectionBias = 200 subtracted from their eviction score. They remain ELIGIBLE so the global cacheCeilingBytes invariant always holds even when every family sits below its floor.

Eviction score

Every non-pinned, non-Urgent entry is scored by a single scalar (viewer/src/renderer/SpatialLruEviction.ts):

score = recencyAge + bandPenalty * 1000 + spatialDistance * 10
+ (familyBelowFloor ? -floorProtectionBias : 0)

Higher score evicts earlier. bandPenalty is 0 for Current and 50 for Preload, so the band-scaled term contributes either 0 or 50_000. Recency is in frames; spatial distance is in tile-grid units to the live view centre. Pinned entries and Urgent-band entries return Number.NEGATIVE_INFINITY, guaranteeing the candidate pool never picks them.

maxEvictPerFrame

maxEvictPerFrame = 16 (viewer/src/config/CacheConfig.ts). The maintain pass evicts at most 16 entries per call, so a large pressure event drains across multiple frames instead of stalling the WebGL driver with a hundred geometry/texture disposes in one go.

Pin lifetime

A cache entry can be pinned in two ways:

  • Refcount: pin(key) / unpin(key) increments and decrements pinCount. An entry with pinCount > 0 is never evicted.
  • Time-based: pinUntil(key, untilMs) holds the entry resident until wall clock passes untilMs. Used by the predictive-preload path with PRELOAD_CACHE_PIN_MS = 250.

safeDisposeTexture

safeDisposeTexture (viewer/src/renderer/SpatialLruEviction.ts) disposes a Three.js texture AND closes its underlying ImageBitmap. The default Texture.dispose() does not (Three.js issue #23953); without the ImageBitmap close, decoded raster bytes stay GPU-resident forever and the cache's byte accounting diverges from real GPU memory.

Per-preset cache budgets

Mapv10ThreeRenderer.ts defines CACHE_BUDGETS:

PresetterrainBytesruntimeBytes
province-slice24 MiB32 MiB
regional-slice24 MiB32 MiB
realm-slice64 MiB96 MiB
continent128 MiB192 MiB

The active preset is selected from the run manifest's scale hint and applied via setCacheBudgets.

Decoder Worker Pool

DecoderWorkerPool (viewer/src/workers/WorkerPool.ts) owns N dedicated Worker instances and dispatches decode requests round-robin via nextWorkerIndex. Pool size: min(6, max(2, hardwareConcurrency - 1)) (WorkerPool.ts). On a 12-core machine this gives 6 workers; on a 2-core machine, 2.

Init protocol

Each worker is spawned with name: "mapv10-decoder-N" and immediately receives an init message containing the world dimensions and exaggeration (WorkerPool.ts):

{ kind: "init", worldWidthKm, worldHeightKm, exaggeration }

Worker capabilities

A single worker handles HTTP fetch, byte-length verification against the manifest, typed-array decode (raster and mesh), mesh world-transform, computeVertexNormals, worldKm sidecar generation, and vector-tile JSON parsing. Decoded buffers are returned to the main thread as Transferable objects (zero-copy).

Abort protocol

The pool's dispatch method registers an abort listener on the caller's AbortSignal. On abort, the pool posts { kind: "abort", requestId } to the worker (WorkerPool.ts). The worker responds with an error whose name === "AbortError".

Crash recovery

handleWorkerCrash (WorkerPool.ts) starts a replacement worker, hooks its message, error, and messageerror listeners, replays the init message containing world dimensions and exaggeration, and rejects every pending job that was dispatched to the failed worker.

Camera and Controls

The camera is a THREE.PerspectiveCamera(50, 1, 0.1, 2000) (Mapv10ThreeRenderer.ts) — 50° vertical FOV, 0.1 km near, 2000 km far. Default pose (Mapv10ThreeRenderer.ts): position (0, 96, 120), target (0, 0, 0). The aspect ratio is updated on every resize.

OrbitControls configuration

From Mapv10ThreeRenderer.ts:

PropertyValue
enableDampingtrue
dampingFactor0.12
rotateSpeed0.68
panSpeed0.72
zoomSpeed0.82
screenSpacePanningfalse
minDistance35
maxDistance260
maxPolarAngleMath.PI * 0.48 (≈86°)
target(0, 0, 0)

screenSpacePanning = false keeps the pan motion in the world plane so panning along an angled view does not drift the target into the air. The polar-angle ceiling at 86° prevents the camera from going below the strategic plane.

Distance limits

minDistance = 35 and maxDistance = 260 are the pre-load defaults. After loadRun finishes, maxDistance is recomputed against maxExtent * 1.8 (Mapv10ThreeRenderer.ts) so a continent-scale run permits a camera that frames the entire run from above without the orbit controls clamping it mid-zoom.

Zoom bands

The camera state classifies into one of four zoom bands (Mapv10ThreeRenderer.ts): continent (0), realm (1), province (2), location (3). The band drives label visibility, mode-active texture sampling, and a few HUD details.

Scripted camera bypass

__mapv10SetCameraPose(positionKm, targetKm, fovDeg, options) accepts a bypassInteractiveLimits flag. When set, the renderer relaxes the distance clamps for the duration of the script, allowing verifier scenarios to frame poses outside the interactive envelope.

Frame APIs

  • frameSelection(target: MapEntityRef) (Mapv10ThreeRenderer.ts) — frame the camera to the bounding sphere of an entity reference; falls back to frameRun() if the run is not yet loaded.
  • frameWorld(pointKm, distanceKm?) (Mapv10ThreeRenderer.ts) — frame to a world-km point at an optional distance. Heuristic — picks a pose relative to a target.
  • frameRun() (Mapv10ThreeRenderer.ts) — frame the camera to the active run's world bounds.

Input

Pointer drag-vs-click guard

The shell holds a dragClickGuard with a clickMovementThresholdSq = 6 * 6 px² (viewer/src/ui/shell.ts drag-click guard). On pointerdown the start coordinates are recorded; on pointermove while the same pointerId is held, the maximum squared distance is tracked; on pointerup, if the distance exceeded the threshold, the next click is suppressed via suppressNextClick.

WASD / Arrow keyboard navigation

NAVIGATION_KEYS (Mapv10ThreeRenderer.ts) is KeyW, KeyA, KeyS, KeyD, ArrowUp, ArrowLeft, ArrowDown, ArrowRight, ShiftLeft, ShiftRight. Holding a Shift key multiplies translation speed by 1.8 (Mapv10ThreeRenderer.ts). The handler at handleKeyboardNavigationDown ignores key events whose target is an editable element (isEditableKeyboardTarget test).

Pointer foveation throttle

handlePointerMove (Mapv10ThreeRenderer.ts) throttles pointer-move priority recomputes to once per 50 ms. Pointer motion alone does NOT call markFrameDirty() — only the next frame that runs for unrelated reasons picks up the new foveation pointer position.

HUD toggle

F3 toggles the performance HUD; Shift+F3 toggles the verbose HUD readout. Both shortcuts are scoped to non-editable targets.

UI Shell

The viewer's first screen is the map, not a dashboard. Persistent UI is limited to compact top controls, a layer panel, an inspector, a scale ruler, the optional HUD, and a debug drawer hidden by default.

Entry

viewer/src/main.ts is a 10-line bootstrap that imports the stylesheet, locates the #app root, and calls createMapv10Shell(root) from ui/shell.ts.

Top bar

The top bar holds the current run chip, the map-mode tabs, and a Products toggle that surfaces the debug drawer.

Loading panel

A loading section reports the active load phase, a progress bar, and any fatal error text. The phases mirror the loadRun event sequence (Mapv10ThreeRenderer.ts).

Layer panel

Eight toggles plus an overlay slider (shell.ts):

ToggleDefault
Terrainon
Wateron
Borderson
Routeson
Influenceon
Markersoff
Labelson
Mesh (wireframe)off
Overlay slider0

A stream sub-panel below the toggles shows phase, summary, network counter, and per-family progress. A status line below that surfaces fatal status messages.

Inspector

A right-hand inspector aside lists the current selection; before any selection it shows "click the map" placeholder text.

Scale ruler

A bottom-left ruler bar shows the current camera-distance scale and the world target the ruler is anchored to. The snapshot is recomputed on every HUD refresh tick and on demand via __mapv10ScaleRulerProbe.

Performance HUD

A bottom-right data-hud panel shows FPS, frame time, p50/p95/p99 percentiles, the current LOD level, tile counts, byte budgets, cache state, band counts, pin counts, network counts, and within-band priority channels. F3 toggles its visibility; Shift+F3 toggles verbose mode.

The HUD refresh runs on a window.setInterval at 250 ms (viewer/src/ui/shell.ts); the scale ruler also refreshes every 250 ms (shell.ts). HUD-affecting renderer events (run-loaded, error, camera-changed) trigger an immediate refreshHud() outside the interval so the HUD does not lag a quarter-second behind state changes.

Debug drawer

A hidden-by-default drawer with two sections: Products (the manifest's declared artifact list) and Previews (per-product PNG thumbnails). Toggle state lives on [data-debug-drawer].

GL error surfacing

shell.ts intercepts console.error and console.warn, applies a regex (WebGL|THREE|texSubImage2D|...|GPU stall) to the formatted message text, and pushes matches into __mapv10Ready.errors. The verifier (and external checkers) treat any captured warning as a readiness error so a silent driver-level upload failure cannot pass acceptance.

Map Modes

The unified terrain shader serves seven map modes in a single GL program. Mode swap is a single uModeId uniform write — no GL program recompile, no material clone.

ModeuModeIdDescription
Geography0Splat-map blend across the eight active material classes (band A grass/rock/forest/snow-or-wetland; band B sand-coast/bare-earth/ice-glacier/riverbank-mud), Toksvig roughness composition, altitude tinting, aerial haze, micro-relief.
Height1Per-vertex vColor height ramp (CPU pre-computed per tile).
Slope2Per-vertex vColor slope ramp.
Normal3Per-vertex vColor world-normal ramp.
Political4uColorMapLut keyed by RGB-unpacked uGlobalIdMap sample at world-space UV. The continent-wide ID raster avoids the tile-grid checkerboard that per-tile ID rasters produce at coarse LODs (mode-downsampling each tile to one dominant id per texel).
Routes5Desaturated splat blend; route ribbons handle the foreground.
Influence6Effective splat blend with generated corruption intensity from uInfluenceMaskCorruption emphasized for strategic territory readability.

Selection / hover composition (uSelectionMask, uHoveredId, uBorderSdf, uStateField) runs in every mode regardless of the dispatch branch. Layer toggles compose orthogonally — disabling Borders zeroes uBorderOpacity without affecting the rest of the shader.

The viewer does not expose "Scalar overlay" or "Debug" as first-class modes. The scalar overlay is setOverlayState writing to the uStateField sampler binding (the producer transport is a future wave; see Known Limitations). Debug is the Products drawer plus the F3 HUD, not a render mode.

Layer Toggles

Eight layer flags live in MapLayerState (Mapv10ThreeRenderer.ts):

LayerDefaultHow it renders
terrainonThe terrain pass group.
wateronThe water pass group; lake / ocean / river batches.
bordersonNOT a separate mesh layer — uBorderOpacity uniform in the unified terrain shader.
routesonThe route pass group; ribbon meshes plus crossings.
influenceonGenerated corruption/influence blend in the unified terrain shader; influence mode forces it visible.
markersoffThe marker pass group; sample-point sprites.
labelsonThe label pass group; sprite-per-label.
wireframeoffA THREE.WireframeGeometry overlay added per terrain mesh; renders as LineSegments.

Markers are off by default to keep a fresh load uncluttered. Borders are NOT a THREE.Group — disabling the layer animates uBorderOpacity to zero in the terrain shader so the band fades cleanly without a draw-call swap.

Interaction, Picking, and Selection

The viewer uses a CPU THREE.Raycaster against the active primary terrain meshes (Mapv10ThreeRenderer.ts interaction methods). There is no GPU ID-buffer readback. The renderer owns semantic interaction resolution and scene state; the shell passes canvas pixels to renderer commands and renders the typed payloads it receives back.

inspectAt, hoverAt, and selectAt

Defined at Mapv10ThreeRenderer.ts. Sequence:

  1. Convert canvas pixels to NDC via the canvas bounding-rect mapping (Y is flipped because canvas-Y grows downward and NDC-Y grows upward).
  2. Raycast against the active primary terrain meshes.
  3. Convert the hit point from scene space to world km.
  4. Resolve the semantic target from generated products and visible vector ownership.
  5. Build a MapInteractionPayload with state, target, world km, screen px, display name, ancestry, source product keys, cursor hint, clear reason when relevant, and inspector fields.

pick(screenX, screenY) remains a compatibility/debug helper. It delegates to the same resolver and returns the selected target plus payload when a target is present.

Semantic ID lookup

The resolver samples the per-cell raster at the hit world-km coordinate from the resident provinceId.u32.bin and locationId.u32.bin sidecars, walks visible-rendered-entity overrides, resolves water meshes, route centerlines, map features, and terrain-cell sidecars, and returns a MapEntityRef (viewer/src/events/types.ts):

| { kind: "continent"; id: string }
| { kind: "realm"; id: string }
| { kind: "province"; id: string }
| { kind: "location"; id: string }
| { kind: "water"; id: string }
| { kind: "route"; id: string }
| { kind: "feature"; id: string }
| { kind: "terrain-cell"; id: string }

Hover and selection

Hover and selection emit hover-changed and selection-changed events carrying MapInteractionPayload. Location/province hover writes uHoveredId and location/province selection drives the SelectionMask texture (viewer/src/renderer/SelectionMask.ts), an R8 per-locationId texture sampled in the unified shader. Route, water, feature, and label emphasis is kept inside the renderer through auxiliary meshes, scale/color/render-order changes, and label entityId userdata.

Terrain-cell inspector payloads expose tile id, z/x/y, row/column/index, height, slope, water class, base/effective biome and material weights, base/effective forest/wetland fields, and influence fields from generated influence products. Base forestMask and wetlandMask remain preserved generated products; the runtime tile cache loads their per-tile sidecars for inspection while the manifest loader leaves the full-resolution root rasters unfetched on default run load. The renderer continues to consume the generated effective tile products. Influence inspection reports active type key/id, intensity, mask value, available source/rule ids, effective product keys, and the marker that base terrain products are preserved rather than mutated.

The shell handles UX commands only: pointer move calls hoverAt, click calls selectAt, empty click clears selection with empty-click, Escape clears with escape, and pointer leave clears hover with pointer-leave. The renderer provides cursor hints and the shell renders the hover tooltip, selected inspector, and breadcrumb from the payload.

raycastSurfaceAtCanvasPx

Mapv10ThreeRenderer.ts raycasts at a canvas pixel against the active terrain meshes; if every terrain ray misses, it falls back to an analytical sea-level plane. Used by the scale ruler so the world-target readout stays sensible even when the camera frames open ocean.

Labels

Generator-side payload

The generator emits per-label records as part of each runtime tile (viewer/src/data/types.ts):

{ id, text, entityKind, entityId, point, priority,
minZoomBand, maxZoomBand, category }

Viewer-side handling

The renderer:

  • Filters labels by zoom band against [minZoomBand, maxZoomBand].
  • Runs collision avoidance in screen space, dropping lower-priority labels that overlap higher-priority labels.
  • Fades labels in and out reusing uTileOpacity over LOD_FADE_MS = 320.
  • Highlights hovered and selected entities through per-sprite tinting.

LabelTextureCache

viewer/src/renderer/LabelTextureCache.ts rasterises label text via Canvas2D and memoises the result, keyed by text string. A label whose text repeats (e.g. "Forest") gets one shared THREE.Texture rather than one per sprite.

Layout cadence

Two constants control label work (Mapv10ThreeRenderer.ts label cadence constants):

  • LABEL_LAYOUT_INTERVAL_MS = 100 — minimum interval between collision recomputes.
  • LABEL_WORK_ITEMS_PER_FRAME = 16 — cap on label updates per frame inside the FrameBudget so a heavy label region cannot burn the budget alone.

The label sprites live in the labelGroup (mapv10-label-pass), one THREE.Sprite per visible label.

URL Params

The viewer reads exactly one URL parameter: ?manifest=<value>. normalizeManifestUrl(value) (viewer/src/ui/manifestUrl.ts) applies these rules:

  • Empty / null / whitespace → DEFAULT_MANIFEST_URL (/mapv10/runs/continent-lod6/manifest.json).
  • Aliases continent, continent-lod, continent-lod6 → default manifest URL (manifestUrl.ts).
  • Absolute URL (http:// or https://): preserved, with the legacy one-segment / two-segment run-path normalisation applied if relevant.
  • Root-relative path: parsed against https://mapv10.local, then aliases and legacy-run paths apply, then .json is appended if absent.

There is no camera-pose, mode, or layer URL deep-link. Two viewers visiting the same ?manifest= value start at the same default pose.

Debug and Scripting Hooks

The shell installs the following globals on window so verifier scenarios and external checkers can drive the viewer without going through the DOM. All assignments live in createMapv10Shell (viewer/src/ui/shell.ts); the renderer itself exposes typed methods which the shell re-binds as globals.

GlobalRole
__mapv10Ready{ loaded, errors } readiness state; verifier polls until loaded and errors.length === 0.
__mapv10PickAtCenter()Pick at viewport centre; returns MapPickResult | null.
__mapv10PickAtCanvasPoint(xRatio, yRatio)Pick at fractional canvas coords.
__mapv10HoverAtCanvasPoint(xRatio, yRatio)Run renderer-owned hover resolution at fractional canvas coords.
__mapv10SelectAtCanvasPoint(xRatio, yRatio)Run renderer-owned selection resolution at fractional canvas coords.
__mapv10ClearHover()Clear hover through the renderer with the pointer-leave reason.
__mapv10ClearSelection(reason?)Clear selection through the renderer, defaulting to scenario-clear.
__mapv10SetMode(mode)Set the current map mode.
__mapv10SetLayerState(partial)Patch one or more layer flags.
__mapv10FrameEntity(target)Frame a MapEntityRef.
__mapv10FrameWorld(pointKm, distanceKm?)Frame a world-km point.
__mapv10CameraState()Snapshot the live camera state.
__mapv10SetCameraPose(positionKm, targetKm, fovDeg, options)Scripted camera pose; honours bypassInteractiveLimits.
__mapv10WaitForTilesSettled(timeoutMs?)Resolve when the scheduler has zero in-flight and zero pending.
__mapv10WaitForFrame()Resolve after one stable on-screen render.
__mapv10TerrainGeometryProbe()Inspect resident terrain meshes' triangle counts and bounds.
__mapv10TerrainShadingProbe()Inspect terrain shader uniforms and visual config.
__mapv10TerrainSelectionProbe()Inspect selection-mask and resolved RenderCommit.
__mapv10ScaleRulerProbe()Snapshot the scale-ruler state.
__mapv10RunScenario(id)Run a scripted scenario by id.
__mapv10ListScenarios()List the registered scenario ids.
__mapv10SetCacheConfig(partial)Patch the cache config at runtime.
__mapv10SetTerrainVisualConfig(partial)Patch the terrain visual config at runtime.
__mapv10TerrainVisualConfig()Read the active terrain visual config.
__mapv10StartZoomTrace(options?)Start a zoom-trace session.
__mapv10MarkZoomTrace(label, detail?)Mark a zoom-trace event.
__mapv10StopZoomTrace()Stop and return the report.
__mapv10ZoomTraceReport()Read the latest zoom-trace report.
__mapv10DebugState()Snapshot of HUD-relevant state used by the HUD renderer itself.

The Playwright verifier scripts under viewer/scripts/ are the primary consumer; the verifier polls __mapv10Ready, drives camera poses through __mapv10SetCameraPose, and asserts on probes.

Known Limitations

  1. State-field producer unwired. uStateField is bound to a world-space RGBA8 render target so the shader's texture() call is always valid. The shader samples it with continent/world UV derived from per-fragment world km and uWorldBoundsKm; the producer transport that writes live overlay data into that render target is a future wave.

  2. JFA ping-pong unconnected. FBOPool.acquirePingPong accepts the "jfa" spec name (FBOPool.ts FBOSpec and acquirePingPong), but no caller writes to or reads from the JFA ping-pong pair. Borders render exclusively from the per-tile static SDF rasters baked offline.

  3. Hardcoded constants without CacheConfig entries. The following numeric thresholds are baked into source files rather than living on a UI-exposed config field:

    • MAX_IN_FLIGHT_TILE_REQUESTS = 24 (Mapv10ThreeRenderer.ts)
    • TERRAIN_NODE_CREATES_PER_FRAME = 24 (Mapv10ThreeRenderer.ts)
    • TERRAIN_NODE_DISPOSES_PER_FRAME = 19 (Mapv10ThreeRenderer.ts)
    • TERRAIN_TEXTURE_CREATES_PER_FRAME = 96 (Mapv10ThreeRenderer.ts)
    • AUXILIARY_NODE_DISPOSES_PER_FRAME = 64 (Mapv10ThreeRenderer.ts)
    • LOD_FADE_MS = 320 (MaterialFade.ts)
    • TERRAIN_RASTER_SAMPLE_ERROR_TARGET_PX = 6.0 (VisibilitySet.ts)
    • MAX_MISSING_ROUTE_ASSETS_PER_FRAME = 64 (TileStreamingPlanner.ts)
    • ROUTE_REQUEST_THROTTLE_THRESHOLD = 128 (TileStreamingPlanner.ts)

    The TERRAIN_RASTER_SAMPLE_ERROR_TARGET_PX value remains locked. See wave-3b-lod-ladder-camera-decision.md for the accepted and implemented z6/z7 ladder context before changing physical LOD products.

  4. Oversized route aggregate keys are a string-literal workaround. OVERSIZED_ROUTE_AGGREGATE_KEYS = new Set(["route.aggregate.z2", "route.aggregate.z3"]) (TileStreamingPlanner.ts) hard-codes the aggregate-key list rather than deriving it from the manifest.

  5. No HTTP retry. A transient HTTP error on a tile permanently fails the tile in this version of the scheduler.

  6. Markers off by default and undocumented in-app. The default markers: false (Mapv10ThreeRenderer.ts) silently hides the sample-point markers; nothing in the UI calls out that the layer exists.

  7. No keyboard shortcut for mode switching. The map-mode tabs in the top bar are mouse-only; there is no key binding for cycling modes.

  8. Only ?manifest= deep-link. The URL does not encode the camera pose, map mode, or layer state. Two visitors to the same URL share the run but not the view.

Source / Symbol Map

A pointer index for navigation:

PathWhat it owns
viewer/src/main.tsBootstrap.
viewer/src/ui/shell.tsDOM, top bar, layer panel, inspector, HUD, debug drawer, __mapv10* globals.
viewer/src/ui/manifestUrl.tsURL param normalisation.
viewer/src/ui/loadErrorPresentation.tsFatal-error formatting for the loading panel.
viewer/src/renderer/Mapv10ThreeRenderer.tsRenderer core.
viewer/src/renderer/MaterialFade.tsShared material factories and the unified terrain shader.
viewer/src/renderer/lod/VisibilitySet.tsPure visibility-set selection.
viewer/src/renderer/lod/TileStreamingPlanner.tsCache fan-out and scheduler reconcile.
viewer/src/renderer/lod/RenderResolver.tsPer-frame render commit and previous-frame fallback.
viewer/src/renderer/lod/TilePriority.tsBands, banded priority, DESP and preload constants.
viewer/src/renderer/TileCache.tsTerrainTileCache, MeshAssetCache, TileRequestScheduler.
viewer/src/renderer/RuntimeTileCache.tsRuntimeTileCache, the CacheCoordinator.
viewer/src/renderer/SpatialLruEviction.tsEviction-score formula and safeDisposeTexture.
viewer/src/renderer/SamplerProbe.tsGL sampler-budget probe.
viewer/src/renderer/FBOPool.tsRender-target pool (state-field RT, JFA ping-pong reservation).
viewer/src/renderer/SelectionMask.tsPer-locationId selection texture.
viewer/src/renderer/ColorMapLut.tsProvince-color-seeds LUT builder.
viewer/src/renderer/LabelTextureCache.tsCanvas2D label rasterisation memoiser.
viewer/src/renderer/FrameBudget.tsPer-frame work scheduler with lane fairness.
viewer/src/renderer/SpatialLruEviction.tsEviction policy.
viewer/src/workers/WorkerPool.tsDecoderWorkerPool.
viewer/src/workers/decoderWorker.tsWorker body.
viewer/src/workers/decoderProtocol.tsWire-format types.
viewer/src/data/types.tsRun / tile / label data shapes.
viewer/src/data/manifestLoader.tsManifest parsing and validation.
viewer/src/data/meshLoader.tsMesh decoding entry point.
viewer/src/data/rasterLoader.tsRaster decoding entry point.
viewer/src/events/types.tsMapEntityRef, renderer events.
viewer/src/config/CacheConfig.tsCache budget config.
viewer/src/config/LodVisualConfig.tsPer-zoom-band visual config.
viewer/src/config/TerrainVisualConfig.tsTerrain visual config (sun direction, atmosphere, relief).
viewer/src/config/ScaleRulerConfig.tsRuler tick spacing.
viewer/server/mapv10ArtifactMiddleware.tsVite artifact middleware.
viewer/scripts/Verifier and bootstrap scripts.

For complementary documentation see ./generator.md (generator pipeline and stage reference), ./export-contract.md (export schema for Stage 15 consumers), scenarios.md (scripted camera scenarios), extending.md (adding map modes, layers, or label categories), and wave-protocol.md (the four-step wave protocol that gates every change).