merge: WMO render group materializer
Work-Package: M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001 Agent: sindo-main-codex Tests: 63/64 autonomous headless verifiers; proprietary ADT probe unavailable; checkpoint dry-run 7/7 Fidelity: behavior-preserving WMO group materialization extraction
This commit is contained in:
@@ -1465,6 +1465,19 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
||||
cases and 1,000 current-Mesh calls. This is orchestration extraction, not new
|
||||
build-12340 material or visual parity evidence.
|
||||
|
||||
## 2026-08-01 WMO Render Group Materializer Extraction
|
||||
|
||||
- `WmoRenderGroupMaterializer` now owns creation and single attachment of the
|
||||
lightweight cached WMO `MeshInstance3D` and `MultiMeshInstance3D` groups.
|
||||
- Indexed names/transforms, `Group_N`/`DoodadGroup_N` fallbacks, exact
|
||||
Mesh/MultiMesh identity, shadow mode and positive visibility range/margin are
|
||||
unchanged.
|
||||
- `StreamingWorldLoader` retains build-step selection, runtime Mesh finalization,
|
||||
queue cursors, permits, optional Editor ownership and placement lifetime.
|
||||
- Asset-free verification covers `37` presentation/ownership/source cases and
|
||||
1,000 simple group attachments. This extraction adds no asset-backed GPU,
|
||||
leak, p95/p99 or original-client visual-fidelity evidence.
|
||||
|
||||
## Practical Rule For Future Work
|
||||
|
||||
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001:sindo-main-codex:2026-08-03 -->
|
||||
|
||||
## Owner
|
||||
|
||||
- Agent ID: `sindo-main-codex`
|
||||
- Target: M03 Renderer Facade and Safe Extraction
|
||||
- Branch: `work/sindo-main-codex/m03-wmo-render-group-materializer`
|
||||
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-wmo-render-group-materializer`
|
||||
|
||||
## Outcome
|
||||
|
||||
Move one-step WMO MeshInstance3D and MultiMeshInstance3D creation, render
|
||||
settings and attachment from the loader into one main-thread materializer.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Change WMO placement, resource loading, build-step planning or queue state.
|
||||
- Change runtime Mesh finalization, WMOBuilder material behavior or cache formats.
|
||||
- Change permits, visibility distances, shadow policy or visible output.
|
||||
- Generalize M2 and WMO materialization behind a shared abstraction.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/wmo/wmo_render_group_materializer.gd`,
|
||||
`src/tools/verify_wmo_render_group_materializer.gd`,
|
||||
`docs/modules/wmo-render-group-materializer.md`, this claim
|
||||
- Shared: loader, renderer module registry/specification, `RENDER.md`, M03 Evidence
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Mesh and MultiMesh resources retain exact identity.
|
||||
- Names and optional transforms preserve existing indexed fallback behavior.
|
||||
- Shadow and positive visibility-range settings are applied unchanged.
|
||||
- The supplied WMO root becomes the sole SceneTree owner of the created node.
|
||||
- Editor persisted ownership remains an explicit loader composition policy.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: accepted WMO render build queue/planner and runtime Mesh finalizer
|
||||
- Blocks: remaining loader-owned WMO runtime traversal cleanup
|
||||
|
||||
## Verification
|
||||
|
||||
- Synthetic Mesh/MultiMesh identity, naming, transform, rendering, attachment,
|
||||
invalid-input, ownership, source and bounded-timing contracts; adjacent WMO and
|
||||
renderer gates; full autonomous suite and checkpoint dry-run.
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline API docs; module API/I/O/ownership; data-flow/sequence/dependency
|
||||
diagrams; adjacent renderer docs and M03 Evidence.
|
||||
|
||||
## Status
|
||||
|
||||
- State: ready
|
||||
- Done: implementation, verification and documentation
|
||||
- Next: integrator review and merge
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `f4d5e29` (`render: extract WMO render group materializer`)
|
||||
- Results: materializer PASS `cases=37 iterations=1000 elapsed_ms=2.771`;
|
||||
autonomous headless suite `63/64`, with only the proprietary
|
||||
`verify_adt_m2_placements.gd` probe unavailable because `data/extracted` is
|
||||
absent; cold/editor parse completed without script diagnostics after restoring
|
||||
ignored generated/native worktree artifacts; checkpoint dry-run retained
|
||||
`7/7`; documentation passed with `50` module specifications; coordination
|
||||
passed with `77` historical expired-claim warnings.
|
||||
- Fidelity: exact Mesh/MultiMesh identity, indexed/fallback presentation,
|
||||
render settings, attachment and scheduler/queue boundaries are unchanged.
|
||||
No private-asset or original-client visual parity claim is added.
|
||||
- Remaining risks: synchronous main-thread Node creation remains; no private WMO
|
||||
visual comparison, long traversal, leak/GPU or p95/p99 evidence.
|
||||
- Documentation: new full module specification with API/I/O and data-flow,
|
||||
sequence/dependency diagrams; renderer registry/source map and `RENDER.md`
|
||||
updated.
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001:f4d5e29 -->
|
||||
@@ -50,6 +50,7 @@
|
||||
| WMO scene Resource cache state | Implemented extraction | [`wmo-scene-resource-cache-state.md`](wmo-scene-resource-cache-state.md) |
|
||||
| WMO scene Resource finalizer | Implemented extraction | [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.md) |
|
||||
| WMO runtime Mesh finalizer | Implemented extraction | [`wmo-runtime-mesh-finalizer.md`](wmo-runtime-mesh-finalizer.md) |
|
||||
| WMO render group materializer | Implemented extraction | [`wmo-render-group-materializer.md`](wmo-render-group-materializer.md) |
|
||||
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
|
||||
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
|
||||
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
# WMO Render Group Materializer
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented |
|
||||
| Target | M03 Renderer Facade and Safe Extraction |
|
||||
| Work package | `M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001` |
|
||||
| Owner | Render |
|
||||
| Last verified | 2026-08-01 |
|
||||
|
||||
## Purpose
|
||||
|
||||
`WmoRenderGroupMaterializer` creates and attaches one lightweight cached WMO
|
||||
render group on the renderer main thread. It owns the duplicated
|
||||
`MeshInstance3D`/`MultiMeshInstance3D` presentation rules that previously lived
|
||||
inside `StreamingWorldLoader`.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- select a build step, advance/cancel a job or consume a render permit;
|
||||
- load, cache, validate or finalize WMO Resources;
|
||||
- resolve placement transforms or own the placement root;
|
||||
- choose Editor persistence policy or serialize generated nodes;
|
||||
- change WMO cache formats, materials, visibility or shadow policy.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
The loader obtains a queue-owned WMO root and render Resource, asks
|
||||
`WmoRenderBuildStepPlanner` for one operation, and finalizes the selected Mesh.
|
||||
The materializer then performs only indexed node presentation and attachment.
|
||||
The loader retains scheduler, queue and Editor composition responsibilities.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Queue[WmoRenderBuildQueue] --> Loader[StreamingWorldLoader]
|
||||
Planner[WmoRenderBuildStepPlanner] --> Loader
|
||||
Loader --> Finalizer[WmoRuntimeMeshFinalizer]
|
||||
Loader --> Materializer[WmoRenderGroupMaterializer]
|
||||
Materializer --> MeshNode[MeshInstance3D]
|
||||
Materializer --> MultiMeshNode[MultiMeshInstance3D]
|
||||
MeshNode --> Root[Queue-owned WMO Node3D root]
|
||||
MultiMeshNode --> Root
|
||||
Loader --> EditorOwner[Optional Editor owner assignment]
|
||||
```
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Role | Thread/lifetime | Failure behavior |
|
||||
|---|---|---|---|
|
||||
| `materialize_mesh_group(...)` | Create, configure and attach one indexed Mesh group | Renderer main thread; stateless after return | Null/invalid parent, null Mesh or negative index returns null |
|
||||
| `materialize_multimesh_group(...)` | Create, configure and attach one indexed MultiMesh doodad group | Renderer main thread; stateless after return | Null/invalid parent, null MultiMesh or negative index returns null |
|
||||
|
||||
Both methods preserve exact Resource identity. `group_index` selects the optional
|
||||
name and transform; missing names use the historical indexed fallback and a
|
||||
missing transform leaves `Transform3D.IDENTITY`. Positive visibility range
|
||||
applies its caller-supplied margin. Shadow mode is always applied explicitly.
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Queue-owned WMO `Node3D` root | `WmoRenderBuildQueue` via loader | Materializer | Borrowed; not retained | One main-thread call |
|
||||
| Input | Selected `Mesh` or `MultiMesh` | WMO render Resource via loader | Materializer | Borrowed exact Resource | Parent-node lifetime after attach |
|
||||
| Input | Names, transforms and selected index | WMO render Resource/planner via loader | Materializer | Borrowed value collections | One call |
|
||||
| Input | Visibility end/margin and shadow flag | Loader quality profile | Materializer | Scalar values | One call |
|
||||
| Output | Attached `MeshInstance3D` or `MultiMeshInstance3D` | Materializer | Loader/SceneTree | Parent root owns node and Resource reference | Until placement release/world teardown |
|
||||
|
||||
Side effects:
|
||||
|
||||
- allocates exactly one Godot geometry node for valid input;
|
||||
- applies name, optional transform, shadow and optional visibility settings;
|
||||
- attaches the node exactly once to the supplied WMO root.
|
||||
|
||||
It performs no filesystem, ResourceLoader, worker, RenderingServer RID, cache,
|
||||
queue, permit, logging or direct Editor-owner mutation.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Input[Root + Resource + indexed metadata + render settings] --> Validate{Valid root/resource/index?}
|
||||
Validate -->|no| Null[Return null; no attachment]
|
||||
Validate -->|yes| Kind{Mesh or MultiMesh method}
|
||||
Kind --> Mesh[Create MeshInstance3D]
|
||||
Kind --> Multi[Create MultiMeshInstance3D]
|
||||
Mesh --> Configure[Name + optional transform + render settings]
|
||||
Multi --> Configure
|
||||
Configure --> Attach[Attach once to WMO root]
|
||||
Attach --> Return[Return borrowed attached node]
|
||||
```
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant Queue as WmoRenderBuildQueue
|
||||
participant Finalizer as WmoRuntimeMeshFinalizer
|
||||
participant Materializer as WmoRenderGroupMaterializer
|
||||
participant Root as WMO Node3D root
|
||||
Loader->>Queue: read front job and cursors
|
||||
Loader->>Finalizer: finalize selected Mesh
|
||||
Loader->>Materializer: materialize selected group
|
||||
Materializer->>Root: add_child(geometry instance)
|
||||
Materializer-->>Loader: attached node or null
|
||||
Loader->>Loader: optional Editor ownership
|
||||
Loader->>Queue: adopt planned cursors
|
||||
Loader->>Loader: consume one group permit
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> Materializer[WmoRenderGroupMaterializer]
|
||||
Materializer --> Engine[Node3D / GeometryInstance3D / Mesh / MultiMesh]
|
||||
Materializer -. no dependency .-> IO[ResourceLoader / FileAccess]
|
||||
Materializer -. no dependency .-> Queue[WmoRenderBuildQueue]
|
||||
Materializer -. no dependency .-> Scheduler[RenderBudgetScheduler]
|
||||
Materializer -. no dependency .-> Finalizer[WmoRuntimeMeshFinalizer]
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- Both public methods are main-thread only because they mutate the SceneTree.
|
||||
- The caller owns the parent root; after attachment the root owns the new node.
|
||||
- The node retains the exact input Mesh or MultiMesh Resource reference.
|
||||
- The service retains no Node, Resource, RID, collection or per-group state.
|
||||
- Loader-owned optional recursive Editor ownership runs after successful return.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure/state | Detection | Behavior | Recovery |
|
||||
|---|---|---|---|
|
||||
| Null/freed parent | Guard | Return null; allocate/attach nothing | Caller validates current queue job |
|
||||
| Null Resource | Guard | Return null | Loader advances the historically selected cursor |
|
||||
| Negative index | Guard | Return null | Planner supplies non-negative selected indices |
|
||||
| Missing name | Bounds check | Use `Group_N` or `DoodadGroup_N` | Rebuild cache metadata if desired |
|
||||
| Missing transform | Bounds check | Retain identity transform | Rebuild cache metadata if desired |
|
||||
| Placement cancellation | Loader/queue | Parent and children released by existing lifecycle | Re-request placement later |
|
||||
| Shutdown | Loader lifecycle | Service has no retained state to drain | New loader composes a new service |
|
||||
|
||||
## Configuration, capabilities and profiles
|
||||
|
||||
The service introduces no configuration or capability. It accepts the existing
|
||||
`wmo_visibility_range`, `CHUNK_SIZE` margin and `wmo_cast_shadows` values chosen
|
||||
by the loader quality profile. Blizzlike and Enhanced selection remains outside.
|
||||
|
||||
## Persistence, cache and migrations
|
||||
|
||||
No persisted data or cache format changes. The service neither reads nor writes
|
||||
WMO cache files and requires no migration or rebake.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
The service emits no logs or metrics. Existing `wmo_groups` queue depth, build
|
||||
permits and loader lifecycle diagnostics remain authoritative.
|
||||
|
||||
## Verification and fidelity evidence
|
||||
|
||||
- `verify_wmo_render_group_materializer.gd` covers exact Mesh/MultiMesh identity,
|
||||
indexed and fallback names, optional transforms, shadows, positive/disabled
|
||||
visibility, attachment, invalid input, source boundaries and 1,000 groups.
|
||||
- Adjacent WMO queue/planner/finalizer and checkpoint regressions protect the
|
||||
unchanged orchestration and visible output.
|
||||
- This is an exact code-motion extraction of existing Godot presentation rules;
|
||||
it adds no original-client 3.3.5a visual parity claim.
|
||||
|
||||
## Performance budgets
|
||||
|
||||
The synthetic contract requires 1,000 simple Mesh group materializations in
|
||||
under one second. Production work remains limited to one group per scheduler
|
||||
permit. Asset-backed CPU/GPU p95/p99 and long traversal remain required evidence.
|
||||
|
||||
## Extension points
|
||||
|
||||
- Asset-backed WMO traversal can measure group attachment and lifetime without
|
||||
changing this API.
|
||||
- Additional render settings belong here only when they apply equally to both
|
||||
lightweight group-node kinds and have fidelity evidence.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Mesh group materialization | Implemented extraction | Identity/name/transform/render/attachment contract | Asset-backed visual/GPU p95/p99 pending |
|
||||
| MultiMesh doodad group materialization | Implemented extraction | Identity/name/transform/render/attachment contract | Asset-backed traversal/leak evidence pending |
|
||||
| Build planning/queue progress | Loader-owned | Existing planner/queue regressions | Further orchestration extraction pending |
|
||||
| Editor persistence ownership | Loader-owned | Source-boundary contract | Editor scene-save integration evidence pending |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- SceneTree node creation remains synchronous main-thread work by design.
|
||||
- The synthetic fixture does not measure private WMO assets, GPU upload, portal
|
||||
visibility, original-client visuals, long traversal or leak behavior.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/wmo/wmo_render_group_materializer.gd` | Indexed geometry-node creation, settings and attachment |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, finalization, queue, permits, Editor ownership and lifecycle |
|
||||
| `src/tools/verify_wmo_render_group_materializer.gd` | Synthetic contract, ownership boundary and timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`wmo-render-build-step-planner.md`](wmo-render-build-step-planner.md)
|
||||
- [`wmo-render-build-queue.md`](wmo-render-build-queue.md)
|
||||
- [`wmo-runtime-mesh-finalizer.md`](wmo-runtime-mesh-finalizer.md)
|
||||
- [`world-renderer.md`](world-renderer.md)
|
||||
- [`../../RENDER.md`](../../RENDER.md)
|
||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||
@@ -7,7 +7,7 @@
|
||||
| Status | Partial |
|
||||
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
|
||||
| Owners | Renderer workstream / milestone integrator |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animation-playback`, 2026-07-18 |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-render-group-materializer`, 2026-08-01 |
|
||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||
|
||||
## Purpose
|
||||
@@ -70,6 +70,8 @@ flowchart LR
|
||||
WmoBuildStep --> Loader
|
||||
Loader --> WmoBuildQueue[WmoRenderBuildQueue]
|
||||
WmoBuildQueue --> Loader
|
||||
Loader --> WmoGroupMaterializer[WmoRenderGroupMaterializer]
|
||||
WmoGroupMaterializer --> Scene
|
||||
Native --> Parsed[Parsed tile/model data]
|
||||
Parsed --> Loader
|
||||
Loader --> Scene[SceneTree nodes]
|
||||
@@ -164,6 +166,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
||||
| `WmoPlacementRegistry.add_reference/release_reference/contains/active_count/diagnostic_snapshot/clear` | Internal WMO service | Owns placement-key to tile/global reference sets | Renderer main thread; map session | Empty/unknown/non-owner input is rejected without mutation |
|
||||
| `WmoRenderBuildStepPlanner.plan_step` | Internal pure WMO service | Selects one mesh-first lightweight render-group operation and next cursors | Main/any thread; stateless | Raw integer comparisons are preserved without clamping |
|
||||
| `WmoRenderBuildQueue` / `WmoRenderBuildJob` | Internal WMO pending-state service | Owns typed root/resource/cursor jobs and FIFO placement keys | Renderer main thread; map session | Invalid enqueue rejected; duplicate/stale behavior preserved |
|
||||
| `WmoRenderGroupMaterializer.materialize_mesh_group/materialize_multimesh_group` | Internal WMO scene-materialization service | Creates, configures and attaches one indexed lightweight render group | Renderer main thread; stateless after each call | Invalid parent/resource/index returns null without attachment |
|
||||
| `WmoRenderResourceCacheState` | Internal WMO cache-state service | Owns validated Resources, negative entries and pending cache paths | Renderer main thread; map/cache session | Invalid/occupied request and unknown completion are rejected |
|
||||
| `WmoRenderResourceFinalizer` | Internal WMO terminal-I/O service | Polls lightweight render requests, validates script/format and publishes Resource/missing outcomes | Renderer main thread; stateless across calls | Non-terminal retained; failed/null/wrong/stale complete missing |
|
||||
| `WmoSceneResourceCacheState` | Internal WMO cache-state service | Owns validated PackedScenes, negative entries and pending `.tscn` paths | Renderer main thread; map/cache session | Direct missing and terminal request transitions remain distinct |
|
||||
@@ -206,6 +209,7 @@ loader configuration remains transitional composition data, not a caller API.
|
||||
| Internal WMO ownership | Resolved placement key and tile/global reference key | Loader / `WmoPlacementRegistry` | Loader create/retain/final-free decisions | Registry-owned String sets; detached diagnostics | Map session or final release |
|
||||
| Internal WMO build step | Mesh/MultiMesh counts and job cursors | Loader / `WmoRenderBuildStepPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One group operation |
|
||||
| Internal WMO pending build | Placement key, Node3D root, WMO Resource and cursors | Loader / `WmoRenderBuildQueue` | Loader drain and step planner adapter | Queue-owned job and strong references | Until cancel/clear/replacement |
|
||||
| Internal WMO group materialization | Parent root, exact Mesh/MultiMesh, indexed metadata and render settings | Loader / `WmoRenderGroupMaterializer` | Attached geometry node | Parent owns node and exact Resource reference | One main-thread group operation |
|
||||
| Internal WMO render cache | Normalized path, cache path and validated Resource | Loader / `WmoRenderResourceCacheState` | Loader lookup, ResourceLoader poll and build queue | State-owned Resource/path references; detached request snapshots | Until transient/full clear |
|
||||
| Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear |
|
||||
| Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset |
|
||||
@@ -413,14 +417,16 @@ sequenceDiagram
|
||||
reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation
|
||||
and cursor plan. `WmoRenderBuildQueue` owns typed pending jobs, FIFO keys and
|
||||
strong root/resource references without freeing engine objects.
|
||||
`WmoRenderGroupMaterializer` owns indexed MeshInstance3D/MultiMeshInstance3D
|
||||
creation, render settings and attachment without retaining engine objects.
|
||||
`WmoRenderResourceCacheState` owns validated render Resources, negative entries
|
||||
and pending cache paths; `WmoRenderResourceFinalizer` owns its terminal
|
||||
ResourceLoader polling and script/format validation. `WmoSceneResourceCacheState`
|
||||
similarly owns validated PackedScenes, negative entries and pending `.tscn`
|
||||
paths; `WmoSceneResourceFinalizer` owns terminal ResourceLoader I/O and
|
||||
validation-probe lifetime. The loader retains request admission, FileAccess
|
||||
size checks, live fallback, materialization, permits, validity reactions and
|
||||
every placed-Node lifecycle action.
|
||||
size checks, live fallback, Mesh finalization, permits, Editor ownership,
|
||||
validity reactions and every placed-Node lifecycle action.
|
||||
- `AdtWaterLoadPipelineState` owns pending request order/deduplication, opaque
|
||||
active task IDs and the worker-safe parsed-result mailbox. The loader retains
|
||||
WorkerThreadPool start/wait, ADTLoader parsing, concurrency/finalize permits,
|
||||
@@ -558,6 +564,9 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
transition, completion/raw integer behavior, source ownership and bounded timing.
|
||||
- WMO render build queue contract: typed references/cursors, FIFO, duplicate
|
||||
replacement, stale-front cleanup, cancel/clear engine lifetime and bounded timing.
|
||||
- WMO render group materializer contract: exact Resource identity, indexed and
|
||||
fallback names/transforms, render settings, attachment, source ownership and
|
||||
bounded main-thread timing.
|
||||
- WMO render Resource cache contract: invalid/duplicate request rejection,
|
||||
validated/missing terminal transitions, transient/full reset, detached sorted
|
||||
diagnostics, loader-owned version validation and bounded timing.
|
||||
@@ -617,7 +626,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| WMO placement resolver | Implemented extraction | Scene-free path/identity/transform/source/timing contract | Asset-backed comparison pending |
|
||||
| WMO placement registry | Implemented extraction | Scene-free ownership/lifecycle/source/timing contract | Build/resource state and asset-backed cross-tile corpus pending |
|
||||
| WMO render build step planner | Implemented extraction | Scene-free order/cursor/source/timing contract | Asset-backed traversal p95/p99 pending |
|
||||
| WMO render build queue | Implemented extraction | Typed lifecycle/order/ownership/source/timing contract | Materialization and asset-backed traversal/leak evidence pending |
|
||||
| WMO render build queue | Implemented extraction | Typed lifecycle/order/ownership/source/timing contract | Asset-backed traversal/leak evidence pending |
|
||||
| WMO render group materializer | Implemented extraction | Synthetic Resource/name/transform/render/attachment/source/timing contract | Asset-backed visual/leak/GPU p95/p99 pending |
|
||||
| WMO render Resource cache state | Implemented extraction | Scene-free lifecycle/exclusivity/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending |
|
||||
| WMO render Resource finalizer | Implemented extraction | Status/order/script/format/adoption/source/timing contract | Serialized/asset-backed corrupt-cache and leak evidence pending |
|
||||
| WMO scene Resource cache state | Implemented extraction | Scene-free lifecycle/direct-missing/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending |
|
||||
@@ -700,6 +710,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| `src/render/wmo/wmo_render_resource_finalizer.gd` | Lightweight WMO terminal polling, validation and publication |
|
||||
| `src/render/wmo/wmo_scene_resource_finalizer.gd` | Cached WMO terminal polling, probe validation/lifetime and publication |
|
||||
| `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Cached WMO runtime refresh admission, surface iteration and material reconstruction |
|
||||
| `src/render/wmo/wmo_render_group_materializer.gd` | Indexed lightweight WMO geometry-node creation, render setup and attachment |
|
||||
| `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
|
||||
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
|
||||
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets |
|
||||
@@ -737,6 +748,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| `src/tools/verify_wmo_render_resource_finalizer.gd` | WMO render status/order/validation/adoption/boundary/timing regression |
|
||||
| `src/tools/verify_wmo_scene_resource_finalizer.gd` | WMO scene status/order/probe/lifetime/adoption/boundary/timing regression |
|
||||
| `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | WMO Mesh identity/version/material-definition/boundary/timing regression |
|
||||
| `src/tools/verify_wmo_render_group_materializer.gd` | WMO render-group Resource/name/transform/render/attachment/boundary/timing regression |
|
||||
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
|
||||
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
|
||||
| `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
class_name WmoRenderGroupMaterializer
|
||||
extends RefCounted
|
||||
|
||||
## Creates and attaches one lightweight cached WMO render group. Resource
|
||||
## finalization, build-step selection and queue progress remain caller-owned.
|
||||
## All methods mutate SceneTree nodes and must run on the renderer main thread.
|
||||
|
||||
|
||||
## Creates one MeshInstance3D for [param group_index], applies the indexed name
|
||||
## and optional transform contracts, then attaches it to [param wmo_parent_root].
|
||||
## The parent owns the returned node; the exact Mesh identity is retained.
|
||||
## Null/invalid parents, null meshes and negative indices return null.
|
||||
func materialize_mesh_group(
|
||||
wmo_parent_root: Node3D,
|
||||
mesh: Mesh,
|
||||
group_names: PackedStringArray,
|
||||
group_transforms: Array,
|
||||
group_index: int,
|
||||
visibility_range_end: float,
|
||||
visibility_range_end_margin: float,
|
||||
cast_shadows: bool) -> MeshInstance3D:
|
||||
if (
|
||||
wmo_parent_root == null
|
||||
or not is_instance_valid(wmo_parent_root)
|
||||
or mesh == null
|
||||
or group_index < 0
|
||||
):
|
||||
return null
|
||||
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
mesh_instance.name = (
|
||||
group_names[group_index]
|
||||
if group_index < group_names.size()
|
||||
else "Group_%d" % group_index
|
||||
)
|
||||
mesh_instance.mesh = mesh
|
||||
if group_index < group_transforms.size():
|
||||
mesh_instance.transform = group_transforms[group_index]
|
||||
_apply_render_settings(
|
||||
mesh_instance,
|
||||
visibility_range_end,
|
||||
visibility_range_end_margin,
|
||||
cast_shadows
|
||||
)
|
||||
wmo_parent_root.add_child(mesh_instance)
|
||||
return mesh_instance
|
||||
|
||||
|
||||
## Creates one MultiMeshInstance3D for [param group_index], applies the indexed
|
||||
## name and optional transform contracts, then attaches it to the supplied root.
|
||||
## The parent owns the returned node; the exact MultiMesh identity is retained.
|
||||
## Null/invalid parents, null MultiMeshes and negative indices return null.
|
||||
func materialize_multimesh_group(
|
||||
wmo_parent_root: Node3D,
|
||||
multimesh: MultiMesh,
|
||||
group_names: PackedStringArray,
|
||||
group_transforms: Array,
|
||||
group_index: int,
|
||||
visibility_range_end: float,
|
||||
visibility_range_end_margin: float,
|
||||
cast_shadows: bool) -> MultiMeshInstance3D:
|
||||
if (
|
||||
wmo_parent_root == null
|
||||
or not is_instance_valid(wmo_parent_root)
|
||||
or multimesh == null
|
||||
or group_index < 0
|
||||
):
|
||||
return null
|
||||
|
||||
var multimesh_instance := MultiMeshInstance3D.new()
|
||||
multimesh_instance.name = (
|
||||
group_names[group_index]
|
||||
if group_index < group_names.size()
|
||||
else "DoodadGroup_%d" % group_index
|
||||
)
|
||||
multimesh_instance.multimesh = multimesh
|
||||
if group_index < group_transforms.size():
|
||||
multimesh_instance.transform = group_transforms[group_index]
|
||||
_apply_render_settings(
|
||||
multimesh_instance,
|
||||
visibility_range_end,
|
||||
visibility_range_end_margin,
|
||||
cast_shadows
|
||||
)
|
||||
wmo_parent_root.add_child(multimesh_instance)
|
||||
return multimesh_instance
|
||||
|
||||
|
||||
func _apply_render_settings(
|
||||
geometry_instance: GeometryInstance3D,
|
||||
visibility_range_end: float,
|
||||
visibility_range_end_margin: float,
|
||||
cast_shadows: bool) -> void:
|
||||
geometry_instance.cast_shadow = (
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||
if cast_shadows
|
||||
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
)
|
||||
if visibility_range_end > 0.0:
|
||||
geometry_instance.visibility_range_end = visibility_range_end
|
||||
geometry_instance.visibility_range_end_margin = visibility_range_end_margin
|
||||
@@ -0,0 +1 @@
|
||||
uid://cu4tw6868rbkm
|
||||
@@ -36,6 +36,9 @@ const WMO_SCENE_RESOURCE_FINALIZER_SCRIPT := preload(
|
||||
const WMO_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_runtime_mesh_finalizer.gd"
|
||||
)
|
||||
const WMO_RENDER_GROUP_MATERIALIZER_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_render_group_materializer.gd"
|
||||
)
|
||||
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
||||
const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
|
||||
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
|
||||
@@ -381,6 +384,7 @@ var _wmo_scene_resource_finalizer := WMO_SCENE_RESOURCE_FINALIZER_SCRIPT.new(
|
||||
var _wmo_runtime_mesh_finalizer := WMO_RUNTIME_MESH_FINALIZER_SCRIPT.new(
|
||||
WMO_BUILDER_SCRIPT
|
||||
)
|
||||
var _wmo_render_group_materializer := WMO_RENDER_GROUP_MATERIALIZER_SCRIPT.new()
|
||||
var _wmo_missing_cache: Dictionary = {}
|
||||
var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new()
|
||||
var _world_wmo_root: Node3D
|
||||
@@ -3846,25 +3850,18 @@ func _process_wmo_render_build_jobs() -> void:
|
||||
var mesh := meshes[selected_index] as Mesh
|
||||
if mesh != null:
|
||||
_wmo_runtime_mesh_finalizer.finalize_mesh(mesh, extracted_dir)
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
mesh_instance.name = (
|
||||
mesh_names[selected_index]
|
||||
if selected_index < mesh_names.size()
|
||||
else "Group_%d" % selected_index
|
||||
var mesh_instance := _wmo_render_group_materializer.materialize_mesh_group(
|
||||
root as Node3D,
|
||||
mesh,
|
||||
mesh_names,
|
||||
mesh_transforms,
|
||||
selected_index,
|
||||
wmo_visibility_range,
|
||||
CHUNK_SIZE,
|
||||
wmo_cast_shadows
|
||||
)
|
||||
mesh_instance.mesh = mesh
|
||||
if selected_index < mesh_transforms.size():
|
||||
mesh_instance.transform = mesh_transforms[selected_index]
|
||||
mesh_instance.cast_shadow = (
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||
if wmo_cast_shadows
|
||||
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
)
|
||||
if wmo_visibility_range > 0.0:
|
||||
mesh_instance.visibility_range_end = wmo_visibility_range
|
||||
mesh_instance.visibility_range_end_margin = CHUNK_SIZE
|
||||
(root as Node3D).add_child(mesh_instance)
|
||||
_set_editor_owner_recursive(mesh_instance)
|
||||
if mesh_instance != null:
|
||||
_set_editor_owner_recursive(mesh_instance)
|
||||
_wmo_render_build_queue.adopt_cursors(
|
||||
unique_key,
|
||||
int(build_step["next_mesh_index"]),
|
||||
@@ -3877,25 +3874,20 @@ func _process_wmo_render_build_jobs() -> void:
|
||||
var multimesh := multimeshes[selected_index] as MultiMesh
|
||||
if multimesh != null:
|
||||
_wmo_runtime_mesh_finalizer.finalize_mesh(multimesh.mesh, extracted_dir)
|
||||
var multimesh_instance := MultiMeshInstance3D.new()
|
||||
multimesh_instance.name = (
|
||||
multimesh_names[selected_index]
|
||||
if selected_index < multimesh_names.size()
|
||||
else "DoodadGroup_%d" % selected_index
|
||||
var multimesh_instance := (
|
||||
_wmo_render_group_materializer.materialize_multimesh_group(
|
||||
root as Node3D,
|
||||
multimesh,
|
||||
multimesh_names,
|
||||
multimesh_transforms,
|
||||
selected_index,
|
||||
wmo_visibility_range,
|
||||
CHUNK_SIZE,
|
||||
wmo_cast_shadows
|
||||
)
|
||||
)
|
||||
multimesh_instance.multimesh = multimesh
|
||||
if selected_index < multimesh_transforms.size():
|
||||
multimesh_instance.transform = multimesh_transforms[selected_index]
|
||||
multimesh_instance.cast_shadow = (
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||
if wmo_cast_shadows
|
||||
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
)
|
||||
if wmo_visibility_range > 0.0:
|
||||
multimesh_instance.visibility_range_end = wmo_visibility_range
|
||||
multimesh_instance.visibility_range_end_margin = CHUNK_SIZE
|
||||
(root as Node3D).add_child(multimesh_instance)
|
||||
_set_editor_owner_recursive(multimesh_instance)
|
||||
if multimesh_instance != null:
|
||||
_set_editor_owner_recursive(multimesh_instance)
|
||||
_wmo_render_build_queue.adopt_cursors(
|
||||
unique_key,
|
||||
int(build_step["next_mesh_index"]),
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free WMO render-group Resource identity, indexed presentation,
|
||||
## attachment, ownership-boundary and bounded-timing regression.
|
||||
|
||||
const MATERIALIZER_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_render_group_materializer.gd"
|
||||
)
|
||||
const MATERIALIZER_PATH := "res://src/render/wmo/wmo_render_group_materializer.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_mesh_group_contract(failures)
|
||||
_verify_multimesh_group_contract(failures)
|
||||
_verify_fallback_and_zero_visibility_contract(failures)
|
||||
_verify_invalid_inputs(failures)
|
||||
_verify_source_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("WMO_RENDER_GROUP_MATERIALIZER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"WMO_RENDER_GROUP_MATERIALIZER PASS cases=37 iterations=1000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_mesh_group_contract(failures: Array[String]) -> void:
|
||||
var materializer := MATERIALIZER_SCRIPT.new()
|
||||
var parent_root := Node3D.new()
|
||||
get_root().add_child(parent_root)
|
||||
var mesh := ArrayMesh.new()
|
||||
var group_transform := Transform3D(Basis.IDENTITY, Vector3(1.0, 2.0, 3.0))
|
||||
var mesh_instance: MeshInstance3D = materializer.materialize_mesh_group(
|
||||
parent_root,
|
||||
mesh,
|
||||
PackedStringArray(["Exterior", "Interior"]),
|
||||
[Transform3D.IDENTITY, group_transform],
|
||||
1,
|
||||
1800.0,
|
||||
533.33333,
|
||||
false
|
||||
)
|
||||
_expect_true(mesh_instance != null, "valid Mesh group returned", failures)
|
||||
_expect_same(mesh_instance.get_parent(), parent_root, "Mesh group attached once", failures)
|
||||
_expect_equal(parent_root.get_child_count(), 1, "one Mesh parent child", failures)
|
||||
_expect_string_equal(mesh_instance.name, "Interior", "indexed Mesh name", failures)
|
||||
_expect_same(mesh_instance.mesh, mesh, "exact Mesh identity", failures)
|
||||
_expect_true(mesh_instance.transform == group_transform, "indexed Mesh transform", failures)
|
||||
_expect_equal(
|
||||
mesh_instance.cast_shadow,
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_OFF,
|
||||
"Mesh shadow disabled",
|
||||
failures
|
||||
)
|
||||
_expect_float_equal(mesh_instance.visibility_range_end, 1800.0, "Mesh visibility end", failures)
|
||||
_expect_float_equal(
|
||||
mesh_instance.visibility_range_end_margin,
|
||||
533.33333,
|
||||
"Mesh visibility margin",
|
||||
failures
|
||||
)
|
||||
parent_root.free()
|
||||
|
||||
|
||||
func _verify_multimesh_group_contract(failures: Array[String]) -> void:
|
||||
var materializer := MATERIALIZER_SCRIPT.new()
|
||||
var parent_root := Node3D.new()
|
||||
get_root().add_child(parent_root)
|
||||
var multimesh := MultiMesh.new()
|
||||
multimesh.transform_format = MultiMesh.TRANSFORM_3D
|
||||
multimesh.mesh = ArrayMesh.new()
|
||||
multimesh.instance_count = 1
|
||||
var group_transform := Transform3D(Basis.IDENTITY, Vector3(4.0, 5.0, 6.0))
|
||||
var multimesh_instance: MultiMeshInstance3D = (
|
||||
materializer.materialize_multimesh_group(
|
||||
parent_root,
|
||||
multimesh,
|
||||
PackedStringArray(["Doodads"]),
|
||||
[group_transform],
|
||||
0,
|
||||
2600.0,
|
||||
400.0,
|
||||
true
|
||||
)
|
||||
)
|
||||
_expect_same(
|
||||
multimesh_instance.get_parent(),
|
||||
parent_root,
|
||||
"MultiMesh group attached once",
|
||||
failures
|
||||
)
|
||||
_expect_string_equal(multimesh_instance.name, "Doodads", "indexed MultiMesh name", failures)
|
||||
_expect_same(multimesh_instance.multimesh, multimesh, "exact MultiMesh identity", failures)
|
||||
_expect_true(
|
||||
multimesh_instance.transform == group_transform,
|
||||
"indexed MultiMesh transform",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
multimesh_instance.cast_shadow,
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_ON,
|
||||
"MultiMesh shadow enabled",
|
||||
failures
|
||||
)
|
||||
_expect_float_equal(
|
||||
multimesh_instance.visibility_range_end,
|
||||
2600.0,
|
||||
"MultiMesh visibility end",
|
||||
failures
|
||||
)
|
||||
parent_root.free()
|
||||
|
||||
|
||||
func _verify_fallback_and_zero_visibility_contract(failures: Array[String]) -> void:
|
||||
var materializer := MATERIALIZER_SCRIPT.new()
|
||||
var parent_root := Node3D.new()
|
||||
var mesh_instance: MeshInstance3D = materializer.materialize_mesh_group(
|
||||
parent_root,
|
||||
ArrayMesh.new(),
|
||||
PackedStringArray(),
|
||||
[],
|
||||
7,
|
||||
0.0,
|
||||
99.0,
|
||||
true
|
||||
)
|
||||
_expect_string_equal(mesh_instance.name, "Group_7", "Mesh fallback name", failures)
|
||||
_expect_true(mesh_instance.transform == Transform3D.IDENTITY, "missing Mesh transform stays identity", failures)
|
||||
_expect_float_equal(mesh_instance.visibility_range_end, 0.0, "zero Mesh visibility unchanged", failures)
|
||||
_expect_float_equal(mesh_instance.visibility_range_end_margin, 0.0, "disabled Mesh margin unchanged", failures)
|
||||
var multimesh_instance: MultiMeshInstance3D = (
|
||||
materializer.materialize_multimesh_group(
|
||||
parent_root,
|
||||
MultiMesh.new(),
|
||||
PackedStringArray(),
|
||||
[],
|
||||
8,
|
||||
0.0,
|
||||
99.0,
|
||||
false
|
||||
)
|
||||
)
|
||||
_expect_string_equal(
|
||||
multimesh_instance.name,
|
||||
"DoodadGroup_8",
|
||||
"MultiMesh fallback name",
|
||||
failures
|
||||
)
|
||||
_expect_equal(parent_root.get_child_count(), 2, "fallback groups attached", failures)
|
||||
parent_root.free()
|
||||
|
||||
|
||||
func _verify_invalid_inputs(failures: Array[String]) -> void:
|
||||
var materializer := MATERIALIZER_SCRIPT.new()
|
||||
var parent_root := Node3D.new()
|
||||
var mesh := ArrayMesh.new()
|
||||
_expect_true(
|
||||
materializer.materialize_mesh_group(
|
||||
null, mesh, PackedStringArray(), [], 0, 0.0, 0.0, true
|
||||
) == null,
|
||||
"null Mesh parent rejected",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
materializer.materialize_mesh_group(
|
||||
parent_root, null, PackedStringArray(), [], 0, 0.0, 0.0, true
|
||||
) == null,
|
||||
"null Mesh rejected",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
materializer.materialize_mesh_group(
|
||||
parent_root, mesh, PackedStringArray(), [], -1, 0.0, 0.0, true
|
||||
) == null,
|
||||
"negative Mesh index rejected",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
materializer.materialize_multimesh_group(
|
||||
null, MultiMesh.new(), PackedStringArray(), [], 0, 0.0, 0.0, true
|
||||
) == null,
|
||||
"null MultiMesh parent rejected",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
materializer.materialize_multimesh_group(
|
||||
parent_root, null, PackedStringArray(), [], 0, 0.0, 0.0, true
|
||||
) == null,
|
||||
"null MultiMesh rejected",
|
||||
failures
|
||||
)
|
||||
_expect_equal(parent_root.get_child_count(), 0, "invalid inputs do not attach", failures)
|
||||
parent_root.free()
|
||||
|
||||
|
||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
var materializer_source := FileAccess.get_file_as_string(MATERIALIZER_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
var loader_adapter_source := _source_between(
|
||||
loader_source,
|
||||
"func _process_wmo_render_build_jobs()",
|
||||
"func _cancel_wmo_render_build_job(",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("WMO_RENDER_GROUP_MATERIALIZER_SCRIPT.new()"),
|
||||
"loader composes WMO group materializer",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_adapter_source.count("_wmo_render_group_materializer.materialize_"),
|
||||
2,
|
||||
"loader delegates both group kinds",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
loader_adapter_source.contains("MeshInstance3D.new()"),
|
||||
"loader releases MeshInstance construction",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
loader_adapter_source.contains("MultiMeshInstance3D.new()"),
|
||||
"loader releases MultiMeshInstance construction",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_adapter_source.count("_set_editor_owner_recursive("),
|
||||
2,
|
||||
"loader retains Editor ownership",
|
||||
failures
|
||||
)
|
||||
for required_rule in [
|
||||
"wmo_parent_root.add_child(mesh_instance)",
|
||||
"wmo_parent_root.add_child(multimesh_instance)",
|
||||
"else \"Group_%d\" % group_index",
|
||||
"else \"DoodadGroup_%d\" % group_index",
|
||||
"geometry_instance.visibility_range_end_margin = visibility_range_end_margin",
|
||||
]:
|
||||
_expect_true(
|
||||
materializer_source.contains(required_rule),
|
||||
"materializer owns %s" % required_rule,
|
||||
failures
|
||||
)
|
||||
for forbidden_dependency in [
|
||||
"ResourceLoader.",
|
||||
"FileAccess.",
|
||||
"WorkerThreadPool.",
|
||||
"RenderingServer.",
|
||||
".owner =",
|
||||
"_wmo_render_build_queue",
|
||||
"_render_budget_scheduler",
|
||||
"WmoRuntimeMeshFinalizer",
|
||||
]:
|
||||
_expect_false(
|
||||
materializer_source.contains(forbidden_dependency),
|
||||
"materializer omits %s ownership" % forbidden_dependency,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var materializer := MATERIALIZER_SCRIPT.new()
|
||||
var parent_root := Node3D.new()
|
||||
get_root().add_child(parent_root)
|
||||
var mesh := ArrayMesh.new()
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for group_index in 1000:
|
||||
materializer.materialize_mesh_group(
|
||||
parent_root,
|
||||
mesh,
|
||||
PackedStringArray(),
|
||||
[],
|
||||
group_index,
|
||||
0.0,
|
||||
0.0,
|
||||
false
|
||||
)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_equal(parent_root.get_child_count(), 1000, "timing groups complete", failures)
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "1000 groups under one second", failures)
|
||||
parent_root.free()
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _source_between(
|
||||
source: String,
|
||||
start_marker: String,
|
||||
end_marker: String,
|
||||
failures: Array[String]) -> String:
|
||||
var start_offset := source.find(start_marker)
|
||||
var end_offset := source.find(end_marker, start_offset + start_marker.length())
|
||||
if start_offset < 0 or end_offset <= start_offset:
|
||||
failures.append("cannot isolate source between %s and %s" % [start_marker, end_marker])
|
||||
return ""
|
||||
return source.substr(start_offset, end_offset - start_offset)
|
||||
|
||||
|
||||
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
if not condition:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(not condition, label, failures)
|
||||
|
||||
|
||||
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_float_equal(actual: float, expected: float, label: String, failures: Array[String]) -> void:
|
||||
if not is_equal_approx(actual, expected):
|
||||
failures.append("%s expected=%.6f actual=%.6f" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_string_equal(actual: String, expected: String, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
|
||||
if not is_same(actual, expected):
|
||||
failures.append(label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b1kt1agus8efe
|
||||
Reference in New Issue
Block a user