# 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)
