Compare commits
58 Commits
5aa40d6afb
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| dd8cfbf79c | |||
| fa85985ec5 | |||
| f68e971cce | |||
| 7b0e1eac5f | |||
| 6607100686 | |||
| 5e2afbc400 | |||
| 30945615b9 | |||
| c5ecaba0ab | |||
| e990a6503d | |||
| e2bb501695 | |||
| ef782d1f2b | |||
| 83b3618c50 | |||
| ccbb27a9fd | |||
| 82df7a555d | |||
| 5f6287905c | |||
| 8ec13d6018 | |||
| 38f831ea2f | |||
| 0544cf22df | |||
| 19ca8f48e8 | |||
| 324e64dd6e | |||
| 90b1dac56e | |||
| 874fd0f3f2 | |||
| 9cb8bdb137 | |||
| 235be3a36c | |||
| 4f8fc3b3b8 | |||
| e1b2886657 | |||
| f4707750b7 | |||
| 78adb54cfe | |||
| b7c036f14d | |||
| 7973945582 | |||
| 4387c66c0a | |||
| ada0fd9467 | |||
| ff4f320412 | |||
| c83929428d | |||
| 71fa093118 | |||
| e690756301 | |||
| b4955d6162 | |||
| b4b94b3cbb | |||
| d39f591e50 | |||
| 4314f961f6 | |||
| c9547041da | |||
| 60603e18c8 | |||
| 3411156f8f | |||
| e576ae2cad | |||
| a878e7c4e7 | |||
| ab40a76b62 | |||
| e1bb105fff | |||
| 040b635ec6 | |||
| 1e740aaa65 | |||
| c06aed568b | |||
| 01bad4ca26 | |||
| 3a6b1d306d | |||
| e420a8724f | |||
| 1e73e597f8 | |||
| f9e5a4f2e0 | |||
| 16053b46ca | |||
| c5900368d4 | |||
| 79bc4e6ffc |
@@ -27,6 +27,18 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
|
|||||||
|
|
||||||
- `src/render/world_render_facade.gd` - стабильная граница runtime/tools для focus и metrics.
|
- `src/render/world_render_facade.gd` - стабильная граница runtime/tools для focus и metrics.
|
||||||
- `src/scenes/streaming/streaming_world_loader.gd` - внутренний runtime streamer мира.
|
- `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/render/liquid/adt_water_scene_finalizer.gd` - stateless main-thread ADT water build/attach and optional persisted Editor ownership through the existing ADTBuilder.
|
||||||
|
- `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` - memoized billboard/UV-rotation decision for stale cached M2 runtime mesh refresh.
|
||||||
|
- `src/render/m2/m2_mesh_load_pipeline_state.gd` - static M2 threaded-load request records, terminal statuses and completion-order finalize FIFO without I/O or Mesh ownership.
|
||||||
|
- `src/render/m2/m2_mesh_resource_cache_state.gd` - normalized-path prepared static M2 Mesh references with final-shutdown lifetime.
|
||||||
|
- `src/render/m2/m2_mesh_resource_extractor.gd` - first-Mesh selection from direct/PackedScene/Node inputs with temporary instance cleanup.
|
||||||
- `src/scenes/streaming/eastern_kingdoms_streaming.tscn` - текущая сцена для проверки Azeroth/Eastern Kingdoms.
|
- `src/scenes/streaming/eastern_kingdoms_streaming.tscn` - текущая сцена для проверки Azeroth/Eastern Kingdoms.
|
||||||
- `addons/mpq_extractor/loaders/adt_builder.gd` - сборка ADT terrain, control splat материалов и MH2O liquids.
|
- `addons/mpq_extractor/loaders/adt_builder.gd` - сборка ADT terrain, control splat материалов и MH2O liquids.
|
||||||
- `addons/mpq_extractor/loaders/m2_builder.gd` - сборка статического M2 mesh/material.
|
- `addons/mpq_extractor/loaders/m2_builder.gd` - сборка статического M2 mesh/material.
|
||||||
@@ -394,6 +406,12 @@ Known limits:
|
|||||||
- `WMOBuilder` строит group meshes;
|
- `WMOBuilder` строит group meshes;
|
||||||
- runtime registry дедуплицирует WMO по `unique_id`;
|
- runtime registry дедуплицирует WMO по `unique_id`;
|
||||||
- WMO на границе ADT не дублируются;
|
- 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` во время движения;
|
- большие WMO не должны инстанцироваться как тяжелые `.tscn` во время движения;
|
||||||
- добавлен lightweight render-cache path для WMO groups;
|
- добавлен lightweight render-cache path для WMO groups;
|
||||||
- `wmo_render_group_ops_per_tick` ограничивает подключение WMO groups по кадрам;
|
- `wmo_render_group_ops_per_tick` ограничивает подключение WMO groups по кадрам;
|
||||||
@@ -1025,6 +1043,63 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
|||||||
- Cache formats, quality profiles, batching output and visible rules are unchanged.
|
- Cache formats, quality profiles, batching output and visible rules are unchanged.
|
||||||
Asset-backed p95/p99 and spatial-cell batching evidence remain pending.
|
Asset-backed p95/p99 and spatial-cell batching evidence remain pending.
|
||||||
|
|
||||||
|
## 2026-07-17 M2 Runtime Mesh Rebuild Classifier Extraction
|
||||||
|
|
||||||
|
- `M2RuntimeMeshRebuildClassifier` now owns the memoized decision used when a
|
||||||
|
stale cached M2 mesh may need an in-memory rebuild from raw data.
|
||||||
|
- Existing triggers are unchanged: any billboard flag/positive billboard vertex
|
||||||
|
count, or a referenced non-identity UV rotation/absolute rotation speed above
|
||||||
|
`0.000001` across at most four texture stages.
|
||||||
|
- Invalid batch variants and combo/transform indices remain skipped. The first
|
||||||
|
decision for a normalized path remains cached until the same two historical
|
||||||
|
map/shutdown clear sites.
|
||||||
|
- `StreamingWorldLoader` still loads raw M2 data, checks
|
||||||
|
`M2_MATERIAL_REFRESH_VERSION`, calls `M2Builder`, adopts/falls back to Meshes
|
||||||
|
and owns ResourceLoader/finalization budgets.
|
||||||
|
- No cache version, shader, material, geometry or visible rule changed. Synthetic
|
||||||
|
predicate timing is not asset-backed descriptor-pressure/p95/p99 evidence.
|
||||||
|
|
||||||
|
## 2026-07-17 M2 Mesh Load Pipeline State Extraction
|
||||||
|
|
||||||
|
- `M2MeshLoadPipelineState` now owns static M2 successful threaded-load request
|
||||||
|
records and the FIFO populated when ResourceLoader polling reaches loaded or
|
||||||
|
failed terminal status.
|
||||||
|
- Pending polling snapshots preserve Dictionary insertion order; terminal FIFO
|
||||||
|
order follows completion transition order. Existing `m2_mesh` metrics still
|
||||||
|
count pending plus finalize records at all three sites.
|
||||||
|
- Shutdown still drains every pending ResourceLoader path before state clear;
|
||||||
|
map reset and shutdown retain the same two clear sites.
|
||||||
|
- `StreamingWorldLoader` still selects `.tscn`/`.glb` paths, excludes animated
|
||||||
|
pivot-prefix GLBs, performs all ResourceLoader calls, consumes
|
||||||
|
`M2_MESH_FINALIZE`, extracts/prepares Meshes and updates Mesh/missing caches.
|
||||||
|
- Cache formats, material refresh, profiles and visible output are unchanged.
|
||||||
|
Synthetic state timing is not asset-backed I/O, leak or p95/p99 evidence.
|
||||||
|
|
||||||
|
## 2026-07-17 M2 Mesh Resource Cache State Extraction
|
||||||
|
|
||||||
|
- `M2MeshResourceCacheState` now owns normalized-path references to prepared
|
||||||
|
static M2 Mesh resources. Lookup returns the exact retained reference and a
|
||||||
|
later store for one path keeps the historical replacement behavior.
|
||||||
|
- The cache retains map-refresh lifetime and clears only at the existing final
|
||||||
|
runtime-cache shutdown site after asynchronous work is drained.
|
||||||
|
- `StreamingWorldLoader` still owns normalization, `.tscn`/`.glb` selection,
|
||||||
|
ResourceLoader I/O, Mesh extraction, material refresh/rebuild, shared missing
|
||||||
|
and prototype/animated caches, permits and MultiMesh materialization.
|
||||||
|
- Cache formats, refresh metadata, profiles and visible output are unchanged.
|
||||||
|
Synthetic cache timing is not asset-backed memory/leak or p95/p99 evidence.
|
||||||
|
|
||||||
|
## 2026-07-17 M2 Mesh Resource Extractor
|
||||||
|
|
||||||
|
- `M2MeshResourceExtractor` now selects the first Mesh from direct Mesh
|
||||||
|
Resources, temporary PackedScene instances and existing prototype/rebuild
|
||||||
|
Node subtrees using the prior depth-first child order.
|
||||||
|
- PackedScene extraction still instantiates exactly once and synchronously frees
|
||||||
|
the temporary root before returning the borrowed Mesh reference.
|
||||||
|
- `StreamingWorldLoader` still owns ResourceLoader I/O, raw M2 loading,
|
||||||
|
material refresh/rebuild, cache/missing adoption, permits and MultiMesh builds.
|
||||||
|
- Cache formats, traversal rules, profiles and visible output are unchanged.
|
||||||
|
Synthetic traversal timing is not asset-backed leak or p95/p99 evidence.
|
||||||
|
|
||||||
## 2026-07-17 WMO Placement Resolver Extraction
|
## 2026-07-17 WMO Placement Resolver Extraction
|
||||||
|
|
||||||
- `WmoPlacementResolver` now owns lowercase/slash cache-key normalization,
|
- `WmoPlacementResolver` now owns lowercase/slash cache-key normalization,
|
||||||
@@ -1037,6 +1112,67 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
|||||||
- Cache formats, placement values, profiles and visible rules are unchanged.
|
- Cache formats, placement values, profiles and visible rules are unchanged.
|
||||||
Asset-backed placement/p95/p99 and general WMO parity remain pending.
|
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 owns `water_loaded` timing. Main-thread Water subtree construction
|
||||||
|
and attachment are delegated separately as described below.
|
||||||
|
- 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-17 ADT Water Scene Finalizer Extraction
|
||||||
|
|
||||||
|
- `AdtWaterSceneFinalizer` now owns the two main-thread operations that turn
|
||||||
|
validated parsed ADT data into a Water subtree: the existing
|
||||||
|
`ADTBuilder.build_tile_water_scene` call and attachment to the tile root.
|
||||||
|
- Both raw tile finalization and delayed cached-tile water results use the same
|
||||||
|
service. Optional persisted Editor ownership is assigned recursively only
|
||||||
|
when the loader supplies its existing edited-scene owner.
|
||||||
|
- The service is stateless and retains no Node, Mesh, material or RID. The tile
|
||||||
|
SceneTree subtree remains the lifetime owner; dry data still causes ADTBuilder
|
||||||
|
to free its temporary empty Water root.
|
||||||
|
- Parsing, task/result state, concurrency, stale validation, `WATER_FINALIZE`
|
||||||
|
permits, `enable_water` and `water_loaded` timing remain loader-owned.
|
||||||
|
- Geometry, materials, coordinates, cache formats, profiles and visible output
|
||||||
|
are unchanged. Synthetic geometry/ownership evidence is not an MH2O/MCLQ or
|
||||||
|
original-client parity claim; asset-backed traversal/p95/p99 remains pending.
|
||||||
|
|
||||||
## 2026-07-16 World Environment Snapshot Facade
|
## 2026-07-16 World Environment Snapshot Facade
|
||||||
|
|
||||||
- `WorldEnvironmentSnapshot` carries one immutable finite time-of-day value,
|
- `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,116 @@
|
|||||||
|
# M03-RND-ADT-WATER-SCENE-FINALIZER-001 — ADT water scene finalizer
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M03-RND-ADT-WATER-SCENE-FINALIZER-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-scene-finalizer`
|
||||||
|
- Lease expires UTC: 2026-07-20
|
||||||
|
- Integrator: M03 milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Extract main-thread ADT water subtree construction, tile-root attachment and
|
||||||
|
optional recursive editor ownership from `StreamingWorldLoader` while retaining
|
||||||
|
the existing `ADTBuilder` geometry/material implementation.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Parse or reinterpret MH2O/MCLQ data.
|
||||||
|
- Own pending requests, WorkerThreadPool tasks or parsed-result delivery.
|
||||||
|
- Consume `WATER_FINALIZE` permits or validate tile/path/result staleness.
|
||||||
|
- Mark tile state `water_loaded` or choose whether water is enabled.
|
||||||
|
- Change liquid geometry, materials, coordinates, cache formats or profiles.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: `src/render/liquid/adt_water_scene_finalizer.gd`,
|
||||||
|
`src/tools/verify_adt_water_scene_finalizer.gd`,
|
||||||
|
`docs/modules/adt-water-scene-finalizer.md`, this claim
|
||||||
|
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||||
|
`src/tools/verify_adt_water_load_pipeline_state.gd`,
|
||||||
|
`docs/modules/adt-water-load-pipeline-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 resources, caches and
|
||||||
|
proprietary renderer corpus
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- Empty water data or invalid tile root produces no Node and no scene mutation.
|
||||||
|
- Existing `ADTBuilder.build_tile_water_scene` remains the only geometry/material source.
|
||||||
|
- A non-empty built Water root is attached exactly once to the supplied tile root.
|
||||||
|
- Optional editor ownership is assigned recursively only when explicitly enabled
|
||||||
|
and an edited scene root is available.
|
||||||
|
- The returned Node is borrowed and remains owned by the tile SceneTree subtree.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: accepted water pipeline package on current master `e1b2886`
|
||||||
|
- Blocks: further liquid finalization/material responsibility extraction
|
||||||
|
- External state: none; verification uses synthetic liquid dictionaries
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: dedicated empty/invalid/build/attach/editor-owner/source/timing
|
||||||
|
verifier; water pipeline, materials, shutdown, facade, internal-access,
|
||||||
|
manifest, documentation and coordination gates
|
||||||
|
- Fixtures: synthetic one-cell liquid mask/heights and temporary Node roots
|
||||||
|
- Fidelity evidence: exact existing ADTBuilder output and two attachment paths preserved
|
||||||
|
- Performance budget: 100 empty finalization attempts under 1 second
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline public API docs
|
||||||
|
- Module specification with inputs/outputs, ownership and source map
|
||||||
|
- Data-flow, lifecycle/state, main-thread sequence and dependency diagrams
|
||||||
|
- Water-pipeline, world-renderer, module registry and RENDER status updates
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important names introduced: `AdtWaterSceneFinalizer`
|
||||||
|
- Simplest considered solution: one RefCounted with one explicit attach operation
|
||||||
|
- Rejected complexity/abstractions: generic scene finalizer, signals, callbacks,
|
||||||
|
builder interface, queue ownership or alternate liquid renderer
|
||||||
|
- Unavoidable complexity and justification: editor-persisted generated subtrees
|
||||||
|
require recursive owner assignment after SceneTree attachment
|
||||||
|
- Measured optimization evidence: bounded synthetic no-water loop
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: accepted
|
||||||
|
- Done: stateless finalizer implemented; both loader water paths migrated;
|
||||||
|
synthetic scene/owner regression and required documentation added
|
||||||
|
- Next: continue liquid parsing/task execution extraction or another remaining M03 boundary
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-ADT-WATER-SCENE-FINALIZER-001:874fd0f -->
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: `235be3a`
|
||||||
|
- Results: scene finalizer PASS (`cases=8`, `iterations=100`,
|
||||||
|
`elapsed_ms=0.042`; initial `0.024ms`); synthetic liquid produced one
|
||||||
|
four-vertex/six-index surface with tile attachment and recursive owner; water
|
||||||
|
pipeline (`119.648ms`), WMO/M2/terrain/facade/scheduler/streaming/coordinate,
|
||||||
|
materials, shutdown, internal-access `36` and 32 headless contract regressions
|
||||||
|
PASS; documentation and coordination gates PASS; checkpoint dry-run PASS `7/7`.
|
||||||
|
- Remaining risks: main-thread mesh/material construction remains synchronous
|
||||||
|
behind one existing permit; the Dictionary builder boundary remains untyped;
|
||||||
|
proprietary MH2O/MCLQ traversal/leak/p95/p99/original-client evidence is absent.
|
||||||
|
A verifier launched concurrently with the initial cold editor import observed
|
||||||
|
the pre-existing global-class cache race; serialized editor parse and shutdown
|
||||||
|
rerun passed, with only expected missing private asset diagnostics.
|
||||||
|
- Documentation updated: inline public API; new
|
||||||
|
`docs/modules/adt-water-scene-finalizer.md`; water-pipeline spec, module registry,
|
||||||
|
world renderer and `RENDER.md`; data-flow/state/sequence/dependency diagrams,
|
||||||
|
ownership, main-thread effects, failure/recovery, migration and source map included.
|
||||||
|
- Integration: merged as `874fd0f`; post-merge finalizer (`cases=8`,
|
||||||
|
`iterations=100`, `elapsed_ms=0.025`), water pipeline (`98.398ms`), shutdown,
|
||||||
|
renderer materials, facade, internal-access (`private_symbols=36`), `7/7`
|
||||||
|
manifest, documentation and coordination gates passed.
|
||||||
|
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M03-RND-ADT-WATER-SCENE-FINALIZER-001:235be3a -->
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# M03-RND-M2-MESH-LOAD-PIPELINE-001 — M2 mesh load pipeline state
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M03-RND-M2-MESH-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-m2-mesh-load-pipeline`
|
||||||
|
- Lease expires UTC: 2026-07-20
|
||||||
|
- Integrator: M03 milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Extract static M2 threaded ResourceLoader request records and their terminal
|
||||||
|
finalize FIFO from `StreamingWorldLoader`, preserving request order, status
|
||||||
|
transition, metrics and reset/shutdown behavior.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Call ResourceLoader or choose `.tscn`/`.glb` cache paths.
|
||||||
|
- Own M2 Mesh, scene, missing or material-refresh caches.
|
||||||
|
- Extract Mesh from Resources or rebuild/adopt runtime Meshes.
|
||||||
|
- Consume `M2_MESH_FINALIZE` permits.
|
||||||
|
- Change cache formats, animation loading or visible M2 behavior.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: `src/render/m2/m2_mesh_load_pipeline_state.gd`,
|
||||||
|
`src/tools/verify_m2_mesh_load_pipeline_state.gd`,
|
||||||
|
`docs/modules/m2-mesh-load-pipeline-state.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 M2 resources, caches and
|
||||||
|
proprietary renderer corpus
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- One normalized path owns at most one pending request.
|
||||||
|
- Pending records preserve Dictionary insertion order for polling snapshots.
|
||||||
|
- Terminal completion removes the request and appends its record/status to FIFO.
|
||||||
|
- Finalize pop remains oldest-first; duplicate/unknown transitions are rejected.
|
||||||
|
- Total work count remains pending plus finalize records.
|
||||||
|
- Clear drops bookkeeping only; loader drains ResourceLoader requests first.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: accepted classifier package on current master `8ec13d6`
|
||||||
|
- Blocks: further M2 Mesh cache/finalization service extraction
|
||||||
|
- External state: none; verification uses synthetic paths and opaque statuses
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: dedicated validation/order/transition/FIFO/clear/source/timing
|
||||||
|
verifier; M2 classifier/builder/grouper/transform/registry, materials, shutdown,
|
||||||
|
facade, internal-access, manifest, documentation and coordination gates
|
||||||
|
- Fixtures: synthetic normalized paths, resource paths and integer terminal statuses
|
||||||
|
- Fidelity evidence: exact request/finalize transitions and metric/reset sites preserved
|
||||||
|
- Performance budget: 100 cycles over 256 request/completion/pop records under 1 second
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline public API docs
|
||||||
|
- Module specification with inputs/outputs, ownership and source map
|
||||||
|
- Data-flow, lifecycle/state, sequence and dependency diagrams
|
||||||
|
- World-renderer, module registry and RENDER status updates
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important names introduced: `M2MeshLoadPipelineState`
|
||||||
|
- Simplest considered solution: one RefCounted with request map and finalize FIFO
|
||||||
|
- Rejected complexity/abstractions: generic ResourceLoader queue, signals,
|
||||||
|
callbacks, Mesh cache ownership or animation/static pipeline merger
|
||||||
|
- Unavoidable complexity and justification: polling and budgeted finalization are
|
||||||
|
distinct lifecycle stages across frames
|
||||||
|
- Measured optimization evidence: bounded synthetic transition loop
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: accepted
|
||||||
|
- Done: state holder, loader adapters, contract verifier and required documentation
|
||||||
|
- Next: further M2 Mesh cache/finalizer extraction
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: `82df7a5`
|
||||||
|
- Results: 34 headless contract regressions pass; dedicated pipeline state
|
||||||
|
verifier passes 100-by-256 transitions in 100.273 ms; documentation and
|
||||||
|
coordination gates pass; checkpoint dry-run passes 7/7.
|
||||||
|
- Remaining risks: no private asset traversal, leak/descriptor-pressure or
|
||||||
|
original-client visual comparison; ResourceLoader I/O and Mesh cache/finalizer
|
||||||
|
ownership remain in `StreamingWorldLoader` by design.
|
||||||
|
- Documentation updated: inline API; `m2-mesh-load-pipeline-state.md` with
|
||||||
|
data-flow, state, sequence and dependency diagrams; module registry,
|
||||||
|
`world-renderer.md` and `RENDER.md`.
|
||||||
|
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-MESH-LOAD-PIPELINE-001:82df7a5 -->
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
- Merge commit: `83b3618`
|
||||||
|
- Post-merge results: pipeline state `cases=11 iterations=100
|
||||||
|
elapsed_ms=69.196`; classifier, materials, shutdown, facade, internal-access
|
||||||
|
`34`, manifest `7/7`, documentation and coordination gates passed.
|
||||||
|
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-MESH-LOAD-PIPELINE-001:83b3618 -->
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# M03-RND-M2-MESH-RESOURCE-CACHE-001 — M2 mesh resource cache state
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M03-RND-M2-MESH-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-m2-mesh-resource-cache`
|
||||||
|
- Lease expires UTC: 2026-07-20
|
||||||
|
- Integrator: M03 milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Extract the normalized-path to static M2 Mesh resource cache from
|
||||||
|
`StreamingWorldLoader`, preserving lookup, replacement, shutdown lifetime and
|
||||||
|
all existing cache consumers.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Own the shared M2 missing-path cache or animated/prototype node caches.
|
||||||
|
- Call ResourceLoader, extract Meshes from Resources or select cache paths.
|
||||||
|
- Prepare, rebuild or materialize M2 Meshes.
|
||||||
|
- Change cache formats, material refresh, animation or visible behavior.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: `src/render/m2/m2_mesh_resource_cache_state.gd`,
|
||||||
|
`src/tools/verify_m2_mesh_resource_cache_state.gd`,
|
||||||
|
`docs/modules/m2-mesh-resource-cache-state.md`, this claim
|
||||||
|
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||||
|
`src/tools/verify_m2_mesh_load_pipeline_state.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 M2 resources, caches and
|
||||||
|
proprietary renderer corpus
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- Non-empty normalized M2 paths map to at most one Mesh resource reference.
|
||||||
|
- Store replaces the previous reference for the same path.
|
||||||
|
- Lookup returns the exact retained Mesh reference; unknown/empty paths return null.
|
||||||
|
- Clear releases all cache references only at the existing final-shutdown site.
|
||||||
|
- Shared missing/prototype/animated caches remain loader-owned and unchanged.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: accepted mesh load pipeline package on master `ef782d1`
|
||||||
|
- Blocks: isolated M2 Mesh resource finalizer extraction
|
||||||
|
- External state: none; verification uses synthetic Mesh resources
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: dedicated validation/store/replace/lookup/clear/source/timing
|
||||||
|
verifier; mesh pipeline/classifier/material/shutdown/M2 build/facade/internal-
|
||||||
|
access/manifest, documentation and coordination gates
|
||||||
|
- Fixtures: synthetic ArrayMesh resources and normalized paths
|
||||||
|
- Fidelity evidence: exact cache call-site and shutdown lifetime preservation
|
||||||
|
- Performance budget: 100 cycles over 256 store/lookup entries under 1 second
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline public API docs
|
||||||
|
- Module specification with inputs/outputs, ownership and source map
|
||||||
|
- Data-flow, lifecycle/state and dependency diagrams
|
||||||
|
- World-renderer, module registry and RENDER status updates
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important name introduced: `M2MeshResourceCacheState`
|
||||||
|
- Simplest solution: one RefCounted with one typed-purpose Dictionary
|
||||||
|
- Rejected complexity: generic resource cache, missing-state merger, eviction,
|
||||||
|
signals, callbacks or materialization ownership
|
||||||
|
- Unavoidable complexity: none beyond retaining engine Resource references
|
||||||
|
- Measured optimization evidence: bounded synthetic store/lookup loop
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: accepted
|
||||||
|
- Done: cache state, loader/test adapters, contract verifier and required documentation
|
||||||
|
- Next: isolated M2 Mesh resource finalizer extraction
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: `e990a65`
|
||||||
|
- Results: dedicated cache verifier passes 100-by-256 store/lookups in
|
||||||
|
38.974 ms; 35 headless renderer/coordinate regressions pass; checkpoint
|
||||||
|
dry-run passes 7/7; documentation and coordination gates pass.
|
||||||
|
- Remaining risks: no private M2 traversal, Mesh memory/leak/descriptor-pressure
|
||||||
|
run or original-client visual comparison; extraction/preparation, shared
|
||||||
|
missing state and materialization remain loader-owned by design.
|
||||||
|
- Documentation updated: inline API; `m2-mesh-resource-cache-state.md` with
|
||||||
|
data-flow, lifecycle, sequence and dependency diagrams; pipeline spec, module
|
||||||
|
registry, `world-renderer.md` and `RENDER.md`.
|
||||||
|
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-MESH-RESOURCE-CACHE-001:e990a65 -->
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
- Merge commit: `3094561`
|
||||||
|
- Post-merge results: cache state `cases=9 iterations=100
|
||||||
|
elapsed_ms=38.686`; pipeline, classifier, materials, shutdown, facade,
|
||||||
|
internal-access `34`, manifest `7/7`, documentation and coordination gates passed.
|
||||||
|
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-MESH-RESOURCE-CACHE-001:3094561 -->
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001 — M2 mesh resource extractor
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M03-RND-M2-MESH-RESOURCE-EXTRACTOR-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-mesh-resource-extractor`
|
||||||
|
- Lease expires UTC: 2026-07-20
|
||||||
|
- Integrator: M03 milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Extract first-Mesh selection from loaded M2 Resources and Node subtrees,
|
||||||
|
including temporary PackedScene instance cleanup, from `StreamingWorldLoader`.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Call ResourceLoader or select/request cache paths.
|
||||||
|
- Refresh/rebuild materials or load raw M2 data.
|
||||||
|
- Own the Mesh cache, missing state, prototypes or MultiMesh materialization.
|
||||||
|
- Change traversal order, cache formats, profiles or visible behavior.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: `src/render/m2/m2_mesh_resource_extractor.gd`,
|
||||||
|
`src/tools/verify_m2_mesh_resource_extractor.gd`,
|
||||||
|
`docs/modules/m2-mesh-resource-extractor.md`, this claim
|
||||||
|
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||||
|
M2 pipeline/cache/material/shutdown verifiers, `docs/modules/world-renderer.md`,
|
||||||
|
`docs/modules/README.md`, M2 module specs, `RENDER.md`,
|
||||||
|
`targets/03-renderer-facade.md`
|
||||||
|
- Generated/ignored: `.godot`, native DLL, generated M2 resources, caches and
|
||||||
|
proprietary renderer corpus
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- A direct Mesh Resource returns the same Mesh reference.
|
||||||
|
- A PackedScene is instantiated once, searched depth-first in child order and
|
||||||
|
its temporary root is freed before return.
|
||||||
|
- A Node subtree search returns the first MeshInstance3D Mesh in historical
|
||||||
|
depth-first preorder without changing the tree.
|
||||||
|
- Null, unsupported Resources and subtrees without a Mesh return null.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: accepted Mesh resource cache package on master `5e2afbc`
|
||||||
|
- Blocks: isolated material refresh/rebuild finalizer extraction
|
||||||
|
- External state: none; verification uses synthetic Mesh/Node/PackedScene fixtures
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: dedicated direct/PackedScene/subtree/order/lifetime/source/timing
|
||||||
|
verifier; M2 pipeline/cache/classifier/material/shutdown/build/facade/internal-
|
||||||
|
access/manifest, documentation and coordination gates
|
||||||
|
- Fixtures: ArrayMesh, MeshInstance3D subtrees and synthetic PackedScene
|
||||||
|
- Fidelity evidence: exact traversal order and temporary-node destruction
|
||||||
|
- Performance budget: 10,000 synthetic subtree lookups under 1 second
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline public API docs
|
||||||
|
- Module specification with inputs/outputs, ownership and source map
|
||||||
|
- Data-flow, lifecycle/state, sequence and dependency diagrams
|
||||||
|
- M2 cache/pipeline, world-renderer, registry and RENDER updates
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important name introduced: `M2MeshResourceExtractor`
|
||||||
|
- Simplest solution: one stateless RefCounted with two explicit extraction APIs
|
||||||
|
- Rejected complexity: callback-driven finalizer, generic scene utility,
|
||||||
|
cache ownership, signals or visitor framework
|
||||||
|
- Unavoidable complexity: temporary PackedScene instance must be freed while
|
||||||
|
its referenced Mesh remains borrowed
|
||||||
|
- Measured optimization evidence: bounded synthetic depth-first traversal
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: accepted
|
||||||
|
- Done: extractor, three loader/test adapters, contract verifier and required documentation
|
||||||
|
- Next: isolated M2 Mesh material preparation finalizer extraction
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: `7b0e1ea`
|
||||||
|
- Results: extractor passes direct/PackedScene/subtree/order/lifetime contracts
|
||||||
|
and 10,000 traversals in 39.243 ms; 36 headless renderer/coordinate
|
||||||
|
regressions pass; checkpoint dry-run passes 7/7; documentation and
|
||||||
|
coordination gates pass.
|
||||||
|
- Remaining risks: synchronous PackedScene instantiation remains behind the
|
||||||
|
existing permit; no private M2 corrupt-scene/traversal/leak/p95/p99 or
|
||||||
|
original-client comparison; material preparation remains loader-owned.
|
||||||
|
- Documentation updated: inline API; `m2-mesh-resource-extractor.md` with
|
||||||
|
data-flow, lifecycle, sequence and dependency diagrams; cache/pipeline specs,
|
||||||
|
module registry, `world-renderer.md` and `RENDER.md`.
|
||||||
|
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001:7b0e1ea -->
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
- Merge commit: `fa85985`
|
||||||
|
- Post-merge results: extractor `cases=10 iterations=10000
|
||||||
|
elapsed_ms=38.172`; cache, pipeline, classifier, materials, shutdown, facade,
|
||||||
|
internal-access `34`, manifest `7/7`, documentation and coordination gates passed.
|
||||||
|
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001:fa85985 -->
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001 — M2 runtime rebuild classifier
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-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-runtime-rebuild-classifier`
|
||||||
|
- Lease expires UTC: 2026-07-20
|
||||||
|
- Integrator: M03 milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Extract the memoized decision that stale cached M2 meshes require an in-memory
|
||||||
|
runtime rebuild because raw batches contain billboard vertices or referenced UV
|
||||||
|
rotation transforms.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Load raw M2 files or cache Resources.
|
||||||
|
- Build, replace or mutate Meshes, materials, Nodes or RIDs.
|
||||||
|
- Change `M2_MATERIAL_REFRESH_VERSION` or cache formats.
|
||||||
|
- Interpret animation, particles, ribbons or general M2 shader fidelity.
|
||||||
|
- Change path normalization, ResourceLoader transitions or finalize budgets.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd`,
|
||||||
|
`src/tools/verify_m2_runtime_mesh_rebuild_classifier.gd`,
|
||||||
|
`docs/modules/m2-runtime-mesh-rebuild-classifier.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/M2 resources, caches
|
||||||
|
and proprietary renderer corpus
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- Billboard metadata requires rebuild when `has_billboard` is true or
|
||||||
|
`billboard_vertex_count` is positive in any Dictionary batch.
|
||||||
|
- UV rotation requires non-empty transforms and combos plus a referenced
|
||||||
|
non-identity `Vector4` rotation or absolute rotation speed above `0.000001`.
|
||||||
|
- Only up to four texture stages per batch are inspected; invalid variants and
|
||||||
|
out-of-range combo/transform indices are skipped.
|
||||||
|
- The first decision for a normalized path is memoized until clear, matching the
|
||||||
|
existing loader cache lifetime.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: current accepted master `90b1dac`
|
||||||
|
- Blocks: further M2 mesh request/cache/finalization extraction
|
||||||
|
- External state: none; verification uses synthetic raw M2 Dictionaries
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: dedicated billboard/UV/index/stage/cache/clear/source/timing verifier;
|
||||||
|
M2 builder/grouper/transform/registry, materials, shutdown, facade,
|
||||||
|
internal-access, manifest, documentation and coordination gates
|
||||||
|
- Fixtures: synthetic batches, transform combos and rotation dictionaries
|
||||||
|
- Fidelity evidence: exact existing predicate thresholds, stage cap and cache lifetime preserved
|
||||||
|
- Performance budget: 100 classifications over 256 paths under 1 second
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline public API docs
|
||||||
|
- Module specification with inputs/outputs, ownership and source map
|
||||||
|
- Data-flow, lifecycle/state, sequence and dependency diagrams
|
||||||
|
- World-renderer, module registry and RENDER status updates
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important names introduced: `M2RuntimeMeshRebuildClassifier`
|
||||||
|
- Simplest considered solution: one RefCounted with one memoized query and clear
|
||||||
|
- Rejected complexity/abstractions: generic predicate cache, strategy interface,
|
||||||
|
ResourceLoader wrapper, signals or typed rewrite of raw parser Dictionaries
|
||||||
|
- Unavoidable complexity and justification: four-stage combo indirection is the
|
||||||
|
existing build-12340 M2 material boundary
|
||||||
|
- Measured optimization evidence: bounded synthetic classification loop
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: accepted
|
||||||
|
- Done: classifier/cache implemented; loader query and both clear sites migrated;
|
||||||
|
synthetic predicate/cache verifier and required documentation added
|
||||||
|
- Next: continue M2 mesh request/cache/finalization extraction
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001:38f831e -->
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: `19ca8f4`
|
||||||
|
- Results: classifier PASS (`cases=12`, `iterations=100`,
|
||||||
|
`elapsed_ms=64.585`; initial `35.461ms`); billboard flag/count, invalid
|
||||||
|
variants/indices, UV rotation/speed/epsilon, four-stage cap, memoization,
|
||||||
|
detached diagnostics and clear contracts PASS; 33 M2/WMO/liquid/terrain/
|
||||||
|
facade/scheduler/streaming/coordinate/material/shutdown regressions PASS;
|
||||||
|
internal-access decreased to `35`; documentation and coordination gates PASS;
|
||||||
|
checkpoint dry-run PASS `7/7`.
|
||||||
|
- Remaining risks: raw parser data remains Dictionary-based; the first decision
|
||||||
|
for a path remains authoritative until clear; classification covers the current
|
||||||
|
four-stage material implementation only; private asset descriptor-pressure,
|
||||||
|
traversal, p95/p99 and paired original-client evidence remain absent.
|
||||||
|
- Documentation updated: inline public API; new
|
||||||
|
`docs/modules/m2-runtime-mesh-rebuild-classifier.md`; module registry, world
|
||||||
|
renderer and `RENDER.md`; data-flow/state/sequence/dependency diagrams,
|
||||||
|
ownership, failure/recovery, thresholds, cache lifetime and source map included.
|
||||||
|
- Integration: merged as `38f831e`; post-merge classifier (`cases=12`,
|
||||||
|
`iterations=100`, `elapsed_ms=45.439`), materials, shutdown, M2 batch planner,
|
||||||
|
facade, internal-access (`private_symbols=35`), `7/7` manifest, documentation
|
||||||
|
and coordination gates passed.
|
||||||
|
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001:19ca8f4 -->
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# M03-RND-WMO-PLACEMENT-REGISTRY-001 — WMO placement registry
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M03-RND-WMO-PLACEMENT-REGISTRY-001:sindo-main-codex:2026-07-20 -->
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Target: M03
|
||||||
|
- Program: RND
|
||||||
|
- Owner/Agent ID: sindo-main-codex
|
||||||
|
- Branch: `work/sindo-main-codex/m03-wmo-placement-registry`
|
||||||
|
- Lease expires UTC: 2026-07-20
|
||||||
|
- Integrator: M03 milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Extract WMO placement-key to tile-reference ownership from the loader while
|
||||||
|
leaving every Node and renderer-resource lifecycle in the loader.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Change placement identity rules or WMO transforms.
|
||||||
|
- Move Node maps, world roots, tile state, jobs, queues or cancellation.
|
||||||
|
- Move cache/load requests, material refresh or render-group building.
|
||||||
|
- Change duplicate ordering, global-WMO lifetime or build budgets.
|
||||||
|
- Claim asset-backed WMO fidelity or performance acceptance.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: `src/render/wmo/wmo_placement_registry.gd`,
|
||||||
|
`src/tools/verify_wmo_placement_registry.gd`,
|
||||||
|
`docs/modules/wmo-placement-registry.md`, this claim
|
||||||
|
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||||
|
`docs/modules/wmo-placement-resolver.md`, `docs/modules/world-renderer.md`,
|
||||||
|
`docs/modules/README.md`, `RENDER.md`, `targets/03-renderer-facade.md`
|
||||||
|
- Generated/ignored: `.godot`, native DLL, generated ADT/WMO resources, caches
|
||||||
|
and proprietary renderer corpus
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- Input: resolved WMO placement key and tile/global reference key
|
||||||
|
- `add_reference` creates a key/ref set or idempotently retains an existing ref
|
||||||
|
- `release_reference` erases only the supplied ref and returns true only when the
|
||||||
|
final ref removes the placement key
|
||||||
|
- `contains`, `active_count`, detached sorted diagnostics and idempotent `clear`
|
||||||
|
- Loader retains key→Node mapping and reacts to final release by cancelling the
|
||||||
|
render build and queue-freeing the corresponding Node
|
||||||
|
- Cache/schema/coordinate/profile versions: unchanged
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: current master `5aa40d6`, `WmoPlacementResolver` identity contract
|
||||||
|
- Blocks: WMO build/resource state decomposition
|
||||||
|
- External state: none; tests use synthetic placement/tile keys
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: dedicated add/shared/idempotent/non-owner/final/global/clear/
|
||||||
|
diagnostics/source/timing verifier; resolver/material/shutdown plus M2/terrain/
|
||||||
|
facade/internal-access/manifest/scheduler/streaming/coordinate/docs/coord gates
|
||||||
|
- Fidelity evidence: exact shared-ref and final-release behavior preserved
|
||||||
|
- Performance budget: dictionary membership over active WMO keys/refs; no Node,
|
||||||
|
Resource, RID, task, queue, mutex, cache or file access
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline registry API documentation
|
||||||
|
- New module spec with inputs/outputs, data-flow, lifecycle/state, ownership,
|
||||||
|
failure/recovery, performance, fidelity limitations and source map
|
||||||
|
- Placement resolver, renderer/module registry and `RENDER.md` updates
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important name: `WmoPlacementRegistry`
|
||||||
|
- Simplest solution: one private key-to-reference-set Dictionary
|
||||||
|
- Rejected complexity: generic ref-count registry, signals, callbacks, Resources,
|
||||||
|
weak refs or Node ownership inside the service
|
||||||
|
- Unavoidable complexity: loader keeps a parallel key-to-Node map
|
||||||
|
- Measured optimization evidence: bounded synthetic add/release loop
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: accepted
|
||||||
|
- Done: registry extracted; global/tile adapters migrated; Node/resource/job
|
||||||
|
ownership retained in loader; contract, regressions, dry-run and docs passed
|
||||||
|
- Next: WMO build/resource state extraction and asset-backed placement evidence
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-PLACEMENT-REGISTRY-001:c590036 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-PLACEMENT-REGISTRY-001:f9e5a4f -->
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: `c590036`
|
||||||
|
- Results: `verify_wmo_placement_registry.gd` PASS cases=10,
|
||||||
|
iterations=100, elapsed_ms=53.941; 21 adjacent WMO/M2/terrain/facade/
|
||||||
|
scheduler/streaming/coordinate regressions PASS; documentation and
|
||||||
|
coordination gates PASS; render dry-run PASS 7/7.
|
||||||
|
- Remaining risks: no proprietary asset corpus or original-client paired WMO
|
||||||
|
capture; dry-run reports expected missing ADT/DBC/character assets; WMO
|
||||||
|
build/resource state remains loader-owned.
|
||||||
|
- Documentation updated: `docs/modules/wmo-placement-registry.md`,
|
||||||
|
`docs/modules/wmo-placement-resolver.md`, `docs/modules/world-renderer.md`,
|
||||||
|
`docs/modules/README.md`, `RENDER.md`; inline public API comments added.
|
||||||
|
- Integration: accepted in master merge `f9e5a4f`; post-merge registry
|
||||||
|
(`cases=10`, `iterations=100`, `elapsed_ms=54.178`), resolver, runtime-cache
|
||||||
|
shutdown, facade, documentation and coordination gates passed.
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# M03-RND-WMO-RENDER-BUILD-PLANNER-001 — WMO render build step planner
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M03-RND-WMO-RENDER-BUILD-PLANNER-001:sindo-main-codex:2026-07-20 -->
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Target: M03
|
||||||
|
- Program: RND
|
||||||
|
- Owner/Agent ID: sindo-main-codex
|
||||||
|
- Branch: `work/sindo-main-codex/m03-wmo-render-build-planner`
|
||||||
|
- Lease expires UTC: 2026-07-20
|
||||||
|
- Integrator: M03 milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Extract the pure one-operation WMO render build cursor decision from the loader
|
||||||
|
while preserving mesh-before-MultiMesh ordering and one budget permit per group.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Move Nodes, Resources, materials, jobs, queues, cancellation or permits.
|
||||||
|
- Change WMO placement, cache loading, transforms, visibility or shadows.
|
||||||
|
- Change build ordering, group names, editor ownership or finalization thread.
|
||||||
|
- Change cache formats, settings, presets or renderer diagnostics.
|
||||||
|
- Claim asset-backed WMO fidelity or p95/p99 acceptance.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: `src/render/wmo/wmo_render_build_step_planner.gd`,
|
||||||
|
`src/tools/verify_wmo_render_build_step_planner.gd`,
|
||||||
|
`docs/modules/wmo-render-build-step-planner.md`, this claim
|
||||||
|
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||||
|
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
|
||||||
|
`targets/03-renderer-facade.md`
|
||||||
|
- Generated/ignored: `.godot`, native DLL, generated ADT/WMO resources, caches
|
||||||
|
and proprietary renderer corpus
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- Input: mesh count/index and MultiMesh count/index
|
||||||
|
- Output: `mesh`, `multimesh` or `complete` operation, selected index and next
|
||||||
|
cursor values
|
||||||
|
- Mesh is selected while its cursor is below count; MultiMesh is selected only
|
||||||
|
after mesh exhaustion; complete follows both exhausted ranges
|
||||||
|
- Loader retains cursor adoption, Resource array access, Node creation, material
|
||||||
|
refresh, ownership, queue cancellation and budget permit consumption
|
||||||
|
- Cache/schema/coordinate/profile versions: unchanged
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: current master `1e73e59`, accepted WMO placement registry/resolver
|
||||||
|
- Blocks: further WMO render build state decomposition
|
||||||
|
- External state: none; verification uses scalar synthetic fixtures
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: dedicated ordering/cursor/empty/raw-edge/source/timing verifier;
|
||||||
|
WMO registry/resolver/material/shutdown plus M2/terrain/facade/internal-access/
|
||||||
|
manifest/scheduler/streaming/coordinate/docs/coord gates
|
||||||
|
- Fidelity evidence: exact mesh-first, one-step cursor behavior preserved
|
||||||
|
- Performance budget: 20,000 pure plans under 1 second; no Node, Resource, RID,
|
||||||
|
task, queue, mutex, cache or file access
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline public planner API documentation
|
||||||
|
- New module spec with inputs/outputs, data flow, sequence, ownership,
|
||||||
|
failure/recovery, performance/fidelity limits and source map
|
||||||
|
- Renderer module registry, world renderer and `RENDER.md` updates
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important name: `WmoRenderBuildStepPlanner`
|
||||||
|
- Simplest solution: one stateless function and three operation constants
|
||||||
|
- Rejected complexity: job class, iterator, generic cursor framework, callbacks,
|
||||||
|
signals or ownership of engine objects
|
||||||
|
- Unavoidable complexity: loader still has separate mesh/MultiMesh materializers
|
||||||
|
- Measured optimization evidence: bounded synthetic planning loop
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: accepted
|
||||||
|
- Done: planner and loader adapter implemented; Node/Resource/material/job/queue/
|
||||||
|
permit ownership retained in loader; regressions, dry-run and docs passed
|
||||||
|
- Next: WMO build job/resource ownership or liquid service extraction
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-RENDER-BUILD-PLANNER-001:c06aed5 -->
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: `3a6b1d3`
|
||||||
|
- Results: dedicated planner PASS (`cases=9`, `iterations=20000`,
|
||||||
|
`elapsed_ms=11.914`); WMO registry/resolver/material/shutdown and 18 adjacent
|
||||||
|
M2/terrain/facade/scheduler/streaming/coordinate regressions PASS;
|
||||||
|
documentation and coordination gates PASS; checkpoint dry-run PASS `7/7`.
|
||||||
|
- Remaining risks: no proprietary WMO corpus, original-client paired capture or
|
||||||
|
traversal p95/p99; dry-run reports expected missing ADT/DBC/character assets;
|
||||||
|
WMO Resource arrays and untyped build jobs/queues remain loader-owned.
|
||||||
|
- Documentation updated: inline planner API;
|
||||||
|
`docs/modules/wmo-render-build-step-planner.md`, module registry,
|
||||||
|
`docs/modules/world-renderer.md`, `RENDER.md` with data-flow and sequence
|
||||||
|
diagrams, ownership, failure/recovery and source map.
|
||||||
|
- Integration: accepted in master merge `c06aed5`; post-merge planner
|
||||||
|
(`cases=9`, `iterations=20000`, `elapsed_ms=11.059`), WMO registry/resolver,
|
||||||
|
runtime-cache shutdown, facade, documentation and coordination gates passed.
|
||||||
|
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-RENDER-BUILD-PLANNER-001:3a6b1d3 -->
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# M03-RND-WMO-RENDER-BUILD-QUEUE-001 — WMO render build queue
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M03-RND-WMO-RENDER-BUILD-QUEUE-001:sindo-main-codex:2026-07-20 -->
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Target: M03
|
||||||
|
- Program: RND
|
||||||
|
- Owner/Agent ID: sindo-main-codex
|
||||||
|
- Branch: `work/sindo-main-codex/m03-wmo-render-build-queue`
|
||||||
|
- Lease expires UTC: 2026-07-20
|
||||||
|
- Integrator: M03 milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Extract typed lightweight-WMO render build jobs and their FIFO key queue from
|
||||||
|
the loader while leaving all engine-object destruction and materialization in
|
||||||
|
the loader.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Queue-free Nodes or free Resources/RIDs.
|
||||||
|
- Move material refresh, Node creation, editor ownership or budget permits.
|
||||||
|
- Change duplicate enqueue, stale-key cleanup, cancel or clear ordering.
|
||||||
|
- Change placement, cache loading/versioning, transforms, shadows or visibility.
|
||||||
|
- Claim asset-backed WMO fidelity, leak acceptance or traversal p95/p99.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: `src/render/wmo/wmo_render_build_job.gd`,
|
||||||
|
`src/render/wmo/wmo_render_build_queue.gd`,
|
||||||
|
`src/tools/verify_wmo_render_build_queue.gd`,
|
||||||
|
`docs/modules/wmo-render-build-queue.md`, this claim
|
||||||
|
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||||
|
`docs/modules/wmo-render-build-step-planner.md`,
|
||||||
|
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
|
||||||
|
`targets/03-renderer-facade.md`
|
||||||
|
- Generated/ignored: `.godot`, native DLL, generated ADT/WMO resources, caches
|
||||||
|
and proprietary renderer corpus
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- Typed job: placement key, strong Node3D/Resource references and two cursors
|
||||||
|
- FIFO queue preserves duplicate key append and first-occurrence erase behavior
|
||||||
|
- Stale front keys remain observable for loader-compatible pop-and-continue
|
||||||
|
- Cancel releases the keyed job reference and first queued key only
|
||||||
|
- Clear releases every job/key reference idempotently; never frees engine objects
|
||||||
|
- Loader retains validity checks, planner invocation, cursor adoption,
|
||||||
|
materialization, permits, cancellation reactions and Node destruction
|
||||||
|
- Cache/schema/coordinate/profile versions: unchanged
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: current master `1e740aa`, accepted WMO build step planner
|
||||||
|
- Blocks: further WMO cache/load and materialization decomposition
|
||||||
|
- External state: none; verification uses synthetic Node3D/Resource instances
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: dedicated invalid/FIFO/duplicate/stale/cursor/cancel/clear/
|
||||||
|
diagnostics/source/timing verifier; WMO planner/registry/resolver/material/
|
||||||
|
shutdown plus M2/terrain/facade/internal-access/manifest/scheduler/streaming/
|
||||||
|
coordinate/docs/coord gates
|
||||||
|
- Fidelity evidence: exact current queue/dictionary behavior preserved
|
||||||
|
- Performance budget: 100 cycles over 256 jobs under 1 second
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline job/queue API documentation
|
||||||
|
- New module spec with inputs/outputs, data flow, state/sequence, ownership,
|
||||||
|
threading, failure/recovery, performance/fidelity limits and source map
|
||||||
|
- Build-step planner, renderer module registry, world renderer and RENDER updates
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important names: `WmoRenderBuildJob`, `WmoRenderBuildQueue`
|
||||||
|
- Simplest solution: one typed record plus Dictionary/Array queue composition
|
||||||
|
- Rejected complexity: generic job scheduler, signals, callbacks, mutexes,
|
||||||
|
Resources as job records or engine lifetime callbacks
|
||||||
|
- Unavoidable complexity: duplicate keys can intentionally leave stale entries
|
||||||
|
- Measured optimization evidence: bounded synthetic enqueue/cancel/clear loop
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: accepted
|
||||||
|
- Done: typed job/queue implemented; three clear sites, metrics, enqueue, drain and
|
||||||
|
cancel migrated; engine destruction/materialization retained in loader
|
||||||
|
- Next: WMO cache/load state or liquid service extraction
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-RENDER-BUILD-QUEUE-001:60603e1 -->
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: `e576ae2` (implementation `e1bb105` plus cold-load fix)
|
||||||
|
- Results: queue PASS (`cases=11`, `iterations=100`, `elapsed_ms=59.874`);
|
||||||
|
WMO planner/registry/resolver/material/shutdown and 18 adjacent M2/terrain/
|
||||||
|
facade/scheduler/streaming/coordinate regressions PASS; documentation and
|
||||||
|
coordination gates PASS; checkpoint dry-run PASS `7/7`.
|
||||||
|
- Cold-load recovery: post-merge smoke exposed an editor class-cache dependency;
|
||||||
|
typed queue accessors removed the loader/global-class dependency and a fresh
|
||||||
|
run without `.godot` passed (`cases=11`, `elapsed_ms=57.835`).
|
||||||
|
- Remaining risks: strong Node/Resource references require every loader cancel/
|
||||||
|
clear path; duplicate enqueue intentionally preserves stale FIFO keys; no
|
||||||
|
proprietary WMO corpus, original-client paired capture, leak run or traversal
|
||||||
|
p95/p99; dry-run reports expected missing ADT/DBC/character assets.
|
||||||
|
- Documentation updated: inline job/queue API;
|
||||||
|
`docs/modules/wmo-render-build-queue.md`, build-step planner spec, module
|
||||||
|
registry, world renderer and `RENDER.md`; data-flow/state/sequence diagrams,
|
||||||
|
ownership, threading, failure/recovery and source map included.
|
||||||
|
- Integration: implementation merged as `a878e7c`; its first post-merge smoke
|
||||||
|
exposed and rejected a cold global-class registration dependency. Fix merged
|
||||||
|
as `60603e1`; post-fix master queue (`cases=11`, `iterations=100`,
|
||||||
|
`elapsed_ms=56.454`), planner, registry, resolver, shutdown, facade,
|
||||||
|
documentation and coordination gates passed without parse diagnostics.
|
||||||
|
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-RENDER-BUILD-QUEUE-001:e576ae2 -->
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# M03-RND-WMO-RENDER-RESOURCE-CACHE-001 — WMO render resource cache state
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M03-RND-WMO-RENDER-RESOURCE-CACHE-001:sindo-main-codex:2026-07-20 -->
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Target: M03
|
||||||
|
- Program: RND
|
||||||
|
- Owner/Agent ID: sindo-main-codex
|
||||||
|
- Branch: `work/sindo-main-codex/m03-wmo-render-resource-cache`
|
||||||
|
- Lease expires UTC: 2026-07-20
|
||||||
|
- Integrator: M03 milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Extract lightweight-WMO render Resource cache, negative cache and asynchronous
|
||||||
|
request bookkeeping from the loader while preserving loader-owned I/O and
|
||||||
|
cache-format validation.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Call `ResourceLoader`, construct cache paths or inspect files.
|
||||||
|
- Validate `WMOStreamingResource` script identity or format version.
|
||||||
|
- Own cached WMO scenes, live prototypes, placement/build jobs or Nodes/RIDs.
|
||||||
|
- Change fallback order, retry behavior, quality profiles or visual output.
|
||||||
|
- Claim asset-backed WMO fidelity, leak acceptance or traversal p95/p99.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: `src/render/wmo/wmo_render_resource_cache_state.gd`,
|
||||||
|
`src/tools/verify_wmo_render_resource_cache_state.gd`,
|
||||||
|
`docs/modules/wmo-render-resource-cache-state.md`, this claim
|
||||||
|
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||||
|
`src/tools/verify_render_runtime_cache_shutdown.gd`,
|
||||||
|
`docs/modules/wmo-render-build-queue.md`, `docs/modules/world-renderer.md`,
|
||||||
|
`docs/modules/README.md`, `RENDER.md`, `targets/03-renderer-facade.md`
|
||||||
|
- Generated/ignored: `.godot`, native DLL, generated ADT/WMO resources, caches
|
||||||
|
and proprietary renderer corpus
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- State keys are loader-normalized relative WMO paths copied as `String`.
|
||||||
|
- Cached values are already validated `Resource` references.
|
||||||
|
- Pending values are cache paths for loader-owned `ResourceLoader` requests.
|
||||||
|
- Terminal request removal precedes validated-resource or missing-state adoption.
|
||||||
|
- Request/missing reset preserves accepted resources; full clear releases all.
|
||||||
|
- Cache/schema/coordinate/profile versions and validation rules remain unchanged.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: current master `c954704`, accepted WMO render build queue
|
||||||
|
- Blocks: further WMO cached-scene/load and materialization decomposition
|
||||||
|
- External state: none; verification uses synthetic Resources and paths
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: dedicated lifecycle/request/cache/reset/diagnostics/source/timing
|
||||||
|
verifier; WMO queue/planner/registry/resolver/material/shutdown plus adjacent
|
||||||
|
renderer/streaming/coordinate/documentation/coordination gates
|
||||||
|
- Fixtures: synthetic Resource instances and normalized relative/cache paths
|
||||||
|
- Fidelity evidence: exact current cache/missing/request transitions preserved
|
||||||
|
- Performance budget: 100 cycles over 256 resource/request keys under 1 second
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline public API docs
|
||||||
|
- Module specification with inputs/outputs, ownership and source map
|
||||||
|
- Data-flow, lifecycle/state and loader interaction sequence diagrams
|
||||||
|
- World-renderer, module registry, WMO queue and RENDER status updates
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important names introduced: `WmoRenderResourceCacheState`
|
||||||
|
- Simplest considered solution: one RefCounted around three Dictionaries
|
||||||
|
- Rejected complexity/abstractions: generic cache, callbacks, signals, mutexes,
|
||||||
|
ResourceLoader wrapper, persistence layer or shared WMO/M2 cache hierarchy
|
||||||
|
- Unavoidable complexity and justification: partial reset must retain validated
|
||||||
|
Resources while discarding pending/negative state at map reset and shutdown
|
||||||
|
- Measured optimization evidence: bounded synthetic store/request/reset loop
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: accepted
|
||||||
|
- Done: state holder implemented; loader cache/request/reset/metrics adapters
|
||||||
|
migrated; shutdown regression and required documentation updated
|
||||||
|
- Next: cached WMO scene/load state or liquid service extraction
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-RENDER-RESOURCE-CACHE-001:b4955d6 -->
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: `d39f591`
|
||||||
|
- Results: cache-state PASS (`cases=10`, `iterations=100`,
|
||||||
|
`elapsed_ms=58.727`); direct no-`.godot` cache-state run PASS (`45.361ms`);
|
||||||
|
WMO queue/planner/registry/resolver/material/shutdown plus 18 adjacent
|
||||||
|
M2/terrain/facade/scheduler/streaming/coordinate regressions PASS;
|
||||||
|
documentation and coordination gates PASS; checkpoint dry-run PASS `7/7`.
|
||||||
|
- Remaining risks: accepted Resources remain strongly referenced until full
|
||||||
|
shutdown release; cached-scene and ResourceLoader I/O seams remain in loader;
|
||||||
|
no proprietary corrupt-cache fixture, asset-backed WMO traversal/leak run,
|
||||||
|
p95/p99 or original-client paired evidence. Full loader cannot be used as a
|
||||||
|
direct empty-class-cache smoke because pre-existing typed global coordinate/
|
||||||
|
focus dependencies require editor class registration; the extracted service
|
||||||
|
itself passed cold and normal editor parse passed.
|
||||||
|
- Documentation updated: inline public API; new
|
||||||
|
`docs/modules/wmo-render-resource-cache-state.md`; module registry, WMO queue,
|
||||||
|
world renderer and `RENDER.md`; data-flow/state/sequence diagrams, ownership,
|
||||||
|
threading, cache-version boundary, failure/recovery and source map included.
|
||||||
|
- Integration: merged as `b4955d6`; post-merge cache state (`cases=10`,
|
||||||
|
`iterations=100`, `elapsed_ms=51.949`), shutdown, WMO queue/resolver, facade,
|
||||||
|
internal-access (`private_symbols=41`), `7/7` manifest, documentation and
|
||||||
|
coordination gates passed.
|
||||||
|
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-RENDER-RESOURCE-CACHE-001:d39f591 -->
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# M03-RND-WMO-SCENE-RESOURCE-CACHE-001 — WMO cached-scene resource state
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M03-RND-WMO-SCENE-RESOURCE-CACHE-001:sindo-main-codex:2026-07-20 -->
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Target: M03
|
||||||
|
- Program: RND
|
||||||
|
- Owner/Agent ID: sindo-main-codex
|
||||||
|
- Branch: `work/sindo-main-codex/m03-wmo-scene-resource-cache`
|
||||||
|
- Lease expires UTC: 2026-07-20
|
||||||
|
- Integrator: M03 milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Extract cached-WMO `PackedScene`, negative-cache and asynchronous request state
|
||||||
|
from the loader while preserving loader-owned file limits, I/O and scene-cache
|
||||||
|
version validation.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Call `ResourceLoader`/`FileAccess`, construct `.tscn` paths or measure files.
|
||||||
|
- Instantiate scenes or validate WMO builder cache metadata/version.
|
||||||
|
- Own live WMO prototypes, placement/build jobs, instantiated Nodes or RIDs.
|
||||||
|
- Change missing/oversize/request-error fallback behavior or debug output.
|
||||||
|
- Claim asset-backed WMO fidelity, leak acceptance or traversal p95/p99.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: `src/render/wmo/wmo_scene_resource_cache_state.gd`,
|
||||||
|
`src/tools/verify_wmo_scene_resource_cache_state.gd`,
|
||||||
|
`docs/modules/wmo-scene-resource-cache-state.md`, this claim
|
||||||
|
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||||
|
`src/tools/verify_render_runtime_cache_shutdown.gd`,
|
||||||
|
`docs/modules/wmo-render-resource-cache-state.md`,
|
||||||
|
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
|
||||||
|
`targets/03-renderer-facade.md`
|
||||||
|
- Generated/ignored: `.godot`, native DLL, generated ADT/WMO resources, caches
|
||||||
|
and proprietary renderer corpus
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- State keys are loader-normalized relative WMO paths copied as `String`.
|
||||||
|
- Cached values are loader-validated `PackedScene` references.
|
||||||
|
- Pending values are `.tscn` paths for loader-owned threaded requests.
|
||||||
|
- Missing state accepts absent file, oversize, request-start, load and validation failures.
|
||||||
|
- Terminal pending removal precedes validated-scene or missing adoption.
|
||||||
|
- Transient clear retains accepted scenes; full clear releases all state.
|
||||||
|
- Cache metadata/version, file-size limit, profile and fallback rules are unchanged.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: current master `e690756`; accepted WMO render Resource cache state
|
||||||
|
- Blocks: further WMO I/O/fallback and materialization decomposition
|
||||||
|
- External state: none; verification uses synthetic PackedScenes and paths
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: dedicated lifecycle/request/missing/reset/diagnostics/source/timing
|
||||||
|
verifier; WMO render-cache/queue/planner/registry/resolver/material/shutdown plus
|
||||||
|
adjacent renderer/streaming/coordinate/documentation/coordination gates
|
||||||
|
- Fixtures: synthetic PackedScene instances and normalized relative/cache paths
|
||||||
|
- Fidelity evidence: exact current cached-scene/missing/request transitions preserved
|
||||||
|
- Performance budget: 100 cycles over 256 scene/request keys under 1 second
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline public API docs
|
||||||
|
- Module specification with inputs/outputs, ownership and source map
|
||||||
|
- Data-flow, lifecycle/state and loader interaction sequence diagrams
|
||||||
|
- World-renderer, module registry, render-cache and RENDER status updates
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important names introduced: `WmoSceneResourceCacheState`
|
||||||
|
- Simplest considered solution: one RefCounted around three Dictionaries
|
||||||
|
- Rejected complexity/abstractions: generic cache hierarchy, callbacks, signals,
|
||||||
|
mutexes, ResourceLoader wrapper, persistence layer or render/scene cache merger
|
||||||
|
- Unavoidable complexity and justification: missing can be recorded without a
|
||||||
|
pending request, and partial reset must retain validated PackedScenes
|
||||||
|
- Measured optimization evidence: bounded synthetic store/request/reset loop
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: accepted
|
||||||
|
- Done: state holder implemented; loader lookup/request/drain/reset/metrics
|
||||||
|
adapters migrated; shutdown regression and required documentation updated
|
||||||
|
- Next: WMO I/O/live fallback or liquid service extraction
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-SCENE-RESOURCE-CACHE-001:ada0fd9 -->
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: `c839294`
|
||||||
|
- Results: scene cache-state PASS (`cases=11`, `iterations=100`,
|
||||||
|
`elapsed_ms=40.768`; initial direct cold run `43.636ms`); render cache-state,
|
||||||
|
WMO queue/planner/registry/resolver/material/shutdown plus 18 adjacent
|
||||||
|
M2/terrain/facade/scheduler/streaming/coordinate regressions PASS;
|
||||||
|
documentation and coordination gates PASS; checkpoint dry-run PASS `7/7`.
|
||||||
|
- Remaining risks: accepted PackedScenes remain strongly referenced until full
|
||||||
|
shutdown release; ResourceLoader/FileAccess I/O, cache-validation probes and
|
||||||
|
live fallback remain in loader; no proprietary stale/oversize fixture,
|
||||||
|
asset-backed traversal/leak run, p95/p99 or original-client paired evidence.
|
||||||
|
The extracted scene state passed cold; full loader cold execution still needs
|
||||||
|
editor class registration for pre-existing focus/coordinate global classes.
|
||||||
|
- Documentation updated: inline public API; new
|
||||||
|
`docs/modules/wmo-scene-resource-cache-state.md`; render-cache spec, module
|
||||||
|
registry, world renderer and `RENDER.md`; data-flow/state/sequence diagrams,
|
||||||
|
ownership, threading, file-size/cache-version boundary, failure/recovery and
|
||||||
|
source map included.
|
||||||
|
- Integration: merged as `ada0fd9`; post-merge scene state (`cases=11`,
|
||||||
|
`iterations=100`, `elapsed_ms=40.113`), render state (`43.434ms`), shutdown,
|
||||||
|
WMO queue/resolver, facade, internal-access (`private_symbols=40`), `7/7`
|
||||||
|
manifest, documentation and coordination gates passed.
|
||||||
|
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-SCENE-RESOURCE-CACHE-001:c839294 -->
|
||||||
@@ -15,11 +15,22 @@
|
|||||||
| Terrain quality Mesh cache | Implemented extraction | [`terrain-quality-mesh-cache.md`](terrain-quality-mesh-cache.md) |
|
| 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 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) |
|
| 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) |
|
||||||
|
| ADT water scene finalizer | Implemented extraction | [`adt-water-scene-finalizer.md`](adt-water-scene-finalizer.md) |
|
||||||
| M2 unique placement registry | Implemented extraction | [`m2-unique-placement-registry.md`](m2-unique-placement-registry.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 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 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) |
|
| M2 build batch planner | Implemented extraction | [`m2-build-batch-planner.md`](m2-build-batch-planner.md) |
|
||||||
|
| M2 runtime mesh rebuild classifier | Implemented extraction | [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md) |
|
||||||
|
| M2 mesh load pipeline state | Implemented extraction | [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md) |
|
||||||
|
| M2 mesh resource cache state | Implemented extraction | [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md) |
|
||||||
|
| M2 mesh resource extractor | Implemented extraction | [`m2-mesh-resource-extractor.md`](m2-mesh-resource-extractor.md) |
|
||||||
| WMO placement resolver | Implemented extraction | [`wmo-placement-resolver.md`](wmo-placement-resolver.md) |
|
| WMO placement resolver | Implemented extraction | [`wmo-placement-resolver.md`](wmo-placement-resolver.md) |
|
||||||
|
| WMO placement registry | Implemented extraction | [`wmo-placement-registry.md`](wmo-placement-registry.md) |
|
||||||
|
| WMO 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) |
|
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
|
||||||
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
|
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
|
||||||
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
# 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 --> Finalizer[AdtWaterSceneFinalizer]
|
||||||
|
Finalizer --> Builder[ADTBuilder water scene]
|
||||||
|
Finalizer --> 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 Finalizer as AdtWaterSceneFinalizer
|
||||||
|
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->>Finalizer: attach_water_scene(data, origin, tile root, editor owner)
|
||||||
|
Loader->>Loader: mark water_loaded
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
Loader[StreamingWorldLoader] --> State[AdtWaterLoadPipelineState]
|
||||||
|
Loader --> Scheduler[RenderBudgetScheduler]
|
||||||
|
Loader --> Worker[WorkerThreadPool / ADTLoader]
|
||||||
|
Loader --> Finalizer[AdtWaterSceneFinalizer]
|
||||||
|
Finalizer --> Builder[ADTBuilder / SceneTree]
|
||||||
|
Worker -->|publish only| State
|
||||||
|
State -. no dependency .-> Scheduler
|
||||||
|
State -. no dependency .-> Finalizer
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 and the tile subtree; `AdtWaterSceneFinalizer` performs
|
||||||
|
the main-thread ADTBuilder call and attachment without retaining Nodes.
|
||||||
|
- `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 is now delegated to `AdtWaterSceneFinalizer`,
|
||||||
|
independently of this async bookkeeping service.
|
||||||
|
|
||||||
|
## 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 | Implemented extraction | Dedicated synthetic scene/ownership verifier | Asset-backed traversal/hitch evidence pending |
|
||||||
|
|
||||||
|
## 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/render/liquid/adt_water_scene_finalizer.gd` | Main-thread ADTBuilder call, attach and optional editor ownership |
|
||||||
|
| `src/scenes/streaming/streaming_world_loader.gd` | Parse task execution, permits, stale checks and tile loaded timing |
|
||||||
|
| `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)
|
||||||
|
- [`adt-water-scene-finalizer.md`](adt-water-scene-finalizer.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)
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
# ADT Water Scene Finalizer
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Status | Implemented extraction |
|
||||||
|
| Target/work package | M03 / `M03-RND-ADT-WATER-SCENE-FINALIZER-001` |
|
||||||
|
| Owners | Main-thread ADT water subtree build, attach and optional editor ownership |
|
||||||
|
| Last verified | Worktree `work/sindo-main-codex/m03-adt-water-scene-finalizer`, 2026-07-17 |
|
||||||
|
| Profiles/capabilities | Existing ADT MH2O/MCLQ scene output; profile-independent adapter |
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Provide one liquid-specific main-thread boundary for converting parsed ADT
|
||||||
|
water data through the existing `ADTBuilder`, attaching the resulting `Water`
|
||||||
|
subtree to a caller-owned tile root and assigning optional persisted Editor
|
||||||
|
ownership. The service is stateless and retains no Node or Resource.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Parse or validate MH2O/MCLQ binary structures.
|
||||||
|
- Replace ADTBuilder geometry, material or coordinate calculations.
|
||||||
|
- Own water request/task/result state, permits or concurrency.
|
||||||
|
- Validate tile/path/root freshness beyond receiving a valid root.
|
||||||
|
- Mark `water_loaded`, enable water or remove water subtrees.
|
||||||
|
|
||||||
|
## Context and boundaries
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Parse[Parsed ADT Dictionary] --> Loader[StreamingWorldLoader]
|
||||||
|
Loader -->|validated data, origin, tile root| Finalizer[AdtWaterSceneFinalizer]
|
||||||
|
Finalizer --> Builder[ADTBuilder]
|
||||||
|
Builder --> Water[Water Node3D or null]
|
||||||
|
Water --> Finalizer
|
||||||
|
Finalizer -->|main-thread add_child| Tile[Caller-owned tile root]
|
||||||
|
Loader -->|after return| Loaded[water_loaded timing]
|
||||||
|
```
|
||||||
|
|
||||||
|
Allowed dependencies are `ADTBuilder` and Godot Node/mesh types needed to adopt
|
||||||
|
its output. WorkerThreadPool, scheduler, tile-state dictionaries, facade,
|
||||||
|
gameplay, network and editor plugin packages are forbidden.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `attach_water_scene(water_data, tile_origin_godot_units, tile_root, editor_owner)` | Command/query | Build and attach one existing-format water subtree | Renderer main thread; returned Node is borrowed for tile lifetime | Empty data, invalid root or dry build returns null without attachment |
|
||||||
|
|
||||||
|
`editor_owner` is optional. When supplied it must be an ancestor suitable for
|
||||||
|
Godot `Node.owner`; ownership is recursively assigned to the generated subtree.
|
||||||
|
|
||||||
|
## Inputs and outputs
|
||||||
|
|
||||||
|
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Input | Parsed ADT water-containing Dictionary | ADTLoader/raw tile path | Finalizer/ADTBuilder | Caller retains Dictionary; not mutated | One main-thread call |
|
||||||
|
| Input | Tile origin in Godot world units | Loader tile state/ADTBuilder | Finalizer | Value copy | One call |
|
||||||
|
| Input | Valid tile root | StreamingWorldLoader | Finalizer | Caller/SceneTree-owned | Must outlive call and attached subtree |
|
||||||
|
| Input | Optional edited scene root | Loader Editor policy | Finalizer | SceneTree-owned borrowed Node | One call |
|
||||||
|
| Output | Attached `Water` Node3D or null | Finalizer | Loader/SceneTree | Tile root owns attached Node | Tile subtree lifetime |
|
||||||
|
|
||||||
|
Side effects are limited to ADTBuilder mesh/material creation, `add_child` and
|
||||||
|
optional recursive `Node.owner` assignment. No cache, filesystem, task or tile
|
||||||
|
state is mutated.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Input[water data + Godot origin + tile root] --> Guard{Data and root valid?}
|
||||||
|
Guard -->|no| None[Return null; unchanged]
|
||||||
|
Guard -->|yes| Build[ADTBuilder.build_tile_water_scene]
|
||||||
|
Build --> Dry{Water root exists?}
|
||||||
|
Dry -->|no| None
|
||||||
|
Dry -->|yes| Attach[tile root add_child]
|
||||||
|
Attach --> Owner{Editor owner supplied?}
|
||||||
|
Owner -->|yes| Assign[Assign owner recursively]
|
||||||
|
Owner -->|no| Return[Return borrowed root]
|
||||||
|
Assign --> Return
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle/state
|
||||||
|
|
||||||
|
The service has no retained lifecycle. Each call moves one temporary builder
|
||||||
|
result through the following call-local states.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Validating
|
||||||
|
Validating --> Rejected: empty data or invalid root
|
||||||
|
Validating --> Building: valid input
|
||||||
|
Building --> Dry: no geometry
|
||||||
|
Building --> Attached: Water root built
|
||||||
|
Attached --> EditorOwned: owner supplied
|
||||||
|
Attached --> Complete: no owner
|
||||||
|
EditorOwned --> Complete
|
||||||
|
Rejected --> [*]
|
||||||
|
Dry --> [*]
|
||||||
|
Complete --> [*]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Main sequence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Loader as StreamingWorldLoader main thread
|
||||||
|
participant Finalizer as AdtWaterSceneFinalizer
|
||||||
|
participant Builder as ADTBuilder
|
||||||
|
participant Tile as Tile root
|
||||||
|
Loader->>Finalizer: attach_water_scene(data, origin, tile, editor owner)
|
||||||
|
Finalizer->>Builder: build_tile_water_scene(data, origin)
|
||||||
|
Builder-->>Finalizer: Water root or null
|
||||||
|
alt Water root exists
|
||||||
|
Finalizer->>Tile: add_child(Water)
|
||||||
|
opt editor owner supplied
|
||||||
|
Finalizer->>Finalizer: assign owner recursively
|
||||||
|
end
|
||||||
|
Finalizer-->>Loader: borrowed Water root
|
||||||
|
else dry/invalid
|
||||||
|
Finalizer-->>Loader: null
|
||||||
|
end
|
||||||
|
Loader->>Loader: preserve existing water_loaded timing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
Loader[StreamingWorldLoader] --> Finalizer[AdtWaterSceneFinalizer]
|
||||||
|
Finalizer --> Builder[ADTBuilder]
|
||||||
|
Builder --> Material[WowLiquidMaterial]
|
||||||
|
Finalizer --> Scene[Godot SceneTree]
|
||||||
|
Pipeline[AdtWaterLoadPipelineState] --> Loader
|
||||||
|
Scheduler[RenderBudgetScheduler] --> Loader
|
||||||
|
Finalizer -. no dependency .-> Pipeline
|
||||||
|
Finalizer -. no dependency .-> Scheduler
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ownership, threading and resources
|
||||||
|
|
||||||
|
- The method is main-thread only because it creates Mesh/Material/Node objects
|
||||||
|
and mutates the SceneTree.
|
||||||
|
- `ADTBuilder` owns temporary construction. It frees an empty `Water` root and
|
||||||
|
returns null for dry/invalid liquid geometry.
|
||||||
|
- After `add_child`, the caller's tile subtree owns the returned Water root and
|
||||||
|
all MeshInstance children; the finalizer retains no references.
|
||||||
|
- Optional editor ownership affects serialization ownership only and does not
|
||||||
|
replace SceneTree lifetime ownership.
|
||||||
|
- Parsed Dictionaries are borrowed and not mutated.
|
||||||
|
|
||||||
|
## Errors, cancellation and recovery
|
||||||
|
|
||||||
|
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Empty parsed data | Input guard | Return null, no builder call | Contract verifier | Loader preserves existing loaded timing |
|
||||||
|
| Missing/freed tile root | Input guard | Return null, no scene mutation | Contract verifier | Stale-result validation normally rejects earlier |
|
||||||
|
| Dry or malformed liquid arrays | ADTBuilder returns null | No attachment | Builder/shutdown regression | Reload after corrected source |
|
||||||
|
| Tile/path stale | Loader before call | Finalizer is not invoked | Existing loader behavior | Current tile may enqueue again |
|
||||||
|
| Shutdown/cancellation | Loader drains/cancels before call | No retained finalizer work | Shutdown verifier | New loader starts stateless |
|
||||||
|
|
||||||
|
## Configuration and capabilities
|
||||||
|
|
||||||
|
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `enable_water` | Loader setting | Quality/custom | Yes | Gates both caller paths; not owned here |
|
||||||
|
| `water_finalize_ops_per_tick` | `1` | Quality/custom | Yes | Budgets async calls before finalizer |
|
||||||
|
| `editor_persist_generated_nodes` | `false` | Editor only | Yes | Supplies or omits editor owner |
|
||||||
|
|
||||||
|
## Persistence, cache and migration
|
||||||
|
|
||||||
|
The service writes no files and changes no cache/resource schema. Existing ADT,
|
||||||
|
terrain cache and liquid material versions remain valid; no rebuild or migration
|
||||||
|
is required. Persisted Editor ownership follows the loader's existing opt-in.
|
||||||
|
|
||||||
|
## Diagnostics and observability
|
||||||
|
|
||||||
|
- Logs: none; existing loader/builder diagnostics remain authoritative.
|
||||||
|
- Metrics: no independent counters; existing water queue/finalize metrics remain.
|
||||||
|
- Debug views: generated `Water/Liquid_<id>` names remain unchanged.
|
||||||
|
- Correlation IDs: tile/path correlation stays in loader pipeline state.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `verify_adt_water_scene_finalizer.gd`: empty/invalid/dry guards, one-cell
|
||||||
|
geometry, attachment, recursive owner, loader delegation and bounded timing.
|
||||||
|
- `verify_adt_water_load_pipeline_state.gd`: adjacent async ownership and loaded timing.
|
||||||
|
- Renderer material and shutdown verifiers cover material construction and dry-root cleanup.
|
||||||
|
- Fidelity evidence is the unchanged ADTBuilder implementation and unchanged two
|
||||||
|
loader call positions; no new original-client liquid parity claim is made.
|
||||||
|
- Performance budget: 100 empty calls under one second.
|
||||||
|
|
||||||
|
## Extension points
|
||||||
|
|
||||||
|
- A future liquid renderer may replace the internal builder only after paired
|
||||||
|
MH2O/MCLQ fixtures establish equivalent geometry/material behavior.
|
||||||
|
- Removal/eviction remains ordinary tile subtree ownership and needs no parallel API.
|
||||||
|
|
||||||
|
## Capability status
|
||||||
|
|
||||||
|
| Capability | Status | Evidence | Gap/next step |
|
||||||
|
|---|---|---|---|
|
||||||
|
| ADT water scene finalization seam | Implemented extraction | Synthetic geometry/attach/owner/source verifier | Asset-backed traversal and hitch metrics pending |
|
||||||
|
| ADT liquid geometry/materials | Existing implementation | ADTBuilder/material/checkpoint evidence | MH2O/MCLQ and original-client parity incomplete |
|
||||||
|
| Async water scheduling | Existing extracted state | Water pipeline verifier | Active worker interruption remains loader-owned |
|
||||||
|
|
||||||
|
## Known gaps and risks
|
||||||
|
|
||||||
|
- The service intentionally wraps the existing Dictionary-based ADTBuilder boundary.
|
||||||
|
- It does not verify that `editor_owner` is an ancestor; the loader supplies the
|
||||||
|
edited scene root under the existing policy.
|
||||||
|
- Mesh/material creation remains synchronous main-thread work behind the existing permit.
|
||||||
|
- No proprietary liquid corpus, long traversal leak run, p95/p99 or paired capture is included.
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Path | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `src/render/liquid/adt_water_scene_finalizer.gd` | Main-thread build, attach and optional recursive editor ownership |
|
||||||
|
| `addons/mpq_extractor/loaders/adt_builder.gd` | Existing ADT liquid geometry/material creation and dry-root cleanup |
|
||||||
|
| `src/render/liquid/adt_water_load_pipeline_state.gd` | Pending/task/result state before finalization |
|
||||||
|
| `src/scenes/streaming/streaming_world_loader.gd` | Permits, stale checks, tile state and finalizer calls |
|
||||||
|
| `src/tools/verify_adt_water_scene_finalizer.gd` | Synthetic scene/ownership/boundary/timing regression |
|
||||||
|
|
||||||
|
## Related decisions and references
|
||||||
|
|
||||||
|
- [`adt-water-load-pipeline-state.md`](adt-water-load-pipeline-state.md)
|
||||||
|
- [`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)
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
# M2 Mesh Load Pipeline State
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Status | Implemented extraction |
|
||||||
|
| Target/work package | M03 / `M03-RND-M2-MESH-LOAD-PIPELINE-001` |
|
||||||
|
| Owners | Static M2 threaded request records and terminal finalize FIFO |
|
||||||
|
| Last verified | Worktree `work/sindo-main-codex/m03-m2-mesh-load-pipeline`, 2026-07-17 |
|
||||||
|
| Profiles/capabilities | Existing static M2 cached `.tscn`/`.glb` load path |
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Own cross-frame bookkeeping between successful static M2 ResourceLoader request
|
||||||
|
start, terminal loaded/failed polling and budgeted main-thread finalization. The
|
||||||
|
loader retains every I/O and engine-resource operation.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Call ResourceLoader or select cache paths/formats.
|
||||||
|
- Own M2 scene, missing or material-refresh caches, or prepared Mesh references.
|
||||||
|
- Extract Meshes from PackedScene/GLB Resources.
|
||||||
|
- Consume finalize permits or rebuild/adopt runtime Meshes.
|
||||||
|
- Merge static and animated M2 pipelines.
|
||||||
|
|
||||||
|
## Context and boundaries
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Need[Static M2 mesh needed] --> Loader[StreamingWorldLoader]
|
||||||
|
Loader --> IO[ResourceLoader request]
|
||||||
|
Loader --> State[M2MeshLoadPipelineState]
|
||||||
|
State --> Poll[Detached pending records]
|
||||||
|
Poll --> Loader
|
||||||
|
Loader --> IO
|
||||||
|
Loader -->|terminal status| State
|
||||||
|
State --> FIFO[Finalize FIFO]
|
||||||
|
FIFO --> Loader
|
||||||
|
Loader --> Budget[M2_MESH_FINALIZE permit]
|
||||||
|
Loader --> Cache[M2 Mesh resource cache or shared missing cache]
|
||||||
|
```
|
||||||
|
|
||||||
|
Allowed dependencies are value containers, Strings and opaque integer statuses.
|
||||||
|
ResourceLoader, filesystem, WorkerThreadPool, Mesh/Resource/Node/RID ownership,
|
||||||
|
scheduler and other application layers are forbidden.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `remember_request(normalized_relative_path, resource_path)` | Command/query | Insert one pending request | Renderer main thread; until terminal/discard/clear | Empty/duplicate rejected |
|
||||||
|
| `has_request(path)` | Query | Test pending dedupe | Main thread | Empty/unknown false |
|
||||||
|
| `request_records_snapshot()` | Query | Copy pending records in insertion order | Main thread; caller-owned | None |
|
||||||
|
| `complete_request(path, terminal_status)` | Command/query | Move request to terminal FIFO | Main thread after poll | Unknown rejected |
|
||||||
|
| `discard_request(path)` | Command/query | Remove request without finalization | Main thread | Unknown false |
|
||||||
|
| `has_finalize_record()` / `pop_finalize_record()` | Query/command | Observe/pop completion-order FIFO | Main-thread budget drain | Empty pop returns `{}` |
|
||||||
|
| `total_work_count()` | Query | Preserve pending-plus-finalize metric | Main thread | None |
|
||||||
|
| `pending_request_count()` / `finalize_record_count()` | Query | Stage diagnostics | Main thread | None |
|
||||||
|
| `clear()` | Command | Drop bookkeeping after I/O drain/reset | Main thread | Idempotent; does not drain I/O |
|
||||||
|
| `diagnostic_snapshot()` | Query | Detached request/finalize records | Main thread | No Resources exposed |
|
||||||
|
|
||||||
|
## Inputs and outputs
|
||||||
|
|
||||||
|
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Input | Normalized M2 path and cache Resource path | Loader request adapter | Pending map | Copied Strings | Until terminal/discard/clear |
|
||||||
|
| Input | Opaque terminal ResourceLoader status | Loader poll adapter | Finalize record | Integer value | Until pop/clear |
|
||||||
|
| Output | Detached pending records | Pipeline state | Loader poll/shutdown adapter | Caller-owned copies | One poll/wait pass |
|
||||||
|
| Output | Oldest terminal record | Pipeline state | Loader finalizer | Record ownership transferred | One finalize attempt |
|
||||||
|
| Output | Detached diagnostics | Pipeline state | Verifier/future metrics | Caller-owned copies | Snapshot lifetime |
|
||||||
|
|
||||||
|
Side effects are limited to collection mutation and retaining String/integer values.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start[Successful ResourceLoader request] --> Remember[remember request]
|
||||||
|
Remember --> Snapshot[request records snapshot]
|
||||||
|
Snapshot --> Poll[Loader polls status]
|
||||||
|
Poll --> Active{In progress?}
|
||||||
|
Active -->|yes| Snapshot
|
||||||
|
Active -->|no| Complete[complete request with terminal status]
|
||||||
|
Complete --> FIFO[Append finalize FIFO]
|
||||||
|
FIFO --> Permit{Loader permit available?}
|
||||||
|
Permit -->|no| FIFO
|
||||||
|
Permit -->|yes| Pop[Pop oldest terminal record]
|
||||||
|
Pop --> Finalize[Loader gets Resource and delegates first-Mesh extraction]
|
||||||
|
Finalize --> Adopt[Loader prepares and adopts Mesh/missing state]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle/state
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Absent
|
||||||
|
Absent --> Pending: remember request
|
||||||
|
Pending --> Pending: non-terminal poll
|
||||||
|
Pending --> TerminalQueued: complete request
|
||||||
|
Pending --> Absent: discard
|
||||||
|
TerminalQueued --> Absent: pop finalize
|
||||||
|
Pending --> Absent: clear
|
||||||
|
TerminalQueued --> Absent: clear
|
||||||
|
```
|
||||||
|
|
||||||
|
One path has at most one pending request. After completion it may be requested
|
||||||
|
again only if loader cache/missing rules permit it.
|
||||||
|
|
||||||
|
## Main sequence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Loader as StreamingWorldLoader
|
||||||
|
participant Resource as ResourceLoader
|
||||||
|
participant State as M2MeshLoadPipelineState
|
||||||
|
participant Budget as RenderBudgetScheduler
|
||||||
|
Loader->>Resource: load_threaded_request(cache path)
|
||||||
|
Resource-->>Loader: OK or ERR_BUSY
|
||||||
|
Loader->>State: remember_request(normalized, cache path)
|
||||||
|
loop frames
|
||||||
|
Loader->>State: request_records_snapshot()
|
||||||
|
Loader->>Resource: load_threaded_get_status(path)
|
||||||
|
end
|
||||||
|
Loader->>State: complete_request(normalized, terminal status)
|
||||||
|
Loader->>Budget: try_consume_permit(M2_MESH_FINALIZE)
|
||||||
|
Loader->>State: pop_finalize_record()
|
||||||
|
Loader->>Resource: load_threaded_get(path)
|
||||||
|
Loader->>Loader: extract/refresh/adopt Mesh or mark missing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
Loader[StreamingWorldLoader] --> State[M2MeshLoadPipelineState]
|
||||||
|
Loader --> Resource[ResourceLoader]
|
||||||
|
Loader --> Budget[RenderBudgetScheduler]
|
||||||
|
Loader --> MeshCache[M2 Mesh resource cache and shared missing cache]
|
||||||
|
Loader --> Classifier[M2RuntimeMeshRebuildClassifier]
|
||||||
|
State -. no dependency .-> Resource
|
||||||
|
State -. no dependency .-> Budget
|
||||||
|
State -. no dependency .-> MeshCache
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ownership, threading and resources
|
||||||
|
|
||||||
|
- Main thread serializes all state mutation and snapshots.
|
||||||
|
- State owns only request/finalize Dictionaries containing Strings and status integers.
|
||||||
|
- Loader owns ResourceLoader request lifetime and drains active paths before shutdown clear.
|
||||||
|
- `M2MeshResourceCacheState` owns prepared static Mesh references; the loader
|
||||||
|
owns shared missing state, material refresh, M2Builder and other engine resources.
|
||||||
|
- Snapshot and diagnostic records are detached; caller mutation cannot affect state.
|
||||||
|
|
||||||
|
## Errors, cancellation and recovery
|
||||||
|
|
||||||
|
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Empty/duplicate request | State guard | Reject unchanged | Contract verifier | Correct caller/request later |
|
||||||
|
| Request start failure | Loader return code | No state insert; mark missing | Existing loader behavior | Cache/source correction |
|
||||||
|
| Non-terminal status | Loader poll | Keep pending | Existing queue metric | Poll next frame |
|
||||||
|
| Terminal load failure | Status in popped record | Loader marks missing | Existing missing behavior | World/cache reload |
|
||||||
|
| Empty defensive path | Loader before poll | Discard and mark missing | Source regression | Correct request producer |
|
||||||
|
| Shutdown | Loader drains pending Resource paths | Clear state | Shutdown verifier | New loader starts empty |
|
||||||
|
|
||||||
|
## Configuration and capabilities
|
||||||
|
|
||||||
|
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `m2_mesh_finalize_ops_per_tick` | `1` | Quality/custom | Yes | Bounds terminal FIFO pops outside state |
|
||||||
|
| Cache extension order | `.tscn`, `.glb` | Existing M2 path | No | Loader selects request path |
|
||||||
|
| Animated pivot-prefix GLB exclusion | Existing rule | All | No | Loader filters before request |
|
||||||
|
|
||||||
|
## Persistence, cache and migration
|
||||||
|
|
||||||
|
No pipeline state is serialized. M2 cache formats, material refresh version and
|
||||||
|
rebuild policy are unchanged; no migration or rebake is required.
|
||||||
|
|
||||||
|
## Diagnostics and observability
|
||||||
|
|
||||||
|
- `total_work_count` preserves all three historical `m2_mesh` metrics.
|
||||||
|
- Diagnostic snapshots expose only paths/statuses, never loaded Resources or Meshes.
|
||||||
|
- Existing loader logs and hitch sections remain unchanged.
|
||||||
|
- Normalized M2 path is the correlation key.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `verify_m2_mesh_load_pipeline_state.gd`: validation, dedupe, insertion-order
|
||||||
|
polling, terminal transition, completion FIFO, opaque status, discard, metrics,
|
||||||
|
detached diagnostics, loader ownership and 100-by-256 timing.
|
||||||
|
- Classifier/material/shutdown/M2 placement/build regressions cover adjacent behavior.
|
||||||
|
- Fidelity evidence is exact state/lifecycle extraction; no asset-backed or
|
||||||
|
original-client visual parity claim is made.
|
||||||
|
- Performance budget: 25,600 request/complete/pop transitions under one second.
|
||||||
|
|
||||||
|
## Extension points
|
||||||
|
|
||||||
|
- Mesh/missing cache adoption can move to a separate finalizer without changing this state.
|
||||||
|
- Animated M2 has distinct candidate/static fallback semantics and remains separate.
|
||||||
|
|
||||||
|
## Capability status
|
||||||
|
|
||||||
|
| Capability | Status | Evidence | Gap/next step |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Static M2 request/finalize state | Implemented extraction | Contract/source/timing verifier | Asset-backed long traversal pending |
|
||||||
|
| ResourceLoader I/O | Existing loader-owned | Shutdown/material regressions | I/O adapter extraction optional |
|
||||||
|
| Mesh cache | Implemented extraction | Mesh resource cache state verifier | Asset-backed memory/leak run pending |
|
||||||
|
| First-Mesh extraction | Implemented extraction | Resource/order/lifetime verifier | Asset-backed corrupt-scene fixture pending |
|
||||||
|
| Mesh materialization | Existing loader-owned | Renderer/material tests | Separate preparation finalizer pending |
|
||||||
|
|
||||||
|
## Known gaps and risks
|
||||||
|
|
||||||
|
- Records remain Dictionaries because ResourceLoader polling is a dynamic Godot boundary.
|
||||||
|
- Clear does not drain ResourceLoader; caller ordering is mandatory.
|
||||||
|
- Status integers are intentionally opaque inside state.
|
||||||
|
- No private asset traversal, leak/descriptor-pressure or p95/p99 run is included.
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Path | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Pending records, terminal FIFO and metrics |
|
||||||
|
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared static Mesh references and final clear |
|
||||||
|
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene lifetime |
|
||||||
|
| `src/scenes/streaming/streaming_world_loader.gd` | Cache path choice, I/O polling, permits and Mesh/missing adoption |
|
||||||
|
| `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Downstream stale Mesh rebuild decision |
|
||||||
|
| `src/tools/verify_m2_mesh_load_pipeline_state.gd` | Lifecycle/boundary/timing regression |
|
||||||
|
|
||||||
|
## Related decisions and references
|
||||||
|
|
||||||
|
- [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md)
|
||||||
|
- [`m2-build-batch-planner.md`](m2-build-batch-planner.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,213 @@
|
|||||||
|
# M2 Mesh Resource Cache State
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Status | Implemented extraction |
|
||||||
|
| Target/work package | M03 / `M03-RND-M2-MESH-RESOURCE-CACHE-001` |
|
||||||
|
| Owners | Prepared static M2 Mesh resource references by normalized path |
|
||||||
|
| Last verified | Worktree `work/sindo-main-codex/m03-m2-mesh-resource-cache`, 2026-07-17 |
|
||||||
|
| Profiles/capabilities | Existing static M2 cached and raw fallback paths |
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Own the runtime cache of prepared static M2 `Mesh` references outside
|
||||||
|
`StreamingWorldLoader`. The loader remains responsible for finding, loading,
|
||||||
|
extracting, refreshing and materializing each Mesh.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Own the shared M2 missing-path, prototype or animated-scene caches.
|
||||||
|
- Call ResourceLoader/FileAccess or select `.tscn`/`.glb` paths.
|
||||||
|
- Extract a Mesh from a Resource or Node.
|
||||||
|
- Refresh materials, rebuild Meshes or create MultiMesh instances.
|
||||||
|
- Add eviction, persistence, cache versions or quality-profile behavior.
|
||||||
|
|
||||||
|
## Context and boundaries
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Need[Static M2 mesh lookup] --> Loader[StreamingWorldLoader]
|
||||||
|
Loader --> Cache[M2MeshResourceCacheState]
|
||||||
|
Cache -->|cached Mesh or null| Loader
|
||||||
|
Loader --> Pipeline[M2MeshLoadPipelineState]
|
||||||
|
Pipeline --> IO[ResourceLoader]
|
||||||
|
IO --> Extract[M2MeshResourceExtractor]
|
||||||
|
Extract --> Prepare[Loader runtime preparation]
|
||||||
|
Prepare --> Cache
|
||||||
|
Loader --> Build[M2 MultiMesh materialization]
|
||||||
|
```
|
||||||
|
|
||||||
|
The cache may retain `Mesh` resources and copied normalized-path Strings. It has
|
||||||
|
no Node, ResourceLoader, filesystem, worker, scheduler, missing-state, builder,
|
||||||
|
gameplay, network or editor dependency.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `store_mesh(path, mesh)` | Command/query | Retain or replace one prepared Mesh | Renderer main thread; final shutdown | Empty path/null Mesh rejected |
|
||||||
|
| `has_mesh(path)` | Query | Test whether a Mesh is retained | Renderer main thread | Empty/unknown false |
|
||||||
|
| `find_mesh(path)` | Query | Borrow exact retained Mesh reference | Renderer main thread | Empty/unknown null |
|
||||||
|
| `mesh_count()` | Query | Return retained entry count | Diagnostics/tests | None |
|
||||||
|
| `normalized_paths_snapshot()` | Query | Copy insertion-order path keys | Diagnostics/tests | Never exposes Mesh references |
|
||||||
|
| `clear()` | Command | Release every retained Mesh reference | Final shutdown after async drain | Idempotent |
|
||||||
|
|
||||||
|
## Inputs and outputs
|
||||||
|
|
||||||
|
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Input | Normalized relative M2 path | Loader normalization adapter | Cache key | Copied String | Cache lifetime |
|
||||||
|
| Input | Prepared static Mesh | Loader finalizer or synchronous fallback | Cache value | Strong reference retained | Until replacement/clear |
|
||||||
|
| Output | Borrowed exact Mesh | Cache lookup | Static M2 batch builder | Cache retains reference | One build/materialization pass |
|
||||||
|
| Output | Detached normalized paths | Cache diagnostics | Verifier/future metrics | Caller-owned Strings | Snapshot lifetime |
|
||||||
|
|
||||||
|
Store and clear mutate only the cache Dictionary and Resource reference counts.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Lookup[Lookup normalized M2 path] --> Cached{Mesh cached?}
|
||||||
|
Cached -->|yes| Return[Return retained Mesh]
|
||||||
|
Cached -->|no| Missing{Shared missing state?}
|
||||||
|
Missing -->|yes| Null[Return null]
|
||||||
|
Missing -->|no| Load[Loader request or synchronous fallback]
|
||||||
|
Load --> Prepare[Loader extracts and prepares Mesh]
|
||||||
|
Prepare --> Valid{Prepared Mesh valid?}
|
||||||
|
Valid -->|yes| Store[Store or replace cache entry]
|
||||||
|
Valid -->|no| MarkMissing[Loader updates shared missing state]
|
||||||
|
Store --> Return
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle/state
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Absent
|
||||||
|
Absent --> Cached: store valid Mesh
|
||||||
|
Cached --> Cached: replace Mesh reference
|
||||||
|
Cached --> Absent: final shutdown clear
|
||||||
|
```
|
||||||
|
|
||||||
|
Map/world refresh retains cached Meshes. Only the existing final runtime-cache
|
||||||
|
release clears them after asynchronous shutdown drain.
|
||||||
|
|
||||||
|
## Main sequence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Loader as StreamingWorldLoader
|
||||||
|
participant Cache as M2MeshResourceCacheState
|
||||||
|
participant Pipeline as M2MeshLoadPipelineState
|
||||||
|
Loader->>Cache: find/has normalized path
|
||||||
|
alt cache miss
|
||||||
|
Loader->>Pipeline: request/poll/finalize record
|
||||||
|
Loader->>Loader: ResourceLoader get + extract + prepare
|
||||||
|
Loader->>Cache: store_mesh(path, prepared Mesh)
|
||||||
|
end
|
||||||
|
Cache-->>Loader: exact retained Mesh
|
||||||
|
Loader->>Loader: materialize static M2 batch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
Loader[StreamingWorldLoader] --> Cache[M2MeshResourceCacheState]
|
||||||
|
Loader --> Pipeline[M2MeshLoadPipelineState]
|
||||||
|
Loader --> Classifier[M2RuntimeMeshRebuildClassifier]
|
||||||
|
Loader --> Builder[M2Builder and MultiMesh]
|
||||||
|
Cache --> Mesh[Godot Mesh Resource]
|
||||||
|
Cache -. no dependency .-> Pipeline
|
||||||
|
Cache -. no dependency .-> Classifier
|
||||||
|
Cache -. no dependency .-> Builder
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ownership, threading and resources
|
||||||
|
|
||||||
|
- Renderer main-thread adapters serialize every cache operation.
|
||||||
|
- The cache owns normalized-path Strings and strong Mesh references.
|
||||||
|
- Borrowed Mesh lookups do not transfer ownership or duplicate resources.
|
||||||
|
- `M2MeshResourceExtractor` owns first-Mesh selection and temporary PackedScene
|
||||||
|
instances. The loader owns missing/prototype/animated state and MultiMeshes.
|
||||||
|
- The loader drains asynchronous work before the final cache clear.
|
||||||
|
|
||||||
|
## Errors, cancellation and recovery
|
||||||
|
|
||||||
|
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Empty path or null Mesh | Cache guard | Reject unchanged | Contract verifier | Correct caller input |
|
||||||
|
| Unknown lookup | Membership query | Return null | Existing loader path | Request/load or missing decision |
|
||||||
|
| Replacement | Existing key | Retain new exact reference | Contract verifier | Normal subsequent lookup |
|
||||||
|
| Load/preparation failure | Loader boundary | Do not store; loader marks missing | Existing renderer behavior | Cache/source correction and reload |
|
||||||
|
| Final shutdown | Loader lifecycle | Clear releases references | Shutdown/source verifier | New loader starts empty |
|
||||||
|
|
||||||
|
## Configuration and capabilities
|
||||||
|
|
||||||
|
The module adds no setting, budget or profile branch. Cache path order,
|
||||||
|
`M2_MATERIAL_REFRESH_VERSION`, finalize permits and batching remain loader-owned.
|
||||||
|
|
||||||
|
## Persistence, cache and migration
|
||||||
|
|
||||||
|
The state is runtime-only and serializes nothing. Existing `.tscn`/`.glb`
|
||||||
|
formats, import caches and material refresh metadata are unchanged; no migration
|
||||||
|
or rebake is required.
|
||||||
|
|
||||||
|
## Diagnostics and observability
|
||||||
|
|
||||||
|
`mesh_count` and `normalized_paths_snapshot` expose scalar ownership diagnostics
|
||||||
|
without Mesh references. Existing renderer metrics/logging are unchanged because
|
||||||
|
the historical Mesh cache had no queue contribution or log site.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `verify_m2_mesh_resource_cache_state.gd`: invalid input, exact reference,
|
||||||
|
replacement, lookup, detached path snapshot, idempotent clear, loader source
|
||||||
|
ownership and 100-by-256 bounded timing.
|
||||||
|
- Mesh pipeline/classifier/material/shutdown/M2 build/facade regressions cover
|
||||||
|
adjacent behavior.
|
||||||
|
- Fidelity evidence is exact cache/lifetime extraction; no original-client
|
||||||
|
visual-parity or asset-backed performance claim is made.
|
||||||
|
|
||||||
|
## Extension points
|
||||||
|
|
||||||
|
- A separate M2 Mesh resource finalizer may later return a prepared Mesh for
|
||||||
|
`store_mesh` without changing cache ownership.
|
||||||
|
- Eviction requires measured memory pressure and explicit lifetime semantics.
|
||||||
|
|
||||||
|
## Capability status
|
||||||
|
|
||||||
|
| Capability | Status | Evidence | Gap/next step |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Prepared static M2 Mesh cache | Implemented extraction | Contract/source/timing verifier | Asset-backed memory/leak run pending |
|
||||||
|
| M2 Mesh request lifecycle | Implemented extraction | Pipeline state verifier | ResourceLoader I/O remains loader-owned |
|
||||||
|
| M2 Mesh extraction | Implemented extraction | Resource/order/lifetime verifier | Asset-backed corrupt-scene fixture pending |
|
||||||
|
| M2 Mesh preparation | Existing loader-owned | Material/classifier regressions | Separate finalizer extraction next |
|
||||||
|
|
||||||
|
## Known gaps and risks
|
||||||
|
|
||||||
|
- Strong Mesh references intentionally remain until final shutdown.
|
||||||
|
- Shared missing state cannot move here without also redesigning prototype and
|
||||||
|
animated lookup behavior.
|
||||||
|
- No proprietary M2 traversal, leak/descriptor-pressure, p95/p99 or paired
|
||||||
|
original-client capture is included.
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Path | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared Mesh references and final clear |
|
||||||
|
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene lifetime |
|
||||||
|
| `src/scenes/streaming/streaming_world_loader.gd` | Normalization, I/O, preparation, missing state and materialization |
|
||||||
|
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Pending request and terminal finalize records |
|
||||||
|
| `src/tools/verify_m2_mesh_resource_cache_state.gd` | Cache ownership/lifetime/boundary/timing regression |
|
||||||
|
|
||||||
|
## Related decisions and references
|
||||||
|
|
||||||
|
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
|
||||||
|
- [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.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,207 @@
|
|||||||
|
# M2 Mesh Resource Extractor
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Status | Implemented extraction |
|
||||||
|
| Target/work package | M03 / `M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001` |
|
||||||
|
| Owners | First-Mesh selection and temporary PackedScene instance lifetime |
|
||||||
|
| Last verified | Worktree `work/sindo-main-codex/m03-m2-mesh-resource-extractor`, 2026-07-17 |
|
||||||
|
| Profiles/capabilities | Static M2 threaded cache and synchronous raw/prototype fallback paths |
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Select the first M2 Mesh from a direct Resource, PackedScene or existing Node
|
||||||
|
subtree while keeping traversal and temporary-node lifetime outside
|
||||||
|
`StreamingWorldLoader`.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Request/load Resources or choose cache paths.
|
||||||
|
- Load raw M2 data, refresh materials or rebuild Meshes.
|
||||||
|
- Own Mesh/prototype/missing caches or pending/finalize queues.
|
||||||
|
- Create MultiMeshes, attach Nodes or consume render permits.
|
||||||
|
- Change first-Mesh selection order or visible output.
|
||||||
|
|
||||||
|
## Context and boundaries
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Pipeline[M2 load pipeline] --> Loader[StreamingWorldLoader]
|
||||||
|
Loader --> Resource[Loaded Mesh or PackedScene Resource]
|
||||||
|
Resource --> Extractor[M2MeshResourceExtractor]
|
||||||
|
Extractor -->|first Mesh or null| Loader
|
||||||
|
Loader --> Prepare[Material refresh/rebuild]
|
||||||
|
Prepare --> Cache[M2MeshResourceCacheState]
|
||||||
|
Prototype[Existing M2 Node subtree] --> Extractor
|
||||||
|
```
|
||||||
|
|
||||||
|
The extractor may inspect `Resource`, `PackedScene`, `Node`, `MeshInstance3D`
|
||||||
|
and `Mesh` engine types. It has no ResourceLoader, filesystem, worker, cache,
|
||||||
|
scheduler, builder, gameplay, network or editor dependency.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `extract_first_mesh(resource)` | Query with temporary ownership | Return direct Mesh or first Mesh from one PackedScene instance | Renderer main thread; temporary root freed before return | Null/unsupported/empty returns null |
|
||||||
|
| `find_first_mesh_in_subtree(root)` | Query | Return first Mesh in historical depth-first preorder | Renderer main thread; caller-owned subtree unchanged | Null/no Mesh returns null |
|
||||||
|
|
||||||
|
## Inputs and outputs
|
||||||
|
|
||||||
|
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Input | Direct Mesh Resource | Loader threaded finalizer | Extractor | Borrowed Resource | One call |
|
||||||
|
| Input | PackedScene Resource | Loader threaded finalizer | Extractor | Borrowed scene; extractor owns temporary instance | Temporary root until return |
|
||||||
|
| Input | Existing prototype/rebuild Node subtree | Loader builder/fallback | Extractor | Borrowed Node tree | One traversal |
|
||||||
|
| Output | First Mesh reference | Extractor | Loader preparation/cache adapter | Borrowed exact reference | Cache may retain later |
|
||||||
|
|
||||||
|
The only side effect is synchronous creation and immediate `free()` of a
|
||||||
|
temporary PackedScene instance.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Input[Resource or Node subtree] --> Direct{Direct Mesh?}
|
||||||
|
Direct -->|yes| Return[Return exact Mesh]
|
||||||
|
Direct -->|no| Packed{PackedScene?}
|
||||||
|
Packed -->|yes| Instantiate[Instantiate temporary root]
|
||||||
|
Packed -->|no| Traverse[Traverse caller-owned subtree]
|
||||||
|
Instantiate --> TraverseTemp[Depth-first preorder traversal]
|
||||||
|
TraverseTemp --> Free[Free temporary root]
|
||||||
|
Free --> ReturnOrNull[Return first Mesh or null]
|
||||||
|
Traverse --> ReturnOrNull
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle/state
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Stateless
|
||||||
|
Stateless --> TemporaryInstance: extract PackedScene
|
||||||
|
TemporaryInstance --> Traversing: instantiate succeeded
|
||||||
|
Traversing --> Stateless: free temporary root before return
|
||||||
|
Stateless --> Stateless: direct Mesh/subtree/invalid input
|
||||||
|
```
|
||||||
|
|
||||||
|
No state or Resource reference is retained between calls.
|
||||||
|
|
||||||
|
## Main sequence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Loader as StreamingWorldLoader
|
||||||
|
participant Extractor as M2MeshResourceExtractor
|
||||||
|
participant Scene as PackedScene
|
||||||
|
Loader->>Extractor: extract_first_mesh(loaded Resource)
|
||||||
|
alt direct Mesh
|
||||||
|
Extractor-->>Loader: same Mesh reference
|
||||||
|
else PackedScene
|
||||||
|
Extractor->>Scene: instantiate()
|
||||||
|
Scene-->>Extractor: temporary root
|
||||||
|
Extractor->>Extractor: depth-first first-Mesh search
|
||||||
|
Extractor->>Extractor: temporary_root.free()
|
||||||
|
Extractor-->>Loader: Mesh or null
|
||||||
|
end
|
||||||
|
Loader->>Loader: prepare and cache Mesh or mark missing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
Loader[StreamingWorldLoader] --> Extractor[M2MeshResourceExtractor]
|
||||||
|
Extractor --> Types[Resource / PackedScene / Node / Mesh]
|
||||||
|
Loader --> Pipeline[M2MeshLoadPipelineState]
|
||||||
|
Loader --> Cache[M2MeshResourceCacheState]
|
||||||
|
Loader --> Classifier[M2RuntimeMeshRebuildClassifier]
|
||||||
|
Extractor -. no dependency .-> Pipeline
|
||||||
|
Extractor -. no dependency .-> Cache
|
||||||
|
Extractor -. no dependency .-> Classifier
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ownership, threading and resources
|
||||||
|
|
||||||
|
- The renderer main thread performs all traversal and PackedScene instantiation.
|
||||||
|
- Direct and subtree Mesh references are borrowed without duplication.
|
||||||
|
- A PackedScene temporary root is extractor-owned and synchronously freed.
|
||||||
|
- Existing prototype/rebuild subtrees remain caller-owned and are never mutated.
|
||||||
|
- Cache retention and Mesh material preparation remain loader/service concerns.
|
||||||
|
|
||||||
|
## Errors, cancellation and recovery
|
||||||
|
|
||||||
|
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Null/unsupported Resource | Type guard | Return null | Contract verifier | Loader marks missing/falls back |
|
||||||
|
| PackedScene instantiate failure | Null root | Return null | Existing loader behavior | Rebuild cache/source |
|
||||||
|
| No Mesh in subtree | Traversal exhaustion | Return null | Contract verifier | Loader marks missing/falls back |
|
||||||
|
| Null MeshInstance Mesh | Null traversal result | Parent continues with later siblings | Order verifier | Normal sibling search |
|
||||||
|
| Shutdown/cancel | No retained state | No action | Ownership docs | Caller owns surrounding job |
|
||||||
|
|
||||||
|
## Configuration and capabilities
|
||||||
|
|
||||||
|
The module adds no settings or profile branches. Cache format order, material
|
||||||
|
refresh version, render permits and batching remain unchanged outside it.
|
||||||
|
|
||||||
|
## Persistence, cache and migration
|
||||||
|
|
||||||
|
The extractor is stateless and serializes nothing. Existing `.tscn`/`.glb`
|
||||||
|
formats, cache versions and rebuild metadata are unchanged; no migration or
|
||||||
|
rebake is required.
|
||||||
|
|
||||||
|
## Diagnostics and observability
|
||||||
|
|
||||||
|
The module emits no logs and retains no counters. Loader correlation remains the
|
||||||
|
normalized M2 path. Its verifier measures traversal time and temporary Node count.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `verify_m2_mesh_resource_extractor.gd`: direct/invalid Resources, depth-first
|
||||||
|
child order, null-Mesh sibling continuation, subtree ownership, PackedScene
|
||||||
|
exact Mesh identity, temporary Node release, source boundaries and 10,000
|
||||||
|
bounded traversals.
|
||||||
|
- M2 pipeline/cache/classifier/material/shutdown/build regressions cover adjacent
|
||||||
|
behavior. No asset-backed visual parity claim is made.
|
||||||
|
|
||||||
|
## Extension points
|
||||||
|
|
||||||
|
- Material refresh/rebuild can become a separate finalizer consuming the Mesh
|
||||||
|
returned here and storing its result in `M2MeshResourceCacheState`.
|
||||||
|
- Broader generic scene traversal is intentionally excluded until another real
|
||||||
|
consumer requires the same exact contract.
|
||||||
|
|
||||||
|
## Capability status
|
||||||
|
|
||||||
|
| Capability | Status | Evidence | Gap/next step |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Direct/PackedScene first-Mesh extraction | Implemented extraction | Resource/order/lifetime verifier | Asset-backed corrupt-scene fixture pending |
|
||||||
|
| Prototype/rebuild subtree selection | Implemented extraction | Synthetic preorder verifier | Asset-backed M2 traversal pending |
|
||||||
|
| Material refresh/rebuild finalization | Existing loader-owned | Classifier/material regressions | Separate finalizer extraction next |
|
||||||
|
|
||||||
|
## Known gaps and risks
|
||||||
|
|
||||||
|
- PackedScene instantiation remains synchronous main-thread work behind the
|
||||||
|
existing finalize permit.
|
||||||
|
- The first Mesh rule intentionally ignores additional MeshInstances.
|
||||||
|
- No proprietary M2 corpus, corrupt cache, leak/descriptor-pressure, p95/p99 or
|
||||||
|
paired original-client capture is included.
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Path | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh traversal and temporary PackedScene lifetime |
|
||||||
|
| `src/scenes/streaming/streaming_world_loader.gd` | ResourceLoader, preparation, caching, missing state and materialization |
|
||||||
|
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared Mesh retention |
|
||||||
|
| `src/tools/verify_m2_mesh_resource_extractor.gd` | Resource/order/lifetime/boundary/timing regression |
|
||||||
|
|
||||||
|
## Related decisions and references
|
||||||
|
|
||||||
|
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
|
||||||
|
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
|
||||||
|
- [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.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,221 @@
|
|||||||
|
# M2 Runtime Mesh Rebuild Classifier
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Status | Implemented extraction |
|
||||||
|
| Target/work package | M03 / `M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001` |
|
||||||
|
| Owners | Billboard/UV-rotation rebuild decision and per-path memoization |
|
||||||
|
| Last verified | Worktree `work/sindo-main-codex/m03-m2-runtime-rebuild-classifier`, 2026-07-17 |
|
||||||
|
| Profiles/capabilities | Existing M2 material-refresh path; profile-independent |
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Determine whether a stale cached M2 mesh must be rebuilt in memory from raw M2
|
||||||
|
data to restore billboard custom attributes or UV-rotation material inputs. The
|
||||||
|
first decision is memoized by normalized relative path for the current map/
|
||||||
|
runtime-cache lifetime.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Read M2 files or invoke `M2Loader`/`M2Builder`.
|
||||||
|
- Own cached Meshes, scenes, materials, Nodes or RIDs.
|
||||||
|
- Select cache paths, perform ResourceLoader transitions or consume permits.
|
||||||
|
- Change refresh/cache format versions or shader behavior.
|
||||||
|
- Classify animation, particles, ribbons or general visual fidelity.
|
||||||
|
|
||||||
|
## Context and boundaries
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Cache[Stale cached M2 Mesh] --> Loader[StreamingWorldLoader]
|
||||||
|
Loader --> Raw[M2Loader raw Dictionary]
|
||||||
|
Raw --> Classifier[M2RuntimeMeshRebuildClassifier]
|
||||||
|
Classifier --> Decision[Memoized rebuild boolean]
|
||||||
|
Decision --> Loader
|
||||||
|
Loader -->|true| Builder[M2Builder rebuild]
|
||||||
|
Loader -->|false| Refresh[Mark material refresh version]
|
||||||
|
```
|
||||||
|
|
||||||
|
Allowed dependencies are Dictionary/Array, `PackedInt32Array`, `Vector4` and
|
||||||
|
scalar math. Files, ResourceLoader, parser/builder classes, SceneTree,
|
||||||
|
WorkerThreadPool, Mesh/Node/RID ownership and other layers are forbidden.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `needs_runtime_mesh_rebuild(normalized_relative_path, raw_m2_data)` | Memoized query | Detect billboard or referenced UV rotation requirements | Renderer main thread; cached until clear | Invalid variants/indices are skipped; first path decision wins |
|
||||||
|
| `clear()` | Command | Drop all memoized path decisions | Main thread; map reset/shutdown | Idempotent |
|
||||||
|
| `cached_path_count()` | Query | Return memoized path count | Main thread | None |
|
||||||
|
| `diagnostic_snapshot()` | Query | Return detached path/decision map | Main thread; caller-owned | None |
|
||||||
|
|
||||||
|
## Inputs and outputs
|
||||||
|
|
||||||
|
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Input | Normalized M2 relative path | Loader cache lookup | Classifier cache key | String value copied | Until clear |
|
||||||
|
| Input | Raw M2 batches/transforms/combos Dictionary | Native `M2Loader` adapter | Classifier | Borrowed; not mutated | First classification call per path |
|
||||||
|
| Output | Rebuild-required boolean | Classifier | Loader material-refresh adapter | Scalar | Memoized until clear |
|
||||||
|
| Output | Detached decision Dictionary | Classifier | Verifier/future diagnostics | Caller-owned copy | Snapshot lifetime |
|
||||||
|
|
||||||
|
Side effects are limited to inserting and clearing boolean memoization entries.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Query[path + raw M2 data] --> Cached{Path cached?}
|
||||||
|
Cached -->|yes| Return[Return first decision]
|
||||||
|
Cached -->|no| Billboard{Any billboard flag or positive vertex count?}
|
||||||
|
Billboard -->|yes| StoreTrue[Store true]
|
||||||
|
Billboard -->|no| Inputs{Transforms and combos present?}
|
||||||
|
Inputs -->|no| StoreFalse[Store false]
|
||||||
|
Inputs -->|yes| Batch[For each Dictionary batch]
|
||||||
|
Batch --> Stage[Inspect at most four referenced stages]
|
||||||
|
Stage --> Rotation{Non-identity rotation or speed above epsilon?}
|
||||||
|
Rotation -->|yes| StoreTrue
|
||||||
|
Rotation -->|no| StoreFalse
|
||||||
|
StoreTrue --> Return
|
||||||
|
StoreFalse --> Return
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle/state
|
||||||
|
|
||||||
|
Each path moves from unknown to one immutable cached boolean. Clear returns all
|
||||||
|
paths to unknown; no resource lifecycle participates.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Unknown
|
||||||
|
Unknown --> RebuildRequired: billboard or referenced UV rotation
|
||||||
|
Unknown --> RebuildNotRequired: no relevant metadata
|
||||||
|
RebuildRequired --> RebuildRequired: repeated query
|
||||||
|
RebuildNotRequired --> RebuildNotRequired: repeated query
|
||||||
|
RebuildRequired --> Unknown: clear
|
||||||
|
RebuildNotRequired --> Unknown: clear
|
||||||
|
```
|
||||||
|
|
||||||
|
## Main sequence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Loader as StreamingWorldLoader
|
||||||
|
participant Native as M2Loader
|
||||||
|
participant Classifier as M2RuntimeMeshRebuildClassifier
|
||||||
|
participant Builder as M2Builder
|
||||||
|
Loader->>Native: load_m2(normalized path)
|
||||||
|
Native-->>Loader: raw Dictionary
|
||||||
|
Loader->>Classifier: needs_runtime_mesh_rebuild(path, data)
|
||||||
|
Classifier-->>Loader: memoized boolean
|
||||||
|
alt rebuild required
|
||||||
|
Loader->>Builder: rebuild mesh from raw data
|
||||||
|
else no rebuild required
|
||||||
|
Loader->>Loader: stamp material refresh version
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
Loader[StreamingWorldLoader] --> Classifier[M2RuntimeMeshRebuildClassifier]
|
||||||
|
Loader --> Native[M2Loader]
|
||||||
|
Loader --> Builder[M2Builder]
|
||||||
|
Loader --> Cache[M2 Mesh/Scene caches]
|
||||||
|
Classifier -. no dependency .-> Native
|
||||||
|
Classifier -. no dependency .-> Builder
|
||||||
|
Classifier -. no dependency .-> Cache
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ownership, threading and resources
|
||||||
|
|
||||||
|
- The classifier owns only normalized-path boolean entries.
|
||||||
|
- The loader owns raw loading, cache/resource selection, Mesh replacement and
|
||||||
|
material refresh metadata.
|
||||||
|
- Raw Dictionaries are borrowed and never retained; only the computed boolean is cached.
|
||||||
|
- Current calls and clears are serialized on the renderer main thread.
|
||||||
|
- No Node, Resource, Mesh, material or RID reference crosses into the classifier.
|
||||||
|
|
||||||
|
## Errors, cancellation and recovery
|
||||||
|
|
||||||
|
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Non-Dictionary batch/transform | Type guard | Skip entry or use identity defaults | Synthetic verifier | Correct parser data on next cleared lifetime |
|
||||||
|
| Missing transforms/combos | Empty guard | No UV-driven rebuild | Synthetic verifier | Billboard classification still applies |
|
||||||
|
| Invalid combo/transform index | Bounds guard | Skip stage | Synthetic verifier | Correct raw data and clear |
|
||||||
|
| Repeated path with changed data | Cache hit | Preserve first decision | Memoization fixture | Map/reset clear recomputes |
|
||||||
|
| Load/build failure | Outside classifier | Existing mesh fallback remains | Loader diagnostics | Existing reload/fallback path |
|
||||||
|
|
||||||
|
## Configuration and capabilities
|
||||||
|
|
||||||
|
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Maximum texture stages inspected | `4` | Existing M2 material path | No | Preserves historical four-stage shader boundary |
|
||||||
|
| UV rotation-speed epsilon | `0.000001` exclusive | Existing M2 material path | No | Larger absolute speed requires rebuild |
|
||||||
|
| `M2_MATERIAL_REFRESH_VERSION` | Loader constant `2` | All | Code version | Gates whether classifier is called |
|
||||||
|
|
||||||
|
## Persistence, cache and migration
|
||||||
|
|
||||||
|
Memoized booleans are runtime-only and are cleared at the two historical loader
|
||||||
|
reset/shutdown sites. No persisted cache version or rebuild instruction changes.
|
||||||
|
|
||||||
|
## Diagnostics and observability
|
||||||
|
|
||||||
|
- Logs: none.
|
||||||
|
- Metrics: `cached_path_count` is contract-level only and not added to runtime logs.
|
||||||
|
- Debug views: detached path-to-boolean snapshot.
|
||||||
|
- Correlation IDs: normalized M2 relative path.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `verify_m2_runtime_mesh_rebuild_classifier.gd`: billboard flag/count, invalid
|
||||||
|
variants, identity/non-identity rotation, positive/negative speed, exact
|
||||||
|
epsilon, four-stage cap, invalid indices, memoization, clear, source boundary
|
||||||
|
and 100-by-256 timing.
|
||||||
|
- Existing M2 builder/material verifiers cover downstream shader/cache behavior.
|
||||||
|
- Fidelity evidence is exact predicate/cache extraction. No new asset-backed or
|
||||||
|
original-client parity claim is made.
|
||||||
|
- Performance budget: 25,600 classifications and clears under one second.
|
||||||
|
|
||||||
|
## Extension points
|
||||||
|
|
||||||
|
- A typed raw M2 material descriptor may replace the Dictionary only with parser
|
||||||
|
fixtures and a compatibility adapter.
|
||||||
|
- Mesh request/cache/finalization state can be extracted separately without
|
||||||
|
changing this rebuild predicate.
|
||||||
|
|
||||||
|
## Capability status
|
||||||
|
|
||||||
|
| Capability | Status | Evidence | Gap/next step |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Billboard rebuild classification | Implemented extraction | Flag/count fixtures | Asset-backed cache refresh timing pending |
|
||||||
|
| UV-rotation rebuild classification | Implemented extraction | Combo/stage/threshold fixtures | More build-12340 material fixtures pending |
|
||||||
|
| Runtime memoization lifetime | Implemented extraction | First-decision/clear/source fixtures | Byte/count runtime metric not exposed |
|
||||||
|
| M2 mesh loading/finalization | Existing loader-owned | Material/shutdown regressions | Separate state extraction pending |
|
||||||
|
|
||||||
|
## Known gaps and risks
|
||||||
|
|
||||||
|
- Raw parser data remains Dictionary-based.
|
||||||
|
- The first decision wins even if different raw data is passed for the same path;
|
||||||
|
this intentionally preserves prior cache behavior.
|
||||||
|
- Only four texture stages are inspected, matching current material support.
|
||||||
|
- No private asset traversal, descriptor-pressure, p95/p99 or paired visual run is included.
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Path | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Predicate, thresholds and boolean memoization |
|
||||||
|
| `src/scenes/streaming/streaming_world_loader.gd` | Raw loading, Mesh rebuild/adoption, refresh version and clear calls |
|
||||||
|
| `addons/mpq_extractor/loaders/m2_builder.gd` | Mesh/material rebuild implementation |
|
||||||
|
| `src/tools/verify_m2_runtime_mesh_rebuild_classifier.gd` | Synthetic predicate/cache/boundary/timing regression |
|
||||||
|
|
||||||
|
## Related decisions and references
|
||||||
|
|
||||||
|
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
|
||||||
|
- [`m2-placement-grouper.md`](m2-placement-grouper.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,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)
|
||||||
@@ -19,7 +19,8 @@ live-prototype instance paths.
|
|||||||
## Non-goals
|
## Non-goals
|
||||||
|
|
||||||
- Parse ADT/WDT or convert canonical WoW coordinates.
|
- Parse ADT/WDT or convert canonical WoW coordinates.
|
||||||
- Own WMO registry references, tile state, jobs, queues or retries.
|
- Own WMO registry references, tile state, jobs, queues or retries; reference
|
||||||
|
ownership belongs to `WmoPlacementRegistry`.
|
||||||
- Load/validate cached scenes or lightweight render resources.
|
- Load/validate cached scenes or lightweight render resources.
|
||||||
- Create Nodes, Mesh/MultiMesh, materials or RIDs.
|
- Create Nodes, Mesh/MultiMesh, materials or RIDs.
|
||||||
- Implement portals, rooms, visibility, materials, doodads or WMO parity.
|
- Implement portals, rooms, visibility, materials, doodads or WMO parity.
|
||||||
@@ -34,7 +35,7 @@ flowchart LR
|
|||||||
Resolver --> Identity[Registry unique key]
|
Resolver --> Identity[Registry unique key]
|
||||||
Resolver --> Transform[World Transform3D]
|
Resolver --> Transform[World Transform3D]
|
||||||
CacheKey --> Cache[Loader WMO caches/requests]
|
CacheKey --> Cache[Loader WMO caches/requests]
|
||||||
Identity --> Registry[Loader WMO registry]
|
Identity --> Registry[WmoPlacementRegistry]
|
||||||
Transform --> RenderRoot[Lightweight render root]
|
Transform --> RenderRoot[Lightweight render root]
|
||||||
Transform --> Scene[Cached scene instance]
|
Transform --> Scene[Cached scene instance]
|
||||||
Transform --> Live[Live prototype instance]
|
Transform --> Live[Live prototype instance]
|
||||||
@@ -60,7 +61,7 @@ WorkerThreadPool, mutexes, files, gameplay, network and editor UI are forbidden.
|
|||||||
| Input | Raw placement Dictionary | WDT global or ADT MODF parser result | Identity/transform queries | Read-only shallow value | One call |
|
| Input | 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 |
|
| 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 | Normalized relative path | Resolver | Render/scene cache and load-request maps | New String value | Request/cache lookup |
|
||||||
| Output | `uid:*` or `tile:*:*` key | Resolver | Loader WMO registry/ref arrays | New String value | Until unregister/reset |
|
| Output | `uid:*` or `tile:*:*` key | Resolver | `WmoPlacementRegistry` and loader ref arrays | New String value | Until unregister/reset |
|
||||||
| Output | World `Transform3D` | Resolver | Three WMO instance adapters | Value copy | Instance lifetime after assignment |
|
| Output | World `Transform3D` | Resolver | Three WMO instance adapters | Value copy | Instance lifetime after assignment |
|
||||||
|
|
||||||
The resolver retains no source Dictionary, output or engine resource.
|
The resolver retains no source Dictionary, output or engine resource.
|
||||||
@@ -91,7 +92,7 @@ and shutdown require no resolver operation.
|
|||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant Loader as StreamingWorldLoader
|
participant Loader as StreamingWorldLoader
|
||||||
participant Resolver as WmoPlacementResolver
|
participant Resolver as WmoPlacementResolver
|
||||||
participant Registry as Loader WMO registry/cache
|
participant Registry as WmoPlacementRegistry
|
||||||
participant Instance as Render/cached/live instance
|
participant Instance as Render/cached/live instance
|
||||||
Loader->>Resolver: normalize_relative_path(path)
|
Loader->>Resolver: normalize_relative_path(path)
|
||||||
Resolver-->>Loader: cache key
|
Resolver-->>Loader: cache key
|
||||||
@@ -105,8 +106,9 @@ sequenceDiagram
|
|||||||
## Ownership, threading and resources
|
## Ownership, threading and resources
|
||||||
|
|
||||||
- The resolver owns only call-local scalar/value calculations.
|
- The resolver owns only call-local scalar/value calculations.
|
||||||
- The loader owns WMO registry entries/ref sets, cache/load-request state, build
|
- `WmoPlacementRegistry` owns placement-key reference sets. The loader owns its
|
||||||
jobs/queues, resource fallback and cancellation.
|
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.
|
- The loader and builders own every Node/Mesh/MultiMesh/material/RID lifecycle.
|
||||||
- Pure calls are thread-safe; current consumers execute on the main thread.
|
- Pure calls are thread-safe; current consumers execute on the main thread.
|
||||||
|
|
||||||
@@ -152,8 +154,8 @@ in the facade/loader diagnostic snapshot.
|
|||||||
|
|
||||||
## Extension points
|
## Extension points
|
||||||
|
|
||||||
- A later package may extract WMO registry ownership/ref release using these
|
- `WmoPlacementRegistry` consumes these stable keys; later packages may extract
|
||||||
stable keys.
|
WMO build/resource state without changing the resolver.
|
||||||
- Render-group build cursor and resource-selection state require separate
|
- Render-group build cursor and resource-selection state require separate
|
||||||
lifecycle/cancellation contracts.
|
lifecycle/cancellation contracts.
|
||||||
|
|
||||||
@@ -162,7 +164,8 @@ in the facade/loader diagnostic snapshot.
|
|||||||
| Capability | Status | Evidence | Gap/next step |
|
| Capability | Status | Evidence | Gap/next step |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Consistent WMO placement identity/transform | Implemented extraction | Contract/source/timing verifier | Asset-backed placement comparison pending |
|
| Consistent WMO placement identity/transform | Implemented extraction | Contract/source/timing verifier | Asset-backed placement comparison pending |
|
||||||
| WMO registry/build services | Remains in loader | Existing runtime regressions | Stateful extraction pending |
|
| WMO placement registry | Implemented extraction | Ownership/source/timing verifier | Asset-backed cross-tile corpus pending |
|
||||||
|
| WMO build services | Remains in loader | Existing runtime regressions | Stateful extraction pending |
|
||||||
| Portals/rooms/material fidelity | Partial | M00 checkpoint/material evidence | Broader WMO implementation pending |
|
| Portals/rooms/material fidelity | Partial | M00 checkpoint/material evidence | Broader WMO implementation pending |
|
||||||
|
|
||||||
## Known gaps and risks
|
## Known gaps and risks
|
||||||
@@ -177,11 +180,13 @@ in the facade/loader diagnostic snapshot.
|
|||||||
| Path | Responsibility |
|
| Path | Responsibility |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `src/render/wmo/wmo_placement_resolver.gd` | Path, identity and transform rules |
|
| `src/render/wmo/wmo_placement_resolver.gd` | Path, identity and transform rules |
|
||||||
| `src/scenes/streaming/streaming_world_loader.gd` | Registry/cache/build/resource/Node ownership |
|
| `src/render/wmo/wmo_placement_registry.gd` | Placement reference-set ownership |
|
||||||
|
| `src/scenes/streaming/streaming_world_loader.gd` | Node map/cache/build/resource ownership |
|
||||||
| `src/tools/verify_wmo_placement_resolver.gd` | Contract, source and timing regression |
|
| `src/tools/verify_wmo_placement_resolver.gd` | Contract, source and timing regression |
|
||||||
|
|
||||||
## Related decisions and references
|
## Related decisions and references
|
||||||
|
|
||||||
- [`world-renderer.md`](world-renderer.md)
|
- [`world-renderer.md`](world-renderer.md)
|
||||||
|
- [`wmo-placement-registry.md`](wmo-placement-registry.md)
|
||||||
- [`../../RENDER.md`](../../RENDER.md)
|
- [`../../RENDER.md`](../../RENDER.md)
|
||||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
| Status | Partial |
|
| Status | Partial |
|
||||||
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
|
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
|
||||||
| Owners | Renderer workstream / milestone integrator |
|
| Owners | Renderer workstream / milestone integrator |
|
||||||
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-placement-resolver`, 2026-07-17 |
|
| Last verified | Worktree `work/sindo-main-codex/m03-m2-mesh-resource-extractor`, 2026-07-17 |
|
||||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
@@ -56,6 +56,12 @@ flowchart LR
|
|||||||
M2Batch --> Loader
|
M2Batch --> Loader
|
||||||
Loader --> WmoPlacement[WmoPlacementResolver]
|
Loader --> WmoPlacement[WmoPlacementResolver]
|
||||||
WmoPlacement --> Loader
|
WmoPlacement --> Loader
|
||||||
|
Loader --> WmoRegistry[WmoPlacementRegistry]
|
||||||
|
WmoRegistry --> Loader
|
||||||
|
Loader --> WmoBuildStep[WmoRenderBuildStepPlanner]
|
||||||
|
WmoBuildStep --> Loader
|
||||||
|
Loader --> WmoBuildQueue[WmoRenderBuildQueue]
|
||||||
|
WmoBuildQueue --> Loader
|
||||||
Native --> Parsed[Parsed tile/model data]
|
Native --> Parsed[Parsed tile/model data]
|
||||||
Parsed --> Loader
|
Parsed --> Loader
|
||||||
Loader --> Scene[SceneTree nodes]
|
Loader --> Scene[SceneTree nodes]
|
||||||
@@ -126,7 +132,18 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
|||||||
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
||||||
|
| `M2RuntimeMeshRebuildClassifier` | Internal memoized M2 service | Detects billboard/UV-rotation metadata requiring stale cached-mesh rebuild | Renderer main thread; cached until reset | Invalid variants/indices skipped; first path decision wins |
|
||||||
|
| `M2MeshLoadPipelineState` | Internal M2 async-state service | Owns static Mesh pending request records and terminal finalize FIFO | Renderer main thread; map/session | Empty/duplicate/unknown transitions rejected |
|
||||||
|
| `M2MeshResourceCacheState` | Internal M2 Resource cache | Owns prepared static Mesh references by normalized path | Renderer main thread; final shutdown | Empty path/null Mesh rejected; same path replaces |
|
||||||
|
| `M2MeshResourceExtractor` | Internal M2 scene/resource service | Selects first Mesh from direct Resource, PackedScene or Node subtree | Renderer main thread; stateless except temporary instance | Invalid/no Mesh returns null; temporary PackedScene root freed |
|
||||||
| `WmoPlacementResolver.normalize_relative_path/resolve_unique_key/resolve_world_transform` | Internal pure WMO service | Resolves cache key, registry identity and world transform | Main/any thread; stateless | Missing UID uses tile/index fallback; transform fields use historical defaults |
|
| `WmoPlacementResolver.normalize_relative_path/resolve_unique_key/resolve_world_transform` | Internal pure WMO service | Resolves cache key, registry identity and world transform | Main/any thread; stateless | Missing UID uses tile/index fallback; transform fields use historical defaults |
|
||||||
|
| `WmoPlacementRegistry.add_reference/release_reference/contains/active_count/diagnostic_snapshot/clear` | Internal WMO service | Owns placement-key to tile/global reference sets | Renderer main thread; map session | Empty/unknown/non-owner input is rejected without mutation |
|
||||||
|
| `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 |
|
||||||
|
| `AdtWaterSceneFinalizer.attach_water_scene` | Internal liquid main-thread service | Builds and attaches one existing-format ADT Water subtree | Main thread; stateless, returned Node tile-owned | Empty/invalid/dry input returns null without attachment |
|
||||||
|
|
||||||
Публичным contract не считаются `StreamingWorldLoader` methods/properties,
|
Публичным contract не считаются `StreamingWorldLoader` methods/properties,
|
||||||
внутренние dictionaries, queues, job records и generated node names. Scene-owned
|
внутренние dictionaries, queues, job records и generated node names. Scene-owned
|
||||||
@@ -156,6 +173,12 @@ loader configuration remains transitional composition data, not a caller API.
|
|||||||
| Internal grouping | Tile origin, M2 names and placements | Loader / `M2PlacementGrouper` | Loader worker result/build job | Fresh Dictionary/Transform3D arrays | One grouping task |
|
| Internal 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 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 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 | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
|
||||||
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
|
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
|
||||||
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
|
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
|
||||||
@@ -205,7 +228,10 @@ flowchart TD
|
|||||||
M2Grouper --> M2Batch[M2BuildBatchPlanner]
|
M2Grouper --> M2Batch[M2BuildBatchPlanner]
|
||||||
M2Batch --> M2
|
M2Batch --> M2
|
||||||
R --> WmoPlacement[WmoPlacementResolver]
|
R --> WmoPlacement[WmoPlacementResolver]
|
||||||
WmoPlacement --> WMO
|
WmoPlacement --> WmoRegistry[WmoPlacementRegistry]
|
||||||
|
WmoRegistry --> WmoBuildQueue[WmoRenderBuildQueue]
|
||||||
|
WmoBuildQueue --> WmoBuildStep[WmoRenderBuildStepPlanner]
|
||||||
|
WmoBuildStep --> WMO
|
||||||
R --> B[RenderBudgetScheduler permits]
|
R --> B[RenderBudgetScheduler permits]
|
||||||
B --> Terrain[Terrain attach/upgrade]
|
B --> Terrain[Terrain attach/upgrade]
|
||||||
B --> M2[M2 group/MultiMesh attach]
|
B --> M2[M2 group/MultiMesh attach]
|
||||||
@@ -328,7 +354,33 @@ sequenceDiagram
|
|||||||
- `M2BuildBatchPlanner` is stateless and owns only call-local scalar plans. The
|
- `M2BuildBatchPlanner` is stateless and owns only call-local scalar plans. The
|
||||||
loader retains queue/resource transitions, cursor adoption and materialization.
|
loader retains queue/resource transitions, cursor adoption and materialization.
|
||||||
- `WmoPlacementResolver` is stateless and owns only call-local cache-key,
|
- `WmoPlacementResolver` is stateless and owns only call-local cache-key,
|
||||||
identity and transform values. The loader retains WMO refs/caches/jobs/resources.
|
identity and transform values. `WmoPlacementRegistry` owns only placement-key
|
||||||
|
reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation
|
||||||
|
and cursor plan. `WmoRenderBuildQueue` owns typed pending jobs, FIFO keys and
|
||||||
|
strong root/resource references without freeing engine objects.
|
||||||
|
`WmoRenderResourceCacheState` owns validated render Resources, negative entries
|
||||||
|
and pending cache paths. `WmoSceneResourceCacheState` similarly owns validated
|
||||||
|
PackedScenes, negative entries and pending `.tscn` paths. The loader retains
|
||||||
|
ResourceLoader/FileAccess I/O, size and cache-version validation, live fallback,
|
||||||
|
materialization, permits, validity reactions and every Node lifecycle action.
|
||||||
|
- `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 `water_loaded` timing. `AdtWaterSceneFinalizer`
|
||||||
|
performs the existing ADTBuilder call, tile attach and optional recursive
|
||||||
|
Editor ownership on the main thread without retaining the generated subtree.
|
||||||
|
- `M2RuntimeMeshRebuildClassifier` owns only normalized-path boolean decisions
|
||||||
|
for billboard/UV-rotation material refresh. The loader retains raw M2 loading,
|
||||||
|
refresh-version checks, Mesh rebuild/adoption and both historical clear sites.
|
||||||
|
- `M2MeshLoadPipelineState` owns static M2 pending Resource paths, opaque
|
||||||
|
terminal statuses and completion-order finalize FIFO. The loader retains cache
|
||||||
|
path selection, ResourceLoader calls, permits, shared missing state and adoption.
|
||||||
|
- `M2MeshResourceCacheState` owns prepared static Mesh references and releases
|
||||||
|
them at the existing final-shutdown site. The loader retains material
|
||||||
|
refresh/rebuild, missing/prototype state and materialization.
|
||||||
|
- `M2MeshResourceExtractor` owns depth-first first-Mesh selection and temporary
|
||||||
|
PackedScene instance destruction. The loader retains ResourceLoader I/O,
|
||||||
|
runtime preparation, cache/missing adoption and materialization.
|
||||||
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
|
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
|
||||||
the facade never returns Mesh, Node, tile-state or queue references.
|
the facade never returns Mesh, Node, tile-state or queue references.
|
||||||
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
|
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
|
||||||
@@ -422,6 +474,21 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
count, completion/cursor rules, source boundary and bounded timing.
|
count, completion/cursor rules, source boundary and bounded timing.
|
||||||
- WMO placement contract: cache-path normalization, positive/fallback identity,
|
- WMO placement contract: cache-path normalization, positive/fallback identity,
|
||||||
default/rotated/unclamped transforms, three consumers and bounded timing.
|
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
|
- Budget scheduler contract: exact lane exhaustion, shared chunk removal/create
|
||||||
priority, independent lanes, frame reset, invalid limits, terminal cancellation,
|
priority, independent lanes, frame reset, invalid limits, terminal cancellation,
|
||||||
dependency boundaries and bounded permit timing without loading a world scene.
|
dependency boundaries and bounded permit timing without loading a world scene.
|
||||||
@@ -455,8 +522,14 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| M2 placement transform resolver | Implemented extraction | Scene-free formula/source/timing contract across three consumers | Asset-backed visual recheck and general placement parity pending |
|
| M2 placement 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 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 |
|
| M2 build batch planner | Implemented extraction | Scene-free limit/count/cursor/source/timing contract | Queue/resource state, spatial cells and asset-backed p95/p99 remain pending |
|
||||||
| WMO placement resolver | Implemented extraction | Scene-free path/identity/transform/source/timing contract | Registry/build extraction and asset-backed comparison pending |
|
| WMO placement resolver | Implemented extraction | Scene-free path/identity/transform/source/timing contract | Asset-backed comparison pending |
|
||||||
|
| WMO placement registry | Implemented extraction | Scene-free ownership/lifecycle/source/timing contract | Build/resource state and asset-backed cross-tile corpus pending |
|
||||||
|
| WMO render build step planner | Implemented extraction | Scene-free order/cursor/source/timing contract | Asset-backed traversal p95/p99 pending |
|
||||||
|
| WMO render build queue | Implemented extraction | Typed lifecycle/order/ownership/source/timing contract | Materialization and asset-backed traversal/leak evidence pending |
|
||||||
|
| WMO render Resource cache state | Implemented extraction | Scene-free lifecycle/exclusivity/source/timing plus shutdown contract | ResourceLoader I/O and asset-backed traversal/leak evidence pending |
|
||||||
|
| WMO scene Resource cache state | Implemented extraction | Scene-free lifecycle/direct-missing/source/timing plus shutdown contract | ResourceLoader/live-fallback extraction and asset-backed traversal/leak evidence pending |
|
||||||
| WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity |
|
| 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 |
|
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
|
||||||
| Sky/light | Partial | DBC controller/logs | Zone transition paired validation |
|
| Sky/light | Partial | DBC controller/logs | Zone transition paired validation |
|
||||||
| Character presentation | Prototype | Character creator/player experiment | Full customization/equipment states |
|
| Character presentation | Prototype | Character creator/player experiment | Full customization/equipment states |
|
||||||
@@ -506,6 +579,12 @@ 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/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/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/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/liquid/adt_water_scene_finalizer.gd` | Main-thread ADT water build, tile attachment and optional editor ownership |
|
||||||
|
| `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Billboard/UV-rotation stale cached-mesh rebuild decision and memoization |
|
||||||
|
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Static M2 pending Resource paths, terminal statuses and finalize FIFO |
|
||||||
|
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared static M2 Mesh references and final-shutdown release |
|
||||||
|
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene instance lifetime |
|
||||||
| `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
|
| `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
|
||||||
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
|
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
|
||||||
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets |
|
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets |
|
||||||
@@ -518,6 +597,12 @@ 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_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_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_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_adt_water_scene_finalizer.gd` | Synthetic ADT water scene/ownership/boundary/timing regression |
|
||||||
|
| `src/tools/verify_m2_runtime_mesh_rebuild_classifier.gd` | M2 rebuild predicate/cache/boundary/timing regression |
|
||||||
|
| `src/tools/verify_m2_mesh_load_pipeline_state.gd` | Static M2 request/terminal/FIFO/boundary/timing regression |
|
||||||
|
| `src/tools/verify_m2_mesh_resource_cache_state.gd` | Static M2 Mesh cache ownership/lifetime/boundary/timing regression |
|
||||||
|
| `src/tools/verify_m2_mesh_resource_extractor.gd` | M2 direct/PackedScene/subtree order/lifetime/boundary/timing regression |
|
||||||
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
|
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
|
||||||
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
|
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
|
||||||
| `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
|
| `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
|
||||||
|
|||||||
@@ -0,0 +1,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
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
class_name AdtWaterSceneFinalizer
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Builds and attaches one ADT water subtree on the renderer main thread. The
|
||||||
|
## supplied tile root owns the returned subtree; this service retains no state.
|
||||||
|
|
||||||
|
const ADT_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/adt_builder.gd")
|
||||||
|
|
||||||
|
|
||||||
|
## Builds water from parsed ADT data and attaches it once to [param tile_root].
|
||||||
|
## [param tile_origin_godot_units] is the existing Godot-world tile origin used
|
||||||
|
## by [ADTBuilder]. When [param editor_owner] is non-null and is an ancestor,
|
||||||
|
## ownership is assigned recursively for persisted Editor previews. Returns the
|
||||||
|
## attached borrowed root, or null for empty/invalid input or dry ADT data.
|
||||||
|
## This method mutates the SceneTree and must run on the main thread.
|
||||||
|
func attach_water_scene(
|
||||||
|
water_data: Dictionary,
|
||||||
|
tile_origin_godot_units: Vector3,
|
||||||
|
tile_root: Node,
|
||||||
|
editor_owner: Node = null) -> Node3D:
|
||||||
|
if water_data.is_empty() or tile_root == null or not is_instance_valid(tile_root):
|
||||||
|
return null
|
||||||
|
|
||||||
|
var water_root: Node3D = ADT_BUILDER_SCRIPT.build_tile_water_scene(
|
||||||
|
water_data,
|
||||||
|
tile_origin_godot_units
|
||||||
|
)
|
||||||
|
if water_root == null:
|
||||||
|
return null
|
||||||
|
|
||||||
|
tile_root.add_child(water_root)
|
||||||
|
if editor_owner != null and is_instance_valid(editor_owner):
|
||||||
|
_assign_owner_recursive(water_root, editor_owner)
|
||||||
|
return water_root
|
||||||
|
|
||||||
|
|
||||||
|
func _assign_owner_recursive(generated_node: Node, editor_owner: Node) -> void:
|
||||||
|
generated_node.owner = editor_owner
|
||||||
|
for child in generated_node.get_children():
|
||||||
|
_assign_owner_recursive(child, editor_owner)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ck4sdc8a3kg00
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
class_name M2MeshLoadPipelineState
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Owns static M2 threaded-load request records and the terminal finalize FIFO.
|
||||||
|
## The caller owns ResourceLoader I/O, permits, Mesh caches and materialization.
|
||||||
|
|
||||||
|
var _request_by_normalized_path: Dictionary = {}
|
||||||
|
var _finalize_records: Array[Dictionary] = []
|
||||||
|
|
||||||
|
|
||||||
|
## Records one normalized M2 path and ResourceLoader path. Empty or duplicate
|
||||||
|
## values are rejected without mutation.
|
||||||
|
func remember_request(normalized_relative_path: String, resource_path: String) -> bool:
|
||||||
|
if normalized_relative_path.is_empty() or resource_path.is_empty():
|
||||||
|
return false
|
||||||
|
if _request_by_normalized_path.has(normalized_relative_path):
|
||||||
|
return false
|
||||||
|
_request_by_normalized_path[normalized_relative_path] = {
|
||||||
|
"normalized": normalized_relative_path,
|
||||||
|
"path": resource_path,
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether one normalized path currently has a pending threaded request.
|
||||||
|
func has_request(normalized_relative_path: String) -> bool:
|
||||||
|
return (
|
||||||
|
not normalized_relative_path.is_empty()
|
||||||
|
and _request_by_normalized_path.has(normalized_relative_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns detached pending records in their existing Dictionary insertion order.
|
||||||
|
func request_records_snapshot() -> Array[Dictionary]:
|
||||||
|
var records: Array[Dictionary] = []
|
||||||
|
for request_variant in _request_by_normalized_path.values():
|
||||||
|
var request: Dictionary = request_variant
|
||||||
|
records.append(request.duplicate())
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
## Removes one pending request and appends its terminal status to the finalize
|
||||||
|
## FIFO. Unknown paths are rejected without mutation.
|
||||||
|
func complete_request(normalized_relative_path: String, terminal_status: int) -> bool:
|
||||||
|
if not _request_by_normalized_path.has(normalized_relative_path):
|
||||||
|
return false
|
||||||
|
var request: Dictionary = _request_by_normalized_path[normalized_relative_path]
|
||||||
|
_request_by_normalized_path.erase(normalized_relative_path)
|
||||||
|
var finalize_record := request.duplicate()
|
||||||
|
finalize_record["status"] = terminal_status
|
||||||
|
_finalize_records.append(finalize_record)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Removes one pending request without enqueuing finalization. Returns whether a
|
||||||
|
## record was removed.
|
||||||
|
func discard_request(normalized_relative_path: String) -> bool:
|
||||||
|
return _request_by_normalized_path.erase(normalized_relative_path)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether a terminal record is waiting for budgeted finalization.
|
||||||
|
func has_finalize_record() -> bool:
|
||||||
|
return not _finalize_records.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
## Removes and returns the oldest terminal record. Empty means none is ready.
|
||||||
|
func pop_finalize_record() -> Dictionary:
|
||||||
|
if _finalize_records.is_empty():
|
||||||
|
return {}
|
||||||
|
return _finalize_records.pop_front()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns pending plus terminal-finalize work for existing renderer metrics.
|
||||||
|
func total_work_count() -> int:
|
||||||
|
return _request_by_normalized_path.size() + _finalize_records.size()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the pending request count.
|
||||||
|
func pending_request_count() -> int:
|
||||||
|
return _request_by_normalized_path.size()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the terminal finalize FIFO count.
|
||||||
|
func finalize_record_count() -> int:
|
||||||
|
return _finalize_records.size()
|
||||||
|
|
||||||
|
|
||||||
|
## Clears request/finalize bookkeeping. The caller must drain ResourceLoader
|
||||||
|
## requests before orderly shutdown clear.
|
||||||
|
func clear() -> void:
|
||||||
|
_request_by_normalized_path.clear()
|
||||||
|
_finalize_records.clear()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns detached request and finalize records without loaded Resources/Meshes.
|
||||||
|
func diagnostic_snapshot() -> Dictionary:
|
||||||
|
var finalize_records: Array[Dictionary] = []
|
||||||
|
for record in _finalize_records:
|
||||||
|
finalize_records.append(record.duplicate())
|
||||||
|
return {
|
||||||
|
"requests": request_records_snapshot(),
|
||||||
|
"finalize_records": finalize_records,
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dadsxuswq0tnd
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
class_name M2MeshResourceCacheState
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Owns normalized-path references to prepared static M2 Mesh resources.
|
||||||
|
## The caller owns loading, preparation, missing-state and materialization.
|
||||||
|
|
||||||
|
var _mesh_by_normalized_path: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
## Stores one prepared Mesh for a non-empty normalized M2 path. A later store
|
||||||
|
## for the same path replaces the retained reference.
|
||||||
|
func store_mesh(normalized_relative_path: String, mesh: Mesh) -> bool:
|
||||||
|
if normalized_relative_path.is_empty() or mesh == null:
|
||||||
|
return false
|
||||||
|
_mesh_by_normalized_path[normalized_relative_path] = mesh
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether one normalized path currently has a retained Mesh.
|
||||||
|
func has_mesh(normalized_relative_path: String) -> bool:
|
||||||
|
return (
|
||||||
|
not normalized_relative_path.is_empty()
|
||||||
|
and _mesh_by_normalized_path.has(normalized_relative_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the exact retained Mesh reference, or null for empty/unknown paths.
|
||||||
|
func find_mesh(normalized_relative_path: String) -> Mesh:
|
||||||
|
if not has_mesh(normalized_relative_path):
|
||||||
|
return null
|
||||||
|
return _mesh_by_normalized_path[normalized_relative_path] as Mesh
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the number of retained static M2 Mesh resources.
|
||||||
|
func mesh_count() -> int:
|
||||||
|
return _mesh_by_normalized_path.size()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns a detached insertion-order list of cached normalized paths.
|
||||||
|
func normalized_paths_snapshot() -> Array[String]:
|
||||||
|
var normalized_paths: Array[String] = []
|
||||||
|
for normalized_path_variant in _mesh_by_normalized_path.keys():
|
||||||
|
normalized_paths.append(String(normalized_path_variant))
|
||||||
|
return normalized_paths
|
||||||
|
|
||||||
|
|
||||||
|
## Releases all retained Mesh references. The loader calls this only after
|
||||||
|
## asynchronous work has completed during final scene shutdown.
|
||||||
|
func clear() -> void:
|
||||||
|
_mesh_by_normalized_path.clear()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dhkyjcrdblgkd
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
class_name M2MeshResourceExtractor
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Selects the first Mesh from loaded M2 Resources or existing Node subtrees.
|
||||||
|
## The caller owns ResourceLoader I/O, cache adoption and material preparation.
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the direct Mesh Resource or the first Mesh in a temporary PackedScene
|
||||||
|
## instance. The temporary instance is freed before this method returns.
|
||||||
|
func extract_first_mesh(resource: Resource) -> Mesh:
|
||||||
|
if resource is Mesh:
|
||||||
|
return resource as Mesh
|
||||||
|
if not (resource is PackedScene):
|
||||||
|
return null
|
||||||
|
var temporary_root := (resource as PackedScene).instantiate()
|
||||||
|
if temporary_root == null:
|
||||||
|
return null
|
||||||
|
var mesh := find_first_mesh_in_subtree(temporary_root)
|
||||||
|
temporary_root.free()
|
||||||
|
return mesh
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the first MeshInstance3D Mesh in depth-first preorder. The subtree
|
||||||
|
## and Mesh ownership remain unchanged.
|
||||||
|
func find_first_mesh_in_subtree(root: Node) -> Mesh:
|
||||||
|
if root == null:
|
||||||
|
return null
|
||||||
|
if root is MeshInstance3D:
|
||||||
|
return (root as MeshInstance3D).mesh
|
||||||
|
for child in root.get_children():
|
||||||
|
var mesh := find_first_mesh_in_subtree(child)
|
||||||
|
if mesh != null:
|
||||||
|
return mesh
|
||||||
|
return null
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dea4gekrxshjf
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
class_name M2RuntimeMeshRebuildClassifier
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Memoizes whether raw M2 billboard or UV-rotation metadata requires replacing
|
||||||
|
## a stale cached runtime mesh. Owns no parser, Mesh, material, Node or RID.
|
||||||
|
|
||||||
|
const UV_ROTATION_SPEED_EPSILON := 0.000001
|
||||||
|
const MAXIMUM_TEXTURE_STAGES := 4
|
||||||
|
|
||||||
|
var _rebuild_required_by_normalized_path: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the first memoized rebuild decision for [param normalized_relative_path].
|
||||||
|
## [param raw_m2_data] is borrowed and not mutated. The decision remains cached
|
||||||
|
## until [method clear], matching the loader map/shutdown reset lifetime.
|
||||||
|
func needs_runtime_mesh_rebuild(
|
||||||
|
normalized_relative_path: String,
|
||||||
|
raw_m2_data: Dictionary) -> bool:
|
||||||
|
if _rebuild_required_by_normalized_path.has(normalized_relative_path):
|
||||||
|
return bool(_rebuild_required_by_normalized_path[normalized_relative_path])
|
||||||
|
var rebuild_required := (
|
||||||
|
_raw_data_has_billboards(raw_m2_data)
|
||||||
|
or _raw_data_has_uv_rotation(raw_m2_data)
|
||||||
|
)
|
||||||
|
_rebuild_required_by_normalized_path[normalized_relative_path] = rebuild_required
|
||||||
|
return rebuild_required
|
||||||
|
|
||||||
|
|
||||||
|
## Clears every memoized path decision. No Mesh or resource is retained or freed.
|
||||||
|
func clear() -> void:
|
||||||
|
_rebuild_required_by_normalized_path.clear()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the number of memoized normalized paths.
|
||||||
|
func cached_path_count() -> int:
|
||||||
|
return _rebuild_required_by_normalized_path.size()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns a detached normalized-path to boolean decision snapshot.
|
||||||
|
func diagnostic_snapshot() -> Dictionary:
|
||||||
|
return _rebuild_required_by_normalized_path.duplicate()
|
||||||
|
|
||||||
|
|
||||||
|
func _raw_data_has_billboards(raw_m2_data: Dictionary) -> bool:
|
||||||
|
for batch_variant in raw_m2_data.get("batches", []):
|
||||||
|
if not (batch_variant is Dictionary):
|
||||||
|
continue
|
||||||
|
var batch: Dictionary = batch_variant
|
||||||
|
if (
|
||||||
|
bool(batch.get("has_billboard", false))
|
||||||
|
or int(batch.get("billboard_vertex_count", 0)) > 0
|
||||||
|
):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _raw_data_has_uv_rotation(raw_m2_data: Dictionary) -> bool:
|
||||||
|
var texture_transforms: Array = raw_m2_data.get("texture_transforms", [])
|
||||||
|
if texture_transforms.is_empty():
|
||||||
|
return false
|
||||||
|
var texture_transform_combos: PackedInt32Array = raw_m2_data.get(
|
||||||
|
"texture_transform_combos",
|
||||||
|
PackedInt32Array()
|
||||||
|
)
|
||||||
|
if texture_transform_combos.is_empty():
|
||||||
|
return false
|
||||||
|
for batch_variant in raw_m2_data.get("batches", []):
|
||||||
|
if not (batch_variant is Dictionary):
|
||||||
|
continue
|
||||||
|
var batch: Dictionary = batch_variant
|
||||||
|
var texture_count := maxi(1, int(batch.get("texture_count", 1)))
|
||||||
|
var base_combo_index := int(batch.get("texture_transform_combo_index", -1))
|
||||||
|
for texture_stage in mini(texture_count, MAXIMUM_TEXTURE_STAGES):
|
||||||
|
var combo_index := base_combo_index + texture_stage
|
||||||
|
if combo_index < 0 or combo_index >= texture_transform_combos.size():
|
||||||
|
continue
|
||||||
|
var transform_index := int(texture_transform_combos[combo_index])
|
||||||
|
if transform_index < 0 or transform_index >= texture_transforms.size():
|
||||||
|
continue
|
||||||
|
var texture_transform: Dictionary = (
|
||||||
|
texture_transforms[transform_index]
|
||||||
|
if texture_transforms[transform_index] is Dictionary
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
var rotation: Vector4 = texture_transform.get(
|
||||||
|
"rotation",
|
||||||
|
Vector4(1.0, 0.0, 0.0, 1.0)
|
||||||
|
)
|
||||||
|
if not rotation.is_equal_approx(Vector4(1.0, 0.0, 0.0, 1.0)):
|
||||||
|
return true
|
||||||
|
if absf(float(texture_transform.get("rotation_speed", 0.0))) > UV_ROTATION_SPEED_EPSILON:
|
||||||
|
return true
|
||||||
|
return false
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://breu8vpmm88en
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
class_name WmoRenderBuildJob
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Holds one lightweight-WMO build cursor and strong engine-object references.
|
||||||
|
## Releasing this record never frees the referenced Node or Resource.
|
||||||
|
|
||||||
|
var _placement_key: String
|
||||||
|
var _root: Node3D
|
||||||
|
var _render_resource: Resource
|
||||||
|
var _mesh_index: int = 0
|
||||||
|
var _multimesh_index: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
func _init(placement_key: String, root: Node3D, render_resource: Resource) -> void:
|
||||||
|
_placement_key = placement_key
|
||||||
|
_root = root
|
||||||
|
_render_resource = render_resource
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the immutable placement identity used by the queue.
|
||||||
|
func placement_key() -> String:
|
||||||
|
return _placement_key
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the strongly referenced scene root without transferring ownership.
|
||||||
|
func root() -> Node3D:
|
||||||
|
return _root
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the strongly referenced render resource without transferring ownership.
|
||||||
|
func render_resource() -> Resource:
|
||||||
|
return _render_resource
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the current mesh-group cursor.
|
||||||
|
func mesh_index() -> int:
|
||||||
|
return _mesh_index
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the current doodad MultiMesh-group cursor.
|
||||||
|
func multimesh_index() -> int:
|
||||||
|
return _multimesh_index
|
||||||
|
|
||||||
|
|
||||||
|
## Atomically adopts both cursor values returned by the pure step planner.
|
||||||
|
func adopt_cursors(next_mesh_index: int, next_multimesh_index: int) -> void:
|
||||||
|
_mesh_index = next_mesh_index
|
||||||
|
_multimesh_index = next_multimesh_index
|
||||||
|
|
||||||
|
|
||||||
|
## Returns detached scalar state and never exposes engine-object references.
|
||||||
|
func diagnostic_snapshot() -> Dictionary:
|
||||||
|
return {
|
||||||
|
"placement_key": _placement_key,
|
||||||
|
"mesh_index": _mesh_index,
|
||||||
|
"multimesh_index": _multimesh_index,
|
||||||
|
"has_root": _root != null,
|
||||||
|
"has_render_resource": _render_resource != null,
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://gyjq5dqueyi7
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
class_name WmoRenderBuildQueue
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Owns pending lightweight-WMO job records and FIFO placement keys.
|
||||||
|
|
||||||
|
const JOB_SCRIPT := preload("res://src/render/wmo/wmo_render_build_job.gd")
|
||||||
|
|
||||||
|
var _jobs_by_placement_key: Dictionary = {}
|
||||||
|
var _queued_placement_keys: Array[String] = []
|
||||||
|
|
||||||
|
|
||||||
|
## Enqueues a new typed job. Duplicate placement keys replace the keyed job but
|
||||||
|
## append another FIFO entry, preserving the loader's historical behavior.
|
||||||
|
func enqueue(placement_key: String, root: Node3D, render_resource: Resource) -> bool:
|
||||||
|
if placement_key.is_empty() or root == null or render_resource == null:
|
||||||
|
return false
|
||||||
|
_jobs_by_placement_key[placement_key] = JOB_SCRIPT.new(
|
||||||
|
placement_key,
|
||||||
|
root,
|
||||||
|
render_resource
|
||||||
|
)
|
||||||
|
_queued_placement_keys.append(placement_key)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether at least one FIFO key, including a stale key, is pending.
|
||||||
|
func has_pending() -> bool:
|
||||||
|
return not _queued_placement_keys.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the front FIFO key, or an empty String when the queue is empty.
|
||||||
|
func front_key() -> String:
|
||||||
|
if _queued_placement_keys.is_empty():
|
||||||
|
return ""
|
||||||
|
return _queued_placement_keys.front()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether a FIFO placement key still has a current job record.
|
||||||
|
func has_job(placement_key: String) -> bool:
|
||||||
|
return _jobs_by_placement_key.has(placement_key)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the current job record as its engine base type, or null for a stale
|
||||||
|
## key. Runtime consumers should prefer the typed accessors below.
|
||||||
|
func job_for(placement_key: String) -> RefCounted:
|
||||||
|
return _jobs_by_placement_key.get(placement_key, null) as RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the borrowed root for a current job, or null for a stale key.
|
||||||
|
func root_for(placement_key: String) -> Node3D:
|
||||||
|
var job := job_for(placement_key)
|
||||||
|
if job == null:
|
||||||
|
return null
|
||||||
|
return job.call("root") as Node3D
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the borrowed render resource for a current job, or null when stale.
|
||||||
|
func render_resource_for(placement_key: String) -> Resource:
|
||||||
|
var job := job_for(placement_key)
|
||||||
|
if job == null:
|
||||||
|
return null
|
||||||
|
return job.call("render_resource") as Resource
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the current mesh cursor, or zero for a stale key.
|
||||||
|
func mesh_index_for(placement_key: String) -> int:
|
||||||
|
var job := job_for(placement_key)
|
||||||
|
if job == null:
|
||||||
|
return 0
|
||||||
|
return int(job.call("mesh_index"))
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the current MultiMesh cursor, or zero for a stale key.
|
||||||
|
func multimesh_index_for(placement_key: String) -> int:
|
||||||
|
var job := job_for(placement_key)
|
||||||
|
if job == null:
|
||||||
|
return 0
|
||||||
|
return int(job.call("multimesh_index"))
|
||||||
|
|
||||||
|
|
||||||
|
## Adopts both cursors for a current job and returns whether it existed.
|
||||||
|
func adopt_cursors(
|
||||||
|
placement_key: String,
|
||||||
|
next_mesh_index: int,
|
||||||
|
next_multimesh_index: int
|
||||||
|
) -> bool:
|
||||||
|
var job := job_for(placement_key)
|
||||||
|
if job == null:
|
||||||
|
return false
|
||||||
|
job.call("adopt_cursors", next_mesh_index, next_multimesh_index)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Pops and returns the front FIFO key, or an empty String when already empty.
|
||||||
|
func pop_front() -> String:
|
||||||
|
if _queued_placement_keys.is_empty():
|
||||||
|
return ""
|
||||||
|
return _queued_placement_keys.pop_front()
|
||||||
|
|
||||||
|
|
||||||
|
## Cancels the keyed job and erases only the first matching FIFO key. Returns
|
||||||
|
## whether an active job record existed. Engine objects are never freed here.
|
||||||
|
func cancel(placement_key: String) -> bool:
|
||||||
|
var had_job := _jobs_by_placement_key.has(placement_key)
|
||||||
|
_jobs_by_placement_key.erase(placement_key)
|
||||||
|
_queued_placement_keys.erase(placement_key)
|
||||||
|
return had_job
|
||||||
|
|
||||||
|
|
||||||
|
## Releases all job/key references without freeing referenced engine objects.
|
||||||
|
func clear() -> void:
|
||||||
|
_jobs_by_placement_key.clear()
|
||||||
|
_queued_placement_keys.clear()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns FIFO entry count, including duplicate and stale keys.
|
||||||
|
func pending_count() -> int:
|
||||||
|
return _queued_placement_keys.size()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns active keyed job count, excluding stale FIFO keys.
|
||||||
|
func active_job_count() -> int:
|
||||||
|
return _jobs_by_placement_key.size()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns detached FIFO order and placement-key-sorted scalar job diagnostics.
|
||||||
|
func diagnostic_snapshot() -> Dictionary:
|
||||||
|
var placement_keys: Array = _jobs_by_placement_key.keys()
|
||||||
|
placement_keys.sort()
|
||||||
|
var jobs: Array[Dictionary] = []
|
||||||
|
for placement_key_variant in placement_keys:
|
||||||
|
var placement_key := String(placement_key_variant)
|
||||||
|
var job: RefCounted = job_for(placement_key)
|
||||||
|
if job != null:
|
||||||
|
jobs.append(job.call("diagnostic_snapshot") as Dictionary)
|
||||||
|
return {
|
||||||
|
"queued_placement_keys": _queued_placement_keys.duplicate(),
|
||||||
|
"jobs": jobs,
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b165sgg76lrt8
|
||||||
@@ -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
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,350 @@
|
|||||||
|
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",
|
||||||
|
"_adt_water_scene_finalizer.attach_water_scene(",
|
||||||
|
"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
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Synthetic main-thread scene, ownership, boundary and timing regression for
|
||||||
|
## ADT water finalization.
|
||||||
|
|
||||||
|
const FINALIZER_SCRIPT := preload("res://src/render/liquid/adt_water_scene_finalizer.gd")
|
||||||
|
const FINALIZER_PATH := "res://src/render/liquid/adt_water_scene_finalizer.gd"
|
||||||
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var failures: Array[String] = []
|
||||||
|
_verify_invalid_and_dry_input(failures)
|
||||||
|
_verify_build_attach_geometry_and_owner(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_SCENE_FINALIZER: %s" % failure)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
"ADT_WATER_SCENE_FINALIZER PASS cases=8 iterations=100 elapsed_ms=%.3f"
|
||||||
|
% elapsed_milliseconds
|
||||||
|
)
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_invalid_and_dry_input(failures: Array[String]) -> void:
|
||||||
|
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||||
|
var tile_root := Node3D.new()
|
||||||
|
_expect_null(
|
||||||
|
finalizer.call("attach_water_scene", {}, Vector3.ZERO, tile_root, null),
|
||||||
|
"empty water data returns null",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(tile_root.get_child_count(), 0, "empty data does not mutate tile", failures)
|
||||||
|
_expect_null(
|
||||||
|
finalizer.call("attach_water_scene", _synthetic_water_data(), Vector3.ZERO, null, null),
|
||||||
|
"missing tile root returns null",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_null(
|
||||||
|
finalizer.call(
|
||||||
|
"attach_water_scene",
|
||||||
|
{"chunks": [{"origin": Vector3.ZERO, "liquids": []}]},
|
||||||
|
Vector3.ZERO,
|
||||||
|
tile_root,
|
||||||
|
null
|
||||||
|
),
|
||||||
|
"dry parsed ADT returns null",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(tile_root.get_child_count(), 0, "dry data does not mutate tile", failures)
|
||||||
|
tile_root.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_build_attach_geometry_and_owner(failures: Array[String]) -> void:
|
||||||
|
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||||
|
var editor_scene_root := Node3D.new()
|
||||||
|
var tile_root := Node3D.new()
|
||||||
|
get_root().add_child(editor_scene_root)
|
||||||
|
editor_scene_root.add_child(tile_root)
|
||||||
|
|
||||||
|
var tile_origin := Vector3(10.0, 20.0, 30.0)
|
||||||
|
var water_root: Node3D = finalizer.call(
|
||||||
|
"attach_water_scene",
|
||||||
|
_synthetic_water_data(tile_origin),
|
||||||
|
tile_origin,
|
||||||
|
tile_root,
|
||||||
|
editor_scene_root
|
||||||
|
)
|
||||||
|
_expect_not_null(water_root, "synthetic liquid builds a water root", failures)
|
||||||
|
if water_root != null:
|
||||||
|
_expect_true(water_root.get_parent() == tile_root, "water root attached once", failures)
|
||||||
|
_expect_equal(tile_root.get_child_count(), 1, "tile owns one water root", failures)
|
||||||
|
_expect_true(water_root.owner == editor_scene_root, "water root editor owner", failures)
|
||||||
|
_expect_equal(water_root.get_child_count(), 1, "one liquid surface grouped", failures)
|
||||||
|
if water_root.get_child_count() == 1:
|
||||||
|
var liquid_mesh_instance := water_root.get_child(0) as MeshInstance3D
|
||||||
|
_expect_not_null(liquid_mesh_instance, "liquid child is MeshInstance3D", failures)
|
||||||
|
if liquid_mesh_instance != null:
|
||||||
|
_expect_true(
|
||||||
|
liquid_mesh_instance.owner == editor_scene_root,
|
||||||
|
"liquid child editor owner",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
var liquid_mesh := liquid_mesh_instance.mesh as ArrayMesh
|
||||||
|
_expect_not_null(liquid_mesh, "liquid surface has ArrayMesh", failures)
|
||||||
|
if liquid_mesh != null:
|
||||||
|
_expect_equal(
|
||||||
|
liquid_mesh.surface_get_array_len(0),
|
||||||
|
4,
|
||||||
|
"one enabled liquid cell has four vertices",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(
|
||||||
|
liquid_mesh.surface_get_array_index_len(0),
|
||||||
|
6,
|
||||||
|
"one enabled liquid cell has six indices",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
editor_scene_root.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||||
|
var finalizer_source := FileAccess.get_file_as_string(FINALIZER_PATH)
|
||||||
|
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||||
|
_expect_true(
|
||||||
|
finalizer_source.contains("ADT_BUILDER_SCRIPT.build_tile_water_scene"),
|
||||||
|
"finalizer retains existing ADTBuilder implementation",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
finalizer_source.contains("tile_root.add_child(water_root)"),
|
||||||
|
"finalizer owns water subtree attachment",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(
|
||||||
|
loader_source.count("_adt_water_scene_finalizer.attach_water_scene("),
|
||||||
|
2,
|
||||||
|
"both loader water paths delegate finalization",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_false(
|
||||||
|
loader_source.contains("_builder.build_tile_water_scene"),
|
||||||
|
"loader no longer builds water scenes directly",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
loader_source.contains("state[\"water_loaded\"] = true"),
|
||||||
|
"loader retains water-loaded timing",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
loader_source.contains("WATER_FINALIZE"),
|
||||||
|
"loader retains finalization permit ownership",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||||
|
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||||
|
var tile_root := Node3D.new()
|
||||||
|
var started_microseconds := Time.get_ticks_usec()
|
||||||
|
for _iteration in range(100):
|
||||||
|
finalizer.call("attach_water_scene", {}, Vector3.ZERO, tile_root, null)
|
||||||
|
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||||
|
_expect_true(elapsed_milliseconds < 1000.0, "100 empty attempts under 1 second", failures)
|
||||||
|
_expect_equal(tile_root.get_child_count(), 0, "timing loop creates no nodes", failures)
|
||||||
|
tile_root.free()
|
||||||
|
return elapsed_milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
func _synthetic_water_data(tile_origin: Vector3 = Vector3.ZERO) -> Dictionary:
|
||||||
|
var liquid_mask := PackedByteArray()
|
||||||
|
liquid_mask.resize(64)
|
||||||
|
liquid_mask.fill(0)
|
||||||
|
liquid_mask[0] = 1
|
||||||
|
var liquid_heights := PackedFloat32Array()
|
||||||
|
liquid_heights.resize(81)
|
||||||
|
liquid_heights.fill(tile_origin.y + 5.0)
|
||||||
|
return {
|
||||||
|
"chunks": [{
|
||||||
|
"origin": tile_origin,
|
||||||
|
"liquids": [{
|
||||||
|
"liquid_id": 7,
|
||||||
|
"mask": liquid_mask,
|
||||||
|
"heights": liquid_heights,
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(label)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
_expect_true(not condition, label, failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_null(value: Variant, label: String, failures: Array[String]) -> void:
|
||||||
|
_expect_true(value == null, label, failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_not_null(value: Variant, label: String, failures: Array[String]) -> void:
|
||||||
|
_expect_true(value != null, label, failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual != expected:
|
||||||
|
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c1gqfpxg7ajsh
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Synthetic request/terminal/FIFO/lifecycle/boundary/timing regression for
|
||||||
|
## static M2 threaded mesh loading.
|
||||||
|
|
||||||
|
const PIPELINE_SCRIPT := preload("res://src/render/m2/m2_mesh_load_pipeline_state.gd")
|
||||||
|
const PIPELINE_PATH := "res://src/render/m2/m2_mesh_load_pipeline_state.gd"
|
||||||
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var failures: Array[String] = []
|
||||||
|
_verify_validation_dedupe_and_order(failures)
|
||||||
|
_verify_terminal_transition_and_fifo(failures)
|
||||||
|
_verify_discard_metrics_clear_and_diagnostics(failures)
|
||||||
|
_verify_ownership_boundaries(failures)
|
||||||
|
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||||
|
if not failures.is_empty():
|
||||||
|
for failure in failures:
|
||||||
|
push_error("M2_MESH_LOAD_PIPELINE_STATE: %s" % failure)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
"M2_MESH_LOAD_PIPELINE_STATE PASS cases=11 iterations=100 elapsed_ms=%.3f"
|
||||||
|
% elapsed_milliseconds
|
||||||
|
)
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_validation_dedupe_and_order(failures: Array[String]) -> void:
|
||||||
|
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||||
|
_expect_false(pipeline.call("remember_request", "", "res://a.tscn"), "empty normalized path rejected", failures)
|
||||||
|
_expect_false(pipeline.call("remember_request", "world/a.m2", ""), "empty resource path rejected", failures)
|
||||||
|
_expect_true(pipeline.call("remember_request", "world/a.m2", "res://a.tscn"), "first request accepted", failures)
|
||||||
|
_expect_false(pipeline.call("remember_request", "world/a.m2", "res://duplicate.glb"), "duplicate request rejected", failures)
|
||||||
|
_expect_true(pipeline.call("remember_request", "world/b.m2", "res://b.glb"), "second request accepted", failures)
|
||||||
|
_expect_true(pipeline.call("has_request", "world/a.m2"), "request observable", failures)
|
||||||
|
var records: Array[Dictionary] = pipeline.call("request_records_snapshot")
|
||||||
|
_expect_equal(records.size(), 2, "request record count", failures)
|
||||||
|
_expect_string_equal(String(records[0]["normalized"]), "world/a.m2", "request insertion order first", failures)
|
||||||
|
_expect_string_equal(String(records[1]["normalized"]), "world/b.m2", "request insertion order second", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_terminal_transition_and_fifo(failures: Array[String]) -> void:
|
||||||
|
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||||
|
pipeline.call("remember_request", "world/a.m2", "res://a.tscn")
|
||||||
|
pipeline.call("remember_request", "world/b.m2", "res://b.glb")
|
||||||
|
_expect_false(pipeline.call("complete_request", "world/missing.m2", 3), "unknown completion rejected", failures)
|
||||||
|
_expect_true(pipeline.call("complete_request", "world/b.m2", 4), "second request completes first", failures)
|
||||||
|
_expect_true(pipeline.call("complete_request", "world/a.m2", 3), "first request completes second", failures)
|
||||||
|
_expect_false(pipeline.call("has_request", "world/a.m2"), "completed request removed", failures)
|
||||||
|
_expect_equal(int(pipeline.call("pending_request_count")), 0, "all requests completed", failures)
|
||||||
|
_expect_equal(int(pipeline.call("finalize_record_count")), 2, "two terminal records", failures)
|
||||||
|
var first: Dictionary = pipeline.call("pop_finalize_record")
|
||||||
|
var second: Dictionary = pipeline.call("pop_finalize_record")
|
||||||
|
_expect_string_equal(String(first["normalized"]), "world/b.m2", "completion FIFO first", failures)
|
||||||
|
_expect_equal(int(first["status"]), 4, "first opaque status retained", failures)
|
||||||
|
_expect_string_equal(String(second["normalized"]), "world/a.m2", "completion FIFO second", failures)
|
||||||
|
_expect_equal(int(second["status"]), 3, "second opaque status retained", failures)
|
||||||
|
_expect_false(pipeline.call("has_finalize_record"), "finalize FIFO drained", failures)
|
||||||
|
_expect_true((pipeline.call("pop_finalize_record") as Dictionary).is_empty(), "empty pop returns Dictionary", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_discard_metrics_clear_and_diagnostics(failures: Array[String]) -> void:
|
||||||
|
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||||
|
pipeline.call("remember_request", "world/a.m2", "res://a.tscn")
|
||||||
|
pipeline.call("remember_request", "world/b.m2", "res://b.glb")
|
||||||
|
_expect_true(pipeline.call("discard_request", "world/a.m2"), "pending request discarded", failures)
|
||||||
|
_expect_false(pipeline.call("discard_request", "world/a.m2"), "discard is idempotent", failures)
|
||||||
|
pipeline.call("complete_request", "world/b.m2", 3)
|
||||||
|
_expect_equal(int(pipeline.call("total_work_count")), 1, "total includes finalize only", failures)
|
||||||
|
var snapshot: Dictionary = pipeline.call("diagnostic_snapshot")
|
||||||
|
(snapshot["finalize_records"] as Array)[0]["path"] = "mutated"
|
||||||
|
var fresh_snapshot: Dictionary = pipeline.call("diagnostic_snapshot")
|
||||||
|
_expect_string_equal(
|
||||||
|
String((fresh_snapshot["finalize_records"] as Array)[0]["path"]),
|
||||||
|
"res://b.glb",
|
||||||
|
"diagnostics detached",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
pipeline.call("clear")
|
||||||
|
pipeline.call("clear")
|
||||||
|
_expect_equal(int(pipeline.call("total_work_count")), 0, "clear removes all work", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||||
|
var pipeline_source := FileAccess.get_file_as_string(PIPELINE_PATH)
|
||||||
|
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||||
|
_expect_true(
|
||||||
|
loader_source.contains("M2_MESH_LOAD_PIPELINE_STATE_SCRIPT.new()"),
|
||||||
|
"loader composes pipeline state",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_false(loader_source.contains("var _m2_mesh_load_requests:"), "legacy request field removed", failures)
|
||||||
|
_expect_false(loader_source.contains("var _m2_mesh_finalize_queue:"), "legacy finalize field removed", failures)
|
||||||
|
_expect_equal(
|
||||||
|
loader_source.count("_m2_mesh_load_pipeline_state.total_work_count()"),
|
||||||
|
3,
|
||||||
|
"three existing metrics delegate",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(
|
||||||
|
loader_source.count("_m2_mesh_load_pipeline_state.clear()"),
|
||||||
|
2,
|
||||||
|
"two existing clear sites delegate",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
for retained_loader_rule in [
|
||||||
|
"ResourceLoader.load_threaded_request(",
|
||||||
|
"ResourceLoader.load_threaded_get_status(path)",
|
||||||
|
"ResourceLoader.load_threaded_get(path)",
|
||||||
|
"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_MESH_FINALIZE",
|
||||||
|
"_m2_mesh_resource_cache_state.store_mesh(",
|
||||||
|
"_m2_missing_cache[normalized_rel]",
|
||||||
|
]:
|
||||||
|
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||||
|
for forbidden_dependency in [
|
||||||
|
"ResourceLoader.",
|
||||||
|
"WorkerThreadPool.",
|
||||||
|
": Mesh",
|
||||||
|
"Node3D",
|
||||||
|
"RID(",
|
||||||
|
"_m2_mesh_cache",
|
||||||
|
"_m2_mesh_resource_cache_state",
|
||||||
|
"_m2_missing_cache",
|
||||||
|
]:
|
||||||
|
_expect_false(
|
||||||
|
pipeline_source.contains(forbidden_dependency),
|
||||||
|
"pipeline omits %s ownership" % forbidden_dependency,
|
||||||
|
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 path_index in range(256):
|
||||||
|
var normalized_path := "world/model_%d.m2" % path_index
|
||||||
|
pipeline.call("remember_request", normalized_path, "res://model_%d.tscn" % path_index)
|
||||||
|
for path_index in range(256):
|
||||||
|
pipeline.call("complete_request", "world/model_%d.m2" % path_index, 3)
|
||||||
|
while pipeline.call("has_finalize_record"):
|
||||||
|
pipeline.call("pop_finalize_record")
|
||||||
|
pipeline.call("clear")
|
||||||
|
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||||
|
_expect_true(elapsed_milliseconds < 1000.0, "100 by 256 transitions under 1 second", failures)
|
||||||
|
return elapsed_milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(label)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
_expect_true(not condition, label, failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual != expected:
|
||||||
|
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_string_equal(actual: String, expected: String, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual != expected:
|
||||||
|
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bdn3pdtg62ou1
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Synthetic ownership/lifetime/boundary/timing regression for the prepared
|
||||||
|
## static M2 Mesh resource cache.
|
||||||
|
|
||||||
|
const CACHE_SCRIPT := preload("res://src/render/m2/m2_mesh_resource_cache_state.gd")
|
||||||
|
const CACHE_PATH := "res://src/render/m2/m2_mesh_resource_cache_state.gd"
|
||||||
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var failures: Array[String] = []
|
||||||
|
_verify_validation_store_lookup_and_replace(failures)
|
||||||
|
_verify_clear_and_diagnostics(failures)
|
||||||
|
_verify_ownership_boundaries(failures)
|
||||||
|
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||||
|
if not failures.is_empty():
|
||||||
|
for failure in failures:
|
||||||
|
push_error("M2_MESH_RESOURCE_CACHE_STATE: %s" % failure)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
"M2_MESH_RESOURCE_CACHE_STATE PASS cases=9 iterations=100 elapsed_ms=%.3f"
|
||||||
|
% elapsed_milliseconds
|
||||||
|
)
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_validation_store_lookup_and_replace(failures: Array[String]) -> void:
|
||||||
|
var cache: RefCounted = CACHE_SCRIPT.new()
|
||||||
|
var first_mesh := ArrayMesh.new()
|
||||||
|
var replacement_mesh := ArrayMesh.new()
|
||||||
|
_expect_false(cache.call("store_mesh", "", first_mesh), "empty path rejected", failures)
|
||||||
|
_expect_false(cache.call("store_mesh", "world/a.m2", null), "null Mesh rejected", failures)
|
||||||
|
_expect_true(cache.call("store_mesh", "world/a.m2", first_mesh), "first Mesh stored", failures)
|
||||||
|
_expect_true(cache.call("has_mesh", "world/a.m2"), "stored Mesh observable", failures)
|
||||||
|
_expect_same(cache.call("find_mesh", "world/a.m2"), first_mesh, "exact Mesh retained", failures)
|
||||||
|
_expect_true(
|
||||||
|
cache.call("store_mesh", "world/a.m2", replacement_mesh),
|
||||||
|
"same-path Mesh replaced",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_same(
|
||||||
|
cache.call("find_mesh", "world/a.m2"),
|
||||||
|
replacement_mesh,
|
||||||
|
"replacement Mesh retained",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(int(cache.call("mesh_count")), 1, "replacement keeps one entry", failures)
|
||||||
|
_expect_same(cache.call("find_mesh", "world/missing.m2"), null, "unknown path returns null", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_clear_and_diagnostics(failures: Array[String]) -> void:
|
||||||
|
var cache: RefCounted = CACHE_SCRIPT.new()
|
||||||
|
cache.call("store_mesh", "world/a.m2", ArrayMesh.new())
|
||||||
|
cache.call("store_mesh", "world/b.m2", ArrayMesh.new())
|
||||||
|
var normalized_paths: Array[String] = cache.call("normalized_paths_snapshot")
|
||||||
|
_expect_equal(normalized_paths.size(), 2, "diagnostic path count", failures)
|
||||||
|
_expect_string_equal(normalized_paths[0], "world/a.m2", "diagnostic insertion order first", failures)
|
||||||
|
_expect_string_equal(normalized_paths[1], "world/b.m2", "diagnostic insertion order second", failures)
|
||||||
|
normalized_paths.clear()
|
||||||
|
_expect_equal(int(cache.call("mesh_count")), 2, "diagnostics detached", failures)
|
||||||
|
cache.call("clear")
|
||||||
|
cache.call("clear")
|
||||||
|
_expect_equal(int(cache.call("mesh_count")), 0, "clear releases all entries", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||||
|
var cache_source := FileAccess.get_file_as_string(CACHE_PATH)
|
||||||
|
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||||
|
_expect_true(
|
||||||
|
loader_source.contains("M2_MESH_RESOURCE_CACHE_STATE_SCRIPT.new()"),
|
||||||
|
"loader composes Mesh cache state",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_false(loader_source.contains("var _m2_mesh_cache:"), "legacy Mesh cache removed", failures)
|
||||||
|
_expect_equal(
|
||||||
|
loader_source.count("_m2_mesh_resource_cache_state.store_mesh("),
|
||||||
|
2,
|
||||||
|
"two existing stores delegate",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(
|
||||||
|
loader_source.count("_m2_mesh_resource_cache_state.find_mesh("),
|
||||||
|
2,
|
||||||
|
"two existing lookups delegate",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(
|
||||||
|
loader_source.count("_m2_mesh_resource_cache_state.clear()"),
|
||||||
|
1,
|
||||||
|
"final shutdown clear delegates",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
for retained_loader_rule in [
|
||||||
|
"ResourceLoader.load_threaded_get(path)",
|
||||||
|
"_m2_mesh_resource_extractor.extract_first_mesh(resource)",
|
||||||
|
"_prepare_m2_mesh_for_runtime(normalized_rel, mesh)",
|
||||||
|
"_m2_missing_cache[normalized_rel]",
|
||||||
|
"_m2_scene_cache[normalized_rel]",
|
||||||
|
]:
|
||||||
|
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||||
|
for forbidden_dependency in [
|
||||||
|
"ResourceLoader.",
|
||||||
|
"FileAccess.",
|
||||||
|
"WorkerThreadPool.",
|
||||||
|
"PackedScene",
|
||||||
|
"Node3D",
|
||||||
|
"M2Builder",
|
||||||
|
"_m2_missing_cache",
|
||||||
|
"_m2_scene_cache",
|
||||||
|
]:
|
||||||
|
_expect_false(
|
||||||
|
cache_source.contains(forbidden_dependency),
|
||||||
|
"cache omits %s ownership" % forbidden_dependency,
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||||
|
var cache: RefCounted = CACHE_SCRIPT.new()
|
||||||
|
var meshes: Array[Mesh] = []
|
||||||
|
for _path_index in range(256):
|
||||||
|
meshes.append(ArrayMesh.new())
|
||||||
|
var started_microseconds := Time.get_ticks_usec()
|
||||||
|
for _iteration in range(100):
|
||||||
|
for path_index in range(256):
|
||||||
|
cache.call("store_mesh", "world/model_%d.m2" % path_index, meshes[path_index])
|
||||||
|
for path_index in range(256):
|
||||||
|
cache.call("find_mesh", "world/model_%d.m2" % path_index)
|
||||||
|
cache.call("clear")
|
||||||
|
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||||
|
_expect_true(elapsed_milliseconds < 1000.0, "100 by 256 store/lookups under 1 second", failures)
|
||||||
|
return elapsed_milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(label)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
_expect_true(not condition, label, failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual != expected:
|
||||||
|
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_string_equal(actual: String, expected: String, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual != expected:
|
||||||
|
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
|
||||||
|
if not is_same(actual, expected):
|
||||||
|
failures.append(label)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ld4k7pnaueno
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Synthetic direct-Resource/PackedScene/subtree/order/lifetime/boundary/timing
|
||||||
|
## regression for first-Mesh extraction.
|
||||||
|
|
||||||
|
const EXTRACTOR_SCRIPT := preload("res://src/render/m2/m2_mesh_resource_extractor.gd")
|
||||||
|
const EXTRACTOR_PATH := "res://src/render/m2/m2_mesh_resource_extractor.gd"
|
||||||
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var failures: Array[String] = []
|
||||||
|
_verify_direct_and_invalid_resources(failures)
|
||||||
|
_verify_depth_first_subtree_order(failures)
|
||||||
|
_verify_packed_scene_mesh_and_temporary_lifetime(failures)
|
||||||
|
_verify_ownership_boundaries(failures)
|
||||||
|
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||||
|
if not failures.is_empty():
|
||||||
|
for failure in failures:
|
||||||
|
push_error("M2_MESH_RESOURCE_EXTRACTOR: %s" % failure)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
"M2_MESH_RESOURCE_EXTRACTOR PASS cases=10 iterations=10000 elapsed_ms=%.3f"
|
||||||
|
% elapsed_milliseconds
|
||||||
|
)
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_direct_and_invalid_resources(failures: Array[String]) -> void:
|
||||||
|
var extractor: RefCounted = EXTRACTOR_SCRIPT.new()
|
||||||
|
var direct_mesh := ArrayMesh.new()
|
||||||
|
_expect_same(
|
||||||
|
extractor.call("extract_first_mesh", direct_mesh),
|
||||||
|
direct_mesh,
|
||||||
|
"direct Mesh reference retained",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_same(extractor.call("extract_first_mesh", Resource.new()), null, "unsupported Resource returns null", failures)
|
||||||
|
_expect_same(extractor.call("extract_first_mesh", null), null, "null Resource returns null", failures)
|
||||||
|
_expect_same(extractor.call("find_first_mesh_in_subtree", null), null, "null subtree returns null", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_depth_first_subtree_order(failures: Array[String]) -> void:
|
||||||
|
var extractor: RefCounted = EXTRACTOR_SCRIPT.new()
|
||||||
|
var root := Node3D.new()
|
||||||
|
var first_branch := Node3D.new()
|
||||||
|
var second_branch := Node3D.new()
|
||||||
|
var first_mesh_instance := MeshInstance3D.new()
|
||||||
|
var second_mesh_instance := MeshInstance3D.new()
|
||||||
|
var first_mesh := ArrayMesh.new()
|
||||||
|
var second_mesh := ArrayMesh.new()
|
||||||
|
first_mesh_instance.mesh = first_mesh
|
||||||
|
second_mesh_instance.mesh = second_mesh
|
||||||
|
root.add_child(first_branch)
|
||||||
|
root.add_child(second_branch)
|
||||||
|
first_branch.add_child(first_mesh_instance)
|
||||||
|
second_branch.add_child(second_mesh_instance)
|
||||||
|
_expect_same(
|
||||||
|
extractor.call("find_first_mesh_in_subtree", root),
|
||||||
|
first_mesh,
|
||||||
|
"depth-first child order retained",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_same(first_mesh_instance.get_parent(), first_branch, "subtree ownership unchanged", failures)
|
||||||
|
first_mesh_instance.mesh = null
|
||||||
|
_expect_same(
|
||||||
|
extractor.call("find_first_mesh_in_subtree", root),
|
||||||
|
second_mesh,
|
||||||
|
"null MeshInstance Mesh skipped by parent traversal",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
root.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_packed_scene_mesh_and_temporary_lifetime(failures: Array[String]) -> void:
|
||||||
|
var extractor: RefCounted = EXTRACTOR_SCRIPT.new()
|
||||||
|
var source_root := Node3D.new()
|
||||||
|
var mesh_instance := MeshInstance3D.new()
|
||||||
|
var packed_mesh := ArrayMesh.new()
|
||||||
|
mesh_instance.mesh = packed_mesh
|
||||||
|
source_root.add_child(mesh_instance)
|
||||||
|
mesh_instance.owner = source_root
|
||||||
|
var packed_scene := PackedScene.new()
|
||||||
|
_expect_equal(packed_scene.pack(source_root), OK, "synthetic PackedScene packs", failures)
|
||||||
|
source_root.free()
|
||||||
|
var node_count_before := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
|
||||||
|
var extracted_mesh: Mesh = extractor.call("extract_first_mesh", packed_scene)
|
||||||
|
var node_count_after := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
|
||||||
|
_expect_same(extracted_mesh, packed_mesh, "PackedScene first Mesh retained", failures)
|
||||||
|
_expect_equal(node_count_after, node_count_before, "temporary PackedScene root freed", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||||
|
var extractor_source := FileAccess.get_file_as_string(EXTRACTOR_PATH)
|
||||||
|
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||||
|
_expect_true(
|
||||||
|
loader_source.contains("M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new()"),
|
||||||
|
"loader composes Mesh resource extractor",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_false(
|
||||||
|
loader_source.contains("func _extract_first_mesh_from_m2_resource("),
|
||||||
|
"legacy Resource extractor removed",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_false(
|
||||||
|
loader_source.contains("func _find_first_mesh_recursive("),
|
||||||
|
"legacy subtree extractor removed",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(
|
||||||
|
loader_source.count("_m2_mesh_resource_extractor.extract_first_mesh("),
|
||||||
|
1,
|
||||||
|
"threaded Resource finalization delegates",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(
|
||||||
|
loader_source.count("_m2_mesh_resource_extractor.find_first_mesh_in_subtree("),
|
||||||
|
2,
|
||||||
|
"rebuild and synchronous fallback delegate",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
for retained_loader_rule in [
|
||||||
|
"ResourceLoader.load_threaded_get(path)",
|
||||||
|
"_prepare_m2_mesh_for_runtime(normalized_rel, mesh)",
|
||||||
|
"_m2_mesh_resource_cache_state.store_mesh(",
|
||||||
|
"M2_BUILDER_SCRIPT.build(data, extracted_dir)",
|
||||||
|
"_m2_missing_cache[normalized_rel]",
|
||||||
|
]:
|
||||||
|
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||||
|
for forbidden_dependency in [
|
||||||
|
"ResourceLoader.",
|
||||||
|
"FileAccess.",
|
||||||
|
"WorkerThreadPool.",
|
||||||
|
"M2Builder",
|
||||||
|
"_m2_mesh_resource_cache_state",
|
||||||
|
"_m2_missing_cache",
|
||||||
|
"MultiMesh",
|
||||||
|
]:
|
||||||
|
_expect_false(
|
||||||
|
extractor_source.contains(forbidden_dependency),
|
||||||
|
"extractor omits %s ownership" % forbidden_dependency,
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(extractor_source.contains("temporary_root.free()"), "temporary root free remains explicit", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||||
|
var extractor: RefCounted = EXTRACTOR_SCRIPT.new()
|
||||||
|
var root := Node3D.new()
|
||||||
|
var branch := root
|
||||||
|
for _depth_index in range(8):
|
||||||
|
var child_branch := Node3D.new()
|
||||||
|
branch.add_child(child_branch)
|
||||||
|
branch = child_branch
|
||||||
|
var mesh_instance := MeshInstance3D.new()
|
||||||
|
mesh_instance.mesh = ArrayMesh.new()
|
||||||
|
branch.add_child(mesh_instance)
|
||||||
|
var started_microseconds := Time.get_ticks_usec()
|
||||||
|
for _iteration in range(10000):
|
||||||
|
extractor.call("find_first_mesh_in_subtree", root)
|
||||||
|
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||||
|
root.free()
|
||||||
|
_expect_true(elapsed_milliseconds < 1000.0, "10000 subtree lookups under 1 second", failures)
|
||||||
|
return elapsed_milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(label)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
_expect_true(not condition, label, failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual != expected:
|
||||||
|
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
|
||||||
|
if not is_same(actual, expected):
|
||||||
|
failures.append(label)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bgvidp5u31sud
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Synthetic predicate, memoization, boundary and timing regression for stale
|
||||||
|
## cached M2 runtime mesh rebuild decisions.
|
||||||
|
|
||||||
|
const CLASSIFIER_SCRIPT := preload(
|
||||||
|
"res://src/render/m2/m2_runtime_mesh_rebuild_classifier.gd"
|
||||||
|
)
|
||||||
|
const CLASSIFIER_PATH := "res://src/render/m2/m2_runtime_mesh_rebuild_classifier.gd"
|
||||||
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var failures: Array[String] = []
|
||||||
|
_verify_billboard_decisions(failures)
|
||||||
|
_verify_uv_rotation_decisions(failures)
|
||||||
|
_verify_stage_and_index_boundaries(failures)
|
||||||
|
_verify_memoization_clear_and_diagnostics(failures)
|
||||||
|
_verify_ownership_boundaries(failures)
|
||||||
|
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||||
|
if not failures.is_empty():
|
||||||
|
for failure in failures:
|
||||||
|
push_error("M2_RUNTIME_MESH_REBUILD_CLASSIFIER: %s" % failure)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
"M2_RUNTIME_MESH_REBUILD_CLASSIFIER PASS cases=12 iterations=100 elapsed_ms=%.3f"
|
||||||
|
% elapsed_milliseconds
|
||||||
|
)
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_billboard_decisions(failures: Array[String]) -> void:
|
||||||
|
_expect_false(_classify("empty", {}), "empty data does not rebuild", failures)
|
||||||
|
_expect_false(
|
||||||
|
_classify("invalid", {"batches": ["invalid", 7, null]}),
|
||||||
|
"non-Dictionary batches skipped",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
_classify("flag", {"batches": [{"has_billboard": true}]}),
|
||||||
|
"billboard flag rebuilds",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
_classify("vertices", {"batches": [{"billboard_vertex_count": 1}]}),
|
||||||
|
"positive billboard vertex count rebuilds",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_false(
|
||||||
|
_classify("zero_vertices", {"batches": [{"billboard_vertex_count": 0}]}),
|
||||||
|
"zero billboard vertex count does not rebuild",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_uv_rotation_decisions(failures: Array[String]) -> void:
|
||||||
|
_expect_false(
|
||||||
|
_classify("identity", _uv_data([_rotation_transform()])),
|
||||||
|
"identity UV rotation does not rebuild",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
_classify(
|
||||||
|
"rotation",
|
||||||
|
_uv_data([_rotation_transform(Vector4(0.0, 1.0, -1.0, 0.0))])
|
||||||
|
),
|
||||||
|
"non-identity UV rotation rebuilds",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
_classify("positive_speed", _uv_data([_rotation_transform(Vector4(1, 0, 0, 1), 0.000002)])),
|
||||||
|
"positive UV rotation speed rebuilds",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
_classify("negative_speed", _uv_data([_rotation_transform(Vector4(1, 0, 0, 1), -0.000002)])),
|
||||||
|
"negative UV rotation speed rebuilds",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_false(
|
||||||
|
_classify("epsilon", _uv_data([_rotation_transform(Vector4(1, 0, 0, 1), 0.000001)])),
|
||||||
|
"exact UV rotation epsilon does not rebuild",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_stage_and_index_boundaries(failures: Array[String]) -> void:
|
||||||
|
var fifth_stage_transforms: Array = [
|
||||||
|
_rotation_transform(),
|
||||||
|
_rotation_transform(),
|
||||||
|
_rotation_transform(),
|
||||||
|
_rotation_transform(),
|
||||||
|
_rotation_transform(Vector4(0.0, 1.0, -1.0, 0.0)),
|
||||||
|
]
|
||||||
|
_expect_false(
|
||||||
|
_classify(
|
||||||
|
"fifth_stage",
|
||||||
|
_uv_data(fifth_stage_transforms, PackedInt32Array([0, 1, 2, 3, 4]), 5)
|
||||||
|
),
|
||||||
|
"fifth texture stage remains ignored",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_false(
|
||||||
|
_classify(
|
||||||
|
"invalid_indices",
|
||||||
|
_uv_data([_rotation_transform(Vector4(0, 1, -1, 0))], PackedInt32Array([-1, 9]), 2)
|
||||||
|
),
|
||||||
|
"invalid transform indices skipped",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_false(
|
||||||
|
_classify(
|
||||||
|
"invalid_transform",
|
||||||
|
_uv_data(["not-a-transform"], PackedInt32Array([0]), 1)
|
||||||
|
),
|
||||||
|
"non-Dictionary transform uses identity defaults",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_memoization_clear_and_diagnostics(failures: Array[String]) -> void:
|
||||||
|
var classifier: RefCounted = CLASSIFIER_SCRIPT.new()
|
||||||
|
var billboard_data := {"batches": [{"has_billboard": true}]}
|
||||||
|
_expect_true(
|
||||||
|
classifier.call("needs_runtime_mesh_rebuild", "world/tree.m2", billboard_data),
|
||||||
|
"first true decision",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
classifier.call("needs_runtime_mesh_rebuild", "world/tree.m2", {}),
|
||||||
|
"first path decision memoized",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
classifier.call("needs_runtime_mesh_rebuild", "world/rock.m2", {})
|
||||||
|
_expect_equal(int(classifier.call("cached_path_count")), 2, "cached path count", failures)
|
||||||
|
var snapshot: Dictionary = classifier.call("diagnostic_snapshot")
|
||||||
|
snapshot["world/tree.m2"] = false
|
||||||
|
_expect_true(
|
||||||
|
classifier.call("needs_runtime_mesh_rebuild", "world/tree.m2", {}),
|
||||||
|
"diagnostics detached",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
classifier.call("clear")
|
||||||
|
_expect_equal(int(classifier.call("cached_path_count")), 0, "clear count", failures)
|
||||||
|
_expect_false(
|
||||||
|
classifier.call("needs_runtime_mesh_rebuild", "world/tree.m2", {}),
|
||||||
|
"clear allows recomputation",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||||
|
var classifier_source := FileAccess.get_file_as_string(CLASSIFIER_PATH)
|
||||||
|
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||||
|
_expect_true(
|
||||||
|
loader_source.contains("M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()"),
|
||||||
|
"loader composes classifier",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(
|
||||||
|
loader_source.count("_m2_runtime_mesh_rebuild_classifier.clear()"),
|
||||||
|
2,
|
||||||
|
"loader retains two classifier clear sites",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_false(
|
||||||
|
loader_source.contains("_m2_runtime_rebuild_required_cache"),
|
||||||
|
"loader removes legacy rebuild cache",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
for removed_function in [
|
||||||
|
"func _m2_raw_data_needs_runtime_mesh_rebuild",
|
||||||
|
"func _m2_raw_data_has_billboards",
|
||||||
|
"func _m2_raw_data_has_uv_rotation",
|
||||||
|
]:
|
||||||
|
_expect_false(loader_source.contains(removed_function), "loader removes %s" % removed_function, failures)
|
||||||
|
for forbidden_dependency in [
|
||||||
|
"ResourceLoader.",
|
||||||
|
"WorkerThreadPool.",
|
||||||
|
"M2_BUILDER_SCRIPT",
|
||||||
|
"ClassDB.instantiate",
|
||||||
|
"FileAccess.",
|
||||||
|
]:
|
||||||
|
_expect_false(
|
||||||
|
classifier_source.contains(forbidden_dependency),
|
||||||
|
"classifier omits %s dependency" % forbidden_dependency,
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||||
|
var classifier: RefCounted = CLASSIFIER_SCRIPT.new()
|
||||||
|
var ordinary_data := {"batches": [{"has_billboard": false}]}
|
||||||
|
var started_microseconds := Time.get_ticks_usec()
|
||||||
|
for iteration in range(100):
|
||||||
|
for path_index in range(256):
|
||||||
|
classifier.call(
|
||||||
|
"needs_runtime_mesh_rebuild",
|
||||||
|
"world/model_%d.m2" % path_index,
|
||||||
|
ordinary_data
|
||||||
|
)
|
||||||
|
classifier.call("clear")
|
||||||
|
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||||
|
_expect_true(elapsed_milliseconds < 1000.0, "100 by 256 classifications under 1 second", failures)
|
||||||
|
return elapsed_milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
func _classify(normalized_path: String, raw_data: Dictionary) -> bool:
|
||||||
|
var classifier: RefCounted = CLASSIFIER_SCRIPT.new()
|
||||||
|
return bool(classifier.call("needs_runtime_mesh_rebuild", normalized_path, raw_data))
|
||||||
|
|
||||||
|
|
||||||
|
func _uv_data(
|
||||||
|
texture_transforms: Array,
|
||||||
|
texture_transform_combos: PackedInt32Array = PackedInt32Array([0]),
|
||||||
|
texture_count: int = 1) -> Dictionary:
|
||||||
|
return {
|
||||||
|
"texture_transforms": texture_transforms,
|
||||||
|
"texture_transform_combos": texture_transform_combos,
|
||||||
|
"batches": [{
|
||||||
|
"texture_count": texture_count,
|
||||||
|
"texture_transform_combo_index": 0,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _rotation_transform(
|
||||||
|
rotation: Vector4 = Vector4(1.0, 0.0, 0.0, 1.0),
|
||||||
|
rotation_speed: float = 0.0) -> Dictionary:
|
||||||
|
return {
|
||||||
|
"rotation": rotation,
|
||||||
|
"rotation_speed": rotation_speed,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(label)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
_expect_true(not condition, label, failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual != expected:
|
||||||
|
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cc62iwxjrkte6
|
||||||
@@ -35,13 +35,19 @@ func _verify_runtime_cache_ownership() -> void:
|
|||||||
loader.set("_m2_scene_cache", {"static": static_m2_prototype})
|
loader.set("_m2_scene_cache", {"static": static_m2_prototype})
|
||||||
loader.set("_m2_animated_scene_cache", {"animated": animated_m2_prototype})
|
loader.set("_m2_animated_scene_cache", {"animated": animated_m2_prototype})
|
||||||
loader.set("_wmo_prototype_cache", {"wmo": wmo_prototype})
|
loader.set("_wmo_prototype_cache", {"wmo": wmo_prototype})
|
||||||
loader.set("_m2_mesh_cache", {"mesh": ArrayMesh.new()})
|
var m2_mesh_resource_cache_state: RefCounted = loader.get("_m2_mesh_resource_cache_state")
|
||||||
|
m2_mesh_resource_cache_state.call("store_mesh", "mesh", ArrayMesh.new())
|
||||||
loader.set("_m2_static_animation_cache", {"static": true})
|
loader.set("_m2_static_animation_cache", {"static": true})
|
||||||
loader.set("_m2_missing_cache", {"missing": true})
|
loader.set("_m2_missing_cache", {"missing": true})
|
||||||
loader.set("_wmo_render_cache", {"render": Resource.new()})
|
var wmo_render_cache_state: RefCounted = loader.get("_wmo_render_resource_cache_state")
|
||||||
loader.set("_wmo_scene_resource_cache", {"scene": PackedScene.new()})
|
wmo_render_cache_state.call("remember_request", "render", "res://render.res")
|
||||||
loader.set("_wmo_render_missing_cache", {"missing": true})
|
wmo_render_cache_state.call("complete_request_with_resource", "render", Resource.new())
|
||||||
loader.set("_wmo_scene_cache_missing", {"missing": true})
|
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("_wmo_missing_cache", {"missing": true})
|
||||||
loader.set("_shared_tex_cache", {"texture": ImageTexture.new()})
|
loader.set("_shared_tex_cache", {"texture": ImageTexture.new()})
|
||||||
|
|
||||||
@@ -54,18 +60,36 @@ func _verify_runtime_cache_ownership() -> void:
|
|||||||
"_m2_scene_cache",
|
"_m2_scene_cache",
|
||||||
"_m2_animated_scene_cache",
|
"_m2_animated_scene_cache",
|
||||||
"_wmo_prototype_cache",
|
"_wmo_prototype_cache",
|
||||||
"_m2_mesh_cache",
|
|
||||||
"_m2_static_animation_cache",
|
"_m2_static_animation_cache",
|
||||||
"_m2_missing_cache",
|
"_m2_missing_cache",
|
||||||
"_wmo_render_cache",
|
|
||||||
"_wmo_scene_resource_cache",
|
|
||||||
"_wmo_render_missing_cache",
|
|
||||||
"_wmo_scene_cache_missing",
|
|
||||||
"_wmo_missing_cache",
|
"_wmo_missing_cache",
|
||||||
"_shared_tex_cache",
|
"_shared_tex_cache",
|
||||||
]:
|
]:
|
||||||
var cache: Dictionary = loader.get(cache_name)
|
var cache: Dictionary = loader.get(cache_name)
|
||||||
_expect(cache.is_empty(), "%s should be empty after shutdown" % cache_name)
|
_expect(cache.is_empty(), "%s should be empty after shutdown" % cache_name)
|
||||||
|
_expect(
|
||||||
|
int(m2_mesh_resource_cache_state.call("mesh_count")) == 0,
|
||||||
|
"M2 Mesh Resources should be empty after shutdown"
|
||||||
|
)
|
||||||
|
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()
|
loader.free()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -27,7 +27,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
## Evidence
|
## Evidence
|
||||||
|
|
||||||
- Date: 2026-07-17
|
- Date: 2026-07-17
|
||||||
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`, `f88bf97`, `3f84717`, `d2cb52f`;
|
- 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`, `874fd0f`, `38f831e`, `83b3618`, `3094561`, `fa85985`;
|
||||||
packages `M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-001`,
|
packages `M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-001`,
|
||||||
`M03-RND-FACADE-GROUND-QUERY-001`, `M03-RND-FACADE-ENVIRONMENT-001`,
|
`M03-RND-FACADE-GROUND-QUERY-001`, `M03-RND-FACADE-ENVIRONMENT-001`,
|
||||||
`M03-RND-FACADE-ENTITY-001`, `M03-RND-TERRAIN-CACHE-SERVICE-001`,
|
`M03-RND-FACADE-ENTITY-001`, `M03-RND-TERRAIN-CACHE-SERVICE-001`,
|
||||||
@@ -36,7 +36,18 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
`M03-RND-M2-PLACEMENT-TRANSFORM-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-M2-BUILD-BATCH-PLANNER-001`,
|
||||||
`M03-RND-WMO-PLACEMENT-RESOLVER-001`
|
`M03-RND-WMO-PLACEMENT-RESOLVER-001`,
|
||||||
|
`M03-RND-WMO-PLACEMENT-REGISTRY-001`,
|
||||||
|
`M03-RND-WMO-RENDER-BUILD-PLANNER-001`,
|
||||||
|
`M03-RND-WMO-RENDER-BUILD-QUEUE-001`,
|
||||||
|
`M03-RND-WMO-RENDER-RESOURCE-CACHE-001`,
|
||||||
|
`M03-RND-WMO-SCENE-RESOURCE-CACHE-001`,
|
||||||
|
`M03-RND-ADT-WATER-LOAD-PIPELINE-001`,
|
||||||
|
`M03-RND-ADT-WATER-SCENE-FINALIZER-001`,
|
||||||
|
`M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001`,
|
||||||
|
`M03-RND-M2-MESH-LOAD-PIPELINE-001`,
|
||||||
|
`M03-RND-M2-MESH-RESOURCE-CACHE-001`,
|
||||||
|
`M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001`
|
||||||
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
|
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
|
||||||
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
|
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
|
||||||
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner
|
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner
|
||||||
@@ -73,6 +84,82 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
WMO placement resolver passed `cases=9 iterations=20000 elapsed_ms=27.919`;
|
WMO placement resolver passed `cases=9 iterations=20000 elapsed_ms=27.919`;
|
||||||
post-merge renderer materials, shutdown, internal-access `42`, facade,
|
post-merge renderer materials, shutdown, internal-access `42`, facade,
|
||||||
`7/7` manifest, documentation and coordination gates remained green.
|
`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.
|
||||||
|
ADT water scene finalizer passed `cases=8 iterations=100 elapsed_ms=0.042`
|
||||||
|
after an initial `0.024ms` run; synthetic data produced one four-vertex/
|
||||||
|
six-index liquid surface with exact tile attachment and recursive editor owner.
|
||||||
|
Water pipeline, WMO/M2/terrain/facade/scheduler/streaming/coordinate, material,
|
||||||
|
shutdown and 32 headless contract regressions passed; checkpoint dry-run
|
||||||
|
retained `7/7` plans. Post-merge finalizer (`0.025ms`), water pipeline
|
||||||
|
(`98.398ms`), shutdown, materials, facade, internal-access `36`, `7/7`
|
||||||
|
manifest, documentation and coordination gates remained green.
|
||||||
|
M2 runtime mesh rebuild classifier passed `cases=12 iterations=100
|
||||||
|
elapsed_ms=64.585` after an initial `35.461ms` run; billboard flag/count,
|
||||||
|
invalid variants/indices, UV rotation/speed/epsilon, four-stage cap,
|
||||||
|
memoization, detached diagnostics and both clear sites passed. Thirty-three
|
||||||
|
M2/WMO/liquid/terrain/facade/scheduler/streaming/coordinate/material/shutdown
|
||||||
|
regressions and checkpoint dry-run `7/7` passed. Post-merge classifier
|
||||||
|
(`45.439ms`), materials, shutdown, M2 batch planner, facade, internal-access
|
||||||
|
`35`, `7/7` manifest, documentation and coordination gates remained green.
|
||||||
|
M2 mesh load pipeline state passed `cases=11 iterations=100
|
||||||
|
elapsed_ms=100.273` with request validation/dedupe, insertion-order polling,
|
||||||
|
terminal transition/completion FIFO, opaque statuses, discard, detached
|
||||||
|
diagnostics, metrics, clear and 100-by-256 timing. Thirty-four adjacent
|
||||||
|
headless regressions and checkpoint dry-run `7/7` passed. Post-merge pipeline
|
||||||
|
state (`69.196ms`), classifier, materials, shutdown, facade, internal-access
|
||||||
|
`34`, `7/7` manifest, documentation and coordination gates remained green.
|
||||||
|
M2 mesh resource cache state passed `cases=9 iterations=100
|
||||||
|
elapsed_ms=38.974` with invalid-input guards, exact-reference lookup,
|
||||||
|
replacement, detached path diagnostics, idempotent clear and 100-by-256
|
||||||
|
timing. Thirty-five renderer/coordinate regressions and checkpoint dry-run
|
||||||
|
`7/7` passed. Post-merge cache state (`38.686ms`), pipeline, classifier,
|
||||||
|
materials, shutdown, facade, internal-access `34`, `7/7` manifest,
|
||||||
|
documentation and coordination gates remained green.
|
||||||
|
M2 mesh resource extractor passed `cases=10 iterations=10000
|
||||||
|
elapsed_ms=39.243` with direct/invalid Resource, depth-first order,
|
||||||
|
null-Mesh sibling continuation, subtree ownership, PackedScene Mesh identity
|
||||||
|
and temporary-root release contracts. Thirty-six renderer/coordinate
|
||||||
|
regressions and checkpoint dry-run `7/7` passed. Post-merge extractor
|
||||||
|
(`38.172ms`), cache, pipeline, classifier, materials, shutdown, facade,
|
||||||
|
internal-access `34`, `7/7` manifest, documentation and coordination gates
|
||||||
|
remained green.
|
||||||
M02 terrain-query regression
|
M02 terrain-query regression
|
||||||
remained green (13 pre-existing expired M00 claim warnings).
|
remained green (13 pre-existing expired M00 claim warnings).
|
||||||
- Fidelity comparison: all 16 historical operation limits and drain sites were
|
- Fidelity comparison: all 16 historical operation limits and drain sites were
|
||||||
@@ -104,6 +191,53 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
WMO cache-key normalization, positive MODF identity with tile/index fallback
|
WMO cache-key normalization, positive MODF identity with tile/index fallback
|
||||||
and world position/Euler/unclamped-scale transforms are unchanged across
|
and world position/Euler/unclamped-scale transforms are unchanged across
|
||||||
lightweight render, cached-scene and live-prototype paths.
|
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,
|
||||||
|
tile subtree lifetime and `water_loaded` timing.
|
||||||
|
Both raw-tile and delayed cached-tile water paths now call the same stateless
|
||||||
|
finalizer. It invokes the unchanged `ADTBuilder.build_tile_water_scene`,
|
||||||
|
attaches the returned root once and applies the existing optional recursive
|
||||||
|
Editor ownership; tile subtree lifetime and dry-root cleanup are unchanged.
|
||||||
|
M2 stale-cache rebuild selection retains the same positive billboard flag/
|
||||||
|
vertex-count tests, referenced non-identity UV rotation, exclusive
|
||||||
|
`0.000001` speed threshold, four-stage cap and first-decision-per-path cache
|
||||||
|
lifetime. Raw loading, material refresh version, M2Builder rebuild/fallback and
|
||||||
|
Mesh adoption remain loader-owned.
|
||||||
|
Static M2 mesh pending requests preserve normalized-path dedupe and Dictionary
|
||||||
|
insertion polling order; loaded/failed transitions preserve completion-order
|
||||||
|
finalization FIFO and opaque statuses. All three pending-plus-finalize metrics,
|
||||||
|
both clear sites and shutdown ResourceLoader drain order are unchanged; cache
|
||||||
|
path selection, I/O, permits, shared missing state and adoption remain loader-owned.
|
||||||
|
Prepared static M2 Mesh lookups retain exact Resource identity, same-path
|
||||||
|
replacement and map-refresh lifetime; the only clear remains final shutdown
|
||||||
|
after asynchronous drain. Shared missing/prototype/animated caches,
|
||||||
|
refresh/rebuild and materialization remain loader-owned.
|
||||||
|
Direct Mesh Resources, temporary PackedScene instances and prototype/rebuild
|
||||||
|
subtrees retain the same depth-first first-Mesh rule. PackedScene still
|
||||||
|
instantiates once and frees its temporary root synchronously before return;
|
||||||
|
the extractor retains no state or Resource reference.
|
||||||
No new build-12340 parity claim is made; asset-backed visual/p95/p99 comparison remains pending.
|
No new build-12340 parity claim is made; asset-backed visual/p95/p99 comparison remains pending.
|
||||||
- Changed files: scene-free scheduler and verifier; streamer budget composition/
|
- Changed files: scene-free scheduler and verifier; streamer budget composition/
|
||||||
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes;
|
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes;
|
||||||
@@ -118,9 +252,23 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
three transform consumers, stateless M2 placement grouper and worker adapter,
|
three transform consumers, stateless M2 placement grouper and worker adapter,
|
||||||
stateless M2 build batch planner and loader cursor adapter,
|
stateless M2 build batch planner and loader cursor adapter,
|
||||||
stateless WMO placement resolver and seven cache/identity/transform adapters,
|
stateless WMO placement resolver and seven cache/identity/transform adapters,
|
||||||
|
stateful WMO placement registry and global/tile add/release/clear adapters,
|
||||||
|
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,
|
||||||
|
stateless ADT water scene finalizer and both loader build/attach adapters,
|
||||||
|
memoized M2 runtime mesh rebuild classifier and loader query/clear adapters,
|
||||||
|
M2 mesh load pipeline state and loader request/poll/finalize/metric/clear adapters,
|
||||||
|
prepared M2 Mesh resource cache state and loader lookup/store/shutdown adapters,
|
||||||
|
stateless M2 Mesh resource extractor and three loader traversal adapters,
|
||||||
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
|
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
|
||||||
- Remaining risks: queue storage length and worker concurrency/stale-result logic
|
- Remaining risks: worker concurrency and stale-result validation remain
|
||||||
remain streamer-owned; cancellation stops new permits but does not interrupt
|
streamer-owned; cancellation stops new permits but does not interrupt
|
||||||
in-flight work; the environment snapshot currently covers world time but not weather
|
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
|
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
|
network-scale hot paths; terrain cache is entry-count bounded without Mesh-byte
|
||||||
@@ -131,8 +279,15 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
private Azeroth data was absent for the asset-backed ADT placement probe; M2
|
private Azeroth data was absent for the asset-backed ADT placement probe; M2
|
||||||
asset-backed visual transform recheck, grouping/build/tasks/caches/finalization
|
asset-backed visual transform recheck, grouping/build/tasks/caches/finalization
|
||||||
and boundary traversal p95/p99 remain pending; path grouping is not yet
|
and boundary traversal p95/p99 remain pending; path grouping is not yet
|
||||||
culling-driven spatial-cell grouping; M2 resource/queue transitions and raw
|
culling-driven spatial-cell grouping; M2 rebuild classification and static
|
||||||
build-job Dictionaries remain in the loader; WMO registry/build/resource state
|
request/finalize bookkeeping, prepared Mesh cache ownership and first-Mesh
|
||||||
and asset-backed placement/portal/material evidence remain pending;
|
traversal are separated, but ResourceLoader I/O, shared missing state, Mesh
|
||||||
M03 still needs further terrain/M2/WMO/liquid service extraction
|
preparation and finalization remain in the loader; raw
|
||||||
and dependency/performance acceptance.
|
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 and active worker interruption remain loader-owned;
|
||||||
|
materialization is separated but remains synchronous main-thread work behind
|
||||||
|
the existing permit, 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user