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
10 KiB
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.
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
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
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
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.gdcovers 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 |