# WMO Render Group Materializer ## Metadata | Field | Value | |---|---| | Status | Implemented | | Target | M03 Renderer Facade and Safe Extraction | | Work package | `M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001` | | Owner | Render | | Last verified | 2026-08-01 | ## Purpose `WmoRenderGroupMaterializer` creates and attaches one lightweight cached WMO render group on the renderer main thread. It owns the duplicated `MeshInstance3D`/`MultiMeshInstance3D` presentation rules that previously lived inside `StreamingWorldLoader`. ## Non-goals - select a build step, advance/cancel a job or consume a render permit; - load, cache, validate or finalize WMO Resources; - resolve placement transforms or own the placement root; - choose Editor persistence policy or serialize generated nodes; - change WMO cache formats, materials, visibility or shadow policy. ## Context and boundaries The loader obtains a queue-owned WMO root and render Resource, asks `WmoRenderBuildStepPlanner` for one operation, and finalizes the selected Mesh. The materializer then performs only indexed node presentation and attachment. The loader retains scheduler, queue and Editor composition responsibilities. ```mermaid flowchart LR Queue[WmoRenderBuildQueue] --> Loader[StreamingWorldLoader] Planner[WmoRenderBuildStepPlanner] --> Loader Loader --> Finalizer[WmoRuntimeMeshFinalizer] Loader --> Materializer[WmoRenderGroupMaterializer] Materializer --> MeshNode[MeshInstance3D] Materializer --> MultiMeshNode[MultiMeshInstance3D] MeshNode --> Root[Queue-owned WMO Node3D root] MultiMeshNode --> Root Loader --> EditorOwner[Optional Editor owner assignment] ``` ## Public API | Symbol | Role | Thread/lifetime | Failure behavior | |---|---|---|---| | `materialize_mesh_group(...)` | Create, configure and attach one indexed Mesh group | Renderer main thread; stateless after return | Null/invalid parent, null Mesh or negative index returns null | | `materialize_multimesh_group(...)` | Create, configure and attach one indexed MultiMesh doodad group | Renderer main thread; stateless after return | Null/invalid parent, null MultiMesh or negative index returns null | Both methods preserve exact Resource identity. `group_index` selects the optional name and transform; missing names use the historical indexed fallback and a missing transform leaves `Transform3D.IDENTITY`. Positive visibility range applies its caller-supplied margin. Shadow mode is always applied explicitly. ## Inputs and outputs | Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | |---|---|---|---|---|---| | Input | Queue-owned WMO `Node3D` root | `WmoRenderBuildQueue` via loader | Materializer | Borrowed; not retained | One main-thread call | | Input | Selected `Mesh` or `MultiMesh` | WMO render Resource via loader | Materializer | Borrowed exact Resource | Parent-node lifetime after attach | | Input | Names, transforms and selected index | WMO render Resource/planner via loader | Materializer | Borrowed value collections | One call | | Input | Visibility end/margin and shadow flag | Loader quality profile | Materializer | Scalar values | One call | | Output | Attached `MeshInstance3D` or `MultiMeshInstance3D` | Materializer | Loader/SceneTree | Parent root owns node and Resource reference | Until placement release/world teardown | Side effects: - allocates exactly one Godot geometry node for valid input; - applies name, optional transform, shadow and optional visibility settings; - attaches the node exactly once to the supplied WMO root. It performs no filesystem, ResourceLoader, worker, RenderingServer RID, cache, queue, permit, logging or direct Editor-owner mutation. ## Data flow ```mermaid flowchart LR Input[Root + Resource + indexed metadata + render settings] --> Validate{Valid root/resource/index?} Validate -->|no| Null[Return null; no attachment] Validate -->|yes| Kind{Mesh or MultiMesh method} Kind --> Mesh[Create MeshInstance3D] Kind --> Multi[Create MultiMeshInstance3D] Mesh --> Configure[Name + optional transform + render settings] Multi --> Configure Configure --> Attach[Attach once to WMO root] Attach --> Return[Return borrowed attached node] ``` ## Main sequence ```mermaid sequenceDiagram participant Loader as StreamingWorldLoader participant Queue as WmoRenderBuildQueue participant Finalizer as WmoRuntimeMeshFinalizer participant Materializer as WmoRenderGroupMaterializer participant Root as WMO Node3D root Loader->>Queue: read front job and cursors Loader->>Finalizer: finalize selected Mesh Loader->>Materializer: materialize selected group Materializer->>Root: add_child(geometry instance) Materializer-->>Loader: attached node or null Loader->>Loader: optional Editor ownership Loader->>Queue: adopt planned cursors Loader->>Loader: consume one group permit ``` ## Dependency diagram ```mermaid flowchart TB Loader[StreamingWorldLoader] --> Materializer[WmoRenderGroupMaterializer] Materializer --> Engine[Node3D / GeometryInstance3D / Mesh / MultiMesh] Materializer -. no dependency .-> IO[ResourceLoader / FileAccess] Materializer -. no dependency .-> Queue[WmoRenderBuildQueue] Materializer -. no dependency .-> Scheduler[RenderBudgetScheduler] Materializer -. no dependency .-> Finalizer[WmoRuntimeMeshFinalizer] ``` ## Ownership, threading and resources - Both public methods are main-thread only because they mutate the SceneTree. - The caller owns the parent root; after attachment the root owns the new node. - The node retains the exact input Mesh or MultiMesh Resource reference. - The service retains no Node, Resource, RID, collection or per-group state. - Loader-owned optional recursive Editor ownership runs after successful return. ## Errors, cancellation and recovery | Failure/state | Detection | Behavior | Recovery | |---|---|---|---| | Null/freed parent | Guard | Return null; allocate/attach nothing | Caller validates current queue job | | Null Resource | Guard | Return null | Loader advances the historically selected cursor | | Negative index | Guard | Return null | Planner supplies non-negative selected indices | | Missing name | Bounds check | Use `Group_N` or `DoodadGroup_N` | Rebuild cache metadata if desired | | Missing transform | Bounds check | Retain identity transform | Rebuild cache metadata if desired | | Placement cancellation | Loader/queue | Parent and children released by existing lifecycle | Re-request placement later | | Shutdown | Loader lifecycle | Service has no retained state to drain | New loader composes a new service | ## Configuration, capabilities and profiles The service introduces no configuration or capability. It accepts the existing `wmo_visibility_range`, `CHUNK_SIZE` margin and `wmo_cast_shadows` values chosen by the loader quality profile. Blizzlike and Enhanced selection remains outside. ## Persistence, cache and migrations No persisted data or cache format changes. The service neither reads nor writes WMO cache files and requires no migration or rebake. ## Diagnostics and observability The service emits no logs or metrics. Existing `wmo_groups` queue depth, build permits and loader lifecycle diagnostics remain authoritative. ## Verification and fidelity evidence - `verify_wmo_render_group_materializer.gd` covers exact Mesh/MultiMesh identity, indexed and fallback names, optional transforms, shadows, positive/disabled visibility, attachment, invalid input, source boundaries and 1,000 groups. - Adjacent WMO queue/planner/finalizer and checkpoint regressions protect the unchanged orchestration and visible output. - This is an exact code-motion extraction of existing Godot presentation rules; it adds no original-client 3.3.5a visual parity claim. ## Performance budgets The synthetic contract requires 1,000 simple Mesh group materializations in under one second. Production work remains limited to one group per scheduler permit. Asset-backed CPU/GPU p95/p99 and long traversal remain required evidence. ## Extension points - Asset-backed WMO traversal can measure group attachment and lifetime without changing this API. - Additional render settings belong here only when they apply equally to both lightweight group-node kinds and have fidelity evidence. ## Capability status | Capability | Status | Evidence | Gap/next step | |---|---|---|---| | Mesh group materialization | Implemented extraction | Identity/name/transform/render/attachment contract | Asset-backed visual/GPU p95/p99 pending | | MultiMesh doodad group materialization | Implemented extraction | Identity/name/transform/render/attachment contract | Asset-backed traversal/leak evidence pending | | Build planning/queue progress | Loader-owned | Existing planner/queue regressions | Further orchestration extraction pending | | Editor persistence ownership | Loader-owned | Source-boundary contract | Editor scene-save integration evidence pending | ## Known gaps and risks - SceneTree node creation remains synchronous main-thread work by design. - The synthetic fixture does not measure private WMO assets, GPU upload, portal visibility, original-client visuals, long traversal or leak behavior. ## Source map | Path | Responsibility | |---|---| | `src/render/wmo/wmo_render_group_materializer.gd` | Indexed geometry-node creation, settings and attachment | | `src/scenes/streaming/streaming_world_loader.gd` | Composition, finalization, queue, permits, Editor ownership and lifecycle | | `src/tools/verify_wmo_render_group_materializer.gd` | Synthetic contract, ownership boundary and timing regression | ## Related decisions and references - [`wmo-render-build-step-planner.md`](wmo-render-build-step-planner.md) - [`wmo-render-build-queue.md`](wmo-render-build-queue.md) - [`wmo-runtime-mesh-finalizer.md`](wmo-runtime-mesh-finalizer.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)