From 543ee1572b29ebc5f48c4e0067f9da8a7bf83716 Mon Sep 17 00:00:00 2001 From: sindoring Date: Sat, 1 Aug 2026 10:11:29 +0400 Subject: [PATCH 1/3] coordination: claim WMO render group materializer Work-Package: M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001 Agent: sindo-main-codex Tests: not run (coordination only) Fidelity: behavior-preserving extraction planned --- ...3-RND-WMO-RENDER-GROUP-MATERIALIZER-001.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 coordination/claims/M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001.md diff --git a/coordination/claims/M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001.md b/coordination/claims/M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001.md new file mode 100644 index 0000000..38474bd --- /dev/null +++ b/coordination/claims/M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001.md @@ -0,0 +1,60 @@ +# M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001 + + + +## Owner + +- Agent ID: `sindo-main-codex` +- Target: M03 Renderer Facade and Safe Extraction +- Branch: `work/sindo-main-codex/m03-wmo-render-group-materializer` +- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-wmo-render-group-materializer` + +## Outcome + +Move one-step WMO MeshInstance3D and MultiMeshInstance3D creation, render +settings and attachment from the loader into one main-thread materializer. + +## Non-goals + +- Change WMO placement, resource loading, build-step planning or queue state. +- Change runtime Mesh finalization, WMOBuilder material behavior or cache formats. +- Change permits, visibility distances, shadow policy or visible output. +- Generalize M2 and WMO materialization behind a shared abstraction. + +## Paths + +- Exclusive: `src/render/wmo/wmo_render_group_materializer.gd`, + `src/tools/verify_wmo_render_group_materializer.gd`, + `docs/modules/wmo-render-group-materializer.md`, this claim +- Shared: loader, renderer module registry/specification, `RENDER.md`, M03 Evidence + +## Contracts and data + +- Mesh and MultiMesh resources retain exact identity. +- Names and optional transforms preserve existing indexed fallback behavior. +- Shadow and positive visibility-range settings are applied unchanged. +- The supplied WMO root becomes the sole SceneTree owner of the created node. +- Editor persisted ownership is assigned only when an explicit owner is supplied. + +## Dependencies + +- Requires: accepted WMO render build queue/planner and runtime Mesh finalizer +- Blocks: remaining loader-owned WMO runtime traversal cleanup + +## Verification + +- Synthetic Mesh/MultiMesh identity, naming, transform, rendering, attachment, + invalid-input, ownership, source and bounded-timing contracts; adjacent WMO and + renderer gates; full autonomous suite and checkpoint dry-run. + +## Documentation deliverables + +- Inline API docs; module API/I/O/ownership; data-flow/sequence/dependency + diagrams; adjacent renderer docs and M03 Evidence. + +## Status + +- State: active +- Done: scope and contracts identified +- Next: implement and verify materializer delegation +- Blocked by: From f4d5e29cc9c6a7fcefd0a790f06c1205075adf2f Mon Sep 17 00:00:00 2001 From: sindoring Date: Sat, 1 Aug 2026 10:22:21 +0400 Subject: [PATCH 2/3] render: extract WMO render group materializer Work-Package: M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001 Agent: sindo-main-codex Tests: 63/64 autonomous verifiers passed; proprietary ADT probe unavailable; baseline dry-run 7/7; documentation and coordination gates passed Fidelity: exact behavior-preserving node materialization extraction; no new 3.3.5a parity claim --- RENDER.md | 13 + ...3-RND-WMO-RENDER-GROUP-MATERIALIZER-001.md | 2 +- docs/modules/README.md | 1 + docs/modules/wmo-render-group-materializer.md | 214 ++++++++++++ docs/modules/world-renderer.md | 20 +- .../wmo/wmo_render_group_materializer.gd | 101 ++++++ .../wmo/wmo_render_group_materializer.gd.uid | 1 + .../streaming/streaming_world_loader.gd | 64 ++-- .../verify_wmo_render_group_materializer.gd | 330 ++++++++++++++++++ ...erify_wmo_render_group_materializer.gd.uid | 1 + 10 files changed, 706 insertions(+), 41 deletions(-) create mode 100644 docs/modules/wmo-render-group-materializer.md create mode 100644 src/render/wmo/wmo_render_group_materializer.gd create mode 100644 src/render/wmo/wmo_render_group_materializer.gd.uid create mode 100644 src/tools/verify_wmo_render_group_materializer.gd create mode 100644 src/tools/verify_wmo_render_group_materializer.gd.uid diff --git a/RENDER.md b/RENDER.md index 5d5b62a..ad4b608 100644 --- a/RENDER.md +++ b/RENDER.md @@ -1465,6 +1465,19 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe' cases and 1,000 current-Mesh calls. This is orchestration extraction, not new build-12340 material or visual parity evidence. +## 2026-08-01 WMO Render Group Materializer Extraction + +- `WmoRenderGroupMaterializer` now owns creation and single attachment of the + lightweight cached WMO `MeshInstance3D` and `MultiMeshInstance3D` groups. +- Indexed names/transforms, `Group_N`/`DoodadGroup_N` fallbacks, exact + Mesh/MultiMesh identity, shadow mode and positive visibility range/margin are + unchanged. +- `StreamingWorldLoader` retains build-step selection, runtime Mesh finalization, + queue cursors, permits, optional Editor ownership and placement lifetime. +- Asset-free verification covers `37` presentation/ownership/source cases and + 1,000 simple group attachments. This extraction adds no asset-backed GPU, + leak, p95/p99 or original-client visual-fidelity 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. diff --git a/coordination/claims/M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001.md b/coordination/claims/M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001.md index 38474bd..497feaa 100644 --- a/coordination/claims/M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001.md +++ b/coordination/claims/M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001.md @@ -34,7 +34,7 @@ settings and attachment from the loader into one main-thread materializer. - Names and optional transforms preserve existing indexed fallback behavior. - Shadow and positive visibility-range settings are applied unchanged. - The supplied WMO root becomes the sole SceneTree owner of the created node. -- Editor persisted ownership is assigned only when an explicit owner is supplied. +- Editor persisted ownership remains an explicit loader composition policy. ## Dependencies diff --git a/docs/modules/README.md b/docs/modules/README.md index 9a2c584..7b09af8 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -50,6 +50,7 @@ | WMO scene Resource cache state | Implemented extraction | [`wmo-scene-resource-cache-state.md`](wmo-scene-resource-cache-state.md) | | WMO scene Resource finalizer | Implemented extraction | [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.md) | | 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) | | 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) | diff --git a/docs/modules/wmo-render-group-materializer.md b/docs/modules/wmo-render-group-materializer.md new file mode 100644 index 0000000..449a6a7 --- /dev/null +++ b/docs/modules/wmo-render-group-materializer.md @@ -0,0 +1,214 @@ +# 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) diff --git a/docs/modules/world-renderer.md b/docs/modules/world-renderer.md index 84cf232..06f2fbd 100644 --- a/docs/modules/world-renderer.md +++ b/docs/modules/world-renderer.md @@ -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-m2-animation-playback`, 2026-07-18 | +| Last verified | Worktree `work/sindo-main-codex/m03-wmo-render-group-materializer`, 2026-08-01 | | Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete | ## Purpose @@ -70,6 +70,8 @@ flowchart LR WmoBuildStep --> Loader Loader --> WmoBuildQueue[WmoRenderBuildQueue] WmoBuildQueue --> Loader + Loader --> WmoGroupMaterializer[WmoRenderGroupMaterializer] + WmoGroupMaterializer --> Scene Native --> Parsed[Parsed tile/model data] Parsed --> Loader Loader --> Scene[SceneTree nodes] @@ -164,6 +166,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state | `WmoPlacementRegistry.add_reference/release_reference/contains/active_count/diagnostic_snapshot/clear` | Internal WMO service | Owns placement-key to tile/global reference sets | Renderer main thread; map session | Empty/unknown/non-owner input is rejected without mutation | | `WmoRenderBuildStepPlanner.plan_step` | Internal pure WMO service | Selects one mesh-first lightweight render-group operation and next cursors | Main/any thread; stateless | Raw integer comparisons are preserved without clamping | | `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 | | `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 | @@ -206,6 +209,7 @@ loader configuration remains transitional composition data, not a caller API. | Internal WMO ownership | Resolved placement key and tile/global reference key | Loader / `WmoPlacementRegistry` | Loader create/retain/final-free decisions | Registry-owned String sets; detached diagnostics | Map session or final release | | Internal WMO build step | Mesh/MultiMesh counts and job cursors | Loader / `WmoRenderBuildStepPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One group operation | | 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 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 | @@ -413,14 +417,16 @@ sequenceDiagram reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation and cursor plan. `WmoRenderBuildQueue` owns typed pending jobs, FIFO keys and strong root/resource references without freeing engine objects. + `WmoRenderGroupMaterializer` owns indexed MeshInstance3D/MultiMeshInstance3D + creation, render settings and attachment without retaining engine objects. `WmoRenderResourceCacheState` owns validated render Resources, negative entries and pending cache paths; `WmoRenderResourceFinalizer` owns its terminal ResourceLoader polling and script/format validation. `WmoSceneResourceCacheState` similarly owns validated PackedScenes, negative entries and pending `.tscn` paths; `WmoSceneResourceFinalizer` owns terminal ResourceLoader I/O and validation-probe lifetime. The loader retains request admission, FileAccess - size checks, live fallback, materialization, permits, validity reactions and - every placed-Node lifecycle action. + size checks, live fallback, Mesh finalization, permits, Editor ownership, + validity reactions and every placed-Node lifecycle action. - `AdtWaterLoadPipelineState` owns pending request order/deduplication, opaque active task IDs and the worker-safe parsed-result mailbox. The loader retains WorkerThreadPool start/wait, ADTLoader parsing, concurrency/finalize permits, @@ -558,6 +564,9 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m transition, completion/raw integer behavior, source ownership and bounded timing. - WMO render build queue contract: typed references/cursors, FIFO, duplicate replacement, stale-front cleanup, cancel/clear engine lifetime and bounded timing. +- WMO render group materializer contract: exact Resource identity, indexed and + fallback names/transforms, render settings, attachment, source ownership 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. @@ -617,7 +626,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | WMO placement resolver | Implemented extraction | Scene-free path/identity/transform/source/timing contract | Asset-backed comparison pending | | WMO placement registry | Implemented extraction | Scene-free ownership/lifecycle/source/timing contract | Build/resource state and asset-backed cross-tile corpus pending | | WMO render build step planner | Implemented extraction | Scene-free order/cursor/source/timing contract | Asset-backed traversal p95/p99 pending | -| WMO render build queue | Implemented extraction | Typed lifecycle/order/ownership/source/timing contract | Materialization and asset-backed traversal/leak evidence pending | +| 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 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 | @@ -700,6 +710,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | `src/render/wmo/wmo_render_resource_finalizer.gd` | Lightweight WMO terminal polling, validation and publication | | `src/render/wmo/wmo_scene_resource_finalizer.gd` | Cached WMO terminal polling, probe validation/lifetime and publication | | `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/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 | @@ -737,6 +748,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | `src/tools/verify_wmo_render_resource_finalizer.gd` | WMO render status/order/validation/adoption/boundary/timing regression | | `src/tools/verify_wmo_scene_resource_finalizer.gd` | WMO scene status/order/probe/lifetime/adoption/boundary/timing regression | | `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_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 | diff --git a/src/render/wmo/wmo_render_group_materializer.gd b/src/render/wmo/wmo_render_group_materializer.gd new file mode 100644 index 0000000..0e75a61 --- /dev/null +++ b/src/render/wmo/wmo_render_group_materializer.gd @@ -0,0 +1,101 @@ +class_name WmoRenderGroupMaterializer +extends RefCounted + +## Creates and attaches one lightweight cached WMO render group. Resource +## finalization, build-step selection and queue progress remain caller-owned. +## All methods mutate SceneTree nodes and must run on the renderer main thread. + + +## Creates one MeshInstance3D for [param group_index], applies the indexed name +## and optional transform contracts, then attaches it to [param wmo_parent_root]. +## The parent owns the returned node; the exact Mesh identity is retained. +## Null/invalid parents, null meshes and negative indices return null. +func materialize_mesh_group( + wmo_parent_root: Node3D, + mesh: Mesh, + group_names: PackedStringArray, + group_transforms: Array, + group_index: int, + visibility_range_end: float, + visibility_range_end_margin: float, + cast_shadows: bool) -> MeshInstance3D: + if ( + wmo_parent_root == null + or not is_instance_valid(wmo_parent_root) + or mesh == null + or group_index < 0 + ): + return null + + var mesh_instance := MeshInstance3D.new() + mesh_instance.name = ( + group_names[group_index] + if group_index < group_names.size() + else "Group_%d" % group_index + ) + mesh_instance.mesh = mesh + if group_index < group_transforms.size(): + mesh_instance.transform = group_transforms[group_index] + _apply_render_settings( + mesh_instance, + visibility_range_end, + visibility_range_end_margin, + cast_shadows + ) + wmo_parent_root.add_child(mesh_instance) + return mesh_instance + + +## Creates one MultiMeshInstance3D for [param group_index], applies the indexed +## name and optional transform contracts, then attaches it to the supplied root. +## The parent owns the returned node; the exact MultiMesh identity is retained. +## Null/invalid parents, null MultiMeshes and negative indices return null. +func materialize_multimesh_group( + wmo_parent_root: Node3D, + multimesh: MultiMesh, + group_names: PackedStringArray, + group_transforms: Array, + group_index: int, + visibility_range_end: float, + visibility_range_end_margin: float, + cast_shadows: bool) -> MultiMeshInstance3D: + if ( + wmo_parent_root == null + or not is_instance_valid(wmo_parent_root) + or multimesh == null + or group_index < 0 + ): + return null + + var multimesh_instance := MultiMeshInstance3D.new() + multimesh_instance.name = ( + group_names[group_index] + if group_index < group_names.size() + else "DoodadGroup_%d" % group_index + ) + multimesh_instance.multimesh = multimesh + if group_index < group_transforms.size(): + multimesh_instance.transform = group_transforms[group_index] + _apply_render_settings( + multimesh_instance, + visibility_range_end, + visibility_range_end_margin, + cast_shadows + ) + wmo_parent_root.add_child(multimesh_instance) + return multimesh_instance + + +func _apply_render_settings( + geometry_instance: GeometryInstance3D, + visibility_range_end: float, + visibility_range_end_margin: float, + cast_shadows: bool) -> void: + geometry_instance.cast_shadow = ( + GeometryInstance3D.SHADOW_CASTING_SETTING_ON + if cast_shadows + else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + ) + if visibility_range_end > 0.0: + geometry_instance.visibility_range_end = visibility_range_end + geometry_instance.visibility_range_end_margin = visibility_range_end_margin diff --git a/src/render/wmo/wmo_render_group_materializer.gd.uid b/src/render/wmo/wmo_render_group_materializer.gd.uid new file mode 100644 index 0000000..f886750 --- /dev/null +++ b/src/render/wmo/wmo_render_group_materializer.gd.uid @@ -0,0 +1 @@ +uid://cu4tw6868rbkm diff --git a/src/scenes/streaming/streaming_world_loader.gd b/src/scenes/streaming/streaming_world_loader.gd index fe5ccc5..fea2a65 100644 --- a/src/scenes/streaming/streaming_world_loader.gd +++ b/src/scenes/streaming/streaming_world_loader.gd @@ -36,6 +36,9 @@ const WMO_SCENE_RESOURCE_FINALIZER_SCRIPT := preload( const WMO_RUNTIME_MESH_FINALIZER_SCRIPT := preload( "res://src/render/wmo/wmo_runtime_mesh_finalizer.gd" ) +const WMO_RENDER_GROUP_MATERIALIZER_SCRIPT := preload( + "res://src/render/wmo/wmo_render_group_materializer.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") @@ -381,6 +384,7 @@ var _wmo_scene_resource_finalizer := WMO_SCENE_RESOURCE_FINALIZER_SCRIPT.new( var _wmo_runtime_mesh_finalizer := WMO_RUNTIME_MESH_FINALIZER_SCRIPT.new( WMO_BUILDER_SCRIPT ) +var _wmo_render_group_materializer := WMO_RENDER_GROUP_MATERIALIZER_SCRIPT.new() var _wmo_missing_cache: Dictionary = {} var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new() var _world_wmo_root: Node3D @@ -3846,25 +3850,18 @@ func _process_wmo_render_build_jobs() -> void: var mesh := meshes[selected_index] as Mesh if mesh != null: _wmo_runtime_mesh_finalizer.finalize_mesh(mesh, extracted_dir) - var mesh_instance := MeshInstance3D.new() - mesh_instance.name = ( - mesh_names[selected_index] - if selected_index < mesh_names.size() - else "Group_%d" % selected_index + var mesh_instance := _wmo_render_group_materializer.materialize_mesh_group( + root as Node3D, + mesh, + mesh_names, + mesh_transforms, + selected_index, + wmo_visibility_range, + CHUNK_SIZE, + wmo_cast_shadows ) - mesh_instance.mesh = mesh - if selected_index < mesh_transforms.size(): - mesh_instance.transform = mesh_transforms[selected_index] - mesh_instance.cast_shadow = ( - GeometryInstance3D.SHADOW_CASTING_SETTING_ON - if wmo_cast_shadows - else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF - ) - if wmo_visibility_range > 0.0: - mesh_instance.visibility_range_end = wmo_visibility_range - mesh_instance.visibility_range_end_margin = CHUNK_SIZE - (root as Node3D).add_child(mesh_instance) - _set_editor_owner_recursive(mesh_instance) + if mesh_instance != null: + _set_editor_owner_recursive(mesh_instance) _wmo_render_build_queue.adopt_cursors( unique_key, int(build_step["next_mesh_index"]), @@ -3877,25 +3874,20 @@ func _process_wmo_render_build_jobs() -> void: var multimesh := multimeshes[selected_index] as MultiMesh if multimesh != null: _wmo_runtime_mesh_finalizer.finalize_mesh(multimesh.mesh, extracted_dir) - var multimesh_instance := MultiMeshInstance3D.new() - multimesh_instance.name = ( - multimesh_names[selected_index] - if selected_index < multimesh_names.size() - else "DoodadGroup_%d" % selected_index + var multimesh_instance := ( + _wmo_render_group_materializer.materialize_multimesh_group( + root as Node3D, + multimesh, + multimesh_names, + multimesh_transforms, + selected_index, + wmo_visibility_range, + CHUNK_SIZE, + wmo_cast_shadows + ) ) - multimesh_instance.multimesh = multimesh - if selected_index < multimesh_transforms.size(): - multimesh_instance.transform = multimesh_transforms[selected_index] - multimesh_instance.cast_shadow = ( - GeometryInstance3D.SHADOW_CASTING_SETTING_ON - if wmo_cast_shadows - else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF - ) - if wmo_visibility_range > 0.0: - multimesh_instance.visibility_range_end = wmo_visibility_range - multimesh_instance.visibility_range_end_margin = CHUNK_SIZE - (root as Node3D).add_child(multimesh_instance) - _set_editor_owner_recursive(multimesh_instance) + if multimesh_instance != null: + _set_editor_owner_recursive(multimesh_instance) _wmo_render_build_queue.adopt_cursors( unique_key, int(build_step["next_mesh_index"]), diff --git a/src/tools/verify_wmo_render_group_materializer.gd b/src/tools/verify_wmo_render_group_materializer.gd new file mode 100644 index 0000000..3b0a77d --- /dev/null +++ b/src/tools/verify_wmo_render_group_materializer.gd @@ -0,0 +1,330 @@ +extends SceneTree + +## Asset-free WMO render-group Resource identity, indexed presentation, +## attachment, ownership-boundary and bounded-timing regression. + +const MATERIALIZER_SCRIPT := preload( + "res://src/render/wmo/wmo_render_group_materializer.gd" +) +const MATERIALIZER_PATH := "res://src/render/wmo/wmo_render_group_materializer.gd" +const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" + + +func _initialize() -> void: + var failures: Array[String] = [] + _verify_mesh_group_contract(failures) + _verify_multimesh_group_contract(failures) + _verify_fallback_and_zero_visibility_contract(failures) + _verify_invalid_inputs(failures) + _verify_source_boundaries(failures) + var elapsed_milliseconds := _verify_bounded_timing(failures) + if not failures.is_empty(): + for failure in failures: + push_error("WMO_RENDER_GROUP_MATERIALIZER: %s" % failure) + quit(1) + return + print( + "WMO_RENDER_GROUP_MATERIALIZER PASS cases=37 iterations=1000 elapsed_ms=%.3f" + % elapsed_milliseconds + ) + quit(0) + + +func _verify_mesh_group_contract(failures: Array[String]) -> void: + var materializer := MATERIALIZER_SCRIPT.new() + var parent_root := Node3D.new() + get_root().add_child(parent_root) + var mesh := ArrayMesh.new() + var group_transform := Transform3D(Basis.IDENTITY, Vector3(1.0, 2.0, 3.0)) + var mesh_instance: MeshInstance3D = materializer.materialize_mesh_group( + parent_root, + mesh, + PackedStringArray(["Exterior", "Interior"]), + [Transform3D.IDENTITY, group_transform], + 1, + 1800.0, + 533.33333, + false + ) + _expect_true(mesh_instance != null, "valid Mesh group returned", failures) + _expect_same(mesh_instance.get_parent(), parent_root, "Mesh group attached once", failures) + _expect_equal(parent_root.get_child_count(), 1, "one Mesh parent child", failures) + _expect_string_equal(mesh_instance.name, "Interior", "indexed Mesh name", failures) + _expect_same(mesh_instance.mesh, mesh, "exact Mesh identity", failures) + _expect_true(mesh_instance.transform == group_transform, "indexed Mesh transform", failures) + _expect_equal( + mesh_instance.cast_shadow, + GeometryInstance3D.SHADOW_CASTING_SETTING_OFF, + "Mesh shadow disabled", + failures + ) + _expect_float_equal(mesh_instance.visibility_range_end, 1800.0, "Mesh visibility end", failures) + _expect_float_equal( + mesh_instance.visibility_range_end_margin, + 533.33333, + "Mesh visibility margin", + failures + ) + parent_root.free() + + +func _verify_multimesh_group_contract(failures: Array[String]) -> void: + var materializer := MATERIALIZER_SCRIPT.new() + var parent_root := Node3D.new() + get_root().add_child(parent_root) + var multimesh := MultiMesh.new() + multimesh.transform_format = MultiMesh.TRANSFORM_3D + multimesh.mesh = ArrayMesh.new() + multimesh.instance_count = 1 + var group_transform := Transform3D(Basis.IDENTITY, Vector3(4.0, 5.0, 6.0)) + var multimesh_instance: MultiMeshInstance3D = ( + materializer.materialize_multimesh_group( + parent_root, + multimesh, + PackedStringArray(["Doodads"]), + [group_transform], + 0, + 2600.0, + 400.0, + true + ) + ) + _expect_same( + multimesh_instance.get_parent(), + parent_root, + "MultiMesh group attached once", + failures + ) + _expect_string_equal(multimesh_instance.name, "Doodads", "indexed MultiMesh name", failures) + _expect_same(multimesh_instance.multimesh, multimesh, "exact MultiMesh identity", failures) + _expect_true( + multimesh_instance.transform == group_transform, + "indexed MultiMesh transform", + failures + ) + _expect_equal( + multimesh_instance.cast_shadow, + GeometryInstance3D.SHADOW_CASTING_SETTING_ON, + "MultiMesh shadow enabled", + failures + ) + _expect_float_equal( + multimesh_instance.visibility_range_end, + 2600.0, + "MultiMesh visibility end", + failures + ) + parent_root.free() + + +func _verify_fallback_and_zero_visibility_contract(failures: Array[String]) -> void: + var materializer := MATERIALIZER_SCRIPT.new() + var parent_root := Node3D.new() + var mesh_instance: MeshInstance3D = materializer.materialize_mesh_group( + parent_root, + ArrayMesh.new(), + PackedStringArray(), + [], + 7, + 0.0, + 99.0, + true + ) + _expect_string_equal(mesh_instance.name, "Group_7", "Mesh fallback name", failures) + _expect_true(mesh_instance.transform == Transform3D.IDENTITY, "missing Mesh transform stays identity", failures) + _expect_float_equal(mesh_instance.visibility_range_end, 0.0, "zero Mesh visibility unchanged", failures) + _expect_float_equal(mesh_instance.visibility_range_end_margin, 0.0, "disabled Mesh margin unchanged", failures) + var multimesh_instance: MultiMeshInstance3D = ( + materializer.materialize_multimesh_group( + parent_root, + MultiMesh.new(), + PackedStringArray(), + [], + 8, + 0.0, + 99.0, + false + ) + ) + _expect_string_equal( + multimesh_instance.name, + "DoodadGroup_8", + "MultiMesh fallback name", + failures + ) + _expect_equal(parent_root.get_child_count(), 2, "fallback groups attached", failures) + parent_root.free() + + +func _verify_invalid_inputs(failures: Array[String]) -> void: + var materializer := MATERIALIZER_SCRIPT.new() + var parent_root := Node3D.new() + var mesh := ArrayMesh.new() + _expect_true( + materializer.materialize_mesh_group( + null, mesh, PackedStringArray(), [], 0, 0.0, 0.0, true + ) == null, + "null Mesh parent rejected", + failures + ) + _expect_true( + materializer.materialize_mesh_group( + parent_root, null, PackedStringArray(), [], 0, 0.0, 0.0, true + ) == null, + "null Mesh rejected", + failures + ) + _expect_true( + materializer.materialize_mesh_group( + parent_root, mesh, PackedStringArray(), [], -1, 0.0, 0.0, true + ) == null, + "negative Mesh index rejected", + failures + ) + _expect_true( + materializer.materialize_multimesh_group( + null, MultiMesh.new(), PackedStringArray(), [], 0, 0.0, 0.0, true + ) == null, + "null MultiMesh parent rejected", + failures + ) + _expect_true( + materializer.materialize_multimesh_group( + parent_root, null, PackedStringArray(), [], 0, 0.0, 0.0, true + ) == null, + "null MultiMesh rejected", + failures + ) + _expect_equal(parent_root.get_child_count(), 0, "invalid inputs do not attach", failures) + parent_root.free() + + +func _verify_source_boundaries(failures: Array[String]) -> void: + var materializer_source := FileAccess.get_file_as_string(MATERIALIZER_PATH) + var loader_source := FileAccess.get_file_as_string(LOADER_PATH) + var loader_adapter_source := _source_between( + loader_source, + "func _process_wmo_render_build_jobs()", + "func _cancel_wmo_render_build_job(", + failures + ) + _expect_true( + loader_source.contains("WMO_RENDER_GROUP_MATERIALIZER_SCRIPT.new()"), + "loader composes WMO group materializer", + failures + ) + _expect_equal( + loader_adapter_source.count("_wmo_render_group_materializer.materialize_"), + 2, + "loader delegates both group kinds", + failures + ) + _expect_false( + loader_adapter_source.contains("MeshInstance3D.new()"), + "loader releases MeshInstance construction", + failures + ) + _expect_false( + loader_adapter_source.contains("MultiMeshInstance3D.new()"), + "loader releases MultiMeshInstance construction", + failures + ) + _expect_equal( + loader_adapter_source.count("_set_editor_owner_recursive("), + 2, + "loader retains Editor ownership", + failures + ) + for required_rule in [ + "wmo_parent_root.add_child(mesh_instance)", + "wmo_parent_root.add_child(multimesh_instance)", + "else \"Group_%d\" % group_index", + "else \"DoodadGroup_%d\" % group_index", + "geometry_instance.visibility_range_end_margin = visibility_range_end_margin", + ]: + _expect_true( + materializer_source.contains(required_rule), + "materializer owns %s" % required_rule, + failures + ) + for forbidden_dependency in [ + "ResourceLoader.", + "FileAccess.", + "WorkerThreadPool.", + "RenderingServer.", + ".owner =", + "_wmo_render_build_queue", + "_render_budget_scheduler", + "WmoRuntimeMeshFinalizer", + ]: + _expect_false( + materializer_source.contains(forbidden_dependency), + "materializer omits %s ownership" % forbidden_dependency, + failures + ) + + +func _verify_bounded_timing(failures: Array[String]) -> float: + var materializer := MATERIALIZER_SCRIPT.new() + var parent_root := Node3D.new() + get_root().add_child(parent_root) + var mesh := ArrayMesh.new() + var started_microseconds := Time.get_ticks_usec() + for group_index in 1000: + materializer.materialize_mesh_group( + parent_root, + mesh, + PackedStringArray(), + [], + group_index, + 0.0, + 0.0, + false + ) + var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0 + _expect_equal(parent_root.get_child_count(), 1000, "timing groups complete", failures) + _expect_true(elapsed_milliseconds < 1000.0, "1000 groups under one second", failures) + parent_root.free() + return elapsed_milliseconds + + +func _source_between( + source: String, + start_marker: String, + end_marker: String, + failures: Array[String]) -> String: + var start_offset := source.find(start_marker) + var end_offset := source.find(end_marker, start_offset + start_marker.length()) + if start_offset < 0 or end_offset <= start_offset: + failures.append("cannot isolate source between %s and %s" % [start_marker, end_marker]) + return "" + return source.substr(start_offset, end_offset - start_offset) + + +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_float_equal(actual: float, expected: float, label: String, failures: Array[String]) -> void: + if not is_equal_approx(actual, expected): + failures.append("%s expected=%.6f actual=%.6f" % [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) diff --git a/src/tools/verify_wmo_render_group_materializer.gd.uid b/src/tools/verify_wmo_render_group_materializer.gd.uid new file mode 100644 index 0000000..c7770ab --- /dev/null +++ b/src/tools/verify_wmo_render_group_materializer.gd.uid @@ -0,0 +1 @@ +uid://b1kt1agus8efe From 6deb80c0dd91c7fdb64bb8a76c267eedb702603e Mon Sep 17 00:00:00 2001 From: sindoring Date: Sat, 1 Aug 2026 10:22:44 +0400 Subject: [PATCH 3/3] coordination: hand off WMO render group materializer Work-Package: M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001 Agent: sindo-main-codex Tests: documented in handoff Fidelity: exact behavior-preserving extraction --- ...3-RND-WMO-RENDER-GROUP-MATERIALIZER-001.md | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/coordination/claims/M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001.md b/coordination/claims/M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001.md index 497feaa..343a753 100644 --- a/coordination/claims/M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001.md +++ b/coordination/claims/M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001.md @@ -54,7 +54,28 @@ settings and attachment from the loader into one main-thread materializer. ## Status -- State: active -- Done: scope and contracts identified -- Next: implement and verify materializer delegation +- State: ready +- Done: implementation, verification and documentation +- Next: integrator review and merge - Blocked by: + +## Handoff + +- Commit: `f4d5e29` (`render: extract WMO render group materializer`) +- Results: materializer PASS `cases=37 iterations=1000 elapsed_ms=2.771`; + autonomous headless suite `63/64`, with only the proprietary + `verify_adt_m2_placements.gd` probe unavailable because `data/extracted` is + absent; cold/editor parse completed without script diagnostics after restoring + ignored generated/native worktree artifacts; checkpoint dry-run retained + `7/7`; documentation passed with `50` module specifications; coordination + passed with `77` historical expired-claim warnings. +- Fidelity: exact Mesh/MultiMesh identity, indexed/fallback presentation, + render settings, attachment and scheduler/queue boundaries are unchanged. + No private-asset or original-client visual parity claim is added. +- Remaining risks: synchronous main-thread Node creation remains; no private WMO + visual comparison, long traversal, leak/GPU or p95/p99 evidence. +- Documentation: new full module specification with API/I/O and data-flow, + sequence/dependency diagrams; renderer registry/source map and `RENDER.md` + updated. + +