Files
open-wc/docs/modules/wmo-scene-instance-factory.md
sindoring 7e97b19095 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
2026-08-01 10:49:56 +04:00

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.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