diff --git a/RENDER.md b/RENDER.md index cf734c6..20148d3 100644 --- a/RENDER.md +++ b/RENDER.md @@ -33,6 +33,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g - `src/render/wmo/wmo_render_build_queue.gd` / `wmo_render_build_job.gd` - typed pending group jobs, FIFO placement keys and strong root/resource references without engine destruction. - `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_render_resource_finalizer.gd` - lightweight WMO terminal polling, script/format validation and Resource/missing publication. +- `src/render/wmo/wmo_scene_resource_finalizer.gd` - cached WMO terminal polling, PackedScene probe validation/lifetime and scene/missing publication. - `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. @@ -1349,8 +1350,9 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe' - `WmoSceneResourceCacheState` now owns validated cached-WMO PackedScenes, negative entries and normalized-path to pending-`.tscn` records. - `StreamingWorldLoader` still checks file existence and - `wmo_max_runtime_scene_mb`, calls `ResourceLoader`, instantiates a validation - probe, checks WMOBuilder cache metadata and frees the probe before adoption. + `wmo_max_runtime_scene_mb` and starts requests. `WmoSceneResourceFinalizer` + owns terminal ResourceLoader I/O, validation-probe instantiation, WMOBuilder + metadata validation and probe release before adoption. - Missing files, oversize scenes, request errors, load failures and stale scenes retain their prior negative-cache and live-prototype fallback behavior. - Map reset clears pending/negative state while retaining accepted scenes; final @@ -1358,6 +1360,21 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe' - Asset-backed oversize/stale fixtures, traversal/leak p95/p99 and paired fidelity evidence remain pending. +## 2026-07-18 WMO Scene Resource Finalizer Extraction + +- `WmoSceneResourceFinalizer` now owns cached WMO `.tscn` terminal status + polling, loaded Resource/PackedScene validation, call-local probe lifetime and + exact scene/missing publication. +- Pending snapshots preserve Dictionary insertion order. Non-terminal requests + remain pending; failed, null, wrong-type, wrong-root and stale outcomes keep + the existing negative-cache transition. +- Current scenes retain exact PackedScene identity. Accepted and rejected probes + are freed before return; the rejected non-Node3D root now also releases its + temporary Node, closing a leak without changing fallback or visible output. +- Loader retains file/size admission, oversize log, request start, live fallback, + placed Node materialization and shutdown order. Synthetic fixtures are not + serialized private assets, long leak, p95/p99 or visual-fidelity evidence. + ## 2026-07-17 ADT Water Load Pipeline State Extraction - `AdtWaterLoadPipelineState` now owns ADT water pending FIFO/deduplication, diff --git a/coordination/claims/M03-RND-WMO-SCENE-RESOURCE-FINALIZER-001.md b/coordination/claims/M03-RND-WMO-SCENE-RESOURCE-FINALIZER-001.md index f0bd21b..a519f72 100644 --- a/coordination/claims/M03-RND-WMO-SCENE-RESOURCE-FINALIZER-001.md +++ b/coordination/claims/M03-RND-WMO-SCENE-RESOURCE-FINALIZER-001.md @@ -55,14 +55,21 @@ probe lifetime and scene/missing publication from the loader into one service. ## Status -- State: active -- Done: ownership/compatibility contract -- Next: implementation, verification and documentation +- State: ready +- Done: implementation, verification and documentation +- Next: merge and post-merge acceptance - Blocked by: ## Handoff -- Commit: -- Results: -- Remaining risks: -- Documentation updated: +- Commit: `7779e9e` (`render: extract WMO scene resource finalizer`) +- Results: finalizer PASS `cases=26 iterations=1000 elapsed_ms=4.186`; + adjacent WMO scene/render cache/finalizer/queue/planner/registry/resolver and + shutdown checks passed; autonomous headless suite `61/61`; documentation `48`; + coordination passed with `34` historical warnings; checkpoint dry-run `7/7`. +- Remaining risks: PackedScene probe validation remains synchronous main-thread + work; no serialized private WMO stale/oversize, long leak, visual, + descriptor-pressure or p95/p99 evidence was added. +- Documentation updated: new full module specification with API/I/O and + data-flow/state/sequence/dependency diagrams; adjacent WMO cache/world-renderer + specs, module registry, `RENDER.md` and M03 Evidence. diff --git a/docs/modules/README.md b/docs/modules/README.md index d54e626..ceed940 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -48,6 +48,7 @@ | WMO render Resource cache state | Implemented extraction | [`wmo-render-resource-cache-state.md`](wmo-render-resource-cache-state.md) | | WMO render Resource finalizer | Implemented extraction | [`wmo-render-resource-finalizer.md`](wmo-render-resource-finalizer.md) | | WMO scene Resource cache state | Implemented extraction | [`wmo-scene-resource-cache-state.md`](wmo-scene-resource-cache-state.md) | +| WMO scene Resource finalizer | Implemented extraction | [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.md) | | Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) | | Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) | | Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) | diff --git a/docs/modules/wmo-scene-resource-cache-state.md b/docs/modules/wmo-scene-resource-cache-state.md index 3decbce..8574bb1 100644 --- a/docs/modules/wmo-scene-resource-cache-state.md +++ b/docs/modules/wmo-scene-resource-cache-state.md @@ -14,8 +14,9 @@ Own mutually exclusive cached, missing and pending states for normalized WMO scene paths. The state accepts only caller-validated `PackedScene` references and -records `.tscn` paths for threaded requests whose I/O, size limit and cache -metadata validation remain in `StreamingWorldLoader`. +records `.tscn` paths for threaded requests. Size admission remains in +`StreamingWorldLoader`; terminal I/O/probe validation belongs to +`WmoSceneResourceFinalizer`. ## Non-goals @@ -33,8 +34,8 @@ flowchart LR Loader --> State[WmoSceneResourceCacheState] Loader --> Size[File existence and size limit] Size --> ResourceLoader[Godot ResourceLoader] - ResourceLoader --> Loader - Loader --> Probe[Instantiate, metadata/version check, free] + ResourceLoader --> Finalizer[WmoSceneResourceFinalizer] + Finalizer --> Probe[Instantiate, metadata/version check, free] Probe -->|accepted PackedScene or missing| State Loader --> Live[Live-prototype fallback] ``` @@ -82,7 +83,7 @@ flowchart TD Check -->|allowed| Request[Start threaded request] Request -->|error| Mark Request -->|accepted| Remember[remember_request] - Remember --> Poll[Loader polls snapshot] + Remember --> Poll[Scene Resource finalizer polls snapshot] Poll -->|failure| CompleteMissing[complete as missing] Poll -->|loaded| Validate[Instantiate and validate cache metadata] Validate -->|valid| CompleteScene[complete with scene] @@ -114,24 +115,27 @@ stateDiagram-v2 sequenceDiagram participant Loader as StreamingWorldLoader participant State as WmoSceneResourceCacheState + participant Finalizer as WmoSceneResourceFinalizer participant RL as ResourceLoader Loader->>State: cached/missing/pending queries Loader->>Loader: exists and wmo_max_runtime_scene_mb check Loader->>RL: load_threaded_request(.tscn) Loader->>State: remember_request(normalized, path) loop renderer tick - Loader->>State: request_paths_snapshot() - Loader->>RL: load_threaded_get_status(path) + Loader->>Finalizer: poll_terminal_requests(State) + Finalizer->>State: request_paths_snapshot() + Finalizer->>RL: load_threaded_get_status(path) end - Loader->>RL: load_threaded_get(path) - Loader->>Loader: instantiate, validate WMO metadata, free probe - Loader->>State: complete with scene or as missing + Finalizer->>RL: load_threaded_get(path) + Finalizer->>Finalizer: instantiate, validate WMO metadata, free probe + Finalizer->>State: complete with scene or as missing ``` ## Ownership, threading and resources - State owns three Dictionaries and strong references to accepted PackedScenes. -- Loader owns paths, file measurement, requests, validation and all Node lifetime. +- Loader owns paths, file measurement, request admission and placed Nodes; + finalizer owns terminal I/O and call-local validation-probe lifetime. - All mutation is serialized on the renderer main thread; no mutex is required. - Detached request snapshots allow terminal removal while polling. - Scene instantiation borrows the PackedScene and does not transfer cache ownership. @@ -143,7 +147,7 @@ sequenceDiagram | Missing `.tscn` | Loader ResourceLoader existence check | Direct missing state | Transient reset permits later retry | | Oversize `.tscn` | Loader byte limit | Missing plus unchanged debug log | Raise limit/rebuild, then reset | | Request-start/load failure | Loader error/status | Direct or terminal missing | Reset and retry later | -| Wrong type/stale metadata | Loader PackedScene/probe validation | Terminal missing; probe freed | Rebuild cache and reset | +| Wrong type/stale metadata | Finalizer PackedScene/probe validation | Terminal missing; probe freed | Rebuild cache and reset | | Shutdown while pending | Loader drains snapshot | Clear transient, then full cache release | New loader starts absent | ## Configuration and capabilities @@ -184,8 +188,9 @@ in the loader; the state emits no logs. | Capability | Status | Evidence | Gap/next step | |---|---|---|---| | Cached WMO PackedScene state | Implemented extraction | Lifecycle/source/timing and shutdown verifiers | Asset-backed traversal/leak evidence pending | -| Size and cache metadata validation | Preserved in loader | Source boundary and WMO regressions | Oversize/stale asset fixtures could follow | -| ResourceLoader I/O and live fallback | Partial/loader-owned | Existing runtime behavior | Separate extraction if justified | +| Size admission | Preserved in loader | Source boundary and WMO regressions | Oversize asset fixture could follow | +| Terminal I/O and cache validation | Implemented extraction | Finalizer type/lifetime/source/timing verifier | Serialized stale fixture pending | +| Live fallback | Loader-owned | Existing runtime behavior | Separate extraction if justified | ## Known gaps and risks @@ -199,7 +204,8 @@ in the loader; the state emits no logs. | Path | Responsibility | |---|---| | `src/render/wmo/wmo_scene_resource_cache_state.gd` | Scene/missing/request state and resets | -| `src/scenes/streaming/streaming_world_loader.gd` | Paths, file limit, requests, validation, fallback and Node lifetime | +| `src/render/wmo/wmo_scene_resource_finalizer.gd` | Terminal I/O, probe validation/lifetime and publication | +| `src/scenes/streaming/streaming_world_loader.gd` | Paths, file limit, request admission, fallback and placed Nodes | | `src/tools/verify_wmo_scene_resource_cache_state.gd` | State, boundary and timing regression | | `src/tools/verify_render_runtime_cache_shutdown.gd` | Loader final cache ownership regression | diff --git a/docs/modules/wmo-scene-resource-finalizer.md b/docs/modules/wmo-scene-resource-finalizer.md new file mode 100644 index 0000000..11a57cf --- /dev/null +++ b/docs/modules/wmo-scene-resource-finalizer.md @@ -0,0 +1,233 @@ +# WMO Scene Resource Finalizer + +## Metadata + +| Field | Value | +|---|---| +| Status | Implemented extraction | +| Target/work package | M03 / `M03-RND-WMO-SCENE-RESOURCE-FINALIZER-001` | +| Owners | Cached WMO PackedScene terminal polling, validation-probe lifetime and scene/missing publication | +| Last verified | Worktree `work/sindo-main-codex/m03-wmo-scene-resource-finalizer`, 2026-07-18 | +| Profiles/capabilities | Profile-independent cached WMO `.tscn` finalization | + +## Purpose + +Finalize pending cached WMO `.tscn` requests outside `StreamingWorldLoader`: +poll terminal status, retrieve a PackedScene, instantiate one validation probe, +apply existing WMOBuilder cache metadata rules, release the probe and publish +either the exact scene or the historical missing outcome. + +## Non-goals + +- Select paths, measure file size or start ResourceLoader requests. +- Instantiate placed WMO scenes or execute live WMOBuilder fallback. +- Own cache lifetime, placements, build queues, permits or attached Nodes. +- Change cache metadata/version rules, fallback order or visible behavior. + +## Context and boundaries + +```mermaid +flowchart LR + Loader[StreamingWorldLoader] -->|compose and tick| Finalizer[WmoSceneResourceFinalizer] + Cache[WmoSceneResourceCacheState] -->|pending snapshot| Finalizer + Finalizer --> ResourceLoader + Finalizer --> Probe[Temporary Node3D probe] + Probe --> Validator[WMOBuilder cache validator] + Finalizer -->|PackedScene or missing| Cache + Cache --> Fallback[Placed cached scene or live fallback] +``` + +The service may depend on ResourceLoader, PackedScene/Node3D lifetime and the +injected cache validator. FileAccess, size policy, placement, attached SceneTree, +scheduler and application layers remain outside it. + +## Public API + +| Symbol | Kind | Purpose | Thread/lifetime | Errors | +|---|---|---|---|---| +| `poll_terminal_requests(cache_state)` | Command/query | Poll every pending scene path once and publish terminal outcomes | Renderer main thread; stateless between calls | Null state returns zero; non-terminal retained | +| `is_scene_cache_current(scene)` | Command/query | Instantiate, validate and release one probe | Renderer main thread; call-local Node | Null/no validator/wrong root/stale false | +| `load_threaded_get_status(path)` | Boundary query | Read threaded status | Renderer main thread; injectable tests | ResourceLoader semantics | +| `load_threaded_get(path)` | Boundary query | Retrieve terminal Resource | Renderer main thread; injectable tests | Null/wrong type rejected | + +## Inputs and outputs + +| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | +|---|---|---|---|---|---| +| Input | Detached normalized-path to `.tscn` path Dictionary | Scene cache state | Finalizer polling | Caller-owned copy | One pass | +| Input | Opaque load status/terminal Resource | ResourceLoader | Finalizer | Value/borrowed Resource | One path | +| Input | Cached WMO PackedScene | ResourceLoader | Probe validator | Borrowed reference | One completion | +| Input | Metadata validator | Loader composition | Finalizer | Borrowed Object | Finalizer lifetime | +| Output | Exact validated PackedScene | Finalizer | Scene cache state | Cache adopts strong reference | Until full clear | +| Output | Missing transition | Finalizer | Scene cache state | Path-only state | Until transient clear | +| Side effect | Temporary validation Node | PackedScene/finalizer | Validator | Finalizer-owned | Freed before return | + +## Data flow + +```mermaid +flowchart TD + Snapshot[Detached pending snapshot] --> Poll[Poll next status in insertion order] + Poll --> Terminal{Loaded or failed?} + Terminal -->|no| Retain[Retain pending] + Terminal -->|failed| Missing[Complete missing] + Terminal -->|loaded| Get[Get Resource] + Get --> Type{PackedScene?} + Type -->|no| Missing + Type -->|yes| Instantiate[Instantiate one probe] + Instantiate --> Root{Node3D root?} + Root -->|no| FreeRejected[Free rejected Node] --> Missing + Root -->|yes| Validate[WMOBuilder metadata validation] + Validate --> Free[Free Node3D probe] + Free --> Current{Current?} + Current -->|yes| Adopt[Adopt exact PackedScene] + Current -->|no| Missing +``` + +## Lifecycle/state + +```mermaid +stateDiagram-v2 + [*] --> Polling + Polling --> Pending: non-terminal + Polling --> Probing: loaded PackedScene + Polling --> Missing: failed/null/wrong type + Probing --> Missing: wrong root or stale metadata + Probing --> Cached: current metadata + Probing --> Released: probe freed before outcome + Released --> Missing + Released --> Cached + Pending --> Polling: later tick +``` + +The finalizer retains no request or Node. Cache state owns Pending/Cached/Missing +lifetime; every instantiated probe is synchronously released before return. + +## Main sequence + +```mermaid +sequenceDiagram + participant Loader as StreamingWorldLoader + participant Finalizer as WmoSceneResourceFinalizer + participant Cache as WmoSceneResourceCacheState + participant RL as ResourceLoader + participant Validator as WMOBuilder validator + Loader->>Finalizer: poll_terminal_requests(Cache) + Finalizer->>Cache: request_paths_snapshot() + loop insertion-ordered paths + Finalizer->>RL: load_threaded_get_status(path) + alt failed + Finalizer->>Cache: complete_request_as_missing(path) + else loaded + Finalizer->>RL: load_threaded_get(path) + Finalizer->>Finalizer: instantiate Node3D probe + Finalizer->>Validator: is_scene_cache_current(probe) + Finalizer->>Finalizer: free probe + Finalizer->>Cache: complete with exact scene or missing + else non-terminal + Finalizer->>Finalizer: retain request + end + end +``` + +## Dependency diagram + +```mermaid +flowchart TB + Loader[StreamingWorldLoader] --> Finalizer[WmoSceneResourceFinalizer] + Loader --> Cache[WmoSceneResourceCacheState] + Finalizer --> Cache + Finalizer --> ResourceLoader + Finalizer --> PackedScene + Finalizer --> Node3D + Finalizer --> Validator[WMOBuilder cache validator] + Finalizer -. no dependency .-> FileAccess + Finalizer -. no dependency .-> Placement + Finalizer -. no dependency .-> Scheduler +``` + +## Ownership, threading and resources + +- Renderer main thread serializes polling, instantiation and cache publication. +- Finalizer owns every call-local validation probe and frees it before return. +- Non-Node3D rejected roots are also freed, closing the prior leak edge. +- Cache state adopts accepted exact PackedScene references until full clear. +- Loader owns admission/size policy, live fallback, placed Nodes and shutdown order. + +## Errors, cancellation and recovery + +| Failure/state | Detection | Behavior | Diagnostic | Recovery | +|---|---|---|---|---| +| Null cache state | Guard | Return zero | Contract verifier | Correct composition | +| Non-terminal | Status | Retain pending | Pending count | Poll later | +| Failed/null/wrong Resource | Status/type | Complete missing | Synthetic fixture | Reset/rebuild cache | +| Non-Node3D root | Probe type | Free probe; complete missing | Type/lifetime fixture | Rebuild cache | +| Stale metadata | Injected validator | Free probe; complete missing | Validation fixture | Rebuild/reset | +| Shutdown | Loader lifecycle | Drain then clear cache | Shutdown verifier | New loader starts absent | + +## Configuration and capabilities + +The loader injects the existing WMOBuilder cache validator. `wmo_cache_dir`, +`wmo_max_runtime_scene_mb`, debug logging, request timing and WMO budgets remain +loader-owned. No setting, profile or cache version is added. + +## Persistence, cache and migration + +No data is serialized. Existing WMOBuilder metadata/version acceptance is called +unchanged before adoption. The rejected-root lifetime fix needs no migration or +rebake and does not alter accepted cache output. + +## Diagnostics and observability + +- Poll returns terminal completion count for tests/future metrics. +- Existing pending count and renderer `wmobuild` metrics remain unchanged. +- No logs are emitted; oversize debug logging remains at loader admission. +- Normalized WMO relative path remains the correlation key. + +## Verification + +- `verify_wmo_scene_resource_finalizer.gd`: null/non-terminal, insertion order, + failed/load boundary, null/wrong/non-Node3D/stale rejection, current exact + identity, accepted/rejected probe release, source ownership and timing. +- Scene cache, render finalizer/cache, WMO queue/planner/registry/resolver, + shutdown, facade, internal-access and baseline regressions cover neighbors. +- Fidelity evidence is exact orchestration extraction plus rejected-root leak + repair; no asset-backed visual or original-client parity claim is made. + +## Extension points + +- Legal current/stale serialized `.tscn` fixtures can extend evidence unchanged. +- Placed-scene preparation stays separate because it mutates an attached WMO + instance rather than a call-local validation probe. + +## Capability status + +| Capability | Status | Evidence | Gap/next step | +|---|---|---|---| +| Cached WMO scene terminal polling | Implemented extraction | Status/order/source/timing verifier | Asset-backed traversal pending | +| Probe validation/lifetime | Implemented extraction | Current/stale/type/free fixtures | Serialized fixture pending | +| Scene/missing publication | Implemented extraction | Exact identity/negative fixtures | Asset-backed lifetime/leak run pending | +| File-size admission/live fallback | Loader-owned | Existing scene-cache regressions | Separate extraction if justified | + +## Known gaps and risks + +- PackedScene instantiation/metadata validation remains synchronous main-thread work. +- No proprietary WMO corpus, serialized stale/oversize fixture, long leak run, + traversal p95/p99 or paired original-client capture is included. + +## Source map + +| Path | Responsibility | +|---|---| +| `src/render/wmo/wmo_scene_resource_finalizer.gd` | Terminal polling, probe validation/lifetime and publication | +| `src/render/wmo/wmo_scene_resource_cache_state.gd` | Scene/missing/request ownership and resets | +| `src/scenes/streaming/streaming_world_loader.gd` | Admission/size policy, live fallback and placed Node ownership | +| `addons/mpq_extractor/loaders/wmo_builder.gd` | Existing cache metadata/version validation | +| `src/tools/verify_wmo_scene_resource_finalizer.gd` | Terminal/probe/lifetime/source/timing regression | + +## Related decisions and references + +- [`wmo-scene-resource-cache-state.md`](wmo-scene-resource-cache-state.md) +- [`wmo-render-resource-finalizer.md`](wmo-render-resource-finalizer.md) +- [`world-renderer.md`](world-renderer.md) +- [`../../RENDER.md`](../../RENDER.md) +- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md) diff --git a/docs/modules/world-renderer.md b/docs/modules/world-renderer.md index 8efc031..effb0d6 100644 --- a/docs/modules/world-renderer.md +++ b/docs/modules/world-renderer.md @@ -167,6 +167,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 | | `WmoRenderResourceFinalizer` | Internal WMO terminal-I/O service | Polls lightweight render requests, validates script/format and publishes Resource/missing outcomes | Renderer main thread; stateless across calls | Non-terminal retained; failed/null/wrong/stale complete missing | | `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 | +| `WmoSceneResourceFinalizer` | Internal WMO terminal-I/O service | Polls cached scene requests, validates/frees probes and publishes scene/missing outcomes | Renderer main thread; stateless across calls | Failed/wrong/stale scenes complete missing; probes always released | | `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 | @@ -416,9 +417,10 @@ sequenceDiagram and pending cache paths; `WmoRenderResourceFinalizer` owns its terminal ResourceLoader polling and script/format validation. `WmoSceneResourceCacheState` similarly owns validated PackedScenes, negative entries and pending `.tscn` - paths. The loader retains request admission, PackedScene/FileAccess I/O, size - and scene-cache validation, live fallback, materialization, permits, validity - reactions and every Node lifecycle action. + paths; `WmoSceneResourceFinalizer` owns terminal ResourceLoader I/O and + validation-probe lifetime. The loader retains request admission, FileAccess + size checks, live fallback, materialization, permits, validity reactions and + every placed-Node lifecycle action. - `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, @@ -618,7 +620,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | WMO render build queue | Implemented extraction | Typed lifecycle/order/ownership/source/timing contract | Materialization and asset-backed traversal/leak evidence pending | | WMO render Resource cache state | Implemented extraction | Scene-free lifecycle/exclusivity/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending | | WMO render Resource finalizer | Implemented extraction | Status/order/script/format/adoption/source/timing contract | Serialized/asset-backed corrupt-cache and leak evidence pending | -| WMO scene Resource cache state | Implemented extraction | Scene-free lifecycle/direct-missing/source/timing plus shutdown contract | ResourceLoader/live-fallback extraction and asset-backed traversal/leak evidence pending | +| WMO scene Resource cache state | Implemented extraction | Scene-free lifecycle/direct-missing/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending | +| WMO scene Resource finalizer | Implemented extraction | Status/order/type/probe/lifetime/adoption/source/timing contract | Serialized stale/oversize and asset-backed evidence pending | | WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity | | ADT water load pipeline state | Implemented extraction | Scene-free FIFO/task/thread/source/timing contract | Parse/finalization and asset-backed traversal/leak evidence pending | | Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity | @@ -694,6 +697,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | `src/render/m2/m2_native_animation_resource_observer.gd` | Native candidate/read/build/cache observation | | `src/render/m2/m2_prototype_cache_state.gd` | Detached prototype ownership and negative lookup outcomes | | `src/render/wmo/wmo_render_resource_finalizer.gd` | Lightweight WMO terminal polling, validation and publication | +| `src/render/wmo/wmo_scene_resource_finalizer.gd` | Cached WMO terminal polling, probe validation/lifetime and publication | | `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 | @@ -729,6 +733,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | `src/tools/verify_m2_native_animation_resource_observer.gd` | Native candidate/cache/raw/build/adoption/boundary/timing regression | | `src/tools/verify_m2_prototype_cache_state.gd` | M2 prototype identity/negative/lifecycle/boundary/timing regression | | `src/tools/verify_wmo_render_resource_finalizer.gd` | WMO render status/order/validation/adoption/boundary/timing regression | +| `src/tools/verify_wmo_scene_resource_finalizer.gd` | WMO scene status/order/probe/lifetime/adoption/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/wmo/wmo_scene_resource_finalizer.gd b/src/render/wmo/wmo_scene_resource_finalizer.gd new file mode 100644 index 0000000..506aff0 --- /dev/null +++ b/src/render/wmo/wmo_scene_resource_finalizer.gd @@ -0,0 +1,97 @@ +class_name WmoSceneResourceFinalizer +extends RefCounted + +## Polls cached WMO PackedScene requests and publishes validated outcomes. +## File-size admission, live fallback and attached Node lifetime remain caller-owned. + +var _scene_cache_validator: Object +var _resource_loader_adapter: Object + + +func _init( + scene_cache_validator: Object, + resource_loader_adapter: Object = null +) -> void: + _scene_cache_validator = scene_cache_validator + _resource_loader_adapter = resource_loader_adapter + + +## Polls a detached pending snapshot in insertion order. Terminal requests are +## published as the exact validated PackedScene or the historical missing state. +func poll_terminal_requests(scene_resource_cache_state: RefCounted) -> int: + if scene_resource_cache_state == null: + return 0 + var completed_request_count := 0 + var request_paths: Dictionary = scene_resource_cache_state.call( + "request_paths_snapshot" + ) + for normalized_relative_path_variant in request_paths.keys(): + var normalized_relative_path := String(normalized_relative_path_variant) + var resource_path := String(request_paths[normalized_relative_path_variant]) + var load_status := load_threaded_get_status(resource_path) + if ( + load_status != ResourceLoader.THREAD_LOAD_LOADED + and load_status != ResourceLoader.THREAD_LOAD_FAILED + ): + continue + if load_status != ResourceLoader.THREAD_LOAD_LOADED: + scene_resource_cache_state.call( + "complete_request_as_missing", + normalized_relative_path + ) + completed_request_count += 1 + continue + var loaded_resource := load_threaded_get(resource_path) + var loaded_scene := loaded_resource as PackedScene + if is_scene_cache_current(loaded_scene): + scene_resource_cache_state.call( + "complete_request_with_scene", + normalized_relative_path, + loaded_scene + ) + else: + scene_resource_cache_state.call( + "complete_request_as_missing", + normalized_relative_path + ) + completed_request_count += 1 + return completed_request_count + + +## Instantiates one Node3D probe, delegates the existing WMOBuilder metadata +## check and synchronously releases the probe before returning. +func is_scene_cache_current(scene: PackedScene) -> bool: + if scene == null or _scene_cache_validator == null: + return false + var instantiated_node := scene.instantiate() + if not (instantiated_node is Node3D): + if instantiated_node != null: + instantiated_node.free() + return false + var instance := instantiated_node as Node3D + var is_current := bool(_scene_cache_validator.call( + "is_scene_cache_current", + instance + )) + instance.free() + return is_current + + +## Production ResourceLoader status boundary; injectable for synthetic tests. +func load_threaded_get_status(resource_path: String) -> int: + if _resource_loader_adapter != null: + return int(_resource_loader_adapter.call( + "load_threaded_get_status", + resource_path + )) + return ResourceLoader.load_threaded_get_status(resource_path) + + +## Production ResourceLoader result boundary; injectable for synthetic tests. +func load_threaded_get(resource_path: String) -> Resource: + if _resource_loader_adapter != null: + return _resource_loader_adapter.call( + "load_threaded_get", + resource_path + ) as Resource + return ResourceLoader.load_threaded_get(resource_path) diff --git a/src/render/wmo/wmo_scene_resource_finalizer.gd.uid b/src/render/wmo/wmo_scene_resource_finalizer.gd.uid new file mode 100644 index 0000000..5243058 --- /dev/null +++ b/src/render/wmo/wmo_scene_resource_finalizer.gd.uid @@ -0,0 +1 @@ +uid://cp2r3tadn8l6q diff --git a/src/scenes/streaming/streaming_world_loader.gd b/src/scenes/streaming/streaming_world_loader.gd index 440e8dc..f6568de 100644 --- a/src/scenes/streaming/streaming_world_loader.gd +++ b/src/scenes/streaming/streaming_world_loader.gd @@ -30,6 +30,9 @@ const WMO_RENDER_RESOURCE_FINALIZER_SCRIPT := preload( const WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT := preload( "res://src/render/wmo/wmo_scene_resource_cache_state.gd" ) +const WMO_SCENE_RESOURCE_FINALIZER_SCRIPT := preload( + "res://src/render/wmo/wmo_scene_resource_finalizer.gd" +) const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd") const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd") const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd") @@ -370,6 +373,9 @@ var _wmo_render_resource_finalizer := WMO_RENDER_RESOURCE_FINALIZER_SCRIPT.new( ) var _wmo_render_build_queue := WMO_RENDER_BUILD_QUEUE_SCRIPT.new() var _wmo_scene_resource_cache_state := WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT.new() +var _wmo_scene_resource_finalizer := WMO_SCENE_RESOURCE_FINALIZER_SCRIPT.new( + WMO_BUILDER_SCRIPT +) var _wmo_missing_cache: Dictionary = {} var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new() var _world_wmo_root: Node3D @@ -3711,24 +3717,9 @@ func _process_wmo_build_jobs() -> void: func _drain_wmo_scene_loads() -> void: - var request_paths := _wmo_scene_resource_cache_state.request_paths_snapshot() - for normalized_rel_variant in request_paths.keys(): - var normalized_rel := String(normalized_rel_variant) - var path := String(request_paths[normalized_rel]) - var status := ResourceLoader.load_threaded_get_status(path) - if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED: - continue - if status != ResourceLoader.THREAD_LOAD_LOADED: - _wmo_scene_resource_cache_state.complete_request_as_missing(normalized_rel) - continue - var resource: Resource = ResourceLoader.load_threaded_get(path) - if resource is PackedScene and _is_wmo_scene_cache_current(resource as PackedScene): - _wmo_scene_resource_cache_state.complete_request_with_scene( - normalized_rel, - resource as PackedScene - ) - else: - _wmo_scene_resource_cache_state.complete_request_as_missing(normalized_rel) + _wmo_scene_resource_finalizer.poll_terminal_requests( + _wmo_scene_resource_cache_state + ) func _drain_wmo_render_loads() -> void: @@ -3926,17 +3917,6 @@ func _get_resource_file_size(path: String) -> int: return int(length) -func _is_wmo_scene_cache_current(scene: PackedScene) -> bool: - if scene == null: - return false - var instance := scene.instantiate() as Node3D - if instance == null: - return false - var current := _is_wmo_node_cache_current(instance) - instance.free() - return current - - func _is_wmo_node_cache_current(node: Node) -> bool: if node == null: return false diff --git a/src/tools/verify_wmo_scene_resource_cache_state.gd b/src/tools/verify_wmo_scene_resource_cache_state.gd index dffec63..0c7d8b8 100644 --- a/src/tools/verify_wmo_scene_resource_cache_state.gd +++ b/src/tools/verify_wmo_scene_resource_cache_state.gd @@ -6,6 +6,7 @@ const CACHE_STATE_SCRIPT := preload( "res://src/render/wmo/wmo_scene_resource_cache_state.gd" ) const CACHE_STATE_PATH := "res://src/render/wmo/wmo_scene_resource_cache_state.gd" +const FINALIZER_PATH := "res://src/render/wmo/wmo_scene_resource_finalizer.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" @@ -233,6 +234,7 @@ func _verify_detached_sorted_diagnostics(failures: Array[String]) -> void: func _verify_ownership_boundaries(failures: Array[String]) -> void: var loader_source := _read_text(LOADER_PATH, failures) var cache_state_source := _read_text(CACHE_STATE_PATH, failures) + var finalizer_source := _read_text(FINALIZER_PATH, failures) _expect_true( loader_source.contains("WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT.new()"), "loader composes scene cache state", @@ -266,15 +268,22 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void: for retained_loader_rule in [ "wmo_max_runtime_scene_mb", "_get_resource_file_size(cache_path)", - "_is_wmo_scene_cache_current(resource as PackedScene)", - "scene.instantiate() as Node3D", - "WMO_BUILDER_SCRIPT.is_scene_cache_current(node)", ]: _expect_true( loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures ) + for finalizer_rule in [ + "scene.instantiate()", + "is_scene_cache_current", + "instance.free()", + ]: + _expect_true( + finalizer_source.contains(finalizer_rule), + "finalizer owns %s" % finalizer_rule, + failures + ) func _verify_bounded_timing(failures: Array[String]) -> float: diff --git a/src/tools/verify_wmo_scene_resource_finalizer.gd b/src/tools/verify_wmo_scene_resource_finalizer.gd new file mode 100644 index 0000000..fd38eaa --- /dev/null +++ b/src/tools/verify_wmo_scene_resource_finalizer.gd @@ -0,0 +1,346 @@ +extends SceneTree + +## Asset-free terminal-I/O, probe validation/lifetime, adoption, boundary and +## timing regression for the cached WMO PackedScene finalizer. + +const FINALIZER_SCRIPT := preload("res://src/render/wmo/wmo_scene_resource_finalizer.gd") +const CACHE_STATE_SCRIPT := preload("res://src/render/wmo/wmo_scene_resource_cache_state.gd") +const FINALIZER_PATH := "res://src/render/wmo/wmo_scene_resource_finalizer.gd" +const CACHE_STATE_PATH := "res://src/render/wmo/wmo_scene_resource_cache_state.gd" +const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" + + +class FakeSceneCacheValidator extends RefCounted: + var validated_nodes: Array[Node] = [] + + func is_scene_cache_current(node: Node) -> bool: + validated_nodes.append(node) + return bool(node.get_meta("current_wmo_cache", false)) + + +class FakeResourceLoaderAdapter extends RefCounted: + var status_by_path: Dictionary = {} + var resource_by_path: Dictionary = {} + var status_paths: Array[String] = [] + var loaded_paths: Array[String] = [] + + func load_threaded_get_status(resource_path: String) -> int: + status_paths.append(resource_path) + return int(status_by_path.get(resource_path, ResourceLoader.THREAD_LOAD_IN_PROGRESS)) + + func load_threaded_get(resource_path: String) -> Resource: + loaded_paths.append(resource_path) + return resource_by_path.get(resource_path) as Resource + + +func _initialize() -> void: + var failures: Array[String] = [] + _verify_null_and_non_terminal_poll(failures) + _verify_failed_and_loaded_poll_order(failures) + _verify_loaded_resource_rejections(failures) + _verify_current_scene_adoption_and_probe_lifetime(failures) + _verify_validation_contract(failures) + _verify_source_boundaries(failures) + var elapsed_milliseconds := _verify_bounded_timing(failures) + if not failures.is_empty(): + for failure in failures: + push_error("WMO_SCENE_RESOURCE_FINALIZER: %s" % failure) + quit(1) + return + print( + "WMO_SCENE_RESOURCE_FINALIZER PASS cases=26 iterations=1000 elapsed_ms=%.3f" + % elapsed_milliseconds + ) + quit(0) + + +func _verify_null_and_non_terminal_poll(failures: Array[String]) -> void: + var validator := FakeSceneCacheValidator.new() + var adapter := FakeResourceLoaderAdapter.new() + var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator, adapter) + _expect_equal( + int(finalizer.call("poll_terminal_requests", null)), + 0, + "null cache state rejected", + failures + ) + var cache_state: RefCounted = CACHE_STATE_SCRIPT.new() + cache_state.call("remember_request", "world/pending.wmo", "res://cache/pending.tscn") + _expect_equal( + int(finalizer.call("poll_terminal_requests", cache_state)), + 0, + "non-terminal request retained", + failures + ) + _expect_true( + bool(cache_state.call("has_request", "world/pending.wmo")), + "pending state remains", + failures + ) + _expect_string_array( + adapter.status_paths, + ["res://cache/pending.tscn"], + "pending path polled once", + failures + ) + _expect_equal(adapter.loaded_paths.size(), 0, "pending scene not loaded", failures) + + +func _verify_failed_and_loaded_poll_order(failures: Array[String]) -> void: + var validator := FakeSceneCacheValidator.new() + var adapter := FakeResourceLoaderAdapter.new() + var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator, adapter) + var current_scene := _packed_scene(true, true, failures) + var cache_state: RefCounted = CACHE_STATE_SCRIPT.new() + var requests := { + "world/pending.wmo": "res://cache/pending.tscn", + "world/failed.wmo": "res://cache/failed.tscn", + "world/loaded.wmo": "res://cache/loaded.tscn", + } + for normalized_relative_path in requests: + cache_state.call( + "remember_request", + normalized_relative_path, + requests[normalized_relative_path] + ) + adapter.status_by_path = { + "res://cache/failed.tscn": ResourceLoader.THREAD_LOAD_FAILED, + "res://cache/loaded.tscn": ResourceLoader.THREAD_LOAD_LOADED, + } + adapter.resource_by_path["res://cache/loaded.tscn"] = current_scene + _expect_equal( + int(finalizer.call("poll_terminal_requests", cache_state)), + 2, + "failed and loaded scenes complete", + failures + ) + _expect_string_array( + adapter.status_paths, + [ + "res://cache/pending.tscn", + "res://cache/failed.tscn", + "res://cache/loaded.tscn", + ], + "detached insertion order preserved", + failures + ) + _expect_string_array( + adapter.loaded_paths, + ["res://cache/loaded.tscn"], + "only loaded terminal scene retrieved", + failures + ) + _expect_true( + bool(cache_state.call("is_missing", "world/failed.wmo")), + "failed request publishes missing", + failures + ) + _expect_true( + cache_state.call("scene_for", "world/loaded.wmo") == current_scene, + "loaded scene identity adopted", + failures + ) + + +func _verify_loaded_resource_rejections(failures: Array[String]) -> void: + var validator := FakeSceneCacheValidator.new() + var adapter := FakeResourceLoaderAdapter.new() + var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator, adapter) + var rejected_resources := { + "null": null, + "wrong_type": Resource.new(), + "stale": _packed_scene(false, true, failures), + "non_node_3d": _packed_scene(true, false, failures), + } + var cache_state: RefCounted = CACHE_STATE_SCRIPT.new() + for rejection_name in rejected_resources: + var normalized_relative_path := "world/%s.wmo" % rejection_name + var cache_path := "res://cache/%s.tscn" % rejection_name + cache_state.call("remember_request", normalized_relative_path, cache_path) + adapter.status_by_path[cache_path] = ResourceLoader.THREAD_LOAD_LOADED + adapter.resource_by_path[cache_path] = rejected_resources[rejection_name] + _expect_equal( + int(finalizer.call("poll_terminal_requests", cache_state)), + 4, + "all invalid loaded Resources complete", + failures + ) + for rejection_name in rejected_resources: + _expect_true( + bool(cache_state.call("is_missing", "world/%s.wmo" % rejection_name)), + "%s loaded Resource publishes missing" % rejection_name, + failures + ) + _expect_equal( + validator.validated_nodes.size(), + 1, + "only Node3D stale scene reaches validator", + failures + ) + _expect_false( + is_instance_valid(validator.validated_nodes[0]), + "stale validation probe released", + failures + ) + + +func _verify_current_scene_adoption_and_probe_lifetime(failures: Array[String]) -> void: + var validator := FakeSceneCacheValidator.new() + var adapter := FakeResourceLoaderAdapter.new() + var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator, adapter) + var current_scene := _packed_scene(true, true, failures) + var cache_state: RefCounted = CACHE_STATE_SCRIPT.new() + cache_state.call("remember_request", "world/current.wmo", "res://cache/current.tscn") + adapter.status_by_path["res://cache/current.tscn"] = ResourceLoader.THREAD_LOAD_LOADED + adapter.resource_by_path["res://cache/current.tscn"] = current_scene + finalizer.call("poll_terminal_requests", cache_state) + _expect_true( + cache_state.call("scene_for", "world/current.wmo") == current_scene, + "current PackedScene exact identity adopted", + failures + ) + _expect_equal( + validator.validated_nodes.size(), + 1, + "current scene instantiated exactly once", + failures + ) + _expect_false( + is_instance_valid(validator.validated_nodes[0]), + "accepted validation probe released", + failures + ) + + +func _verify_validation_contract(failures: Array[String]) -> void: + var validator := FakeSceneCacheValidator.new() + var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator) + var current_scene := _packed_scene(true, true, failures) + _expect_true( + bool(finalizer.call("is_scene_cache_current", current_scene)), + "current Node3D scene accepted", + failures + ) + _expect_false( + bool(finalizer.call("is_scene_cache_current", null)), + "null scene rejected", + failures + ) + var missing_validator_finalizer: RefCounted = FINALIZER_SCRIPT.new(null) + _expect_false( + bool(missing_validator_finalizer.call("is_scene_cache_current", current_scene)), + "missing validator rejects scene without instantiation", + failures + ) + + +func _verify_source_boundaries(failures: Array[String]) -> void: + var loader_source := _read_text(LOADER_PATH, failures) + var finalizer_source := _read_text(FINALIZER_PATH, failures) + var cache_source := _read_text(CACHE_STATE_PATH, failures) + _expect_true( + loader_source.contains("WMO_SCENE_RESOURCE_FINALIZER_SCRIPT.new("), + "loader composes scene finalizer", + failures + ) + _expect_true( + loader_source.contains("_wmo_scene_resource_finalizer.poll_terminal_requests("), + "loader delegates scene terminal polling", + failures + ) + _expect_false( + loader_source.contains("func _is_wmo_scene_cache_current("), + "loader releases PackedScene probe validation", + failures + ) + for required_token in [ + "ResourceLoader.load_threaded_get_status(resource_path)", + "ResourceLoader.load_threaded_get(resource_path)", + "scene.instantiate()", + "is_scene_cache_current", + "instance.free()", + "complete_request_with_scene", + "complete_request_as_missing", + ]: + _expect_true( + finalizer_source.contains(required_token), + "finalizer owns %s" % required_token, + failures + ) + _expect_false( + cache_source.contains("ResourceLoader."), + "cache state remains free of ResourceLoader", + failures + ) + + +func _verify_bounded_timing(failures: Array[String]) -> float: + var adapter := FakeResourceLoaderAdapter.new() + var finalizer: RefCounted = FINALIZER_SCRIPT.new( + FakeSceneCacheValidator.new(), + adapter + ) + var started_microseconds := Time.get_ticks_usec() + for iteration in range(1000): + var cache_state: RefCounted = CACHE_STATE_SCRIPT.new() + var normalized_relative_path := "world/object_%d.wmo" % iteration + var cache_path := "res://cache/object_%d.tscn" % iteration + cache_state.call("remember_request", normalized_relative_path, cache_path) + adapter.status_by_path[cache_path] = ResourceLoader.THREAD_LOAD_FAILED + finalizer.call("poll_terminal_requests", cache_state) + var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0 + if elapsed_milliseconds >= 1000.0: + failures.append( + "1000 terminal polls took %.3fms (budget < 1000ms)" % elapsed_milliseconds + ) + return elapsed_milliseconds + + +func _packed_scene( + is_current: bool, + use_node_3d_root: bool, + failures: Array[String] +) -> PackedScene: + var root: Node = Node3D.new() if use_node_3d_root else Node.new() + root.set_meta("current_wmo_cache", is_current) + var scene := PackedScene.new() + var pack_error := scene.pack(root) + root.free() + if pack_error != OK: + failures.append("cannot pack synthetic scene: %d" % pack_error) + return scene + + +func _read_text(path: String, failures: Array[String]) -> String: + var file := FileAccess.open(path, FileAccess.READ) + if file == null: + failures.append("cannot read %s" % path) + return "" + var source := file.get_as_text() + file.close() + return source + + +func _expect_true(value: bool, label: String, failures: Array[String]) -> void: + if not value: + failures.append(label) + + +func _expect_false(value: bool, label: String, failures: Array[String]) -> void: + if value: + failures.append(label) + + +func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void: + if actual != expected: + failures.append("%s: expected %d, got %d" % [label, expected, actual]) + + +func _expect_string_array( + actual: Array, + expected: Array, + label: String, + failures: Array[String] +) -> void: + if actual != expected: + failures.append("%s: expected %s, got %s" % [label, expected, actual]) diff --git a/src/tools/verify_wmo_scene_resource_finalizer.gd.uid b/src/tools/verify_wmo_scene_resource_finalizer.gd.uid new file mode 100644 index 0000000..d96b1ef --- /dev/null +++ b/src/tools/verify_wmo_scene_resource_finalizer.gd.uid @@ -0,0 +1 @@ +uid://cs6gj0x3i3fg6 diff --git a/targets/03-renderer-facade.md b/targets/03-renderer-facade.md index 5dbe2c4..13b16bb 100644 --- a/targets/03-renderer-facade.md +++ b/targets/03-renderer-facade.md @@ -43,6 +43,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру `M03-RND-WMO-RENDER-RESOURCE-CACHE-001`, `M03-RND-WMO-RENDER-RESOURCE-FINALIZER-001`, `M03-RND-WMO-SCENE-RESOURCE-CACHE-001`, + `M03-RND-WMO-SCENE-RESOURCE-FINALIZER-001`, `M03-RND-ADT-WATER-LOAD-PIPELINE-001`, `M03-RND-ADT-WATER-SCENE-FINALIZER-001`, `M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001`, @@ -142,6 +143,13 @@ Runtime и Editor используют facade; planner/scheduler тестиру Post-merge scene state (`40.113ms`), render state (`43.434ms`), shutdown, WMO queue/resolver, facade, internal-access `40`, `7/7` manifest, documentation and coordination gates remained green. + WMO scene Resource finalizer passed `cases=26 iterations=1000 + elapsed_ms=4.186` with insertion-order status polling, non-terminal retention, + failed/null/wrong-type/non-Node3D/stale rejection, current exact PackedScene + adoption, accepted/rejected probe release and source boundaries. All 61 + autonomous headless regressions passed; internal-access remained `30`, + documentation covered `48` module specifications, coordination retained `34` + historical expired-claim warnings and checkpoint dry-run kept all `7/7` plans. ADT water load pipeline state passed `cases=12 iterations=100 elapsed_ms=86.170`; its initial direct no-`.godot` run passed (`94.576ms`), WMO caches/queue/planner/registry/resolver/material/shutdown and 18 adjacent @@ -407,9 +415,10 @@ Runtime и Editor используют facade; planner/scheduler тестиру loader-owned request admission and shutdown drain order are unchanged. Cached WMO scene paths retain direct negative caching for missing/oversize/ request-start failures, terminal missing state for load/validation failures and - the prior transient-reset versus final-shutdown lifetime. File-size limits, - `PackedScene` probe instantiation, WMOBuilder metadata validation and probe-node - destruction remain loader-owned before state accepts a scene. + the prior transient-reset versus final-shutdown lifetime. File-size limits and + request admission remain loader-owned; terminal I/O, `PackedScene` probe, + WMOBuilder metadata validation and probe release belong to the scene finalizer. + Rejected non-Node3D probes are now freed without changing fallback or output. ADT water requests retain FIFO order and tile-key deduplication; tile release removes pending requests without cancelling active work, task completion and result publication order are unchanged, and the mutex remains limited to the @@ -487,8 +496,9 @@ Runtime и Editor используют facade; planner/scheduler тестиру typed WMO render build job/queue and loader enqueue/drain/cancel/clear adapters, lightweight WMO render Resource/missing/request state and loader lookup/request/ reset adapters, plus terminal Resource polling/validation/publication finalizer, - cached WMO PackedScene/missing/request state and loader file-size/I/O/probe/ - validation/reset adapters, + cached WMO PackedScene/missing/request state and loader file-size/admission/ + reset adapters, plus terminal PackedScene polling/probe/lifetime/publication + finalizer, ADT water pending/task/result pipeline state and loader enqueue/start/publish/ drain/release/reset/shutdown/metrics adapters, stateless ADT water scene finalizer and both loader build/attach adapters, @@ -548,8 +558,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру observers produce typed per-operation state, including native observation; animation and static Mesh terminal ResourceLoader drains are extracted, while action execution, material-prototype lookup and root cleanup remain loader-owned; - cached-scene WMO ResourceLoader/FileAccess I/O, live fallback and group - materialization remain in the loader; lightweight render terminal I/O is separated; + cached-scene WMO FileAccess/size admission, live fallback and group + materialization remain in the loader; both WMO terminal I/O paths are separated; asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending; ADT water parsing and active worker interruption remain loader-owned; materialization is separated but remains synchronous main-thread work behind