From 235be3a36c7fa033d49acd0efd00f72a8b3bb517 Mon Sep 17 00:00:00 2001 From: sindoring Date: Fri, 17 Jul 2026 09:48:56 +0400 Subject: [PATCH] refactor(M03): extract ADT water scene finalizer --- RENDER.md | 21 +- .../M03-RND-ADT-WATER-SCENE-FINALIZER-001.md | 1 + docs/modules/README.md | 1 + docs/modules/adt-water-load-pipeline-state.md | 29 ++- docs/modules/adt-water-scene-finalizer.md | 232 ++++++++++++++++++ docs/modules/world-renderer.md | 9 +- .../liquid/adt_water_scene_finalizer.gd | 40 +++ .../liquid/adt_water_scene_finalizer.gd.uid | 1 + .../streaming/streaming_world_loader.gd | 37 +-- .../verify_adt_water_load_pipeline_state.gd | 3 +- src/tools/verify_adt_water_scene_finalizer.gd | 194 +++++++++++++++ .../verify_adt_water_scene_finalizer.gd.uid | 1 + 12 files changed, 538 insertions(+), 31 deletions(-) create mode 100644 docs/modules/adt-water-scene-finalizer.md create mode 100644 src/render/liquid/adt_water_scene_finalizer.gd create mode 100644 src/render/liquid/adt_water_scene_finalizer.gd.uid create mode 100644 src/tools/verify_adt_water_scene_finalizer.gd create mode 100644 src/tools/verify_adt_water_scene_finalizer.gd.uid diff --git a/RENDER.md b/RENDER.md index bcd6ab1..4ca734e 100644 --- a/RENDER.md +++ b/RENDER.md @@ -34,6 +34,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g - `src/render/wmo/wmo_render_resource_cache_state.gd` - validated lightweight WMO render Resources, negative cache and pending cache paths without ResourceLoader I/O. - `src/render/wmo/wmo_scene_resource_cache_state.gd` - validated cached-WMO PackedScenes, negative cache and pending `.tscn` paths without file/I/O/Node ownership. - `src/render/liquid/adt_water_load_pipeline_state.gd` - ADT water pending FIFO/dedupe, active task IDs and worker-safe parsed-result mailbox without parser or Node ownership. +- `src/render/liquid/adt_water_scene_finalizer.gd` - stateless main-thread ADT water build/attach and optional persisted Editor ownership through the existing ADTBuilder. - `src/scenes/streaming/eastern_kingdoms_streaming.tscn` - текущая сцена для проверки Azeroth/Eastern Kingdoms. - `addons/mpq_extractor/loaders/adt_builder.gd` - сборка ADT terrain, control splat материалов и MH2O liquids. - `addons/mpq_extractor/loaders/m2_builder.gd` - сборка статического M2 mesh/material. @@ -1084,7 +1085,8 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe' active WorkerThreadPool task IDs and the mutex-protected parsed-result mailbox. - `StreamingWorldLoader` still selects concurrency, starts/waits tasks, invokes `ADTLoader`, consumes `WATER_FINALIZE` permits, rejects stale tile/path/root - results and builds/attaches water Nodes through `ADTBuilder` on the main thread. + results and owns `water_loaded` timing. Main-thread Water subtree construction + and attachment are delegated separately as described below. - Tile release still cancels only pending water requests; active work completes and its result is discarded when tile ownership is stale. Shutdown waits task IDs before clearing state; reset does not interrupt workers. @@ -1093,6 +1095,23 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe' - Asset-backed MH2O/MCLQ fidelity, long-traversal leak/p95/p99 and paired capture evidence remain pending. +## 2026-07-17 ADT Water Scene Finalizer Extraction + +- `AdtWaterSceneFinalizer` now owns the two main-thread operations that turn + validated parsed ADT data into a Water subtree: the existing + `ADTBuilder.build_tile_water_scene` call and attachment to the tile root. +- Both raw tile finalization and delayed cached-tile water results use the same + service. Optional persisted Editor ownership is assigned recursively only + when the loader supplies its existing edited-scene owner. +- The service is stateless and retains no Node, Mesh, material or RID. The tile + SceneTree subtree remains the lifetime owner; dry data still causes ADTBuilder + to free its temporary empty Water root. +- Parsing, task/result state, concurrency, stale validation, `WATER_FINALIZE` + permits, `enable_water` and `water_loaded` timing remain loader-owned. +- Geometry, materials, coordinates, cache formats, profiles and visible output + are unchanged. Synthetic geometry/ownership evidence is not an MH2O/MCLQ or + original-client parity claim; asset-backed traversal/p95/p99 remains pending. + ## 2026-07-16 World Environment Snapshot Facade - `WorldEnvironmentSnapshot` carries one immutable finite time-of-day value, diff --git a/coordination/claims/M03-RND-ADT-WATER-SCENE-FINALIZER-001.md b/coordination/claims/M03-RND-ADT-WATER-SCENE-FINALIZER-001.md index 75a2158..85eecad 100644 --- a/coordination/claims/M03-RND-ADT-WATER-SCENE-FINALIZER-001.md +++ b/coordination/claims/M03-RND-ADT-WATER-SCENE-FINALIZER-001.md @@ -31,6 +31,7 @@ the existing `ADTBuilder` geometry/material implementation. `src/tools/verify_adt_water_scene_finalizer.gd`, `docs/modules/adt-water-scene-finalizer.md`, this claim - Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`, + `src/tools/verify_adt_water_load_pipeline_state.gd`, `docs/modules/adt-water-load-pipeline-state.md`, `docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`, `targets/03-renderer-facade.md` diff --git a/docs/modules/README.md b/docs/modules/README.md index b062be5..ea90aee 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -16,6 +16,7 @@ | Terrain chunk LOD planner | Implemented extraction | [`terrain-chunk-lod-planner.md`](terrain-chunk-lod-planner.md) | | Terrain chunk geometry queue planner | Implemented extraction | [`terrain-chunk-geometry-queue-planner.md`](terrain-chunk-geometry-queue-planner.md) | | ADT water load pipeline state | Implemented extraction | [`adt-water-load-pipeline-state.md`](adt-water-load-pipeline-state.md) | +| ADT water scene finalizer | Implemented extraction | [`adt-water-scene-finalizer.md`](adt-water-scene-finalizer.md) | | M2 unique placement registry | Implemented extraction | [`m2-unique-placement-registry.md`](m2-unique-placement-registry.md) | | M2 placement transform resolver | Implemented | [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md) | | M2 placement grouper | Implemented extraction | [`m2-placement-grouper.md`](m2-placement-grouper.md) | diff --git a/docs/modules/adt-water-load-pipeline-state.md b/docs/modules/adt-water-load-pipeline-state.md index 1a227df..e9186c4 100644 --- a/docs/modules/adt-water-load-pipeline-state.md +++ b/docs/modules/adt-water-load-pipeline-state.md @@ -38,8 +38,9 @@ flowchart LR State --> Result[Parsed ADT result] Result --> Loader Loader --> Budget[WATER_FINALIZE permit] - Loader --> Builder[ADTBuilder water scene] - Builder --> Scene[Main-thread tile Node attach] + Loader --> Finalizer[AdtWaterSceneFinalizer] + Finalizer --> Builder[ADTBuilder water scene] + Finalizer --> Scene[Main-thread tile Node attach] ``` Allowed dependencies are RefCounted, Dictionary/Array/String and one Godot @@ -127,7 +128,7 @@ sequenceDiagram participant State as AdtWaterLoadPipelineState participant Worker as WorkerThreadPool task participant Budget as RenderBudgetScheduler - participant Builder as ADTBuilder + participant Finalizer as AdtWaterSceneFinalizer Loader->>State: enqueue_request(tile key, ADT path) Loader->>State: take_next_request() Loader->>Worker: add_task(parse binding) @@ -140,8 +141,8 @@ sequenceDiagram Loader->>Worker: wait_for_task_completion(task ID) Loader->>State: complete_active_task(key) Loader->>Loader: reject stale tile/path/root - Loader->>Builder: build_tile_water_scene(data, origin) - Loader->>Loader: attach Node and mark water_loaded + Loader->>Finalizer: attach_water_scene(data, origin, tile root, editor owner) + Loader->>Loader: mark water_loaded ``` ## Dependency diagram @@ -151,10 +152,11 @@ flowchart TB Loader[StreamingWorldLoader] --> State[AdtWaterLoadPipelineState] Loader --> Scheduler[RenderBudgetScheduler] Loader --> Worker[WorkerThreadPool / ADTLoader] - Loader --> Builder[ADTBuilder / SceneTree] + Loader --> Finalizer[AdtWaterSceneFinalizer] + Finalizer --> Builder[ADTBuilder / SceneTree] Worker -->|publish only| State State -. no dependency .-> Scheduler - State -. no dependency .-> Builder + State -. no dependency .-> Finalizer ``` ## Ownership, threading and resources @@ -163,7 +165,8 @@ flowchart TB - Worker threads may call only `publish_result`; result observe/pop/clear also lock. - Parsed water Dictionary identity is retained until main-thread pop. - Loader owns WorkerThreadPool task lifetime and must wait before orderly shutdown clear. -- Loader owns tile states, ADTBuilder, water Nodes and every render resource. +- Loader owns tile states and the tile subtree; `AdtWaterSceneFinalizer` performs + the main-thread ADTBuilder call and attachment without retaining Nodes. - `clear` cannot cancel a worker; post-reset publication becomes a stale result and is discarded because no active task/tile ownership remains. @@ -211,8 +214,8 @@ versions are unchanged; no migration or rebuild is required. ## Extension points - ADT parse execution can later move behind a parser/job adapter without changing state. -- Water scene materialization can be extracted separately because it owns Nodes - and main-thread effects rather than async bookkeeping. +- Water scene materialization is now delegated to `AdtWaterSceneFinalizer`, + independently of this async bookkeeping service. ## Capability status @@ -220,7 +223,7 @@ versions are unchanged; no migration or rebuild is required. |---|---|---|---| | ADT water load pipeline state | Implemented extraction | FIFO/task/thread/source/timing verifier | Asset-backed traversal/leak evidence pending | | ADT liquid parsing | Existing/loader-owned adapter | Native ADTLoader and dry-run paths | MH2O/MCLQ fidelity fixtures pending | -| Water scene finalization | Existing/loader-owned | Scheduler/material/shutdown regressions | Separate materialization extraction possible | +| Water scene finalization | Implemented extraction | Dedicated synthetic scene/ownership verifier | Asset-backed traversal/hitch evidence pending | ## Known gaps and risks @@ -235,13 +238,15 @@ versions are unchanged; no migration or rebuild is required. | Path | Responsibility | |---|---| | `src/render/liquid/adt_water_load_pipeline_state.gd` | Request/task/result bookkeeping and result mutex | -| `src/scenes/streaming/streaming_world_loader.gd` | Parse task execution, permits, stale checks and Node finalization | +| `src/render/liquid/adt_water_scene_finalizer.gd` | Main-thread ADTBuilder call, attach and optional editor ownership | +| `src/scenes/streaming/streaming_world_loader.gd` | Parse task execution, permits, stale checks and tile loaded timing | | `addons/mpq_extractor/loaders/adt_builder.gd` | ADT liquid geometry/material construction | | `src/tools/verify_adt_water_load_pipeline_state.gd` | Lifecycle/thread/boundary/timing regression | ## Related decisions and references - [`world-renderer.md`](world-renderer.md) +- [`adt-water-scene-finalizer.md`](adt-water-scene-finalizer.md) - [`render-budget-scheduler.md`](render-budget-scheduler.md) - [`../../RENDER.md`](../../RENDER.md) - [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md) diff --git a/docs/modules/adt-water-scene-finalizer.md b/docs/modules/adt-water-scene-finalizer.md new file mode 100644 index 0000000..ed37431 --- /dev/null +++ b/docs/modules/adt-water-scene-finalizer.md @@ -0,0 +1,232 @@ +# ADT Water Scene Finalizer + +## Metadata + +| Field | Value | +|---|---| +| Status | Implemented extraction | +| Target/work package | M03 / `M03-RND-ADT-WATER-SCENE-FINALIZER-001` | +| Owners | Main-thread ADT water subtree build, attach and optional editor ownership | +| Last verified | Worktree `work/sindo-main-codex/m03-adt-water-scene-finalizer`, 2026-07-17 | +| Profiles/capabilities | Existing ADT MH2O/MCLQ scene output; profile-independent adapter | + +## Purpose + +Provide one liquid-specific main-thread boundary for converting parsed ADT +water data through the existing `ADTBuilder`, attaching the resulting `Water` +subtree to a caller-owned tile root and assigning optional persisted Editor +ownership. The service is stateless and retains no Node or Resource. + +## Non-goals + +- Parse or validate MH2O/MCLQ binary structures. +- Replace ADTBuilder geometry, material or coordinate calculations. +- Own water request/task/result state, permits or concurrency. +- Validate tile/path/root freshness beyond receiving a valid root. +- Mark `water_loaded`, enable water or remove water subtrees. + +## Context and boundaries + +```mermaid +flowchart LR + Parse[Parsed ADT Dictionary] --> Loader[StreamingWorldLoader] + Loader -->|validated data, origin, tile root| Finalizer[AdtWaterSceneFinalizer] + Finalizer --> Builder[ADTBuilder] + Builder --> Water[Water Node3D or null] + Water --> Finalizer + Finalizer -->|main-thread add_child| Tile[Caller-owned tile root] + Loader -->|after return| Loaded[water_loaded timing] +``` + +Allowed dependencies are `ADTBuilder` and Godot Node/mesh types needed to adopt +its output. WorkerThreadPool, scheduler, tile-state dictionaries, facade, +gameplay, network and editor plugin packages are forbidden. + +## Public API + +| Symbol | Kind | Purpose | Thread/lifetime | Errors | +|---|---|---|---|---| +| `attach_water_scene(water_data, tile_origin_godot_units, tile_root, editor_owner)` | Command/query | Build and attach one existing-format water subtree | Renderer main thread; returned Node is borrowed for tile lifetime | Empty data, invalid root or dry build returns null without attachment | + +`editor_owner` is optional. When supplied it must be an ancestor suitable for +Godot `Node.owner`; ownership is recursively assigned to the generated subtree. + +## Inputs and outputs + +| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | +|---|---|---|---|---|---| +| Input | Parsed ADT water-containing Dictionary | ADTLoader/raw tile path | Finalizer/ADTBuilder | Caller retains Dictionary; not mutated | One main-thread call | +| Input | Tile origin in Godot world units | Loader tile state/ADTBuilder | Finalizer | Value copy | One call | +| Input | Valid tile root | StreamingWorldLoader | Finalizer | Caller/SceneTree-owned | Must outlive call and attached subtree | +| Input | Optional edited scene root | Loader Editor policy | Finalizer | SceneTree-owned borrowed Node | One call | +| Output | Attached `Water` Node3D or null | Finalizer | Loader/SceneTree | Tile root owns attached Node | Tile subtree lifetime | + +Side effects are limited to ADTBuilder mesh/material creation, `add_child` and +optional recursive `Node.owner` assignment. No cache, filesystem, task or tile +state is mutated. + +## Data flow + +```mermaid +flowchart TD + Input[water data + Godot origin + tile root] --> Guard{Data and root valid?} + Guard -->|no| None[Return null; unchanged] + Guard -->|yes| Build[ADTBuilder.build_tile_water_scene] + Build --> Dry{Water root exists?} + Dry -->|no| None + Dry -->|yes| Attach[tile root add_child] + Attach --> Owner{Editor owner supplied?} + Owner -->|yes| Assign[Assign owner recursively] + Owner -->|no| Return[Return borrowed root] + Assign --> Return +``` + +## Lifecycle/state + +The service has no retained lifecycle. Each call moves one temporary builder +result through the following call-local states. + +```mermaid +stateDiagram-v2 + [*] --> Validating + Validating --> Rejected: empty data or invalid root + Validating --> Building: valid input + Building --> Dry: no geometry + Building --> Attached: Water root built + Attached --> EditorOwned: owner supplied + Attached --> Complete: no owner + EditorOwned --> Complete + Rejected --> [*] + Dry --> [*] + Complete --> [*] +``` + +## Main sequence + +```mermaid +sequenceDiagram + participant Loader as StreamingWorldLoader main thread + participant Finalizer as AdtWaterSceneFinalizer + participant Builder as ADTBuilder + participant Tile as Tile root + Loader->>Finalizer: attach_water_scene(data, origin, tile, editor owner) + Finalizer->>Builder: build_tile_water_scene(data, origin) + Builder-->>Finalizer: Water root or null + alt Water root exists + Finalizer->>Tile: add_child(Water) + opt editor owner supplied + Finalizer->>Finalizer: assign owner recursively + end + Finalizer-->>Loader: borrowed Water root + else dry/invalid + Finalizer-->>Loader: null + end + Loader->>Loader: preserve existing water_loaded timing +``` + +## Dependency diagram + +```mermaid +flowchart TB + Loader[StreamingWorldLoader] --> Finalizer[AdtWaterSceneFinalizer] + Finalizer --> Builder[ADTBuilder] + Builder --> Material[WowLiquidMaterial] + Finalizer --> Scene[Godot SceneTree] + Pipeline[AdtWaterLoadPipelineState] --> Loader + Scheduler[RenderBudgetScheduler] --> Loader + Finalizer -. no dependency .-> Pipeline + Finalizer -. no dependency .-> Scheduler +``` + +## Ownership, threading and resources + +- The method is main-thread only because it creates Mesh/Material/Node objects + and mutates the SceneTree. +- `ADTBuilder` owns temporary construction. It frees an empty `Water` root and + returns null for dry/invalid liquid geometry. +- After `add_child`, the caller's tile subtree owns the returned Water root and + all MeshInstance children; the finalizer retains no references. +- Optional editor ownership affects serialization ownership only and does not + replace SceneTree lifetime ownership. +- Parsed Dictionaries are borrowed and not mutated. + +## Errors, cancellation and recovery + +| Failure | Detection | Behavior | Diagnostic | Recovery | +|---|---|---|---|---| +| Empty parsed data | Input guard | Return null, no builder call | Contract verifier | Loader preserves existing loaded timing | +| Missing/freed tile root | Input guard | Return null, no scene mutation | Contract verifier | Stale-result validation normally rejects earlier | +| Dry or malformed liquid arrays | ADTBuilder returns null | No attachment | Builder/shutdown regression | Reload after corrected source | +| Tile/path stale | Loader before call | Finalizer is not invoked | Existing loader behavior | Current tile may enqueue again | +| Shutdown/cancellation | Loader drains/cancels before call | No retained finalizer work | Shutdown verifier | New loader starts stateless | + +## Configuration and capabilities + +| Setting/capability | Default | Profile | Runtime mutable | Effect | +|---|---|---|---|---| +| `enable_water` | Loader setting | Quality/custom | Yes | Gates both caller paths; not owned here | +| `water_finalize_ops_per_tick` | `1` | Quality/custom | Yes | Budgets async calls before finalizer | +| `editor_persist_generated_nodes` | `false` | Editor only | Yes | Supplies or omits editor owner | + +## Persistence, cache and migration + +The service writes no files and changes no cache/resource schema. Existing ADT, +terrain cache and liquid material versions remain valid; no rebuild or migration +is required. Persisted Editor ownership follows the loader's existing opt-in. + +## Diagnostics and observability + +- Logs: none; existing loader/builder diagnostics remain authoritative. +- Metrics: no independent counters; existing water queue/finalize metrics remain. +- Debug views: generated `Water/Liquid_` names remain unchanged. +- Correlation IDs: tile/path correlation stays in loader pipeline state. + +## Verification + +- `verify_adt_water_scene_finalizer.gd`: empty/invalid/dry guards, one-cell + geometry, attachment, recursive owner, loader delegation and bounded timing. +- `verify_adt_water_load_pipeline_state.gd`: adjacent async ownership and loaded timing. +- Renderer material and shutdown verifiers cover material construction and dry-root cleanup. +- Fidelity evidence is the unchanged ADTBuilder implementation and unchanged two + loader call positions; no new original-client liquid parity claim is made. +- Performance budget: 100 empty calls under one second. + +## Extension points + +- A future liquid renderer may replace the internal builder only after paired + MH2O/MCLQ fixtures establish equivalent geometry/material behavior. +- Removal/eviction remains ordinary tile subtree ownership and needs no parallel API. + +## Capability status + +| Capability | Status | Evidence | Gap/next step | +|---|---|---|---| +| ADT water scene finalization seam | Implemented extraction | Synthetic geometry/attach/owner/source verifier | Asset-backed traversal and hitch metrics pending | +| ADT liquid geometry/materials | Existing implementation | ADTBuilder/material/checkpoint evidence | MH2O/MCLQ and original-client parity incomplete | +| Async water scheduling | Existing extracted state | Water pipeline verifier | Active worker interruption remains loader-owned | + +## Known gaps and risks + +- The service intentionally wraps the existing Dictionary-based ADTBuilder boundary. +- It does not verify that `editor_owner` is an ancestor; the loader supplies the + edited scene root under the existing policy. +- Mesh/material creation remains synchronous main-thread work behind the existing permit. +- No proprietary liquid corpus, long traversal leak run, p95/p99 or paired capture is included. + +## Source map + +| Path | Responsibility | +|---|---| +| `src/render/liquid/adt_water_scene_finalizer.gd` | Main-thread build, attach and optional recursive editor ownership | +| `addons/mpq_extractor/loaders/adt_builder.gd` | Existing ADT liquid geometry/material creation and dry-root cleanup | +| `src/render/liquid/adt_water_load_pipeline_state.gd` | Pending/task/result state before finalization | +| `src/scenes/streaming/streaming_world_loader.gd` | Permits, stale checks, tile state and finalizer calls | +| `src/tools/verify_adt_water_scene_finalizer.gd` | Synthetic scene/ownership/boundary/timing regression | + +## Related decisions and references + +- [`adt-water-load-pipeline-state.md`](adt-water-load-pipeline-state.md) +- [`world-renderer.md`](world-renderer.md) +- [`render-budget-scheduler.md`](render-budget-scheduler.md) +- [`../../RENDER.md`](../../RENDER.md) +- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md) diff --git a/docs/modules/world-renderer.md b/docs/modules/world-renderer.md index aebb3b6..cb68134 100644 --- a/docs/modules/world-renderer.md +++ b/docs/modules/world-renderer.md @@ -7,7 +7,7 @@ | Status | Partial | | Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package | | Owners | Renderer workstream / milestone integrator | -| Last verified | Worktree `work/sindo-main-codex/m03-adt-water-load-pipeline`, 2026-07-17 | +| Last verified | Worktree `work/sindo-main-codex/m03-adt-water-scene-finalizer`, 2026-07-17 | | Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete | ## Purpose @@ -139,6 +139,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state | `WmoRenderResourceCacheState` | Internal WMO cache-state service | Owns validated Resources, negative entries and pending cache paths | Renderer main thread; map/cache session | Invalid/occupied request and unknown completion are rejected | | `WmoSceneResourceCacheState` | Internal WMO cache-state service | Owns validated PackedScenes, negative entries and pending `.tscn` paths | Renderer main thread; map/cache session | Direct missing and terminal request transitions remain distinct | | `AdtWaterLoadPipelineState` | Internal liquid async-state service | Owns ADT water FIFO/dedupe, active task IDs and mutex result mailbox | Main-thread state; worker result publication | Invalid/duplicate requests rejected; clear does not interrupt workers | +| `AdtWaterSceneFinalizer.attach_water_scene` | Internal liquid main-thread service | Builds and attaches one existing-format ADT Water subtree | Main thread; stateless, returned Node tile-owned | Empty/invalid/dry input returns null without attachment | Публичным contract не считаются `StreamingWorldLoader` methods/properties, внутренние dictionaries, queues, job records и generated node names. Scene-owned @@ -361,7 +362,9 @@ sequenceDiagram - `AdtWaterLoadPipelineState` owns pending request order/deduplication, opaque active task IDs and the worker-safe parsed-result mailbox. The loader retains WorkerThreadPool start/wait, ADTLoader parsing, concurrency/finalize permits, - stale tile/path/root checks and ADTBuilder/Node finalization. + stale tile/path/root checks and `water_loaded` timing. `AdtWaterSceneFinalizer` + performs the existing ADTBuilder call, tile attach and optional recursive + Editor ownership on the main thread without retaining the generated subtree. - Rendered-ground query results and diagnostic snapshots are caller-owned values; the facade never returns Mesh, Node, tile-state or queue references. - Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay @@ -561,6 +564,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | `src/render/terrain/rendered_ground_sample.gd` | Immutable renderer-owned available/unavailable ground query result | | `src/render/terrain/terrain_quality_mesh_cache.gd` | Full-quality terrain revisit Mesh retention and LRU ownership | | `src/render/liquid/adt_water_load_pipeline_state.gd` | ADT water request/task/result bookkeeping and worker-safe mailbox | +| `src/render/liquid/adt_water_scene_finalizer.gd` | Main-thread ADT water build, tile attachment and optional editor ownership | | `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 | @@ -574,6 +578,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | `src/tools/verify_rendered_ground_sample.gd` | Cold-start renderer ground result invariant regression | | `src/tools/verify_terrain_quality_mesh_cache.gd` | Terrain cache admission, LRU, ownership and loader-boundary regression | | `src/tools/verify_adt_water_load_pipeline_state.gd` | ADT water FIFO/task/thread/boundary/timing regression | +| `src/tools/verify_adt_water_scene_finalizer.gd` | Synthetic ADT water scene/ownership/boundary/timing regression | | `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression | | `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression | | `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields | diff --git a/src/render/liquid/adt_water_scene_finalizer.gd b/src/render/liquid/adt_water_scene_finalizer.gd new file mode 100644 index 0000000..6e2e0b4 --- /dev/null +++ b/src/render/liquid/adt_water_scene_finalizer.gd @@ -0,0 +1,40 @@ +class_name AdtWaterSceneFinalizer +extends RefCounted + +## Builds and attaches one ADT water subtree on the renderer main thread. The +## supplied tile root owns the returned subtree; this service retains no state. + +const ADT_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/adt_builder.gd") + + +## Builds water from parsed ADT data and attaches it once to [param tile_root]. +## [param tile_origin_godot_units] is the existing Godot-world tile origin used +## by [ADTBuilder]. When [param editor_owner] is non-null and is an ancestor, +## ownership is assigned recursively for persisted Editor previews. Returns the +## attached borrowed root, or null for empty/invalid input or dry ADT data. +## This method mutates the SceneTree and must run on the main thread. +func attach_water_scene( + water_data: Dictionary, + tile_origin_godot_units: Vector3, + tile_root: Node, + editor_owner: Node = null) -> Node3D: + if water_data.is_empty() or tile_root == null or not is_instance_valid(tile_root): + return null + + var water_root: Node3D = ADT_BUILDER_SCRIPT.build_tile_water_scene( + water_data, + tile_origin_godot_units + ) + if water_root == null: + return null + + tile_root.add_child(water_root) + if editor_owner != null and is_instance_valid(editor_owner): + _assign_owner_recursive(water_root, editor_owner) + return water_root + + +func _assign_owner_recursive(generated_node: Node, editor_owner: Node) -> void: + generated_node.owner = editor_owner + for child in generated_node.get_children(): + _assign_owner_recursive(child, editor_owner) diff --git a/src/render/liquid/adt_water_scene_finalizer.gd.uid b/src/render/liquid/adt_water_scene_finalizer.gd.uid new file mode 100644 index 0000000..fd8d3b4 --- /dev/null +++ b/src/render/liquid/adt_water_scene_finalizer.gd.uid @@ -0,0 +1 @@ +uid://ck4sdc8a3kg00 diff --git a/src/scenes/streaming/streaming_world_loader.gd b/src/scenes/streaming/streaming_world_loader.gd index e5fc565..841f9f3 100644 --- a/src/scenes/streaming/streaming_world_loader.gd +++ b/src/scenes/streaming/streaming_world_loader.gd @@ -45,6 +45,9 @@ const TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER_SCRIPT := preload( const ADT_WATER_LOAD_PIPELINE_STATE_SCRIPT := preload( "res://src/render/liquid/adt_water_load_pipeline_state.gd" ) +const ADT_WATER_SCENE_FINALIZER_SCRIPT := preload( + "res://src/render/liquid/adt_water_scene_finalizer.gd" +) const M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT := preload( "res://src/render/m2/m2_unique_placement_registry.gd" ) @@ -260,6 +263,7 @@ var _terrain_splat_tasks: Dictionary = {} var _terrain_splat_result_mutex := Mutex.new() var _terrain_splat_result_queue: Array = [] var _adt_water_load_pipeline_state := ADT_WATER_LOAD_PIPELINE_STATE_SCRIPT.new() +var _adt_water_scene_finalizer := ADT_WATER_SCENE_FINALIZER_SCRIPT.new() var _tile_result_mutex := Mutex.new() var _tile_result_queue: Array = [] var _shared_tex_cache: Dictionary = {} @@ -2294,10 +2298,12 @@ func _drain_water_load_results() -> void: var data: Dictionary = result.get("data", {}) if not data.is_empty(): var origin: Vector3 = state.get("origin", Vector3.ZERO) - var water_root: Node3D = _builder.build_tile_water_scene(data, origin) - if water_root: - tile_root.add_child(water_root) - _set_editor_owner_recursive(water_root) + _adt_water_scene_finalizer.attach_water_scene( + data, + origin, + tile_root, + _editor_owner_for_generated_nodes() + ) state["water_loaded"] = true _tile_states[key] = state @@ -2448,10 +2454,12 @@ func _finalize_loaded_tile(request: Dictionary, data: Dictionary) -> void: tile_root.position = tile_origin if enable_water: - var water_root: Node3D = _builder.build_tile_water_scene(data, tile_origin) - if water_root: - tile_root.add_child(water_root) - _set_editor_owner_recursive(water_root) + _adt_water_scene_finalizer.attach_water_scene( + data, + tile_origin, + tile_root, + _editor_owner_for_generated_nodes() + ) if enable_m2_placeholders: _build_tile_m2_placeholders( tile_root, @@ -5421,12 +5429,7 @@ func _tile_key(tx: int, ty: int) -> String: func _set_editor_owner_recursive(node: Node) -> void: - if not Engine.is_editor_hint(): - return - if not editor_persist_generated_nodes: - return - - var edited_root := get_tree().edited_scene_root + var edited_root := _editor_owner_for_generated_nodes() if edited_root == null: return @@ -5435,6 +5438,12 @@ func _set_editor_owner_recursive(node: Node) -> void: _set_editor_owner_recursive(child) +func _editor_owner_for_generated_nodes() -> Node: + if not Engine.is_editor_hint() or not editor_persist_generated_nodes: + return null + return get_tree().edited_scene_root + + func _make_editor_signature() -> String: var values := [ extracted_dir, diff --git a/src/tools/verify_adt_water_load_pipeline_state.gd b/src/tools/verify_adt_water_load_pipeline_state.gd index 72b8f24..9bf10f7 100644 --- a/src/tools/verify_adt_water_load_pipeline_state.gd +++ b/src/tools/verify_adt_water_load_pipeline_state.gd @@ -241,8 +241,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void: "ClassDB.instantiate(\"ADTLoader\")", "WorkerThreadPool.add_task(_load_tile_water_task.bind(key, path))", "RENDER_BUDGET_SCHEDULER_SCRIPT.WATER_FINALIZE", - "_builder.build_tile_water_scene(data, origin)", - "tile_root.add_child(water_root)", + "_adt_water_scene_finalizer.attach_water_scene(", "state[\"water_loaded\"] = true", ]: _expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures) diff --git a/src/tools/verify_adt_water_scene_finalizer.gd b/src/tools/verify_adt_water_scene_finalizer.gd new file mode 100644 index 0000000..ff58b26 --- /dev/null +++ b/src/tools/verify_adt_water_scene_finalizer.gd @@ -0,0 +1,194 @@ +extends SceneTree + +## Synthetic main-thread scene, ownership, boundary and timing regression for +## ADT water finalization. + +const FINALIZER_SCRIPT := preload("res://src/render/liquid/adt_water_scene_finalizer.gd") +const FINALIZER_PATH := "res://src/render/liquid/adt_water_scene_finalizer.gd" +const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" + + +func _initialize() -> void: + var failures: Array[String] = [] + _verify_invalid_and_dry_input(failures) + _verify_build_attach_geometry_and_owner(failures) + _verify_ownership_boundaries(failures) + var elapsed_milliseconds := _verify_bounded_timing(failures) + if not failures.is_empty(): + for failure in failures: + push_error("ADT_WATER_SCENE_FINALIZER: %s" % failure) + quit(1) + return + print( + "ADT_WATER_SCENE_FINALIZER PASS cases=8 iterations=100 elapsed_ms=%.3f" + % elapsed_milliseconds + ) + quit(0) + + +func _verify_invalid_and_dry_input(failures: Array[String]) -> void: + var finalizer: RefCounted = FINALIZER_SCRIPT.new() + var tile_root := Node3D.new() + _expect_null( + finalizer.call("attach_water_scene", {}, Vector3.ZERO, tile_root, null), + "empty water data returns null", + failures + ) + _expect_equal(tile_root.get_child_count(), 0, "empty data does not mutate tile", failures) + _expect_null( + finalizer.call("attach_water_scene", _synthetic_water_data(), Vector3.ZERO, null, null), + "missing tile root returns null", + failures + ) + _expect_null( + finalizer.call( + "attach_water_scene", + {"chunks": [{"origin": Vector3.ZERO, "liquids": []}]}, + Vector3.ZERO, + tile_root, + null + ), + "dry parsed ADT returns null", + failures + ) + _expect_equal(tile_root.get_child_count(), 0, "dry data does not mutate tile", failures) + tile_root.free() + + +func _verify_build_attach_geometry_and_owner(failures: Array[String]) -> void: + var finalizer: RefCounted = FINALIZER_SCRIPT.new() + var editor_scene_root := Node3D.new() + var tile_root := Node3D.new() + get_root().add_child(editor_scene_root) + editor_scene_root.add_child(tile_root) + + var tile_origin := Vector3(10.0, 20.0, 30.0) + var water_root: Node3D = finalizer.call( + "attach_water_scene", + _synthetic_water_data(tile_origin), + tile_origin, + tile_root, + editor_scene_root + ) + _expect_not_null(water_root, "synthetic liquid builds a water root", failures) + if water_root != null: + _expect_true(water_root.get_parent() == tile_root, "water root attached once", failures) + _expect_equal(tile_root.get_child_count(), 1, "tile owns one water root", failures) + _expect_true(water_root.owner == editor_scene_root, "water root editor owner", failures) + _expect_equal(water_root.get_child_count(), 1, "one liquid surface grouped", failures) + if water_root.get_child_count() == 1: + var liquid_mesh_instance := water_root.get_child(0) as MeshInstance3D + _expect_not_null(liquid_mesh_instance, "liquid child is MeshInstance3D", failures) + if liquid_mesh_instance != null: + _expect_true( + liquid_mesh_instance.owner == editor_scene_root, + "liquid child editor owner", + failures + ) + var liquid_mesh := liquid_mesh_instance.mesh as ArrayMesh + _expect_not_null(liquid_mesh, "liquid surface has ArrayMesh", failures) + if liquid_mesh != null: + _expect_equal( + liquid_mesh.surface_get_array_len(0), + 4, + "one enabled liquid cell has four vertices", + failures + ) + _expect_equal( + liquid_mesh.surface_get_array_index_len(0), + 6, + "one enabled liquid cell has six indices", + failures + ) + editor_scene_root.free() + + +func _verify_ownership_boundaries(failures: Array[String]) -> void: + var finalizer_source := FileAccess.get_file_as_string(FINALIZER_PATH) + var loader_source := FileAccess.get_file_as_string(LOADER_PATH) + _expect_true( + finalizer_source.contains("ADT_BUILDER_SCRIPT.build_tile_water_scene"), + "finalizer retains existing ADTBuilder implementation", + failures + ) + _expect_true( + finalizer_source.contains("tile_root.add_child(water_root)"), + "finalizer owns water subtree attachment", + failures + ) + _expect_equal( + loader_source.count("_adt_water_scene_finalizer.attach_water_scene("), + 2, + "both loader water paths delegate finalization", + failures + ) + _expect_false( + loader_source.contains("_builder.build_tile_water_scene"), + "loader no longer builds water scenes directly", + failures + ) + _expect_true( + loader_source.contains("state[\"water_loaded\"] = true"), + "loader retains water-loaded timing", + failures + ) + _expect_true( + loader_source.contains("WATER_FINALIZE"), + "loader retains finalization permit ownership", + failures + ) + + +func _verify_bounded_timing(failures: Array[String]) -> float: + var finalizer: RefCounted = FINALIZER_SCRIPT.new() + var tile_root := Node3D.new() + var started_microseconds := Time.get_ticks_usec() + for _iteration in range(100): + finalizer.call("attach_water_scene", {}, Vector3.ZERO, tile_root, null) + var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0 + _expect_true(elapsed_milliseconds < 1000.0, "100 empty attempts under 1 second", failures) + _expect_equal(tile_root.get_child_count(), 0, "timing loop creates no nodes", failures) + tile_root.free() + return elapsed_milliseconds + + +func _synthetic_water_data(tile_origin: Vector3 = Vector3.ZERO) -> Dictionary: + var liquid_mask := PackedByteArray() + liquid_mask.resize(64) + liquid_mask.fill(0) + liquid_mask[0] = 1 + var liquid_heights := PackedFloat32Array() + liquid_heights.resize(81) + liquid_heights.fill(tile_origin.y + 5.0) + return { + "chunks": [{ + "origin": tile_origin, + "liquids": [{ + "liquid_id": 7, + "mask": liquid_mask, + "heights": liquid_heights, + }], + }], + } + + +func _expect_true(condition: bool, label: String, failures: Array[String]) -> void: + if not condition: + failures.append(label) + + +func _expect_false(condition: bool, label: String, failures: Array[String]) -> void: + _expect_true(not condition, label, failures) + + +func _expect_null(value: Variant, label: String, failures: Array[String]) -> void: + _expect_true(value == null, label, failures) + + +func _expect_not_null(value: Variant, label: String, failures: Array[String]) -> void: + _expect_true(value != null, label, failures) + + +func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void: + if actual != expected: + failures.append("%s expected=%d actual=%d" % [label, expected, actual]) diff --git a/src/tools/verify_adt_water_scene_finalizer.gd.uid b/src/tools/verify_adt_water_scene_finalizer.gd.uid new file mode 100644 index 0000000..567219d --- /dev/null +++ b/src/tools/verify_adt_water_scene_finalizer.gd.uid @@ -0,0 +1 @@ +uid://c1gqfpxg7ajsh