merge: WMO render group materializer

Work-Package: M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001
Agent: sindo-main-codex
Tests: 63/64 autonomous headless verifiers; proprietary ADT probe unavailable; checkpoint dry-run 7/7
Fidelity: behavior-preserving WMO group materialization extraction
This commit is contained in:
2026-08-01 10:22:58 +04:00
10 changed files with 786 additions and 40 deletions
+13
View File
@@ -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.
@@ -0,0 +1,81 @@
# M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001
<!-- OPENWC_CLAIM:M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001:sindo-main-codex:2026-08-03 -->
## 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 remains an explicit loader composition policy.
## 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: 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.
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001:f4d5e29 -->
+1
View File
@@ -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) |
@@ -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)
+16 -4
View File
@@ -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 |
@@ -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
@@ -0,0 +1 @@
uid://cu4tw6868rbkm
+28 -36
View File
@@ -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"]),
@@ -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)
@@ -0,0 +1 @@
uid://b1kt1agus8efe