Merge work package M03-RND-STREAMING-PLANNER-001
Work-Package: M03-RND-STREAMING-PLANNER-001 Agent: sindo-main-codex Tests: planner/facade/focus/coordinate/manifest/shutdown contracts, dry-run and repository gates Fidelity: preserves target sets, boundary prefetch, queue priority, caches and visible renderer behavior
This commit is contained in:
@@ -889,6 +889,20 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
|||||||
visibility, materials or animation. Environment, entity visuals and ground
|
visibility, materials or animation. Environment, entity visuals and ground
|
||||||
query remain explicit follow-up facade contracts in M03.
|
query remain explicit follow-up facade contracts in M03.
|
||||||
|
|
||||||
|
## 2026-07-15 StreamingTargetPlanner Extraction
|
||||||
|
|
||||||
|
- Runtime and editor wanted/retained ADT set calculation now lives in the
|
||||||
|
scene-free `StreamingTargetPlanner`.
|
||||||
|
- `StreamingTargetPolicy` preserves existing chunk/tile radii, warm/prewarm/
|
||||||
|
retain margins and the clamped boundary-prefetch threshold.
|
||||||
|
- `StreamingTargetPlan` freezes its focus/wanted/retained result; the streamer
|
||||||
|
still owns available paths, load-queue distance sorting, loaded-tile LOD/detail
|
||||||
|
state, async work, caches, nodes and RIDs.
|
||||||
|
- Asset-free fixtures cover center, edge/corner prefetch, catalog filtering,
|
||||||
|
clamp and editor behavior plus bounded High-like iteration timing.
|
||||||
|
- Cache versions, quality profiles, queue priority and visible rendering rules
|
||||||
|
are unchanged by the extraction.
|
||||||
|
|
||||||
## Practical Rule For Future Work
|
## Practical Rule For Future Work
|
||||||
|
|
||||||
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
|
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# M03-RND-STREAMING-PLANNER-001 — Pure streaming target planner
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M03-RND-STREAMING-PLANNER-001:sindo-main-codex:2026-07-17 -->
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Target: M03
|
||||||
|
- Program: RND
|
||||||
|
- Owner/Agent ID: sindo-main-codex
|
||||||
|
- Branch: `work/sindo-main-codex/m03-streaming-target-planner`
|
||||||
|
- Lease expires UTC: 2026-07-17
|
||||||
|
- Integrator: M03 milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Extract runtime/editor ADT wanted/retained-set calculation and boundary prefetch
|
||||||
|
selection from `StreamingWorldLoader` into a scene-free, deterministic
|
||||||
|
`StreamingTargetPlanner` with typed focus input and asset-free contract tests.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Change load/retain radii, boundary thresholds, queue priority or LOD policy.
|
||||||
|
- Move queue mutation, async jobs, cache ownership or GPU finalization.
|
||||||
|
- Extract `RenderBudgetScheduler` or asset services in this package.
|
||||||
|
- Change cache formats, renderer profiles or target milestone status.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: `src/render/streaming/streaming_target_planner.gd`,
|
||||||
|
`src/render/streaming/streaming_target_policy.gd`,
|
||||||
|
`src/render/streaming/streaming_target_plan.gd`,
|
||||||
|
`src/tools/verify_streaming_target_planner.gd`,
|
||||||
|
`docs/modules/streaming-target-planner.md`, this claim
|
||||||
|
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||||
|
`src/tools/verify_coordinate_conversion_boundaries.gd`,
|
||||||
|
`docs/modules/world-renderer.md`, `docs/modules/coordinate-mapping.md`,
|
||||||
|
`docs/modules/README.md`, `RENDER.md`
|
||||||
|
- Generated/ignored: `.godot`, native DLL, generated cache resources and
|
||||||
|
proprietary renderer corpus
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- Input: immutable `GodotWorldPosition`, available-tile key set and immutable
|
||||||
|
`StreamingTargetPolicy`
|
||||||
|
- Output: immutable `StreamingTargetPlan` with focus tile plus detached
|
||||||
|
wanted/retained tile-key sets
|
||||||
|
- Tile-key representation remains the streamer's existing `<x>_<y>` internal key
|
||||||
|
- Schema/cache/coordinate versions: unchanged
|
||||||
|
- Consumers: `StreamingWorldLoader` runtime/editor refresh paths
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: M01 typed coordinates/focus and integrated facade package `a100ced`
|
||||||
|
- Blocks: budget scheduler extraction and planner performance regression gate
|
||||||
|
- External state: none; tests use synthetic tile catalogs
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: dedicated planner verifier, facade/focus/coordinate regressions,
|
||||||
|
renderer manifest/dry-run and repository gates
|
||||||
|
- Fixtures: center, edge, corner, unavailable-tile, clamped-radius and editor cases
|
||||||
|
- Fidelity evidence: expected tile sets are captured from current formulas; full
|
||||||
|
renderer dry-run retains all seven checkpoint plans
|
||||||
|
- Performance budget: planner work remains bounded by square radii and creates no
|
||||||
|
Node, Resource, task, cache or RID
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline API docs for planner/policy/plan
|
||||||
|
- New module specification with input/output table and data-flow diagram
|
||||||
|
- Updated world-renderer data-flow/sequence/ownership/source map
|
||||||
|
- Updated module registry and renderer notes
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important names: `StreamingTargetPlanner`, `StreamingTargetPolicy`,
|
||||||
|
`StreamingTargetPlan`
|
||||||
|
- Simplest approach: one pure calculator plus two immutable value objects
|
||||||
|
- Rejected complexity: generic graph planner, priority framework, service
|
||||||
|
container, job ownership or duplicate available-tile catalog
|
||||||
|
- Unavoidable complexity: boundary-prefetch cross product must preserve corner
|
||||||
|
behavior and existing string tile keys
|
||||||
|
- Measured optimization evidence: no new optimization; exact extraction
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: ready-for-review
|
||||||
|
- Done: extracted immutable policy/plan and pure planner, migrated runtime/editor
|
||||||
|
target refresh, added behavior/dependency/performance coverage and module docs
|
||||||
|
- Next: M03 integrator reviews and merges `e887c3b`
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M03-RND-STREAMING-PLANNER-001:e887c3b -->
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: `e887c3b`
|
||||||
|
- Results: runtime/editor wanted and retained ADT sets plus boundary-prefetch
|
||||||
|
centers are calculated by scene-free `StreamingTargetPlanner`; streamer keeps
|
||||||
|
queue ordering, LOD/detail state, jobs, caches, nodes and RIDs.
|
||||||
|
- Verification: planner `cases=5 iterations=250 average_ms=1.765`; facade
|
||||||
|
`delegation=3 runtime_scenes=2 tools=3`; focus `contract=1 runtime_scenes=2
|
||||||
|
capture_tools=3`; coordinate boundary `files=109 consumers=6`; renderer
|
||||||
|
manifest `checkpoints=7 coverage=7 caches=7`; shutdown ownership passed;
|
||||||
|
documentation, coordination and diff gates passed.
|
||||||
|
- Fidelity: center/corner/catalog/clamp/editor fixtures preserve the extracted
|
||||||
|
formulas; seven-checkpoint dry-run retained all plans. No radius, threshold,
|
||||||
|
load priority, cache version, placement, material, animation or visibility
|
||||||
|
rule changed. No build-12340 parity claim is added.
|
||||||
|
- External/local inputs: ignored native DLL and generated ADT resource scripts
|
||||||
|
were copied from master for worktree smoke only. Proprietary ADT and character
|
||||||
|
assets were unavailable, so the dry-run emitted expected degraded-data diagnostics.
|
||||||
|
- Remaining risks: exact p95/p99 impact still needs an asset-backed baseline;
|
||||||
|
pathological custom radii are not benchmarked; tile-key strings remain the
|
||||||
|
existing internal catalog representation; queue/LOD application remains monolithic.
|
||||||
|
- Documentation updated: new planner module spec with API/input/output/data-flow/
|
||||||
|
sequence/ownership/recovery/performance/source-map sections; world renderer,
|
||||||
|
coordinate mapping, module registry and renderer implementation notes updated.
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
| 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) |
|
||||||
|
| Streaming target planner | Implemented | [`streaming-target-planner.md`](streaming-target-planner.md) |
|
||||||
| Network/session | Planned | [`../../targets/roadmap/03-network-and-session.md`](../../targets/roadmap/03-network-and-session.md) |
|
| Network/session | Planned | [`../../targets/roadmap/03-network-and-session.md`](../../targets/roadmap/03-network-and-session.md) |
|
||||||
| Gameplay | Prototype/Planned | [`../../targets/roadmap/04-gameplay-systems.md`](../../targets/roadmap/04-gameplay-systems.md) |
|
| Gameplay | Prototype/Planned | [`../../targets/roadmap/04-gameplay-systems.md`](../../targets/roadmap/04-gameplay-systems.md) |
|
||||||
| UI/Lua/audio | Prototype/Planned | [`../../targets/roadmap/05-ui-lua-audio.md`](../../targets/roadmap/05-ui-lua-audio.md) |
|
| UI/Lua/audio | Prototype/Planned | [`../../targets/roadmap/05-ui-lua-audio.md`](../../targets/roadmap/05-ui-lua-audio.md) |
|
||||||
|
|||||||
@@ -116,9 +116,12 @@ flowchart LR
|
|||||||
Godot -->|typed direct adapter| Tile
|
Godot -->|typed direct adapter| Tile
|
||||||
Godot -->|typed direct adapter| Local
|
Godot -->|typed direct adapter| Local
|
||||||
Godot --> TerrainQuery[AdtTerrainQuery consumer]
|
Godot --> TerrainQuery[AdtTerrainQuery consumer]
|
||||||
|
Godot --> TargetPlanner[StreamingTargetPlanner consumer]
|
||||||
TerrainQuery --> Tile
|
TerrainQuery --> Tile
|
||||||
TerrainQuery --> Local
|
TerrainQuery --> Local
|
||||||
TerrainQuery --> Chunk
|
TerrainQuery --> Chunk
|
||||||
|
TargetPlanner --> Tile
|
||||||
|
TargetPlanner --> Local
|
||||||
Tile -->|tile plus local| Godot
|
Tile -->|tile plus local| Godot
|
||||||
ServerYaw[ServerWorldYaw] -->|normalize only| CanonicalYaw[CanonicalWowWorldYaw]
|
ServerYaw[ServerWorldYaw] -->|normalize only| CanonicalYaw[CanonicalWowWorldYaw]
|
||||||
CanonicalYaw -->|same numeric world angle| GodotYaw[GodotWorldYaw]
|
CanonicalYaw -->|same numeric world angle| GodotYaw[GodotWorldYaw]
|
||||||
@@ -249,6 +252,7 @@ raw three-number arrays are not an accepted new persistence contract.
|
|||||||
| `src/tools/verify_coordinate_golden_fixtures.gd` | Cross-source fixture schema and mapper validation |
|
| `src/tools/verify_coordinate_golden_fixtures.gd` | Cross-source fixture schema and mapper validation |
|
||||||
| `src/tools/verify_coordinate_conversion_boundaries.gd` | Repository-wide manual conversion and required-consumer gate |
|
| `src/tools/verify_coordinate_conversion_boundaries.gd` | Repository-wide manual conversion and required-consumer gate |
|
||||||
| `src/gameplay/terrain/adt_terrain_query.gd` | Typed Godot-position consumer for ADT tile/local/chunk lookup |
|
| `src/gameplay/terrain/adt_terrain_query.gd` | Typed Godot-position consumer for ADT tile/local/chunk lookup |
|
||||||
|
| `src/render/streaming/streaming_target_planner.gd` | Typed Godot-position consumer for ADT focus tile and boundary-local lookup |
|
||||||
| `src/tools/server_spawn_render_manifest.json` | Pinned server-spawn camera, player, marker and expected ADT ownership |
|
| `src/tools/server_spawn_render_manifest.json` | Pinned server-spawn camera, player, marker and expected ADT ownership |
|
||||||
| `src/tools/verify_server_spawn_renderer.gd` | Fixture-to-renderer manifest contract verifier |
|
| `src/tools/verify_server_spawn_renderer.gd` | Fixture-to-renderer manifest contract verifier |
|
||||||
| `docs/adr/0001-canonical-world-coordinates.md` | Normative axes, units, boundaries and rollout decision |
|
| `docs/adr/0001-canonical-world-coordinates.md` | Normative axes, units, boundaries and rollout decision |
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
# Streaming Target Planner
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Status | Implemented |
|
||||||
|
| Target/work package | `M03-RND-STREAMING-PLANNER-001` |
|
||||||
|
| Owners | Renderer workstream / M03 integrator |
|
||||||
|
| Last verified | `e887c3b`, 2026-07-15 |
|
||||||
|
| Profiles/capabilities | Runtime and Editor preview ADT target selection; profile values supplied by caller |
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Calculate which available ADT tiles are wanted and retained for one typed focus
|
||||||
|
without accessing the SceneTree, worker tasks, renderer queues, caches or GPU
|
||||||
|
resources. Runtime planning preserves prewarm, hysteresis and boundary-prefetch
|
||||||
|
behavior; editor planning preserves the square preview radius.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Mutate load/finalize/eviction queues.
|
||||||
|
- Prioritize queued tile paths by world distance.
|
||||||
|
- Calculate chunk or tile LOD levels after a tile is loaded.
|
||||||
|
- Own the available WDT tile catalog or cache paths.
|
||||||
|
- Create, cancel or await jobs.
|
||||||
|
- Implement M2, WMO, liquid or terrain attachment.
|
||||||
|
|
||||||
|
## Context and boundaries
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Focus[GodotWorldPosition] --> Planner[StreamingTargetPlanner]
|
||||||
|
Policy[StreamingTargetPolicy] --> Planner
|
||||||
|
Catalog[Available tile keys from WDT/directory] --> Planner
|
||||||
|
Planner --> Plan[StreamingTargetPlan]
|
||||||
|
Plan --> Streamer[StreamingWorldLoader queue/state apply]
|
||||||
|
Streamer --> Queue[Tile load queue]
|
||||||
|
```
|
||||||
|
|
||||||
|
Allowed dependencies:
|
||||||
|
|
||||||
|
- typed coordinate values and `CoordinateMapper`;
|
||||||
|
- immutable planner policy/result contracts;
|
||||||
|
- read-only available-tile key membership.
|
||||||
|
|
||||||
|
Forbidden dependencies:
|
||||||
|
|
||||||
|
- `Node`, SceneTree and viewport/camera discovery;
|
||||||
|
- `WorkerThreadPool`, `ResourceLoader` and filesystem access;
|
||||||
|
- `RenderingServer`, meshes, materials, nodes and RIDs;
|
||||||
|
- gameplay, network, UI, editor UI or server adapters;
|
||||||
|
- streamer queues, tile states and cache dictionaries.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `StreamingTargetPlanner.plan_runtime` | Pure method | Builds wanted/retained sets with edge/corner prefetch | Any thread if caller does not mutate catalog concurrently; call-scoped | Empty catalog produces empty sets |
|
||||||
|
| `StreamingTargetPlanner.plan_editor` | Pure method | Builds identical editor wanted/retained square sets | Same as runtime planning | Negative radius clamps to zero |
|
||||||
|
| `StreamingTargetPolicy` | Immutable value | Captures radius margins and boundary threshold for one plan | Caller-owned, shareable | Values retain scene input; effective margins/threshold are clamped |
|
||||||
|
| `visible_tile_radius` | Pure query | Resolves chunk-radius versus tile-radius visibility | Any thread/value lifetime | No failure |
|
||||||
|
| `load_tile_radius` | Pure query | Adds non-negative prewarm margin | Any thread/value lifetime | No failure |
|
||||||
|
| `retained_tile_radius` | Pure query | Adds non-negative hysteresis margin | Any thread/value lifetime | No failure |
|
||||||
|
| `StreamingTargetPlan` | Immutable value | Holds focus tile and read-only wanted/retained key sets | Caller-owned plan lifetime | No failure |
|
||||||
|
| `wanted_tile_keys` | Read-only query | Returns immutable wanted set | Plan lifetime | Mutation is rejected by Godot read-only Dictionary |
|
||||||
|
| `retained_tile_keys` | Read-only query | Returns immutable retained set | Plan lifetime | Mutation is rejected by Godot read-only Dictionary |
|
||||||
|
|
||||||
|
## Inputs and outputs
|
||||||
|
|
||||||
|
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Input | `GodotWorldPosition` | `WorldRenderFacade`/streamer focus adapter | Planner | Immutable caller value | One plan |
|
||||||
|
| Input | Available `<tile_x>_<tile_y>` keys | `StreamingWorldLoader` WDT/directory catalog | Planner membership checks | Loader-owned, read-only during call | Map session |
|
||||||
|
| Input | `StreamingTargetPolicy` | Streamer configuration snapshot | Planner | Immutable caller value | One or more plans |
|
||||||
|
| Input | Editor preview radius | Editor scene configuration | Planner | Scalar copy | One plan |
|
||||||
|
| Output | `StreamingTargetPlan.focus_tile` | Planner/`CoordinateMapper` | Streamer diagnostics | Immutable typed coordinate | Plan lifetime |
|
||||||
|
| Output | Wanted tile-key set | Planner | Streamer queue/state apply | Plan-owned read-only Dictionary | Plan lifetime |
|
||||||
|
| Output | Retained tile-key set | Planner | Streamer hysteresis/state apply | Plan-owned read-only Dictionary | Plan lifetime |
|
||||||
|
|
||||||
|
Side effects:
|
||||||
|
|
||||||
|
- none;
|
||||||
|
- no input mutation;
|
||||||
|
- no logging, I/O, task submission, cache access, SceneTree or GPU work.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Position[GodotWorldPosition] --> Tile[CoordinateMapper.godot_to_adt_tile]
|
||||||
|
Position --> Local[CoordinateMapper.godot_to_adt_tile_local]
|
||||||
|
Policy[StreamingTargetPolicy] --> Radii[load and retained radii]
|
||||||
|
Policy --> Threshold[clamped boundary threshold]
|
||||||
|
Tile --> Centers[base prefetch center]
|
||||||
|
Local --> Centers
|
||||||
|
Threshold --> Centers
|
||||||
|
Centers --> Wanted[available keys within load radius]
|
||||||
|
Centers --> Retained[available keys within retained radius]
|
||||||
|
Catalog[Available tile keys] --> Wanted
|
||||||
|
Catalog --> Retained
|
||||||
|
Wanted --> Plan[StreamingTargetPlan]
|
||||||
|
Retained --> Plan
|
||||||
|
Tile --> Plan
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle/state
|
||||||
|
|
||||||
|
The planner is stateless. A call creates local center/set dictionaries, freezes
|
||||||
|
the result dictionaries in `StreamingTargetPlan`, returns the plan and retains
|
||||||
|
no reference to inputs or output. There is no retry, cancellation or shutdown
|
||||||
|
state machine.
|
||||||
|
|
||||||
|
## Main sequence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Stream as StreamingWorldLoader
|
||||||
|
participant Planner as StreamingTargetPlanner
|
||||||
|
participant Mapper as CoordinateMapper
|
||||||
|
participant Plan as StreamingTargetPlan
|
||||||
|
Stream->>Planner: plan_runtime(catalog, typed focus, policy)
|
||||||
|
Planner->>Mapper: focus tile and tile-local position
|
||||||
|
Mapper-->>Planner: typed coordinates
|
||||||
|
Planner->>Planner: prefetch centers and available key sets
|
||||||
|
Planner->>Plan: freeze focus/wanted/retained result
|
||||||
|
Plan-->>Stream: immutable target plan
|
||||||
|
Stream->>Stream: apply queues, LOD and detail state
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ownership, threading and resources
|
||||||
|
|
||||||
|
- `StreamingWorldLoader` owns the available catalog and must not mutate it during a planning call.
|
||||||
|
- `StreamingTargetPolicy` and typed focus remain caller-owned immutable values.
|
||||||
|
- The planner owns temporary center/set values only until plan construction.
|
||||||
|
- `StreamingTargetPlan` owns its read-only result dictionaries.
|
||||||
|
- The planner creates no Node, Resource, RID, thread, mutex or task.
|
||||||
|
- Current runtime calls occur on the main thread; scene-free contracts permit a
|
||||||
|
future worker call only after catalog publication/ownership is explicit.
|
||||||
|
|
||||||
|
## Errors, cancellation and recovery
|
||||||
|
|
||||||
|
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Empty/unavailable catalog | No membership matches | Valid plan with empty sets | Streamer retains existing missing-data diagnostics | Load a valid WDT/directory catalog and replan |
|
||||||
|
| Focus outside catalog/grid | Typed coordinate does not match keys | Valid focus tile with filtered empty/partial sets | Caller diagnostic if required | Supply valid focus/map or catalog |
|
||||||
|
| Negative prewarm/retain margin | Policy effective-radius query | Margin clamps to zero | Contract test | Correct configuration if negative value was unintended |
|
||||||
|
| Boundary threshold outside range | Policy query | Clamp to `0.0..0.49` | Contract test | Correct configuration or accept safe clamp |
|
||||||
|
| Plan no longer current | New focus/config/catalog revision | Caller discards ephemeral plan | Streamer refresh diagnostics | Replan from current inputs |
|
||||||
|
|
||||||
|
Cancellation is not applicable: planning is synchronous, bounded and owns no
|
||||||
|
external work. A returned stale plan is simply not retained by the planner.
|
||||||
|
|
||||||
|
## Configuration and capabilities
|
||||||
|
|
||||||
|
| Setting/capability | Default source | Profile | Runtime mutable | Effect |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `lod2_radius_chunks` | Streamer scene/profile | Renderer | Yes at composition/debug boundary | Converts chunk coverage to minimum ADT radius |
|
||||||
|
| `lod2_tile_radius` | Streamer scene/profile | Renderer | Yes | Minimum visible ADT radius |
|
||||||
|
| `warm_tile_margin` | Streamer scene/profile | Renderer | Yes | Extends chunk-derived visible radius |
|
||||||
|
| `prewarm_tile_margin` | Streamer scene/profile | Renderer | Yes | Extends wanted/load radius |
|
||||||
|
| `retain_tile_margin` | Streamer scene/profile | Renderer | Yes | Extends retained hysteresis radius |
|
||||||
|
| `boundary_prefetch_threshold` | Streamer scene/profile | Renderer | Yes | Adds adjacent/corner prefetch centers near tile boundaries |
|
||||||
|
| Editor preview radius | Editor scene | Authoring preview | Yes | Square wanted/retained preview set |
|
||||||
|
|
||||||
|
The planner does not select `Blizzlike335` or `Enhanced`; it applies the values
|
||||||
|
already selected by renderer composition.
|
||||||
|
|
||||||
|
## Persistence, cache and migration
|
||||||
|
|
||||||
|
- Planner contracts are runtime-only and are not serialized.
|
||||||
|
- Existing tile keys and cache paths are unchanged.
|
||||||
|
- No cache/resource/schema version bump or migration is required.
|
||||||
|
|
||||||
|
## Diagnostics and observability
|
||||||
|
|
||||||
|
- Logs remain emitted by `StreamingWorldLoader` after applying a plan.
|
||||||
|
- `StreamingTargetPlan` exposes focus/wanted/retained data for deterministic tests.
|
||||||
|
- Dedicated verifier reports case count, iteration count, total time and average plan time.
|
||||||
|
- No correlation ID is required for synchronous ephemeral plans.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Unit/contract: `src/tools/verify_streaming_target_planner.gd` covers center
|
||||||
|
radii, edge/corner cross product, catalog filtering, clamps, editor behavior,
|
||||||
|
input/result ownership and scene-free dependencies.
|
||||||
|
- Integration: streamer source gate confirms both runtime/editor delegation and
|
||||||
|
removal of `_predictive_focus_tiles` from the monolith.
|
||||||
|
- Fidelity evidence: expected sets encode the pre-extraction formulas; renderer
|
||||||
|
checkpoint dry-run must retain all seven checkpoint plans.
|
||||||
|
- Performance budget: 250 synthetic High-like corner plans average at most
|
||||||
|
`4.0 ms` each on the headless regression runner; measured result is recorded
|
||||||
|
in test output.
|
||||||
|
- Manual diagnostics: unchanged `StreamingWorld` focus/wanted/queued log.
|
||||||
|
|
||||||
|
## Extension points
|
||||||
|
|
||||||
|
- Direction/velocity/frustum priority can extend policy/input only with fixtures
|
||||||
|
and a compatibility-safe default reproducing the current position-only plan.
|
||||||
|
- Multi-focus planning may union additional typed centers without giving the
|
||||||
|
planner ownership of queues or consumers.
|
||||||
|
- Queue ordering remains a separate scheduler/application responsibility.
|
||||||
|
|
||||||
|
## Capability status
|
||||||
|
|
||||||
|
| Capability | Status | Evidence | Gap/next step |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Runtime wanted/retained sets | Implemented | Center/corner/catalog fixtures | Add paired traversal performance evidence |
|
||||||
|
| Boundary prefetch centers | Implemented | Edge/corner cross-product fixture | Direction/velocity prediction not implemented |
|
||||||
|
| Editor preview set | Implemented | Negative/radius-one fixtures | Editor viewport integration remains existing adapter |
|
||||||
|
| Scene-free deterministic execution | Implemented | Dependency/source gate and 250-iteration run | Worker execution is unnecessary today |
|
||||||
|
| Queue priority/LOD | Not in module | Preserved in streamer | Separate extraction packages |
|
||||||
|
|
||||||
|
## Known gaps and risks
|
||||||
|
|
||||||
|
- Tile-key sets preserve the streamer's internal string representation; a typed
|
||||||
|
catalog migration is deferred until it can remove, rather than duplicate, that source of truth.
|
||||||
|
- Planner time scales with the union of square radii/prefetch centers. The
|
||||||
|
current bounded regression covers High-like corner input but not pathological custom radii.
|
||||||
|
- Exact p95/p99 frame impact still requires renderer baseline runs with local assets.
|
||||||
|
- Direction, velocity and camera frustum do not influence target selection yet.
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Path | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `src/render/streaming/streaming_target_planner.gd` | Pure runtime/editor set calculation and boundary centers |
|
||||||
|
| `src/render/streaming/streaming_target_policy.gd` | Immutable radius/threshold snapshot and effective clamps |
|
||||||
|
| `src/render/streaming/streaming_target_plan.gd` | Immutable typed focus and read-only result sets |
|
||||||
|
| `src/scenes/streaming/streaming_world_loader.gd` | Planner composition and plan application to queues/state |
|
||||||
|
| `src/tools/verify_streaming_target_planner.gd` | Asset-free behavior/dependency/performance regression |
|
||||||
|
|
||||||
|
## Related decisions and references
|
||||||
|
|
||||||
|
- [`world-renderer.md`](world-renderer.md)
|
||||||
|
- [`../../targets/03-renderer-facade.md`](../../targets/03-renderer-facade.md)
|
||||||
|
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||||
|
- [`../GODOT_BEST_PRACTICES.md`](../GODOT_BEST_PRACTICES.md)
|
||||||
|
- [`../CODING_STANDARD.md`](../CODING_STANDARD.md)
|
||||||
@@ -34,6 +34,9 @@ flowchart LR
|
|||||||
Adapter --> Focus
|
Adapter --> Focus
|
||||||
Focus --> Facade[WorldRenderFacade]
|
Focus --> Facade[WorldRenderFacade]
|
||||||
Facade --> Loader[StreamingWorldLoader internal]
|
Facade --> Loader[StreamingWorldLoader internal]
|
||||||
|
Loader --> Planner[StreamingTargetPlanner]
|
||||||
|
Planner --> TargetPlan[StreamingTargetPlan]
|
||||||
|
TargetPlan --> Loader
|
||||||
Assets[Extracted WDT/ADT/M2/WMO/BLP] --> Native[Native loaders]
|
Assets[Extracted WDT/ADT/M2/WMO/BLP] --> Native[Native loaders]
|
||||||
Cache[Baked terrain/M2/WMO caches] --> Loader
|
Cache[Baked terrain/M2/WMO caches] --> Loader
|
||||||
Native --> Parsed[Parsed tile/model data]
|
Native --> Parsed[Parsed tile/model data]
|
||||||
@@ -94,7 +97,8 @@ loader configuration remains transitional composition data, not a caller API.
|
|||||||
| Input | WDT/ADT/M2/WMO/BLP bytes | Extracted asset repository | Native loaders/builders | Loader result owns parsed data | Worker or controlled load |
|
| Input | WDT/ADT/M2/WMO/BLP bytes | Extracted asset repository | Native loaders/builders | Loader result owns parsed data | Worker or controlled load |
|
||||||
| Input | Baked resources | Cache build tools | Streamer/builders | Shared immutable resource/cache entry | Session/cache lifetime |
|
| Input | Baked resources | Cache build tools | Streamer/builders | Shared immutable resource/cache entry | Session/cache lifetime |
|
||||||
| Test input | Server-spawn render manifest | Coordinate fixture/verifier | Checkpoint capture tool | Repository-owned diagnostic data | Test-process lifetime |
|
| Test input | Server-spawn render manifest | Coordinate fixture/verifier | Checkpoint capture tool | Repository-owned diagnostic data | Test-process lifetime |
|
||||||
| Output | Desired tile/detail operations | Streaming planner inside loader | Finalize queues | Loader-owned | Cross-frame |
|
| Output | `StreamingTargetPlan` | `StreamingTargetPlanner` | Streamer queue/state apply | Immutable ephemeral value | One refresh |
|
||||||
|
| 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 |
|
||||||
| Output | Logs/diagnostics | Hitch/stat instrumentation | Logs/baseline tools | Immutable records | Periodic/frame |
|
| Output | Logs/diagnostics | Hitch/stat instrumentation | Logs/baseline tools | Immutable records | Periodic/frame |
|
||||||
@@ -121,8 +125,9 @@ flowchart TD
|
|||||||
A --> F[StreamingFocus]
|
A --> F[StreamingFocus]
|
||||||
EA --> F
|
EA --> F
|
||||||
F --> RF[WorldRenderFacade]
|
F --> RF[WorldRenderFacade]
|
||||||
RF --> T[StreamingWorldLoader target refresh]
|
RF --> Planner[StreamingTargetPlanner]
|
||||||
WDT[WDT tile catalog] --> T
|
Planner --> T[StreamingTargetPlan apply]
|
||||||
|
WDT[WDT tile catalog] --> Planner
|
||||||
T --> Q[Tile load queue]
|
T --> Q[Tile load queue]
|
||||||
Q --> Parse[Worker parse/cache load]
|
Q --> Parse[Worker parse/cache load]
|
||||||
Parse --> R[Result queues]
|
Parse --> R[Result queues]
|
||||||
@@ -136,8 +141,8 @@ flowchart TD
|
|||||||
WMO --> World
|
WMO --> World
|
||||||
Liquid --> World
|
Liquid --> World
|
||||||
Marker --> World
|
Marker --> World
|
||||||
F --> E[Eviction/retention decisions]
|
F --> Eviction[Eviction/retention decisions]
|
||||||
E --> World
|
Eviction --> World
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lifecycle/state
|
## Lifecycle/state
|
||||||
@@ -166,13 +171,16 @@ sequenceDiagram
|
|||||||
participant Focus as StreamingFocus adapter
|
participant Focus as StreamingFocus adapter
|
||||||
participant Facade as WorldRenderFacade
|
participant Facade as WorldRenderFacade
|
||||||
participant Stream as StreamingWorldLoader internal
|
participant Stream as StreamingWorldLoader internal
|
||||||
|
participant Planner as StreamingTargetPlanner
|
||||||
participant Worker as Worker task
|
participant Worker as Worker task
|
||||||
participant Budget as Main-thread budget
|
participant Budget as Main-thread budget
|
||||||
participant Render as SceneTree/RenderingServer
|
participant Render as SceneTree/RenderingServer
|
||||||
Source->>Focus: sample explicit Godot position
|
Source->>Focus: sample explicit Godot position
|
||||||
Focus->>Facade: set/refresh typed focus
|
Focus->>Facade: set/refresh typed focus
|
||||||
Facade->>Stream: delegate focus and refresh
|
Facade->>Stream: delegate focus and refresh
|
||||||
Stream->>Stream: desired tiles and priorities
|
Stream->>Planner: typed focus, catalog and target policy
|
||||||
|
Planner-->>Stream: immutable wanted/retained plan
|
||||||
|
Stream->>Stream: queue priority and loaded-tile LOD/detail state
|
||||||
Stream->>Worker: parse/load tile and detail data
|
Stream->>Worker: parse/load tile and detail data
|
||||||
Worker-->>Stream: immutable result
|
Worker-->>Stream: immutable result
|
||||||
Stream->>Budget: enqueue finalize operations
|
Stream->>Budget: enqueue finalize operations
|
||||||
@@ -189,6 +197,8 @@ sequenceDiagram
|
|||||||
- `StreamingWorldLoader` владеет tile states, queues, task registries, active world instances и render caches session scope.
|
- `StreamingWorldLoader` владеет tile states, queues, task registries, active world instances и render caches session scope.
|
||||||
- `WorldRenderFacade` owns only scene-relative adapter paths. It does not own the
|
- `WorldRenderFacade` owns only scene-relative adapter paths. It does not own the
|
||||||
streamer, focus source, returned metrics, queues, caches, nodes or RIDs.
|
streamer, focus source, returned metrics, queues, caches, nodes or RIDs.
|
||||||
|
- `StreamingTargetPlanner` is stateless and owns only call-local calculations;
|
||||||
|
its immutable plan is consumed synchronously by the streamer.
|
||||||
- Focus producers own the immutable `StreamingFocus` reference; the loader retains
|
- Focus producers own the immutable `StreamingFocus` reference; the loader retains
|
||||||
the latest reference and converts it to `Vector3` only inside the render boundary.
|
the latest reference and converts it to `Vector3` only inside the render boundary.
|
||||||
- Worker tasks не должны менять SceneTree и shared Resource concurrently.
|
- Worker tasks не должны менять SceneTree и shared Resource concurrently.
|
||||||
@@ -249,6 +259,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
- 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
|
||||||
|
High-like iteration timing without loading a world scene.
|
||||||
- Integration/E2E: Eastern Kingdoms/Kalimdor streaming scenes, seven M00
|
- Integration/E2E: Eastern Kingdoms/Kalimdor streaming scenes, seven M00
|
||||||
cold/warm checkpoints and one dedicated mapped server-spawn checkpoint.
|
cold/warm checkpoints and one dedicated mapped server-spawn checkpoint.
|
||||||
- Fidelity evidence: семь локальных build 12340 reference JPG покрывают terrain/ADT/M2/WMO/liquid/sky и реальный `GryphonRoost01`; automated cold/warm metrics имеют `compared=14`, `missing=0`. Sky использует приблизительно paired camera, а animated-M2 остаётся asset-paired/placement-unpaired из-за synthetic probe.
|
- Fidelity evidence: семь локальных build 12340 reference JPG покрывают terrain/ADT/M2/WMO/liquid/sky и реальный `GryphonRoost01`; automated cold/warm metrics имеют `compared=14`, `missing=0`. Sky использует приблизительно paired camera, а animated-M2 остаётся asset-paired/placement-unpaired из-за synthetic probe.
|
||||||
@@ -275,6 +287,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| Camera-independent streaming focus | Implemented | Typed contract, two player-focused runtime scenes and three explicitly camera-focused diagnostic tools | Multi-focus/direction/velocity belong to planner extraction |
|
| Camera-independent streaming focus | Implemented | Typed contract, two player-focused runtime scenes and three explicitly camera-focused diagnostic tools | Multi-focus/direction/velocity belong to planner extraction |
|
||||||
| Golden server-spawn visualization | Implemented | Pinned AzerothCore fixture, validated renderer manifest and diagnostic origin-marker capture at ADT `(32,48)`/MCNK `(3,12)` | No TrinityCore row or original-client visual-parity claim |
|
| Golden server-spawn visualization | Implemented | Pinned AzerothCore fixture, validated renderer manifest and diagnostic origin-marker capture at ADT `(32,48)`/MCNK `(3,12)` | No TrinityCore row or original-client visual-parity claim |
|
||||||
| Stable renderer facade | Partial | `M03-RND-FACADE-001` contract/runtime/tool regressions | Focus and metrics migrated; environment/entity visuals/ground query remain |
|
| Stable renderer facade | Partial | `M03-RND-FACADE-001` contract/runtime/tool regressions | Focus and metrics migrated; environment/entity visuals/ground query remain |
|
||||||
|
| 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 |
|
||||||
|
|
||||||
## Known gaps and risks
|
## Known gaps and risks
|
||||||
|
|
||||||
@@ -301,10 +314,14 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| Path | Responsibility |
|
| Path | Responsibility |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `src/render/world_render_facade.gd` | Stable caller boundary for typed focus and detached renderer metrics |
|
| `src/render/world_render_facade.gd` | Stable caller boundary for typed focus and detached renderer metrics |
|
||||||
|
| `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
|
||||||
|
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
|
||||||
|
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets |
|
||||||
| `src/scenes/streaming/streaming_world_loader.gd` | Current runtime orchestration, queues, caches and attachment |
|
| `src/scenes/streaming/streaming_world_loader.gd` | Current runtime orchestration, queues, caches and attachment |
|
||||||
| `src/domain/streaming/streaming_focus.gd` | Immutable camera-independent streaming position contract |
|
| `src/domain/streaming/streaming_focus.gd` | Immutable camera-independent streaming position contract |
|
||||||
| `src/tools/verify_streaming_focus.gd` | Headless contract, dependency and runtime/tool wiring regression |
|
| `src/tools/verify_streaming_focus.gd` | Headless contract, dependency and runtime/tool wiring regression |
|
||||||
| `src/tools/verify_world_render_facade.gd` | Facade delegation, snapshot isolation and consumer wiring regression |
|
| `src/tools/verify_world_render_facade.gd` | Facade delegation, snapshot isolation and consumer wiring regression |
|
||||||
|
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
|
||||||
| `addons/mpq_extractor/loaders/adt_builder.gd` | Terrain/material/liquid construction |
|
| `addons/mpq_extractor/loaders/adt_builder.gd` | Terrain/material/liquid construction |
|
||||||
| `addons/mpq_extractor/loaders/m2_builder.gd` | Static M2 mesh/material construction |
|
| `addons/mpq_extractor/loaders/m2_builder.gd` | Static M2 mesh/material construction |
|
||||||
| `addons/mpq_extractor/loaders/m2_native_animated_builder.gd` | Native animated M2 experiment |
|
| `addons/mpq_extractor/loaders/m2_native_animated_builder.gd` | Native animated M2 experiment |
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
class_name StreamingTargetPlan
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Immutable result of one runtime or editor target-planning operation.
|
||||||
|
## Tile-key sets use the streamer's existing `<tile_x>_<tile_y>` representation.
|
||||||
|
|
||||||
|
var focus_tile: AdtTileCoordinate:
|
||||||
|
get:
|
||||||
|
return _focus_tile
|
||||||
|
|
||||||
|
var _focus_tile: AdtTileCoordinate
|
||||||
|
var _wanted_tile_keys: Dictionary
|
||||||
|
var _retained_tile_keys: Dictionary
|
||||||
|
|
||||||
|
|
||||||
|
## Takes ownership of planner-created sets and freezes them. The planner creates
|
||||||
|
## these dictionaries independently from its catalog input, so no copy is needed.
|
||||||
|
func _init(
|
||||||
|
focus_tile_value: AdtTileCoordinate,
|
||||||
|
wanted_tile_keys_value: Dictionary,
|
||||||
|
retained_tile_keys_value: Dictionary
|
||||||
|
) -> void:
|
||||||
|
_focus_tile = focus_tile_value
|
||||||
|
_wanted_tile_keys = wanted_tile_keys_value
|
||||||
|
_retained_tile_keys = retained_tile_keys_value
|
||||||
|
_wanted_tile_keys.make_read_only()
|
||||||
|
_retained_tile_keys.make_read_only()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the immutable wanted tile-key set.
|
||||||
|
func wanted_tile_keys() -> Dictionary:
|
||||||
|
return _wanted_tile_keys
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the immutable retained tile-key set.
|
||||||
|
func retained_tile_keys() -> Dictionary:
|
||||||
|
return _retained_tile_keys
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://fq1ax8tdju2q
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
class_name StreamingTargetPlanner
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Scene-free deterministic calculator for runtime/editor ADT target sets.
|
||||||
|
## It creates no tasks or renderer resources and never mutates its inputs.
|
||||||
|
|
||||||
|
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
||||||
|
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
|
||||||
|
const TARGET_PLAN_SCRIPT := preload("res://src/render/streaming/streaming_target_plan.gd")
|
||||||
|
|
||||||
|
|
||||||
|
## Builds runtime wanted and retained sets, including the existing edge/corner
|
||||||
|
## prefetch centers. `available_tile_paths_by_key` is read only for membership.
|
||||||
|
func plan_runtime(
|
||||||
|
available_tile_paths_by_key: Dictionary,
|
||||||
|
focus_position: GodotWorldPosition,
|
||||||
|
policy: StreamingTargetPolicy
|
||||||
|
) -> StreamingTargetPlan:
|
||||||
|
var focus_tile = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(focus_position)
|
||||||
|
var prefetch_centers := _prefetch_centers(focus_position, focus_tile, policy)
|
||||||
|
var wanted_tile_keys := _available_keys_within_radius(
|
||||||
|
available_tile_paths_by_key,
|
||||||
|
prefetch_centers,
|
||||||
|
policy.load_tile_radius()
|
||||||
|
)
|
||||||
|
var retained_tile_keys := _available_keys_within_radius(
|
||||||
|
available_tile_paths_by_key,
|
||||||
|
prefetch_centers,
|
||||||
|
policy.retained_tile_radius()
|
||||||
|
)
|
||||||
|
return TARGET_PLAN_SCRIPT.new(focus_tile, wanted_tile_keys, retained_tile_keys)
|
||||||
|
|
||||||
|
|
||||||
|
## Builds the editor square preview set. Editor wanted and retained sets are
|
||||||
|
## identical and a negative radius is clamped to the focus tile.
|
||||||
|
func plan_editor(
|
||||||
|
available_tile_paths_by_key: Dictionary,
|
||||||
|
focus_position: GodotWorldPosition,
|
||||||
|
editor_preview_tile_radius: int
|
||||||
|
) -> StreamingTargetPlan:
|
||||||
|
var focus_tile = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(focus_position)
|
||||||
|
var centers: Array[AdtTileCoordinate] = [focus_tile]
|
||||||
|
var wanted_tile_keys := _available_keys_within_radius(
|
||||||
|
available_tile_paths_by_key,
|
||||||
|
centers,
|
||||||
|
maxi(0, editor_preview_tile_radius)
|
||||||
|
)
|
||||||
|
return TARGET_PLAN_SCRIPT.new(focus_tile, wanted_tile_keys, wanted_tile_keys)
|
||||||
|
|
||||||
|
|
||||||
|
func _prefetch_centers(
|
||||||
|
focus_position: GodotWorldPosition,
|
||||||
|
focus_tile: AdtTileCoordinate,
|
||||||
|
policy: StreamingTargetPolicy
|
||||||
|
) -> Array[AdtTileCoordinate]:
|
||||||
|
var centers: Array[AdtTileCoordinate] = [focus_tile]
|
||||||
|
var threshold := policy.clamped_boundary_prefetch_threshold()
|
||||||
|
if threshold <= 0.0:
|
||||||
|
return centers
|
||||||
|
|
||||||
|
var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(focus_position)
|
||||||
|
var local_x_fraction: float = tile_local_position.east_yards / COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
|
||||||
|
var local_z_fraction: float = tile_local_position.south_yards / COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
|
||||||
|
var tile_x_offsets: Array[int] = [0]
|
||||||
|
var tile_y_offsets: Array[int] = [0]
|
||||||
|
|
||||||
|
if local_x_fraction <= threshold:
|
||||||
|
tile_x_offsets.append(-1)
|
||||||
|
elif local_x_fraction >= 1.0 - threshold:
|
||||||
|
tile_x_offsets.append(1)
|
||||||
|
|
||||||
|
if local_z_fraction <= threshold:
|
||||||
|
tile_y_offsets.append(-1)
|
||||||
|
elif local_z_fraction >= 1.0 - threshold:
|
||||||
|
tile_y_offsets.append(1)
|
||||||
|
|
||||||
|
for tile_x_offset in tile_x_offsets:
|
||||||
|
for tile_y_offset in tile_y_offsets:
|
||||||
|
if tile_x_offset == 0 and tile_y_offset == 0:
|
||||||
|
continue
|
||||||
|
centers.append(ADT_TILE_COORDINATE_SCRIPT.new(
|
||||||
|
focus_tile.tile_x + tile_x_offset,
|
||||||
|
focus_tile.tile_y + tile_y_offset
|
||||||
|
))
|
||||||
|
return centers
|
||||||
|
|
||||||
|
|
||||||
|
func _available_keys_within_radius(
|
||||||
|
available_tile_paths_by_key: Dictionary,
|
||||||
|
centers: Array[AdtTileCoordinate],
|
||||||
|
tile_radius: int
|
||||||
|
) -> Dictionary:
|
||||||
|
var available_tile_keys: Dictionary = {}
|
||||||
|
for center in centers:
|
||||||
|
for tile_y in range(center.tile_y - tile_radius, center.tile_y + tile_radius + 1):
|
||||||
|
for tile_x in range(center.tile_x - tile_radius, center.tile_x + tile_radius + 1):
|
||||||
|
var tile_key := _tile_key(tile_x, tile_y)
|
||||||
|
if available_tile_paths_by_key.has(tile_key):
|
||||||
|
available_tile_keys[tile_key] = true
|
||||||
|
return available_tile_keys
|
||||||
|
|
||||||
|
|
||||||
|
func _tile_key(tile_x: int, tile_y: int) -> String:
|
||||||
|
return "%d_%d" % [tile_x, tile_y]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://crqjbgoe3f6ea
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
class_name StreamingTargetPolicy
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Immutable runtime tile-radius and boundary-prefetch policy consumed by
|
||||||
|
## [StreamingTargetPlanner]. Values preserve scene configuration exactly;
|
||||||
|
## effective radii and thresholds apply the existing renderer clamps.
|
||||||
|
|
||||||
|
const CHUNKS_PER_ADT_TILE := 16
|
||||||
|
|
||||||
|
var lod2_radius_chunks: int:
|
||||||
|
get:
|
||||||
|
return _lod2_radius_chunks
|
||||||
|
|
||||||
|
var lod2_tile_radius: int:
|
||||||
|
get:
|
||||||
|
return _lod2_tile_radius
|
||||||
|
|
||||||
|
var warm_tile_margin: int:
|
||||||
|
get:
|
||||||
|
return _warm_tile_margin
|
||||||
|
|
||||||
|
var prewarm_tile_margin: int:
|
||||||
|
get:
|
||||||
|
return _prewarm_tile_margin
|
||||||
|
|
||||||
|
var retain_tile_margin: int:
|
||||||
|
get:
|
||||||
|
return _retain_tile_margin
|
||||||
|
|
||||||
|
var boundary_prefetch_threshold: float:
|
||||||
|
get:
|
||||||
|
return _boundary_prefetch_threshold
|
||||||
|
|
||||||
|
var _lod2_radius_chunks: int
|
||||||
|
var _lod2_tile_radius: int
|
||||||
|
var _warm_tile_margin: int
|
||||||
|
var _prewarm_tile_margin: int
|
||||||
|
var _retain_tile_margin: int
|
||||||
|
var _boundary_prefetch_threshold: float
|
||||||
|
|
||||||
|
|
||||||
|
## Captures one renderer configuration snapshot for a deterministic plan.
|
||||||
|
func _init(
|
||||||
|
lod2_radius_chunks_value: int,
|
||||||
|
lod2_tile_radius_value: int,
|
||||||
|
warm_tile_margin_value: int,
|
||||||
|
prewarm_tile_margin_value: int,
|
||||||
|
retain_tile_margin_value: int,
|
||||||
|
boundary_prefetch_threshold_value: float
|
||||||
|
) -> void:
|
||||||
|
_lod2_radius_chunks = lod2_radius_chunks_value
|
||||||
|
_lod2_tile_radius = lod2_tile_radius_value
|
||||||
|
_warm_tile_margin = warm_tile_margin_value
|
||||||
|
_prewarm_tile_margin = prewarm_tile_margin_value
|
||||||
|
_retain_tile_margin = retain_tile_margin_value
|
||||||
|
_boundary_prefetch_threshold = boundary_prefetch_threshold_value
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the current visible ADT radius derived from chunk and tile settings.
|
||||||
|
func visible_tile_radius() -> int:
|
||||||
|
return calculate_visible_tile_radius(_lod2_radius_chunks, _lod2_tile_radius, _warm_tile_margin)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the visible radius plus the non-negative prewarm margin.
|
||||||
|
func load_tile_radius() -> int:
|
||||||
|
return calculate_load_tile_radius(
|
||||||
|
_lod2_radius_chunks,
|
||||||
|
_lod2_tile_radius,
|
||||||
|
_warm_tile_margin,
|
||||||
|
_prewarm_tile_margin
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the load radius plus the non-negative hysteresis margin.
|
||||||
|
func retained_tile_radius() -> int:
|
||||||
|
return load_tile_radius() + maxi(0, _retain_tile_margin)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the edge fraction accepted by the existing prefetch policy.
|
||||||
|
func clamped_boundary_prefetch_threshold() -> float:
|
||||||
|
return clampf(_boundary_prefetch_threshold, 0.0, 0.49)
|
||||||
|
|
||||||
|
|
||||||
|
## Calculates the visible radius without allocating a policy. The streamer uses
|
||||||
|
## this hot-path form while finalizing tiles between planning refreshes.
|
||||||
|
static func calculate_visible_tile_radius(
|
||||||
|
lod2_radius_chunks_value: int,
|
||||||
|
lod2_tile_radius_value: int,
|
||||||
|
warm_tile_margin_value: int
|
||||||
|
) -> int:
|
||||||
|
var chunk_tile_radius := int(ceil(
|
||||||
|
float(lod2_radius_chunks_value) / float(CHUNKS_PER_ADT_TILE)
|
||||||
|
)) + warm_tile_margin_value
|
||||||
|
return maxi(chunk_tile_radius, lod2_tile_radius_value)
|
||||||
|
|
||||||
|
|
||||||
|
## Calculates the load radius without allocating a policy.
|
||||||
|
static func calculate_load_tile_radius(
|
||||||
|
lod2_radius_chunks_value: int,
|
||||||
|
lod2_tile_radius_value: int,
|
||||||
|
warm_tile_margin_value: int,
|
||||||
|
prewarm_tile_margin_value: int
|
||||||
|
) -> int:
|
||||||
|
return calculate_visible_tile_radius(
|
||||||
|
lod2_radius_chunks_value,
|
||||||
|
lod2_tile_radius_value,
|
||||||
|
warm_tile_margin_value
|
||||||
|
) + maxi(0, prewarm_tile_margin_value)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cfrad5wr668w5
|
||||||
@@ -17,6 +17,8 @@ const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot
|
|||||||
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
||||||
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
|
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
|
||||||
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 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 REQUIRED_BAKED_TILE_FORMAT_VERSION := 5
|
const REQUIRED_BAKED_TILE_FORMAT_VERSION := 5
|
||||||
const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1
|
const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1
|
||||||
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
|
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
|
||||||
@@ -174,6 +176,7 @@ const QUALITY_HIGH := "High"
|
|||||||
@export var hitch_profiler_threshold_ms: float = 20.0
|
@export var hitch_profiler_threshold_ms: float = 20.0
|
||||||
|
|
||||||
var _builder
|
var _builder
|
||||||
|
var _streaming_target_planner := STREAMING_TARGET_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 = {}
|
||||||
@@ -720,57 +723,47 @@ func _refresh_editor_streaming_targets_at(focus_pos: Vector3, force: bool) -> vo
|
|||||||
if not force and not _should_refresh_focus(focus_pos):
|
if not force and not _should_refresh_focus(focus_pos):
|
||||||
return
|
return
|
||||||
|
|
||||||
var focus_tile := _world_to_tile(focus_pos)
|
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(focus_pos.x, focus_pos.y, focus_pos.z)
|
||||||
var total_tile_radius: int = maxi(0, editor_preview_tile_radius)
|
var target_plan = _streaming_target_planner.plan_editor(
|
||||||
|
_available_tiles,
|
||||||
|
typed_focus_position,
|
||||||
|
editor_preview_tile_radius
|
||||||
|
)
|
||||||
|
var wanted_tiles: Dictionary = target_plan.wanted_tile_keys()
|
||||||
|
|
||||||
var wanted_tiles: Dictionary = {}
|
_apply_streaming_target(wanted_tiles, target_plan.retained_tile_keys(), focus_pos)
|
||||||
for ty in range(focus_tile.y - total_tile_radius, focus_tile.y + total_tile_radius + 1):
|
|
||||||
for tx in range(focus_tile.x - total_tile_radius, focus_tile.x + total_tile_radius + 1):
|
|
||||||
var key := _tile_key(tx, ty)
|
|
||||||
if _available_tiles.has(key):
|
|
||||||
wanted_tiles[key] = true
|
|
||||||
|
|
||||||
_apply_streaming_target(wanted_tiles, wanted_tiles, focus_pos)
|
|
||||||
_note_refresh_focus(focus_pos)
|
_note_refresh_focus(focus_pos)
|
||||||
|
|
||||||
if force or debug_streaming:
|
if force or debug_streaming:
|
||||||
print("StreamingWorld(editor): focus_tile=%s wanted=%d queued=%d" % [
|
print("StreamingWorld(editor): focus_tile=%s wanted=%d queued=%d" % [
|
||||||
focus_tile, wanted_tiles.size(), _tile_load_queue.size()])
|
Vector2i(target_plan.focus_tile.tile_x, target_plan.focus_tile.tile_y),
|
||||||
|
wanted_tiles.size(),
|
||||||
|
_tile_load_queue.size(),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
func _refresh_streaming_targets_at(focus_pos: Vector3, force: bool) -> void:
|
func _refresh_streaming_targets_at(focus_pos: Vector3, force: bool) -> void:
|
||||||
if not force and not _should_refresh_focus(focus_pos):
|
if not force and not _should_refresh_focus(focus_pos):
|
||||||
return
|
return
|
||||||
|
|
||||||
var focus_tile := _world_to_tile(focus_pos)
|
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(focus_pos.x, focus_pos.y, focus_pos.z)
|
||||||
|
var target_plan = _streaming_target_planner.plan_runtime(
|
||||||
# Chunk-LOD radius → tile radius (ceiling), plus margin for pre-warming.
|
_available_tiles,
|
||||||
var focus_tiles := _predictive_focus_tiles(focus_pos)
|
typed_focus_position,
|
||||||
var total_tile_radius: int = _runtime_load_tile_radius()
|
_streaming_target_policy()
|
||||||
var retain_tile_radius: int = _runtime_retain_tile_radius()
|
)
|
||||||
|
var wanted_tiles: Dictionary = target_plan.wanted_tile_keys()
|
||||||
var retained_tiles: Dictionary = {}
|
var retained_tiles: Dictionary = target_plan.retained_tile_keys()
|
||||||
for center in focus_tiles:
|
|
||||||
for ty in range(center.y - retain_tile_radius, center.y + retain_tile_radius + 1):
|
|
||||||
for tx in range(center.x - retain_tile_radius, center.x + retain_tile_radius + 1):
|
|
||||||
var retained_key := _tile_key(tx, ty)
|
|
||||||
if _available_tiles.has(retained_key):
|
|
||||||
retained_tiles[retained_key] = true
|
|
||||||
|
|
||||||
var wanted_tiles: Dictionary = {}
|
|
||||||
for center in focus_tiles:
|
|
||||||
for ty in range(center.y - total_tile_radius, center.y + total_tile_radius + 1):
|
|
||||||
for tx in range(center.x - total_tile_radius, center.x + total_tile_radius + 1):
|
|
||||||
var key := _tile_key(tx, ty)
|
|
||||||
if _available_tiles.has(key):
|
|
||||||
wanted_tiles[key] = true
|
|
||||||
|
|
||||||
_apply_streaming_target(wanted_tiles, retained_tiles, focus_pos)
|
_apply_streaming_target(wanted_tiles, retained_tiles, focus_pos)
|
||||||
_note_refresh_focus(focus_pos)
|
_note_refresh_focus(focus_pos)
|
||||||
|
|
||||||
if force or debug_streaming:
|
if force or debug_streaming:
|
||||||
print("StreamingWorld: focus_tile=%s wanted=%d queued=%d" % [
|
print("StreamingWorld: focus_tile=%s wanted=%d queued=%d" % [
|
||||||
focus_tile, wanted_tiles.size(), _tile_load_queue.size()])
|
Vector2i(target_plan.focus_tile.tile_x, target_plan.focus_tile.tile_y),
|
||||||
|
wanted_tiles.size(),
|
||||||
|
_tile_load_queue.size(),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -2891,50 +2884,24 @@ func _compute_desired_lods(state: Dictionary, cam_pos: Vector3) -> Dictionary:
|
|||||||
return desired
|
return desired
|
||||||
|
|
||||||
|
|
||||||
func _runtime_visible_tile_radius() -> int:
|
|
||||||
var chunk_tile_radius: int = int(ceil(float(lod2_radius_chunks) / 16.0)) + warm_tile_margin
|
|
||||||
return maxi(chunk_tile_radius, lod2_tile_radius)
|
|
||||||
|
|
||||||
|
|
||||||
func _runtime_load_tile_radius() -> int:
|
func _runtime_load_tile_radius() -> int:
|
||||||
return _runtime_visible_tile_radius() + maxi(0, prewarm_tile_margin)
|
return STREAMING_TARGET_POLICY_SCRIPT.calculate_load_tile_radius(
|
||||||
|
lod2_radius_chunks,
|
||||||
|
lod2_tile_radius,
|
||||||
|
warm_tile_margin,
|
||||||
|
prewarm_tile_margin
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _runtime_retain_tile_radius() -> int:
|
func _streaming_target_policy() -> StreamingTargetPolicy:
|
||||||
return _runtime_load_tile_radius() + maxi(0, retain_tile_margin)
|
return STREAMING_TARGET_POLICY_SCRIPT.new(
|
||||||
|
lod2_radius_chunks,
|
||||||
|
lod2_tile_radius,
|
||||||
func _predictive_focus_tiles(focus_pos: Vector3) -> Array[Vector2i]:
|
warm_tile_margin,
|
||||||
var base := _world_to_tile(focus_pos)
|
prewarm_tile_margin,
|
||||||
var result: Array[Vector2i] = [base]
|
retain_tile_margin,
|
||||||
var threshold: float = clampf(boundary_prefetch_threshold, 0.0, 0.49)
|
boundary_prefetch_threshold
|
||||||
if threshold <= 0.0:
|
)
|
||||||
return result
|
|
||||||
|
|
||||||
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(focus_pos.x, focus_pos.y, focus_pos.z)
|
|
||||||
var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(typed_focus_position)
|
|
||||||
var local_x: float = tile_local_position.east_yards / TILE_SIZE
|
|
||||||
var local_z: float = tile_local_position.south_yards / TILE_SIZE
|
|
||||||
var x_offsets: Array[int] = [0]
|
|
||||||
var z_offsets: Array[int] = [0]
|
|
||||||
|
|
||||||
if local_x <= threshold:
|
|
||||||
x_offsets.append(-1)
|
|
||||||
elif local_x >= 1.0 - threshold:
|
|
||||||
x_offsets.append(1)
|
|
||||||
|
|
||||||
if local_z <= threshold:
|
|
||||||
z_offsets.append(-1)
|
|
||||||
elif local_z >= 1.0 - threshold:
|
|
||||||
z_offsets.append(1)
|
|
||||||
|
|
||||||
for ox in x_offsets:
|
|
||||||
for oz in z_offsets:
|
|
||||||
if ox == 0 and oz == 0:
|
|
||||||
continue
|
|
||||||
result.append(Vector2i(base.x + ox, base.y + oz))
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
func _compute_desired_tile_lod(state: Dictionary, cam_pos: Vector3) -> int:
|
func _compute_desired_tile_lod(state: Dictionary, cam_pos: Vector3) -> int:
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ const ALLOWED_LEGACY_NAME_PATHS: Array[String] = [
|
|||||||
]
|
]
|
||||||
const REQUIRED_MAPPER_CALLS := {
|
const REQUIRED_MAPPER_CALLS := {
|
||||||
"res://src/scenes/sky/wow_sky_controller.gd": ["godot_to_canonical", "godot_to_adt_tile", "godot_to_adt_tile_local"],
|
"res://src/scenes/sky/wow_sky_controller.gd": ["godot_to_canonical", "godot_to_adt_tile", "godot_to_adt_tile_local"],
|
||||||
"res://src/scenes/streaming/streaming_world_loader.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local", "adt_tile_local_to_godot"],
|
"res://src/scenes/streaming/streaming_world_loader.gd": ["godot_to_adt_tile", "adt_tile_local_to_godot"],
|
||||||
|
"res://src/render/streaming/streaming_target_planner.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local"],
|
||||||
"res://src/scenes/player/third_person_wow_controller.gd": ["adt_tile_local_to_godot"],
|
"res://src/scenes/player/third_person_wow_controller.gd": ["adt_tile_local_to_godot"],
|
||||||
"res://src/gameplay/terrain/adt_terrain_query.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local", "adt_tile_local_to_chunk"],
|
"res://src/gameplay/terrain/adt_terrain_query.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local", "adt_tile_local_to_chunk"],
|
||||||
"res://src/tools/probe_render_terrain_height.gd": ["godot_to_adt_tile"],
|
"res://src/tools/probe_render_terrain_height.gd": ["godot_to_adt_tile"],
|
||||||
@@ -60,7 +61,7 @@ func _initialize() -> void:
|
|||||||
quit(1)
|
quit(1)
|
||||||
return
|
return
|
||||||
|
|
||||||
print("COORDINATE_CONVERSION_BOUNDARIES PASS files=%d native_boundary=1 oracle=1 consumers=5 classifier_cases=6" % source_paths.size())
|
print("COORDINATE_CONVERSION_BOUNDARIES PASS files=%d native_boundary=1 oracle=1 consumers=6 classifier_cases=6" % source_paths.size())
|
||||||
quit(0)
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Asset-free M03 regression for pure runtime/editor ADT target planning.
|
||||||
|
|
||||||
|
const PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
|
||||||
|
const POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
|
||||||
|
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
||||||
|
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
|
||||||
|
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")
|
||||||
|
|
||||||
|
const PLANNER_PATH := "res://src/render/streaming/streaming_target_planner.gd"
|
||||||
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||||
|
const PERFORMANCE_ITERATIONS := 250
|
||||||
|
const MAXIMUM_AVERAGE_PLAN_MILLISECONDS := 4.0
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var failures: Array[String] = []
|
||||||
|
var planner = PLANNER_SCRIPT.new()
|
||||||
|
_verify_center_radii_and_isolation(planner, failures)
|
||||||
|
_verify_corner_prefetch_and_catalog_filter(planner, failures)
|
||||||
|
_verify_policy_clamps(planner, failures)
|
||||||
|
_verify_editor_plan(planner, failures)
|
||||||
|
_verify_scene_free_extraction(failures)
|
||||||
|
var elapsed_milliseconds := _measure_planner_iterations(planner, failures)
|
||||||
|
|
||||||
|
if not failures.is_empty():
|
||||||
|
for failure in failures:
|
||||||
|
push_error("STREAMING_TARGET_PLANNER: %s" % failure)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
print("STREAMING_TARGET_PLANNER PASS cases=5 iterations=%d elapsed_ms=%.3f average_ms=%.3f" % [
|
||||||
|
PERFORMANCE_ITERATIONS,
|
||||||
|
elapsed_milliseconds,
|
||||||
|
elapsed_milliseconds / float(PERFORMANCE_ITERATIONS),
|
||||||
|
])
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_center_radii_and_isolation(planner, failures: Array[String]) -> void:
|
||||||
|
var available_tiles := _tile_catalog(28, 36, 44, 52)
|
||||||
|
var focus_position = _tile_position(32, 48, 0.5, 0.5)
|
||||||
|
var policy = POLICY_SCRIPT.new(0, 1, 0, 1, 1, 0.35)
|
||||||
|
var plan = planner.plan_runtime(available_tiles, focus_position, policy)
|
||||||
|
var wanted_tile_keys: Dictionary = plan.wanted_tile_keys()
|
||||||
|
var retained_tile_keys: Dictionary = plan.retained_tile_keys()
|
||||||
|
|
||||||
|
_expect_equal(plan.focus_tile.tile_x, 32, "center focus tile X", failures)
|
||||||
|
_expect_equal(plan.focus_tile.tile_y, 48, "center focus tile Y", failures)
|
||||||
|
_expect_equal(wanted_tile_keys.size(), 25, "center wanted 5x5", failures)
|
||||||
|
_expect_equal(retained_tile_keys.size(), 49, "center retained 7x7", failures)
|
||||||
|
_expect_true(wanted_tile_keys.has("30_46"), "wanted includes north-west radius edge", failures)
|
||||||
|
_expect_true(wanted_tile_keys.has("34_50"), "wanted includes south-east radius edge", failures)
|
||||||
|
_expect_true(retained_tile_keys.has("29_45"), "retained includes hysteresis edge", failures)
|
||||||
|
|
||||||
|
_expect_true(wanted_tile_keys.is_read_only(), "wanted result is immutable", failures)
|
||||||
|
_expect_true(retained_tile_keys.is_read_only(), "retained result is immutable", failures)
|
||||||
|
_expect_equal(available_tiles.size(), 81, "available catalog remains unchanged", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_corner_prefetch_and_catalog_filter(planner, failures: Array[String]) -> void:
|
||||||
|
var available_tiles := {
|
||||||
|
"32_48": "tile-a",
|
||||||
|
"33_48": "tile-b",
|
||||||
|
"32_49": "tile-c",
|
||||||
|
"33_49": "tile-d",
|
||||||
|
"31_48": "outside-prefetch",
|
||||||
|
}
|
||||||
|
var focus_position = _tile_position(32, 48, 0.99, 0.99)
|
||||||
|
var policy = POLICY_SCRIPT.new(0, 0, 0, 0, 0, 0.35)
|
||||||
|
var plan = planner.plan_runtime(available_tiles, focus_position, policy)
|
||||||
|
_expect_set_equal(
|
||||||
|
plan.wanted_tile_keys(),
|
||||||
|
["32_48", "33_48", "32_49", "33_49"],
|
||||||
|
"corner prefetch cross product",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_set_equal(
|
||||||
|
plan.retained_tile_keys(),
|
||||||
|
["32_48", "33_48", "32_49", "33_49"],
|
||||||
|
"corner retained cross product",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_policy_clamps(planner, failures: Array[String]) -> void:
|
||||||
|
var policy = POLICY_SCRIPT.new(16, 2, -5, -3, -4, 0.9)
|
||||||
|
_expect_equal(policy.visible_tile_radius(), 2, "visible radius keeps tile minimum", failures)
|
||||||
|
_expect_equal(policy.load_tile_radius(), 2, "negative prewarm clamps to zero", failures)
|
||||||
|
_expect_equal(policy.retained_tile_radius(), 2, "negative retain clamps to zero", failures)
|
||||||
|
_expect_near(policy.clamped_boundary_prefetch_threshold(), 0.49, "threshold upper clamp", failures)
|
||||||
|
|
||||||
|
var plan = planner.plan_runtime(
|
||||||
|
_tile_catalog(30, 34, 46, 50),
|
||||||
|
_tile_position(32, 48, 0.5, 0.5),
|
||||||
|
policy
|
||||||
|
)
|
||||||
|
_expect_equal(plan.wanted_tile_keys().size(), 25, "clamped policy wanted radius", failures)
|
||||||
|
_expect_equal(plan.retained_tile_keys().size(), 25, "clamped policy retained radius", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_editor_plan(planner, failures: Array[String]) -> void:
|
||||||
|
var available_tiles := _tile_catalog(31, 33, 47, 49)
|
||||||
|
var focus_position = _tile_position(32, 48, 0.5, 0.5)
|
||||||
|
var negative_radius_plan = planner.plan_editor(available_tiles, focus_position, -4)
|
||||||
|
_expect_set_equal(
|
||||||
|
negative_radius_plan.wanted_tile_keys(),
|
||||||
|
["32_48"],
|
||||||
|
"editor negative radius clamps to focus",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_set_equal(
|
||||||
|
negative_radius_plan.retained_tile_keys(),
|
||||||
|
["32_48"],
|
||||||
|
"editor retained matches wanted",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
|
||||||
|
var radius_one_plan = planner.plan_editor(available_tiles, focus_position, 1)
|
||||||
|
_expect_equal(radius_one_plan.wanted_tile_keys().size(), 9, "editor radius one 3x3", failures)
|
||||||
|
_expect_set_equal(
|
||||||
|
radius_one_plan.retained_tile_keys(),
|
||||||
|
radius_one_plan.wanted_tile_keys().keys(),
|
||||||
|
"editor radius retained matches wanted",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_scene_free_extraction(failures: Array[String]) -> void:
|
||||||
|
var planner_source := _read_text(PLANNER_PATH, failures)
|
||||||
|
_expect_true(planner_source.contains("extends RefCounted"), "planner is RefCounted", failures)
|
||||||
|
_expect_true(not planner_source.contains("extends Node"), "planner does not inherit Node", failures)
|
||||||
|
_expect_true(not planner_source.contains("Vector3"), "planner keeps typed position boundary", failures)
|
||||||
|
for forbidden_text in ["WorkerThreadPool", "RenderingServer", "ResourceLoader", "_tile_load_queue"]:
|
||||||
|
_expect_true(not planner_source.contains(forbidden_text), "planner omits %s" % forbidden_text, failures)
|
||||||
|
|
||||||
|
var loader_source := _read_text(LOADER_PATH, failures)
|
||||||
|
_expect_true(loader_source.contains("_streaming_target_planner.plan_runtime"), "loader delegates runtime planning", failures)
|
||||||
|
_expect_true(loader_source.contains("_streaming_target_planner.plan_editor"), "loader delegates editor planning", failures)
|
||||||
|
_expect_true(not loader_source.contains("func _predictive_focus_tiles"), "loader no longer owns prefetch planner", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _measure_planner_iterations(planner, failures: Array[String]) -> float:
|
||||||
|
var available_tiles := _tile_catalog(24, 40, 40, 56)
|
||||||
|
var focus_position = _tile_position(32, 48, 0.99, 0.99)
|
||||||
|
var policy = POLICY_SCRIPT.new(40, 5, 1, 3, 4, 0.42)
|
||||||
|
var started_microseconds := Time.get_ticks_usec()
|
||||||
|
for _iteration in PERFORMANCE_ITERATIONS:
|
||||||
|
planner.plan_runtime(available_tiles, focus_position, policy)
|
||||||
|
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||||
|
var average_plan_milliseconds := elapsed_milliseconds / float(PERFORMANCE_ITERATIONS)
|
||||||
|
_expect_true(
|
||||||
|
average_plan_milliseconds <= MAXIMUM_AVERAGE_PLAN_MILLISECONDS,
|
||||||
|
"average plan stays within %.1f ms (actual %.3f)" % [
|
||||||
|
MAXIMUM_AVERAGE_PLAN_MILLISECONDS,
|
||||||
|
average_plan_milliseconds,
|
||||||
|
],
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
return elapsed_milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
func _tile_catalog(minimum_tile_x: int, maximum_tile_x: int, minimum_tile_y: int, maximum_tile_y: int) -> Dictionary:
|
||||||
|
var available_tiles: Dictionary = {}
|
||||||
|
for tile_y in range(minimum_tile_y, maximum_tile_y + 1):
|
||||||
|
for tile_x in range(minimum_tile_x, maximum_tile_x + 1):
|
||||||
|
available_tiles["%d_%d" % [tile_x, tile_y]] = "synthetic"
|
||||||
|
return available_tiles
|
||||||
|
|
||||||
|
|
||||||
|
func _tile_position(tile_x: int, tile_y: int, east_fraction: float, south_fraction: float) -> GodotWorldPosition:
|
||||||
|
var tile_coordinate = ADT_TILE_COORDINATE_SCRIPT.new(tile_x, tile_y)
|
||||||
|
var tile_local_position = ADT_TILE_LOCAL_POSITION_SCRIPT.new(
|
||||||
|
east_fraction * COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS,
|
||||||
|
0.0,
|
||||||
|
south_fraction * COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
|
||||||
|
)
|
||||||
|
return COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(tile_coordinate, tile_local_position)
|
||||||
|
|
||||||
|
|
||||||
|
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_set_equal(actual_set: Dictionary, expected_keys: Array, label: String, failures: Array[String]) -> void:
|
||||||
|
var actual_keys := actual_set.keys()
|
||||||
|
actual_keys.sort()
|
||||||
|
var sorted_expected_keys := expected_keys.duplicate()
|
||||||
|
sorted_expected_keys.sort()
|
||||||
|
if actual_keys != sorted_expected_keys:
|
||||||
|
failures.append("%s expected %s, got %s" % [label, sorted_expected_keys, actual_keys])
|
||||||
|
|
||||||
|
|
||||||
|
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_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
if not actual_value:
|
||||||
|
failures.append("%s expected true" % label)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b5unt23vi8krk
|
||||||
Reference in New Issue
Block a user