render: extract WMO render group materializer

Work-Package: M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001
Agent: sindo-main-codex
Tests: 63/64 autonomous verifiers passed; proprietary ADT probe unavailable; baseline dry-run 7/7; documentation and coordination gates passed
Fidelity: exact behavior-preserving node materialization extraction; no new 3.3.5a parity claim
This commit is contained in:
2026-08-01 10:22:21 +04:00
parent 543ee1572b
commit f4d5e29cc9
10 changed files with 706 additions and 41 deletions
+13
View File
@@ -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 cases and 1,000 current-Mesh calls. This is orchestration extraction, not new
build-12340 material or visual parity evidence. 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 ## Practical Rule For Future Work
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it. If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
@@ -34,7 +34,7 @@ settings and attachment from the loader into one main-thread materializer.
- Names and optional transforms preserve existing indexed fallback behavior. - Names and optional transforms preserve existing indexed fallback behavior.
- Shadow and positive visibility-range settings are applied unchanged. - Shadow and positive visibility-range settings are applied unchanged.
- The supplied WMO root becomes the sole SceneTree owner of the created node. - The supplied WMO root becomes the sole SceneTree owner of the created node.
- Editor persisted ownership is assigned only when an explicit owner is supplied. - Editor persisted ownership remains an explicit loader composition policy.
## Dependencies ## Dependencies
+1
View File
@@ -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 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 scene Resource finalizer | Implemented extraction | [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.md) |
| WMO runtime Mesh finalizer | Implemented extraction | [`wmo-runtime-mesh-finalizer.md`](wmo-runtime-mesh-finalizer.md) | | WMO runtime Mesh finalizer | Implemented extraction | [`wmo-runtime-mesh-finalizer.md`](wmo-runtime-mesh-finalizer.md) |
| WMO render group materializer | Implemented extraction | [`wmo-render-group-materializer.md`](wmo-render-group-materializer.md) |
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) | | Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) | | Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) | | Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
@@ -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)
+16 -4
View File
@@ -7,7 +7,7 @@
| Status | Partial | | Status | Partial |
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package | | Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
| Owners | Renderer workstream / milestone integrator | | Owners | Renderer workstream / milestone integrator |
| Last verified | Worktree `work/sindo-main-codex/m03-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 | | Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose ## Purpose
@@ -70,6 +70,8 @@ flowchart LR
WmoBuildStep --> Loader WmoBuildStep --> Loader
Loader --> WmoBuildQueue[WmoRenderBuildQueue] Loader --> WmoBuildQueue[WmoRenderBuildQueue]
WmoBuildQueue --> Loader WmoBuildQueue --> Loader
Loader --> WmoGroupMaterializer[WmoRenderGroupMaterializer]
WmoGroupMaterializer --> Scene
Native --> Parsed[Parsed tile/model data] Native --> Parsed[Parsed tile/model data]
Parsed --> Loader Parsed --> Loader
Loader --> Scene[SceneTree nodes] Loader --> Scene[SceneTree nodes]
@@ -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 | | `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 | | `WmoRenderBuildStepPlanner.plan_step` | Internal pure WMO service | Selects one mesh-first lightweight render-group operation and next cursors | Main/any thread; stateless | Raw integer comparisons are preserved without clamping |
| `WmoRenderBuildQueue` / `WmoRenderBuildJob` | Internal WMO pending-state service | Owns typed root/resource/cursor jobs and FIFO placement keys | Renderer main thread; map session | Invalid enqueue rejected; duplicate/stale behavior preserved | | `WmoRenderBuildQueue` / `WmoRenderBuildJob` | Internal WMO pending-state service | Owns typed root/resource/cursor jobs and FIFO placement keys | Renderer main thread; map session | Invalid enqueue rejected; duplicate/stale behavior preserved |
| `WmoRenderGroupMaterializer.materialize_mesh_group/materialize_multimesh_group` | Internal WMO scene-materialization service | Creates, configures and attaches one indexed lightweight render group | Renderer main thread; stateless after each call | Invalid parent/resource/index returns null without attachment |
| `WmoRenderResourceCacheState` | Internal WMO cache-state service | Owns validated Resources, negative entries and pending cache paths | Renderer main thread; map/cache session | Invalid/occupied request and unknown completion are rejected | | `WmoRenderResourceCacheState` | Internal WMO cache-state service | Owns validated Resources, negative entries and pending cache paths | Renderer main thread; map/cache session | Invalid/occupied request and unknown completion are rejected |
| `WmoRenderResourceFinalizer` | Internal WMO terminal-I/O service | Polls lightweight render requests, validates script/format and publishes Resource/missing outcomes | Renderer main thread; stateless across calls | Non-terminal retained; failed/null/wrong/stale complete missing | | `WmoRenderResourceFinalizer` | Internal WMO terminal-I/O service | Polls lightweight render requests, validates script/format and publishes Resource/missing outcomes | Renderer main thread; stateless across calls | Non-terminal retained; failed/null/wrong/stale complete missing |
| `WmoSceneResourceCacheState` | Internal WMO cache-state service | Owns validated PackedScenes, negative entries and pending `.tscn` paths | Renderer main thread; map/cache session | Direct missing and terminal request transitions remain distinct | | `WmoSceneResourceCacheState` | Internal WMO cache-state service | Owns validated PackedScenes, negative entries and pending `.tscn` paths | Renderer main thread; map/cache session | Direct missing and terminal request transitions remain distinct |
@@ -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 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 build step | Mesh/MultiMesh counts and job cursors | Loader / `WmoRenderBuildStepPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One group operation |
| Internal WMO pending build | Placement key, Node3D root, WMO Resource and cursors | Loader / `WmoRenderBuildQueue` | Loader drain and step planner adapter | Queue-owned job and strong references | Until cancel/clear/replacement | | Internal WMO pending build | Placement key, Node3D root, WMO Resource and cursors | Loader / `WmoRenderBuildQueue` | Loader drain and step planner adapter | Queue-owned job and strong references | Until cancel/clear/replacement |
| Internal WMO group materialization | Parent root, exact Mesh/MultiMesh, indexed metadata and render settings | Loader / `WmoRenderGroupMaterializer` | Attached geometry node | Parent owns node and exact Resource reference | One main-thread group operation |
| Internal WMO render cache | Normalized path, cache path and validated Resource | Loader / `WmoRenderResourceCacheState` | Loader lookup, ResourceLoader poll and build queue | State-owned Resource/path references; detached request snapshots | Until transient/full clear | | Internal WMO render cache | Normalized path, cache path and validated Resource | Loader / `WmoRenderResourceCacheState` | Loader lookup, ResourceLoader poll and build queue | State-owned Resource/path references; detached request snapshots | Until transient/full clear |
| Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear | | Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear |
| Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset | | Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset |
@@ -413,14 +417,16 @@ sequenceDiagram
reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation
and cursor plan. `WmoRenderBuildQueue` owns typed pending jobs, FIFO keys and and cursor plan. `WmoRenderBuildQueue` owns typed pending jobs, FIFO keys and
strong root/resource references without freeing engine objects. 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 `WmoRenderResourceCacheState` owns validated render Resources, negative entries
and pending cache paths; `WmoRenderResourceFinalizer` owns its terminal and pending cache paths; `WmoRenderResourceFinalizer` owns its terminal
ResourceLoader polling and script/format validation. `WmoSceneResourceCacheState` ResourceLoader polling and script/format validation. `WmoSceneResourceCacheState`
similarly owns validated PackedScenes, negative entries and pending `.tscn` similarly owns validated PackedScenes, negative entries and pending `.tscn`
paths; `WmoSceneResourceFinalizer` owns terminal ResourceLoader I/O and paths; `WmoSceneResourceFinalizer` owns terminal ResourceLoader I/O and
validation-probe lifetime. The loader retains request admission, FileAccess validation-probe lifetime. The loader retains request admission, FileAccess
size checks, live fallback, materialization, permits, validity reactions and size checks, live fallback, Mesh finalization, permits, Editor ownership,
every placed-Node lifecycle action. validity reactions and every placed-Node lifecycle action.
- `AdtWaterLoadPipelineState` owns pending request order/deduplication, opaque - `AdtWaterLoadPipelineState` owns pending request order/deduplication, opaque
active task IDs and the worker-safe parsed-result mailbox. The loader retains active task IDs and the worker-safe parsed-result mailbox. The loader retains
WorkerThreadPool start/wait, ADTLoader parsing, concurrency/finalize permits, 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. transition, completion/raw integer behavior, source ownership and bounded timing.
- WMO render build queue contract: typed references/cursors, FIFO, duplicate - WMO render build queue contract: typed references/cursors, FIFO, duplicate
replacement, stale-front cleanup, cancel/clear engine lifetime and bounded timing. 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, - WMO render Resource cache contract: invalid/duplicate request rejection,
validated/missing terminal transitions, transient/full reset, detached sorted validated/missing terminal transitions, transient/full reset, detached sorted
diagnostics, loader-owned version validation and bounded timing. diagnostics, loader-owned version validation and bounded timing.
@@ -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 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 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 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 cache state | Implemented extraction | Scene-free lifecycle/exclusivity/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending |
| WMO render Resource finalizer | Implemented extraction | Status/order/script/format/adoption/source/timing contract | Serialized/asset-backed corrupt-cache and leak evidence pending | | WMO render Resource finalizer | Implemented extraction | Status/order/script/format/adoption/source/timing contract | Serialized/asset-backed corrupt-cache and leak evidence pending |
| WMO scene Resource cache state | Implemented extraction | Scene-free lifecycle/direct-missing/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending | | WMO scene Resource cache state | Implemented extraction | Scene-free lifecycle/direct-missing/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending |
@@ -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_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_scene_resource_finalizer.gd` | Cached WMO terminal polling, probe validation/lifetime and publication |
| `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Cached WMO runtime refresh admission, surface iteration and material reconstruction | | `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Cached WMO runtime refresh admission, surface iteration and material reconstruction |
| `src/render/wmo/wmo_render_group_materializer.gd` | Indexed lightweight WMO geometry-node creation, render setup and attachment |
| `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation | | `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy | | `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets | | `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets |
@@ -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_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_scene_resource_finalizer.gd` | WMO scene status/order/probe/lifetime/adoption/boundary/timing regression |
| `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | WMO Mesh identity/version/material-definition/boundary/timing regression | | `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | WMO Mesh identity/version/material-definition/boundary/timing regression |
| `src/tools/verify_wmo_render_group_materializer.gd` | WMO render-group Resource/name/transform/render/attachment/boundary/timing regression |
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression | | `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression | | `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
| `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields | | `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
@@ -0,0 +1,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
+28 -36
View File
@@ -36,6 +36,9 @@ const WMO_SCENE_RESOURCE_FINALIZER_SCRIPT := preload(
const WMO_RUNTIME_MESH_FINALIZER_SCRIPT := preload( const WMO_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
"res://src/render/wmo/wmo_runtime_mesh_finalizer.gd" "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_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd") const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd") const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
@@ -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( var _wmo_runtime_mesh_finalizer := WMO_RUNTIME_MESH_FINALIZER_SCRIPT.new(
WMO_BUILDER_SCRIPT WMO_BUILDER_SCRIPT
) )
var _wmo_render_group_materializer := WMO_RENDER_GROUP_MATERIALIZER_SCRIPT.new()
var _wmo_missing_cache: Dictionary = {} var _wmo_missing_cache: Dictionary = {}
var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new() var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new()
var _world_wmo_root: Node3D var _world_wmo_root: Node3D
@@ -3846,25 +3850,18 @@ func _process_wmo_render_build_jobs() -> void:
var mesh := meshes[selected_index] as Mesh var mesh := meshes[selected_index] as Mesh
if mesh != null: if mesh != null:
_wmo_runtime_mesh_finalizer.finalize_mesh(mesh, extracted_dir) _wmo_runtime_mesh_finalizer.finalize_mesh(mesh, extracted_dir)
var mesh_instance := MeshInstance3D.new() var mesh_instance := _wmo_render_group_materializer.materialize_mesh_group(
mesh_instance.name = ( root as Node3D,
mesh_names[selected_index] mesh,
if selected_index < mesh_names.size() mesh_names,
else "Group_%d" % selected_index mesh_transforms,
selected_index,
wmo_visibility_range,
CHUNK_SIZE,
wmo_cast_shadows
) )
mesh_instance.mesh = mesh if mesh_instance != null:
if selected_index < mesh_transforms.size(): _set_editor_owner_recursive(mesh_instance)
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)
_wmo_render_build_queue.adopt_cursors( _wmo_render_build_queue.adopt_cursors(
unique_key, unique_key,
int(build_step["next_mesh_index"]), int(build_step["next_mesh_index"]),
@@ -3877,25 +3874,20 @@ func _process_wmo_render_build_jobs() -> void:
var multimesh := multimeshes[selected_index] as MultiMesh var multimesh := multimeshes[selected_index] as MultiMesh
if multimesh != null: if multimesh != null:
_wmo_runtime_mesh_finalizer.finalize_mesh(multimesh.mesh, extracted_dir) _wmo_runtime_mesh_finalizer.finalize_mesh(multimesh.mesh, extracted_dir)
var multimesh_instance := MultiMeshInstance3D.new() var multimesh_instance := (
multimesh_instance.name = ( _wmo_render_group_materializer.materialize_multimesh_group(
multimesh_names[selected_index] root as Node3D,
if selected_index < multimesh_names.size() multimesh,
else "DoodadGroup_%d" % selected_index multimesh_names,
multimesh_transforms,
selected_index,
wmo_visibility_range,
CHUNK_SIZE,
wmo_cast_shadows
)
) )
multimesh_instance.multimesh = multimesh if multimesh_instance != null:
if selected_index < multimesh_transforms.size(): _set_editor_owner_recursive(multimesh_instance)
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)
_wmo_render_build_queue.adopt_cursors( _wmo_render_build_queue.adopt_cursors(
unique_key, unique_key,
int(build_step["next_mesh_index"]), 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