# 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)