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:
@@ -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.
|
||||||
|
|||||||
@@ -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) |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user