Compare commits
10 Commits
ea08b8ef51
...
57ae1b8f16
| Author | SHA1 | Date | |
|---|---|---|---|
| 57ae1b8f16 | |||
| 24aef13801 | |||
| 4b90dbc1c1 | |||
| f800e11cd2 | |||
| 0d2c820ea7 | |||
| b568768866 | |||
| f36fabb8e1 | |||
| 9e69a8a95b | |||
| eb94cfc646 | |||
| aa21aafdfa |
@@ -950,6 +950,33 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
|||||||
- The ray height, 3x3 tile search and `2/5/10/20/40`-unit nearby fallback are
|
- The ray height, 3x3 tile search and `2/5/10/20/40`-unit nearby fallback are
|
||||||
preserved. No terrain geometry, cache, streaming or visual behavior changed.
|
preserved. No terrain geometry, cache, streaming or visual behavior changed.
|
||||||
|
|
||||||
|
## 2026-07-16 Terrain Chunk LOD Planner Extraction
|
||||||
|
|
||||||
|
- `TerrainChunkLodPlanner` now owns the pure mapping from populated parsed ADT
|
||||||
|
chunk indices to desired terrain LOD 0/1/2.
|
||||||
|
- `TerrainChunkLodPolicy` snapshots the existing enabled flag, three raw chunk
|
||||||
|
radii and chunk size once per streaming-target application.
|
||||||
|
- Horizontal XZ distance to the chunk center, inclusive squared thresholds,
|
||||||
|
sparse-index omission and historical negative-radius squaring are unchanged.
|
||||||
|
- `StreamingWorldLoader` still owns tile/chunk state, queues, operation budgets,
|
||||||
|
creation/removal, Mesh/Node/RID finalization and tile-LOD fallback.
|
||||||
|
- Cache formats, quality presets and visible behavior are unchanged. The
|
||||||
|
scene-free contract is formula evidence only; asset-backed p95/p99 and visual
|
||||||
|
acceptance remain required.
|
||||||
|
|
||||||
|
## 2026-07-16 Terrain Chunk Geometry Queue Planner Extraction
|
||||||
|
|
||||||
|
- `TerrainChunkGeometryQueuePlanner` now compares current and desired chunk state
|
||||||
|
and returns fresh chunk create/remove request arrays.
|
||||||
|
- LOD mismatches and absent chunks retain the existing request shape; creates
|
||||||
|
remain sorted by horizontal distance from chunk origin to streaming focus.
|
||||||
|
- Desired tile LOD still removes every current chunk, while sparse parsed chunk
|
||||||
|
records retain the existing tile-origin distance fallback.
|
||||||
|
- `StreamingWorldLoader` still owns queue storage/drains, removal-first execution,
|
||||||
|
the shared `chunk_geometry` budget lane and every Mesh/Node/RID side effect.
|
||||||
|
- Cache formats, quality profiles and visual rules are unchanged. Asset-backed
|
||||||
|
p95/p99 and visual acceptance remain required.
|
||||||
|
|
||||||
## 2026-07-16 World Environment Snapshot Facade
|
## 2026-07-16 World Environment Snapshot Facade
|
||||||
|
|
||||||
- `WorldEnvironmentSnapshot` carries one immutable finite time-of-day value,
|
- `WorldEnvironmentSnapshot` carries one immutable finite time-of-day value,
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# M03-RND-TERRAIN-CHUNK-QUEUE-001 — Pure terrain chunk geometry queue planner
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M03-RND-TERRAIN-CHUNK-QUEUE-001:sindo-main-codex:2026-07-19 -->
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Target: M03
|
||||||
|
- Program: RND
|
||||||
|
- Owner/Agent ID: sindo-main-codex
|
||||||
|
- Branch: `work/sindo-main-codex/m03-terrain-chunk-queue`
|
||||||
|
- Lease expires UTC: 2026-07-19
|
||||||
|
- Integrator: M03 milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Extract chunk-geometry create/remove desired-operation calculation from the
|
||||||
|
streamer into a scene-free `TerrainChunkGeometryQueuePlanner`.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Move queue storage/drains, render budgets, tile-LOD queues or async tasks.
|
||||||
|
- Move chunk Mesh/Node/RID creation/removal or terrain state ownership.
|
||||||
|
- Change removal-first execution, nearest-first create order or LOD selection.
|
||||||
|
- Change coordinate conversions, radii, cache formats or quality profiles.
|
||||||
|
- Add dependencies or mark the broad terrain service checklist complete.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: `src/render/terrain/terrain_chunk_geometry_queue_planner.gd`,
|
||||||
|
`src/tools/verify_terrain_chunk_geometry_queue_planner.gd`,
|
||||||
|
`docs/modules/terrain-chunk-geometry-queue-planner.md`, this claim
|
||||||
|
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||||
|
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
|
||||||
|
`targets/03-renderer-facade.md`
|
||||||
|
- Generated/ignored: `.godot`, native DLL, generated ADT resources, caches and
|
||||||
|
proprietary renderer corpus
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- Input: loader-owned tile-state read model plus immutable `GodotWorldPosition`
|
||||||
|
- Output: fresh create/remove request arrays; planner retains no queue state
|
||||||
|
- Removal: current chunks absent from desired state, or all current chunks while
|
||||||
|
tile LOD is desired
|
||||||
|
- Creation: missing or LOD-mismatched chunks, sorted by origin XZ distance
|
||||||
|
- Queue/cache/schema/coordinate versions: unchanged
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: integrated terrain LOD planner master `b568768`
|
||||||
|
- Blocks: broader terrain service extraction and dependency/performance acceptance
|
||||||
|
- External state: none; tests use synthetic tile/chunk dictionaries
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: dedicated removal/create/mismatch/tile-fallback/sparse/order and
|
||||||
|
bounded timing verifier; loader source boundary; facade/cache/LOD/internal-access/
|
||||||
|
manifest/shutdown/scheduler/streaming planner and repository gates
|
||||||
|
- Fidelity evidence: extracted branch/order/distance rules and loader drain order
|
||||||
|
- Performance budget: bounded linear planning plus existing create sort; no engine
|
||||||
|
resource, task, cache or persistent allocation framework
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline planner API documentation
|
||||||
|
- New module spec with inputs/outputs, data-flow, lifecycle, sequence, ownership,
|
||||||
|
failure, performance and source map
|
||||||
|
- World renderer/module registry/`RENDER.md` updates
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important name: `TerrainChunkGeometryQueuePlanner`
|
||||||
|
- Simplest solution: one stateless planner returning two fresh arrays
|
||||||
|
- Rejected complexity: generic operation graph, queue class, callback strategy,
|
||||||
|
spatial index or ownership transfer
|
||||||
|
- Unavoidable complexity: raw dictionaries remain at the loader/ADT adapter boundary
|
||||||
|
- Measured optimization evidence: bounded synthetic multi-tile verifier
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: accepted
|
||||||
|
- Done: pure planner, loader queue adoption, semantic/timing regressions and docs
|
||||||
|
- Next: asset-backed performance acceptance and further terrain-service extraction later in M03
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M03-RND-TERRAIN-CHUNK-QUEUE-001:f800e11 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-TERRAIN-CHUNK-QUEUE-001:24aef13 -->
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: `f800e11` (`refactor(M03): extract terrain chunk queue planner`)
|
||||||
|
- Results: dedicated contract PASS (`cases=8`, `iterations=100`, `235.096 ms`);
|
||||||
|
LOD/cache/facade/internal-access/manifest/shutdown/scheduler/streaming/focus/
|
||||||
|
ground/terrain-query/coordinate regressions PASS; internal inventory remains
|
||||||
|
`42`; editor parse has no script errors; documentation/coordination/diff gates
|
||||||
|
PASS; checkpoint dry-run planned `7/7` entries with expected missing-private-
|
||||||
|
corpus diagnostics
|
||||||
|
- Remaining risks: planner adds a second linear tile-state pass; asset-backed
|
||||||
|
p95/p99 and visual acceptance remain pending; queue drains, terrain state and
|
||||||
|
Mesh/Node/RID finalization remain loader-owned
|
||||||
|
- Documentation updated: `terrain-chunk-geometry-queue-planner.md`, module
|
||||||
|
registry, `world-renderer.md`, `RENDER.md`, inline API documentation and
|
||||||
|
data-flow/lifecycle/sequence diagrams
|
||||||
|
- Integration: accepted in master merge `24aef13`; post-merge planner,
|
||||||
|
internal-access, facade and manifest gates passed
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# M03-RND-TERRAIN-LOD-PLANNER-001 — Pure terrain chunk LOD planner
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M03-RND-TERRAIN-LOD-PLANNER-001:sindo-main-codex:2026-07-18 -->
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Target: M03
|
||||||
|
- Program: RND
|
||||||
|
- Owner/Agent ID: sindo-main-codex
|
||||||
|
- Branch: `work/sindo-main-codex/m03-terrain-chunk-lod-planner`
|
||||||
|
- Lease expires UTC: 2026-07-18
|
||||||
|
- Integrator: M03 milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Extract chunk-level terrain LOD desired-state calculation from the streamer into
|
||||||
|
a scene-free `TerrainChunkLodPlanner` with immutable policy and typed focus input.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Move chunk/tile queues, loaded state, Mesh/Node/RID creation or budget drains.
|
||||||
|
- Change radii, chunk size, threshold inclusivity, priority sorting or tile LOD policy.
|
||||||
|
- Normalize/reorder custom radii or reinterpret negative values.
|
||||||
|
- Move ADT parsing, quality cache/tasks, materials or ground query.
|
||||||
|
- Add dependencies, persistence, workers or mark terrain extraction complete.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: `src/render/terrain/terrain_chunk_lod_policy.gd`,
|
||||||
|
`src/render/terrain/terrain_chunk_lod_planner.gd`,
|
||||||
|
`src/tools/verify_terrain_chunk_lod_planner.gd`,
|
||||||
|
`docs/modules/terrain-chunk-lod-planner.md`, this claim
|
||||||
|
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||||
|
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
|
||||||
|
`targets/03-renderer-facade.md`
|
||||||
|
- Generated/ignored: `.godot`, native DLL, generated resources, caches and
|
||||||
|
proprietary renderer corpus
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- Input: ADT chunk adapter records, immutable `GodotWorldPosition`, immutable
|
||||||
|
enabled/radii/chunk-size policy
|
||||||
|
- Output: detached Dictionary mapping source chunk index to LOD `0`, `1` or `2`
|
||||||
|
- Formula: horizontal distance to `origin + half chunk`; inclusive squared thresholds
|
||||||
|
- Disabled policy and empty/sparse chunks return absent entries exactly as before
|
||||||
|
- Queue/cache/schema/coordinate versions: unchanged
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: integrated terrain cache slice master `ea08b8e`
|
||||||
|
- Blocks: broader terrain service extraction and dependency/performance acceptance
|
||||||
|
- External state: none; tests use synthetic chunk adapter records
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: dedicated center/boundary/outside/sparse/disabled/custom-radius and
|
||||||
|
bounded timing verifier; loader source boundary; facade/cache/internal-access/
|
||||||
|
manifest/shutdown/scheduler/streaming planner and repository gates
|
||||||
|
- Fidelity evidence: extracted formulas and all seven baseline plans remain unchanged
|
||||||
|
- Performance budget: bounded linear scan over at most the ADT chunk record count;
|
||||||
|
no Node, Resource, RID, task, cache entry or allocation framework
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline API docs for policy/planner
|
||||||
|
- New module spec with inputs/outputs, data-flow, lifecycle, sequence, ownership,
|
||||||
|
failure, performance and source map
|
||||||
|
- World renderer/module registry/`RENDER.md` updates
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important names: `TerrainChunkLodPolicy`, `TerrainChunkLodPlanner`
|
||||||
|
- Simplest solution: immutable scalar policy plus one pure linear planner
|
||||||
|
- Rejected complexity: generic spatial index, quadtree, callback strategy,
|
||||||
|
normalized profile model or queue ownership
|
||||||
|
- Unavoidable complexity: raw chunk dictionaries remain at the ADT adapter boundary
|
||||||
|
- Measured optimization evidence: verifier records bounded High-like planning time
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: accepted
|
||||||
|
- Done: immutable policy, pure planner, both loader call sites, regressions and docs
|
||||||
|
- Next: asset-backed performance acceptance and further terrain-service extraction later in M03
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M03-RND-TERRAIN-LOD-PLANNER-001:eb94cfc -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-TERRAIN-LOD-PLANNER-001:f36fabb -->
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: `eb94cfc` (`refactor(M03): extract terrain chunk LOD planner`)
|
||||||
|
- Results: dedicated contract PASS (`cases=7`, `iterations=250`, `12.726 ms`);
|
||||||
|
cache/facade/internal-access/manifest/shutdown/scheduler/streaming-focus/
|
||||||
|
streaming-planner/ground/terrain-query/coordinate regressions PASS; editor parse
|
||||||
|
has no script errors; documentation and coordination gates PASS; checkpoint
|
||||||
|
dry-run planned `7/7` entries with expected missing-private-corpus diagnostics
|
||||||
|
- Remaining risks: asset-backed p95/p99 and visual acceptance remain pending;
|
||||||
|
terrain state, queue, quality-upgrade and finalization ownership remains in loader
|
||||||
|
- Documentation updated: `terrain-chunk-lod-planner.md`, module registry,
|
||||||
|
`world-renderer.md`, `RENDER.md`, inline public API documentation and data-flow,
|
||||||
|
lifecycle and sequence diagrams
|
||||||
|
- Integration: accepted in master merge `f36fabb`; post-merge planner, facade,
|
||||||
|
internal-access and manifest gates passed
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
| Local player movement | Implemented | [`local-player-movement.md`](local-player-movement.md) |
|
| Local player movement | Implemented | [`local-player-movement.md`](local-player-movement.md) |
|
||||||
| Terrain query | Implemented | [`terrain-query.md`](terrain-query.md) |
|
| Terrain query | Implemented | [`terrain-query.md`](terrain-query.md) |
|
||||||
| Terrain quality Mesh cache | Implemented extraction | [`terrain-quality-mesh-cache.md`](terrain-quality-mesh-cache.md) |
|
| Terrain quality Mesh cache | Implemented extraction | [`terrain-quality-mesh-cache.md`](terrain-quality-mesh-cache.md) |
|
||||||
|
| Terrain chunk LOD planner | Implemented extraction | [`terrain-chunk-lod-planner.md`](terrain-chunk-lod-planner.md) |
|
||||||
|
| Terrain chunk geometry queue planner | Implemented extraction | [`terrain-chunk-geometry-queue-planner.md`](terrain-chunk-geometry-queue-planner.md) |
|
||||||
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
|
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
|
||||||
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
|
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
|
||||||
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
# Terrain Chunk Geometry Queue Planner
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Status | Implemented extraction |
|
||||||
|
| Target/work package | M03 / `M03-RND-TERRAIN-CHUNK-QUEUE-001` |
|
||||||
|
| Owners | Pure chunk geometry create/remove request calculation |
|
||||||
|
| Last verified | Worktree `work/sindo-main-codex/m03-terrain-chunk-queue`, 2026-07-16 |
|
||||||
|
| Profiles/capabilities | Existing chunk/tile LOD desired state; all profiles |
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Compare loader-owned current chunk geometry with desired chunk/tile LOD state
|
||||||
|
and produce fresh removal and nearest-first creation request arrays.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Retain, drain or bound queues, or own per-frame operation permits.
|
||||||
|
- Create/remove Mesh, Node, material or RID resources.
|
||||||
|
- Calculate desired chunk/tile LOD, parse ADT data or schedule worker tasks.
|
||||||
|
- Change cache versions, renderer profiles or visible fidelity rules.
|
||||||
|
- Provide a generic queue/operation framework.
|
||||||
|
|
||||||
|
## Context and boundaries
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Current[Loader-owned tile/chunk state] --> Planner[TerrainChunkGeometryQueuePlanner]
|
||||||
|
Desired[Desired chunk and tile LOD state] --> Planner
|
||||||
|
Focus[GodotWorldPosition] --> Planner
|
||||||
|
Planner --> Remove[Stable-order remove requests]
|
||||||
|
Planner --> Create[Nearest-first create requests]
|
||||||
|
Remove --> Loader[StreamingWorldLoader queues]
|
||||||
|
Create --> Loader
|
||||||
|
Loader --> Budget[RenderBudgetScheduler permit]
|
||||||
|
Budget --> GPU[Existing Mesh/Node/RID side effects]
|
||||||
|
```
|
||||||
|
|
||||||
|
Allowed dependencies are typed Godot position and call-local scalar/container
|
||||||
|
values. SceneTree, RenderingServer, ResourceLoader, worker APIs, gameplay,
|
||||||
|
network, editor UI, files and persistent caches are forbidden.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `TerrainChunkGeometryQueuePlanner.plan(tile_states, focus_position)` | Pure query | Return fresh `create_requests` and `remove_requests` arrays | Synchronous; caller owns result | Null/wrong focus returns two empty arrays |
|
||||||
|
|
||||||
|
Script-identity validation uses a preload and therefore works from a cold Godot
|
||||||
|
global-class cache.
|
||||||
|
|
||||||
|
## Inputs and outputs
|
||||||
|
|
||||||
|
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Input | Tile-state read model with current chunks and desired LODs | Streamer | Planner | Loader-owned; read only | One rebuild |
|
||||||
|
| Input | Desired tile LOD | Tile LOD calculation | Planner | Scalar in loader state | One rebuild |
|
||||||
|
| Input | Parsed chunk origins and tile fallback origin | ADT result/loader | Planner | Loader-owned; read only | Map session |
|
||||||
|
| Input | `GodotWorldPosition` | Streamer focus adapter | Planner | Immutable caller reference | One rebuild |
|
||||||
|
| Output | Remove `{tile, chunk}` records | Planner | Loader removal queue | Fresh caller array/dictionaries | Until drained/rebuilt |
|
||||||
|
| Output | Create `{tile, chunk, lod, dist_sq}` records | Planner | Loader creation queue | Fresh nearest-first array/dictionaries | Until drained/rebuilt |
|
||||||
|
|
||||||
|
Distance is horizontal XZ squared distance from the chunk origin, not its center.
|
||||||
|
Y is intentionally ignored. A missing/sparse parsed origin falls back to the
|
||||||
|
tile state's existing origin exactly as before extraction.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start[plan] --> FocusValid{Typed focus?}
|
||||||
|
FocusValid -->|no| Empty[Return two empty arrays]
|
||||||
|
FocusValid -->|yes| Tile[Iterate tile state insertion order]
|
||||||
|
Tile --> TileLod{Desired tile LOD active?}
|
||||||
|
TileLod -->|yes| RemoveAll[Append every current chunk removal]
|
||||||
|
TileLod -->|no| Compare[Compare current and desired chunks]
|
||||||
|
Compare --> RemoveMissing[Append current chunks absent from desired]
|
||||||
|
Compare --> CreateMismatch[Append LOD-mismatched creates]
|
||||||
|
Compare --> CreateMissing[Append absent desired creates]
|
||||||
|
CreateMismatch --> Origin[Parsed chunk origin or tile fallback]
|
||||||
|
CreateMissing --> Origin
|
||||||
|
Origin --> Distance[Attach squared XZ distance]
|
||||||
|
RemoveAll --> Next[Next tile]
|
||||||
|
RemoveMissing --> Next
|
||||||
|
Distance --> Next
|
||||||
|
Next --> Sort[Sort creates nearest-first]
|
||||||
|
Sort --> Result[Return fresh arrays]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle/state
|
||||||
|
|
||||||
|
The planner is stateless. Each loader queue rebuild supplies current read models
|
||||||
|
and receives new arrays. The loader adopts and drains those arrays until another
|
||||||
|
rebuild replaces them. Planner destruction requires no cleanup or cancellation.
|
||||||
|
|
||||||
|
## Main sequence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Loader as StreamingWorldLoader
|
||||||
|
participant Planner as TerrainChunkGeometryQueuePlanner
|
||||||
|
participant Budget as RenderBudgetScheduler
|
||||||
|
participant Render as Existing terrain finalization
|
||||||
|
Loader->>Planner: plan(tile states, typed focus)
|
||||||
|
Planner-->>Loader: fresh create/remove arrays
|
||||||
|
Loader->>Budget: request shared chunk_geometry permit
|
||||||
|
Budget-->>Loader: permit
|
||||||
|
Loader->>Render: pop_back removal first
|
||||||
|
Loader->>Budget: request shared chunk_geometry permit
|
||||||
|
Budget-->>Loader: permit
|
||||||
|
Loader->>Render: pop_front nearest creation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ownership, threading and resources
|
||||||
|
|
||||||
|
- The planner owns only call-local arrays and dictionaries until return.
|
||||||
|
- The loader owns adopted queue storage, drain ordering, tile/chunk state and all
|
||||||
|
Mesh/Node/material/RID side effects.
|
||||||
|
- Calls and drains currently run on the renderer main thread. The planner has no
|
||||||
|
lock, task, retained resource or concurrent mutation.
|
||||||
|
- Removal and creation continue to share the existing `chunk_geometry` budget lane.
|
||||||
|
|
||||||
|
## Errors, cancellation and recovery
|
||||||
|
|
||||||
|
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Null/wrong focus script | Identity guard | Return empty request arrays | Contract verifier | Supply typed focus and rebuild |
|
||||||
|
| Sparse/out-of-range chunk data | Origin resolver | Use tile fallback origin | Synthetic fixture | Normal ADT completion restores precise origin |
|
||||||
|
| Tile LOD becomes desired | Desired tile LOD check | Remove every current chunk; no chunk creates | Queue metrics | Existing tile-LOD create path takes over |
|
||||||
|
| Queue rebuild before drain | Loader lifecycle | Replace old desired-operation arrays | Existing queue metrics | Latest state wins |
|
||||||
|
| Shutdown/map reset | Loader lifecycle | Loader clears queues; planner has nothing to cancel | Shutdown verifier | New session replans |
|
||||||
|
|
||||||
|
## Configuration and capabilities
|
||||||
|
|
||||||
|
The planner introduces no settings. It consumes already-calculated desired LOD
|
||||||
|
state. Operation limits, profile defaults and removal-first drain order remain
|
||||||
|
loader/scheduler configuration.
|
||||||
|
|
||||||
|
## Persistence, cache and migration
|
||||||
|
|
||||||
|
No serialization, cache entry or format changes. Baked terrain, control-splat
|
||||||
|
and splat caches require no rebuild or migration.
|
||||||
|
|
||||||
|
## Diagnostics and observability
|
||||||
|
|
||||||
|
The planner emits no logs. Existing queue metrics observe the adopted arrays.
|
||||||
|
The contract verifier reports semantic cases and bounded multi-tile timing.
|
||||||
|
Asset-backed p95/p99 remains milestone acceptance work.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `verify_terrain_chunk_geometry_queue_planner.gd`: missing/mismatched/unchanged
|
||||||
|
chunks, tile-LOD removal, sparse origin fallback, XZ-only distance, stable
|
||||||
|
removals, nearest creates, invalid focus, loader boundary and bounded timing.
|
||||||
|
- Existing LOD planner, terrain cache, facade, internal-access, manifest,
|
||||||
|
shutdown, scheduler, streaming and coordinate regressions remain required.
|
||||||
|
- Fidelity evidence is exact branch/request/order preservation only. No new
|
||||||
|
build-12340 visual parity claim is made.
|
||||||
|
|
||||||
|
## Extension points
|
||||||
|
|
||||||
|
- Extract tile-LOD operation planning as its own bounded contract if justified.
|
||||||
|
- Add explicit typed tile-state adapters only when more than one consumer exists.
|
||||||
|
|
||||||
|
## Capability status
|
||||||
|
|
||||||
|
| Capability | Status | Evidence | Gap/next step |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Pure chunk create/remove planning | Implemented extraction | Asset-free semantic/source/timing verifier | Asset-backed p95/p99 pending |
|
||||||
|
| Queue execution/resource ownership | Remains in loader | Drain source boundary and scheduler regression | Further terrain decomposition required |
|
||||||
|
|
||||||
|
## Known gaps and risks
|
||||||
|
|
||||||
|
- Raw tile/chunk dictionaries remain a transitional ADT adapter boundary.
|
||||||
|
- The planner performs a second linear tile-state pass beside tile-LOD planning;
|
||||||
|
bounded synthetic timing is green, but asset-backed frame metrics are pending.
|
||||||
|
- Equal-distance create ordering retains comparator behavior without a tie-breaker.
|
||||||
|
- Terrain geometry finalization and state mutation remain monolithic.
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Path | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `src/render/terrain/terrain_chunk_geometry_queue_planner.gd` | Pure request calculation and nearest-first creation order |
|
||||||
|
| `src/scenes/streaming/streaming_world_loader.gd` | Queue adoption, tile-LOD planning, budgets and render side effects |
|
||||||
|
| `src/tools/verify_terrain_chunk_geometry_queue_planner.gd` | Semantic, boundary and timing regression |
|
||||||
|
|
||||||
|
## Related decisions and references
|
||||||
|
|
||||||
|
- [`terrain-chunk-lod-planner.md`](terrain-chunk-lod-planner.md)
|
||||||
|
- [`world-renderer.md`](world-renderer.md)
|
||||||
|
- [`../../RENDER.md`](../../RENDER.md)
|
||||||
|
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
# 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)
|
||||||
@@ -5,9 +5,9 @@
|
|||||||
| Field | Value |
|
| Field | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Status | Partial |
|
| Status | Partial |
|
||||||
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; `M03-RND-TERRAIN-CACHE-SERVICE-001` |
|
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain cache/LOD/geometry-queue packages |
|
||||||
| Owners | Renderer workstream / milestone integrator |
|
| Owners | Renderer workstream / milestone integrator |
|
||||||
| Last verified | Worktree `work/sindo-main-codex/m03-terrain-quality-cache`, 2026-07-16 |
|
| Last verified | Worktree `work/sindo-main-codex/m03-terrain-chunk-queue`, 2026-07-16 |
|
||||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
@@ -42,6 +42,10 @@ flowchart LR
|
|||||||
Cache[Baked terrain/M2/WMO caches] --> Loader
|
Cache[Baked terrain/M2/WMO caches] --> Loader
|
||||||
Loader --> TerrainCache[TerrainQualityMeshCache]
|
Loader --> TerrainCache[TerrainQualityMeshCache]
|
||||||
TerrainCache --> Loader
|
TerrainCache --> Loader
|
||||||
|
Loader --> ChunkLod[TerrainChunkLodPlanner]
|
||||||
|
ChunkLod --> Loader
|
||||||
|
Loader --> ChunkQueue[TerrainChunkGeometryQueuePlanner]
|
||||||
|
ChunkQueue --> Loader
|
||||||
Native --> Parsed[Parsed tile/model data]
|
Native --> Parsed[Parsed tile/model data]
|
||||||
Parsed --> Loader
|
Parsed --> Loader
|
||||||
Loader --> Scene[SceneTree nodes]
|
Loader --> Scene[SceneTree nodes]
|
||||||
@@ -106,6 +110,8 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
|||||||
| `runtime_stats_enabled` | Exported property | Enables periodic performance snapshot | Runtime mutable | Logging overhead only |
|
| `runtime_stats_enabled` | Exported property | Enables periodic performance snapshot | Runtime mutable | Logging overhead only |
|
||||||
| `hitch_profiler_enabled` | Exported property | Enables named hitch sections | Runtime mutable | Logging overhead only |
|
| `hitch_profiler_enabled` | Exported property | Enables named hitch sections | Runtime mutable | Logging overhead only |
|
||||||
| `TerrainQualityMeshCache.store/restore/clear` | Internal terrain service | Owns admitted full-quality revisit Mesh references and LRU | Renderer thread/map session | Rejected/missed entry leaves loader fallback unchanged |
|
| `TerrainQualityMeshCache.store/restore/clear` | Internal terrain service | Owns admitted full-quality revisit Mesh references and LRU | Renderer thread/map session | Rejected/missed entry leaves loader fallback unchanged |
|
||||||
|
| `TerrainChunkLodPlanner.plan` | Internal pure terrain query | Maps populated parsed chunk indices to desired LOD 0/1/2 | Synchronous per target refresh | Invalid/disabled input returns empty for existing tile-LOD fallback |
|
||||||
|
| `TerrainChunkGeometryQueuePlanner.plan` | Internal pure terrain query | Produces chunk create/remove requests from current and desired state | Synchronous per queue rebuild | Invalid focus returns empty arrays |
|
||||||
|
|
||||||
Публичным contract не считаются `StreamingWorldLoader` methods/properties,
|
Публичным contract не считаются `StreamingWorldLoader` methods/properties,
|
||||||
внутренние dictionaries, queues, job records и generated node names. Scene-owned
|
внутренние dictionaries, queues, job records и generated node names. Scene-owned
|
||||||
@@ -128,6 +134,8 @@ loader configuration remains transitional composition data, not a caller API.
|
|||||||
| Output | `StreamingTargetPlan` | `StreamingTargetPlanner` | Streamer queue/state apply | Immutable ephemeral value | One refresh |
|
| Output | `StreamingTargetPlan` | `StreamingTargetPlanner` | Streamer queue/state apply | Immutable ephemeral value | One refresh |
|
||||||
| Internal control | Per-frame lane limits and boolean permits | Streamer configuration / `RenderBudgetScheduler` | Ordered streamer drains | Scheduler-owned counters | Main thread/frame |
|
| Internal control | Per-frame lane limits and boolean permits | Streamer configuration / `RenderBudgetScheduler` | Ordered streamer drains | Scheduler-owned counters | Main thread/frame |
|
||||||
| Internal cache | Full-quality terrain Mesh/source | Loader terrain upgrade / `TerrainQualityMeshCache` | Revisited tile state | Cache-retained Mesh reference | Map session until eviction/reset |
|
| Internal cache | Full-quality terrain Mesh/source | Loader terrain upgrade / `TerrainQualityMeshCache` | Revisited tile state | Cache-retained Mesh reference | Map session until eviction/reset |
|
||||||
|
| Internal plan | Parsed chunks, typed focus and immutable chunk-LOD policy | Loader / `TerrainChunkLodPlanner` | Loader desired state | Fresh dictionary; no retained resources | One target refresh |
|
||||||
|
| Internal plan | Current/desired chunk state and typed focus | Loader / `TerrainChunkGeometryQueuePlanner` | Loader chunk queues | Fresh request arrays; no retained resources | One queue rebuild |
|
||||||
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
|
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
|
||||||
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
|
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
|
||||||
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
|
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
|
||||||
@@ -167,6 +175,10 @@ flowchart TD
|
|||||||
Parse --> R[Result queues]
|
Parse --> R[Result queues]
|
||||||
R --> TerrainCache[TerrainQualityMeshCache store]
|
R --> TerrainCache[TerrainQualityMeshCache store]
|
||||||
TerrainCache --> Revisit[Revisited tile state restore]
|
TerrainCache --> Revisit[Revisited tile state restore]
|
||||||
|
R --> ChunkLod[TerrainChunkLodPlanner]
|
||||||
|
ChunkLod --> DesiredChunkLod[Desired chunk LOD state]
|
||||||
|
DesiredChunkLod --> ChunkQueue[TerrainChunkGeometryQueuePlanner]
|
||||||
|
ChunkQueue --> B
|
||||||
R --> B[RenderBudgetScheduler permits]
|
R --> B[RenderBudgetScheduler permits]
|
||||||
B --> Terrain[Terrain attach/upgrade]
|
B --> Terrain[Terrain attach/upgrade]
|
||||||
B --> M2[M2 group/MultiMesh attach]
|
B --> M2[M2 group/MultiMesh attach]
|
||||||
@@ -272,6 +284,12 @@ sequenceDiagram
|
|||||||
- `TerrainQualityMeshCache` owns only full-quality revisit Mesh references and
|
- `TerrainQualityMeshCache` owns only full-quality revisit Mesh references and
|
||||||
LRU keys. The loader retains terrain tasks/results, tile state, source choice,
|
LRU keys. The loader retains terrain tasks/results, tile state, source choice,
|
||||||
cache versions and every material/Node/RID finalization side effect.
|
cache versions and every material/Node/RID finalization side effect.
|
||||||
|
- `TerrainChunkLodPlanner` is stateless and owns only call-local horizontal
|
||||||
|
distance calculations. The loader retains parsed chunks, desired state,
|
||||||
|
queues, budgets and all Mesh/Node/RID application side effects.
|
||||||
|
- `TerrainChunkGeometryQueuePlanner` is stateless and owns only call-local
|
||||||
|
create/remove request calculation and sorting. The loader adopts the fresh
|
||||||
|
arrays and retains removal-first drains, budgets and render side effects.
|
||||||
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
|
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
|
||||||
the facade never returns Mesh, Node, tile-state or queue references.
|
the facade never returns Mesh, Node, tile-state or queue references.
|
||||||
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
|
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
|
||||||
@@ -348,6 +366,12 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
- Unit/contract tests: streaming-focus contract/wiring, material mapping, unique-ID dedupe, placement probes, baseline manifest, five-point coordinate calibration, synthetic perceptual checkpoint diff, camera-pose grid plan.
|
- Unit/contract tests: streaming-focus contract/wiring, material mapping, unique-ID dedupe, placement probes, baseline manifest, five-point coordinate calibration, synthetic perceptual checkpoint diff, camera-pose grid plan.
|
||||||
- Pure planner contract: center/corner/catalog/clamp/editor cases plus bounded
|
- Pure planner contract: center/corner/catalog/clamp/editor cases plus bounded
|
||||||
High-like iteration timing without loading a world scene.
|
High-like iteration timing without loading a world scene.
|
||||||
|
- Terrain chunk LOD contract: horizontal chunk-center distance, inclusive
|
||||||
|
thresholds, sparse indices, disabled/raw-custom policy, loader delegation and
|
||||||
|
bounded 250-by-256 planning without loading a world scene.
|
||||||
|
- Terrain chunk geometry queue contract: current/desired comparison, tile-LOD
|
||||||
|
takeover, sparse origin fallback, stable removals, nearest creates, loader
|
||||||
|
drain boundary and bounded multi-tile timing without a world scene.
|
||||||
- Budget scheduler contract: exact lane exhaustion, shared chunk removal/create
|
- Budget scheduler contract: exact lane exhaustion, shared chunk removal/create
|
||||||
priority, independent lanes, frame reset, invalid limits, terminal cancellation,
|
priority, independent lanes, frame reset, invalid limits, terminal cancellation,
|
||||||
dependency boundaries and bounded permit timing without loading a world scene.
|
dependency boundaries and bounded permit timing without loading a world scene.
|
||||||
@@ -387,6 +411,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| Environment snapshot input | Implemented boundary / Partial fidelity | Immutable time contract, facade/controller delegation and checkpoint migration | Weather/indoor state and paired zone-transition fidelity remain |
|
| Environment snapshot input | Implemented boundary / Partial fidelity | Immutable time contract, facade/controller delegation and checkpoint migration | Weather/indoor state and paired zone-transition fidelity remain |
|
||||||
| Entity presentation commands | Implemented boundary / Prototype visuals | Versioned typed snapshot plus present/remove lifecycle and runtime service composition | Display/equipment/animation/effects and async scale path remain |
|
| Entity presentation commands | Implemented boundary / Prototype visuals | Versioned typed snapshot plus present/remove lifecycle and runtime service composition | Display/equipment/animation/effects and async scale path remain |
|
||||||
| Terrain quality Mesh cache service | Implemented extraction | Scene-free admission/LRU/clear contract and loader delegation | Remaining terrain build/state/finalization is monolithic |
|
| Terrain quality Mesh cache service | Implemented extraction | Scene-free admission/LRU/clear contract and loader delegation | Remaining terrain build/state/finalization is monolithic |
|
||||||
|
| Terrain chunk LOD planner | Implemented extraction | Scene-free formula/dependency/timing contract and loader delegation | Queue/state/finalization and asset-backed p95/p99 remain pending |
|
||||||
|
| Terrain chunk geometry queue planner | Implemented extraction | Scene-free request/order/dependency/timing contract and loader delegation | Queue drains/state/finalization and asset-backed p95/p99 remain pending |
|
||||||
| Pure streaming target planner | Implemented | `M03-RND-STREAMING-PLANNER-001` asset-free contract/performance regression | Queue scheduling and loaded-tile LOD application remain in streamer |
|
| Pure streaming target planner | Implemented | `M03-RND-STREAMING-PLANNER-001` asset-free contract/performance regression | Queue scheduling and loaded-tile LOD application remain in streamer |
|
||||||
| Bounded render budget scheduler | Implemented | `M03-RND-SCHEDULER-001` asset-free permit/cancellation/performance regression | Queue storage, worker concurrency and operation execution remain in streamer/services |
|
| Bounded render budget scheduler | Implemented | `M03-RND-SCHEDULER-001` asset-free permit/cancellation/performance regression | Queue storage, worker concurrency and operation execution remain in streamer/services |
|
||||||
| Gameplay/editor/tool internal-access boundary | Implemented | Access gate scans gameplay, EditorPlugin packages and terrain probe against actual private streamer declarations | Add future renderer diagnostic tools explicitly to the protected list |
|
| Gameplay/editor/tool internal-access boundary | Implemented | Access gate scans gameplay, EditorPlugin packages and terrain probe against actual private streamer declarations | Add future renderer diagnostic tools explicitly to the protected list |
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
class_name TerrainChunkGeometryQueuePlanner
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Pure desired-operation planner for terrain chunk geometry queues.
|
||||||
|
|
||||||
|
const GODOT_WORLD_POSITION_SCRIPT := preload(
|
||||||
|
"res://src/domain/coordinates/godot_world_position.gd"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns fresh nearest-first create and stable-order remove request arrays.
|
||||||
|
## The caller retains queue storage, execution order and every render side effect.
|
||||||
|
func plan(tile_states: Dictionary, focus_position: RefCounted) -> Dictionary:
|
||||||
|
var chunk_create_requests: Array = []
|
||||||
|
var chunk_remove_requests: Array = []
|
||||||
|
if (
|
||||||
|
focus_position == null
|
||||||
|
or focus_position.get_script() != GODOT_WORLD_POSITION_SCRIPT
|
||||||
|
):
|
||||||
|
return _create_plan(chunk_create_requests, chunk_remove_requests)
|
||||||
|
|
||||||
|
var focus_x_units := float(focus_position.get("x_units"))
|
||||||
|
var focus_z_units := float(focus_position.get("z_units"))
|
||||||
|
for tile_key_variant in tile_states.keys():
|
||||||
|
var tile_key := String(tile_key_variant)
|
||||||
|
var tile_state: Dictionary = tile_states[tile_key_variant]
|
||||||
|
var current_chunks: Dictionary = tile_state["chunks"]
|
||||||
|
var desired_lods: Dictionary = tile_state["desired_lods"]
|
||||||
|
var desired_tile_lod := int(tile_state.get("desired_tile_lod", -1))
|
||||||
|
|
||||||
|
if desired_tile_lod >= 0:
|
||||||
|
for chunk_id in current_chunks.keys():
|
||||||
|
chunk_remove_requests.append({"tile": tile_key, "chunk": chunk_id})
|
||||||
|
continue
|
||||||
|
|
||||||
|
for chunk_id in current_chunks.keys():
|
||||||
|
if not desired_lods.has(chunk_id):
|
||||||
|
chunk_remove_requests.append({"tile": tile_key, "chunk": chunk_id})
|
||||||
|
continue
|
||||||
|
if int(current_chunks[chunk_id]["lod"]) != int(desired_lods[chunk_id]):
|
||||||
|
chunk_create_requests.append(_create_chunk_request(
|
||||||
|
tile_key,
|
||||||
|
chunk_id,
|
||||||
|
int(desired_lods[chunk_id]),
|
||||||
|
_resolve_chunk_origin(tile_state, int(chunk_id)),
|
||||||
|
focus_x_units,
|
||||||
|
focus_z_units
|
||||||
|
))
|
||||||
|
|
||||||
|
for chunk_id in desired_lods.keys():
|
||||||
|
if current_chunks.has(chunk_id):
|
||||||
|
continue
|
||||||
|
chunk_create_requests.append(_create_chunk_request(
|
||||||
|
tile_key,
|
||||||
|
chunk_id,
|
||||||
|
int(desired_lods[chunk_id]),
|
||||||
|
_resolve_chunk_origin(tile_state, int(chunk_id)),
|
||||||
|
focus_x_units,
|
||||||
|
focus_z_units
|
||||||
|
))
|
||||||
|
|
||||||
|
chunk_create_requests.sort_custom(func(
|
||||||
|
left_request: Dictionary,
|
||||||
|
right_request: Dictionary
|
||||||
|
) -> bool:
|
||||||
|
return float(left_request["dist_sq"]) < float(right_request["dist_sq"])
|
||||||
|
)
|
||||||
|
return _create_plan(chunk_create_requests, chunk_remove_requests)
|
||||||
|
|
||||||
|
|
||||||
|
func _resolve_chunk_origin(tile_state: Dictionary, chunk_id: int) -> Vector3:
|
||||||
|
var chunks: Array = tile_state["data"].get("chunks", [])
|
||||||
|
if chunk_id >= 0 and chunk_id < chunks.size():
|
||||||
|
var chunk: Dictionary = chunks[chunk_id]
|
||||||
|
if not chunk.is_empty():
|
||||||
|
return chunk.get("origin", Vector3.ZERO)
|
||||||
|
return tile_state.get("origin", Vector3.ZERO)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_chunk_request(
|
||||||
|
tile_key: String,
|
||||||
|
chunk_id: Variant,
|
||||||
|
desired_lod: int,
|
||||||
|
chunk_origin: Vector3,
|
||||||
|
focus_x_units: float,
|
||||||
|
focus_z_units: float
|
||||||
|
) -> Dictionary:
|
||||||
|
var delta_x_units := chunk_origin.x - focus_x_units
|
||||||
|
var delta_z_units := chunk_origin.z - focus_z_units
|
||||||
|
return {
|
||||||
|
"tile": tile_key,
|
||||||
|
"chunk": chunk_id,
|
||||||
|
"lod": desired_lod,
|
||||||
|
"dist_sq": delta_x_units * delta_x_units + delta_z_units * delta_z_units,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _create_plan(chunk_create_requests: Array, chunk_remove_requests: Array) -> Dictionary:
|
||||||
|
return {
|
||||||
|
"create_requests": chunk_create_requests,
|
||||||
|
"remove_requests": chunk_remove_requests,
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ct1ar5inci34u
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
class_name TerrainChunkLodPlanner
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Pure linear desired-state planner for parsed ADT chunk records.
|
||||||
|
|
||||||
|
const POLICY_SCRIPT := preload("res://src/render/terrain/terrain_chunk_lod_policy.gd")
|
||||||
|
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||||
|
|
||||||
|
|
||||||
|
## Maps source chunk indices to LOD 0/1/2 using horizontal distance from the
|
||||||
|
## typed focus to each chunk center. Empty/out-of-range chunks remain absent.
|
||||||
|
func plan(
|
||||||
|
chunks: Array,
|
||||||
|
focus_position: RefCounted,
|
||||||
|
policy: RefCounted
|
||||||
|
) -> Dictionary:
|
||||||
|
if (
|
||||||
|
focus_position == null
|
||||||
|
or focus_position.get_script() != GODOT_WORLD_POSITION_SCRIPT
|
||||||
|
or policy == null
|
||||||
|
or policy.get_script() != POLICY_SCRIPT
|
||||||
|
or not bool(policy.get("is_enabled"))
|
||||||
|
):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
var chunk_size_units := float(policy.get("chunk_size_units"))
|
||||||
|
var lod0_radius_squared := (float(policy.get("lod0_radius_chunks")) * chunk_size_units) ** 2
|
||||||
|
var lod1_radius_squared := (float(policy.get("lod1_radius_chunks")) * chunk_size_units) ** 2
|
||||||
|
var lod2_radius_squared := (float(policy.get("lod2_radius_chunks")) * chunk_size_units) ** 2
|
||||||
|
var focus_x_units := float(focus_position.get("x_units"))
|
||||||
|
var focus_z_units := float(focus_position.get("z_units"))
|
||||||
|
var desired_lods: Dictionary = {}
|
||||||
|
|
||||||
|
for chunk_id in range(chunks.size()):
|
||||||
|
var chunk: Dictionary = chunks[chunk_id]
|
||||||
|
if chunk.is_empty():
|
||||||
|
continue
|
||||||
|
var origin: Vector3 = chunk.get("origin", Vector3.ZERO)
|
||||||
|
var center_x_units := origin.x + chunk_size_units * 0.5
|
||||||
|
var center_z_units := origin.z + chunk_size_units * 0.5
|
||||||
|
var delta_x_units := center_x_units - focus_x_units
|
||||||
|
var delta_z_units := center_z_units - focus_z_units
|
||||||
|
var distance_squared_units := (
|
||||||
|
delta_x_units * delta_x_units + delta_z_units * delta_z_units
|
||||||
|
)
|
||||||
|
if distance_squared_units <= lod0_radius_squared:
|
||||||
|
desired_lods[chunk_id] = 0
|
||||||
|
elif distance_squared_units <= lod1_radius_squared:
|
||||||
|
desired_lods[chunk_id] = 1
|
||||||
|
elif distance_squared_units <= lod2_radius_squared:
|
||||||
|
desired_lods[chunk_id] = 2
|
||||||
|
|
||||||
|
return desired_lods
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://x8simqkfctqw
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
class_name TerrainChunkLodPolicy
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Immutable snapshot of the streamer's chunk-terrain LOD configuration.
|
||||||
|
|
||||||
|
var is_enabled: bool:
|
||||||
|
get:
|
||||||
|
return _is_enabled
|
||||||
|
|
||||||
|
var lod0_radius_chunks: int:
|
||||||
|
get:
|
||||||
|
return _lod0_radius_chunks
|
||||||
|
|
||||||
|
var lod1_radius_chunks: int:
|
||||||
|
get:
|
||||||
|
return _lod1_radius_chunks
|
||||||
|
|
||||||
|
var lod2_radius_chunks: int:
|
||||||
|
get:
|
||||||
|
return _lod2_radius_chunks
|
||||||
|
|
||||||
|
var chunk_size_units: float:
|
||||||
|
get:
|
||||||
|
return _chunk_size_units
|
||||||
|
|
||||||
|
var _is_enabled: bool
|
||||||
|
var _lod0_radius_chunks: int
|
||||||
|
var _lod1_radius_chunks: int
|
||||||
|
var _lod2_radius_chunks: int
|
||||||
|
var _chunk_size_units: float
|
||||||
|
|
||||||
|
|
||||||
|
## Captures one renderer configuration snapshot for a deterministic plan.
|
||||||
|
func _init(
|
||||||
|
is_enabled_value: bool,
|
||||||
|
lod0_radius_chunks_value: int,
|
||||||
|
lod1_radius_chunks_value: int,
|
||||||
|
lod2_radius_chunks_value: int,
|
||||||
|
chunk_size_units_value: float
|
||||||
|
) -> void:
|
||||||
|
_is_enabled = is_enabled_value
|
||||||
|
_lod0_radius_chunks = lod0_radius_chunks_value
|
||||||
|
_lod1_radius_chunks = lod1_radius_chunks_value
|
||||||
|
_lod2_radius_chunks = lod2_radius_chunks_value
|
||||||
|
_chunk_size_units = chunk_size_units_value
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dmwi83dp52pa8
|
||||||
@@ -19,6 +19,11 @@ const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_ti
|
|||||||
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")
|
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")
|
||||||
const RENDERED_GROUND_SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd")
|
const RENDERED_GROUND_SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd")
|
||||||
const TERRAIN_QUALITY_MESH_CACHE_SCRIPT := preload("res://src/render/terrain/terrain_quality_mesh_cache.gd")
|
const TERRAIN_QUALITY_MESH_CACHE_SCRIPT := preload("res://src/render/terrain/terrain_quality_mesh_cache.gd")
|
||||||
|
const TERRAIN_CHUNK_LOD_POLICY_SCRIPT := preload("res://src/render/terrain/terrain_chunk_lod_policy.gd")
|
||||||
|
const TERRAIN_CHUNK_LOD_PLANNER_SCRIPT := preload("res://src/render/terrain/terrain_chunk_lod_planner.gd")
|
||||||
|
const TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER_SCRIPT := preload(
|
||||||
|
"res://src/render/terrain/terrain_chunk_geometry_queue_planner.gd"
|
||||||
|
)
|
||||||
const STREAMING_TARGET_PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
|
const STREAMING_TARGET_PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
|
||||||
const STREAMING_TARGET_POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
|
const STREAMING_TARGET_POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
|
||||||
const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd")
|
const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd")
|
||||||
@@ -181,6 +186,10 @@ const QUALITY_HIGH := "High"
|
|||||||
|
|
||||||
var _builder
|
var _builder
|
||||||
var _streaming_target_planner := STREAMING_TARGET_PLANNER_SCRIPT.new()
|
var _streaming_target_planner := STREAMING_TARGET_PLANNER_SCRIPT.new()
|
||||||
|
var _terrain_chunk_lod_planner := TERRAIN_CHUNK_LOD_PLANNER_SCRIPT.new()
|
||||||
|
var _terrain_chunk_geometry_operation_planner := (
|
||||||
|
TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER_SCRIPT.new()
|
||||||
|
)
|
||||||
var _map_dir := ""
|
var _map_dir := ""
|
||||||
var _abs_map_dir := ""
|
var _abs_map_dir := ""
|
||||||
var _wdt_info: Dictionary = {}
|
var _wdt_info: Dictionary = {}
|
||||||
@@ -797,6 +806,8 @@ func _refresh_streaming_targets_at(focus_pos: Vector3, force: bool) -> void:
|
|||||||
|
|
||||||
func _apply_streaming_target(wanted_tiles: Dictionary, retained_tiles: Dictionary, focus_pos: Vector3) -> void:
|
func _apply_streaming_target(wanted_tiles: Dictionary, retained_tiles: Dictionary, focus_pos: Vector3) -> void:
|
||||||
_last_focus_pos = focus_pos
|
_last_focus_pos = focus_pos
|
||||||
|
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(focus_pos.x, focus_pos.y, focus_pos.z)
|
||||||
|
var terrain_chunk_lod_policy = _create_terrain_chunk_lod_policy()
|
||||||
|
|
||||||
var filtered_load_queue: Array = []
|
var filtered_load_queue: Array = []
|
||||||
for request in _tile_load_queue:
|
for request in _tile_load_queue:
|
||||||
@@ -831,7 +842,7 @@ func _apply_streaming_target(wanted_tiles: Dictionary, retained_tiles: Dictionar
|
|||||||
state["wanted"] = wanted
|
state["wanted"] = wanted
|
||||||
state["retained"] = retained
|
state["retained"] = retained
|
||||||
if wanted:
|
if wanted:
|
||||||
state["desired_lods"] = _compute_desired_lods(state, focus_pos)
|
state["desired_lods"] = _compute_desired_lods(state, typed_focus_position, terrain_chunk_lod_policy)
|
||||||
state["desired_tile_lod"] = -1 if not state["desired_lods"].is_empty() else _compute_desired_tile_lod(state, focus_pos)
|
state["desired_tile_lod"] = -1 if not state["desired_lods"].is_empty() else _compute_desired_tile_lod(state, focus_pos)
|
||||||
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos)
|
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos)
|
||||||
elif retained:
|
elif retained:
|
||||||
@@ -852,34 +863,29 @@ func _apply_streaming_target(wanted_tiles: Dictionary, retained_tiles: Dictionar
|
|||||||
_rebuild_chunk_queues()
|
_rebuild_chunk_queues()
|
||||||
|
|
||||||
|
|
||||||
func _chunk_origin_for(state: Dictionary, chunk_id: int) -> Vector3:
|
|
||||||
var chunks: Array = state["data"].get("chunks", [])
|
|
||||||
if chunk_id >= 0 and chunk_id < chunks.size():
|
|
||||||
var c: Dictionary = chunks[chunk_id]
|
|
||||||
if not c.is_empty():
|
|
||||||
return c.get("origin", Vector3.ZERO)
|
|
||||||
return state.get("origin", Vector3.ZERO)
|
|
||||||
|
|
||||||
|
|
||||||
func _rebuild_chunk_queues() -> void:
|
func _rebuild_chunk_queues() -> void:
|
||||||
_chunk_remove_queue.clear()
|
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(
|
||||||
_chunk_create_queue.clear()
|
_last_focus_pos.x,
|
||||||
|
_last_focus_pos.y,
|
||||||
|
_last_focus_pos.z
|
||||||
|
)
|
||||||
|
var chunk_queue_plan: Dictionary = _terrain_chunk_geometry_operation_planner.plan(
|
||||||
|
_tile_states,
|
||||||
|
typed_focus_position
|
||||||
|
)
|
||||||
|
_chunk_remove_queue = chunk_queue_plan.get("remove_requests", [])
|
||||||
|
_chunk_create_queue = chunk_queue_plan.get("create_requests", [])
|
||||||
_tile_lod_remove_queue.clear()
|
_tile_lod_remove_queue.clear()
|
||||||
_tile_lod_create_queue.clear()
|
_tile_lod_create_queue.clear()
|
||||||
|
|
||||||
for key in _tile_states.keys():
|
for key in _tile_states.keys():
|
||||||
var state: Dictionary = _tile_states[key]
|
var state: Dictionary = _tile_states[key]
|
||||||
var current_chunks: Dictionary = state["chunks"]
|
|
||||||
var desired_lods: Dictionary = state["desired_lods"]
|
|
||||||
var current_tile_lod: int = int(state.get("tile_lod", -1))
|
var current_tile_lod: int = int(state.get("tile_lod", -1))
|
||||||
var desired_tile_lod: int = int(state.get("desired_tile_lod", -1))
|
var desired_tile_lod: int = int(state.get("desired_tile_lod", -1))
|
||||||
var current_tile_lod_visible: bool = bool(state.get("tile_lod_visible", false))
|
var current_tile_lod_visible: bool = bool(state.get("tile_lod_visible", false))
|
||||||
var desired_tile_lod_visible: bool = bool(state.get("desired_tile_lod_visible", false))
|
var desired_tile_lod_visible: bool = bool(state.get("desired_tile_lod_visible", false))
|
||||||
|
|
||||||
if desired_tile_lod >= 0:
|
if desired_tile_lod >= 0:
|
||||||
for chunk_id in current_chunks.keys():
|
|
||||||
_chunk_remove_queue.append({"tile": key, "chunk": chunk_id})
|
|
||||||
|
|
||||||
if current_tile_lod != desired_tile_lod or current_tile_lod_visible != desired_tile_lod_visible:
|
if current_tile_lod != desired_tile_lod or current_tile_lod_visible != desired_tile_lod_visible:
|
||||||
_tile_lod_create_queue.append({
|
_tile_lod_create_queue.append({
|
||||||
"tile": key,
|
"tile": key,
|
||||||
@@ -900,37 +906,7 @@ func _rebuild_chunk_queues() -> void:
|
|||||||
if current_tile_lod >= 0 and _is_tile_chunk_set_ready(state):
|
if current_tile_lod >= 0 and _is_tile_chunk_set_ready(state):
|
||||||
_tile_lod_remove_queue.append({"tile": key})
|
_tile_lod_remove_queue.append({"tile": key})
|
||||||
|
|
||||||
for chunk_id in current_chunks.keys():
|
# Sort tile LOD creates after fully rebuilding; chunk creates arrive nearest-first.
|
||||||
if not desired_lods.has(chunk_id):
|
|
||||||
_chunk_remove_queue.append({"tile": key, "chunk": chunk_id})
|
|
||||||
continue
|
|
||||||
if current_chunks[chunk_id]["lod"] != desired_lods[chunk_id]:
|
|
||||||
var lod_change_origin: Vector3 = _chunk_origin_for(state, chunk_id)
|
|
||||||
var dx2: float = lod_change_origin.x - _last_focus_pos.x
|
|
||||||
var dz2: float = lod_change_origin.z - _last_focus_pos.z
|
|
||||||
_chunk_create_queue.append({
|
|
||||||
"tile": key,
|
|
||||||
"chunk": chunk_id,
|
|
||||||
"lod": desired_lods[chunk_id],
|
|
||||||
"dist_sq": dx2 * dx2 + dz2 * dz2,
|
|
||||||
})
|
|
||||||
|
|
||||||
for chunk_id in desired_lods.keys():
|
|
||||||
if current_chunks.has(chunk_id):
|
|
||||||
continue
|
|
||||||
var chunk_origin: Vector3 = _chunk_origin_for(state, chunk_id)
|
|
||||||
var dx: float = chunk_origin.x - _last_focus_pos.x
|
|
||||||
var dz: float = chunk_origin.z - _last_focus_pos.z
|
|
||||||
_chunk_create_queue.append({
|
|
||||||
"tile": key,
|
|
||||||
"chunk": chunk_id,
|
|
||||||
"lod": desired_lods[chunk_id],
|
|
||||||
"dist_sq": dx * dx + dz * dz,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Sort after fully rebuilding so nearest chunks are processed first
|
|
||||||
_chunk_create_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
|
||||||
return a["dist_sq"] < b["dist_sq"])
|
|
||||||
_tile_lod_create_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
_tile_lod_create_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||||
return a["dist_sq"] < b["dist_sq"])
|
return a["dist_sq"] < b["dist_sq"])
|
||||||
|
|
||||||
@@ -2492,7 +2468,16 @@ func _finalize_loaded_tile(request: Dictionary, data: Dictionary) -> void:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Use _last_focus_pos — already set correctly for both editor and game.
|
# Use _last_focus_pos — already set correctly for both editor and game.
|
||||||
state["desired_lods"] = _compute_desired_lods(state, _last_focus_pos)
|
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(
|
||||||
|
_last_focus_pos.x,
|
||||||
|
_last_focus_pos.y,
|
||||||
|
_last_focus_pos.z
|
||||||
|
)
|
||||||
|
state["desired_lods"] = _compute_desired_lods(
|
||||||
|
state,
|
||||||
|
typed_focus_position,
|
||||||
|
_create_terrain_chunk_lod_policy()
|
||||||
|
)
|
||||||
state["desired_tile_lod"] = -1 if not state["desired_lods"].is_empty() else _compute_desired_tile_lod(state, _last_focus_pos)
|
state["desired_tile_lod"] = -1 if not state["desired_lods"].is_empty() else _compute_desired_tile_lod(state, _last_focus_pos)
|
||||||
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos)
|
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos)
|
||||||
_tile_states[key] = state
|
_tile_states[key] = state
|
||||||
@@ -3045,33 +3030,23 @@ func _reset_terrain_root_children() -> void:
|
|||||||
child.free()
|
child.free()
|
||||||
|
|
||||||
|
|
||||||
func _compute_desired_lods(state: Dictionary, cam_pos: Vector3) -> Dictionary:
|
func _compute_desired_lods(
|
||||||
if not use_chunk_terrain_lods:
|
state: Dictionary,
|
||||||
return {}
|
typed_focus_position: RefCounted,
|
||||||
|
terrain_chunk_lod_policy: RefCounted
|
||||||
var desired := {}
|
) -> Dictionary:
|
||||||
var chunks: Array = state["data"].get("chunks", [])
|
var chunks: Array = state["data"].get("chunks", [])
|
||||||
|
return _terrain_chunk_lod_planner.plan(chunks, typed_focus_position, terrain_chunk_lod_policy)
|
||||||
|
|
||||||
var lod0_sq: float = (lod0_radius_chunks * CHUNK_SIZE) ** 2
|
|
||||||
var lod1_sq: float = (lod1_radius_chunks * CHUNK_SIZE) ** 2
|
|
||||||
var lod2_sq: float = (lod2_radius_chunks * CHUNK_SIZE) ** 2
|
|
||||||
|
|
||||||
for chunk_id in range(chunks.size()):
|
func _create_terrain_chunk_lod_policy() -> RefCounted:
|
||||||
var chunk: Dictionary = chunks[chunk_id]
|
return TERRAIN_CHUNK_LOD_POLICY_SCRIPT.new(
|
||||||
if chunk.is_empty():
|
use_chunk_terrain_lods,
|
||||||
continue
|
lod0_radius_chunks,
|
||||||
|
lod1_radius_chunks,
|
||||||
var origin: Vector3 = chunk.get("origin", Vector3.ZERO)
|
lod2_radius_chunks,
|
||||||
var center := origin + Vector3(CHUNK_SIZE * 0.5, 0.0, CHUNK_SIZE * 0.5)
|
CHUNK_SIZE
|
||||||
var dx := center.x - cam_pos.x
|
)
|
||||||
var dz := center.z - cam_pos.z
|
|
||||||
var dist_sq := dx * dx + dz * dz
|
|
||||||
|
|
||||||
if dist_sq <= lod0_sq: desired[chunk_id] = 0
|
|
||||||
elif dist_sq <= lod1_sq: desired[chunk_id] = 1
|
|
||||||
elif dist_sq <= lod2_sq: desired[chunk_id] = 2
|
|
||||||
|
|
||||||
return desired
|
|
||||||
|
|
||||||
|
|
||||||
func _runtime_load_tile_radius() -> int:
|
func _runtime_load_tile_radius() -> int:
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Asset-free semantic, dependency and timing regression for chunk queue planning.
|
||||||
|
|
||||||
|
const PLANNER_SCRIPT := preload(
|
||||||
|
"res://src/render/terrain/terrain_chunk_geometry_queue_planner.gd"
|
||||||
|
)
|
||||||
|
const POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||||
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var failures: Array[String] = []
|
||||||
|
_verify_create_remove_and_order(failures)
|
||||||
|
_verify_tile_lod_removes_all(failures)
|
||||||
|
_verify_fallback_origin_and_vertical_invariance(failures)
|
||||||
|
_verify_invalid_focus(failures)
|
||||||
|
_verify_loader_boundary(failures)
|
||||||
|
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||||
|
if not failures.is_empty():
|
||||||
|
for failure in failures:
|
||||||
|
push_error("TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER: %s" % failure)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
"TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER PASS cases=8 iterations=100 elapsed_ms=%.3f"
|
||||||
|
% elapsed_milliseconds
|
||||||
|
)
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_create_remove_and_order(failures: Array[String]) -> void:
|
||||||
|
var planner: RefCounted = PLANNER_SCRIPT.new()
|
||||||
|
var tile_states := {
|
||||||
|
"A": _create_tile_state(
|
||||||
|
{0: {"lod": 0}, 1: {"lod": 1}, 3: {"lod": 0}},
|
||||||
|
{0: 0, 1: 2, 2: 1},
|
||||||
|
[
|
||||||
|
{"origin": Vector3(100.0, 0.0, 0.0)},
|
||||||
|
{"origin": Vector3(30.0, 8000.0, 0.0)},
|
||||||
|
{"origin": Vector3(10.0, -8000.0, 0.0)},
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"B": _create_tile_state({5: {"lod": 0}}, {}, []),
|
||||||
|
}
|
||||||
|
var plan: Dictionary = planner.call("plan", tile_states, POSITION_SCRIPT.new(0.0, 50.0, 0.0))
|
||||||
|
var creates: Array = plan["create_requests"]
|
||||||
|
var removes: Array = plan["remove_requests"]
|
||||||
|
_expect_equal(creates.size(), 2, "missing and mismatched chunks create", failures)
|
||||||
|
_expect_equal(int(creates[0]["chunk"]), 2, "nearest missing chunk first", failures)
|
||||||
|
_expect_equal(int(creates[1]["chunk"]), 1, "farther LOD mismatch second", failures)
|
||||||
|
_expect_equal(int(creates[1]["lod"]), 2, "mismatch uses desired LOD", failures)
|
||||||
|
_expect_equal(removes.size(), 2, "undesired current chunks remove", failures)
|
||||||
|
_expect_string_equal(String(removes[0]["tile"]), "A", "stable tile removal order", failures)
|
||||||
|
_expect_equal(int(removes[0]["chunk"]), 3, "first undesired chunk", failures)
|
||||||
|
_expect_equal(int(removes[1]["chunk"]), 5, "second tile undesired chunk", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_tile_lod_removes_all(failures: Array[String]) -> void:
|
||||||
|
var tile_state := _create_tile_state({7: {"lod": 0}, 8: {"lod": 1}}, {7: 0}, [])
|
||||||
|
tile_state["desired_tile_lod"] = 3
|
||||||
|
var plan: Dictionary = PLANNER_SCRIPT.new().call(
|
||||||
|
"plan",
|
||||||
|
{"tile": tile_state},
|
||||||
|
POSITION_SCRIPT.new(0.0, 0.0, 0.0)
|
||||||
|
)
|
||||||
|
_expect_true((plan["create_requests"] as Array).is_empty(), "tile LOD suppresses chunk creates", failures)
|
||||||
|
_expect_equal((plan["remove_requests"] as Array).size(), 2, "tile LOD removes every chunk", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_fallback_origin_and_vertical_invariance(failures: Array[String]) -> void:
|
||||||
|
var tile_state := _create_tile_state({}, {9: 1}, [])
|
||||||
|
tile_state["origin"] = Vector3(4.0, 99999.0, 3.0)
|
||||||
|
var plan: Dictionary = PLANNER_SCRIPT.new().call(
|
||||||
|
"plan",
|
||||||
|
{"fallback": tile_state},
|
||||||
|
POSITION_SCRIPT.new(0.0, -99999.0, 0.0)
|
||||||
|
)
|
||||||
|
var request: Dictionary = (plan["create_requests"] as Array)[0]
|
||||||
|
_expect_near(float(request["dist_sq"]), 25.0, "fallback origin uses horizontal distance", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_invalid_focus(failures: Array[String]) -> void:
|
||||||
|
var plan: Dictionary = PLANNER_SCRIPT.new().call("plan", {}, null)
|
||||||
|
_expect_true((plan["create_requests"] as Array).is_empty(), "invalid focus create empty", failures)
|
||||||
|
_expect_true((plan["remove_requests"] as Array).is_empty(), "invalid focus remove empty", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_loader_boundary(failures: Array[String]) -> void:
|
||||||
|
var source := _read_text(LOADER_PATH, failures)
|
||||||
|
_expect_true(source.contains("TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER_SCRIPT.new()"), "loader composes planner", failures)
|
||||||
|
_expect_true(source.contains("_terrain_chunk_geometry_operation_planner.plan("), "loader delegates plan", failures)
|
||||||
|
_expect_true(not source.contains("func _chunk_origin_for("), "loader omits chunk-origin planning", failures)
|
||||||
|
_expect_true(source.contains("_remove_chunk(_chunk_remove_queue.pop_back())"), "loader retains removal-first drain", failures)
|
||||||
|
_expect_true(source.contains("_create_chunk(_chunk_create_queue.pop_front())"), "loader retains nearest-first drain", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||||
|
var tile_states: Dictionary = {}
|
||||||
|
for tile_index in range(64):
|
||||||
|
var current_chunks: Dictionary = {}
|
||||||
|
var desired_lods: Dictionary = {}
|
||||||
|
var parsed_chunks: Array = []
|
||||||
|
for chunk_index in range(16):
|
||||||
|
current_chunks[chunk_index] = {"lod": chunk_index % 3}
|
||||||
|
desired_lods[chunk_index] = (chunk_index + 1) % 3
|
||||||
|
parsed_chunks.append({
|
||||||
|
"origin": Vector3(float(tile_index * 16 + chunk_index), 0.0, float(tile_index))
|
||||||
|
})
|
||||||
|
tile_states["tile-%d" % tile_index] = _create_tile_state(
|
||||||
|
current_chunks,
|
||||||
|
desired_lods,
|
||||||
|
parsed_chunks
|
||||||
|
)
|
||||||
|
var planner: RefCounted = PLANNER_SCRIPT.new()
|
||||||
|
var focus: RefCounted = POSITION_SCRIPT.new(0.0, 0.0, 0.0)
|
||||||
|
var started_microseconds := Time.get_ticks_usec()
|
||||||
|
for _iteration in range(100):
|
||||||
|
planner.call("plan", tile_states, focus)
|
||||||
|
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||||
|
_expect_true(elapsed_milliseconds < 1000.0, "100 multi-tile plans remain bounded", failures)
|
||||||
|
return elapsed_milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
func _create_tile_state(
|
||||||
|
current_chunks: Dictionary,
|
||||||
|
desired_lods: Dictionary,
|
||||||
|
parsed_chunks: Array
|
||||||
|
) -> Dictionary:
|
||||||
|
return {
|
||||||
|
"chunks": current_chunks,
|
||||||
|
"desired_lods": desired_lods,
|
||||||
|
"desired_tile_lod": -1,
|
||||||
|
"data": {"chunks": parsed_chunks},
|
||||||
|
"origin": Vector3.ZERO,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _read_text(path: String, failures: Array[String]) -> String:
|
||||||
|
var file := FileAccess.open(path, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
failures.append("cannot open %s" % path)
|
||||||
|
return ""
|
||||||
|
return file.get_as_text()
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_equal(actual_value: int, expected_value: int, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual_value != expected_value:
|
||||||
|
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_near(actual_value: float, expected_value: float, label: String, failures: Array[String]) -> void:
|
||||||
|
if not is_equal_approx(actual_value, expected_value):
|
||||||
|
failures.append("%s expected %.3f, got %.3f" % [label, expected_value, actual_value])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_string_equal(
|
||||||
|
actual_value: String,
|
||||||
|
expected_value: String,
|
||||||
|
label: String,
|
||||||
|
failures: Array[String]
|
||||||
|
) -> void:
|
||||||
|
if actual_value != expected_value:
|
||||||
|
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
if not actual_value:
|
||||||
|
failures.append("%s expected true" % label)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dcimxvm2atvst
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Asset-free formula, dependency and bounded timing regression for chunk LOD.
|
||||||
|
|
||||||
|
const PLANNER_SCRIPT := preload("res://src/render/terrain/terrain_chunk_lod_planner.gd")
|
||||||
|
const POLICY_SCRIPT := preload("res://src/render/terrain/terrain_chunk_lod_policy.gd")
|
||||||
|
const POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||||
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var failures: Array[String] = []
|
||||||
|
_verify_formula(failures)
|
||||||
|
_verify_disabled_and_custom_policy(failures)
|
||||||
|
_verify_loader_boundary(failures)
|
||||||
|
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||||
|
if not failures.is_empty():
|
||||||
|
for failure in failures:
|
||||||
|
push_error("TERRAIN_CHUNK_LOD_PLANNER: %s" % failure)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
print("TERRAIN_CHUNK_LOD_PLANNER PASS cases=7 iterations=250 elapsed_ms=%.3f" % elapsed_milliseconds)
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_formula(failures: Array[String]) -> void:
|
||||||
|
var planner: RefCounted = PLANNER_SCRIPT.new()
|
||||||
|
var policy: RefCounted = POLICY_SCRIPT.new(true, 1, 2, 3, 10.0)
|
||||||
|
var focus: RefCounted = POSITION_SCRIPT.new(0.0, 9999.0, 0.0)
|
||||||
|
var chunks: Array = [
|
||||||
|
{"origin": Vector3(-5.0, 2000.0, -5.0)},
|
||||||
|
{"origin": Vector3(5.0, -3000.0, -5.0)},
|
||||||
|
{"origin": Vector3(5.001, 0.0, -5.0)},
|
||||||
|
{"origin": Vector3(15.0, 0.0, -5.0)},
|
||||||
|
{"origin": Vector3(20.0, 0.0, -5.0)},
|
||||||
|
{"origin": Vector3(25.0, 0.0, -5.0)},
|
||||||
|
{"origin": Vector3(25.001, 0.0, -5.0)},
|
||||||
|
{},
|
||||||
|
]
|
||||||
|
var desired: Dictionary = planner.call("plan", chunks, focus, policy)
|
||||||
|
_expect_equal(int(desired.get(0, -1)), 0, "center ignores vertical distance", failures)
|
||||||
|
_expect_equal(int(desired.get(1, -1)), 0, "lod0 inclusive boundary", failures)
|
||||||
|
_expect_equal(int(desired.get(2, -1)), 1, "just outside lod0", failures)
|
||||||
|
_expect_equal(int(desired.get(3, -1)), 1, "lod1 inclusive boundary", failures)
|
||||||
|
_expect_equal(int(desired.get(4, -1)), 2, "between lod1 and lod2", failures)
|
||||||
|
_expect_equal(int(desired.get(5, -1)), 2, "lod2 inclusive boundary", failures)
|
||||||
|
_expect_true(not desired.has(6) and not desired.has(7), "outside and sparse chunks absent", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_disabled_and_custom_policy(failures: Array[String]) -> void:
|
||||||
|
var planner: RefCounted = PLANNER_SCRIPT.new()
|
||||||
|
var focus: RefCounted = POSITION_SCRIPT.new(0.0, 0.0, 0.0)
|
||||||
|
var chunks: Array = [{"origin": Vector3(-5.0, 0.0, -5.0)}]
|
||||||
|
_expect_true((planner.call("plan", chunks, focus, POLICY_SCRIPT.new(false, 1, 2, 3, 10.0)) as Dictionary).is_empty(), "disabled policy empty", failures)
|
||||||
|
var negative_policy: RefCounted = POLICY_SCRIPT.new(true, -1, -2, -3, 10.0)
|
||||||
|
_expect_equal(int((planner.call("plan", chunks, focus, negative_policy) as Dictionary).get(0, -1)), 0, "negative radius keeps squared semantics", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_loader_boundary(failures: Array[String]) -> void:
|
||||||
|
var source := _read_text(LOADER_PATH, failures)
|
||||||
|
_expect_true(source.contains("TERRAIN_CHUNK_LOD_PLANNER_SCRIPT.new()"), "loader composes planner", failures)
|
||||||
|
_expect_true(source.contains("_terrain_chunk_lod_planner.plan(chunks, typed_focus_position, terrain_chunk_lod_policy)"), "loader delegates plan", failures)
|
||||||
|
_expect_true(not source.contains("dist_sq <= lod0_sq"), "loader omits chunk LOD formula", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||||
|
var planner: RefCounted = PLANNER_SCRIPT.new()
|
||||||
|
var policy: RefCounted = POLICY_SCRIPT.new(true, 10, 20, 40, 33.333333)
|
||||||
|
var focus: RefCounted = POSITION_SCRIPT.new(1000.0, 0.0, 1000.0)
|
||||||
|
var chunks: Array = []
|
||||||
|
for chunk_index in range(256):
|
||||||
|
chunks.append({"origin": Vector3(float(chunk_index % 16) * 33.333333, 0.0, float(chunk_index / 16) * 33.333333)})
|
||||||
|
var started_microseconds := Time.get_ticks_usec()
|
||||||
|
for _iteration in range(250):
|
||||||
|
planner.call("plan", chunks, focus, policy)
|
||||||
|
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||||
|
_expect_true(elapsed_milliseconds < 1000.0, "250 plans remain bounded", failures)
|
||||||
|
return elapsed_milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
func _read_text(path: String, failures: Array[String]) -> String:
|
||||||
|
var file := FileAccess.open(path, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
failures.append("cannot open %s" % path)
|
||||||
|
return ""
|
||||||
|
return file.get_as_text()
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_equal(actual_value: int, expected_value: int, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual_value != expected_value:
|
||||||
|
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
if not actual_value:
|
||||||
|
failures.append("%s expected true" % label)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://2oljbpoxndx3
|
||||||
@@ -27,10 +27,11 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
## Evidence
|
## Evidence
|
||||||
|
|
||||||
- Date: 2026-07-16
|
- Date: 2026-07-16
|
||||||
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`;
|
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`;
|
||||||
packages `M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-001`,
|
packages `M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-001`,
|
||||||
`M03-RND-FACADE-GROUND-QUERY-001`, `M03-RND-FACADE-ENVIRONMENT-001`,
|
`M03-RND-FACADE-GROUND-QUERY-001`, `M03-RND-FACADE-ENVIRONMENT-001`,
|
||||||
`M03-RND-FACADE-ENTITY-001`, `M03-RND-TERRAIN-CACHE-SERVICE-001`
|
`M03-RND-FACADE-ENTITY-001`, `M03-RND-TERRAIN-CACHE-SERVICE-001`,
|
||||||
|
`M03-RND-TERRAIN-LOD-PLANNER-001`, `M03-RND-TERRAIN-CHUNK-QUEUE-001`
|
||||||
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
|
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
|
||||||
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
|
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
|
||||||
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner
|
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner
|
||||||
@@ -47,6 +48,11 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
`delegation=9 entity_contract=6`, while character presentation remained `9/10/3/1`.
|
`delegation=9 entity_contract=6`, while character presentation remained `9/10/3/1`.
|
||||||
Terrain quality cache passed `contract_cases=15 loader_calls=3`; internal-access
|
Terrain quality cache passed `contract_cases=15 loader_calls=3`; internal-access
|
||||||
inventory decreased to `42` because the loader no longer owns the LRU-order field.
|
inventory decreased to `42` because the loader no longer owns the LRU-order field.
|
||||||
|
Terrain chunk LOD planner passed `cases=7 iterations=250 elapsed_ms=12.630`;
|
||||||
|
post-merge facade, internal-access and `7/7` manifest gates remained green.
|
||||||
|
Terrain chunk geometry queue planner passed `cases=8 iterations=100
|
||||||
|
elapsed_ms=229.324`; internal-access remained `42` and post-merge facade and
|
||||||
|
`7/7` manifest gates stayed green.
|
||||||
M02 terrain-query regression
|
M02 terrain-query regression
|
||||||
remained green (13 pre-existing expired M00 claim warnings).
|
remained green (13 pre-existing expired M00 claim warnings).
|
||||||
- Fidelity comparison: all 16 historical operation limits and drain sites were
|
- Fidelity comparison: all 16 historical operation limits and drain sites were
|
||||||
@@ -60,6 +66,11 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
baseline plans remain unchanged by the new present/remove ownership boundary.
|
baseline plans remain unchanged by the new present/remove ownership boundary.
|
||||||
Terrain revisit cache retains the same admitted/excluded sources, LRU touch/trim,
|
Terrain revisit cache retains the same admitted/excluded sources, LRU touch/trim,
|
||||||
profile capacities and three loader adapter sites; no cache format changed.
|
profile capacities and three loader adapter sites; no cache format changed.
|
||||||
|
Chunk LOD keeps horizontal XZ chunk-center distance, inclusive squared thresholds,
|
||||||
|
sparse-index omission and raw negative-radius squaring at both loader call sites.
|
||||||
|
Chunk operation planning preserves stable removals, nearest-first create sorting,
|
||||||
|
tile-LOD remove-all takeover, sparse tile-origin fallback and XZ-only distance;
|
||||||
|
loader still drains removals first through the shared `chunk_geometry` budget.
|
||||||
No new build-12340 parity claim is made; asset-backed visual/p95/p99 comparison remains pending.
|
No new build-12340 parity claim is made; asset-backed visual/p95/p99 comparison remains pending.
|
||||||
- Changed files: scene-free scheduler and verifier; streamer budget composition/
|
- Changed files: scene-free scheduler and verifier; streamer budget composition/
|
||||||
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes;
|
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes;
|
||||||
@@ -67,6 +78,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
immutable environment snapshot, facade/sky adapter, migrated checkpoint time input,
|
immutable environment snapshot, facade/sky adapter, migrated checkpoint time input,
|
||||||
contract-v1 entity snapshot, renderer-owned entity presenter, runtime composition,
|
contract-v1 entity snapshot, renderer-owned entity presenter, runtime composition,
|
||||||
scene-free terrain quality Mesh cache and loader store/restore/clear adapters,
|
scene-free terrain quality Mesh cache and loader store/restore/clear adapters,
|
||||||
|
immutable terrain chunk LOD policy, pure planner and both loader plan adapters,
|
||||||
|
pure terrain chunk geometry queue planner and loader queue-adoption adapter,
|
||||||
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
|
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
|
||||||
- Remaining risks: queue storage length and worker concurrency/stale-result logic
|
- Remaining risks: queue storage length and worker concurrency/stale-result logic
|
||||||
remain streamer-owned; cancellation stops new permits but does not interrupt
|
remain streamer-owned; cancellation stops new permits but does not interrupt
|
||||||
@@ -74,5 +87,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
or indoor state; entity PackedScene loading is synchronous and not accepted for
|
or indoor state; entity PackedScene loading is synchronous and not accepted for
|
||||||
network-scale hot paths; terrain cache is entry-count bounded without Mesh-byte
|
network-scale hot paths; terrain cache is entry-count bounded without Mesh-byte
|
||||||
metrics and the remaining terrain build/tasks/state/finalization stay monolithic;
|
metrics and the remaining terrain build/tasks/state/finalization stay monolithic;
|
||||||
|
chunk LOD asset-backed p95/p99 and visual acceptance remain pending;
|
||||||
|
chunk queue planning adds a second tile-state pass whose asset-backed p95/p99
|
||||||
|
still needs acceptance, while queue drains and geometry finalization remain monolithic;
|
||||||
M03 still needs further terrain/M2/WMO/liquid service extraction
|
M03 still needs further terrain/M2/WMO/liquid service extraction
|
||||||
and dependency/performance acceptance.
|
and dependency/performance acceptance.
|
||||||
|
|||||||
Reference in New Issue
Block a user