render: extract WMO runtime scene preparer

Work-Package: M03-RND-WMO-RUNTIME-SCENE-PREPARER-001
Agent: sindo-main-codex
Tests: 64/65 headless verifiers passed; proprietary ADT probe unavailable; checkpoint dry-run 7/7; docs and coordination passed
Fidelity: preserves cached/live preparation distinction, direct occluder policy and shadow semantics
This commit is contained in:
2026-08-01 10:37:23 +04:00
parent cfa3dc1009
commit ffed91c364
11 changed files with 675 additions and 53 deletions
+13
View File
@@ -1478,6 +1478,19 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
1,000 simple group attachments. This extraction adds no asset-backed GPU,
leak, p95/p99 or original-client visual-fidelity evidence.
## 2026-08-01 WMO Runtime Scene Preparer Extraction
- `WmoRuntimeScenePreparer` now owns cached WMO parent-before-children Mesh/
MultiMesh finalization and the shared cached/live render-policy preparation.
- The historical path distinction is unchanged: live-built duplicates do not
cross the cached runtime Mesh finalizer boundary.
- Disabled occlusion still removes only the direct child named `Occluders`;
enabled shadows still set descendant GeometryInstance3D nodes ON, while the
disabled shadow branch preserves existing values.
- Instantiation, placement, attachment, registry lifetime, Editor ownership,
queues and permits remain loader-owned. Synthetic traversal timing is not
private-asset visual, leak, GPU or p95/p99 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.
+1
View File
@@ -51,6 +51,7 @@
| 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) |
| WMO runtime scene preparer | Implemented extraction | [`wmo-runtime-scene-preparer.md`](wmo-runtime-scene-preparer.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) |
+19 -7
View File
@@ -27,14 +27,17 @@ refresh version and reconstruct eligible surface materials through WMOBuilder.
```mermaid
flowchart LR
Loader[StreamingWorldLoader traversal/build step] -->|Mesh plus extracted directory| Finalizer[WmoRuntimeMeshFinalizer]
Loader[StreamingWorldLoader render build step] -->|Mesh plus extracted directory| Finalizer[WmoRuntimeMeshFinalizer]
Preparer[WmoRuntimeScenePreparer cached traversal] -->|Mesh plus extracted directory| Finalizer
Finalizer -->|material definition plus compact texture paths| Builder[WMOBuilder material boundary]
Builder -->|rebuilt Material| Finalizer
Finalizer -->|same Mesh identity| Loader
Finalizer -->|same Mesh identity| Preparer
```
The loader owns scene traversal and composition. The finalizer owns only the
in-place Resource operation and depends on an injected WMO material builder.
The runtime scene preparer owns cached-scene traversal; the loader owns render
build-step selection and composition. The finalizer owns only the in-place
Resource operation and depends on an injected WMO material builder.
## Public API
@@ -52,7 +55,7 @@ in-place Resource operation and depends on an injected WMO material builder.
| Input | Texture paths, WMO flags/shader/blend and cached colors | Surface Material metadata/parameters | Finalizer | Borrowed values | One surface rebuild |
| Output | Compact texture-path array and material definition | Finalizer | WMOBuilder | Detached values | One builder call |
| Output | Rebuilt Material | WMOBuilder | ArrayMesh surface | Surface adopts exact Resource | Mesh lifetime |
| Output | Exact input Mesh | Finalizer | Loader traversal/build step | Caller retains ownership | Existing cache/scene lifetime |
| Output | Exact input Mesh | Finalizer | Runtime scene preparer or loader build step | Caller retains ownership | Existing cache/scene lifetime |
## Data flow
@@ -96,10 +99,15 @@ in the extracted loader behavior. The finalizer retains no Mesh or Material.
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Preparer as WmoRuntimeScenePreparer
participant Finalizer as WmoRuntimeMeshFinalizer
participant Mesh as ArrayMesh
participant Builder as WMOBuilder
alt lightweight render build step
Loader->>Finalizer: finalize_mesh(mesh, extracted_directory)
else cached scene traversal
Preparer->>Finalizer: finalize_mesh(mesh, extracted_directory)
end
Finalizer->>Mesh: read/stamp refresh metadata
loop each stale ArrayMesh surface
Finalizer->>Mesh: surface_get_material(index)
@@ -109,7 +117,8 @@ sequenceDiagram
Finalizer->>Mesh: surface_set_material when non-null
end
end
Finalizer-->>Loader: exact Mesh identity
Finalizer-->>Loader: exact Mesh identity for build path
Finalizer-->>Preparer: exact Mesh identity for cached traversal path
```
## Dependency diagram
@@ -117,6 +126,7 @@ sequenceDiagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Finalizer[WmoRuntimeMeshFinalizer]
Preparer[WmoRuntimeScenePreparer] --> Finalizer
Finalizer --> Engine[Mesh / ArrayMesh / Material]
Finalizer --> Builder[Injected WMO material builder]
Finalizer -. no dependency .-> Nodes[Node traversal/lifetime]
@@ -184,7 +194,8 @@ metrics and loader diagnostics remain the operational correlation surface.
|---|---|---|---|
| Runtime refresh admission | Implemented extraction | Identity/version/type contracts | Serialized cache fixture pending |
| Cached material definition reconstruction | Implemented extraction | Path/metadata/color contracts | Asset-backed visual comparison pending |
| Scene traversal/materialization | Loader-owned | Existing WMO regressions | Further safe extraction pending |
| Scene traversal | Runtime scene preparer-owned | Cached/live traversal regressions | Asset-backed traversal pending |
| Node materialization | Loader/group-materializer-owned | Existing WMO regressions | Further safe extraction pending |
## Known gaps and risks
@@ -199,7 +210,8 @@ metrics and loader diagnostics remain the operational correlation surface.
|---|---|
| `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Refresh admission, surface iteration and material definition reconstruction |
| `addons/mpq_extractor/loaders/wmo_builder.gd` | WMO shader/material construction semantics |
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, Node traversal, build jobs and lifetime |
| `src/render/wmo/wmo_runtime_scene_preparer.gd` | Cached scene traversal and finalizer delegation |
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, build jobs, placement and lifetime |
| `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | Identity/version/material/source/timing regression |
## Related decisions and references
+211
View File
@@ -0,0 +1,211 @@
# WMO Runtime Scene Preparer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target | M03 Renderer Facade and Safe Extraction |
| Work package | `M03-RND-WMO-RUNTIME-SCENE-PREPARER-001` |
| Owner | Render |
| Last verified | 2026-08-01 |
## Purpose
`WmoRuntimeScenePreparer` applies the existing post-instantiation rules to a
borrowed WMO subtree on the renderer main thread. Cached scenes receive recursive
Mesh finalization before render policy; live-built duplicates receive render
policy only.
## Non-goals
- load, validate, instantiate, duplicate, place or attach a WMO scene;
- change `WmoRuntimeMeshFinalizer` material/version rules;
- own the supplied subtree or decide Editor persistence;
- change cache formats, queue progress, permits or shutdown;
- implement portal/room visibility or recursive occluder discovery.
## Context and boundaries
The loader distinguishes validated cached PackedScene instances from duplicated
live-built prototypes. That distinction remains explicit through two public
methods so the live path does not gain cached-Mesh refresh behavior.
```mermaid
flowchart LR
Cached[Validated cached WMO instance] --> Loader[StreamingWorldLoader]
Live[Duplicated live-built WMO instance] --> Loader
Loader -->|prepare_cached_instance| Preparer[WmoRuntimeScenePreparer]
Loader -->|prepare_live_instance| Preparer
Preparer -->|cached only| MeshFinalizer[WmoRuntimeMeshFinalizer]
Preparer --> Policy[Direct Occluders policy + recursive shadow enabling]
Policy --> Borrowed[Borrowed WMO Node3D subtree]
Loader --> Placement[Placement/attachment/registry lifecycle]
```
## Public API
| Symbol | Role | Thread/lifetime | Failure behavior |
|---|---|---|---|
| `prepare_cached_instance(instance, extracted_directory, enable_occlusion_culling, cast_shadows)` | Finalize cached subtree Meshes, then apply render policy | Renderer main thread; stateless after return | Null/freed root returns false |
| `prepare_live_instance(instance, enable_occlusion_culling, cast_shadows)` | Apply render policy without Mesh finalization | Renderer main thread; stateless after return | Null/freed root returns false |
Both methods borrow the root and return a success flag. Disabled occlusion removes
only the direct child named `Occluders`, matching the previous loader lookup.
Enabled shadows set all descendant `GeometryInstance3D` nodes to ON. Disabled
shadows preserve every existing value rather than forcing OFF.
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Cached or live-built WMO `Node3D` | Loader scene/prototype adapter | Preparer | Borrowed exact subtree | One main-thread call |
| Input | Extracted directory | Loader configuration | Runtime Mesh finalizer via preparer | Borrowed String | Cached preparation call |
| Input | Occlusion/shadow policy | Loader quality configuration | Preparer | Scalar values | One call |
| Internal | Mesh/MultiMesh Mesh reference | Borrowed subtree traversal | `WmoRuntimeMeshFinalizer` | Exact Resource reference; not retained by preparer | One cached call |
| Output | Success flag | Preparer | Loader | Value | Immediate |
| Side effect | Mesh refresh, optional child removal and shadow mutation | Preparer | Borrowed subtree | SceneTree remains loader/placement-owned | Until subtree release |
No filesystem, ResourceLoader, worker, RID, cache, queue, placement, attachment,
Editor-owner or diagnostic side effect is introduced.
## Data flow
```mermaid
flowchart LR
Input[Borrowed root + path kind + policies] --> Valid{Root valid?}
Valid -->|no| False[Return false]
Valid -->|yes cached| Walk[Parent-before-children traversal]
Valid -->|yes live| Policy
Walk --> Mesh{MeshInstance or non-null MultiMesh?}
Mesh -->|yes| Finalize[WmoRuntimeMeshFinalizer.finalize_mesh]
Mesh -->|no| Next[Continue]
Finalize --> Next
Next --> Policy[Apply direct Occluders and shadow policies]
Policy --> True[Return true]
```
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Preparer as WmoRuntimeScenePreparer
participant Finalizer as WmoRuntimeMeshFinalizer
participant Root as Borrowed WMO subtree
alt cached scene
Loader->>Preparer: prepare_cached_instance(root, directory, policies)
loop parent-before-children Mesh traversal
Preparer->>Finalizer: finalize_mesh(exact Mesh, directory)
end
else live-built duplicate
Loader->>Preparer: prepare_live_instance(root, policies)
end
Preparer->>Root: optional direct Occluders removal
Preparer->>Root: optional recursive shadow ON
Preparer-->>Loader: true/false
Loader->>Loader: place, attach, register and own lifetime
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Preparer[WmoRuntimeScenePreparer]
Preparer --> Finalizer[WmoRuntimeMeshFinalizer]
Preparer --> Engine[Node3D / GeometryInstance3D / Mesh / MultiMesh]
Preparer -. no dependency .-> IO[ResourceLoader / FileAccess]
Preparer -. no dependency .-> Queue[WMO queues / RenderBudgetScheduler]
Preparer -. no dependency .-> Placement[WmoPlacementResolver / Registry]
```
## Ownership, threading and resources
- Calls are main-thread only because Mesh Resources and SceneTree nodes mutate.
- The loader/placement registry retains ownership of the root and descendants.
- Mesh finalization receives exact borrowed Resource identities.
- A removed direct `Occluders` child is detached and `queue_free()`d as before.
- The service retains no Node, Resource, RID, path, collection or state after return.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Recovery |
|---|---|---|---|
| Null/freed root | Guard | Return false, no traversal | Caller drops/retries stale placement |
| Missing finalizer composition | Null dependency | Skip Mesh refresh but apply render policy | Fix renderer composition before cached use |
| Null Mesh | Exact finalizer call for MeshInstance; finalizer handles null | Continue traversal | Cache rebuild/finalizer diagnostics |
| Null MultiMesh or its Mesh | Guard/finalizer contract | Skip null MultiMesh; finalizer handles null Mesh | Continue safely |
| Occlusion disabled | Direct-child lookup | Detach and queue-free `Occluders` | Reinstantiate to restore subtree |
| Shadows disabled | Policy branch | Preserve current node settings | Re-run with enabled policy if required |
| Placement cancellation/shutdown | Loader lifecycle | Service has no retained work to cancel | Existing subtree release/drain order |
## Configuration and capabilities
The service introduces no setting. It receives existing
`enable_occlusion_culling` and `wmo_cast_shadows` values. The direct-child name
`Occluders` and shadow-ON behavior are compatibility rules, not new capabilities.
## Persistence, cache and migration
No serialized format or cache version changes. Cached Mesh refresh metadata
continues to belong to `WmoRuntimeMeshFinalizer`; no rebake or migration is needed.
## Diagnostics and observability
The preparer emits no log or metric. Loader placement/build metrics and the Mesh
finalizer contracts remain the diagnostic surfaces.
## Verification
- `verify_wmo_runtime_scene_preparer.gd` covers cached parent-before-children
Mesh/MultiMesh identity, directory forwarding, null MultiMesh, direct versus
nested `Occluders`, enabled/preserved shadows, live finalizer suppression,
missing dependency, invalid roots, source boundaries and 1,000 traversals.
- Adjacent WMO finalizer/cache/queue/shutdown and checkpoint checks protect the
unchanged lifecycle and presentation behavior.
- Fidelity evidence is exact behavior-preserving extraction; no asset-backed or
original-client 3.3.5a visual-parity claim is added.
The synthetic budget is 1,000 two-Mesh cached preparations under one second.
Asset-backed CPU/GPU p95/p99, traversal and leak measurements remain pending.
## Extension points
- Asset-backed traversal can validate material identity and subtree lifetime
without expanding this service contract.
- Portal/room visibility requires its own documented WMO service and evidence;
it must not be hidden inside generic subtree preparation.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Cached WMO subtree Mesh finalization | Implemented extraction | Exact traversal/resource/directory contract | Asset-backed visual/GPU p95/p99 pending |
| Live/cached render policy | Implemented extraction | Direct occluder and recursive shadow contract | Portal/room and asset-backed traversal pending |
| Placement/attachment/lifetime | Loader-owned | Existing registry/shutdown regressions | Further orchestration extraction pending |
## Known gaps and risks
- Recursive Mesh finalization and shadow mutation remain synchronous main-thread work.
- The missing-finalizer branch degrades safely for isolated tests but production
composition must always inject `WmoRuntimeMeshFinalizer`.
- No private WMO corpus, portal/room behavior, long traversal, leak/GPU timing or
paired original-client capture is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_runtime_scene_preparer.gd` | Cached/live subtree traversal and render policy |
| `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Borrowed Mesh refresh/version/material rules |
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, instantiation, placement, attachment and lifetime |
| `src/tools/verify_wmo_runtime_scene_preparer.gd` | Synthetic traversal/policy/boundary/timing regression |
## Related decisions and references
- [`wmo-runtime-mesh-finalizer.md`](wmo-runtime-mesh-finalizer.md)
- [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.md)
- [`wmo-render-group-materializer.md`](wmo-render-group-materializer.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)
+13 -1
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-wmo-render-group-materializer`, 2026-08-01 |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-runtime-scene-preparer`, 2026-08-01 |
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose
@@ -72,6 +72,8 @@ flowchart LR
WmoBuildQueue --> Loader
Loader --> WmoGroupMaterializer[WmoRenderGroupMaterializer]
WmoGroupMaterializer --> Scene
Loader --> WmoScenePreparer[WmoRuntimeScenePreparer]
WmoScenePreparer --> Scene
Native --> Parsed[Parsed tile/model data]
Parsed --> Loader
Loader --> Scene[SceneTree nodes]
@@ -167,6 +169,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
| `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 |
| `WmoRuntimeScenePreparer.prepare_cached_instance/prepare_live_instance` | Internal WMO subtree-preparation service | Preserves cached/live Mesh-finalization distinction, direct occluder policy and recursive shadow enabling | Renderer main thread; stateless after each call | Null/freed root returns false |
| `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 |
@@ -210,6 +213,7 @@ loader configuration remains transitional composition data, not a caller API.
| 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 subtree preparation | Cached/live root, extracted directory and render policies | Loader / `WmoRuntimeScenePreparer` | Borrowed subtree and runtime Mesh finalizer | Loader/placement owns subtree; preparer retains nothing | One main-thread instance preparation |
| 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 |
@@ -419,6 +423,8 @@ sequenceDiagram
strong root/resource references without freeing engine objects.
`WmoRenderGroupMaterializer` owns indexed MeshInstance3D/MultiMeshInstance3D
creation, render settings and attachment without retaining engine objects.
`WmoRuntimeScenePreparer` owns cached-only Mesh traversal/finalization plus the
shared direct-Occluders and recursive shadow policies for cached/live roots.
`WmoRenderResourceCacheState` owns validated render Resources, negative entries
and pending cache paths; `WmoRenderResourceFinalizer` owns its terminal
ResourceLoader polling and script/format validation. `WmoSceneResourceCacheState`
@@ -567,6 +573,9 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
- WMO render group materializer contract: exact Resource identity, indexed and
fallback names/transforms, render settings, attachment, source ownership and
bounded main-thread timing.
- WMO runtime scene preparer contract: cached/live finalizer distinction,
exact Mesh traversal order, direct occluder removal, recursive shadow policy,
ownership boundaries 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.
@@ -628,6 +637,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| 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 | 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 runtime scene preparer | Implemented extraction | Synthetic cached/live traversal/occluder/shadow/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 |
@@ -711,6 +721,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `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/wmo/wmo_runtime_scene_preparer.gd` | Cached/live WMO subtree Mesh traversal and render policy |
| `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 |
@@ -749,6 +760,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `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_wmo_runtime_scene_preparer.gd` | WMO cached/live traversal/occluder/shadow/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,80 @@
class_name WmoRuntimeScenePreparer
extends RefCounted
## Applies the existing cached/live WMO subtree preparation rules on the
## renderer main thread. Placement, attachment and subtree lifetime stay caller-owned.
var _runtime_mesh_finalizer: Object
func _init(runtime_mesh_finalizer: Object) -> void:
_runtime_mesh_finalizer = runtime_mesh_finalizer
## Prepares an instantiated cached WMO scene by finalizing every borrowed Mesh,
## then applying the historical direct Occluders-child and shadow policies.
## Returns false for a null/freed root without mutating or retaining anything.
func prepare_cached_instance(
instance: Node3D,
extracted_directory: String,
enable_occlusion_culling: bool,
cast_shadows: bool) -> bool:
if instance == null or not is_instance_valid(instance):
return false
_finalize_meshes_in_subtree(instance, extracted_directory)
_apply_runtime_render_policy(instance, enable_occlusion_culling, cast_shadows)
return true
## Prepares a duplicated live-built WMO without re-finalizing its Meshes.
## Returns false for a null/freed root without mutating or retaining anything.
func prepare_live_instance(
instance: Node3D,
enable_occlusion_culling: bool,
cast_shadows: bool) -> bool:
if instance == null or not is_instance_valid(instance):
return false
_apply_runtime_render_policy(instance, enable_occlusion_culling, cast_shadows)
return true
func _finalize_meshes_in_subtree(node: Node, extracted_directory: String) -> void:
if _runtime_mesh_finalizer != null:
if node is MeshInstance3D:
_runtime_mesh_finalizer.call(
"finalize_mesh",
(node as MeshInstance3D).mesh,
extracted_directory
)
elif node is MultiMeshInstance3D:
var multimesh := (node as MultiMeshInstance3D).multimesh
if multimesh != null:
_runtime_mesh_finalizer.call(
"finalize_mesh",
multimesh.mesh,
extracted_directory
)
for child in node.get_children():
_finalize_meshes_in_subtree(child, extracted_directory)
func _apply_runtime_render_policy(
instance: Node3D,
enable_occlusion_culling: bool,
cast_shadows: bool) -> void:
if not enable_occlusion_culling:
var occluders := instance.get_node_or_null("Occluders")
if occluders != null:
instance.remove_child(occluders)
occluders.queue_free()
if cast_shadows:
_enable_shadow_casting_recursive(instance)
func _enable_shadow_casting_recursive(node: Node) -> void:
if node is GeometryInstance3D:
(node as GeometryInstance3D).cast_shadow = (
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
)
for child in node.get_children():
_enable_shadow_casting_recursive(child)
@@ -0,0 +1 @@
uid://d1t0vlkco8kw8
+16 -40
View File
@@ -39,6 +39,9 @@ const WMO_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
const WMO_RENDER_GROUP_MATERIALIZER_SCRIPT := preload(
"res://src/render/wmo/wmo_render_group_materializer.gd"
)
const WMO_RUNTIME_SCENE_PREPARER_SCRIPT := preload(
"res://src/render/wmo/wmo_runtime_scene_preparer.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")
@@ -385,6 +388,9 @@ 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_runtime_scene_preparer := WMO_RUNTIME_SCENE_PREPARER_SCRIPT.new(
_wmo_runtime_mesh_finalizer
)
var _wmo_missing_cache: Dictionary = {}
var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new()
var _world_wmo_root: Node3D
@@ -3935,28 +3941,12 @@ func _instantiate_wmo_scene(rel_path: String, scene: PackedScene, placement: Dic
func _prepare_runtime_wmo_instance(instance: Node3D) -> void:
_refresh_cached_wmo_materials_recursive(instance)
if not enable_occlusion_culling:
var occluders := instance.get_node_or_null("Occluders")
if occluders != null:
instance.remove_child(occluders)
occluders.queue_free()
if wmo_cast_shadows:
_apply_shadow_cast_recursive(instance, true)
func _refresh_cached_wmo_materials_recursive(node: Node) -> void:
if node is MeshInstance3D:
_wmo_runtime_mesh_finalizer.finalize_mesh(
(node as MeshInstance3D).mesh,
extracted_dir
_wmo_runtime_scene_preparer.prepare_cached_instance(
instance,
extracted_dir,
enable_occlusion_culling,
wmo_cast_shadows
)
elif node is MultiMeshInstance3D:
var multimesh := (node as MultiMeshInstance3D).multimesh
if multimesh != null:
_wmo_runtime_mesh_finalizer.finalize_mesh(multimesh.mesh, extracted_dir)
for child in node.get_children():
_refresh_cached_wmo_materials_recursive(child)
func _cancel_wmo_build_job(tile_key: String) -> void:
@@ -4466,18 +4456,6 @@ func _apply_visibility_range_recursive(node: Node, range_end: float) -> void:
_apply_visibility_range_recursive(child, range_end)
func _apply_shadow_cast_recursive(node: Node, cast_shadows: bool) -> void:
if node is GeometryInstance3D:
var geometry := node as GeometryInstance3D
geometry.cast_shadow = (
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
if cast_shadows
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
)
for child in node.get_children():
_apply_shadow_cast_recursive(child, cast_shadows)
func _strip_occluders_recursive(node: Node) -> void:
for child in node.get_children():
if child is OccluderInstance3D:
@@ -4659,13 +4637,11 @@ func _instantiate_wmo_world(rel_path: String, placement: Dictionary) -> Node3D:
# WMOs are parented to _terrain_root, whose position absorbs the editor
# offset — use world-space placement.pos directly (no tile_origin subtraction).
instance.transform = _wmo_placement_resolver.resolve_world_transform(placement)
if not enable_occlusion_culling:
var occluders := instance.get_node_or_null("Occluders")
if occluders != null:
instance.remove_child(occluders)
occluders.queue_free()
if wmo_cast_shadows:
_apply_shadow_cast_recursive(instance, true)
_wmo_runtime_scene_preparer.prepare_live_instance(
instance,
enable_occlusion_culling,
wmo_cast_shadows
)
return instance
+10 -3
View File
@@ -7,6 +7,7 @@ const FINALIZER_SCRIPT := preload(
"res://src/render/wmo/wmo_runtime_mesh_finalizer.gd"
)
const FINALIZER_PATH := "res://src/render/wmo/wmo_runtime_mesh_finalizer.gd"
const SCENE_PREPARER_PATH := "res://src/render/wmo/wmo_runtime_scene_preparer.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
@@ -42,7 +43,7 @@ func _initialize() -> void:
quit(1)
return
print(
"WMO_RUNTIME_MESH_FINALIZER PASS cases=27 iterations=1000 elapsed_ms=%.3f"
"WMO_RUNTIME_MESH_FINALIZER PASS cases=28 iterations=1000 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
@@ -198,6 +199,7 @@ func _verify_missing_builder_contract(failures: Array[String]) -> void:
func _verify_source_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures)
var finalizer_source := _read_text(FINALIZER_PATH, failures)
var scene_preparer_source := _read_text(SCENE_PREPARER_PATH, failures)
_expect_true(
loader_source.contains("WMO_RUNTIME_MESH_FINALIZER_SCRIPT.new("),
"loader composes runtime Mesh finalizer",
@@ -229,9 +231,14 @@ func _verify_source_boundaries(failures: Array[String]) -> void:
"finalizer owns %s" % owned_token,
failures
)
_expect_false(
loader_source.contains("func _refresh_cached_wmo_materials_recursive("),
"loader releases scene traversal",
failures
)
_expect_true(
loader_source.contains("func _refresh_cached_wmo_materials_recursive(node: Node)"),
"loader retains scene traversal",
scene_preparer_source.contains("func _finalize_meshes_in_subtree("),
"runtime scene preparer owns scene traversal",
failures
)
@@ -0,0 +1,308 @@
extends SceneTree
## Asset-free cached/live WMO subtree traversal, render-policy, ownership and
## bounded-timing regression for runtime scene preparation.
const PREPARER_SCRIPT := preload(
"res://src/render/wmo/wmo_runtime_scene_preparer.gd"
)
const PREPARER_PATH := "res://src/render/wmo/wmo_runtime_scene_preparer.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
class FakeRuntimeMeshFinalizer extends RefCounted:
var finalized_meshes: Array[Mesh] = []
var extracted_directories: Array[String] = []
func finalize_mesh(mesh: Mesh, extracted_directory: String) -> Mesh:
finalized_meshes.append(mesh)
extracted_directories.append(extracted_directory)
return mesh
func _initialize() -> void:
var failures: Array[String] = []
_verify_cached_instance_contract(failures)
_verify_enabled_occlusion_and_disabled_shadow_contract(failures)
_verify_live_instance_contract(failures)
_verify_missing_finalizer_and_invalid_input(failures)
_verify_source_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("WMO_RUNTIME_SCENE_PREPARER: %s" % failure)
quit(1)
return
print(
"WMO_RUNTIME_SCENE_PREPARER PASS cases=31 iterations=1000 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_cached_instance_contract(failures: Array[String]) -> void:
var finalizer := FakeRuntimeMeshFinalizer.new()
var preparer := PREPARER_SCRIPT.new(finalizer)
var fixture := _create_scene_fixture()
var instance: Node3D = fixture["root"]
var mesh_instance: MeshInstance3D = fixture["mesh_instance"]
var multimesh_instance: MultiMeshInstance3D = fixture["multimesh_instance"]
var direct_occluders: Node3D = fixture["direct_occluders"]
var nested_occluders: Node3D = fixture["nested_occluders"]
get_root().add_child(instance)
var prepared: bool = preparer.prepare_cached_instance(
instance,
"res://fixture/extracted",
false,
true
)
_expect_true(prepared, "cached instance accepted", failures)
_expect_equal(finalizer.finalized_meshes.size(), 2, "two non-null Meshes finalized", failures)
_expect_same(finalizer.finalized_meshes[0], mesh_instance.mesh, "Mesh traversal first", failures)
_expect_same(
finalizer.finalized_meshes[1],
multimesh_instance.multimesh.mesh,
"MultiMesh Mesh traversal second",
failures
)
_expect_true(
finalizer.extracted_directories == [
"res://fixture/extracted",
"res://fixture/extracted",
],
"extracted directory forwarded for every Mesh",
failures
)
_expect_true(instance.get_node_or_null("Occluders") == null, "direct Occluders removed", failures)
_expect_true(direct_occluders.is_queued_for_deletion(), "direct Occluders queued for deletion", failures)
_expect_same(
nested_occluders.get_parent(),
fixture["nested_root"],
"nested same-name node retained",
failures
)
_expect_equal(
mesh_instance.cast_shadow,
GeometryInstance3D.SHADOW_CASTING_SETTING_ON,
"Mesh shadows enabled",
failures
)
_expect_equal(
multimesh_instance.cast_shadow,
GeometryInstance3D.SHADOW_CASTING_SETTING_ON,
"MultiMesh shadows enabled",
failures
)
instance.free()
func _verify_enabled_occlusion_and_disabled_shadow_contract(
failures: Array[String]) -> void:
var finalizer := FakeRuntimeMeshFinalizer.new()
var preparer := PREPARER_SCRIPT.new(finalizer)
var fixture := _create_scene_fixture()
var instance: Node3D = fixture["root"]
var mesh_instance: MeshInstance3D = fixture["mesh_instance"]
var multimesh_instance: MultiMeshInstance3D = fixture["multimesh_instance"]
mesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_DOUBLE_SIDED
multimesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
_expect_true(
preparer.prepare_cached_instance(instance, "res://data/extracted", true, false),
"enabled-occlusion cached instance accepted",
failures
)
_expect_same(
fixture["direct_occluders"].get_parent(),
instance,
"enabled occlusion retains direct child",
failures
)
_expect_equal(
mesh_instance.cast_shadow,
GeometryInstance3D.SHADOW_CASTING_SETTING_DOUBLE_SIDED,
"disabled shadow policy preserves Mesh value",
failures
)
_expect_equal(
multimesh_instance.cast_shadow,
GeometryInstance3D.SHADOW_CASTING_SETTING_OFF,
"disabled shadow policy preserves MultiMesh value",
failures
)
instance.free()
func _verify_live_instance_contract(failures: Array[String]) -> void:
var finalizer := FakeRuntimeMeshFinalizer.new()
var preparer := PREPARER_SCRIPT.new(finalizer)
var fixture := _create_scene_fixture()
var instance: Node3D = fixture["root"]
_expect_true(
preparer.prepare_live_instance(instance, false, true),
"live instance accepted",
failures
)
_expect_equal(finalizer.finalized_meshes.size(), 0, "live path skips Mesh finalizer", failures)
_expect_true(instance.get_node_or_null("Occluders") == null, "live direct Occluders removed", failures)
_expect_equal(
fixture["mesh_instance"].cast_shadow,
GeometryInstance3D.SHADOW_CASTING_SETTING_ON,
"live Mesh shadows enabled",
failures
)
instance.free()
func _verify_missing_finalizer_and_invalid_input(failures: Array[String]) -> void:
var preparer := PREPARER_SCRIPT.new(null)
var fixture := _create_scene_fixture()
var instance: Node3D = fixture["root"]
_expect_true(
preparer.prepare_cached_instance(instance, "res://data/extracted", false, true),
"missing finalizer retains render-policy preparation",
failures
)
_expect_true(instance.get_node_or_null("Occluders") == null, "missing finalizer still applies occluder policy", failures)
_expect_false(
preparer.prepare_cached_instance(null, "res://data/extracted", false, true),
"null cached root rejected",
failures
)
_expect_false(
preparer.prepare_live_instance(null, false, true),
"null live root rejected",
failures
)
instance.free()
func _verify_source_boundaries(failures: Array[String]) -> void:
var preparer_source := FileAccess.get_file_as_string(PREPARER_PATH)
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
_expect_true(
loader_source.contains("WMO_RUNTIME_SCENE_PREPARER_SCRIPT.new("),
"loader composes runtime scene preparer",
failures
)
_expect_equal(
loader_source.count("_wmo_runtime_scene_preparer.prepare_cached_instance("),
1,
"loader delegates cached preparation once",
failures
)
_expect_equal(
loader_source.count("_wmo_runtime_scene_preparer.prepare_live_instance("),
1,
"loader delegates live preparation once",
failures
)
for released_token in [
"func _refresh_cached_wmo_materials_recursive(",
"func _apply_shadow_cast_recursive(",
]:
_expect_false(
loader_source.contains(released_token),
"loader releases %s" % released_token,
failures
)
for owned_token in [
"func _finalize_meshes_in_subtree(",
"instance.get_node_or_null(\"Occluders\")",
"instance.remove_child(occluders)",
"func _enable_shadow_casting_recursive(",
"GeometryInstance3D.SHADOW_CASTING_SETTING_ON",
]:
_expect_true(
preparer_source.contains(owned_token),
"preparer owns %s" % owned_token,
failures
)
for forbidden_dependency in [
"ResourceLoader.",
"FileAccess.",
"WorkerThreadPool.",
"RenderingServer.",
".owner =",
"add_child(",
"duplicate(",
"_wmo_render_build_queue",
"_render_budget_scheduler",
]:
_expect_false(
preparer_source.contains(forbidden_dependency),
"preparer omits %s ownership" % forbidden_dependency,
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var finalizer := FakeRuntimeMeshFinalizer.new()
var preparer := PREPARER_SCRIPT.new(finalizer)
var fixture := _create_scene_fixture(false)
var instance: Node3D = fixture["root"]
var started_microseconds := Time.get_ticks_usec()
for iteration in 1000:
preparer.prepare_cached_instance(instance, "res://data/extracted", true, false)
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_equal(finalizer.finalized_meshes.size(), 2000, "timing traversal complete", failures)
_expect_true(elapsed_milliseconds < 1000.0, "1000 preparations under one second", failures)
instance.free()
return elapsed_milliseconds
func _create_scene_fixture(include_direct_occluders: bool = true) -> Dictionary:
var root := Node3D.new()
root.name = "FixtureWmo"
var mesh_instance := MeshInstance3D.new()
mesh_instance.name = "Exterior"
mesh_instance.mesh = ArrayMesh.new()
mesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
root.add_child(mesh_instance)
var nested_root := Node3D.new()
nested_root.name = "Nested"
root.add_child(nested_root)
var multimesh := MultiMesh.new()
multimesh.transform_format = MultiMesh.TRANSFORM_3D
multimesh.mesh = ArrayMesh.new()
var multimesh_instance := MultiMeshInstance3D.new()
multimesh_instance.name = "Doodads"
multimesh_instance.multimesh = multimesh
multimesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
nested_root.add_child(multimesh_instance)
var null_multimesh_instance := MultiMeshInstance3D.new()
nested_root.add_child(null_multimesh_instance)
var nested_occluders := Node3D.new()
nested_occluders.name = "Occluders"
nested_root.add_child(nested_occluders)
var direct_occluders: Node3D = null
if include_direct_occluders:
direct_occluders = Node3D.new()
direct_occluders.name = "Occluders"
root.add_child(direct_occluders)
return {
"root": root,
"mesh_instance": mesh_instance,
"nested_root": nested_root,
"multimesh_instance": multimesh_instance,
"nested_occluders": nested_occluders,
"direct_occluders": direct_occluders,
}
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_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
if not is_same(actual, expected):
failures.append(label)
@@ -0,0 +1 @@
uid://bigpi11md6vxd