merge: WMO scene resource finalizer

This commit is contained in:
2026-07-18 14:17:09 +04:00
13 changed files with 780 additions and 67 deletions
+19 -2
View File
@@ -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_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_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_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/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_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/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, - `WmoSceneResourceCacheState` now owns validated cached-WMO PackedScenes,
negative entries and normalized-path to pending-`.tscn` records. negative entries and normalized-path to pending-`.tscn` records.
- `StreamingWorldLoader` still checks file existence and - `StreamingWorldLoader` still checks file existence and
`wmo_max_runtime_scene_mb`, calls `ResourceLoader`, instantiates a validation `wmo_max_runtime_scene_mb` and starts requests. `WmoSceneResourceFinalizer`
probe, checks WMOBuilder cache metadata and frees the probe before adoption. 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 - Missing files, oversize scenes, request errors, load failures and stale scenes
retain their prior negative-cache and live-prototype fallback behavior. retain their prior negative-cache and live-prototype fallback behavior.
- Map reset clears pending/negative state while retaining accepted scenes; final - 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 - Asset-backed oversize/stale fixtures, traversal/leak p95/p99 and paired fidelity
evidence remain pending. 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 ## 2026-07-17 ADT Water Load Pipeline State Extraction
- `AdtWaterLoadPipelineState` now owns ADT water pending FIFO/deduplication, - `AdtWaterLoadPipelineState` now owns ADT water pending FIFO/deduplication,
@@ -55,14 +55,21 @@ probe lifetime and scene/missing publication from the loader into one service.
## Status ## Status
- State: active - State: ready
- Done: ownership/compatibility contract - Done: implementation, verification and documentation
- Next: implementation, verification and documentation - Next: merge and post-merge acceptance
- Blocked by: - Blocked by:
## Handoff ## Handoff
- Commit: - Commit: `7779e9e` (`render: extract WMO scene resource finalizer`)
- Results: - Results: finalizer PASS `cases=26 iterations=1000 elapsed_ms=4.186`;
- Remaining risks: adjacent WMO scene/render cache/finalizer/queue/planner/registry/resolver and
- Documentation updated: 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.
+1
View File
@@ -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 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 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 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) | | Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) | | Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) | | Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
+21 -15
View File
@@ -14,8 +14,9 @@
Own mutually exclusive cached, missing and pending states for normalized WMO Own mutually exclusive cached, missing and pending states for normalized WMO
scene paths. The state accepts only caller-validated `PackedScene` references and scene paths. The state accepts only caller-validated `PackedScene` references and
records `.tscn` paths for threaded requests whose I/O, size limit and cache records `.tscn` paths for threaded requests. Size admission remains in
metadata validation remain in `StreamingWorldLoader`. `StreamingWorldLoader`; terminal I/O/probe validation belongs to
`WmoSceneResourceFinalizer`.
## Non-goals ## Non-goals
@@ -33,8 +34,8 @@ flowchart LR
Loader --> State[WmoSceneResourceCacheState] Loader --> State[WmoSceneResourceCacheState]
Loader --> Size[File existence and size limit] Loader --> Size[File existence and size limit]
Size --> ResourceLoader[Godot ResourceLoader] Size --> ResourceLoader[Godot ResourceLoader]
ResourceLoader --> Loader ResourceLoader --> Finalizer[WmoSceneResourceFinalizer]
Loader --> Probe[Instantiate, metadata/version check, free] Finalizer --> Probe[Instantiate, metadata/version check, free]
Probe -->|accepted PackedScene or missing| State Probe -->|accepted PackedScene or missing| State
Loader --> Live[Live-prototype fallback] Loader --> Live[Live-prototype fallback]
``` ```
@@ -82,7 +83,7 @@ flowchart TD
Check -->|allowed| Request[Start threaded request] Check -->|allowed| Request[Start threaded request]
Request -->|error| Mark Request -->|error| Mark
Request -->|accepted| Remember[remember_request] Request -->|accepted| Remember[remember_request]
Remember --> Poll[Loader polls snapshot] Remember --> Poll[Scene Resource finalizer polls snapshot]
Poll -->|failure| CompleteMissing[complete as missing] Poll -->|failure| CompleteMissing[complete as missing]
Poll -->|loaded| Validate[Instantiate and validate cache metadata] Poll -->|loaded| Validate[Instantiate and validate cache metadata]
Validate -->|valid| CompleteScene[complete with scene] Validate -->|valid| CompleteScene[complete with scene]
@@ -114,24 +115,27 @@ stateDiagram-v2
sequenceDiagram sequenceDiagram
participant Loader as StreamingWorldLoader participant Loader as StreamingWorldLoader
participant State as WmoSceneResourceCacheState participant State as WmoSceneResourceCacheState
participant Finalizer as WmoSceneResourceFinalizer
participant RL as ResourceLoader participant RL as ResourceLoader
Loader->>State: cached/missing/pending queries Loader->>State: cached/missing/pending queries
Loader->>Loader: exists and wmo_max_runtime_scene_mb check Loader->>Loader: exists and wmo_max_runtime_scene_mb check
Loader->>RL: load_threaded_request(.tscn) Loader->>RL: load_threaded_request(.tscn)
Loader->>State: remember_request(normalized, path) Loader->>State: remember_request(normalized, path)
loop renderer tick loop renderer tick
Loader->>State: request_paths_snapshot() Loader->>Finalizer: poll_terminal_requests(State)
Loader->>RL: load_threaded_get_status(path) Finalizer->>State: request_paths_snapshot()
Finalizer->>RL: load_threaded_get_status(path)
end end
Loader->>RL: load_threaded_get(path) Finalizer->>RL: load_threaded_get(path)
Loader->>Loader: instantiate, validate WMO metadata, free probe Finalizer->>Finalizer: instantiate, validate WMO metadata, free probe
Loader->>State: complete with scene or as missing Finalizer->>State: complete with scene or as missing
``` ```
## Ownership, threading and resources ## Ownership, threading and resources
- State owns three Dictionaries and strong references to accepted PackedScenes. - 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. - All mutation is serialized on the renderer main thread; no mutex is required.
- Detached request snapshots allow terminal removal while polling. - Detached request snapshots allow terminal removal while polling.
- Scene instantiation borrows the PackedScene and does not transfer cache ownership. - 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 | | 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 | | 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 | | 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 | | Shutdown while pending | Loader drains snapshot | Clear transient, then full cache release | New loader starts absent |
## Configuration and capabilities ## Configuration and capabilities
@@ -184,8 +188,9 @@ in the loader; the state emits no logs.
| Capability | Status | Evidence | Gap/next step | | Capability | Status | Evidence | Gap/next step |
|---|---|---|---| |---|---|---|---|
| Cached WMO PackedScene state | Implemented extraction | Lifecycle/source/timing and shutdown verifiers | Asset-backed traversal/leak evidence pending | | 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 | | Size admission | Preserved in loader | Source boundary and WMO regressions | Oversize asset fixture could follow |
| ResourceLoader I/O and live fallback | Partial/loader-owned | Existing runtime behavior | Separate extraction if justified | | 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 ## Known gaps and risks
@@ -199,7 +204,8 @@ in the loader; the state emits no logs.
| Path | Responsibility | | Path | Responsibility |
|---|---| |---|---|
| `src/render/wmo/wmo_scene_resource_cache_state.gd` | Scene/missing/request state and resets | | `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_wmo_scene_resource_cache_state.gd` | State, boundary and timing regression |
| `src/tools/verify_render_runtime_cache_shutdown.gd` | Loader final cache ownership regression | | `src/tools/verify_render_runtime_cache_shutdown.gd` | Loader final cache ownership regression |
@@ -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)
+9 -4
View File
@@ -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 | | `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 | | `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 | | `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 | | `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 | | `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 and pending cache paths; `WmoRenderResourceFinalizer` owns its terminal
ResourceLoader polling and script/format validation. `WmoSceneResourceCacheState` ResourceLoader polling and script/format validation. `WmoSceneResourceCacheState`
similarly owns validated PackedScenes, negative entries and pending `.tscn` similarly owns validated PackedScenes, negative entries and pending `.tscn`
paths. The loader retains request admission, PackedScene/FileAccess I/O, size paths; `WmoSceneResourceFinalizer` owns terminal ResourceLoader I/O and
and scene-cache validation, live fallback, materialization, permits, validity validation-probe lifetime. The loader retains request admission, FileAccess
reactions and every Node lifecycle action. size checks, live fallback, materialization, permits, validity reactions and
every placed-Node lifecycle action.
- `AdtWaterLoadPipelineState` owns pending request order/deduplication, opaque - `AdtWaterLoadPipelineState` owns pending request order/deduplication, opaque
active task IDs and the worker-safe parsed-result mailbox. The loader retains active task IDs and the worker-safe parsed-result mailbox. The loader retains
WorkerThreadPool start/wait, ADTLoader parsing, concurrency/finalize permits, 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 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 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 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 | | 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 | | 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 | | 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_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/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_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_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_policy.gd` | Immutable renderer radius/prefetch policy |
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets | | `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_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_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_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_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_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 | | `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
@@ -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)
@@ -0,0 +1 @@
uid://cp2r3tadn8l6q
+9 -29
View File
@@ -30,6 +30,9 @@ const WMO_RENDER_RESOURCE_FINALIZER_SCRIPT := preload(
const WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT := preload( const WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT := preload(
"res://src/render/wmo/wmo_scene_resource_cache_state.gd" "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_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 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") 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_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_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_missing_cache: Dictionary = {}
var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new() var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new()
var _world_wmo_root: Node3D var _world_wmo_root: Node3D
@@ -3711,24 +3717,9 @@ func _process_wmo_build_jobs() -> void:
func _drain_wmo_scene_loads() -> void: func _drain_wmo_scene_loads() -> void:
var request_paths := _wmo_scene_resource_cache_state.request_paths_snapshot() _wmo_scene_resource_finalizer.poll_terminal_requests(
for normalized_rel_variant in request_paths.keys(): _wmo_scene_resource_cache_state
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)
func _drain_wmo_render_loads() -> void: func _drain_wmo_render_loads() -> void:
@@ -3926,17 +3917,6 @@ func _get_resource_file_size(path: String) -> int:
return int(length) 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: func _is_wmo_node_cache_current(node: Node) -> bool:
if node == null: if node == null:
return false return false
@@ -6,6 +6,7 @@ const CACHE_STATE_SCRIPT := preload(
"res://src/render/wmo/wmo_scene_resource_cache_state.gd" "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 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" 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: func _verify_ownership_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures) var loader_source := _read_text(LOADER_PATH, failures)
var cache_state_source := _read_text(CACHE_STATE_PATH, failures) var cache_state_source := _read_text(CACHE_STATE_PATH, failures)
var finalizer_source := _read_text(FINALIZER_PATH, failures)
_expect_true( _expect_true(
loader_source.contains("WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT.new()"), loader_source.contains("WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT.new()"),
"loader composes scene cache state", "loader composes scene cache state",
@@ -266,15 +268,22 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
for retained_loader_rule in [ for retained_loader_rule in [
"wmo_max_runtime_scene_mb", "wmo_max_runtime_scene_mb",
"_get_resource_file_size(cache_path)", "_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( _expect_true(
loader_source.contains(retained_loader_rule), loader_source.contains(retained_loader_rule),
"loader retains %s" % retained_loader_rule, "loader retains %s" % retained_loader_rule,
failures 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: func _verify_bounded_timing(failures: Array[String]) -> float:
@@ -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])
@@ -0,0 +1 @@
uid://cs6gj0x3i3fg6
+17 -7
View File
@@ -43,6 +43,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
`M03-RND-WMO-RENDER-RESOURCE-CACHE-001`, `M03-RND-WMO-RENDER-RESOURCE-CACHE-001`,
`M03-RND-WMO-RENDER-RESOURCE-FINALIZER-001`, `M03-RND-WMO-RENDER-RESOURCE-FINALIZER-001`,
`M03-RND-WMO-SCENE-RESOURCE-CACHE-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-LOAD-PIPELINE-001`,
`M03-RND-ADT-WATER-SCENE-FINALIZER-001`, `M03-RND-ADT-WATER-SCENE-FINALIZER-001`,
`M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-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, Post-merge scene state (`40.113ms`), render state (`43.434ms`), shutdown,
WMO queue/resolver, facade, internal-access `40`, `7/7` manifest, WMO queue/resolver, facade, internal-access `40`, `7/7` manifest,
documentation and coordination gates remained green. 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 ADT water load pipeline state passed `cases=12 iterations=100
elapsed_ms=86.170`; its initial direct no-`.godot` run passed (`94.576ms`), elapsed_ms=86.170`; its initial direct no-`.godot` run passed (`94.576ms`),
WMO caches/queue/planner/registry/resolver/material/shutdown and 18 adjacent 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. loader-owned request admission and shutdown drain order are unchanged.
Cached WMO scene paths retain direct negative caching for missing/oversize/ Cached WMO scene paths retain direct negative caching for missing/oversize/
request-start failures, terminal missing state for load/validation failures and request-start failures, terminal missing state for load/validation failures and
the prior transient-reset versus final-shutdown lifetime. File-size limits, the prior transient-reset versus final-shutdown lifetime. File-size limits and
`PackedScene` probe instantiation, WMOBuilder metadata validation and probe-node request admission remain loader-owned; terminal I/O, `PackedScene` probe,
destruction remain loader-owned before state accepts a scene. 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 ADT water requests retain FIFO order and tile-key deduplication; tile release
removes pending requests without cancelling active work, task completion and removes pending requests without cancelling active work, task completion and
result publication order are unchanged, and the mutex remains limited to the 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, typed WMO render build job/queue and loader enqueue/drain/cancel/clear adapters,
lightweight WMO render Resource/missing/request state and loader lookup/request/ lightweight WMO render Resource/missing/request state and loader lookup/request/
reset adapters, plus terminal Resource polling/validation/publication finalizer, reset adapters, plus terminal Resource polling/validation/publication finalizer,
cached WMO PackedScene/missing/request state and loader file-size/I/O/probe/ cached WMO PackedScene/missing/request state and loader file-size/admission/
validation/reset adapters, reset adapters, plus terminal PackedScene polling/probe/lifetime/publication
finalizer,
ADT water pending/task/result pipeline state and loader enqueue/start/publish/ ADT water pending/task/result pipeline state and loader enqueue/start/publish/
drain/release/reset/shutdown/metrics adapters, drain/release/reset/shutdown/metrics adapters,
stateless ADT water scene finalizer and both loader build/attach 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; observers produce typed per-operation state, including native observation;
animation and static Mesh terminal ResourceLoader drains are extracted, while animation and static Mesh terminal ResourceLoader drains are extracted, while
action execution, material-prototype lookup and root cleanup remain loader-owned; action execution, material-prototype lookup and root cleanup remain loader-owned;
cached-scene WMO ResourceLoader/FileAccess I/O, live fallback and group cached-scene WMO FileAccess/size admission, live fallback and group
materialization remain in the loader; lightweight render terminal I/O is separated; 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; asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending;
ADT water parsing and active worker interruption remain loader-owned; ADT water parsing and active worker interruption remain loader-owned;
materialization is separated but remains synchronous main-thread work behind materialization is separated but remains synchronous main-thread work behind