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
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.
+1
View File
@@ -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) |
+20 -8
View File
@@ -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
+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 |
| 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
+23 -26
View File
@@ -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:
+10 -3
View File
@@ -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