Compare commits

...

43 Commits

Author SHA1 Message Date
sindoring e1b2886657 docs(M03): accept ADT water load pipeline state 2026-07-17 09:41:05 +04:00
sindoring f4707750b7 merge M03 ADT water load pipeline state 2026-07-17 09:39:33 +04:00
sindoring 78adb54cfe docs(M03): hand off ADT water load pipeline state 2026-07-17 09:39:24 +04:00
sindoring b7c036f14d refactor(M03): extract ADT water load pipeline state 2026-07-17 09:38:24 +04:00
sindoring 7973945582 coord(M03): claim ADT water load pipeline state 2026-07-17 09:32:25 +04:00
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
sindoring 5aa40d6afb docs(M03): accept WMO placement resolver 2026-07-17 00:24:37 +04:00
sindoring d2cb52ff6b merge M03 WMO placement resolver 2026-07-17 00:23:29 +04:00
sindoring c2e0696fe0 docs(M03): hand off WMO placement resolver 2026-07-17 00:23:18 +04:00
sindoring 05e216a953 refactor(M03): extract WMO placement resolver 2026-07-17 00:22:53 +04:00
sindoring f01302fd44 coord(M03): claim WMO placement resolver 2026-07-17 00:16:39 +04:00
sindoring 4f40c779b9 docs(M03): accept M2 build batch planner 2026-07-17 00:14:06 +04:00
sindoring 3f847171f8 merge M03 M2 build batch planner 2026-07-17 00:13:11 +04:00
sindoring af9d077905 docs(M03): hand off M2 build batch planner 2026-07-17 00:13:01 +04:00
sindoring be6db7f7e0 refactor(M03): extract M2 build batch planner 2026-07-17 00:12:42 +04:00
sindoring a562d126b9 coord(M03): claim M2 build batch planner 2026-07-17 00:08:54 +04:00
57 changed files with 6094 additions and 247 deletions
+79
View File
@@ -27,6 +27,13 @@ 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/render/liquid/adt_water_load_pipeline_state.gd` - ADT water pending FIFO/dedupe, active task IDs and worker-safe parsed-result mailbox without parser or 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 +401,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 по кадрам;
@@ -1014,6 +1027,72 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- Cache formats, batching, profiles and visible rules are unchanged. Asset-backed
traversal p95/p99, visual recheck and spatial-cell grouping remain pending.
## 2026-07-17 M2 Build Batch Planner Extraction
- `M2BuildBatchPlanner` now owns pure static/animated batch-limit selection,
remaining-count capping and next-offset/group-completion calculation.
- Existing animated/static limits still clamp to at least one; resource readiness,
queue rotation, serial progression and `M2_BUILD` permit consumption are unchanged.
- The loader retains build jobs/queues, tile cancellation, animation/mesh caches,
retries and all Node/MultiMesh/Mesh/material/RID finalization.
- Cache formats, quality profiles, batching output and visible rules are unchanged.
Asset-backed p95/p99 and spatial-cell batching evidence remain pending.
## 2026-07-17 WMO Placement Resolver Extraction
- `WmoPlacementResolver` now owns lowercase/slash cache-key normalization,
positive MODF `uid:<decimal>` identity with the legacy tile/index fallback and
world `Transform3D` composition.
- Lightweight render roots, cached scenes and live prototypes now share the same
position/Euler/unclamped-scale formula.
- The loader retains registry refs, jobs/queues, cache/load state, resource
fallback, cancellation and all Node/Mesh/MultiMesh/material/RID ownership.
- 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-17 ADT Water Load Pipeline State Extraction
- `AdtWaterLoadPipelineState` now owns ADT water pending FIFO/deduplication,
active WorkerThreadPool task IDs and the mutex-protected parsed-result mailbox.
- `StreamingWorldLoader` still selects concurrency, starts/waits tasks, invokes
`ADTLoader`, consumes `WATER_FINALIZE` permits, rejects stale tile/path/root
results and builds/attaches water Nodes through `ADTBuilder` on the main thread.
- Tile release still cancels only pending water requests; active work completes
and its result is discarded when tile ownership is stale. Shutdown waits task
IDs before clearing state; reset does not interrupt workers.
- Pending-plus-active `water` metrics, parsing, formats, materials, profiles and
visible behavior are unchanged.
- Asset-backed MH2O/MCLQ fidelity, long-traversal leak/p95/p99 and paired capture
evidence remain pending.
## 2026-07-16 World Environment Snapshot Facade
- `WorldEnvironmentSnapshot` carries one immutable finite time-of-day value,
@@ -0,0 +1,115 @@
# M03-RND-ADT-WATER-LOAD-PIPELINE-001 — ADT water load pipeline state
<!-- OPENWC_CLAIM:M03-RND-ADT-WATER-LOAD-PIPELINE-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-adt-water-load-pipeline`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract ADT water pending-request FIFO/deduplication, active worker task IDs and
worker-safe result mailbox from the loader while preserving loader-owned parse,
budget, stale-result and scene finalization behavior.
## Non-goals
- Instantiate `ADTLoader`, start/wait WorkerThreadPool tasks or parse ADT bytes.
- Consume `WATER_FINALIZE` permits or choose concurrency/profile limits.
- Build/attach/free water Nodes, materials, Meshes or RIDs.
- Change tile/path stale checks, empty-data behavior or `water_loaded` timing.
- Change MH2O/MCLQ parsing or claim liquid fidelity/performance parity.
## Paths
- Exclusive: `src/render/liquid/adt_water_load_pipeline_state.gd`,
`src/tools/verify_adt_water_load_pipeline_state.gd`,
`docs/modules/adt-water-load-pipeline-state.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`src/tools/verify_render_runtime_cache_shutdown.gd`,
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated ADT resources, caches and
proprietary renderer corpus
## Contracts and data
- Pending requests preserve FIFO and tile-key deduplication.
- Tile release removes all matching pending entries but does not cancel active work.
- Active task IDs are loader-created opaque integers keyed by tile.
- Worker publication and main-thread result pop/clear are mutex protected.
- Result order and Dictionary payload identity remain unchanged.
- Clear releases bookkeeping only; loader waits tasks at orderly shutdown.
- Water parsing, coordinate, material, profile and visual rules are unchanged.
## Dependencies
- Requires: current master `4387c66`; accepted scheduler and renderer lifecycle
- Blocks: further liquid parse/finalization decomposition
- External state: none; verification uses synthetic task IDs and payloads
## Verification
- Commands: dedicated FIFO/dedupe/task/result/cancel/clear/thread/source/timing
verifier; shutdown, water/material and adjacent renderer/streaming/coordinate/
documentation/coordination gates
- Fixtures: synthetic tile/path requests, task IDs and water dictionaries
- Fidelity evidence: exact current queue/task/result transitions preserved
- Performance budget: 100 cycles over 256 requests/results under 1 second
## Documentation deliverables
- Inline public API docs
- Module specification with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state, worker sequence and dependency diagrams
- World-renderer, module registry and RENDER status updates
## Simplicity and naming
- Important names introduced: `AdtWaterLoadPipelineState`
- Simplest considered solution: one RefCounted with FIFO/dedupe/task maps and one mutex mailbox
- Rejected complexity/abstractions: generic async scheduler, signals, callbacks,
WorkerThreadPool wrapper, parser/material ownership or multi-lane framework
- Unavoidable complexity and justification: result mailbox crosses worker/main threads
- Measured optimization evidence: bounded synthetic lifecycle loop
## Status
- State: accepted
- Done: pipeline state implemented; loader enqueue/start/publish/drain/release/
reset/shutdown/metrics adapters migrated; required documentation added
- Next: continue liquid parse/finalization extraction or another remaining M03
service boundary
- Blocked by:
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-ADT-WATER-LOAD-PIPELINE-001:f470775 -->
## Handoff
- Commit: `b7c036f`
- Results: water pipeline PASS (`cases=12`, `iterations=100`,
`elapsed_ms=86.170`; initial direct cold run `94.576ms`); real Thread result
publication, FIFO/dedupe/task/cancel/clear/source contracts PASS; WMO caches/
queue/planner/registry/resolver, materials, 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: clear does not interrupt active WorkerThreadPool work and a
post-reset publication remains stale until drained/cleared, matching prior
behavior; payload Dictionaries require no mutation after publication; parser,
main-thread water materialization and asset-backed MH2O/MCLQ traversal/leak/
p95/p99/original-client evidence remain pending.
- Documentation updated: inline public API; new
`docs/modules/adt-water-load-pipeline-state.md`; module registry, world renderer
and `RENDER.md`; data-flow/state/sequence/dependency diagrams, ownership,
threading, failure/recovery, profiles, performance limit and source map included.
- Integration: merged as `f470775`; post-merge water pipeline (`cases=12`,
`iterations=100`, `elapsed_ms=86.935`), shutdown, renderer materials, facade,
internal-access (`private_symbols=36`), `7/7` manifest, documentation and
coordination gates passed.
<!-- OPENWC_HANDOFF:READY:M03-RND-ADT-WATER-LOAD-PIPELINE-001:b7c036f -->
@@ -0,0 +1,110 @@
# M03-RND-M2-BUILD-BATCH-PLANNER-001 — M2 build batch planner
<!-- OPENWC_CLAIM:M03-RND-M2-BUILD-BATCH-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-m2-build-batch-planner`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract pure static/animated M2 build batch sizing and cursor progression from
the loader build-job loop into a scene-free planner.
## Non-goals
- Move build-job/queue ownership, tile eligibility or cancellation.
- Move animation/mesh requests, cache lookup, queue rotation or retry behavior.
- Create Nodes, MultiMesh, Mesh, materials or RIDs.
- Change render-budget permit consumption or per-profile batch limits.
- Introduce spatial-cell batching or claim asset-backed performance parity.
## Paths
- Exclusive: `src/render/m2/m2_build_batch_planner.gd`,
`src/tools/verify_m2_build_batch_planner.gd`,
`docs/modules/m2-build-batch-planner.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`docs/modules/m2-placement-grouper.md`, `docs/modules/world-renderer.md`,
`docs/modules/README.md`, `RENDER.md`, `targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated ADT resources, caches and
proprietary renderer corpus
## Contracts and data
- Input: transform count, current offset, animated/static path selector and raw
animated/static batch limits
- Output: fresh plan Dictionary with effective batch size, batch count, next
offset and group-complete flag
- Selected batch limit clamps to at least one
- Batch count clamps to remaining non-negative transforms
- Group completes when no batch can be consumed or offset plus batch reaches end
- Queue/cache/schema/coordinate/profile values: unchanged
## Dependencies
- Requires: current master `9d74d99`; grouped transform arrays from
`M2PlacementGrouper`
- Blocks: broader M2 build-job state extraction
- External state: none; tests use scalar synthetic counts/limits
## Verification
- Commands: dedicated static/animated/limit/remaining/complete/empty/offset and
bounded timing verifier; loader source boundary; grouper/transform/registry plus
facade/internal-access/manifest/shutdown/documentation/coordination gates
- Fidelity evidence: exact batch/count/cursor formulas moved unchanged
- Performance budget: constant-time scalar/container calculation; no Node,
Resource, RID, task, queue, mutex, cache or file access
## Documentation deliverables
- Inline planner API documentation
- New module spec with inputs/outputs, data-flow, lifecycle, ownership, failure,
performance, fidelity limitations and source map
- Grouper, renderer/module registry and `RENDER.md` updates
## Simplicity and naming
- Important name: `M2BuildBatchPlanner`
- Simplest solution: one stateless `plan_batch` query with a small detached result
- Rejected complexity: job classes, state machine framework, callbacks, signals,
Resources, strategies or scheduler integration
- Unavoidable complexity: loader retains raw build-job Dictionaries
- Measured optimization evidence: bounded synthetic planning loop
## Status
- State: accepted
- Done: planner, loader adapter, regressions and documentation verified
- Next: M2 queue/resource state extraction and spatial-cell evidence
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-BUILD-BATCH-PLANNER-001:be6db7f -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-BUILD-BATCH-PLANNER-001:3f84717 -->
## Handoff
- Commit: `be6db7f` (`refactor(M03): extract M2 build batch planner`)
- Results: dedicated planner PASS (`cases=8`, `iterations=20000`,
`elapsed_ms=13.464`); grouper, transform resolver, M2 registry/dedupe, terrain
cache/LOD/queue, facade, internal-access, manifest, shutdown, scheduler,
streaming planner/focus, terrain-query and coordinate-boundary suites PASS;
editor import parsed with only expected missing private corpus diagnostics;
documentation PASS (`module_specs=18`, `required_files=7`); coordination PASS
with 23 pre-existing expired M00/M01 warnings; checkpoint dry-run planned `7/7`.
- Remaining risks: no private asset-backed p95/p99 or visual comparison; result
and loader job remain Dictionaries; resource/queue transitions and spatial-cell
batching are still loader-owned; negative offset behavior remains permissive.
- Documentation updated: inline public API; new
`docs/modules/m2-build-batch-planner.md`; placement grouper module spec,
renderer module registry, `docs/modules/world-renderer.md` and `RENDER.md`.
- Integration: accepted in master merge `3f84717`; post-merge planner
(`cases=8`, `iterations=20000`, `elapsed_ms=14.532`), grouper, transform
resolver, registry, internal-access, facade, manifest, documentation and
coordination gates passed.
@@ -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,110 @@
# M03-RND-WMO-PLACEMENT-RESOLVER-001 — WMO placement resolver
<!-- OPENWC_CLAIM:M03-RND-WMO-PLACEMENT-RESOLVER-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-resolver`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract pure WMO cache-path normalization, placement identity and world-transform
resolution from the loader into a scene-free service.
## Non-goals
- Change WDT/ADT coordinate conversion or WMO placement values.
- Own WMO registry references, tile lifecycle, jobs, queues or retries.
- Load cached/live WMO resources or validate cache format versions.
- Create Nodes, Mesh/MultiMesh, materials or RIDs.
- Change portal/room/material/occlusion behavior or claim WMO parity.
## Paths
- Exclusive: `src/render/wmo/wmo_placement_resolver.gd`,
`src/tools/verify_wmo_placement_resolver.gd`,
`docs/modules/wmo-placement-resolver.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
- `normalize_relative_path`: replace backslashes, lowercase entire cache key
- `resolve_unique_key`: positive `unique_id` becomes `uid:<decimal>`; otherwise
preserve `tile:<tile-key>:<placement-index>` fallback
- `resolve_world_transform`: defaults position/rotation to zero and scale to one;
compose the same Godot Euler basis and un-clamped uniform scale
- Three transform consumers: render-cache root, cached scene and live prototype
- Cache/schema/coordinate/profile versions: unchanged
## Dependencies
- Requires: current master `4f40c77`
- Blocks: WMO registry/build decomposition
- External state: none; tests use synthetic paths and placement dictionaries
## Verification
- Commands: dedicated path/identity/default/rotation/scale/source and bounded
timing verifier; M2/terrain/facade/internal-access/manifest/shutdown/scheduler/
streaming/coordinate/documentation/coordination gates
- Fidelity evidence: exact normalization, identity and property-transform rules
moved unchanged
- Performance budget: constant-time string/value math; no Node, Resource, RID,
task, queue, mutex, cache or file access
## Documentation deliverables
- Inline resolver API documentation
- New module spec with inputs/outputs, data-flow, lifecycle, ownership, failure,
performance, fidelity limitations and source map
- Renderer/module registry and `RENDER.md` updates
## Simplicity and naming
- Important name: `WmoPlacementResolver`
- Simplest solution: one stateless service with three explicit pure queries
- Rejected complexity: placement class hierarchy, generic model resolver,
Resources, strategies, callbacks or registry framework
- Unavoidable complexity: raw placement Dictionary remains parser boundary
- Measured optimization evidence: bounded synthetic resolver loop
## Status
- State: accepted
- Done: resolver, seven loader adapters, regressions and documentation verified
- Next: WMO registry/build extraction and asset-backed placement evidence
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-PLACEMENT-RESOLVER-001:05e216a -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-PLACEMENT-RESOLVER-001:d2cb52f -->
## Handoff
- Commit: `05e216a` (`refactor(M03): extract WMO placement resolver`)
- Results: dedicated resolver PASS (`cases=9`, `iterations=20000`,
`elapsed_ms=28.332`), including historical Node3D property equivalence;
renderer materials, M2 planner/grouper/transform/registry/dedupe, terrain
cache/LOD/queue, facade, internal-access, manifest, shutdown, scheduler,
streaming planner/focus, terrain-query and coordinate-boundary suites PASS;
editor import parsed with only expected missing private corpus diagnostics;
documentation PASS (`module_specs=19`, `required_files=7`); coordination PASS
with 23 pre-existing expired M00/M01 warnings; checkpoint dry-run planned `7/7`.
- Remaining risks: no private asset-backed WMO placement/visual/p95/p99 evidence;
old caches without positive UID retain per-tile fallback; raw Dictionaries and
WMO registry/build/resource state remain loader-owned; scale stays unclamped.
- Documentation updated: inline public API; new
`docs/modules/wmo-placement-resolver.md`; renderer module registry,
`docs/modules/world-renderer.md` and `RENDER.md`.
- Integration: accepted in master merge `d2cb52f`; post-merge resolver
(`cases=9`, `iterations=20000`, `elapsed_ms=27.919`), renderer materials,
shutdown, internal-access, facade, manifest, 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 -->
+8
View File
@@ -15,9 +15,17 @@
| Terrain quality Mesh cache | Implemented extraction | [`terrain-quality-mesh-cache.md`](terrain-quality-mesh-cache.md) |
| Terrain chunk LOD planner | Implemented extraction | [`terrain-chunk-lod-planner.md`](terrain-chunk-lod-planner.md) |
| Terrain chunk geometry queue planner | Implemented extraction | [`terrain-chunk-geometry-queue-planner.md`](terrain-chunk-geometry-queue-planner.md) |
| ADT water load pipeline state | Implemented extraction | [`adt-water-load-pipeline-state.md`](adt-water-load-pipeline-state.md) |
| M2 unique placement registry | Implemented extraction | [`m2-unique-placement-registry.md`](m2-unique-placement-registry.md) |
| M2 placement transform resolver | Implemented | [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md) |
| 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) |
@@ -0,0 +1,247 @@
# ADT Water Load Pipeline State
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-ADT-WATER-LOAD-PIPELINE-001` |
| Owners | ADT water pending FIFO/dedupe, active task IDs and worker-safe result mailbox |
| Last verified | Worktree `work/sindo-main-codex/m03-adt-water-load-pipeline`, 2026-07-17 |
| Profiles/capabilities | ADT MH2O/MCLQ water loading; profile-independent state |
## Purpose
Own the cross-frame and cross-thread bookkeeping for asynchronous ADT water
loads outside `StreamingWorldLoader`: FIFO requests, queued-key deduplication,
opaque active task IDs and parsed-result delivery. The loader retains task
execution, parsing, budgets, stale-result decisions and scene finalization.
## Non-goals
- Start, cancel or wait WorkerThreadPool tasks.
- Instantiate `ADTLoader` or interpret MH2O/MCLQ data.
- Select concurrency limits or consume render permits.
- Create, attach or free Nodes, Meshes, materials or RIDs.
- Change liquid visuals, coordinates, tile acceptance or loaded-state timing.
## Context and boundaries
```mermaid
flowchart LR
Tile[Loaded tile needing water] --> Loader[StreamingWorldLoader]
Loader --> State[AdtWaterLoadPipelineState]
State --> Request[Oldest tile/path request]
Request --> Loader
Loader --> Worker[WorkerThreadPool ADTLoader task]
Worker --> State
State --> Result[Parsed ADT result]
Result --> Loader
Loader --> Budget[WATER_FINALIZE permit]
Loader --> Builder[ADTBuilder water scene]
Builder --> Scene[Main-thread tile Node attach]
```
Allowed dependencies are RefCounted, Dictionary/Array/String and one Godot
`Mutex` around results. WorkerThreadPool, ADTLoader, scheduler, tile state,
scene-tree/render resources and gameplay/editor/network are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `enqueue_request(tile_key, adt_path)` | Command/query | Append deduplicated FIFO request | Renderer main thread; until dequeue/cancel/clear | Empty/queued/active returns false |
| `take_next_request()` | Command/query | Pop oldest request and release queued marker | Renderer main thread | Empty returns `{}` |
| `cancel_pending_requests(tile_key)` | Command/query | Remove all matching pending entries | Tile release/main thread | Empty/unknown removes zero; active unaffected |
| `remember_active_task(tile_key, task_id)` | Command/query | Retain loader-created opaque task ID | Main thread; until completion/clear | Empty/duplicate returns false |
| `has_active_task` / `active_task_id_for` | Query | Test/borrow active task identity | Main thread | Unknown ID is `-1` |
| `complete_active_task(tile_key)` | Command/query | Remove and return completed task ID | Main thread after wait | Unknown returns `-1` |
| `active_task_ids_snapshot()` | Query | Copy IDs for orderly shutdown waiting | Main thread | Detached Array |
| `publish_result(tile_key, path, data)` | Command/query | Append parsed result under mutex | Worker or main thread | Empty key/path returns false |
| `has_result()` / `pop_result()` | Query/command | Observe/pop oldest result under mutex | Main-thread drain | Empty pop returns `{}` |
| `total_load_count()` | Query | Pending plus active metric | Main thread | Results intentionally excluded |
| `pending_request_count()` / `active_task_count()` | Query | Support loader concurrency loop | Main thread | None |
| `clear()` | Command | Release all bookkeeping and results | Main thread after wait or map reset | Does not interrupt workers |
| `diagnostic_snapshot()` | Query | Return detached scalar requests/tasks/results | Main thread; result mailbox locked | Water data omitted |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Tile key and ADT path | Loader tile state | Pending FIFO | Copied Strings | Until dequeue/cancel/clear |
| Input | Opaque task ID | Loader WorkerThreadPool adapter | Active task map | Integer value | Until wait/completion/clear |
| Input | Tile/path/parsed ADT Dictionary | Worker parser adapter | Result mailbox | Dictionary reference retained | Until pop/clear |
| Output | Oldest request Dictionary | Pipeline state | Loader task-start adapter | Fresh record from FIFO | One start attempt |
| Output | Active task ID snapshot | Pipeline state | Loader shutdown wait | Detached Array | Shutdown loop |
| Output | Oldest result Dictionary | Pipeline state | Loader finalize adapter | Mailbox record transferred | One finalize attempt |
| Output | Scalar diagnostics | Pipeline state | Verifier/future metrics | Detached records | Snapshot lifetime |
Side effects are limited to collection mutation, reference retention and result
mutex locking. Parsing, permits and scene mutation remain outside the module.
## Data flow
```mermaid
flowchart TD
Enqueue[enqueue tile/path] --> Duplicate{Queued or active?}
Duplicate -->|yes| Reject[false; unchanged]
Duplicate -->|no| FIFO[Append FIFO and queued key]
FIFO --> Take[take next; release queued marker]
Take --> Validate[Loader checks tile/path]
Validate --> Start[Loader starts worker and remembers task ID]
Start --> Parse[Worker parses ADT]
Parse --> Publish[Mutex-protected publish result]
Publish --> Permit[Loader obtains WATER_FINALIZE permit]
Permit --> Pop[Pop oldest result]
Pop --> Wait[Loader waits task and completes active ID]
Wait --> Stale{Tile/path/root current?}
Stale -->|no| Discard[Discard result]
Stale -->|yes| Attach[Build and attach water scene]
```
## Lifecycle/state
A tile request progresses through `Absent`, `Queued`, `Active` and
`ResultReady`; the loader wait/completion returns bookkeeping to `Absent` before
validating/attaching the result. Queued tile release cancels only `Queued`.
Active work is not interrupted; a later stale result is drained and discarded.
```mermaid
stateDiagram-v2
[*] --> Absent
Absent --> Queued: accepted enqueue
Queued --> Absent: pending cancellation or invalid dequeue
Queued --> Active: loader starts task
Active --> ResultReady: worker publishes result
ResultReady --> Absent: loader waits and completes task
Queued --> Absent: clear
Active --> Absent: clear bookkeeping only
ResultReady --> Absent: clear
```
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader main thread
participant State as AdtWaterLoadPipelineState
participant Worker as WorkerThreadPool task
participant Budget as RenderBudgetScheduler
participant Builder as ADTBuilder
Loader->>State: enqueue_request(tile key, ADT path)
Loader->>State: take_next_request()
Loader->>Worker: add_task(parse binding)
Loader->>State: remember_active_task(key, task ID)
Worker->>Worker: ADTLoader.load_adt(path)
Worker->>State: publish_result(key, path, data)
Note over State: result mutex protects publication/pop
Loader->>Budget: try_consume_permit(WATER_FINALIZE)
Loader->>State: pop_result()
Loader->>Worker: wait_for_task_completion(task ID)
Loader->>State: complete_active_task(key)
Loader->>Loader: reject stale tile/path/root
Loader->>Builder: build_tile_water_scene(data, origin)
Loader->>Loader: attach Node and mark water_loaded
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> State[AdtWaterLoadPipelineState]
Loader --> Scheduler[RenderBudgetScheduler]
Loader --> Worker[WorkerThreadPool / ADTLoader]
Loader --> Builder[ADTBuilder / SceneTree]
Worker -->|publish only| State
State -. no dependency .-> Scheduler
State -. no dependency .-> Builder
```
## Ownership, threading and resources
- Main thread owns pending and active-task collection mutation.
- Worker threads may call only `publish_result`; result observe/pop/clear also lock.
- Parsed water Dictionary identity is retained until main-thread pop.
- Loader owns WorkerThreadPool task lifetime and must wait before orderly shutdown clear.
- Loader owns tile states, ADTBuilder, water Nodes and every render resource.
- `clear` cannot cancel a worker; post-reset publication becomes a stale result
and is discarded because no active task/tile ownership remains.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty/duplicate request | Enqueue guards | Reject unchanged | Contract verifier | Correct caller state |
| Tile removed while queued | Loader release | Remove matching pending requests | Source/lifecycle verifier | Reload can enqueue later |
| Tile removed while active | Loader stale check after result | Wait task, remove active ID, discard result | Existing metrics | Reload can enqueue later |
| Parser unavailable/empty | Worker publishes empty Dictionary | Loader marks `water_loaded` without Node | Existing behavior | Future tile reload |
| Path changed | Loader compares result path | Discard after task completion | Source regression | Current tile can request again |
| Shutdown | Loader snapshots/waits active IDs | Clear all bookkeeping/results | Shutdown/source verifier | New loader starts empty |
## Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| `enable_water` | Loader setting | Quality/custom | Yes | Gates enqueue/process, unchanged |
| `max_concurrent_water_tasks` | Loader setting | Quality/custom | Yes | Bounds active tasks outside state |
| `water_finalize_ops_per_tick` | Loader setting | Quality/custom | Yes | Bounds result pops via scheduler |
## Persistence, cache and migration
No state is serialized or cached. ADT source/cache formats and liquid material
versions are unchanged; no migration or rebuild is required.
## Diagnostics and observability
- `total_load_count` preserves the prior pending-plus-active `water` metric.
- Snapshot exposes FIFO tile/path, sorted active keys and result tile/path only.
- Parsed liquid payloads, Nodes and task internals are not exposed.
- No logs or correlation IDs are introduced.
## Verification
- `verify_adt_water_load_pipeline_state.gd`: invalid/dedupe/FIFO behavior,
active IDs, payload identity, real Thread publication, pending cancellation,
clear, detached diagnostics, loader boundaries and bounded timing.
- Existing material/shutdown/facade/streaming regressions cover adjacent behavior.
- Fidelity evidence is preservation of current scheduling/finalize transitions;
no MH2O/MCLQ visual or original-client parity claim is made.
- Performance budget: 100 cycles of 256 request/task/result transitions under 1 second.
## Extension points
- ADT parse execution can later move behind a parser/job adapter without changing state.
- Water scene materialization can be extracted separately because it owns Nodes
and main-thread effects rather than async bookkeeping.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| ADT water load pipeline state | Implemented extraction | FIFO/task/thread/source/timing verifier | Asset-backed traversal/leak evidence pending |
| ADT liquid parsing | Existing/loader-owned adapter | Native ADTLoader and dry-run paths | MH2O/MCLQ fidelity fixtures pending |
| Water scene finalization | Existing/loader-owned | Scheduler/material/shutdown regressions | Separate materialization extraction possible |
## Known gaps and risks
- `clear` intentionally does not interrupt active WorkerThreadPool work.
- Result payload Dictionaries are retained by reference and must not be mutated after publication.
- Result availability and pop use separate mutex acquisitions; one serialized
main-thread consumer preserves ordering, but the API is not a multi-consumer queue.
- No proprietary liquid corpus, traversal/leak run, p95/p99 or paired capture is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/liquid/adt_water_load_pipeline_state.gd` | Request/task/result bookkeeping and result mutex |
| `src/scenes/streaming/streaming_world_loader.gd` | Parse task execution, permits, stale checks and Node finalization |
| `addons/mpq_extractor/loaders/adt_builder.gd` | ADT liquid geometry/material construction |
| `src/tools/verify_adt_water_load_pipeline_state.gd` | Lifecycle/thread/boundary/timing regression |
## Related decisions and references
- [`world-renderer.md`](world-renderer.md)
- [`render-budget-scheduler.md`](render-budget-scheduler.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+187
View File
@@ -0,0 +1,187 @@
# M2 Build Batch Planner
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-M2-BUILD-BATCH-PLANNER-001` |
| Owners | Pure static/animated M2 batch sizing and group cursor progression |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-build-batch-planner`, 2026-07-17 |
| Profiles/capabilities | Existing static MultiMesh and animated-instance build paths |
## Purpose
Select the effective per-tick M2 batch limit, cap it to remaining transforms and
plan the next group offset without accessing loader state or renderer resources.
## Non-goals
- Own M2 build jobs, queues, tile state, retries or cancellation.
- Decide whether an animated prototype or static mesh is ready.
- Rotate queues or consume `RenderBudgetScheduler` permits.
- Create or modify Nodes, MultiMesh, Mesh, materials or RIDs.
- Introduce spatial cells or change profile batch limits.
## Context and boundaries
```mermaid
flowchart LR
Groups[M2PlacementGrouper transform arrays] --> Loader[Loader build loop]
Limits[Animated/static batch limits] --> Planner[M2BuildBatchPlanner]
Loader --> Planner
Planner --> Plan[Detached batch plan]
Plan --> Loader
Loader --> Ready{Resource ready?}
Ready --> Materialize[Animated or MultiMesh materialization]
Loader --> Budget[RenderBudgetScheduler permit]
```
Allowed dependencies are integer/boolean scalar math and a fresh Dictionary.
Node, SceneTree, RenderingServer, ResourceLoader, WorkerThreadPool, mutexes,
renderer caches, gameplay, network, editor UI and files are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `plan_batch(transform_count, current_offset, has_animated_prototype, animated_batch_limit, static_batch_limit)` | Pure query | Return the selected batch size/count and next group cursor | Any thread; call-local result | Non-positive selected limit clamps to one; empty/past-end range completes |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Transform count | Grouped transform array | Planner remaining-count formula | Scalar copy | One plan |
| Input | Current group offset | Loader build job | Planner cursor formula | Scalar copy | One plan |
| Input | Animated prototype availability | Loader resource selection | Planner limit selector | Scalar copy | One plan |
| Input | Animated/static raw limits | Renderer profile/exports | Planner clamp | Scalar copy | One plan |
| Output | `effective_batch_size` | Planner | Tests/diagnostics | Detached integer | One plan |
| Output | `batch_count` | Planner | Loader materialization adapter | Detached integer | One plan |
| Output | `next_offset` | Planner | Loader build-job cursor | Detached integer | Until next batch |
| Output | `group_complete` | Planner | Loader group-index progression | Detached boolean | One plan |
The output Dictionary is newly allocated and caller-owned. The planner retains
no reference to it or any input.
## Data flow
```mermaid
flowchart TD
Select{Animated prototype?} -->|yes| Animated[animated_batch_limit]
Select -->|no| Static[static_batch_limit]
Animated --> Clamp[max 1]
Static --> Clamp
Clamp --> Remaining[max 0 of transform_count - current_offset]
Remaining --> Count[min effective limit, remaining]
Count --> Complete{count <= 0 or offset + count >= total?}
Complete -->|yes| Done[group_complete true; next_offset 0]
Complete -->|no| Continue[group_complete false; next_offset offset + count]
```
## Lifecycle/state
The planner is stateless. It has no initialization, reset, cancellation,
recovery or shutdown work. Each call returns a complete detached value plan.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader build loop
participant Planner as M2BuildBatchPlanner
participant Materializer as Animated/MultiMesh adapter
participant Scheduler as RenderBudgetScheduler
Loader->>Planner: plan_batch(count, offset, path kind, limits)
Planner-->>Loader: batch_count, next_offset, group_complete
Loader->>Loader: resolve/request prototype or mesh
Loader->>Materializer: materialize selected transform slice
Loader->>Loader: adopt cursor/group completion
Loader->>Scheduler: consume M2_BUILD permit
```
## Ownership, threading and resources
- The planner owns only call-local scalar values and the returned Dictionary.
- The loader owns build-job Dictionaries, queue ordering, tile checks, resource
readiness/retry, serial numbers and group-index mutation.
- Materializers own main-thread Node/MultiMesh construction under loader roots.
- The scheduler owns the frame-local `M2_BUILD` counter.
- Pure planning is thread-safe, though the current adapter calls it on main thread.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Zero/negative selected limit | Scalar clamp | Effective size becomes one | Contract fixture | Correct profile value later |
| Empty transform group | Remaining count zero | Count zero; group completes | Contract fixture | Loader advances group |
| Offset at/past end | Remaining count clamp | Count zero; offset resets | Contract fixture | Loader advances group |
| Negative offset | No new validation | Historical arithmetic is preserved | Contract fixture | Loader inputs should remain valid |
| Resource pending/missing | Outside planner | Loader rotates or advances as before | Existing cache/build diagnostics | Resource loader retry/fallback |
| Tile cancelled | Outside planner | Loader cancels job before planning/materialization | Existing lifecycle gates | Eligible tile may requeue |
## Configuration and capabilities
The planner introduces no setting. It consumes existing
`m2_animated_instances_per_tick` and `m2_multimesh_batch_size` values after the
loader determines whether an animated prototype is available.
## Persistence, cache and migration
No persistence, schema or cache format participates. ADT/M2 cache versions,
quality presets and rebuild instructions are unchanged.
## Diagnostics and observability
The planner emits no logs. Its verifier covers deterministic formula/source
boundaries and records bounded 20,000-plan timing. Existing loader metrics retain
queue depth, build activity and hitch observability.
## Verification
- `verify_m2_build_batch_planner.gd`: static/animated selection, invalid-limit
clamp, remaining cap, exact completion, empty/past-end and negative-offset
behavior, loader/dependency source boundaries and bounded timing.
- Grouper, transform resolver, unique registry/dedupe, facade, internal-access,
manifest, shutdown, scheduler, streaming, coordinate, documentation and
coordination regressions remain required.
- Fidelity evidence is formula extraction only. Asset-backed frame pacing and
visual output require the renderer checkpoint/performance workflow.
## Extension points
- A later package may define typed build-job state once queue/resource transitions
are extracted together with explicit cancellation and recovery.
- Spatial-cell batching must use measured culling/performance evidence and must
not silently change this model-path batch cursor.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Static/animated batch cursor planning | Implemented extraction | Contract/source/timing verifier | Asset-backed p95/p99 pending |
| Build queue/resource state machine | Remains in loader | Existing lifecycle regressions | Stateful extraction pending |
| Spatial-cell batching | Planned | Renderer roadmap | Culling evidence/design pending |
## Known gaps and risks
- The result remains a Dictionary while loader build jobs are untyped Dictionaries.
- Negative offsets are preserved rather than rejected to avoid a hidden behavior change.
- Planning timing does not measure resource loading or materialization.
- Private asset-backed p95/p99 and visual comparison remain unavailable.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_build_batch_planner.gd` | Pure limit/count/cursor planning |
| `src/scenes/streaming/streaming_world_loader.gd` | Build job, queue, resources, materialization and budgets |
| `src/render/m2/m2_placement_grouper.gd` | Produces grouped transform arrays |
| `src/tools/verify_m2_build_batch_planner.gd` | Formula, source and timing regression |
## Related decisions and references
- [`m2-placement-grouper.md`](m2-placement-grouper.md)
- [`m2-placement-transform-resolver.md`](m2-placement-transform-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)
+4 -2
View File
@@ -153,8 +153,8 @@ continue to own task/build/cache observability.
## Extension points
- A later package may extract the M2 build-job state machine behind explicit
budgets while retaining this pure grouping input.
- `M2BuildBatchPlanner` now owns pure batch sizing/cursor progression; the
remaining build-job state machine stays behind explicit loader budgets.
- Spatial cells require measured culling evidence and are outside this contract.
## Capability status
@@ -179,11 +179,13 @@ continue to own task/build/cache observability.
|---|---|
| `src/render/m2/m2_placement_grouper.gd` | Validation, normalization and grouping |
| `src/render/m2/m2_placement_transform_resolver.gd` | Basis and origin rules |
| `src/render/m2/m2_build_batch_planner.gd` | Downstream batch sizing and cursor planning |
| `src/scenes/streaming/streaming_world_loader.gd` | Worker/result/build lifecycle and resources |
| `src/tools/verify_m2_placement_grouper.gd` | Contract, dependency and timing regression |
## Related decisions and references
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
- [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md)
- [`m2-unique-placement-registry.md`](m2-unique-placement-registry.md)
- [`world-renderer.md`](world-renderer.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)
+192
View File
@@ -0,0 +1,192 @@
# WMO Placement Resolver
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-WMO-PLACEMENT-RESOLVER-001` |
| Owners | Pure WMO cache-key, placement-identity and world-transform rules |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-placement-resolver`, 2026-07-17 |
| Profiles/capabilities | Existing ADT/WDT WMO placement paths |
## Purpose
Provide one scene-free boundary for normalized WMO cache keys, MODF placement
identity and world-space Godot transforms across render-cache, cached-scene and
live-prototype instance paths.
## Non-goals
- Parse ADT/WDT or convert canonical WoW coordinates.
- 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.
## Context and boundaries
```mermaid
flowchart LR
Parsed[ADT/WDT WMO placement] --> Loader[StreamingWorldLoader adapter]
Loader --> Resolver[WmoPlacementResolver]
Resolver --> CacheKey[Normalized cache key]
Resolver --> Identity[Registry unique key]
Resolver --> Transform[World Transform3D]
CacheKey --> Cache[Loader WMO caches/requests]
Identity --> Registry[WmoPlacementRegistry]
Transform --> RenderRoot[Lightweight render root]
Transform --> Scene[Cached scene instance]
Transform --> Live[Live prototype instance]
```
Allowed dependencies are Dictionary/String values and Godot `Vector3`, `Basis`
and `Transform3D` math. Node, SceneTree, RenderingServer, ResourceLoader,
WorkerThreadPool, mutexes, files, gameplay, network and editor UI are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `normalize_relative_path(relative_wmo_path)` | Pure query | Return lowercase slash-normalized cache/lookup key | Any thread; value result | Empty input returns empty |
| `resolve_unique_key(placement, tile_key, placement_index)` | Pure query | Return positive UID identity or tile/index fallback | Any thread; value result | Missing/non-positive UID uses fallback |
| `resolve_world_transform(placement)` | Pure query | Compose world-space position, Godot Euler rotation and uniform scale | Any thread; value result | Missing fields use zero/zero/one defaults; scale is not clamped |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Relative WMO path | WDT/ADT name table | Path normalizer | Caller String | One call |
| 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 | `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.
## Data flow
```mermaid
flowchart TD
Path[relative path] --> Slash[replace backslash with slash]
Slash --> Lower[lowercase entire key]
Placement[placement Dictionary] --> UID{unique_id > 0?}
UID -->|yes| UidKey[uid decimal]
UID -->|no| TileKey[tile key + placement index]
Placement --> Defaults[position/rotation/scale defaults]
Defaults --> Basis[Godot Basis.from_euler]
Basis --> Scale[unclamped uniform scale]
Scale --> World[world Transform3D]
```
## Lifecycle/state
The resolver is stateless. Construction, world reset, map switch, cancellation
and shutdown require no resolver operation.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Resolver as WmoPlacementResolver
participant Registry as WmoPlacementRegistry
participant Instance as Render/cached/live instance
Loader->>Resolver: normalize_relative_path(path)
Resolver-->>Loader: cache key
Loader->>Resolver: resolve_unique_key(placement, tile, index)
Resolver-->>Registry: identity adopted by loader
Loader->>Resolver: resolve_world_transform(placement)
Resolver-->>Loader: value Transform3D
Loader->>Instance: assign transform and attach/build
```
## Ownership, threading and resources
- The resolver owns only call-local scalar/value calculations.
- `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.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty path | Empty String | Empty normalized key | Contract fixture | Loader cache path rejects/degrades |
| Missing/non-positive UID | Integer rule | Tile/index key prevents same-tile collisions | Contract fixture | Rebake old ADT cache to restore cross-tile UID dedupe |
| Missing transform fields | Dictionary defaults | Zero position/rotation; scale one | Contract fixture | Repair parser/cache input |
| Zero/negative scale | No historical clamp | Preserve zero/mirrored basis | Contract fixture | Preserve source behavior |
| Resource missing/pending | Outside resolver | Loader retry/fallback path unchanged | Existing loader diagnostics | Restore cache/raw asset |
| Tile unregister/reset | Outside resolver | Loader releases refs/nodes/jobs | Shutdown/registry regressions | New placements resolve afresh |
## Configuration and capabilities
The resolver introduces no setting or profile branch. WMO radii, visibility,
shadow/occlusion flags, cache size limits and build budgets remain loader-owned.
## Persistence, cache and migration
No format is written. Existing WMO `.tscn`/lightweight render cache versions and
ADT/WDT formats are unchanged. Older placement caches without positive UID retain
the existing per-tile fallback and require no migration for this extraction.
## Diagnostics and observability
The resolver emits no logs. Its verifier records path/identity/transform/source
contracts and bounded 20,000-triple timing. WMO queue/cache/instance metrics remain
in the facade/loader diagnostic snapshot.
## Verification
- `verify_wmo_placement_resolver.gd`: slash/case/empty path, positive UID,
missing/zero/negative UID fallback, default and rotated/scaled transform,
unclamped scale, historical Node3D property equivalence, stateless results,
all loader adapters, dependency boundary
and bounded timing.
- Existing WMO material/cache/shutdown and renderer baseline regressions remain
required alongside M2/terrain/facade/internal-access/coordinate gates.
- Fidelity evidence is exact extraction of current placement rules. No new
original-client WMO visual/portal/material parity claim is made.
## Extension points
- `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.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Consistent WMO placement identity/transform | Implemented extraction | Contract/source/timing verifier | Asset-backed placement comparison 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
- Raw placement Dictionaries remain the parser/cache boundary.
- Old caches without UID cannot dedupe one WMO across ADT tile boundaries.
- WMO scale remains intentionally unclamped, including zero/negative input.
- Asset-backed visual/p95/p99 evidence is unavailable in this package.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_placement_resolver.gd` | Path, identity and transform rules |
| `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)
+80 -3
View File
@@ -5,9 +5,9 @@
| Field | Value |
|---|---|
| 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 unique/transform/grouping packages |
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
| Owners | Renderer workstream / milestone integrator |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-placement-grouper`, 2026-07-16 |
| Last verified | Worktree `work/sindo-main-codex/m03-adt-water-load-pipeline`, 2026-07-17 |
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose
@@ -52,6 +52,16 @@ flowchart LR
M2Grouper --> M2Transform[M2PlacementTransformResolver]
M2Transform --> M2Grouper
M2Transform --> Loader
Loader --> M2Batch[M2BuildBatchPlanner]
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]
@@ -121,6 +131,14 @@ from externally reading/writing loader-private queue, task, cache and tile-state
| `M2UniquePlacementRegistry.reserve/release/clear` | Internal M2 service | Owns positive cross-tile ADT placement-ID reservations | Renderer thread/map session | Invalid/unkeyed/non-owner inputs preserve documented fallback |
| `M2PlacementTransformResolver.resolve_basis/resolve_origin_offset` | Internal pure M2 service | Resolves regular and calibrated model-specific ADT placement transforms | Worker/main thread; stateless | Unknown paths use regular basis and zero offset |
| `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 |
| `AdtWaterLoadPipelineState` | Internal liquid async-state service | Owns ADT water FIFO/dedupe, active task IDs and mutex result mailbox | Main-thread state; worker result publication | Invalid/duplicate requests rejected; clear does not interrupt workers |
Публичным contract не считаются `StreamingWorldLoader` methods/properties,
внутренние dictionaries, queues, job records и generated node names. Scene-owned
@@ -148,6 +166,14 @@ loader configuration remains transitional composition data, not a caller API.
| Internal registry | Tile key and M2 placement unique IDs | Loader / `M2UniquePlacementRegistry` | Filtered grouping input and tile retry state | Registry-owned strings; fresh result arrays | Map session |
| Internal transform | Rotation/path/scale | Loader or grouper / `M2PlacementTransformResolver` | Group/placeholder/instance transforms | Value-only Basis/Vector3 | One placement |
| 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 |
| Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset |
| 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 |
@@ -194,7 +220,13 @@ flowchart TD
R --> M2Registry[M2UniquePlacementRegistry reserve]
M2Registry --> M2Grouper[M2PlacementGrouper]
M2Transform[M2PlacementTransformResolver] --> M2Grouper
M2Grouper --> M2
M2Grouper --> M2Batch[M2BuildBatchPlanner]
M2Batch --> M2
R --> WmoPlacement[WmoPlacementResolver]
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]
@@ -314,6 +346,22 @@ sequenceDiagram
transforms and every build/render side effect remain loader-owned.
- `M2PlacementGrouper` is stateless and owns only call-local grouped transforms.
The loader retains tasks, mutex/result queues, stale checks and build state.
- `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. `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.
- `AdtWaterLoadPipelineState` owns pending request order/deduplication, opaque
active task IDs and the worker-safe parsed-result mailbox. The loader retains
WorkerThreadPool start/wait, ADTLoader parsing, concurrency/finalize permits,
stale tile/path/root checks and ADTBuilder/Node finalization.
- 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
@@ -403,6 +451,25 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
normalization, scale compensation, all three consumers and bounded timing.
- M2 placement grouper contract: invalid/default records, historical path rules,
group order, local transforms, fresh output, source boundary and bounded timing.
- M2 build batch contract: static/animated limit selection, clamp, remaining
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.
- ADT water load pipeline contract: FIFO/dedupe, active task identity, real
worker-thread publication, pending-only cancellation, result payload/order,
clear/source boundaries 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.
@@ -435,7 +502,15 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| M2 unique placement registry | Implemented extraction | Scene-free ownership/lifecycle/timing contract and historical `uid:11785` smoke | Group/build/tasks/finalization and asset-backed p95/p99 remain pending |
| 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 | 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 |
| ADT water load pipeline state | Implemented extraction | Scene-free FIFO/task/thread/source/timing contract | Parse/finalization and asset-backed traversal/leak evidence pending |
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
| Sky/light | Partial | DBC controller/logs | Zone transition paired validation |
| Character presentation | Prototype | Character creator/player experiment | Full customization/equipment states |
@@ -485,6 +560,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/render/entities/world_entity_presenter.gd` | Main-thread entity visual subtree ownership and lifecycle |
| `src/render/terrain/rendered_ground_sample.gd` | Immutable renderer-owned available/unavailable ground query result |
| `src/render/terrain/terrain_quality_mesh_cache.gd` | Full-quality terrain revisit Mesh retention and LRU ownership |
| `src/render/liquid/adt_water_load_pipeline_state.gd` | ADT water request/task/result bookkeeping and worker-safe mailbox |
| `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets |
@@ -497,6 +573,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/tools/verify_world_entity_presentation.gd` | Entity snapshot, create/update/replace/remove and ownership regression |
| `src/tools/verify_rendered_ground_sample.gd` | Cold-start renderer ground result invariant regression |
| `src/tools/verify_terrain_quality_mesh_cache.gd` | Terrain cache admission, LRU, ownership and loader-boundary regression |
| `src/tools/verify_adt_water_load_pipeline_state.gd` | ADT water FIFO/task/thread/boundary/timing regression |
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
| `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
@@ -0,0 +1,167 @@
class_name AdtWaterLoadPipelineState
extends RefCounted
## Owns ADT water request, active-task and worker-result bookkeeping. The caller
## owns WorkerThreadPool operations, ADT parsing, permits and scene finalization.
var _pending_requests: Array[Dictionary] = []
var _queued_tile_keys: Dictionary = {}
var _active_task_ids_by_tile_key: Dictionary = {}
var _result_mutex := Mutex.new()
var _results: Array[Dictionary] = []
## Appends one tile/path request in FIFO order. Invalid, queued or active tile
## keys are rejected without mutation.
func enqueue_request(tile_key: String, adt_path: String) -> bool:
if tile_key.is_empty() or adt_path.is_empty():
return false
if _queued_tile_keys.has(tile_key) or _active_task_ids_by_tile_key.has(tile_key):
return false
_pending_requests.append({
"key": tile_key,
"path": adt_path,
})
_queued_tile_keys[tile_key] = true
return true
## Removes and returns the oldest request, also releasing its queued-key marker.
## An empty Dictionary means no pending request exists.
func take_next_request() -> Dictionary:
if _pending_requests.is_empty():
return {}
var request: Dictionary = _pending_requests.pop_front()
_queued_tile_keys.erase(String(request.get("key", "")))
return request
## Removes all pending requests for one tile without affecting active work.
## Returns the number of removed FIFO entries.
func cancel_pending_requests(tile_key: String) -> int:
if tile_key.is_empty():
return 0
_queued_tile_keys.erase(tile_key)
var removed_count := 0
for request_index in range(_pending_requests.size() - 1, -1, -1):
var request: Dictionary = _pending_requests[request_index]
if String(request.get("key", "")) != tile_key:
continue
_pending_requests.remove_at(request_index)
removed_count += 1
return removed_count
## Records one caller-started worker task. The opaque task ID is retained until
## the caller waits for and completes it.
func remember_active_task(tile_key: String, task_id: int) -> bool:
if tile_key.is_empty() or _active_task_ids_by_tile_key.has(tile_key):
return false
_active_task_ids_by_tile_key[tile_key] = task_id
return true
## Returns whether a worker task is active for the tile.
func has_active_task(tile_key: String) -> bool:
return not tile_key.is_empty() and _active_task_ids_by_tile_key.has(tile_key)
## Returns the opaque active task ID, or -1 for an unknown tile.
func active_task_id_for(tile_key: String) -> int:
return int(_active_task_ids_by_tile_key.get(tile_key, -1))
## Removes and returns one completed task ID, or -1 for an unknown tile.
func complete_active_task(tile_key: String) -> int:
if tile_key.is_empty() or not _active_task_ids_by_tile_key.has(tile_key):
return -1
var task_id := int(_active_task_ids_by_tile_key[tile_key])
_active_task_ids_by_tile_key.erase(tile_key)
return task_id
## Returns a detached snapshot of opaque task IDs for orderly shutdown waiting.
func active_task_ids_snapshot() -> Array:
return _active_task_ids_by_tile_key.values().duplicate()
## Publishes one parsed ADT result from a worker thread. Payload identity is
## retained; the caller must not mutate it after publication.
func publish_result(tile_key: String, adt_path: String, water_data: Dictionary) -> bool:
if tile_key.is_empty() or adt_path.is_empty():
return false
_result_mutex.lock()
_results.append({
"key": tile_key,
"path": adt_path,
"data": water_data,
})
_result_mutex.unlock()
return true
## Returns whether a worker result is currently ready.
func has_result() -> bool:
_result_mutex.lock()
var result_available := not _results.is_empty()
_result_mutex.unlock()
return result_available
## Removes and returns the oldest worker result. An empty Dictionary means no
## result was ready. Intended for the serialized renderer main thread.
func pop_result() -> Dictionary:
_result_mutex.lock()
var result: Dictionary = {}
if not _results.is_empty():
result = _results.pop_front()
_result_mutex.unlock()
return result
## Returns pending plus active load count for existing renderer metrics.
func total_load_count() -> int:
return _pending_requests.size() + _active_task_ids_by_tile_key.size()
## Returns the pending request count.
func pending_request_count() -> int:
return _pending_requests.size()
## Returns the active worker-task count.
func active_task_count() -> int:
return _active_task_ids_by_tile_key.size()
## Clears bookkeeping and queued results without interrupting worker tasks.
## Orderly shutdown must wait the task-ID snapshot before calling this method.
func clear() -> void:
_pending_requests.clear()
_queued_tile_keys.clear()
_active_task_ids_by_tile_key.clear()
_result_mutex.lock()
_results.clear()
_result_mutex.unlock()
## Returns detached FIFO/task/result scalar diagnostics without water payloads.
func diagnostic_snapshot() -> Dictionary:
var pending_requests: Array[Dictionary] = []
for request in _pending_requests:
pending_requests.append(request.duplicate())
var active_tile_keys: Array = _active_task_ids_by_tile_key.keys()
active_tile_keys.sort()
_result_mutex.lock()
var result_records: Array[Dictionary] = []
for result in _results:
result_records.append({
"key": String(result.get("key", "")),
"path": String(result.get("path", "")),
})
_result_mutex.unlock()
return {
"pending_requests": pending_requests,
"active_tile_keys": active_tile_keys.duplicate(),
"result_records": result_records,
}
@@ -0,0 +1 @@
uid://pwqm3n25pnn7
+30
View File
@@ -0,0 +1,30 @@
class_name M2BuildBatchPlanner
extends RefCounted
## Stateless M2 build batch sizing and group-cursor progression.
## Plans one static or animated build batch. The returned Dictionary is detached
## and contains effective_batch_size, batch_count, next_offset and group_complete.
func plan_batch(
transform_count: int,
current_offset: int,
has_animated_prototype: bool,
animated_batch_limit: int,
static_batch_limit: int
) -> Dictionary:
var selected_batch_limit := (
animated_batch_limit if has_animated_prototype else static_batch_limit
)
var effective_batch_size := maxi(1, selected_batch_limit)
var remaining_transform_count := maxi(0, transform_count - current_offset)
var batch_count := mini(effective_batch_size, remaining_transform_count)
var is_group_complete := (
batch_count <= 0 or current_offset + batch_count >= transform_count
)
return {
"effective_batch_size": effective_batch_size,
"batch_count": batch_count,
"next_offset": 0 if is_group_complete else current_offset + batch_count,
"group_complete": is_group_complete,
}
@@ -0,0 +1 @@
uid://d0my7yntfsvuy
+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
+32
View File
@@ -0,0 +1,32 @@
class_name WmoPlacementResolver
extends RefCounted
## Stateless WMO cache-key, placement-identity and world-transform rules.
## Returns the historical lowercase slash-normalized relative WMO cache key.
func normalize_relative_path(relative_wmo_path: String) -> String:
return relative_wmo_path.replace("\\", "/").to_lower()
## Returns the positive MODF unique-ID key or the historical tile/index fallback.
func resolve_unique_key(
placement: Dictionary,
tile_key: String,
placement_index: int
) -> String:
var unique_id := int(placement.get("unique_id", -1))
if unique_id > 0:
return "uid:%d" % unique_id
return "tile:%s:%d" % [tile_key, placement_index]
## Returns the world-space Godot transform used by every WMO instance path.
func resolve_world_transform(placement: Dictionary) -> Transform3D:
var world_position: Vector3 = placement.get("pos", Vector3.ZERO)
var rotation_radians: Vector3 = placement.get("rot", Vector3.ZERO)
var scale_value := float(placement.get("scale", 1.0))
return Transform3D(
Basis.from_euler(rotation_radians).scaled(Vector3.ONE * scale_value),
world_position
)
@@ -0,0 +1 @@
uid://02omydj0sxmo
+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
+227 -226
View File
@@ -9,6 +9,24 @@ const SPLAT_TILE_SCRIPT := preload("res://src/resources/splat_adt_tile.gd")
const CONTROL_SPLAT_TILE_SCRIPT := preload("res://src/resources/control_splat_adt_tile.gd")
const WMO_STREAMING_SCRIPT := preload("res://src/resources/wmo_streaming_resource.gd")
const WMO_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/wmo_builder.gd")
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")
@@ -24,6 +42,9 @@ const TERRAIN_CHUNK_LOD_PLANNER_SCRIPT := preload("res://src/render/terrain/terr
const TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER_SCRIPT := preload(
"res://src/render/terrain/terrain_chunk_geometry_queue_planner.gd"
)
const ADT_WATER_LOAD_PIPELINE_STATE_SCRIPT := preload(
"res://src/render/liquid/adt_water_load_pipeline_state.gd"
)
const M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT := preload(
"res://src/render/m2/m2_unique_placement_registry.gd"
)
@@ -33,6 +54,9 @@ const M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT := preload(
const M2_PLACEMENT_GROUPER_SCRIPT := preload(
"res://src/render/m2/m2_placement_grouper.gd"
)
const M2_BUILD_BATCH_PLANNER_SCRIPT := preload(
"res://src/render/m2/m2_build_batch_planner.gd"
)
const STREAMING_TARGET_PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
const STREAMING_TARGET_POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd")
@@ -219,6 +243,7 @@ var _m2_unique_placement_registry := (
)
var _m2_placement_transform_resolver := M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()
var _m2_placement_grouper := M2_PLACEMENT_GROUPER_SCRIPT.new()
var _m2_build_batch_planner := M2_BUILD_BATCH_PLANNER_SCRIPT.new()
var _m2_mesh_cache: Dictionary = {}
var _m2_mesh_load_requests: Dictionary = {}
var _m2_mesh_finalize_queue: Array = []
@@ -234,11 +259,7 @@ var _terrain_splat_cache_tasks: Dictionary = {}
var _terrain_splat_tasks: Dictionary = {}
var _terrain_splat_result_mutex := Mutex.new()
var _terrain_splat_result_queue: Array = []
var _water_load_queue: Array = []
var _water_load_queued: Dictionary = {}
var _water_load_tasks: Dictionary = {}
var _water_result_mutex := Mutex.new()
var _water_result_queue: Array = []
var _adt_water_load_pipeline_state := ADT_WATER_LOAD_PIPELINE_STATE_SCRIPT.new()
var _tile_result_mutex := Mutex.new()
var _tile_result_queue: Array = []
var _shared_tex_cache: Dictionary = {}
@@ -264,21 +285,19 @@ 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 = {}
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:
@@ -449,10 +468,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()
@@ -981,14 +998,18 @@ func _log_hitch_profile(start_usec: int, timings: Array[String], did_refresh: bo
_terrain_upgrade_tasks.size(),
_terrain_control_splat_cache_tasks.size(),
_terrain_splat_queue_size(),
_water_load_queue.size() + _water_load_tasks.size(),
_adt_water_load_pipeline_state.total_load_count(),
_detail_asset_queue.size(),
_m2_group_tasks.size(),
_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(),
@@ -1008,14 +1029,18 @@ func render_baseline_snapshot() -> Dictionary:
"terrain_upgrade": _terrain_upgrade_tasks.size(),
"terrain_control": _terrain_control_splat_cache_tasks.size(),
"terrain_splat": _terrain_splat_queue_size(),
"water": _water_load_queue.size() + _water_load_tasks.size(),
"water": _adt_water_load_pipeline_state.total_load_count(),
"detail": _detail_asset_queue.size(),
"m2_task": _m2_group_tasks.size(),
"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(),
@@ -1023,7 +1048,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(),
}
@@ -1279,7 +1304,7 @@ func _tick_runtime_stats(delta: float) -> void:
_terrain_upgrade_tasks.size(),
_terrain_control_splat_cache_tasks.size(),
_terrain_splat_queue_size(),
_water_load_queue.size() + _water_load_tasks.size(),
_adt_water_load_pipeline_state.total_load_count(),
terrain_baked_full,
terrain_control_splat,
terrain_splat,
@@ -1296,8 +1321,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(),
@@ -1305,7 +1334,7 @@ func _tick_runtime_stats(delta: float) -> void:
m2_tiles,
m2_multimeshes,
m2_instances,
_wmo_registry.size(),
_wmo_placement_registry.active_count(),
])
@@ -2200,24 +2229,20 @@ func _request_tile_water_load(state: Dictionary) -> void:
var path := String(state.get("path", ""))
if key.is_empty() or path.is_empty():
return
if _water_load_tasks.has(key) or _water_load_queued.has(key):
return
_water_load_queue.append({
"key": key,
"path": path,
})
_water_load_queued[key] = true
_adt_water_load_pipeline_state.enqueue_request(key, path)
func _process_water_load_queue() -> void:
if not enable_water:
return
var limit := maxi(1, max_concurrent_water_tasks)
while _water_load_tasks.size() < limit and not _water_load_queue.is_empty():
var request: Dictionary = _water_load_queue.pop_front()
while (
_adt_water_load_pipeline_state.active_task_count() < limit
and _adt_water_load_pipeline_state.pending_request_count() > 0
):
var request: Dictionary = _adt_water_load_pipeline_state.take_next_request()
var key := String(request.get("key", ""))
_water_load_queued.erase(key)
if key.is_empty() or _water_load_tasks.has(key):
if key.is_empty() or _adt_water_load_pipeline_state.has_active_task(key):
continue
if not _tile_states.has(key):
continue
@@ -2225,7 +2250,7 @@ func _process_water_load_queue() -> void:
if path.is_empty():
continue
var task_id: int = WorkerThreadPool.add_task(_load_tile_water_task.bind(key, path))
_water_load_tasks[key] = task_id
_adt_water_load_pipeline_state.remember_active_task(key, task_id)
func _load_tile_water_task(key: String, adt_path: String) -> void:
@@ -2234,13 +2259,7 @@ func _load_tile_water_task(key: String, adt_path: String) -> void:
if loader:
data = loader.call("load_adt", adt_path)
_water_result_mutex.lock()
_water_result_queue.append({
"key": key,
"path": adt_path,
"data": data,
})
_water_result_mutex.unlock()
_adt_water_load_pipeline_state.publish_result(key, adt_path, data)
func _drain_water_load_results() -> void:
@@ -2248,20 +2267,20 @@ func _drain_water_load_results() -> void:
if not _render_budget_scheduler.has_remaining_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.WATER_FINALIZE):
return
_water_result_mutex.lock()
while not _water_result_queue.is_empty() and _render_budget_scheduler.try_consume_permit(
while _adt_water_load_pipeline_state.has_result() and _render_budget_scheduler.try_consume_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.WATER_FINALIZE):
results.append(_water_result_queue.pop_front())
_water_result_mutex.unlock()
results.append(_adt_water_load_pipeline_state.pop_result())
for result in results:
var key := String(result.get("key", ""))
if key.is_empty():
continue
if not _water_load_tasks.has(key):
if not _adt_water_load_pipeline_state.has_active_task(key):
continue
WorkerThreadPool.wait_for_task_completion(int(_water_load_tasks[key]))
_water_load_tasks.erase(key)
WorkerThreadPool.wait_for_task_completion(
_adt_water_load_pipeline_state.active_task_id_for(key)
)
_adt_water_load_pipeline_state.complete_active_task(key)
if _shutting_down or not _tile_states.has(key):
continue
@@ -2329,26 +2348,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():
@@ -2388,14 +2408,9 @@ func _wait_for_tile_tasks() -> void:
_terrain_splat_result_queue.clear()
_terrain_splat_result_mutex.unlock()
for task_id in _water_load_tasks.values():
for task_id in _adt_water_load_pipeline_state.active_task_ids_snapshot():
WorkerThreadPool.wait_for_task_completion(int(task_id))
_water_load_tasks.clear()
_water_load_queue.clear()
_water_load_queued.clear()
_water_result_mutex.lock()
_water_result_queue.clear()
_water_result_mutex.unlock()
_adt_water_load_pipeline_state.clear()
func _should_accept_loaded_tile(request: Dictionary) -> bool:
@@ -2930,11 +2945,7 @@ func _release_tile(key: String) -> void:
_terrain_control_splat_cache_tasks.erase(key)
_terrain_splat_cache_tasks.erase(key)
_terrain_splat_tasks.erase(key)
_water_load_queued.erase(key)
for i in range(_water_load_queue.size() - 1, -1, -1):
var pending: Dictionary = _water_load_queue[i]
if String(pending.get("key", "")) == key:
_water_load_queue.remove_at(i)
_adt_water_load_pipeline_state.cancel_pending_requests(key)
var tile_lod_rid: RID = state.get("tile_lod_rid", RID())
if tile_lod_rid.is_valid():
_free_render_instance(tile_lod_rid)
@@ -2965,12 +2976,7 @@ func _clear_streamed_world() -> void:
_terrain_splat_result_mutex.lock()
_terrain_splat_result_queue.clear()
_terrain_splat_result_mutex.unlock()
_water_load_queue.clear()
_water_load_queued.clear()
_water_load_tasks.clear()
_water_result_mutex.lock()
_water_result_queue.clear()
_water_result_mutex.unlock()
_adt_water_load_pipeline_state.clear()
_detail_asset_queue.clear()
_detail_asset_queued.clear()
for key in _m2_build_jobs.keys():
@@ -2984,11 +2990,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()
@@ -3355,7 +3358,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()
@@ -3379,10 +3382,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:
@@ -3553,11 +3553,13 @@ func _process_wmo_build_jobs() -> void:
var placement: Dictionary = placement_variant
var name_id: int = int(placement.get("name_id", -1))
if name_id >= 0 and name_id < wmo_names.size():
var unique_key := _wmo_unique_key(placement, 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
var unique_key := _wmo_placement_resolver.resolve_unique_key(
placement,
tile_key,
index
)
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("\\", "/")
@@ -3567,20 +3569,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 := _normalize_wmo_rel_path(rel_path)
if _wmo_render_load_requests.has(normalized_rel):
var normalized_rel := _wmo_placement_resolver.normalize_relative_path(
rel_path
)
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)
@@ -3588,10 +3589,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)
@@ -3599,10 +3601,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
@@ -3633,33 +3636,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 (
@@ -3667,24 +3673,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
func _normalize_wmo_rel_path(rel_path: String) -> String:
return rel_path.replace("\\", "/").to_lower()
_wmo_render_resource_cache_state.complete_request_as_missing(normalized_rel)
func _get_wmo_render_or_request(rel_path: String) -> Resource:
var normalized_rel := _normalize_wmo_rel_path(rel_path)
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")
@@ -3697,31 +3702,31 @@ 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
func _get_wmo_scene_or_request(rel_path: String) -> PackedScene:
var normalized_rel := _normalize_wmo_rel_path(rel_path)
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),
@@ -3735,49 +3740,37 @@ 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
func _instantiate_wmo_render_root(rel_path: String, placement: Dictionary) -> Node3D:
var root := Node3D.new()
root.name = rel_path.get_file().get_basename()
root.position = placement.get("pos", Vector3.ZERO)
root.rotation = placement.get("rot", Vector3.ZERO)
var scale_value: float = float(placement.get("scale", 1.0))
root.scale = Vector3.ONE * scale_value
root.transform = _wmo_placement_resolver.resolve_world_transform(placement)
return root
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
@@ -3785,16 +3778,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
@@ -3805,28 +3813,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
@@ -3837,8 +3844,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
@@ -3846,9 +3856,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:
@@ -3889,10 +3897,7 @@ func _instantiate_wmo_scene(rel_path: String, scene: PackedScene, placement: Dic
instance.free()
return null
instance.name = rel_path.get_file().get_basename()
instance.position = placement.get("pos", Vector3.ZERO)
instance.rotation = placement.get("rot", Vector3.ZERO)
var scale_value: float = float(placement.get("scale", 1.0))
instance.scale = Vector3.ONE * scale_value
instance.transform = _wmo_placement_resolver.resolve_world_transform(placement)
return instance
@@ -3990,6 +3995,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", ""))
@@ -3999,44 +4015,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 _wmo_unique_key(placement: Dictionary, tile_key: String, idx: int) -> String:
var uid: int = int(placement.get("unique_id", -1))
# uniqueId is always set in vanilla/3.3.5a MODF data, but older baked caches
# predating the ADTLoader unique_id field will miss it. Fall back to a
# per-tile synthetic key — no cross-tile dedup, but also no collisions.
if uid > 0:
return "uid:%d" % uid
return "tile:%s:%d" % [tile_key, idx]
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
@@ -4315,14 +4318,15 @@ func _process_m2_build_jobs() -> void:
_m2_build_queue.append(key)
_render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.M2_BUILD)
continue
var batch_size: int = (
maxi(1, m2_animated_instances_per_tick)
if animated_prototype != null
else maxi(1, m2_multimesh_batch_size)
var batch_plan: Dictionary = _m2_build_batch_planner.plan_batch(
transforms.size(),
offset,
animated_prototype != null,
m2_animated_instances_per_tick,
m2_multimesh_batch_size
)
var batch_count: int = mini(batch_size, maxi(0, transforms.size() - offset))
var batch_count: int = int(batch_plan["batch_count"])
var serial: int = int(job.get("serial", 0))
var consumed := batch_count
if batch_count > 0:
if animated_prototype != null:
_materialize_m2_animated_batch(root as Node3D, rel_path, animated_prototype, transforms, offset, batch_count, serial)
@@ -4337,11 +4341,11 @@ func _process_m2_build_jobs() -> void:
else:
_materialize_m2_group_batch(root as Node3D, rel_path, mesh, transforms, offset, batch_count, serial)
job["serial"] = serial + 1
if consumed <= 0 or offset + consumed >= transforms.size():
if bool(batch_plan["group_complete"]):
job["index"] = index + 1
job["offset"] = 0
else:
job["offset"] = offset + consumed
job["offset"] = int(batch_plan["next_offset"])
_m2_build_jobs[key] = job
_render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.M2_BUILD)
@@ -4893,10 +4897,7 @@ func _instantiate_wmo_world(rel_path: String, placement: Dictionary) -> Node3D:
instance.name = rel_path.get_file().get_basename()
# WMOs are parented to _terrain_root, whose position absorbs the editor
# offset — use world-space placement.pos directly (no tile_origin subtraction).
instance.position = placement.get("pos", Vector3.ZERO)
instance.rotation = placement.get("rot", Vector3.ZERO)
var scale_value: float = float(placement.get("scale", 1.0))
instance.scale = Vector3.ONE * scale_value
instance.transform = _wmo_placement_resolver.resolve_world_transform(placement)
if not enable_occlusion_culling:
var occluders := instance.get_node_or_null("Occluders")
if occluders != null:
@@ -0,0 +1,351 @@
extends SceneTree
## Asset-free lifecycle, threading, boundary and timing regression for ADT water loads.
const PIPELINE_SCRIPT := preload(
"res://src/render/liquid/adt_water_load_pipeline_state.gd"
)
const PIPELINE_PATH := "res://src/render/liquid/adt_water_load_pipeline_state.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_invalid_dedupe_and_fifo(failures)
_verify_active_task_lifecycle(failures)
_verify_result_mailbox_and_payload_identity(failures)
_verify_worker_publication(failures)
_verify_pending_cancellation(failures)
_verify_clear_and_detached_diagnostics(failures)
_verify_ownership_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("ADT_WATER_LOAD_PIPELINE_STATE: %s" % failure)
quit(1)
return
print(
"ADT_WATER_LOAD_PIPELINE_STATE PASS cases=12 iterations=100 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_invalid_dedupe_and_fifo(failures: Array[String]) -> void:
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
_expect_false(
pipeline.call("enqueue_request", "", "res://tile.adt"),
"empty tile key rejected",
failures
)
_expect_false(
pipeline.call("enqueue_request", "31:49", ""),
"empty path rejected",
failures
)
_expect_true(
pipeline.call("enqueue_request", "31:49", "res://a.adt"),
"first request accepted",
failures
)
_expect_false(
pipeline.call("enqueue_request", "31:49", "res://duplicate.adt"),
"queued duplicate rejected",
failures
)
pipeline.call("enqueue_request", "32:49", "res://b.adt")
_expect_equal(int(pipeline.call("pending_request_count")), 2, "pending count", failures)
var first_request: Dictionary = pipeline.call("take_next_request")
var second_request: Dictionary = pipeline.call("take_next_request")
_expect_string_equal(String(first_request["key"]), "31:49", "FIFO first key", failures)
_expect_string_equal(String(second_request["key"]), "32:49", "FIFO second key", failures)
_expect_true(
pipeline.call("enqueue_request", "31:49", "res://retry.adt"),
"dequeued key can be requested again",
failures
)
func _verify_active_task_lifecycle(failures: Array[String]) -> void:
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
_expect_false(
pipeline.call("remember_active_task", "", 7),
"empty active key rejected",
failures
)
_expect_true(
pipeline.call("remember_active_task", "31:49", 101),
"active task accepted",
failures
)
_expect_false(
pipeline.call("remember_active_task", "31:49", 202),
"duplicate active task rejected",
failures
)
_expect_true(
pipeline.call("has_active_task", "31:49"),
"active task observable",
failures
)
_expect_equal(
int(pipeline.call("active_task_id_for", "31:49")),
101,
"opaque task ID retained",
failures
)
_expect_false(
pipeline.call("enqueue_request", "31:49", "res://active.adt"),
"active tile rejects enqueue",
failures
)
_expect_equal(
int(pipeline.call("complete_active_task", "31:49")),
101,
"completion returns task ID",
failures
)
_expect_equal(
int(pipeline.call("complete_active_task", "31:49")),
-1,
"unknown completion rejected",
failures
)
func _verify_result_mailbox_and_payload_identity(failures: Array[String]) -> void:
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
var water_data := {"chunks": [{"liquid": true}]}
_expect_false(
pipeline.call("publish_result", "", "res://a.adt", water_data),
"empty result key rejected",
failures
)
_expect_true(
pipeline.call("publish_result", "31:49", "res://a.adt", water_data),
"valid result published",
failures
)
_expect_true(pipeline.call("has_result"), "published result observable", failures)
var result: Dictionary = pipeline.call("pop_result")
_expect_string_equal(String(result["key"]), "31:49", "result key", failures)
_expect_string_equal(String(result["path"]), "res://a.adt", "result path", failures)
_expect_true(result["data"] == water_data, "water payload identity retained", failures)
_expect_false(pipeline.call("has_result"), "result pop empties mailbox", failures)
_expect_true(
(pipeline.call("pop_result") as Dictionary).is_empty(),
"empty result pop is safe",
failures
)
func _verify_worker_publication(failures: Array[String]) -> void:
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
var worker := Thread.new()
var start_error := worker.start(
Callable(pipeline, "publish_result").bind(
"worker",
"res://worker.adt",
{"worker": true}
)
)
_expect_equal(int(start_error), OK, "worker publication starts", failures)
if start_error == OK:
worker.wait_to_finish()
_expect_true(pipeline.call("has_result"), "worker result safely visible", failures)
var result: Dictionary = pipeline.call("pop_result")
_expect_string_equal(String(result.get("key", "")), "worker", "worker result key", failures)
func _verify_pending_cancellation(failures: Array[String]) -> void:
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
pipeline.call("enqueue_request", "31:49", "res://a.adt")
pipeline.call("enqueue_request", "32:49", "res://b.adt")
pipeline.call("remember_active_task", "31:49-active", 12)
_expect_equal(
int(pipeline.call("cancel_pending_requests", "31:49")),
1,
"tile cancellation removes pending request",
failures
)
_expect_equal(int(pipeline.call("pending_request_count")), 1, "other pending retained", failures)
_expect_true(
pipeline.call("has_active_task", "31:49-active"),
"pending cancellation leaves active work",
failures
)
_expect_equal(
int(pipeline.call("cancel_pending_requests", "unknown")),
0,
"unknown cancellation unchanged",
failures
)
func _verify_clear_and_detached_diagnostics(failures: Array[String]) -> void:
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
pipeline.call("enqueue_request", "32:49", "res://b.adt")
pipeline.call("enqueue_request", "31:49", "res://a.adt")
pipeline.call("remember_active_task", "z", 2)
pipeline.call("remember_active_task", "a", 1)
pipeline.call("publish_result", "result", "res://result.adt", {"secret": "payload"})
var snapshot: Dictionary = pipeline.call("diagnostic_snapshot")
_expect_string_array(
snapshot["active_tile_keys"],
["a", "z"],
"active diagnostics sorted",
failures
)
_expect_equal(
(snapshot["pending_requests"] as Array).size(),
2,
"pending diagnostics count",
failures
)
_expect_false(
(snapshot["result_records"][0] as Dictionary).has("data"),
"diagnostics omit water payload",
failures
)
(snapshot["pending_requests"] as Array).clear()
var fresh_snapshot: Dictionary = pipeline.call("diagnostic_snapshot")
_expect_equal(
(fresh_snapshot["pending_requests"] as Array).size(),
2,
"diagnostics detached",
failures
)
pipeline.call("clear")
pipeline.call("clear")
_expect_equal(int(pipeline.call("total_load_count")), 0, "clear load count", failures)
_expect_false(pipeline.call("has_result"), "clear result mailbox", failures)
func _verify_ownership_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures)
var pipeline_source := _read_text(PIPELINE_PATH, failures)
_expect_true(
loader_source.contains("ADT_WATER_LOAD_PIPELINE_STATE_SCRIPT.new()"),
"loader composes pipeline state",
failures
)
for legacy_field in [
"var _water_load_queue:",
"var _water_load_queued:",
"var _water_load_tasks:",
"var _water_result_mutex",
"var _water_result_queue:",
]:
_expect_false(loader_source.contains(legacy_field), "loader removes %s" % legacy_field, failures)
for retained_loader_rule in [
"ClassDB.instantiate(\"ADTLoader\")",
"WorkerThreadPool.add_task(_load_tile_water_task.bind(key, path))",
"RENDER_BUDGET_SCHEDULER_SCRIPT.WATER_FINALIZE",
"_builder.build_tile_water_scene(data, origin)",
"tile_root.add_child(water_root)",
"state[\"water_loaded\"] = true",
]:
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
for forbidden_token in [
"WorkerThreadPool.",
"ADTLoader",
"RenderBudgetScheduler",
"Node3D",
"Mesh",
"RID",
"_tile_states",
]:
_expect_false(
pipeline_source.contains(forbidden_token),
"pipeline omits %s ownership" % forbidden_token,
failures
)
_expect_equal(
_count_occurrences(loader_source, "_adt_water_load_pipeline_state.clear()"),
2,
"loader retains two clear sites",
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
var started_microseconds := Time.get_ticks_usec()
for iteration in range(100):
for tile_index in range(256):
var tile_key := "tile:%d" % tile_index
pipeline.call("enqueue_request", tile_key, "res://tile_%d.adt" % tile_index)
while int(pipeline.call("pending_request_count")) > 0:
var request: Dictionary = pipeline.call("take_next_request")
var tile_key := String(request["key"])
pipeline.call("remember_active_task", tile_key, tile_key.hash())
pipeline.call("publish_result", tile_key, String(request["path"]), {})
while pipeline.call("has_result"):
var result: Dictionary = pipeline.call("pop_result")
pipeline.call("complete_active_task", String(result["key"]))
pipeline.call("clear")
_expect_equal(
int(pipeline.call("total_load_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 request/task/result 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 _count_occurrences(text: String, token: String) -> int:
return text.split(token).size() - 1
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://bb3v38yw47utf
+217
View File
@@ -0,0 +1,217 @@
extends SceneTree
## Asset-free contract, dependency and timing regression for M2 batch planning.
const PLANNER_SCRIPT := preload("res://src/render/m2/m2_build_batch_planner.gd")
const PLANNER_PATH := "res://src/render/m2/m2_build_batch_planner.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_static_batch_limit(failures)
_verify_animated_batch_limit(failures)
_verify_non_positive_limit_clamp(failures)
_verify_remaining_count_cap(failures)
_verify_exact_group_completion(failures)
_verify_empty_and_past_end(failures)
_verify_historical_negative_offset(failures)
_verify_source_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("M2_BUILD_BATCH_PLANNER: %s" % failure)
quit(1)
return
print(
"M2_BUILD_BATCH_PLANNER PASS cases=8 iterations=20000 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_static_batch_limit(failures: Array[String]) -> void:
var plan := _plan(300, 0, false, 8, 128)
_expect_plan(plan, 128, 128, 128, false, "static batch", failures)
func _verify_animated_batch_limit(failures: Array[String]) -> void:
var plan := _plan(300, 0, true, 8, 128)
_expect_plan(plan, 8, 8, 8, false, "animated batch", failures)
func _verify_non_positive_limit_clamp(failures: Array[String]) -> void:
var animated_plan := _plan(4, 0, true, 0, 128)
_expect_plan(animated_plan, 1, 1, 1, false, "zero animated limit", failures)
var static_plan := _plan(4, 0, false, 8, -10)
_expect_plan(static_plan, 1, 1, 1, false, "negative static limit", failures)
func _verify_remaining_count_cap(failures: Array[String]) -> void:
var plan := _plan(140, 128, false, 8, 128)
_expect_plan(plan, 128, 12, 0, true, "remaining cap", failures)
func _verify_exact_group_completion(failures: Array[String]) -> void:
var plan := _plan(16, 8, true, 8, 128)
_expect_plan(plan, 8, 8, 0, true, "exact completion", failures)
func _verify_empty_and_past_end(failures: Array[String]) -> void:
var empty_plan := _plan(0, 0, false, 8, 128)
_expect_plan(empty_plan, 128, 0, 0, true, "empty group", failures)
var past_end_plan := _plan(10, 11, false, 8, 128)
_expect_plan(past_end_plan, 128, 0, 0, true, "past end", failures)
func _verify_historical_negative_offset(failures: Array[String]) -> void:
var plan := _plan(10, -2, false, 8, 4)
_expect_plan(plan, 4, 4, 2, false, "negative offset", failures)
func _verify_source_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures)
var planner_source := _read_text(PLANNER_PATH, failures)
var build_loop_source := _source_between(
loader_source,
"func _process_m2_build_jobs()",
"func _materialize_m2_group_batch(",
failures
)
_expect_equal(
build_loop_source.count("_m2_build_batch_planner.plan_batch("),
1,
"one loader planner adapter",
failures
)
_expect_true(
not build_loop_source.contains("var batch_size:"),
"loader omits batch-size formula",
failures
)
_expect_true(
not build_loop_source.contains("mini(batch_size"),
"loader omits batch-count formula",
failures
)
for forbidden_dependency in [
"WorkerThreadPool",
"RenderingServer",
"ResourceLoader",
"MultiMesh",
"Node",
"Mutex",
]:
_expect_true(
not planner_source.contains(forbidden_dependency),
"planner omits %s" % forbidden_dependency,
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var planner := PLANNER_SCRIPT.new()
var started_microseconds := Time.get_ticks_usec()
for iteration in range(20000):
var plan: Dictionary = planner.plan_batch(
512,
iteration % 512,
iteration % 3 == 0,
8,
128
)
if int(plan.get("batch_count", -1)) < 0:
failures.append("timing iteration returned negative batch count")
break
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_true(elapsed_milliseconds < 1000.0, "20000 batch plans bounded", failures)
return elapsed_milliseconds
func _plan(
transform_count: int,
current_offset: int,
has_animated_prototype: bool,
animated_batch_limit: int,
static_batch_limit: int
) -> Dictionary:
var planner := PLANNER_SCRIPT.new()
return planner.plan_batch(
transform_count,
current_offset,
has_animated_prototype,
animated_batch_limit,
static_batch_limit
)
func _expect_plan(
plan: Dictionary,
expected_batch_size: int,
expected_batch_count: int,
expected_next_offset: int,
expected_group_complete: bool,
label: String,
failures: Array[String]
) -> void:
_expect_equal(
int(plan.get("effective_batch_size", -1)),
expected_batch_size,
"%s effective batch size" % label,
failures
)
_expect_equal(
int(plan.get("batch_count", -1)),
expected_batch_count,
"%s batch count" % label,
failures
)
_expect_equal(
int(plan.get("next_offset", -1)),
expected_next_offset,
"%s next offset" % label,
failures
)
_expect_true(
bool(plan.get("group_complete", not expected_group_complete))
== expected_group_complete,
"%s group complete" % 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 _source_between(
source: String,
start_marker: String,
end_marker: String,
failures: Array[String]
) -> String:
var start_offset := source.find(start_marker)
var end_offset := source.find(end_marker, start_offset + start_marker.length())
if start_offset < 0 or end_offset <= start_offset:
failures.append("cannot isolate source between %s and %s" % [start_marker, end_marker])
return ""
return source.substr(start_offset, end_offset - start_offset)
func _expect_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)
@@ -0,0 +1 @@
uid://bw088yviu4kk3
@@ -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
+266
View File
@@ -0,0 +1,266 @@
extends SceneTree
## Asset-free contract, dependency and timing regression for WMO placements.
const RESOLVER_SCRIPT := preload("res://src/render/wmo/wmo_placement_resolver.gd")
const RESOLVER_PATH := "res://src/render/wmo/wmo_placement_resolver.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_path_normalization(failures)
_verify_positive_unique_id(failures)
_verify_synthetic_unique_keys(failures)
_verify_default_transform(failures)
_verify_position_rotation_and_scale(failures)
_verify_unclamped_scale(failures)
_verify_node_property_equivalence(failures)
_verify_source_boundaries(failures)
_verify_fresh_stateless_results(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("WMO_PLACEMENT_RESOLVER: %s" % failure)
quit(1)
return
print(
"WMO_PLACEMENT_RESOLVER PASS cases=9 iterations=20000 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_path_normalization(failures: Array[String]) -> void:
var resolver := RESOLVER_SCRIPT.new()
_expect_equal_string(
resolver.normalize_relative_path("World\\WMO\\Stormwind\\Keep.WMO"),
"world/wmo/stormwind/keep.wmo",
"slash and case normalization",
failures
)
_expect_equal_string(resolver.normalize_relative_path(""), "", "empty path", failures)
func _verify_positive_unique_id(failures: Array[String]) -> void:
var resolver := RESOLVER_SCRIPT.new()
_expect_equal_string(
resolver.resolve_unique_key({"unique_id": 11785}, "31:49", 6),
"uid:11785",
"positive unique ID",
failures
)
func _verify_synthetic_unique_keys(failures: Array[String]) -> void:
var resolver := RESOLVER_SCRIPT.new()
for placement in [{}, {"unique_id": 0}, {"unique_id": -4}]:
_expect_equal_string(
resolver.resolve_unique_key(placement, "31:49", 6),
"tile:31:49:6",
"synthetic unique key",
failures
)
func _verify_default_transform(failures: Array[String]) -> void:
var resolver := RESOLVER_SCRIPT.new()
var transform: Transform3D = resolver.resolve_world_transform({})
_expect_basis_near(transform.basis, Basis.IDENTITY, "default basis", failures)
_expect_vector_near(transform.origin, Vector3.ZERO, "default origin", failures)
func _verify_position_rotation_and_scale(failures: Array[String]) -> void:
var resolver := RESOLVER_SCRIPT.new()
var position := Vector3(10.0, 20.0, 30.0)
var rotation := Vector3(0.2, -0.4, 0.6)
var transform: Transform3D = resolver.resolve_world_transform({
"pos": position,
"rot": rotation,
"scale": 2.5,
})
_expect_basis_near(
transform.basis,
Basis.from_euler(rotation).scaled(Vector3.ONE * 2.5),
"rotated scaled basis",
failures
)
_expect_vector_near(transform.origin, position, "world origin", failures)
func _verify_unclamped_scale(failures: Array[String]) -> void:
var resolver := RESOLVER_SCRIPT.new()
var zero_transform: Transform3D = resolver.resolve_world_transform({"scale": 0.0})
_expect_basis_near(
zero_transform.basis,
Basis.IDENTITY.scaled(Vector3.ZERO),
"zero scale",
failures
)
var negative_transform: Transform3D = resolver.resolve_world_transform({"scale": -2.0})
_expect_basis_near(
negative_transform.basis,
Basis.IDENTITY.scaled(Vector3.ONE * -2.0),
"negative scale",
failures
)
func _verify_node_property_equivalence(failures: Array[String]) -> void:
var resolver := RESOLVER_SCRIPT.new()
for scale_value in [2.5, 0.0, -2.0]:
var placement := {
"pos": Vector3(10.0, 20.0, 30.0),
"rot": Vector3(0.2, -0.4, 0.6),
"scale": scale_value,
}
var historical_node := Node3D.new()
historical_node.position = placement["pos"]
historical_node.rotation = placement["rot"]
historical_node.scale = Vector3.ONE * scale_value
var historical_transform := historical_node.transform
historical_node.free()
var resolved_transform: Transform3D = resolver.resolve_world_transform(placement)
_expect_basis_near(
resolved_transform.basis,
historical_transform.basis,
"Node3D property equivalence scale %s" % scale_value,
failures
)
_expect_vector_near(
resolved_transform.origin,
historical_transform.origin,
"Node3D origin equivalence scale %s" % scale_value,
failures
)
func _verify_source_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures)
var resolver_source := _read_text(RESOLVER_PATH, failures)
_expect_true(
loader_source.contains("WMO_PLACEMENT_RESOLVER_SCRIPT.new()"),
"loader composes resolver",
failures
)
_expect_equal_int(
loader_source.count("_wmo_placement_resolver.normalize_relative_path("),
3,
"three normalization adapters",
failures
)
_expect_equal_int(
loader_source.count("_wmo_placement_resolver.resolve_unique_key("),
1,
"one identity adapter",
failures
)
_expect_equal_int(
loader_source.count("_wmo_placement_resolver.resolve_world_transform("),
3,
"three transform adapters",
failures
)
_expect_true(
not loader_source.contains("func _normalize_wmo_rel_path("),
"loader omits normalization formula",
failures
)
_expect_true(
not loader_source.contains("func _wmo_unique_key("),
"loader omits identity formula",
failures
)
for forbidden_dependency in [
"Node",
"ResourceLoader",
"RenderingServer",
"WorkerThreadPool",
"Mutex",
"FileAccess",
]:
_expect_true(
not resolver_source.contains(forbidden_dependency),
"resolver omits %s" % forbidden_dependency,
failures
)
func _verify_fresh_stateless_results(failures: Array[String]) -> void:
var resolver := RESOLVER_SCRIPT.new()
var placement := {"pos": Vector3(1.0, 2.0, 3.0), "scale": 4.0}
var first: Transform3D = resolver.resolve_world_transform(placement)
first.origin = Vector3.ZERO
var second: Transform3D = resolver.resolve_world_transform(placement)
_expect_vector_near(second.origin, Vector3(1.0, 2.0, 3.0), "stateless result", failures)
func _verify_bounded_timing(failures: Array[String]) -> float:
var resolver := RESOLVER_SCRIPT.new()
var placement := {
"unique_id": 11785,
"pos": Vector3(10.0, 20.0, 30.0),
"rot": Vector3(0.1, 0.2, 0.3),
"scale": 1.5,
}
var started_microseconds := Time.get_ticks_usec()
for iteration in range(20000):
resolver.normalize_relative_path("World\\WMO\\Stormwind\\Keep.WMO")
resolver.resolve_unique_key(placement, "31:49", iteration)
resolver.resolve_world_transform(placement)
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_true(elapsed_milliseconds < 1000.0, "20000 placement triples 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_basis_near(
actual_basis: Basis,
expected_basis: Basis,
label: String,
failures: Array[String]
) -> void:
if not actual_basis.is_equal_approx(expected_basis):
failures.append("%s basis mismatch" % label)
func _expect_vector_near(
actual_value: Vector3,
expected_value: Vector3,
label: String,
failures: Array[String]
) -> void:
if not actual_value.is_equal_approx(expected_value):
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
func _expect_equal_string(
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_equal_int(
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)
@@ -0,0 +1 @@
uid://c5xnyoqrtceex
+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
+106 -8
View File
@@ -26,15 +26,23 @@ Runtime и Editor используют facade; planner/scheduler тестиру
## Evidence
- Date: 2026-07-16
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`, `f88bf97`;
- Date: 2026-07-17
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`, `f88bf97`, `3f84717`, `d2cb52f`, `f9e5a4f`, `c06aed5`, `a878e7c`, `60603e1`, `b4955d6`, `ada0fd9`, `f470775`;
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`,
`M03-RND-TERRAIN-LOD-PLANNER-001`, `M03-RND-TERRAIN-CHUNK-QUEUE-001`,
`M03-RND-M2-UNIQUE-REGISTRY-001`,
`M03-RND-M2-PLACEMENT-TRANSFORM-001`,
`M03-RND-M2-PLACEMENT-GROUPER-001`
`M03-RND-M2-PLACEMENT-GROUPER-001`,
`M03-RND-M2-BUILD-BATCH-PLANNER-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`,
`M03-RND-ADT-WATER-LOAD-PIPELINE-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
@@ -65,6 +73,50 @@ Runtime и Editor используют facade; planner/scheduler тестиру
M2 placement grouper passed `cases=9 iterations=100 placements=256
elapsed_ms=67.104`; post-merge transform resolver, registry, internal-access
`42`, facade, `7/7` manifest, documentation and coordination gates remained green.
M2 build batch planner passed `cases=8 iterations=20000 elapsed_ms=14.532`;
post-merge grouper, transform resolver, registry, internal-access `42`, facade,
`7/7` manifest, documentation and coordination gates remained green.
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.
ADT water load pipeline state passed `cases=12 iterations=100
elapsed_ms=86.170`; its initial direct no-`.godot` run passed (`94.576ms`),
WMO caches/queue/planner/registry/resolver/material/shutdown and 18 adjacent
M2/terrain/facade/scheduler/streaming/coordinate regressions passed, and the
checkpoint dry-run retained `7/7` plans. Post-merge water state (`86.935ms`),
shutdown, renderer materials, facade, internal-access `36`, `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
@@ -91,6 +143,36 @@ Runtime и Editor используют facade; planner/scheduler тестиру
M2 worker grouping retains invalid-record filtering, case-sensitive lowercase
`.mdx/.mdl` conversion, first-seen group/per-group placement order, tile-local
origin subtraction and the `0.0001` scale clamp.
M2 build batching retains animated/static limit selection, minimum-one clamp,
remaining-transform cap and the exact offset/group-completion cursor rules.
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.
ADT water requests retain FIFO order and tile-key deduplication; tile release
removes pending requests without cancelling active work, task completion and
result publication order are unchanged, and the mutex remains limited to the
worker/main-thread mailbox. The loader still owns ADT parsing, concurrency and
profile limits, stale tile/path/root rejection, `WATER_FINALIZE` permits,
water Node construction/attachment and `water_loaded` timing.
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;
@@ -103,9 +185,20 @@ Runtime и Editor используют facade; planner/scheduler тестиру
stateful M2 unique placement registry, loader reserve/release/clear adapters and
migrated historical dedupe smoke, stateless M2 placement transform resolver and
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,
ADT water pending/task/result pipeline state and loader enqueue/start/publish/
drain/release/reset/shutdown/metrics 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
- Remaining risks: worker concurrency and stale-result validation remain
streamer-owned; cancellation stops new permits but does not interrupt
in-flight work; the environment snapshot currently covers world time but not weather
or indoor state; entity PackedScene loading is synchronous and not accepted for
network-scale hot paths; terrain cache is entry-count bounded without Mesh-byte
@@ -116,6 +209,11 @@ Runtime и Editor используют facade; planner/scheduler тестиру
private Azeroth data was absent for the asset-backed ADT placement probe; M2
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;
M03 still needs further terrain/M2/WMO/liquid service extraction
and dependency/performance acceptance.
culling-driven spatial-cell grouping; M2 resource/queue transitions and raw
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;
ADT water parsing, main-thread materialization and active worker interruption
remain loader-owned, and asset-backed MH2O/MCLQ traversal/leak/p95/p99/
original-client evidence is pending; M03 still needs further terrain/M2/WMO/
liquid service extraction and dependency/performance acceptance.