From 0c24f96ae1df9d070ac89e33843afa609faed703 Mon Sep 17 00:00:00 2001 From: sindoring Date: Wed, 15 Jul 2026 23:58:57 +0400 Subject: [PATCH 1/3] rnd(M03): claim streaming target planner Work-Package: M03-RND-STREAMING-PLANNER-001 Agent: sindo-main-codex Tests: not run (coordination-only commit) Fidelity: no runtime change --- .../claims/M03-RND-STREAMING-PLANNER-001.md | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 coordination/claims/M03-RND-STREAMING-PLANNER-001.md diff --git a/coordination/claims/M03-RND-STREAMING-PLANNER-001.md b/coordination/claims/M03-RND-STREAMING-PLANNER-001.md new file mode 100644 index 0000000..8389ea0 --- /dev/null +++ b/coordination/claims/M03-RND-STREAMING-PLANNER-001.md @@ -0,0 +1,95 @@ +# M03-RND-STREAMING-PLANNER-001 — Pure streaming target planner + + + +## 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`, + `docs/modules/world-renderer.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 `_` 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: active +- Done: claim and isolated worktree created +- Next: implement pure contracts, migrate runtime/editor refresh, verify +- Blocked by: + +## Handoff + +- Commit: +- Results: +- Remaining risks: +- Documentation updated: From e887c3bad5a0eb069e567a6981659b98cd745280 Mon Sep 17 00:00:00 2001 From: sindoring Date: Thu, 16 Jul 2026 00:08:02 +0400 Subject: [PATCH 2/3] rnd(M03): extract streaming target planner Work-Package: M03-RND-STREAMING-PLANNER-001 Agent: sindo-main-codex Tests: planner/facade/focus/coordinate/manifest/shutdown contracts, seven-checkpoint dry-run, repository gates Fidelity: preserves wanted/retained radii, boundary prefetch, editor preview, queue priority and cache/render behavior --- RENDER.md | 14 + .../claims/M03-RND-STREAMING-PLANNER-001.md | 4 +- docs/modules/README.md | 1 + docs/modules/coordinate-mapping.md | 4 + docs/modules/streaming-target-planner.md | 239 ++++++++++++++++++ docs/modules/world-renderer.md | 29 ++- src/render/streaming/streaming_target_plan.gd | 37 +++ .../streaming/streaming_target_plan.gd.uid | 1 + .../streaming/streaming_target_planner.gd | 104 ++++++++ .../streaming/streaming_target_planner.gd.uid | 1 + .../streaming/streaming_target_policy.gd | 108 ++++++++ .../streaming/streaming_target_policy.gd.uid | 1 + .../streaming/streaming_world_loader.gd | 117 +++------ ...verify_coordinate_conversion_boundaries.gd | 5 +- src/tools/verify_streaming_target_planner.gd | 211 ++++++++++++++++ .../verify_streaming_target_planner.gd.uid | 1 + 16 files changed, 793 insertions(+), 84 deletions(-) create mode 100644 docs/modules/streaming-target-planner.md create mode 100644 src/render/streaming/streaming_target_plan.gd create mode 100644 src/render/streaming/streaming_target_plan.gd.uid create mode 100644 src/render/streaming/streaming_target_planner.gd create mode 100644 src/render/streaming/streaming_target_planner.gd.uid create mode 100644 src/render/streaming/streaming_target_policy.gd create mode 100644 src/render/streaming/streaming_target_policy.gd.uid create mode 100644 src/tools/verify_streaming_target_planner.gd create mode 100644 src/tools/verify_streaming_target_planner.gd.uid diff --git a/RENDER.md b/RENDER.md index e71fbb1..53fd503 100644 --- a/RENDER.md +++ b/RENDER.md @@ -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 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 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. diff --git a/coordination/claims/M03-RND-STREAMING-PLANNER-001.md b/coordination/claims/M03-RND-STREAMING-PLANNER-001.md index 8389ea0..6138127 100644 --- a/coordination/claims/M03-RND-STREAMING-PLANNER-001.md +++ b/coordination/claims/M03-RND-STREAMING-PLANNER-001.md @@ -32,7 +32,9 @@ selection from `StreamingWorldLoader` into a scene-free, deterministic `src/tools/verify_streaming_target_planner.gd`, `docs/modules/streaming-target-planner.md`, this claim - Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`, - `docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md` + `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 diff --git a/docs/modules/README.md b/docs/modules/README.md index ac6a46a..de4fe8c 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -15,6 +15,7 @@ | Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.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) | +| 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) | | 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) | diff --git a/docs/modules/coordinate-mapping.md b/docs/modules/coordinate-mapping.md index 0c24593..fe495f9 100644 --- a/docs/modules/coordinate-mapping.md +++ b/docs/modules/coordinate-mapping.md @@ -116,9 +116,12 @@ flowchart LR Godot -->|typed direct adapter| Tile Godot -->|typed direct adapter| Local Godot --> TerrainQuery[AdtTerrainQuery consumer] + Godot --> TargetPlanner[StreamingTargetPlanner consumer] TerrainQuery --> Tile TerrainQuery --> Local TerrainQuery --> Chunk + TargetPlanner --> Tile + TargetPlanner --> Local Tile -->|tile plus local| Godot ServerYaw[ServerWorldYaw] -->|normalize only| CanonicalYaw[CanonicalWowWorldYaw] 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_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/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/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 | diff --git a/docs/modules/streaming-target-planner.md b/docs/modules/streaming-target-planner.md new file mode 100644 index 0000000..b0e59cd --- /dev/null +++ b/docs/modules/streaming-target-planner.md @@ -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 | `work/sindo-main-codex/m03-streaming-target-planner`, 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 `_` 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) diff --git a/docs/modules/world-renderer.md b/docs/modules/world-renderer.md index 247ea3d..1c6946b 100644 --- a/docs/modules/world-renderer.md +++ b/docs/modules/world-renderer.md @@ -34,6 +34,9 @@ flowchart LR Adapter --> Focus Focus --> Facade[WorldRenderFacade] Facade --> Loader[StreamingWorldLoader internal] + Loader --> Planner[StreamingTargetPlanner] + Planner --> TargetPlan[StreamingTargetPlan] + TargetPlan --> Loader Assets[Extracted WDT/ADT/M2/WMO/BLP] --> Native[Native loaders] Cache[Baked terrain/M2/WMO caches] --> Loader 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 | 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 | -| 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 | 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 | @@ -121,8 +125,9 @@ flowchart TD A --> F[StreamingFocus] EA --> F F --> RF[WorldRenderFacade] - RF --> T[StreamingWorldLoader target refresh] - WDT[WDT tile catalog] --> T + RF --> Planner[StreamingTargetPlanner] + Planner --> T[StreamingTargetPlan apply] + WDT[WDT tile catalog] --> Planner T --> Q[Tile load queue] Q --> Parse[Worker parse/cache load] Parse --> R[Result queues] @@ -136,8 +141,8 @@ flowchart TD WMO --> World Liquid --> World Marker --> World - F --> E[Eviction/retention decisions] - E --> World + F --> Eviction[Eviction/retention decisions] + Eviction --> World ``` ## Lifecycle/state @@ -166,13 +171,16 @@ sequenceDiagram participant Focus as StreamingFocus adapter participant Facade as WorldRenderFacade participant Stream as StreamingWorldLoader internal + participant Planner as StreamingTargetPlanner participant Worker as Worker task participant Budget as Main-thread budget participant Render as SceneTree/RenderingServer Source->>Focus: sample explicit Godot position Focus->>Facade: set/refresh typed focus 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 Worker-->>Stream: immutable result Stream->>Budget: enqueue finalize operations @@ -189,6 +197,8 @@ sequenceDiagram - `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 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 the latest reference and converts it to `Vector3` only inside the render boundary. - Worker tasks не должны менять SceneTree и shared Resource concurrently. @@ -249,6 +259,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m ## 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. +- 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 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. @@ -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 | | 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 | +| 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 @@ -301,10 +314,14 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | Path | Responsibility | |---|---| | `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/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_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/m2_builder.gd` | Static M2 mesh/material construction | | `addons/mpq_extractor/loaders/m2_native_animated_builder.gd` | Native animated M2 experiment | diff --git a/src/render/streaming/streaming_target_plan.gd b/src/render/streaming/streaming_target_plan.gd new file mode 100644 index 0000000..f9a85fc --- /dev/null +++ b/src/render/streaming/streaming_target_plan.gd @@ -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 `_` 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 diff --git a/src/render/streaming/streaming_target_plan.gd.uid b/src/render/streaming/streaming_target_plan.gd.uid new file mode 100644 index 0000000..de4e08e --- /dev/null +++ b/src/render/streaming/streaming_target_plan.gd.uid @@ -0,0 +1 @@ +uid://fq1ax8tdju2q diff --git a/src/render/streaming/streaming_target_planner.gd b/src/render/streaming/streaming_target_planner.gd new file mode 100644 index 0000000..dbed908 --- /dev/null +++ b/src/render/streaming/streaming_target_planner.gd @@ -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] diff --git a/src/render/streaming/streaming_target_planner.gd.uid b/src/render/streaming/streaming_target_planner.gd.uid new file mode 100644 index 0000000..bf1ab28 --- /dev/null +++ b/src/render/streaming/streaming_target_planner.gd.uid @@ -0,0 +1 @@ +uid://crqjbgoe3f6ea diff --git a/src/render/streaming/streaming_target_policy.gd b/src/render/streaming/streaming_target_policy.gd new file mode 100644 index 0000000..9fcc541 --- /dev/null +++ b/src/render/streaming/streaming_target_policy.gd @@ -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) diff --git a/src/render/streaming/streaming_target_policy.gd.uid b/src/render/streaming/streaming_target_policy.gd.uid new file mode 100644 index 0000000..297f07f --- /dev/null +++ b/src/render/streaming/streaming_target_policy.gd.uid @@ -0,0 +1 @@ +uid://cfrad5wr668w5 diff --git a/src/scenes/streaming/streaming_world_loader.gd b/src/scenes/streaming/streaming_world_loader.gd index 53fdbd5..1dbb006 100644 --- a/src/scenes/streaming/streaming_world_loader.gd +++ b/src/scenes/streaming/streaming_world_loader.gd @@ -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 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 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_SPLAT_TILE_FORMAT_VERSION := 1 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 var _builder +var _streaming_target_planner := STREAMING_TARGET_PLANNER_SCRIPT.new() var _map_dir := "" var _abs_map_dir := "" 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): return - var focus_tile := _world_to_tile(focus_pos) - var total_tile_radius: int = maxi(0, editor_preview_tile_radius) + 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_editor( + _available_tiles, + typed_focus_position, + editor_preview_tile_radius + ) + var wanted_tiles: Dictionary = target_plan.wanted_tile_keys() - var wanted_tiles: Dictionary = {} - 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) + _apply_streaming_target(wanted_tiles, target_plan.retained_tile_keys(), focus_pos) _note_refresh_focus(focus_pos) if force or debug_streaming: 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: if not force and not _should_refresh_focus(focus_pos): return - var focus_tile := _world_to_tile(focus_pos) - - # Chunk-LOD radius → tile radius (ceiling), plus margin for pre-warming. - var focus_tiles := _predictive_focus_tiles(focus_pos) - var total_tile_radius: int = _runtime_load_tile_radius() - var retain_tile_radius: int = _runtime_retain_tile_radius() - - var retained_tiles: Dictionary = {} - 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 + 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( + _available_tiles, + typed_focus_position, + _streaming_target_policy() + ) + var wanted_tiles: Dictionary = target_plan.wanted_tile_keys() + var retained_tiles: Dictionary = target_plan.retained_tile_keys() _apply_streaming_target(wanted_tiles, retained_tiles, focus_pos) _note_refresh_focus(focus_pos) if force or debug_streaming: 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 -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: - 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: - return _runtime_load_tile_radius() + maxi(0, retain_tile_margin) - - -func _predictive_focus_tiles(focus_pos: Vector3) -> Array[Vector2i]: - var base := _world_to_tile(focus_pos) - var result: Array[Vector2i] = [base] - var threshold: float = clampf(boundary_prefetch_threshold, 0.0, 0.49) - 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 _streaming_target_policy() -> StreamingTargetPolicy: + return STREAMING_TARGET_POLICY_SCRIPT.new( + lod2_radius_chunks, + lod2_tile_radius, + warm_tile_margin, + prewarm_tile_margin, + retain_tile_margin, + boundary_prefetch_threshold + ) func _compute_desired_tile_lod(state: Dictionary, cam_pos: Vector3) -> int: diff --git a/src/tools/verify_coordinate_conversion_boundaries.gd b/src/tools/verify_coordinate_conversion_boundaries.gd index 794ae77..dd2f096 100644 --- a/src/tools/verify_coordinate_conversion_boundaries.gd +++ b/src/tools/verify_coordinate_conversion_boundaries.gd @@ -19,7 +19,8 @@ const ALLOWED_LEGACY_NAME_PATHS: Array[String] = [ ] 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/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/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"], @@ -60,7 +61,7 @@ func _initialize() -> void: quit(1) 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) diff --git a/src/tools/verify_streaming_target_planner.gd b/src/tools/verify_streaming_target_planner.gd new file mode 100644 index 0000000..e60f6b1 --- /dev/null +++ b/src/tools/verify_streaming_target_planner.gd @@ -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) diff --git a/src/tools/verify_streaming_target_planner.gd.uid b/src/tools/verify_streaming_target_planner.gd.uid new file mode 100644 index 0000000..4a19212 --- /dev/null +++ b/src/tools/verify_streaming_target_planner.gd.uid @@ -0,0 +1 @@ +uid://b5unt23vi8krk From 5b10288ff76c28f33e11c03a6b6cd6132d8a4378 Mon Sep 17 00:00:00 2001 From: sindoring Date: Thu, 16 Jul 2026 00:08:24 +0400 Subject: [PATCH 3/3] docs(M03): publish streaming planner handoff Work-Package: M03-RND-STREAMING-PLANNER-001 Agent: sindo-main-codex Tests: planner and repository gates pass Fidelity: no renderer policy or visual change --- .../claims/M03-RND-STREAMING-PLANNER-001.md | 35 +++++++++++++++---- docs/modules/streaming-target-planner.md | 2 +- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/coordination/claims/M03-RND-STREAMING-PLANNER-001.md b/coordination/claims/M03-RND-STREAMING-PLANNER-001.md index 6138127..b79b294 100644 --- a/coordination/claims/M03-RND-STREAMING-PLANNER-001.md +++ b/coordination/claims/M03-RND-STREAMING-PLANNER-001.md @@ -84,14 +84,35 @@ selection from `StreamingWorldLoader` into a scene-free, deterministic ## Status -- State: active -- Done: claim and isolated worktree created -- Next: implement pure contracts, migrate runtime/editor refresh, verify +- 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: + + ## Handoff -- Commit: -- Results: -- Remaining risks: -- Documentation updated: +- 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. diff --git a/docs/modules/streaming-target-planner.md b/docs/modules/streaming-target-planner.md index b0e59cd..3ca7178 100644 --- a/docs/modules/streaming-target-planner.md +++ b/docs/modules/streaming-target-planner.md @@ -7,7 +7,7 @@ | Status | Implemented | | Target/work package | `M03-RND-STREAMING-PLANNER-001` | | Owners | Renderer workstream / M03 integrator | -| Last verified | `work/sindo-main-codex/m03-streaming-target-planner`, 2026-07-15 | +| Last verified | `e887c3b`, 2026-07-15 | | Profiles/capabilities | Runtime and Editor preview ADT target selection; profile values supplied by caller | ## Purpose