Compare commits

...

8 Commits

14 changed files with 836 additions and 46 deletions
+3
View File
@@ -30,6 +30,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
- `src/render/wmo/wmo_placement_resolver.gd` - value-only WMO path, identity and transform rules. - `src/render/wmo/wmo_placement_resolver.gd` - value-only WMO path, identity and transform rules.
- `src/render/wmo/wmo_placement_registry.gd` - WMO placement-key to tile/global reference sets; Nodes and render jobs remain in the streamer. - `src/render/wmo/wmo_placement_registry.gd` - WMO placement-key to tile/global reference sets; Nodes and render jobs remain in the streamer.
- `src/render/wmo/wmo_render_build_step_planner.gd` - mesh-first lightweight WMO group operation and cursor planning without Nodes or Resources. - `src/render/wmo/wmo_render_build_step_planner.gd` - mesh-first lightweight WMO group operation and cursor planning without Nodes or Resources.
- `src/render/wmo/wmo_render_build_queue.gd` / `wmo_render_build_job.gd` - typed pending group jobs, FIFO placement keys and strong root/resource references without engine destruction.
- `src/scenes/streaming/eastern_kingdoms_streaming.tscn` - текущая сцена для проверки Azeroth/Eastern Kingdoms. - `src/scenes/streaming/eastern_kingdoms_streaming.tscn` - текущая сцена для проверки Azeroth/Eastern Kingdoms.
- `addons/mpq_extractor/loaders/adt_builder.gd` - сборка ADT terrain, control splat материалов и MH2O liquids. - `addons/mpq_extractor/loaders/adt_builder.gd` - сборка ADT terrain, control splat материалов и MH2O liquids.
- `addons/mpq_extractor/loaders/m2_builder.gd` - сборка статического M2 mesh/material. - `addons/mpq_extractor/loaders/m2_builder.gd` - сборка статического M2 mesh/material.
@@ -401,6 +402,8 @@ Known limits:
the parallel key-to-Node map and performs final-release cancellation/free; the parallel key-to-Node map and performs final-release cancellation/free;
- `WmoRenderBuildStepPlanner` selects one mesh or MultiMesh group and its next - `WmoRenderBuildStepPlanner` selects one mesh or MultiMesh group and its next
cursors; the streamer still materializes Nodes and consumes the frame permit; cursors; the streamer still materializes Nodes and consumes the frame permit;
- `WmoRenderBuildQueue` owns pending typed jobs/FIFO keys and releases references
on cancel/reset; the streamer still validates and frees engine objects;
- большие WMO не должны инстанцироваться как тяжелые `.tscn` во время движения; - большие WMO не должны инстанцироваться как тяжелые `.tscn` во время движения;
- добавлен lightweight render-cache path для WMO groups; - добавлен lightweight render-cache path для WMO groups;
- `wmo_render_group_ops_per_tick` ограничивает подключение WMO groups по кадрам; - `wmo_render_group_ops_per_tick` ограничивает подключение WMO groups по кадрам;
@@ -0,0 +1,117 @@
# M03-RND-WMO-RENDER-BUILD-QUEUE-001 — WMO render build queue
<!-- OPENWC_CLAIM:M03-RND-WMO-RENDER-BUILD-QUEUE-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-wmo-render-build-queue`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract typed lightweight-WMO render build jobs and their FIFO key queue from
the loader while leaving all engine-object destruction and materialization in
the loader.
## Non-goals
- Queue-free Nodes or free Resources/RIDs.
- Move material refresh, Node creation, editor ownership or budget permits.
- Change duplicate enqueue, stale-key cleanup, cancel or clear ordering.
- Change placement, cache loading/versioning, transforms, shadows or visibility.
- Claim asset-backed WMO fidelity, leak acceptance or traversal p95/p99.
## Paths
- Exclusive: `src/render/wmo/wmo_render_build_job.gd`,
`src/render/wmo/wmo_render_build_queue.gd`,
`src/tools/verify_wmo_render_build_queue.gd`,
`docs/modules/wmo-render-build-queue.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`docs/modules/wmo-render-build-step-planner.md`,
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated ADT/WMO resources, caches
and proprietary renderer corpus
## Contracts and data
- Typed job: placement key, strong Node3D/Resource references and two cursors
- FIFO queue preserves duplicate key append and first-occurrence erase behavior
- Stale front keys remain observable for loader-compatible pop-and-continue
- Cancel releases the keyed job reference and first queued key only
- Clear releases every job/key reference idempotently; never frees engine objects
- Loader retains validity checks, planner invocation, cursor adoption,
materialization, permits, cancellation reactions and Node destruction
- Cache/schema/coordinate/profile versions: unchanged
## Dependencies
- Requires: current master `1e740aa`, accepted WMO build step planner
- Blocks: further WMO cache/load and materialization decomposition
- External state: none; verification uses synthetic Node3D/Resource instances
## Verification
- Commands: dedicated invalid/FIFO/duplicate/stale/cursor/cancel/clear/
diagnostics/source/timing verifier; WMO planner/registry/resolver/material/
shutdown plus M2/terrain/facade/internal-access/manifest/scheduler/streaming/
coordinate/docs/coord gates
- Fidelity evidence: exact current queue/dictionary behavior preserved
- Performance budget: 100 cycles over 256 jobs under 1 second
## Documentation deliverables
- Inline job/queue API documentation
- New module spec with inputs/outputs, data flow, state/sequence, ownership,
threading, failure/recovery, performance/fidelity limits and source map
- Build-step planner, renderer module registry, world renderer and RENDER updates
## Simplicity and naming
- Important names: `WmoRenderBuildJob`, `WmoRenderBuildQueue`
- Simplest solution: one typed record plus Dictionary/Array queue composition
- Rejected complexity: generic job scheduler, signals, callbacks, mutexes,
Resources as job records or engine lifetime callbacks
- Unavoidable complexity: duplicate keys can intentionally leave stale entries
- Measured optimization evidence: bounded synthetic enqueue/cancel/clear loop
## Status
- State: accepted
- Done: typed job/queue implemented; three clear sites, metrics, enqueue, drain and
cancel migrated; engine destruction/materialization retained in loader
- Next: WMO cache/load state or liquid service extraction
- Blocked by:
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-RENDER-BUILD-QUEUE-001:60603e1 -->
## Handoff
- Commit: `e576ae2` (implementation `e1bb105` plus cold-load fix)
- Results: queue PASS (`cases=11`, `iterations=100`, `elapsed_ms=59.874`);
WMO planner/registry/resolver/material/shutdown and 18 adjacent M2/terrain/
facade/scheduler/streaming/coordinate regressions PASS; documentation and
coordination gates PASS; checkpoint dry-run PASS `7/7`.
- Cold-load recovery: post-merge smoke exposed an editor class-cache dependency;
typed queue accessors removed the loader/global-class dependency and a fresh
run without `.godot` passed (`cases=11`, `elapsed_ms=57.835`).
- Remaining risks: strong Node/Resource references require every loader cancel/
clear path; duplicate enqueue intentionally preserves stale FIFO keys; no
proprietary WMO corpus, original-client paired capture, leak run or traversal
p95/p99; dry-run reports expected missing ADT/DBC/character assets.
- Documentation updated: inline job/queue API;
`docs/modules/wmo-render-build-queue.md`, build-step planner spec, module
registry, world renderer and `RENDER.md`; data-flow/state/sequence diagrams,
ownership, threading, failure/recovery and source map included.
- Integration: implementation merged as `a878e7c`; its first post-merge smoke
exposed and rejected a cold global-class registration dependency. Fix merged
as `60603e1`; post-fix master queue (`cases=11`, `iterations=100`,
`elapsed_ms=56.454`), planner, registry, resolver, shutdown, facade,
documentation and coordination gates passed without parse diagnostics.
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-RENDER-BUILD-QUEUE-001:e576ae2 -->
+1
View File
@@ -22,6 +22,7 @@
| WMO placement resolver | Implemented extraction | [`wmo-placement-resolver.md`](wmo-placement-resolver.md) | | WMO placement resolver | Implemented extraction | [`wmo-placement-resolver.md`](wmo-placement-resolver.md) |
| WMO placement registry | Implemented extraction | [`wmo-placement-registry.md`](wmo-placement-registry.md) | | WMO placement registry | Implemented extraction | [`wmo-placement-registry.md`](wmo-placement-registry.md) |
| WMO render build step planner | Implemented extraction | [`wmo-render-build-step-planner.md`](wmo-render-build-step-planner.md) | | WMO render build step planner | Implemented extraction | [`wmo-render-build-step-planner.md`](wmo-render-build-step-planner.md) |
| WMO render build queue | Implemented extraction | [`wmo-render-build-queue.md`](wmo-render-build-queue.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) |
+242
View File
@@ -0,0 +1,242 @@
# WMO Render Build Queue
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-WMO-RENDER-BUILD-QUEUE-001` |
| Owners | Typed lightweight-WMO build jobs and FIFO placement-key queue |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-render-build-queue`, 2026-07-17 |
| Profiles/capabilities | Lightweight WMO render-cache path; profile-independent |
## Purpose
Own pending lightweight-WMO render build records and their FIFO placement-key
order outside the monolithic loader. Each typed job retains its placement key,
scene root, render resource and mesh/MultiMesh cursors while the loader performs
all validation, planning, materialization, permit consumption and destruction.
## Non-goals
- Free or queue-free Nodes, Resources or RIDs.
- Create MeshInstance3D/MultiMeshInstance3D or refresh materials.
- Own frame permits, placement references, caches or load requests.
- Change duplicate enqueue, stale-key cleanup, cancellation or ordering.
- Change cache versions, WMO transforms, shadows, visibility or fidelity rules.
## Context and boundaries
```mermaid
flowchart LR
Placement[Successful WMO placement] --> Loader[StreamingWorldLoader]
Root[Node3D root] --> Loader
Cache[WMOStreamingResource] --> Loader
Loader --> Queue[WmoRenderBuildQueue]
Queue --> Job[WmoRenderBuildJob]
Job --> Loader
Loader --> Planner[WmoRenderBuildStepPlanner]
Planner --> Step[Operation and next cursors]
Step --> Loader
Loader --> Scene[Main-thread group Node attach]
Loader --> Budget[RenderBudgetScheduler permit]
```
The queue may strongly reference Node3D and Resource through typed jobs, but it
must not destroy, mutate or inspect their render content. RenderingServer, RID,
Mesh/MultiMesh instances, files, worker threads, mutexes, gameplay, network and
editor UI are forbidden dependencies.
## Public API
### `WmoRenderBuildJob`
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| constructor `(placement_key, root, render_resource)` | Constructor | Create cursor-zero job with strong references | Renderer main thread; until cancel/clear/replacement | Queue validates inputs before construction |
| `placement_key()` | Query | Return immutable queue identity | Any serialized caller; job lifetime | None |
| `root()` | Query | Borrow retained Node3D reference | Main thread; no ownership transfer | May later be invalid; loader checks |
| `render_resource()` | Query | Borrow retained Resource reference | Main thread; no ownership transfer | Queue does not validate cache contents |
| `mesh_index()` / `multimesh_index()` | Query | Return current group cursors | Serialized job drain | None |
| `adopt_cursors(next_mesh_index, next_multimesh_index)` | Command | Atomically adopt planner cursors | Renderer main thread | Raw integers are accepted |
| `diagnostic_snapshot()` | Query | Return detached scalar state and presence flags | Caller-owned result | Never exposes engine objects |
### `WmoRenderBuildQueue`
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `enqueue(placement_key, root, render_resource)` | Command | Replace keyed job and append FIFO key | Renderer main thread; map session | Empty/null input returns false |
| `has_pending()` / `pending_count()` | Query | Observe FIFO entries including duplicate/stale keys | Renderer main thread | None |
| `active_job_count()` | Query | Count current keyed jobs | Renderer main thread | None |
| `front_key()` / `pop_front()` | Queue operation | Inspect/remove one front FIFO key | Renderer main thread | Empty queue returns empty String |
| `has_job(placement_key)` | Query | Distinguish a live job from a stale FIFO key | Renderer main thread | Unknown key returns false |
| `job_for(placement_key)` | Diagnostic/test query | Borrow current job through its `RefCounted` base or return null | Renderer main thread | Runtime loader uses typed accessors instead |
| `root_for` / `render_resource_for` | Typed query | Borrow current engine references without exposing job implementation type | Renderer main thread | Stale key returns null |
| `mesh_index_for` / `multimesh_index_for` | Typed query | Return current cursors | Renderer main thread | Stale key returns zero |
| `adopt_cursors(placement_key, next_mesh_index, next_multimesh_index)` | Command/query | Atomically update current job cursors | Renderer main thread | Stale key returns false |
| `cancel(placement_key)` | Command/query | Remove keyed job and first matching FIFO key | Renderer main thread | Unknown key returns false; stale duplicates remain |
| `clear()` | Command | Release all job/key references idempotently | Reset/shutdown | Does not free engine objects |
| `diagnostic_snapshot()` | Query | Return detached FIFO order and key-sorted scalar jobs | Caller-owned result | None |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Resolved placement key | Placement/build adapter | Queue/job | Copied String | Until cancel/clear/replacement |
| Input | WMO root Node3D | Loader instance adapter | Job | Strong reference; loader owns engine lifetime | Until job release |
| Input | Validated WMOStreamingResource | Loader cache adapter | Job | Strong Resource reference; cache/loader owns semantics | Until job release |
| Input | Next mesh/MultiMesh cursors | Build step planner through loader | Job | Integer values | Until next adoption |
| Output | FIFO front key and typed job | Queue | Loader drain | Borrowed references | One drain iteration |
| Output | Detached scalar diagnostics | Job/queue | Verifier/future facade adapter | Fresh values | Caller lifetime |
Side effects are limited to reference retention/release, cursor mutation and
Dictionary/Array ordering. Scene tree, material, RID, file and permit effects
remain outside the module.
## Data flow
```mermaid
flowchart TD
Enqueue[enqueue key/root/resource] --> Valid{Inputs valid?}
Valid -->|no| Reject[false; unchanged]
Valid -->|yes| Replace[Replace keyed typed job]
Replace --> Append[Append key even when duplicate]
Drain[front key] --> Lookup{Current keyed job exists?}
Lookup -->|no stale| Pop[pop front and continue]
Lookup -->|yes| Borrow[Borrow job to loader]
Borrow --> Plan[Loader invokes step planner]
Plan --> Adopt[Job adopts both next cursors]
Cancel[cancel key] --> EraseJob[Erase keyed job]
EraseJob --> EraseFirst[Erase first matching FIFO key]
```
## Lifecycle/state
The queue can be `Empty`, `Pending` or `StaleFront`. Duplicate enqueue followed
by cancel can intentionally produce `StaleFront`, matching the previous separate
Dictionary/Array behavior. The loader pops a stale front without consuming a
permit. `clear` returns every state to `Empty` without freeing engine objects.
```mermaid
stateDiagram-v2
[*] --> Empty
Empty --> Pending: valid enqueue
Pending --> Pending: enqueue / cursor adoption
Pending --> Empty: final cancel or clear
Pending --> StaleFront: duplicate key then cancel first occurrence
StaleFront --> Pending: pop stale, more live entries remain
StaleFront --> Empty: pop stale or clear
```
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Queue as WmoRenderBuildQueue
participant Job as WmoRenderBuildJob
participant Planner as WmoRenderBuildStepPlanner
Loader->>Queue: enqueue(key, root, resource)
loop while permit and pending key
Loader->>Queue: front_key + has_job
alt stale key
Loader->>Queue: pop_front
else live job
Loader->>Queue: typed root/resource/cursor accessors
Loader->>Planner: plan_step(counts, job cursors)
Planner-->>Loader: operation and next cursors
Loader->>Loader: materialize one group on main thread
Loader->>Queue: adopt_cursors(key, next mesh, next MultiMesh)
Loader->>Loader: consume one permit
end
end
Loader->>Queue: cancel(key) or clear()
Note over Queue: references released; Nodes/Resources not freed
```
## Ownership, threading and resources
- Queue owns its job Dictionary and FIFO key Array.
- Job owns strong references, but loader owns Node/Resource semantic lifetime.
- Loader checks `is_instance_valid`, creates/frees Nodes and controls caches.
- Loader invokes planner and adopts cursor output on the main thread.
- Queue/job contain no mutex; all mutation is serialized by the renderer drain.
- `clear` may release the last Resource reference but never calls engine free APIs.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty key/null root/resource | Enqueue guards | Reject without mutation | Contract verifier | Repair caller input |
| Duplicate key | Existing keyed job | Replace job and append key | Queue diagnostics | Historical stale handling remains |
| Stale FIFO key | `job_for` returns null | Loader pops front without permit | Contract/source verifier | Continue drain |
| Root invalidated externally | Loader validity check | Loader cancels keyed job | Shutdown/runtime diagnostics | Placement can rebuild later |
| Resource absent | Loader check | Loader cancels keyed job | Runtime diagnostics | Reload/requeue placement |
| Final placement release | Placement registry returns final | Loader cancels build then frees Node | Registry/shutdown regressions | Later placement starts fresh |
| Reset/shutdown | Loader lifecycle | Queue clear releases references | Shutdown verifier | Next map starts empty |
## Configuration and capabilities
The module introduces no settings or profile branches. WMO group permit count,
quality presets, cache limits, visibility and shadows remain loader-owned.
## Persistence, cache and migration
No data is serialized. WMO cache format/version/path and load request behavior
are unchanged. No migration or cache rebuild is required.
## Diagnostics and observability
`pending_count` preserves existing queue metrics, including stale/duplicate keys.
The detached snapshot exposes FIFO order, cursor values and root/resource presence
without exposing mutable jobs or engine objects. No runtime logs are emitted.
## Verification
- `verify_wmo_render_build_queue.gd`: invalid inputs, FIFO, typed references,
cursor adoption, duplicate replacement, stale pop, first-key cancel, clear,
engine lifetime, detached sorted diagnostics, source boundaries and timing.
- WMO planner/registry/resolver/material/shutdown and renderer regressions verify
unchanged ordering, permit use, destruction and surrounding lifecycle.
- Fidelity evidence is exact preservation of current queue/Dictionary semantics;
no original-client visual or asset-backed performance claim is made.
## Extension points
- Cache/load-request ownership can be extracted separately without changing jobs.
- A later typed materialization command may consume the borrowed job and planner
output while this queue remains the pending-state owner.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| WMO render build pending-state ownership | Implemented | Lifecycle/source/timing verifier | Asset-backed traversal/leak evidence pending |
| WMO group planning | Implemented | Build-step planner verifier | Asset-backed p95/p99 pending |
| WMO materialization/cache loading | Partial | Existing runtime regressions | Separate loader seams remain |
## Known gaps and risks
- Jobs intentionally retain strong Node/Resource references until cancel/clear.
- Duplicate enqueue preserves a stale-key edge case rather than normalizing it.
- Resource array shape remains a WMO cache-builder invariant.
- No proprietary WMO corpus, traversal profile or original-client paired capture
is included in this package.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_render_build_job.gd` | Typed references and cursors |
| `src/render/wmo/wmo_render_build_queue.gd` | Keyed jobs, FIFO order, cancel/clear and diagnostics |
| `src/render/wmo/wmo_render_build_step_planner.gd` | Pure operation/cursor planning |
| `src/scenes/streaming/streaming_world_loader.gd` | Validation, materialization, permits and engine destruction |
| `src/tools/verify_wmo_render_build_queue.gd` | Lifecycle, ownership, boundary and timing regression |
## Related decisions and references
- [`wmo-render-build-step-planner.md`](wmo-render-build-step-planner.md)
- [`wmo-placement-registry.md`](wmo-placement-registry.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)
@@ -104,7 +104,8 @@ sequenceDiagram
## Ownership, threading and resources ## Ownership, threading and resources
- The planner owns only call-local scalar comparisons and the returned value map. - The planner owns only call-local scalar comparisons and the returned value map.
- The loader owns Resources, mesh arrays, build jobs/queue and cursor adoption. - `WmoRenderBuildQueue` owns typed jobs, FIFO keys and strong Node3D/Resource
references. The loader owns mesh-array interpretation and engine lifetime.
- The loader creates and owns every MeshInstance3D/MultiMeshInstance3D and - The loader creates and owns every MeshInstance3D/MultiMeshInstance3D and
performs material refresh and editor-owner assignment on the main thread. performs material refresh and editor-owner assignment on the main thread.
- `RenderBudgetScheduler` owns permits; the planner neither reads nor consumes one. - `RenderBudgetScheduler` owns permits; the planner neither reads nor consumes one.
@@ -149,8 +150,8 @@ existing loader/facade diagnostics.
## Extension points ## Extension points
- WMO render build job/queue ownership may be extracted separately after a typed - `WmoRenderBuildQueue` now owns typed pending state; cache/load requests and
cancellation and Resource lifetime contract exists. materialization remain separate extraction seams.
- Materialization can later receive typed commands without changing this planner. - Materialization can later receive typed commands without changing this planner.
## Capability status ## Capability status
@@ -158,7 +159,7 @@ existing loader/facade diagnostics.
| Capability | Status | Evidence | Gap/next step | | Capability | Status | Evidence | Gap/next step |
|---|---|---|---| |---|---|---|---|
| WMO render build step planning | Implemented | Pure contract/source/timing verifier | Asset-backed traversal p95/p99 pending | | WMO render build step planning | Implemented | Pure contract/source/timing verifier | Asset-backed traversal p95/p99 pending |
| WMO build job/resource ownership | Partial | Existing loader/runtime regressions | Separate state/cancellation package | | WMO build job/queue ownership | Implemented | Typed lifecycle/source/timing verifier | Asset-backed traversal/leak evidence pending |
| WMO visual/portal/material fidelity | Partial | Renderer baseline/material checks | Original-client paired evidence pending | | WMO visual/portal/material fidelity | Partial | Renderer baseline/material checks | Original-client paired evidence pending |
## Known gaps and risks ## Known gaps and risks
@@ -173,12 +174,15 @@ existing loader/facade diagnostics.
| Path | Responsibility | | Path | Responsibility |
|---|---| |---|---|
| `src/render/wmo/wmo_render_build_step_planner.gd` | Pure operation and cursor selection | | `src/render/wmo/wmo_render_build_step_planner.gd` | Pure operation and cursor selection |
| `src/scenes/streaming/streaming_world_loader.gd` | Resource validation, jobs/queues, materialization, permits and cancellation | | `src/render/wmo/wmo_render_build_job.gd` | Typed engine references and cursors |
| `src/render/wmo/wmo_render_build_queue.gd` | Pending jobs, FIFO keys and cancellation state |
| `src/scenes/streaming/streaming_world_loader.gd` | Resource validation, materialization, permits and engine lifetime |
| `src/tools/verify_wmo_render_build_step_planner.gd` | Contract, boundary and timing regression | | `src/tools/verify_wmo_render_build_step_planner.gd` | Contract, boundary and timing regression |
## Related decisions and references ## Related decisions and references
- [`wmo-placement-registry.md`](wmo-placement-registry.md) - [`wmo-placement-registry.md`](wmo-placement-registry.md)
- [`wmo-render-build-queue.md`](wmo-render-build-queue.md)
- [`wmo-placement-resolver.md`](wmo-placement-resolver.md) - [`wmo-placement-resolver.md`](wmo-placement-resolver.md)
- [`world-renderer.md`](world-renderer.md) - [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md) - [`../../RENDER.md`](../../RENDER.md)
+15 -5
View File
@@ -7,7 +7,7 @@
| Status | Partial | | Status | Partial |
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package | | Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
| Owners | Renderer workstream / milestone integrator | | Owners | Renderer workstream / milestone integrator |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-render-build-planner`, 2026-07-17 | | Last verified | Worktree `work/sindo-main-codex/m03-wmo-render-build-queue`, 2026-07-17 |
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete | | Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose ## Purpose
@@ -60,6 +60,8 @@ flowchart LR
WmoRegistry --> Loader WmoRegistry --> Loader
Loader --> WmoBuildStep[WmoRenderBuildStepPlanner] Loader --> WmoBuildStep[WmoRenderBuildStepPlanner]
WmoBuildStep --> Loader WmoBuildStep --> Loader
Loader --> WmoBuildQueue[WmoRenderBuildQueue]
WmoBuildQueue --> Loader
Native --> Parsed[Parsed tile/model data] Native --> Parsed[Parsed tile/model data]
Parsed --> Loader Parsed --> Loader
Loader --> Scene[SceneTree nodes] Loader --> Scene[SceneTree nodes]
@@ -133,6 +135,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
| `WmoPlacementResolver.normalize_relative_path/resolve_unique_key/resolve_world_transform` | Internal pure WMO service | Resolves cache key, registry identity and world transform | Main/any thread; stateless | Missing UID uses tile/index fallback; transform fields use historical defaults | | `WmoPlacementResolver.normalize_relative_path/resolve_unique_key/resolve_world_transform` | Internal pure WMO service | Resolves cache key, registry identity and world transform | Main/any thread; stateless | Missing UID uses tile/index fallback; transform fields use historical defaults |
| `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 |
Публичным contract не считаются `StreamingWorldLoader` methods/properties, Публичным contract не считаются `StreamingWorldLoader` methods/properties,
внутренние dictionaries, queues, job records и generated node names. Scene-owned внутренние dictionaries, queues, job records и generated node names. Scene-owned
@@ -164,6 +167,7 @@ loader configuration remains transitional composition data, not a caller API.
| Internal WMO placement | Path, MODF placement, tile/index | Loader / `WmoPlacementResolver` | WMO caches, registry and three instance adapters | Value-only String/Transform3D | Lookup/placement lifetime | | Internal WMO placement | Path, MODF placement, tile/index | Loader / `WmoPlacementResolver` | WMO caches, registry and three instance adapters | Value-only String/Transform3D | Lookup/placement lifetime |
| 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 |
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame | | Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach | | Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand | | Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
@@ -214,7 +218,8 @@ flowchart TD
M2Batch --> M2 M2Batch --> M2
R --> WmoPlacement[WmoPlacementResolver] R --> WmoPlacement[WmoPlacementResolver]
WmoPlacement --> WmoRegistry[WmoPlacementRegistry] WmoPlacement --> WmoRegistry[WmoPlacementRegistry]
WmoRegistry --> WmoBuildStep[WmoRenderBuildStepPlanner] WmoRegistry --> WmoBuildQueue[WmoRenderBuildQueue]
WmoBuildQueue --> WmoBuildStep[WmoRenderBuildStepPlanner]
WmoBuildStep --> WMO WmoBuildStep --> WMO
R --> B[RenderBudgetScheduler permits] R --> B[RenderBudgetScheduler permits]
B --> Terrain[Terrain attach/upgrade] B --> Terrain[Terrain attach/upgrade]
@@ -340,8 +345,10 @@ sequenceDiagram
- `WmoPlacementResolver` is stateless and owns only call-local cache-key, - `WmoPlacementResolver` is stateless and owns only call-local cache-key,
identity and transform values. `WmoPlacementRegistry` owns only placement-key identity and transform values. `WmoPlacementRegistry` owns only placement-key
reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation
and cursor plan. The loader retains the key-to-Node map, caches, jobs, queues, and cursor plan. `WmoRenderBuildQueue` owns typed pending jobs, FIFO keys and
resources, cursor adoption, permits, cancellation and every Node lifecycle action. strong root/resource references without freeing engine objects. The loader
retains the placement key-to-Node map, caches/loads, materialization, permits,
validity reactions and every Node lifecycle action.
- Rendered-ground query results and diagnostic snapshots are caller-owned values; - Rendered-ground query results and diagnostic snapshots are caller-owned values;
the facade never returns Mesh, Node, tile-state or queue references. the facade never returns Mesh, Node, tile-state or queue references.
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay - Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
@@ -439,6 +446,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
lifecycle, detached sorted diagnostics, source ownership and bounded timing. lifecycle, detached sorted diagnostics, source ownership and bounded timing.
- WMO render build step contract: mesh-before-MultiMesh order, one-index cursor - WMO render build step contract: mesh-before-MultiMesh order, one-index cursor
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
replacement, stale-front cleanup, cancel/clear engine lifetime and bounded timing.
- Budget scheduler contract: exact lane exhaustion, shared chunk removal/create - Budget scheduler contract: exact lane exhaustion, shared chunk removal/create
priority, independent lanes, frame reset, invalid limits, terminal cancellation, priority, independent lanes, frame reset, invalid limits, terminal cancellation,
dependency boundaries and bounded permit timing without loading a world scene. dependency boundaries and bounded permit timing without loading a world scene.
@@ -474,7 +483,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| M2 build batch planner | Implemented extraction | Scene-free limit/count/cursor/source/timing contract | Queue/resource state, spatial cells and asset-backed p95/p99 remain pending | | M2 build batch planner | Implemented extraction | Scene-free limit/count/cursor/source/timing contract | Queue/resource state, spatial cells and asset-backed p95/p99 remain pending |
| 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 | Job/resource ownership and 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 | Cache/load state and asset-backed traversal/leak evidence pending |
| WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity | | WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity |
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity | | Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
| Sky/light | Partial | DBC controller/logs | Zone transition paired validation | | Sky/light | Partial | DBC controller/logs | Zone transition paired validation |
+59
View File
@@ -0,0 +1,59 @@
class_name WmoRenderBuildJob
extends RefCounted
## Holds one lightweight-WMO build cursor and strong engine-object references.
## Releasing this record never frees the referenced Node or Resource.
var _placement_key: String
var _root: Node3D
var _render_resource: Resource
var _mesh_index: int = 0
var _multimesh_index: int = 0
func _init(placement_key: String, root: Node3D, render_resource: Resource) -> void:
_placement_key = placement_key
_root = root
_render_resource = render_resource
## Returns the immutable placement identity used by the queue.
func placement_key() -> String:
return _placement_key
## Returns the strongly referenced scene root without transferring ownership.
func root() -> Node3D:
return _root
## Returns the strongly referenced render resource without transferring ownership.
func render_resource() -> Resource:
return _render_resource
## Returns the current mesh-group cursor.
func mesh_index() -> int:
return _mesh_index
## Returns the current doodad MultiMesh-group cursor.
func multimesh_index() -> int:
return _multimesh_index
## Atomically adopts both cursor values returned by the pure step planner.
func adopt_cursors(next_mesh_index: int, next_multimesh_index: int) -> void:
_mesh_index = next_mesh_index
_multimesh_index = next_multimesh_index
## Returns detached scalar state and never exposes engine-object references.
func diagnostic_snapshot() -> Dictionary:
return {
"placement_key": _placement_key,
"mesh_index": _mesh_index,
"multimesh_index": _multimesh_index,
"has_root": _root != null,
"has_render_resource": _render_resource != null,
}
@@ -0,0 +1 @@
uid://gyjq5dqueyi7
+139
View File
@@ -0,0 +1,139 @@
class_name WmoRenderBuildQueue
extends RefCounted
## Owns pending lightweight-WMO job records and FIFO placement keys.
const JOB_SCRIPT := preload("res://src/render/wmo/wmo_render_build_job.gd")
var _jobs_by_placement_key: Dictionary = {}
var _queued_placement_keys: Array[String] = []
## Enqueues a new typed job. Duplicate placement keys replace the keyed job but
## append another FIFO entry, preserving the loader's historical behavior.
func enqueue(placement_key: String, root: Node3D, render_resource: Resource) -> bool:
if placement_key.is_empty() or root == null or render_resource == null:
return false
_jobs_by_placement_key[placement_key] = JOB_SCRIPT.new(
placement_key,
root,
render_resource
)
_queued_placement_keys.append(placement_key)
return true
## Returns whether at least one FIFO key, including a stale key, is pending.
func has_pending() -> bool:
return not _queued_placement_keys.is_empty()
## Returns the front FIFO key, or an empty String when the queue is empty.
func front_key() -> String:
if _queued_placement_keys.is_empty():
return ""
return _queued_placement_keys.front()
## Returns whether a FIFO placement key still has a current job record.
func has_job(placement_key: String) -> bool:
return _jobs_by_placement_key.has(placement_key)
## Returns the current job record as its engine base type, or null for a stale
## key. Runtime consumers should prefer the typed accessors below.
func job_for(placement_key: String) -> RefCounted:
return _jobs_by_placement_key.get(placement_key, null) as RefCounted
## Returns the borrowed root for a current job, or null for a stale key.
func root_for(placement_key: String) -> Node3D:
var job := job_for(placement_key)
if job == null:
return null
return job.call("root") as Node3D
## Returns the borrowed render resource for a current job, or null when stale.
func render_resource_for(placement_key: String) -> Resource:
var job := job_for(placement_key)
if job == null:
return null
return job.call("render_resource") as Resource
## Returns the current mesh cursor, or zero for a stale key.
func mesh_index_for(placement_key: String) -> int:
var job := job_for(placement_key)
if job == null:
return 0
return int(job.call("mesh_index"))
## Returns the current MultiMesh cursor, or zero for a stale key.
func multimesh_index_for(placement_key: String) -> int:
var job := job_for(placement_key)
if job == null:
return 0
return int(job.call("multimesh_index"))
## Adopts both cursors for a current job and returns whether it existed.
func adopt_cursors(
placement_key: String,
next_mesh_index: int,
next_multimesh_index: int
) -> bool:
var job := job_for(placement_key)
if job == null:
return false
job.call("adopt_cursors", next_mesh_index, next_multimesh_index)
return true
## Pops and returns the front FIFO key, or an empty String when already empty.
func pop_front() -> String:
if _queued_placement_keys.is_empty():
return ""
return _queued_placement_keys.pop_front()
## Cancels the keyed job and erases only the first matching FIFO key. Returns
## whether an active job record existed. Engine objects are never freed here.
func cancel(placement_key: String) -> bool:
var had_job := _jobs_by_placement_key.has(placement_key)
_jobs_by_placement_key.erase(placement_key)
_queued_placement_keys.erase(placement_key)
return had_job
## Releases all job/key references without freeing referenced engine objects.
func clear() -> void:
_jobs_by_placement_key.clear()
_queued_placement_keys.clear()
## Returns FIFO entry count, including duplicate and stale keys.
func pending_count() -> int:
return _queued_placement_keys.size()
## Returns active keyed job count, excluding stale FIFO keys.
func active_job_count() -> int:
return _jobs_by_placement_key.size()
## Returns detached FIFO order and placement-key-sorted scalar job diagnostics.
func diagnostic_snapshot() -> Dictionary:
var placement_keys: Array = _jobs_by_placement_key.keys()
placement_keys.sort()
var jobs: Array[Dictionary] = []
for placement_key_variant in placement_keys:
var placement_key := String(placement_key_variant)
var job: RefCounted = job_for(placement_key)
if job != null:
jobs.append(job.call("diagnostic_snapshot") as Dictionary)
return {
"queued_placement_keys": _queued_placement_keys.duplicate(),
"jobs": jobs,
}
@@ -0,0 +1 @@
uid://b165sgg76lrt8
+26 -32
View File
@@ -18,6 +18,9 @@ const WMO_PLACEMENT_REGISTRY_SCRIPT := preload(
const WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT := preload( const WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT := preload(
"res://src/render/wmo/wmo_render_build_step_planner.gd" "res://src/render/wmo/wmo_render_build_step_planner.gd"
) )
const WMO_RENDER_BUILD_QUEUE_SCRIPT := preload(
"res://src/render/wmo/wmo_render_build_queue.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_ANIMATED_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd") const M2_NATIVE_ANIMATED_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_native_animated_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")
@@ -280,8 +283,7 @@ var _wmo_prototype_cache: Dictionary = {}
var _wmo_render_cache: Dictionary = {} var _wmo_render_cache: Dictionary = {}
var _wmo_render_missing_cache: Dictionary = {} var _wmo_render_missing_cache: Dictionary = {}
var _wmo_render_load_requests: Dictionary = {} var _wmo_render_load_requests: Dictionary = {}
var _wmo_render_build_jobs: Dictionary = {} var _wmo_render_build_queue := WMO_RENDER_BUILD_QUEUE_SCRIPT.new()
var _wmo_render_build_queue: Array = []
var _wmo_scene_resource_cache: Dictionary = {} var _wmo_scene_resource_cache: Dictionary = {}
var _wmo_scene_cache_missing: Dictionary = {} var _wmo_scene_cache_missing: Dictionary = {}
var _wmo_scene_load_requests: Dictionary = {} var _wmo_scene_load_requests: Dictionary = {}
@@ -1004,7 +1006,7 @@ func _log_hitch_profile(start_usec: int, timings: Array[String], did_refresh: bo
_m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(), _m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(),
_m2_build_queue.size(), _m2_build_queue.size(),
_wmo_build_queue.size() + _wmo_scene_load_requests.size() + _wmo_render_load_requests.size(), _wmo_build_queue.size() + _wmo_scene_load_requests.size() + _wmo_render_load_requests.size(),
_wmo_render_build_queue.size(), _wmo_render_build_queue.pending_count(),
_tile_lod_create_queue.size(), _tile_lod_create_queue.size(),
_tile_lod_remove_queue.size(), _tile_lod_remove_queue.size(),
_chunk_create_queue.size(), _chunk_create_queue.size(),
@@ -1031,7 +1033,7 @@ func render_baseline_snapshot() -> Dictionary:
"m2_mesh": _m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(), "m2_mesh": _m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(),
"m2_build": _m2_build_queue.size(), "m2_build": _m2_build_queue.size(),
"wmo_build": _wmo_build_queue.size() + _wmo_scene_load_requests.size() + _wmo_render_load_requests.size(), "wmo_build": _wmo_build_queue.size() + _wmo_scene_load_requests.size() + _wmo_render_load_requests.size(),
"wmo_groups": _wmo_render_build_queue.size(), "wmo_groups": _wmo_render_build_queue.pending_count(),
"lod_create": _tile_lod_create_queue.size(), "lod_create": _tile_lod_create_queue.size(),
"lod_remove": _tile_lod_remove_queue.size(), "lod_remove": _tile_lod_remove_queue.size(),
"chunk_create": _chunk_create_queue.size(), "chunk_create": _chunk_create_queue.size(),
@@ -1313,7 +1315,7 @@ func _tick_runtime_stats(delta: float) -> void:
_m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(), _m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(),
_m2_build_queue.size(), _m2_build_queue.size(),
_wmo_build_queue.size() + _wmo_scene_load_requests.size() + _wmo_render_load_requests.size(), _wmo_build_queue.size() + _wmo_scene_load_requests.size() + _wmo_render_load_requests.size(),
_wmo_render_build_queue.size(), _wmo_render_build_queue.pending_count(),
_tile_lod_create_queue.size(), _tile_lod_create_queue.size(),
_tile_lod_remove_queue.size(), _tile_lod_remove_queue.size(),
_chunk_create_queue.size(), _chunk_create_queue.size(),
@@ -2364,7 +2366,6 @@ func _wait_for_tile_tasks() -> void:
_wmo_render_load_requests.clear() _wmo_render_load_requests.clear()
_wmo_render_missing_cache.clear() _wmo_render_missing_cache.clear()
_wmo_scene_cache_missing.clear() _wmo_scene_cache_missing.clear()
_wmo_render_build_jobs.clear()
_wmo_render_build_queue.clear() _wmo_render_build_queue.clear()
for pending in _terrain_upgrade_tasks.values(): for pending in _terrain_upgrade_tasks.values():
@@ -3004,7 +3005,6 @@ func _clear_streamed_world() -> void:
_wmo_render_load_requests.clear() _wmo_render_load_requests.clear()
_wmo_render_missing_cache.clear() _wmo_render_missing_cache.clear()
_wmo_scene_cache_missing.clear() _wmo_scene_cache_missing.clear()
_wmo_render_build_jobs.clear()
_wmo_render_build_queue.clear() _wmo_render_build_queue.clear()
_tile_result_mutex.lock() _tile_result_mutex.lock()
@@ -3761,32 +3761,23 @@ func _instantiate_wmo_render_root(rel_path: String, placement: Dictionary) -> No
func _start_wmo_render_build(unique_key: String, root: Node3D, render_resource: Resource) -> void: func _start_wmo_render_build(unique_key: String, root: Node3D, render_resource: Resource) -> void:
if unique_key.is_empty() or root == null or render_resource == null: _wmo_render_build_queue.enqueue(unique_key, root, render_resource)
return
_wmo_render_build_jobs[unique_key] = {
"root": root,
"resource": render_resource,
"mesh_index": 0,
"multimesh_index": 0,
}
_wmo_render_build_queue.append(unique_key)
func _process_wmo_render_build_jobs() -> void: func _process_wmo_render_build_jobs() -> void:
while _render_budget_scheduler.has_remaining_permit( while _render_budget_scheduler.has_remaining_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_RENDER_GROUP_BUILD) and not _wmo_render_build_queue.is_empty(): RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_RENDER_GROUP_BUILD) and _wmo_render_build_queue.has_pending():
var unique_key := String(_wmo_render_build_queue.front()) var unique_key := _wmo_render_build_queue.front_key()
if not _wmo_render_build_jobs.has(unique_key): if not _wmo_render_build_queue.has_job(unique_key):
_wmo_render_build_queue.pop_front() _wmo_render_build_queue.pop_front()
continue continue
var job: Dictionary = _wmo_render_build_jobs[unique_key] var root: Node = _wmo_render_build_queue.root_for(unique_key)
var root: Node = job.get("root", null)
if root == null or not is_instance_valid(root): if root == null or not is_instance_valid(root):
_cancel_wmo_render_build_job(unique_key) _cancel_wmo_render_build_job(unique_key)
continue continue
var render_resource: Resource = job.get("resource", null) var render_resource: Resource = _wmo_render_build_queue.render_resource_for(unique_key)
if render_resource == null: if render_resource == null:
_cancel_wmo_render_build_job(unique_key) _cancel_wmo_render_build_job(unique_key)
continue continue
@@ -3794,11 +3785,11 @@ func _process_wmo_render_build_jobs() -> void:
var meshes: Array = render_resource.get("meshes") var meshes: Array = render_resource.get("meshes")
var mesh_transforms: Array = render_resource.get("mesh_transforms") var mesh_transforms: Array = render_resource.get("mesh_transforms")
var mesh_names: PackedStringArray = render_resource.get("mesh_names") var mesh_names: PackedStringArray = render_resource.get("mesh_names")
var mesh_index: int = int(job.get("mesh_index", 0)) var mesh_index := _wmo_render_build_queue.mesh_index_for(unique_key)
var multimeshes: Array = render_resource.get("multimeshes") var multimeshes: Array = render_resource.get("multimeshes")
var multimesh_transforms: Array = render_resource.get("multimesh_transforms") var multimesh_transforms: Array = render_resource.get("multimesh_transforms")
var multimesh_names: PackedStringArray = render_resource.get("multimesh_names") var multimesh_names: PackedStringArray = render_resource.get("multimesh_names")
var multimesh_index: int = int(job.get("multimesh_index", 0)) var multimesh_index := _wmo_render_build_queue.multimesh_index_for(unique_key)
var build_step: Dictionary = _wmo_render_build_step_planner.plan_step( var build_step: Dictionary = _wmo_render_build_step_planner.plan_step(
meshes.size(), meshes.size(),
mesh_index, mesh_index,
@@ -3829,8 +3820,11 @@ func _process_wmo_render_build_jobs() -> void:
mesh_instance.visibility_range_end_margin = CHUNK_SIZE mesh_instance.visibility_range_end_margin = CHUNK_SIZE
(root as Node3D).add_child(mesh_instance) (root as Node3D).add_child(mesh_instance)
_set_editor_owner_recursive(mesh_instance) _set_editor_owner_recursive(mesh_instance)
job["mesh_index"] = int(build_step["next_mesh_index"]) _wmo_render_build_queue.adopt_cursors(
_wmo_render_build_jobs[unique_key] = job unique_key,
int(build_step["next_mesh_index"]),
int(build_step["next_multimesh_index"])
)
_render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_RENDER_GROUP_BUILD) _render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_RENDER_GROUP_BUILD)
continue continue
@@ -3857,8 +3851,11 @@ func _process_wmo_render_build_jobs() -> void:
multimesh_instance.visibility_range_end_margin = CHUNK_SIZE multimesh_instance.visibility_range_end_margin = CHUNK_SIZE
(root as Node3D).add_child(multimesh_instance) (root as Node3D).add_child(multimesh_instance)
_set_editor_owner_recursive(multimesh_instance) _set_editor_owner_recursive(multimesh_instance)
job["multimesh_index"] = int(build_step["next_multimesh_index"]) _wmo_render_build_queue.adopt_cursors(
_wmo_render_build_jobs[unique_key] = job unique_key,
int(build_step["next_mesh_index"]),
int(build_step["next_multimesh_index"])
)
_render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_RENDER_GROUP_BUILD) _render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_RENDER_GROUP_BUILD)
continue continue
@@ -3866,9 +3863,7 @@ func _process_wmo_render_build_jobs() -> void:
func _cancel_wmo_render_build_job(unique_key: String) -> void: func _cancel_wmo_render_build_job(unique_key: String) -> void:
if _wmo_render_build_jobs.has(unique_key): _wmo_render_build_queue.cancel(unique_key)
_wmo_render_build_jobs.erase(unique_key)
_wmo_render_build_queue.erase(unique_key)
func _get_resource_file_size(path: String) -> int: func _get_resource_file_size(path: String) -> int:
@@ -4043,7 +4038,6 @@ func _unregister_tile_wmos(state: Dictionary) -> void:
func _clear_wmo_registry() -> void: func _clear_wmo_registry() -> void:
_wmo_render_build_jobs.clear()
_wmo_render_build_queue.clear() _wmo_render_build_queue.clear()
for node_variant in _wmo_instance_nodes_by_unique_key.values(): for node_variant in _wmo_instance_nodes_by_unique_key.values():
if not (node_variant is Node): if not (node_variant is Node):
+205
View File
@@ -0,0 +1,205 @@
extends SceneTree
## Asset-free lifecycle, ordering, ownership and timing regression for WMO jobs.
const QUEUE_SCRIPT := preload("res://src/render/wmo/wmo_render_build_queue.gd")
const QUEUE_PATH := "res://src/render/wmo/wmo_render_build_queue.gd"
const JOB_PATH := "res://src/render/wmo/wmo_render_build_job.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_invalid_and_fifo_jobs(failures)
_verify_cursor_adoption(failures)
_verify_duplicate_and_stale_key_behavior(failures)
_verify_cancel_clear_and_engine_lifetime(failures)
_verify_detached_sorted_diagnostics(failures)
_verify_loader_and_engine_lifetime_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("WMO_RENDER_BUILD_QUEUE: %s" % failure)
quit(1)
return
print(
"WMO_RENDER_BUILD_QUEUE PASS cases=11 iterations=100 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_invalid_and_fifo_jobs(failures: Array[String]) -> void:
var queue: RefCounted = QUEUE_SCRIPT.new()
var root := Node3D.new()
var render_resource := Resource.new()
_expect_false(queue.call("enqueue", "", root, render_resource), "empty key rejected", failures)
_expect_false(queue.call("enqueue", "uid:1", null, render_resource), "null root rejected", failures)
_expect_false(queue.call("enqueue", "uid:1", root, null), "null resource rejected", failures)
_expect_true(queue.call("enqueue", "uid:2", root, render_resource), "first enqueue", failures)
_expect_true(queue.call("enqueue", "uid:3", root, render_resource), "second enqueue", failures)
_expect_string_equal(String(queue.call("front_key")), "uid:2", "FIFO front", failures)
_expect_equal(int(queue.call("pending_count")), 2, "FIFO pending count", failures)
_expect_equal(int(queue.call("active_job_count")), 2, "FIFO active count", failures)
var first_job: RefCounted = queue.call("job_for", "uid:2")
_expect_string_equal(String(first_job.call("placement_key")), "uid:2", "typed job key", failures)
_expect_true(first_job.call("root") == root, "job retains root", failures)
_expect_true(first_job.call("render_resource") == render_resource, "job retains resource", failures)
queue.call("clear")
root.free()
func _verify_cursor_adoption(failures: Array[String]) -> void:
var queue: RefCounted = QUEUE_SCRIPT.new()
var root := Node3D.new()
queue.call("enqueue", "uid:4", root, Resource.new())
var job: RefCounted = queue.call("job_for", "uid:4")
_expect_equal(int(job.call("mesh_index")), 0, "initial mesh cursor", failures)
_expect_equal(int(job.call("multimesh_index")), 0, "initial multimesh cursor", failures)
job.call("adopt_cursors", 7, 3)
_expect_equal(int(job.call("mesh_index")), 7, "adopted mesh cursor", failures)
_expect_equal(int(job.call("multimesh_index")), 3, "adopted multimesh cursor", failures)
queue.call("clear")
root.free()
func _verify_duplicate_and_stale_key_behavior(failures: Array[String]) -> void:
var queue: RefCounted = QUEUE_SCRIPT.new()
var first_root := Node3D.new()
var second_root := Node3D.new()
queue.call("enqueue", "uid:5", first_root, Resource.new())
queue.call("enqueue", "uid:5", second_root, Resource.new())
_expect_equal(int(queue.call("pending_count")), 2, "duplicate keys append", failures)
_expect_equal(int(queue.call("active_job_count")), 1, "duplicate job replaces", failures)
var replacement_job: RefCounted = queue.call("job_for", "uid:5")
_expect_true(replacement_job.call("root") == second_root, "duplicate latest job wins", failures)
_expect_true(queue.call("cancel", "uid:5"), "duplicate cancel finds job", failures)
_expect_equal(int(queue.call("pending_count")), 1, "cancel erases first duplicate key", failures)
_expect_true(queue.call("job_for", "uid:5") == null, "remaining duplicate key is stale", failures)
_expect_string_equal(String(queue.call("pop_front")), "uid:5", "stale front pop", failures)
_expect_false(queue.call("has_pending"), "stale queue drained", failures)
first_root.free()
second_root.free()
func _verify_cancel_clear_and_engine_lifetime(failures: Array[String]) -> void:
var queue: RefCounted = QUEUE_SCRIPT.new()
var root := Node3D.new()
queue.call("enqueue", "uid:6", root, Resource.new())
_expect_false(queue.call("cancel", "uid:missing"), "unknown cancel", failures)
_expect_true(is_instance_valid(root), "unknown cancel keeps root valid", failures)
queue.call("clear")
queue.call("clear")
_expect_equal(int(queue.call("pending_count")), 0, "clear idempotent", failures)
_expect_true(is_instance_valid(root), "clear does not free root", failures)
root.free()
func _verify_detached_sorted_diagnostics(failures: Array[String]) -> void:
var queue: RefCounted = QUEUE_SCRIPT.new()
var root := Node3D.new()
queue.call("enqueue", "uid:20", root, Resource.new())
queue.call("enqueue", "uid:10", root, Resource.new())
var snapshot: Dictionary = queue.call("diagnostic_snapshot")
_expect_string_array(
snapshot["queued_placement_keys"],
["uid:20", "uid:10"],
"diagnostic queue order",
failures
)
var jobs: Array = snapshot["jobs"]
_expect_string_equal(String(jobs[0]["placement_key"]), "uid:10", "diagnostic job sort", failures)
(snapshot["queued_placement_keys"] as Array).append("mutated")
jobs[0]["mesh_index"] = 99
var fresh_snapshot: Dictionary = queue.call("diagnostic_snapshot")
_expect_string_array(
fresh_snapshot["queued_placement_keys"],
["uid:20", "uid:10"],
"diagnostic queue detached",
failures
)
_expect_equal(int((fresh_snapshot["jobs"] as Array)[0]["mesh_index"]), 0, "job diagnostic detached", failures)
queue.call("clear")
root.free()
func _verify_loader_and_engine_lifetime_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures)
var queue_source := _read_text(QUEUE_PATH, failures)
var job_source := _read_text(JOB_PATH, failures)
_expect_true(
loader_source.contains("WMO_RENDER_BUILD_QUEUE_SCRIPT.new()"),
"loader composes queue",
failures
)
_expect_true(loader_source.contains("_wmo_render_build_queue.enqueue("), "loader delegates enqueue", failures)
_expect_true(loader_source.contains("_wmo_render_build_queue.has_job("), "loader checks stale keys", failures)
_expect_true(loader_source.contains("_wmo_render_build_queue.root_for("), "loader borrows root", failures)
_expect_true(loader_source.contains("_wmo_render_build_queue.render_resource_for("), "loader borrows resource", failures)
_expect_true(loader_source.contains("_wmo_render_build_queue.adopt_cursors("), "loader delegates cursors", failures)
_expect_true(loader_source.contains("_wmo_render_build_queue.cancel("), "loader delegates cancel", failures)
_expect_true(loader_source.contains("_wmo_render_build_queue.clear()"), "loader delegates clear", failures)
_expect_false(loader_source.contains("var _wmo_render_build_jobs"), "legacy job dictionary removed", failures)
_expect_true(
loader_source.contains("MeshInstance3D.new()")
and loader_source.contains("MultiMeshInstance3D.new()")
and loader_source.contains("try_consume_permit("),
"loader retains materialization and permits",
failures
)
for forbidden_token in ["queue_free", "RenderingServer", "RID", "MeshInstance3D", "MultiMeshInstance3D", "FileAccess", "Thread", "Mutex"]:
_expect_false(
queue_source.contains(forbidden_token) or job_source.contains(forbidden_token),
"queue/job omit %s ownership" % forbidden_token,
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var queue: RefCounted = QUEUE_SCRIPT.new()
var root := Node3D.new()
var render_resource := Resource.new()
var started_microseconds := Time.get_ticks_usec()
for _iteration in range(100):
for job_index in range(256):
queue.call("enqueue", "uid:%d" % job_index, root, render_resource)
for job_index in range(256):
queue.call("cancel", "uid:%d" % job_index)
_expect_equal(int(queue.call("pending_count")), 0, "timing cycle drains", failures)
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_true(elapsed_milliseconds < 1000.0, "100 queue cycles remain bounded", failures)
root.free()
return elapsed_milliseconds
func _read_text(path: String, failures: Array[String]) -> String:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
failures.append("cannot open %s" % path)
return ""
return file.get_as_text()
func _expect_equal(actual_value: int, expected_value: int, label: String, failures: Array[String]) -> void:
if actual_value != expected_value:
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
func _expect_string_equal(actual_value: String, expected_value: String, label: String, failures: Array[String]) -> void:
if actual_value != expected_value:
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
func _expect_string_array(actual_values: Array, expected_values: Array, label: String, failures: Array[String]) -> void:
if actual_values != expected_values:
failures.append("%s expected %s, got %s" % [label, expected_values, actual_values])
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
if not actual_value:
failures.append("%s expected true" % label)
func _expect_false(actual_value: bool, label: String, failures: Array[String]) -> void:
if actual_value:
failures.append("%s expected false" % label)
@@ -0,0 +1 @@
uid://s6bsyt6bv1uq
+17 -4
View File
@@ -27,7 +27,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
## Evidence ## Evidence
- Date: 2026-07-17 - Date: 2026-07-17
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`, `f88bf97`, `3f84717`, `d2cb52f`, `f9e5a4f`, `c06aed5`; - Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`, `f88bf97`, `3f84717`, `d2cb52f`, `f9e5a4f`, `c06aed5`, `a878e7c`, `60603e1`;
packages `M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-001`, packages `M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-001`,
`M03-RND-FACADE-GROUND-QUERY-001`, `M03-RND-FACADE-ENVIRONMENT-001`, `M03-RND-FACADE-GROUND-QUERY-001`, `M03-RND-FACADE-ENVIRONMENT-001`,
`M03-RND-FACADE-ENTITY-001`, `M03-RND-TERRAIN-CACHE-SERVICE-001`, `M03-RND-FACADE-ENTITY-001`, `M03-RND-TERRAIN-CACHE-SERVICE-001`,
@@ -38,7 +38,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
`M03-RND-M2-BUILD-BATCH-PLANNER-001`, `M03-RND-M2-BUILD-BATCH-PLANNER-001`,
`M03-RND-WMO-PLACEMENT-RESOLVER-001`, `M03-RND-WMO-PLACEMENT-RESOLVER-001`,
`M03-RND-WMO-PLACEMENT-REGISTRY-001`, `M03-RND-WMO-PLACEMENT-REGISTRY-001`,
`M03-RND-WMO-RENDER-BUILD-PLANNER-001` `M03-RND-WMO-RENDER-BUILD-PLANNER-001`,
`M03-RND-WMO-RENDER-BUILD-QUEUE-001`
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown - Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates. headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner - Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner
@@ -85,6 +86,13 @@ Runtime и Editor используют facade; planner/scheduler тестиру
regressions and the `7/7` checkpoint dry-run passed; post-merge planner regressions and the `7/7` checkpoint dry-run passed; post-merge planner
(`11.059ms`), registry, resolver, shutdown, facade, documentation and (`11.059ms`), registry, resolver, shutdown, facade, documentation and
coordination gates remained green. coordination gates remained green.
WMO render build queue passed `cases=11 iterations=100 elapsed_ms=59.874`;
WMO planner/registry/resolver/material/shutdown, 18 adjacent regressions and
the `7/7` checkpoint dry-run passed. The first post-merge smoke exposed an
editor class-cache dependency; a fresh no-`.godot` run verified the fix
(`57.835ms`), and post-fix master passed queue (`56.454ms`), planner,
registry, resolver, shutdown, facade, documentation and coordination gates
without parse diagnostics.
M02 terrain-query regression M02 terrain-query regression
remained green (13 pre-existing expired M00 claim warnings). remained green (13 pre-existing expired M00 claim warnings).
- Fidelity comparison: all 16 historical operation limits and drain sites were - Fidelity comparison: all 16 historical operation limits and drain sites were
@@ -122,6 +130,10 @@ Runtime и Editor используют facade; planner/scheduler тестиру
Lightweight WMO render groups retain mesh-before-MultiMesh order, one cursor Lightweight WMO render groups retain mesh-before-MultiMesh order, one cursor
increment and one budget permit per valid or null group; completion still increment and one budget permit per valid or null group; completion still
cancels the job after both raw integer cursor comparisons are exhausted. cancels the job after both raw integer cursor comparisons are exhausted.
Pending WMO render builds retain typed root/resource/cursors, FIFO order,
duplicate replacement, stale-front pop, first-occurrence cancel and three
idempotent clear sites. Queue release never frees engine objects; the loader
retains validity checks, materialization, permits and Node destruction.
No new build-12340 parity claim is made; asset-backed visual/p95/p99 comparison remains pending. No new build-12340 parity claim is made; asset-backed visual/p95/p99 comparison remains pending.
- Changed files: scene-free scheduler and verifier; streamer budget composition/ - Changed files: scene-free scheduler and verifier; streamer budget composition/
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes; permit delegation; scheduler module spec; world renderer/module registry/RENDER notes;
@@ -138,6 +150,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
stateless WMO placement resolver and seven cache/identity/transform adapters, stateless WMO placement resolver and seven cache/identity/transform adapters,
stateful WMO placement registry and global/tile add/release/clear adapters, stateful WMO placement registry and global/tile add/release/clear adapters,
stateless WMO render build step planner and loader cursor-adoption adapter, stateless WMO render build step planner and loader cursor-adoption adapter,
typed WMO render build job/queue and loader enqueue/drain/cancel/clear adapters,
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence. expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
- Remaining risks: queue storage length and worker concurrency/stale-result logic - Remaining risks: queue storage length and worker concurrency/stale-result logic
remain streamer-owned; cancellation stops new permits but does not interrupt remain streamer-owned; cancellation stops new permits but does not interrupt
@@ -152,7 +165,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
asset-backed visual transform recheck, grouping/build/tasks/caches/finalization asset-backed visual transform recheck, grouping/build/tasks/caches/finalization
and boundary traversal p95/p99 remain pending; path grouping is not yet and boundary traversal p95/p99 remain pending; path grouping is not yet
culling-driven spatial-cell grouping; M2 resource/queue transitions and raw culling-driven spatial-cell grouping; M2 resource/queue transitions and raw
build-job Dictionaries remain in the loader; WMO build job/resource state and WMO cache/load requests and group materialization remain in the loader;
asset-backed placement/portal/material evidence remain pending; asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending;
M03 still needs further terrain/M2/WMO/liquid service extraction M03 still needs further terrain/M2/WMO/liquid service extraction
and dependency/performance acceptance. and dependency/performance acceptance.