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, 1,000 simple group attachments. This extraction adds no asset-backed GPU,
leak, p95/p99 or original-client visual-fidelity evidence. 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 ## 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. 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 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 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 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) | | Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.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) | | 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 ```mermaid
flowchart LR 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] Finalizer -->|material definition plus compact texture paths| Builder[WMOBuilder material boundary]
Builder -->|rebuilt Material| Finalizer Builder -->|rebuilt Material| Finalizer
Finalizer -->|same Mesh identity| Loader Finalizer -->|same Mesh identity| Loader
Finalizer -->|same Mesh identity| Preparer
``` ```
The loader owns scene traversal and composition. The finalizer owns only the The runtime scene preparer owns cached-scene traversal; the loader owns render
in-place Resource operation and depends on an injected WMO material builder. build-step selection and composition. The finalizer owns only the in-place
Resource operation and depends on an injected WMO material builder.
## Public API ## 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 | | 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 | 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 | 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 ## Data flow
@@ -96,10 +99,15 @@ in the extracted loader behavior. The finalizer retains no Mesh or Material.
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant Loader as StreamingWorldLoader participant Loader as StreamingWorldLoader
participant Preparer as WmoRuntimeScenePreparer
participant Finalizer as WmoRuntimeMeshFinalizer participant Finalizer as WmoRuntimeMeshFinalizer
participant Mesh as ArrayMesh participant Mesh as ArrayMesh
participant Builder as WMOBuilder participant Builder as WMOBuilder
alt lightweight render build step
Loader->>Finalizer: finalize_mesh(mesh, extracted_directory) 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 Finalizer->>Mesh: read/stamp refresh metadata
loop each stale ArrayMesh surface loop each stale ArrayMesh surface
Finalizer->>Mesh: surface_get_material(index) Finalizer->>Mesh: surface_get_material(index)
@@ -109,7 +117,8 @@ sequenceDiagram
Finalizer->>Mesh: surface_set_material when non-null Finalizer->>Mesh: surface_set_material when non-null
end end
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 ## Dependency diagram
@@ -117,6 +126,7 @@ sequenceDiagram
```mermaid ```mermaid
flowchart TB flowchart TB
Loader[StreamingWorldLoader] --> Finalizer[WmoRuntimeMeshFinalizer] Loader[StreamingWorldLoader] --> Finalizer[WmoRuntimeMeshFinalizer]
Preparer[WmoRuntimeScenePreparer] --> Finalizer
Finalizer --> Engine[Mesh / ArrayMesh / Material] Finalizer --> Engine[Mesh / ArrayMesh / Material]
Finalizer --> Builder[Injected WMO material builder] Finalizer --> Builder[Injected WMO material builder]
Finalizer -. no dependency .-> Nodes[Node traversal/lifetime] 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 | | 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 | | 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 ## 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 | | `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 | | `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 | | `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | Identity/version/material/source/timing regression |
## Related decisions and references ## 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 | | 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 | | 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 | | 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 | | Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose ## Purpose
@@ -72,6 +72,8 @@ flowchart LR
WmoBuildQueue --> Loader WmoBuildQueue --> Loader
Loader --> WmoGroupMaterializer[WmoRenderGroupMaterializer] Loader --> WmoGroupMaterializer[WmoRenderGroupMaterializer]
WmoGroupMaterializer --> Scene WmoGroupMaterializer --> Scene
Loader --> WmoScenePreparer[WmoRuntimeScenePreparer]
WmoScenePreparer --> Scene
Native --> Parsed[Parsed tile/model data] Native --> Parsed[Parsed tile/model data]
Parsed --> Loader Parsed --> Loader
Loader --> Scene[SceneTree nodes] 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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 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 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 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 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 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 | | 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. strong root/resource references without freeing engine objects.
`WmoRenderGroupMaterializer` owns indexed MeshInstance3D/MultiMeshInstance3D `WmoRenderGroupMaterializer` owns indexed MeshInstance3D/MultiMeshInstance3D
creation, render settings and attachment without retaining engine objects. 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 `WmoRenderResourceCacheState` owns validated render Resources, negative entries
and pending cache paths; `WmoRenderResourceFinalizer` owns its terminal and pending cache paths; `WmoRenderResourceFinalizer` owns its terminal
ResourceLoader polling and script/format validation. `WmoSceneResourceCacheState` 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 - WMO render group materializer contract: exact Resource identity, indexed and
fallback names/transforms, render settings, attachment, source ownership and fallback names/transforms, render settings, attachment, source ownership and
bounded main-thread timing. 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, - WMO render Resource cache contract: invalid/duplicate request rejection,
validated/missing terminal transitions, transient/full reset, detached sorted validated/missing terminal transitions, transient/full reset, detached sorted
diagnostics, loader-owned version validation and bounded timing. 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 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 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 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 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 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 | | 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_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_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_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_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_policy.gd` | Immutable renderer radius/prefetch policy |
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets | | `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_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_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_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_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_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 | | `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( const WMO_RENDER_GROUP_MATERIALIZER_SCRIPT := preload(
"res://src/render/wmo/wmo_render_group_materializer.gd" "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_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 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") 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 WMO_BUILDER_SCRIPT
) )
var _wmo_render_group_materializer := WMO_RENDER_GROUP_MATERIALIZER_SCRIPT.new() 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_missing_cache: Dictionary = {}
var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new() var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new()
var _world_wmo_root: Node3D 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: func _prepare_runtime_wmo_instance(instance: Node3D) -> void:
_refresh_cached_wmo_materials_recursive(instance) _wmo_runtime_scene_preparer.prepare_cached_instance(
if not enable_occlusion_culling: instance,
var occluders := instance.get_node_or_null("Occluders") extracted_dir,
if occluders != null: enable_occlusion_culling,
instance.remove_child(occluders) wmo_cast_shadows
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
) )
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: 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) _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: func _strip_occluders_recursive(node: Node) -> void:
for child in node.get_children(): for child in node.get_children():
if child is OccluderInstance3D: 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 # WMOs are parented to _terrain_root, whose position absorbs the editor
# offset — use world-space placement.pos directly (no tile_origin subtraction). # offset — use world-space placement.pos directly (no tile_origin subtraction).
instance.transform = _wmo_placement_resolver.resolve_world_transform(placement) instance.transform = _wmo_placement_resolver.resolve_world_transform(placement)
if not enable_occlusion_culling: _wmo_runtime_scene_preparer.prepare_live_instance(
var occluders := instance.get_node_or_null("Occluders") instance,
if occluders != null: enable_occlusion_culling,
instance.remove_child(occluders) wmo_cast_shadows
occluders.queue_free() )
if wmo_cast_shadows:
_apply_shadow_cast_recursive(instance, true)
return instance return instance
+10 -3
View File
@@ -7,6 +7,7 @@ const FINALIZER_SCRIPT := preload(
"res://src/render/wmo/wmo_runtime_mesh_finalizer.gd" "res://src/render/wmo/wmo_runtime_mesh_finalizer.gd"
) )
const FINALIZER_PATH := "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" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
@@ -42,7 +43,7 @@ func _initialize() -> void:
quit(1) quit(1)
return return
print( 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 % elapsed_milliseconds
) )
quit(0) quit(0)
@@ -198,6 +199,7 @@ func _verify_missing_builder_contract(failures: Array[String]) -> void:
func _verify_source_boundaries(failures: Array[String]) -> void: func _verify_source_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures) var loader_source := _read_text(LOADER_PATH, failures)
var finalizer_source := _read_text(FINALIZER_PATH, failures) var finalizer_source := _read_text(FINALIZER_PATH, failures)
var scene_preparer_source := _read_text(SCENE_PREPARER_PATH, failures)
_expect_true( _expect_true(
loader_source.contains("WMO_RUNTIME_MESH_FINALIZER_SCRIPT.new("), loader_source.contains("WMO_RUNTIME_MESH_FINALIZER_SCRIPT.new("),
"loader composes runtime Mesh finalizer", "loader composes runtime Mesh finalizer",
@@ -229,9 +231,14 @@ func _verify_source_boundaries(failures: Array[String]) -> void:
"finalizer owns %s" % owned_token, "finalizer owns %s" % owned_token,
failures failures
) )
_expect_false(
loader_source.contains("func _refresh_cached_wmo_materials_recursive("),
"loader releases scene traversal",
failures
)
_expect_true( _expect_true(
loader_source.contains("func _refresh_cached_wmo_materials_recursive(node: Node)"), scene_preparer_source.contains("func _finalize_meshes_in_subtree("),
"loader retains scene traversal", "runtime scene preparer owns scene traversal",
failures 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