Compare commits

...

28 Commits

Author SHA1 Message Date
sindoring 4387c66c0a docs(M03): accept WMO scene resource cache state 2026-07-17 01:20:29 +04:00
sindoring ada0fd9467 merge M03 WMO scene resource cache state 2026-07-17 01:19:40 +04:00
sindoring ff4f320412 docs(M03): hand off WMO scene resource cache state 2026-07-17 01:19:36 +04:00
sindoring c83929428d refactor(M03): extract WMO scene resource cache state 2026-07-17 01:19:19 +04:00
sindoring 71fa093118 coord(M03): claim WMO scene resource cache state 2026-07-17 01:14:11 +04:00
sindoring e690756301 docs(M03): accept WMO render resource cache state 2026-07-17 01:12:30 +04:00
sindoring b4955d6162 merge M03 WMO render resource cache state 2026-07-17 01:11:32 +04:00
sindoring b4b94b3cbb docs(M03): hand off WMO render resource cache state 2026-07-17 01:11:27 +04:00
sindoring d39f591e50 refactor(M03): extract WMO render resource cache state 2026-07-17 01:11:11 +04:00
sindoring 4314f961f6 coord(M03): claim WMO render resource cache state 2026-07-17 01:04:15 +04:00
sindoring c9547041da docs(M03): accept WMO render build queue 2026-07-17 01:00:40 +04:00
sindoring 60603e18c8 merge M03 WMO render build queue cold-load fix 2026-07-17 00:59:50 +04:00
sindoring 3411156f8f docs(M03): refresh WMO queue handoff 2026-07-17 00:59:44 +04:00
sindoring e576ae2cad fix(M03): make WMO build queue cold-load safe 2026-07-17 00:59:11 +04:00
sindoring a878e7c4e7 merge M03 WMO render build queue 2026-07-17 00:57:20 +04:00
sindoring ab40a76b62 docs(M03): hand off WMO render build queue 2026-07-17 00:57:11 +04:00
sindoring e1bb105fff refactor(M03): extract WMO render build queue 2026-07-17 00:56:55 +04:00
sindoring 040b635ec6 coord(M03): claim WMO render build queue 2026-07-17 00:51:33 +04:00
sindoring 1e740aaa65 docs(M03): accept WMO render build planner 2026-07-17 00:49:17 +04:00
sindoring c06aed568b merge M03 WMO render build planner 2026-07-17 00:48:26 +04:00
sindoring 01bad4ca26 docs(M03): hand off WMO render build planner 2026-07-17 00:48:13 +04:00
sindoring 3a6b1d306d refactor(M03): extract WMO render build planner 2026-07-17 00:47:54 +04:00
sindoring e420a8724f coord(M03): claim WMO render build planner 2026-07-17 00:42:19 +04:00
sindoring 1e73e597f8 docs(M03): accept WMO placement registry 2026-07-17 00:34:25 +04:00
sindoring f9e5a4f2e0 merge M03 WMO placement registry 2026-07-17 00:33:34 +04:00
sindoring 16053b46ca docs(M03): hand off WMO placement registry 2026-07-17 00:33:23 +04:00
sindoring c5900368d4 refactor(M03): extract WMO placement registry 2026-07-17 00:33:09 +04:00
sindoring 79bc4e6ffc coord(M03): claim WMO placement registry 2026-07-17 00:26:32 +04:00
39 changed files with 3905 additions and 162 deletions
+40
View File
@@ -27,6 +27,12 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
- `src/render/world_render_facade.gd` - стабильная граница runtime/tools для focus и metrics.
- `src/scenes/streaming/streaming_world_loader.gd` - внутренний runtime streamer мира.
- `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_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/render/wmo/wmo_render_resource_cache_state.gd` - validated lightweight WMO render Resources, negative cache and pending cache paths without ResourceLoader I/O.
- `src/render/wmo/wmo_scene_resource_cache_state.gd` - validated cached-WMO PackedScenes, negative cache and pending `.tscn` paths without file/I/O/Node ownership.
- `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/m2_builder.gd` - сборка статического M2 mesh/material.
@@ -394,6 +400,12 @@ Known limits:
- `WMOBuilder` строит group meshes;
- runtime registry дедуплицирует WMO по `unique_id`;
- WMO на границе ADT не дублируются;
- `WmoPlacementRegistry` owns only placement reference sets; the streamer keeps
the parallel key-to-Node map and performs final-release cancellation/free;
- `WmoRenderBuildStepPlanner` selects one mesh or MultiMesh group and its next
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` во время движения;
- добавлен lightweight render-cache path для WMO groups;
- `wmo_render_group_ops_per_tick` ограничивает подключение WMO groups по кадрам;
@@ -1037,6 +1049,34 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- Cache formats, placement values, profiles and visible rules are unchanged.
Asset-backed placement/p95/p99 and general WMO parity remain pending.
## 2026-07-17 WMO Render Resource Cache State Extraction
- `WmoRenderResourceCacheState` now owns validated lightweight-WMO render
Resources, negative entries and normalized-path to pending-cache-path records.
- `StreamingWorldLoader` still constructs cache paths, calls `ResourceLoader`,
polls requests and validates `WMOStreamingResource` script identity plus
`FORMAT_VERSION` before completing cache state.
- Map reset and orderly request draining clear pending/negative state while
retaining accepted Resources; final runtime cache release clears all state.
- Missing render-cache files still are not negatively cached, preserving retry
and cached-scene/live fallback behavior. Formats, profiles and visuals are unchanged.
- Asset-backed corrupt-cache, traversal/leak p95/p99 and paired fidelity evidence
remain pending.
## 2026-07-17 WMO Scene Resource Cache State Extraction
- `WmoSceneResourceCacheState` now owns validated cached-WMO PackedScenes,
negative entries and normalized-path to pending-`.tscn` records.
- `StreamingWorldLoader` still checks file existence and
`wmo_max_runtime_scene_mb`, calls `ResourceLoader`, instantiates a validation
probe, checks WMOBuilder cache metadata and frees the probe before adoption.
- Missing files, oversize scenes, request errors, load failures and stale scenes
retain their prior negative-cache and live-prototype fallback behavior.
- Map reset clears pending/negative state while retaining accepted scenes; final
shutdown releases all scene state. Formats, profiles and visuals are unchanged.
- Asset-backed oversize/stale fixtures, traversal/leak p95/p99 and paired fidelity
evidence remain pending.
## 2026-07-16 World Environment Snapshot Facade
- `WorldEnvironmentSnapshot` carries one immutable finite time-of-day value,
@@ -0,0 +1,106 @@
# M03-RND-WMO-PLACEMENT-REGISTRY-001 — WMO placement registry
<!-- OPENWC_CLAIM:M03-RND-WMO-PLACEMENT-REGISTRY-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-placement-registry`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract WMO placement-key to tile-reference ownership from the loader while
leaving every Node and renderer-resource lifecycle in the loader.
## Non-goals
- Change placement identity rules or WMO transforms.
- Move Node maps, world roots, tile state, jobs, queues or cancellation.
- Move cache/load requests, material refresh or render-group building.
- Change duplicate ordering, global-WMO lifetime or build budgets.
- Claim asset-backed WMO fidelity or performance acceptance.
## Paths
- Exclusive: `src/render/wmo/wmo_placement_registry.gd`,
`src/tools/verify_wmo_placement_registry.gd`,
`docs/modules/wmo-placement-registry.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`docs/modules/wmo-placement-resolver.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
- Input: resolved WMO placement key and tile/global reference key
- `add_reference` creates a key/ref set or idempotently retains an existing ref
- `release_reference` erases only the supplied ref and returns true only when the
final ref removes the placement key
- `contains`, `active_count`, detached sorted diagnostics and idempotent `clear`
- Loader retains key→Node mapping and reacts to final release by cancelling the
render build and queue-freeing the corresponding Node
- Cache/schema/coordinate/profile versions: unchanged
## Dependencies
- Requires: current master `5aa40d6`, `WmoPlacementResolver` identity contract
- Blocks: WMO build/resource state decomposition
- External state: none; tests use synthetic placement/tile keys
## Verification
- Commands: dedicated add/shared/idempotent/non-owner/final/global/clear/
diagnostics/source/timing verifier; resolver/material/shutdown plus M2/terrain/
facade/internal-access/manifest/scheduler/streaming/coordinate/docs/coord gates
- Fidelity evidence: exact shared-ref and final-release behavior preserved
- Performance budget: dictionary membership over active WMO keys/refs; no Node,
Resource, RID, task, queue, mutex, cache or file access
## Documentation deliverables
- Inline registry API documentation
- New module spec with inputs/outputs, data-flow, lifecycle/state, ownership,
failure/recovery, performance, fidelity limitations and source map
- Placement resolver, renderer/module registry and `RENDER.md` updates
## Simplicity and naming
- Important name: `WmoPlacementRegistry`
- Simplest solution: one private key-to-reference-set Dictionary
- Rejected complexity: generic ref-count registry, signals, callbacks, Resources,
weak refs or Node ownership inside the service
- Unavoidable complexity: loader keeps a parallel key-to-Node map
- Measured optimization evidence: bounded synthetic add/release loop
## Status
- State: accepted
- Done: registry extracted; global/tile adapters migrated; Node/resource/job
ownership retained in loader; contract, regressions, dry-run and docs passed
- Next: WMO build/resource state extraction and asset-backed placement evidence
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-PLACEMENT-REGISTRY-001:c590036 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-PLACEMENT-REGISTRY-001:f9e5a4f -->
## Handoff
- Commit: `c590036`
- Results: `verify_wmo_placement_registry.gd` PASS cases=10,
iterations=100, elapsed_ms=53.941; 21 adjacent WMO/M2/terrain/facade/
scheduler/streaming/coordinate regressions PASS; documentation and
coordination gates PASS; render dry-run PASS 7/7.
- Remaining risks: no proprietary asset corpus or original-client paired WMO
capture; dry-run reports expected missing ADT/DBC/character assets; WMO
build/resource state remains loader-owned.
- Documentation updated: `docs/modules/wmo-placement-registry.md`,
`docs/modules/wmo-placement-resolver.md`, `docs/modules/world-renderer.md`,
`docs/modules/README.md`, `RENDER.md`; inline public API comments added.
- Integration: accepted in master merge `f9e5a4f`; post-merge registry
(`cases=10`, `iterations=100`, `elapsed_ms=54.178`), resolver, runtime-cache
shutdown, facade, documentation and coordination gates passed.
@@ -0,0 +1,108 @@
# M03-RND-WMO-RENDER-BUILD-PLANNER-001 — WMO render build step planner
<!-- OPENWC_CLAIM:M03-RND-WMO-RENDER-BUILD-PLANNER-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-planner`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract the pure one-operation WMO render build cursor decision from the loader
while preserving mesh-before-MultiMesh ordering and one budget permit per group.
## Non-goals
- Move Nodes, Resources, materials, jobs, queues, cancellation or permits.
- Change WMO placement, cache loading, transforms, visibility or shadows.
- Change build ordering, group names, editor ownership or finalization thread.
- Change cache formats, settings, presets or renderer diagnostics.
- Claim asset-backed WMO fidelity or p95/p99 acceptance.
## Paths
- Exclusive: `src/render/wmo/wmo_render_build_step_planner.gd`,
`src/tools/verify_wmo_render_build_step_planner.gd`,
`docs/modules/wmo-render-build-step-planner.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`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
- Input: mesh count/index and MultiMesh count/index
- Output: `mesh`, `multimesh` or `complete` operation, selected index and next
cursor values
- Mesh is selected while its cursor is below count; MultiMesh is selected only
after mesh exhaustion; complete follows both exhausted ranges
- Loader retains cursor adoption, Resource array access, Node creation, material
refresh, ownership, queue cancellation and budget permit consumption
- Cache/schema/coordinate/profile versions: unchanged
## Dependencies
- Requires: current master `1e73e59`, accepted WMO placement registry/resolver
- Blocks: further WMO render build state decomposition
- External state: none; verification uses scalar synthetic fixtures
## Verification
- Commands: dedicated ordering/cursor/empty/raw-edge/source/timing verifier;
WMO registry/resolver/material/shutdown plus M2/terrain/facade/internal-access/
manifest/scheduler/streaming/coordinate/docs/coord gates
- Fidelity evidence: exact mesh-first, one-step cursor behavior preserved
- Performance budget: 20,000 pure plans under 1 second; no Node, Resource, RID,
task, queue, mutex, cache or file access
## Documentation deliverables
- Inline public planner API documentation
- New module spec with inputs/outputs, data flow, sequence, ownership,
failure/recovery, performance/fidelity limits and source map
- Renderer module registry, world renderer and `RENDER.md` updates
## Simplicity and naming
- Important name: `WmoRenderBuildStepPlanner`
- Simplest solution: one stateless function and three operation constants
- Rejected complexity: job class, iterator, generic cursor framework, callbacks,
signals or ownership of engine objects
- Unavoidable complexity: loader still has separate mesh/MultiMesh materializers
- Measured optimization evidence: bounded synthetic planning loop
## Status
- State: accepted
- Done: planner and loader adapter implemented; Node/Resource/material/job/queue/
permit ownership retained in loader; regressions, dry-run and docs passed
- Next: WMO build job/resource ownership or liquid service extraction
- Blocked by:
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-RENDER-BUILD-PLANNER-001:c06aed5 -->
## Handoff
- Commit: `3a6b1d3`
- Results: dedicated planner PASS (`cases=9`, `iterations=20000`,
`elapsed_ms=11.914`); WMO 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`.
- Remaining risks: no proprietary WMO corpus, original-client paired capture or
traversal p95/p99; dry-run reports expected missing ADT/DBC/character assets;
WMO Resource arrays and untyped build jobs/queues remain loader-owned.
- Documentation updated: inline planner API;
`docs/modules/wmo-render-build-step-planner.md`, module registry,
`docs/modules/world-renderer.md`, `RENDER.md` with data-flow and sequence
diagrams, ownership, failure/recovery and source map.
- Integration: accepted in master merge `c06aed5`; post-merge planner
(`cases=9`, `iterations=20000`, `elapsed_ms=11.059`), WMO registry/resolver,
runtime-cache shutdown, facade, documentation and coordination gates passed.
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-RENDER-BUILD-PLANNER-001:3a6b1d3 -->
@@ -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 -->
@@ -0,0 +1,115 @@
# M03-RND-WMO-RENDER-RESOURCE-CACHE-001 — WMO render resource cache state
<!-- OPENWC_CLAIM:M03-RND-WMO-RENDER-RESOURCE-CACHE-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-resource-cache`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract lightweight-WMO render Resource cache, negative cache and asynchronous
request bookkeeping from the loader while preserving loader-owned I/O and
cache-format validation.
## Non-goals
- Call `ResourceLoader`, construct cache paths or inspect files.
- Validate `WMOStreamingResource` script identity or format version.
- Own cached WMO scenes, live prototypes, placement/build jobs or Nodes/RIDs.
- Change fallback order, retry behavior, quality profiles or visual output.
- Claim asset-backed WMO fidelity, leak acceptance or traversal p95/p99.
## Paths
- Exclusive: `src/render/wmo/wmo_render_resource_cache_state.gd`,
`src/tools/verify_wmo_render_resource_cache_state.gd`,
`docs/modules/wmo-render-resource-cache-state.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`src/tools/verify_render_runtime_cache_shutdown.gd`,
`docs/modules/wmo-render-build-queue.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
- State keys are loader-normalized relative WMO paths copied as `String`.
- Cached values are already validated `Resource` references.
- Pending values are cache paths for loader-owned `ResourceLoader` requests.
- Terminal request removal precedes validated-resource or missing-state adoption.
- Request/missing reset preserves accepted resources; full clear releases all.
- Cache/schema/coordinate/profile versions and validation rules remain unchanged.
## Dependencies
- Requires: current master `c954704`, accepted WMO render build queue
- Blocks: further WMO cached-scene/load and materialization decomposition
- External state: none; verification uses synthetic Resources and paths
## Verification
- Commands: dedicated lifecycle/request/cache/reset/diagnostics/source/timing
verifier; WMO queue/planner/registry/resolver/material/shutdown plus adjacent
renderer/streaming/coordinate/documentation/coordination gates
- Fixtures: synthetic Resource instances and normalized relative/cache paths
- Fidelity evidence: exact current cache/missing/request transitions preserved
- Performance budget: 100 cycles over 256 resource/request keys under 1 second
## Documentation deliverables
- Inline public API docs
- Module specification with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state and loader interaction sequence diagrams
- World-renderer, module registry, WMO queue and RENDER status updates
## Simplicity and naming
- Important names introduced: `WmoRenderResourceCacheState`
- Simplest considered solution: one RefCounted around three Dictionaries
- Rejected complexity/abstractions: generic cache, callbacks, signals, mutexes,
ResourceLoader wrapper, persistence layer or shared WMO/M2 cache hierarchy
- Unavoidable complexity and justification: partial reset must retain validated
Resources while discarding pending/negative state at map reset and shutdown
- Measured optimization evidence: bounded synthetic store/request/reset loop
## Status
- State: accepted
- Done: state holder implemented; loader cache/request/reset/metrics adapters
migrated; shutdown regression and required documentation updated
- Next: cached WMO scene/load state or liquid service extraction
- Blocked by:
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-RENDER-RESOURCE-CACHE-001:b4955d6 -->
## Handoff
- Commit: `d39f591`
- Results: cache-state PASS (`cases=10`, `iterations=100`,
`elapsed_ms=58.727`); direct no-`.godot` cache-state run PASS (`45.361ms`);
WMO queue/planner/registry/resolver/material/shutdown plus 18 adjacent
M2/terrain/facade/scheduler/streaming/coordinate regressions PASS;
documentation and coordination gates PASS; checkpoint dry-run PASS `7/7`.
- Remaining risks: accepted Resources remain strongly referenced until full
shutdown release; cached-scene and ResourceLoader I/O seams remain in loader;
no proprietary corrupt-cache fixture, asset-backed WMO traversal/leak run,
p95/p99 or original-client paired evidence. Full loader cannot be used as a
direct empty-class-cache smoke because pre-existing typed global coordinate/
focus dependencies require editor class registration; the extracted service
itself passed cold and normal editor parse passed.
- Documentation updated: inline public API; new
`docs/modules/wmo-render-resource-cache-state.md`; module registry, WMO queue,
world renderer and `RENDER.md`; data-flow/state/sequence diagrams, ownership,
threading, cache-version boundary, failure/recovery and source map included.
- Integration: merged as `b4955d6`; post-merge cache state (`cases=10`,
`iterations=100`, `elapsed_ms=51.949`), shutdown, WMO queue/resolver, facade,
internal-access (`private_symbols=41`), `7/7` manifest, documentation and
coordination gates passed.
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-RENDER-RESOURCE-CACHE-001:d39f591 -->
@@ -0,0 +1,117 @@
# M03-RND-WMO-SCENE-RESOURCE-CACHE-001 — WMO cached-scene resource state
<!-- OPENWC_CLAIM:M03-RND-WMO-SCENE-RESOURCE-CACHE-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-scene-resource-cache`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract cached-WMO `PackedScene`, negative-cache and asynchronous request state
from the loader while preserving loader-owned file limits, I/O and scene-cache
version validation.
## Non-goals
- Call `ResourceLoader`/`FileAccess`, construct `.tscn` paths or measure files.
- Instantiate scenes or validate WMO builder cache metadata/version.
- Own live WMO prototypes, placement/build jobs, instantiated Nodes or RIDs.
- Change missing/oversize/request-error fallback behavior or debug output.
- Claim asset-backed WMO fidelity, leak acceptance or traversal p95/p99.
## Paths
- Exclusive: `src/render/wmo/wmo_scene_resource_cache_state.gd`,
`src/tools/verify_wmo_scene_resource_cache_state.gd`,
`docs/modules/wmo-scene-resource-cache-state.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`src/tools/verify_render_runtime_cache_shutdown.gd`,
`docs/modules/wmo-render-resource-cache-state.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
- State keys are loader-normalized relative WMO paths copied as `String`.
- Cached values are loader-validated `PackedScene` references.
- Pending values are `.tscn` paths for loader-owned threaded requests.
- Missing state accepts absent file, oversize, request-start, load and validation failures.
- Terminal pending removal precedes validated-scene or missing adoption.
- Transient clear retains accepted scenes; full clear releases all state.
- Cache metadata/version, file-size limit, profile and fallback rules are unchanged.
## Dependencies
- Requires: current master `e690756`; accepted WMO render Resource cache state
- Blocks: further WMO I/O/fallback and materialization decomposition
- External state: none; verification uses synthetic PackedScenes and paths
## Verification
- Commands: dedicated lifecycle/request/missing/reset/diagnostics/source/timing
verifier; WMO render-cache/queue/planner/registry/resolver/material/shutdown plus
adjacent renderer/streaming/coordinate/documentation/coordination gates
- Fixtures: synthetic PackedScene instances and normalized relative/cache paths
- Fidelity evidence: exact current cached-scene/missing/request transitions preserved
- Performance budget: 100 cycles over 256 scene/request keys under 1 second
## Documentation deliverables
- Inline public API docs
- Module specification with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state and loader interaction sequence diagrams
- World-renderer, module registry, render-cache and RENDER status updates
## Simplicity and naming
- Important names introduced: `WmoSceneResourceCacheState`
- Simplest considered solution: one RefCounted around three Dictionaries
- Rejected complexity/abstractions: generic cache hierarchy, callbacks, signals,
mutexes, ResourceLoader wrapper, persistence layer or render/scene cache merger
- Unavoidable complexity and justification: missing can be recorded without a
pending request, and partial reset must retain validated PackedScenes
- Measured optimization evidence: bounded synthetic store/request/reset loop
## Status
- State: accepted
- Done: state holder implemented; loader lookup/request/drain/reset/metrics
adapters migrated; shutdown regression and required documentation updated
- Next: WMO I/O/live fallback or liquid service extraction
- Blocked by:
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-SCENE-RESOURCE-CACHE-001:ada0fd9 -->
## Handoff
- Commit: `c839294`
- Results: scene cache-state PASS (`cases=11`, `iterations=100`,
`elapsed_ms=40.768`; initial direct cold run `43.636ms`); render cache-state,
WMO queue/planner/registry/resolver/material/shutdown plus 18 adjacent
M2/terrain/facade/scheduler/streaming/coordinate regressions PASS;
documentation and coordination gates PASS; checkpoint dry-run PASS `7/7`.
- Remaining risks: accepted PackedScenes remain strongly referenced until full
shutdown release; ResourceLoader/FileAccess I/O, cache-validation probes and
live fallback remain in loader; no proprietary stale/oversize fixture,
asset-backed traversal/leak run, p95/p99 or original-client paired evidence.
The extracted scene state passed cold; full loader cold execution still needs
editor class registration for pre-existing focus/coordinate global classes.
- Documentation updated: inline public API; new
`docs/modules/wmo-scene-resource-cache-state.md`; render-cache spec, module
registry, world renderer and `RENDER.md`; data-flow/state/sequence diagrams,
ownership, threading, file-size/cache-version boundary, failure/recovery and
source map included.
- Integration: merged as `ada0fd9`; post-merge scene state (`cases=11`,
`iterations=100`, `elapsed_ms=40.113`), render state (`43.434ms`), shutdown,
WMO queue/resolver, facade, internal-access (`private_symbols=40`), `7/7`
manifest, documentation and coordination gates passed.
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-SCENE-RESOURCE-CACHE-001:c839294 -->
+5
View File
@@ -20,6 +20,11 @@
| M2 placement grouper | Implemented extraction | [`m2-placement-grouper.md`](m2-placement-grouper.md) |
| M2 build batch planner | Implemented extraction | [`m2-build-batch-planner.md`](m2-build-batch-planner.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 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) |
| WMO render Resource cache state | Implemented extraction | [`wmo-render-resource-cache-state.md`](wmo-render-resource-cache-state.md) |
| WMO scene Resource cache state | Implemented extraction | [`wmo-scene-resource-cache-state.md`](wmo-scene-resource-cache-state.md) |
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
+207
View File
@@ -0,0 +1,207 @@
# WMO Placement Registry
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-WMO-PLACEMENT-REGISTRY-001` |
| Owners | WMO placement-key to tile/global reference-set ownership |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-placement-registry`, 2026-07-17 |
| Profiles/capabilities | Existing ADT/WDT WMO placement paths; profile-independent |
## Purpose
Own the reference sets that keep one resolved WMO placement active across one
or more ADT tiles, or across the global WDT placement lifetime. This separates
value-only placement ownership from scene and render-resource ownership.
## Non-goals
- Resolve WMO paths, placement identity or world transforms.
- Own Nodes, Resources, RIDs, roots, jobs, queues, tasks or caches.
- Load, instantiate, build or free WMO content.
- Change duplicate order, budgets, retries or global-WMO behavior.
- Claim original-client WMO visual, portal, room or material parity.
## Context and boundaries
```mermaid
flowchart LR
Parsed[ADT/WDT placement] --> Resolver[WmoPlacementResolver]
Resolver --> Key[Placement key]
Tile[Tile/global reference key] --> Loader[StreamingWorldLoader]
Key --> Loader
Loader --> Registry[WmoPlacementRegistry]
Registry --> Decision[Existing or final-release decision]
Decision --> Loader
Loader --> NodeMap[Loader key-to-Node map]
Loader --> Jobs[Loader jobs/queues/resources]
```
The registry depends only on `RefCounted`, `Dictionary`, `Array` and `String`.
SceneTree, Node, Resource, RID, files, loaders, threads, locks, network,
gameplay and editor UI are forbidden dependencies.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `add_reference(placement_key, reference_key)` | Command/query | Idempotently add a reference; return true only when creating the placement key | Renderer main thread; map session | Empty key returns false and changes nothing |
| `release_reference(placement_key, reference_key)` | Command/query | Remove an owned reference; return true only on final removal | Renderer main thread; map session | Empty, unknown or non-owner input returns false |
| `contains(placement_key)` | Query | Test whether any reference keeps a placement active | Renderer main thread; map session | Empty key returns false |
| `active_count()` | Query | Count active placement keys, not references | Renderer main thread; map session | None |
| `diagnostic_snapshot()` | Query | Return detached, placement/reference-sorted records | Renderer main thread; caller-owned result | None |
| `clear()` | Command | Remove all ownership records idempotently | Renderer main thread; reset/shutdown | None |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Resolved `uid:*`, `tile:*:*` or `global` placement key | `WmoPlacementResolver` / loader global adapter | Registry | Copied String dictionary key | Until final release/clear |
| Input | ADT tile key or `__global__` reference key | Loader | Registry | Copied String set key | Until release/clear |
| Output | First-placement Boolean | Registry | Loader creation path | Value | One call |
| Output | Final-release Boolean | Registry | Loader cancellation/free path | Value | One call |
| Output | Active count and sorted diagnostic records | Registry | Renderer diagnostics/verifier | Detached values | Caller lifetime |
## Data flow
```mermaid
flowchart TD
Add[add placement + reference] --> Valid{Both non-empty?}
Valid -->|no| Reject[false; unchanged]
Valid -->|yes| Existing{Placement exists?}
Existing -->|no| Create[Create reference set; true]
Existing -->|yes| Retain[Idempotently add reference; false]
Release[release placement + reference] --> Owner{Known owner reference?}
Owner -->|no| Reject
Owner -->|yes| Erase[Erase reference]
Erase --> Empty{Set empty?}
Empty -->|no| Retained[false; placement remains]
Empty -->|yes| Final[Erase placement; true]
```
## Lifecycle and state
The states for one placement are `Absent`, `Referenced` and back to `Absent`.
The first valid add enters `Referenced`; further tile/global adds remain there.
Only release of the final owned reference returns to `Absent`. Map reset and
shutdown call `clear`, which is safe from either state.
```mermaid
stateDiagram-v2
[*] --> Absent
Absent --> Referenced: first valid add / true
Referenced --> Referenced: idempotent/shared add / false
Referenced --> Referenced: non-owner/non-final release / false
Referenced --> Absent: final owned release / true
Referenced --> Absent: clear
```
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Registry as WmoPlacementRegistry
participant Nodes as Loader Node map
Loader->>Registry: contains(placement key)
alt existing placement
Loader->>Registry: add_reference(key, tile)
else new placement built successfully
Loader->>Nodes: store key-to-Node
Loader->>Registry: add_reference(key, tile/global)
end
Loader->>Registry: release_reference(key, tile)
alt final reference
Registry-->>Loader: true
Loader->>Loader: cancel render build
Loader->>Nodes: queue-free and erase Node
else shared/unknown reference
Registry-->>Loader: false
end
```
## Ownership, threading and resources
- The registry owns only placement-key Dictionaries and reference-key sets.
- The loader owns the parallel placement-key-to-Node map and every Node action.
- The loader also owns WMO roots, tile state, caches, requests, jobs, queues,
retries, cancellation and render-resource lifetime.
- Current calls are serialized on the renderer main thread. The registry has no
lock and must not be mutated concurrently.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty placement/reference key | API guard | Reject without mutation | Contract verifier | Repair caller input |
| Duplicate add | Existing set member | Idempotent retain | Sorted snapshot | No recovery required |
| Unknown/non-owner release | Missing placement/reference | Ignore without mutation | Contract verifier | Release from actual owner or reset |
| Tile build cancellation | Loader-owned condition | Loader cancels job, then releases recorded refs | Existing queue metrics | Retry when tile becomes wanted |
| Final release | Empty set after erase | Return true and erase registry record | Active count | Loader cancels/free Node exactly once |
| Map reset/shutdown | Loader lifecycle | Loader frees Nodes/jobs and clears registry | Shutdown regression | Next map begins empty |
## Configuration and capabilities
There is no setting, profile branch or capacity knob. WMO tile radius, cache
limits, render budgets and visibility settings remain loader-owned.
## Persistence, cache and migration
No data is serialized and no cache/schema/version changes. Placement and
reference keys exist only for the current renderer map session.
## Diagnostics and observability
`active_count` backs existing WMO instance metrics. `diagnostic_snapshot`
returns deterministic detached records for tests and future read-only
diagnostics; mutating the returned arrays cannot alter registry state.
## Verification
- `verify_wmo_placement_registry.gd`: invalid/first/idempotent/shared/non-owner/
final/global/clear behavior, deterministic detached diagnostics, source
ownership boundaries and bounded 100-by-256 shared-reference cycles.
- WMO resolver, material, runtime-cache shutdown and the renderer regression
suite verify the unchanged loader adapters and surrounding lifecycle.
- Fidelity evidence is exact preservation of the repository's prior shared-ref
and final-release semantics. Asset-backed 3.3.5a comparison is still pending.
## Extension points
- WMO build/resource state can be extracted separately without moving Nodes
into this registry.
- A future diagnostic adapter may expose the detached snapshot through the
facade without exposing mutable loader internals.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| WMO placement reference ownership | Implemented extraction | Contract/source/timing verifier | Asset-backed cross-tile corpus pending |
| WMO build/resource scheduling | Remains in loader | Existing runtime regressions | Separate state/cancellation package |
| WMO visual/portal/material fidelity | Partial | Renderer baseline/material checks | Original-client paired evidence pending |
## Known gaps and risks
- Correctness assumes the loader records each successful placement reference in
the tile `wmo_refs` array before unregister.
- Old caches without positive UID retain resolver-defined per-tile identity.
- No proprietary WMO corpus or original-client paired capture is in this package.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_placement_registry.gd` | Placement-key/reference-set ownership |
| `src/render/wmo/wmo_placement_resolver.gd` | Stable path, identity and transform rules |
| `src/scenes/streaming/streaming_world_loader.gd` | Node map, instances, resources, jobs, queues and lifecycle reactions |
| `src/tools/verify_wmo_placement_registry.gd` | Contract, boundary and timing regression |
## Related decisions and references
- [`wmo-placement-resolver.md`](wmo-placement-resolver.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)
+15 -10
View File
@@ -19,7 +19,8 @@ live-prototype instance paths.
## Non-goals
- Parse ADT/WDT or convert canonical WoW coordinates.
- Own WMO registry references, tile state, jobs, queues or retries.
- Own WMO registry references, tile state, jobs, queues or retries; reference
ownership belongs to `WmoPlacementRegistry`.
- Load/validate cached scenes or lightweight render resources.
- Create Nodes, Mesh/MultiMesh, materials or RIDs.
- Implement portals, rooms, visibility, materials, doodads or WMO parity.
@@ -34,7 +35,7 @@ flowchart LR
Resolver --> Identity[Registry unique key]
Resolver --> Transform[World Transform3D]
CacheKey --> Cache[Loader WMO caches/requests]
Identity --> Registry[Loader WMO registry]
Identity --> Registry[WmoPlacementRegistry]
Transform --> RenderRoot[Lightweight render root]
Transform --> Scene[Cached scene instance]
Transform --> Live[Live prototype instance]
@@ -60,7 +61,7 @@ WorkerThreadPool, mutexes, files, gameplay, network and editor UI are forbidden.
| Input | Raw placement Dictionary | WDT global or ADT MODF parser result | Identity/transform queries | Read-only shallow value | One call |
| Input | Tile key and placement index | Loader build job | Synthetic identity fallback | Copied scalar/String | Registry entry lifetime |
| Output | Normalized relative path | Resolver | Render/scene cache and load-request maps | New String value | Request/cache lookup |
| Output | `uid:*` or `tile:*:*` key | Resolver | Loader WMO registry/ref arrays | New String value | Until unregister/reset |
| Output | `uid:*` or `tile:*:*` key | Resolver | `WmoPlacementRegistry` and loader ref arrays | New String value | Until unregister/reset |
| Output | World `Transform3D` | Resolver | Three WMO instance adapters | Value copy | Instance lifetime after assignment |
The resolver retains no source Dictionary, output or engine resource.
@@ -91,7 +92,7 @@ and shutdown require no resolver operation.
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Resolver as WmoPlacementResolver
participant Registry as Loader WMO registry/cache
participant Registry as WmoPlacementRegistry
participant Instance as Render/cached/live instance
Loader->>Resolver: normalize_relative_path(path)
Resolver-->>Loader: cache key
@@ -105,8 +106,9 @@ sequenceDiagram
## Ownership, threading and resources
- The resolver owns only call-local scalar/value calculations.
- The loader owns WMO registry entries/ref sets, cache/load-request state, build
jobs/queues, resource fallback and cancellation.
- `WmoPlacementRegistry` owns placement-key reference sets. The loader owns its
key-to-Node map, cache/load-request state, jobs/queues, resource fallback and
cancellation.
- The loader and builders own every Node/Mesh/MultiMesh/material/RID lifecycle.
- Pure calls are thread-safe; current consumers execute on the main thread.
@@ -152,8 +154,8 @@ in the facade/loader diagnostic snapshot.
## Extension points
- A later package may extract WMO registry ownership/ref release using these
stable keys.
- `WmoPlacementRegistry` consumes these stable keys; later packages may extract
WMO build/resource state without changing the resolver.
- Render-group build cursor and resource-selection state require separate
lifecycle/cancellation contracts.
@@ -162,7 +164,8 @@ in the facade/loader diagnostic snapshot.
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Consistent WMO placement identity/transform | Implemented extraction | Contract/source/timing verifier | Asset-backed placement comparison pending |
| WMO registry/build services | Remains in loader | Existing runtime regressions | Stateful extraction pending |
| WMO placement registry | Implemented extraction | Ownership/source/timing verifier | Asset-backed cross-tile corpus pending |
| WMO build services | Remains in loader | Existing runtime regressions | Stateful extraction pending |
| Portals/rooms/material fidelity | Partial | M00 checkpoint/material evidence | Broader WMO implementation pending |
## Known gaps and risks
@@ -177,11 +180,13 @@ in the facade/loader diagnostic snapshot.
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_placement_resolver.gd` | Path, identity and transform rules |
| `src/scenes/streaming/streaming_world_loader.gd` | Registry/cache/build/resource/Node ownership |
| `src/render/wmo/wmo_placement_registry.gd` | Placement reference-set ownership |
| `src/scenes/streaming/streaming_world_loader.gd` | Node map/cache/build/resource ownership |
| `src/tools/verify_wmo_placement_resolver.gd` | Contract, source and timing regression |
## Related decisions and references
- [`world-renderer.md`](world-renderer.md)
- [`wmo-placement-registry.md`](wmo-placement-registry.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+244
View File
@@ -0,0 +1,244 @@
# 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, render cache state or load I/O.
- 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 render Resource state | Implemented extraction | Cache-state lifecycle/source/timing verifier | Asset-backed traversal/leak evidence pending |
| WMO materialization/cache I/O | Partial | Existing runtime regressions | Cached-scene and I/O 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-render-resource-cache-state.md`](wmo-render-resource-cache-state.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)
@@ -0,0 +1,189 @@
# WMO Render Build Step Planner
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-WMO-RENDER-BUILD-PLANNER-001` |
| Owners | Pure mesh-first WMO render-group cursor decision |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-render-build-planner`, 2026-07-17 |
| Profiles/capabilities | Lightweight WMO render-cache path; profile-independent |
## Purpose
Select exactly one next lightweight WMO render build operation from mesh and
MultiMesh counts/cursors. The planner makes the ordering and cursor transition
testable without loading a WMO, creating a Node or consuming a frame budget.
## Non-goals
- Own render Resources, Nodes, materials, jobs, queues or permits.
- Read WMO arrays, create group instances or adopt cursors in a job.
- Change group ordering, names, transforms, shadows or visibility.
- Load, validate, version or persist a WMO cache.
- Implement portals, rooms, doodad semantics or WMO visual parity.
## Context and boundaries
```mermaid
flowchart LR
Cache[WMOStreamingResource arrays] --> Loader[StreamingWorldLoader]
Job[Loader mesh/MultiMesh cursors] --> Loader
Loader --> Planner[WmoRenderBuildStepPlanner]
Planner --> Step[Operation + selected index + next cursors]
Step --> Loader
Loader --> Material[Material refresh]
Loader --> Node[MeshInstance3D or MultiMeshInstance3D]
Loader --> Budget[RenderBudgetScheduler permit]
```
The planner depends only on `RefCounted`, integers, `StringName` and a fresh
Dictionary result. Node, Resource, Mesh, MultiMesh, RID, SceneTree, files,
threads, locks, caches, gameplay, network and editor UI are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `OPERATION_MESH` | Constant | Identifies one mesh-group operation | Immutable | None |
| `OPERATION_MULTIMESH` | Constant | Identifies one doodad MultiMesh-group operation | Immutable | None |
| `OPERATION_COMPLETE` | Constant | Identifies exhausted mesh and MultiMesh ranges | Immutable | None |
| `plan_step(mesh_count, mesh_index, multimesh_count, multimesh_index)` | Pure query | Select one operation, selected index and next cursors | Any thread; caller-owned result | No clamp/error branch; raw integer comparisons are preserved |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Mesh array count and current mesh cursor | Loader resource/job adapter | Planner | Integer values | One call |
| Input | MultiMesh array count and current MultiMesh cursor | Loader resource/job adapter | Planner | Integer values | One call |
| Output | `mesh`, `multimesh` or `complete` operation | Planner | Loader materializer/cancellation branch | Immutable `StringName` | One call |
| Output | Selected index | Planner | Loader Resource array access | Integer value | One operation |
| Output | Next mesh and MultiMesh cursors | Planner | Loader job adoption | Fresh Dictionary values | Until adopted/discarded |
The planner retains no input or output. Scene-tree mutation, material refresh,
job writes, permit consumption and cancellation are loader side effects.
## Data flow
```mermaid
flowchart TD
Input[Counts and cursors] --> MeshRemaining{mesh index < mesh count?}
MeshRemaining -->|yes| MeshStep[mesh at mesh index; increment mesh cursor]
MeshRemaining -->|no| MultiRemaining{MultiMesh index < MultiMesh count?}
MultiRemaining -->|yes| MultiStep[MultiMesh at its index; increment MultiMesh cursor]
MultiRemaining -->|no| Complete[complete; keep both cursors]
```
## Lifecycle/state
The planner is stateless. State belongs to the loader job. Repeated loader calls
advance through `mesh` steps, then `multimesh` steps, then `complete`. Planner
construction, cancellation, map reset and shutdown require no cleanup.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Planner as WmoRenderBuildStepPlanner
participant Scene as SceneTree
participant Budget as RenderBudgetScheduler
Loader->>Planner: plan_step(counts, cursors)
Planner-->>Loader: operation, selected index, next cursors
alt mesh or MultiMesh
Loader->>Loader: refresh material and create group Node
Loader->>Scene: attach Node and editor owner
Loader->>Loader: adopt returned cursor
Loader->>Budget: consume one WMO group permit
else complete
Loader->>Loader: cancel/remove job and queue key
end
```
## Ownership, threading and resources
- The planner owns only call-local scalar comparisons and the returned value map.
- `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
performs material refresh and editor-owner assignment on the main thread.
- `RenderBudgetScheduler` owns permits; the planner neither reads nor consumes one.
- Pure calls are thread-safe because the planner has no mutable state.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty arrays | Both `0 < 0` comparisons false | Return `complete` | Contract verifier | Loader removes job |
| Cursor at/beyond count | Raw integer comparison | Skip that range | Contract verifier | Continue next range or complete |
| Negative cursor/count | Raw historical comparison | No clamp; select exactly as prior loader condition | Edge fixture | Caller should maintain valid job cursors |
| Missing/invalid Resource | Loader validation before planning | Loader cancels job | Existing shutdown/runtime diagnostics | Reload/requeue placement |
| Root freed | Loader validity check before planning | Loader cancels job | Runtime-cache shutdown verifier | Placement may rebuild later |
| Placement final release | Loader/registry lifecycle | Loader cancels queued build | Registry regression | Later placement starts at cursor zero |
## Configuration and capabilities
The planner has no setting or profile branch. `wmo_render_group_ops_per_tick`,
quality presets, shadow/visibility settings and cache limits remain loader-owned.
## Persistence, cache and migration
No data is persisted. WMO render resource format/version, cache path and job
Dictionary fields are unchanged. No migration or cache rebuild is required.
## Diagnostics and observability
The planner emits no logs. Its verifier reports operation/cursor/source contracts
and bounded timing. Queue lengths, active WMO count and hitch sections remain in
existing loader/facade diagnostics.
## Verification
- `verify_wmo_render_build_step_planner.gd`: mesh-first order, cursor increments,
completion, empty and raw negative integer behavior, fresh results, loader
delegation, dependency boundary and 20,000-plan timing.
- WMO registry/resolver/material/shutdown plus renderer regressions verify the
unchanged ownership and surrounding runtime path.
- Fidelity evidence is exact extraction of existing mesh-before-MultiMesh and
one-index-per-permit behavior. No asset-backed build-12340 visual claim is made.
## Extension points
- `WmoRenderBuildQueue` now owns typed pending state; cache/load requests and
materialization remain separate extraction seams.
- Materialization can later receive typed commands without changing this planner.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| WMO render build step planning | Implemented | Pure contract/source/timing verifier | Asset-backed traversal p95/p99 pending |
| 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 |
## Known gaps and risks
- Loader build jobs remain untyped Dictionaries.
- Resource array shape/length consistency remains a cache-builder invariant.
- No proprietary WMO corpus, traversal profile or paired client capture is part
of this extraction.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_render_build_step_planner.gd` | Pure operation and cursor selection |
| `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 |
## Related decisions and references
- [`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)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
@@ -0,0 +1,220 @@
# WMO Render Resource Cache State
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-WMO-RENDER-RESOURCE-CACHE-001` |
| Owners | Validated lightweight-WMO render Resources, negative cache and pending request paths |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-render-resource-cache`, 2026-07-17 |
| Profiles/capabilities | Lightweight WMO render-cache path; profile-independent |
## Purpose
Own the mutually exclusive cached, missing and pending states for normalized
lightweight-WMO render paths outside the monolithic streamer. The state holder
accepts only caller-validated `Resource` references and records cache paths for
threaded requests whose I/O lifecycle remains in `StreamingWorldLoader`.
## Non-goals
- Normalize WMO paths or construct `.res` cache paths.
- Call `ResourceLoader`, inspect files or validate script/format versions.
- Own WMO cached scenes, live prototypes, placements, build jobs or engine Nodes.
- Change fallback order, retry timing, quality profiles or visible output.
- Persist, migrate or build WMO caches.
## Context and boundaries
```mermaid
flowchart LR
Placement[Normalized WMO render lookup] --> Loader[StreamingWorldLoader]
Loader --> State[WmoRenderResourceCacheState]
State -->|cached Resource| Loader
Loader -->|cache path| ResourceLoader[Godot ResourceLoader]
ResourceLoader -->|status and loaded Resource| Loader
Loader --> Validate[Script and FORMAT_VERSION validation]
Validate -->|accepted Resource or missing| State
Loader --> Fallback[Cached scene or live-prototype fallback]
```
The module may retain validated `Resource` references and copied path Strings.
It has no FileAccess, ResourceLoader, Node, RID, worker, mutex, gameplay, network
or editor dependency. Cache validation stays at the I/O boundary in the loader.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `resource_for(path)` | Query | Borrow validated cached Resource | Renderer main thread; until full clear | Empty/unknown returns null |
| `contains_resource(path)` | Query | Test accepted cache state | Renderer main thread | Empty returns false |
| `is_missing(path)` | Query | Test negative cache state | Renderer main thread | Empty returns false |
| `has_request(path)` | Query | Test pending threaded-request state | Renderer main thread | Empty returns false |
| `remember_request(path, cache_path)` | Command/query | Record one loader-started request | Renderer main thread; until terminal/reset | Invalid or occupied state returns false |
| `request_paths_snapshot()` | Query | Copy pending normalized/cache-path mapping | Poll or shutdown drain | Detached Dictionary |
| `complete_request_with_resource(path, resource)` | Command/query | Remove pending request and adopt caller-validated Resource | Terminal loader poll | Unknown/null returns false |
| `complete_request_as_missing(path)` | Command/query | Remove pending request and adopt negative state | Terminal loader poll | Unknown returns false |
| `clear_transient_state()` | Command | Clear pending and missing while retaining accepted Resources | Map reset/request drain | Idempotent |
| `clear_all()` | Command | Release Resources, pending and missing | Final runtime cache release | Idempotent |
| `pending_request_count()` | Query | Preserve renderer queue metric contribution | Renderer diagnostics | None |
| `diagnostic_snapshot()` | Query | Return sorted detached scalar keys | Tests/diagnostics | Never exposes Resources |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Normalized relative WMO path | Placement resolver through loader | Cache state | Copied String | Map/cache lifetime |
| Input | `.res` cache path after accepted threaded-request result | Loader | Pending state | Copied String | Until terminal/reset |
| Input | Script/version-validated WMO Resource | Loader validation adapter | Resource cache | Strong reference | Until full clear |
| Output | Borrowed validated Resource | Cache state | Loader render-root/build adapters | State retains reference | Lookup/build start |
| Output | Detached pending-path snapshot | Cache state | Loader ResourceLoader polling/shutdown | Caller-owned Dictionary | One drain pass |
| Output | Sorted scalar diagnostic keys | Cache state | Verifier/future metrics | Caller-owned values | Snapshot lifetime |
## Data flow
```mermaid
flowchart TD
Lookup[Lookup normalized path] --> Cached{Cached?}
Cached -->|yes| Return[Return validated Resource]
Cached -->|no| Blocked{Missing or pending?}
Blocked -->|yes| Null[Return null; loader waits/falls back]
Blocked -->|no| Start[Loader starts threaded request]
Start --> Remember[remember_request]
Remember --> Poll[Loader polls detached request snapshot]
Poll --> Terminal{Loaded or failed?}
Terminal -->|no| Poll
Terminal -->|failed| Missing[complete as missing]
Terminal -->|loaded| Validate[Loader validates script/version]
Validate -->|valid| Adopt[complete with Resource]
Validate -->|invalid| Missing
```
## Lifecycle/state
Each normalized path is `Absent`, `Pending`, `Cached` or `Missing`. Valid request
completion leaves exactly one terminal state. Transient clear moves `Pending` and
`Missing` to `Absent` but retains `Cached`; full clear moves every state to
`Absent` and releases retained Resources.
```mermaid
stateDiagram-v2
[*] --> Absent
Absent --> Pending: remember valid request
Pending --> Cached: validated Resource completion
Pending --> Missing: load/validation failure
Pending --> Absent: clear transient/full
Missing --> Absent: clear transient/full
Cached --> Absent: full clear
```
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant State as WmoRenderResourceCacheState
participant RL as ResourceLoader
Loader->>State: cached/missing/pending queries
Loader->>RL: exists + load_threaded_request(cache path)
Loader->>State: remember_request(normalized, cache path)
loop renderer tick
Loader->>State: request_paths_snapshot()
Loader->>RL: load_threaded_get_status(cache path)
end
alt load failed
Loader->>State: complete_request_as_missing(normalized)
else loaded
Loader->>RL: load_threaded_get(cache path)
Loader->>Loader: validate script and FORMAT_VERSION
Loader->>State: complete with Resource or as missing
end
```
## Ownership, threading and resources
- The state owns three Dictionaries and strong references to accepted Resources.
- The loader owns normalization, cache paths, ResourceLoader calls and validation.
- All mutation is serialized by the renderer main-thread lookup/drain lifecycle.
- No mutex or callback is needed; detached request snapshots permit safe removal.
- The loader/build queue borrow Resources without transferring ownership.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty path/cache path | Request guard | Reject unchanged | Contract verifier | Repair caller input |
| Duplicate or occupied path | State membership | Reject request unchanged | Contract/source verifier | Await terminal/reset |
| Threaded load failed | Loader status | Complete as missing | Existing renderer metrics | Transient reset permits retry |
| Wrong script/old format | Loader validation | Complete as missing | Cache-version source gate | Rebuild cache, then reset |
| Shutdown while pending | Loader drains ResourceLoader snapshot | Clear transient afterward | Shutdown verifier | Next runtime starts absent |
| Final cache release | Loader shutdown lifecycle | Full clear releases Resources | Shutdown verifier | Recreated with loader |
## Configuration and capabilities
The module adds no setting or profile branch. `wmo_render_cache_dir`, resource
format version, request scheduling and WMO build budgets remain loader-owned.
## Persistence, cache and migration
The state is runtime-only and serializes nothing. `WMOStreamingResource` script
identity and `FORMAT_VERSION` validation remain unchanged in the loader; no cache
migration or rebuild is introduced by this extraction.
## Diagnostics and observability
`pending_request_count` preserves the existing `wmobuild` metric contribution.
Diagnostics expose only sorted normalized-path keys for cached/missing/pending
states; Resource references and cache file paths are not exposed. No logs emit.
## Verification
- `verify_wmo_render_resource_cache_state.gd`: invalid input, detached requests,
successful/missing terminal transitions, exclusivity, partial/full reset,
sorted diagnostics, ownership/source boundary and bounded timing.
- `verify_render_runtime_cache_shutdown.gd`: full release of accepted Resources
and negative state through the loader lifecycle.
- WMO queue/planner/registry/resolver and surrounding renderer regressions verify
unchanged fallback/build behavior. This package makes no visual parity claim.
## Extension points
- Cached WMO scene request state is a separate typed module because its size
limit, PackedScene validation and missing-file behavior differ.
- A later loader I/O adapter may consume the detached request snapshot while
preserving this state contract and cache-version boundary.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Lightweight WMO render Resource state | Implemented extraction | Lifecycle/source/timing and shutdown verifiers | Asset-backed traversal/leak evidence pending |
| Cache script/version validation | Preserved in loader | Source boundary and WMO regressions | Dedicated corrupt-cache fixture could follow |
| Cached WMO scene state | Implemented extraction | Scene-cache lifecycle/source/timing verifier | Asset-backed traversal/leak evidence pending |
| WMO materialization | Partial/loader-owned | Queue/planner regressions | Further safe extraction |
## Known gaps and risks
- Strong Resource references live until final cache release, matching prior state.
- Missing cache files remain unrecorded for render Resources, so lookup can retry;
this intentionally differs from the cached-scene path.
- No proprietary WMO corpus, corrupt-version fixture, leak run, traversal p95/p99
or paired original-client capture is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_render_resource_cache_state.gd` | Resource/missing/request state and resets |
| `src/scenes/streaming/streaming_world_loader.gd` | Normalize, request, poll, validate, fallback and shutdown I/O |
| `src/tools/verify_wmo_render_resource_cache_state.gd` | State, boundary and timing regression |
| `src/tools/verify_render_runtime_cache_shutdown.gd` | Loader final Resource ownership regression |
## Related decisions and references
- [`wmo-render-build-queue.md`](wmo-render-build-queue.md)
- [`wmo-scene-resource-cache-state.md`](wmo-scene-resource-cache-state.md)
- [`wmo-placement-resolver.md`](wmo-placement-resolver.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)
@@ -0,0 +1,212 @@
# WMO Scene Resource Cache State
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-WMO-SCENE-RESOURCE-CACHE-001` |
| Owners | Validated cached-WMO PackedScenes, negative cache and pending request paths |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-scene-resource-cache`, 2026-07-17 |
| Profiles/capabilities | Cached WMO `.tscn` fallback; profile-independent state |
## Purpose
Own mutually exclusive cached, missing and pending states for normalized WMO
scene paths. The state accepts only caller-validated `PackedScene` references and
records `.tscn` paths for threaded requests whose I/O, size limit and cache
metadata validation remain in `StreamingWorldLoader`.
## Non-goals
- Normalize WMO paths, build `.tscn` paths or measure files.
- Call ResourceLoader/FileAccess, instantiate scenes or validate cache metadata.
- Own live prototypes, placement/build jobs, instantiated Nodes or RIDs.
- Change missing, oversize, request-error, retry or fallback behavior.
- Persist, migrate or build WMO caches.
## Context and boundaries
```mermaid
flowchart LR
Lookup[Normalized WMO scene lookup] --> Loader[StreamingWorldLoader]
Loader --> State[WmoSceneResourceCacheState]
Loader --> Size[File existence and size limit]
Size --> ResourceLoader[Godot ResourceLoader]
ResourceLoader --> Loader
Loader --> Probe[Instantiate, metadata/version check, free]
Probe -->|accepted PackedScene or missing| State
Loader --> Live[Live-prototype fallback]
```
The module retains validated PackedScenes and copied paths only. ResourceLoader,
FileAccess, WMOBuilder, Node, RID, worker, gameplay, network and editor concerns
remain outside it.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `scene_for(path)` / `contains_scene(path)` | Query | Borrow/test validated PackedScene | Renderer main thread; until full clear | Empty/unknown returns null/false |
| `is_missing(path)` / `has_request(path)` | Query | Test negative/pending state | Renderer main thread | Empty returns false |
| `remember_request(path, cache_path)` | Command/query | Record loader-started request | Until terminal/reset | Invalid/occupied returns false |
| `mark_missing(path)` | Command/query | Record absent/oversize/request-start failure without pending state | Until transient/full clear | Invalid/occupied returns false |
| `request_paths_snapshot()` | Query | Copy pending normalized/cache-path mapping | Poll/shutdown drain | Detached Dictionary |
| `complete_request_with_scene(path, scene)` | Command/query | Remove pending and adopt validated PackedScene | Terminal poll | Unknown/null returns false |
| `complete_request_as_missing(path)` | Command/query | Remove pending and adopt negative state | Terminal poll | Unknown returns false |
| `clear_transient_state()` | Command | Clear pending/missing and retain scenes | Map reset/request drain | Idempotent |
| `clear_all()` | Command | Release scenes and transient state | Final runtime-cache release | Idempotent |
| `pending_request_count()` | Query | Preserve renderer queue metric | Diagnostics | None |
| `diagnostic_snapshot()` | Query | Return sorted detached scalar keys | Tests/diagnostics | No PackedScene exposure |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Normalized WMO path | Placement resolver through loader | State | Copied String | Cache lifetime |
| Input | `.tscn` path after accepted request start | Loader | Pending state | Copied String | Until terminal/reset |
| Input | Metadata/version-validated PackedScene | Loader probe adapter | Scene cache | Strong reference | Until full clear |
| Input | Missing decision | Loader file/size/request/load/validation adapters | Negative cache | Boolean membership | Until transient/full clear |
| Output | Borrowed PackedScene | State | Loader scene instantiation | State retains reference | Lookup/instantiate call |
| Output | Detached request snapshot | State | Loader polling/shutdown | Caller-owned Dictionary | One drain pass |
## Data flow
```mermaid
flowchart TD
Lookup --> Known{Cached, missing or pending?}
Known -->|cached| Return[Return PackedScene]
Known -->|missing/pending| Null[Return null]
Known -->|absent| Check[Loader checks file and size]
Check -->|missing/oversize| Mark[mark_missing]
Check -->|allowed| Request[Start threaded request]
Request -->|error| Mark
Request -->|accepted| Remember[remember_request]
Remember --> Poll[Loader polls snapshot]
Poll -->|failure| CompleteMissing[complete as missing]
Poll -->|loaded| Validate[Instantiate and validate cache metadata]
Validate -->|valid| CompleteScene[complete with scene]
Validate -->|invalid| CompleteMissing
```
## Lifecycle/state
Each normalized path is `Absent`, `Pending`, `Cached` or `Missing`. Direct file,
size and request-start rejection moves `Absent` directly to `Missing`; terminal
loads move `Pending` to `Cached` or `Missing`. Transient clear retains `Cached`,
while full clear returns every path to `Absent`.
```mermaid
stateDiagram-v2
[*] --> Absent
Absent --> Pending: accepted request
Absent --> Missing: absent, oversize or request error
Pending --> Cached: validated PackedScene
Pending --> Missing: load or validation failure
Pending --> Absent: transient/full clear
Missing --> Absent: transient/full clear
Cached --> Absent: full clear
```
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant State as WmoSceneResourceCacheState
participant RL as ResourceLoader
Loader->>State: cached/missing/pending queries
Loader->>Loader: exists and wmo_max_runtime_scene_mb check
Loader->>RL: load_threaded_request(.tscn)
Loader->>State: remember_request(normalized, path)
loop renderer tick
Loader->>State: request_paths_snapshot()
Loader->>RL: load_threaded_get_status(path)
end
Loader->>RL: load_threaded_get(path)
Loader->>Loader: instantiate, validate WMO metadata, free probe
Loader->>State: complete with scene or as missing
```
## Ownership, threading and resources
- State owns three Dictionaries and strong references to accepted PackedScenes.
- Loader owns paths, file measurement, requests, validation and all Node lifetime.
- All mutation is serialized on the renderer main thread; no mutex is required.
- Detached request snapshots allow terminal removal while polling.
- Scene instantiation borrows the PackedScene and does not transfer cache ownership.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Recovery |
|---|---|---|---|
| Missing `.tscn` | Loader ResourceLoader existence check | Direct missing state | Transient reset permits later retry |
| Oversize `.tscn` | Loader byte limit | Missing plus unchanged debug log | Raise limit/rebuild, then reset |
| Request-start/load failure | Loader error/status | Direct or terminal missing | Reset and retry later |
| Wrong type/stale metadata | Loader PackedScene/probe validation | Terminal missing; probe freed | Rebuild cache and reset |
| Shutdown while pending | Loader drains snapshot | Clear transient, then full cache release | New loader starts absent |
## Configuration and capabilities
No setting is added. `wmo_cache_dir`, `wmo_max_runtime_scene_mb`, request timing,
quality profiles and WMO build budgets stay loader-owned.
## Persistence, cache and migration
The module is runtime-only. Existing WMOBuilder cache metadata/version checks
remain unchanged and execute before state adoption. No cache format, migration or
rebuild is introduced.
## Diagnostics and observability
Pending count preserves the existing `wmobuild` metric contribution. The snapshot
exposes sorted scene/missing/request keys only. Existing oversize logging remains
in the loader; the state emits no logs.
## Verification
- `verify_wmo_scene_resource_cache_state.gd`: invalid input, direct missing,
exclusivity, request completion, partial/full reset, diagnostics, boundaries
and bounded timing.
- `verify_render_runtime_cache_shutdown.gd`: loader full release of scenes,
negative and pending state.
- Adjacent WMO/renderer regressions verify unchanged lookup/fallback behavior.
This extraction makes no visual or build-12340 parity claim.
## Extension points
- A later I/O adapter may consume request snapshots while retaining this contract.
- Scene instantiation/material preparation can be extracted separately because it
owns Nodes and main-thread side effects rather than cache state.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Cached WMO PackedScene state | Implemented extraction | Lifecycle/source/timing and shutdown verifiers | Asset-backed traversal/leak evidence pending |
| Size and cache metadata validation | Preserved in loader | Source boundary and WMO regressions | Oversize/stale asset fixtures could follow |
| ResourceLoader I/O and live fallback | Partial/loader-owned | Existing runtime behavior | Separate extraction if justified |
## Known gaps and risks
- PackedScenes remain strongly referenced until final shutdown cache release.
- Negative caching suppresses retries until transient reset, matching prior behavior.
- No proprietary WMO corpus, stale/oversize asset fixture, leak run, traversal
p95/p99 or paired original-client capture is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_scene_resource_cache_state.gd` | Scene/missing/request state and resets |
| `src/scenes/streaming/streaming_world_loader.gd` | Paths, file limit, requests, validation, fallback and Node lifetime |
| `src/tools/verify_wmo_scene_resource_cache_state.gd` | State, boundary and timing regression |
| `src/tools/verify_render_runtime_cache_shutdown.gd` | Loader final cache ownership regression |
## Related decisions and references
- [`wmo-render-resource-cache-state.md`](wmo-render-resource-cache-state.md)
- [`wmo-render-build-queue.md`](wmo-render-build-queue.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)
+48 -4
View File
@@ -7,7 +7,7 @@
| Status | Partial |
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
| Owners | Renderer workstream / milestone integrator |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-placement-resolver`, 2026-07-17 |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-scene-resource-cache`, 2026-07-17 |
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose
@@ -56,6 +56,12 @@ flowchart LR
M2Batch --> Loader
Loader --> WmoPlacement[WmoPlacementResolver]
WmoPlacement --> Loader
Loader --> WmoRegistry[WmoPlacementRegistry]
WmoRegistry --> Loader
Loader --> WmoBuildStep[WmoRenderBuildStepPlanner]
WmoBuildStep --> Loader
Loader --> WmoBuildQueue[WmoRenderBuildQueue]
WmoBuildQueue --> Loader
Native --> Parsed[Parsed tile/model data]
Parsed --> Loader
Loader --> Scene[SceneTree nodes]
@@ -127,6 +133,11 @@ from externally reading/writing loader-private queue, task, cache and tile-state
| `M2PlacementGrouper.group_placements` | Internal pure M2 service | Validates and groups ordered tile-local placement transforms by normalized path | Worker/main thread; stateless | Invalid variants/name IDs/empty paths are skipped |
| `M2BuildBatchPlanner.plan_batch` | Internal pure M2 service | Selects static/animated batch count and next group cursor | Main/any thread; stateless | Non-positive selected limit clamps to one; empty range completes |
| `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 |
| `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 |
| `WmoRenderResourceCacheState` | Internal WMO cache-state service | Owns validated Resources, negative entries and pending cache paths | Renderer main thread; map/cache session | Invalid/occupied request and unknown completion are rejected |
| `WmoSceneResourceCacheState` | Internal WMO cache-state service | Owns validated PackedScenes, negative entries and pending `.tscn` paths | Renderer main thread; map/cache session | Direct missing and terminal request transitions remain distinct |
Публичным contract не считаются `StreamingWorldLoader` methods/properties,
внутренние dictionaries, queues, job records и generated node names. Scene-owned
@@ -156,6 +167,11 @@ loader configuration remains transitional composition data, not a caller API.
| Internal grouping | Tile origin, M2 names and placements | Loader / `M2PlacementGrouper` | Loader worker result/build job | Fresh Dictionary/Transform3D arrays | One grouping task |
| Internal batch plan | Transform count/offset, path kind and limits | Loader / `M2BuildBatchPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One build operation |
| 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 build step | Mesh/MultiMesh counts and job cursors | Loader / `WmoRenderBuildStepPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One group operation |
| Internal WMO pending build | Placement key, Node3D root, WMO Resource and cursors | Loader / `WmoRenderBuildQueue` | Loader drain and step planner adapter | Queue-owned job and strong references | Until cancel/clear/replacement |
| Internal WMO render cache | Normalized path, cache path and validated Resource | Loader / `WmoRenderResourceCacheState` | Loader lookup, ResourceLoader poll and build queue | State-owned Resource/path references; detached request snapshots | Until transient/full clear |
| Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear |
| 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 | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
@@ -205,7 +221,10 @@ flowchart TD
M2Grouper --> M2Batch[M2BuildBatchPlanner]
M2Batch --> M2
R --> WmoPlacement[WmoPlacementResolver]
WmoPlacement --> WMO
WmoPlacement --> WmoRegistry[WmoPlacementRegistry]
WmoRegistry --> WmoBuildQueue[WmoRenderBuildQueue]
WmoBuildQueue --> WmoBuildStep[WmoRenderBuildStepPlanner]
WmoBuildStep --> WMO
R --> B[RenderBudgetScheduler permits]
B --> Terrain[Terrain attach/upgrade]
B --> M2[M2 group/MultiMesh attach]
@@ -328,7 +347,15 @@ sequenceDiagram
- `M2BuildBatchPlanner` is stateless and owns only call-local scalar plans. The
loader retains queue/resource transitions, cursor adoption and materialization.
- `WmoPlacementResolver` is stateless and owns only call-local cache-key,
identity and transform values. The loader retains WMO refs/caches/jobs/resources.
identity and transform values. `WmoPlacementRegistry` owns only placement-key
reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation
and cursor plan. `WmoRenderBuildQueue` owns typed pending jobs, FIFO keys and
strong root/resource references without freeing engine objects.
`WmoRenderResourceCacheState` owns validated render Resources, negative entries
and pending cache paths. `WmoSceneResourceCacheState` similarly owns validated
PackedScenes, negative entries and pending `.tscn` paths. The loader retains
ResourceLoader/FileAccess I/O, size and cache-version validation, live fallback,
materialization, permits, validity reactions and every Node lifecycle action.
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
the facade never returns Mesh, Node, tile-state or queue references.
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
@@ -422,6 +449,18 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
count, completion/cursor rules, source boundary and bounded timing.
- WMO placement contract: cache-path normalization, positive/fallback identity,
default/rotated/unclamped transforms, three consumers and bounded timing.
- WMO placement registry contract: first/idempotent/shared/non-owner/final/global
lifecycle, detached sorted diagnostics, source ownership and bounded timing.
- WMO render build step contract: mesh-before-MultiMesh order, one-index cursor
transition, completion/raw integer behavior, source ownership and bounded timing.
- WMO render build queue contract: typed references/cursors, FIFO, duplicate
replacement, stale-front cleanup, cancel/clear engine lifetime and bounded timing.
- WMO render Resource cache contract: invalid/duplicate request rejection,
validated/missing terminal transitions, transient/full reset, detached sorted
diagnostics, loader-owned version validation and bounded timing.
- WMO scene Resource cache contract: direct missing sources, pending terminal
transitions, transient/full reset, detached diagnostics, loader-owned size/
metadata validation and bounded timing.
- Budget scheduler contract: exact lane exhaustion, shared chunk removal/create
priority, independent lanes, frame reset, invalid limits, terminal cancellation,
dependency boundaries and bounded permit timing without loading a world scene.
@@ -455,7 +494,12 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| M2 placement transform resolver | Implemented extraction | Scene-free formula/source/timing contract across three consumers | Asset-backed visual recheck and general placement parity pending |
| M2 placement grouper | Implemented extraction | Scene-free validation/order/transform/source/timing contract | Worker/build 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 | Registry/build extraction and 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 render build step planner | Implemented extraction | Scene-free order/cursor/source/timing contract | Asset-backed traversal p95/p99 pending |
| WMO render build queue | Implemented extraction | Typed lifecycle/order/ownership/source/timing contract | Materialization and asset-backed traversal/leak evidence pending |
| WMO render Resource cache state | Implemented extraction | Scene-free lifecycle/exclusivity/source/timing plus shutdown contract | ResourceLoader I/O and asset-backed traversal/leak evidence pending |
| WMO scene Resource cache state | Implemented extraction | Scene-free lifecycle/direct-missing/source/timing plus shutdown contract | ResourceLoader/live-fallback extraction and asset-backed traversal/leak evidence pending |
| WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity |
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
| Sky/light | Partial | DBC controller/logs | Zone transition paired validation |
+69
View File
@@ -0,0 +1,69 @@
class_name WmoPlacementRegistry
extends RefCounted
## Owns WMO placement-key to tile/global reference-key sets without Nodes.
var _reference_keys_by_placement_key: Dictionary = {}
## Adds one idempotent reference and returns true only for a newly registered
## placement key. Empty placement/reference keys are rejected.
func add_reference(placement_key: String, reference_key: String) -> bool:
if placement_key.is_empty() or reference_key.is_empty():
return false
var is_new_placement := not _reference_keys_by_placement_key.has(placement_key)
if is_new_placement:
_reference_keys_by_placement_key[placement_key] = {}
var reference_keys: Dictionary = _reference_keys_by_placement_key[placement_key]
reference_keys[reference_key] = true
return is_new_placement
## Releases one reference and returns true only when the final reference removes
## the placement key. Unknown and non-owner references leave state unchanged.
func release_reference(placement_key: String, reference_key: String) -> bool:
if placement_key.is_empty() or reference_key.is_empty():
return false
if not _reference_keys_by_placement_key.has(placement_key):
return false
var reference_keys: Dictionary = _reference_keys_by_placement_key[placement_key]
if not reference_keys.has(reference_key):
return false
reference_keys.erase(reference_key)
if not reference_keys.is_empty():
return false
_reference_keys_by_placement_key.erase(placement_key)
return true
## Returns whether the placement key currently has at least one reference.
func contains(placement_key: String) -> bool:
return not placement_key.is_empty() and _reference_keys_by_placement_key.has(placement_key)
## Removes every placement/reference record. Safe to call repeatedly.
func clear() -> void:
_reference_keys_by_placement_key.clear()
## Returns the number of currently referenced placement keys.
func active_count() -> int:
return _reference_keys_by_placement_key.size()
## Returns detached, placement/reference-key-sorted diagnostic records.
func diagnostic_snapshot() -> Array[Dictionary]:
var placement_keys: Array = _reference_keys_by_placement_key.keys()
placement_keys.sort()
var snapshot: Array[Dictionary] = []
for placement_key_variant in placement_keys:
var placement_key := String(placement_key_variant)
var reference_keys: Array = (
_reference_keys_by_placement_key[placement_key] as Dictionary
).keys()
reference_keys.sort()
snapshot.append({
"placement_key": placement_key,
"reference_keys": reference_keys.duplicate(),
})
return snapshot
@@ -0,0 +1 @@
uid://cfbo5fw33vw4f
+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
@@ -0,0 +1,40 @@
class_name WmoRenderBuildStepPlanner
extends RefCounted
## Selects one value-only WMO render-group build operation without engine objects.
const OPERATION_MESH: StringName = &"mesh"
const OPERATION_MULTIMESH: StringName = &"multimesh"
const OPERATION_COMPLETE: StringName = &"complete"
## Selects the next mesh-first build operation and returns its selected index
## plus the cursor values to adopt after that one operation. Counts and cursors
## are intentionally not clamped so the loader's historical comparisons remain
## exact for every integer input.
func plan_step(
mesh_count: int,
mesh_index: int,
multimesh_count: int,
multimesh_index: int
) -> Dictionary:
if mesh_index < mesh_count:
return {
"operation": OPERATION_MESH,
"selected_index": mesh_index,
"next_mesh_index": mesh_index + 1,
"next_multimesh_index": multimesh_index,
}
if multimesh_index < multimesh_count:
return {
"operation": OPERATION_MULTIMESH,
"selected_index": multimesh_index,
"next_mesh_index": mesh_index,
"next_multimesh_index": multimesh_index + 1,
}
return {
"operation": OPERATION_COMPLETE,
"selected_index": -1,
"next_mesh_index": mesh_index,
"next_multimesh_index": multimesh_index,
}
@@ -0,0 +1 @@
uid://dqogt0el0x3kx
@@ -0,0 +1,126 @@
class_name WmoRenderResourceCacheState
extends RefCounted
## Owns validated lightweight-WMO render resources and their load state.
## ResourceLoader I/O and cache-format validation remain caller responsibilities.
var _resources_by_normalized_path: Dictionary = {}
var _missing_normalized_paths: Dictionary = {}
var _request_cache_paths_by_normalized_path: Dictionary = {}
## Returns the validated cached Resource for a normalized WMO path, or null.
func resource_for(normalized_relative_path: String) -> Resource:
if normalized_relative_path.is_empty():
return null
return _resources_by_normalized_path.get(normalized_relative_path) as Resource
## Returns whether a validated Resource is cached for the normalized WMO path.
func contains_resource(normalized_relative_path: String) -> bool:
return (
not normalized_relative_path.is_empty()
and _resources_by_normalized_path.has(normalized_relative_path)
)
## Returns whether the normalized WMO path is in the negative cache.
func is_missing(normalized_relative_path: String) -> bool:
return (
not normalized_relative_path.is_empty()
and _missing_normalized_paths.has(normalized_relative_path)
)
## Returns whether the normalized WMO path has a pending threaded load request.
func has_request(normalized_relative_path: String) -> bool:
return (
not normalized_relative_path.is_empty()
and _request_cache_paths_by_normalized_path.has(normalized_relative_path)
)
## Records one loader-owned threaded request. Cached, missing, duplicate or empty
## paths are rejected so each normalized WMO path has exactly one state.
func remember_request(normalized_relative_path: String, cache_path: String) -> bool:
if normalized_relative_path.is_empty() or cache_path.is_empty():
return false
if (
_resources_by_normalized_path.has(normalized_relative_path)
or _missing_normalized_paths.has(normalized_relative_path)
or _request_cache_paths_by_normalized_path.has(normalized_relative_path)
):
return false
_request_cache_paths_by_normalized_path[normalized_relative_path] = cache_path
return true
## Returns a detached normalized-path to cache-path snapshot for loader-owned
## ResourceLoader polling or shutdown draining.
func request_paths_snapshot() -> Dictionary:
return _request_cache_paths_by_normalized_path.duplicate()
## Completes a pending request with a caller-validated Resource. Returns false
## for unknown requests or null Resources and otherwise adopts the reference.
func complete_request_with_resource(
normalized_relative_path: String,
validated_resource: Resource
) -> bool:
if (
normalized_relative_path.is_empty()
or validated_resource == null
or not _request_cache_paths_by_normalized_path.has(normalized_relative_path)
):
return false
_request_cache_paths_by_normalized_path.erase(normalized_relative_path)
_missing_normalized_paths.erase(normalized_relative_path)
_resources_by_normalized_path[normalized_relative_path] = validated_resource
return true
## Completes a pending request as missing after load failure or validation
## rejection. Returns false for an unknown request.
func complete_request_as_missing(normalized_relative_path: String) -> bool:
if (
normalized_relative_path.is_empty()
or not _request_cache_paths_by_normalized_path.has(normalized_relative_path)
):
return false
_request_cache_paths_by_normalized_path.erase(normalized_relative_path)
_resources_by_normalized_path.erase(normalized_relative_path)
_missing_normalized_paths[normalized_relative_path] = true
return true
## Clears pending requests and negative entries while retaining validated
## Resources for map reset or orderly shutdown request draining.
func clear_transient_state() -> void:
_request_cache_paths_by_normalized_path.clear()
_missing_normalized_paths.clear()
## Releases every cached Resource and transient entry. Safe to call repeatedly.
func clear_all() -> void:
_resources_by_normalized_path.clear()
clear_transient_state()
## Returns the number of pending threaded load requests.
func pending_request_count() -> int:
return _request_cache_paths_by_normalized_path.size()
## Returns detached, sorted scalar keys without exposing Resource references.
func diagnostic_snapshot() -> Dictionary:
var resource_paths: Array = _resources_by_normalized_path.keys()
var missing_paths: Array = _missing_normalized_paths.keys()
var request_paths: Array = _request_cache_paths_by_normalized_path.keys()
resource_paths.sort()
missing_paths.sort()
request_paths.sort()
return {
"resource_paths": resource_paths.duplicate(),
"missing_paths": missing_paths.duplicate(),
"request_paths": request_paths.duplicate(),
}
@@ -0,0 +1 @@
uid://b1g4s1rqrptsw
@@ -0,0 +1,134 @@
class_name WmoSceneResourceCacheState
extends RefCounted
## Owns validated cached-WMO PackedScenes and their load state. File limits,
## ResourceLoader I/O and scene-cache validation remain caller responsibilities.
var _scenes_by_normalized_path: Dictionary = {}
var _missing_normalized_paths: Dictionary = {}
var _request_cache_paths_by_normalized_path: Dictionary = {}
## Returns the validated PackedScene for a normalized WMO path, or null.
func scene_for(normalized_relative_path: String) -> PackedScene:
if normalized_relative_path.is_empty():
return null
return _scenes_by_normalized_path.get(normalized_relative_path) as PackedScene
## Returns whether a validated PackedScene is cached for the normalized path.
func contains_scene(normalized_relative_path: String) -> bool:
return (
not normalized_relative_path.is_empty()
and _scenes_by_normalized_path.has(normalized_relative_path)
)
## Returns whether the normalized path is in the negative cache.
func is_missing(normalized_relative_path: String) -> bool:
return (
not normalized_relative_path.is_empty()
and _missing_normalized_paths.has(normalized_relative_path)
)
## Returns whether the normalized path has a pending threaded load request.
func has_request(normalized_relative_path: String) -> bool:
return (
not normalized_relative_path.is_empty()
and _request_cache_paths_by_normalized_path.has(normalized_relative_path)
)
## Records one loader-started request. Occupied or empty paths are rejected.
func remember_request(normalized_relative_path: String, cache_path: String) -> bool:
if normalized_relative_path.is_empty() or cache_path.is_empty():
return false
if _has_any_state(normalized_relative_path):
return false
_request_cache_paths_by_normalized_path[normalized_relative_path] = cache_path
return true
## Records an absent, oversize or request-start failure without requiring a
## pending request. Occupied and empty paths are rejected.
func mark_missing(normalized_relative_path: String) -> bool:
if normalized_relative_path.is_empty() or _has_any_state(normalized_relative_path):
return false
_missing_normalized_paths[normalized_relative_path] = true
return true
## Returns a detached normalized-path to cache-path snapshot for loader polling.
func request_paths_snapshot() -> Dictionary:
return _request_cache_paths_by_normalized_path.duplicate()
## Completes a pending request with a caller-validated PackedScene.
func complete_request_with_scene(
normalized_relative_path: String,
validated_scene: PackedScene
) -> bool:
if (
normalized_relative_path.is_empty()
or validated_scene == null
or not _request_cache_paths_by_normalized_path.has(normalized_relative_path)
):
return false
_request_cache_paths_by_normalized_path.erase(normalized_relative_path)
_missing_normalized_paths.erase(normalized_relative_path)
_scenes_by_normalized_path[normalized_relative_path] = validated_scene
return true
## Completes a pending request as missing after load or validation failure.
func complete_request_as_missing(normalized_relative_path: String) -> bool:
if (
normalized_relative_path.is_empty()
or not _request_cache_paths_by_normalized_path.has(normalized_relative_path)
):
return false
_request_cache_paths_by_normalized_path.erase(normalized_relative_path)
_scenes_by_normalized_path.erase(normalized_relative_path)
_missing_normalized_paths[normalized_relative_path] = true
return true
## Clears pending and negative state while retaining validated PackedScenes.
func clear_transient_state() -> void:
_request_cache_paths_by_normalized_path.clear()
_missing_normalized_paths.clear()
## Releases every cached scene and transient entry. Safe to call repeatedly.
func clear_all() -> void:
_scenes_by_normalized_path.clear()
clear_transient_state()
## Returns the number of pending threaded load requests.
func pending_request_count() -> int:
return _request_cache_paths_by_normalized_path.size()
## Returns detached, sorted scalar keys without exposing PackedScene references.
func diagnostic_snapshot() -> Dictionary:
var scene_paths: Array = _scenes_by_normalized_path.keys()
var missing_paths: Array = _missing_normalized_paths.keys()
var request_paths: Array = _request_cache_paths_by_normalized_path.keys()
scene_paths.sort()
missing_paths.sort()
request_paths.sort()
return {
"scene_paths": scene_paths.duplicate(),
"missing_paths": missing_paths.duplicate(),
"request_paths": request_paths.duplicate(),
}
func _has_any_state(normalized_relative_path: String) -> bool:
return (
_scenes_by_normalized_path.has(normalized_relative_path)
or _missing_normalized_paths.has(normalized_relative_path)
or _request_cache_paths_by_normalized_path.has(normalized_relative_path)
)
@@ -0,0 +1 @@
uid://b4geepupdscu7
+170 -136
View File
@@ -12,6 +12,21 @@ const WMO_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/wmo_buil
const WMO_PLACEMENT_RESOLVER_SCRIPT := preload(
"res://src/render/wmo/wmo_placement_resolver.gd"
)
const WMO_PLACEMENT_REGISTRY_SCRIPT := preload(
"res://src/render/wmo/wmo_placement_registry.gd"
)
const WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT := preload(
"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 WMO_RENDER_RESOURCE_CACHE_STATE_SCRIPT := preload(
"res://src/render/wmo/wmo_render_resource_cache_state.gd"
)
const WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT := preload(
"res://src/render/wmo/wmo_scene_resource_cache_state.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_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
@@ -271,14 +286,9 @@ var _tile_mesh_cache: Dictionary = {}
var _tile_mesh_cache_order: Array = []
var _terrain_quality_mesh_cache: RefCounted = TERRAIN_QUALITY_MESH_CACHE_SCRIPT.new()
var _wmo_prototype_cache: Dictionary = {}
var _wmo_render_cache: Dictionary = {}
var _wmo_render_missing_cache: Dictionary = {}
var _wmo_render_load_requests: Dictionary = {}
var _wmo_render_build_jobs: Dictionary = {}
var _wmo_render_build_queue: Array = []
var _wmo_scene_resource_cache: Dictionary = {}
var _wmo_scene_cache_missing: Dictionary = {}
var _wmo_scene_load_requests: Dictionary = {}
var _wmo_render_resource_cache_state := WMO_RENDER_RESOURCE_CACHE_STATE_SCRIPT.new()
var _wmo_render_build_queue := WMO_RENDER_BUILD_QUEUE_SCRIPT.new()
var _wmo_scene_resource_cache_state := WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT.new()
var _wmo_missing_cache: Dictionary = {}
var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new()
var _m2_scene_cache: Dictionary = {}
@@ -286,7 +296,9 @@ var _m2_animated_scene_cache: Dictionary = {}
var _m2_static_animation_cache: Dictionary = {}
var _m2_missing_cache: Dictionary = {}
var _world_wmo_root: Node3D
var _wmo_registry: Dictionary = {}
var _wmo_placement_registry := WMO_PLACEMENT_REGISTRY_SCRIPT.new()
var _wmo_render_build_step_planner := WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.new()
var _wmo_instance_nodes_by_unique_key: Dictionary = {}
func _ready() -> void:
@@ -457,10 +469,8 @@ func _release_runtime_caches_for_shutdown() -> void:
_m2_mesh_cache.clear()
_m2_static_animation_cache.clear()
_m2_missing_cache.clear()
_wmo_render_cache.clear()
_wmo_scene_resource_cache.clear()
_wmo_render_missing_cache.clear()
_wmo_scene_cache_missing.clear()
_wmo_render_resource_cache_state.clear_all()
_wmo_scene_resource_cache_state.clear_all()
_wmo_missing_cache.clear()
_shared_tex_cache.clear()
@@ -995,8 +1005,12 @@ func _log_hitch_profile(start_usec: int, timings: Array[String], did_refresh: bo
_m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(),
_m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(),
_m2_build_queue.size(),
_wmo_build_queue.size() + _wmo_scene_load_requests.size() + _wmo_render_load_requests.size(),
_wmo_render_build_queue.size(),
(
_wmo_build_queue.size()
+ _wmo_scene_resource_cache_state.pending_request_count()
+ _wmo_render_resource_cache_state.pending_request_count()
),
_wmo_render_build_queue.pending_count(),
_tile_lod_create_queue.size(),
_tile_lod_remove_queue.size(),
_chunk_create_queue.size(),
@@ -1022,8 +1036,12 @@ func render_baseline_snapshot() -> Dictionary:
"m2_animation": _m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(),
"m2_mesh": _m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(),
"m2_build": _m2_build_queue.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_build": (
_wmo_build_queue.size()
+ _wmo_scene_resource_cache_state.pending_request_count()
+ _wmo_render_resource_cache_state.pending_request_count()
),
"wmo_groups": _wmo_render_build_queue.pending_count(),
"lod_create": _tile_lod_create_queue.size(),
"lod_remove": _tile_lod_remove_queue.size(),
"chunk_create": _chunk_create_queue.size(),
@@ -1031,7 +1049,7 @@ func render_baseline_snapshot() -> Dictionary:
},
"cache_entries": _tile_mesh_cache.size(),
"m2_active_unique_ids": _m2_unique_placement_registry.active_count(),
"wmo_instances": _wmo_registry.size(),
"wmo_instances": _wmo_placement_registry.active_count(),
}
@@ -1304,8 +1322,12 @@ func _tick_runtime_stats(delta: float) -> void:
_m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(),
_m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(),
_m2_build_queue.size(),
_wmo_build_queue.size() + _wmo_scene_load_requests.size() + _wmo_render_load_requests.size(),
_wmo_render_build_queue.size(),
(
_wmo_build_queue.size()
+ _wmo_scene_resource_cache_state.pending_request_count()
+ _wmo_render_resource_cache_state.pending_request_count()
),
_wmo_render_build_queue.pending_count(),
_tile_lod_create_queue.size(),
_tile_lod_remove_queue.size(),
_chunk_create_queue.size(),
@@ -1313,7 +1335,7 @@ func _tick_runtime_stats(delta: float) -> void:
m2_tiles,
m2_multimeshes,
m2_instances,
_wmo_registry.size(),
_wmo_placement_registry.active_count(),
])
@@ -2337,26 +2359,27 @@ func _wait_for_tile_tasks() -> void:
_m2_animation_load_requests.clear()
_m2_animation_finalize_queue.clear()
for path_variant in _wmo_scene_load_requests.values():
var wmo_scene_request_paths := _wmo_scene_resource_cache_state.request_paths_snapshot()
for path_variant in wmo_scene_request_paths.values():
var path := String(path_variant)
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_wmo_scene_load_requests.clear()
_wmo_scene_resource_cache_state.clear_transient_state()
for path_variant in _wmo_render_load_requests.values():
var wmo_render_request_paths := (
_wmo_render_resource_cache_state.request_paths_snapshot()
)
for path_variant in wmo_render_request_paths.values():
var path := String(path_variant)
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_wmo_render_load_requests.clear()
_wmo_render_missing_cache.clear()
_wmo_scene_cache_missing.clear()
_wmo_render_build_jobs.clear()
_wmo_render_resource_cache_state.clear_transient_state()
_wmo_render_build_queue.clear()
for pending in _terrain_upgrade_tasks.values():
@@ -2992,11 +3015,8 @@ func _clear_streamed_world() -> void:
_m2_animation_finalize_queue.clear()
_wmo_build_jobs.clear()
_wmo_build_queue.clear()
_wmo_scene_load_requests.clear()
_wmo_render_load_requests.clear()
_wmo_render_missing_cache.clear()
_wmo_scene_cache_missing.clear()
_wmo_render_build_jobs.clear()
_wmo_scene_resource_cache_state.clear_transient_state()
_wmo_render_resource_cache_state.clear_transient_state()
_wmo_render_build_queue.clear()
_tile_result_mutex.lock()
@@ -3363,7 +3383,7 @@ func _wdt_has_global_wmo() -> bool:
func _place_global_wmo() -> void:
if not _wdt_has_global_wmo():
return
if _wmo_registry.has("global"):
if _wmo_placement_registry.contains("global"):
return
_ensure_world_wmo_root()
@@ -3387,10 +3407,7 @@ func _place_global_wmo() -> void:
_world_wmo_root.add_child(instance)
_set_editor_owner_recursive(instance)
_wmo_registry["global"] = {
"node": instance,
"refs": {"__global__": true},
}
_register_wmo_instance_reference("global", "__global__", instance)
func _sync_detail_assets(state: Dictionary, focus_pos: Vector3) -> Dictionary:
@@ -3566,10 +3583,8 @@ func _process_wmo_build_jobs() -> void:
tile_key,
index
)
if _wmo_registry.has(unique_key):
var entry: Dictionary = _wmo_registry[unique_key]
var entry_refs: Dictionary = entry["refs"]
entry_refs[tile_key] = true
if _wmo_placement_registry.contains(unique_key):
_wmo_placement_registry.add_reference(unique_key, tile_key)
refs.append(unique_key)
else:
var rel_path: String = str(wmo_names[name_id]).replace("\\", "/")
@@ -3579,22 +3594,19 @@ func _process_wmo_build_jobs() -> void:
if instance != null:
_world_wmo_root.add_child(instance)
_set_editor_owner_recursive(instance)
_wmo_registry[unique_key] = {
"node": instance,
"refs": {tile_key: true},
}
_register_wmo_instance_reference(unique_key, tile_key, instance)
refs.append(unique_key)
_start_wmo_render_build(unique_key, instance, render_resource)
else:
var normalized_rel := _wmo_placement_resolver.normalize_relative_path(
rel_path
)
if _wmo_render_load_requests.has(normalized_rel):
if _wmo_render_resource_cache_state.has_request(normalized_rel):
advance_job = false
else:
var scene := _get_wmo_scene_or_request(rel_path)
if scene == null:
if _wmo_scene_load_requests.has(normalized_rel):
if _wmo_scene_resource_cache_state.has_request(normalized_rel):
advance_job = false
else:
var live_instance := _instantiate_wmo_world(rel_path, placement)
@@ -3602,10 +3614,11 @@ func _process_wmo_build_jobs() -> void:
_prepare_runtime_wmo_instance(live_instance)
_world_wmo_root.add_child(live_instance)
_set_editor_owner_recursive(live_instance)
_wmo_registry[unique_key] = {
"node": live_instance,
"refs": {tile_key: true},
}
_register_wmo_instance_reference(
unique_key,
tile_key,
live_instance
)
refs.append(unique_key)
else:
var scene_instance := _instantiate_wmo_scene(rel_path, scene, placement)
@@ -3613,10 +3626,11 @@ func _process_wmo_build_jobs() -> void:
_prepare_runtime_wmo_instance(scene_instance)
_world_wmo_root.add_child(scene_instance)
_set_editor_owner_recursive(scene_instance)
_wmo_registry[unique_key] = {
"node": scene_instance,
"refs": {tile_key: true},
}
_register_wmo_instance_reference(
unique_key,
tile_key,
scene_instance
)
refs.append(unique_key)
if advance_job:
job["refs"] = refs
@@ -3647,33 +3661,36 @@ func _process_wmo_build_jobs() -> void:
func _drain_wmo_scene_loads() -> void:
for normalized_rel_variant in _wmo_scene_load_requests.keys():
var request_paths := _wmo_scene_resource_cache_state.request_paths_snapshot()
for normalized_rel_variant in request_paths.keys():
var normalized_rel := String(normalized_rel_variant)
var path := String(_wmo_scene_load_requests[normalized_rel])
var path := String(request_paths[normalized_rel])
var status := ResourceLoader.load_threaded_get_status(path)
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
continue
_wmo_scene_load_requests.erase(normalized_rel)
if status != ResourceLoader.THREAD_LOAD_LOADED:
_wmo_scene_cache_missing[normalized_rel] = true
_wmo_scene_resource_cache_state.complete_request_as_missing(normalized_rel)
continue
var resource: Resource = ResourceLoader.load_threaded_get(path)
if resource is PackedScene and _is_wmo_scene_cache_current(resource as PackedScene):
_wmo_scene_resource_cache[normalized_rel] = resource as PackedScene
_wmo_scene_resource_cache_state.complete_request_with_scene(
normalized_rel,
resource as PackedScene
)
else:
_wmo_scene_cache_missing[normalized_rel] = true
_wmo_scene_resource_cache_state.complete_request_as_missing(normalized_rel)
func _drain_wmo_render_loads() -> void:
for normalized_rel_variant in _wmo_render_load_requests.keys():
var request_paths := _wmo_render_resource_cache_state.request_paths_snapshot()
for normalized_rel_variant in request_paths.keys():
var normalized_rel := String(normalized_rel_variant)
var path := String(_wmo_render_load_requests[normalized_rel])
var path := String(request_paths[normalized_rel])
var status := ResourceLoader.load_threaded_get_status(path)
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
continue
_wmo_render_load_requests.erase(normalized_rel)
if status != ResourceLoader.THREAD_LOAD_LOADED:
_wmo_render_missing_cache[normalized_rel] = true
_wmo_render_resource_cache_state.complete_request_as_missing(normalized_rel)
continue
var resource: Resource = ResourceLoader.load_threaded_get(path)
if (
@@ -3681,20 +3698,23 @@ func _drain_wmo_render_loads() -> void:
and resource.get_script() == WMO_STREAMING_SCRIPT
and int(resource.get("format_version")) >= WMO_STREAMING_SCRIPT.FORMAT_VERSION
):
_wmo_render_cache[normalized_rel] = resource
_wmo_render_resource_cache_state.complete_request_with_resource(
normalized_rel,
resource
)
else:
_wmo_render_missing_cache[normalized_rel] = true
_wmo_render_resource_cache_state.complete_request_as_missing(normalized_rel)
func _get_wmo_render_or_request(rel_path: String) -> Resource:
var normalized_rel := _wmo_placement_resolver.normalize_relative_path(rel_path)
if normalized_rel.is_empty():
return null
if _wmo_render_cache.has(normalized_rel):
return _wmo_render_cache[normalized_rel]
if _wmo_render_missing_cache.has(normalized_rel):
if _wmo_render_resource_cache_state.contains_resource(normalized_rel):
return _wmo_render_resource_cache_state.resource_for(normalized_rel)
if _wmo_render_resource_cache_state.is_missing(normalized_rel):
return null
if _wmo_render_load_requests.has(normalized_rel):
if _wmo_render_resource_cache_state.has_request(normalized_rel):
return null
var cache_path := wmo_render_cache_dir.path_join(normalized_rel.get_basename() + ".res")
@@ -3707,7 +3727,7 @@ func _get_wmo_render_or_request(rel_path: String) -> Resource:
false,
ResourceLoader.CACHE_MODE_REUSE)
if err == OK or err == ERR_BUSY:
_wmo_render_load_requests[normalized_rel] = cache_path
_wmo_render_resource_cache_state.remember_request(normalized_rel, cache_path)
return null
@@ -3715,23 +3735,23 @@ func _get_wmo_scene_or_request(rel_path: String) -> PackedScene:
var normalized_rel := _wmo_placement_resolver.normalize_relative_path(rel_path)
if normalized_rel.is_empty():
return null
if _wmo_scene_resource_cache.has(normalized_rel):
return _wmo_scene_resource_cache[normalized_rel]
if _wmo_scene_cache_missing.has(normalized_rel):
if _wmo_scene_resource_cache_state.contains_scene(normalized_rel):
return _wmo_scene_resource_cache_state.scene_for(normalized_rel)
if _wmo_scene_resource_cache_state.is_missing(normalized_rel):
return null
if _wmo_scene_load_requests.has(normalized_rel):
if _wmo_scene_resource_cache_state.has_request(normalized_rel):
return null
var cache_path := wmo_cache_dir.path_join(normalized_rel.get_basename() + ".tscn")
if not ResourceLoader.exists(cache_path):
_wmo_scene_cache_missing[normalized_rel] = true
_wmo_scene_resource_cache_state.mark_missing(normalized_rel)
return null
if wmo_max_runtime_scene_mb > 0.0:
var size_bytes := _get_resource_file_size(cache_path)
var max_bytes := int(wmo_max_runtime_scene_mb * 1024.0 * 1024.0)
if size_bytes > max_bytes:
_wmo_scene_cache_missing[normalized_rel] = true
_wmo_scene_resource_cache_state.mark_missing(normalized_rel)
if debug_streaming:
print("StreamingWorld: skipping large WMO %.2f MB: %s" % [
float(size_bytes) / (1024.0 * 1024.0),
@@ -3745,9 +3765,9 @@ func _get_wmo_scene_or_request(rel_path: String) -> PackedScene:
false,
ResourceLoader.CACHE_MODE_REUSE)
if err == OK or err == ERR_BUSY:
_wmo_scene_load_requests[normalized_rel] = cache_path
_wmo_scene_resource_cache_state.remember_request(normalized_rel, cache_path)
else:
_wmo_scene_cache_missing[normalized_rel] = true
_wmo_scene_resource_cache_state.mark_missing(normalized_rel)
return null
@@ -3759,32 +3779,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:
if unique_key.is_empty() or root == null or render_resource == null:
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)
_wmo_render_build_queue.enqueue(unique_key, root, render_resource)
func _process_wmo_render_build_jobs() -> void:
while _render_budget_scheduler.has_remaining_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_RENDER_GROUP_BUILD) and not _wmo_render_build_queue.is_empty():
var unique_key := String(_wmo_render_build_queue.front())
if not _wmo_render_build_jobs.has(unique_key):
RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_RENDER_GROUP_BUILD) and _wmo_render_build_queue.has_pending():
var unique_key := _wmo_render_build_queue.front_key()
if not _wmo_render_build_queue.has_job(unique_key):
_wmo_render_build_queue.pop_front()
continue
var job: Dictionary = _wmo_render_build_jobs[unique_key]
var root: Node = job.get("root", null)
var root: Node = _wmo_render_build_queue.root_for(unique_key)
if root == null or not is_instance_valid(root):
_cancel_wmo_render_build_job(unique_key)
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:
_cancel_wmo_render_build_job(unique_key)
continue
@@ -3792,16 +3803,31 @@ func _process_wmo_render_build_jobs() -> void:
var meshes: Array = render_resource.get("meshes")
var mesh_transforms: Array = render_resource.get("mesh_transforms")
var mesh_names: PackedStringArray = render_resource.get("mesh_names")
var mesh_index: int = int(job.get("mesh_index", 0))
if mesh_index < meshes.size():
var mesh := meshes[mesh_index] as Mesh
var mesh_index := _wmo_render_build_queue.mesh_index_for(unique_key)
var multimeshes: Array = render_resource.get("multimeshes")
var multimesh_transforms: Array = render_resource.get("multimesh_transforms")
var multimesh_names: PackedStringArray = render_resource.get("multimesh_names")
var multimesh_index := _wmo_render_build_queue.multimesh_index_for(unique_key)
var build_step: Dictionary = _wmo_render_build_step_planner.plan_step(
meshes.size(),
mesh_index,
multimeshes.size(),
multimesh_index
)
var selected_index := int(build_step["selected_index"])
if build_step["operation"] == WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.OPERATION_MESH:
var mesh := meshes[selected_index] as Mesh
if mesh != null:
_refresh_cached_wmo_mesh_materials(mesh)
var mesh_instance := MeshInstance3D.new()
mesh_instance.name = mesh_names[mesh_index] if mesh_index < mesh_names.size() else "Group_%d" % mesh_index
mesh_instance.name = (
mesh_names[selected_index]
if selected_index < mesh_names.size()
else "Group_%d" % selected_index
)
mesh_instance.mesh = mesh
if mesh_index < mesh_transforms.size():
mesh_instance.transform = mesh_transforms[mesh_index]
if selected_index < mesh_transforms.size():
mesh_instance.transform = mesh_transforms[selected_index]
mesh_instance.cast_shadow = (
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
if wmo_cast_shadows
@@ -3812,28 +3838,27 @@ func _process_wmo_render_build_jobs() -> void:
mesh_instance.visibility_range_end_margin = CHUNK_SIZE
(root as Node3D).add_child(mesh_instance)
_set_editor_owner_recursive(mesh_instance)
job["mesh_index"] = mesh_index + 1
_wmo_render_build_jobs[unique_key] = job
_wmo_render_build_queue.adopt_cursors(
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)
continue
var multimeshes: Array = render_resource.get("multimeshes")
var multimesh_transforms: Array = render_resource.get("multimesh_transforms")
var multimesh_names: PackedStringArray = render_resource.get("multimesh_names")
var multimesh_index: int = int(job.get("multimesh_index", 0))
if multimesh_index < multimeshes.size():
var multimesh := multimeshes[multimesh_index] as MultiMesh
if build_step["operation"] == WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.OPERATION_MULTIMESH:
var multimesh := multimeshes[selected_index] as MultiMesh
if multimesh != null:
_refresh_cached_wmo_mesh_materials(multimesh.mesh)
var multimesh_instance := MultiMeshInstance3D.new()
multimesh_instance.name = (
multimesh_names[multimesh_index]
if multimesh_index < multimesh_names.size()
else "DoodadGroup_%d" % multimesh_index
multimesh_names[selected_index]
if selected_index < multimesh_names.size()
else "DoodadGroup_%d" % selected_index
)
multimesh_instance.multimesh = multimesh
if multimesh_index < multimesh_transforms.size():
multimesh_instance.transform = multimesh_transforms[multimesh_index]
if selected_index < multimesh_transforms.size():
multimesh_instance.transform = multimesh_transforms[selected_index]
multimesh_instance.cast_shadow = (
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
if wmo_cast_shadows
@@ -3844,8 +3869,11 @@ func _process_wmo_render_build_jobs() -> void:
multimesh_instance.visibility_range_end_margin = CHUNK_SIZE
(root as Node3D).add_child(multimesh_instance)
_set_editor_owner_recursive(multimesh_instance)
job["multimesh_index"] = multimesh_index + 1
_wmo_render_build_jobs[unique_key] = job
_wmo_render_build_queue.adopt_cursors(
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)
continue
@@ -3853,9 +3881,7 @@ func _process_wmo_render_build_jobs() -> void:
func _cancel_wmo_render_build_job(unique_key: String) -> void:
if _wmo_render_build_jobs.has(unique_key):
_wmo_render_build_jobs.erase(unique_key)
_wmo_render_build_queue.erase(unique_key)
_wmo_render_build_queue.cancel(unique_key)
func _get_resource_file_size(path: String) -> int:
@@ -3994,6 +4020,17 @@ func _cancel_wmo_build_job(tile_key: String) -> void:
_wmo_build_queue.erase(tile_key)
func _register_wmo_instance_reference(
placement_key: String,
reference_key: String,
instance: Node3D
) -> void:
if placement_key.is_empty() or reference_key.is_empty() or instance == null:
return
_wmo_instance_nodes_by_unique_key[placement_key] = instance
_wmo_placement_registry.add_reference(placement_key, reference_key)
func _unregister_tile_wmos(state: Dictionary) -> void:
var refs: Array = state.get("wmo_refs", [])
var tile_key: String = String(state.get("key", ""))
@@ -4003,34 +4040,31 @@ func _unregister_tile_wmos(state: Dictionary) -> void:
state["wmo_building"] = false
return
for unique_key in refs:
if not _wmo_registry.has(unique_key):
for unique_key_variant in refs:
var unique_key := String(unique_key_variant)
if not _wmo_placement_registry.contains(unique_key):
continue
var entry: Dictionary = _wmo_registry[unique_key]
var entry_refs: Dictionary = entry["refs"]
entry_refs.erase(tile_key)
if entry_refs.is_empty():
if _wmo_placement_registry.release_reference(unique_key, tile_key):
_cancel_wmo_render_build_job(String(unique_key))
var node: Node = entry.get("node", null)
if node:
var node: Node = _wmo_instance_nodes_by_unique_key.get(unique_key, null)
if node != null and is_instance_valid(node):
node.queue_free()
_wmo_registry.erase(unique_key)
_wmo_instance_nodes_by_unique_key.erase(unique_key)
state["wmo_refs"] = []
state["wmo_building"] = false
func _clear_wmo_registry() -> void:
_wmo_render_build_jobs.clear()
_wmo_render_build_queue.clear()
for entry_variant in _wmo_registry.values():
if not (entry_variant is Dictionary):
for node_variant in _wmo_instance_nodes_by_unique_key.values():
if not (node_variant is Node):
continue
var entry: Dictionary = entry_variant
var node: Node = entry.get("node", null)
if node and is_instance_valid(node):
var node := node_variant as Node
if is_instance_valid(node):
_queue_free_streamed_node(node)
_wmo_registry.clear()
_wmo_instance_nodes_by_unique_key.clear()
_wmo_placement_registry.clear()
if _world_wmo_root and is_instance_valid(_world_wmo_root):
_queue_free_streamed_node(_world_wmo_root)
_world_wmo_root = null
@@ -38,10 +38,15 @@ func _verify_runtime_cache_ownership() -> void:
loader.set("_m2_mesh_cache", {"mesh": ArrayMesh.new()})
loader.set("_m2_static_animation_cache", {"static": true})
loader.set("_m2_missing_cache", {"missing": true})
loader.set("_wmo_render_cache", {"render": Resource.new()})
loader.set("_wmo_scene_resource_cache", {"scene": PackedScene.new()})
loader.set("_wmo_render_missing_cache", {"missing": true})
loader.set("_wmo_scene_cache_missing", {"missing": true})
var wmo_render_cache_state: RefCounted = loader.get("_wmo_render_resource_cache_state")
wmo_render_cache_state.call("remember_request", "render", "res://render.res")
wmo_render_cache_state.call("complete_request_with_resource", "render", Resource.new())
wmo_render_cache_state.call("remember_request", "missing", "res://missing.res")
wmo_render_cache_state.call("complete_request_as_missing", "missing")
var wmo_scene_cache_state: RefCounted = loader.get("_wmo_scene_resource_cache_state")
wmo_scene_cache_state.call("remember_request", "scene", "res://scene.tscn")
wmo_scene_cache_state.call("complete_request_with_scene", "scene", PackedScene.new())
wmo_scene_cache_state.call("mark_missing", "missing")
loader.set("_wmo_missing_cache", {"missing": true})
loader.set("_shared_tex_cache", {"texture": ImageTexture.new()})
@@ -57,15 +62,30 @@ func _verify_runtime_cache_ownership() -> void:
"_m2_mesh_cache",
"_m2_static_animation_cache",
"_m2_missing_cache",
"_wmo_render_cache",
"_wmo_scene_resource_cache",
"_wmo_render_missing_cache",
"_wmo_scene_cache_missing",
"_wmo_missing_cache",
"_shared_tex_cache",
]:
var cache: Dictionary = loader.get(cache_name)
_expect(cache.is_empty(), "%s should be empty after shutdown" % cache_name)
var wmo_render_snapshot: Dictionary = wmo_render_cache_state.call("diagnostic_snapshot")
_expect(
(wmo_render_snapshot["resource_paths"] as Array).is_empty(),
"WMO render Resources should be empty after shutdown"
)
_expect(
(wmo_render_snapshot["missing_paths"] as Array).is_empty(),
"WMO render negative cache should be empty after shutdown"
)
_expect(
(wmo_render_snapshot["request_paths"] as Array).is_empty(),
"WMO render requests should be empty after shutdown"
)
var wmo_scene_snapshot: Dictionary = wmo_scene_cache_state.call("diagnostic_snapshot")
for state_key in ["scene_paths", "missing_paths", "request_paths"]:
_expect(
(wmo_scene_snapshot[state_key] as Array).is_empty(),
"WMO scene %s should be empty after shutdown" % state_key
)
loader.free()
+203
View File
@@ -0,0 +1,203 @@
extends SceneTree
## Asset-free ownership, lifecycle, dependency and timing regression for WMOs.
const REGISTRY_SCRIPT := preload("res://src/render/wmo/wmo_placement_registry.gd")
const REGISTRY_PATH := "res://src/render/wmo/wmo_placement_registry.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_invalid_and_first_reference(failures)
_verify_idempotent_and_shared_references(failures)
_verify_owner_checked_final_release(failures)
_verify_global_reference_and_clear(failures)
_verify_detached_sorted_diagnostics(failures)
_verify_ownership_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("WMO_PLACEMENT_REGISTRY: %s" % failure)
quit(1)
return
print(
"WMO_PLACEMENT_REGISTRY PASS cases=10 iterations=100 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_invalid_and_first_reference(failures: Array[String]) -> void:
var registry: RefCounted = REGISTRY_SCRIPT.new()
_expect_false(registry.call("add_reference", "", "tile-a"), "empty placement rejected", failures)
_expect_false(registry.call("add_reference", "uid:7", ""), "empty reference rejected", failures)
_expect_true(registry.call("add_reference", "uid:7", "tile-a"), "first reference registers", failures)
_expect_true(registry.call("contains", "uid:7"), "registered placement contained", failures)
_expect_equal(int(registry.call("active_count")), 1, "first active count", failures)
func _verify_idempotent_and_shared_references(failures: Array[String]) -> void:
var registry: RefCounted = REGISTRY_SCRIPT.new()
registry.call("add_reference", "uid:11", "tile-a")
_expect_false(
registry.call("add_reference", "uid:11", "tile-a"),
"same reference idempotent",
failures
)
_expect_false(
registry.call("add_reference", "uid:11", "tile-b"),
"shared reference retains placement",
failures
)
_expect_equal(int(registry.call("active_count")), 1, "shared active count", failures)
_expect_false(
registry.call("release_reference", "uid:11", "tile-a"),
"non-final release retains placement",
failures
)
_expect_true(registry.call("contains", "uid:11"), "shared placement remains", failures)
func _verify_owner_checked_final_release(failures: Array[String]) -> void:
var registry: RefCounted = REGISTRY_SCRIPT.new()
registry.call("add_reference", "uid:19", "tile-owner")
_expect_false(
registry.call("release_reference", "uid:19", "tile-other"),
"non-owner release ignored",
failures
)
_expect_false(
registry.call("release_reference", "uid:404", "tile-owner"),
"unknown placement release ignored",
failures
)
_expect_true(
registry.call("release_reference", "uid:19", "tile-owner"),
"final owner release removes placement",
failures
)
_expect_false(registry.call("contains", "uid:19"), "released placement absent", failures)
func _verify_global_reference_and_clear(failures: Array[String]) -> void:
var registry: RefCounted = REGISTRY_SCRIPT.new()
_expect_true(
registry.call("add_reference", "global", "__global__"),
"global reference registers",
failures
)
_expect_true(
registry.call("release_reference", "global", "__global__"),
"global final release removes",
failures
)
registry.call("add_reference", "uid:3", "tile-a")
registry.call("clear")
registry.call("clear")
_expect_equal(int(registry.call("active_count")), 0, "clear idempotent", failures)
func _verify_detached_sorted_diagnostics(failures: Array[String]) -> void:
var registry: RefCounted = REGISTRY_SCRIPT.new()
registry.call("add_reference", "uid:20", "tile-b")
registry.call("add_reference", "uid:10", "tile-c")
registry.call("add_reference", "uid:10", "tile-a")
var snapshot: Array = registry.call("diagnostic_snapshot")
_expect_equal(snapshot.size(), 2, "diagnostic record count", failures)
_expect_string_equal(String(snapshot[0]["placement_key"]), "uid:10", "placement sort", failures)
_expect_string_array(snapshot[0]["reference_keys"], ["tile-a", "tile-c"], "reference sort", failures)
(snapshot[0]["reference_keys"] as Array).append("mutated")
var fresh_snapshot: Array = registry.call("diagnostic_snapshot")
_expect_string_array(
fresh_snapshot[0]["reference_keys"],
["tile-a", "tile-c"],
"diagnostics detached",
failures
)
func _verify_ownership_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures)
var registry_source := _read_text(REGISTRY_PATH, failures)
_expect_true(
loader_source.contains("WMO_PLACEMENT_REGISTRY_SCRIPT.new()"),
"loader composes registry",
failures
)
_expect_true(loader_source.contains("_wmo_placement_registry.add_reference("), "loader delegates add", failures)
_expect_true(loader_source.contains("_wmo_placement_registry.release_reference("), "loader delegates release", failures)
_expect_true(loader_source.contains("_wmo_placement_registry.clear()"), "loader delegates clear", failures)
_expect_true(
loader_source.contains("_wmo_instance_nodes_by_unique_key"),
"loader retains node map",
failures
)
_expect_false(loader_source.contains("var _wmo_registry"), "legacy mixed registry removed", failures)
for forbidden_token in ["extends Node", "Node3D", "Resource", "RID", "Thread", "Mutex", "Semaphore"]:
_expect_false(
registry_source.contains(forbidden_token),
"registry omits %s ownership" % forbidden_token,
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var registry: RefCounted = REGISTRY_SCRIPT.new()
var started_microseconds := Time.get_ticks_usec()
for iteration in range(100):
for placement_index in range(256):
var placement_key := "uid:%d" % placement_index
registry.call("add_reference", placement_key, "tile-a")
registry.call("add_reference", placement_key, "tile-b")
for placement_index in range(256):
var placement_key := "uid:%d" % placement_index
registry.call("release_reference", placement_key, "tile-a")
registry.call("release_reference", placement_key, "tile-b")
_expect_equal(int(registry.call("active_count")), 0, "timing cycle %d clears" % iteration, failures)
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_true(elapsed_milliseconds < 1000.0, "100 shared-reference cycles remain bounded", failures)
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://dprfwayji363v
+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
@@ -0,0 +1,213 @@
extends SceneTree
## Asset-free ordering, cursor, dependency and timing regression for WMO builds.
const PLANNER_SCRIPT := preload("res://src/render/wmo/wmo_render_build_step_planner.gd")
const PLANNER_PATH := "res://src/render/wmo/wmo_render_build_step_planner.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_mesh_steps_precede_multimeshes(failures)
_verify_multimesh_cursor_and_completion(failures)
_verify_empty_and_raw_integer_behavior(failures)
_verify_fresh_results(failures)
_verify_loader_and_dependency_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("WMO_RENDER_BUILD_STEP_PLANNER: %s" % failure)
quit(1)
return
print(
"WMO_RENDER_BUILD_STEP_PLANNER PASS cases=9 iterations=20000 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_mesh_steps_precede_multimeshes(failures: Array[String]) -> void:
var planner: RefCounted = PLANNER_SCRIPT.new()
var first: Dictionary = planner.call("plan_step", 2, 0, 3, 0)
_expect_operation(first, PLANNER_SCRIPT.OPERATION_MESH, 0, 1, 0, "first mesh", failures)
var second: Dictionary = planner.call("plan_step", 2, 1, 3, 0)
_expect_operation(second, PLANNER_SCRIPT.OPERATION_MESH, 1, 2, 0, "second mesh", failures)
var after_meshes: Dictionary = planner.call("plan_step", 2, 2, 3, 0)
_expect_operation(
after_meshes,
PLANNER_SCRIPT.OPERATION_MULTIMESH,
0,
2,
1,
"multimesh follows meshes",
failures
)
func _verify_multimesh_cursor_and_completion(failures: Array[String]) -> void:
var planner: RefCounted = PLANNER_SCRIPT.new()
var last_multimesh: Dictionary = planner.call("plan_step", 1, 1, 2, 1)
_expect_operation(
last_multimesh,
PLANNER_SCRIPT.OPERATION_MULTIMESH,
1,
1,
2,
"last multimesh",
failures
)
var complete: Dictionary = planner.call("plan_step", 1, 1, 2, 2)
_expect_operation(
complete,
PLANNER_SCRIPT.OPERATION_COMPLETE,
-1,
1,
2,
"both ranges complete",
failures
)
func _verify_empty_and_raw_integer_behavior(failures: Array[String]) -> void:
var planner: RefCounted = PLANNER_SCRIPT.new()
var empty: Dictionary = planner.call("plan_step", 0, 0, 0, 0)
_expect_operation(empty, PLANNER_SCRIPT.OPERATION_COMPLETE, -1, 0, 0, "empty", failures)
var negative_mesh_cursor: Dictionary = planner.call("plan_step", 1, -1, 0, 0)
_expect_operation(
negative_mesh_cursor,
PLANNER_SCRIPT.OPERATION_MESH,
-1,
0,
0,
"negative mesh cursor is not clamped",
failures
)
var negative_counts: Dictionary = planner.call("plan_step", -1, 0, -1, 0)
_expect_operation(
negative_counts,
PLANNER_SCRIPT.OPERATION_COMPLETE,
-1,
0,
0,
"negative counts preserve comparisons",
failures
)
func _verify_fresh_results(failures: Array[String]) -> void:
var planner: RefCounted = PLANNER_SCRIPT.new()
var first: Dictionary = planner.call("plan_step", 1, 0, 0, 0)
first["next_mesh_index"] = 99
var second: Dictionary = planner.call("plan_step", 1, 0, 0, 0)
_expect_equal(int(second["next_mesh_index"]), 1, "result is fresh", failures)
func _verify_loader_and_dependency_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures)
var planner_source := _read_text(PLANNER_PATH, failures)
_expect_true(
loader_source.contains("WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.new()"),
"loader composes planner",
failures
)
_expect_true(
loader_source.contains("_wmo_render_build_step_planner.plan_step("),
"loader delegates step decision",
failures
)
_expect_true(
loader_source.contains("MeshInstance3D.new()")
and loader_source.contains("MultiMeshInstance3D.new()"),
"loader retains node materialization",
failures
)
_expect_false(
loader_source.contains("if mesh_index < meshes.size():"),
"loader omits mesh cursor decision",
failures
)
for forbidden_token in [
"extends Node", "Node3D", "Resource", "Mesh", "RID", "Thread", "Mutex", "FileAccess"
]:
_expect_false(
planner_source.contains(forbidden_token),
"planner omits %s dependency" % forbidden_token,
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var planner: RefCounted = PLANNER_SCRIPT.new()
var started_microseconds := Time.get_ticks_usec()
var checksum := 0
for iteration in range(20000):
var step: Dictionary = planner.call(
"plan_step",
48,
iteration % 49,
24,
iteration % 25
)
checksum += int(step["selected_index"])
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_true(checksum > 0, "timing checksum", failures)
_expect_true(elapsed_milliseconds < 1000.0, "20,000 plans remain bounded", failures)
return elapsed_milliseconds
func _expect_operation(
actual_step: Dictionary,
expected_operation: StringName,
expected_selected_index: int,
expected_next_mesh_index: int,
expected_next_multimesh_index: int,
label: String,
failures: Array[String]
) -> void:
if actual_step["operation"] != expected_operation:
failures.append(
"%s operation expected %s, got %s"
% [label, expected_operation, actual_step["operation"]]
)
_expect_equal(
int(actual_step["selected_index"]),
expected_selected_index,
"%s selected" % label,
failures
)
_expect_equal(
int(actual_step["next_mesh_index"]),
expected_next_mesh_index,
"%s next mesh" % label,
failures
)
_expect_equal(
int(actual_step["next_multimesh_index"]),
expected_next_multimesh_index,
"%s next multimesh" % label,
failures
)
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_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://w68w77v5sps4
@@ -0,0 +1,335 @@
extends SceneTree
## Asset-free lifecycle, ownership, dependency and timing regression for the
## lightweight-WMO render Resource cache state.
const CACHE_STATE_SCRIPT := preload(
"res://src/render/wmo/wmo_render_resource_cache_state.gd"
)
const CACHE_STATE_PATH := "res://src/render/wmo/wmo_render_resource_cache_state.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_invalid_and_request_state(failures)
_verify_validated_resource_completion(failures)
_verify_missing_completion_and_exclusivity(failures)
_verify_transient_and_full_clear(failures)
_verify_detached_sorted_diagnostics(failures)
_verify_ownership_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("WMO_RENDER_RESOURCE_CACHE_STATE: %s" % failure)
quit(1)
return
print(
"WMO_RENDER_RESOURCE_CACHE_STATE PASS cases=10 iterations=100 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_invalid_and_request_state(failures: Array[String]) -> void:
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
_expect_false(
cache_state.call("remember_request", "", "res://cache.res"),
"empty normalized path rejected",
failures
)
_expect_false(
cache_state.call("remember_request", "world/a.wmo", ""),
"empty cache path rejected",
failures
)
_expect_true(
cache_state.call("remember_request", "world/a.wmo", "res://cache/a.res"),
"valid request accepted",
failures
)
_expect_true(
cache_state.call("has_request", "world/a.wmo"),
"pending request observable",
failures
)
_expect_false(
cache_state.call("remember_request", "world/a.wmo", "res://cache/other.res"),
"duplicate request rejected",
failures
)
var request_snapshot: Dictionary = cache_state.call("request_paths_snapshot")
_expect_string_equal(
String(request_snapshot.get("world/a.wmo", "")),
"res://cache/a.res",
"request cache path preserved",
failures
)
request_snapshot.clear()
_expect_true(
cache_state.call("has_request", "world/a.wmo"),
"request snapshot detached",
failures
)
func _verify_validated_resource_completion(failures: Array[String]) -> void:
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
var validated_resource := Resource.new()
cache_state.call("remember_request", "world/b.wmo", "res://cache/b.res")
_expect_false(
cache_state.call("complete_request_with_resource", "world/b.wmo", null),
"null completion rejected",
failures
)
_expect_true(
cache_state.call(
"complete_request_with_resource",
"world/b.wmo",
validated_resource
),
"validated Resource completes request",
failures
)
_expect_false(
cache_state.call("has_request", "world/b.wmo"),
"successful completion removes request",
failures
)
_expect_true(
cache_state.call("contains_resource", "world/b.wmo"),
"successful completion cached",
failures
)
_expect_true(
cache_state.call("resource_for", "world/b.wmo") == validated_resource,
"cached Resource identity preserved",
failures
)
_expect_false(
cache_state.call("remember_request", "world/b.wmo", "res://cache/b2.res"),
"cached path rejects request",
failures
)
func _verify_missing_completion_and_exclusivity(failures: Array[String]) -> void:
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
_expect_false(
cache_state.call("complete_request_as_missing", "world/unknown.wmo"),
"unknown completion rejected",
failures
)
cache_state.call("remember_request", "world/missing.wmo", "res://cache/missing.res")
_expect_true(
cache_state.call("complete_request_as_missing", "world/missing.wmo"),
"failed request completes as missing",
failures
)
_expect_true(
cache_state.call("is_missing", "world/missing.wmo"),
"negative cache observable",
failures
)
_expect_false(
cache_state.call("has_request", "world/missing.wmo"),
"missing completion removes request",
failures
)
_expect_false(
cache_state.call(
"remember_request",
"world/missing.wmo",
"res://cache/retry.res"
),
"negative cache rejects request",
failures
)
func _verify_transient_and_full_clear(failures: Array[String]) -> void:
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
var retained_resource := Resource.new()
cache_state.call("remember_request", "world/retained.wmo", "res://cache/retained.res")
cache_state.call(
"complete_request_with_resource",
"world/retained.wmo",
retained_resource
)
cache_state.call("remember_request", "world/pending.wmo", "res://cache/pending.res")
cache_state.call("remember_request", "world/missing.wmo", "res://cache/missing.res")
cache_state.call("complete_request_as_missing", "world/missing.wmo")
cache_state.call("clear_transient_state")
_expect_true(
cache_state.call("resource_for", "world/retained.wmo") == retained_resource,
"transient clear retains validated Resource",
failures
)
_expect_equal(
int(cache_state.call("pending_request_count")),
0,
"transient clear removes requests",
failures
)
_expect_false(
cache_state.call("is_missing", "world/missing.wmo"),
"transient clear removes negative cache",
failures
)
cache_state.call("clear_all")
cache_state.call("clear_all")
_expect_false(
cache_state.call("contains_resource", "world/retained.wmo"),
"full clear idempotently releases Resource",
failures
)
func _verify_detached_sorted_diagnostics(failures: Array[String]) -> void:
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
for normalized_path in ["world/z.wmo", "world/a.wmo"]:
cache_state.call("remember_request", normalized_path, "res://cache/%s.res" % normalized_path)
var snapshot: Dictionary = cache_state.call("diagnostic_snapshot")
_expect_string_array(
snapshot["request_paths"],
["world/a.wmo", "world/z.wmo"],
"request diagnostics sorted",
failures
)
(snapshot["request_paths"] as Array).clear()
var fresh_snapshot: Dictionary = cache_state.call("diagnostic_snapshot")
_expect_equal(
(fresh_snapshot["request_paths"] as Array).size(),
2,
"diagnostics detached",
failures
)
func _verify_ownership_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures)
var cache_state_source := _read_text(CACHE_STATE_PATH, failures)
_expect_true(
loader_source.contains("WMO_RENDER_RESOURCE_CACHE_STATE_SCRIPT.new()"),
"loader composes cache state",
failures
)
for legacy_field in [
"var _wmo_render_cache:",
"var _wmo_render_missing_cache:",
"var _wmo_render_load_requests:",
]:
_expect_false(
loader_source.contains(legacy_field),
"loader removes %s" % legacy_field,
failures
)
for loader_owned_token in [
"ResourceLoader.",
"FileAccess.",
"WMO_STREAMING_SCRIPT",
"format_version",
"Node3D",
"RID",
"Thread",
"Mutex",
"Semaphore",
]:
_expect_false(
cache_state_source.contains(loader_owned_token),
"cache state omits %s dependency" % loader_owned_token,
failures
)
_expect_true(
loader_source.contains("resource.get_script() == WMO_STREAMING_SCRIPT"),
"loader retains script validation",
failures
)
_expect_true(
loader_source.contains("WMO_STREAMING_SCRIPT.FORMAT_VERSION"),
"loader retains cache format validation",
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
var started_microseconds := Time.get_ticks_usec()
for iteration in range(100):
for resource_index in range(256):
var normalized_path := "world/object_%d.wmo" % resource_index
cache_state.call(
"remember_request",
normalized_path,
"res://cache/object_%d.res" % resource_index
)
if resource_index % 2 == 0:
cache_state.call(
"complete_request_with_resource",
normalized_path,
Resource.new()
)
else:
cache_state.call("complete_request_as_missing", normalized_path)
cache_state.call("clear_all")
_expect_equal(
int(cache_state.call("pending_request_count")),
0,
"timing cycle %d clears" % iteration,
failures
)
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_true(
elapsed_milliseconds < 1000.0,
"100 resource/request cycles remain bounded",
failures
)
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://b5kuqdm8au7o1
@@ -0,0 +1,363 @@
extends SceneTree
## Asset-free state, ownership, dependency and timing regression for cached WMO scenes.
const CACHE_STATE_SCRIPT := preload(
"res://src/render/wmo/wmo_scene_resource_cache_state.gd"
)
const CACHE_STATE_PATH := "res://src/render/wmo/wmo_scene_resource_cache_state.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_invalid_and_request_state(failures)
_verify_direct_missing_and_exclusivity(failures)
_verify_validated_scene_completion(failures)
_verify_failed_request_completion(failures)
_verify_transient_and_full_clear(failures)
_verify_detached_sorted_diagnostics(failures)
_verify_ownership_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("WMO_SCENE_RESOURCE_CACHE_STATE: %s" % failure)
quit(1)
return
print(
"WMO_SCENE_RESOURCE_CACHE_STATE PASS cases=11 iterations=100 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_invalid_and_request_state(failures: Array[String]) -> void:
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
_expect_false(
cache_state.call("remember_request", "", "res://cache/a.tscn"),
"empty normalized path rejected",
failures
)
_expect_false(
cache_state.call("remember_request", "world/a.wmo", ""),
"empty cache path rejected",
failures
)
_expect_true(
cache_state.call("remember_request", "world/a.wmo", "res://cache/a.tscn"),
"valid request accepted",
failures
)
_expect_true(
cache_state.call("has_request", "world/a.wmo"),
"pending request observable",
failures
)
_expect_equal(
int(cache_state.call("pending_request_count")),
1,
"pending request count",
failures
)
_expect_false(
cache_state.call("remember_request", "world/a.wmo", "res://cache/other.tscn"),
"duplicate request rejected",
failures
)
var request_snapshot: Dictionary = cache_state.call("request_paths_snapshot")
_expect_string_equal(
String(request_snapshot.get("world/a.wmo", "")),
"res://cache/a.tscn",
"request cache path preserved",
failures
)
request_snapshot.clear()
_expect_true(
cache_state.call("has_request", "world/a.wmo"),
"request snapshot detached",
failures
)
func _verify_direct_missing_and_exclusivity(failures: Array[String]) -> void:
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
_expect_false(cache_state.call("mark_missing", ""), "empty missing rejected", failures)
_expect_true(
cache_state.call("mark_missing", "world/missing.wmo"),
"direct missing accepted",
failures
)
_expect_true(
cache_state.call("is_missing", "world/missing.wmo"),
"direct missing observable",
failures
)
_expect_false(
cache_state.call("mark_missing", "world/missing.wmo"),
"duplicate missing rejected",
failures
)
_expect_false(
cache_state.call(
"remember_request",
"world/missing.wmo",
"res://cache/retry.tscn"
),
"missing path rejects request",
failures
)
func _verify_validated_scene_completion(failures: Array[String]) -> void:
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
var validated_scene := PackedScene.new()
cache_state.call("remember_request", "world/b.wmo", "res://cache/b.tscn")
_expect_false(
cache_state.call("complete_request_with_scene", "world/b.wmo", null),
"null completion rejected",
failures
)
_expect_true(
cache_state.call("complete_request_with_scene", "world/b.wmo", validated_scene),
"validated PackedScene completes request",
failures
)
_expect_false(
cache_state.call("has_request", "world/b.wmo"),
"successful completion removes request",
failures
)
_expect_true(
cache_state.call("contains_scene", "world/b.wmo"),
"successful completion cached",
failures
)
_expect_true(
cache_state.call("scene_for", "world/b.wmo") == validated_scene,
"cached PackedScene identity preserved",
failures
)
_expect_false(
cache_state.call("mark_missing", "world/b.wmo"),
"cached scene rejects missing state",
failures
)
func _verify_failed_request_completion(failures: Array[String]) -> void:
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
_expect_false(
cache_state.call("complete_request_as_missing", "world/unknown.wmo"),
"unknown request completion rejected",
failures
)
cache_state.call("remember_request", "world/failed.wmo", "res://cache/failed.tscn")
_expect_true(
cache_state.call("complete_request_as_missing", "world/failed.wmo"),
"failed request completes as missing",
failures
)
_expect_true(
cache_state.call("is_missing", "world/failed.wmo"),
"failed request negative-cached",
failures
)
_expect_false(
cache_state.call("has_request", "world/failed.wmo"),
"failed completion removes request",
failures
)
func _verify_transient_and_full_clear(failures: Array[String]) -> void:
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
var retained_scene := PackedScene.new()
cache_state.call("remember_request", "world/retained.wmo", "res://retained.tscn")
cache_state.call("complete_request_with_scene", "world/retained.wmo", retained_scene)
cache_state.call("remember_request", "world/pending.wmo", "res://pending.tscn")
cache_state.call("mark_missing", "world/missing.wmo")
cache_state.call("clear_transient_state")
_expect_true(
cache_state.call("scene_for", "world/retained.wmo") == retained_scene,
"transient clear retains PackedScene",
failures
)
_expect_equal(
int(cache_state.call("pending_request_count")),
0,
"transient clear removes requests",
failures
)
_expect_false(
cache_state.call("is_missing", "world/missing.wmo"),
"transient clear removes missing state",
failures
)
cache_state.call("clear_all")
cache_state.call("clear_all")
_expect_false(
cache_state.call("contains_scene", "world/retained.wmo"),
"full clear idempotently releases PackedScene",
failures
)
func _verify_detached_sorted_diagnostics(failures: Array[String]) -> void:
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
cache_state.call("remember_request", "world/z.wmo", "res://z.tscn")
cache_state.call("remember_request", "world/a.wmo", "res://a.tscn")
cache_state.call("mark_missing", "world/m.wmo")
var snapshot: Dictionary = cache_state.call("diagnostic_snapshot")
_expect_string_array(
snapshot["request_paths"],
["world/a.wmo", "world/z.wmo"],
"request diagnostics sorted",
failures
)
_expect_string_array(
snapshot["missing_paths"],
["world/m.wmo"],
"missing diagnostics",
failures
)
(snapshot["request_paths"] as Array).clear()
var fresh_snapshot: Dictionary = cache_state.call("diagnostic_snapshot")
_expect_equal(
(fresh_snapshot["request_paths"] as Array).size(),
2,
"diagnostics detached",
failures
)
func _verify_ownership_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures)
var cache_state_source := _read_text(CACHE_STATE_PATH, failures)
_expect_true(
loader_source.contains("WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT.new()"),
"loader composes scene cache state",
failures
)
for legacy_field in [
"var _wmo_scene_resource_cache:",
"var _wmo_scene_cache_missing:",
"var _wmo_scene_load_requests:",
]:
_expect_false(
loader_source.contains(legacy_field),
"loader removes %s" % legacy_field,
failures
)
for loader_owned_token in [
"ResourceLoader.",
"FileAccess.",
"WMO_BUILDER_SCRIPT",
"instantiate()",
"Node3D",
"RID",
"Mutex",
"Semaphore",
]:
_expect_false(
cache_state_source.contains(loader_owned_token),
"scene cache state omits %s dependency" % loader_owned_token,
failures
)
for retained_loader_rule in [
"wmo_max_runtime_scene_mb",
"_get_resource_file_size(cache_path)",
"_is_wmo_scene_cache_current(resource as PackedScene)",
"scene.instantiate() as Node3D",
"WMO_BUILDER_SCRIPT.is_scene_cache_current(node)",
]:
_expect_true(
loader_source.contains(retained_loader_rule),
"loader retains %s" % retained_loader_rule,
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
var started_microseconds := Time.get_ticks_usec()
for iteration in range(100):
for scene_index in range(256):
var normalized_path := "world/object_%d.wmo" % scene_index
if scene_index % 3 == 0:
cache_state.call("mark_missing", normalized_path)
else:
cache_state.call(
"remember_request",
normalized_path,
"res://cache/object_%d.tscn" % scene_index
)
if scene_index % 2 == 0:
cache_state.call(
"complete_request_with_scene",
normalized_path,
PackedScene.new()
)
else:
cache_state.call("complete_request_as_missing", normalized_path)
cache_state.call("clear_all")
_expect_equal(
int(cache_state.call("pending_request_count")),
0,
"timing cycle %d clears" % iteration,
failures
)
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_true(
elapsed_milliseconds < 1000.0,
"100 scene/request cycles remain bounded",
failures
)
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://ccuuoqli83f6f
+67 -4
View File
@@ -27,7 +27,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
## Evidence
- Date: 2026-07-17
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`, `f88bf97`, `3f84717`, `d2cb52f`;
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`, `f88bf97`, `3f84717`, `d2cb52f`, `f9e5a4f`, `c06aed5`, `a878e7c`, `60603e1`, `b4955d6`, `ada0fd9`;
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-ENTITY-001`, `M03-RND-TERRAIN-CACHE-SERVICE-001`,
@@ -36,7 +36,12 @@ Runtime и Editor используют facade; planner/scheduler тестиру
`M03-RND-M2-PLACEMENT-TRANSFORM-001`,
`M03-RND-M2-PLACEMENT-GROUPER-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-RENDER-BUILD-PLANNER-001`,
`M03-RND-WMO-RENDER-BUILD-QUEUE-001`,
`M03-RND-WMO-RENDER-RESOURCE-CACHE-001`,
`M03-RND-WMO-SCENE-RESOURCE-CACHE-001`
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner
@@ -73,6 +78,37 @@ Runtime и Editor используют facade; planner/scheduler тестиру
WMO placement resolver passed `cases=9 iterations=20000 elapsed_ms=27.919`;
post-merge renderer materials, shutdown, internal-access `42`, facade,
`7/7` manifest, documentation and coordination gates remained green.
WMO placement registry passed `cases=10 iterations=100 elapsed_ms=53.941`;
all 21 adjacent WMO/M2/terrain/facade/scheduler/streaming/coordinate
regressions and the `7/7` checkpoint dry-run passed; post-merge registry
(`54.178ms`), resolver, shutdown, facade, documentation and coordination
gates remained green.
WMO render build step planner passed `cases=9 iterations=20000
elapsed_ms=11.914`; WMO registry/resolver/material/shutdown, 18 adjacent
regressions and the `7/7` checkpoint dry-run passed; post-merge planner
(`11.059ms`), registry, resolver, shutdown, facade, documentation and
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.
WMO render Resource cache state passed `cases=10 iterations=100
elapsed_ms=58.727`; direct no-`.godot` service verification passed
(`45.361ms`), WMO queue/planner/registry/resolver/material/shutdown and 18
adjacent regressions passed, and the checkpoint dry-run retained `7/7` plans.
Post-merge cache state (`51.949ms`), shutdown, WMO queue/resolver, facade,
internal-access `41`, `7/7` manifest, documentation and coordination gates
remained green.
WMO scene Resource cache state passed `cases=11 iterations=100
elapsed_ms=40.768`; its initial direct no-`.godot` run passed (`43.636ms`),
render-cache state, WMO queue/planner/registry/resolver/material/shutdown and
18 adjacent regressions passed, and checkpoint dry-run retained `7/7` plans.
Post-merge scene state (`40.113ms`), render state (`43.434ms`), shutdown,
WMO queue/resolver, facade, internal-access `40`, `7/7` manifest,
documentation and coordination gates remained green.
M02 terrain-query regression
remained green (13 pre-existing expired M00 claim warnings).
- Fidelity comparison: all 16 historical operation limits and drain sites were
@@ -104,6 +140,25 @@ Runtime и Editor используют facade; planner/scheduler тестиру
WMO cache-key normalization, positive MODF identity with tile/index fallback
and world position/Euler/unclamped-scale transforms are unchanged across
lightweight render, cached-scene and live-prototype paths.
WMO first/idempotent/shared/non-owner/final/global reference behavior is
unchanged; the extracted registry owns only String reference sets while the
loader retains the parallel key-to-Node map and final-release cancellation/free.
Lightweight WMO render groups retain mesh-before-MultiMesh order, one cursor
increment and one budget permit per valid or null group; completion still
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.
Lightweight WMO render cache paths retain the same cached/pending/missing
transitions, retry behavior for absent files and map-reset versus final-shutdown
lifetime. `ResourceLoader`, script identity and `FORMAT_VERSION` validation
remain loader-owned before the extracted state accepts a Resource.
Cached WMO scene paths retain direct negative caching for missing/oversize/
request-start failures, terminal missing state for load/validation failures and
the prior transient-reset versus final-shutdown lifetime. File-size limits,
`PackedScene` probe instantiation, WMOBuilder metadata validation and probe-node
destruction remain loader-owned before state accepts a scene.
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/
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes;
@@ -118,6 +173,13 @@ Runtime и Editor используют facade; planner/scheduler тестиру
three transform consumers, stateless M2 placement grouper and worker adapter,
stateless M2 build batch planner and loader cursor adapter,
stateless WMO placement resolver and seven cache/identity/transform adapters,
stateful WMO placement registry and global/tile add/release/clear adapters,
stateless WMO render build step planner and loader cursor-adoption adapter,
typed WMO render build job/queue and loader enqueue/drain/cancel/clear adapters,
lightweight WMO render Resource/missing/request state and loader I/O/validation/
reset adapters,
cached WMO PackedScene/missing/request state and loader file-size/I/O/probe/
validation/reset adapters,
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
- Remaining risks: queue storage length and worker concurrency/stale-result logic
remain streamer-owned; cancellation stops new permits but does not interrupt
@@ -132,7 +194,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
asset-backed visual transform recheck, grouping/build/tasks/caches/finalization
and boundary traversal p95/p99 remain pending; path grouping is not yet
culling-driven spatial-cell grouping; M2 resource/queue transitions and raw
build-job Dictionaries remain in the loader; WMO registry/build/resource state
and asset-backed placement/portal/material evidence remain pending;
WMO ResourceLoader/FileAccess I/O, live fallback and group materialization
remain in the loader;
asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending;
M03 still needs further terrain/M2/WMO/liquid service extraction
and dependency/performance acceptance.