render: extract WMO scene instance factory

Work-Package: M03-RND-WMO-SCENE-INSTANCE-FACTORY-001
Agent: sindo-main-codex
Tests: 65/66 headless verifiers; proprietary ADT probe unavailable; checkpoint dry-run 7/7; docs and coordination passed
Fidelity: valid cached/live naming, placement, validation and Resource identity preserved; invalid-root leak fixed
This commit is contained in:
2026-08-01 10:49:56 +04:00
parent 07521ee6a4
commit 7e97b19095
11 changed files with 668 additions and 38 deletions
+12
View File
@@ -1491,6 +1491,18 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
queues and permits remain loader-owned. Synthetic traversal timing is not queues and permits remain loader-owned. Synthetic traversal timing is not
private-asset visual, leak, GPU or p95/p99 evidence. private-asset visual, leak, GPU or p95/p99 evidence.
## 2026-08-01 WMO Scene Instance Factory Extraction
- `WmoSceneInstanceFactory` now owns cached PackedScene instantiation/currentness
validation and live-prototype duplication with shared basename/placement rules.
- Cached validation still precedes placement; live duplicates still skip the
scene-cache validator. Accepted descendant Resources retain exact identity.
- Invalid non-Node3D cached roots are now freed synchronously, closing an
error-path lifetime leak that normal scene-cache admission already prevents.
- Source lookup, ResourceLoader, runtime preparation, attachment, registry,
queues and permits remain loader-owned. Synthetic factory timing is not
private-asset visual, leak/GPU or p95/p99 evidence.
## Practical Rule For Future Work ## Practical Rule For Future Work
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it. If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
+1
View File
@@ -52,6 +52,7 @@
| WMO runtime Mesh finalizer | Implemented extraction | [`wmo-runtime-mesh-finalizer.md`](wmo-runtime-mesh-finalizer.md) | | WMO runtime Mesh finalizer | Implemented extraction | [`wmo-runtime-mesh-finalizer.md`](wmo-runtime-mesh-finalizer.md) |
| WMO render group materializer | Implemented extraction | [`wmo-render-group-materializer.md`](wmo-render-group-materializer.md) | | WMO render group materializer | Implemented extraction | [`wmo-render-group-materializer.md`](wmo-render-group-materializer.md) |
| WMO runtime scene preparer | Implemented extraction | [`wmo-runtime-scene-preparer.md`](wmo-runtime-scene-preparer.md) | | WMO runtime scene preparer | Implemented extraction | [`wmo-runtime-scene-preparer.md`](wmo-runtime-scene-preparer.md) |
| WMO scene instance factory | Implemented extraction | [`wmo-scene-instance-factory.md`](wmo-scene-instance-factory.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) |
+18 -6
View File
@@ -7,7 +7,7 @@
| Status | Implemented | | Status | Implemented |
| Target/work package | M03 / `M03-RND-WMO-PLACEMENT-RESOLVER-001` | | Target/work package | M03 / `M03-RND-WMO-PLACEMENT-RESOLVER-001` |
| Owners | Pure WMO cache-key, placement-identity and world-transform rules | | Owners | Pure WMO cache-key, placement-identity and world-transform rules |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-placement-resolver`, 2026-07-17 | | Last verified | Worktree `work/sindo-main-codex/m03-wmo-scene-instance-factory`, 2026-08-01 |
| Profiles/capabilities | Existing ADT/WDT WMO placement paths | | Profiles/capabilities | Existing ADT/WDT WMO placement paths |
## Purpose ## Purpose
@@ -31,14 +31,17 @@ live-prototype instance paths.
flowchart LR flowchart LR
Parsed[ADT/WDT WMO placement] --> Loader[StreamingWorldLoader adapter] Parsed[ADT/WDT WMO placement] --> Loader[StreamingWorldLoader adapter]
Loader --> Resolver[WmoPlacementResolver] Loader --> Resolver[WmoPlacementResolver]
Loader --> Factory[WmoSceneInstanceFactory]
Factory --> Resolver
Resolver --> CacheKey[Normalized cache key] Resolver --> CacheKey[Normalized cache key]
Resolver --> Identity[Registry unique key] Resolver --> Identity[Registry unique key]
Resolver --> Transform[World Transform3D] Resolver --> Transform[World Transform3D]
CacheKey --> Cache[Loader WMO caches/requests] CacheKey --> Cache[Loader WMO caches/requests]
Identity --> Registry[WmoPlacementRegistry] Identity --> Registry[WmoPlacementRegistry]
Transform --> RenderRoot[Lightweight render root] Transform --> RenderRoot[Lightweight render root]
Transform --> Scene[Cached scene instance] Transform --> Factory
Transform --> Live[Live prototype instance] Factory --> Scene[Cached scene instance]
Factory --> Live[Live prototype instance]
``` ```
Allowed dependencies are Dictionary/String values and Godot `Vector3`, `Basis` Allowed dependencies are Dictionary/String values and Godot `Vector3`, `Basis`
@@ -62,7 +65,7 @@ WorkerThreadPool, mutexes, files, gameplay, network and editor UI are forbidden.
| Input | Tile key and placement index | Loader build job | Synthetic identity fallback | Copied scalar/String | Registry entry lifetime | | Input | Tile key and placement index | Loader build job | Synthetic identity fallback | Copied scalar/String | Registry entry lifetime |
| Output | Normalized relative path | Resolver | Render/scene cache and load-request maps | New String value | Request/cache lookup | | Output | Normalized relative path | Resolver | Render/scene cache and load-request maps | New String value | Request/cache lookup |
| Output | `uid:*` or `tile:*:*` key | Resolver | `WmoPlacementRegistry` and loader ref arrays | New String value | Until unregister/reset | | Output | `uid:*` or `tile:*:*` key | Resolver | `WmoPlacementRegistry` and loader ref arrays | New String value | Until unregister/reset |
| Output | World `Transform3D` | Resolver | Three WMO instance adapters | Value copy | Instance lifetime after assignment | | Output | World `Transform3D` | Resolver | Lightweight render-root adapter and cached/live instance factory | Value copy | Instance lifetime after assignment |
The resolver retains no source Dictionary, output or engine resource. The resolver retains no source Dictionary, output or engine resource.
@@ -91,6 +94,7 @@ and shutdown require no resolver operation.
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant Loader as StreamingWorldLoader participant Loader as StreamingWorldLoader
participant Factory as WmoSceneInstanceFactory
participant Resolver as WmoPlacementResolver participant Resolver as WmoPlacementResolver
participant Registry as WmoPlacementRegistry participant Registry as WmoPlacementRegistry
participant Instance as Render/cached/live instance participant Instance as Render/cached/live instance
@@ -98,9 +102,15 @@ sequenceDiagram
Resolver-->>Loader: cache key Resolver-->>Loader: cache key
Loader->>Resolver: resolve_unique_key(placement, tile, index) Loader->>Resolver: resolve_unique_key(placement, tile, index)
Resolver-->>Registry: identity adopted by loader Resolver-->>Registry: identity adopted by loader
alt lightweight render root
Loader->>Resolver: resolve_world_transform(placement) Loader->>Resolver: resolve_world_transform(placement)
Resolver-->>Loader: value Transform3D Resolver-->>Loader: value Transform3D
Loader->>Instance: assign transform and attach/build else cached/live instance
Loader->>Factory: create with placement
Factory->>Resolver: resolve_world_transform(placement)
Resolver-->>Factory: value Transform3D
end
Loader->>Instance: attach/build prepared instance
``` ```
## Ownership, threading and resources ## Ownership, threading and resources
@@ -109,7 +119,9 @@ sequenceDiagram
- `WmoPlacementRegistry` owns placement-key reference sets. The loader owns its - `WmoPlacementRegistry` owns placement-key reference sets. The loader owns its
key-to-Node map, cache/load-request state, jobs/queues, resource fallback and key-to-Node map, cache/load-request state, jobs/queues, resource fallback and
cancellation. cancellation.
- The loader and builders own every Node/Mesh/MultiMesh/material/RID lifecycle. - `WmoSceneInstanceFactory` owns detached cached/live candidate roots until
rejection or transfer; the loader/builders own attachment and remaining
Node/Mesh/MultiMesh/material/RID lifecycle.
- Pure calls are thread-safe; current consumers execute on the main thread. - Pure calls are thread-safe; current consumers execute on the main thread.
## Errors, cancellation and recovery ## Errors, cancellation and recovery
+219
View File
@@ -0,0 +1,219 @@
# WMO Scene Instance Factory
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target | M03 Renderer Facade and Safe Extraction |
| Work package | `M03-RND-WMO-SCENE-INSTANCE-FACTORY-001` |
| Owner | Render |
| Last verified | 2026-08-01 |
## Purpose
`WmoSceneInstanceFactory` creates detached WMO `Node3D` roots from validated
cached `PackedScene` resources or live-built prototypes. It owns cache-currentness
validation, basename assignment and canonical placement-resolver delegation.
## Non-goals
- look up/load/cache PackedScenes or build live WMO prototypes;
- apply Mesh/material/occluder/shadow runtime preparation;
- attach nodes, assign Editor ownership or manage placement references;
- own queues, permits, cache versions or world teardown;
- define WMO placement formulas or scene-cache currentness rules.
## Context and boundaries
The loader selects cached versus live sources. The factory creates a detached
instance and applies identity/placement. `WmoRuntimeScenePreparer` then applies
path-specific presentation policy before the loader attaches/registers the root.
```mermaid
flowchart LR
Cache[WMO PackedScene cache] --> Loader[StreamingWorldLoader]
Prototype[Live WMO prototype cache/build] --> Loader
Loader --> Factory[WmoSceneInstanceFactory]
Validator[WMOBuilder scene-cache validator] --> Factory
Resolver[WmoPlacementResolver] --> Factory
Factory --> Detached[Detached WMO Node3D]
Detached --> Preparer[WmoRuntimeScenePreparer]
Preparer --> Loader
Loader --> Scene[Attachment and placement registry]
```
## Public API
| Symbol | Role | Thread/lifetime | Failure behavior |
|---|---|---|---|
| `is_cached_node_current(node)` | Delegate one Node to the injected cache validator | Renderer main thread; no retention | Null/missing validator returns false |
| `instantiate_cached_scene(relative_path, scene, placement)` | Instantiate, type-check, validate, name and place a cached scene | Renderer main thread; detached result caller-owned | Invalid input/root/stale/dependency returns null; created rejected roots freed |
| `duplicate_live_prototype(relative_path, prototype, placement)` | Duplicate, name and place a live prototype | Renderer main thread; detached result caller-owned | Null/missing resolver/unexpected duplicate returns null |
The cached path validates before placement. The live path deliberately skips the
scene-cache validator. Both paths use `get_file().get_basename()` and the exact
`WmoPlacementResolver.resolve_world_transform` result.
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Cached `PackedScene` or live prototype `Node3D` | Loader cache/build adapters | Factory | Borrowed Resource/Node | One main-thread call |
| Input | Relative WMO path and placement Dictionary | Loader placement job | Factory | Borrowed values | One call |
| Internal | Candidate root | PackedScene instantiate/prototype duplicate | Validator/factory | Factory-owned until accepted | One call |
| Output | Detached named/placed `Node3D` | Factory | Runtime scene preparer/loader | Ownership transfers to caller | Until attachment/release |
| Output | Currentness bool | Validator via factory | Loader cache admission | Value | Immediate |
Side effects are limited to scene instantiation/duplication, candidate name and
transform mutation, and synchronous free of rejected candidates. No attachment,
filesystem, ResourceLoader, worker, RID, queue, cache or Editor-owner mutation.
## Data flow
```mermaid
flowchart LR
Source[PackedScene or live prototype] --> Create{Cached or live?}
Create -->|cached| Instantiate[PackedScene.instantiate]
Create -->|live| Duplicate[prototype.duplicate]
Instantiate --> Type{Node3D?}
Duplicate --> Type
Type -->|no| Free[Free created candidate and return null]
Type -->|yes cached| Current{Cache current?}
Type -->|yes live| Identity[Apply basename]
Current -->|no| Free
Current -->|yes| Identity
Identity --> Resolve[WmoPlacementResolver]
Resolve --> Return[Return detached Node3D]
```
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Factory as WmoSceneInstanceFactory
participant Validator as WMOBuilder
participant Resolver as WmoPlacementResolver
alt cached source
Loader->>Factory: instantiate_cached_scene(path, scene, placement)
Factory->>Factory: instantiate and require Node3D
Factory->>Validator: is_scene_cache_current(root)
Validator-->>Factory: current/stale
else live source
Loader->>Factory: duplicate_live_prototype(path, prototype, placement)
Factory->>Factory: duplicate and require Node3D
end
Factory->>Factory: assign basename
Factory->>Resolver: resolve_world_transform(placement)
Resolver-->>Factory: exact Transform3D
Factory-->>Loader: detached Node3D or null
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Factory[WmoSceneInstanceFactory]
Factory --> Validator[Injected WMO scene-cache validator]
Factory --> Resolver[Injected WmoPlacementResolver]
Factory --> Engine[PackedScene / Node3D / Transform3D]
Factory -. no dependency .-> Preparation[WmoRuntimeScenePreparer]
Factory -. no dependency .-> IO[ResourceLoader / FileAccess]
Factory -. no dependency .-> Queue[WMO queues / scheduler]
```
## Ownership, threading and resources
- Calls are renderer-main-thread only because PackedScene/Node APIs mutate.
- The source scene/prototype remains caller/cache-owned.
- The factory owns a newly created root until rejection or successful return.
- Successful return transfers detached-root ownership to the caller.
- Descendant Mesh/Material Resources retain engine duplicate/instantiate identity.
- The factory retains only injected stateless dependencies, never Nodes/Resources.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Recovery |
|---|---|---|---|
| Null scene/prototype | Guard | Return null without allocation | Correct caller source |
| Missing validator | Currentness guard | Cached candidate rejected/freed | Fix composition |
| Missing resolver | Guard before creation | Return null without allocation | Fix composition |
| Non-Node3D root | Runtime type check | Free candidate and return null | Rebuild invalid cache/source |
| Stale cached root | Injected validator | Free candidate; skip placement | Rebuild cache/current metadata |
| Placement cancellation | Loader lifecycle | Detached/attached result released by caller | Existing retry path |
| Shutdown | No retained candidates | Nothing to drain | Existing loader teardown |
The non-Node3D cached rejection now frees the created invalid root synchronously.
Normal admitted caches already enforce Node3D through the scene finalizer, so this
closes an error-path lifetime leak without changing valid rendered output.
## Configuration and capabilities
No new settings. Cache-currentness rules belong to the injected WMOBuilder
boundary; placement formulas belong to `WmoPlacementResolver`.
## Persistence, cache and migration
No format/version change and no rebake. The factory reads no files and writes no
metadata. Existing cache validator version policy remains authoritative.
## Diagnostics and observability
The factory emits no logs or metrics. Loader cache/placement metrics and
synthetic rejection contracts remain the diagnostic surfaces.
## Verification
- `verify_wmo_scene_instance_factory.gd` covers cached validation-before-placement,
exact accepted root/descendant Resource identity, stale-root free, non-Node3D
rejection, live validator suppression, detached ownership, dependencies,
basename/Transform3D application, source boundaries and 1,000 duplicates.
- Adjacent scene finalizer, placement resolver, runtime preparer, shutdown and
checkpoint regressions protect lifecycle and visible output.
- Fidelity evidence is behavior-preserving extraction for valid inputs. The
invalid non-Node3D free is a lifetime fix, not a visual 3.3.5a change.
The synthetic budget requires 1,000 simple live duplicates in under one second.
Asset-backed CPU/GPU p95/p99 and long-traversal evidence remain pending.
## Extension points
- Asset-backed cached/live instances can compare placement and lifetime without
changing the factory API.
- New source kinds should be separate explicit methods only when their validation
and identity semantics differ materially.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Cached WMO instantiation | Implemented extraction | Type/currentness/name/placement/lifetime contract | Serialized asset-backed cache corpus pending |
| Live prototype duplication | Implemented extraction | Identity/name/placement/validator-suppression contract | Asset-backed traversal/leak evidence pending |
| Runtime preparation | Separate implemented service | Runtime scene preparer regression | Visual/GPU p95/p99 pending |
| Attachment/registry lifetime | Loader-owned | Existing WMO placement/shutdown regressions | Further orchestration extraction pending |
## Known gaps and risks
- Scene instantiation/duplication remains synchronous main-thread work.
- No private WMO corpus, portal/room behavior, long traversal, leak/GPU timing or
paired original-client capture is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_scene_instance_factory.gd` | Cached/live creation, validation, identity and placement |
| `src/render/wmo/wmo_placement_resolver.gd` | Canonical WMO placement Transform3D |
| `src/render/wmo/wmo_runtime_scene_preparer.gd` | Post-factory cached/live render preparation |
| `src/scenes/streaming/streaming_world_loader.gd` | Source selection, cache/prototype lookup, attachment and lifetime |
| `src/tools/verify_wmo_scene_instance_factory.gd` | Synthetic type/identity/lifetime/boundary/timing regression |
## Related decisions and references
- [`wmo-placement-resolver.md`](wmo-placement-resolver.md)
- [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.md)
- [`wmo-runtime-scene-preparer.md`](wmo-runtime-scene-preparer.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)
+14 -1
View File
@@ -7,7 +7,7 @@
| Status | Partial | | Status | Partial |
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package | | Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
| Owners | Renderer workstream / milestone integrator | | Owners | Renderer workstream / milestone integrator |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-runtime-scene-preparer`, 2026-08-01 | | Last verified | Worktree `work/sindo-main-codex/m03-wmo-scene-instance-factory`, 2026-08-01 |
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete | | Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose ## Purpose
@@ -74,6 +74,8 @@ flowchart LR
WmoGroupMaterializer --> Scene WmoGroupMaterializer --> Scene
Loader --> WmoScenePreparer[WmoRuntimeScenePreparer] Loader --> WmoScenePreparer[WmoRuntimeScenePreparer]
WmoScenePreparer --> Scene WmoScenePreparer --> Scene
Loader --> WmoInstanceFactory[WmoSceneInstanceFactory]
WmoInstanceFactory --> WmoScenePreparer
Native --> Parsed[Parsed tile/model data] Native --> Parsed[Parsed tile/model data]
Parsed --> Loader Parsed --> Loader
Loader --> Scene[SceneTree nodes] Loader --> Scene[SceneTree nodes]
@@ -170,6 +172,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
| `WmoRenderBuildQueue` / `WmoRenderBuildJob` | Internal WMO pending-state service | Owns typed root/resource/cursor jobs and FIFO placement keys | Renderer main thread; map session | Invalid enqueue rejected; duplicate/stale behavior preserved | | `WmoRenderBuildQueue` / `WmoRenderBuildJob` | Internal WMO pending-state service | Owns typed root/resource/cursor jobs and FIFO placement keys | Renderer main thread; map session | Invalid enqueue rejected; duplicate/stale behavior preserved |
| `WmoRenderGroupMaterializer.materialize_mesh_group/materialize_multimesh_group` | Internal WMO scene-materialization service | Creates, configures and attaches one indexed lightweight render group | Renderer main thread; stateless after each call | Invalid parent/resource/index returns null without attachment | | `WmoRenderGroupMaterializer.materialize_mesh_group/materialize_multimesh_group` | Internal WMO scene-materialization service | Creates, configures and attaches one indexed lightweight render group | Renderer main thread; stateless after each call | Invalid parent/resource/index returns null without attachment |
| `WmoRuntimeScenePreparer.prepare_cached_instance/prepare_live_instance` | Internal WMO subtree-preparation service | Preserves cached/live Mesh-finalization distinction, direct occluder policy and recursive shadow enabling | Renderer main thread; stateless after each call | Null/freed root returns false | | `WmoRuntimeScenePreparer.prepare_cached_instance/prepare_live_instance` | Internal WMO subtree-preparation service | Preserves cached/live Mesh-finalization distinction, direct occluder policy and recursive shadow enabling | Renderer main thread; stateless after each call | Null/freed root returns false |
| `WmoSceneInstanceFactory.instantiate_cached_scene/duplicate_live_prototype` | Internal WMO instance-creation service | Validates/creates detached cached/live roots and applies shared name/placement | Renderer main thread; stateless after each call | Invalid/stale/dependency failure returns null and frees created rejects |
| `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 |
@@ -214,6 +217,7 @@ loader configuration remains transitional composition data, not a caller API.
| Internal WMO pending build | Placement key, Node3D root, WMO Resource and cursors | Loader / `WmoRenderBuildQueue` | Loader drain and step planner adapter | Queue-owned job and strong references | Until cancel/clear/replacement | | Internal WMO pending build | Placement key, Node3D root, WMO Resource and cursors | Loader / `WmoRenderBuildQueue` | Loader drain and step planner adapter | Queue-owned job and strong references | Until cancel/clear/replacement |
| Internal WMO group materialization | Parent root, exact Mesh/MultiMesh, indexed metadata and render settings | Loader / `WmoRenderGroupMaterializer` | Attached geometry node | Parent owns node and exact Resource reference | One main-thread group operation | | Internal WMO group materialization | Parent root, exact Mesh/MultiMesh, indexed metadata and render settings | Loader / `WmoRenderGroupMaterializer` | Attached geometry node | Parent owns node and exact Resource reference | One main-thread group operation |
| Internal WMO subtree preparation | Cached/live root, extracted directory and render policies | Loader / `WmoRuntimeScenePreparer` | Borrowed subtree and runtime Mesh finalizer | Loader/placement owns subtree; preparer retains nothing | One main-thread instance preparation | | Internal WMO subtree preparation | Cached/live root, extracted directory and render policies | Loader / `WmoRuntimeScenePreparer` | Borrowed subtree and runtime Mesh finalizer | Loader/placement owns subtree; preparer retains nothing | One main-thread instance preparation |
| Internal WMO instance creation | Cached PackedScene or live prototype, path and placement | Loader / `WmoSceneInstanceFactory` | Runtime scene preparer and attachment adapter | Factory owns candidate until detached-root transfer | One main-thread creation |
| Internal WMO render cache | Normalized path, cache path and validated Resource | Loader / `WmoRenderResourceCacheState` | Loader lookup, ResourceLoader poll and build queue | State-owned Resource/path references; detached request snapshots | Until transient/full clear | | Internal WMO render cache | Normalized path, cache path and validated Resource | Loader / `WmoRenderResourceCacheState` | Loader lookup, ResourceLoader poll and build queue | State-owned Resource/path references; detached request snapshots | Until transient/full clear |
| Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear | | Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear |
| Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset | | Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset |
@@ -425,6 +429,9 @@ sequenceDiagram
creation, render settings and attachment without retaining engine objects. creation, render settings and attachment without retaining engine objects.
`WmoRuntimeScenePreparer` owns cached-only Mesh traversal/finalization plus the `WmoRuntimeScenePreparer` owns cached-only Mesh traversal/finalization plus the
shared direct-Occluders and recursive shadow policies for cached/live roots. shared direct-Occluders and recursive shadow policies for cached/live roots.
`WmoSceneInstanceFactory` owns cached/live detached-root creation, cache
validation, basename and placement application; loader retains source lookup,
runtime preparation, attachment and lifetime.
`WmoRenderResourceCacheState` owns validated render Resources, negative entries `WmoRenderResourceCacheState` owns validated render Resources, negative entries
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`
@@ -576,6 +583,9 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
- WMO runtime scene preparer contract: cached/live finalizer distinction, - WMO runtime scene preparer contract: cached/live finalizer distinction,
exact Mesh traversal order, direct occluder removal, recursive shadow policy, exact Mesh traversal order, direct occluder removal, recursive shadow policy,
ownership boundaries and bounded main-thread timing. ownership boundaries and bounded main-thread timing.
- WMO scene instance factory contract: cached validation-before-placement,
stale/type rejection lifetime, live validator suppression, exact descendant
Resource identity, naming/placement and bounded main-thread timing.
- WMO render Resource cache contract: invalid/duplicate request rejection, - WMO render Resource cache contract: invalid/duplicate request rejection,
validated/missing terminal transitions, transient/full reset, detached sorted validated/missing terminal transitions, transient/full reset, detached sorted
diagnostics, loader-owned version validation and bounded timing. diagnostics, loader-owned version validation and bounded timing.
@@ -638,6 +648,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| WMO render build queue | Implemented extraction | Typed lifecycle/order/ownership/source/timing contract | Asset-backed traversal/leak evidence pending | | WMO render build queue | Implemented extraction | Typed lifecycle/order/ownership/source/timing contract | Asset-backed traversal/leak evidence pending |
| WMO render group materializer | Implemented extraction | Synthetic Resource/name/transform/render/attachment/source/timing contract | Asset-backed visual/leak/GPU p95/p99 pending | | WMO render group materializer | Implemented extraction | Synthetic Resource/name/transform/render/attachment/source/timing contract | Asset-backed visual/leak/GPU p95/p99 pending |
| WMO runtime scene preparer | Implemented extraction | Synthetic cached/live traversal/occluder/shadow/source/timing contract | Asset-backed visual/leak/GPU p95/p99 pending | | WMO runtime scene preparer | Implemented extraction | Synthetic cached/live traversal/occluder/shadow/source/timing contract | Asset-backed visual/leak/GPU p95/p99 pending |
| WMO scene instance factory | Implemented extraction | Synthetic cached/live type/identity/lifetime/name/placement/source/timing contract | Serialized/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 | 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 |
@@ -722,6 +733,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Cached WMO runtime refresh admission, surface iteration and material reconstruction | | `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Cached WMO runtime refresh admission, surface iteration and material reconstruction |
| `src/render/wmo/wmo_render_group_materializer.gd` | Indexed lightweight WMO geometry-node creation, render setup and attachment | | `src/render/wmo/wmo_render_group_materializer.gd` | Indexed lightweight WMO geometry-node creation, render setup and attachment |
| `src/render/wmo/wmo_runtime_scene_preparer.gd` | Cached/live WMO subtree Mesh traversal and render policy | | `src/render/wmo/wmo_runtime_scene_preparer.gd` | Cached/live WMO subtree Mesh traversal and render policy |
| `src/render/wmo/wmo_scene_instance_factory.gd` | Cached/live detached-root creation, validation, identity and placement |
| `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 |
@@ -761,6 +773,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | WMO Mesh identity/version/material-definition/boundary/timing regression | | `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | WMO Mesh identity/version/material-definition/boundary/timing regression |
| `src/tools/verify_wmo_render_group_materializer.gd` | WMO render-group Resource/name/transform/render/attachment/boundary/timing regression | | `src/tools/verify_wmo_render_group_materializer.gd` | WMO render-group Resource/name/transform/render/attachment/boundary/timing regression |
| `src/tools/verify_wmo_runtime_scene_preparer.gd` | WMO cached/live traversal/occluder/shadow/boundary/timing regression | | `src/tools/verify_wmo_runtime_scene_preparer.gd` | WMO cached/live traversal/occluder/shadow/boundary/timing regression |
| `src/tools/verify_wmo_scene_instance_factory.gd` | WMO cached/live type/identity/lifetime/name/placement/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,73 @@
class_name WmoSceneInstanceFactory
extends RefCounted
## Creates detached cached/live WMO Node3D instances with the existing cache
## validation, naming and world-placement rules. Runtime preparation is separate.
var _scene_cache_validator: Object
var _placement_resolver: Object
func _init(scene_cache_validator: Object, placement_resolver: Object) -> void:
_scene_cache_validator = scene_cache_validator
_placement_resolver = placement_resolver
## Returns whether [param node] satisfies the injected WMO scene-cache contract.
## Null nodes or missing validators return false without mutation.
func is_cached_node_current(node: Node) -> bool:
if node == null or _scene_cache_validator == null:
return false
return bool(_scene_cache_validator.call("is_scene_cache_current", node))
## Instantiates, validates, names and places a cached WMO PackedScene. Rejected
## instantiated roots are freed synchronously. The accepted detached Node3D is
## caller-owned and retains its exact descendant Resource identities.
func instantiate_cached_scene(
relative_path: String,
scene: PackedScene,
placement: Dictionary) -> Node3D:
if scene == null or _placement_resolver == null:
return null
var instantiated_root := scene.instantiate()
if not (instantiated_root is Node3D):
if instantiated_root != null:
instantiated_root.free()
return null
var instance := instantiated_root as Node3D
if not is_cached_node_current(instance):
instance.free()
return null
_apply_identity_and_placement(instance, relative_path, placement)
return instance
## Duplicates, names and places a live-built WMO prototype. The detached result
## is caller-owned. Null inputs, missing placement composition or an unexpected
## non-Node3D duplicate return null; rejected duplicates are freed synchronously.
func duplicate_live_prototype(
relative_path: String,
prototype: Node3D,
placement: Dictionary) -> Node3D:
if prototype == null or _placement_resolver == null:
return null
var duplicated_root := prototype.duplicate()
if not (duplicated_root is Node3D):
if duplicated_root != null:
duplicated_root.free()
return null
var instance := duplicated_root as Node3D
_apply_identity_and_placement(instance, relative_path, placement)
return instance
func _apply_identity_and_placement(
instance: Node3D,
relative_path: String,
placement: Dictionary) -> void:
instance.name = relative_path.get_file().get_basename()
instance.transform = _placement_resolver.call(
"resolve_world_transform",
placement
) as Transform3D
@@ -0,0 +1 @@
uid://c13w66d7uaf2n
+23 -26
View File
@@ -42,6 +42,9 @@ const WMO_RENDER_GROUP_MATERIALIZER_SCRIPT := preload(
const WMO_RUNTIME_SCENE_PREPARER_SCRIPT := preload( const WMO_RUNTIME_SCENE_PREPARER_SCRIPT := preload(
"res://src/render/wmo/wmo_runtime_scene_preparer.gd" "res://src/render/wmo/wmo_runtime_scene_preparer.gd"
) )
const WMO_SCENE_INSTANCE_FACTORY_SCRIPT := preload(
"res://src/render/wmo/wmo_scene_instance_factory.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")
@@ -393,6 +396,10 @@ var _wmo_runtime_scene_preparer := WMO_RUNTIME_SCENE_PREPARER_SCRIPT.new(
) )
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 _wmo_scene_instance_factory := WMO_SCENE_INSTANCE_FACTORY_SCRIPT.new(
WMO_BUILDER_SCRIPT,
_wmo_placement_resolver
)
var _world_wmo_root: Node3D var _world_wmo_root: Node3D
var _wmo_placement_registry := WMO_PLACEMENT_REGISTRY_SCRIPT.new() var _wmo_placement_registry := WMO_PLACEMENT_REGISTRY_SCRIPT.new()
var _wmo_render_build_step_planner := WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.new() var _wmo_render_build_step_planner := WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.new()
@@ -3692,7 +3699,13 @@ func _process_wmo_build_jobs() -> void:
) )
refs.append(unique_key) refs.append(unique_key)
else: else:
var scene_instance := _instantiate_wmo_scene(rel_path, scene, placement) var scene_instance := (
_wmo_scene_instance_factory.instantiate_cached_scene(
rel_path,
scene,
placement
)
)
if scene_instance != null: if scene_instance != null:
_prepare_runtime_wmo_instance(scene_instance) _prepare_runtime_wmo_instance(scene_instance)
_world_wmo_root.add_child(scene_instance) _world_wmo_root.add_child(scene_instance)
@@ -3920,26 +3933,6 @@ func _get_resource_file_size(path: String) -> int:
return int(length) return int(length)
func _is_wmo_node_cache_current(node: Node) -> bool:
if node == null:
return false
return bool(WMO_BUILDER_SCRIPT.is_scene_cache_current(node))
func _instantiate_wmo_scene(rel_path: String, scene: PackedScene, placement: Dictionary) -> Node3D:
if scene == null:
return null
var instance := scene.instantiate() as Node3D
if instance == null:
return null
if not _is_wmo_node_cache_current(instance):
instance.free()
return null
instance.name = rel_path.get_file().get_basename()
instance.transform = _wmo_placement_resolver.resolve_world_transform(placement)
return instance
func _prepare_runtime_wmo_instance(instance: Node3D) -> void: func _prepare_runtime_wmo_instance(instance: Node3D) -> void:
_wmo_runtime_scene_preparer.prepare_cached_instance( _wmo_runtime_scene_preparer.prepare_cached_instance(
instance, instance,
@@ -4632,11 +4625,15 @@ func _instantiate_wmo_world(rel_path: String, placement: Dictionary) -> Node3D:
if prototype == null: if prototype == null:
return null return null
var instance := prototype.duplicate()
instance.name = rel_path.get_file().get_basename()
# WMOs are parented to _terrain_root, whose position absorbs the editor # WMOs are parented to _terrain_root, whose position absorbs the editor
# offset — use world-space placement.pos directly (no tile_origin subtraction). # offset — the factory applies world-space placement.pos directly.
instance.transform = _wmo_placement_resolver.resolve_world_transform(placement) var instance := _wmo_scene_instance_factory.duplicate_live_prototype(
rel_path,
prototype,
placement
)
if instance == null:
return null
_wmo_runtime_scene_preparer.prepare_live_instance( _wmo_runtime_scene_preparer.prepare_live_instance(
instance, instance,
enable_occlusion_culling, enable_occlusion_culling,
@@ -4736,7 +4733,7 @@ func _get_or_load_wmo_prototype(rel_path: String) -> Node3D:
var resource: Resource = load(cache_path) var resource: Resource = load(cache_path)
if resource is PackedScene: if resource is PackedScene:
var node = (resource as PackedScene).instantiate() var node = (resource as PackedScene).instantiate()
if node is Node3D and _is_wmo_node_cache_current(node): if node is Node3D and _wmo_scene_instance_factory.is_cached_node_current(node):
_wmo_prototype_cache[normalized_rel] = node as Node3D _wmo_prototype_cache[normalized_rel] = node as Node3D
return node as Node3D return node as Node3D
if node is Node: if node is Node:
+10 -3
View File
@@ -4,6 +4,7 @@ extends SceneTree
const RESOLVER_SCRIPT := preload("res://src/render/wmo/wmo_placement_resolver.gd") const RESOLVER_SCRIPT := preload("res://src/render/wmo/wmo_placement_resolver.gd")
const RESOLVER_PATH := "res://src/render/wmo/wmo_placement_resolver.gd" const RESOLVER_PATH := "res://src/render/wmo/wmo_placement_resolver.gd"
const INSTANCE_FACTORY_PATH := "res://src/render/wmo/wmo_scene_instance_factory.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
@@ -25,7 +26,7 @@ func _initialize() -> void:
quit(1) quit(1)
return return
print( print(
"WMO_PLACEMENT_RESOLVER PASS cases=9 iterations=20000 elapsed_ms=%.3f" "WMO_PLACEMENT_RESOLVER PASS cases=10 iterations=20000 elapsed_ms=%.3f"
% elapsed_milliseconds % elapsed_milliseconds
) )
quit(0) quit(0)
@@ -138,6 +139,7 @@ func _verify_node_property_equivalence(failures: Array[String]) -> void:
func _verify_source_boundaries(failures: Array[String]) -> void: func _verify_source_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures) var loader_source := _read_text(LOADER_PATH, failures)
var resolver_source := _read_text(RESOLVER_PATH, failures) var resolver_source := _read_text(RESOLVER_PATH, failures)
var instance_factory_source := _read_text(INSTANCE_FACTORY_PATH, failures)
_expect_true( _expect_true(
loader_source.contains("WMO_PLACEMENT_RESOLVER_SCRIPT.new()"), loader_source.contains("WMO_PLACEMENT_RESOLVER_SCRIPT.new()"),
"loader composes resolver", "loader composes resolver",
@@ -157,8 +159,13 @@ func _verify_source_boundaries(failures: Array[String]) -> void:
) )
_expect_equal_int( _expect_equal_int(
loader_source.count("_wmo_placement_resolver.resolve_world_transform("), loader_source.count("_wmo_placement_resolver.resolve_world_transform("),
3, 1,
"three transform adapters", "one direct lightweight render-root transform adapter",
failures
)
_expect_true(
instance_factory_source.contains('"resolve_world_transform"'),
"cached/live instance factory delegates transform resolution",
failures failures
) )
_expect_true( _expect_true(
@@ -0,0 +1,294 @@
extends SceneTree
## Asset-free WMO cached/live instance validation, identity, placement,
## lifetime, source-boundary and bounded-timing regression.
const FACTORY_SCRIPT := preload("res://src/render/wmo/wmo_scene_instance_factory.gd")
const FACTORY_PATH := "res://src/render/wmo/wmo_scene_instance_factory.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
class FakeSceneCacheValidator extends RefCounted:
var is_current := true
var inspected_nodes: Array[Node] = []
var events: Array[String]
func _init(shared_events: Array[String] = []) -> void:
events = shared_events
func is_scene_cache_current(node: Node) -> bool:
inspected_nodes.append(node)
events.append("validate")
return is_current
class FakePlacementResolver extends RefCounted:
var resolved_transform := Transform3D(Basis.IDENTITY, Vector3(4.0, 5.0, 6.0))
var placements: Array[Dictionary] = []
var events: Array[String]
func _init(shared_events: Array[String] = []) -> void:
events = shared_events
func resolve_world_transform(placement: Dictionary) -> Transform3D:
placements.append(placement.duplicate(true))
events.append("resolve")
return resolved_transform
func _initialize() -> void:
var failures: Array[String] = []
_verify_cached_scene_contract(failures)
_verify_stale_and_wrong_root_rejection(failures)
_verify_live_prototype_contract(failures)
_verify_invalid_composition(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_INSTANCE_FACTORY: %s" % failure)
quit(1)
return
print(
"WMO_SCENE_INSTANCE_FACTORY PASS cases=41 iterations=1000 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_cached_scene_contract(failures: Array[String]) -> void:
var events: Array[String] = []
var validator := FakeSceneCacheValidator.new(events)
var resolver := FakePlacementResolver.new(events)
var factory := FACTORY_SCRIPT.new(validator, resolver)
var fixture := _create_packed_wmo_scene()
var scene: PackedScene = fixture["scene"]
var source_mesh: Mesh = fixture["mesh"]
var placement := {"pos": Vector3(1.0, 2.0, 3.0), "rotation": Vector3.ZERO}
var instance: Node3D = factory.instantiate_cached_scene(
"World/Wmo/Stormwind/Keep.wmo",
scene,
placement
)
_expect_true(instance != null, "current cached scene accepted", failures)
_expect_true(instance.get_parent() == null, "cached result remains detached", failures)
_expect_string_equal(instance.name, "Keep", "cached basename applied", failures)
_expect_true(instance.transform == resolver.resolved_transform, "cached transform applied", failures)
_expect_equal(validator.inspected_nodes.size(), 1, "cached root validated once", failures)
_expect_same(validator.inspected_nodes[0], instance, "accepted exact root validated", failures)
_expect_true(resolver.placements == [placement], "cached placement forwarded", failures)
_expect_true(events == ["validate", "resolve"], "validate precedes placement", failures)
var mesh_child := instance.get_node_or_null("Group") as MeshInstance3D
_expect_true(mesh_child != null, "cached Mesh child retained", failures)
_expect_same(mesh_child.mesh, source_mesh, "cached descendant Mesh identity retained", failures)
_expect_true(factory.is_cached_node_current(instance), "public currentness delegates", failures)
instance.free()
func _verify_stale_and_wrong_root_rejection(failures: Array[String]) -> void:
var validator := FakeSceneCacheValidator.new()
validator.is_current = false
var resolver := FakePlacementResolver.new()
var factory := FACTORY_SCRIPT.new(validator, resolver)
var fixture := _create_packed_wmo_scene()
_expect_true(
factory.instantiate_cached_scene("world/stale.wmo", fixture["scene"], {}) == null,
"stale cached scene rejected",
failures
)
_expect_equal(validator.inspected_nodes.size(), 1, "stale root validated once", failures)
_expect_false(
is_instance_valid(validator.inspected_nodes[0]),
"stale instantiated root freed",
failures
)
_expect_equal(resolver.placements.size(), 0, "stale root skips placement", failures)
var control_root := Control.new()
var wrong_root_scene := PackedScene.new()
_expect_equal(wrong_root_scene.pack(control_root), OK, "wrong-root fixture packed", failures)
control_root.free()
validator.is_current = true
_expect_true(
factory.instantiate_cached_scene("world/control.wmo", wrong_root_scene, {}) == null,
"non-Node3D cached root rejected",
failures
)
_expect_equal(validator.inspected_nodes.size(), 1, "wrong root skips validator", failures)
func _verify_live_prototype_contract(failures: Array[String]) -> void:
var validator := FakeSceneCacheValidator.new()
var resolver := FakePlacementResolver.new()
var factory := FACTORY_SCRIPT.new(validator, resolver)
var prototype := Node3D.new()
prototype.name = "Prototype"
var source_mesh := ArrayMesh.new()
var mesh_child := MeshInstance3D.new()
mesh_child.name = "Group"
mesh_child.mesh = source_mesh
prototype.add_child(mesh_child)
var placement := {"pos": Vector3(7.0, 8.0, 9.0)}
var instance: Node3D = factory.duplicate_live_prototype(
"World/Live/Abbey.wmo",
prototype,
placement
)
_expect_true(instance != null, "live prototype duplicated", failures)
_expect_false(is_same(instance, prototype), "live root identity isolated", failures)
_expect_true(instance.get_parent() == null, "live result remains detached", failures)
_expect_string_equal(instance.name, "Abbey", "live basename applied", failures)
_expect_true(instance.transform == resolver.resolved_transform, "live transform applied", failures)
_expect_same(
(instance.get_node("Group") as MeshInstance3D).mesh,
source_mesh,
"live descendant Mesh identity retained",
failures
)
_expect_equal(validator.inspected_nodes.size(), 0, "live path skips cache validator", failures)
_expect_true(resolver.placements == [placement], "live placement forwarded", failures)
instance.free()
prototype.free()
func _verify_invalid_composition(failures: Array[String]) -> void:
var validator := FakeSceneCacheValidator.new()
var resolver := FakePlacementResolver.new()
var factory := FACTORY_SCRIPT.new(validator, resolver)
_expect_false(factory.is_cached_node_current(null), "null currentness rejected", failures)
_expect_true(
factory.instantiate_cached_scene("world/a.wmo", null, {}) == null,
"null PackedScene rejected",
failures
)
_expect_true(
factory.duplicate_live_prototype("world/a.wmo", null, {}) == null,
"null prototype rejected",
failures
)
var fixture := _create_packed_wmo_scene()
var missing_validator_factory := FACTORY_SCRIPT.new(null, resolver)
_expect_true(
missing_validator_factory.instantiate_cached_scene(
"world/a.wmo", fixture["scene"], {}
) == null,
"missing validator rejects cached instance",
failures
)
var missing_resolver_factory := FACTORY_SCRIPT.new(validator, null)
_expect_true(
missing_resolver_factory.instantiate_cached_scene(
"world/a.wmo", fixture["scene"], {}
) == null,
"missing resolver rejects cached instance before allocation",
failures
)
var prototype := Node3D.new()
_expect_true(
missing_resolver_factory.duplicate_live_prototype("world/a.wmo", prototype, {}) == null,
"missing resolver rejects live duplicate",
failures
)
prototype.free()
func _verify_source_boundaries(failures: Array[String]) -> void:
var factory_source := FileAccess.get_file_as_string(FACTORY_PATH)
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
_expect_true(
loader_source.contains("WMO_SCENE_INSTANCE_FACTORY_SCRIPT.new("),
"loader composes scene instance factory",
failures
)
_expect_true(
loader_source.contains("_wmo_scene_instance_factory.instantiate_cached_scene("),
"loader delegates cached instantiation",
failures
)
_expect_true(
loader_source.contains("_wmo_scene_instance_factory.duplicate_live_prototype("),
"loader delegates live duplication",
failures
)
for released_token in [
"func _is_wmo_node_cache_current(",
"func _instantiate_wmo_scene(",
]:
_expect_false(loader_source.contains(released_token), "loader releases %s" % released_token, failures)
for owned_token in [
"scene.instantiate()",
"prototype.duplicate()",
"is_scene_cache_current",
"relative_path.get_file().get_basename()",
"resolve_world_transform",
]:
_expect_true(factory_source.contains(owned_token), "factory owns %s" % owned_token, failures)
for forbidden_dependency in [
"ResourceLoader.",
"FileAccess.",
"WorkerThreadPool.",
"RenderingServer.",
"add_child(",
".owner =",
"WmoRuntimeScenePreparer",
"_wmo_render_build_queue",
]:
_expect_false(factory_source.contains(forbidden_dependency), "factory omits %s" % forbidden_dependency, failures)
func _verify_bounded_timing(failures: Array[String]) -> float:
var validator := FakeSceneCacheValidator.new()
var resolver := FakePlacementResolver.new()
var factory := FACTORY_SCRIPT.new(validator, resolver)
var prototype := Node3D.new()
var started_microseconds := Time.get_ticks_usec()
for iteration in 1000:
var instance: Node3D = factory.duplicate_live_prototype(
"world/timing.wmo", prototype, {}
)
instance.free()
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_equal(resolver.placements.size(), 1000, "timing duplicates complete", failures)
_expect_true(elapsed_milliseconds < 1000.0, "1000 duplicates under one second", failures)
prototype.free()
return elapsed_milliseconds
func _create_packed_wmo_scene() -> Dictionary:
var source_root := Node3D.new()
var mesh := ArrayMesh.new()
var mesh_child := MeshInstance3D.new()
mesh_child.name = "Group"
mesh_child.mesh = mesh
source_root.add_child(mesh_child)
mesh_child.owner = source_root
var scene := PackedScene.new()
var pack_error := scene.pack(source_root)
if pack_error != OK:
push_error("WMO_SCENE_INSTANCE_FACTORY: cannot pack fixture")
source_root.free()
return {"scene": scene, "mesh": mesh}
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
if not condition:
failures.append(label)
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
_expect_true(not condition, label, failures)
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
if actual != expected:
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
func _expect_string_equal(actual: String, expected: String, label: String, failures: Array[String]) -> void:
if actual != expected:
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
func _expect_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
if not is_same(actual, expected):
failures.append(label)
@@ -0,0 +1 @@
uid://cae6gor0iqjv4