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:
@@ -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
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
| 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 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) |
|
||||
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
|
||||
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
| Status | Implemented |
|
||||
| Target/work package | M03 / `M03-RND-WMO-PLACEMENT-RESOLVER-001` |
|
||||
| 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 |
|
||||
|
||||
## Purpose
|
||||
@@ -31,14 +31,17 @@ live-prototype instance paths.
|
||||
flowchart LR
|
||||
Parsed[ADT/WDT WMO placement] --> Loader[StreamingWorldLoader adapter]
|
||||
Loader --> Resolver[WmoPlacementResolver]
|
||||
Loader --> Factory[WmoSceneInstanceFactory]
|
||||
Factory --> Resolver
|
||||
Resolver --> CacheKey[Normalized cache key]
|
||||
Resolver --> Identity[Registry unique key]
|
||||
Resolver --> Transform[World Transform3D]
|
||||
CacheKey --> Cache[Loader WMO caches/requests]
|
||||
Identity --> Registry[WmoPlacementRegistry]
|
||||
Transform --> RenderRoot[Lightweight render root]
|
||||
Transform --> Scene[Cached scene instance]
|
||||
Transform --> Live[Live prototype instance]
|
||||
Transform --> Factory
|
||||
Factory --> Scene[Cached scene instance]
|
||||
Factory --> Live[Live prototype instance]
|
||||
```
|
||||
|
||||
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 |
|
||||
| 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 | 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.
|
||||
|
||||
@@ -91,6 +94,7 @@ and shutdown require no resolver operation.
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant Factory as WmoSceneInstanceFactory
|
||||
participant Resolver as WmoPlacementResolver
|
||||
participant Registry as WmoPlacementRegistry
|
||||
participant Instance as Render/cached/live instance
|
||||
@@ -98,9 +102,15 @@ sequenceDiagram
|
||||
Resolver-->>Loader: cache key
|
||||
Loader->>Resolver: resolve_unique_key(placement, tile, index)
|
||||
Resolver-->>Registry: identity adopted by loader
|
||||
Loader->>Resolver: resolve_world_transform(placement)
|
||||
Resolver-->>Loader: value Transform3D
|
||||
Loader->>Instance: assign transform and attach/build
|
||||
alt lightweight render root
|
||||
Loader->>Resolver: resolve_world_transform(placement)
|
||||
Resolver-->>Loader: value Transform3D
|
||||
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
|
||||
@@ -109,7 +119,9 @@ sequenceDiagram
|
||||
- `WmoPlacementRegistry` owns placement-key reference sets. The loader owns its
|
||||
key-to-Node map, cache/load-request state, jobs/queues, resource fallback and
|
||||
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.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
@@ -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)
|
||||
@@ -7,7 +7,7 @@
|
||||
| Status | Partial |
|
||||
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
|
||||
| Owners | Renderer workstream / milestone integrator |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-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 |
|
||||
|
||||
## Purpose
|
||||
@@ -74,6 +74,8 @@ flowchart LR
|
||||
WmoGroupMaterializer --> Scene
|
||||
Loader --> WmoScenePreparer[WmoRuntimeScenePreparer]
|
||||
WmoScenePreparer --> Scene
|
||||
Loader --> WmoInstanceFactory[WmoSceneInstanceFactory]
|
||||
WmoInstanceFactory --> WmoScenePreparer
|
||||
Native --> Parsed[Parsed tile/model data]
|
||||
Parsed --> Loader
|
||||
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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
@@ -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 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 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 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 |
|
||||
@@ -425,6 +429,9 @@ sequenceDiagram
|
||||
creation, render settings and attachment without retaining engine objects.
|
||||
`WmoRuntimeScenePreparer` owns cached-only Mesh traversal/finalization plus the
|
||||
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
|
||||
and pending cache paths; `WmoRenderResourceFinalizer` owns its terminal
|
||||
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,
|
||||
exact Mesh traversal order, direct occluder removal, recursive shadow policy,
|
||||
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,
|
||||
validated/missing terminal transitions, transient/full reset, detached sorted
|
||||
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 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 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 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 |
|
||||
@@ -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_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_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_policy.gd` | Immutable renderer radius/prefetch policy |
|
||||
| `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_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_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_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 |
|
||||
|
||||
@@ -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
|
||||
@@ -42,6 +42,9 @@ const WMO_RENDER_GROUP_MATERIALIZER_SCRIPT := preload(
|
||||
const WMO_RUNTIME_SCENE_PREPARER_SCRIPT := preload(
|
||||
"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_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.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_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 _wmo_placement_registry := WMO_PLACEMENT_REGISTRY_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)
|
||||
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:
|
||||
_prepare_runtime_wmo_instance(scene_instance)
|
||||
_world_wmo_root.add_child(scene_instance)
|
||||
@@ -3920,26 +3933,6 @@ func _get_resource_file_size(path: String) -> int:
|
||||
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:
|
||||
_wmo_runtime_scene_preparer.prepare_cached_instance(
|
||||
instance,
|
||||
@@ -4632,11 +4625,15 @@ func _instantiate_wmo_world(rel_path: String, placement: Dictionary) -> Node3D:
|
||||
if prototype == 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
|
||||
# offset — use world-space placement.pos directly (no tile_origin subtraction).
|
||||
instance.transform = _wmo_placement_resolver.resolve_world_transform(placement)
|
||||
# offset — the factory applies world-space placement.pos directly.
|
||||
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(
|
||||
instance,
|
||||
enable_occlusion_culling,
|
||||
@@ -4736,7 +4733,7 @@ func _get_or_load_wmo_prototype(rel_path: String) -> Node3D:
|
||||
var resource: Resource = load(cache_path)
|
||||
if resource is PackedScene:
|
||||
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
|
||||
return node as Node3D
|
||||
if node is Node:
|
||||
|
||||
@@ -4,6 +4,7 @@ extends SceneTree
|
||||
|
||||
const RESOLVER_SCRIPT := preload("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"
|
||||
|
||||
|
||||
@@ -25,7 +26,7 @@ func _initialize() -> void:
|
||||
quit(1)
|
||||
return
|
||||
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
|
||||
)
|
||||
quit(0)
|
||||
@@ -138,6 +139,7 @@ func _verify_node_property_equivalence(failures: Array[String]) -> void:
|
||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
var loader_source := _read_text(LOADER_PATH, failures)
|
||||
var resolver_source := _read_text(RESOLVER_PATH, failures)
|
||||
var instance_factory_source := _read_text(INSTANCE_FACTORY_PATH, failures)
|
||||
_expect_true(
|
||||
loader_source.contains("WMO_PLACEMENT_RESOLVER_SCRIPT.new()"),
|
||||
"loader composes resolver",
|
||||
@@ -157,8 +159,13 @@ func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
)
|
||||
_expect_equal_int(
|
||||
loader_source.count("_wmo_placement_resolver.resolve_world_transform("),
|
||||
3,
|
||||
"three transform adapters",
|
||||
1,
|
||||
"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
|
||||
)
|
||||
_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
|
||||
Reference in New Issue
Block a user