diff --git a/RENDER.md b/RENDER.md index ad4b608..e57fe40 100644 --- a/RENDER.md +++ b/RENDER.md @@ -1478,6 +1478,19 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe' 1,000 simple group attachments. This extraction adds no asset-backed GPU, leak, p95/p99 or original-client visual-fidelity evidence. +## 2026-08-01 WMO Runtime Scene Preparer Extraction + +- `WmoRuntimeScenePreparer` now owns cached WMO parent-before-children Mesh/ + MultiMesh finalization and the shared cached/live render-policy preparation. +- The historical path distinction is unchanged: live-built duplicates do not + cross the cached runtime Mesh finalizer boundary. +- Disabled occlusion still removes only the direct child named `Occluders`; + enabled shadows still set descendant GeometryInstance3D nodes ON, while the + disabled shadow branch preserves existing values. +- Instantiation, placement, attachment, registry lifetime, Editor ownership, + queues and permits remain loader-owned. Synthetic traversal timing is not + private-asset visual, leak, GPU or p95/p99 evidence. + ## Practical Rule For Future Work If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it. diff --git a/docs/modules/README.md b/docs/modules/README.md index 7b09af8..918a793 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -51,6 +51,7 @@ | WMO scene Resource finalizer | Implemented extraction | [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.md) | | WMO runtime Mesh finalizer | Implemented extraction | [`wmo-runtime-mesh-finalizer.md`](wmo-runtime-mesh-finalizer.md) | | WMO render group materializer | Implemented extraction | [`wmo-render-group-materializer.md`](wmo-render-group-materializer.md) | +| WMO runtime scene preparer | Implemented extraction | [`wmo-runtime-scene-preparer.md`](wmo-runtime-scene-preparer.md) | | Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) | | Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) | | Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) | diff --git a/docs/modules/wmo-runtime-mesh-finalizer.md b/docs/modules/wmo-runtime-mesh-finalizer.md index 667b389..54c82ca 100644 --- a/docs/modules/wmo-runtime-mesh-finalizer.md +++ b/docs/modules/wmo-runtime-mesh-finalizer.md @@ -27,14 +27,17 @@ refresh version and reconstruct eligible surface materials through WMOBuilder. ```mermaid flowchart LR - Loader[StreamingWorldLoader traversal/build step] -->|Mesh plus extracted directory| Finalizer[WmoRuntimeMeshFinalizer] + Loader[StreamingWorldLoader render build step] -->|Mesh plus extracted directory| Finalizer[WmoRuntimeMeshFinalizer] + Preparer[WmoRuntimeScenePreparer cached traversal] -->|Mesh plus extracted directory| Finalizer Finalizer -->|material definition plus compact texture paths| Builder[WMOBuilder material boundary] Builder -->|rebuilt Material| Finalizer Finalizer -->|same Mesh identity| Loader + Finalizer -->|same Mesh identity| Preparer ``` -The loader owns scene traversal and composition. The finalizer owns only the -in-place Resource operation and depends on an injected WMO material builder. +The runtime scene preparer owns cached-scene traversal; the loader owns render +build-step selection and composition. The finalizer owns only the in-place +Resource operation and depends on an injected WMO material builder. ## Public API @@ -52,7 +55,7 @@ in-place Resource operation and depends on an injected WMO material builder. | Input | Texture paths, WMO flags/shader/blend and cached colors | Surface Material metadata/parameters | Finalizer | Borrowed values | One surface rebuild | | Output | Compact texture-path array and material definition | Finalizer | WMOBuilder | Detached values | One builder call | | Output | Rebuilt Material | WMOBuilder | ArrayMesh surface | Surface adopts exact Resource | Mesh lifetime | -| Output | Exact input Mesh | Finalizer | Loader traversal/build step | Caller retains ownership | Existing cache/scene lifetime | +| Output | Exact input Mesh | Finalizer | Runtime scene preparer or loader build step | Caller retains ownership | Existing cache/scene lifetime | ## Data flow @@ -96,10 +99,15 @@ in the extracted loader behavior. The finalizer retains no Mesh or Material. ```mermaid sequenceDiagram participant Loader as StreamingWorldLoader + participant Preparer as WmoRuntimeScenePreparer participant Finalizer as WmoRuntimeMeshFinalizer participant Mesh as ArrayMesh participant Builder as WMOBuilder - Loader->>Finalizer: finalize_mesh(mesh, extracted_directory) + alt lightweight render build step + Loader->>Finalizer: finalize_mesh(mesh, extracted_directory) + else cached scene traversal + Preparer->>Finalizer: finalize_mesh(mesh, extracted_directory) + end Finalizer->>Mesh: read/stamp refresh metadata loop each stale ArrayMesh surface Finalizer->>Mesh: surface_get_material(index) @@ -109,7 +117,8 @@ sequenceDiagram Finalizer->>Mesh: surface_set_material when non-null end end - Finalizer-->>Loader: exact Mesh identity + Finalizer-->>Loader: exact Mesh identity for build path + Finalizer-->>Preparer: exact Mesh identity for cached traversal path ``` ## Dependency diagram @@ -117,6 +126,7 @@ sequenceDiagram ```mermaid flowchart TB Loader[StreamingWorldLoader] --> Finalizer[WmoRuntimeMeshFinalizer] + Preparer[WmoRuntimeScenePreparer] --> Finalizer Finalizer --> Engine[Mesh / ArrayMesh / Material] Finalizer --> Builder[Injected WMO material builder] Finalizer -. no dependency .-> Nodes[Node traversal/lifetime] @@ -184,7 +194,8 @@ metrics and loader diagnostics remain the operational correlation surface. |---|---|---|---| | Runtime refresh admission | Implemented extraction | Identity/version/type contracts | Serialized cache fixture pending | | Cached material definition reconstruction | Implemented extraction | Path/metadata/color contracts | Asset-backed visual comparison pending | -| Scene traversal/materialization | Loader-owned | Existing WMO regressions | Further safe extraction pending | +| Scene traversal | Runtime scene preparer-owned | Cached/live traversal regressions | Asset-backed traversal pending | +| Node materialization | Loader/group-materializer-owned | Existing WMO regressions | Further safe extraction pending | ## Known gaps and risks @@ -199,7 +210,8 @@ metrics and loader diagnostics remain the operational correlation surface. |---|---| | `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Refresh admission, surface iteration and material definition reconstruction | | `addons/mpq_extractor/loaders/wmo_builder.gd` | WMO shader/material construction semantics | -| `src/scenes/streaming/streaming_world_loader.gd` | Composition, Node traversal, build jobs and lifetime | +| `src/render/wmo/wmo_runtime_scene_preparer.gd` | Cached scene traversal and finalizer delegation | +| `src/scenes/streaming/streaming_world_loader.gd` | Composition, build jobs, placement and lifetime | | `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | Identity/version/material/source/timing regression | ## Related decisions and references diff --git a/docs/modules/wmo-runtime-scene-preparer.md b/docs/modules/wmo-runtime-scene-preparer.md new file mode 100644 index 0000000..2bb8080 --- /dev/null +++ b/docs/modules/wmo-runtime-scene-preparer.md @@ -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) diff --git a/docs/modules/world-renderer.md b/docs/modules/world-renderer.md index 06f2fbd..5c258ad 100644 --- a/docs/modules/world-renderer.md +++ b/docs/modules/world-renderer.md @@ -7,7 +7,7 @@ | Status | Partial | | Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package | | Owners | Renderer workstream / milestone integrator | -| Last verified | Worktree `work/sindo-main-codex/m03-wmo-render-group-materializer`, 2026-08-01 | +| Last verified | Worktree `work/sindo-main-codex/m03-wmo-runtime-scene-preparer`, 2026-08-01 | | Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete | ## Purpose @@ -72,6 +72,8 @@ flowchart LR WmoBuildQueue --> Loader Loader --> WmoGroupMaterializer[WmoRenderGroupMaterializer] WmoGroupMaterializer --> Scene + Loader --> WmoScenePreparer[WmoRuntimeScenePreparer] + WmoScenePreparer --> Scene Native --> Parsed[Parsed tile/model data] Parsed --> Loader Loader --> Scene[SceneTree nodes] @@ -167,6 +169,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state | `WmoRenderBuildStepPlanner.plan_step` | Internal pure WMO service | Selects one mesh-first lightweight render-group operation and next cursors | Main/any thread; stateless | Raw integer comparisons are preserved without clamping | | `WmoRenderBuildQueue` / `WmoRenderBuildJob` | Internal WMO pending-state service | Owns typed root/resource/cursor jobs and FIFO placement keys | Renderer main thread; map session | Invalid enqueue rejected; duplicate/stale behavior preserved | | `WmoRenderGroupMaterializer.materialize_mesh_group/materialize_multimesh_group` | Internal WMO scene-materialization service | Creates, configures and attaches one indexed lightweight render group | Renderer main thread; stateless after each call | Invalid parent/resource/index returns null without attachment | +| `WmoRuntimeScenePreparer.prepare_cached_instance/prepare_live_instance` | Internal WMO subtree-preparation service | Preserves cached/live Mesh-finalization distinction, direct occluder policy and recursive shadow enabling | Renderer main thread; stateless after each call | Null/freed root returns false | | `WmoRenderResourceCacheState` | Internal WMO cache-state service | Owns validated Resources, negative entries and pending cache paths | Renderer main thread; map/cache session | Invalid/occupied request and unknown completion are rejected | | `WmoRenderResourceFinalizer` | Internal WMO terminal-I/O service | Polls lightweight render requests, validates script/format and publishes Resource/missing outcomes | Renderer main thread; stateless across calls | Non-terminal retained; failed/null/wrong/stale complete missing | | `WmoSceneResourceCacheState` | Internal WMO cache-state service | Owns validated PackedScenes, negative entries and pending `.tscn` paths | Renderer main thread; map/cache session | Direct missing and terminal request transitions remain distinct | @@ -210,6 +213,7 @@ loader configuration remains transitional composition data, not a caller API. | Internal WMO build step | Mesh/MultiMesh counts and job cursors | Loader / `WmoRenderBuildStepPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One group operation | | Internal WMO pending build | Placement key, Node3D root, WMO Resource and cursors | Loader / `WmoRenderBuildQueue` | Loader drain and step planner adapter | Queue-owned job and strong references | Until cancel/clear/replacement | | Internal WMO group materialization | Parent root, exact Mesh/MultiMesh, indexed metadata and render settings | Loader / `WmoRenderGroupMaterializer` | Attached geometry node | Parent owns node and exact Resource reference | One main-thread group operation | +| Internal WMO subtree preparation | Cached/live root, extracted directory and render policies | Loader / `WmoRuntimeScenePreparer` | Borrowed subtree and runtime Mesh finalizer | Loader/placement owns subtree; preparer retains nothing | One main-thread instance preparation | | Internal WMO render cache | Normalized path, cache path and validated Resource | Loader / `WmoRenderResourceCacheState` | Loader lookup, ResourceLoader poll and build queue | State-owned Resource/path references; detached request snapshots | Until transient/full clear | | Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear | | Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset | @@ -419,6 +423,8 @@ sequenceDiagram strong root/resource references without freeing engine objects. `WmoRenderGroupMaterializer` owns indexed MeshInstance3D/MultiMeshInstance3D creation, render settings and attachment without retaining engine objects. + `WmoRuntimeScenePreparer` owns cached-only Mesh traversal/finalization plus the + shared direct-Occluders and recursive shadow policies for cached/live roots. `WmoRenderResourceCacheState` owns validated render Resources, negative entries and pending cache paths; `WmoRenderResourceFinalizer` owns its terminal ResourceLoader polling and script/format validation. `WmoSceneResourceCacheState` @@ -567,6 +573,9 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m - WMO render group materializer contract: exact Resource identity, indexed and fallback names/transforms, render settings, attachment, source ownership and bounded main-thread timing. +- WMO runtime scene preparer contract: cached/live finalizer distinction, + exact Mesh traversal order, direct occluder removal, recursive shadow policy, + ownership boundaries and bounded main-thread timing. - WMO render Resource cache contract: invalid/duplicate request rejection, validated/missing terminal transitions, transient/full reset, detached sorted diagnostics, loader-owned version validation and bounded timing. @@ -628,6 +637,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | WMO render build step planner | Implemented extraction | Scene-free order/cursor/source/timing contract | Asset-backed traversal p95/p99 pending | | WMO render build queue | Implemented extraction | Typed lifecycle/order/ownership/source/timing contract | Asset-backed traversal/leak evidence pending | | WMO render group materializer | Implemented extraction | Synthetic Resource/name/transform/render/attachment/source/timing contract | Asset-backed visual/leak/GPU p95/p99 pending | +| WMO runtime scene preparer | Implemented extraction | Synthetic cached/live traversal/occluder/shadow/source/timing contract | Asset-backed visual/leak/GPU p95/p99 pending | | WMO render Resource cache state | Implemented extraction | Scene-free lifecycle/exclusivity/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending | | WMO render Resource finalizer | Implemented extraction | Status/order/script/format/adoption/source/timing contract | Serialized/asset-backed corrupt-cache and leak evidence pending | | WMO scene Resource cache state | Implemented extraction | Scene-free lifecycle/direct-missing/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending | @@ -711,6 +721,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | `src/render/wmo/wmo_scene_resource_finalizer.gd` | Cached WMO terminal polling, probe validation/lifetime and publication | | `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Cached WMO runtime refresh admission, surface iteration and material reconstruction | | `src/render/wmo/wmo_render_group_materializer.gd` | Indexed lightweight WMO geometry-node creation, render setup and attachment | +| `src/render/wmo/wmo_runtime_scene_preparer.gd` | Cached/live WMO subtree Mesh traversal and render policy | | `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation | | `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy | | `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets | @@ -749,6 +760,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | `src/tools/verify_wmo_scene_resource_finalizer.gd` | WMO scene status/order/probe/lifetime/adoption/boundary/timing regression | | `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | WMO Mesh identity/version/material-definition/boundary/timing regression | | `src/tools/verify_wmo_render_group_materializer.gd` | WMO render-group Resource/name/transform/render/attachment/boundary/timing regression | +| `src/tools/verify_wmo_runtime_scene_preparer.gd` | WMO cached/live traversal/occluder/shadow/boundary/timing regression | | `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression | | `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression | | `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields | diff --git a/src/render/wmo/wmo_runtime_scene_preparer.gd b/src/render/wmo/wmo_runtime_scene_preparer.gd new file mode 100644 index 0000000..015104f --- /dev/null +++ b/src/render/wmo/wmo_runtime_scene_preparer.gd @@ -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) diff --git a/src/render/wmo/wmo_runtime_scene_preparer.gd.uid b/src/render/wmo/wmo_runtime_scene_preparer.gd.uid new file mode 100644 index 0000000..397b52d --- /dev/null +++ b/src/render/wmo/wmo_runtime_scene_preparer.gd.uid @@ -0,0 +1 @@ +uid://d1t0vlkco8kw8 diff --git a/src/scenes/streaming/streaming_world_loader.gd b/src/scenes/streaming/streaming_world_loader.gd index fea2a65..ecadaa0 100644 --- a/src/scenes/streaming/streaming_world_loader.gd +++ b/src/scenes/streaming/streaming_world_loader.gd @@ -39,6 +39,9 @@ const WMO_RUNTIME_MESH_FINALIZER_SCRIPT := preload( const WMO_RENDER_GROUP_MATERIALIZER_SCRIPT := preload( "res://src/render/wmo/wmo_render_group_materializer.gd" ) +const WMO_RUNTIME_SCENE_PREPARER_SCRIPT := preload( + "res://src/render/wmo/wmo_runtime_scene_preparer.gd" +) const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd") const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd") const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd") @@ -385,6 +388,9 @@ var _wmo_runtime_mesh_finalizer := WMO_RUNTIME_MESH_FINALIZER_SCRIPT.new( WMO_BUILDER_SCRIPT ) var _wmo_render_group_materializer := WMO_RENDER_GROUP_MATERIALIZER_SCRIPT.new() +var _wmo_runtime_scene_preparer := WMO_RUNTIME_SCENE_PREPARER_SCRIPT.new( + _wmo_runtime_mesh_finalizer +) var _wmo_missing_cache: Dictionary = {} var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new() var _world_wmo_root: Node3D @@ -3935,28 +3941,12 @@ func _instantiate_wmo_scene(rel_path: String, scene: PackedScene, placement: Dic func _prepare_runtime_wmo_instance(instance: Node3D) -> void: - _refresh_cached_wmo_materials_recursive(instance) - if not enable_occlusion_culling: - var occluders := instance.get_node_or_null("Occluders") - if occluders != null: - instance.remove_child(occluders) - occluders.queue_free() - if wmo_cast_shadows: - _apply_shadow_cast_recursive(instance, true) - - -func _refresh_cached_wmo_materials_recursive(node: Node) -> void: - if node is MeshInstance3D: - _wmo_runtime_mesh_finalizer.finalize_mesh( - (node as MeshInstance3D).mesh, - extracted_dir - ) - 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) + _wmo_runtime_scene_preparer.prepare_cached_instance( + instance, + extracted_dir, + enable_occlusion_culling, + wmo_cast_shadows + ) func _cancel_wmo_build_job(tile_key: String) -> void: @@ -4466,18 +4456,6 @@ func _apply_visibility_range_recursive(node: Node, range_end: float) -> void: _apply_visibility_range_recursive(child, range_end) -func _apply_shadow_cast_recursive(node: Node, cast_shadows: bool) -> void: - if node is GeometryInstance3D: - var geometry := node as GeometryInstance3D - geometry.cast_shadow = ( - GeometryInstance3D.SHADOW_CASTING_SETTING_ON - if cast_shadows - else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF - ) - for child in node.get_children(): - _apply_shadow_cast_recursive(child, cast_shadows) - - func _strip_occluders_recursive(node: Node) -> void: for child in node.get_children(): if child is OccluderInstance3D: @@ -4659,13 +4637,11 @@ func _instantiate_wmo_world(rel_path: String, placement: Dictionary) -> Node3D: # WMOs are parented to _terrain_root, whose position absorbs the editor # offset — use world-space placement.pos directly (no tile_origin subtraction). instance.transform = _wmo_placement_resolver.resolve_world_transform(placement) - if not enable_occlusion_culling: - var occluders := instance.get_node_or_null("Occluders") - if occluders != null: - instance.remove_child(occluders) - occluders.queue_free() - if wmo_cast_shadows: - _apply_shadow_cast_recursive(instance, true) + _wmo_runtime_scene_preparer.prepare_live_instance( + instance, + enable_occlusion_culling, + wmo_cast_shadows + ) return instance diff --git a/src/tools/verify_wmo_runtime_mesh_finalizer.gd b/src/tools/verify_wmo_runtime_mesh_finalizer.gd index a5a2506..3b84965 100644 --- a/src/tools/verify_wmo_runtime_mesh_finalizer.gd +++ b/src/tools/verify_wmo_runtime_mesh_finalizer.gd @@ -7,6 +7,7 @@ const FINALIZER_SCRIPT := preload( "res://src/render/wmo/wmo_runtime_mesh_finalizer.gd" ) const FINALIZER_PATH := "res://src/render/wmo/wmo_runtime_mesh_finalizer.gd" +const SCENE_PREPARER_PATH := "res://src/render/wmo/wmo_runtime_scene_preparer.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" @@ -42,7 +43,7 @@ func _initialize() -> void: quit(1) return print( - "WMO_RUNTIME_MESH_FINALIZER PASS cases=27 iterations=1000 elapsed_ms=%.3f" + "WMO_RUNTIME_MESH_FINALIZER PASS cases=28 iterations=1000 elapsed_ms=%.3f" % elapsed_milliseconds ) quit(0) @@ -198,6 +199,7 @@ func _verify_missing_builder_contract(failures: Array[String]) -> void: func _verify_source_boundaries(failures: Array[String]) -> void: var loader_source := _read_text(LOADER_PATH, failures) var finalizer_source := _read_text(FINALIZER_PATH, failures) + var scene_preparer_source := _read_text(SCENE_PREPARER_PATH, failures) _expect_true( loader_source.contains("WMO_RUNTIME_MESH_FINALIZER_SCRIPT.new("), "loader composes runtime Mesh finalizer", @@ -229,9 +231,14 @@ func _verify_source_boundaries(failures: Array[String]) -> void: "finalizer owns %s" % owned_token, failures ) + _expect_false( + loader_source.contains("func _refresh_cached_wmo_materials_recursive("), + "loader releases scene traversal", + failures + ) _expect_true( - loader_source.contains("func _refresh_cached_wmo_materials_recursive(node: Node)"), - "loader retains scene traversal", + scene_preparer_source.contains("func _finalize_meshes_in_subtree("), + "runtime scene preparer owns scene traversal", failures ) diff --git a/src/tools/verify_wmo_runtime_scene_preparer.gd b/src/tools/verify_wmo_runtime_scene_preparer.gd new file mode 100644 index 0000000..65ed7ea --- /dev/null +++ b/src/tools/verify_wmo_runtime_scene_preparer.gd @@ -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) diff --git a/src/tools/verify_wmo_runtime_scene_preparer.gd.uid b/src/tools/verify_wmo_runtime_scene_preparer.gd.uid new file mode 100644 index 0000000..a8a65d9 --- /dev/null +++ b/src/tools/verify_wmo_runtime_scene_preparer.gd.uid @@ -0,0 +1 @@ +uid://bigpi11md6vxd