Compare commits

..

104 Commits

Author SHA1 Message Date
sindoring 4387c66c0a docs(M03): accept WMO scene resource cache state 2026-07-17 01:20:29 +04:00
sindoring ada0fd9467 merge M03 WMO scene resource cache state 2026-07-17 01:19:40 +04:00
sindoring ff4f320412 docs(M03): hand off WMO scene resource cache state 2026-07-17 01:19:36 +04:00
sindoring c83929428d refactor(M03): extract WMO scene resource cache state 2026-07-17 01:19:19 +04:00
sindoring 71fa093118 coord(M03): claim WMO scene resource cache state 2026-07-17 01:14:11 +04:00
sindoring e690756301 docs(M03): accept WMO render resource cache state 2026-07-17 01:12:30 +04:00
sindoring b4955d6162 merge M03 WMO render resource cache state 2026-07-17 01:11:32 +04:00
sindoring b4b94b3cbb docs(M03): hand off WMO render resource cache state 2026-07-17 01:11:27 +04:00
sindoring d39f591e50 refactor(M03): extract WMO render resource cache state 2026-07-17 01:11:11 +04:00
sindoring 4314f961f6 coord(M03): claim WMO render resource cache state 2026-07-17 01:04:15 +04:00
sindoring c9547041da docs(M03): accept WMO render build queue 2026-07-17 01:00:40 +04:00
sindoring 60603e18c8 merge M03 WMO render build queue cold-load fix 2026-07-17 00:59:50 +04:00
sindoring 3411156f8f docs(M03): refresh WMO queue handoff 2026-07-17 00:59:44 +04:00
sindoring e576ae2cad fix(M03): make WMO build queue cold-load safe 2026-07-17 00:59:11 +04:00
sindoring a878e7c4e7 merge M03 WMO render build queue 2026-07-17 00:57:20 +04:00
sindoring ab40a76b62 docs(M03): hand off WMO render build queue 2026-07-17 00:57:11 +04:00
sindoring e1bb105fff refactor(M03): extract WMO render build queue 2026-07-17 00:56:55 +04:00
sindoring 040b635ec6 coord(M03): claim WMO render build queue 2026-07-17 00:51:33 +04:00
sindoring 1e740aaa65 docs(M03): accept WMO render build planner 2026-07-17 00:49:17 +04:00
sindoring c06aed568b merge M03 WMO render build planner 2026-07-17 00:48:26 +04:00
sindoring 01bad4ca26 docs(M03): hand off WMO render build planner 2026-07-17 00:48:13 +04:00
sindoring 3a6b1d306d refactor(M03): extract WMO render build planner 2026-07-17 00:47:54 +04:00
sindoring e420a8724f coord(M03): claim WMO render build planner 2026-07-17 00:42:19 +04:00
sindoring 1e73e597f8 docs(M03): accept WMO placement registry 2026-07-17 00:34:25 +04:00
sindoring f9e5a4f2e0 merge M03 WMO placement registry 2026-07-17 00:33:34 +04:00
sindoring 16053b46ca docs(M03): hand off WMO placement registry 2026-07-17 00:33:23 +04:00
sindoring c5900368d4 refactor(M03): extract WMO placement registry 2026-07-17 00:33:09 +04:00
sindoring 79bc4e6ffc coord(M03): claim WMO placement registry 2026-07-17 00:26:32 +04:00
sindoring 5aa40d6afb docs(M03): accept WMO placement resolver 2026-07-17 00:24:37 +04:00
sindoring d2cb52ff6b merge M03 WMO placement resolver 2026-07-17 00:23:29 +04:00
sindoring c2e0696fe0 docs(M03): hand off WMO placement resolver 2026-07-17 00:23:18 +04:00
sindoring 05e216a953 refactor(M03): extract WMO placement resolver 2026-07-17 00:22:53 +04:00
sindoring f01302fd44 coord(M03): claim WMO placement resolver 2026-07-17 00:16:39 +04:00
sindoring 4f40c779b9 docs(M03): accept M2 build batch planner 2026-07-17 00:14:06 +04:00
sindoring 3f847171f8 merge M03 M2 build batch planner 2026-07-17 00:13:11 +04:00
sindoring af9d077905 docs(M03): hand off M2 build batch planner 2026-07-17 00:13:01 +04:00
sindoring be6db7f7e0 refactor(M03): extract M2 build batch planner 2026-07-17 00:12:42 +04:00
sindoring a562d126b9 coord(M03): claim M2 build batch planner 2026-07-17 00:08:54 +04:00
sindoring 9d74d996d5 docs(M03): accept M2 placement grouper 2026-07-17 00:07:11 +04:00
sindoring f88bf9714e merge M03 M2 placement grouper 2026-07-17 00:06:19 +04:00
sindoring 7587d3bd3c docs(M03): hand off M2 placement grouper 2026-07-17 00:06:10 +04:00
sindoring 9df32d38ef refactor(M03): extract M2 placement grouper 2026-07-17 00:05:49 +04:00
sindoring 9c9020e60f coord(M03): claim M2 placement grouper 2026-07-16 23:57:35 +04:00
sindoring c34e538dcf docs(M03): accept M2 placement transform resolver 2026-07-16 23:54:41 +04:00
sindoring 396be5e69d merge M03 M2 placement transform resolver 2026-07-16 23:52:34 +04:00
sindoring 1792938567 docs(M03): hand off M2 placement transform package 2026-07-16 23:52:20 +04:00
sindoring 0a09d7bc1e refactor(M03): extract M2 placement transform resolver 2026-07-16 23:51:59 +04:00
sindoring 91f0ba8433 coord(M03): claim M2 placement transform resolver 2026-07-16 23:38:29 +04:00
sindoring 1b6c46870d docs(M03): accept M2 unique placement registry 2026-07-16 10:02:14 +04:00
sindoring 2342430439 merge M03 M2 unique placement registry 2026-07-16 10:01:34 +04:00
sindoring c41e8886a6 docs(coordination): hand off M2 unique registry 2026-07-16 10:01:23 +04:00
sindoring acb3a809bb refactor(M03): extract M2 unique placement registry 2026-07-16 10:01:09 +04:00
sindoring c13c993645 coord(M03): claim M2 unique placement registry 2026-07-16 09:54:44 +04:00
sindoring 57ae1b8f16 docs(M03): accept terrain chunk queue planner 2026-07-16 09:52:59 +04:00
sindoring 24aef13801 merge M03 terrain chunk queue planner 2026-07-16 09:52:22 +04:00
sindoring 4b90dbc1c1 docs(coordination): hand off terrain chunk queue planner 2026-07-16 09:52:18 +04:00
sindoring f800e11cd2 refactor(M03): extract terrain chunk queue planner 2026-07-16 09:52:01 +04:00
sindoring 0d2c820ea7 coord(M03): claim terrain chunk queue planner 2026-07-16 09:47:11 +04:00
sindoring b568768866 docs(M03): accept terrain chunk LOD planner 2026-07-16 09:45:09 +04:00
sindoring f36fabb8e1 merge M03 terrain chunk LOD planner 2026-07-16 09:44:31 +04:00
sindoring 9e69a8a95b docs(coordination): hand off terrain LOD planner 2026-07-16 09:44:27 +04:00
sindoring eb94cfc646 refactor(M03): extract terrain chunk LOD planner 2026-07-16 09:44:11 +04:00
sindoring aa21aafdfa coord(M03): claim terrain chunk LOD planner 2026-07-16 09:37:45 +04:00
sindoring ea08b8ef51 docs(M03): accept terrain mesh cache integration 2026-07-16 09:30:59 +04:00
sindoring 630a0c131d merge(M03): integrate terrain mesh cache service 2026-07-16 09:30:02 +04:00
sindoring 10815be766 coord(M03): hand off terrain mesh cache service 2026-07-16 09:29:55 +04:00
sindoring 97480e06bb refactor(M03): extract terrain quality mesh cache 2026-07-16 09:29:38 +04:00
sindoring 6617dfe74e coord(M03): claim terrain mesh cache service 2026-07-16 09:25:43 +04:00
sindoring 61bad09a9c docs(M03): accept entity presentation integration 2026-07-16 01:12:55 +04:00
sindoring 606770c525 merge(M03): integrate entity presentation facade 2026-07-16 01:12:21 +04:00
sindoring b73b2e74a2 coord(M03): hand off entity presentation facade 2026-07-16 01:12:17 +04:00
sindoring 1807c3a363 feat(M03): add entity presentation facade 2026-07-16 01:11:58 +04:00
sindoring 3c5880f190 coord(M03): extend entity presentation fixture claim 2026-07-16 01:05:21 +04:00
sindoring a7b2f9475b coord(M03): claim entity presentation facade 2026-07-16 01:04:32 +04:00
sindoring a85e416cb8 docs(M03): accept environment snapshot integration 2026-07-16 01:01:25 +04:00
sindoring c69abd634f merge(M03): integrate environment snapshot facade 2026-07-16 01:00:41 +04:00
sindoring cd23a62be8 coord(M03): hand off environment snapshot facade 2026-07-16 01:00:36 +04:00
sindoring 2ee647a220 feat(M03): add environment snapshot facade 2026-07-16 01:00:19 +04:00
sindoring 60f1e363c0 coord(M03): claim facade environment snapshot 2026-07-16 00:55:17 +04:00
sindoring cb103e1fde docs(M03): accept facade ground query integration 2026-07-16 00:52:46 +04:00
sindoring d6e5b530ef merge(M03): integrate ground query cold-start fix 2026-07-16 00:52:08 +04:00
sindoring 8c46065ada coord(M03): update ground query handoff 2026-07-16 00:52:04 +04:00
sindoring 9408d887ba fix(M03): support cold rendered ground sample startup 2026-07-16 00:51:52 +04:00
sindoring 80cb08422b merge(M03): integrate facade ground query 2026-07-16 00:49:53 +04:00
sindoring 0773de5977 coord(M03): hand off facade ground query 2026-07-16 00:49:47 +04:00
sindoring b697a896e8 rnd(M03): add rendered ground query facade 2026-07-16 00:49:28 +04:00
sindoring 6117e5282e coord(M03): claim facade ground query 2026-07-16 00:40:46 +04:00
sindoring 53d84ccc8a docs(M03): accept streamer internal access gate 2026-07-16 00:38:58 +04:00
sindoring 7e35de7f61 merge(M03): integrate streamer internal access gate 2026-07-16 00:37:29 +04:00
sindoring ba9f89691d coord(M03): hand off streamer access gate 2026-07-16 00:37:15 +04:00
sindoring b58bf2caa6 test(M03): enforce streamer internal access boundary 2026-07-16 00:37:00 +04:00
sindoring 52ea639d64 coord(M03): claim streamer internal access gate 2026-07-16 00:33:32 +04:00
sindoring a023d76eea docs(M03): accept render budget scheduler integration 2026-07-16 00:32:11 +04:00
sindoring 7ece2abea2 merge(M03): integrate render budget scheduler 2026-07-16 00:31:30 +04:00
sindoring 10c5d4cc12 coord(M03): hand off render budget scheduler 2026-07-16 00:31:21 +04:00
sindoring e52f703da5 rnd(M03): extract render budget scheduler 2026-07-16 00:31:01 +04:00
sindoring 91f0724ce2 coord(M03): claim render budget scheduler 2026-07-16 00:22:16 +04:00
sindoring 650d584ff0 docs(M03): accept streaming planner integration
Work-Package: M03-RND-STREAMING-PLANNER-001
Agent: sindo-main-codex
Tests: post-merge planner/facade/focus/coordinate/manifest/shutdown and repository gates pass
Fidelity: target selection formulas and renderer behavior preserved
2026-07-16 00:11:57 +04:00
sindoring 643850c8ee Merge planner cold-start fix
Work-Package: M03-RND-STREAMING-PLANNER-001
Agent: sindo-main-codex
Tests: stale-class-cache headless verifier passes
Fidelity: no planning behavior change
2026-07-16 00:11:20 +04:00
sindoring ad8fc41ace fix(M03): support cold planner startup
Work-Package: M03-RND-STREAMING-PLANNER-001
Agent: sindo-main-codex
Tests: stale-class-cache headless planner verifier passes
Fidelity: type-resolution-only fix; planning behavior unchanged
2026-07-16 00:11:07 +04:00
sindoring d0f74f840b Merge work package M03-RND-STREAMING-PLANNER-001
Work-Package: M03-RND-STREAMING-PLANNER-001
Agent: sindo-main-codex
Tests: planner/facade/focus/coordinate/manifest/shutdown contracts, dry-run and repository gates
Fidelity: preserves target sets, boundary prefetch, queue priority, caches and visible renderer behavior
2026-07-16 00:08:32 +04:00
sindoring 5b10288ff7 docs(M03): publish streaming planner handoff
Work-Package: M03-RND-STREAMING-PLANNER-001
Agent: sindo-main-codex
Tests: planner and repository gates pass
Fidelity: no renderer policy or visual change
2026-07-16 00:08:24 +04:00
sindoring e887c3bad5 rnd(M03): extract streaming target planner
Work-Package: M03-RND-STREAMING-PLANNER-001
Agent: sindo-main-codex
Tests: planner/facade/focus/coordinate/manifest/shutdown contracts, seven-checkpoint dry-run, repository gates
Fidelity: preserves wanted/retained radii, boundary prefetch, editor preview, queue priority and cache/render behavior
2026-07-16 00:08:02 +04:00
sindoring 0c24f96ae1 rnd(M03): claim streaming target planner
Work-Package: M03-RND-STREAMING-PLANNER-001
Agent: sindo-main-codex
Tests: not run (coordination-only commit)
Fidelity: no runtime change
2026-07-15 23:58:57 +04:00
137 changed files with 12700 additions and 793 deletions
+229
View File
@@ -27,6 +27,12 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
- `src/render/world_render_facade.gd` - стабильная граница runtime/tools для focus и metrics.
- `src/scenes/streaming/streaming_world_loader.gd` - внутренний runtime streamer мира.
- `src/render/wmo/wmo_placement_resolver.gd` - value-only WMO path, identity and transform rules.
- `src/render/wmo/wmo_placement_registry.gd` - WMO placement-key to tile/global reference sets; Nodes and render jobs remain in the streamer.
- `src/render/wmo/wmo_render_build_step_planner.gd` - mesh-first lightweight WMO group operation and cursor planning without Nodes or Resources.
- `src/render/wmo/wmo_render_build_queue.gd` / `wmo_render_build_job.gd` - typed pending group jobs, FIFO placement keys and strong root/resource references without engine destruction.
- `src/render/wmo/wmo_render_resource_cache_state.gd` - validated lightweight WMO render Resources, negative cache and pending cache paths without ResourceLoader I/O.
- `src/render/wmo/wmo_scene_resource_cache_state.gd` - validated cached-WMO PackedScenes, negative cache and pending `.tscn` paths without file/I/O/Node ownership.
- `src/scenes/streaming/eastern_kingdoms_streaming.tscn` - текущая сцена для проверки Azeroth/Eastern Kingdoms.
- `addons/mpq_extractor/loaders/adt_builder.gd` - сборка ADT terrain, control splat материалов и MH2O liquids.
- `addons/mpq_extractor/loaders/m2_builder.gd` - сборка статического M2 mesh/material.
@@ -394,6 +400,12 @@ Known limits:
- `WMOBuilder` строит group meshes;
- runtime registry дедуплицирует WMO по `unique_id`;
- WMO на границе ADT не дублируются;
- `WmoPlacementRegistry` owns only placement reference sets; the streamer keeps
the parallel key-to-Node map and performs final-release cancellation/free;
- `WmoRenderBuildStepPlanner` selects one mesh or MultiMesh group and its next
cursors; the streamer still materializes Nodes and consumes the frame permit;
- `WmoRenderBuildQueue` owns pending typed jobs/FIFO keys and releases references
on cancel/reset; the streamer still validates and frees engine objects;
- большие WMO не должны инстанцироваться как тяжелые `.tscn` во время движения;
- добавлен lightweight render-cache path для WMO groups;
- `wmo_render_group_ops_per_tick` ограничивает подключение WMO groups по кадрам;
@@ -889,6 +901,223 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
visibility, materials or animation. Environment, entity visuals and ground
query remain explicit follow-up facade contracts in M03.
## 2026-07-15 StreamingTargetPlanner Extraction
- Runtime and editor wanted/retained ADT set calculation now lives in the
scene-free `StreamingTargetPlanner`.
- `StreamingTargetPolicy` preserves existing chunk/tile radii, warm/prewarm/
retain margins and the clamped boundary-prefetch threshold.
- `StreamingTargetPlan` freezes its focus/wanted/retained result; the streamer
still owns available paths, load-queue distance sorting, loaded-tile LOD/detail
state, async work, caches, nodes and RIDs.
- Asset-free fixtures cover center, edge/corner prefetch, catalog filtering,
clamp and editor behavior plus bounded High-like iteration timing.
- Cache versions, quality profiles, queue priority and visible rendering rules
are unchanged by the extraction.
## 2026-07-16 RenderBudgetScheduler Extraction
- The scene-free `RenderBudgetScheduler` now owns the per-frame counters for 16
named renderer operation lanes. It owns no queue, task, cache, Node or RID.
- `StreamingWorldLoader` snapshots its existing exported limits once per process
frame and asks for a boolean permit immediately before each historically
budgeted operation.
- Chunk removals and creations intentionally share one `chunk_geometry` lane;
the existing removal-first drain order and combined limit are preserved.
- Loader teardown cancels permit issuance before waiting for asynchronous work.
Existing worker/result staleness checks and resource cleanup remain unchanged.
- Asset-free contracts cover exact bounds, independent/shared lanes, frame reset,
invalid inputs, terminal cancellation, detached diagnostics and hot-path timing.
- Defaults, quality presets, queue order, cache versions and visible rendering
rules are unchanged. Asset-backed p95/p99 comparison remains required.
## 2026-07-16 Streamer Internal Access Gate
- `verify_renderer_internal_access.gd` derives private queue, task, cache and
tile-state field names from `StreamingWorldLoader` declarations and rejects
external member/reflection access in gameplay and Godot `EditorPlugin` package sources.
- The gate follows the implementation inventory instead of duplicating a manual
list, so a newly named private renderer field is covered automatically when it
matches those ownership categories.
- Scene composition may still reference the loader script and the facade's
implementation path; those are composition details, not mutable queue access.
- Renderer diagnostic probes are added explicitly as their facade contracts land.
- This is a source-only boundary check and changes no renderer behavior or fidelity.
## 2026-07-16 Rendered Ground Query Facade
- `WorldRenderFacade.sample_ground_height()` accepts `GodotWorldPosition` and
returns renderer-owned immutable `RenderedGroundSample` from already loaded
quality/tile-LOD render meshes, avoiding a render-to-gameplay dependency.
- Its factories load the predeclared Script path instead of relying on a warm
global-class cache; a dedicated cold-start verifier covers this bootstrap path.
- `renderer_ground_query_snapshot()` preserves the terrain probe's tile readiness,
queue position, mesh bounds and nearby seam-fallback diagnostics as a detached
Dictionary; callers receive no queue, tile-state, Mesh or Node reference.
- `probe_render_terrain_height.gd` now uses this facade snapshot and no longer reads
`_tile_states`, `_tile_load_queue`, `_tile_loading_tasks` or `_available_tiles`.
- Gameplay continues using the independently composed M02 `TerrainQuery`/
`AdtTerrainQuery`. Loaded render meshes are diagnostic and are not authoritative
movement collision.
- The ray height, 3x3 tile search and `2/5/10/20/40`-unit nearby fallback are
preserved. No terrain geometry, cache, streaming or visual behavior changed.
## 2026-07-16 Terrain Chunk LOD Planner Extraction
- `TerrainChunkLodPlanner` now owns the pure mapping from populated parsed ADT
chunk indices to desired terrain LOD 0/1/2.
- `TerrainChunkLodPolicy` snapshots the existing enabled flag, three raw chunk
radii and chunk size once per streaming-target application.
- Horizontal XZ distance to the chunk center, inclusive squared thresholds,
sparse-index omission and historical negative-radius squaring are unchanged.
- `StreamingWorldLoader` still owns tile/chunk state, queues, operation budgets,
creation/removal, Mesh/Node/RID finalization and tile-LOD fallback.
- Cache formats, quality presets and visible behavior are unchanged. The
scene-free contract is formula evidence only; asset-backed p95/p99 and visual
acceptance remain required.
## 2026-07-16 Terrain Chunk Geometry Queue Planner Extraction
- `TerrainChunkGeometryQueuePlanner` now compares current and desired chunk state
and returns fresh chunk create/remove request arrays.
- LOD mismatches and absent chunks retain the existing request shape; creates
remain sorted by horizontal distance from chunk origin to streaming focus.
- Desired tile LOD still removes every current chunk, while sparse parsed chunk
records retain the existing tile-origin distance fallback.
- `StreamingWorldLoader` still owns queue storage/drains, removal-first execution,
the shared `chunk_geometry` budget lane and every Mesh/Node/RID side effect.
- Cache formats, quality profiles and visual rules are unchanged. Asset-backed
p95/p99 and visual acceptance remain required.
## 2026-07-16 M2 Unique Placement Registry Extraction
- `M2UniquePlacementRegistry` now owns positive ADT `MDDF.uniqueId` reservations
across streamed tiles using the existing `uid:<decimal>` keys.
- Missing/zero/negative IDs remain untracked; duplicates inside one reservation
call are suppressed and another tile records one skipped key until owner release.
- Owner release still lets the loader find eligible waiting tiles and enqueue the
existing detail retry path. First ownership remains tile-processing-order based.
- `StreamingWorldLoader` still owns tile state, retry queues, grouping/build tasks,
M2 caches, MultiMesh/Node/Mesh/material/RID creation and all operation budgets.
- M2/ADT cache formats, transforms, profiles and visible rules are unchanged.
Asset-backed cross-boundary p95/p99 and visual acceptance remain required.
## 2026-07-16 M2 Placement Transform Resolver Extraction
- `M2PlacementTransformResolver` now owns the pure ADT M2 basis and local
origin-compensation formulas used by worker grouping, placeholders and direct
instance creation.
- Regular models retain `Basis.from_euler`; `ElwynnCliffRock01/02` retain their
narrow shell correction; `NewWaterfall` and `ElwynnTallWaterfall01` retain
calibrated world yaw, local fall-axis twist and tall-sheet anchor compensation.
- Path separator/case/MDX handling, constants and caller-specific final scale
clamps are unchanged. The resolver owns no Node, task, cache or RID.
- Cache formats, placement positions and visible rules are unchanged by the
extraction. Asset-backed visual recheck and general placement parity remain gaps.
## 2026-07-16 M2 Placement Grouper Extraction
- `M2PlacementGrouper` now owns pure worker-side validation, historical path
normalization and ordered tile-local transform grouping of ADT M2 placements.
- Invalid records, backslash replacement, lowercase `.mdx/.mdl` conversion,
first-seen ordering and the `0.0001` scale clamp are unchanged.
- The loader retains tasks, mutex/result queues, stale-result checks, tile/build
state, caches and all Node/MultiMesh/Mesh/material/RID finalization.
- Cache formats, batching, profiles and visible rules are unchanged. Asset-backed
traversal p95/p99, visual recheck and spatial-cell grouping remain pending.
## 2026-07-17 M2 Build Batch Planner Extraction
- `M2BuildBatchPlanner` now owns pure static/animated batch-limit selection,
remaining-count capping and next-offset/group-completion calculation.
- Existing animated/static limits still clamp to at least one; resource readiness,
queue rotation, serial progression and `M2_BUILD` permit consumption are unchanged.
- The loader retains build jobs/queues, tile cancellation, animation/mesh caches,
retries and all Node/MultiMesh/Mesh/material/RID finalization.
- Cache formats, quality profiles, batching output and visible rules are unchanged.
Asset-backed p95/p99 and spatial-cell batching evidence remain pending.
## 2026-07-17 WMO Placement Resolver Extraction
- `WmoPlacementResolver` now owns lowercase/slash cache-key normalization,
positive MODF `uid:<decimal>` identity with the legacy tile/index fallback and
world `Transform3D` composition.
- Lightweight render roots, cached scenes and live prototypes now share the same
position/Euler/unclamped-scale formula.
- The loader retains registry refs, jobs/queues, cache/load state, resource
fallback, cancellation and all Node/Mesh/MultiMesh/material/RID ownership.
- Cache formats, placement values, profiles and visible rules are unchanged.
Asset-backed placement/p95/p99 and general WMO parity remain pending.
## 2026-07-17 WMO Render Resource Cache State Extraction
- `WmoRenderResourceCacheState` now owns validated lightweight-WMO render
Resources, negative entries and normalized-path to pending-cache-path records.
- `StreamingWorldLoader` still constructs cache paths, calls `ResourceLoader`,
polls requests and validates `WMOStreamingResource` script identity plus
`FORMAT_VERSION` before completing cache state.
- Map reset and orderly request draining clear pending/negative state while
retaining accepted Resources; final runtime cache release clears all state.
- Missing render-cache files still are not negatively cached, preserving retry
and cached-scene/live fallback behavior. Formats, profiles and visuals are unchanged.
- Asset-backed corrupt-cache, traversal/leak p95/p99 and paired fidelity evidence
remain pending.
## 2026-07-17 WMO Scene Resource Cache State Extraction
- `WmoSceneResourceCacheState` now owns validated cached-WMO PackedScenes,
negative entries and normalized-path to pending-`.tscn` records.
- `StreamingWorldLoader` still checks file existence and
`wmo_max_runtime_scene_mb`, calls `ResourceLoader`, instantiates a validation
probe, checks WMOBuilder cache metadata and frees the probe before adoption.
- Missing files, oversize scenes, request errors, load failures and stale scenes
retain their prior negative-cache and live-prototype fallback behavior.
- Map reset clears pending/negative state while retaining accepted scenes; final
shutdown releases all scene state. Formats, profiles and visuals are unchanged.
- Asset-backed oversize/stale fixtures, traversal/leak p95/p99 and paired fidelity
evidence remain pending.
## 2026-07-16 World Environment Snapshot Facade
- `WorldEnvironmentSnapshot` carries one immutable finite time-of-day value,
normalized to `[0, 24)` hours; invalid non-finite input is explicit.
- `WorldRenderFacade.apply_environment_snapshot()` delegates the snapshot to the
scene-owned `WowSkyController`. The facade owns no `Environment`, sun, skybox,
shader global or DBC sampling state.
- Checkpoint capture now submits its manifest time through the facade instead of
locating `WowSkyController` and mutating `use_system_time`, `time_speed` and
`fixed_time_hours` directly.
- Applying a snapshot performs the same three fixed-clock assignments as the old
capture path. Light/Area/Skybox DBC lookup, interpolation, fog, sun and shader
behavior remain unchanged; weather and indoor environment state are follow-up work.
## 2026-07-16 World Entity Presentation Facade
- `EntityPresentationSnapshot` contract version 1 combines session `EntityId`,
typed Godot world position, visual PackedScene path, finite yaw/scale and visibility.
- `WorldRenderFacade.present_entity()` and `remove_entity()` delegate to an explicit
scene-owned `WorldEntityPresenter`; both runtime scenes compose that service.
- The presenter owns only visual subtrees. Same-path snapshots update transform/
visibility, changed paths replace after successful instantiation, and removal
detaches plus queues the owned root for deletion.
- The local player, authoritative state, GUID mapping and asset/display selection
remain outside this service. Runtime emits no entity commands yet, so baseline
output is unchanged.
- PackedScene loading is currently synchronous on create/replace and is a documented
prototype limitation, not an accepted network-scale frame-critical path.
## 2026-07-16 Terrain Quality Mesh Cache Extraction
- `TerrainQualityMeshCache` now owns full-quality terrain revisit Mesh references,
tile-key LRU order and source admission outside `StreamingWorldLoader`.
- The historical rules are unchanged: successful restore touches newest; store
trims oldest to `terrain_quality_mesh_cache_limit`; zero/negative capacity keeps
no entries; control-splat/splat sources are never retained in this revisit cache.
- The loader keeps three narrow adapters: restore into tile state, store after a
full-quality result and clear during existing world reset/shutdown.
- ADT parsing, quality tasks/results, tile state, cache format versions, material/
Node/RID finalization, budgets and visible terrain behavior remain loader-owned.
## Practical Rule For Future Work
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
@@ -0,0 +1,130 @@
# M03-RND-FACADE-ENTITY-001 — World entity presentation commands
<!-- OPENWC_CLAIM:M03-RND-FACADE-ENTITY-001:sindo-main-codex:2026-07-18 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-facade-entity`
- Lease expires UTC: 2026-07-18
- Integrator: M03 milestone integrator
## Outcome
Add immutable, versioned world-entity presentation input and stable facade
commands for idempotent present/update and removal, backed by a renderer-owned
scene service with explicit entity subtree ownership.
## Non-goals
- Define authoritative entity/gameplay/network state or allocate/map identities.
- Move the local player controller/presenters behind the world-entity service.
- Implement build-12340 display IDs, equipment, animation, effects or visibility rules.
- Add background loading, pooling, MultiMesh conversion or a generic ECS/framework.
- Change streamer queues, caches, world assets, baseline visuals or target status.
## Paths
- Exclusive: `src/render/entities/entity_presentation_snapshot.gd`,
`src/render/entities/world_entity_presenter.gd`,
`src/tools/verify_world_entity_presentation.gd`,
`src/tests/scenes/world_entity_visual_replacement_fixture.tscn`,
`docs/modules/world-entity-presentation.md`, this claim
- Shared/hotspots: `src/render/world_render_facade.gd`, both runtime streaming
scenes, `src/tools/verify_world_render_facade.gd`, `docs/modules/world-renderer.md`,
`docs/modules/README.md`, `RENDER.md`, `targets/03-renderer-facade.md`
- Reused unchanged: `EntityId`, `GodotWorldPosition`, existing asset-free
character presentation model fixture
- Generated/ignored: `.godot`, native DLL, generated resources, caches and
proprietary renderer corpus
## Contracts and data
- Contract version: `EntityPresentationSnapshot.CONTRACT_VERSION = 1`
- Snapshot: valid session-local `EntityId`, typed Godot world position, visual
PackedScene path, finite yaw radians, positive finite uniform scale and visibility
- Public API: `WorldRenderFacade.present_entity`, `remove_entity`,
`entity_presentation_snapshot`
- Presenter behavior: present is idempotent by entity key; same visual updates
transform/visibility, changed visual replaces the owned subtree; remove is explicit
- Schema/cache/coordinate contracts: no persisted schema or cache change
## Dependencies
- Requires: integrated M03 environment facade master `a85e416`, M01 identity and
coordinate contracts
- Blocks: completion of the first M03 facade checklist item and later entity renderer extraction
- External state: none; tests use a repository-owned asset-free PackedScene fixture
## Verification
- Commands: cold snapshot contract; presenter lifecycle/ownership regression;
facade delegation and runtime wiring; identity/coordinate/internal-access;
renderer manifest/shutdown/planner/scheduler/focus and repository gates
- Fixtures: invalid identity/position/yaw/scale/path, spawn, idempotent update,
visual replacement, missing asset, removal, detached diagnostic snapshot and teardown
- Fidelity evidence: no runtime commands are produced yet, so existing scene output
and seven baseline plans remain unchanged
- Performance budget: constant-time entity lookup; synchronous resource load only
on first presentation/visual replacement and explicitly excluded from hot-path acceptance
## Documentation deliverables
- Inline public API docs: snapshot, facade commands and presenter lifecycle
- Module specification: new world-entity-presentation specification
- Data-flow diagram: read model through facade into owned visual subtree
- Sequence/state/dependency diagrams: present/update/replace/remove/failure/teardown
- Source map/status updates: module registry, world renderer and `RENDER.md`
## Simplicity and naming
- Important names introduced: `EntityPresentationSnapshot`, `WorldEntityPresenter`,
`present_entity`, `remove_entity`
- Simplest considered solution: one immutable full visual snapshot and one
dictionary-owned Node3D presenter below an explicit scene service
- Rejected complexity/abstractions: ECS, event bus, command hierarchy, factory
registry, pooling, async loader framework or duplicate identity registry
- Unavoidable complexity and justification: renderer must own and deterministically
replace/free visual subtrees while authoritative state remains external
- Measured optimization evidence: no optimization claim; lifecycle is asset-free tested
## Status
- State: ready-for-review
- Done: immutable contract-v1 snapshot, presenter ownership lifecycle, facade
commands, runtime composition, asset-free regressions and module documentation
- Next: M03 integrator reviews and merges `1807c3a`
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-FACADE-ENTITY-001:1807c3a -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-FACADE-ENTITY-001:606770c -->
## Handoff
- Commit: `1807c3a`
- Results: facade now exposes idempotent `present_entity`, explicit
`remove_entity` and detached diagnostics. Both runtime scenes compose a
`WorldEntityPresenter` that owns one replaceable visual subtree per valid
session `EntityId`; local player and authoritative state remain separate.
- Verification: entity `snapshot_cases=8 lifecycle_cases=14 runtime_scenes=2`;
facade `delegation=9 entity_contract=6`; identities `5/5/6/5`; character
presentation `9/10/3/1`; coordinate boundary `files=119`; internal access
`43/7/9/1`; environment/ground/terrain, manifest `7/7`, shutdown, scheduler
(`20000` iterations), planner (`250` iterations), focus, editor parse, dry-run,
coordination, documentation and diff gates passed.
- Fidelity: runtime produces no world-entity commands yet, so all seven baseline
plans and existing player/world output remain unchanged. No build-12340
display, equipment, animation, effects or visibility parity claim is added.
- External/local inputs: ignored native DLL and generated ADT scripts supported
editor/dry-run only. Missing proprietary ADT/DBC/character corpus produced the
expected degraded diagnostics.
- Remaining risks: PackedScene create/replace loads synchronously and is not an
accepted network-scale hot path; resource paths are adapter inputs rather than
stable content IDs; animation/equipment/effects and scale profiling remain open.
- Documentation updated: new world-entity-presentation API/input-output/data-flow/
lifecycle/sequence/ownership/failure/configuration/migration/diagnostic/source-map
spec; world renderer, module registry and `RENDER.md` updated.
- Integration: accepted in master merge `606770c`; post-merge entity/facade,
character, internal-access, manifest, shutdown, scheduler and planner gates passed.
@@ -0,0 +1,120 @@
# M03-RND-FACADE-ENVIRONMENT-001 — Renderer environment snapshot boundary
<!-- OPENWC_CLAIM:M03-RND-FACADE-ENVIRONMENT-001:sindo-main-codex:2026-07-18 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-facade-environment`
- Lease expires UTC: 2026-07-18
- Integrator: M03 milestone integrator
## Outcome
Add an immutable world-environment snapshot accepted by `WorldRenderFacade`,
delegate it to the existing DBC sky controller, and migrate deterministic render
checkpoint time setup away from direct sky-controller property mutation.
## Non-goals
- Change Light/Area/Skybox DBC selection, interpolation, fog, sun or shader rules.
- Add weather, indoor lighting, new clock semantics or build-12340 parity claims.
- Extract the sky service or move its Environment/Node/RID ownership.
- Change runtime scene defaults, cache formats, streaming behavior or target status.
## Paths
- Exclusive: `src/render/environment/world_environment_snapshot.gd`,
`src/tools/verify_world_environment_snapshot.gd`, this claim
- Shared/hotspots: `src/render/world_render_facade.gd`,
`src/scenes/sky/wow_sky_controller.gd`, both runtime streaming scenes,
`src/tools/capture_render_checkpoints.gd`,
`src/tools/verify_world_render_facade.gd`, `docs/modules/world-renderer.md`,
`docs/modules/README.md`, `RENDER.md`, `targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated resources, caches and
proprietary renderer corpus
## Contracts and data
- Public API: immutable `WorldEnvironmentSnapshot` and
`WorldRenderFacade.apply_environment_snapshot`
- Snapshot input: finite time-of-day hours normalized into one 24-hour day
- Adapter: explicit `world_environment_controller_path`; facade/controller retain
no ownership of caller snapshot
- Schema/cache/coordinate contracts and runtime defaults: unchanged
- Consumer: deterministic renderer checkpoint capture; future world-state adapter
## Dependencies
- Requires: integrated M03 facade/ground-query master `cb103e1`
- Blocks: completion of the facade environment checklist slice and later sky extraction
- External state: none for contract tests; visual comparison still needs proprietary corpus
## Verification
- Commands: dedicated cold snapshot contract, facade delegation/wiring regression,
capture source-boundary regression, renderer manifest/dry-run and repository gates
- Fixtures: normalization, non-finite rejection, missing/wrong controller,
exact snapshot delegation and existing runtime scene wiring
- Fidelity evidence: preserve existing fixed-time application and all DBC-derived
visual logic; seven checkpoint plans remain valid
- Performance budget: one immutable value allocation per environment update and
constant-time main-thread delegation
## Documentation deliverables
- Inline public API docs: snapshot, facade adapter and sky-controller application
- Module specification: update existing world-renderer specification
- Data-flow diagram: snapshot producer through facade into DBC sky controller
- Sequence/state/dependency diagrams: application/failure path and ownership notes
- Source map/status updates: world-renderer module, module registry if needed, `RENDER.md`
## Simplicity and naming
- Important names introduced: `WorldEnvironmentSnapshot`, `apply_environment_snapshot`
- Simplest considered solution: one immutable time snapshot over the existing sky implementation
- Rejected complexity/abstractions: generic weather graph, duplicated lighting DTO,
event bus, service container or alternate Environment owner
- Unavoidable complexity and justification: facade resolves a separate scene-owned
sky controller because the streamer does not own outdoor environment rendering
- Measured optimization evidence: not applicable; bounded constant-time adapter
## Status
- State: ready-for-review
- Done: immutable value, facade/controller adapter, checkpoint migration,
regressions and renderer module documentation
- Next: M03 integrator reviews and merges `2ee647a`
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-FACADE-ENVIRONMENT-001:2ee647a -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-FACADE-ENVIRONMENT-001:c69abd6 -->
## Handoff
- Commit: `2ee647a`
- Results: `WorldEnvironmentSnapshot` normalizes finite time into one day and
exposes non-finite failure explicitly. `WorldRenderFacade` delegates the
immutable value to the existing sky controller; checkpoint capture no longer
locates or mutates that controller directly.
- Verification: snapshot `contract=5`; facade `delegation=6 ground_contract=2
environment_contract=7 runtime_scenes=2 tools=3`; internal access `43/7/9/1`;
terrain, coordinate (`files=116 consumers=5`), manifest (`7/7`), shutdown,
scheduler (`20000` iterations), planner (`250` iterations), focus, editor parse,
dry-run capture, coordination, documentation and diff gates passed.
- Fidelity: applying a snapshot retains the previous three fixed-clock assignments
and leaves DBC profile/area/skybox lookup, interpolation, fog, sun, shader,
cache and streaming rules unchanged. Dry-run retained all seven plans and
reported dusk as `time=19.00`. No build-12340 parity claim is added.
- External/local inputs: ignored native DLL and generated ADT resource scripts
were copied from master for parse/dry-run only. Missing proprietary ADT/DBC/
character data produced expected degraded diagnostics.
- Remaining risks: the snapshot currently covers exact world time only; weather,
indoor state and asset-backed paired zone-transition evidence remain open.
- Documentation updated: inline value/facade/controller API plus world-renderer
API/input-output/data-flow/sequence/ownership/failure/capability/source-map and
`RENDER.md` implementation notes.
- Integration: accepted in master merge `c69abd6`; post-merge snapshot, facade,
internal-access, manifest, shutdown, scheduler, planner and focus gates passed.
@@ -0,0 +1,115 @@
# M03-RND-FACADE-GROUND-QUERY-001 — Rendered ground query facade
<!-- OPENWC_CLAIM:M03-RND-FACADE-GROUND-QUERY-001:sindo-main-codex:2026-07-18 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-facade-ground-query`
- Lease expires UTC: 2026-07-18
- Integrator: M03 milestone integrator
## Outcome
Expose typed ground-height sampling and a detached readiness diagnostic through
`WorldRenderFacade`, using already loaded render terrain meshes, and migrate the
terrain-height probe away from direct streamer state/queue access.
## Non-goals
- Replace the M02 gameplay-owned `TerrainQuery`/`AdtTerrainQuery` composition.
- Make rendered mesh sampling authoritative for movement, collision or server state.
- Add slopes, holes, liquid, WMO/M2 collision or async query callbacks.
- Change terrain meshes, streaming policy, queue order, cache formats or visuals.
- Complete environment/entity-visual facade contracts or mark M03 complete.
## Paths
- Exclusive: this claim
- Shared/hotspots: `src/render/world_render_facade.gd`,
`src/scenes/streaming/streaming_world_loader.gd`,
`src/tools/probe_render_terrain_height.gd`,
`src/tools/verify_world_render_facade.gd`,
`src/tools/verify_renderer_internal_access.gd`, `docs/modules/world-renderer.md`,
`docs/modules/terrain-query.md`, `RENDER.md`, `targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated ADT resource scripts,
extracted renderer corpus and probe reports
## Contracts and data
- Input: immutable `GodotWorldPosition` in Godot world units
- Output: renderer-owned immutable `RenderedGroundSample` plus optional detached diagnostic Dictionary
- Sampling source: loaded quality or tile-LOD Mesh and its live tile transform
- M02 ADT query cache, renderer cache versions and coordinate contracts: unchanged
## Dependencies
- Requires: M02 terrain query contract and integrated M03 facade/access gate `53d84cc`
- Blocks: removal of renderer diagnostic internal-access exception and later terrain service extraction
- External state: asset-backed probe is optional; contract tests use a facade double
## Verification
- Commands: facade query/snapshot isolation contract, internal-access gate,
terrain-query regression, focus/coordinate/manifest/shutdown regressions and repository gates
- Fixtures: available/unavailable sample, detached diagnostic, probe source migration
- Fidelity evidence: mesh ray/nearest fallback algorithm moves unchanged from the probe
- Performance budget: on-demand diagnostic query only; no per-frame query added
## Documentation deliverables
- Inline facade/streamer API docs
- World-renderer API/data-flow/sequence/ownership/recovery/source-map updates
- Terrain-query backend status/ownership clarification and renderer implementation note
## Simplicity and naming
- Important names: `sample_ground_height`, `renderer_ground_query_snapshot`
- Simplest approach: one typed facade query over one existing diagnostic read model
- Rejected complexity: duplicate ADT cache, physics service, callback bus, query node,
generic spatial interface or new dependency
- Unavoidable complexity: triangle-mesh ray query retains the existing bounded
nearby fallback for seam/edge diagnostic behavior
## Status
- State: ready-for-review
- Done: added renderer-owned typed sample, facade query/snapshot, migrated terrain
probe, expanded internal-access/coordinate gates and updated module docs
- Next: M03 integrator reviews and merges `b697a89` plus cold-start fix `9408d88`
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-FACADE-GROUND-QUERY-001:9408d88 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-FACADE-GROUND-QUERY-001:d6e5b53 -->
## Handoff
- Commits: implementation `b697a89`; cold-start fix `9408d88`
- Results: `WorldRenderFacade` delegates loaded-mesh ground sampling and returns
renderer-owned `RenderedGroundSample`; detached diagnostic snapshots preserve
the probe report while hiding queue/tile/Mesh/Node references. Gameplay remains
on the independent M02 `TerrainQuery` composition.
- Verification: facade `delegation=5 ground_contract=2 runtime_scenes=2 tools=3`;
cold rendered-ground value `contract=3` from an empty Godot class cache;
internal access `private_symbols=43 gameplay=7 editor_sources=9 renderer_tools=1`;
terrain query contracts passed; coordinate boundary `files=113 consumers=5`;
renderer manifest `7/7`; shutdown ownership, scheduler/planner, coordination,
documentation and diff gates passed.
- Fidelity: the probe's 5000-unit ray height, 3x3 tile search, highest-hit choice,
readiness fields and `2/5/10/20/40` nearby fallback moved unchanged. No terrain
geometry, streaming, cache, material, placement or visual rule changed. No
build-12340 parity claim is added.
- External/local inputs: ignored native DLL and generated ADT resource scripts
were copied from master only for editor/streamer parse and then removed.
Proprietary extracted world data was unavailable, producing expected degraded diagnostics.
- Remaining risks: triangle-mesh generation is on-demand/main-thread and diagnostic;
it is unsuitable for per-physics-frame gameplay; exact asset-backed probe output
still depends on local corpus readiness; environment/entity facade contracts remain.
- Documentation: inline facade/loader/value API; world-renderer API/data-flow/
sequence/ownership/recovery/capability/source map; terrain-query authority
clarification and renderer implementation notes updated.
- Integration: implementation accepted in master merge `80cb084`; cold-start fix
accepted in merge `d6e5b53`. Post-merge cold/facade/access/terrain/coordinate/
manifest/shutdown/scheduler/planner/focus and repository gates passed.
@@ -0,0 +1,106 @@
# M03-RND-INTERNAL-ACCESS-GATE-001 — Streamer internal access gate
<!-- OPENWC_CLAIM:M03-RND-INTERNAL-ACCESS-GATE-001:sindo-main-codex:2026-07-18 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-streamer-internal-access-gate`
- Lease expires UTC: 2026-07-18
- Integrator: M03 milestone integrator
## Outcome
Add an automated repository boundary gate proving gameplay code and Godot
`EditorPlugin` implementations do not access `StreamingWorldLoader` private
queue, task, cache or tile-state fields.
## Non-goals
- Change renderer behavior, queues, jobs, cache formats or facade contracts.
- Ban scene composition references to the loader script or facade adapter path.
- Migrate renderer-only diagnostic probes that inspect internal readiness/terrain;
those require the later metrics/ground-query facade contract.
- Mark M03 complete.
## Paths
- Exclusive: `src/tools/verify_renderer_internal_access.gd`, this claim
- Shared/hotspots: `docs/modules/world-renderer.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Read-only scan scope: `src/gameplay/**/*.gd`, `src/editor/**/*.gd`, Godot
`EditorPlugin` package scripts under `src/**/*.gd` and `addons/**/*.gd`, and
the streamer field declarations
## Contracts and data
- Input: repository GDScript source and actual private streamer declarations
- Output: deterministic pass/fail diagnostics with consumer path and symbol
- Runtime, scene, cache, coordinate and asset contracts: unchanged
## Dependencies
- Requires: integrated facade and scheduler master `a023d76`
- Blocks: safe service extraction and M03 dependency regression acceptance
- External state: none
## Verification
- Commands: dedicated internal-access gate, facade regression, coordination,
documentation and diff gates
- Fixtures: gameplay source discovery, EditorPlugin discovery and synthetic
forbidden symbol detection
- Fidelity evidence: source-only enforcement; no runtime or visual code changes
- Performance budget: one bounded repository source scan in a headless test process
## Documentation deliverables
- World-renderer boundary/verification/known-gap/source-map updates
- Renderer implementation note documenting diagnostic-tool exception
- No new module spec because this package adds a verifier to the existing renderer module
## Simplicity and naming
- Important name: `verify_renderer_internal_access.gd`
- Simplest approach: derive forbidden fields from the current streamer instead
of maintaining a second hand-written queue list
- Rejected complexity: language plugin, custom linter framework or runtime proxy
- Unavoidable complexity: recursive source discovery expands each actual
`EditorPlugin` root to its package scripts while excluding reference mirrors
## Status
- State: ready-for-review
- Done: derived 43 private renderer symbols, scanned seven gameplay and nine
EditorPlugin-package sources, added synthetic positive/negative fixtures and docs
- Next: M03 integrator reviews and merges `b58bf2c`
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-INTERNAL-ACCESS-GATE-001:b58bf2c -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-INTERNAL-ACCESS-GATE-001:7e35de7 -->
## Handoff
- Commit: `b58bf2c`
- Results: repository gate derives private queue/task/cache/state declarations
from the streamer and rejects external member/reflection access in gameplay and
EditorPlugin package sources. Current scope passes with `43/7/9` inventory counts.
- Verification: internal access, facade and budget scheduler headless contracts;
Godot editor scan with local ignored dependencies; coordination, documentation
and diff gates passed.
- Fidelity: source-only enforcement changes no runtime configuration, queue order,
cache version, placement, material, animation or visibility behavior. No
build-12340 parity claim is added.
- External/local inputs: ignored native DLL and two generated ADT resource scripts
were copied from master only for facade/editor smoke and then removed. Proprietary
extracted world data was unavailable, producing expected degraded-data diagnostics.
- Remaining risks: renderer diagnostic terrain probe remains an explicit exception;
whitespace-obfuscated dynamic reflection is outside the lightweight source gate;
future non-EditorPlugin authoring code is already prohibited by repository policy.
- Documentation: world-renderer boundary/verification/capability/known-gap/source
map and `RENDER.md` implementation note updated; no new module was introduced.
- Integration: accepted in master merge `7e35de7`; post-merge access gate
`private_symbols=43 gameplay=7 editor_sources=9`, facade/scheduler/planner and
repository gates passed.
@@ -0,0 +1,110 @@
# M03-RND-M2-BUILD-BATCH-PLANNER-001 — M2 build batch planner
<!-- OPENWC_CLAIM:M03-RND-M2-BUILD-BATCH-PLANNER-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-m2-build-batch-planner`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract pure static/animated M2 build batch sizing and cursor progression from
the loader build-job loop into a scene-free planner.
## Non-goals
- Move build-job/queue ownership, tile eligibility or cancellation.
- Move animation/mesh requests, cache lookup, queue rotation or retry behavior.
- Create Nodes, MultiMesh, Mesh, materials or RIDs.
- Change render-budget permit consumption or per-profile batch limits.
- Introduce spatial-cell batching or claim asset-backed performance parity.
## Paths
- Exclusive: `src/render/m2/m2_build_batch_planner.gd`,
`src/tools/verify_m2_build_batch_planner.gd`,
`docs/modules/m2-build-batch-planner.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`docs/modules/m2-placement-grouper.md`, `docs/modules/world-renderer.md`,
`docs/modules/README.md`, `RENDER.md`, `targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated ADT resources, caches and
proprietary renderer corpus
## Contracts and data
- Input: transform count, current offset, animated/static path selector and raw
animated/static batch limits
- Output: fresh plan Dictionary with effective batch size, batch count, next
offset and group-complete flag
- Selected batch limit clamps to at least one
- Batch count clamps to remaining non-negative transforms
- Group completes when no batch can be consumed or offset plus batch reaches end
- Queue/cache/schema/coordinate/profile values: unchanged
## Dependencies
- Requires: current master `9d74d99`; grouped transform arrays from
`M2PlacementGrouper`
- Blocks: broader M2 build-job state extraction
- External state: none; tests use scalar synthetic counts/limits
## Verification
- Commands: dedicated static/animated/limit/remaining/complete/empty/offset and
bounded timing verifier; loader source boundary; grouper/transform/registry plus
facade/internal-access/manifest/shutdown/documentation/coordination gates
- Fidelity evidence: exact batch/count/cursor formulas moved unchanged
- Performance budget: constant-time scalar/container calculation; no Node,
Resource, RID, task, queue, mutex, cache or file access
## Documentation deliverables
- Inline planner API documentation
- New module spec with inputs/outputs, data-flow, lifecycle, ownership, failure,
performance, fidelity limitations and source map
- Grouper, renderer/module registry and `RENDER.md` updates
## Simplicity and naming
- Important name: `M2BuildBatchPlanner`
- Simplest solution: one stateless `plan_batch` query with a small detached result
- Rejected complexity: job classes, state machine framework, callbacks, signals,
Resources, strategies or scheduler integration
- Unavoidable complexity: loader retains raw build-job Dictionaries
- Measured optimization evidence: bounded synthetic planning loop
## Status
- State: accepted
- Done: planner, loader adapter, regressions and documentation verified
- Next: M2 queue/resource state extraction and spatial-cell evidence
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-BUILD-BATCH-PLANNER-001:be6db7f -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-BUILD-BATCH-PLANNER-001:3f84717 -->
## Handoff
- Commit: `be6db7f` (`refactor(M03): extract M2 build batch planner`)
- Results: dedicated planner PASS (`cases=8`, `iterations=20000`,
`elapsed_ms=13.464`); grouper, transform resolver, M2 registry/dedupe, terrain
cache/LOD/queue, facade, internal-access, manifest, shutdown, scheduler,
streaming planner/focus, terrain-query and coordinate-boundary suites PASS;
editor import parsed with only expected missing private corpus diagnostics;
documentation PASS (`module_specs=18`, `required_files=7`); coordination PASS
with 23 pre-existing expired M00/M01 warnings; checkpoint dry-run planned `7/7`.
- Remaining risks: no private asset-backed p95/p99 or visual comparison; result
and loader job remain Dictionaries; resource/queue transitions and spatial-cell
batching are still loader-owned; negative offset behavior remains permissive.
- Documentation updated: inline public API; new
`docs/modules/m2-build-batch-planner.md`; placement grouper module spec,
renderer module registry, `docs/modules/world-renderer.md` and `RENDER.md`.
- Integration: accepted in master merge `3f84717`; post-merge planner
(`cases=8`, `iterations=20000`, `elapsed_ms=14.532`), grouper, transform
resolver, registry, internal-access, facade, manifest, documentation and
coordination gates passed.
@@ -0,0 +1,114 @@
# M03-RND-M2-PLACEMENT-GROUPER-001 — M2 placement grouper
<!-- OPENWC_CLAIM:M03-RND-M2-PLACEMENT-GROUPER-001:sindo-main-codex:2026-07-19 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-m2-placement-grouper`
- Lease expires UTC: 2026-07-19
- Integrator: M03 milestone integrator
## Outcome
Extract pure ADT M2 placement validation, path normalization and local-transform
grouping from the worker callback into a scene-free service.
## Non-goals
- Change placement ordering, coordinate conversion, transform formulas or clamps.
- Move WorkerThreadPool tasks, mutex/result queues, tile state or retry behavior.
- Move build jobs, animation/mesh loading, caches, Nodes, MultiMesh or RIDs.
- Generalize grouping policy, change batching or add a dependency.
- Claim build-12340 placement or visual parity beyond preserved formulas.
## Paths
- Exclusive: `src/render/m2/m2_placement_grouper.gd`,
`src/tools/verify_m2_placement_grouper.gd`,
`docs/modules/m2-placement-grouper.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`src/tools/verify_m2_placement_transform_resolver.gd`,
`docs/modules/m2-placement-transform-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 resources, caches and
proprietary renderer corpus
## Contracts and data
- Input: tile-local origin, MMDX name table and raw ADT M2 placement array
- Output: fresh Dictionary keyed by historically normalized relative M2 path;
each value is a fresh ordered Array of tile-local `Transform3D` values
- Invalid variants, out-of-range name IDs and empty paths are skipped
- Backslashes become slashes; lowercase `.mdx/.mdl` suffixes become `.m2`
- Transform basis/origin delegates to `M2PlacementTransformResolver`; final basis
scale retains the grouping-path clamp `max(scale, 0.0001)`
- Cache/schema/coordinate versions: unchanged
## Dependencies
- Requires: current master `c34e538`, `M2PlacementTransformResolver`
- Blocks: broader M2 worker/build decomposition
- External state: none; tests use synthetic names and placement dictionaries
## Verification
- Commands: dedicated invalid/name/path/group-order/default/transform/scale and
bounded timing verifier; loader source boundary; resolver/registry/dedupe plus
facade/internal-access/manifest/shutdown/documentation/coordination gates
- Fidelity evidence: exact loop rules and transform composition moved unchanged
- Performance budget: linear over placement count with fresh output containers;
no Node, Resource, RID, task, mutex, cache or file access
## Documentation deliverables
- Inline grouper API documentation
- New module spec with inputs/outputs, data-flow, lifecycle, ownership, failure,
performance, fidelity limitations and source map
- Transform resolver, renderer/module registry and `RENDER.md` updates
## Simplicity and naming
- Important name: `M2PlacementGrouper`
- Simplest solution: one stateless `group_placements` query
- Rejected complexity: group/result classes, strategy interfaces, callbacks,
Resources, policies or generic spatial batching framework
- Unavoidable complexity: raw ADT dictionaries remain at the parser adapter
- Measured optimization evidence: bounded synthetic grouping loop
## Status
- State: accepted
- Done: pure grouper, worker adapter, regressions and documentation verified
- Next: M2 worker/build state extraction and spatial-cell evidence
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-PLACEMENT-GROUPER-001:9df32d3 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-PLACEMENT-GROUPER-001:f88bf97 -->
## Handoff
- Commit: `9df32d3` (`refactor(M03): extract M2 placement grouper`)
- Results: dedicated grouper PASS (`cases=9`, `iterations=100`,
`placements=256`, `elapsed_ms=67.736`); transform resolver PASS (`cases=9`,
`iterations=10000`, `elapsed_ms=28.571`); M2 registry/dedupe, terrain
cache/LOD/queue, facade, internal-access, manifest, shutdown, scheduler,
streaming planner/focus, terrain-query and coordinate-boundary suites PASS;
editor import parsed the project with only expected missing private corpus
diagnostics; documentation PASS (`module_specs=17`, `required_files=7`);
coordination PASS with 23 pre-existing expired M00/M01 warnings; checkpoint
dry-run planned `7/7`.
- Remaining risks: no private asset-backed traversal/visual comparison;
uppercase extension preservation is historical but inconsistent with the
resolver's case-insensitive exception matching; grouping remains model-path
based rather than spatial-cell based; worker/build state and p95/p99 remain.
- Documentation updated: inline public API; new
`docs/modules/m2-placement-grouper.md`; transform resolver module spec,
renderer module registry, `docs/modules/world-renderer.md` and `RENDER.md`.
- Integration: accepted in master merge `f88bf97`; post-merge grouper
(`cases=9`, `iterations=100`, `placements=256`, `elapsed_ms=67.104`),
transform resolver, registry, internal-access, facade, manifest,
documentation and coordination gates passed.
@@ -0,0 +1,107 @@
# M03-RND-M2-PLACEMENT-TRANSFORM-001 — M2 placement transform resolver
<!-- OPENWC_CLAIM:M03-RND-M2-PLACEMENT-TRANSFORM-001:sindo-main-codex:2026-07-19 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-m2-placement-transform`
- Lease expires UTC: 2026-07-19
- Integrator: M03 milestone integrator
## Outcome
Extract pure ADT M2 placement basis/origin-offset calculation, including the
existing waterfall and Elwynn cliff-rock calibrations, into a scene-free service.
## Non-goals
- Change coordinate conversion, placement positions, rotations or scale clamps.
- Generalize model-specific corrections or add new allowlisted models.
- Move grouping/tasks, MultiMesh/Node/Mesh/material/RID creation or caches.
- Change M2 animation/material behavior, cache versions or renderer profiles.
- Claim build-12340 placement parity beyond existing fixtures.
## Paths
- Exclusive: `src/render/m2/m2_placement_transform_resolver.gd`,
`src/tools/verify_m2_placement_transform_resolver.gd`,
`docs/modules/m2-placement-transform-resolver.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated ADT resources, caches and
proprietary renderer corpus
## Contracts and data
- Input: ADT-loader Godot Euler rotation, normalized/original M2 relative path,
and raw placement scale where origin compensation is requested
- Output: `Basis` and local origin-offset `Vector3`; no retained state
- Regular models use `Basis.from_euler`; two cliff rocks retain narrow YZX-style
shell correction; two waterfall sheets retain yaw/twist/anchor compensation
- Scale clamps remain caller-owned for final basis and resolver-owned at `0.0001`
only for historical waterfall anchor compensation
- Cache/schema/coordinate versions: unchanged
## Dependencies
- Requires: current master `1b6c468`
- Blocks: pure M2 grouping extraction and broader M2 service decomposition
- External state: none; tests use synthetic rotations/paths and pinned constants
## Verification
- Commands: dedicated regular/path-normalization/cliffrock/waterfall/twist-anchor/
scale and bounded timing verifier; three loader call-site/source boundaries;
M2 unique/dedupe plus facade/terrain/internal-access/manifest/shutdown gates
- Fidelity evidence: exact formulas/constants and model allowlists moved unchanged
- Performance budget: allocation-free scalar/Basis math per placement aside from
path normalization; no Node, Resource, RID, task, cache or file access
## Documentation deliverables
- Inline resolver API documentation
- New module spec with inputs/outputs, data-flow, lifecycle, ownership, failure,
performance, fidelity limitations and source map
- World renderer/module registry/`RENDER.md` updates
## Simplicity and naming
- Important name: `M2PlacementTransformResolver`
- Simplest solution: stateless service with basis and origin-offset queries
- Rejected complexity: transform strategy hierarchy, generic correction registry,
Resources, data-driven override format or callback injection
- Unavoidable complexity: four measured model-specific exceptions remain explicit
- Measured optimization evidence: bounded synthetic transform loop
## Status
- State: accepted
- Done: resolver extracted, three call sites migrated, contracts and docs verified
- Next: pure M2 grouping extraction and broader M2 service decomposition
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-PLACEMENT-TRANSFORM-001:0a09d7b -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-PLACEMENT-TRANSFORM-001:396be5e -->
## Handoff
- Commit: `0a09d7b` (`refactor(M03): extract M2 placement transform resolver`)
- Results: dedicated formula/source/timing contract PASS (`cases=9`,
`iterations=10000`, `elapsed_ms=28.732`); cold editor parse PASS;
documentation gate PASS (`module_specs=16`, `required_files=7`);
coordination gate PASS with 23 pre-existing expired M00/M01 warnings; related
M2 unique/dedupe, terrain cache/LOD/queue, facade, internal-access, manifest,
shutdown, scheduler, streaming focus/planner and coordinate-boundary suites
PASS; renderer checkpoint dry-run PASS `7/7`.
- Remaining risks: no private asset-backed visual recheck in this package;
general ADT M2 placement parity and asset-backed p95/p99 remain pending.
- Documentation updated: inline public API; new
`docs/modules/m2-placement-transform-resolver.md`; renderer module registry,
`docs/modules/world-renderer.md` and `RENDER.md`.
- Integration: accepted in master merge `396be5e`; post-merge resolver
(`cases=9`, `iterations=10000`, `elapsed_ms=27.409`), M2 registry,
internal-access, facade, manifest, documentation and coordination gates passed.
@@ -0,0 +1,105 @@
# M03-RND-M2-UNIQUE-REGISTRY-001 — M2 unique placement registry
<!-- OPENWC_CLAIM:M03-RND-M2-UNIQUE-REGISTRY-001:sindo-main-codex:2026-07-19 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-m2-unique-registry`
- Lease expires UTC: 2026-07-19
- Integrator: M03 milestone integrator
## Outcome
Extract cross-tile positive `MDDF.uniqueId` reservation ownership from the
streamer into a scene-free `M2UniquePlacementRegistry`.
## Non-goals
- Move tile state, candidate requeue, group/build tasks or result queues.
- Move MultiMesh/Node/Mesh/material creation, animation or resource loading.
- Change unique-key format, first-owner semantics or unkeyed placement behavior.
- Change placement transforms, detail radius, cache versions or quality profiles.
- Add dependencies or mark the broad M2 service complete.
## Paths
- Exclusive: `src/render/m2/m2_unique_placement_registry.gd`,
`src/tools/verify_m2_unique_placement_registry.gd`,
`docs/modules/m2-unique-placement-registry.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`src/tools/verify_m2_unique_dedupe.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
- Input: tile key and raw ADT M2 placement dictionaries
- Reserve output: fresh filtered placements, owned unique keys and skipped keys
- Release input/output: tile key plus claimed keys; only current owner releases
- Positive IDs map to `uid:<decimal>`; zero/negative/missing IDs remain untracked
- Registry is session-memory only and clears on renderer world reset
- Queue/cache/schema/coordinate versions: unchanged
## Dependencies
- Requires: current master `57ae1b8`
- Blocks: broader M2 service extraction and dependency/performance acceptance
- External state: none; tests use synthetic placement dictionaries
## Verification
- Commands: dedicated unkeyed/duplicate/cross-tile/release/clear/detached-
diagnostic and bounded timing verifier; loader source boundary; M2 unique-id
smoke plus facade/terrain/internal-access/manifest/shutdown/scheduler gates
- Fidelity evidence: exact positive-ID key format and first-owner/retry behavior
- Performance budget: dictionary membership over placement count; no Node,
Resource, RID, task, file or persistent cache
## Documentation deliverables
- Inline registry API documentation
- New module spec with inputs/outputs, data-flow, lifecycle, sequence, ownership,
failure, recovery, performance and source map
- World renderer/module registry/`RENDER.md` updates
## Simplicity and naming
- Important name: `M2UniquePlacementRegistry`
- Simplest solution: one private key-to-owner dictionary and explicit operations
- Rejected complexity: generic identity registry, signals, callbacks, Resources,
locks or ownership graph
- Unavoidable complexity: raw placement dictionaries remain at ADT adapter boundary
- Measured optimization evidence: bounded synthetic reserve/release verifier
## Status
- State: accepted
- Done: stateful registry, loader adapters, migrated smoke, regressions and docs
- Next: asset-backed boundary traversal and further M2-service extraction later in M03
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-UNIQUE-REGISTRY-001:acb3a80 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-UNIQUE-REGISTRY-001:2342430 -->
## Handoff
- Commit: `acb3a80` (`refactor(M03): extract M2 unique placement registry`)
- Results: dedicated contract PASS (`cases=10`, `iterations=100`, `35.688 ms`);
historical `uid:11785` smoke PASS; terrain/facade/internal-access/manifest/
shutdown/scheduler/streaming/focus/terrain-query/coordinate regressions PASS;
internal inventory remains `42`; editor parse and documentation/coordination/
diff gates PASS; checkpoint dry-run planned `7/7`
- Remaining risks: private Azeroth corpus was absent, so the asset-backed ADT
placement probe could not open the map and is not counted as passed evidence;
first ownership remains processing-order based; M2 grouping/build/tasks/caches/
finalization and asset-backed p95/p99 remain pending
- Documentation updated: `m2-unique-placement-registry.md`, module registry,
`world-renderer.md`, `RENDER.md`, inline API documentation and data-flow,
lifecycle and sequence diagrams
- Integration: accepted in master merge `2342430`; post-merge registry, legacy
smoke, internal-access, facade and manifest gates passed
@@ -0,0 +1,116 @@
# M03-RND-SCHEDULER-001 — Bounded render budget scheduler
<!-- OPENWC_CLAIM:M03-RND-SCHEDULER-001:sindo-main-codex:2026-07-18 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-render-budget-scheduler`
- Lease expires UTC: 2026-07-18
- Integrator: M03 milestone integrator
## Outcome
Extract the streamer's per-frame main-thread operation quotas into a scene-free
`RenderBudgetScheduler`, preserving existing queue priority and quota values and
stopping permit issuance after renderer teardown cancellation.
## Non-goals
- Move queue, worker-task, cache, Node, Resource or RID ownership out of the streamer.
- Change queue ordering, concurrency limits, exported defaults or visual behavior.
- Add a generic job framework, priority graph, time-based adaptive budgeting or dependency.
- Change cache formats, renderer profiles or target milestone status.
## Paths
- Exclusive: `src/render/streaming/render_budget_scheduler.gd`,
`src/tools/verify_render_budget_scheduler.gd`,
`docs/modules/render-budget-scheduler.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 cache resources and
proprietary renderer corpus
## Contracts and data
- Input: immutable per-frame map from documented operation lane IDs to
non-negative operation limits
- Output: boolean permits and remaining/consumed diagnostic snapshots
- Lifecycle: `begin_frame` resets permits; `cancel` permanently rejects permits
for the current scheduler instance
- Queue contents, ordering, task concurrency and schema/cache versions: unchanged
- Consumer: `StreamingWorldLoader` main-thread queue/finalize drains
## Dependencies
- Requires: integrated M03 facade and streaming planner on master `650d584`
- Blocks: renderer performance regression gate and per-service extraction
- External state: none; tests use synthetic lane limits and no world assets
## Verification
- Commands: dedicated scheduler verifier, facade/planner/focus/coordinate
regressions, renderer manifest/dry-run and repository gates
- Fixtures: shared lane priority, independent lanes, zero/negative limits,
frame reset, unknown lane and cancellation
- Fidelity evidence: operation defaults and loop order are captured before
migration; all seven renderer checkpoint plans remain valid
- Performance budget: permit checks are constant-time dictionary operations and
create no Node, Resource, worker task, cache entry or RID
## Documentation deliverables
- Inline API docs for scheduler lifecycle, permits and diagnostics
- New module specification with input/output table, data-flow, sequence and
lifecycle diagrams
- Updated world-renderer flow/ownership/source map, module registry and renderer notes
## Simplicity and naming
- Important name: `RenderBudgetScheduler`
- Simplest approach: one small main-thread service with named quota lanes
- Rejected complexity: generic job graph, callbacks, queue ownership, adaptive
timing controller, service container or third-party dependency
- Unavoidable complexity: chunk removal and creation intentionally share one
lane so removal retains priority while consuming the same historical budget
- Measured optimization evidence: no optimization claim; exact bounded extraction
## Status
- State: ready-for-review
- Done: extracted all 16 historical operation quotas, migrated ordered drains,
added terminal teardown cancellation, asset-free verification and module docs
- Next: M03 integrator reviews and merges `e52f703`
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-SCHEDULER-001:e52f703 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-SCHEDULER-001:7ece2ab -->
## Handoff
- Commit: `e52f703`
- Results: `RenderBudgetScheduler` owns only per-frame lane counters and terminal
cancellation; `StreamingWorldLoader` retains queue order, task/cache/node/RID
ownership and operation execution. Chunk removal/create share one historical budget.
- Verification: scheduler `cases=6 iterations=20000 elapsed_ms=7.495`; planner
`cases=5 average_ms=1.879`; facade/focus contracts passed; coordinate boundary
`files=111 consumers=6`; renderer manifest `checkpoints=7 coverage=7 caches=7`;
shutdown ownership, documentation, coordination and diff gates passed.
- Fidelity: exported defaults, quality presets, queue order, operation sites,
cache versions and visible rendering rules are unchanged. Seven checkpoint
plans remain valid. No build-12340 parity claim is added.
- External/local inputs: ignored native DLL and two generated ADT resource scripts
were copied from master only for the worktree parse/smoke and then removed.
Proprietary extracted ADT data was unavailable, producing expected degraded-data diagnostics.
- Remaining risks: queue storage length and worker concurrency remain streamer-owned;
cancellation stops permit issuance but does not interrupt in-flight work; exact
p95/p99 impact still requires an asset-backed baseline.
- Documentation: new scheduler module spec with API, input/output, data-flow,
sequence, lifecycle, ownership, cancellation, performance and source-map sections;
world renderer, module registry and renderer implementation notes updated.
- Integration: accepted in master merge `7ece2ab`; post-merge scheduler timing
`cases=6 iterations=20000 elapsed_ms=10.216`, all listed regressions and gates passed.
@@ -0,0 +1,120 @@
# M03-RND-STREAMING-PLANNER-001 — Pure streaming target planner
<!-- OPENWC_CLAIM:M03-RND-STREAMING-PLANNER-001:sindo-main-codex:2026-07-17 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-streaming-target-planner`
- Lease expires UTC: 2026-07-17
- Integrator: M03 milestone integrator
## Outcome
Extract runtime/editor ADT wanted/retained-set calculation and boundary prefetch
selection from `StreamingWorldLoader` into a scene-free, deterministic
`StreamingTargetPlanner` with typed focus input and asset-free contract tests.
## Non-goals
- Change load/retain radii, boundary thresholds, queue priority or LOD policy.
- Move queue mutation, async jobs, cache ownership or GPU finalization.
- Extract `RenderBudgetScheduler` or asset services in this package.
- Change cache formats, renderer profiles or target milestone status.
## Paths
- Exclusive: `src/render/streaming/streaming_target_planner.gd`,
`src/render/streaming/streaming_target_policy.gd`,
`src/render/streaming/streaming_target_plan.gd`,
`src/tools/verify_streaming_target_planner.gd`,
`docs/modules/streaming-target-planner.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`src/tools/verify_coordinate_conversion_boundaries.gd`,
`docs/modules/world-renderer.md`, `docs/modules/coordinate-mapping.md`,
`docs/modules/README.md`, `RENDER.md`
- Generated/ignored: `.godot`, native DLL, generated cache resources and
proprietary renderer corpus
## Contracts and data
- Input: immutable `GodotWorldPosition`, available-tile key set and immutable
`StreamingTargetPolicy`
- Output: immutable `StreamingTargetPlan` with focus tile plus detached
wanted/retained tile-key sets
- Tile-key representation remains the streamer's existing `<x>_<y>` internal key
- Schema/cache/coordinate versions: unchanged
- Consumers: `StreamingWorldLoader` runtime/editor refresh paths
## Dependencies
- Requires: M01 typed coordinates/focus and integrated facade package `a100ced`
- Blocks: budget scheduler extraction and planner performance regression gate
- External state: none; tests use synthetic tile catalogs
## Verification
- Commands: dedicated planner verifier, facade/focus/coordinate regressions,
renderer manifest/dry-run and repository gates
- Fixtures: center, edge, corner, unavailable-tile, clamped-radius and editor cases
- Fidelity evidence: expected tile sets are captured from current formulas; full
renderer dry-run retains all seven checkpoint plans
- Performance budget: planner work remains bounded by square radii and creates no
Node, Resource, task, cache or RID
## Documentation deliverables
- Inline API docs for planner/policy/plan
- New module specification with input/output table and data-flow diagram
- Updated world-renderer data-flow/sequence/ownership/source map
- Updated module registry and renderer notes
## Simplicity and naming
- Important names: `StreamingTargetPlanner`, `StreamingTargetPolicy`,
`StreamingTargetPlan`
- Simplest approach: one pure calculator plus two immutable value objects
- Rejected complexity: generic graph planner, priority framework, service
container, job ownership or duplicate available-tile catalog
- Unavoidable complexity: boundary-prefetch cross product must preserve corner
behavior and existing string tile keys
- Measured optimization evidence: no new optimization; exact extraction
## Status
- State: ready-for-review
- Done: extracted immutable policy/plan and pure planner, migrated runtime/editor
target refresh, added behavior/dependency/performance coverage and module docs
- Next: integrated in master; next M03 package extracts the budget scheduler
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-STREAMING-PLANNER-001:ad8fc41 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-STREAMING-PLANNER-001:643850c -->
## Handoff
- Commit: `e887c3b`; cold-start fix `ad8fc41`
- Integration: accepted in master merges `d0f74f8` and `643850c`
- Results: runtime/editor wanted and retained ADT sets plus boundary-prefetch
centers are calculated by scene-free `StreamingTargetPlanner`; streamer keeps
queue ordering, LOD/detail state, jobs, caches, nodes and RIDs.
- Verification: planner `cases=5 iterations=250 average_ms=1.765`; facade
`delegation=3 runtime_scenes=2 tools=3`; focus `contract=1 runtime_scenes=2
capture_tools=3`; coordinate boundary `files=109 consumers=6`; renderer
manifest `checkpoints=7 coverage=7 caches=7`; shutdown ownership passed;
documentation, coordination and diff gates passed.
- Fidelity: center/corner/catalog/clamp/editor fixtures preserve the extracted
formulas; seven-checkpoint dry-run retained all plans. No radius, threshold,
load priority, cache version, placement, material, animation or visibility
rule changed. No build-12340 parity claim is added.
- External/local inputs: ignored native DLL and generated ADT resource scripts
were copied from master for worktree smoke only. Proprietary ADT and character
assets were unavailable, so the dry-run emitted expected degraded-data diagnostics.
- Remaining risks: exact p95/p99 impact still needs an asset-backed baseline;
pathological custom radii are not benchmarked; tile-key strings remain the
existing internal catalog representation; queue/LOD application remains monolithic.
- Documentation updated: new planner module spec with API/input/output/data-flow/
sequence/ownership/recovery/performance/source-map sections; world renderer,
coordinate mapping, module registry and renderer implementation notes updated.
@@ -0,0 +1,118 @@
# M03-RND-TERRAIN-CACHE-SERVICE-001 — Terrain quality mesh cache service
<!-- OPENWC_CLAIM:M03-RND-TERRAIN-CACHE-SERVICE-001:sindo-main-codex:2026-07-18 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-terrain-quality-cache`
- Lease expires UTC: 2026-07-18
- Integrator: M03 milestone integrator
## Outcome
Extract the full-quality terrain revisit Mesh cache, its LRU ownership and
source-admission rules from `StreamingWorldLoader` into a small scene-free
`TerrainQualityMeshCache`, preserving all call sites and observable behavior.
## Non-goals
- Move ADT parsing, tile state, quality-upgrade tasks/results or SceneTree/RID finalization.
- Change cache capacity defaults, quality profiles, source priority or cache formats.
- Cache control-splat/splat meshes that the current loader deliberately rejects.
- Extract chunk LOD, terrain material building, ground query or other asset services.
- Add dependencies, persistence, background work or mark the terrain service complete.
## Paths
- Exclusive: `src/render/terrain/terrain_quality_mesh_cache.gd`,
`src/tools/verify_terrain_quality_mesh_cache.gd`,
`docs/modules/terrain-quality-mesh-cache.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 resources, caches and
proprietary renderer corpus
## Contracts and data
- Public service API: `store`, `restore`, `clear`, detached `diagnostic_snapshot`
- Entry: tile key, retained Mesh reference and source string
- Admission: non-empty key, non-null Mesh, source other than
`control_splat_cache`, `splat_cache` or `splat`
- LRU: successful restore touches newest; store touches then trims oldest to
`max(0, capacity)`; invalid restored records are removed
- Persistence/cache format: none; session-memory ownership only
## Dependencies
- Requires: integrated facade contracts master `61bad09`
- Blocks: later terrain service extraction and cache/finalization acceptance audit
- External state: none; tests use synthetic ArrayMesh references
## Verification
- Commands: dedicated cache contract/LRU/ownership regression; streamer source
boundary; shutdown/facade/internal-access/manifest/planner/scheduler and repository gates
- Fixtures: admission/rejection, replacement, LRU touch/eviction, zero/negative
capacity, invalid record cleanup, clear and detached diagnostics
- Fidelity evidence: same sources/capacity/call sites; all seven baseline plans unchanged
- Performance budget: dictionary lookup plus bounded single-entry LRU operations;
no Node, task, file, cache serialization or RID creation
## Documentation deliverables
- Inline public API docs: store/restore/clear/diagnostics and ownership
- Module specification: new terrain quality mesh cache specification
- Data-flow diagram: upgrade result through cache to revisit state
- Sequence/state/dependency diagrams: store/restore/evict/reset and failure paths
- Source map/status updates: module registry, world renderer and `RENDER.md`
## Simplicity and naming
- Important names introduced: `TerrainQualityMeshCache`
- Simplest considered solution: one RefCounted with dictionary plus ordered tile-key array
- Rejected complexity/abstractions: generic cache framework, weak references,
callbacks, Resource cache integration, background eviction or persistence
- Unavoidable complexity and justification: Mesh references must survive unloaded
tile state while LRU keeps revisit memory bounded
- Measured optimization evidence: exact ownership extraction, no optimization claim
## Status
- State: ready-for-review
- Done: scene-free cache ownership/LRU extraction, loader adapter migration,
contract regression and module documentation
- Next: M03 integrator reviews and merges `97480e0`
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-TERRAIN-CACHE-SERVICE-001:97480e0 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-TERRAIN-CACHE-SERVICE-001:630a0c1 -->
## Handoff
- Commit: `97480e0`
- Results: `TerrainQualityMeshCache` now owns retained full-quality Mesh
references, source admission and oldest-to-newest LRU. The loader retains only
restore-to-state, store-after-upgrade and clear lifecycle adapters.
- Verification: cache `contract_cases=15 loader_calls=3`; facade/entity/ground/
terrain regressions; internal access `private_symbols=42 gameplay=7
editor_sources=9 renderer_tools=1`; manifest `7/7`; shutdown; scheduler
`20000` iterations; planner `250` iterations; focus; editor parse; dry-run;
coordination, documentation and diff gates passed.
- Fidelity: `baked_full` revisit retention, three excluded splat sources, LRU
touch/trim, profile capacities and all call sites remain unchanged. All seven
checkpoint plans remain valid. No visible or build-12340 parity claim is added.
- External/local inputs: ignored native DLL and generated ADT scripts supported
parse/dry-run only; missing proprietary ADT/DBC/character data produced expected diagnostics.
- Remaining risks: entry count does not measure Mesh bytes; ordered-array touch
remains linear; asset-backed revisit flash, memory and p95/p99 evidence remain pending;
terrain build/tasks/state/finalization are still monolithic.
- Documentation updated: new terrain cache API/input-output/data-flow/lifecycle/
sequence/ownership/failure/configuration/migration/diagnostic/source-map spec;
world renderer, registry and `RENDER.md` updated.
- Integration: accepted in master merge `630a0c1`; post-merge cache, facade,
internal-access, manifest, shutdown, scheduler and planner gates passed.
@@ -0,0 +1,104 @@
# M03-RND-TERRAIN-CHUNK-QUEUE-001 — Pure terrain chunk geometry queue planner
<!-- OPENWC_CLAIM:M03-RND-TERRAIN-CHUNK-QUEUE-001:sindo-main-codex:2026-07-19 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-terrain-chunk-queue`
- Lease expires UTC: 2026-07-19
- Integrator: M03 milestone integrator
## Outcome
Extract chunk-geometry create/remove desired-operation calculation from the
streamer into a scene-free `TerrainChunkGeometryQueuePlanner`.
## Non-goals
- Move queue storage/drains, render budgets, tile-LOD queues or async tasks.
- Move chunk Mesh/Node/RID creation/removal or terrain state ownership.
- Change removal-first execution, nearest-first create order or LOD selection.
- Change coordinate conversions, radii, cache formats or quality profiles.
- Add dependencies or mark the broad terrain service checklist complete.
## Paths
- Exclusive: `src/render/terrain/terrain_chunk_geometry_queue_planner.gd`,
`src/tools/verify_terrain_chunk_geometry_queue_planner.gd`,
`docs/modules/terrain-chunk-geometry-queue-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 resources, caches and
proprietary renderer corpus
## Contracts and data
- Input: loader-owned tile-state read model plus immutable `GodotWorldPosition`
- Output: fresh create/remove request arrays; planner retains no queue state
- Removal: current chunks absent from desired state, or all current chunks while
tile LOD is desired
- Creation: missing or LOD-mismatched chunks, sorted by origin XZ distance
- Queue/cache/schema/coordinate versions: unchanged
## Dependencies
- Requires: integrated terrain LOD planner master `b568768`
- Blocks: broader terrain service extraction and dependency/performance acceptance
- External state: none; tests use synthetic tile/chunk dictionaries
## Verification
- Commands: dedicated removal/create/mismatch/tile-fallback/sparse/order and
bounded timing verifier; loader source boundary; facade/cache/LOD/internal-access/
manifest/shutdown/scheduler/streaming planner and repository gates
- Fidelity evidence: extracted branch/order/distance rules and loader drain order
- Performance budget: bounded linear planning plus existing create sort; no engine
resource, task, cache or persistent allocation framework
## Documentation deliverables
- Inline planner API documentation
- New module spec with inputs/outputs, data-flow, lifecycle, sequence, ownership,
failure, performance and source map
- World renderer/module registry/`RENDER.md` updates
## Simplicity and naming
- Important name: `TerrainChunkGeometryQueuePlanner`
- Simplest solution: one stateless planner returning two fresh arrays
- Rejected complexity: generic operation graph, queue class, callback strategy,
spatial index or ownership transfer
- Unavoidable complexity: raw dictionaries remain at the loader/ADT adapter boundary
- Measured optimization evidence: bounded synthetic multi-tile verifier
## Status
- State: accepted
- Done: pure planner, loader queue adoption, semantic/timing regressions and docs
- Next: asset-backed performance acceptance and further terrain-service extraction later in M03
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-TERRAIN-CHUNK-QUEUE-001:f800e11 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-TERRAIN-CHUNK-QUEUE-001:24aef13 -->
## Handoff
- Commit: `f800e11` (`refactor(M03): extract terrain chunk queue planner`)
- Results: dedicated contract PASS (`cases=8`, `iterations=100`, `235.096 ms`);
LOD/cache/facade/internal-access/manifest/shutdown/scheduler/streaming/focus/
ground/terrain-query/coordinate regressions PASS; internal inventory remains
`42`; editor parse has no script errors; documentation/coordination/diff gates
PASS; checkpoint dry-run planned `7/7` entries with expected missing-private-
corpus diagnostics
- Remaining risks: planner adds a second linear tile-state pass; asset-backed
p95/p99 and visual acceptance remain pending; queue drains, terrain state and
Mesh/Node/RID finalization remain loader-owned
- Documentation updated: `terrain-chunk-geometry-queue-planner.md`, module
registry, `world-renderer.md`, `RENDER.md`, inline API documentation and
data-flow/lifecycle/sequence diagrams
- Integration: accepted in master merge `24aef13`; post-merge planner,
internal-access, facade and manifest gates passed
@@ -0,0 +1,103 @@
# M03-RND-TERRAIN-LOD-PLANNER-001 — Pure terrain chunk LOD planner
<!-- OPENWC_CLAIM:M03-RND-TERRAIN-LOD-PLANNER-001:sindo-main-codex:2026-07-18 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-terrain-chunk-lod-planner`
- Lease expires UTC: 2026-07-18
- Integrator: M03 milestone integrator
## Outcome
Extract chunk-level terrain LOD desired-state calculation from the streamer into
a scene-free `TerrainChunkLodPlanner` with immutable policy and typed focus input.
## Non-goals
- Move chunk/tile queues, loaded state, Mesh/Node/RID creation or budget drains.
- Change radii, chunk size, threshold inclusivity, priority sorting or tile LOD policy.
- Normalize/reorder custom radii or reinterpret negative values.
- Move ADT parsing, quality cache/tasks, materials or ground query.
- Add dependencies, persistence, workers or mark terrain extraction complete.
## Paths
- Exclusive: `src/render/terrain/terrain_chunk_lod_policy.gd`,
`src/render/terrain/terrain_chunk_lod_planner.gd`,
`src/tools/verify_terrain_chunk_lod_planner.gd`,
`docs/modules/terrain-chunk-lod-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 resources, caches and
proprietary renderer corpus
## Contracts and data
- Input: ADT chunk adapter records, immutable `GodotWorldPosition`, immutable
enabled/radii/chunk-size policy
- Output: detached Dictionary mapping source chunk index to LOD `0`, `1` or `2`
- Formula: horizontal distance to `origin + half chunk`; inclusive squared thresholds
- Disabled policy and empty/sparse chunks return absent entries exactly as before
- Queue/cache/schema/coordinate versions: unchanged
## Dependencies
- Requires: integrated terrain cache slice master `ea08b8e`
- Blocks: broader terrain service extraction and dependency/performance acceptance
- External state: none; tests use synthetic chunk adapter records
## Verification
- Commands: dedicated center/boundary/outside/sparse/disabled/custom-radius and
bounded timing verifier; loader source boundary; facade/cache/internal-access/
manifest/shutdown/scheduler/streaming planner and repository gates
- Fidelity evidence: extracted formulas and all seven baseline plans remain unchanged
- Performance budget: bounded linear scan over at most the ADT chunk record count;
no Node, Resource, RID, task, cache entry or allocation framework
## Documentation deliverables
- Inline API docs for policy/planner
- New module spec with inputs/outputs, data-flow, lifecycle, sequence, ownership,
failure, performance and source map
- World renderer/module registry/`RENDER.md` updates
## Simplicity and naming
- Important names: `TerrainChunkLodPolicy`, `TerrainChunkLodPlanner`
- Simplest solution: immutable scalar policy plus one pure linear planner
- Rejected complexity: generic spatial index, quadtree, callback strategy,
normalized profile model or queue ownership
- Unavoidable complexity: raw chunk dictionaries remain at the ADT adapter boundary
- Measured optimization evidence: verifier records bounded High-like planning time
## Status
- State: accepted
- Done: immutable policy, pure planner, both loader call sites, regressions and docs
- Next: asset-backed performance acceptance and further terrain-service extraction later in M03
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-TERRAIN-LOD-PLANNER-001:eb94cfc -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-TERRAIN-LOD-PLANNER-001:f36fabb -->
## Handoff
- Commit: `eb94cfc` (`refactor(M03): extract terrain chunk LOD planner`)
- Results: dedicated contract PASS (`cases=7`, `iterations=250`, `12.726 ms`);
cache/facade/internal-access/manifest/shutdown/scheduler/streaming-focus/
streaming-planner/ground/terrain-query/coordinate regressions PASS; editor parse
has no script errors; documentation and coordination gates PASS; checkpoint
dry-run planned `7/7` entries with expected missing-private-corpus diagnostics
- Remaining risks: asset-backed p95/p99 and visual acceptance remain pending;
terrain state, queue, quality-upgrade and finalization ownership remains in loader
- Documentation updated: `terrain-chunk-lod-planner.md`, module registry,
`world-renderer.md`, `RENDER.md`, inline public API documentation and data-flow,
lifecycle and sequence diagrams
- Integration: accepted in master merge `f36fabb`; post-merge planner, facade,
internal-access and manifest gates passed
@@ -0,0 +1,106 @@
# M03-RND-WMO-PLACEMENT-REGISTRY-001 — WMO placement registry
<!-- OPENWC_CLAIM:M03-RND-WMO-PLACEMENT-REGISTRY-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-wmo-placement-registry`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract WMO placement-key to tile-reference ownership from the loader while
leaving every Node and renderer-resource lifecycle in the loader.
## Non-goals
- Change placement identity rules or WMO transforms.
- Move Node maps, world roots, tile state, jobs, queues or cancellation.
- Move cache/load requests, material refresh or render-group building.
- Change duplicate ordering, global-WMO lifetime or build budgets.
- Claim asset-backed WMO fidelity or performance acceptance.
## Paths
- Exclusive: `src/render/wmo/wmo_placement_registry.gd`,
`src/tools/verify_wmo_placement_registry.gd`,
`docs/modules/wmo-placement-registry.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`docs/modules/wmo-placement-resolver.md`, `docs/modules/world-renderer.md`,
`docs/modules/README.md`, `RENDER.md`, `targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated ADT/WMO resources, caches
and proprietary renderer corpus
## Contracts and data
- Input: resolved WMO placement key and tile/global reference key
- `add_reference` creates a key/ref set or idempotently retains an existing ref
- `release_reference` erases only the supplied ref and returns true only when the
final ref removes the placement key
- `contains`, `active_count`, detached sorted diagnostics and idempotent `clear`
- Loader retains key→Node mapping and reacts to final release by cancelling the
render build and queue-freeing the corresponding Node
- Cache/schema/coordinate/profile versions: unchanged
## Dependencies
- Requires: current master `5aa40d6`, `WmoPlacementResolver` identity contract
- Blocks: WMO build/resource state decomposition
- External state: none; tests use synthetic placement/tile keys
## Verification
- Commands: dedicated add/shared/idempotent/non-owner/final/global/clear/
diagnostics/source/timing verifier; resolver/material/shutdown plus M2/terrain/
facade/internal-access/manifest/scheduler/streaming/coordinate/docs/coord gates
- Fidelity evidence: exact shared-ref and final-release behavior preserved
- Performance budget: dictionary membership over active WMO keys/refs; no Node,
Resource, RID, task, queue, mutex, cache or file access
## Documentation deliverables
- Inline registry API documentation
- New module spec with inputs/outputs, data-flow, lifecycle/state, ownership,
failure/recovery, performance, fidelity limitations and source map
- Placement resolver, renderer/module registry and `RENDER.md` updates
## Simplicity and naming
- Important name: `WmoPlacementRegistry`
- Simplest solution: one private key-to-reference-set Dictionary
- Rejected complexity: generic ref-count registry, signals, callbacks, Resources,
weak refs or Node ownership inside the service
- Unavoidable complexity: loader keeps a parallel key-to-Node map
- Measured optimization evidence: bounded synthetic add/release loop
## Status
- State: accepted
- Done: registry extracted; global/tile adapters migrated; Node/resource/job
ownership retained in loader; contract, regressions, dry-run and docs passed
- Next: WMO build/resource state extraction and asset-backed placement evidence
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-PLACEMENT-REGISTRY-001:c590036 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-PLACEMENT-REGISTRY-001:f9e5a4f -->
## Handoff
- Commit: `c590036`
- Results: `verify_wmo_placement_registry.gd` PASS cases=10,
iterations=100, elapsed_ms=53.941; 21 adjacent WMO/M2/terrain/facade/
scheduler/streaming/coordinate regressions PASS; documentation and
coordination gates PASS; render dry-run PASS 7/7.
- Remaining risks: no proprietary asset corpus or original-client paired WMO
capture; dry-run reports expected missing ADT/DBC/character assets; WMO
build/resource state remains loader-owned.
- Documentation updated: `docs/modules/wmo-placement-registry.md`,
`docs/modules/wmo-placement-resolver.md`, `docs/modules/world-renderer.md`,
`docs/modules/README.md`, `RENDER.md`; inline public API comments added.
- Integration: accepted in master merge `f9e5a4f`; post-merge registry
(`cases=10`, `iterations=100`, `elapsed_ms=54.178`), resolver, runtime-cache
shutdown, facade, documentation and coordination gates passed.
@@ -0,0 +1,110 @@
# M03-RND-WMO-PLACEMENT-RESOLVER-001 — WMO placement resolver
<!-- OPENWC_CLAIM:M03-RND-WMO-PLACEMENT-RESOLVER-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-wmo-placement-resolver`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract pure WMO cache-path normalization, placement identity and world-transform
resolution from the loader into a scene-free service.
## Non-goals
- Change WDT/ADT coordinate conversion or WMO placement values.
- Own WMO registry references, tile lifecycle, jobs, queues or retries.
- Load cached/live WMO resources or validate cache format versions.
- Create Nodes, Mesh/MultiMesh, materials or RIDs.
- Change portal/room/material/occlusion behavior or claim WMO parity.
## Paths
- Exclusive: `src/render/wmo/wmo_placement_resolver.gd`,
`src/tools/verify_wmo_placement_resolver.gd`,
`docs/modules/wmo-placement-resolver.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated ADT/WMO resources, caches
and proprietary renderer corpus
## Contracts and data
- `normalize_relative_path`: replace backslashes, lowercase entire cache key
- `resolve_unique_key`: positive `unique_id` becomes `uid:<decimal>`; otherwise
preserve `tile:<tile-key>:<placement-index>` fallback
- `resolve_world_transform`: defaults position/rotation to zero and scale to one;
compose the same Godot Euler basis and un-clamped uniform scale
- Three transform consumers: render-cache root, cached scene and live prototype
- Cache/schema/coordinate/profile versions: unchanged
## Dependencies
- Requires: current master `4f40c77`
- Blocks: WMO registry/build decomposition
- External state: none; tests use synthetic paths and placement dictionaries
## Verification
- Commands: dedicated path/identity/default/rotation/scale/source and bounded
timing verifier; M2/terrain/facade/internal-access/manifest/shutdown/scheduler/
streaming/coordinate/documentation/coordination gates
- Fidelity evidence: exact normalization, identity and property-transform rules
moved unchanged
- Performance budget: constant-time string/value math; no Node, Resource, RID,
task, queue, mutex, cache or file access
## Documentation deliverables
- Inline resolver API documentation
- New module spec with inputs/outputs, data-flow, lifecycle, ownership, failure,
performance, fidelity limitations and source map
- Renderer/module registry and `RENDER.md` updates
## Simplicity and naming
- Important name: `WmoPlacementResolver`
- Simplest solution: one stateless service with three explicit pure queries
- Rejected complexity: placement class hierarchy, generic model resolver,
Resources, strategies, callbacks or registry framework
- Unavoidable complexity: raw placement Dictionary remains parser boundary
- Measured optimization evidence: bounded synthetic resolver loop
## Status
- State: accepted
- Done: resolver, seven loader adapters, regressions and documentation verified
- Next: WMO registry/build extraction and asset-backed placement evidence
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-PLACEMENT-RESOLVER-001:05e216a -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-PLACEMENT-RESOLVER-001:d2cb52f -->
## Handoff
- Commit: `05e216a` (`refactor(M03): extract WMO placement resolver`)
- Results: dedicated resolver PASS (`cases=9`, `iterations=20000`,
`elapsed_ms=28.332`), including historical Node3D property equivalence;
renderer materials, M2 planner/grouper/transform/registry/dedupe, terrain
cache/LOD/queue, facade, internal-access, manifest, shutdown, scheduler,
streaming planner/focus, terrain-query and coordinate-boundary suites PASS;
editor import parsed with only expected missing private corpus diagnostics;
documentation PASS (`module_specs=19`, `required_files=7`); coordination PASS
with 23 pre-existing expired M00/M01 warnings; checkpoint dry-run planned `7/7`.
- Remaining risks: no private asset-backed WMO placement/visual/p95/p99 evidence;
old caches without positive UID retain per-tile fallback; raw Dictionaries and
WMO registry/build/resource state remain loader-owned; scale stays unclamped.
- Documentation updated: inline public API; new
`docs/modules/wmo-placement-resolver.md`; renderer module registry,
`docs/modules/world-renderer.md` and `RENDER.md`.
- Integration: accepted in master merge `d2cb52f`; post-merge resolver
(`cases=9`, `iterations=20000`, `elapsed_ms=27.919`), renderer materials,
shutdown, internal-access, facade, manifest, documentation and coordination
gates passed.
@@ -0,0 +1,108 @@
# M03-RND-WMO-RENDER-BUILD-PLANNER-001 — WMO render build step planner
<!-- OPENWC_CLAIM:M03-RND-WMO-RENDER-BUILD-PLANNER-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-wmo-render-build-planner`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract the pure one-operation WMO render build cursor decision from the loader
while preserving mesh-before-MultiMesh ordering and one budget permit per group.
## Non-goals
- Move Nodes, Resources, materials, jobs, queues, cancellation or permits.
- Change WMO placement, cache loading, transforms, visibility or shadows.
- Change build ordering, group names, editor ownership or finalization thread.
- Change cache formats, settings, presets or renderer diagnostics.
- Claim asset-backed WMO fidelity or p95/p99 acceptance.
## Paths
- Exclusive: `src/render/wmo/wmo_render_build_step_planner.gd`,
`src/tools/verify_wmo_render_build_step_planner.gd`,
`docs/modules/wmo-render-build-step-planner.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated ADT/WMO resources, caches
and proprietary renderer corpus
## Contracts and data
- Input: mesh count/index and MultiMesh count/index
- Output: `mesh`, `multimesh` or `complete` operation, selected index and next
cursor values
- Mesh is selected while its cursor is below count; MultiMesh is selected only
after mesh exhaustion; complete follows both exhausted ranges
- Loader retains cursor adoption, Resource array access, Node creation, material
refresh, ownership, queue cancellation and budget permit consumption
- Cache/schema/coordinate/profile versions: unchanged
## Dependencies
- Requires: current master `1e73e59`, accepted WMO placement registry/resolver
- Blocks: further WMO render build state decomposition
- External state: none; verification uses scalar synthetic fixtures
## Verification
- Commands: dedicated ordering/cursor/empty/raw-edge/source/timing verifier;
WMO registry/resolver/material/shutdown plus M2/terrain/facade/internal-access/
manifest/scheduler/streaming/coordinate/docs/coord gates
- Fidelity evidence: exact mesh-first, one-step cursor behavior preserved
- Performance budget: 20,000 pure plans under 1 second; no Node, Resource, RID,
task, queue, mutex, cache or file access
## Documentation deliverables
- Inline public planner API documentation
- New module spec with inputs/outputs, data flow, sequence, ownership,
failure/recovery, performance/fidelity limits and source map
- Renderer module registry, world renderer and `RENDER.md` updates
## Simplicity and naming
- Important name: `WmoRenderBuildStepPlanner`
- Simplest solution: one stateless function and three operation constants
- Rejected complexity: job class, iterator, generic cursor framework, callbacks,
signals or ownership of engine objects
- Unavoidable complexity: loader still has separate mesh/MultiMesh materializers
- Measured optimization evidence: bounded synthetic planning loop
## Status
- State: accepted
- Done: planner and loader adapter implemented; Node/Resource/material/job/queue/
permit ownership retained in loader; regressions, dry-run and docs passed
- Next: WMO build job/resource ownership or liquid service extraction
- Blocked by:
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-RENDER-BUILD-PLANNER-001:c06aed5 -->
## Handoff
- Commit: `3a6b1d3`
- Results: dedicated planner PASS (`cases=9`, `iterations=20000`,
`elapsed_ms=11.914`); WMO registry/resolver/material/shutdown and 18 adjacent
M2/terrain/facade/scheduler/streaming/coordinate regressions PASS;
documentation and coordination gates PASS; checkpoint dry-run PASS `7/7`.
- Remaining risks: no proprietary WMO corpus, original-client paired capture or
traversal p95/p99; dry-run reports expected missing ADT/DBC/character assets;
WMO Resource arrays and untyped build jobs/queues remain loader-owned.
- Documentation updated: inline planner API;
`docs/modules/wmo-render-build-step-planner.md`, module registry,
`docs/modules/world-renderer.md`, `RENDER.md` with data-flow and sequence
diagrams, ownership, failure/recovery and source map.
- Integration: accepted in master merge `c06aed5`; post-merge planner
(`cases=9`, `iterations=20000`, `elapsed_ms=11.059`), WMO registry/resolver,
runtime-cache shutdown, facade, documentation and coordination gates passed.
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-RENDER-BUILD-PLANNER-001:3a6b1d3 -->
@@ -0,0 +1,117 @@
# M03-RND-WMO-RENDER-BUILD-QUEUE-001 — WMO render build queue
<!-- OPENWC_CLAIM:M03-RND-WMO-RENDER-BUILD-QUEUE-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-wmo-render-build-queue`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract typed lightweight-WMO render build jobs and their FIFO key queue from
the loader while leaving all engine-object destruction and materialization in
the loader.
## Non-goals
- Queue-free Nodes or free Resources/RIDs.
- Move material refresh, Node creation, editor ownership or budget permits.
- Change duplicate enqueue, stale-key cleanup, cancel or clear ordering.
- Change placement, cache loading/versioning, transforms, shadows or visibility.
- Claim asset-backed WMO fidelity, leak acceptance or traversal p95/p99.
## Paths
- Exclusive: `src/render/wmo/wmo_render_build_job.gd`,
`src/render/wmo/wmo_render_build_queue.gd`,
`src/tools/verify_wmo_render_build_queue.gd`,
`docs/modules/wmo-render-build-queue.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`docs/modules/wmo-render-build-step-planner.md`,
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated ADT/WMO resources, caches
and proprietary renderer corpus
## Contracts and data
- Typed job: placement key, strong Node3D/Resource references and two cursors
- FIFO queue preserves duplicate key append and first-occurrence erase behavior
- Stale front keys remain observable for loader-compatible pop-and-continue
- Cancel releases the keyed job reference and first queued key only
- Clear releases every job/key reference idempotently; never frees engine objects
- Loader retains validity checks, planner invocation, cursor adoption,
materialization, permits, cancellation reactions and Node destruction
- Cache/schema/coordinate/profile versions: unchanged
## Dependencies
- Requires: current master `1e740aa`, accepted WMO build step planner
- Blocks: further WMO cache/load and materialization decomposition
- External state: none; verification uses synthetic Node3D/Resource instances
## Verification
- Commands: dedicated invalid/FIFO/duplicate/stale/cursor/cancel/clear/
diagnostics/source/timing verifier; WMO planner/registry/resolver/material/
shutdown plus M2/terrain/facade/internal-access/manifest/scheduler/streaming/
coordinate/docs/coord gates
- Fidelity evidence: exact current queue/dictionary behavior preserved
- Performance budget: 100 cycles over 256 jobs under 1 second
## Documentation deliverables
- Inline job/queue API documentation
- New module spec with inputs/outputs, data flow, state/sequence, ownership,
threading, failure/recovery, performance/fidelity limits and source map
- Build-step planner, renderer module registry, world renderer and RENDER updates
## Simplicity and naming
- Important names: `WmoRenderBuildJob`, `WmoRenderBuildQueue`
- Simplest solution: one typed record plus Dictionary/Array queue composition
- Rejected complexity: generic job scheduler, signals, callbacks, mutexes,
Resources as job records or engine lifetime callbacks
- Unavoidable complexity: duplicate keys can intentionally leave stale entries
- Measured optimization evidence: bounded synthetic enqueue/cancel/clear loop
## Status
- State: accepted
- Done: typed job/queue implemented; three clear sites, metrics, enqueue, drain and
cancel migrated; engine destruction/materialization retained in loader
- Next: WMO cache/load state or liquid service extraction
- Blocked by:
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-RENDER-BUILD-QUEUE-001:60603e1 -->
## Handoff
- Commit: `e576ae2` (implementation `e1bb105` plus cold-load fix)
- Results: queue PASS (`cases=11`, `iterations=100`, `elapsed_ms=59.874`);
WMO planner/registry/resolver/material/shutdown and 18 adjacent M2/terrain/
facade/scheduler/streaming/coordinate regressions PASS; documentation and
coordination gates PASS; checkpoint dry-run PASS `7/7`.
- Cold-load recovery: post-merge smoke exposed an editor class-cache dependency;
typed queue accessors removed the loader/global-class dependency and a fresh
run without `.godot` passed (`cases=11`, `elapsed_ms=57.835`).
- Remaining risks: strong Node/Resource references require every loader cancel/
clear path; duplicate enqueue intentionally preserves stale FIFO keys; no
proprietary WMO corpus, original-client paired capture, leak run or traversal
p95/p99; dry-run reports expected missing ADT/DBC/character assets.
- Documentation updated: inline job/queue API;
`docs/modules/wmo-render-build-queue.md`, build-step planner spec, module
registry, world renderer and `RENDER.md`; data-flow/state/sequence diagrams,
ownership, threading, failure/recovery and source map included.
- Integration: implementation merged as `a878e7c`; its first post-merge smoke
exposed and rejected a cold global-class registration dependency. Fix merged
as `60603e1`; post-fix master queue (`cases=11`, `iterations=100`,
`elapsed_ms=56.454`), planner, registry, resolver, shutdown, facade,
documentation and coordination gates passed without parse diagnostics.
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-RENDER-BUILD-QUEUE-001:e576ae2 -->
@@ -0,0 +1,115 @@
# M03-RND-WMO-RENDER-RESOURCE-CACHE-001 — WMO render resource cache state
<!-- OPENWC_CLAIM:M03-RND-WMO-RENDER-RESOURCE-CACHE-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-wmo-render-resource-cache`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract lightweight-WMO render Resource cache, negative cache and asynchronous
request bookkeeping from the loader while preserving loader-owned I/O and
cache-format validation.
## Non-goals
- Call `ResourceLoader`, construct cache paths or inspect files.
- Validate `WMOStreamingResource` script identity or format version.
- Own cached WMO scenes, live prototypes, placement/build jobs or Nodes/RIDs.
- Change fallback order, retry behavior, quality profiles or visual output.
- Claim asset-backed WMO fidelity, leak acceptance or traversal p95/p99.
## Paths
- Exclusive: `src/render/wmo/wmo_render_resource_cache_state.gd`,
`src/tools/verify_wmo_render_resource_cache_state.gd`,
`docs/modules/wmo-render-resource-cache-state.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`src/tools/verify_render_runtime_cache_shutdown.gd`,
`docs/modules/wmo-render-build-queue.md`, `docs/modules/world-renderer.md`,
`docs/modules/README.md`, `RENDER.md`, `targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated ADT/WMO resources, caches
and proprietary renderer corpus
## Contracts and data
- State keys are loader-normalized relative WMO paths copied as `String`.
- Cached values are already validated `Resource` references.
- Pending values are cache paths for loader-owned `ResourceLoader` requests.
- Terminal request removal precedes validated-resource or missing-state adoption.
- Request/missing reset preserves accepted resources; full clear releases all.
- Cache/schema/coordinate/profile versions and validation rules remain unchanged.
## Dependencies
- Requires: current master `c954704`, accepted WMO render build queue
- Blocks: further WMO cached-scene/load and materialization decomposition
- External state: none; verification uses synthetic Resources and paths
## Verification
- Commands: dedicated lifecycle/request/cache/reset/diagnostics/source/timing
verifier; WMO queue/planner/registry/resolver/material/shutdown plus adjacent
renderer/streaming/coordinate/documentation/coordination gates
- Fixtures: synthetic Resource instances and normalized relative/cache paths
- Fidelity evidence: exact current cache/missing/request transitions preserved
- Performance budget: 100 cycles over 256 resource/request keys under 1 second
## Documentation deliverables
- Inline public API docs
- Module specification with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state and loader interaction sequence diagrams
- World-renderer, module registry, WMO queue and RENDER status updates
## Simplicity and naming
- Important names introduced: `WmoRenderResourceCacheState`
- Simplest considered solution: one RefCounted around three Dictionaries
- Rejected complexity/abstractions: generic cache, callbacks, signals, mutexes,
ResourceLoader wrapper, persistence layer or shared WMO/M2 cache hierarchy
- Unavoidable complexity and justification: partial reset must retain validated
Resources while discarding pending/negative state at map reset and shutdown
- Measured optimization evidence: bounded synthetic store/request/reset loop
## Status
- State: accepted
- Done: state holder implemented; loader cache/request/reset/metrics adapters
migrated; shutdown regression and required documentation updated
- Next: cached WMO scene/load state or liquid service extraction
- Blocked by:
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-RENDER-RESOURCE-CACHE-001:b4955d6 -->
## Handoff
- Commit: `d39f591`
- Results: cache-state PASS (`cases=10`, `iterations=100`,
`elapsed_ms=58.727`); direct no-`.godot` cache-state run PASS (`45.361ms`);
WMO queue/planner/registry/resolver/material/shutdown plus 18 adjacent
M2/terrain/facade/scheduler/streaming/coordinate regressions PASS;
documentation and coordination gates PASS; checkpoint dry-run PASS `7/7`.
- Remaining risks: accepted Resources remain strongly referenced until full
shutdown release; cached-scene and ResourceLoader I/O seams remain in loader;
no proprietary corrupt-cache fixture, asset-backed WMO traversal/leak run,
p95/p99 or original-client paired evidence. Full loader cannot be used as a
direct empty-class-cache smoke because pre-existing typed global coordinate/
focus dependencies require editor class registration; the extracted service
itself passed cold and normal editor parse passed.
- Documentation updated: inline public API; new
`docs/modules/wmo-render-resource-cache-state.md`; module registry, WMO queue,
world renderer and `RENDER.md`; data-flow/state/sequence diagrams, ownership,
threading, cache-version boundary, failure/recovery and source map included.
- Integration: merged as `b4955d6`; post-merge cache state (`cases=10`,
`iterations=100`, `elapsed_ms=51.949`), shutdown, WMO queue/resolver, facade,
internal-access (`private_symbols=41`), `7/7` manifest, documentation and
coordination gates passed.
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-RENDER-RESOURCE-CACHE-001:d39f591 -->
@@ -0,0 +1,117 @@
# M03-RND-WMO-SCENE-RESOURCE-CACHE-001 — WMO cached-scene resource state
<!-- OPENWC_CLAIM:M03-RND-WMO-SCENE-RESOURCE-CACHE-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-wmo-scene-resource-cache`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract cached-WMO `PackedScene`, negative-cache and asynchronous request state
from the loader while preserving loader-owned file limits, I/O and scene-cache
version validation.
## Non-goals
- Call `ResourceLoader`/`FileAccess`, construct `.tscn` paths or measure files.
- Instantiate scenes or validate WMO builder cache metadata/version.
- Own live WMO prototypes, placement/build jobs, instantiated Nodes or RIDs.
- Change missing/oversize/request-error fallback behavior or debug output.
- Claim asset-backed WMO fidelity, leak acceptance or traversal p95/p99.
## Paths
- Exclusive: `src/render/wmo/wmo_scene_resource_cache_state.gd`,
`src/tools/verify_wmo_scene_resource_cache_state.gd`,
`docs/modules/wmo-scene-resource-cache-state.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`src/tools/verify_render_runtime_cache_shutdown.gd`,
`docs/modules/wmo-render-resource-cache-state.md`,
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated ADT/WMO resources, caches
and proprietary renderer corpus
## Contracts and data
- State keys are loader-normalized relative WMO paths copied as `String`.
- Cached values are loader-validated `PackedScene` references.
- Pending values are `.tscn` paths for loader-owned threaded requests.
- Missing state accepts absent file, oversize, request-start, load and validation failures.
- Terminal pending removal precedes validated-scene or missing adoption.
- Transient clear retains accepted scenes; full clear releases all state.
- Cache metadata/version, file-size limit, profile and fallback rules are unchanged.
## Dependencies
- Requires: current master `e690756`; accepted WMO render Resource cache state
- Blocks: further WMO I/O/fallback and materialization decomposition
- External state: none; verification uses synthetic PackedScenes and paths
## Verification
- Commands: dedicated lifecycle/request/missing/reset/diagnostics/source/timing
verifier; WMO render-cache/queue/planner/registry/resolver/material/shutdown plus
adjacent renderer/streaming/coordinate/documentation/coordination gates
- Fixtures: synthetic PackedScene instances and normalized relative/cache paths
- Fidelity evidence: exact current cached-scene/missing/request transitions preserved
- Performance budget: 100 cycles over 256 scene/request keys under 1 second
## Documentation deliverables
- Inline public API docs
- Module specification with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state and loader interaction sequence diagrams
- World-renderer, module registry, render-cache and RENDER status updates
## Simplicity and naming
- Important names introduced: `WmoSceneResourceCacheState`
- Simplest considered solution: one RefCounted around three Dictionaries
- Rejected complexity/abstractions: generic cache hierarchy, callbacks, signals,
mutexes, ResourceLoader wrapper, persistence layer or render/scene cache merger
- Unavoidable complexity and justification: missing can be recorded without a
pending request, and partial reset must retain validated PackedScenes
- Measured optimization evidence: bounded synthetic store/request/reset loop
## Status
- State: accepted
- Done: state holder implemented; loader lookup/request/drain/reset/metrics
adapters migrated; shutdown regression and required documentation updated
- Next: WMO I/O/live fallback or liquid service extraction
- Blocked by:
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-SCENE-RESOURCE-CACHE-001:ada0fd9 -->
## Handoff
- Commit: `c839294`
- Results: scene cache-state PASS (`cases=11`, `iterations=100`,
`elapsed_ms=40.768`; initial direct cold run `43.636ms`); render cache-state,
WMO queue/planner/registry/resolver/material/shutdown plus 18 adjacent
M2/terrain/facade/scheduler/streaming/coordinate regressions PASS;
documentation and coordination gates PASS; checkpoint dry-run PASS `7/7`.
- Remaining risks: accepted PackedScenes remain strongly referenced until full
shutdown release; ResourceLoader/FileAccess I/O, cache-validation probes and
live fallback remain in loader; no proprietary stale/oversize fixture,
asset-backed traversal/leak run, p95/p99 or original-client paired evidence.
The extracted scene state passed cold; full loader cold execution still needs
editor class registration for pre-existing focus/coordinate global classes.
- Documentation updated: inline public API; new
`docs/modules/wmo-scene-resource-cache-state.md`; render-cache spec, module
registry, world renderer and `RENDER.md`; data-flow/state/sequence diagrams,
ownership, threading, file-size/cache-version boundary, failure/recovery and
source map included.
- Integration: merged as `ada0fd9`; post-merge scene state (`cases=11`,
`iterations=100`, `elapsed_ms=40.113`), render state (`43.434ms`), shutdown,
WMO queue/resolver, facade, internal-access (`private_symbols=40`), `7/7`
manifest, documentation and coordination gates passed.
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-SCENE-RESOURCE-CACHE-001:c839294 -->
+16
View File
@@ -12,9 +12,25 @@
| Player input | Partial | [`player-input.md`](player-input.md) |
| Local player movement | Implemented | [`local-player-movement.md`](local-player-movement.md) |
| Terrain query | Implemented | [`terrain-query.md`](terrain-query.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 geometry queue planner | Implemented extraction | [`terrain-chunk-geometry-queue-planner.md`](terrain-chunk-geometry-queue-planner.md) |
| M2 unique placement registry | Implemented extraction | [`m2-unique-placement-registry.md`](m2-unique-placement-registry.md) |
| M2 placement transform resolver | Implemented | [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md) |
| M2 placement grouper | Implemented extraction | [`m2-placement-grouper.md`](m2-placement-grouper.md) |
| M2 build batch planner | Implemented extraction | [`m2-build-batch-planner.md`](m2-build-batch-planner.md) |
| WMO placement resolver | Implemented extraction | [`wmo-placement-resolver.md`](wmo-placement-resolver.md) |
| WMO placement registry | Implemented extraction | [`wmo-placement-registry.md`](wmo-placement-registry.md) |
| WMO render build step planner | Implemented extraction | [`wmo-render-build-step-planner.md`](wmo-render-build-step-planner.md) |
| WMO render build queue | Implemented extraction | [`wmo-render-build-queue.md`](wmo-render-build-queue.md) |
| WMO render Resource cache state | Implemented extraction | [`wmo-render-resource-cache-state.md`](wmo-render-resource-cache-state.md) |
| WMO scene Resource cache state | Implemented extraction | [`wmo-scene-resource-cache-state.md`](wmo-scene-resource-cache-state.md) |
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
| World entity presentation | Implemented boundary / Prototype visuals | [`world-entity-presentation.md`](world-entity-presentation.md) |
| Streaming target planner | Implemented | [`streaming-target-planner.md`](streaming-target-planner.md) |
| Render budget scheduler | Implemented | [`render-budget-scheduler.md`](render-budget-scheduler.md) |
| Network/session | Planned | [`../../targets/roadmap/03-network-and-session.md`](../../targets/roadmap/03-network-and-session.md) |
| Gameplay | Prototype/Planned | [`../../targets/roadmap/04-gameplay-systems.md`](../../targets/roadmap/04-gameplay-systems.md) |
| UI/Lua/audio | Prototype/Planned | [`../../targets/roadmap/05-ui-lua-audio.md`](../../targets/roadmap/05-ui-lua-audio.md) |
+4
View File
@@ -116,9 +116,12 @@ flowchart LR
Godot -->|typed direct adapter| Tile
Godot -->|typed direct adapter| Local
Godot --> TerrainQuery[AdtTerrainQuery consumer]
Godot --> TargetPlanner[StreamingTargetPlanner consumer]
TerrainQuery --> Tile
TerrainQuery --> Local
TerrainQuery --> Chunk
TargetPlanner --> Tile
TargetPlanner --> Local
Tile -->|tile plus local| Godot
ServerYaw[ServerWorldYaw] -->|normalize only| CanonicalYaw[CanonicalWowWorldYaw]
CanonicalYaw -->|same numeric world angle| GodotYaw[GodotWorldYaw]
@@ -249,6 +252,7 @@ raw three-number arrays are not an accepted new persistence contract.
| `src/tools/verify_coordinate_golden_fixtures.gd` | Cross-source fixture schema and mapper validation |
| `src/tools/verify_coordinate_conversion_boundaries.gd` | Repository-wide manual conversion and required-consumer gate |
| `src/gameplay/terrain/adt_terrain_query.gd` | Typed Godot-position consumer for ADT tile/local/chunk lookup |
| `src/render/streaming/streaming_target_planner.gd` | Typed Godot-position consumer for ADT focus tile and boundary-local lookup |
| `src/tools/server_spawn_render_manifest.json` | Pinned server-spawn camera, player, marker and expected ADT ownership |
| `src/tools/verify_server_spawn_renderer.gd` | Fixture-to-renderer manifest contract verifier |
| `docs/adr/0001-canonical-world-coordinates.md` | Normative axes, units, boundaries and rollout decision |
+187
View File
@@ -0,0 +1,187 @@
# M2 Build Batch Planner
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-M2-BUILD-BATCH-PLANNER-001` |
| Owners | Pure static/animated M2 batch sizing and group cursor progression |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-build-batch-planner`, 2026-07-17 |
| Profiles/capabilities | Existing static MultiMesh and animated-instance build paths |
## Purpose
Select the effective per-tick M2 batch limit, cap it to remaining transforms and
plan the next group offset without accessing loader state or renderer resources.
## Non-goals
- Own M2 build jobs, queues, tile state, retries or cancellation.
- Decide whether an animated prototype or static mesh is ready.
- Rotate queues or consume `RenderBudgetScheduler` permits.
- Create or modify Nodes, MultiMesh, Mesh, materials or RIDs.
- Introduce spatial cells or change profile batch limits.
## Context and boundaries
```mermaid
flowchart LR
Groups[M2PlacementGrouper transform arrays] --> Loader[Loader build loop]
Limits[Animated/static batch limits] --> Planner[M2BuildBatchPlanner]
Loader --> Planner
Planner --> Plan[Detached batch plan]
Plan --> Loader
Loader --> Ready{Resource ready?}
Ready --> Materialize[Animated or MultiMesh materialization]
Loader --> Budget[RenderBudgetScheduler permit]
```
Allowed dependencies are integer/boolean scalar math and a fresh Dictionary.
Node, SceneTree, RenderingServer, ResourceLoader, WorkerThreadPool, mutexes,
renderer caches, gameplay, network, editor UI and files are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `plan_batch(transform_count, current_offset, has_animated_prototype, animated_batch_limit, static_batch_limit)` | Pure query | Return the selected batch size/count and next group cursor | Any thread; call-local result | Non-positive selected limit clamps to one; empty/past-end range completes |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Transform count | Grouped transform array | Planner remaining-count formula | Scalar copy | One plan |
| Input | Current group offset | Loader build job | Planner cursor formula | Scalar copy | One plan |
| Input | Animated prototype availability | Loader resource selection | Planner limit selector | Scalar copy | One plan |
| Input | Animated/static raw limits | Renderer profile/exports | Planner clamp | Scalar copy | One plan |
| Output | `effective_batch_size` | Planner | Tests/diagnostics | Detached integer | One plan |
| Output | `batch_count` | Planner | Loader materialization adapter | Detached integer | One plan |
| Output | `next_offset` | Planner | Loader build-job cursor | Detached integer | Until next batch |
| Output | `group_complete` | Planner | Loader group-index progression | Detached boolean | One plan |
The output Dictionary is newly allocated and caller-owned. The planner retains
no reference to it or any input.
## Data flow
```mermaid
flowchart TD
Select{Animated prototype?} -->|yes| Animated[animated_batch_limit]
Select -->|no| Static[static_batch_limit]
Animated --> Clamp[max 1]
Static --> Clamp
Clamp --> Remaining[max 0 of transform_count - current_offset]
Remaining --> Count[min effective limit, remaining]
Count --> Complete{count <= 0 or offset + count >= total?}
Complete -->|yes| Done[group_complete true; next_offset 0]
Complete -->|no| Continue[group_complete false; next_offset offset + count]
```
## Lifecycle/state
The planner is stateless. It has no initialization, reset, cancellation,
recovery or shutdown work. Each call returns a complete detached value plan.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader build loop
participant Planner as M2BuildBatchPlanner
participant Materializer as Animated/MultiMesh adapter
participant Scheduler as RenderBudgetScheduler
Loader->>Planner: plan_batch(count, offset, path kind, limits)
Planner-->>Loader: batch_count, next_offset, group_complete
Loader->>Loader: resolve/request prototype or mesh
Loader->>Materializer: materialize selected transform slice
Loader->>Loader: adopt cursor/group completion
Loader->>Scheduler: consume M2_BUILD permit
```
## Ownership, threading and resources
- The planner owns only call-local scalar values and the returned Dictionary.
- The loader owns build-job Dictionaries, queue ordering, tile checks, resource
readiness/retry, serial numbers and group-index mutation.
- Materializers own main-thread Node/MultiMesh construction under loader roots.
- The scheduler owns the frame-local `M2_BUILD` counter.
- Pure planning is thread-safe, though the current adapter calls it on main thread.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Zero/negative selected limit | Scalar clamp | Effective size becomes one | Contract fixture | Correct profile value later |
| Empty transform group | Remaining count zero | Count zero; group completes | Contract fixture | Loader advances group |
| Offset at/past end | Remaining count clamp | Count zero; offset resets | Contract fixture | Loader advances group |
| Negative offset | No new validation | Historical arithmetic is preserved | Contract fixture | Loader inputs should remain valid |
| Resource pending/missing | Outside planner | Loader rotates or advances as before | Existing cache/build diagnostics | Resource loader retry/fallback |
| Tile cancelled | Outside planner | Loader cancels job before planning/materialization | Existing lifecycle gates | Eligible tile may requeue |
## Configuration and capabilities
The planner introduces no setting. It consumes existing
`m2_animated_instances_per_tick` and `m2_multimesh_batch_size` values after the
loader determines whether an animated prototype is available.
## Persistence, cache and migration
No persistence, schema or cache format participates. ADT/M2 cache versions,
quality presets and rebuild instructions are unchanged.
## Diagnostics and observability
The planner emits no logs. Its verifier covers deterministic formula/source
boundaries and records bounded 20,000-plan timing. Existing loader metrics retain
queue depth, build activity and hitch observability.
## Verification
- `verify_m2_build_batch_planner.gd`: static/animated selection, invalid-limit
clamp, remaining cap, exact completion, empty/past-end and negative-offset
behavior, loader/dependency source boundaries and bounded timing.
- Grouper, transform resolver, unique registry/dedupe, facade, internal-access,
manifest, shutdown, scheduler, streaming, coordinate, documentation and
coordination regressions remain required.
- Fidelity evidence is formula extraction only. Asset-backed frame pacing and
visual output require the renderer checkpoint/performance workflow.
## Extension points
- A later package may define typed build-job state once queue/resource transitions
are extracted together with explicit cancellation and recovery.
- Spatial-cell batching must use measured culling/performance evidence and must
not silently change this model-path batch cursor.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Static/animated batch cursor planning | Implemented extraction | Contract/source/timing verifier | Asset-backed p95/p99 pending |
| Build queue/resource state machine | Remains in loader | Existing lifecycle regressions | Stateful extraction pending |
| Spatial-cell batching | Planned | Renderer roadmap | Culling evidence/design pending |
## Known gaps and risks
- The result remains a Dictionary while loader build jobs are untyped Dictionaries.
- Negative offsets are preserved rather than rejected to avoid a hidden behavior change.
- Planning timing does not measure resource loading or materialization.
- Private asset-backed p95/p99 and visual comparison remain unavailable.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_build_batch_planner.gd` | Pure limit/count/cursor planning |
| `src/scenes/streaming/streaming_world_loader.gd` | Build job, queue, resources, materialization and budgets |
| `src/render/m2/m2_placement_grouper.gd` | Produces grouped transform arrays |
| `src/tools/verify_m2_build_batch_planner.gd` | Formula, source and timing regression |
## Related decisions and references
- [`m2-placement-grouper.md`](m2-placement-grouper.md)
- [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+193
View File
@@ -0,0 +1,193 @@
# M2 Placement Grouper
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-PLACEMENT-GROUPER-001` |
| Owners | Pure ADT M2 placement validation, normalization and transform grouping |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-placement-grouper`, 2026-07-16 |
| Profiles/capabilities | Existing static M2 worker-grouping path |
## Purpose
Convert valid raw ADT M2 placement records into ordered tile-local transforms
grouped by the historically normalized relative model path.
## Non-goals
- Reserve cross-tile unique IDs or choose placement ownership.
- Own WorkerThreadPool tasks, mutexes, result queues or tile lifecycle.
- Load models, choose animated/static rendering or build MultiMesh batches.
- Create Node, Mesh, material, Resource or RID objects.
- Change path case, transform rules, batching or visual fidelity.
## Context and boundaries
```mermaid
flowchart LR
ADT[ADT names + placements] --> Registry[M2UniquePlacementRegistry]
Registry --> Grouper[M2PlacementGrouper]
Grouper --> Resolver[M2PlacementTransformResolver]
Resolver --> Grouper
Grouper --> Groups[Path to ordered Transform3D arrays]
Groups --> Worker[Loader worker result adapter]
Worker --> Build[Loader-owned build queue]
```
Allowed dependencies are value containers, `Vector3`, `Basis`, `Transform3D`
and `M2PlacementTransformResolver`. SceneTree, Node, RenderingServer,
WorkerThreadPool, mutexes, ResourceLoader, files, gameplay, network and editor UI
are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `group_placements(tile_origin, m2_names, m2_placements)` | Pure query | Return valid placements grouped as ordered tile-local transforms | Worker/main thread; call-local output | Invalid variants/name IDs/empty paths are skipped |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Godot-space tile origin | Loader tile state | Local-position calculation | Value copy | One call |
| Input | ADT MMDX name table | Parsed ADT tile | Path lookup | Read-only PackedStringArray | One call |
| Input | Raw placement dictionaries | Unique registry result | Validation/transforms | Read-only shallow values | One call |
| Output | Relative path to ordered `Transform3D` arrays | Grouper | Loader worker/build adapter | Fresh Dictionary and arrays | One call/build job |
Output retains first-seen group-key order through Godot Dictionary insertion
order and placement order inside each array. Inputs are not retained or returned.
## Data flow
```mermaid
flowchart TD
Call[group_placements] --> Next[Read placement]
Next --> Dict{Dictionary?}
Dict -->|no| Next
Dict -->|yes| Name{Valid name_id?}
Name -->|no| Next
Name -->|yes| Normalize[Backslash to slash; lowercase MDX/MDL to M2]
Normalize --> Empty{Path empty?}
Empty -->|yes| Next
Empty -->|no| Local[world position - tile origin]
Local --> Resolve[Resolve basis and origin offset]
Resolve --> Scale[Apply max scale 0.0001]
Scale --> Append[Append Transform3D to path group]
Append --> Next
Next -->|complete| Return[Return fresh groups]
```
## Lifecycle/state
The grouper retains no state. Every call constructs one stateless transform
resolver and fresh output containers. Reset, cancellation and shutdown require
no grouper action.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as Loader worker callback
participant Grouper as M2PlacementGrouper
participant Resolver as M2PlacementTransformResolver
Loader->>Grouper: group_placements(origin, names, placements)
loop valid placement
Grouper->>Resolver: resolve basis and origin offset
Resolver-->>Grouper: value-only basis and offset
Grouper->>Grouper: append local Transform3D
end
Grouper-->>Loader: fresh path groups
Loader->>Loader: mutex-protected result enqueue
```
## Ownership, threading and resources
- The grouper owns only call-local containers and transforms.
- The loader owns tasks, mutex/result queue, tile/build state, caches, model
loading, Nodes, MultiMesh, Mesh, materials and RIDs.
- Worker use is safe because the grouper and resolver mutate no shared state.
- The caller owns returned containers; mutation cannot affect later calls.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Non-Dictionary placement | Type guard | Skip | Contract fixture | Repair parser input |
| Invalid name ID | Bounds guard | Skip | Contract fixture | Repair name table/placement |
| Empty path | Empty guard | Skip | Contract fixture | Supply valid MMDX entry |
| Missing position/rotation/scale | Defaults | Zero position/rotation and scale 1 | Contract fixture | Expected fallback |
| Zero/negative scale | Historical clamp | Basis scale uses `0.0001` | Contract fixture | Supply valid ADT scale |
| Cancellation after grouping | Loader state check | Discard stale result | Loader regressions | Re-request eligible detail |
Cancellation stays loader-owned because grouping has no task or partial state.
## Configuration and capabilities
No setting or profile branch is introduced. Preserved behavior includes
backslash replacement without lowercasing, case-sensitive lowercase `.mdx/.mdl`
conversion, first-seen ordering, tile-local subtraction and scale clamp `0.0001`.
## Persistence, cache and migration
No persistence or cache format participates. ADT and M2 cache versions are
unchanged, so no rebuild or migration is required.
## Diagnostics and observability
The service emits no logs. Its verifier records contract results, source
boundaries and bounded synthetic 100-by-256 placement timing. Loader metrics
continue to own task/build/cache observability.
## Verification
- `verify_m2_placement_grouper.gd`: invalid input, path behavior, ordering,
defaults, local origin, regular/waterfall transforms, scale clamp, fresh output,
loader/source boundary and timing.
- `verify_m2_placement_transform_resolver.gd`: three transform consumers remain
covered as two direct loader adapters plus the grouper adapter.
- Registry/dedupe, facade, internal-access, manifest, shutdown, documentation and
coordination regressions remain required.
- Fidelity evidence is exact extraction; no new asset-backed parity claim is made.
## Extension points
- `M2BuildBatchPlanner` now owns pure batch sizing/cursor progression; the
remaining build-job state machine stays behind explicit loader budgets.
- Spatial cells require measured culling evidence and are outside this contract.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Pure path/transform grouping | Implemented extraction | Contract/source/timing verifier | Asset-backed traversal timing pending |
| Worker/build orchestration | Remains in loader | Existing regressions | Separate stateful extraction required |
| Spatial MultiMesh cells | Partial | Current path batches | Culling-driven design/evidence pending |
## Known gaps and risks
- Raw placement dictionaries remain the ADT parser boundary.
- Uppercase extensions remain unchanged even though transform exception matching
is case-insensitive.
- Grouping is by model path, not spatial culling cell.
- Asset-backed p95/p99 and visual comparison remain unavailable.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_placement_grouper.gd` | Validation, normalization and grouping |
| `src/render/m2/m2_placement_transform_resolver.gd` | Basis and origin rules |
| `src/render/m2/m2_build_batch_planner.gd` | Downstream batch sizing and cursor planning |
| `src/scenes/streaming/streaming_world_loader.gd` | Worker/result/build lifecycle and resources |
| `src/tools/verify_m2_placement_grouper.gd` | Contract, dependency and timing regression |
## Related decisions and references
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
- [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md)
- [`m2-unique-placement-registry.md`](m2-unique-placement-registry.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
@@ -0,0 +1,187 @@
# M2 Placement Transform Resolver
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-M2-PLACEMENT-TRANSFORM-001` |
| Owners | Pure ADT M2 placement basis and model-specific origin compensation |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-placement-transform`, 2026-07-16 |
| Profiles/capabilities | Existing static M2 paths; four calibrated model exceptions |
## Purpose
Resolve the unscaled Godot-space orientation and optional local origin offset
used by the pure placement grouper, placeholder rendering and instance creation.
## Non-goals
- Convert WoW/server/ADT coordinates or own placement positions.
- Apply final scale clamps, build transforms, group placements or create Nodes.
- Add, generalize or configure model-specific corrections.
- Change M2 materials, animation, caches, visibility or quality profiles.
- Claim general build-12340 placement parity.
## Context and boundaries
```mermaid
flowchart LR
ADT[ADT placement rotation/path/scale] --> Resolver[M2PlacementTransformResolver]
Resolver --> Basis[Unscaled Basis]
Resolver --> Offset[Local origin offset]
Basis --> Group[M2PlacementGrouper]
Basis --> Placeholder[Placeholder MultiMesh path]
Basis --> Instance[Instance path]
Offset --> Group
Offset --> Placeholder
Offset --> Instance
Group --> Loader[StreamingWorldLoader-owned queues/build]
```
Allowed dependencies are `Vector3`, `Basis`, scalar math and path-string
normalization. Node, SceneTree, RenderingServer, ResourceLoader, tasks, caches,
files, gameplay, network and editor UI are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `resolve_basis(rotation_radians, relative_m2_path)` | Pure query | Return unscaled regular or calibrated placement orientation | Worker/main thread; call-local result | Unknown paths use `Basis.from_euler` |
| `resolve_origin_offset(rotation_radians, relative_m2_path, scale_value)` | Pure query | Compensate the measured tall-waterfall twist anchor | Worker/main thread; call-local result | Regular/unanchored paths return zero; scale clamps to `0.0001` for compensation |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Godot Euler rotation in radians | ADT loader placement record | Resolver | Scalar copy | One call |
| Input | Relative M2 path | ADT MMDX/name table | Resolver allowlist | Caller string | One call |
| Input | Raw placement scale | ADT placement record | Offset compensation | Scalar copy | One call |
| Output | Unscaled `Basis` | Resolver | Grouper/placeholder/instance adapter | Value copy | One call |
| Output | Local `Vector3` offset | Resolver | Grouper/placeholder/instance adapter | Value copy | One call |
Callers preserve their historical basis scale minima: grouping/instance use
`0.0001`, while placeholders use `0.01`.
## Data flow
```mermaid
flowchart TD
BasisCall[resolve_basis] --> ModelName[Normalize separators/case/basename]
ModelName --> Waterfall{Waterfall sheet?}
Waterfall -->|yes| WaterfallBasis[+90 degree world yaw * Euler * local-axis twist]
Waterfall -->|no| Cliff{Elwynn cliff rock?}
Cliff -->|yes| CliffBasis[Corrected Y/BACK/RIGHT composition]
Cliff -->|no| Regular[Basis.from_euler]
OffsetCall[resolve_origin_offset] --> Anchored{Tall waterfall anchor?}
Anchored -->|no| Zero[Vector3.ZERO]
Anchored -->|yes| Compensation[(base anchor - twisted anchor) * clamped scale]
```
## Lifecycle/state
The resolver is stateless. It may be shared between the main thread and worker
calls because each result is computed solely from value inputs. Construction,
world reset and shutdown require no clear, cancellation or resource release.
## Main sequence
```mermaid
sequenceDiagram
participant Caller as Grouper or StreamingWorldLoader
participant Resolver as M2PlacementTransformResolver
participant Consumer as Grouper/placeholder/instance path
Caller->>Resolver: resolve_basis(rotation, path)
Resolver-->>Caller: unscaled Basis
Caller->>Resolver: resolve_origin_offset(rotation, path, scale)
Resolver-->>Caller: local offset
Caller->>Consumer: compose historical scaled Transform3D
```
## Ownership, threading and resources
- The resolver retains no mutable state or engine resources.
- The grouper owns worker-path final transforms; the loader owns placement records,
tasks, queues, caches and MultiMesh/Node/Mesh/material/RID creation and cleanup.
- Pure methods are safe for grouper worker use and main-thread consumers.
- Returned `Basis`/`Vector3` values have no lifetime coupling to the resolver.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty/unknown path | Allowlist miss | Regular Euler basis and zero offset | Contract fixture | Expected fallback |
| Mixed case/backslashes/MDX | Normalization | Same calibrated model match | Contract fixture | None required |
| Zero/negative scale for anchored offset | Numeric clamp | Use `0.0001` compensation scale | Contract fixture | Caller may supply valid ADT scale later |
| World reset/shutdown | No retained state | No action | Existing shutdown verifier | New calls remain independent |
There is no asynchronous operation or cancellation inside the resolver.
## Configuration and capabilities
The resolver introduces no settings. The preserved constants and allowlists are:
- world yaw `PI` for `ElwynnCliffRock01/02`;
- world yaw `PI/2` for `NewWaterfall` and `ElwynnTallWaterfall01`;
- measured local axes/twist signs for both waterfall sheets;
- measured tall-waterfall anchor `(-2.667799, 89.62273, 0.00129)`.
## Persistence, cache and migration
No persistence, schema or cache format participates. Existing ADT/M2 cache
versions and rebuild instructions remain unchanged.
## Diagnostics and observability
The resolver emits no logs. The contract verifier compares formulas/constants,
checks two direct loader consumers plus the grouper consumer and records bounded
synthetic timing. Visual
and asset-backed evidence remains in the renderer checkpoint workflow.
## Verification
- `verify_m2_placement_transform_resolver.gd`: regular fallback, path
normalization, both cliff rocks, both waterfalls, twist anchor, negative-scale
compensation, three consumers, source boundary and 10,000-pair timing.
- M2 unique/dedupe, terrain, facade, internal-access, manifest, shutdown,
scheduler, streaming and coordinate regressions remain required.
- Fidelity evidence is exact extraction of existing calibrated formulas. The
private asset/camera comparison that motivated them is not reproduced here.
## Extension points
- `M2PlacementGrouper` composes this resolver for the worker grouping path.
- New model corrections require separate measured fidelity evidence; this module
is not a generic override registry.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Consistent transform rules across three M2 paths | Implemented | Formula/source/timing contract | Asset-backed visual recheck pending |
| General ADT M2 placement parity | Partial | Existing checkpoint notes | Broader fixture coverage required |
## Known gaps and risks
- Four model-specific exceptions are empirical compatibility rules.
- Most placements use raw Godot Euler behavior; independent client-wide
transform validation is incomplete.
- Callers intentionally retain different final scale clamps.
- Grouping, build, cache and render ownership remain in the streamer.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_placement_transform_resolver.gd` | Pure basis/offset rules and calibrated allowlists |
| `src/render/m2/m2_placement_grouper.gd` | Grouping adapter and local Transform3D ownership |
| `src/scenes/streaming/streaming_world_loader.gd` | Two direct adapters and renderer resource ownership |
| `src/tools/verify_m2_placement_transform_resolver.gd` | Formula, source and performance regression |
## Related decisions and references
- [`m2-placement-grouper.md`](m2-placement-grouper.md)
- [`m2-unique-placement-registry.md`](m2-unique-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,220 @@
# M2 Unique Placement Registry
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-UNIQUE-REGISTRY-001` |
| Owners | Cross-tile positive ADT MDDF unique-ID reservations |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-unique-registry`, 2026-07-16 |
| Profiles/capabilities | Existing static M2 placement path; session-memory only |
## Purpose
Ensure a positive ADT `MDDF.uniqueId` has at most one owning streamed tile while
allowing unkeyed placements through and returning skipped keys for loader-driven
candidate retry after the owner unloads.
## Non-goals
- Own tile state, retry queues, worker/group/build jobs or detail radius rules.
- Group placements or create MultiMesh, Mesh, Node, material or RID resources.
- Validate M2 paths, transforms, animation, materials or build-12340 fidelity.
- Persist reservations or define a generic identity/ownership framework.
- Change cache formats, quality profiles or placement visibility.
## Context and boundaries
```mermaid
flowchart LR
ADT[ADT M2 placement dictionaries] --> Loader[StreamingWorldLoader adapter]
Loader --> Registry[M2UniquePlacementRegistry]
Registry --> Accepted[Filtered placements + owned keys]
Registry --> Skipped[Skipped keys]
Accepted --> Group[Existing worker grouping/build path]
Skipped --> TileState[Loader-owned tile retry state]
Unload[Owner tile unload] --> Loader
Loader -->|release keys| Registry
Loader -->|notify released candidates| Retry[Existing detail sync queue]
```
Allowed dependencies are scalar/container values from the ADT adapter. Node,
SceneTree, RenderingServer, ResourceLoader, WorkerThreadPool, gameplay, network,
editor UI, files and persistent caches are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `reserve(tile_key, m2_placements)` | Command/query | Reserve available positive IDs and return fresh filtered/owned/skipped arrays | Renderer owner thread/session | Invalid non-Dictionary entries are ignored; unkeyed entries pass through |
| `release(tile_key, unique_keys)` | Command | Release only keys owned by the supplied tile | Renderer owner thread/session | Unknown/non-owner/empty keys are ignored |
| `clear()` | Lifecycle command | Drop every reservation on world reset/shutdown | Renderer owner thread | Idempotent |
| `active_count()` | Query | Return current reservation count | Renderer owner thread | No failure state |
| `diagnostic_snapshot()` | Read model | Return sorted detached key/tile ownership records | Caller-owned result | Exposes no mutable registry reference |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Tile key | Streamer tile state | Registry reservation owner | Copied string | Reservation lifetime |
| Input | Raw M2 placement dictionaries | ADT parser result | Registry | Loader-owned; read only | One reserve call |
| Input | Claimed key array | Tile state | Registry release | Loader-owned; read only | One release call |
| Output | Filtered placement array | Registry | M2 grouping task adapter | Fresh shallow array; placement dictionaries remain source values | One request |
| Output | Owned `uid:<decimal>` keys | Registry | Loader tile state | Fresh strings/array | Until release |
| Output | Skipped keys | Registry | Loader candidate retry state | Fresh strings/array | Until owner release/retry |
| Output | Released keys | Registry | Loader candidate notifier | Fresh array in input order | One release |
| Output | Detached diagnostics/count | Registry | Metrics/tests | Caller-owned scalars/dictionaries | One query |
Only `unique_id > 0` enters the registry. Missing, zero and negative values have
an empty key and remain untracked, matching the prior loader behavior.
## Data flow
```mermaid
flowchart TD
Reserve[reserve tile + placements] --> Placement[Iterate placement order]
Placement --> Dict{Dictionary?}
Dict -->|no| Placement
Dict -->|yes| Positive{unique_id > 0?}
Positive -->|no| Pass[Append untracked placement]
Positive -->|yes| Owned{Key already owned?}
Owned -->|no| Claim[Set owner and append placement/key]
Owned -->|same tile + already in this call| Duplicate[Suppress duplicate]
Owned -->|same tile + first in this call| PassOwner[Append placement/key]
Owned -->|other tile| Skip[Append skipped key once]
Pass --> Placement
Claim --> Placement
Duplicate --> Placement
PassOwner --> Placement
Skip --> Placement
Release[release tile + keys] --> Owner{Tile is current owner?}
Owner -->|yes| Erase[Erase and return key]
Owner -->|no| Ignore[Leave reservation unchanged]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Empty
Empty --> Reserved: reserve positive unowned ID
Reserved --> Reserved: same-owner reserve / competing skip / non-owner release
Reserved --> Empty: owner releases final ID
Reserved --> Empty: clear world/reset
Empty --> Empty: unkeyed reserve / unknown release / clear
Empty --> [*]
Reserved --> [*]: owner releases registry after clear
```
## Main sequence
```mermaid
sequenceDiagram
participant TileA as First streamed tile
participant Loader as StreamingWorldLoader
participant Registry as M2UniquePlacementRegistry
participant TileB as Cross-border candidate tile
TileA->>Loader: placements include positive unique ID
Loader->>Registry: reserve(tile A, placements)
Registry-->>Loader: accepted placement + owned key
TileB->>Loader: same placement ID
Loader->>Registry: reserve(tile B, placements)
Registry-->>Loader: skipped key
Loader->>Loader: retain skipped key in tile B state
TileA->>Loader: unload/remove details
Loader->>Registry: release(tile A, owned keys)
Registry-->>Loader: released key
Loader->>Loader: enqueue eligible tile B detail retry
```
## Ownership, threading and resources
- The registry owns one private key-to-owner-string dictionary.
- The loader owns tile-held owned/skipped arrays, retry discovery, task/build
lifecycle and every MultiMesh/Node/Mesh/material/RID side effect.
- Calls currently occur on the renderer main thread. The registry has no lock and
must not be mutated concurrently.
- Returned arrays and diagnostic dictionaries are fresh; source placement
dictionaries are passed through shallowly and must remain read-only.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Invalid placement variant | Type guard | Ignore entry | Contract test | Repair parser/adapter input |
| Missing/non-positive ID | Key rule | Pass placement without ownership | Smoke/contract test | Expected legacy behavior |
| Cross-tile duplicate | Owner lookup | Skip placement and return key once | Tile skipped-key state | Owner release triggers eligible candidate retry |
| Non-owner release | Owner comparison | Ignore; preserve owner | Contract test | Release from recorded tile |
| Reset/shutdown | Loader lifecycle | Clear all reservations | Shutdown regression/count | New world reserves afresh |
There is no asynchronous work or cancellation inside the registry. Loader-owned
tasks are cancelled independently before/reset around registry clearing.
## Configuration and capabilities
The registry introduces no settings. It applies to all positive IDs in the
existing static M2 placement path. Detail radius, M2 enablement, build budgets
and animation allowlists remain loader/profile configuration.
## Persistence, cache and migration
Reservations are runtime-only and are not serialized. M2 `.tscn/.glb`, animation,
mesh/material refresh and ADT cache versions remain unchanged. No rebuild or
migration is required.
## Diagnostics and observability
- `active_count()` continues to feed `m2_active_unique_ids` baseline metrics.
- Detached diagnostics sort by unique key for deterministic inspection and expose
only strings/counts.
- Contract timing covers repeated 256-placement reserve/release cycles. Asset-
backed streaming p95/p99 remains milestone acceptance work.
## Verification
- `verify_m2_unique_placement_registry.gd`: invalid/unkeyed handling, within-call
duplicate suppression, same-owner repeat, cross-tile skip, owner-checked release,
retry, idempotent clear, detached/sorted diagnostics, source boundary and timing.
- `verify_m2_unique_dedupe.gd`: existing `uid:11785` cross-tile smoke now targets
the extracted public contract rather than loader-private methods.
- Facade, terrain, internal-access, manifest, shutdown, scheduler, streaming and
coordinate regressions remain required.
- Fidelity evidence is dedupe behavior preservation only. No new build-12340
visual parity claim is made.
## Extension points
- Expose detached ownership diagnostics through facade metrics only if needed.
- Extract grouping/build orchestration as separate services with explicit budgets.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Cross-tile positive-ID ownership | Implemented extraction | Dedicated contract plus legacy smoke | Asset-backed boundary traversal timing pending |
| Candidate retry and M2 build | Remains in loader | Existing release adapter/source boundary | Broader M2 service extraction required |
## Known gaps and risks
- Placement dictionaries are shallow pass-through values at the ADT boundary.
- Same-owner reservations across separate calls intentionally pass again; the
caller's existing root/task guards prevent duplicate build requests.
- First owner follows tile processing order, matching prior behavior rather than
a canonical geometric owner rule.
- Group/build/load/animation caches and render finalization remain monolithic.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_unique_placement_registry.gd` | Positive-ID keying, ownership, release and diagnostics |
| `src/scenes/streaming/streaming_world_loader.gd` | Reservation adapters, tile state, candidate retry and M2 rendering |
| `src/tools/verify_m2_unique_placement_registry.gd` | Contract, lifecycle, source and timing regression |
| `src/tools/verify_m2_unique_dedupe.gd` | Historical cross-tile smoke fixture |
## Related decisions and references
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+216
View File
@@ -0,0 +1,216 @@
# Render Budget Scheduler
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | `M03-RND-SCHEDULER-001` |
| Owners | Renderer workstream / M03 integrator |
| Last verified | Worktree `work/sindo-main-codex/m03-render-budget-scheduler`, 2026-07-16 |
| Profiles/capabilities | Runtime and Editor main-thread operation quotas; profile values supplied by streamer composition |
## Purpose
Issue a bounded number of permits for each renderer operation lane during one
process frame. The scheduler centralizes quota accounting and teardown
cancellation without taking ownership of queues or render work.
## Non-goals
- Own, sort, drain or cap the stored length of a renderer queue.
- Submit, cancel or await worker and `ResourceLoader` tasks.
- Execute callbacks or mutate the SceneTree, resources, caches or RIDs.
- Adapt limits from elapsed time or create a general job/dependency framework.
- Select renderer quality or compatibility profiles.
## Context and boundaries
```mermaid
flowchart LR
Config[Streamer exported per-frame limits] --> Frame[operation limit snapshot]
Frame --> Scheduler[RenderBudgetScheduler]
Queues[StreamingWorldLoader owned queues] --> Drains[Ordered queue drains]
Scheduler -->|boolean permits| Drains
Drains --> Render[Main-thread finalize/attach/evict]
Shutdown[StreamingWorldLoader exit] -->|cancel| Scheduler
Scheduler -. does not own .-> Queues
```
Allowed dependencies are Godot value containers and `RefCounted`. The module
must not depend on `Node`, `WorkerThreadPool`, `ResourceLoader`, `RenderingServer`,
mutexes, renderer assets or streamer queue fields.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Failure behavior |
|---|---|---|---|---|
| Lane constants | `StringName` constants | Stable operation categories used in the frame limit map | Process lifetime | Unknown lanes have zero permits |
| `begin_frame` | Method | Replaces all limits and clears consumption diagnostics for one frame | Main thread; once before drains | Negative limits clamp to zero; cannot revive a cancelled instance |
| `has_remaining_permit` | Query | Tests a lane without consuming | Main thread; current frame | Unknown/exhausted/cancelled returns `false` |
| `try_consume_permit` | Mutation | Atomically checks and consumes one caller operation permit | Main thread; current frame | Unknown/exhausted/cancelled returns `false` |
| `remaining_permits` | Query | Returns a lane's current unconsumed count | Main thread; current frame | Unknown/cancelled returns zero |
| `consumed_permits_snapshot` | Query | Returns detached per-lane diagnostics | Caller owns returned dictionary | Mutation cannot affect scheduler state |
| `cancel` | Lifecycle method | Permanently stops new permits and clears remaining limits | Main thread; renderer teardown | Idempotent; does not interrupt in-flight caller work |
| `is_cancelled` | Query | Reports terminal lifecycle state | Scheduler lifetime | No failure |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | `Dictionary[StringName, int]` operation limits | `StreamingWorldLoader` configuration snapshot | Scheduler | Copied by scheduler | One process frame |
| Input | Lane ID | Ordered streamer drain | Scheduler | Constant value | One permit check |
| Input | Cancellation | Streamer `_exit_tree` | Scheduler | Terminal flag | Renderer instance lifetime |
| Output | Permit boolean | Scheduler | Streamer drain loop | Scalar | One attempted operation |
| Output | Remaining count | Scheduler | Streamer/tests | Scalar | Current frame |
| Output | Consumed snapshot | Scheduler | Diagnostics/tests | Detached caller-owned copy | Current frame snapshot |
The scheduler has no I/O, logging, SceneTree, task, cache or GPU side effects.
## Data flow
```mermaid
flowchart TD
Exports[Exported operation limits] --> Limits[_render_operation_limits_for_frame]
Limits --> Begin[begin_frame]
Begin --> Remaining[remaining permits by lane]
Queue[Non-empty caller-owned queue] --> Check[try_consume_permit]
Remaining --> Check
Check -->|true| Execute[Caller performs one operation]
Check -->|false| Defer[Operation remains queued]
Execute --> Consumed[consumed permits diagnostics]
```
## Lifecycle and state
```mermaid
stateDiagram-v2
[*] --> Ready
Ready --> FrameActive: begin_frame
FrameActive --> FrameActive: consume/check
FrameActive --> FrameActive: begin_frame resets limits
Ready --> Cancelled: cancel
FrameActive --> Cancelled: cancel
Cancelled --> Cancelled: begin_frame/check/cancel
Cancelled --> [*]
```
Cancellation is intentionally terminal. A new renderer scene creates a new
scheduler instance; an old instance cannot accidentally resume after teardown.
## Main sequence
```mermaid
sequenceDiagram
participant Stream as StreamingWorldLoader
participant Budget as RenderBudgetScheduler
participant Queue as Loader-owned queues
participant Render as SceneTree/RenderingServer
Stream->>Budget: begin_frame(operation limits)
loop existing fixed drain order
Stream->>Queue: inspect next operation
Stream->>Budget: try_consume_permit(lane)
Budget-->>Stream: true or false
Stream->>Render: execute one operation when true
end
Stream->>Budget: cancel() during _exit_tree
Budget-->>Stream: later permits rejected
Stream->>Stream: await tasks and release owned resources
```
## Ownership, threading and resources
- `StreamingWorldLoader` owns every queue, task registry, cache, Node, Resource and RID.
- The scheduler owns two small per-frame counter dictionaries and one terminal flag.
- The streamer preserves operation order. In particular, chunk removals and
creations share `CHUNK_GEOMETRY`, with removals drained first.
- All current scheduler calls occur on the Godot main thread. The service has no
mutex and is not safe for concurrent mutation.
- A scheduler instance lives exactly as long as its loader instance.
## Errors, cancellation and recovery
| Condition | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Negative/zero limit | `begin_frame` clamp | Lane issues no permits | Contract verifier | Correct configuration or accept disabled lane |
| Unknown lane | Permit lookup miss | Returns `false`/zero | Contract verifier | Add the lane to the frame snapshot |
| Exhausted lane | Remaining count is zero | Work remains in caller queue for a later frame | Queue-depth/hitch metrics | Next `begin_frame` replenishes permits |
| Renderer teardown | Streamer calls `cancel` before task waits | New permits stop immediately | Shutdown ownership regression | New scene creates a new scheduler |
| In-flight operation at cancel | Caller already consumed permit | Scheduler does not interrupt it | Ownership docs | Caller completes/cleans it through existing shutdown path |
This cancellation scope is permit issuance, not worker cancellation. Existing
task/result staleness and shutdown waits remain streamer responsibilities.
## Configuration and capabilities
The streamer's existing exported values populate 16 lanes: tile finalization,
terrain upgrade/control-splat/splat-cache/splat-build, water finalization, shared
chunk geometry, tile-load starts, tile LOD create/remove, M2 animation/mesh
finalization and build, WMO instance/group build, and detail synchronization.
Defaults and quality-preset assignments are unchanged. `tile_finalize` retains
its historical minimum of one; other negative values behave as zero.
## Persistence, cache and migration
The module is runtime-only and serializes nothing. It changes no cache, asset,
scene, protocol or database format and requires no version bump or migration.
## Diagnostics and observability
- `consumed_permits_snapshot` exposes detached per-frame counts for tests and
future facade metrics without exposing mutable scheduler state.
- Existing queue depths, named hitch sections and `HITCH`/`PERF` logs remain in
`StreamingWorldLoader`.
- No scheduler logging occurs on the hot permit path.
## Verification
- `src/tools/verify_render_budget_scheduler.gd` covers exact exhaustion,
independent lanes, shared removal/create priority, frame reset, invalid/unknown
lanes, detached diagnostics, terminal cancellation and source boundaries.
- The verifier performs 20,000 permit checks with a generous 150 ms regression
ceiling on the headless runner; this is a guard, not a renderer p95 claim.
- Integration regressions must retain the seven renderer checkpoint plans and
existing focus/planner/facade contracts.
- Fidelity evidence is behavior-preserving: exported values, process order,
cache versions and visual operations are unchanged. No build-12340 parity
claim follows from this extraction.
## Extension points
- Add facade consumption metrics using detached snapshots.
- Move a service's queue drain behind its own boundary while retaining these lane IDs.
- Add measured time-aware policy only after asset-backed p95/p99 evidence proves
fixed operation counts insufficient; do not embed callbacks in this scheduler.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Per-lane frame bounds | Implemented | Exact exhaustion and independent-lane contracts | Asset-backed p95/p99 baseline remains |
| Shared removal/create priority | Implemented | Synthetic shared `CHUNK_GEOMETRY` contract | Other service priorities remain streamer-owned |
| Teardown permit cancellation | Implemented | Terminal cancellation contract and loader source gate | Worker stale-result cancellation remains streamer-owned |
| Detached consumption diagnostics | Implemented | Snapshot isolation contract | Not yet exposed through facade metrics |
## Known gaps and risks
- Queue storage length is not capped; only per-frame draining is bounded.
- Worker concurrency and stale-result cancellation remain existing streamer logic.
- Exact p95/p99 impact requires local extracted assets and paired baseline runs.
- Lane IDs are dynamic dictionary keys; the verifier guards the current contract.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/streaming/render_budget_scheduler.gd` | Scene-free lane counters, permits, diagnostics and cancellation |
| `src/scenes/streaming/streaming_world_loader.gd` | Limit composition, fixed drain order, queue/resource ownership and operation execution |
| `src/tools/verify_render_budget_scheduler.gd` | Asset-free behavior, dependency and bounded timing regression |
## Related decisions and references
- [`world-renderer.md`](world-renderer.md)
- [`streaming-target-planner.md`](streaming-target-planner.md)
- [`../../targets/03-renderer-facade.md`](../../targets/03-renderer-facade.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
- [`../CODING_STANDARD.md`](../CODING_STANDARD.md)
+239
View File
@@ -0,0 +1,239 @@
# Streaming Target Planner
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | `M03-RND-STREAMING-PLANNER-001` |
| Owners | Renderer workstream / M03 integrator |
| Last verified | `e887c3b`, 2026-07-15 |
| Profiles/capabilities | Runtime and Editor preview ADT target selection; profile values supplied by caller |
## Purpose
Calculate which available ADT tiles are wanted and retained for one typed focus
without accessing the SceneTree, worker tasks, renderer queues, caches or GPU
resources. Runtime planning preserves prewarm, hysteresis and boundary-prefetch
behavior; editor planning preserves the square preview radius.
## Non-goals
- Mutate load/finalize/eviction queues.
- Prioritize queued tile paths by world distance.
- Calculate chunk or tile LOD levels after a tile is loaded.
- Own the available WDT tile catalog or cache paths.
- Create, cancel or await jobs.
- Implement M2, WMO, liquid or terrain attachment.
## Context and boundaries
```mermaid
flowchart LR
Focus[GodotWorldPosition] --> Planner[StreamingTargetPlanner]
Policy[StreamingTargetPolicy] --> Planner
Catalog[Available tile keys from WDT/directory] --> Planner
Planner --> Plan[StreamingTargetPlan]
Plan --> Streamer[StreamingWorldLoader queue/state apply]
Streamer --> Queue[Tile load queue]
```
Allowed dependencies:
- typed coordinate values and `CoordinateMapper`;
- immutable planner policy/result contracts;
- read-only available-tile key membership.
Forbidden dependencies:
- `Node`, SceneTree and viewport/camera discovery;
- `WorkerThreadPool`, `ResourceLoader` and filesystem access;
- `RenderingServer`, meshes, materials, nodes and RIDs;
- gameplay, network, UI, editor UI or server adapters;
- streamer queues, tile states and cache dictionaries.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `StreamingTargetPlanner.plan_runtime` | Pure method | Builds wanted/retained sets with edge/corner prefetch | Any thread if caller does not mutate catalog concurrently; call-scoped | Empty catalog produces empty sets |
| `StreamingTargetPlanner.plan_editor` | Pure method | Builds identical editor wanted/retained square sets | Same as runtime planning | Negative radius clamps to zero |
| `StreamingTargetPolicy` | Immutable value | Captures radius margins and boundary threshold for one plan | Caller-owned, shareable | Values retain scene input; effective margins/threshold are clamped |
| `visible_tile_radius` | Pure query | Resolves chunk-radius versus tile-radius visibility | Any thread/value lifetime | No failure |
| `load_tile_radius` | Pure query | Adds non-negative prewarm margin | Any thread/value lifetime | No failure |
| `retained_tile_radius` | Pure query | Adds non-negative hysteresis margin | Any thread/value lifetime | No failure |
| `StreamingTargetPlan` | Immutable value | Holds focus tile and read-only wanted/retained key sets | Caller-owned plan lifetime | No failure |
| `wanted_tile_keys` | Read-only query | Returns immutable wanted set | Plan lifetime | Mutation is rejected by Godot read-only Dictionary |
| `retained_tile_keys` | Read-only query | Returns immutable retained set | Plan lifetime | Mutation is rejected by Godot read-only Dictionary |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | `GodotWorldPosition` | `WorldRenderFacade`/streamer focus adapter | Planner | Immutable caller value | One plan |
| Input | Available `<tile_x>_<tile_y>` keys | `StreamingWorldLoader` WDT/directory catalog | Planner membership checks | Loader-owned, read-only during call | Map session |
| Input | `StreamingTargetPolicy` | Streamer configuration snapshot | Planner | Immutable caller value | One or more plans |
| Input | Editor preview radius | Editor scene configuration | Planner | Scalar copy | One plan |
| Output | `StreamingTargetPlan.focus_tile` | Planner/`CoordinateMapper` | Streamer diagnostics | Immutable typed coordinate | Plan lifetime |
| Output | Wanted tile-key set | Planner | Streamer queue/state apply | Plan-owned read-only Dictionary | Plan lifetime |
| Output | Retained tile-key set | Planner | Streamer hysteresis/state apply | Plan-owned read-only Dictionary | Plan lifetime |
Side effects:
- none;
- no input mutation;
- no logging, I/O, task submission, cache access, SceneTree or GPU work.
## Data flow
```mermaid
flowchart TD
Position[GodotWorldPosition] --> Tile[CoordinateMapper.godot_to_adt_tile]
Position --> Local[CoordinateMapper.godot_to_adt_tile_local]
Policy[StreamingTargetPolicy] --> Radii[load and retained radii]
Policy --> Threshold[clamped boundary threshold]
Tile --> Centers[base prefetch center]
Local --> Centers
Threshold --> Centers
Centers --> Wanted[available keys within load radius]
Centers --> Retained[available keys within retained radius]
Catalog[Available tile keys] --> Wanted
Catalog --> Retained
Wanted --> Plan[StreamingTargetPlan]
Retained --> Plan
Tile --> Plan
```
## Lifecycle/state
The planner is stateless. A call creates local center/set dictionaries, freezes
the result dictionaries in `StreamingTargetPlan`, returns the plan and retains
no reference to inputs or output. There is no retry, cancellation or shutdown
state machine.
## Main sequence
```mermaid
sequenceDiagram
participant Stream as StreamingWorldLoader
participant Planner as StreamingTargetPlanner
participant Mapper as CoordinateMapper
participant Plan as StreamingTargetPlan
Stream->>Planner: plan_runtime(catalog, typed focus, policy)
Planner->>Mapper: focus tile and tile-local position
Mapper-->>Planner: typed coordinates
Planner->>Planner: prefetch centers and available key sets
Planner->>Plan: freeze focus/wanted/retained result
Plan-->>Stream: immutable target plan
Stream->>Stream: apply queues, LOD and detail state
```
## Ownership, threading and resources
- `StreamingWorldLoader` owns the available catalog and must not mutate it during a planning call.
- `StreamingTargetPolicy` and typed focus remain caller-owned immutable values.
- The planner owns temporary center/set values only until plan construction.
- `StreamingTargetPlan` owns its read-only result dictionaries.
- The planner creates no Node, Resource, RID, thread, mutex or task.
- Current runtime calls occur on the main thread; scene-free contracts permit a
future worker call only after catalog publication/ownership is explicit.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty/unavailable catalog | No membership matches | Valid plan with empty sets | Streamer retains existing missing-data diagnostics | Load a valid WDT/directory catalog and replan |
| Focus outside catalog/grid | Typed coordinate does not match keys | Valid focus tile with filtered empty/partial sets | Caller diagnostic if required | Supply valid focus/map or catalog |
| Negative prewarm/retain margin | Policy effective-radius query | Margin clamps to zero | Contract test | Correct configuration if negative value was unintended |
| Boundary threshold outside range | Policy query | Clamp to `0.0..0.49` | Contract test | Correct configuration or accept safe clamp |
| Plan no longer current | New focus/config/catalog revision | Caller discards ephemeral plan | Streamer refresh diagnostics | Replan from current inputs |
Cancellation is not applicable: planning is synchronous, bounded and owns no
external work. A returned stale plan is simply not retained by the planner.
## Configuration and capabilities
| Setting/capability | Default source | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| `lod2_radius_chunks` | Streamer scene/profile | Renderer | Yes at composition/debug boundary | Converts chunk coverage to minimum ADT radius |
| `lod2_tile_radius` | Streamer scene/profile | Renderer | Yes | Minimum visible ADT radius |
| `warm_tile_margin` | Streamer scene/profile | Renderer | Yes | Extends chunk-derived visible radius |
| `prewarm_tile_margin` | Streamer scene/profile | Renderer | Yes | Extends wanted/load radius |
| `retain_tile_margin` | Streamer scene/profile | Renderer | Yes | Extends retained hysteresis radius |
| `boundary_prefetch_threshold` | Streamer scene/profile | Renderer | Yes | Adds adjacent/corner prefetch centers near tile boundaries |
| Editor preview radius | Editor scene | Authoring preview | Yes | Square wanted/retained preview set |
The planner does not select `Blizzlike335` or `Enhanced`; it applies the values
already selected by renderer composition.
## Persistence, cache and migration
- Planner contracts are runtime-only and are not serialized.
- Existing tile keys and cache paths are unchanged.
- No cache/resource/schema version bump or migration is required.
## Diagnostics and observability
- Logs remain emitted by `StreamingWorldLoader` after applying a plan.
- `StreamingTargetPlan` exposes focus/wanted/retained data for deterministic tests.
- Dedicated verifier reports case count, iteration count, total time and average plan time.
- No correlation ID is required for synchronous ephemeral plans.
## Verification
- Unit/contract: `src/tools/verify_streaming_target_planner.gd` covers center
radii, edge/corner cross product, catalog filtering, clamps, editor behavior,
input/result ownership and scene-free dependencies.
- Integration: streamer source gate confirms both runtime/editor delegation and
removal of `_predictive_focus_tiles` from the monolith.
- Fidelity evidence: expected sets encode the pre-extraction formulas; renderer
checkpoint dry-run must retain all seven checkpoint plans.
- Performance budget: 250 synthetic High-like corner plans average at most
`4.0 ms` each on the headless regression runner; measured result is recorded
in test output.
- Manual diagnostics: unchanged `StreamingWorld` focus/wanted/queued log.
## Extension points
- Direction/velocity/frustum priority can extend policy/input only with fixtures
and a compatibility-safe default reproducing the current position-only plan.
- Multi-focus planning may union additional typed centers without giving the
planner ownership of queues or consumers.
- Queue ordering remains a separate scheduler/application responsibility.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Runtime wanted/retained sets | Implemented | Center/corner/catalog fixtures | Add paired traversal performance evidence |
| Boundary prefetch centers | Implemented | Edge/corner cross-product fixture | Direction/velocity prediction not implemented |
| Editor preview set | Implemented | Negative/radius-one fixtures | Editor viewport integration remains existing adapter |
| Scene-free deterministic execution | Implemented | Dependency/source gate and 250-iteration run | Worker execution is unnecessary today |
| Queue priority/LOD | Not in module | Preserved in streamer | Separate extraction packages |
## Known gaps and risks
- Tile-key sets preserve the streamer's internal string representation; a typed
catalog migration is deferred until it can remove, rather than duplicate, that source of truth.
- Planner time scales with the union of square radii/prefetch centers. The
current bounded regression covers High-like corner input but not pathological custom radii.
- Exact p95/p99 frame impact still requires renderer baseline runs with local assets.
- Direction, velocity and camera frustum do not influence target selection yet.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/streaming/streaming_target_planner.gd` | Pure runtime/editor set calculation and boundary centers |
| `src/render/streaming/streaming_target_policy.gd` | Immutable radius/threshold snapshot and effective clamps |
| `src/render/streaming/streaming_target_plan.gd` | Immutable typed focus and read-only result sets |
| `src/scenes/streaming/streaming_world_loader.gd` | Planner composition and plan application to queues/state |
| `src/tools/verify_streaming_target_planner.gd` | Asset-free behavior/dependency/performance regression |
## Related decisions and references
- [`world-renderer.md`](world-renderer.md)
- [`../../targets/03-renderer-facade.md`](../../targets/03-renderer-facade.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
- [`../GODOT_BEST_PRACTICES.md`](../GODOT_BEST_PRACTICES.md)
- [`../CODING_STANDARD.md`](../CODING_STANDARD.md)
@@ -0,0 +1,195 @@
# Terrain Chunk Geometry Queue Planner
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-TERRAIN-CHUNK-QUEUE-001` |
| Owners | Pure chunk geometry create/remove request calculation |
| Last verified | Worktree `work/sindo-main-codex/m03-terrain-chunk-queue`, 2026-07-16 |
| Profiles/capabilities | Existing chunk/tile LOD desired state; all profiles |
## Purpose
Compare loader-owned current chunk geometry with desired chunk/tile LOD state
and produce fresh removal and nearest-first creation request arrays.
## Non-goals
- Retain, drain or bound queues, or own per-frame operation permits.
- Create/remove Mesh, Node, material or RID resources.
- Calculate desired chunk/tile LOD, parse ADT data or schedule worker tasks.
- Change cache versions, renderer profiles or visible fidelity rules.
- Provide a generic queue/operation framework.
## Context and boundaries
```mermaid
flowchart LR
Current[Loader-owned tile/chunk state] --> Planner[TerrainChunkGeometryQueuePlanner]
Desired[Desired chunk and tile LOD state] --> Planner
Focus[GodotWorldPosition] --> Planner
Planner --> Remove[Stable-order remove requests]
Planner --> Create[Nearest-first create requests]
Remove --> Loader[StreamingWorldLoader queues]
Create --> Loader
Loader --> Budget[RenderBudgetScheduler permit]
Budget --> GPU[Existing Mesh/Node/RID side effects]
```
Allowed dependencies are typed Godot position and call-local scalar/container
values. SceneTree, RenderingServer, ResourceLoader, worker APIs, gameplay,
network, editor UI, files and persistent caches are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `TerrainChunkGeometryQueuePlanner.plan(tile_states, focus_position)` | Pure query | Return fresh `create_requests` and `remove_requests` arrays | Synchronous; caller owns result | Null/wrong focus returns two empty arrays |
Script-identity validation uses a preload and therefore works from a cold Godot
global-class cache.
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Tile-state read model with current chunks and desired LODs | Streamer | Planner | Loader-owned; read only | One rebuild |
| Input | Desired tile LOD | Tile LOD calculation | Planner | Scalar in loader state | One rebuild |
| Input | Parsed chunk origins and tile fallback origin | ADT result/loader | Planner | Loader-owned; read only | Map session |
| Input | `GodotWorldPosition` | Streamer focus adapter | Planner | Immutable caller reference | One rebuild |
| Output | Remove `{tile, chunk}` records | Planner | Loader removal queue | Fresh caller array/dictionaries | Until drained/rebuilt |
| Output | Create `{tile, chunk, lod, dist_sq}` records | Planner | Loader creation queue | Fresh nearest-first array/dictionaries | Until drained/rebuilt |
Distance is horizontal XZ squared distance from the chunk origin, not its center.
Y is intentionally ignored. A missing/sparse parsed origin falls back to the
tile state's existing origin exactly as before extraction.
## Data flow
```mermaid
flowchart TD
Start[plan] --> FocusValid{Typed focus?}
FocusValid -->|no| Empty[Return two empty arrays]
FocusValid -->|yes| Tile[Iterate tile state insertion order]
Tile --> TileLod{Desired tile LOD active?}
TileLod -->|yes| RemoveAll[Append every current chunk removal]
TileLod -->|no| Compare[Compare current and desired chunks]
Compare --> RemoveMissing[Append current chunks absent from desired]
Compare --> CreateMismatch[Append LOD-mismatched creates]
Compare --> CreateMissing[Append absent desired creates]
CreateMismatch --> Origin[Parsed chunk origin or tile fallback]
CreateMissing --> Origin
Origin --> Distance[Attach squared XZ distance]
RemoveAll --> Next[Next tile]
RemoveMissing --> Next
Distance --> Next
Next --> Sort[Sort creates nearest-first]
Sort --> Result[Return fresh arrays]
```
## Lifecycle/state
The planner is stateless. Each loader queue rebuild supplies current read models
and receives new arrays. The loader adopts and drains those arrays until another
rebuild replaces them. Planner destruction requires no cleanup or cancellation.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Planner as TerrainChunkGeometryQueuePlanner
participant Budget as RenderBudgetScheduler
participant Render as Existing terrain finalization
Loader->>Planner: plan(tile states, typed focus)
Planner-->>Loader: fresh create/remove arrays
Loader->>Budget: request shared chunk_geometry permit
Budget-->>Loader: permit
Loader->>Render: pop_back removal first
Loader->>Budget: request shared chunk_geometry permit
Budget-->>Loader: permit
Loader->>Render: pop_front nearest creation
```
## Ownership, threading and resources
- The planner owns only call-local arrays and dictionaries until return.
- The loader owns adopted queue storage, drain ordering, tile/chunk state and all
Mesh/Node/material/RID side effects.
- Calls and drains currently run on the renderer main thread. The planner has no
lock, task, retained resource or concurrent mutation.
- Removal and creation continue to share the existing `chunk_geometry` budget lane.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Null/wrong focus script | Identity guard | Return empty request arrays | Contract verifier | Supply typed focus and rebuild |
| Sparse/out-of-range chunk data | Origin resolver | Use tile fallback origin | Synthetic fixture | Normal ADT completion restores precise origin |
| Tile LOD becomes desired | Desired tile LOD check | Remove every current chunk; no chunk creates | Queue metrics | Existing tile-LOD create path takes over |
| Queue rebuild before drain | Loader lifecycle | Replace old desired-operation arrays | Existing queue metrics | Latest state wins |
| Shutdown/map reset | Loader lifecycle | Loader clears queues; planner has nothing to cancel | Shutdown verifier | New session replans |
## Configuration and capabilities
The planner introduces no settings. It consumes already-calculated desired LOD
state. Operation limits, profile defaults and removal-first drain order remain
loader/scheduler configuration.
## Persistence, cache and migration
No serialization, cache entry or format changes. Baked terrain, control-splat
and splat caches require no rebuild or migration.
## Diagnostics and observability
The planner emits no logs. Existing queue metrics observe the adopted arrays.
The contract verifier reports semantic cases and bounded multi-tile timing.
Asset-backed p95/p99 remains milestone acceptance work.
## Verification
- `verify_terrain_chunk_geometry_queue_planner.gd`: missing/mismatched/unchanged
chunks, tile-LOD removal, sparse origin fallback, XZ-only distance, stable
removals, nearest creates, invalid focus, loader boundary and bounded timing.
- Existing LOD planner, terrain cache, facade, internal-access, manifest,
shutdown, scheduler, streaming and coordinate regressions remain required.
- Fidelity evidence is exact branch/request/order preservation only. No new
build-12340 visual parity claim is made.
## Extension points
- Extract tile-LOD operation planning as its own bounded contract if justified.
- Add explicit typed tile-state adapters only when more than one consumer exists.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Pure chunk create/remove planning | Implemented extraction | Asset-free semantic/source/timing verifier | Asset-backed p95/p99 pending |
| Queue execution/resource ownership | Remains in loader | Drain source boundary and scheduler regression | Further terrain decomposition required |
## Known gaps and risks
- Raw tile/chunk dictionaries remain a transitional ADT adapter boundary.
- The planner performs a second linear tile-state pass beside tile-LOD planning;
bounded synthetic timing is green, but asset-backed frame metrics are pending.
- Equal-distance create ordering retains comparator behavior without a tie-breaker.
- Terrain geometry finalization and state mutation remain monolithic.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/terrain/terrain_chunk_geometry_queue_planner.gd` | Pure request calculation and nearest-first creation order |
| `src/scenes/streaming/streaming_world_loader.gd` | Queue adoption, tile-LOD planning, budgets and render side effects |
| `src/tools/verify_terrain_chunk_geometry_queue_planner.gd` | Semantic, boundary and timing regression |
## Related decisions and references
- [`terrain-chunk-lod-planner.md`](terrain-chunk-lod-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)
+179
View File
@@ -0,0 +1,179 @@
# Terrain Chunk LOD Planner
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-TERRAIN-LOD-PLANNER-001` |
| Owners | Pure desired chunk-LOD calculation only |
| Last verified | Worktree `work/sindo-main-codex/m03-terrain-chunk-lod-planner`, 2026-07-16 |
| Profiles/capabilities | Existing Performance/Balanced/High/Custom radii |
## Purpose
Calculate the desired terrain LOD for each populated parsed ADT chunk from one
typed Godot-space focus and one immutable renderer-policy snapshot.
## Non-goals
- Own tile/chunk state, queues, budgets, tasks, Mesh, Node, material or RID.
- Select tile LOD, create/remove geometry or apply a completed plan.
- Normalize custom radii, change profile defaults or claim visual parity.
- Parse ADT data or convert between WoW and Godot coordinate spaces.
## Context and boundaries
```mermaid
flowchart LR
Focus[GodotWorldPosition] --> Planner[TerrainChunkLodPlanner]
Chunks[Parsed ADT chunk records] --> Planner
Config[Streamer LOD configuration] --> Policy[TerrainChunkLodPolicy]
Policy --> Planner
Planner --> Desired[chunk index to LOD 0/1/2]
Desired --> Loader[StreamingWorldLoader state adapter]
Loader --> Queue[Existing create/remove queues and budgets]
```
Allowed dependencies are the typed Godot position, immutable policy and
call-local Array/Dictionary/Vector3 values. SceneTree, RenderingServer, gameplay,
network, files, tasks and persistent caches are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `TerrainChunkLodPolicy.new(...)` | Immutable value | Snapshot enabled flag, three raw radii and chunk size | Caller-owned; one planning refresh | Values are preserved without normalization |
| `TerrainChunkLodPlanner.plan(chunks, focus_position, policy)` | Pure query | Map populated source indices to LOD 0/1/2 | Synchronous; result caller-owned | Invalid/null typed inputs or disabled policy return empty |
The planner validates scripts by preloaded identity so it works from a cold
Godot global-class cache.
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Parsed chunk array with `origin: Vector3` | ADT load result | Planner | Loader-owned; read only | One call |
| Input | `GodotWorldPosition` | Streamer focus adapter | Planner | Immutable caller reference | One call |
| Input | `TerrainChunkLodPolicy` | Streamer exported configuration | Planner | Immutable caller reference | One call |
| Output | `{source_chunk_index: 0/1/2}` | Planner | Loader desired state | Fresh caller dictionary | One call |
The Y coordinate is intentionally ignored. Empty chunk records and chunks beyond
the third radius are absent from the result.
## Data flow
```mermaid
flowchart TD
Start[plan] --> Valid{Typed inputs and enabled?}
Valid -->|no| Empty[Return empty dictionary]
Valid -->|yes| Radii[Square raw radius times chunk size]
Radii --> Iterate[Visit source chunk indices in order]
Iterate --> Populated{Record populated?}
Populated -->|no| Iterate
Populated -->|yes| Center[origin XZ plus half chunk]
Center --> Distance[Squared horizontal distance]
Distance --> Threshold{First inclusive threshold}
Threshold -->|LOD0/1/2| Record[Record source index]
Threshold -->|outside| Iterate
Record --> Iterate
Iterate --> Done[Return desired mapping]
```
## Lifecycle/state
The planner is stateless. A policy is constructed for one refresh, read during
synchronous planning and released. The returned dictionary is then owned by the
loader state. There is no cancellation, recovery state or retained resource.
## Main sequence
```mermaid
sequenceDiagram
participant Stream as StreamingWorldLoader
participant Policy as TerrainChunkLodPolicy
participant Planner as TerrainChunkLodPlanner
Stream->>Policy: snapshot enabled/radii/chunk size
Stream->>Planner: plan(chunks, typed focus, policy)
Planner-->>Stream: fresh desired LOD dictionary
Stream->>Stream: existing tile state, queue and budget application
```
## Ownership, threading and resources
- Planner and policy own no engine resources, nodes, RIDs, tasks or caches.
- The loader retains parsed chunks, desired state and every application side effect.
- Current calls run synchronously on the renderer main thread; the pure inputs
also make isolated contract testing possible without a world scene.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Null/wrong focus or policy script | Identity guard | Empty plan | Contract verifier | Supply typed values next refresh |
| Disabled chunk LOD | Policy flag | Empty plan; existing tile-LOD fallback applies | Existing renderer state | Enable profile option |
| Empty chunk record | Record guard | Preserve sparse source index; omit record | None | Not an error |
| Custom negative radius | No normalization | Existing squared-radius behavior is retained | Regression fixture | Change caller configuration if undesired |
There is no asynchronous work or cancellation token.
## Configuration and capabilities
The policy snapshots `use_chunk_terrain_lods`, `lod0_radius_chunks`,
`lod1_radius_chunks`, `lod2_radius_chunks` and the existing `CHUNK_SIZE`. Inclusive
squared thresholds and raw custom values deliberately preserve prior behavior.
## Persistence, cache and migration
No serialization or cache format changes. Existing baked terrain, control-splat
and splat cache versions remain unchanged and require no rebuild.
## Diagnostics and observability
The service emits no logs. The contract verifier reports semantic cases and a
bounded synthetic timing. Asset-backed p95/p99 remains milestone acceptance work.
## Verification
- `verify_terrain_chunk_lod_planner.gd`: center and vertical invariance,
inclusive thresholds, sparse/outside chunks, disabled/custom-negative policy,
loader delegation and 250-by-256 bounded planning.
- Renderer cache, facade, internal-access, baseline, scheduler, target-planner,
focus, ground-query and coordinate-boundary regressions remain required.
- Fidelity evidence is formula preservation only; no new build-12340 visual
equivalence claim is made.
## Extension points
- Add measured direction/velocity only through an explicit policy/contract change.
- Keep queue prioritization and LOD application as separately bounded services.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Pure chunk desired-LOD calculation | Implemented extraction | Asset-free semantic/source-boundary contract | Asset-backed p95/p99 pending |
| Queue/state/finalization ownership | Remains in loader | Source-boundary regression | Further terrain decomposition required |
## Known gaps and risks
- Raw negative and non-monotonic custom radii retain historical squared/ordered
semantics, even though such settings may be surprising.
- The planner is linear in parsed chunk count and allocates one result dictionary.
- Terrain build, quality upgrades and geometry lifecycle remain monolithic.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/terrain/terrain_chunk_lod_policy.gd` | Immutable configuration snapshot |
| `src/render/terrain/terrain_chunk_lod_planner.gd` | Pure horizontal-distance plan |
| `src/scenes/streaming/streaming_world_loader.gd` | Composition and plan application adapter |
| `src/tools/verify_terrain_chunk_lod_planner.gd` | Contract, dependency and timing regression |
## Related decisions and references
- [`world-renderer.md`](world-renderer.md)
- [`terrain-quality-mesh-cache.md`](terrain-quality-mesh-cache.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+198
View File
@@ -0,0 +1,198 @@
# Terrain Quality Mesh Cache
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-TERRAIN-CACHE-SERVICE-001` |
| Owners | Full-quality terrain revisit Mesh retention and LRU policy |
| Last verified | Worktree `work/sindo-main-codex/m03-terrain-quality-cache`, 2026-07-16 |
| Profiles/capabilities | Performance/Balanced/High/Custom capacity; session-memory only |
## Purpose
Retain eligible full-quality terrain Mesh references after ADT tile state unload,
so a revisit can restore quality without flashing to a lower-quality fallback,
while bounding retained references with the existing least-recently-used policy.
## Non-goals
- Parse/build terrain, own tile state or schedule quality-upgrade tasks.
- Create/finalize Nodes or RIDs, mutate materials or choose terrain LOD.
- Persist cache entries or change baked/control-splat/splat format versions.
- Cache control-splat/splat sources whose active radius has separate ownership.
- Provide a generic cache framework or change profile capacity defaults.
## Context and boundaries
```mermaid
flowchart LR
Upgrade[Full-quality baked upgrade result] --> Loader[StreamingWorldLoader adapter]
Loader --> Cache[TerrainQualityMeshCache]
Cache -->|retained Mesh reference| Revisit[Reloaded tile state]
Revisit --> Finalize[Existing tile LOD/finalization path]
Splat[Control/splat quality sources] -. rejected .-> Cache
```
Allowed dependencies: `Mesh`, Dictionary/Array scalar containers and the loader
adapter. Forbidden dependencies: Node/SceneTree/RID, tasks/mutexes, ADT parsing,
gameplay/network/editor UI, files and persistent Resource cache ownership.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `TerrainQualityMeshCache.store(tile_key, mesh, source, capacity)` | Command | Admit/touch a Mesh and trim oldest entries | Owning renderer thread/session | Returns false for rejected input or immediate zero-capacity eviction |
| `TerrainQualityMeshCache.restore(tile_key)` | Query/touch | Return a fresh Mesh/source record and mark newest | Owning renderer thread | Empty on miss; invalid record is removed |
| `TerrainQualityMeshCache.clear()` | Lifecycle command | Release all retained Mesh references and LRU keys | Reset/shutdown | Idempotent |
| `TerrainQualityMeshCache.diagnostic_snapshot()` | Read model | Detached count and oldest-to-newest key/source list | Caller-owned result | Never exposes Mesh references |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Tile key | Streamer tile catalog/state | Cache index | Copied string | Map session |
| Input | Full-quality Mesh | Baked upgrade/fallback path | Cache | Retained shared reference | Until eviction/clear |
| Input | Quality source | Streamer quality adapter | Admission/restore | Copied string | Entry lifetime |
| Input | Current capacity | Active renderer profile | Store trim | Scalar copy | One store |
| Output | Fresh `{mesh, source}` record | Cache | Loader state adapter | Caller dictionary; shared Mesh reference | One restore |
| Output | Detached diagnostics | Cache | Tests/future metrics | Caller-owned scalars | One query |
Side effects are limited to retained reference and LRU container mutation. There
is no scene, GPU, task, file, database, network or persistent-cache mutation.
## Data flow
```mermaid
flowchart TD
Store[store tile/mesh/source/capacity] --> Admit{Valid key + Mesh + source?}
Admit -->|no| Reject[Return false]
Admit -->|yes| Touch[Insert/replace and touch newest]
Touch --> Trim[Evict oldest until count <= capacity]
Restore[restore tile] --> Hit{Valid admitted entry?}
Hit -->|no| Miss[Remove invalid / return empty]
Hit -->|yes| TouchRestore[Touch newest and return fresh record]
Clear[world reset/shutdown] --> Empty[Release all references and keys]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Empty
Empty --> Populated: admitted store with capacity > 0
Populated --> Populated: store / restore touch / bounded eviction
Populated --> Empty: capacity zero store evicts all
Populated --> Empty: clear on reset/shutdown
Empty --> Empty: rejected store / miss / clear
Empty --> [*]
Populated --> [*]: owner releases cache
```
## Main sequence
```mermaid
sequenceDiagram
participant Upgrade as Terrain upgrade result
participant Loader as StreamingWorldLoader
participant Cache as TerrainQualityMeshCache
Upgrade->>Loader: full Mesh + baked_full source
Loader->>Cache: store(key, mesh, source, profile capacity)
Note over Loader,Cache: tile state may later unload
Loader->>Cache: restore(key) on tile creation/revisit
Cache-->>Loader: fresh record or empty
Loader->>Loader: apply Mesh/source to existing tile state
Loader->>Cache: clear() on world reset/shutdown
```
## Ownership, threading and resources
- The cache owns its dictionary, ordered tile keys and retained Mesh references.
- The loader retains tile state, async tasks/results, quality selection and all
Node/RID/material finalization.
- Returned records are new dictionaries; their Mesh is a shared reference.
- Calls currently occur on the main renderer thread. The service has no lock and
must not be mutated concurrently.
- `clear` deterministically drops references during the existing world reset path.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty key/null Mesh | Store guard | Reject unchanged | Boolean/test | Fix caller result |
| Splat/control source | Admission list | Reject unchanged | Boolean/test | Keep source in active-radius ownership |
| Zero/negative capacity | Clamp and trim | Entry immediately evicted; cache empty | Snapshot/count | Increase profile capacity |
| Miss/invalid entry | Restore validation | Remove invalid and return empty | Empty record | Existing loader fallback/rebuild |
| World reset/shutdown | Loader lifecycle | Clear every reference/key | Shutdown regression | Refill during next session |
There is no asynchronous work or cancellation token inside this service.
## Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| `terrain_quality_mesh_cache_limit` | Existing loader value | `0/24/48` in current presets | Read on every store | Maximum retained entries |
| Admitted source | Non-splat (currently `baked_full`) | All | No hidden override | Revisit retention |
| Excluded sources | `control_splat_cache`, `splat_cache`, `splat` | All | No | Preserve active quality ownership |
## Persistence, cache and migration
- This is an in-memory reference cache, not a serialized cache format.
- `REQUIRED_BAKED_TILE_FORMAT_VERSION=5`, control splat v3 and splat v1 remain
loader/resource validation contracts and are unchanged.
- No migration or rebuild is required. Entry keys retain the existing tile-key form.
## Diagnostics and observability
- Detached diagnostics report count and oldest-to-newest tile key/source entries.
- Mesh, material, Node and RID references are deliberately absent.
- Performance is bounded dictionary lookup plus one ordered-array erase/append;
no new optimization or scale claim is made.
## Verification
- `verify_terrain_quality_mesh_cache.gd`: admission, exclusions, Mesh identity,
source, LRU touch/eviction, detached diagnostics, negative capacity and clear.
- Loader source boundary proves cache composition/delegation and removal of its
former LRU array/touch/trim implementation.
- Existing shutdown, facade, internal-access, manifest, planner and scheduler
regressions remain required.
- Fidelity: source rules, capacities and three loader adapter sites are unchanged;
no visible terrain or build-12340 parity change is claimed.
## Extension points
- Expose scalar cache counts through facade metrics when needed.
- Move additional terrain responsibilities only as separate bounded services.
- Replace ordered-array LRU only after measured scale evidence.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Bounded quality Mesh retention | Implemented extraction | Contract/LRU fixtures | Asset-backed memory/revisit timing pending |
| Deterministic reset ownership | Implemented | Clear path plus shutdown regression | Full terrain service still monolithic |
| Persistent terrain cache formats | Unchanged | Loader version constants | Separate acceptance audit remains |
## Known gaps and risks
- Ordered-array touch is linear in entry count, matching prior behavior.
- Mesh memory size is not measured; capacity remains entry-count based.
- Only revisit Mesh ownership moved; build/tasks/state/finalization remain loader-owned.
- Asset-backed flash/revisit timing and p95/p99 evidence remain pending.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/terrain/terrain_quality_mesh_cache.gd` | Session cache entries, LRU and admission |
| `src/scenes/streaming/streaming_world_loader.gd` | Store/restore state adapter and reset lifecycle |
| `src/tools/verify_terrain_quality_mesh_cache.gd` | Scene-free cache and loader-boundary regression |
## Related decisions and references
- [`world-renderer.md`](world-renderer.md)
- [`terrain-query.md`](terrain-query.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+11 -3
View File
@@ -5,10 +5,10 @@
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M02 / M02-GMP-TERRAIN-QUERY-001 |
| Target/work package | M02 / M02-GMP-TERRAIN-QUERY-001; M03 renderer consumer `M03-RND-FACADE-GROUND-QUERY-001` |
| Owners | Gameplay terrain-query contract; ADT adapter owns parsed tile cache |
| Last verified | `a45d521`, 2026-07-14 |
| Profiles/capabilities | Current sandbox ADT ground height; renderer/physics backend planned |
| Profiles/capabilities | Current sandbox ADT ground height; loaded-render-mesh diagnostic query; authoritative physics backend planned |
## Purpose
@@ -47,6 +47,8 @@ Allowed dependencies:
- Existing native `ADTLoader` at the ADT adapter boundary.
- Dynamic dictionaries only inside the native-format adapter.
- Player scene composition may inject any `TerrainQuery` implementation.
- Renderer diagnostics reuse `GodotWorldPosition` through `WorldRenderFacade`
but return renderer-owned `RenderedGroundSample`, preserving the render/gameplay boundary.
Forbidden dependencies:
@@ -65,6 +67,7 @@ Forbidden dependencies:
| `AdtTerrainQuery.new(extracted_directory, map_directory_name, loader_override)` | Adapter constructor | Configures one map and optional test/data-source loader | Query lifetime | Production validates native loader and file presence lazily |
| `AdtTerrainQuery.sample_ground_height(position)` | Adapter method | Loads/caches tile and interpolates ADT outer heights | Main thread; cache lives with query | Returns stable failure code |
| `ThirdPersonWowController.set_terrain_query(query)` | Composition method | Replaces terrain backend independently of movement/presentation | Call before scene ready or on main thread | Null is rejected and current query retained |
| `WorldRenderFacade.sample_ground_height(position)` | Separate renderer diagnostic API | Samples already loaded terrain Mesh and returns `RenderedGroundSample` without exposing streamer state | Main thread; immutable sample | Stable `render_terrain_*` unavailable code; not injected into player by default |
## Inputs and outputs
@@ -206,6 +209,9 @@ Existing ADT/native/cache format versions remain unchanged.
## Extension points
- Streaming renderer or physics backend may implement `TerrainQuery` without changing player/movement.
- The current facade intentionally uses a separate renderer-owned result and is
not a `TerrainQuery` implementation. A future adapter must explicitly translate
readiness/results and define gameplay authority before composition.
- Future result may grow separate slope, surface, liquid or collision contracts;
incompatible additions require a versioned contract rather than Dictionary fields.
- Test/authoring data sources may use constructor loader override.
@@ -218,7 +224,8 @@ Existing ADT/native/cache format versions remain unchanged.
| Typed replaceable ground-height query | Implemented | Contract and injected player regression | Integrate target checklist after review |
| Existing ADT height interpolation | Implemented | Synthetic fractional-grid fixture | Compare terrain contact with build 12340 |
| Per-tile success/failure cache | Implemented | Loader call-count regression | Add explicit invalidation only with authoring/hot reload |
| Renderer/streaming cache backend | Planned | Boundary permits replacement | M03 renderer facade/public terrain backend |
| Renderer loaded-mesh diagnostic backend | Implemented | M03 facade typed sample and detached snapshot contract | Not composed into gameplay; triangle Mesh ray is diagnostic only |
| Authoritative renderer/physics backend | Planned | Boundary permits replacement | Define holes/slopes/collision/readiness semantics before gameplay composition |
| Holes/slopes/collision | Planned | Outside height-only contract | Later movement/physics package |
| Liquid/swim query | Planned | Outside contract | M09/M12 world gameplay |
@@ -240,6 +247,7 @@ Existing ADT/native/cache format versions remain unchanged.
| `src/gameplay/terrain/terrain_query.gd` | Replaceable typed query boundary |
| `src/gameplay/terrain/adt_terrain_query.gd` | Native ADT adapter, cache, chunk selection and interpolation |
| `src/scenes/player/third_person_wow_controller.gd` | Query composition and existing ground-snap consumer |
| `src/render/world_render_facade.gd` | Read-only loaded-render-mesh diagnostic consumer of typed position/sample contracts |
| `src/tools/verify_terrain_query.gd` | Synthetic contract/cache/failure/injection regression |
| `src/tests/scenes/player_input_regression.tscn` | Asset-free player composition fixture |
+207
View File
@@ -0,0 +1,207 @@
# WMO Placement Registry
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-WMO-PLACEMENT-REGISTRY-001` |
| Owners | WMO placement-key to tile/global reference-set ownership |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-placement-registry`, 2026-07-17 |
| Profiles/capabilities | Existing ADT/WDT WMO placement paths; profile-independent |
## Purpose
Own the reference sets that keep one resolved WMO placement active across one
or more ADT tiles, or across the global WDT placement lifetime. This separates
value-only placement ownership from scene and render-resource ownership.
## Non-goals
- Resolve WMO paths, placement identity or world transforms.
- Own Nodes, Resources, RIDs, roots, jobs, queues, tasks or caches.
- Load, instantiate, build or free WMO content.
- Change duplicate order, budgets, retries or global-WMO behavior.
- Claim original-client WMO visual, portal, room or material parity.
## Context and boundaries
```mermaid
flowchart LR
Parsed[ADT/WDT placement] --> Resolver[WmoPlacementResolver]
Resolver --> Key[Placement key]
Tile[Tile/global reference key] --> Loader[StreamingWorldLoader]
Key --> Loader
Loader --> Registry[WmoPlacementRegistry]
Registry --> Decision[Existing or final-release decision]
Decision --> Loader
Loader --> NodeMap[Loader key-to-Node map]
Loader --> Jobs[Loader jobs/queues/resources]
```
The registry depends only on `RefCounted`, `Dictionary`, `Array` and `String`.
SceneTree, Node, Resource, RID, files, loaders, threads, locks, network,
gameplay and editor UI are forbidden dependencies.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `add_reference(placement_key, reference_key)` | Command/query | Idempotently add a reference; return true only when creating the placement key | Renderer main thread; map session | Empty key returns false and changes nothing |
| `release_reference(placement_key, reference_key)` | Command/query | Remove an owned reference; return true only on final removal | Renderer main thread; map session | Empty, unknown or non-owner input returns false |
| `contains(placement_key)` | Query | Test whether any reference keeps a placement active | Renderer main thread; map session | Empty key returns false |
| `active_count()` | Query | Count active placement keys, not references | Renderer main thread; map session | None |
| `diagnostic_snapshot()` | Query | Return detached, placement/reference-sorted records | Renderer main thread; caller-owned result | None |
| `clear()` | Command | Remove all ownership records idempotently | Renderer main thread; reset/shutdown | None |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Resolved `uid:*`, `tile:*:*` or `global` placement key | `WmoPlacementResolver` / loader global adapter | Registry | Copied String dictionary key | Until final release/clear |
| Input | ADT tile key or `__global__` reference key | Loader | Registry | Copied String set key | Until release/clear |
| Output | First-placement Boolean | Registry | Loader creation path | Value | One call |
| Output | Final-release Boolean | Registry | Loader cancellation/free path | Value | One call |
| Output | Active count and sorted diagnostic records | Registry | Renderer diagnostics/verifier | Detached values | Caller lifetime |
## Data flow
```mermaid
flowchart TD
Add[add placement + reference] --> Valid{Both non-empty?}
Valid -->|no| Reject[false; unchanged]
Valid -->|yes| Existing{Placement exists?}
Existing -->|no| Create[Create reference set; true]
Existing -->|yes| Retain[Idempotently add reference; false]
Release[release placement + reference] --> Owner{Known owner reference?}
Owner -->|no| Reject
Owner -->|yes| Erase[Erase reference]
Erase --> Empty{Set empty?}
Empty -->|no| Retained[false; placement remains]
Empty -->|yes| Final[Erase placement; true]
```
## Lifecycle and state
The states for one placement are `Absent`, `Referenced` and back to `Absent`.
The first valid add enters `Referenced`; further tile/global adds remain there.
Only release of the final owned reference returns to `Absent`. Map reset and
shutdown call `clear`, which is safe from either state.
```mermaid
stateDiagram-v2
[*] --> Absent
Absent --> Referenced: first valid add / true
Referenced --> Referenced: idempotent/shared add / false
Referenced --> Referenced: non-owner/non-final release / false
Referenced --> Absent: final owned release / true
Referenced --> Absent: clear
```
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Registry as WmoPlacementRegistry
participant Nodes as Loader Node map
Loader->>Registry: contains(placement key)
alt existing placement
Loader->>Registry: add_reference(key, tile)
else new placement built successfully
Loader->>Nodes: store key-to-Node
Loader->>Registry: add_reference(key, tile/global)
end
Loader->>Registry: release_reference(key, tile)
alt final reference
Registry-->>Loader: true
Loader->>Loader: cancel render build
Loader->>Nodes: queue-free and erase Node
else shared/unknown reference
Registry-->>Loader: false
end
```
## Ownership, threading and resources
- The registry owns only placement-key Dictionaries and reference-key sets.
- The loader owns the parallel placement-key-to-Node map and every Node action.
- The loader also owns WMO roots, tile state, caches, requests, jobs, queues,
retries, cancellation and render-resource lifetime.
- Current calls are serialized on the renderer main thread. The registry has no
lock and must not be mutated concurrently.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty placement/reference key | API guard | Reject without mutation | Contract verifier | Repair caller input |
| Duplicate add | Existing set member | Idempotent retain | Sorted snapshot | No recovery required |
| Unknown/non-owner release | Missing placement/reference | Ignore without mutation | Contract verifier | Release from actual owner or reset |
| Tile build cancellation | Loader-owned condition | Loader cancels job, then releases recorded refs | Existing queue metrics | Retry when tile becomes wanted |
| Final release | Empty set after erase | Return true and erase registry record | Active count | Loader cancels/free Node exactly once |
| Map reset/shutdown | Loader lifecycle | Loader frees Nodes/jobs and clears registry | Shutdown regression | Next map begins empty |
## Configuration and capabilities
There is no setting, profile branch or capacity knob. WMO tile radius, cache
limits, render budgets and visibility settings remain loader-owned.
## Persistence, cache and migration
No data is serialized and no cache/schema/version changes. Placement and
reference keys exist only for the current renderer map session.
## Diagnostics and observability
`active_count` backs existing WMO instance metrics. `diagnostic_snapshot`
returns deterministic detached records for tests and future read-only
diagnostics; mutating the returned arrays cannot alter registry state.
## Verification
- `verify_wmo_placement_registry.gd`: invalid/first/idempotent/shared/non-owner/
final/global/clear behavior, deterministic detached diagnostics, source
ownership boundaries and bounded 100-by-256 shared-reference cycles.
- WMO resolver, material, runtime-cache shutdown and the renderer regression
suite verify the unchanged loader adapters and surrounding lifecycle.
- Fidelity evidence is exact preservation of the repository's prior shared-ref
and final-release semantics. Asset-backed 3.3.5a comparison is still pending.
## Extension points
- WMO build/resource state can be extracted separately without moving Nodes
into this registry.
- A future diagnostic adapter may expose the detached snapshot through the
facade without exposing mutable loader internals.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| WMO placement reference ownership | Implemented extraction | Contract/source/timing verifier | Asset-backed cross-tile corpus pending |
| WMO build/resource scheduling | Remains in loader | Existing runtime regressions | Separate state/cancellation package |
| WMO visual/portal/material fidelity | Partial | Renderer baseline/material checks | Original-client paired evidence pending |
## Known gaps and risks
- Correctness assumes the loader records each successful placement reference in
the tile `wmo_refs` array before unregister.
- Old caches without positive UID retain resolver-defined per-tile identity.
- No proprietary WMO corpus or original-client paired capture is in this package.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_placement_registry.gd` | Placement-key/reference-set ownership |
| `src/render/wmo/wmo_placement_resolver.gd` | Stable path, identity and transform rules |
| `src/scenes/streaming/streaming_world_loader.gd` | Node map, instances, resources, jobs, queues and lifecycle reactions |
| `src/tools/verify_wmo_placement_registry.gd` | Contract, boundary and timing regression |
## Related decisions and references
- [`wmo-placement-resolver.md`](wmo-placement-resolver.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+192
View File
@@ -0,0 +1,192 @@
# WMO Placement Resolver
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-WMO-PLACEMENT-RESOLVER-001` |
| Owners | Pure WMO cache-key, placement-identity and world-transform rules |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-placement-resolver`, 2026-07-17 |
| Profiles/capabilities | Existing ADT/WDT WMO placement paths |
## Purpose
Provide one scene-free boundary for normalized WMO cache keys, MODF placement
identity and world-space Godot transforms across render-cache, cached-scene and
live-prototype instance paths.
## Non-goals
- Parse ADT/WDT or convert canonical WoW coordinates.
- Own WMO registry references, tile state, jobs, queues or retries; reference
ownership belongs to `WmoPlacementRegistry`.
- Load/validate cached scenes or lightweight render resources.
- Create Nodes, Mesh/MultiMesh, materials or RIDs.
- Implement portals, rooms, visibility, materials, doodads or WMO parity.
## Context and boundaries
```mermaid
flowchart LR
Parsed[ADT/WDT WMO placement] --> Loader[StreamingWorldLoader adapter]
Loader --> Resolver[WmoPlacementResolver]
Resolver --> CacheKey[Normalized cache key]
Resolver --> Identity[Registry unique key]
Resolver --> Transform[World Transform3D]
CacheKey --> Cache[Loader WMO caches/requests]
Identity --> Registry[WmoPlacementRegistry]
Transform --> RenderRoot[Lightweight render root]
Transform --> Scene[Cached scene instance]
Transform --> Live[Live prototype instance]
```
Allowed dependencies are Dictionary/String values and Godot `Vector3`, `Basis`
and `Transform3D` math. Node, SceneTree, RenderingServer, ResourceLoader,
WorkerThreadPool, mutexes, files, gameplay, network and editor UI are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `normalize_relative_path(relative_wmo_path)` | Pure query | Return lowercase slash-normalized cache/lookup key | Any thread; value result | Empty input returns empty |
| `resolve_unique_key(placement, tile_key, placement_index)` | Pure query | Return positive UID identity or tile/index fallback | Any thread; value result | Missing/non-positive UID uses fallback |
| `resolve_world_transform(placement)` | Pure query | Compose world-space position, Godot Euler rotation and uniform scale | Any thread; value result | Missing fields use zero/zero/one defaults; scale is not clamped |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Relative WMO path | WDT/ADT name table | Path normalizer | Caller String | One call |
| Input | Raw placement Dictionary | WDT global or ADT MODF parser result | Identity/transform queries | Read-only shallow value | One call |
| Input | Tile key and placement index | Loader build job | Synthetic identity fallback | Copied scalar/String | Registry entry lifetime |
| Output | Normalized relative path | Resolver | Render/scene cache and load-request maps | New String value | Request/cache lookup |
| Output | `uid:*` or `tile:*:*` key | Resolver | `WmoPlacementRegistry` and loader ref arrays | New String value | Until unregister/reset |
| Output | World `Transform3D` | Resolver | Three WMO instance adapters | Value copy | Instance lifetime after assignment |
The resolver retains no source Dictionary, output or engine resource.
## Data flow
```mermaid
flowchart TD
Path[relative path] --> Slash[replace backslash with slash]
Slash --> Lower[lowercase entire key]
Placement[placement Dictionary] --> UID{unique_id > 0?}
UID -->|yes| UidKey[uid decimal]
UID -->|no| TileKey[tile key + placement index]
Placement --> Defaults[position/rotation/scale defaults]
Defaults --> Basis[Godot Basis.from_euler]
Basis --> Scale[unclamped uniform scale]
Scale --> World[world Transform3D]
```
## Lifecycle/state
The resolver is stateless. Construction, world reset, map switch, cancellation
and shutdown require no resolver operation.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Resolver as WmoPlacementResolver
participant Registry as WmoPlacementRegistry
participant Instance as Render/cached/live instance
Loader->>Resolver: normalize_relative_path(path)
Resolver-->>Loader: cache key
Loader->>Resolver: resolve_unique_key(placement, tile, index)
Resolver-->>Registry: identity adopted by loader
Loader->>Resolver: resolve_world_transform(placement)
Resolver-->>Loader: value Transform3D
Loader->>Instance: assign transform and attach/build
```
## Ownership, threading and resources
- The resolver owns only call-local scalar/value calculations.
- `WmoPlacementRegistry` owns placement-key reference sets. The loader owns its
key-to-Node map, cache/load-request state, jobs/queues, resource fallback and
cancellation.
- The loader and builders own every Node/Mesh/MultiMesh/material/RID lifecycle.
- Pure calls are thread-safe; current consumers execute on the main thread.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty path | Empty String | Empty normalized key | Contract fixture | Loader cache path rejects/degrades |
| Missing/non-positive UID | Integer rule | Tile/index key prevents same-tile collisions | Contract fixture | Rebake old ADT cache to restore cross-tile UID dedupe |
| Missing transform fields | Dictionary defaults | Zero position/rotation; scale one | Contract fixture | Repair parser/cache input |
| Zero/negative scale | No historical clamp | Preserve zero/mirrored basis | Contract fixture | Preserve source behavior |
| Resource missing/pending | Outside resolver | Loader retry/fallback path unchanged | Existing loader diagnostics | Restore cache/raw asset |
| Tile unregister/reset | Outside resolver | Loader releases refs/nodes/jobs | Shutdown/registry regressions | New placements resolve afresh |
## Configuration and capabilities
The resolver introduces no setting or profile branch. WMO radii, visibility,
shadow/occlusion flags, cache size limits and build budgets remain loader-owned.
## Persistence, cache and migration
No format is written. Existing WMO `.tscn`/lightweight render cache versions and
ADT/WDT formats are unchanged. Older placement caches without positive UID retain
the existing per-tile fallback and require no migration for this extraction.
## Diagnostics and observability
The resolver emits no logs. Its verifier records path/identity/transform/source
contracts and bounded 20,000-triple timing. WMO queue/cache/instance metrics remain
in the facade/loader diagnostic snapshot.
## Verification
- `verify_wmo_placement_resolver.gd`: slash/case/empty path, positive UID,
missing/zero/negative UID fallback, default and rotated/scaled transform,
unclamped scale, historical Node3D property equivalence, stateless results,
all loader adapters, dependency boundary
and bounded timing.
- Existing WMO material/cache/shutdown and renderer baseline regressions remain
required alongside M2/terrain/facade/internal-access/coordinate gates.
- Fidelity evidence is exact extraction of current placement rules. No new
original-client WMO visual/portal/material parity claim is made.
## Extension points
- `WmoPlacementRegistry` consumes these stable keys; later packages may extract
WMO build/resource state without changing the resolver.
- Render-group build cursor and resource-selection state require separate
lifecycle/cancellation contracts.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Consistent WMO placement identity/transform | Implemented extraction | Contract/source/timing verifier | Asset-backed placement comparison pending |
| WMO placement registry | Implemented extraction | Ownership/source/timing verifier | Asset-backed cross-tile corpus pending |
| WMO build services | Remains in loader | Existing runtime regressions | Stateful extraction pending |
| Portals/rooms/material fidelity | Partial | M00 checkpoint/material evidence | Broader WMO implementation pending |
## Known gaps and risks
- Raw placement Dictionaries remain the parser/cache boundary.
- Old caches without UID cannot dedupe one WMO across ADT tile boundaries.
- WMO scale remains intentionally unclamped, including zero/negative input.
- Asset-backed visual/p95/p99 evidence is unavailable in this package.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_placement_resolver.gd` | Path, identity and transform rules |
| `src/render/wmo/wmo_placement_registry.gd` | Placement reference-set ownership |
| `src/scenes/streaming/streaming_world_loader.gd` | Node map/cache/build/resource ownership |
| `src/tools/verify_wmo_placement_resolver.gd` | Contract, source and timing regression |
## Related decisions and references
- [`world-renderer.md`](world-renderer.md)
- [`wmo-placement-registry.md`](wmo-placement-registry.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+244
View File
@@ -0,0 +1,244 @@
# WMO Render Build Queue
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-WMO-RENDER-BUILD-QUEUE-001` |
| Owners | Typed lightweight-WMO build jobs and FIFO placement-key queue |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-render-build-queue`, 2026-07-17 |
| Profiles/capabilities | Lightweight WMO render-cache path; profile-independent |
## Purpose
Own pending lightweight-WMO render build records and their FIFO placement-key
order outside the monolithic loader. Each typed job retains its placement key,
scene root, render resource and mesh/MultiMesh cursors while the loader performs
all validation, planning, materialization, permit consumption and destruction.
## Non-goals
- Free or queue-free Nodes, Resources or RIDs.
- Create MeshInstance3D/MultiMeshInstance3D or refresh materials.
- Own frame permits, placement references, render cache state or load I/O.
- Change duplicate enqueue, stale-key cleanup, cancellation or ordering.
- Change cache versions, WMO transforms, shadows, visibility or fidelity rules.
## Context and boundaries
```mermaid
flowchart LR
Placement[Successful WMO placement] --> Loader[StreamingWorldLoader]
Root[Node3D root] --> Loader
Cache[WMOStreamingResource] --> Loader
Loader --> Queue[WmoRenderBuildQueue]
Queue --> Job[WmoRenderBuildJob]
Job --> Loader
Loader --> Planner[WmoRenderBuildStepPlanner]
Planner --> Step[Operation and next cursors]
Step --> Loader
Loader --> Scene[Main-thread group Node attach]
Loader --> Budget[RenderBudgetScheduler permit]
```
The queue may strongly reference Node3D and Resource through typed jobs, but it
must not destroy, mutate or inspect their render content. RenderingServer, RID,
Mesh/MultiMesh instances, files, worker threads, mutexes, gameplay, network and
editor UI are forbidden dependencies.
## Public API
### `WmoRenderBuildJob`
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| constructor `(placement_key, root, render_resource)` | Constructor | Create cursor-zero job with strong references | Renderer main thread; until cancel/clear/replacement | Queue validates inputs before construction |
| `placement_key()` | Query | Return immutable queue identity | Any serialized caller; job lifetime | None |
| `root()` | Query | Borrow retained Node3D reference | Main thread; no ownership transfer | May later be invalid; loader checks |
| `render_resource()` | Query | Borrow retained Resource reference | Main thread; no ownership transfer | Queue does not validate cache contents |
| `mesh_index()` / `multimesh_index()` | Query | Return current group cursors | Serialized job drain | None |
| `adopt_cursors(next_mesh_index, next_multimesh_index)` | Command | Atomically adopt planner cursors | Renderer main thread | Raw integers are accepted |
| `diagnostic_snapshot()` | Query | Return detached scalar state and presence flags | Caller-owned result | Never exposes engine objects |
### `WmoRenderBuildQueue`
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `enqueue(placement_key, root, render_resource)` | Command | Replace keyed job and append FIFO key | Renderer main thread; map session | Empty/null input returns false |
| `has_pending()` / `pending_count()` | Query | Observe FIFO entries including duplicate/stale keys | Renderer main thread | None |
| `active_job_count()` | Query | Count current keyed jobs | Renderer main thread | None |
| `front_key()` / `pop_front()` | Queue operation | Inspect/remove one front FIFO key | Renderer main thread | Empty queue returns empty String |
| `has_job(placement_key)` | Query | Distinguish a live job from a stale FIFO key | Renderer main thread | Unknown key returns false |
| `job_for(placement_key)` | Diagnostic/test query | Borrow current job through its `RefCounted` base or return null | Renderer main thread | Runtime loader uses typed accessors instead |
| `root_for` / `render_resource_for` | Typed query | Borrow current engine references without exposing job implementation type | Renderer main thread | Stale key returns null |
| `mesh_index_for` / `multimesh_index_for` | Typed query | Return current cursors | Renderer main thread | Stale key returns zero |
| `adopt_cursors(placement_key, next_mesh_index, next_multimesh_index)` | Command/query | Atomically update current job cursors | Renderer main thread | Stale key returns false |
| `cancel(placement_key)` | Command/query | Remove keyed job and first matching FIFO key | Renderer main thread | Unknown key returns false; stale duplicates remain |
| `clear()` | Command | Release all job/key references idempotently | Reset/shutdown | Does not free engine objects |
| `diagnostic_snapshot()` | Query | Return detached FIFO order and key-sorted scalar jobs | Caller-owned result | None |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Resolved placement key | Placement/build adapter | Queue/job | Copied String | Until cancel/clear/replacement |
| Input | WMO root Node3D | Loader instance adapter | Job | Strong reference; loader owns engine lifetime | Until job release |
| Input | Validated WMOStreamingResource | Loader cache adapter | Job | Strong Resource reference; cache/loader owns semantics | Until job release |
| Input | Next mesh/MultiMesh cursors | Build step planner through loader | Job | Integer values | Until next adoption |
| Output | FIFO front key and typed job | Queue | Loader drain | Borrowed references | One drain iteration |
| Output | Detached scalar diagnostics | Job/queue | Verifier/future facade adapter | Fresh values | Caller lifetime |
Side effects are limited to reference retention/release, cursor mutation and
Dictionary/Array ordering. Scene tree, material, RID, file and permit effects
remain outside the module.
## Data flow
```mermaid
flowchart TD
Enqueue[enqueue key/root/resource] --> Valid{Inputs valid?}
Valid -->|no| Reject[false; unchanged]
Valid -->|yes| Replace[Replace keyed typed job]
Replace --> Append[Append key even when duplicate]
Drain[front key] --> Lookup{Current keyed job exists?}
Lookup -->|no stale| Pop[pop front and continue]
Lookup -->|yes| Borrow[Borrow job to loader]
Borrow --> Plan[Loader invokes step planner]
Plan --> Adopt[Job adopts both next cursors]
Cancel[cancel key] --> EraseJob[Erase keyed job]
EraseJob --> EraseFirst[Erase first matching FIFO key]
```
## Lifecycle/state
The queue can be `Empty`, `Pending` or `StaleFront`. Duplicate enqueue followed
by cancel can intentionally produce `StaleFront`, matching the previous separate
Dictionary/Array behavior. The loader pops a stale front without consuming a
permit. `clear` returns every state to `Empty` without freeing engine objects.
```mermaid
stateDiagram-v2
[*] --> Empty
Empty --> Pending: valid enqueue
Pending --> Pending: enqueue / cursor adoption
Pending --> Empty: final cancel or clear
Pending --> StaleFront: duplicate key then cancel first occurrence
StaleFront --> Pending: pop stale, more live entries remain
StaleFront --> Empty: pop stale or clear
```
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Queue as WmoRenderBuildQueue
participant Job as WmoRenderBuildJob
participant Planner as WmoRenderBuildStepPlanner
Loader->>Queue: enqueue(key, root, resource)
loop while permit and pending key
Loader->>Queue: front_key + has_job
alt stale key
Loader->>Queue: pop_front
else live job
Loader->>Queue: typed root/resource/cursor accessors
Loader->>Planner: plan_step(counts, job cursors)
Planner-->>Loader: operation and next cursors
Loader->>Loader: materialize one group on main thread
Loader->>Queue: adopt_cursors(key, next mesh, next MultiMesh)
Loader->>Loader: consume one permit
end
end
Loader->>Queue: cancel(key) or clear()
Note over Queue: references released; Nodes/Resources not freed
```
## Ownership, threading and resources
- Queue owns its job Dictionary and FIFO key Array.
- Job owns strong references, but loader owns Node/Resource semantic lifetime.
- Loader checks `is_instance_valid`, creates/frees Nodes and controls caches.
- Loader invokes planner and adopts cursor output on the main thread.
- Queue/job contain no mutex; all mutation is serialized by the renderer drain.
- `clear` may release the last Resource reference but never calls engine free APIs.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty key/null root/resource | Enqueue guards | Reject without mutation | Contract verifier | Repair caller input |
| Duplicate key | Existing keyed job | Replace job and append key | Queue diagnostics | Historical stale handling remains |
| Stale FIFO key | `job_for` returns null | Loader pops front without permit | Contract/source verifier | Continue drain |
| Root invalidated externally | Loader validity check | Loader cancels keyed job | Shutdown/runtime diagnostics | Placement can rebuild later |
| Resource absent | Loader check | Loader cancels keyed job | Runtime diagnostics | Reload/requeue placement |
| Final placement release | Placement registry returns final | Loader cancels build then frees Node | Registry/shutdown regressions | Later placement starts fresh |
| Reset/shutdown | Loader lifecycle | Queue clear releases references | Shutdown verifier | Next map starts empty |
## Configuration and capabilities
The module introduces no settings or profile branches. WMO group permit count,
quality presets, cache limits, visibility and shadows remain loader-owned.
## Persistence, cache and migration
No data is serialized. WMO cache format/version/path and load request behavior
are unchanged. No migration or cache rebuild is required.
## Diagnostics and observability
`pending_count` preserves existing queue metrics, including stale/duplicate keys.
The detached snapshot exposes FIFO order, cursor values and root/resource presence
without exposing mutable jobs or engine objects. No runtime logs are emitted.
## Verification
- `verify_wmo_render_build_queue.gd`: invalid inputs, FIFO, typed references,
cursor adoption, duplicate replacement, stale pop, first-key cancel, clear,
engine lifetime, detached sorted diagnostics, source boundaries and timing.
- WMO planner/registry/resolver/material/shutdown and renderer regressions verify
unchanged ordering, permit use, destruction and surrounding lifecycle.
- Fidelity evidence is exact preservation of current queue/Dictionary semantics;
no original-client visual or asset-backed performance claim is made.
## Extension points
- Cache/load-request ownership can be extracted separately without changing jobs.
- A later typed materialization command may consume the borrowed job and planner
output while this queue remains the pending-state owner.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| WMO render build pending-state ownership | Implemented | Lifecycle/source/timing verifier | Asset-backed traversal/leak evidence pending |
| WMO group planning | Implemented | Build-step planner verifier | Asset-backed p95/p99 pending |
| WMO render Resource state | Implemented extraction | Cache-state lifecycle/source/timing verifier | Asset-backed traversal/leak evidence pending |
| WMO materialization/cache I/O | Partial | Existing runtime regressions | Cached-scene and I/O seams remain |
## Known gaps and risks
- Jobs intentionally retain strong Node/Resource references until cancel/clear.
- Duplicate enqueue preserves a stale-key edge case rather than normalizing it.
- Resource array shape remains a WMO cache-builder invariant.
- No proprietary WMO corpus, traversal profile or original-client paired capture
is included in this package.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_render_build_job.gd` | Typed references and cursors |
| `src/render/wmo/wmo_render_build_queue.gd` | Keyed jobs, FIFO order, cancel/clear and diagnostics |
| `src/render/wmo/wmo_render_build_step_planner.gd` | Pure operation/cursor planning |
| `src/scenes/streaming/streaming_world_loader.gd` | Validation, materialization, permits and engine destruction |
| `src/tools/verify_wmo_render_build_queue.gd` | Lifecycle, ownership, boundary and timing regression |
## Related decisions and references
- [`wmo-render-build-step-planner.md`](wmo-render-build-step-planner.md)
- [`wmo-render-resource-cache-state.md`](wmo-render-resource-cache-state.md)
- [`wmo-placement-registry.md`](wmo-placement-registry.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
@@ -0,0 +1,189 @@
# WMO Render Build Step Planner
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-WMO-RENDER-BUILD-PLANNER-001` |
| Owners | Pure mesh-first WMO render-group cursor decision |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-render-build-planner`, 2026-07-17 |
| Profiles/capabilities | Lightweight WMO render-cache path; profile-independent |
## Purpose
Select exactly one next lightweight WMO render build operation from mesh and
MultiMesh counts/cursors. The planner makes the ordering and cursor transition
testable without loading a WMO, creating a Node or consuming a frame budget.
## Non-goals
- Own render Resources, Nodes, materials, jobs, queues or permits.
- Read WMO arrays, create group instances or adopt cursors in a job.
- Change group ordering, names, transforms, shadows or visibility.
- Load, validate, version or persist a WMO cache.
- Implement portals, rooms, doodad semantics or WMO visual parity.
## Context and boundaries
```mermaid
flowchart LR
Cache[WMOStreamingResource arrays] --> Loader[StreamingWorldLoader]
Job[Loader mesh/MultiMesh cursors] --> Loader
Loader --> Planner[WmoRenderBuildStepPlanner]
Planner --> Step[Operation + selected index + next cursors]
Step --> Loader
Loader --> Material[Material refresh]
Loader --> Node[MeshInstance3D or MultiMeshInstance3D]
Loader --> Budget[RenderBudgetScheduler permit]
```
The planner depends only on `RefCounted`, integers, `StringName` and a fresh
Dictionary result. Node, Resource, Mesh, MultiMesh, RID, SceneTree, files,
threads, locks, caches, gameplay, network and editor UI are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `OPERATION_MESH` | Constant | Identifies one mesh-group operation | Immutable | None |
| `OPERATION_MULTIMESH` | Constant | Identifies one doodad MultiMesh-group operation | Immutable | None |
| `OPERATION_COMPLETE` | Constant | Identifies exhausted mesh and MultiMesh ranges | Immutable | None |
| `plan_step(mesh_count, mesh_index, multimesh_count, multimesh_index)` | Pure query | Select one operation, selected index and next cursors | Any thread; caller-owned result | No clamp/error branch; raw integer comparisons are preserved |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Mesh array count and current mesh cursor | Loader resource/job adapter | Planner | Integer values | One call |
| Input | MultiMesh array count and current MultiMesh cursor | Loader resource/job adapter | Planner | Integer values | One call |
| Output | `mesh`, `multimesh` or `complete` operation | Planner | Loader materializer/cancellation branch | Immutable `StringName` | One call |
| Output | Selected index | Planner | Loader Resource array access | Integer value | One operation |
| Output | Next mesh and MultiMesh cursors | Planner | Loader job adoption | Fresh Dictionary values | Until adopted/discarded |
The planner retains no input or output. Scene-tree mutation, material refresh,
job writes, permit consumption and cancellation are loader side effects.
## Data flow
```mermaid
flowchart TD
Input[Counts and cursors] --> MeshRemaining{mesh index < mesh count?}
MeshRemaining -->|yes| MeshStep[mesh at mesh index; increment mesh cursor]
MeshRemaining -->|no| MultiRemaining{MultiMesh index < MultiMesh count?}
MultiRemaining -->|yes| MultiStep[MultiMesh at its index; increment MultiMesh cursor]
MultiRemaining -->|no| Complete[complete; keep both cursors]
```
## Lifecycle/state
The planner is stateless. State belongs to the loader job. Repeated loader calls
advance through `mesh` steps, then `multimesh` steps, then `complete`. Planner
construction, cancellation, map reset and shutdown require no cleanup.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Planner as WmoRenderBuildStepPlanner
participant Scene as SceneTree
participant Budget as RenderBudgetScheduler
Loader->>Planner: plan_step(counts, cursors)
Planner-->>Loader: operation, selected index, next cursors
alt mesh or MultiMesh
Loader->>Loader: refresh material and create group Node
Loader->>Scene: attach Node and editor owner
Loader->>Loader: adopt returned cursor
Loader->>Budget: consume one WMO group permit
else complete
Loader->>Loader: cancel/remove job and queue key
end
```
## Ownership, threading and resources
- The planner owns only call-local scalar comparisons and the returned value map.
- `WmoRenderBuildQueue` owns typed jobs, FIFO keys and strong Node3D/Resource
references. The loader owns mesh-array interpretation and engine lifetime.
- The loader creates and owns every MeshInstance3D/MultiMeshInstance3D and
performs material refresh and editor-owner assignment on the main thread.
- `RenderBudgetScheduler` owns permits; the planner neither reads nor consumes one.
- Pure calls are thread-safe because the planner has no mutable state.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty arrays | Both `0 < 0` comparisons false | Return `complete` | Contract verifier | Loader removes job |
| Cursor at/beyond count | Raw integer comparison | Skip that range | Contract verifier | Continue next range or complete |
| Negative cursor/count | Raw historical comparison | No clamp; select exactly as prior loader condition | Edge fixture | Caller should maintain valid job cursors |
| Missing/invalid Resource | Loader validation before planning | Loader cancels job | Existing shutdown/runtime diagnostics | Reload/requeue placement |
| Root freed | Loader validity check before planning | Loader cancels job | Runtime-cache shutdown verifier | Placement may rebuild later |
| Placement final release | Loader/registry lifecycle | Loader cancels queued build | Registry regression | Later placement starts at cursor zero |
## Configuration and capabilities
The planner has no setting or profile branch. `wmo_render_group_ops_per_tick`,
quality presets, shadow/visibility settings and cache limits remain loader-owned.
## Persistence, cache and migration
No data is persisted. WMO render resource format/version, cache path and job
Dictionary fields are unchanged. No migration or cache rebuild is required.
## Diagnostics and observability
The planner emits no logs. Its verifier reports operation/cursor/source contracts
and bounded timing. Queue lengths, active WMO count and hitch sections remain in
existing loader/facade diagnostics.
## Verification
- `verify_wmo_render_build_step_planner.gd`: mesh-first order, cursor increments,
completion, empty and raw negative integer behavior, fresh results, loader
delegation, dependency boundary and 20,000-plan timing.
- WMO registry/resolver/material/shutdown plus renderer regressions verify the
unchanged ownership and surrounding runtime path.
- Fidelity evidence is exact extraction of existing mesh-before-MultiMesh and
one-index-per-permit behavior. No asset-backed build-12340 visual claim is made.
## Extension points
- `WmoRenderBuildQueue` now owns typed pending state; cache/load requests and
materialization remain separate extraction seams.
- Materialization can later receive typed commands without changing this planner.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| WMO render build step planning | Implemented | Pure contract/source/timing verifier | Asset-backed traversal p95/p99 pending |
| WMO build job/queue ownership | Implemented | Typed lifecycle/source/timing verifier | Asset-backed traversal/leak evidence pending |
| WMO visual/portal/material fidelity | Partial | Renderer baseline/material checks | Original-client paired evidence pending |
## Known gaps and risks
- Loader build jobs remain untyped Dictionaries.
- Resource array shape/length consistency remains a cache-builder invariant.
- No proprietary WMO corpus, traversal profile or paired client capture is part
of this extraction.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_render_build_step_planner.gd` | Pure operation and cursor selection |
| `src/render/wmo/wmo_render_build_job.gd` | Typed engine references and cursors |
| `src/render/wmo/wmo_render_build_queue.gd` | Pending jobs, FIFO keys and cancellation state |
| `src/scenes/streaming/streaming_world_loader.gd` | Resource validation, materialization, permits and engine lifetime |
| `src/tools/verify_wmo_render_build_step_planner.gd` | Contract, boundary and timing regression |
## Related decisions and references
- [`wmo-placement-registry.md`](wmo-placement-registry.md)
- [`wmo-render-build-queue.md`](wmo-render-build-queue.md)
- [`wmo-placement-resolver.md`](wmo-placement-resolver.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
@@ -0,0 +1,220 @@
# WMO Render Resource Cache State
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-WMO-RENDER-RESOURCE-CACHE-001` |
| Owners | Validated lightweight-WMO render Resources, negative cache and pending request paths |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-render-resource-cache`, 2026-07-17 |
| Profiles/capabilities | Lightweight WMO render-cache path; profile-independent |
## Purpose
Own the mutually exclusive cached, missing and pending states for normalized
lightweight-WMO render paths outside the monolithic streamer. The state holder
accepts only caller-validated `Resource` references and records cache paths for
threaded requests whose I/O lifecycle remains in `StreamingWorldLoader`.
## Non-goals
- Normalize WMO paths or construct `.res` cache paths.
- Call `ResourceLoader`, inspect files or validate script/format versions.
- Own WMO cached scenes, live prototypes, placements, build jobs or engine Nodes.
- Change fallback order, retry timing, quality profiles or visible output.
- Persist, migrate or build WMO caches.
## Context and boundaries
```mermaid
flowchart LR
Placement[Normalized WMO render lookup] --> Loader[StreamingWorldLoader]
Loader --> State[WmoRenderResourceCacheState]
State -->|cached Resource| Loader
Loader -->|cache path| ResourceLoader[Godot ResourceLoader]
ResourceLoader -->|status and loaded Resource| Loader
Loader --> Validate[Script and FORMAT_VERSION validation]
Validate -->|accepted Resource or missing| State
Loader --> Fallback[Cached scene or live-prototype fallback]
```
The module may retain validated `Resource` references and copied path Strings.
It has no FileAccess, ResourceLoader, Node, RID, worker, mutex, gameplay, network
or editor dependency. Cache validation stays at the I/O boundary in the loader.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `resource_for(path)` | Query | Borrow validated cached Resource | Renderer main thread; until full clear | Empty/unknown returns null |
| `contains_resource(path)` | Query | Test accepted cache state | Renderer main thread | Empty returns false |
| `is_missing(path)` | Query | Test negative cache state | Renderer main thread | Empty returns false |
| `has_request(path)` | Query | Test pending threaded-request state | Renderer main thread | Empty returns false |
| `remember_request(path, cache_path)` | Command/query | Record one loader-started request | Renderer main thread; until terminal/reset | Invalid or occupied state returns false |
| `request_paths_snapshot()` | Query | Copy pending normalized/cache-path mapping | Poll or shutdown drain | Detached Dictionary |
| `complete_request_with_resource(path, resource)` | Command/query | Remove pending request and adopt caller-validated Resource | Terminal loader poll | Unknown/null returns false |
| `complete_request_as_missing(path)` | Command/query | Remove pending request and adopt negative state | Terminal loader poll | Unknown returns false |
| `clear_transient_state()` | Command | Clear pending and missing while retaining accepted Resources | Map reset/request drain | Idempotent |
| `clear_all()` | Command | Release Resources, pending and missing | Final runtime cache release | Idempotent |
| `pending_request_count()` | Query | Preserve renderer queue metric contribution | Renderer diagnostics | None |
| `diagnostic_snapshot()` | Query | Return sorted detached scalar keys | Tests/diagnostics | Never exposes Resources |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Normalized relative WMO path | Placement resolver through loader | Cache state | Copied String | Map/cache lifetime |
| Input | `.res` cache path after accepted threaded-request result | Loader | Pending state | Copied String | Until terminal/reset |
| Input | Script/version-validated WMO Resource | Loader validation adapter | Resource cache | Strong reference | Until full clear |
| Output | Borrowed validated Resource | Cache state | Loader render-root/build adapters | State retains reference | Lookup/build start |
| Output | Detached pending-path snapshot | Cache state | Loader ResourceLoader polling/shutdown | Caller-owned Dictionary | One drain pass |
| Output | Sorted scalar diagnostic keys | Cache state | Verifier/future metrics | Caller-owned values | Snapshot lifetime |
## Data flow
```mermaid
flowchart TD
Lookup[Lookup normalized path] --> Cached{Cached?}
Cached -->|yes| Return[Return validated Resource]
Cached -->|no| Blocked{Missing or pending?}
Blocked -->|yes| Null[Return null; loader waits/falls back]
Blocked -->|no| Start[Loader starts threaded request]
Start --> Remember[remember_request]
Remember --> Poll[Loader polls detached request snapshot]
Poll --> Terminal{Loaded or failed?}
Terminal -->|no| Poll
Terminal -->|failed| Missing[complete as missing]
Terminal -->|loaded| Validate[Loader validates script/version]
Validate -->|valid| Adopt[complete with Resource]
Validate -->|invalid| Missing
```
## Lifecycle/state
Each normalized path is `Absent`, `Pending`, `Cached` or `Missing`. Valid request
completion leaves exactly one terminal state. Transient clear moves `Pending` and
`Missing` to `Absent` but retains `Cached`; full clear moves every state to
`Absent` and releases retained Resources.
```mermaid
stateDiagram-v2
[*] --> Absent
Absent --> Pending: remember valid request
Pending --> Cached: validated Resource completion
Pending --> Missing: load/validation failure
Pending --> Absent: clear transient/full
Missing --> Absent: clear transient/full
Cached --> Absent: full clear
```
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant State as WmoRenderResourceCacheState
participant RL as ResourceLoader
Loader->>State: cached/missing/pending queries
Loader->>RL: exists + load_threaded_request(cache path)
Loader->>State: remember_request(normalized, cache path)
loop renderer tick
Loader->>State: request_paths_snapshot()
Loader->>RL: load_threaded_get_status(cache path)
end
alt load failed
Loader->>State: complete_request_as_missing(normalized)
else loaded
Loader->>RL: load_threaded_get(cache path)
Loader->>Loader: validate script and FORMAT_VERSION
Loader->>State: complete with Resource or as missing
end
```
## Ownership, threading and resources
- The state owns three Dictionaries and strong references to accepted Resources.
- The loader owns normalization, cache paths, ResourceLoader calls and validation.
- All mutation is serialized by the renderer main-thread lookup/drain lifecycle.
- No mutex or callback is needed; detached request snapshots permit safe removal.
- The loader/build queue borrow Resources without transferring ownership.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty path/cache path | Request guard | Reject unchanged | Contract verifier | Repair caller input |
| Duplicate or occupied path | State membership | Reject request unchanged | Contract/source verifier | Await terminal/reset |
| Threaded load failed | Loader status | Complete as missing | Existing renderer metrics | Transient reset permits retry |
| Wrong script/old format | Loader validation | Complete as missing | Cache-version source gate | Rebuild cache, then reset |
| Shutdown while pending | Loader drains ResourceLoader snapshot | Clear transient afterward | Shutdown verifier | Next runtime starts absent |
| Final cache release | Loader shutdown lifecycle | Full clear releases Resources | Shutdown verifier | Recreated with loader |
## Configuration and capabilities
The module adds no setting or profile branch. `wmo_render_cache_dir`, resource
format version, request scheduling and WMO build budgets remain loader-owned.
## Persistence, cache and migration
The state is runtime-only and serializes nothing. `WMOStreamingResource` script
identity and `FORMAT_VERSION` validation remain unchanged in the loader; no cache
migration or rebuild is introduced by this extraction.
## Diagnostics and observability
`pending_request_count` preserves the existing `wmobuild` metric contribution.
Diagnostics expose only sorted normalized-path keys for cached/missing/pending
states; Resource references and cache file paths are not exposed. No logs emit.
## Verification
- `verify_wmo_render_resource_cache_state.gd`: invalid input, detached requests,
successful/missing terminal transitions, exclusivity, partial/full reset,
sorted diagnostics, ownership/source boundary and bounded timing.
- `verify_render_runtime_cache_shutdown.gd`: full release of accepted Resources
and negative state through the loader lifecycle.
- WMO queue/planner/registry/resolver and surrounding renderer regressions verify
unchanged fallback/build behavior. This package makes no visual parity claim.
## Extension points
- Cached WMO scene request state is a separate typed module because its size
limit, PackedScene validation and missing-file behavior differ.
- A later loader I/O adapter may consume the detached request snapshot while
preserving this state contract and cache-version boundary.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Lightweight WMO render Resource state | Implemented extraction | Lifecycle/source/timing and shutdown verifiers | Asset-backed traversal/leak evidence pending |
| Cache script/version validation | Preserved in loader | Source boundary and WMO regressions | Dedicated corrupt-cache fixture could follow |
| Cached WMO scene state | Implemented extraction | Scene-cache lifecycle/source/timing verifier | Asset-backed traversal/leak evidence pending |
| WMO materialization | Partial/loader-owned | Queue/planner regressions | Further safe extraction |
## Known gaps and risks
- Strong Resource references live until final cache release, matching prior state.
- Missing cache files remain unrecorded for render Resources, so lookup can retry;
this intentionally differs from the cached-scene path.
- No proprietary WMO corpus, corrupt-version fixture, leak run, traversal p95/p99
or paired original-client capture is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_render_resource_cache_state.gd` | Resource/missing/request state and resets |
| `src/scenes/streaming/streaming_world_loader.gd` | Normalize, request, poll, validate, fallback and shutdown I/O |
| `src/tools/verify_wmo_render_resource_cache_state.gd` | State, boundary and timing regression |
| `src/tools/verify_render_runtime_cache_shutdown.gd` | Loader final Resource ownership regression |
## Related decisions and references
- [`wmo-render-build-queue.md`](wmo-render-build-queue.md)
- [`wmo-scene-resource-cache-state.md`](wmo-scene-resource-cache-state.md)
- [`wmo-placement-resolver.md`](wmo-placement-resolver.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
@@ -0,0 +1,212 @@
# WMO Scene Resource Cache State
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-WMO-SCENE-RESOURCE-CACHE-001` |
| Owners | Validated cached-WMO PackedScenes, negative cache and pending request paths |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-scene-resource-cache`, 2026-07-17 |
| Profiles/capabilities | Cached WMO `.tscn` fallback; profile-independent state |
## Purpose
Own mutually exclusive cached, missing and pending states for normalized WMO
scene paths. The state accepts only caller-validated `PackedScene` references and
records `.tscn` paths for threaded requests whose I/O, size limit and cache
metadata validation remain in `StreamingWorldLoader`.
## Non-goals
- Normalize WMO paths, build `.tscn` paths or measure files.
- Call ResourceLoader/FileAccess, instantiate scenes or validate cache metadata.
- Own live prototypes, placement/build jobs, instantiated Nodes or RIDs.
- Change missing, oversize, request-error, retry or fallback behavior.
- Persist, migrate or build WMO caches.
## Context and boundaries
```mermaid
flowchart LR
Lookup[Normalized WMO scene lookup] --> Loader[StreamingWorldLoader]
Loader --> State[WmoSceneResourceCacheState]
Loader --> Size[File existence and size limit]
Size --> ResourceLoader[Godot ResourceLoader]
ResourceLoader --> Loader
Loader --> Probe[Instantiate, metadata/version check, free]
Probe -->|accepted PackedScene or missing| State
Loader --> Live[Live-prototype fallback]
```
The module retains validated PackedScenes and copied paths only. ResourceLoader,
FileAccess, WMOBuilder, Node, RID, worker, gameplay, network and editor concerns
remain outside it.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `scene_for(path)` / `contains_scene(path)` | Query | Borrow/test validated PackedScene | Renderer main thread; until full clear | Empty/unknown returns null/false |
| `is_missing(path)` / `has_request(path)` | Query | Test negative/pending state | Renderer main thread | Empty returns false |
| `remember_request(path, cache_path)` | Command/query | Record loader-started request | Until terminal/reset | Invalid/occupied returns false |
| `mark_missing(path)` | Command/query | Record absent/oversize/request-start failure without pending state | Until transient/full clear | Invalid/occupied returns false |
| `request_paths_snapshot()` | Query | Copy pending normalized/cache-path mapping | Poll/shutdown drain | Detached Dictionary |
| `complete_request_with_scene(path, scene)` | Command/query | Remove pending and adopt validated PackedScene | Terminal poll | Unknown/null returns false |
| `complete_request_as_missing(path)` | Command/query | Remove pending and adopt negative state | Terminal poll | Unknown returns false |
| `clear_transient_state()` | Command | Clear pending/missing and retain scenes | Map reset/request drain | Idempotent |
| `clear_all()` | Command | Release scenes and transient state | Final runtime-cache release | Idempotent |
| `pending_request_count()` | Query | Preserve renderer queue metric | Diagnostics | None |
| `diagnostic_snapshot()` | Query | Return sorted detached scalar keys | Tests/diagnostics | No PackedScene exposure |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Normalized WMO path | Placement resolver through loader | State | Copied String | Cache lifetime |
| Input | `.tscn` path after accepted request start | Loader | Pending state | Copied String | Until terminal/reset |
| Input | Metadata/version-validated PackedScene | Loader probe adapter | Scene cache | Strong reference | Until full clear |
| Input | Missing decision | Loader file/size/request/load/validation adapters | Negative cache | Boolean membership | Until transient/full clear |
| Output | Borrowed PackedScene | State | Loader scene instantiation | State retains reference | Lookup/instantiate call |
| Output | Detached request snapshot | State | Loader polling/shutdown | Caller-owned Dictionary | One drain pass |
## Data flow
```mermaid
flowchart TD
Lookup --> Known{Cached, missing or pending?}
Known -->|cached| Return[Return PackedScene]
Known -->|missing/pending| Null[Return null]
Known -->|absent| Check[Loader checks file and size]
Check -->|missing/oversize| Mark[mark_missing]
Check -->|allowed| Request[Start threaded request]
Request -->|error| Mark
Request -->|accepted| Remember[remember_request]
Remember --> Poll[Loader polls snapshot]
Poll -->|failure| CompleteMissing[complete as missing]
Poll -->|loaded| Validate[Instantiate and validate cache metadata]
Validate -->|valid| CompleteScene[complete with scene]
Validate -->|invalid| CompleteMissing
```
## Lifecycle/state
Each normalized path is `Absent`, `Pending`, `Cached` or `Missing`. Direct file,
size and request-start rejection moves `Absent` directly to `Missing`; terminal
loads move `Pending` to `Cached` or `Missing`. Transient clear retains `Cached`,
while full clear returns every path to `Absent`.
```mermaid
stateDiagram-v2
[*] --> Absent
Absent --> Pending: accepted request
Absent --> Missing: absent, oversize or request error
Pending --> Cached: validated PackedScene
Pending --> Missing: load or validation failure
Pending --> Absent: transient/full clear
Missing --> Absent: transient/full clear
Cached --> Absent: full clear
```
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant State as WmoSceneResourceCacheState
participant RL as ResourceLoader
Loader->>State: cached/missing/pending queries
Loader->>Loader: exists and wmo_max_runtime_scene_mb check
Loader->>RL: load_threaded_request(.tscn)
Loader->>State: remember_request(normalized, path)
loop renderer tick
Loader->>State: request_paths_snapshot()
Loader->>RL: load_threaded_get_status(path)
end
Loader->>RL: load_threaded_get(path)
Loader->>Loader: instantiate, validate WMO metadata, free probe
Loader->>State: complete with scene or as missing
```
## Ownership, threading and resources
- State owns three Dictionaries and strong references to accepted PackedScenes.
- Loader owns paths, file measurement, requests, validation and all Node lifetime.
- All mutation is serialized on the renderer main thread; no mutex is required.
- Detached request snapshots allow terminal removal while polling.
- Scene instantiation borrows the PackedScene and does not transfer cache ownership.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Recovery |
|---|---|---|---|
| Missing `.tscn` | Loader ResourceLoader existence check | Direct missing state | Transient reset permits later retry |
| Oversize `.tscn` | Loader byte limit | Missing plus unchanged debug log | Raise limit/rebuild, then reset |
| Request-start/load failure | Loader error/status | Direct or terminal missing | Reset and retry later |
| Wrong type/stale metadata | Loader PackedScene/probe validation | Terminal missing; probe freed | Rebuild cache and reset |
| Shutdown while pending | Loader drains snapshot | Clear transient, then full cache release | New loader starts absent |
## Configuration and capabilities
No setting is added. `wmo_cache_dir`, `wmo_max_runtime_scene_mb`, request timing,
quality profiles and WMO build budgets stay loader-owned.
## Persistence, cache and migration
The module is runtime-only. Existing WMOBuilder cache metadata/version checks
remain unchanged and execute before state adoption. No cache format, migration or
rebuild is introduced.
## Diagnostics and observability
Pending count preserves the existing `wmobuild` metric contribution. The snapshot
exposes sorted scene/missing/request keys only. Existing oversize logging remains
in the loader; the state emits no logs.
## Verification
- `verify_wmo_scene_resource_cache_state.gd`: invalid input, direct missing,
exclusivity, request completion, partial/full reset, diagnostics, boundaries
and bounded timing.
- `verify_render_runtime_cache_shutdown.gd`: loader full release of scenes,
negative and pending state.
- Adjacent WMO/renderer regressions verify unchanged lookup/fallback behavior.
This extraction makes no visual or build-12340 parity claim.
## Extension points
- A later I/O adapter may consume request snapshots while retaining this contract.
- Scene instantiation/material preparation can be extracted separately because it
owns Nodes and main-thread side effects rather than cache state.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Cached WMO PackedScene state | Implemented extraction | Lifecycle/source/timing and shutdown verifiers | Asset-backed traversal/leak evidence pending |
| Size and cache metadata validation | Preserved in loader | Source boundary and WMO regressions | Oversize/stale asset fixtures could follow |
| ResourceLoader I/O and live fallback | Partial/loader-owned | Existing runtime behavior | Separate extraction if justified |
## Known gaps and risks
- PackedScenes remain strongly referenced until final shutdown cache release.
- Negative caching suppresses retries until transient reset, matching prior behavior.
- No proprietary WMO corpus, stale/oversize asset fixture, leak run, traversal
p95/p99 or paired original-client capture is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_scene_resource_cache_state.gd` | Scene/missing/request state and resets |
| `src/scenes/streaming/streaming_world_loader.gd` | Paths, file limit, requests, validation, fallback and Node lifetime |
| `src/tools/verify_wmo_scene_resource_cache_state.gd` | State, boundary and timing regression |
| `src/tools/verify_render_runtime_cache_shutdown.gd` | Loader final cache ownership regression |
## Related decisions and references
- [`wmo-render-resource-cache-state.md`](wmo-render-resource-cache-state.md)
- [`wmo-render-build-queue.md`](wmo-render-build-queue.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+254
View File
@@ -0,0 +1,254 @@
# World Entity Presentation
## Metadata
| Field | Value |
|---|---|
| Status | Implemented boundary / Prototype visuals |
| Target/work package | M03 / `M03-RND-FACADE-ENTITY-001` |
| Owners | Renderer entity-presentation boundary and visual subtree lifecycle |
| Last verified | Worktree `work/sindo-main-codex/m03-facade-entity`, 2026-07-16 |
| Profiles/capabilities | Contract version 1; asset-free scene presentation only |
## Purpose
Accept immutable session-local entity visual snapshots through
`WorldRenderFacade`, create or update renderer-owned visual subtrees idempotently,
and remove them explicitly without making the renderer authoritative for entity,
gameplay, network or content state.
## Non-goals
- Allocate `EntityId` or map it to `WowGuid`, display ID or server entry.
- Present the separately composed local player through this service.
- Define build-12340 model, equipment, animation, effects or visibility rules.
- Own gameplay/world state, packet decoding, terrain or streaming decisions.
- Provide asynchronous loading, pooling, batching, persistence or cache formats.
## Context and boundaries
```mermaid
flowchart LR
WorldState[Future authoritative WorldState] --> Adapter[Entity presentation adapter]
Adapter --> Snapshot[EntityPresentationSnapshot v1]
Snapshot --> Facade[WorldRenderFacade]
Facade --> Presenter[WorldEntityPresenter]
Asset[PackedScene visual path] --> Presenter
Presenter --> Visual[Owned Node3D visual subtree]
LocalPlayer[Local player composition] -. separate boundary .-> Viewport[Rendered world]
Visual --> Viewport
```
Allowed dependencies:
- immutable M01 `EntityId` and `GodotWorldPosition` values;
- renderer-facing Godot transform and scene APIs;
- an explicit repository/resource path selected outside the renderer;
- `WorldRenderFacade` and main-thread runtime scene composition.
Forbidden dependencies:
- gameplay reducers, intents, combat/movement authority or entity allocation;
- packet codecs, GUID interpretation, server DB access or Content Project writes;
- streamer queues/tasks/caches, terrain query or camera/input state;
- hidden autoload, SceneTree group service lookup or persisted session IDs.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `EntityPresentationSnapshot.CONTRACT_VERSION` | Contract constant | Identifies additive snapshot contract version `1` | Compile/runtime constant | Incompatible changes require migration/ADR |
| `EntityPresentationSnapshot.create(...)` | Immutable value factory | Captures identity, typed position, visual path, yaw, scale and visibility | Any thread to construct; caller-owned | Invalid value carries stable `entity_presentation_*` failure code |
| `WorldRenderFacade.present_entity(snapshot)` | Command | Idempotently creates/updates/replaces one world visual | Main thread; world session | Returns `false` for invalid snapshot, missing service or failed scene creation |
| `WorldRenderFacade.remove_entity(entity_id)` | Command | Removes one known session visual | Main thread; world session | Returns `false` for invalid or unknown identity |
| `WorldRenderFacade.entity_presentation_snapshot()` | Diagnostic query | Returns detached scalar entity inventory | Main thread; caller-owned copy | Missing/invalid service returns empty snapshot with diagnostic |
| `WorldEntityPresenter.present_entity(snapshot)` | Internal service command | Owns visual load, attach, transform update and replacement | Main thread; service lifetime | Failed replacement retains prior valid subtree |
| `WorldEntityPresenter.remove_entity(entity_id)` | Internal service command | Detaches and schedules owned subtree deletion | Main thread | Unknown identity is unchanged/false |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Valid session-local `EntityId` | Future entity registry/adapter | Snapshot/presenter index | Immutable caller reference | One runtime session |
| Input | `GodotWorldPosition` | World-state presentation adapter | Snapshot/presenter transform | Immutable caller reference | One snapshot |
| Input | PackedScene resource path | Asset-selection adapter | Presenter | Copied string; resource remains Godot-owned | Until visual replacement |
| Input | Finite yaw radians, positive scale, visibility | Presentation adapter | Presenter | Scalar copies | One snapshot |
| Output | Owned Node3D subtree | Presenter | SceneTree/viewport | Presenter child ownership | Until replacement/removal/world teardown |
| Output | Detached inventory dictionary | Presenter/facade | Diagnostics/tests | Caller-owned deep copy | One query |
Side effects:
- synchronously loads a PackedScene only for first presentation or visual-path change;
- instantiates, attaches, transforms, detaches and queues visual Node3D subtrees;
- does not mutate authoritative state, streamer queues, caches, files, DB or network;
- does not retain caller snapshots after an entity entry is replaced or removed.
## Data flow
```mermaid
flowchart TD
ReadModel[Entity visual read model] --> Validate[Validate typed immutable snapshot]
Validate -->|invalid| Reject[Return false; no scene mutation]
Validate -->|valid| Lookup[Lookup by EntityId debug key]
Lookup -->|same visual path| Transform[Update world transform and visibility]
Lookup -->|new or changed path| Instantiate[Load and instantiate Node3D]
Instantiate -->|failure| Retain[Retain previous valid visual]
Instantiate -->|success| Replace[Detach old, attach replacement]
Replace --> Transform
Transform --> Inventory[Detached diagnostics]
Remove[remove_entity] --> Detach[Detach and queue_free owned root]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Absent
Absent --> Presented: valid present + successful instantiate
Absent --> Absent: invalid/missing visual
Presented --> Presented: same-path snapshot update
Presented --> Replacing: changed visual path
Replacing --> Presented: replacement succeeds
Replacing --> Presented: replacement fails; retain old
Presented --> Absent: remove_entity
Presented --> [*]: world/service teardown
Absent --> [*]: world/service teardown
```
## Main sequence
```mermaid
sequenceDiagram
participant Adapter as World-state adapter
participant Facade as WorldRenderFacade
participant Presenter as WorldEntityPresenter
participant Loader as Godot ResourceLoader
participant Tree as SceneTree
Adapter->>Facade: present_entity(immutable snapshot)
Facade->>Presenter: present_entity(snapshot)
alt same identity and visual path
Presenter->>Tree: update transform/visibility
else new or changed visual
Presenter->>Loader: load PackedScene
Loader-->>Presenter: scene or failure
alt scene is valid Node3D
Presenter->>Tree: detach old, attach/configure new
else load/shape failure
Presenter-->>Facade: false; old visual retained
end
end
Adapter->>Facade: remove_entity(EntityId)
Facade->>Presenter: remove_entity(EntityId)
Presenter->>Tree: detach + queue_free owned root
```
## Ownership, threading and resources
- `EntityPresentationSnapshot` owns copied scalars/strings and immutable references
to caller-owned identity/position values; it owns no Node, Resource or RID.
- `WorldEntityPresenter` owns its dictionary and every visual root attached below it.
- A dictionary entry retains the latest snapshot only while the entity is presented;
diagnostics expose no Node, Resource or snapshot reference.
- Scene load, instantiation, transform mutation, replacement and removal are main-thread only.
- Successful replacement instantiates first, then detaches the previous root, so an
invalid/missing new asset cannot erase the last valid presentation.
- Parent SceneTree teardown owns attached children; `_exit_tree` drops the index so
no stale lookup survives world unload.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Invalid identity/type | Snapshot/service validation | Reject without mutation | `entity_presentation_invalid_entity_id` | Supply valid session `EntityId` |
| Non-finite position/yaw | Snapshot validation | Reject without mutation | Stable position/yaw failure code | Repair adapter read model |
| Empty path or invalid scale | Snapshot validation | Reject without mutation | Stable path/scale failure code | Supply valid visual descriptor |
| Missing/non-PackedScene asset | Resource existence/load check | New visual absent; old replacement retained | Named presenter warning | Restore asset mapping and resend snapshot |
| Non-Node3D scene root | Instance type check | Free invalid instance; retain old | Named presenter warning | Fix visual scene root |
| Unknown remove | Dictionary lookup | No mutation; return `false` | Caller command result | Reconcile registry/presentation lifecycle |
| World teardown | Scene lifecycle | SceneTree frees children; index clears | Shutdown regression | Recreate service with next world |
There is no background job to cancel. Synchronous asset load is an explicit
prototype limitation and must be replaced/budgeted before network-scale use.
## Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Contract version | `1` | All | No | Identity/path/position/yaw/scale/visibility full snapshot |
| Present semantics | Idempotent by `EntityId` | Runtime/tools | No | Duplicate snapshots do not duplicate roots |
| Resource loading | Synchronous on create/replace | Prototype only | No | Not accepted as frame-critical production path |
| Local player | Separate composition | Current sandbox | No | Prevents facade service from owning gameplay player root |
## Persistence, cache and migration
- No snapshot, entity key or resource instance is persisted.
- `EntityId` remains session-local and must never enter content/cache schemas.
- Resource paths are current renderer adapter inputs, not stable content identity.
- Contract version 1 is additive. Changing identity lifetime, coordinate basis or
persisted representation requires a migration and ADR.
## Diagnostics and observability
- `entity_presentation_snapshot()` reports contract version, entity count and a
sorted list of `EntityId.to_debug_key`, visual path and visibility.
- The snapshot is deep-detached by the facade and contains no Node/Resource/RID.
- Load/type failures include the visual path but never proprietary bytes.
- Future performance telemetry should measure create/replace duration and active
entity/material/RID counts before introducing pooling or batching.
## Verification
- `src/tools/verify_world_entity_presentation.gd` covers eight value-contract and
fourteen lifecycle/ownership cases with repository-owned asset-free scenes.
- `src/tools/verify_world_render_facade.gd` covers command delegation, detached
diagnostics and both runtime scene service paths.
- Identity, coordinate, renderer-internal-access, shutdown, manifest, planner,
scheduler and documentation gates remain required.
- Fidelity boundary: runtime emits no entity commands yet, so existing seven
checkpoint scenes remain unchanged. No build-12340 display/animation parity is claimed.
## Extension points
- AssetRepository/display-ID adapter that produces validated visual descriptors.
- Async/budgeted load and main-thread finalize behind the same facade commands.
- Animation/equipment/effect snapshots after build-12340 fixtures define behavior.
- Spatial batching/MultiMesh only after profiling proves per-Node overhead.
- Entity registry adapter that maps `WowGuid` to session `EntityId` outside render.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Typed entity visual input | Implemented contract | Versioned immutable validation fixtures | Asset/display mapping not implemented |
| Present/update/remove lifecycle | Implemented boundary | Asset-free create/update/replace/remove tests | Network-scale async/budget path missing |
| Deterministic ownership | Implemented | One presenter-owned root per EntityId; teardown checks | RID/material budgets remain future work |
| WoW entity visual fidelity | Planned | No claim | Requires build-12340 display/equipment/animation fixtures |
## Known gaps and risks
- Synchronous PackedScene load may hitch and is not a production hot-path design.
- Resource path is renderer adapter data, not stable authored content identity.
- Full snapshots currently cover transform/visibility only, not animation/equipment/effects.
- One Node3D per entity is unprofiled at world scale; batching is deliberately deferred.
- The local player remains separately composed until gameplay/network architecture is ready.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/entities/entity_presentation_snapshot.gd` | Immutable versioned entity visual input and validation |
| `src/render/entities/world_entity_presenter.gd` | Main-thread entity visual ownership and lifecycle |
| `src/render/world_render_facade.gd` | Stable public present/remove/diagnostic commands |
| `src/tools/verify_world_entity_presentation.gd` | Snapshot and scene lifecycle regression |
| `src/tests/scenes/character_presentation_model_fixture.tscn` | Primary asset-free visual fixture |
| `src/tests/scenes/world_entity_visual_replacement_fixture.tscn` | Replacement asset-free visual fixture |
| `src/scenes/streaming/*_streaming.tscn` | Runtime facade/service composition |
## Related decisions and references
- [`world-renderer.md`](world-renderer.md)
- [`domain-identities.md`](domain-identities.md)
- [`coordinate-mapping.md`](coordinate-mapping.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
- [`../ARCHITECTURE.md`](../ARCHITECTURE.md)
- [`../GODOT_BEST_PRACTICES.md`](../GODOT_BEST_PRACTICES.md)
+281 -21
View File
@@ -5,9 +5,9 @@
| Field | Value |
|---|---|
| Status | Partial |
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; `M03-RND-FACADE-001` |
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
| Owners | Renderer workstream / milestone integrator |
| Last verified | Worktree `work/sindo-main-codex/m03-renderer-facade`, 2026-07-15 |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-scene-resource-cache`, 2026-07-17 |
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose
@@ -34,14 +34,47 @@ flowchart LR
Adapter --> Focus
Focus --> Facade[WorldRenderFacade]
Facade --> Loader[StreamingWorldLoader internal]
Loader --> Planner[StreamingTargetPlanner]
Planner --> TargetPlan[StreamingTargetPlan]
TargetPlan --> Loader
Loader --> Budget[RenderBudgetScheduler]
Assets[Extracted WDT/ADT/M2/WMO/BLP] --> Native[Native loaders]
Cache[Baked terrain/M2/WMO caches] --> Loader
Loader --> TerrainCache[TerrainQualityMeshCache]
TerrainCache --> Loader
Loader --> ChunkLod[TerrainChunkLodPlanner]
ChunkLod --> Loader
Loader --> ChunkQueue[TerrainChunkGeometryQueuePlanner]
ChunkQueue --> Loader
Loader --> M2Registry[M2UniquePlacementRegistry]
M2Registry --> Loader
Loader --> M2Grouper[M2PlacementGrouper]
M2Grouper --> M2Transform[M2PlacementTransformResolver]
M2Transform --> M2Grouper
M2Transform --> Loader
Loader --> M2Batch[M2BuildBatchPlanner]
M2Batch --> Loader
Loader --> WmoPlacement[WmoPlacementResolver]
WmoPlacement --> Loader
Loader --> WmoRegistry[WmoPlacementRegistry]
WmoRegistry --> Loader
Loader --> WmoBuildStep[WmoRenderBuildStepPlanner]
WmoBuildStep --> Loader
Loader --> WmoBuildQueue[WmoRenderBuildQueue]
WmoBuildQueue --> Loader
Native --> Parsed[Parsed tile/model data]
Parsed --> Loader
Loader --> Scene[SceneTree nodes]
Loader --> RS[RenderingServer RIDs/MultiMesh]
Sky[DBC sky/light data] --> SkyController[WowSkyController]
EnvironmentState[World time producer] --> EnvironmentSnapshot[WorldEnvironmentSnapshot]
EnvironmentSnapshot --> Facade
Facade --> SkyController[WowSkyController]
Sky[DBC sky/light data] --> SkyController
SkyController --> Scene
EntityState[Entity visual read model] --> EntitySnapshot[EntityPresentationSnapshot]
EntitySnapshot --> Facade
Facade --> EntityPresenter[WorldEntityPresenter]
EntityPresenter --> Scene
```
Allowed dependencies:
@@ -61,9 +94,12 @@ Forbidden dependencies:
## Public API
Runtime, capture and probe callers use `WorldRenderFacade` for focus and metrics.
`StreamingWorldLoader` remains the internal scene implementation while later M03
packages add environment, entity-visual and ground-query contracts.
Runtime, capture and probe callers use `WorldRenderFacade` for focus, immutable
environment/entity presentation input, metrics and rendered-ground diagnostics.
`StreamingWorldLoader`, `WowSkyController` and `WorldEntityPresenter` remain
internal scene implementations.
Gameplay modules and Godot `EditorPlugin` package sources are repository-gated
from externally reading/writing loader-private queue, task, cache and tile-state fields.
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
@@ -74,11 +110,34 @@ packages add environment, entity-visual and ground-query contracts.
| `WorldRenderFacade.set_streaming_focus` | Public method | Accepts focus from app/editor/tool composition | Main thread/session | Missing internal renderer is diagnosed; no state is duplicated |
| `WorldRenderFacade.refresh_streaming_focus` | Public method | Samples configured source and delegates existing refresh thresholds | Main thread/session | Returns `false` when dependency/focus is unavailable |
| `WorldRenderFacade.renderer_metrics_snapshot` | Public method | Returns a deep-detached queue/cache/activity snapshot | Main thread/caller-owned result | Missing/invalid implementation result returns empty snapshot with diagnostic |
| `WorldRenderFacade.sample_ground_height` | Public read-only query | Samples already loaded render terrain at a typed Godot world position | Main thread; immutable caller-held result | Stable unavailable code for missing mesh/intersection/facade; not gameplay authority |
| `WorldRenderFacade.renderer_ground_query_snapshot` | Public diagnostic query | Returns ground height plus detached tile/readiness context without exposing mutable state | Main thread; caller-owned deep copy | Missing/invalid renderer returns empty snapshot with diagnostic |
| `WorldEnvironmentSnapshot.at_time_of_day` | Immutable value factory | Normalizes a finite authoritative time into `[0, 24)` hours | Any thread; caller-held value | Non-finite input creates invalid value with `environment_time_not_finite` |
| `WorldRenderFacade.apply_environment_snapshot` | Public command | Delegates exact world time to the scene-owned DBC sky controller | Main thread; snapshot remains caller-owned | Returns `false` for invalid input or missing/incompatible controller |
| `WorldRenderFacade.streaming_world_loader_path` | Internal adapter property | Resolves the existing implementation without exposing it to consumers | Main thread, scene lifetime | Missing method/path produces one diagnostic until recovery |
| `WorldRenderFacade.world_environment_controller_path` | Internal adapter property | Resolves the existing `WowSkyController` without exposing it to consumers | Main thread, scene lifetime | Missing method/path produces one diagnostic until recovery |
| `EntityPresentationSnapshot.create` | Immutable value factory | Validates contract-v1 identity, typed position, visual path, yaw, scale and visibility | Any thread; caller-owned | Stable invalid code; never mutates scene |
| `WorldRenderFacade.present_entity` | Public command | Idempotently creates/updates/replaces one session entity visual | Main thread/session | Returns false for invalid input, missing service or visual load/type failure |
| `WorldRenderFacade.remove_entity` | Public command | Removes one known session entity visual | Main thread/session | Invalid/unknown identity returns false |
| `WorldRenderFacade.entity_presentation_snapshot` | Public diagnostic query | Returns detached scalar entity inventory | Main thread/caller-owned copy | Missing/invalid service returns empty dictionary with diagnostic |
| `WorldRenderFacade.world_entity_presenter_path` | Internal adapter property | Resolves renderer-owned entity visual service | Main thread/scene lifetime | Missing methods/path produces one diagnostic until recovery |
| `camera_path` | Exported property | Camera used only by optional automatic overview positioning | Main thread, scene lifetime | Missing camera skips automatic positioning |
| `quality_preset` | Exported property | Applies renderer budgets/radii | Before/at runtime depending property | Invalid combination should be diagnosed |
| `runtime_stats_enabled` | Exported property | Enables periodic performance snapshot | Runtime mutable | Logging overhead only |
| `hitch_profiler_enabled` | Exported property | Enables named hitch sections | Runtime mutable | Logging overhead only |
| `TerrainQualityMeshCache.store/restore/clear` | Internal terrain service | Owns admitted full-quality revisit Mesh references and LRU | Renderer thread/map session | Rejected/missed entry leaves loader fallback unchanged |
| `TerrainChunkLodPlanner.plan` | Internal pure terrain query | Maps populated parsed chunk indices to desired LOD 0/1/2 | Synchronous per target refresh | Invalid/disabled input returns empty for existing tile-LOD fallback |
| `TerrainChunkGeometryQueuePlanner.plan` | Internal pure terrain query | Produces chunk create/remove requests from current and desired state | Synchronous per queue rebuild | Invalid focus returns empty arrays |
| `M2UniquePlacementRegistry.reserve/release/clear` | Internal M2 service | Owns positive cross-tile ADT placement-ID reservations | Renderer thread/map session | Invalid/unkeyed/non-owner inputs preserve documented fallback |
| `M2PlacementTransformResolver.resolve_basis/resolve_origin_offset` | Internal pure M2 service | Resolves regular and calibrated model-specific ADT placement transforms | Worker/main thread; stateless | Unknown paths use regular basis and zero offset |
| `M2PlacementGrouper.group_placements` | Internal pure M2 service | Validates and groups ordered tile-local placement transforms by normalized path | Worker/main thread; stateless | Invalid variants/name IDs/empty paths are skipped |
| `M2BuildBatchPlanner.plan_batch` | Internal pure M2 service | Selects static/animated batch count and next group cursor | Main/any thread; stateless | Non-positive selected limit clamps to one; empty range completes |
| `WmoPlacementResolver.normalize_relative_path/resolve_unique_key/resolve_world_transform` | Internal pure WMO service | Resolves cache key, registry identity and world transform | Main/any thread; stateless | Missing UID uses tile/index fallback; transform fields use historical defaults |
| `WmoPlacementRegistry.add_reference/release_reference/contains/active_count/diagnostic_snapshot/clear` | Internal WMO service | Owns placement-key to tile/global reference sets | Renderer main thread; map session | Empty/unknown/non-owner input is rejected without mutation |
| `WmoRenderBuildStepPlanner.plan_step` | Internal pure WMO service | Selects one mesh-first lightweight render-group operation and next cursors | Main/any thread; stateless | Raw integer comparisons are preserved without clamping |
| `WmoRenderBuildQueue` / `WmoRenderBuildJob` | Internal WMO pending-state service | Owns typed root/resource/cursor jobs and FIFO placement keys | Renderer main thread; map session | Invalid enqueue rejected; duplicate/stale behavior preserved |
| `WmoRenderResourceCacheState` | Internal WMO cache-state service | Owns validated Resources, negative entries and pending cache paths | Renderer main thread; map/cache session | Invalid/occupied request and unknown completion are rejected |
| `WmoSceneResourceCacheState` | Internal WMO cache-state service | Owns validated PackedScenes, negative entries and pending `.tscn` paths | Renderer main thread; map/cache session | Direct missing and terminal request transitions remain distinct |
Публичным contract не считаются `StreamingWorldLoader` methods/properties,
внутренние dictionaries, queues, job records и generated node names. Scene-owned
@@ -90,13 +149,36 @@ loader configuration remains transitional composition data, not a caller API.
|---|---|---|---|---|---|
| Input | `StreamingFocus` | Player/spectator/editor/capture adapter | `WorldRenderFacade` | Immutable caller value | Main thread/session |
| Input | Focus-source `NodePath` | Runtime/test scene composition | `WorldRenderFacade` source adapter | Scene-owned node, sampled position | Main thread/scene lifetime |
| Input | Ground-query `GodotWorldPosition` | Renderer probe/future adapter | `WorldRenderFacade` | Immutable caller-held value | One main-thread query |
| Input | `WorldEnvironmentSnapshot` | World-state/capture adapter | `WorldRenderFacade` / `WowSkyController` | Immutable caller-held value; not retained | One main-thread update |
| Input | `EntityPresentationSnapshot` | Future world-state/preview adapter | `WorldRenderFacade` / entity presenter | Immutable caller-held full visual snapshot | One main-thread present/update |
| Input | `EntityId` removal command | Future entity registry/adapter | `WorldRenderFacade` / entity presenter | Immutable session-local value | One main-thread removal |
| Input | Map/configuration | Scene/app/editor | `StreamingWorldLoader` | Caller config, copied/read | Main thread/session |
| Input | WDT/ADT/M2/WMO/BLP bytes | Extracted asset repository | Native loaders/builders | Loader result owns parsed data | Worker or controlled load |
| Input | Baked resources | Cache build tools | Streamer/builders | Shared immutable resource/cache entry | Session/cache lifetime |
| Test input | Server-spawn render manifest | Coordinate fixture/verifier | Checkpoint capture tool | Repository-owned diagnostic data | Test-process lifetime |
| Output | Desired tile/detail operations | Streaming planner inside loader | Finalize queues | Loader-owned | Cross-frame |
| Output | `StreamingTargetPlan` | `StreamingTargetPlanner` | Streamer queue/state apply | Immutable ephemeral value | One refresh |
| Internal control | Per-frame lane limits and boolean permits | Streamer configuration / `RenderBudgetScheduler` | Ordered streamer drains | Scheduler-owned counters | Main thread/frame |
| Internal cache | Full-quality terrain Mesh/source | Loader terrain upgrade / `TerrainQualityMeshCache` | Revisited tile state | Cache-retained Mesh reference | Map session until eviction/reset |
| Internal plan | Parsed chunks, typed focus and immutable chunk-LOD policy | Loader / `TerrainChunkLodPlanner` | Loader desired state | Fresh dictionary; no retained resources | One target refresh |
| Internal plan | Current/desired chunk state and typed focus | Loader / `TerrainChunkGeometryQueuePlanner` | Loader chunk queues | Fresh request arrays; no retained resources | One queue rebuild |
| Internal registry | Tile key and M2 placement unique IDs | Loader / `M2UniquePlacementRegistry` | Filtered grouping input and tile retry state | Registry-owned strings; fresh result arrays | Map session |
| Internal transform | Rotation/path/scale | Loader or grouper / `M2PlacementTransformResolver` | Group/placeholder/instance transforms | Value-only Basis/Vector3 | One placement |
| Internal grouping | Tile origin, M2 names and placements | Loader / `M2PlacementGrouper` | Loader worker result/build job | Fresh Dictionary/Transform3D arrays | One grouping task |
| Internal batch plan | Transform count/offset, path kind and limits | Loader / `M2BuildBatchPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One build operation |
| Internal WMO placement | Path, MODF placement, tile/index | Loader / `WmoPlacementResolver` | WMO caches, registry and three instance adapters | Value-only String/Transform3D | Lookup/placement lifetime |
| Internal WMO ownership | Resolved placement key and tile/global reference key | Loader / `WmoPlacementRegistry` | Loader create/retain/final-free decisions | Registry-owned String sets; detached diagnostics | Map session or final release |
| Internal WMO build step | Mesh/MultiMesh counts and job cursors | Loader / `WmoRenderBuildStepPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One group operation |
| Internal WMO pending build | Placement key, Node3D root, WMO Resource and cursors | Loader / `WmoRenderBuildQueue` | Loader drain and step planner adapter | Queue-owned job and strong references | Until cancel/clear/replacement |
| Internal WMO render cache | Normalized path, cache path and validated Resource | Loader / `WmoRenderResourceCacheState` | Loader lookup, ResourceLoader poll and build queue | State-owned Resource/path references; detached request snapshots | Until transient/full clear |
| Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear |
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
| Output | `RenderedGroundSample` | `WorldRenderFacade`/loaded terrain | Renderer diagnostics | Immutable caller-held value | One query |
| Output | Detached rendered-ground snapshot | `WorldRenderFacade` | Terrain-height probe | Caller-owned deep copy | One query |
| Output | Entity visual Node3D subtree | `WorldEntityPresenter` | SceneTree/viewport | Presenter-owned child | Until replace/remove/teardown |
| Output | Detached entity inventory | `WorldRenderFacade` | Diagnostics/tests | Caller-owned deep copy | One query |
| Output | Logs/diagnostics | Hitch/stat instrumentation | Logs/baseline tools | Immutable records | Periodic/frame |
| Test output | Diagnostic spawn marker and cold/warm PNGs | Checkpoint capture tool | Human/integrator review | SceneTree marker; `user://` files | One capture process |
@@ -121,23 +203,52 @@ flowchart TD
A --> F[StreamingFocus]
EA --> F
F --> RF[WorldRenderFacade]
RF --> T[StreamingWorldLoader target refresh]
WDT[WDT tile catalog] --> T
RF --> Planner[StreamingTargetPlanner]
Planner --> T[StreamingTargetPlan apply]
WDT[WDT tile catalog] --> Planner
T --> Q[Tile load queue]
Q --> Parse[Worker parse/cache load]
Parse --> R[Result queues]
R --> B[Per-frame budget scheduler]
R --> TerrainCache[TerrainQualityMeshCache store]
TerrainCache --> Revisit[Revisited tile state restore]
R --> ChunkLod[TerrainChunkLodPlanner]
ChunkLod --> DesiredChunkLod[Desired chunk LOD state]
DesiredChunkLod --> ChunkQueue[TerrainChunkGeometryQueuePlanner]
ChunkQueue --> B
R --> M2Registry[M2UniquePlacementRegistry reserve]
M2Registry --> M2Grouper[M2PlacementGrouper]
M2Transform[M2PlacementTransformResolver] --> M2Grouper
M2Grouper --> M2Batch[M2BuildBatchPlanner]
M2Batch --> M2
R --> WmoPlacement[WmoPlacementResolver]
WmoPlacement --> WmoRegistry[WmoPlacementRegistry]
WmoRegistry --> WmoBuildQueue[WmoRenderBuildQueue]
WmoBuildQueue --> WmoBuildStep[WmoRenderBuildStepPlanner]
WmoBuildStep --> WMO
R --> B[RenderBudgetScheduler permits]
B --> Terrain[Terrain attach/upgrade]
B --> M2[M2 group/MultiMesh attach]
B --> WMO[WMO instance/group attach]
B --> Liquid[Liquid attach]
Loaded[Loaded terrain Mesh and Transform] --> Ground[Facade ground query]
Ground --> GroundSample[RenderedGroundSample / detached diagnostics]
WorldTime[Authoritative/test time] --> EnvironmentSnapshot[WorldEnvironmentSnapshot]
EnvironmentSnapshot --> RF
RF --> SkyController[WowSkyController]
DBC[Light/Area/Skybox DBC] --> SkyController
SkyController --> World
EntityReadModel[Entity visual read model] --> EntitySnapshot[EntityPresentationSnapshot]
EntitySnapshot --> RF
RF --> EntityPresenter[WorldEntityPresenter]
EntityAsset[PackedScene path] --> EntityPresenter
EntityPresenter --> World
Terrain --> World[Godot world]
M2 --> World
WMO --> World
Liquid --> World
Marker --> World
F --> E[Eviction/retention decisions]
E --> World
F --> Eviction[Eviction/retention decisions]
Eviction --> World
```
## Lifecycle/state
@@ -166,18 +277,35 @@ sequenceDiagram
participant Focus as StreamingFocus adapter
participant Facade as WorldRenderFacade
participant Stream as StreamingWorldLoader internal
participant Planner as StreamingTargetPlanner
participant Worker as Worker task
participant Budget as Main-thread budget
participant Budget as RenderBudgetScheduler
participant Render as SceneTree/RenderingServer
Source->>Focus: sample explicit Godot position
Focus->>Facade: set/refresh typed focus
Facade->>Stream: delegate focus and refresh
Stream->>Stream: desired tiles and priorities
Stream->>Planner: typed focus, catalog and target policy
Planner-->>Stream: immutable wanted/retained plan
Stream->>Stream: queue priority and loaded-tile LOD/detail state
Stream->>Worker: parse/load tile and detail data
Worker-->>Stream: immutable result
Stream->>Budget: enqueue finalize operations
Budget->>Render: attach bounded terrain/M2/WMO/liquid work
Stream->>Budget: begin_frame(operation limits)
Stream->>Budget: try_consume_permit(lane)
Budget-->>Stream: true while lane remains bounded
Stream->>Render: attach permitted terrain/M2/WMO/liquid work
Stream->>Render: evict outside retention range
Source->>Facade: sample_ground_height(typed position)
Facade->>Stream: sample loaded render terrain
Stream-->>Facade: RenderedGroundSample / diagnostic snapshot
Source->>Facade: apply_environment_snapshot(exact time)
Facade->>Render: delegate to WowSkyController
Render->>Render: freeze local clock; retain DBC sampling/apply rules
Source->>Facade: present_entity(full visual snapshot)
Facade->>Render: delegate to WorldEntityPresenter
Render->>Render: create/update/replace owned entity root
Source->>Facade: remove_entity(EntityId)
Facade->>Render: detach and queue_free owned root
Stream->>Budget: shutdown: cancel permit issuance
Stream->>Worker: shutdown: wait for WorkerThreadPool tasks
Stream->>Stream: shutdown: finish registered ResourceLoader requests
Stream->>Render: clear queues and owned tree nodes/RIDs
@@ -188,7 +316,54 @@ sequenceDiagram
- `StreamingWorldLoader` владеет tile states, queues, task registries, active world instances и render caches session scope.
- `WorldRenderFacade` owns only scene-relative adapter paths. It does not own the
streamer, focus source, returned metrics, queues, caches, nodes or RIDs.
streamer, environment controller, focus source, returned metrics, queues,
caches, nodes or RIDs.
- `WorldEnvironmentSnapshot` is an immutable caller-owned scalar value. Neither
facade nor sky controller retains it; the controller copies normalized hours
into its existing fixed-clock fields on the main thread.
- `WowSkyController` continues to own DBC sampling state and mutations of its
duplicated Godot `Environment`, sun, skybox node and world shader globals.
- `WorldEntityPresenter` owns one optional visual Node3D subtree per presented
`EntityId`; it owns no authoritative entity state and excludes the local player.
- Entity presentation snapshots remain caller-owned inputs. Detached diagnostics
expose only identity debug keys, paths and visibility, never Node/Resource/RID.
- `TerrainQualityMeshCache` owns only full-quality revisit Mesh references and
LRU keys. The loader retains terrain tasks/results, tile state, source choice,
cache versions and every material/Node/RID finalization side effect.
- `TerrainChunkLodPlanner` is stateless and owns only call-local horizontal
distance calculations. The loader retains parsed chunks, desired state,
queues, budgets and all Mesh/Node/RID application side effects.
- `TerrainChunkGeometryQueuePlanner` is stateless and owns only call-local
create/remove request calculation and sorting. The loader adopts the fresh
arrays and retains removal-first drains, budgets and render side effects.
- `M2UniquePlacementRegistry` owns only positive unique-key to tile-key strings.
The loader retains owned/skipped tile arrays, candidate retry, grouping/build
tasks, caches and all MultiMesh/Node/Mesh/material/RID side effects.
- `M2PlacementTransformResolver` is stateless and retains no resources. The
grouper owns worker-path final transforms; direct placeholder/instance
transforms and every build/render side effect remain loader-owned.
- `M2PlacementGrouper` is stateless and owns only call-local grouped transforms.
The loader retains tasks, mutex/result queues, stale checks and build state.
- `M2BuildBatchPlanner` is stateless and owns only call-local scalar plans. The
loader retains queue/resource transitions, cursor adoption and materialization.
- `WmoPlacementResolver` is stateless and owns only call-local cache-key,
identity and transform values. `WmoPlacementRegistry` owns only placement-key
reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation
and cursor plan. `WmoRenderBuildQueue` owns typed pending jobs, FIFO keys and
strong root/resource references without freeing engine objects.
`WmoRenderResourceCacheState` owns validated render Resources, negative entries
and pending cache paths. `WmoSceneResourceCacheState` similarly owns validated
PackedScenes, negative entries and pending `.tscn` paths. The loader retains
ResourceLoader/FileAccess I/O, size and cache-version validation, live fallback,
materialization, permits, validity reactions and every Node lifecycle action.
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
the facade never returns Mesh, Node, tile-state or queue references.
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
collision. M02 `TerrainQuery` composition and ADT cache ownership remain unchanged.
- `StreamingTargetPlanner` is stateless and owns only call-local calculations;
its immutable plan is consumed synchronously by the streamer.
- `RenderBudgetScheduler` owns only per-frame lane counters and a terminal
cancellation flag. The streamer retains queue ordering and all operation side effects.
- Focus producers own the immutable `StreamingFocus` reference; the loader retains
the latest reference and converts it to `Vector3` only inside the render boundary.
- Worker tasks не должны менять SceneTree и shared Resource concurrently.
@@ -207,10 +382,16 @@ sequenceDiagram
| Missing focus source | Explicit NodePath resolution | Retain current streamed content and prior valid focus | One warning until source recovers | Fix composition path or call `set_streaming_focus` |
| Missing renderer implementation | Facade path/method validation | Focus/metrics call fails without mutation | One facade diagnostic until recovery | Fix scene composition path/implementation |
| Invalid metrics result | Facade return-type validation | Return empty caller snapshot | Facade error | Restore `Dictionary` implementation contract |
| Invalid environment time | Snapshot finite-value validation | Reject update; retain prior sky clock | `environment_time_not_finite` on invalid value | Supply a finite time snapshot |
| Missing environment controller | Facade path/method validation | Reject environment update; other facade APIs remain available | One facade diagnostic until recovery | Fix scene composition path/implementation |
| Invalid entity visual snapshot | Contract validation | Reject without scene mutation | Stable `entity_presentation_*` failure code | Repair adapter read model and resend |
| Missing/invalid entity visual asset | Resource existence/load/root check | New entity absent or prior replacement retained | Named presenter warning | Restore asset mapping and resend snapshot |
| Missing entity presenter | Facade path/method validation | Reject entity commands; other facade APIs remain available | One facade diagnostic until recovery | Fix runtime scene composition |
| No focus supplied | Refresh contract | No target recomputation; queues continue draining | `refresh_streaming_focus()` returns `false` | Supply player/editor/capture focus |
| Cache version mismatch | Required format constant | Reject stale cache/fallback | Version diagnostic | Rebuild matching cache |
| Worker failure | Task result/empty data | Skip/degrade affected asset | Queue/task warning | Retry/rebuild source |
| Main-thread hitch | Named section timing | Frame spike, work remains queued | `HITCH` log | Lower budget/fix finalize path |
| Render terrain unavailable | Facade ground query | Immutable unavailable sample | Stable `render_terrain_*` failure code and optional snapshot | Wait for streaming or use gameplay-owned query backend |
| D3D12 descriptor exhaustion | Rendering backend error | Render failure/fallback backend | Godot error + baseline report | Dedup resources/fix settings |
| Teleport/map change | Focus/session transition | Old jobs become stale | Target/session generation | Cancel/drop stale results |
| Shutdown leak | Godot leak/RID diagnostics | Resource retained | Verbose shutdown report + cache shutdown verifier | Drain requests, preserve tree ownership, then free detached prototypes and clear resource caches |
@@ -249,6 +430,47 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
## Verification
- Unit/contract tests: streaming-focus contract/wiring, material mapping, unique-ID dedupe, placement probes, baseline manifest, five-point coordinate calibration, synthetic perceptual checkpoint diff, camera-pose grid plan.
- Pure planner contract: center/corner/catalog/clamp/editor cases plus bounded
High-like iteration timing without loading a world scene.
- Terrain chunk LOD contract: horizontal chunk-center distance, inclusive
thresholds, sparse indices, disabled/raw-custom policy, loader delegation and
bounded 250-by-256 planning without loading a world scene.
- Terrain chunk geometry queue contract: current/desired comparison, tile-LOD
takeover, sparse origin fallback, stable removals, nearest creates, loader
drain boundary and bounded multi-tile timing without a world scene.
- M2 unique placement contract: unkeyed pass-through, within-call/cross-tile
dedupe, owner release/retry, clear, detached diagnostics, loader boundary and
bounded reserve/release timing without a world scene.
- M2 placement transform contract: regular/cliffrock/waterfall formulas, path
normalization, scale compensation, all three consumers and bounded timing.
- M2 placement grouper contract: invalid/default records, historical path rules,
group order, local transforms, fresh output, source boundary and bounded timing.
- M2 build batch contract: static/animated limit selection, clamp, remaining
count, completion/cursor rules, source boundary and bounded timing.
- WMO placement contract: cache-path normalization, positive/fallback identity,
default/rotated/unclamped transforms, three consumers and bounded timing.
- WMO placement registry contract: first/idempotent/shared/non-owner/final/global
lifecycle, detached sorted diagnostics, source ownership and bounded timing.
- WMO render build step contract: mesh-before-MultiMesh order, one-index cursor
transition, completion/raw integer behavior, source ownership and bounded timing.
- WMO render build queue contract: typed references/cursors, FIFO, duplicate
replacement, stale-front cleanup, cancel/clear engine lifetime and bounded timing.
- WMO render Resource cache contract: invalid/duplicate request rejection,
validated/missing terminal transitions, transient/full reset, detached sorted
diagnostics, loader-owned version validation and bounded timing.
- WMO scene Resource cache contract: direct missing sources, pending terminal
transitions, transient/full reset, detached diagnostics, loader-owned size/
metadata validation and bounded timing.
- Budget scheduler contract: exact lane exhaustion, shared chunk removal/create
priority, independent lanes, frame reset, invalid limits, terminal cancellation,
dependency boundaries and bounded permit timing without loading a world scene.
- Internal-access contract: derives the current streamer's private queue/task/
cache/state field inventory and rejects external member/reflection access from
gameplay or EditorPlugin package code.
- Ground-query facade contract: available/unavailable typed result delegation,
detached nested diagnostics and terrain-probe source migration without private access.
- Rendered-ground value contract: available/non-finite/unavailable invariants pass
from a cold Godot class cache before editor import.
- Integration/E2E: Eastern Kingdoms/Kalimdor streaming scenes, seven M00
cold/warm checkpoints and one dedicated mapped server-spawn checkpoint.
- Fidelity evidence: семь локальных build 12340 reference JPG покрывают terrain/ADT/M2/WMO/liquid/sky и реальный `GryphonRoost01`; automated cold/warm metrics имеют `compared=14`, `missing=0`. Sky использует приблизительно paired camera, а animated-M2 остаётся asset-paired/placement-unpaired из-за synthetic probe.
@@ -268,13 +490,32 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|---|---|---|---|
| ADT streaming/terrain | Partial | M00 checkpoints and current scenes | Fidelity/performance gaps remain |
| Static M2 placement | Partial | MultiMesh/dedupe probes | Full materials/animation/effects |
| M2 unique placement registry | Implemented extraction | Scene-free ownership/lifecycle/timing contract and historical `uid:11785` smoke | Group/build/tasks/finalization and asset-backed p95/p99 remain pending |
| M2 placement transform resolver | Implemented extraction | Scene-free formula/source/timing contract across three consumers | Asset-backed visual recheck and general placement parity pending |
| M2 placement grouper | Implemented extraction | Scene-free validation/order/transform/source/timing contract | Worker/build state, spatial cells and asset-backed p95/p99 remain pending |
| M2 build batch planner | Implemented extraction | Scene-free limit/count/cursor/source/timing contract | Queue/resource state, spatial cells and asset-backed p95/p99 remain pending |
| WMO placement resolver | Implemented extraction | Scene-free path/identity/transform/source/timing contract | Asset-backed comparison pending |
| WMO placement registry | Implemented extraction | Scene-free ownership/lifecycle/source/timing contract | Build/resource state and asset-backed cross-tile corpus pending |
| WMO render build step planner | Implemented extraction | Scene-free order/cursor/source/timing contract | Asset-backed traversal p95/p99 pending |
| WMO render build queue | Implemented extraction | Typed lifecycle/order/ownership/source/timing contract | Materialization and asset-backed traversal/leak evidence pending |
| WMO render Resource cache state | Implemented extraction | Scene-free lifecycle/exclusivity/source/timing plus shutdown contract | ResourceLoader I/O and asset-backed traversal/leak evidence pending |
| WMO scene Resource cache state | Implemented extraction | Scene-free lifecycle/direct-missing/source/timing plus shutdown contract | ResourceLoader/live-fallback extraction and asset-backed traversal/leak evidence pending |
| WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity |
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
| Sky/light | Partial | DBC controller/logs | Zone transition paired validation |
| Character presentation | Prototype | Character creator/player experiment | Full customization/equipment states |
| Camera-independent streaming focus | Implemented | Typed contract, two player-focused runtime scenes and three explicitly camera-focused diagnostic tools | Multi-focus/direction/velocity belong to planner extraction |
| Golden server-spawn visualization | Implemented | Pinned AzerothCore fixture, validated renderer manifest and diagnostic origin-marker capture at ADT `(32,48)`/MCNK `(3,12)` | No TrinityCore row or original-client visual-parity claim |
| Stable renderer facade | Partial | `M03-RND-FACADE-001` contract/runtime/tool regressions | Focus and metrics migrated; environment/entity visuals/ground query remain |
| Stable renderer facade | Implemented boundary / Partial fidelity | Facade contracts cover focus, environment, entity visuals, metrics and loaded-mesh ground query | Production consumers and fidelity remain |
| Environment snapshot input | Implemented boundary / Partial fidelity | Immutable time contract, facade/controller delegation and checkpoint migration | Weather/indoor state and paired zone-transition fidelity remain |
| Entity presentation commands | Implemented boundary / Prototype visuals | Versioned typed snapshot plus present/remove lifecycle and runtime service composition | Display/equipment/animation/effects and async scale path remain |
| Terrain quality Mesh cache service | Implemented extraction | Scene-free admission/LRU/clear contract and loader delegation | Remaining terrain build/state/finalization is monolithic |
| Terrain chunk LOD planner | Implemented extraction | Scene-free formula/dependency/timing contract and loader delegation | Queue/state/finalization and asset-backed p95/p99 remain pending |
| Terrain chunk geometry queue planner | Implemented extraction | Scene-free request/order/dependency/timing contract and loader delegation | Queue drains/state/finalization and asset-backed p95/p99 remain pending |
| Pure streaming target planner | Implemented | `M03-RND-STREAMING-PLANNER-001` asset-free contract/performance regression | Queue scheduling and loaded-tile LOD application remain in streamer |
| Bounded render budget scheduler | Implemented | `M03-RND-SCHEDULER-001` asset-free permit/cancellation/performance regression | Queue storage, worker concurrency and operation execution remain in streamer/services |
| Gameplay/editor/tool internal-access boundary | Implemented | Access gate scans gameplay, EditorPlugin packages and terrain probe against actual private streamer declarations | Add future renderer diagnostic tools explicitly to the protected list |
| Rendered terrain ground query | Implemented | `M03-RND-FACADE-GROUND-QUERY-001` typed result/snapshot isolation and probe migration | Diagnostic Mesh ray only; not movement/collision authority |
## Known gaps and risks
@@ -295,16 +536,35 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
- D3D12 descriptor issues remain. Explicit shutdown ownership removes the GUI capture's Node/resource/RID leaks in focused and seven-checkpoint runs; a timing-dependent ObjectDB warning for anonymous zero-reference `RefCounted` objects remains as a separate engine-teardown diagnostic.
- M2/WMO/material/particle/ribbon/portal parity incomplete.
- Public API is mostly exported configuration rather than stable contracts.
- Rendered-ground sampling generates a triangle mesh on demand and retains the
historical nearby seam fallback. It is appropriate for diagnostics, not a
per-physics-frame gameplay query or a claim of exact terrain collision parity.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/world_render_facade.gd` | Stable caller boundary for typed focus and detached renderer metrics |
| `src/render/world_render_facade.gd` | Stable caller boundary for typed focus/environment, detached metrics and rendered-ground queries |
| `src/render/environment/world_environment_snapshot.gd` | Immutable normalized world time input with explicit invalid state |
| `src/render/entities/entity_presentation_snapshot.gd` | Immutable contract-v1 entity visual input and validation |
| `src/render/entities/world_entity_presenter.gd` | Main-thread entity visual subtree ownership and lifecycle |
| `src/render/terrain/rendered_ground_sample.gd` | Immutable renderer-owned available/unavailable ground query result |
| `src/render/terrain/terrain_quality_mesh_cache.gd` | Full-quality terrain revisit Mesh retention and LRU ownership |
| `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets |
| `src/render/streaming/render_budget_scheduler.gd` | Scene-free per-frame operation permits and terminal cancellation |
| `src/scenes/streaming/streaming_world_loader.gd` | Current runtime orchestration, queues, caches and attachment |
| `src/domain/streaming/streaming_focus.gd` | Immutable camera-independent streaming position contract |
| `src/tools/verify_streaming_focus.gd` | Headless contract, dependency and runtime/tool wiring regression |
| `src/tools/verify_world_render_facade.gd` | Facade delegation, snapshot isolation and consumer wiring regression |
| `src/tools/verify_world_render_facade.gd` | Focus/environment/metrics/ground delegation, snapshot isolation and consumer wiring regression |
| `src/tools/verify_world_environment_snapshot.gd` | Cold-start environment value normalization and invalid-input 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_terrain_quality_mesh_cache.gd` | Terrain cache admission, LRU, ownership and loader-boundary regression |
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
| `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
| `addons/mpq_extractor/loaders/adt_builder.gd` | Terrain/material/liquid construction |
| `addons/mpq_extractor/loaders/m2_builder.gd` | Static M2 mesh/material construction |
| `addons/mpq_extractor/loaders/m2_native_animated_builder.gd` | Native animated M2 experiment |
@@ -320,7 +580,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/tools/verify_server_spawn_renderer.gd` | Fixture-to-renderer checkpoint contract verification |
| `tools/sweep_render_checkpoint_camera_pose.ps1` | Offline yaw/pitch capture grid and paired-error ranking |
| `src/tools/verify_render_coordinate_calibration.gd` | Build 12340 camera-coordinate golden point round-trip diagnostic |
| `src/tools/probe_render_terrain_height.gd` | Offline active-mesh terrain height and camera-clearance report |
| `src/tools/probe_render_terrain_height.gd` | Offline facade-backed active-mesh terrain height and camera-clearance report |
| `src/tools/probe_render_camera_occluders.gd` | Scene-tree placement containment and camera-to-target AABB intersection report |
## Related decisions and references
@@ -0,0 +1,104 @@
class_name EntityPresentationSnapshot
extends RefCounted
## Immutable renderer input for one session-local world entity visual.
## Gameplay authority, identity allocation and asset selection remain external.
const CONTRACT_VERSION := 1
const SCRIPT_PATH := "res://src/render/entities/entity_presentation_snapshot.gd"
const ENTITY_ID_SCRIPT := preload("res://src/domain/identity/entity_id.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
var is_valid: bool:
get:
return _failure_code.is_empty()
var failure_code: StringName:
get:
return _failure_code
var entity_id: RefCounted:
get:
return _entity_id
var world_position: RefCounted:
get:
return _world_position
var visual_scene_path: String:
get:
return _visual_scene_path
var yaw_radians: float:
get:
return _yaw_radians
var uniform_scale: float:
get:
return _uniform_scale
var is_visible: bool:
get:
return _is_visible
var _failure_code: StringName
var _entity_id: RefCounted
var _world_position: RefCounted
var _visual_scene_path: String
var _yaw_radians: float
var _uniform_scale: float
var _is_visible: bool
## Creates a version-1 full visual snapshot. Invalid values remain inspectable and
## are rejected without scene mutation by the facade/presenter.
static func create(
entity_id_value: RefCounted,
world_position_value: RefCounted,
visual_scene_path_value: String,
yaw_radians_value: float = 0.0,
uniform_scale_value: float = 1.0,
is_visible_value: bool = true
) -> RefCounted:
return load(SCRIPT_PATH).new(
entity_id_value,
world_position_value,
visual_scene_path_value,
yaw_radians_value,
uniform_scale_value,
is_visible_value
)
func _init(
entity_id_value: RefCounted,
world_position_value: RefCounted,
visual_scene_path_value: String,
yaw_radians_value: float,
uniform_scale_value: float,
is_visible_value: bool
) -> void:
_entity_id = entity_id_value
_world_position = world_position_value
_visual_scene_path = visual_scene_path_value.strip_edges()
_yaw_radians = yaw_radians_value
_uniform_scale = uniform_scale_value
_is_visible = is_visible_value
_failure_code = _validate()
func _validate() -> StringName:
if _entity_id == null or _entity_id.get_script() != ENTITY_ID_SCRIPT or not bool(_entity_id.call("is_valid")):
return &"entity_presentation_invalid_entity_id"
if _world_position == null or _world_position.get_script() != GODOT_WORLD_POSITION_SCRIPT:
return &"entity_presentation_invalid_world_position"
for coordinate_name in [&"x_units", &"y_units", &"z_units"]:
if not is_finite(float(_world_position.get(coordinate_name))):
return &"entity_presentation_position_not_finite"
if _visual_scene_path.is_empty():
return &"entity_presentation_visual_path_empty"
if not is_finite(_yaw_radians):
return &"entity_presentation_yaw_not_finite"
if not is_finite(_uniform_scale) or _uniform_scale <= 0.0:
return &"entity_presentation_scale_invalid"
return &""
@@ -0,0 +1 @@
uid://djvnorls0yf06
@@ -0,0 +1,130 @@
@tool
class_name WorldEntityPresenter
extends Node3D
## Main-thread owner of visual subtrees for session-local world entities.
## The full immutable snapshot is reapplied idempotently by entity identity.
const ENTITY_PRESENTATION_SNAPSHOT_SCRIPT := preload("res://src/render/entities/entity_presentation_snapshot.gd")
const ENTITY_ID_SCRIPT := preload("res://src/domain/identity/entity_id.gd")
var _presented_entities: Dictionary = {}
## Creates or updates one entity visual. A changed visual path is instantiated
## successfully before the previous subtree is detached, so failed replacement
## retains the last valid presentation.
func present_entity(entity_presentation_snapshot: RefCounted) -> bool:
if not _is_valid_snapshot(entity_presentation_snapshot):
return false
var entity_id: RefCounted = entity_presentation_snapshot.get("entity_id")
var entity_key := _entity_key(entity_id)
var visual_scene_path := String(entity_presentation_snapshot.get("visual_scene_path"))
var current_entry: Dictionary = _presented_entities.get(entity_key, {})
var visual_root := current_entry.get("visual_root") as Node3D
if visual_root == null or not is_instance_valid(visual_root) or String(current_entry.get("visual_scene_path", "")) != visual_scene_path:
var replacement_root := _instantiate_visual_root(entity_id, visual_scene_path)
if replacement_root == null:
return false
if visual_root != null and is_instance_valid(visual_root):
remove_child(visual_root)
add_child(replacement_root)
_apply_snapshot_transform(replacement_root, entity_presentation_snapshot)
if visual_root != null and is_instance_valid(visual_root):
visual_root.queue_free()
visual_root = replacement_root
else:
_apply_snapshot_transform(visual_root, entity_presentation_snapshot)
_presented_entities[entity_key] = {
"visual_root": visual_root,
"visual_scene_path": visual_scene_path,
"snapshot": entity_presentation_snapshot,
}
return true
## Removes one owned entity visual. Returns false for an invalid/unknown identity.
func remove_entity(entity_id: RefCounted) -> bool:
if entity_id == null or entity_id.get_script() != ENTITY_ID_SCRIPT or not bool(entity_id.call("is_valid")):
return false
var entity_key := _entity_key(entity_id)
if not _presented_entities.has(entity_key):
return false
var entry: Dictionary = _presented_entities[entity_key]
var visual_root := entry.get("visual_root") as Node3D
_presented_entities.erase(entity_key)
if visual_root != null and is_instance_valid(visual_root):
remove_child(visual_root)
visual_root.queue_free()
return true
## Returns detached scalar diagnostics without exposing owned Nodes or snapshots.
func entity_presentation_snapshot() -> Dictionary:
var entities: Array[Dictionary] = []
var sorted_entity_keys: Array = _presented_entities.keys()
sorted_entity_keys.sort()
for entity_key_variant in sorted_entity_keys:
var entity_key := String(entity_key_variant)
var entry: Dictionary = _presented_entities[entity_key]
var snapshot: RefCounted = entry.get("snapshot")
entities.append({
"entity_key": entity_key,
"visual_scene_path": String(entry.get("visual_scene_path", "")),
"visible": bool(snapshot.get("is_visible")),
})
return {
"contract_version": ENTITY_PRESENTATION_SNAPSHOT_SCRIPT.CONTRACT_VERSION,
"entity_count": entities.size(),
"entities": entities,
}
func _exit_tree() -> void:
_presented_entities.clear()
func _is_valid_snapshot(entity_presentation_snapshot: RefCounted) -> bool:
return (
entity_presentation_snapshot != null
and entity_presentation_snapshot.get_script() == ENTITY_PRESENTATION_SNAPSHOT_SCRIPT
and bool(entity_presentation_snapshot.get("is_valid"))
)
func _entity_key(entity_id: RefCounted) -> String:
return String(entity_id.call("to_debug_key"))
func _instantiate_visual_root(entity_id: RefCounted, visual_scene_path: String) -> Node3D:
if not ResourceLoader.exists(visual_scene_path, "PackedScene"):
push_warning("WorldEntityPresenter cannot find visual scene: %s" % visual_scene_path)
return null
var visual_scene := load(visual_scene_path) as PackedScene
if visual_scene == null:
push_warning("WorldEntityPresenter cannot load visual scene: %s" % visual_scene_path)
return null
var visual_instance := visual_scene.instantiate()
if not (visual_instance is Node3D):
visual_instance.free()
push_warning("WorldEntityPresenter visual scene root is not Node3D: %s" % visual_scene_path)
return null
var visual_root := visual_instance as Node3D
visual_root.name = "EntityVisual_%020d" % int(entity_id.get("session_sequence"))
return visual_root
func _apply_snapshot_transform(visual_root: Node3D, entity_presentation_snapshot: RefCounted) -> void:
var world_position: RefCounted = entity_presentation_snapshot.get("world_position")
var position_units := Vector3(
float(world_position.get("x_units")),
float(world_position.get("y_units")),
float(world_position.get("z_units"))
)
var uniform_scale := float(entity_presentation_snapshot.get("uniform_scale"))
var yaw_radians := float(entity_presentation_snapshot.get("yaw_radians"))
visual_root.global_transform = Transform3D(
Basis(Vector3.UP, yaw_radians).scaled(Vector3.ONE * uniform_scale),
position_units
)
visual_root.visible = bool(entity_presentation_snapshot.get("is_visible"))
@@ -0,0 +1 @@
uid://b0keiyi1wuu5i
@@ -0,0 +1,40 @@
class_name WorldEnvironmentSnapshot
extends RefCounted
## Immutable renderer input describing the authoritative time within one world day.
## DBC light, area, fog and skybox selection remain owned by the sky controller.
const HOURS_PER_DAY := 24.0
const SCRIPT_PATH := "res://src/render/environment/world_environment_snapshot.gd"
var is_valid: bool:
get:
return _is_valid
var time_of_day_hours: float:
get:
return _time_of_day_hours
var failure_code: StringName:
get:
return _failure_code
var _is_valid: bool
var _time_of_day_hours: float
var _failure_code: StringName
## Creates a snapshot and normalizes finite input into the half-open [0, 24) day.
## Non-finite input produces an explicit invalid value instead of renderer state.
static func at_time_of_day(time_of_day_hours_value: float) -> RefCounted:
return load(SCRIPT_PATH).new(time_of_day_hours_value)
func _init(time_of_day_hours_value: float) -> void:
_is_valid = is_finite(time_of_day_hours_value)
_time_of_day_hours = (
fposmod(time_of_day_hours_value, HOURS_PER_DAY)
if _is_valid
else 0.0
)
_failure_code = &"" if _is_valid else &"environment_time_not_finite"
@@ -0,0 +1 @@
uid://dass4mi3ex2fu
+30
View File
@@ -0,0 +1,30 @@
class_name M2BuildBatchPlanner
extends RefCounted
## Stateless M2 build batch sizing and group-cursor progression.
## Plans one static or animated build batch. The returned Dictionary is detached
## and contains effective_batch_size, batch_count, next_offset and group_complete.
func plan_batch(
transform_count: int,
current_offset: int,
has_animated_prototype: bool,
animated_batch_limit: int,
static_batch_limit: int
) -> Dictionary:
var selected_batch_limit := (
animated_batch_limit if has_animated_prototype else static_batch_limit
)
var effective_batch_size := maxi(1, selected_batch_limit)
var remaining_transform_count := maxi(0, transform_count - current_offset)
var batch_count := mini(effective_batch_size, remaining_transform_count)
var is_group_complete := (
batch_count <= 0 or current_offset + batch_count >= transform_count
)
return {
"effective_batch_size": effective_batch_size,
"batch_count": batch_count,
"next_offset": 0 if is_group_complete else current_offset + batch_count,
"group_complete": is_group_complete,
}
@@ -0,0 +1 @@
uid://d0my7yntfsvuy
+65
View File
@@ -0,0 +1,65 @@
class_name M2PlacementGrouper
extends RefCounted
## Stateless ADT M2 placement validation, normalization and transform grouping.
const M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT := preload(
"res://src/render/m2/m2_placement_transform_resolver.gd"
)
## Groups valid placements by normalized relative M2 path while preserving
## placement order inside each group. Returned containers are caller-owned.
func group_placements(
tile_origin: Vector3,
m2_names: PackedStringArray,
m2_placements: Array
) -> Dictionary:
var groups: Dictionary = {}
var placement_transform_resolver := (
M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()
)
for placement_variant in m2_placements:
if not (placement_variant is Dictionary):
continue
var placement: Dictionary = placement_variant
var name_id: int = int(placement.get("name_id", -1))
if name_id < 0 or name_id >= m2_names.size():
continue
var normalized_relative_path := _normalize_relative_path(m2_names[name_id])
if normalized_relative_path.is_empty():
continue
var world_position: Vector3 = placement.get("pos", Vector3.ZERO)
var local_position := world_position - tile_origin
var rotation_radians: Vector3 = placement.get("rot", Vector3.ZERO)
var scale_value := float(placement.get("scale", 1.0))
var placement_basis: Basis = placement_transform_resolver.resolve_basis(
rotation_radians,
normalized_relative_path
)
var origin_offset: Vector3 = placement_transform_resolver.resolve_origin_offset(
rotation_radians,
normalized_relative_path,
scale_value
)
var local_transform := Transform3D(
placement_basis.scaled(Vector3.ONE * maxf(scale_value, 0.0001)),
local_position + origin_offset
)
if not groups.has(normalized_relative_path):
groups[normalized_relative_path] = []
(groups[normalized_relative_path] as Array).append(local_transform)
return groups
func _normalize_relative_path(relative_path: String) -> String:
var normalized_relative_path := relative_path.replace("\\", "/")
if (
normalized_relative_path.ends_with(".mdx")
or normalized_relative_path.ends_with(".mdl")
):
normalized_relative_path = normalized_relative_path.get_basename() + ".m2"
return normalized_relative_path
@@ -0,0 +1 @@
uid://dkktxa5essfn
@@ -0,0 +1,110 @@
class_name M2PlacementTransformResolver
extends RefCounted
## Stateless ADT M2 placement orientation and origin-correction rules.
const CLIFFROCK_WORLD_YAW_OFFSET := PI
const WATERFALL_WORLD_YAW_OFFSET := PI * 0.5
## Resolves the unscaled Godot-space basis for one ADT M2 placement.
func resolve_basis(rotation_radians: Vector3, relative_m2_path: String = "") -> Basis:
if not _is_waterfall_path(relative_m2_path):
return _resolve_regular_basis(rotation_radians, relative_m2_path)
var placement_basis := Basis.from_euler(rotation_radians)
return (
Basis(Vector3.UP, WATERFALL_WORLD_YAW_OFFSET)
* placement_basis
* Basis(
_resolve_waterfall_local_fall_axis(relative_m2_path),
_resolve_waterfall_sheet_twist(relative_m2_path)
)
)
## Resolves local origin compensation for a twisted waterfall sheet. Regular
## models and waterfall models without a measured anchor return zero.
func resolve_origin_offset(
rotation_radians: Vector3,
relative_m2_path: String,
scale_value: float
) -> Vector3:
if not _is_waterfall_path(relative_m2_path):
return Vector3.ZERO
var twist_radians := _resolve_waterfall_sheet_twist(relative_m2_path)
if is_zero_approx(twist_radians):
return Vector3.ZERO
var anchor_local_position := _resolve_waterfall_sheet_twist_anchor(relative_m2_path)
if anchor_local_position == Vector3.ZERO:
return Vector3.ZERO
var placement_basis := (
Basis(Vector3.UP, WATERFALL_WORLD_YAW_OFFSET)
* Basis.from_euler(rotation_radians)
)
var twisted_basis := placement_basis * Basis(
_resolve_waterfall_local_fall_axis(relative_m2_path),
twist_radians
)
return (
placement_basis * anchor_local_position
- twisted_basis * anchor_local_position
) * maxf(scale_value, 0.0001)
func _resolve_regular_basis(rotation_radians: Vector3, relative_m2_path: String) -> Basis:
if not _is_elwynn_cliffrock_path(relative_m2_path):
return Basis.from_euler(rotation_radians)
return (
Basis(Vector3.UP, rotation_radians.y + CLIFFROCK_WORLD_YAW_OFFSET)
* Basis(Vector3.BACK, rotation_radians.x)
* Basis(Vector3.RIGHT, -rotation_radians.z)
)
func _is_waterfall_path(relative_m2_path: String) -> bool:
var model_name := _normalized_model_name(relative_m2_path)
return model_name == "newwaterfall" or model_name == "elwynntallwaterfall01"
func _is_elwynn_cliffrock_path(relative_m2_path: String) -> bool:
var model_name := _normalized_model_name(relative_m2_path)
return model_name == "elwynncliffrock01" or model_name == "elwynncliffrock02"
func _normalized_model_name(relative_m2_path: String) -> String:
return relative_m2_path.replace("\\", "/").to_lower().get_file().get_basename()
func _resolve_waterfall_local_fall_axis(relative_m2_path: String) -> Vector3:
var normalized_path := relative_m2_path.replace("\\", "/").to_lower()
if normalized_path.ends_with("newwaterfall.m2") or normalized_path.ends_with("newwaterfall.mdx"):
return Vector3(-0.138742, 0.990329, 0.0).normalized()
if (
normalized_path.ends_with("elwynntallwaterfall01.m2")
or normalized_path.ends_with("elwynntallwaterfall01.mdx")
):
return Vector3(-0.068962, 0.997619, 0.0).normalized()
return Vector3.UP
func _resolve_waterfall_sheet_twist(relative_m2_path: String) -> float:
var normalized_path := relative_m2_path.replace("\\", "/").to_lower()
if normalized_path.ends_with("newwaterfall.m2") or normalized_path.ends_with("newwaterfall.mdx"):
return PI * 0.5
if (
normalized_path.ends_with("elwynntallwaterfall01.m2")
or normalized_path.ends_with("elwynntallwaterfall01.mdx")
):
return -PI * 0.5
return 0.0
func _resolve_waterfall_sheet_twist_anchor(relative_m2_path: String) -> Vector3:
var normalized_path := relative_m2_path.replace("\\", "/").to_lower()
if (
normalized_path.ends_with("elwynntallwaterfall01.m2")
or normalized_path.ends_with("elwynntallwaterfall01.mdx")
):
return Vector3(-2.667799, 89.62273, 0.00129)
return Vector3.ZERO
@@ -0,0 +1 @@
uid://bcnmp7y8ph3qa
@@ -0,0 +1,88 @@
class_name M2UniquePlacementRegistry
extends RefCounted
## Session-memory ownership registry for positive ADT MDDF unique IDs.
var _owner_tile_by_unique_key: Dictionary = {}
## Reserves unowned positive unique IDs for one tile and returns fresh arrays.
## Unkeyed placements pass through without entering registry ownership.
func reserve(tile_key: String, m2_placements: Array) -> Dictionary:
var filtered_placements: Array = []
var unique_keys: Array = []
var skipped_unique_keys: Array = []
for placement_variant in m2_placements:
if not (placement_variant is Dictionary):
continue
var placement: Dictionary = placement_variant
var unique_key := _unique_key_for_placement(placement)
if unique_key.is_empty():
filtered_placements.append(placement)
continue
if _owner_tile_by_unique_key.has(unique_key):
var owner_tile := String(_owner_tile_by_unique_key[unique_key])
if owner_tile == tile_key and unique_keys.has(unique_key):
continue
if owner_tile != tile_key:
if not skipped_unique_keys.has(unique_key):
skipped_unique_keys.append(unique_key)
continue
_owner_tile_by_unique_key[unique_key] = tile_key
if not unique_keys.has(unique_key):
unique_keys.append(unique_key)
filtered_placements.append(placement)
return {
"placements": filtered_placements,
"unique_keys": unique_keys,
"skipped_unique_keys": skipped_unique_keys,
}
## Releases only keys currently owned by the supplied tile and returns them in
## input order so the loader can notify waiting candidate tiles.
func release(tile_key: String, unique_keys: Array) -> Array:
var released_unique_keys: Array = []
for unique_key_variant in unique_keys:
var unique_key := String(unique_key_variant)
if unique_key.is_empty() or not _owner_tile_by_unique_key.has(unique_key):
continue
if String(_owner_tile_by_unique_key[unique_key]) != tile_key:
continue
_owner_tile_by_unique_key.erase(unique_key)
released_unique_keys.append(unique_key)
return released_unique_keys
## Removes all reservations during world reset or renderer shutdown.
func clear() -> void:
_owner_tile_by_unique_key.clear()
## Returns the number of currently owned positive unique IDs.
func active_count() -> int:
return _owner_tile_by_unique_key.size()
## Returns detached, key-sorted scalar diagnostics without registry references.
func diagnostic_snapshot() -> Dictionary:
var sorted_unique_keys: Array = _owner_tile_by_unique_key.keys()
sorted_unique_keys.sort()
var owners: Array = []
for unique_key_variant in sorted_unique_keys:
var unique_key := String(unique_key_variant)
owners.append({
"unique_key": unique_key,
"tile_key": String(_owner_tile_by_unique_key[unique_key]),
})
return {
"active_count": owners.size(),
"owners": owners,
}
func _unique_key_for_placement(placement: Dictionary) -> String:
var unique_id := int(placement.get("unique_id", -1))
if unique_id <= 0:
return ""
return "uid:%d" % unique_id
@@ -0,0 +1 @@
uid://cqdvx0qnpcte1
@@ -0,0 +1,84 @@
## Issues bounded, per-frame permits for renderer main-thread operation lanes.
##
## The scheduler owns counters only. Queue contents, operation ordering and the
## side effects performed after a permit remain owned by the caller. One instance
## belongs to one renderer lifecycle and must only be used on the main thread.
class_name RenderBudgetScheduler
extends RefCounted
const TILE_FINALIZE: StringName = &"tile_finalize"
const TERRAIN_UPGRADE_FINALIZE: StringName = &"terrain_upgrade_finalize"
const TERRAIN_CONTROL_SPLAT_FINALIZE: StringName = &"terrain_control_splat_finalize"
const TERRAIN_SPLAT_CACHE_FINALIZE: StringName = &"terrain_splat_cache_finalize"
const TERRAIN_SPLAT_BUILD: StringName = &"terrain_splat_build"
const WATER_FINALIZE: StringName = &"water_finalize"
const CHUNK_GEOMETRY: StringName = &"chunk_geometry"
const TILE_LOAD_START: StringName = &"tile_load_start"
const TILE_LOD_CREATE: StringName = &"tile_lod_create"
const TILE_LOD_REMOVE: StringName = &"tile_lod_remove"
const M2_ANIMATION_FINALIZE: StringName = &"m2_animation_finalize"
const M2_MESH_FINALIZE: StringName = &"m2_mesh_finalize"
const M2_BUILD: StringName = &"m2_build"
const WMO_BUILD: StringName = &"wmo_build"
const WMO_RENDER_GROUP_BUILD: StringName = &"wmo_render_group_build"
const DETAIL_ASSET_SYNC: StringName = &"detail_asset_sync"
var _remaining_permits_by_lane: Dictionary = {}
var _consumed_permits_by_lane: Dictionary = {}
var _is_cancelled := false
## Replaces all lane limits for a new frame. Negative limits are treated as zero.
## Calling this after [method cancel] leaves the scheduler cancelled.
func begin_frame(operation_limits_by_lane: Dictionary) -> void:
_remaining_permits_by_lane.clear()
_consumed_permits_by_lane.clear()
if _is_cancelled:
return
for lane_variant in operation_limits_by_lane:
var lane := StringName(lane_variant)
_remaining_permits_by_lane[lane] = maxi(0, int(operation_limits_by_lane[lane_variant]))
_consumed_permits_by_lane[lane] = 0
## Returns whether a lane can issue another permit without consuming it.
## Unknown lanes and cancelled schedulers return false.
func has_remaining_permit(lane: StringName) -> bool:
if _is_cancelled:
return false
return int(_remaining_permits_by_lane.get(lane, 0)) > 0
## Consumes one permit from [param lane] and returns true when it was granted.
## Unknown, exhausted or cancelled lanes return false without changing state.
func try_consume_permit(lane: StringName) -> bool:
if not has_remaining_permit(lane):
return false
_remaining_permits_by_lane[lane] = int(_remaining_permits_by_lane[lane]) - 1
_consumed_permits_by_lane[lane] = int(_consumed_permits_by_lane.get(lane, 0)) + 1
return true
## Returns the unconsumed permit count for one lane.
func remaining_permits(lane: StringName) -> int:
if _is_cancelled:
return 0
return int(_remaining_permits_by_lane.get(lane, 0))
## Returns a detached diagnostic snapshot of permits consumed in this frame.
func consumed_permits_snapshot() -> Dictionary:
return _consumed_permits_by_lane.duplicate()
## Permanently rejects new permits for this scheduler instance and clears limits.
## In-flight work remains the caller's responsibility and is not interrupted.
func cancel() -> void:
_is_cancelled = true
_remaining_permits_by_lane.clear()
## Returns whether [method cancel] ended this scheduler lifecycle.
func is_cancelled() -> bool:
return _is_cancelled
@@ -0,0 +1 @@
uid://byaii7lec82i6
@@ -0,0 +1,37 @@
class_name StreamingTargetPlan
extends RefCounted
## Immutable result of one runtime or editor target-planning operation.
## Tile-key sets use the streamer's existing `<tile_x>_<tile_y>` representation.
var focus_tile: AdtTileCoordinate:
get:
return _focus_tile
var _focus_tile: AdtTileCoordinate
var _wanted_tile_keys: Dictionary
var _retained_tile_keys: Dictionary
## Takes ownership of planner-created sets and freezes them. The planner creates
## these dictionaries independently from its catalog input, so no copy is needed.
func _init(
focus_tile_value: AdtTileCoordinate,
wanted_tile_keys_value: Dictionary,
retained_tile_keys_value: Dictionary
) -> void:
_focus_tile = focus_tile_value
_wanted_tile_keys = wanted_tile_keys_value
_retained_tile_keys = retained_tile_keys_value
_wanted_tile_keys.make_read_only()
_retained_tile_keys.make_read_only()
## Returns the immutable wanted tile-key set.
func wanted_tile_keys() -> Dictionary:
return _wanted_tile_keys
## Returns the immutable retained tile-key set.
func retained_tile_keys() -> Dictionary:
return _retained_tile_keys
@@ -0,0 +1 @@
uid://fq1ax8tdju2q
@@ -0,0 +1,104 @@
class_name StreamingTargetPlanner
extends RefCounted
## Scene-free deterministic calculator for runtime/editor ADT target sets.
## It creates no tasks or renderer resources and never mutates its inputs.
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
const TARGET_PLAN_SCRIPT := preload("res://src/render/streaming/streaming_target_plan.gd")
## Builds runtime wanted and retained sets, including the existing edge/corner
## prefetch centers. `available_tile_paths_by_key` is read only for membership.
func plan_runtime(
available_tile_paths_by_key: Dictionary,
focus_position: GodotWorldPosition,
policy
):
var focus_tile = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(focus_position)
var prefetch_centers := _prefetch_centers(focus_position, focus_tile, policy)
var wanted_tile_keys := _available_keys_within_radius(
available_tile_paths_by_key,
prefetch_centers,
policy.load_tile_radius()
)
var retained_tile_keys := _available_keys_within_radius(
available_tile_paths_by_key,
prefetch_centers,
policy.retained_tile_radius()
)
return TARGET_PLAN_SCRIPT.new(focus_tile, wanted_tile_keys, retained_tile_keys)
## Builds the editor square preview set. Editor wanted and retained sets are
## identical and a negative radius is clamped to the focus tile.
func plan_editor(
available_tile_paths_by_key: Dictionary,
focus_position: GodotWorldPosition,
editor_preview_tile_radius: int
):
var focus_tile = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(focus_position)
var centers: Array[AdtTileCoordinate] = [focus_tile]
var wanted_tile_keys := _available_keys_within_radius(
available_tile_paths_by_key,
centers,
maxi(0, editor_preview_tile_radius)
)
return TARGET_PLAN_SCRIPT.new(focus_tile, wanted_tile_keys, wanted_tile_keys)
func _prefetch_centers(
focus_position: GodotWorldPosition,
focus_tile: AdtTileCoordinate,
policy
) -> Array[AdtTileCoordinate]:
var centers: Array[AdtTileCoordinate] = [focus_tile]
var threshold: float = policy.clamped_boundary_prefetch_threshold()
if threshold <= 0.0:
return centers
var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(focus_position)
var local_x_fraction: float = tile_local_position.east_yards / COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
var local_z_fraction: float = tile_local_position.south_yards / COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
var tile_x_offsets: Array[int] = [0]
var tile_y_offsets: Array[int] = [0]
if local_x_fraction <= threshold:
tile_x_offsets.append(-1)
elif local_x_fraction >= 1.0 - threshold:
tile_x_offsets.append(1)
if local_z_fraction <= threshold:
tile_y_offsets.append(-1)
elif local_z_fraction >= 1.0 - threshold:
tile_y_offsets.append(1)
for tile_x_offset in tile_x_offsets:
for tile_y_offset in tile_y_offsets:
if tile_x_offset == 0 and tile_y_offset == 0:
continue
centers.append(ADT_TILE_COORDINATE_SCRIPT.new(
focus_tile.tile_x + tile_x_offset,
focus_tile.tile_y + tile_y_offset
))
return centers
func _available_keys_within_radius(
available_tile_paths_by_key: Dictionary,
centers: Array[AdtTileCoordinate],
tile_radius: int
) -> Dictionary:
var available_tile_keys: Dictionary = {}
for center in centers:
for tile_y in range(center.tile_y - tile_radius, center.tile_y + tile_radius + 1):
for tile_x in range(center.tile_x - tile_radius, center.tile_x + tile_radius + 1):
var tile_key := _tile_key(tile_x, tile_y)
if available_tile_paths_by_key.has(tile_key):
available_tile_keys[tile_key] = true
return available_tile_keys
func _tile_key(tile_x: int, tile_y: int) -> String:
return "%d_%d" % [tile_x, tile_y]
@@ -0,0 +1 @@
uid://crqjbgoe3f6ea
@@ -0,0 +1,108 @@
class_name StreamingTargetPolicy
extends RefCounted
## Immutable runtime tile-radius and boundary-prefetch policy consumed by
## [StreamingTargetPlanner]. Values preserve scene configuration exactly;
## effective radii and thresholds apply the existing renderer clamps.
const CHUNKS_PER_ADT_TILE := 16
var lod2_radius_chunks: int:
get:
return _lod2_radius_chunks
var lod2_tile_radius: int:
get:
return _lod2_tile_radius
var warm_tile_margin: int:
get:
return _warm_tile_margin
var prewarm_tile_margin: int:
get:
return _prewarm_tile_margin
var retain_tile_margin: int:
get:
return _retain_tile_margin
var boundary_prefetch_threshold: float:
get:
return _boundary_prefetch_threshold
var _lod2_radius_chunks: int
var _lod2_tile_radius: int
var _warm_tile_margin: int
var _prewarm_tile_margin: int
var _retain_tile_margin: int
var _boundary_prefetch_threshold: float
## Captures one renderer configuration snapshot for a deterministic plan.
func _init(
lod2_radius_chunks_value: int,
lod2_tile_radius_value: int,
warm_tile_margin_value: int,
prewarm_tile_margin_value: int,
retain_tile_margin_value: int,
boundary_prefetch_threshold_value: float
) -> void:
_lod2_radius_chunks = lod2_radius_chunks_value
_lod2_tile_radius = lod2_tile_radius_value
_warm_tile_margin = warm_tile_margin_value
_prewarm_tile_margin = prewarm_tile_margin_value
_retain_tile_margin = retain_tile_margin_value
_boundary_prefetch_threshold = boundary_prefetch_threshold_value
## Returns the current visible ADT radius derived from chunk and tile settings.
func visible_tile_radius() -> int:
return calculate_visible_tile_radius(_lod2_radius_chunks, _lod2_tile_radius, _warm_tile_margin)
## Returns the visible radius plus the non-negative prewarm margin.
func load_tile_radius() -> int:
return calculate_load_tile_radius(
_lod2_radius_chunks,
_lod2_tile_radius,
_warm_tile_margin,
_prewarm_tile_margin
)
## Returns the load radius plus the non-negative hysteresis margin.
func retained_tile_radius() -> int:
return load_tile_radius() + maxi(0, _retain_tile_margin)
## Returns the edge fraction accepted by the existing prefetch policy.
func clamped_boundary_prefetch_threshold() -> float:
return clampf(_boundary_prefetch_threshold, 0.0, 0.49)
## Calculates the visible radius without allocating a policy. The streamer uses
## this hot-path form while finalizing tiles between planning refreshes.
static func calculate_visible_tile_radius(
lod2_radius_chunks_value: int,
lod2_tile_radius_value: int,
warm_tile_margin_value: int
) -> int:
var chunk_tile_radius := int(ceil(
float(lod2_radius_chunks_value) / float(CHUNKS_PER_ADT_TILE)
)) + warm_tile_margin_value
return maxi(chunk_tile_radius, lod2_tile_radius_value)
## Calculates the load radius without allocating a policy.
static func calculate_load_tile_radius(
lod2_radius_chunks_value: int,
lod2_tile_radius_value: int,
warm_tile_margin_value: int,
prewarm_tile_margin_value: int
) -> int:
return calculate_visible_tile_radius(
lod2_radius_chunks_value,
lod2_tile_radius_value,
warm_tile_margin_value
) + maxi(0, prewarm_tile_margin_value)
@@ -0,0 +1 @@
uid://cfrad5wr668w5
@@ -0,0 +1,52 @@
class_name RenderedGroundSample
extends RefCounted
## Immutable result of sampling currently loaded render terrain in Godot world units.
## This diagnostic value is renderer-owned and is not authoritative gameplay collision.
var is_available: bool:
get:
return _is_available
var height_units: float:
get:
return _height_units
var failure_code: StringName:
get:
return _failure_code
var _is_available: bool
var _height_units: float
var _failure_code: StringName
const SCRIPT_PATH := "res://src/render/terrain/rendered_ground_sample.gd"
## Creates an available finite rendered-ground sample in Godot world units.
static func available(height_units_value: float) -> RefCounted:
return load(SCRIPT_PATH).new(true, height_units_value, &"")
## Creates an unavailable rendered-ground sample with a stable failure code.
static func unavailable(failure_code_value: StringName) -> RefCounted:
return load(SCRIPT_PATH).new(false, 0.0, failure_code_value)
func _init(
is_available_value: bool,
height_units_value: float,
failure_code_value: StringName
) -> void:
_is_available = is_available_value and is_finite(height_units_value)
_height_units = height_units_value if _is_available else 0.0
if _is_available:
_failure_code = &""
elif not is_finite(height_units_value):
_failure_code = &"render_terrain_height_not_finite"
else:
_failure_code = (
failure_code_value
if not failure_code_value.is_empty()
else &"render_terrain_unavailable"
)
@@ -0,0 +1 @@
uid://c744ldnnn87a8
@@ -0,0 +1,102 @@
class_name TerrainChunkGeometryQueuePlanner
extends RefCounted
## Pure desired-operation planner for terrain chunk geometry queues.
const GODOT_WORLD_POSITION_SCRIPT := preload(
"res://src/domain/coordinates/godot_world_position.gd"
)
## Returns fresh nearest-first create and stable-order remove request arrays.
## The caller retains queue storage, execution order and every render side effect.
func plan(tile_states: Dictionary, focus_position: RefCounted) -> Dictionary:
var chunk_create_requests: Array = []
var chunk_remove_requests: Array = []
if (
focus_position == null
or focus_position.get_script() != GODOT_WORLD_POSITION_SCRIPT
):
return _create_plan(chunk_create_requests, chunk_remove_requests)
var focus_x_units := float(focus_position.get("x_units"))
var focus_z_units := float(focus_position.get("z_units"))
for tile_key_variant in tile_states.keys():
var tile_key := String(tile_key_variant)
var tile_state: Dictionary = tile_states[tile_key_variant]
var current_chunks: Dictionary = tile_state["chunks"]
var desired_lods: Dictionary = tile_state["desired_lods"]
var desired_tile_lod := int(tile_state.get("desired_tile_lod", -1))
if desired_tile_lod >= 0:
for chunk_id in current_chunks.keys():
chunk_remove_requests.append({"tile": tile_key, "chunk": chunk_id})
continue
for chunk_id in current_chunks.keys():
if not desired_lods.has(chunk_id):
chunk_remove_requests.append({"tile": tile_key, "chunk": chunk_id})
continue
if int(current_chunks[chunk_id]["lod"]) != int(desired_lods[chunk_id]):
chunk_create_requests.append(_create_chunk_request(
tile_key,
chunk_id,
int(desired_lods[chunk_id]),
_resolve_chunk_origin(tile_state, int(chunk_id)),
focus_x_units,
focus_z_units
))
for chunk_id in desired_lods.keys():
if current_chunks.has(chunk_id):
continue
chunk_create_requests.append(_create_chunk_request(
tile_key,
chunk_id,
int(desired_lods[chunk_id]),
_resolve_chunk_origin(tile_state, int(chunk_id)),
focus_x_units,
focus_z_units
))
chunk_create_requests.sort_custom(func(
left_request: Dictionary,
right_request: Dictionary
) -> bool:
return float(left_request["dist_sq"]) < float(right_request["dist_sq"])
)
return _create_plan(chunk_create_requests, chunk_remove_requests)
func _resolve_chunk_origin(tile_state: Dictionary, chunk_id: int) -> Vector3:
var chunks: Array = tile_state["data"].get("chunks", [])
if chunk_id >= 0 and chunk_id < chunks.size():
var chunk: Dictionary = chunks[chunk_id]
if not chunk.is_empty():
return chunk.get("origin", Vector3.ZERO)
return tile_state.get("origin", Vector3.ZERO)
func _create_chunk_request(
tile_key: String,
chunk_id: Variant,
desired_lod: int,
chunk_origin: Vector3,
focus_x_units: float,
focus_z_units: float
) -> Dictionary:
var delta_x_units := chunk_origin.x - focus_x_units
var delta_z_units := chunk_origin.z - focus_z_units
return {
"tile": tile_key,
"chunk": chunk_id,
"lod": desired_lod,
"dist_sq": delta_x_units * delta_x_units + delta_z_units * delta_z_units,
}
func _create_plan(chunk_create_requests: Array, chunk_remove_requests: Array) -> Dictionary:
return {
"create_requests": chunk_create_requests,
"remove_requests": chunk_remove_requests,
}
@@ -0,0 +1 @@
uid://ct1ar5inci34u
@@ -0,0 +1,53 @@
class_name TerrainChunkLodPlanner
extends RefCounted
## Pure linear desired-state planner for parsed ADT chunk records.
const POLICY_SCRIPT := preload("res://src/render/terrain/terrain_chunk_lod_policy.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
## Maps source chunk indices to LOD 0/1/2 using horizontal distance from the
## typed focus to each chunk center. Empty/out-of-range chunks remain absent.
func plan(
chunks: Array,
focus_position: RefCounted,
policy: RefCounted
) -> Dictionary:
if (
focus_position == null
or focus_position.get_script() != GODOT_WORLD_POSITION_SCRIPT
or policy == null
or policy.get_script() != POLICY_SCRIPT
or not bool(policy.get("is_enabled"))
):
return {}
var chunk_size_units := float(policy.get("chunk_size_units"))
var lod0_radius_squared := (float(policy.get("lod0_radius_chunks")) * chunk_size_units) ** 2
var lod1_radius_squared := (float(policy.get("lod1_radius_chunks")) * chunk_size_units) ** 2
var lod2_radius_squared := (float(policy.get("lod2_radius_chunks")) * chunk_size_units) ** 2
var focus_x_units := float(focus_position.get("x_units"))
var focus_z_units := float(focus_position.get("z_units"))
var desired_lods: Dictionary = {}
for chunk_id in range(chunks.size()):
var chunk: Dictionary = chunks[chunk_id]
if chunk.is_empty():
continue
var origin: Vector3 = chunk.get("origin", Vector3.ZERO)
var center_x_units := origin.x + chunk_size_units * 0.5
var center_z_units := origin.z + chunk_size_units * 0.5
var delta_x_units := center_x_units - focus_x_units
var delta_z_units := center_z_units - focus_z_units
var distance_squared_units := (
delta_x_units * delta_x_units + delta_z_units * delta_z_units
)
if distance_squared_units <= lod0_radius_squared:
desired_lods[chunk_id] = 0
elif distance_squared_units <= lod1_radius_squared:
desired_lods[chunk_id] = 1
elif distance_squared_units <= lod2_radius_squared:
desired_lods[chunk_id] = 2
return desired_lods
@@ -0,0 +1 @@
uid://x8simqkfctqw
@@ -0,0 +1,45 @@
class_name TerrainChunkLodPolicy
extends RefCounted
## Immutable snapshot of the streamer's chunk-terrain LOD configuration.
var is_enabled: bool:
get:
return _is_enabled
var lod0_radius_chunks: int:
get:
return _lod0_radius_chunks
var lod1_radius_chunks: int:
get:
return _lod1_radius_chunks
var lod2_radius_chunks: int:
get:
return _lod2_radius_chunks
var chunk_size_units: float:
get:
return _chunk_size_units
var _is_enabled: bool
var _lod0_radius_chunks: int
var _lod1_radius_chunks: int
var _lod2_radius_chunks: int
var _chunk_size_units: float
## Captures one renderer configuration snapshot for a deterministic plan.
func _init(
is_enabled_value: bool,
lod0_radius_chunks_value: int,
lod1_radius_chunks_value: int,
lod2_radius_chunks_value: int,
chunk_size_units_value: float
) -> void:
_is_enabled = is_enabled_value
_lod0_radius_chunks = lod0_radius_chunks_value
_lod1_radius_chunks = lod1_radius_chunks_value
_lod2_radius_chunks = lod2_radius_chunks_value
_chunk_size_units = chunk_size_units_value
@@ -0,0 +1 @@
uid://dmwi83dp52pa8
@@ -0,0 +1,73 @@
class_name TerrainQualityMeshCache
extends RefCounted
## Session-memory LRU owner for full-quality terrain Mesh references retained
## across tile unload/revisit. It owns no tile state, Node, RID, task or file.
const EXCLUDED_QUALITY_SOURCES: Array[String] = [
"control_splat_cache",
"splat_cache",
"splat",
]
var _entries_by_tile_key: Dictionary = {}
var _tile_keys_oldest_to_newest: Array[String] = []
## Stores an admitted Mesh reference, touches it as newest and trims oldest
## entries to the supplied non-negative capacity. Returns false when rejected.
func store(tile_key: String, mesh: Mesh, source: String, capacity: int) -> bool:
if tile_key.is_empty() or mesh == null or EXCLUDED_QUALITY_SOURCES.has(source):
return false
_entries_by_tile_key[tile_key] = {"mesh": mesh, "source": source}
_touch(tile_key)
_trim(maxi(0, capacity))
return _entries_by_tile_key.has(tile_key)
## Returns a fresh record containing the retained Mesh/source and touches the key
## as newest. Missing or invalid records return empty and are removed.
func restore(tile_key: String) -> Dictionary:
if not _entries_by_tile_key.has(tile_key):
return {}
var entry: Dictionary = _entries_by_tile_key[tile_key]
var source := String(entry.get("source", "baked_full"))
var mesh := entry.get("mesh") as Mesh
if mesh == null or EXCLUDED_QUALITY_SOURCES.has(source):
_remove(tile_key)
return {}
_touch(tile_key)
return {"mesh": mesh, "source": source}
## Releases every retained Mesh reference and resets LRU order.
func clear() -> void:
_entries_by_tile_key.clear()
_tile_keys_oldest_to_newest.clear()
## Returns detached scalar diagnostics without exposing retained Mesh references.
func diagnostic_snapshot() -> Dictionary:
var entries: Array[Dictionary] = []
for tile_key in _tile_keys_oldest_to_newest:
var entry: Dictionary = _entries_by_tile_key.get(tile_key, {})
entries.append({"tile_key": tile_key, "source": String(entry.get("source", ""))})
return {
"entry_count": entries.size(),
"entries_oldest_to_newest": entries,
}
func _touch(tile_key: String) -> void:
_tile_keys_oldest_to_newest.erase(tile_key)
_tile_keys_oldest_to_newest.append(tile_key)
func _trim(capacity: int) -> void:
while _tile_keys_oldest_to_newest.size() > capacity:
_remove(_tile_keys_oldest_to_newest.front())
func _remove(tile_key: String) -> void:
_entries_by_tile_key.erase(tile_key)
_tile_keys_oldest_to_newest.erase(tile_key)
@@ -0,0 +1 @@
uid://y576l2omqwox
+69
View File
@@ -0,0 +1,69 @@
class_name WmoPlacementRegistry
extends RefCounted
## Owns WMO placement-key to tile/global reference-key sets without Nodes.
var _reference_keys_by_placement_key: Dictionary = {}
## Adds one idempotent reference and returns true only for a newly registered
## placement key. Empty placement/reference keys are rejected.
func add_reference(placement_key: String, reference_key: String) -> bool:
if placement_key.is_empty() or reference_key.is_empty():
return false
var is_new_placement := not _reference_keys_by_placement_key.has(placement_key)
if is_new_placement:
_reference_keys_by_placement_key[placement_key] = {}
var reference_keys: Dictionary = _reference_keys_by_placement_key[placement_key]
reference_keys[reference_key] = true
return is_new_placement
## Releases one reference and returns true only when the final reference removes
## the placement key. Unknown and non-owner references leave state unchanged.
func release_reference(placement_key: String, reference_key: String) -> bool:
if placement_key.is_empty() or reference_key.is_empty():
return false
if not _reference_keys_by_placement_key.has(placement_key):
return false
var reference_keys: Dictionary = _reference_keys_by_placement_key[placement_key]
if not reference_keys.has(reference_key):
return false
reference_keys.erase(reference_key)
if not reference_keys.is_empty():
return false
_reference_keys_by_placement_key.erase(placement_key)
return true
## Returns whether the placement key currently has at least one reference.
func contains(placement_key: String) -> bool:
return not placement_key.is_empty() and _reference_keys_by_placement_key.has(placement_key)
## Removes every placement/reference record. Safe to call repeatedly.
func clear() -> void:
_reference_keys_by_placement_key.clear()
## Returns the number of currently referenced placement keys.
func active_count() -> int:
return _reference_keys_by_placement_key.size()
## Returns detached, placement/reference-key-sorted diagnostic records.
func diagnostic_snapshot() -> Array[Dictionary]:
var placement_keys: Array = _reference_keys_by_placement_key.keys()
placement_keys.sort()
var snapshot: Array[Dictionary] = []
for placement_key_variant in placement_keys:
var placement_key := String(placement_key_variant)
var reference_keys: Array = (
_reference_keys_by_placement_key[placement_key] as Dictionary
).keys()
reference_keys.sort()
snapshot.append({
"placement_key": placement_key,
"reference_keys": reference_keys.duplicate(),
})
return snapshot
@@ -0,0 +1 @@
uid://cfbo5fw33vw4f
+32
View File
@@ -0,0 +1,32 @@
class_name WmoPlacementResolver
extends RefCounted
## Stateless WMO cache-key, placement-identity and world-transform rules.
## Returns the historical lowercase slash-normalized relative WMO cache key.
func normalize_relative_path(relative_wmo_path: String) -> String:
return relative_wmo_path.replace("\\", "/").to_lower()
## Returns the positive MODF unique-ID key or the historical tile/index fallback.
func resolve_unique_key(
placement: Dictionary,
tile_key: String,
placement_index: int
) -> String:
var unique_id := int(placement.get("unique_id", -1))
if unique_id > 0:
return "uid:%d" % unique_id
return "tile:%s:%d" % [tile_key, placement_index]
## Returns the world-space Godot transform used by every WMO instance path.
func resolve_world_transform(placement: Dictionary) -> Transform3D:
var world_position: Vector3 = placement.get("pos", Vector3.ZERO)
var rotation_radians: Vector3 = placement.get("rot", Vector3.ZERO)
var scale_value := float(placement.get("scale", 1.0))
return Transform3D(
Basis.from_euler(rotation_radians).scaled(Vector3.ONE * scale_value),
world_position
)
@@ -0,0 +1 @@
uid://02omydj0sxmo
+59
View File
@@ -0,0 +1,59 @@
class_name WmoRenderBuildJob
extends RefCounted
## Holds one lightweight-WMO build cursor and strong engine-object references.
## Releasing this record never frees the referenced Node or Resource.
var _placement_key: String
var _root: Node3D
var _render_resource: Resource
var _mesh_index: int = 0
var _multimesh_index: int = 0
func _init(placement_key: String, root: Node3D, render_resource: Resource) -> void:
_placement_key = placement_key
_root = root
_render_resource = render_resource
## Returns the immutable placement identity used by the queue.
func placement_key() -> String:
return _placement_key
## Returns the strongly referenced scene root without transferring ownership.
func root() -> Node3D:
return _root
## Returns the strongly referenced render resource without transferring ownership.
func render_resource() -> Resource:
return _render_resource
## Returns the current mesh-group cursor.
func mesh_index() -> int:
return _mesh_index
## Returns the current doodad MultiMesh-group cursor.
func multimesh_index() -> int:
return _multimesh_index
## Atomically adopts both cursor values returned by the pure step planner.
func adopt_cursors(next_mesh_index: int, next_multimesh_index: int) -> void:
_mesh_index = next_mesh_index
_multimesh_index = next_multimesh_index
## Returns detached scalar state and never exposes engine-object references.
func diagnostic_snapshot() -> Dictionary:
return {
"placement_key": _placement_key,
"mesh_index": _mesh_index,
"multimesh_index": _multimesh_index,
"has_root": _root != null,
"has_render_resource": _render_resource != null,
}
@@ -0,0 +1 @@
uid://gyjq5dqueyi7
+139
View File
@@ -0,0 +1,139 @@
class_name WmoRenderBuildQueue
extends RefCounted
## Owns pending lightweight-WMO job records and FIFO placement keys.
const JOB_SCRIPT := preload("res://src/render/wmo/wmo_render_build_job.gd")
var _jobs_by_placement_key: Dictionary = {}
var _queued_placement_keys: Array[String] = []
## Enqueues a new typed job. Duplicate placement keys replace the keyed job but
## append another FIFO entry, preserving the loader's historical behavior.
func enqueue(placement_key: String, root: Node3D, render_resource: Resource) -> bool:
if placement_key.is_empty() or root == null or render_resource == null:
return false
_jobs_by_placement_key[placement_key] = JOB_SCRIPT.new(
placement_key,
root,
render_resource
)
_queued_placement_keys.append(placement_key)
return true
## Returns whether at least one FIFO key, including a stale key, is pending.
func has_pending() -> bool:
return not _queued_placement_keys.is_empty()
## Returns the front FIFO key, or an empty String when the queue is empty.
func front_key() -> String:
if _queued_placement_keys.is_empty():
return ""
return _queued_placement_keys.front()
## Returns whether a FIFO placement key still has a current job record.
func has_job(placement_key: String) -> bool:
return _jobs_by_placement_key.has(placement_key)
## Returns the current job record as its engine base type, or null for a stale
## key. Runtime consumers should prefer the typed accessors below.
func job_for(placement_key: String) -> RefCounted:
return _jobs_by_placement_key.get(placement_key, null) as RefCounted
## Returns the borrowed root for a current job, or null for a stale key.
func root_for(placement_key: String) -> Node3D:
var job := job_for(placement_key)
if job == null:
return null
return job.call("root") as Node3D
## Returns the borrowed render resource for a current job, or null when stale.
func render_resource_for(placement_key: String) -> Resource:
var job := job_for(placement_key)
if job == null:
return null
return job.call("render_resource") as Resource
## Returns the current mesh cursor, or zero for a stale key.
func mesh_index_for(placement_key: String) -> int:
var job := job_for(placement_key)
if job == null:
return 0
return int(job.call("mesh_index"))
## Returns the current MultiMesh cursor, or zero for a stale key.
func multimesh_index_for(placement_key: String) -> int:
var job := job_for(placement_key)
if job == null:
return 0
return int(job.call("multimesh_index"))
## Adopts both cursors for a current job and returns whether it existed.
func adopt_cursors(
placement_key: String,
next_mesh_index: int,
next_multimesh_index: int
) -> bool:
var job := job_for(placement_key)
if job == null:
return false
job.call("adopt_cursors", next_mesh_index, next_multimesh_index)
return true
## Pops and returns the front FIFO key, or an empty String when already empty.
func pop_front() -> String:
if _queued_placement_keys.is_empty():
return ""
return _queued_placement_keys.pop_front()
## Cancels the keyed job and erases only the first matching FIFO key. Returns
## whether an active job record existed. Engine objects are never freed here.
func cancel(placement_key: String) -> bool:
var had_job := _jobs_by_placement_key.has(placement_key)
_jobs_by_placement_key.erase(placement_key)
_queued_placement_keys.erase(placement_key)
return had_job
## Releases all job/key references without freeing referenced engine objects.
func clear() -> void:
_jobs_by_placement_key.clear()
_queued_placement_keys.clear()
## Returns FIFO entry count, including duplicate and stale keys.
func pending_count() -> int:
return _queued_placement_keys.size()
## Returns active keyed job count, excluding stale FIFO keys.
func active_job_count() -> int:
return _jobs_by_placement_key.size()
## Returns detached FIFO order and placement-key-sorted scalar job diagnostics.
func diagnostic_snapshot() -> Dictionary:
var placement_keys: Array = _jobs_by_placement_key.keys()
placement_keys.sort()
var jobs: Array[Dictionary] = []
for placement_key_variant in placement_keys:
var placement_key := String(placement_key_variant)
var job: RefCounted = job_for(placement_key)
if job != null:
jobs.append(job.call("diagnostic_snapshot") as Dictionary)
return {
"queued_placement_keys": _queued_placement_keys.duplicate(),
"jobs": jobs,
}
@@ -0,0 +1 @@
uid://b165sgg76lrt8
@@ -0,0 +1,40 @@
class_name WmoRenderBuildStepPlanner
extends RefCounted
## Selects one value-only WMO render-group build operation without engine objects.
const OPERATION_MESH: StringName = &"mesh"
const OPERATION_MULTIMESH: StringName = &"multimesh"
const OPERATION_COMPLETE: StringName = &"complete"
## Selects the next mesh-first build operation and returns its selected index
## plus the cursor values to adopt after that one operation. Counts and cursors
## are intentionally not clamped so the loader's historical comparisons remain
## exact for every integer input.
func plan_step(
mesh_count: int,
mesh_index: int,
multimesh_count: int,
multimesh_index: int
) -> Dictionary:
if mesh_index < mesh_count:
return {
"operation": OPERATION_MESH,
"selected_index": mesh_index,
"next_mesh_index": mesh_index + 1,
"next_multimesh_index": multimesh_index,
}
if multimesh_index < multimesh_count:
return {
"operation": OPERATION_MULTIMESH,
"selected_index": multimesh_index,
"next_mesh_index": mesh_index,
"next_multimesh_index": multimesh_index + 1,
}
return {
"operation": OPERATION_COMPLETE,
"selected_index": -1,
"next_mesh_index": mesh_index,
"next_multimesh_index": multimesh_index,
}
@@ -0,0 +1 @@
uid://dqogt0el0x3kx
@@ -0,0 +1,126 @@
class_name WmoRenderResourceCacheState
extends RefCounted
## Owns validated lightweight-WMO render resources and their load state.
## ResourceLoader I/O and cache-format validation remain caller responsibilities.
var _resources_by_normalized_path: Dictionary = {}
var _missing_normalized_paths: Dictionary = {}
var _request_cache_paths_by_normalized_path: Dictionary = {}
## Returns the validated cached Resource for a normalized WMO path, or null.
func resource_for(normalized_relative_path: String) -> Resource:
if normalized_relative_path.is_empty():
return null
return _resources_by_normalized_path.get(normalized_relative_path) as Resource
## Returns whether a validated Resource is cached for the normalized WMO path.
func contains_resource(normalized_relative_path: String) -> bool:
return (
not normalized_relative_path.is_empty()
and _resources_by_normalized_path.has(normalized_relative_path)
)
## Returns whether the normalized WMO path is in the negative cache.
func is_missing(normalized_relative_path: String) -> bool:
return (
not normalized_relative_path.is_empty()
and _missing_normalized_paths.has(normalized_relative_path)
)
## Returns whether the normalized WMO path has a pending threaded load request.
func has_request(normalized_relative_path: String) -> bool:
return (
not normalized_relative_path.is_empty()
and _request_cache_paths_by_normalized_path.has(normalized_relative_path)
)
## Records one loader-owned threaded request. Cached, missing, duplicate or empty
## paths are rejected so each normalized WMO path has exactly one state.
func remember_request(normalized_relative_path: String, cache_path: String) -> bool:
if normalized_relative_path.is_empty() or cache_path.is_empty():
return false
if (
_resources_by_normalized_path.has(normalized_relative_path)
or _missing_normalized_paths.has(normalized_relative_path)
or _request_cache_paths_by_normalized_path.has(normalized_relative_path)
):
return false
_request_cache_paths_by_normalized_path[normalized_relative_path] = cache_path
return true
## Returns a detached normalized-path to cache-path snapshot for loader-owned
## ResourceLoader polling or shutdown draining.
func request_paths_snapshot() -> Dictionary:
return _request_cache_paths_by_normalized_path.duplicate()
## Completes a pending request with a caller-validated Resource. Returns false
## for unknown requests or null Resources and otherwise adopts the reference.
func complete_request_with_resource(
normalized_relative_path: String,
validated_resource: Resource
) -> bool:
if (
normalized_relative_path.is_empty()
or validated_resource == null
or not _request_cache_paths_by_normalized_path.has(normalized_relative_path)
):
return false
_request_cache_paths_by_normalized_path.erase(normalized_relative_path)
_missing_normalized_paths.erase(normalized_relative_path)
_resources_by_normalized_path[normalized_relative_path] = validated_resource
return true
## Completes a pending request as missing after load failure or validation
## rejection. Returns false for an unknown request.
func complete_request_as_missing(normalized_relative_path: String) -> bool:
if (
normalized_relative_path.is_empty()
or not _request_cache_paths_by_normalized_path.has(normalized_relative_path)
):
return false
_request_cache_paths_by_normalized_path.erase(normalized_relative_path)
_resources_by_normalized_path.erase(normalized_relative_path)
_missing_normalized_paths[normalized_relative_path] = true
return true
## Clears pending requests and negative entries while retaining validated
## Resources for map reset or orderly shutdown request draining.
func clear_transient_state() -> void:
_request_cache_paths_by_normalized_path.clear()
_missing_normalized_paths.clear()
## Releases every cached Resource and transient entry. Safe to call repeatedly.
func clear_all() -> void:
_resources_by_normalized_path.clear()
clear_transient_state()
## Returns the number of pending threaded load requests.
func pending_request_count() -> int:
return _request_cache_paths_by_normalized_path.size()
## Returns detached, sorted scalar keys without exposing Resource references.
func diagnostic_snapshot() -> Dictionary:
var resource_paths: Array = _resources_by_normalized_path.keys()
var missing_paths: Array = _missing_normalized_paths.keys()
var request_paths: Array = _request_cache_paths_by_normalized_path.keys()
resource_paths.sort()
missing_paths.sort()
request_paths.sort()
return {
"resource_paths": resource_paths.duplicate(),
"missing_paths": missing_paths.duplicate(),
"request_paths": request_paths.duplicate(),
}
@@ -0,0 +1 @@
uid://b1g4s1rqrptsw
@@ -0,0 +1,134 @@
class_name WmoSceneResourceCacheState
extends RefCounted
## Owns validated cached-WMO PackedScenes and their load state. File limits,
## ResourceLoader I/O and scene-cache validation remain caller responsibilities.
var _scenes_by_normalized_path: Dictionary = {}
var _missing_normalized_paths: Dictionary = {}
var _request_cache_paths_by_normalized_path: Dictionary = {}
## Returns the validated PackedScene for a normalized WMO path, or null.
func scene_for(normalized_relative_path: String) -> PackedScene:
if normalized_relative_path.is_empty():
return null
return _scenes_by_normalized_path.get(normalized_relative_path) as PackedScene
## Returns whether a validated PackedScene is cached for the normalized path.
func contains_scene(normalized_relative_path: String) -> bool:
return (
not normalized_relative_path.is_empty()
and _scenes_by_normalized_path.has(normalized_relative_path)
)
## Returns whether the normalized path is in the negative cache.
func is_missing(normalized_relative_path: String) -> bool:
return (
not normalized_relative_path.is_empty()
and _missing_normalized_paths.has(normalized_relative_path)
)
## Returns whether the normalized path has a pending threaded load request.
func has_request(normalized_relative_path: String) -> bool:
return (
not normalized_relative_path.is_empty()
and _request_cache_paths_by_normalized_path.has(normalized_relative_path)
)
## Records one loader-started request. Occupied or empty paths are rejected.
func remember_request(normalized_relative_path: String, cache_path: String) -> bool:
if normalized_relative_path.is_empty() or cache_path.is_empty():
return false
if _has_any_state(normalized_relative_path):
return false
_request_cache_paths_by_normalized_path[normalized_relative_path] = cache_path
return true
## Records an absent, oversize or request-start failure without requiring a
## pending request. Occupied and empty paths are rejected.
func mark_missing(normalized_relative_path: String) -> bool:
if normalized_relative_path.is_empty() or _has_any_state(normalized_relative_path):
return false
_missing_normalized_paths[normalized_relative_path] = true
return true
## Returns a detached normalized-path to cache-path snapshot for loader polling.
func request_paths_snapshot() -> Dictionary:
return _request_cache_paths_by_normalized_path.duplicate()
## Completes a pending request with a caller-validated PackedScene.
func complete_request_with_scene(
normalized_relative_path: String,
validated_scene: PackedScene
) -> bool:
if (
normalized_relative_path.is_empty()
or validated_scene == null
or not _request_cache_paths_by_normalized_path.has(normalized_relative_path)
):
return false
_request_cache_paths_by_normalized_path.erase(normalized_relative_path)
_missing_normalized_paths.erase(normalized_relative_path)
_scenes_by_normalized_path[normalized_relative_path] = validated_scene
return true
## Completes a pending request as missing after load or validation failure.
func complete_request_as_missing(normalized_relative_path: String) -> bool:
if (
normalized_relative_path.is_empty()
or not _request_cache_paths_by_normalized_path.has(normalized_relative_path)
):
return false
_request_cache_paths_by_normalized_path.erase(normalized_relative_path)
_scenes_by_normalized_path.erase(normalized_relative_path)
_missing_normalized_paths[normalized_relative_path] = true
return true
## Clears pending and negative state while retaining validated PackedScenes.
func clear_transient_state() -> void:
_request_cache_paths_by_normalized_path.clear()
_missing_normalized_paths.clear()
## Releases every cached scene and transient entry. Safe to call repeatedly.
func clear_all() -> void:
_scenes_by_normalized_path.clear()
clear_transient_state()
## Returns the number of pending threaded load requests.
func pending_request_count() -> int:
return _request_cache_paths_by_normalized_path.size()
## Returns detached, sorted scalar keys without exposing PackedScene references.
func diagnostic_snapshot() -> Dictionary:
var scene_paths: Array = _scenes_by_normalized_path.keys()
var missing_paths: Array = _missing_normalized_paths.keys()
var request_paths: Array = _request_cache_paths_by_normalized_path.keys()
scene_paths.sort()
missing_paths.sort()
request_paths.sort()
return {
"scene_paths": scene_paths.duplicate(),
"missing_paths": missing_paths.duplicate(),
"request_paths": request_paths.duplicate(),
}
func _has_any_state(normalized_relative_path: String) -> bool:
return (
_scenes_by_normalized_path.has(normalized_relative_path)
or _missing_normalized_paths.has(normalized_relative_path)
or _request_cache_paths_by_normalized_path.has(normalized_relative_path)
)
@@ -0,0 +1 @@
uid://b4geepupdscu7
+133
View File
@@ -7,17 +7,32 @@ extends Node
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
const RENDERED_GROUND_SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd")
const WORLD_ENVIRONMENT_SNAPSHOT_SCRIPT := preload("res://src/render/environment/world_environment_snapshot.gd")
const ENTITY_PRESENTATION_SNAPSHOT_SCRIPT := preload("res://src/render/entities/entity_presentation_snapshot.gd")
const ENTITY_ID_SCRIPT := preload("res://src/domain/identity/entity_id.gd")
## Internal renderer implementation resolved relative to this node.
## The referenced node remains owned by its scene parent.
@export var streaming_world_loader_path: NodePath = NodePath("..")
## Scene-owned outdoor environment implementation resolved relative to this node.
## The facade delegates snapshots but owns neither the controller nor Environment.
@export var world_environment_controller_path: NodePath = NodePath("../WowSkyController")
## Scene-owned service that creates and owns session-local world entity visuals.
@export var world_entity_presenter_path: NodePath = NodePath("../WorldEntityPresenter")
## Optional explicit Node3D source sampled in Godot world coordinates.
## Runtime scenes use the player; capture and probe tools replace it with their camera.
@export var streaming_focus_source_path: NodePath
var _streaming_world_loader: Node
var _world_environment_controller: Node
var _world_entity_presenter: Node
var _missing_loader_reported := false
var _missing_environment_controller_reported := false
var _missing_entity_presenter_reported := false
var _missing_focus_source_reported := false
@@ -66,6 +81,85 @@ func renderer_metrics_snapshot() -> Dictionary:
return (metrics_variant as Dictionary).duplicate(true)
## Samples the currently loaded render terrain at a typed Godot world position.
## This read-only renderer view is diagnostic and does not replace authoritative
## gameplay collision or the independently composed [TerrainQuery].
func sample_ground_height(godot_world_position: GodotWorldPosition) -> RefCounted:
var loader := _resolve_streaming_world_loader()
if loader == null:
return RENDERED_GROUND_SAMPLE_SCRIPT.unavailable(&"render_facade_unavailable")
var sample_variant = loader.call("sample_rendered_ground_height", godot_world_position)
if sample_variant is RefCounted and sample_variant.get_script() == RENDERED_GROUND_SAMPLE_SCRIPT:
return sample_variant
push_error("WorldRenderFacade received an invalid rendered ground sample.")
return RENDERED_GROUND_SAMPLE_SCRIPT.unavailable(&"render_ground_sample_invalid")
## Returns a detached diagnostic snapshot for one rendered-ground query.
## Mutating the result cannot mutate streamer queues, tile state or resources.
func renderer_ground_query_snapshot(godot_world_position: GodotWorldPosition) -> Dictionary:
var loader := _resolve_streaming_world_loader()
if loader == null:
return {}
var snapshot_variant = loader.call("render_ground_query_snapshot", godot_world_position)
if not (snapshot_variant is Dictionary):
push_error("WorldRenderFacade received a non-Dictionary ground query snapshot.")
return {}
return (snapshot_variant as Dictionary).duplicate(true)
## Applies an immutable environment snapshot through the renderer-owned sky path.
## Returns false for an invalid value or unavailable/incompatible controller.
func apply_environment_snapshot(world_environment_snapshot: RefCounted) -> bool:
if (
world_environment_snapshot == null
or world_environment_snapshot.get_script() != WORLD_ENVIRONMENT_SNAPSHOT_SCRIPT
or not bool(world_environment_snapshot.get("is_valid"))
):
return false
var controller := _resolve_world_environment_controller()
if controller == null:
return false
return bool(controller.call("apply_environment_snapshot", world_environment_snapshot))
## Idempotently creates or updates one world entity visual through the renderer-
## owned presenter. The immutable full snapshot remains caller-owned.
func present_entity(entity_presentation_snapshot: RefCounted) -> bool:
if (
entity_presentation_snapshot == null
or entity_presentation_snapshot.get_script() != ENTITY_PRESENTATION_SNAPSHOT_SCRIPT
or not bool(entity_presentation_snapshot.get("is_valid"))
):
return false
var presenter := _resolve_world_entity_presenter()
if presenter == null:
return false
return bool(presenter.call("present_entity", entity_presentation_snapshot))
## Removes one session-local entity visual without changing authoritative state.
func remove_entity(entity_id: RefCounted) -> bool:
if entity_id == null or entity_id.get_script() != ENTITY_ID_SCRIPT or not bool(entity_id.call("is_valid")):
return false
var presenter := _resolve_world_entity_presenter()
if presenter == null:
return false
return bool(presenter.call("remove_entity", entity_id))
## Returns detached entity-presentation diagnostics with no Node references.
func entity_presentation_snapshot() -> Dictionary:
var presenter := _resolve_world_entity_presenter()
if presenter == null:
return {}
var snapshot_variant = presenter.call("entity_presentation_snapshot")
if not (snapshot_variant is Dictionary):
push_error("WorldRenderFacade received a non-Dictionary entity presentation snapshot.")
return {}
return (snapshot_variant as Dictionary).duplicate(true)
func _capture_streaming_focus_from_source() -> void:
if streaming_focus_source_path == NodePath():
return
@@ -99,6 +193,8 @@ func _resolve_streaming_world_loader() -> Node:
&"set_streaming_focus",
&"refresh_streaming_focus",
&"render_baseline_snapshot",
&"sample_rendered_ground_height",
&"render_ground_query_snapshot",
]:
if not candidate.has_method(required_method):
if not _missing_loader_reported:
@@ -108,3 +204,40 @@ func _resolve_streaming_world_loader() -> Node:
_streaming_world_loader = candidate
_missing_loader_reported = false
return _streaming_world_loader
func _resolve_world_environment_controller() -> Node:
if is_instance_valid(_world_environment_controller):
return _world_environment_controller
var candidate := get_node_or_null(world_environment_controller_path)
if candidate == null or not candidate.has_method(&"apply_environment_snapshot"):
if not _missing_environment_controller_reported:
push_error(
"WorldRenderFacade cannot resolve a compatible environment controller at %s"
% world_environment_controller_path
)
_missing_environment_controller_reported = true
return null
_world_environment_controller = candidate
_missing_environment_controller_reported = false
return _world_environment_controller
func _resolve_world_entity_presenter() -> Node:
if is_instance_valid(_world_entity_presenter):
return _world_entity_presenter
var candidate := get_node_or_null(world_entity_presenter_path)
if candidate == null:
if not _missing_entity_presenter_reported:
push_error("WorldRenderFacade cannot resolve entity presenter at %s" % world_entity_presenter_path)
_missing_entity_presenter_reported = true
return null
for required_method in [&"present_entity", &"remove_entity", &"entity_presentation_snapshot"]:
if not candidate.has_method(required_method):
if not _missing_entity_presenter_reported:
push_error("WorldRenderFacade entity presenter lacks %s" % required_method)
_missing_entity_presenter_reported = true
return null
_world_entity_presenter = candidate
_missing_entity_presenter_reported = false
return _world_entity_presenter
+17
View File
@@ -4,6 +4,7 @@ extends Node
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
const WORLD_ENVIRONMENT_SNAPSHOT_SCRIPT := preload("res://src/render/environment/world_environment_snapshot.gd")
const LIGHT_COORD_SCALE := 36.0
const HALF_MINUTES_PER_DAY := 2880
@@ -133,6 +134,22 @@ func _process(delta: float) -> void:
_update_skybox_transform()
## Accepts an authoritative time snapshot without moving DBC visual selection out
## of this controller. Existing fixed-clock behavior is preserved and frozen until
## another snapshot or explicit local clock configuration is supplied.
func apply_environment_snapshot(world_environment_snapshot: RefCounted) -> bool:
if (
world_environment_snapshot == null
or world_environment_snapshot.get_script() != WORLD_ENVIRONMENT_SNAPSHOT_SCRIPT
or not bool(world_environment_snapshot.get("is_valid"))
):
return false
use_system_time = false
time_speed = 0.0
fixed_time_hours = float(world_environment_snapshot.get("time_of_day_hours"))
return true
func _resolve_nodes() -> void:
if not world_environment_path.is_empty():
_world_environment = get_node_or_null(world_environment_path) as WorldEnvironment
@@ -7,6 +7,7 @@
[ext_resource type="Script" uid="uid://bhqbo1ftrt3a3" path="res://src/scenes/character/character_appearance_presenter.gd" id="5_appearance"]
[ext_resource type="Script" uid="uid://c2jnn0e3mmymc" path="res://src/scenes/character/character_animation_presenter.gd" id="6_animation"]
[ext_resource type="Script" path="res://src/render/world_render_facade.gd" id="7_render_facade"]
[ext_resource type="Script" path="res://src/render/entities/world_entity_presenter.gd" id="8_entity_presenter"]
[sub_resource type="CapsuleShape3D" id="PlayerCapsuleShape_1"]
radius = 0.45
@@ -98,6 +99,11 @@ hitch_profiler_enabled = true
script = ExtResource("7_render_facade")
streaming_world_loader_path = NodePath("..")
streaming_focus_source_path = NodePath("../ThirdPersonPlayer")
world_environment_controller_path = NodePath("../WowSkyController")
world_entity_presenter_path = NodePath("../WorldEntityPresenter")
[node name="WorldEntityPresenter" type="Node3D" parent="."]
script = ExtResource("8_entity_presenter")
[node name="ThirdPersonPlayer" type="CharacterBody3D" parent="." unique_id=502573687]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 16800, 80, 26400)
@@ -7,6 +7,7 @@
[ext_resource type="Script" path="res://src/scenes/character/character_appearance_presenter.gd" id="5_appearance"]
[ext_resource type="Script" path="res://src/scenes/character/character_animation_presenter.gd" id="6_animation"]
[ext_resource type="Script" path="res://src/render/world_render_facade.gd" id="7_render_facade"]
[ext_resource type="Script" path="res://src/render/entities/world_entity_presenter.gd" id="8_entity_presenter"]
[sub_resource type="CapsuleShape3D" id="PlayerCapsuleShape_1"]
radius = 0.45
@@ -99,6 +100,11 @@ hitch_profiler_enabled = true
script = ExtResource("7_render_facade")
streaming_world_loader_path = NodePath("..")
streaming_focus_source_path = NodePath("../ThirdPersonPlayer")
world_environment_controller_path = NodePath("../WowSkyController")
world_entity_presenter_path = NodePath("../WorldEntityPresenter")
[node name="WorldEntityPresenter" type="Node3D" parent="."]
script = ExtResource("8_entity_presenter")
[node name="ThirdPersonPlayer" type="CharacterBody3D" parent="." unique_id=502573687]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 16800, 80, 26400)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,10 @@
[gd_scene load_steps=2 format=3]
[sub_resource type="SphereMesh" id="SphereMesh_replacement"]
radius = 0.5
height = 1.0
[node name="ReplacementVisual" type="Node3D"]
[node name="Marker" type="MeshInstance3D" parent="."]
mesh = SubResource("SphereMesh_replacement")
+6 -7
View File
@@ -2,6 +2,7 @@ extends SceneTree
const DEFAULT_MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
const M2_NATIVE_ANIMATED_BUILDER := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd")
const WORLD_ENVIRONMENT_SNAPSHOT_SCRIPT := preload("res://src/render/environment/world_environment_snapshot.gd")
const SHUTDOWN_DRAIN_FRAMES := 2
@@ -130,7 +131,7 @@ func _capture_async() -> void:
camera.make_current()
if player != null:
player.global_position = _vector3(checkpoint.get("player", checkpoint.get("target", [0.0, 0.0, 0.0])))
_set_sky_time(world, float(checkpoint.get("time_hours", 13.0)))
_apply_environment_time(render_facade, float(checkpoint.get("time_hours", 13.0)))
render_facade.call("refresh_streaming_focus", true)
if dry_run:
@@ -308,12 +309,10 @@ func _create_diagnostic_spawn_marker(checkpoint: Dictionary) -> Node3D:
return marker_root
func _set_sky_time(world: Node, time_hours: float) -> void:
var sky := world.get_node_or_null("WowSkyController")
if sky != null:
sky.set("use_system_time", false)
sky.set("time_speed", 0.0)
sky.set("fixed_time_hours", time_hours)
func _apply_environment_time(render_facade: Node, time_hours: float) -> void:
var environment_snapshot: RefCounted = WORLD_ENVIRONMENT_SNAPSHOT_SCRIPT.at_time_of_day(time_hours)
if not bool(render_facade.call("apply_environment_snapshot", environment_snapshot)):
push_error("Render checkpoint could not apply environment time %.3f" % time_hours)
func _environment_metadata() -> Dictionary:
+9 -134
View File
@@ -5,9 +5,7 @@ extends SceneTree
## [--wait 3.0] [--output user://render_terrain_height/report.json]
const MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
const RAY_HEIGHT := 5000.0
func _initialize() -> void:
@@ -61,7 +59,15 @@ func _run_async() -> void:
camera.global_position = camera_position
render_facade.call("refresh_streaming_focus", true)
await create_timer(maxf(0.1, wait_seconds)).timeout
var terrain_sample := _sample_terrain(world, camera_position)
var typed_camera_position = GODOT_WORLD_POSITION_SCRIPT.new(
camera_position.x,
camera_position.y,
camera_position.z
)
var terrain_sample: Dictionary = render_facade.call(
"renderer_ground_query_snapshot",
typed_camera_position
)
terrain_sample["name"] = checkpoint.get("name", "checkpoint")
terrain_sample["camera_y"] = camera_position.y
if terrain_sample.has("terrain_height"):
@@ -94,137 +100,6 @@ func _run_async() -> void:
quit(1 if failed else 0)
func _sample_terrain(world: Node3D, world_position: Vector3) -> Dictionary:
var tile_coordinate := _adt_tile_vector2i(world_position)
var tile_states: Dictionary = world.get("_tile_states")
var highest_terrain_height := -INF
var intersected_tile_key := ""
var ready_mesh_count := 0
for tile_y in range(tile_coordinate.y - 1, tile_coordinate.y + 2):
for tile_x in range(tile_coordinate.x - 1, tile_coordinate.x + 2):
var tile_key := "%d_%d" % [tile_x, tile_y]
if not tile_states.has(tile_key):
continue
var state: Dictionary = tile_states[tile_key]
var terrain_height_variant = _intersect_terrain_state(state, world_position)
if terrain_height_variant == null:
continue
ready_mesh_count += 1
var terrain_height := float(terrain_height_variant)
if terrain_height > highest_terrain_height:
highest_terrain_height = terrain_height
intersected_tile_key = tile_key
if not is_finite(highest_terrain_height):
var missing_result := {
"status": "no_intersection" if ready_mesh_count > 0 else "mesh_not_ready",
"tile": "%d_%d" % [tile_coordinate.x, tile_coordinate.y],
}
missing_result.merge(_tile_runtime_diagnostic(world, String(missing_result.tile), world_position), true)
if bool(missing_result.get("quality_mesh_present", false)) or bool(missing_result.get("tile_lod_mesh_present", false)):
missing_result["status"] = "no_intersection"
missing_result.merge(_nearest_terrain_sample(world, world_position), true)
if missing_result.get("nearest_sample_distance", null) != null:
missing_result["status"] = "sampled_nearby"
missing_result["terrain_height"] = float(missing_result.nearest_sample_height)
return missing_result
return {
"status": "sampled",
"tile": intersected_tile_key,
"terrain_height": highest_terrain_height,
}
func _intersect_terrain_state(state: Dictionary, world_position: Vector3):
var terrain_mesh: Mesh = state.get("quality_terrain_mesh", null)
if terrain_mesh == null:
terrain_mesh = state.get("tile_lod_mesh", null)
if terrain_mesh == null:
return null
var triangle_mesh := terrain_mesh.generate_triangle_mesh()
if triangle_mesh == null:
return null
var tile_root := state.get("root", null) as Node3D
if tile_root == null:
return null
var inverse_transform := tile_root.global_transform.affine_inverse()
var local_ray_origin := inverse_transform * Vector3(world_position.x, RAY_HEIGHT, world_position.z)
var local_ray_direction := inverse_transform.basis * Vector3.DOWN
var intersection: Dictionary = triangle_mesh.intersect_ray(local_ray_origin, local_ray_direction)
if intersection.is_empty():
return null
var local_hit: Vector3 = intersection.get("position", Vector3.ZERO)
var world_hit := tile_root.global_transform * local_hit
return world_hit.y
func _tile_runtime_diagnostic(world: Node3D, tile_key: String, world_position: Vector3) -> Dictionary:
var available_tiles: Dictionary = world.get("_available_tiles")
var loading_tasks: Dictionary = world.get("_tile_loading_tasks")
var tile_states: Dictionary = world.get("_tile_states")
var load_queue: Array = world.get("_tile_load_queue")
var queued_index := -1
for index in load_queue.size():
var request: Dictionary = load_queue[index]
if String(request.get("key", "")) == tile_key:
queued_index = index
break
var diagnostic := {
"available": available_tiles.has(tile_key),
"queued_index": queued_index,
"loading": loading_tasks.has(tile_key),
"state_present": tile_states.has(tile_key),
"load_queue_size": load_queue.size(),
}
if tile_states.has(tile_key):
var state: Dictionary = tile_states[tile_key]
var quality_mesh: Mesh = state.get("quality_terrain_mesh", null)
var tile_lod_mesh: Mesh = state.get("tile_lod_mesh", null)
diagnostic["quality_mesh_present"] = quality_mesh != null
diagnostic["tile_lod_mesh_present"] = tile_lod_mesh != null
diagnostic["quality_source"] = String(state.get("quality_terrain_source", ""))
diagnostic["tile_lod"] = int(state.get("tile_lod", -1))
var tile_root := state.get("root", null) as Node3D
if tile_root != null:
var local_position := tile_root.global_transform.affine_inverse() * world_position
diagnostic["tile_root_position"] = _vector3_array(tile_root.global_position)
diagnostic["probe_local_position"] = _vector3_array(local_position)
var diagnostic_mesh := quality_mesh if quality_mesh != null else tile_lod_mesh
if diagnostic_mesh != null:
var mesh_aabb := diagnostic_mesh.get_aabb()
diagnostic["mesh_aabb_position"] = _vector3_array(mesh_aabb.position)
diagnostic["mesh_aabb_size"] = _vector3_array(mesh_aabb.size)
return diagnostic
func _nearest_terrain_sample(world: Node3D, world_position: Vector3) -> Dictionary:
var tile_states: Dictionary = world.get("_tile_states")
for radius in [2.0, 5.0, 10.0, 20.0, 40.0]:
for offset in [Vector2(radius, 0.0), Vector2(-radius, 0.0), Vector2(0.0, radius), Vector2(0.0, -radius)]:
var sample_position := world_position + Vector3(offset.x, 0.0, offset.y)
var sample_tile := _adt_tile_vector2i(sample_position)
var sample_key := "%d_%d" % [sample_tile.x, sample_tile.y]
if not tile_states.has(sample_key):
continue
var height_variant = _intersect_terrain_state(tile_states[sample_key], sample_position)
if height_variant != null:
return {
"nearest_sample_distance": radius,
"nearest_sample_height": float(height_variant),
"nearest_sample_tile": sample_key,
}
return {"nearest_sample_distance": null}
func _adt_tile_vector2i(world_position: Vector3) -> Vector2i:
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(world_position.x, world_position.y, world_position.z)
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(typed_world_position)
return Vector2i(tile_coordinate.tile_x, tile_coordinate.tile_y)
func _vector3_array(value: Vector3) -> Array[float]:
return [value.x, value.y, value.z]
func _vector3(value_variant) -> Vector3:
if not (value_variant is Array) or value_variant.size() != 3:
return Vector3.ZERO
@@ -19,10 +19,10 @@ const ALLOWED_LEGACY_NAME_PATHS: Array[String] = [
]
const REQUIRED_MAPPER_CALLS := {
"res://src/scenes/sky/wow_sky_controller.gd": ["godot_to_canonical", "godot_to_adt_tile", "godot_to_adt_tile_local"],
"res://src/scenes/streaming/streaming_world_loader.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local", "adt_tile_local_to_godot"],
"res://src/scenes/streaming/streaming_world_loader.gd": ["godot_to_adt_tile", "adt_tile_local_to_godot"],
"res://src/render/streaming/streaming_target_planner.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local"],
"res://src/scenes/player/third_person_wow_controller.gd": ["adt_tile_local_to_godot"],
"res://src/gameplay/terrain/adt_terrain_query.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local", "adt_tile_local_to_chunk"],
"res://src/tools/probe_render_terrain_height.gd": ["godot_to_adt_tile"],
}
+217
View File
@@ -0,0 +1,217 @@
extends SceneTree
## Asset-free contract, dependency and timing regression for M2 batch planning.
const PLANNER_SCRIPT := preload("res://src/render/m2/m2_build_batch_planner.gd")
const PLANNER_PATH := "res://src/render/m2/m2_build_batch_planner.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_static_batch_limit(failures)
_verify_animated_batch_limit(failures)
_verify_non_positive_limit_clamp(failures)
_verify_remaining_count_cap(failures)
_verify_exact_group_completion(failures)
_verify_empty_and_past_end(failures)
_verify_historical_negative_offset(failures)
_verify_source_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("M2_BUILD_BATCH_PLANNER: %s" % failure)
quit(1)
return
print(
"M2_BUILD_BATCH_PLANNER PASS cases=8 iterations=20000 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_static_batch_limit(failures: Array[String]) -> void:
var plan := _plan(300, 0, false, 8, 128)
_expect_plan(plan, 128, 128, 128, false, "static batch", failures)
func _verify_animated_batch_limit(failures: Array[String]) -> void:
var plan := _plan(300, 0, true, 8, 128)
_expect_plan(plan, 8, 8, 8, false, "animated batch", failures)
func _verify_non_positive_limit_clamp(failures: Array[String]) -> void:
var animated_plan := _plan(4, 0, true, 0, 128)
_expect_plan(animated_plan, 1, 1, 1, false, "zero animated limit", failures)
var static_plan := _plan(4, 0, false, 8, -10)
_expect_plan(static_plan, 1, 1, 1, false, "negative static limit", failures)
func _verify_remaining_count_cap(failures: Array[String]) -> void:
var plan := _plan(140, 128, false, 8, 128)
_expect_plan(plan, 128, 12, 0, true, "remaining cap", failures)
func _verify_exact_group_completion(failures: Array[String]) -> void:
var plan := _plan(16, 8, true, 8, 128)
_expect_plan(plan, 8, 8, 0, true, "exact completion", failures)
func _verify_empty_and_past_end(failures: Array[String]) -> void:
var empty_plan := _plan(0, 0, false, 8, 128)
_expect_plan(empty_plan, 128, 0, 0, true, "empty group", failures)
var past_end_plan := _plan(10, 11, false, 8, 128)
_expect_plan(past_end_plan, 128, 0, 0, true, "past end", failures)
func _verify_historical_negative_offset(failures: Array[String]) -> void:
var plan := _plan(10, -2, false, 8, 4)
_expect_plan(plan, 4, 4, 2, false, "negative offset", failures)
func _verify_source_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures)
var planner_source := _read_text(PLANNER_PATH, failures)
var build_loop_source := _source_between(
loader_source,
"func _process_m2_build_jobs()",
"func _materialize_m2_group_batch(",
failures
)
_expect_equal(
build_loop_source.count("_m2_build_batch_planner.plan_batch("),
1,
"one loader planner adapter",
failures
)
_expect_true(
not build_loop_source.contains("var batch_size:"),
"loader omits batch-size formula",
failures
)
_expect_true(
not build_loop_source.contains("mini(batch_size"),
"loader omits batch-count formula",
failures
)
for forbidden_dependency in [
"WorkerThreadPool",
"RenderingServer",
"ResourceLoader",
"MultiMesh",
"Node",
"Mutex",
]:
_expect_true(
not planner_source.contains(forbidden_dependency),
"planner omits %s" % forbidden_dependency,
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var planner := PLANNER_SCRIPT.new()
var started_microseconds := Time.get_ticks_usec()
for iteration in range(20000):
var plan: Dictionary = planner.plan_batch(
512,
iteration % 512,
iteration % 3 == 0,
8,
128
)
if int(plan.get("batch_count", -1)) < 0:
failures.append("timing iteration returned negative batch count")
break
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_true(elapsed_milliseconds < 1000.0, "20000 batch plans bounded", failures)
return elapsed_milliseconds
func _plan(
transform_count: int,
current_offset: int,
has_animated_prototype: bool,
animated_batch_limit: int,
static_batch_limit: int
) -> Dictionary:
var planner := PLANNER_SCRIPT.new()
return planner.plan_batch(
transform_count,
current_offset,
has_animated_prototype,
animated_batch_limit,
static_batch_limit
)
func _expect_plan(
plan: Dictionary,
expected_batch_size: int,
expected_batch_count: int,
expected_next_offset: int,
expected_group_complete: bool,
label: String,
failures: Array[String]
) -> void:
_expect_equal(
int(plan.get("effective_batch_size", -1)),
expected_batch_size,
"%s effective batch size" % label,
failures
)
_expect_equal(
int(plan.get("batch_count", -1)),
expected_batch_count,
"%s batch count" % label,
failures
)
_expect_equal(
int(plan.get("next_offset", -1)),
expected_next_offset,
"%s next offset" % label,
failures
)
_expect_true(
bool(plan.get("group_complete", not expected_group_complete))
== expected_group_complete,
"%s group complete" % label,
failures
)
func _read_text(path: String, failures: Array[String]) -> String:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
failures.append("cannot open %s" % path)
return ""
return file.get_as_text()
func _source_between(
source: String,
start_marker: String,
end_marker: String,
failures: Array[String]
) -> String:
var start_offset := source.find(start_marker)
var end_offset := source.find(end_marker, start_offset + start_marker.length())
if start_offset < 0 or end_offset <= start_offset:
failures.append("cannot isolate source between %s and %s" % [start_marker, end_marker])
return ""
return source.substr(start_offset, end_offset - start_offset)
func _expect_equal(
actual_value: int,
expected_value: int,
label: String,
failures: Array[String]
) -> void:
if actual_value != expected_value:
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
if not actual_value:
failures.append("%s expected true" % label)
@@ -0,0 +1 @@
uid://bw088yviu4kk3
+296
View File
@@ -0,0 +1,296 @@
extends SceneTree
## Asset-free contract, dependency and timing regression for ADT M2 grouping.
const GROUPER_SCRIPT := preload("res://src/render/m2/m2_placement_grouper.gd")
const TRANSFORM_RESOLVER_SCRIPT := preload(
"res://src/render/m2/m2_placement_transform_resolver.gd"
)
const GROUPER_PATH := "res://src/render/m2/m2_placement_grouper.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_empty_and_invalid_inputs(failures)
_verify_historical_path_normalization(failures)
_verify_group_order(failures)
_verify_tile_local_defaults(failures)
_verify_regular_transform(failures)
_verify_waterfall_transform(failures)
_verify_scale_clamp(failures)
_verify_fresh_output(failures)
_verify_source_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("M2_PLACEMENT_GROUPER: %s" % failure)
quit(1)
return
print(
"M2_PLACEMENT_GROUPER PASS cases=9 iterations=100 placements=256 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_empty_and_invalid_inputs(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
_expect_equal(
grouper.group_placements(Vector3.ZERO, PackedStringArray(), []).size(),
0,
"empty",
failures
)
var placements: Array = [
"invalid",
{"name_id": -1},
{"name_id": 2},
{"name_id": 0},
]
var groups: Dictionary = grouper.group_placements(
Vector3.ZERO,
PackedStringArray([""]),
placements
)
_expect_equal(groups.size(), 0, "invalid placements", failures)
func _verify_historical_path_normalization(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var names := PackedStringArray([
"World\\Generic\\Tree.mdx",
"World/Generic/Rock.mdl",
"World/Generic/Upper.MDX",
])
var placements: Array = [{"name_id": 0}, {"name_id": 1}, {"name_id": 2}]
var groups: Dictionary = grouper.group_placements(Vector3.ZERO, names, placements)
_expect_true(groups.has("World/Generic/Tree.m2"), "backslash and mdx normalization", failures)
_expect_true(groups.has("World/Generic/Rock.m2"), "mdl normalization", failures)
_expect_true(groups.has("World/Generic/Upper.MDX"), "uppercase suffix preservation", failures)
func _verify_group_order(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var placements: Array = [
{"name_id": 0, "pos": Vector3(3.0, 0.0, 0.0)},
{"name_id": 1, "pos": Vector3(8.0, 0.0, 0.0)},
{"name_id": 0, "pos": Vector3(5.0, 0.0, 0.0)},
]
var groups: Dictionary = grouper.group_placements(
Vector3.ZERO,
PackedStringArray(["Tree.m2", "Rock.m2"]),
placements
)
_expect_equal(groups.size(), 2, "group count", failures)
var tree_transforms: Array = groups.get("Tree.m2", [])
_expect_equal(tree_transforms.size(), 2, "tree transform count", failures)
if tree_transforms.size() == 2:
_expect_vector_near(tree_transforms[0].origin, Vector3(3.0, 0.0, 0.0), "first tree", failures)
_expect_vector_near(tree_transforms[1].origin, Vector3(5.0, 0.0, 0.0), "second tree", failures)
func _verify_tile_local_defaults(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var groups: Dictionary = grouper.group_placements(
Vector3(10.0, 20.0, 30.0),
PackedStringArray(["Tree.m2"]),
[{"name_id": 0}]
)
var transform: Transform3D = (groups["Tree.m2"] as Array)[0]
_expect_vector_near(transform.origin, Vector3(-10.0, -20.0, -30.0), "default local position", failures)
_expect_basis_near(transform.basis, Basis.IDENTITY, "default basis and scale", failures)
func _verify_regular_transform(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var rotation := Vector3(0.2, -0.4, 0.6)
var groups: Dictionary = grouper.group_placements(
Vector3(1.0, 2.0, 3.0),
PackedStringArray(["Tree.m2"]),
[{
"name_id": 0,
"pos": Vector3(5.0, 8.0, 13.0),
"rot": rotation,
"scale": 2.0,
}]
)
var transform: Transform3D = (groups["Tree.m2"] as Array)[0]
_expect_basis_near(transform.basis, Basis.from_euler(rotation).scaled(Vector3.ONE * 2.0), "regular basis", failures)
_expect_vector_near(transform.origin, Vector3(4.0, 6.0, 10.0), "regular origin", failures)
func _verify_waterfall_transform(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var resolver := TRANSFORM_RESOLVER_SCRIPT.new()
var path := "World/X/ElwynnTallWaterfall01.m2"
var rotation := Vector3(0.05, -0.2, 0.15)
var scale_value := 1.5
var position := Vector3(20.0, 30.0, 40.0)
var tile_origin := Vector3(2.0, 3.0, 4.0)
var groups: Dictionary = grouper.group_placements(
tile_origin,
PackedStringArray([path]),
[{
"name_id": 0,
"pos": position,
"rot": rotation,
"scale": scale_value,
}]
)
var transform: Transform3D = (groups[path] as Array)[0]
var expected_basis: Basis = resolver.resolve_basis(rotation, path).scaled(
Vector3.ONE * scale_value
)
var expected_origin: Vector3 = (
position
- tile_origin
+ resolver.resolve_origin_offset(rotation, path, scale_value)
)
_expect_basis_near(transform.basis, expected_basis, "waterfall basis", failures)
_expect_vector_near(transform.origin, expected_origin, "waterfall origin", failures)
func _verify_scale_clamp(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var groups: Dictionary = grouper.group_placements(
Vector3.ZERO,
PackedStringArray(["Tree.m2"]),
[{"name_id": 0, "scale": -5.0}]
)
var transform: Transform3D = (groups["Tree.m2"] as Array)[0]
_expect_basis_near(
transform.basis,
Basis.IDENTITY.scaled(Vector3.ONE * 0.0001),
"negative scale clamp",
failures
)
func _verify_fresh_output(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var names := PackedStringArray(["Tree.m2"])
var placements: Array = [{"name_id": 0}]
var first: Dictionary = grouper.group_placements(Vector3.ZERO, names, placements)
(first["Tree.m2"] as Array).clear()
var second: Dictionary = grouper.group_placements(Vector3.ZERO, names, placements)
_expect_equal((second["Tree.m2"] as Array).size(), 1, "fresh output", failures)
func _verify_source_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures)
var grouper_source := _read_text(GROUPER_PATH, failures)
var grouping_task_source := _source_between(
loader_source,
"func _group_tile_m2_task(",
"func _drain_m2_group_results()",
failures
)
_expect_equal(
loader_source.count("_m2_placement_grouper.group_placements("),
1,
"one loader grouping adapter",
failures
)
_expect_true(
not grouping_task_source.contains("for placement_variant in m2_placements"),
"loader omits grouping loop",
failures
)
_expect_equal(
grouper_source.count("placement_transform_resolver.resolve_basis("),
1,
"one grouper basis adapter",
failures
)
_expect_equal(
grouper_source.count("placement_transform_resolver.resolve_origin_offset("),
1,
"one grouper offset adapter",
failures
)
for forbidden_dependency in ["WorkerThreadPool", "Mutex", "RenderingServer", "ResourceLoader"]:
_expect_true(
not grouper_source.contains(forbidden_dependency),
"grouper omits %s" % forbidden_dependency,
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var grouper := GROUPER_SCRIPT.new()
var names := PackedStringArray(["Tree.m2", "Rock.mdx"])
var placements: Array = []
for placement_index in range(256):
placements.append({
"name_id": placement_index % 2,
"pos": Vector3(float(placement_index), 0.0, float(-placement_index)),
"rot": Vector3(0.1, 0.2, 0.3),
"scale": 1.0,
})
var started_microseconds := Time.get_ticks_usec()
for iteration in range(100):
var groups: Dictionary = grouper.group_placements(Vector3.ZERO, names, placements)
if groups.size() != 2:
failures.append("timing iteration %d produced wrong group count" % iteration)
break
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_true(elapsed_milliseconds < 1000.0, "100 x 256 grouping 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 _source_between(
source: String,
start_marker: String,
end_marker: String,
failures: Array[String]
) -> String:
var start_offset := source.find(start_marker)
var end_offset := source.find(end_marker, start_offset + start_marker.length())
if start_offset < 0 or end_offset <= start_offset:
failures.append("cannot isolate source between %s and %s" % [start_marker, end_marker])
return ""
return source.substr(start_offset, end_offset - start_offset)
func _expect_basis_near(
actual_basis: Basis,
expected_basis: Basis,
label: String,
failures: Array[String]
) -> void:
if not actual_basis.is_equal_approx(expected_basis):
failures.append("%s basis mismatch" % label)
func _expect_vector_near(
actual_value: Vector3,
expected_value: Vector3,
label: String,
failures: Array[String]
) -> void:
if not actual_value.is_equal_approx(expected_value):
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
func _expect_equal(
actual_value: int,
expected_value: int,
label: String,
failures: Array[String]
) -> void:
if actual_value != expected_value:
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
if not actual_value:
failures.append("%s expected true" % label)
@@ -0,0 +1 @@
uid://bbx57qy0lp11w
@@ -0,0 +1,233 @@
extends SceneTree
## Asset-free formula, dependency and timing regression for ADT M2 transforms.
const RESOLVER_SCRIPT := preload("res://src/render/m2/m2_placement_transform_resolver.gd")
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
const GROUPER_PATH := "res://src/render/m2/m2_placement_grouper.gd"
const WATERFALL_WORLD_YAW_OFFSET := PI * 0.5
const CLIFFROCK_WORLD_YAW_OFFSET := PI
func _initialize() -> void:
var failures: Array[String] = []
_verify_regular_basis(failures)
_verify_cliffrock_basis(failures)
_verify_new_waterfall_basis(failures)
_verify_tall_waterfall_basis_and_offset(failures)
_verify_path_normalization_and_scale_clamp(failures)
_verify_loader_boundary(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("M2_PLACEMENT_TRANSFORM_RESOLVER: %s" % failure)
quit(1)
return
print(
"M2_PLACEMENT_TRANSFORM_RESOLVER PASS cases=9 iterations=10000 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_regular_basis(failures: Array[String]) -> void:
var resolver: RefCounted = RESOLVER_SCRIPT.new()
var rotation := Vector3(0.2, -0.7, 1.1)
_expect_basis_near(
resolver.call("resolve_basis", rotation, "World/Generic/Tree.m2"),
Basis.from_euler(rotation),
"regular basis",
failures
)
_expect_vector_near(
resolver.call("resolve_origin_offset", rotation, "World/Generic/Tree.m2", 3.0),
Vector3.ZERO,
"regular offset",
failures
)
func _verify_cliffrock_basis(failures: Array[String]) -> void:
var resolver: RefCounted = RESOLVER_SCRIPT.new()
var rotation := Vector3(0.3, 0.4, -0.2)
var expected_basis := (
Basis(Vector3.UP, rotation.y + CLIFFROCK_WORLD_YAW_OFFSET)
* Basis(Vector3.BACK, rotation.x)
* Basis(Vector3.RIGHT, -rotation.z)
)
_expect_basis_near(
resolver.call("resolve_basis", rotation, "WORLD\\AZEROTH\\ElwynnCliffRock01.MDX"),
expected_basis,
"cliffrock basis",
failures
)
func _verify_new_waterfall_basis(failures: Array[String]) -> void:
var resolver: RefCounted = RESOLVER_SCRIPT.new()
var rotation := Vector3(-0.1, 0.8, 0.2)
var fall_axis := Vector3(-0.138742, 0.990329, 0.0).normalized()
var expected_basis := (
Basis(Vector3.UP, WATERFALL_WORLD_YAW_OFFSET)
* Basis.from_euler(rotation)
* Basis(fall_axis, PI * 0.5)
)
_expect_basis_near(
resolver.call("resolve_basis", rotation, "world/x/NewWaterfall.m2"),
expected_basis,
"new waterfall basis",
failures
)
_expect_vector_near(
resolver.call("resolve_origin_offset", rotation, "world/x/NewWaterfall.m2", 1.0),
Vector3.ZERO,
"new waterfall offset",
failures
)
func _verify_tall_waterfall_basis_and_offset(failures: Array[String]) -> void:
var resolver: RefCounted = RESOLVER_SCRIPT.new()
var rotation := Vector3(0.05, -0.2, 0.15)
var path := "world/x/ElwynnTallWaterfall01.m2"
var fall_axis := Vector3(-0.068962, 0.997619, 0.0).normalized()
var placement_basis := (
Basis(Vector3.UP, WATERFALL_WORLD_YAW_OFFSET) * Basis.from_euler(rotation)
)
var expected_basis := placement_basis * Basis(fall_axis, -PI * 0.5)
var anchor := Vector3(-2.667799, 89.62273, 0.00129)
var expected_offset := (placement_basis * anchor - expected_basis * anchor) * 1.5
_expect_basis_near(
resolver.call("resolve_basis", rotation, path),
expected_basis,
"tall waterfall basis",
failures
)
_expect_vector_near(
resolver.call("resolve_origin_offset", rotation, path, 1.5),
expected_offset,
"tall waterfall offset",
failures
)
func _verify_path_normalization_and_scale_clamp(failures: Array[String]) -> void:
var resolver: RefCounted = RESOLVER_SCRIPT.new()
var path := "WORLD\\X\\ELWYNNTALLWATERFALL01.MDX"
var minimum_offset: Vector3 = resolver.call("resolve_origin_offset", Vector3.ZERO, path, 0.0001)
_expect_vector_near(
resolver.call("resolve_origin_offset", Vector3.ZERO, path, -9.0),
minimum_offset,
"negative scale clamp",
failures
)
_expect_basis_near(
resolver.call("resolve_basis", Vector3.ZERO, "ElwynnCliffRock02.m2"),
Basis(Vector3.UP, PI),
"second cliffrock",
failures
)
func _verify_loader_boundary(failures: Array[String]) -> void:
var source := _read_text(LOADER_PATH, failures)
var grouper_source := _read_text(GROUPER_PATH, failures)
_expect_true(
source.contains("M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()"),
"loader composes resolver",
failures
)
_expect_equal(
source.count("_m2_placement_transform_resolver.resolve_basis("),
2,
"two direct loader basis adapters",
failures
)
_expect_equal(
source.count("_m2_placement_transform_resolver.resolve_origin_offset("),
2,
"two direct loader offset adapters",
failures
)
_expect_equal(
grouper_source.count("placement_transform_resolver.resolve_basis("),
1,
"one grouped basis adapter",
failures
)
_expect_equal(
grouper_source.count("placement_transform_resolver.resolve_origin_offset("),
1,
"one grouped offset adapter",
failures
)
_expect_true(
not source.contains("func _adt_m2_placement_basis("),
"loader omits basis formula",
failures
)
_expect_true(
not source.contains("func _waterfall_sheet_twist("),
"loader omits waterfall formula",
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var resolver: RefCounted = RESOLVER_SCRIPT.new()
var rotation := Vector3(0.1, 0.2, 0.3)
var started_microseconds := Time.get_ticks_usec()
for iteration in range(10000):
var path := (
"world/x/ElwynnTallWaterfall01.m2"
if iteration % 4 == 0
else "world/generic/Tree.m2"
)
resolver.call("resolve_basis", rotation, path)
resolver.call("resolve_origin_offset", rotation, path, 1.0)
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_true(elapsed_milliseconds < 1000.0, "10000 transform pairs bounded", failures)
return elapsed_milliseconds
func _read_text(path: String, failures: Array[String]) -> String:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
failures.append("cannot open %s" % path)
return ""
return file.get_as_text()
func _expect_basis_near(
actual_basis: Basis,
expected_basis: Basis,
label: String,
failures: Array[String]
) -> void:
if not actual_basis.is_equal_approx(expected_basis):
failures.append("%s basis mismatch" % label)
func _expect_vector_near(
actual_value: Vector3,
expected_value: Vector3,
label: String,
failures: Array[String]
) -> void:
if not actual_value.is_equal_approx(expected_value):
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
func _expect_equal(
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)

Some files were not shown because too many files have changed in this diff Show More