Compare commits
8 Commits
1e740aaa65
...
c9547041da
| Author | SHA1 | Date | |
|---|---|---|---|
| c9547041da | |||
| 60603e18c8 | |||
| 3411156f8f | |||
| e576ae2cad | |||
| a878e7c4e7 | |||
| ab40a76b62 | |||
| e1bb105fff | |||
| 040b635ec6 |
@@ -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 -->
|
||||||
@@ -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) |
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user