# Terrain Chunk LOD Planner ## Metadata | Field | Value | |---|---| | Status | Implemented extraction | | Target/work package | M03 / `M03-RND-TERRAIN-LOD-PLANNER-001` | | Owners | Pure desired chunk-LOD calculation only | | Last verified | Worktree `work/sindo-main-codex/m03-terrain-chunk-lod-planner`, 2026-07-16 | | Profiles/capabilities | Existing Performance/Balanced/High/Custom radii | ## Purpose Calculate the desired terrain LOD for each populated parsed ADT chunk from one typed Godot-space focus and one immutable renderer-policy snapshot. ## Non-goals - Own tile/chunk state, queues, budgets, tasks, Mesh, Node, material or RID. - Select tile LOD, create/remove geometry or apply a completed plan. - Normalize custom radii, change profile defaults or claim visual parity. - Parse ADT data or convert between WoW and Godot coordinate spaces. ## Context and boundaries ```mermaid flowchart LR Focus[GodotWorldPosition] --> Planner[TerrainChunkLodPlanner] Chunks[Parsed ADT chunk records] --> Planner Config[Streamer LOD configuration] --> Policy[TerrainChunkLodPolicy] Policy --> Planner Planner --> Desired[chunk index to LOD 0/1/2] Desired --> Loader[StreamingWorldLoader state adapter] Loader --> Queue[Existing create/remove queues and budgets] ``` Allowed dependencies are the typed Godot position, immutable policy and call-local Array/Dictionary/Vector3 values. SceneTree, RenderingServer, gameplay, network, files, tasks and persistent caches are forbidden. ## Public API | Symbol | Kind | Purpose | Thread/lifetime | Errors | |---|---|---|---|---| | `TerrainChunkLodPolicy.new(...)` | Immutable value | Snapshot enabled flag, three raw radii and chunk size | Caller-owned; one planning refresh | Values are preserved without normalization | | `TerrainChunkLodPlanner.plan(chunks, focus_position, policy)` | Pure query | Map populated source indices to LOD 0/1/2 | Synchronous; result caller-owned | Invalid/null typed inputs or disabled policy return empty | The planner validates scripts by preloaded identity so it works from a cold Godot global-class cache. ## Inputs and outputs | Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | |---|---|---|---|---|---| | Input | Parsed chunk array with `origin: Vector3` | ADT load result | Planner | Loader-owned; read only | One call | | Input | `GodotWorldPosition` | Streamer focus adapter | Planner | Immutable caller reference | One call | | Input | `TerrainChunkLodPolicy` | Streamer exported configuration | Planner | Immutable caller reference | One call | | Output | `{source_chunk_index: 0/1/2}` | Planner | Loader desired state | Fresh caller dictionary | One call | The Y coordinate is intentionally ignored. Empty chunk records and chunks beyond the third radius are absent from the result. ## Data flow ```mermaid flowchart TD Start[plan] --> Valid{Typed inputs and enabled?} Valid -->|no| Empty[Return empty dictionary] Valid -->|yes| Radii[Square raw radius times chunk size] Radii --> Iterate[Visit source chunk indices in order] Iterate --> Populated{Record populated?} Populated -->|no| Iterate Populated -->|yes| Center[origin XZ plus half chunk] Center --> Distance[Squared horizontal distance] Distance --> Threshold{First inclusive threshold} Threshold -->|LOD0/1/2| Record[Record source index] Threshold -->|outside| Iterate Record --> Iterate Iterate --> Done[Return desired mapping] ``` ## Lifecycle/state The planner is stateless. A policy is constructed for one refresh, read during synchronous planning and released. The returned dictionary is then owned by the loader state. There is no cancellation, recovery state or retained resource. ## Main sequence ```mermaid sequenceDiagram participant Stream as StreamingWorldLoader participant Policy as TerrainChunkLodPolicy participant Planner as TerrainChunkLodPlanner Stream->>Policy: snapshot enabled/radii/chunk size Stream->>Planner: plan(chunks, typed focus, policy) Planner-->>Stream: fresh desired LOD dictionary Stream->>Stream: existing tile state, queue and budget application ``` ## Ownership, threading and resources - Planner and policy own no engine resources, nodes, RIDs, tasks or caches. - The loader retains parsed chunks, desired state and every application side effect. - Current calls run synchronously on the renderer main thread; the pure inputs also make isolated contract testing possible without a world scene. ## Errors, cancellation and recovery | Failure | Detection | Behavior | Diagnostic | Recovery | |---|---|---|---|---| | Null/wrong focus or policy script | Identity guard | Empty plan | Contract verifier | Supply typed values next refresh | | Disabled chunk LOD | Policy flag | Empty plan; existing tile-LOD fallback applies | Existing renderer state | Enable profile option | | Empty chunk record | Record guard | Preserve sparse source index; omit record | None | Not an error | | Custom negative radius | No normalization | Existing squared-radius behavior is retained | Regression fixture | Change caller configuration if undesired | There is no asynchronous work or cancellation token. ## Configuration and capabilities The policy snapshots `use_chunk_terrain_lods`, `lod0_radius_chunks`, `lod1_radius_chunks`, `lod2_radius_chunks` and the existing `CHUNK_SIZE`. Inclusive squared thresholds and raw custom values deliberately preserve prior behavior. ## Persistence, cache and migration No serialization or cache format changes. Existing baked terrain, control-splat and splat cache versions remain unchanged and require no rebuild. ## Diagnostics and observability The service emits no logs. The contract verifier reports semantic cases and a bounded synthetic timing. Asset-backed p95/p99 remains milestone acceptance work. ## Verification - `verify_terrain_chunk_lod_planner.gd`: center and vertical invariance, inclusive thresholds, sparse/outside chunks, disabled/custom-negative policy, loader delegation and 250-by-256 bounded planning. - Renderer cache, facade, internal-access, baseline, scheduler, target-planner, focus, ground-query and coordinate-boundary regressions remain required. - Fidelity evidence is formula preservation only; no new build-12340 visual equivalence claim is made. ## Extension points - Add measured direction/velocity only through an explicit policy/contract change. - Keep queue prioritization and LOD application as separately bounded services. ## Capability status | Capability | Status | Evidence | Gap/next step | |---|---|---|---| | Pure chunk desired-LOD calculation | Implemented extraction | Asset-free semantic/source-boundary contract | Asset-backed p95/p99 pending | | Queue/state/finalization ownership | Remains in loader | Source-boundary regression | Further terrain decomposition required | ## Known gaps and risks - Raw negative and non-monotonic custom radii retain historical squared/ordered semantics, even though such settings may be surprising. - The planner is linear in parsed chunk count and allocates one result dictionary. - Terrain build, quality upgrades and geometry lifecycle remain monolithic. ## Source map | Path | Responsibility | |---|---| | `src/render/terrain/terrain_chunk_lod_policy.gd` | Immutable configuration snapshot | | `src/render/terrain/terrain_chunk_lod_planner.gd` | Pure horizontal-distance plan | | `src/scenes/streaming/streaming_world_loader.gd` | Composition and plan application adapter | | `src/tools/verify_terrain_chunk_lod_planner.gd` | Contract, dependency and timing regression | ## Related decisions and references - [`world-renderer.md`](world-renderer.md) - [`terrain-quality-mesh-cache.md`](terrain-quality-mesh-cache.md) - [`../../RENDER.md`](../../RENDER.md) - [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)