Compare commits
36 Commits
a85e416cb8
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d74d996d5 | |||
| f88bf9714e | |||
| 7587d3bd3c | |||
| 9df32d38ef | |||
| 9c9020e60f | |||
| c34e538dcf | |||
| 396be5e69d | |||
| 1792938567 | |||
| 0a09d7bc1e | |||
| 91f0ba8433 | |||
| 1b6c46870d | |||
| 2342430439 | |||
| c41e8886a6 | |||
| acb3a809bb | |||
| c13c993645 | |||
| 57ae1b8f16 | |||
| 24aef13801 | |||
| 4b90dbc1c1 | |||
| f800e11cd2 | |||
| 0d2c820ea7 | |||
| b568768866 | |||
| f36fabb8e1 | |||
| 9e69a8a95b | |||
| eb94cfc646 | |||
| aa21aafdfa | |||
| ea08b8ef51 | |||
| 630a0c131d | |||
| 10815be766 | |||
| 97480e06bb | |||
| 6617dfe74e | |||
| 61bad09a9c | |||
| 606770c525 | |||
| b73b2e74a2 | |||
| 1807c3a363 | |||
| 3c5880f190 | |||
| a7b2f9475b |
@@ -950,6 +950,70 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
||||
- 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-16 World Environment Snapshot Facade
|
||||
|
||||
- `WorldEnvironmentSnapshot` carries one immutable finite time-of-day value,
|
||||
@@ -964,6 +1028,33 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
||||
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,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,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
|
||||
@@ -12,9 +12,16 @@
|
||||
| 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) |
|
||||
| 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) |
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
# 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
|
||||
|
||||
- A later package may extract the M2 build-job state machine behind explicit
|
||||
budgets while retaining this pure grouping input.
|
||||
- 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/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-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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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`; `M03-RND-STREAMING-PLANNER-001`; `M03-RND-SCHEDULER-001`; `M03-RND-INTERNAL-ACCESS-GATE-001`; `M03-RND-FACADE-GROUND-QUERY-001`; `M03-RND-FACADE-ENVIRONMENT-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 unique/transform/grouping packages |
|
||||
| Owners | Renderer workstream / milestone integrator |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-facade-environment`, 2026-07-16 |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-placement-grouper`, 2026-07-16 |
|
||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||
|
||||
## Purpose
|
||||
@@ -40,6 +40,18 @@ flowchart LR
|
||||
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
|
||||
Native --> Parsed[Parsed tile/model data]
|
||||
Parsed --> Loader
|
||||
Loader --> Scene[SceneTree nodes]
|
||||
@@ -49,6 +61,10 @@ flowchart LR
|
||||
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:
|
||||
@@ -69,9 +85,9 @@ Forbidden dependencies:
|
||||
## Public API
|
||||
|
||||
Runtime, capture and probe callers use `WorldRenderFacade` for focus, immutable
|
||||
environment input, metrics and rendered-ground diagnostics. `StreamingWorldLoader`
|
||||
and `WowSkyController` remain internal scene implementations while a later M03
|
||||
package adds entity-visual commands.
|
||||
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.
|
||||
|
||||
@@ -90,10 +106,21 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
||||
| `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 |
|
||||
|
||||
Публичным contract не считаются `StreamingWorldLoader` methods/properties,
|
||||
внутренние dictionaries, queues, job records и generated node names. Scene-owned
|
||||
@@ -107,17 +134,27 @@ loader configuration remains transitional composition data, not a caller API.
|
||||
| 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 | `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 |
|
||||
| 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 |
|
||||
|
||||
@@ -148,6 +185,16 @@ flowchart TD
|
||||
T --> Q[Tile load queue]
|
||||
Q --> Parse[Worker parse/cache load]
|
||||
Parse --> R[Result queues]
|
||||
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 --> M2
|
||||
R --> B[RenderBudgetScheduler permits]
|
||||
B --> Terrain[Terrain attach/upgrade]
|
||||
B --> M2[M2 group/MultiMesh attach]
|
||||
@@ -160,6 +207,11 @@ flowchart TD
|
||||
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
|
||||
@@ -218,6 +270,11 @@ sequenceDiagram
|
||||
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
|
||||
@@ -236,6 +293,27 @@ sequenceDiagram
|
||||
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.
|
||||
- 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
|
||||
@@ -264,6 +342,9 @@ sequenceDiagram
|
||||
| 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 |
|
||||
@@ -309,6 +390,19 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
- 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.
|
||||
- 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.
|
||||
@@ -338,14 +432,21 @@ 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 |
|
||||
| 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 | Facade contracts cover focus, environment time, metrics and loaded-mesh ground query | Entity visuals 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 |
|
||||
@@ -380,7 +481,10 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
|---|---|
|
||||
| `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 |
|
||||
@@ -390,7 +494,9 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| `src/tools/verify_streaming_focus.gd` | Headless contract, dependency and runtime/tool 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 |
|
||||
|
||||
@@ -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,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,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
|
||||
@@ -9,6 +9,8 @@ const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot
|
||||
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.
|
||||
@@ -18,14 +20,19 @@ const WORLD_ENVIRONMENT_SNAPSHOT_SCRIPT := preload("res://src/render/environment
|
||||
## 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
|
||||
|
||||
|
||||
@@ -116,6 +123,43 @@ func apply_environment_snapshot(world_environment_snapshot: RefCounted) -> bool:
|
||||
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
|
||||
@@ -177,3 +221,23 @@ func _resolve_world_environment_controller() -> Node:
|
||||
_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
|
||||
|
||||
@@ -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
|
||||
@@ -99,6 +100,10 @@ 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
|
||||
@@ -100,6 +101,10 @@ 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)
|
||||
|
||||
@@ -18,6 +18,21 @@ const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordina
|
||||
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
|
||||
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")
|
||||
const RENDERED_GROUND_SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd")
|
||||
const TERRAIN_QUALITY_MESH_CACHE_SCRIPT := preload("res://src/render/terrain/terrain_quality_mesh_cache.gd")
|
||||
const TERRAIN_CHUNK_LOD_POLICY_SCRIPT := preload("res://src/render/terrain/terrain_chunk_lod_policy.gd")
|
||||
const TERRAIN_CHUNK_LOD_PLANNER_SCRIPT := preload("res://src/render/terrain/terrain_chunk_lod_planner.gd")
|
||||
const TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER_SCRIPT := preload(
|
||||
"res://src/render/terrain/terrain_chunk_geometry_queue_planner.gd"
|
||||
)
|
||||
const M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_unique_placement_registry.gd"
|
||||
)
|
||||
const M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_placement_transform_resolver.gd"
|
||||
)
|
||||
const M2_PLACEMENT_GROUPER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_placement_grouper.gd"
|
||||
)
|
||||
const STREAMING_TARGET_PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
|
||||
const STREAMING_TARGET_POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
|
||||
const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd")
|
||||
@@ -30,8 +45,6 @@ const RENDER_GROUND_QUERY_RAY_HEIGHT_UNITS := 5000.0
|
||||
|
||||
const TILE_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
|
||||
const CHUNK_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_CHUNK_SIZE_YARDS
|
||||
const ADT_CLIFFROCK_WORLD_YAW_OFFSET := PI
|
||||
const ADT_WATERFALL_WORLD_YAW_OFFSET := PI * 0.5
|
||||
const QUALITY_CUSTOM := "Custom"
|
||||
const QUALITY_PERFORMANCE := "Performance"
|
||||
const QUALITY_BALANCED := "Balanced"
|
||||
@@ -180,6 +193,10 @@ const QUALITY_HIGH := "High"
|
||||
|
||||
var _builder
|
||||
var _streaming_target_planner := STREAMING_TARGET_PLANNER_SCRIPT.new()
|
||||
var _terrain_chunk_lod_planner := TERRAIN_CHUNK_LOD_PLANNER_SCRIPT.new()
|
||||
var _terrain_chunk_geometry_operation_planner := (
|
||||
TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER_SCRIPT.new()
|
||||
)
|
||||
var _map_dir := ""
|
||||
var _abs_map_dir := ""
|
||||
var _wdt_info: Dictionary = {}
|
||||
@@ -197,7 +214,11 @@ var _m2_group_result_mutex := Mutex.new()
|
||||
var _m2_group_result_queue: Array = []
|
||||
var _m2_build_jobs: Dictionary = {}
|
||||
var _m2_build_queue: Array = []
|
||||
var _m2_unique_registry: Dictionary = {}
|
||||
var _m2_unique_placement_registry := (
|
||||
M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT.new()
|
||||
)
|
||||
var _m2_placement_transform_resolver := M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()
|
||||
var _m2_placement_grouper := M2_PLACEMENT_GROUPER_SCRIPT.new()
|
||||
var _m2_mesh_cache: Dictionary = {}
|
||||
var _m2_mesh_load_requests: Dictionary = {}
|
||||
var _m2_mesh_finalize_queue: Array = []
|
||||
@@ -241,8 +262,7 @@ var _last_refresh_focus_pos := Vector3.ZERO
|
||||
var _has_refresh_focus := false
|
||||
var _tile_mesh_cache: Dictionary = {}
|
||||
var _tile_mesh_cache_order: Array = []
|
||||
var _terrain_quality_mesh_cache: Dictionary = {}
|
||||
var _terrain_quality_mesh_cache_order: Array = []
|
||||
var _terrain_quality_mesh_cache: RefCounted = TERRAIN_QUALITY_MESH_CACHE_SCRIPT.new()
|
||||
var _wmo_prototype_cache: Dictionary = {}
|
||||
var _wmo_render_cache: Dictionary = {}
|
||||
var _wmo_render_missing_cache: Dictionary = {}
|
||||
@@ -797,6 +817,8 @@ func _refresh_streaming_targets_at(focus_pos: Vector3, force: bool) -> void:
|
||||
|
||||
func _apply_streaming_target(wanted_tiles: Dictionary, retained_tiles: Dictionary, focus_pos: Vector3) -> void:
|
||||
_last_focus_pos = focus_pos
|
||||
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(focus_pos.x, focus_pos.y, focus_pos.z)
|
||||
var terrain_chunk_lod_policy = _create_terrain_chunk_lod_policy()
|
||||
|
||||
var filtered_load_queue: Array = []
|
||||
for request in _tile_load_queue:
|
||||
@@ -831,7 +853,7 @@ func _apply_streaming_target(wanted_tiles: Dictionary, retained_tiles: Dictionar
|
||||
state["wanted"] = wanted
|
||||
state["retained"] = retained
|
||||
if wanted:
|
||||
state["desired_lods"] = _compute_desired_lods(state, focus_pos)
|
||||
state["desired_lods"] = _compute_desired_lods(state, typed_focus_position, terrain_chunk_lod_policy)
|
||||
state["desired_tile_lod"] = -1 if not state["desired_lods"].is_empty() else _compute_desired_tile_lod(state, focus_pos)
|
||||
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos)
|
||||
elif retained:
|
||||
@@ -852,34 +874,29 @@ func _apply_streaming_target(wanted_tiles: Dictionary, retained_tiles: Dictionar
|
||||
_rebuild_chunk_queues()
|
||||
|
||||
|
||||
func _chunk_origin_for(state: Dictionary, chunk_id: int) -> Vector3:
|
||||
var chunks: Array = state["data"].get("chunks", [])
|
||||
if chunk_id >= 0 and chunk_id < chunks.size():
|
||||
var c: Dictionary = chunks[chunk_id]
|
||||
if not c.is_empty():
|
||||
return c.get("origin", Vector3.ZERO)
|
||||
return state.get("origin", Vector3.ZERO)
|
||||
|
||||
|
||||
func _rebuild_chunk_queues() -> void:
|
||||
_chunk_remove_queue.clear()
|
||||
_chunk_create_queue.clear()
|
||||
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(
|
||||
_last_focus_pos.x,
|
||||
_last_focus_pos.y,
|
||||
_last_focus_pos.z
|
||||
)
|
||||
var chunk_queue_plan: Dictionary = _terrain_chunk_geometry_operation_planner.plan(
|
||||
_tile_states,
|
||||
typed_focus_position
|
||||
)
|
||||
_chunk_remove_queue = chunk_queue_plan.get("remove_requests", [])
|
||||
_chunk_create_queue = chunk_queue_plan.get("create_requests", [])
|
||||
_tile_lod_remove_queue.clear()
|
||||
_tile_lod_create_queue.clear()
|
||||
|
||||
for key in _tile_states.keys():
|
||||
var state: Dictionary = _tile_states[key]
|
||||
var current_chunks: Dictionary = state["chunks"]
|
||||
var desired_lods: Dictionary = state["desired_lods"]
|
||||
var current_tile_lod: int = int(state.get("tile_lod", -1))
|
||||
var desired_tile_lod: int = int(state.get("desired_tile_lod", -1))
|
||||
var current_tile_lod_visible: bool = bool(state.get("tile_lod_visible", false))
|
||||
var desired_tile_lod_visible: bool = bool(state.get("desired_tile_lod_visible", false))
|
||||
|
||||
if desired_tile_lod >= 0:
|
||||
for chunk_id in current_chunks.keys():
|
||||
_chunk_remove_queue.append({"tile": key, "chunk": chunk_id})
|
||||
|
||||
if current_tile_lod != desired_tile_lod or current_tile_lod_visible != desired_tile_lod_visible:
|
||||
_tile_lod_create_queue.append({
|
||||
"tile": key,
|
||||
@@ -900,37 +917,7 @@ func _rebuild_chunk_queues() -> void:
|
||||
if current_tile_lod >= 0 and _is_tile_chunk_set_ready(state):
|
||||
_tile_lod_remove_queue.append({"tile": key})
|
||||
|
||||
for chunk_id in current_chunks.keys():
|
||||
if not desired_lods.has(chunk_id):
|
||||
_chunk_remove_queue.append({"tile": key, "chunk": chunk_id})
|
||||
continue
|
||||
if current_chunks[chunk_id]["lod"] != desired_lods[chunk_id]:
|
||||
var lod_change_origin: Vector3 = _chunk_origin_for(state, chunk_id)
|
||||
var dx2: float = lod_change_origin.x - _last_focus_pos.x
|
||||
var dz2: float = lod_change_origin.z - _last_focus_pos.z
|
||||
_chunk_create_queue.append({
|
||||
"tile": key,
|
||||
"chunk": chunk_id,
|
||||
"lod": desired_lods[chunk_id],
|
||||
"dist_sq": dx2 * dx2 + dz2 * dz2,
|
||||
})
|
||||
|
||||
for chunk_id in desired_lods.keys():
|
||||
if current_chunks.has(chunk_id):
|
||||
continue
|
||||
var chunk_origin: Vector3 = _chunk_origin_for(state, chunk_id)
|
||||
var dx: float = chunk_origin.x - _last_focus_pos.x
|
||||
var dz: float = chunk_origin.z - _last_focus_pos.z
|
||||
_chunk_create_queue.append({
|
||||
"tile": key,
|
||||
"chunk": chunk_id,
|
||||
"lod": desired_lods[chunk_id],
|
||||
"dist_sq": dx * dx + dz * dz,
|
||||
})
|
||||
|
||||
# Sort after fully rebuilding so nearest chunks are processed first
|
||||
_chunk_create_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a["dist_sq"] < b["dist_sq"])
|
||||
# Sort tile LOD creates after fully rebuilding; chunk creates arrive nearest-first.
|
||||
_tile_lod_create_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a["dist_sq"] < b["dist_sq"])
|
||||
|
||||
@@ -1035,7 +1022,7 @@ func render_baseline_snapshot() -> Dictionary:
|
||||
"chunk_remove": _chunk_remove_queue.size(),
|
||||
},
|
||||
"cache_entries": _tile_mesh_cache.size(),
|
||||
"m2_active_unique_ids": _m2_unique_registry.size(),
|
||||
"m2_active_unique_ids": _m2_unique_placement_registry.active_count(),
|
||||
"wmo_instances": _wmo_registry.size(),
|
||||
}
|
||||
|
||||
@@ -2492,7 +2479,16 @@ func _finalize_loaded_tile(request: Dictionary, data: Dictionary) -> void:
|
||||
}
|
||||
|
||||
# Use _last_focus_pos — already set correctly for both editor and game.
|
||||
state["desired_lods"] = _compute_desired_lods(state, _last_focus_pos)
|
||||
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(
|
||||
_last_focus_pos.x,
|
||||
_last_focus_pos.y,
|
||||
_last_focus_pos.z
|
||||
)
|
||||
state["desired_lods"] = _compute_desired_lods(
|
||||
state,
|
||||
typed_focus_position,
|
||||
_create_terrain_chunk_lod_policy()
|
||||
)
|
||||
state["desired_tile_lod"] = -1 if not state["desired_lods"].is_empty() else _compute_desired_tile_lod(state, _last_focus_pos)
|
||||
state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos)
|
||||
_tile_states[key] = state
|
||||
@@ -2980,7 +2976,7 @@ func _clear_streamed_world() -> void:
|
||||
for key in _m2_build_jobs.keys():
|
||||
_cancel_m2_build_job(String(key))
|
||||
_m2_build_queue.clear()
|
||||
_m2_unique_registry.clear()
|
||||
_m2_unique_placement_registry.clear()
|
||||
_m2_mesh_load_requests.clear()
|
||||
_m2_mesh_finalize_queue.clear()
|
||||
_m2_runtime_rebuild_required_cache.clear()
|
||||
@@ -3045,33 +3041,23 @@ func _reset_terrain_root_children() -> void:
|
||||
child.free()
|
||||
|
||||
|
||||
func _compute_desired_lods(state: Dictionary, cam_pos: Vector3) -> Dictionary:
|
||||
if not use_chunk_terrain_lods:
|
||||
return {}
|
||||
|
||||
var desired := {}
|
||||
func _compute_desired_lods(
|
||||
state: Dictionary,
|
||||
typed_focus_position: RefCounted,
|
||||
terrain_chunk_lod_policy: RefCounted
|
||||
) -> Dictionary:
|
||||
var chunks: Array = state["data"].get("chunks", [])
|
||||
return _terrain_chunk_lod_planner.plan(chunks, typed_focus_position, terrain_chunk_lod_policy)
|
||||
|
||||
var lod0_sq: float = (lod0_radius_chunks * CHUNK_SIZE) ** 2
|
||||
var lod1_sq: float = (lod1_radius_chunks * CHUNK_SIZE) ** 2
|
||||
var lod2_sq: float = (lod2_radius_chunks * CHUNK_SIZE) ** 2
|
||||
|
||||
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 := origin + Vector3(CHUNK_SIZE * 0.5, 0.0, CHUNK_SIZE * 0.5)
|
||||
var dx := center.x - cam_pos.x
|
||||
var dz := center.z - cam_pos.z
|
||||
var dist_sq := dx * dx + dz * dz
|
||||
|
||||
if dist_sq <= lod0_sq: desired[chunk_id] = 0
|
||||
elif dist_sq <= lod1_sq: desired[chunk_id] = 1
|
||||
elif dist_sq <= lod2_sq: desired[chunk_id] = 2
|
||||
|
||||
return desired
|
||||
func _create_terrain_chunk_lod_policy() -> RefCounted:
|
||||
return TERRAIN_CHUNK_LOD_POLICY_SCRIPT.new(
|
||||
use_chunk_terrain_lods,
|
||||
lod0_radius_chunks,
|
||||
lod1_radius_chunks,
|
||||
lod2_radius_chunks,
|
||||
CHUNK_SIZE
|
||||
)
|
||||
|
||||
|
||||
func _runtime_load_tile_radius() -> int:
|
||||
@@ -3998,86 +3984,6 @@ func _rebuild_cached_wmo_material(material: Material) -> Material:
|
||||
return WMO_BUILDER_SCRIPT._build_material(mat_def, textures, extracted_dir)
|
||||
|
||||
|
||||
func _adt_m2_placement_basis(rot: Vector3, rel_path: String = "") -> Basis:
|
||||
if not _is_waterfall_m2_path(rel_path):
|
||||
return _adt_m2_regular_placement_basis(rot, rel_path)
|
||||
|
||||
# ADTLoader stores MDDF yaw as raw_yaw - 90 degrees for historical Godot
|
||||
# facing alignment. In our tile-positive ADT world, waterfall models need a
|
||||
# world-space +90 degree yaw to anchor their top to the source pool. Twist
|
||||
# the sheet around its own local fall axis afterwards, so the anchor stays in
|
||||
# place but the alpha plane is not rendered edge-on.
|
||||
var basis := Basis.from_euler(rot)
|
||||
return (
|
||||
Basis(Vector3.UP, ADT_WATERFALL_WORLD_YAW_OFFSET)
|
||||
* basis
|
||||
* Basis(_waterfall_local_fall_axis(rel_path), _waterfall_sheet_twist(rel_path))
|
||||
)
|
||||
|
||||
|
||||
func _adt_m2_regular_placement_basis(rot: Vector3, rel_path: String = "") -> Basis:
|
||||
if not _is_elwynn_cliffrock_m2_path(rel_path):
|
||||
return Basis.from_euler(rot)
|
||||
|
||||
# Elwynn cliff rock M2s are open-backed shell meshes. Plain Godot Euler
|
||||
# points their hollow side out of the cliff at waterfall placements. Keep the
|
||||
# correction model-specific so normal props retain the native ADT rotation.
|
||||
return (
|
||||
Basis(Vector3.UP, rot.y + ADT_CLIFFROCK_WORLD_YAW_OFFSET)
|
||||
* Basis(Vector3.BACK, rot.x)
|
||||
* Basis(Vector3.RIGHT, -rot.z)
|
||||
)
|
||||
|
||||
|
||||
func _adt_m2_placement_origin_offset(rot: Vector3, rel_path: String, scale_value: float) -> Vector3:
|
||||
if not _is_waterfall_m2_path(rel_path):
|
||||
return Vector3.ZERO
|
||||
var twist := _waterfall_sheet_twist(rel_path)
|
||||
if is_zero_approx(twist):
|
||||
return Vector3.ZERO
|
||||
var anchor := _waterfall_sheet_twist_anchor(rel_path)
|
||||
if anchor == Vector3.ZERO:
|
||||
return Vector3.ZERO
|
||||
var base := Basis(Vector3.UP, ADT_WATERFALL_WORLD_YAW_OFFSET) * Basis.from_euler(rot)
|
||||
var twisted := base * Basis(_waterfall_local_fall_axis(rel_path), twist)
|
||||
return (base * anchor - twisted * anchor) * maxf(scale_value, 0.0001)
|
||||
|
||||
|
||||
func _is_waterfall_m2_path(rel_path: String) -> bool:
|
||||
var name := rel_path.replace("\\", "/").to_lower().get_file().get_basename()
|
||||
return name == "newwaterfall" or name == "elwynntallwaterfall01"
|
||||
|
||||
|
||||
func _is_elwynn_cliffrock_m2_path(rel_path: String) -> bool:
|
||||
var name := rel_path.replace("\\", "/").to_lower().get_file().get_basename()
|
||||
return name == "elwynncliffrock01" or name == "elwynncliffrock02"
|
||||
|
||||
|
||||
func _waterfall_local_fall_axis(rel_path: String) -> Vector3:
|
||||
var normalized := rel_path.replace("\\", "/").to_lower()
|
||||
if normalized.ends_with("newwaterfall.m2") or normalized.ends_with("newwaterfall.mdx"):
|
||||
return Vector3(-0.138742, 0.990329, 0.0).normalized()
|
||||
if normalized.ends_with("elwynntallwaterfall01.m2") or normalized.ends_with("elwynntallwaterfall01.mdx"):
|
||||
return Vector3(-0.068962, 0.997619, 0.0).normalized()
|
||||
return Vector3.UP
|
||||
|
||||
|
||||
func _waterfall_sheet_twist(rel_path: String) -> float:
|
||||
var normalized := rel_path.replace("\\", "/").to_lower()
|
||||
if normalized.ends_with("newwaterfall.m2") or normalized.ends_with("newwaterfall.mdx"):
|
||||
return PI * 0.5
|
||||
if normalized.ends_with("elwynntallwaterfall01.m2") or normalized.ends_with("elwynntallwaterfall01.mdx"):
|
||||
return -PI * 0.5
|
||||
return 0.0
|
||||
|
||||
|
||||
func _waterfall_sheet_twist_anchor(rel_path: String) -> Vector3:
|
||||
var normalized := rel_path.replace("\\", "/").to_lower()
|
||||
if normalized.ends_with("elwynntallwaterfall01.m2") or normalized.ends_with("elwynntallwaterfall01.mdx"):
|
||||
return Vector3(-2.667799, 89.62273, 0.00129)
|
||||
return Vector3.ZERO
|
||||
|
||||
|
||||
func _cancel_wmo_build_job(tile_key: String) -> void:
|
||||
if _wmo_build_jobs.has(tile_key):
|
||||
_wmo_build_jobs.erase(tile_key)
|
||||
@@ -4154,7 +4060,10 @@ func _request_tile_m2_assets(
|
||||
if _m2_group_tasks.has(key) or _m2_build_jobs.has(key):
|
||||
return result
|
||||
|
||||
var filtered := _reserve_tile_m2_placements(key, m2_placements)
|
||||
var filtered: Dictionary = _m2_unique_placement_registry.reserve(
|
||||
key,
|
||||
m2_placements
|
||||
)
|
||||
var filtered_placements: Array = filtered.get("placements", [])
|
||||
result["unique_keys"] = filtered.get("unique_keys", [])
|
||||
result["skipped_unique_keys"] = filtered.get("skipped_unique_keys", [])
|
||||
@@ -4168,46 +4077,6 @@ func _request_tile_m2_assets(
|
||||
return result
|
||||
|
||||
|
||||
func _reserve_tile_m2_placements(tile_key: String, m2_placements: Array) -> Dictionary:
|
||||
var filtered_placements: Array = []
|
||||
var unique_keys: Array = []
|
||||
var skipped_unique_keys: Array = []
|
||||
for index in m2_placements.size():
|
||||
var placement_variant = m2_placements[index]
|
||||
if not (placement_variant is Dictionary):
|
||||
continue
|
||||
var placement: Dictionary = placement_variant
|
||||
var unique_key := _m2_unique_key(placement)
|
||||
if unique_key.is_empty():
|
||||
filtered_placements.append(placement)
|
||||
continue
|
||||
if _m2_unique_registry.has(unique_key):
|
||||
var entry: Dictionary = _m2_unique_registry[unique_key]
|
||||
var owner_tile := String(entry.get("tile", ""))
|
||||
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
|
||||
_m2_unique_registry[unique_key] = {"tile": 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,
|
||||
}
|
||||
|
||||
|
||||
func _m2_unique_key(placement: Dictionary) -> String:
|
||||
var uid: int = int(placement.get("unique_id", -1))
|
||||
if uid <= 0:
|
||||
return ""
|
||||
return "uid:%d" % uid
|
||||
|
||||
|
||||
func _release_tile_m2_unique_keys(state: Dictionary, notify_candidates: bool = true) -> void:
|
||||
var tile_key := String(state.get("key", ""))
|
||||
var unique_keys: Array = state.get("m2_unique_keys", [])
|
||||
@@ -4216,16 +4085,7 @@ func _release_tile_m2_unique_keys(state: Dictionary, notify_candidates: bool = t
|
||||
state["m2_skipped_unique_keys"] = []
|
||||
return
|
||||
|
||||
var released_keys: Array = []
|
||||
for unique_key_variant in unique_keys:
|
||||
var unique_key := String(unique_key_variant)
|
||||
if unique_key.is_empty() or not _m2_unique_registry.has(unique_key):
|
||||
continue
|
||||
var entry: Dictionary = _m2_unique_registry[unique_key]
|
||||
if String(entry.get("tile", "")) != tile_key:
|
||||
continue
|
||||
_m2_unique_registry.erase(unique_key)
|
||||
released_keys.append(unique_key)
|
||||
var released_keys: Array = _m2_unique_placement_registry.release(tile_key, unique_keys)
|
||||
|
||||
state["m2_unique_keys"] = []
|
||||
state["m2_skipped_unique_keys"] = []
|
||||
@@ -4263,32 +4123,11 @@ func _group_tile_m2_task(
|
||||
tile_origin: Vector3,
|
||||
m2_names: PackedStringArray,
|
||||
m2_placements: Array) -> void:
|
||||
var groups: Dictionary = {}
|
||||
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: String = str(m2_names[name_id]).replace("\\", "/")
|
||||
if normalized.ends_with(".mdx") or normalized.ends_with(".mdl"):
|
||||
normalized = normalized.get_basename() + ".m2"
|
||||
if normalized.is_empty():
|
||||
continue
|
||||
|
||||
var world_pos: Vector3 = placement.get("pos", Vector3.ZERO)
|
||||
var pos: Vector3 = world_pos - tile_origin
|
||||
var rot: Vector3 = placement.get("rot", Vector3.ZERO)
|
||||
var scale_value: float = float(placement.get("scale", 1.0))
|
||||
var basis := _adt_m2_placement_basis(rot, normalized)
|
||||
var offset := _adt_m2_placement_origin_offset(rot, normalized, scale_value)
|
||||
var xform := Transform3D(basis.scaled(Vector3.ONE * maxf(scale_value, 0.0001)), pos + offset)
|
||||
|
||||
if not groups.has(normalized):
|
||||
groups[normalized] = []
|
||||
(groups[normalized] as Array).append(xform)
|
||||
var groups: Dictionary = _m2_placement_grouper.group_placements(
|
||||
tile_origin,
|
||||
m2_names,
|
||||
m2_placements
|
||||
)
|
||||
|
||||
_m2_group_result_mutex.lock()
|
||||
_m2_group_result_queue.append({
|
||||
@@ -4971,8 +4810,18 @@ func _build_tile_m2_placeholders(tile_root: Node3D, tile_origin: Vector3, m2_nam
|
||||
var local_pos: Vector3 = placement.get("pos", Vector3.ZERO) - tile_origin
|
||||
var rot: Vector3 = placement.get("rot", Vector3.ZERO)
|
||||
var scale_value: float = float(placement.get("scale", 1.0))
|
||||
var xform_basis := _adt_m2_placement_basis(rot, rel_path).scaled(Vector3.ONE * maxf(scale_value, 0.01))
|
||||
var xf := Transform3D(xform_basis, local_pos + _adt_m2_placement_origin_offset(rot, rel_path, scale_value))
|
||||
var xform_basis := _m2_placement_transform_resolver.resolve_basis(
|
||||
rot,
|
||||
rel_path
|
||||
).scaled(Vector3.ONE * maxf(scale_value, 0.01))
|
||||
var xf := Transform3D(
|
||||
xform_basis,
|
||||
local_pos + _m2_placement_transform_resolver.resolve_origin_offset(
|
||||
rot,
|
||||
rel_path,
|
||||
scale_value
|
||||
)
|
||||
)
|
||||
if is_tree:
|
||||
tree_transforms.append(xf)
|
||||
else:
|
||||
@@ -5022,8 +4871,16 @@ func _instantiate_m2(rel_path: String, placement: Dictionary, tile_origin: Vecto
|
||||
var rot: Vector3 = placement.get("rot", Vector3.ZERO)
|
||||
var scale_value: float = float(placement.get("scale", 1.0))
|
||||
node.transform = Transform3D(
|
||||
_adt_m2_placement_basis(rot, rel_path).scaled(Vector3.ONE * maxf(scale_value, 0.0001)),
|
||||
local_pos + _adt_m2_placement_origin_offset(rot, rel_path, scale_value))
|
||||
_m2_placement_transform_resolver.resolve_basis(
|
||||
rot,
|
||||
rel_path
|
||||
).scaled(Vector3.ONE * maxf(scale_value, 0.0001)),
|
||||
local_pos + _m2_placement_transform_resolver.resolve_origin_offset(
|
||||
rot,
|
||||
rel_path,
|
||||
scale_value
|
||||
)
|
||||
)
|
||||
return node
|
||||
|
||||
|
||||
@@ -5500,53 +5357,28 @@ func _clear_tile_mesh_cache() -> void:
|
||||
|
||||
|
||||
func _restore_quality_terrain_mesh(tile_key: String, state: Dictionary) -> Dictionary:
|
||||
if not _terrain_quality_mesh_cache.has(tile_key):
|
||||
var cached: Dictionary = _terrain_quality_mesh_cache.call("restore", tile_key)
|
||||
if cached.is_empty():
|
||||
return state
|
||||
var cached: Dictionary = _terrain_quality_mesh_cache[tile_key]
|
||||
var source := String(cached.get("source", "baked_full"))
|
||||
if source == "control_splat_cache" or source == "splat_cache" or source == "splat":
|
||||
_terrain_quality_mesh_cache.erase(tile_key)
|
||||
_terrain_quality_mesh_cache_order.erase(tile_key)
|
||||
return state
|
||||
var mesh: Mesh = cached.get("mesh", null)
|
||||
if mesh == null:
|
||||
_terrain_quality_mesh_cache.erase(tile_key)
|
||||
_terrain_quality_mesh_cache_order.erase(tile_key)
|
||||
return state
|
||||
state["quality_terrain_mesh"] = mesh
|
||||
state["quality_terrain_source"] = source
|
||||
_touch_quality_terrain_mesh_cache(tile_key)
|
||||
return state
|
||||
|
||||
|
||||
func _store_quality_terrain_mesh(tile_key: String, mesh: Mesh, source: String) -> void:
|
||||
if tile_key.is_empty() or mesh == null:
|
||||
return
|
||||
if source == "control_splat_cache" or source == "splat_cache" or source == "splat":
|
||||
return
|
||||
_terrain_quality_mesh_cache[tile_key] = {
|
||||
"mesh": mesh,
|
||||
"source": source,
|
||||
}
|
||||
_touch_quality_terrain_mesh_cache(tile_key)
|
||||
_trim_quality_terrain_mesh_cache()
|
||||
|
||||
|
||||
func _touch_quality_terrain_mesh_cache(tile_key: String) -> void:
|
||||
_terrain_quality_mesh_cache_order.erase(tile_key)
|
||||
_terrain_quality_mesh_cache_order.append(tile_key)
|
||||
|
||||
|
||||
func _trim_quality_terrain_mesh_cache() -> void:
|
||||
var limit := maxi(0, terrain_quality_mesh_cache_limit)
|
||||
while _terrain_quality_mesh_cache_order.size() > limit:
|
||||
var oldest: String = _terrain_quality_mesh_cache_order.pop_front()
|
||||
_terrain_quality_mesh_cache.erase(oldest)
|
||||
_terrain_quality_mesh_cache.call(
|
||||
"store",
|
||||
tile_key,
|
||||
mesh,
|
||||
source,
|
||||
terrain_quality_mesh_cache_limit
|
||||
)
|
||||
|
||||
|
||||
func _clear_quality_terrain_mesh_cache() -> void:
|
||||
_terrain_quality_mesh_cache.clear()
|
||||
_terrain_quality_mesh_cache_order.clear()
|
||||
_terrain_quality_mesh_cache.call("clear")
|
||||
|
||||
|
||||
func _position_camera_over_world() -> void:
|
||||
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b4a7gtg46jrdn
|
||||
@@ -1,12 +1,14 @@
|
||||
extends SceneTree
|
||||
|
||||
const STREAMING_WORLD_LOADER := preload("res://src/scenes/streaming/streaming_world_loader.gd")
|
||||
const M2_UNIQUE_PLACEMENT_REGISTRY := preload(
|
||||
"res://src/render/m2/m2_unique_placement_registry.gd"
|
||||
)
|
||||
|
||||
var _failures := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var loader = STREAMING_WORLD_LOADER.new()
|
||||
var registry: RefCounted = M2_UNIQUE_PLACEMENT_REGISTRY.new()
|
||||
var placement := {
|
||||
"name_id": 0,
|
||||
"unique_id": 11785,
|
||||
@@ -15,30 +17,24 @@ func _initialize() -> void:
|
||||
"scale": 1.0,
|
||||
}
|
||||
|
||||
var first: Dictionary = loader.call("_reserve_tile_m2_placements", "30_49", [placement])
|
||||
var first: Dictionary = registry.call("reserve", "30_49", [placement])
|
||||
_expect((first.get("placements", []) as Array).size() == 1, "first tile should reserve unique M2 placement")
|
||||
_expect((first.get("unique_keys", []) as Array).has("uid:11785"), "first tile should own uid:11785")
|
||||
_expect((first.get("skipped_unique_keys", []) as Array).is_empty(), "first tile should not skip uid:11785")
|
||||
|
||||
var duplicate: Dictionary = loader.call("_reserve_tile_m2_placements", "31_49", [placement])
|
||||
var duplicate: Dictionary = registry.call("reserve", "31_49", [placement])
|
||||
_expect((duplicate.get("placements", []) as Array).is_empty(), "second tile should skip duplicate uid:11785")
|
||||
_expect((duplicate.get("skipped_unique_keys", []) as Array).has("uid:11785"), "second tile should remember skipped uid:11785")
|
||||
|
||||
loader.call("_release_tile_m2_unique_keys", {
|
||||
"key": "30_49",
|
||||
"m2_unique_keys": ["uid:11785"],
|
||||
"m2_skipped_unique_keys": [],
|
||||
}, false)
|
||||
var after_release: Dictionary = loader.call("_reserve_tile_m2_placements", "31_49", [placement])
|
||||
registry.call("release", "30_49", ["uid:11785"])
|
||||
var after_release: Dictionary = registry.call("reserve", "31_49", [placement])
|
||||
_expect((after_release.get("placements", []) as Array).size() == 1, "second tile should reserve uid:11785 after owner release")
|
||||
_expect((after_release.get("unique_keys", []) as Array).has("uid:11785"), "second tile should own uid:11785 after release")
|
||||
|
||||
var same_tile_loader = STREAMING_WORLD_LOADER.new()
|
||||
var same_tile_duplicate: Dictionary = same_tile_loader.call("_reserve_tile_m2_placements", "31_49", [placement, placement])
|
||||
var same_tile_registry: RefCounted = M2_UNIQUE_PLACEMENT_REGISTRY.new()
|
||||
var same_tile_duplicate: Dictionary = same_tile_registry.call("reserve", "31_49", [placement, placement])
|
||||
_expect((same_tile_duplicate.get("placements", []) as Array).size() == 1, "same tile should keep only one copy of duplicate uid:11785")
|
||||
|
||||
loader.free()
|
||||
same_tile_loader.free()
|
||||
if _failures > 0:
|
||||
push_error("M2 unique dedupe verification failed: %d issue(s)" % _failures)
|
||||
quit(1)
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free ownership, lifecycle, dependency and timing regression for M2 IDs.
|
||||
|
||||
const REGISTRY_SCRIPT := preload("res://src/render/m2/m2_unique_placement_registry.gd")
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_unkeyed_and_invalid_placements(failures)
|
||||
_verify_same_tile_and_cross_tile_deduplication(failures)
|
||||
_verify_owner_checked_release_and_retry(failures)
|
||||
_verify_clear_and_detached_diagnostics(failures)
|
||||
_verify_loader_boundary(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_UNIQUE_PLACEMENT_REGISTRY: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_UNIQUE_PLACEMENT_REGISTRY PASS cases=10 iterations=100 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_unkeyed_and_invalid_placements(failures: Array[String]) -> void:
|
||||
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||
var result: Dictionary = registry.call("reserve", "tile-a", [
|
||||
{"unique_id": -1},
|
||||
{"unique_id": 0},
|
||||
{"name_id": 7},
|
||||
"invalid",
|
||||
])
|
||||
_expect_equal((result["placements"] as Array).size(), 3, "unkeyed placements pass", failures)
|
||||
_expect_true((result["unique_keys"] as Array).is_empty(), "unkeyed placements not owned", failures)
|
||||
_expect_equal(int(registry.call("active_count")), 0, "unkeyed active count", failures)
|
||||
|
||||
|
||||
func _verify_same_tile_and_cross_tile_deduplication(failures: Array[String]) -> void:
|
||||
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||
var placement := {"unique_id": 11785, "name_id": 0}
|
||||
var first: Dictionary = registry.call("reserve", "30_49", [placement, placement])
|
||||
_expect_equal((first["placements"] as Array).size(), 1, "same call duplicate filtered", failures)
|
||||
_expect_string_array((first["unique_keys"] as Array), ["uid:11785"], "first owner key", failures)
|
||||
var same_owner_retry: Dictionary = registry.call("reserve", "30_49", [placement])
|
||||
_expect_equal((same_owner_retry["placements"] as Array).size(), 1, "same owner later call passes", failures)
|
||||
var competing: Dictionary = registry.call("reserve", "31_49", [placement, placement])
|
||||
_expect_true((competing["placements"] as Array).is_empty(), "competing tile filtered", failures)
|
||||
_expect_string_array(
|
||||
competing["skipped_unique_keys"] as Array,
|
||||
["uid:11785"],
|
||||
"competing skip recorded once",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_owner_checked_release_and_retry(failures: Array[String]) -> void:
|
||||
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||
var placement := {"unique_id": 44}
|
||||
registry.call("reserve", "owner", [placement])
|
||||
_expect_true(
|
||||
(registry.call("release", "other", ["uid:44"]) as Array).is_empty(),
|
||||
"non-owner release ignored",
|
||||
failures
|
||||
)
|
||||
_expect_equal(int(registry.call("active_count")), 1, "non-owner keeps reservation", failures)
|
||||
var released: Array = registry.call("release", "owner", ["", "uid:44", "uid:44"])
|
||||
_expect_string_array(released, ["uid:44"], "owner release order and dedupe", failures)
|
||||
var retried: Dictionary = registry.call("reserve", "other", [placement])
|
||||
_expect_equal((retried["placements"] as Array).size(), 1, "waiting tile reserves after release", failures)
|
||||
|
||||
|
||||
func _verify_clear_and_detached_diagnostics(failures: Array[String]) -> void:
|
||||
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||
registry.call("reserve", "tile-b", [{"unique_id": 20}])
|
||||
registry.call("reserve", "tile-a", [{"unique_id": 10}])
|
||||
var snapshot: Dictionary = registry.call("diagnostic_snapshot")
|
||||
var owners: Array = snapshot["owners"]
|
||||
_expect_equal(int(snapshot["active_count"]), 2, "diagnostic count", failures)
|
||||
_expect_string_equal(String(owners[0]["unique_key"]), "uid:10", "diagnostic sorted", failures)
|
||||
owners[0]["tile_key"] = "mutated"
|
||||
var fresh_snapshot: Dictionary = registry.call("diagnostic_snapshot")
|
||||
_expect_string_equal(
|
||||
String((fresh_snapshot["owners"] as Array)[0]["tile_key"]),
|
||||
"tile-a",
|
||||
"diagnostics detached",
|
||||
failures
|
||||
)
|
||||
registry.call("clear")
|
||||
registry.call("clear")
|
||||
_expect_equal(int(registry.call("active_count")), 0, "clear idempotent", failures)
|
||||
|
||||
|
||||
func _verify_loader_boundary(failures: Array[String]) -> void:
|
||||
var source := _read_text(LOADER_PATH, failures)
|
||||
_expect_true(source.contains("M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT.new()"), "loader composes registry", failures)
|
||||
_expect_true(source.contains("_m2_unique_placement_registry.reserve("), "loader delegates reserve", failures)
|
||||
_expect_true(source.contains("_m2_unique_placement_registry.release("), "loader delegates release", failures)
|
||||
_expect_true(source.contains("_m2_unique_placement_registry.clear()"), "loader delegates clear", failures)
|
||||
_expect_true(not source.contains("var _m2_unique_registry"), "loader omits registry dictionary", failures)
|
||||
_expect_true(not source.contains("func _m2_unique_key("), "loader omits key formula", failures)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var placements: Array = []
|
||||
for placement_index in range(256):
|
||||
placements.append({"unique_id": placement_index + 1})
|
||||
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for _iteration in range(100):
|
||||
var reservation: Dictionary = registry.call("reserve", "owner", placements)
|
||||
registry.call("release", "owner", reservation["unique_keys"])
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "100 reserve/release cycles remain bounded", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
return ""
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
func _expect_equal(actual_value: int, expected_value: int, label: String, failures: Array[String]) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_string_equal(
|
||||
actual_value: String,
|
||||
expected_value: String,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_string_array(
|
||||
actual_values: Array,
|
||||
expected_values: Array,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual_values != expected_values:
|
||||
failures.append("%s expected %s, got %s" % [label, expected_values, actual_values])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://inw4fft20be0
|
||||
@@ -0,0 +1,169 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free semantic, dependency and timing regression for chunk queue planning.
|
||||
|
||||
const PLANNER_SCRIPT := preload(
|
||||
"res://src/render/terrain/terrain_chunk_geometry_queue_planner.gd"
|
||||
)
|
||||
const POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_create_remove_and_order(failures)
|
||||
_verify_tile_lod_removes_all(failures)
|
||||
_verify_fallback_origin_and_vertical_invariance(failures)
|
||||
_verify_invalid_focus(failures)
|
||||
_verify_loader_boundary(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER PASS cases=8 iterations=100 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_create_remove_and_order(failures: Array[String]) -> void:
|
||||
var planner: RefCounted = PLANNER_SCRIPT.new()
|
||||
var tile_states := {
|
||||
"A": _create_tile_state(
|
||||
{0: {"lod": 0}, 1: {"lod": 1}, 3: {"lod": 0}},
|
||||
{0: 0, 1: 2, 2: 1},
|
||||
[
|
||||
{"origin": Vector3(100.0, 0.0, 0.0)},
|
||||
{"origin": Vector3(30.0, 8000.0, 0.0)},
|
||||
{"origin": Vector3(10.0, -8000.0, 0.0)},
|
||||
]
|
||||
),
|
||||
"B": _create_tile_state({5: {"lod": 0}}, {}, []),
|
||||
}
|
||||
var plan: Dictionary = planner.call("plan", tile_states, POSITION_SCRIPT.new(0.0, 50.0, 0.0))
|
||||
var creates: Array = plan["create_requests"]
|
||||
var removes: Array = plan["remove_requests"]
|
||||
_expect_equal(creates.size(), 2, "missing and mismatched chunks create", failures)
|
||||
_expect_equal(int(creates[0]["chunk"]), 2, "nearest missing chunk first", failures)
|
||||
_expect_equal(int(creates[1]["chunk"]), 1, "farther LOD mismatch second", failures)
|
||||
_expect_equal(int(creates[1]["lod"]), 2, "mismatch uses desired LOD", failures)
|
||||
_expect_equal(removes.size(), 2, "undesired current chunks remove", failures)
|
||||
_expect_string_equal(String(removes[0]["tile"]), "A", "stable tile removal order", failures)
|
||||
_expect_equal(int(removes[0]["chunk"]), 3, "first undesired chunk", failures)
|
||||
_expect_equal(int(removes[1]["chunk"]), 5, "second tile undesired chunk", failures)
|
||||
|
||||
|
||||
func _verify_tile_lod_removes_all(failures: Array[String]) -> void:
|
||||
var tile_state := _create_tile_state({7: {"lod": 0}, 8: {"lod": 1}}, {7: 0}, [])
|
||||
tile_state["desired_tile_lod"] = 3
|
||||
var plan: Dictionary = PLANNER_SCRIPT.new().call(
|
||||
"plan",
|
||||
{"tile": tile_state},
|
||||
POSITION_SCRIPT.new(0.0, 0.0, 0.0)
|
||||
)
|
||||
_expect_true((plan["create_requests"] as Array).is_empty(), "tile LOD suppresses chunk creates", failures)
|
||||
_expect_equal((plan["remove_requests"] as Array).size(), 2, "tile LOD removes every chunk", failures)
|
||||
|
||||
|
||||
func _verify_fallback_origin_and_vertical_invariance(failures: Array[String]) -> void:
|
||||
var tile_state := _create_tile_state({}, {9: 1}, [])
|
||||
tile_state["origin"] = Vector3(4.0, 99999.0, 3.0)
|
||||
var plan: Dictionary = PLANNER_SCRIPT.new().call(
|
||||
"plan",
|
||||
{"fallback": tile_state},
|
||||
POSITION_SCRIPT.new(0.0, -99999.0, 0.0)
|
||||
)
|
||||
var request: Dictionary = (plan["create_requests"] as Array)[0]
|
||||
_expect_near(float(request["dist_sq"]), 25.0, "fallback origin uses horizontal distance", failures)
|
||||
|
||||
|
||||
func _verify_invalid_focus(failures: Array[String]) -> void:
|
||||
var plan: Dictionary = PLANNER_SCRIPT.new().call("plan", {}, null)
|
||||
_expect_true((plan["create_requests"] as Array).is_empty(), "invalid focus create empty", failures)
|
||||
_expect_true((plan["remove_requests"] as Array).is_empty(), "invalid focus remove empty", failures)
|
||||
|
||||
|
||||
func _verify_loader_boundary(failures: Array[String]) -> void:
|
||||
var source := _read_text(LOADER_PATH, failures)
|
||||
_expect_true(source.contains("TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER_SCRIPT.new()"), "loader composes planner", failures)
|
||||
_expect_true(source.contains("_terrain_chunk_geometry_operation_planner.plan("), "loader delegates plan", failures)
|
||||
_expect_true(not source.contains("func _chunk_origin_for("), "loader omits chunk-origin planning", failures)
|
||||
_expect_true(source.contains("_remove_chunk(_chunk_remove_queue.pop_back())"), "loader retains removal-first drain", failures)
|
||||
_expect_true(source.contains("_create_chunk(_chunk_create_queue.pop_front())"), "loader retains nearest-first drain", failures)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var tile_states: Dictionary = {}
|
||||
for tile_index in range(64):
|
||||
var current_chunks: Dictionary = {}
|
||||
var desired_lods: Dictionary = {}
|
||||
var parsed_chunks: Array = []
|
||||
for chunk_index in range(16):
|
||||
current_chunks[chunk_index] = {"lod": chunk_index % 3}
|
||||
desired_lods[chunk_index] = (chunk_index + 1) % 3
|
||||
parsed_chunks.append({
|
||||
"origin": Vector3(float(tile_index * 16 + chunk_index), 0.0, float(tile_index))
|
||||
})
|
||||
tile_states["tile-%d" % tile_index] = _create_tile_state(
|
||||
current_chunks,
|
||||
desired_lods,
|
||||
parsed_chunks
|
||||
)
|
||||
var planner: RefCounted = PLANNER_SCRIPT.new()
|
||||
var focus: RefCounted = POSITION_SCRIPT.new(0.0, 0.0, 0.0)
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for _iteration in range(100):
|
||||
planner.call("plan", tile_states, focus)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "100 multi-tile plans remain bounded", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _create_tile_state(
|
||||
current_chunks: Dictionary,
|
||||
desired_lods: Dictionary,
|
||||
parsed_chunks: Array
|
||||
) -> Dictionary:
|
||||
return {
|
||||
"chunks": current_chunks,
|
||||
"desired_lods": desired_lods,
|
||||
"desired_tile_lod": -1,
|
||||
"data": {"chunks": parsed_chunks},
|
||||
"origin": Vector3.ZERO,
|
||||
}
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
return ""
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
func _expect_equal(actual_value: int, expected_value: int, label: String, failures: Array[String]) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_near(actual_value: float, expected_value: float, label: String, failures: Array[String]) -> void:
|
||||
if not is_equal_approx(actual_value, expected_value):
|
||||
failures.append("%s expected %.3f, got %.3f" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_string_equal(
|
||||
actual_value: String,
|
||||
expected_value: String,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dcimxvm2atvst
|
||||
@@ -0,0 +1,96 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free formula, dependency and bounded timing regression for chunk LOD.
|
||||
|
||||
const PLANNER_SCRIPT := preload("res://src/render/terrain/terrain_chunk_lod_planner.gd")
|
||||
const POLICY_SCRIPT := preload("res://src/render/terrain/terrain_chunk_lod_policy.gd")
|
||||
const POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_formula(failures)
|
||||
_verify_disabled_and_custom_policy(failures)
|
||||
_verify_loader_boundary(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("TERRAIN_CHUNK_LOD_PLANNER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print("TERRAIN_CHUNK_LOD_PLANNER PASS cases=7 iterations=250 elapsed_ms=%.3f" % elapsed_milliseconds)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_formula(failures: Array[String]) -> void:
|
||||
var planner: RefCounted = PLANNER_SCRIPT.new()
|
||||
var policy: RefCounted = POLICY_SCRIPT.new(true, 1, 2, 3, 10.0)
|
||||
var focus: RefCounted = POSITION_SCRIPT.new(0.0, 9999.0, 0.0)
|
||||
var chunks: Array = [
|
||||
{"origin": Vector3(-5.0, 2000.0, -5.0)},
|
||||
{"origin": Vector3(5.0, -3000.0, -5.0)},
|
||||
{"origin": Vector3(5.001, 0.0, -5.0)},
|
||||
{"origin": Vector3(15.0, 0.0, -5.0)},
|
||||
{"origin": Vector3(20.0, 0.0, -5.0)},
|
||||
{"origin": Vector3(25.0, 0.0, -5.0)},
|
||||
{"origin": Vector3(25.001, 0.0, -5.0)},
|
||||
{},
|
||||
]
|
||||
var desired: Dictionary = planner.call("plan", chunks, focus, policy)
|
||||
_expect_equal(int(desired.get(0, -1)), 0, "center ignores vertical distance", failures)
|
||||
_expect_equal(int(desired.get(1, -1)), 0, "lod0 inclusive boundary", failures)
|
||||
_expect_equal(int(desired.get(2, -1)), 1, "just outside lod0", failures)
|
||||
_expect_equal(int(desired.get(3, -1)), 1, "lod1 inclusive boundary", failures)
|
||||
_expect_equal(int(desired.get(4, -1)), 2, "between lod1 and lod2", failures)
|
||||
_expect_equal(int(desired.get(5, -1)), 2, "lod2 inclusive boundary", failures)
|
||||
_expect_true(not desired.has(6) and not desired.has(7), "outside and sparse chunks absent", failures)
|
||||
|
||||
|
||||
func _verify_disabled_and_custom_policy(failures: Array[String]) -> void:
|
||||
var planner: RefCounted = PLANNER_SCRIPT.new()
|
||||
var focus: RefCounted = POSITION_SCRIPT.new(0.0, 0.0, 0.0)
|
||||
var chunks: Array = [{"origin": Vector3(-5.0, 0.0, -5.0)}]
|
||||
_expect_true((planner.call("plan", chunks, focus, POLICY_SCRIPT.new(false, 1, 2, 3, 10.0)) as Dictionary).is_empty(), "disabled policy empty", failures)
|
||||
var negative_policy: RefCounted = POLICY_SCRIPT.new(true, -1, -2, -3, 10.0)
|
||||
_expect_equal(int((planner.call("plan", chunks, focus, negative_policy) as Dictionary).get(0, -1)), 0, "negative radius keeps squared semantics", failures)
|
||||
|
||||
|
||||
func _verify_loader_boundary(failures: Array[String]) -> void:
|
||||
var source := _read_text(LOADER_PATH, failures)
|
||||
_expect_true(source.contains("TERRAIN_CHUNK_LOD_PLANNER_SCRIPT.new()"), "loader composes planner", failures)
|
||||
_expect_true(source.contains("_terrain_chunk_lod_planner.plan(chunks, typed_focus_position, terrain_chunk_lod_policy)"), "loader delegates plan", failures)
|
||||
_expect_true(not source.contains("dist_sq <= lod0_sq"), "loader omits chunk LOD formula", failures)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var planner: RefCounted = PLANNER_SCRIPT.new()
|
||||
var policy: RefCounted = POLICY_SCRIPT.new(true, 10, 20, 40, 33.333333)
|
||||
var focus: RefCounted = POSITION_SCRIPT.new(1000.0, 0.0, 1000.0)
|
||||
var chunks: Array = []
|
||||
for chunk_index in range(256):
|
||||
chunks.append({"origin": Vector3(float(chunk_index % 16) * 33.333333, 0.0, float(chunk_index / 16) * 33.333333)})
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for _iteration in range(250):
|
||||
planner.call("plan", chunks, focus, policy)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "250 plans remain bounded", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
return ""
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
func _expect_equal(actual_value: int, expected_value: int, label: String, failures: Array[String]) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://2oljbpoxndx3
|
||||
@@ -0,0 +1,67 @@
|
||||
extends SceneTree
|
||||
|
||||
## Scene-free contract, admission and LRU regression for terrain revisit meshes.
|
||||
|
||||
const CACHE_SCRIPT := preload("res://src/render/terrain/terrain_quality_mesh_cache.gd")
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_cache_contract(failures)
|
||||
_verify_loader_boundary(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("TERRAIN_QUALITY_MESH_CACHE: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print("TERRAIN_QUALITY_MESH_CACHE PASS contract_cases=15 loader_calls=3")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_cache_contract(failures: Array[String]) -> void:
|
||||
var cache: RefCounted = CACHE_SCRIPT.new()
|
||||
var mesh_a := ArrayMesh.new()
|
||||
var mesh_b := ArrayMesh.new()
|
||||
var mesh_c := ArrayMesh.new()
|
||||
_expect_true(not bool(cache.call("store", "", mesh_a, "baked_full", 2)), "empty key rejected", failures)
|
||||
_expect_true(not bool(cache.call("store", "a", null, "baked_full", 2)), "null mesh rejected", failures)
|
||||
for excluded_source in ["control_splat_cache", "splat_cache", "splat"]:
|
||||
_expect_true(not bool(cache.call("store", "a", mesh_a, excluded_source, 2)), "%s rejected" % excluded_source, failures)
|
||||
_expect_true(bool(cache.call("store", "a", mesh_a, "baked_full", 2)), "first store", failures)
|
||||
_expect_true(bool(cache.call("store", "b", mesh_b, "baked_full", 2)), "second store", failures)
|
||||
var restored_a: Dictionary = cache.call("restore", "a")
|
||||
_expect_true(restored_a.get("mesh") == mesh_a, "restore retains mesh reference", failures)
|
||||
_expect_true(String(restored_a.get("source", "")) == "baked_full", "restore retains source", failures)
|
||||
_expect_true(bool(cache.call("store", "c", mesh_c, "baked_full", 2)), "third store", failures)
|
||||
_expect_true((cache.call("restore", "b") as Dictionary).is_empty(), "oldest evicted after touch", failures)
|
||||
_expect_true(not (cache.call("restore", "a") as Dictionary).is_empty(), "touched entry retained", failures)
|
||||
var diagnostics: Dictionary = cache.call("diagnostic_snapshot")
|
||||
_expect_true(int(diagnostics.get("entry_count", 0)) == 2, "bounded count", failures)
|
||||
(diagnostics.get("entries_oldest_to_newest") as Array).clear()
|
||||
_expect_true(int((cache.call("diagnostic_snapshot") as Dictionary).get("entry_count", 0)) == 2, "diagnostics detached", failures)
|
||||
_expect_true(not bool(cache.call("store", "zero", ArrayMesh.new(), "baked_full", -1)), "negative capacity clamps to zero", failures)
|
||||
cache.call("clear")
|
||||
_expect_true(int((cache.call("diagnostic_snapshot") as Dictionary).get("entry_count", -1)) == 0, "clear releases entries", failures)
|
||||
|
||||
|
||||
func _verify_loader_boundary(failures: Array[String]) -> void:
|
||||
var source := _read_text(LOADER_PATH, failures)
|
||||
_expect_true(source.contains("TERRAIN_QUALITY_MESH_CACHE_SCRIPT.new()"), "loader composes cache", failures)
|
||||
_expect_true(source.contains('_terrain_quality_mesh_cache.call("restore", tile_key)'), "loader delegates restore", failures)
|
||||
_expect_true(source.contains('_terrain_quality_mesh_cache.call("clear")'), "loader delegates clear", failures)
|
||||
for removed_symbol in ["_terrain_quality_mesh_cache_order", "func _touch_quality_terrain_mesh_cache", "func _trim_quality_terrain_mesh_cache"]:
|
||||
_expect_true(not source.contains(removed_symbol), "loader omits %s" % removed_symbol, failures)
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
return ""
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://khd1ths2b4ya
|
||||
@@ -0,0 +1,157 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free contract, lifecycle and ownership regression for world entity visuals.
|
||||
|
||||
const SNAPSHOT_SCRIPT := preload("res://src/render/entities/entity_presentation_snapshot.gd")
|
||||
const PRESENTER_SCRIPT := preload("res://src/render/entities/world_entity_presenter.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")
|
||||
|
||||
const PRIMARY_VISUAL_PATH := "res://src/tests/scenes/character_presentation_model_fixture.tscn"
|
||||
const REPLACEMENT_VISUAL_PATH := "res://src/tests/scenes/world_entity_visual_replacement_fixture.tscn"
|
||||
const SNAPSHOT_SOURCE_PATH := "res://src/render/entities/entity_presentation_snapshot.gd"
|
||||
const PRESENTER_SOURCE_PATH := "res://src/render/entities/world_entity_presenter.gd"
|
||||
const FACADE_SOURCE_PATH := "res://src/render/world_render_facade.gd"
|
||||
const RUNTIME_SCENE_PATHS: Array[String] = [
|
||||
"res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
|
||||
"res://src/scenes/streaming/kalimdor_streaming.tscn",
|
||||
]
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run_verification.call_deferred()
|
||||
|
||||
|
||||
func _run_verification() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_snapshot_contract(failures)
|
||||
await _verify_presenter_lifecycle(failures)
|
||||
_verify_source_boundaries(failures)
|
||||
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("WORLD_ENTITY_PRESENTATION: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print("WORLD_ENTITY_PRESENTATION PASS snapshot_cases=8 lifecycle_cases=14 runtime_scenes=2")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_snapshot_contract(failures: Array[String]) -> void:
|
||||
var entity_id: RefCounted = ENTITY_ID_SCRIPT.new(7)
|
||||
var position: RefCounted = GODOT_WORLD_POSITION_SCRIPT.new(10.0, 20.0, 30.0)
|
||||
var valid_snapshot := SNAPSHOT_SCRIPT.create(entity_id, position, PRIMARY_VISUAL_PATH, 0.5, 2.0, false)
|
||||
_expect_true(bool(valid_snapshot.get("is_valid")), "valid snapshot", failures)
|
||||
_expect_true(int(SNAPSHOT_SCRIPT.CONTRACT_VERSION) == 1, "contract version", failures)
|
||||
_expect_failure(SNAPSHOT_SCRIPT.create(ENTITY_ID_SCRIPT.new(0), position, PRIMARY_VISUAL_PATH), &"entity_presentation_invalid_entity_id", "invalid entity", failures)
|
||||
_expect_failure(SNAPSHOT_SCRIPT.create(entity_id, RefCounted.new(), PRIMARY_VISUAL_PATH), &"entity_presentation_invalid_world_position", "invalid position type", failures)
|
||||
_expect_failure(SNAPSHOT_SCRIPT.create(entity_id, GODOT_WORLD_POSITION_SCRIPT.new(INF, 0.0, 0.0), PRIMARY_VISUAL_PATH), &"entity_presentation_position_not_finite", "non-finite position", failures)
|
||||
_expect_failure(SNAPSHOT_SCRIPT.create(entity_id, position, ""), &"entity_presentation_visual_path_empty", "empty visual path", failures)
|
||||
_expect_failure(SNAPSHOT_SCRIPT.create(entity_id, position, PRIMARY_VISUAL_PATH, NAN), &"entity_presentation_yaw_not_finite", "non-finite yaw", failures)
|
||||
_expect_failure(SNAPSHOT_SCRIPT.create(entity_id, position, PRIMARY_VISUAL_PATH, 0.0, 0.0), &"entity_presentation_scale_invalid", "zero scale", failures)
|
||||
|
||||
|
||||
func _verify_presenter_lifecycle(failures: Array[String]) -> void:
|
||||
var presenter: Node3D = PRESENTER_SCRIPT.new()
|
||||
presenter.name = "WorldEntityPresenter"
|
||||
root.add_child(presenter)
|
||||
var entity_id: RefCounted = ENTITY_ID_SCRIPT.new(7)
|
||||
var initial_snapshot := SNAPSHOT_SCRIPT.create(
|
||||
entity_id,
|
||||
GODOT_WORLD_POSITION_SCRIPT.new(10.0, 20.0, 30.0),
|
||||
PRIMARY_VISUAL_PATH,
|
||||
0.5,
|
||||
2.0,
|
||||
false
|
||||
)
|
||||
_expect_true(bool(presenter.call("present_entity", initial_snapshot)), "initial present", failures)
|
||||
var visual_root := presenter.get_node_or_null("EntityVisual_00000000000000000007") as Node3D
|
||||
_expect_true(visual_root != null, "owned visual attached", failures)
|
||||
if visual_root != null:
|
||||
_expect_vector_near(visual_root.global_position, Vector3(10.0, 20.0, 30.0), "initial position", failures)
|
||||
_expect_near(visual_root.scale.x, 2.0, "initial scale", failures)
|
||||
_expect_true(not visual_root.visible, "initial visibility", failures)
|
||||
var original_instance_id := visual_root.get_instance_id() if visual_root != null else 0
|
||||
|
||||
var updated_snapshot := SNAPSHOT_SCRIPT.create(
|
||||
entity_id,
|
||||
GODOT_WORLD_POSITION_SCRIPT.new(40.0, 50.0, 60.0),
|
||||
PRIMARY_VISUAL_PATH,
|
||||
1.0,
|
||||
1.5,
|
||||
true
|
||||
)
|
||||
_expect_true(bool(presenter.call("present_entity", updated_snapshot)), "idempotent update", failures)
|
||||
visual_root = presenter.get_node_or_null("EntityVisual_00000000000000000007") as Node3D
|
||||
_expect_true(visual_root != null and visual_root.get_instance_id() == original_instance_id, "same visual root retained", failures)
|
||||
if visual_root != null:
|
||||
_expect_vector_near(visual_root.global_position, Vector3(40.0, 50.0, 60.0), "updated position", failures)
|
||||
_expect_true(visual_root.visible, "updated visibility", failures)
|
||||
|
||||
var failed_replacement := SNAPSHOT_SCRIPT.create(entity_id, GODOT_WORLD_POSITION_SCRIPT.new(1.0, 2.0, 3.0), "res://missing/entity_visual.tscn")
|
||||
_expect_true(not bool(presenter.call("present_entity", failed_replacement)), "missing replacement rejected", failures)
|
||||
visual_root = presenter.get_node_or_null("EntityVisual_00000000000000000007") as Node3D
|
||||
_expect_true(visual_root != null and visual_root.get_instance_id() == original_instance_id, "failed replacement retains visual", failures)
|
||||
|
||||
var replacement_snapshot := SNAPSHOT_SCRIPT.create(entity_id, GODOT_WORLD_POSITION_SCRIPT.new(70.0, 80.0, 90.0), REPLACEMENT_VISUAL_PATH)
|
||||
_expect_true(bool(presenter.call("present_entity", replacement_snapshot)), "valid replacement", failures)
|
||||
visual_root = presenter.get_node_or_null("EntityVisual_00000000000000000007") as Node3D
|
||||
_expect_true(visual_root != null and visual_root.get_instance_id() != original_instance_id, "replacement swaps root", failures)
|
||||
|
||||
var diagnostic_snapshot: Dictionary = presenter.call("entity_presentation_snapshot")
|
||||
_expect_true(int(diagnostic_snapshot.get("contract_version", 0)) == 1, "diagnostic version", failures)
|
||||
_expect_true(int(diagnostic_snapshot.get("entity_count", 0)) == 1, "diagnostic entity count", failures)
|
||||
(diagnostic_snapshot.get("entities") as Array).clear()
|
||||
_expect_true(int((presenter.call("entity_presentation_snapshot") as Dictionary).get("entity_count", 0)) == 1, "diagnostic detached", failures)
|
||||
|
||||
_expect_true(bool(presenter.call("remove_entity", entity_id)), "known entity removed", failures)
|
||||
_expect_true(not bool(presenter.call("remove_entity", entity_id)), "duplicate removal rejected", failures)
|
||||
_expect_true(presenter.get_child_count() == 0, "owned visual detached", failures)
|
||||
presenter.free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
var snapshot_source := _read_text(SNAPSHOT_SOURCE_PATH, failures)
|
||||
for forbidden_snapshot_text in ["extends Node", "Node3D", "PackedScene", "Dictionary", "Vector3"]:
|
||||
_expect_true(not snapshot_source.contains(forbidden_snapshot_text), "snapshot omits %s" % forbidden_snapshot_text, failures)
|
||||
var presenter_source := _read_text(PRESENTER_SOURCE_PATH, failures)
|
||||
for forbidden_presenter_text in ["MoveIntent", "TerrainQuery", "WowGuid", "network", "server_data"]:
|
||||
_expect_true(not presenter_source.contains(forbidden_presenter_text), "presenter omits %s" % forbidden_presenter_text, failures)
|
||||
for required_presenter_text in ["ResourceLoader.exists", "remove_child(visual_root)", "visual_root.queue_free()"]:
|
||||
_expect_true(presenter_source.contains(required_presenter_text), "presenter contains %s" % required_presenter_text, failures)
|
||||
var facade_source := _read_text(FACADE_SOURCE_PATH, failures)
|
||||
for required_facade_method in ["func present_entity(", "func remove_entity(", "func entity_presentation_snapshot()"]:
|
||||
_expect_true(facade_source.contains(required_facade_method), "facade contains %s" % required_facade_method, failures)
|
||||
for scene_path in RUNTIME_SCENE_PATHS:
|
||||
var scene_source := _read_text(scene_path, failures)
|
||||
_expect_true(scene_source.contains("world_entity_presenter.gd"), "%s loads presenter" % scene_path, failures)
|
||||
_expect_true(scene_source.contains('world_entity_presenter_path = NodePath("../WorldEntityPresenter")'), "%s facade path" % scene_path, failures)
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
return ""
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
func _expect_failure(snapshot: RefCounted, expected_code: StringName, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(not bool(snapshot.get("is_valid")), "%s invalid" % label, failures)
|
||||
_expect_true(StringName(snapshot.get("failure_code")) == expected_code, "%s failure code" % label, failures)
|
||||
|
||||
|
||||
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_near(actual_value: float, expected_value: float, label: String, failures: Array[String]) -> void:
|
||||
if not is_equal_approx(actual_value, expected_value):
|
||||
failures.append("%s expected %.3f, got %.3f" % [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://dkkjbc8pcglcp
|
||||
@@ -8,6 +8,8 @@ const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_fo
|
||||
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 WOW_SKY_CONTROLLER_SCRIPT := preload("res://src/scenes/sky/wow_sky_controller.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")
|
||||
|
||||
const RUNTIME_SCENE_PATHS: Array[String] = [
|
||||
"res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
|
||||
@@ -57,6 +59,23 @@ class WorldEnvironmentControllerDouble extends Node:
|
||||
return true
|
||||
|
||||
|
||||
class WorldEntityPresenterDouble extends Node:
|
||||
var presented_snapshot: RefCounted
|
||||
var removed_entity_id: RefCounted
|
||||
var diagnostics := {"entity_count": 1, "entities": [{"entity_key": "entity:7"}]}
|
||||
|
||||
func present_entity(entity_presentation_snapshot: RefCounted) -> bool:
|
||||
presented_snapshot = entity_presentation_snapshot
|
||||
return true
|
||||
|
||||
func remove_entity(entity_id: RefCounted) -> bool:
|
||||
removed_entity_id = entity_id
|
||||
return true
|
||||
|
||||
func entity_presentation_snapshot() -> Dictionary:
|
||||
return diagnostics
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_rendered_ground_sample_contract(failures)
|
||||
@@ -70,7 +89,7 @@ func _initialize() -> void:
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("WORLD_RENDER_FACADE PASS delegation=6 ground_contract=2 environment_contract=7 runtime_scenes=2 tools=3")
|
||||
print("WORLD_RENDER_FACADE PASS delegation=9 ground_contract=2 environment_contract=7 entity_contract=6 runtime_scenes=2 tools=3")
|
||||
quit(0)
|
||||
|
||||
|
||||
@@ -116,14 +135,18 @@ func _verify_delegation_and_snapshot_isolation(failures: Array[String]) -> void:
|
||||
source.position = Vector3(10.0, 20.0, 30.0)
|
||||
var environment_controller := WorldEnvironmentControllerDouble.new()
|
||||
environment_controller.name = "WorldEnvironmentControllerDouble"
|
||||
var entity_presenter := WorldEntityPresenterDouble.new()
|
||||
entity_presenter.name = "WorldEntityPresenterDouble"
|
||||
var facade = WORLD_RENDER_FACADE_SCRIPT.new()
|
||||
facade.name = "WorldRenderFacade"
|
||||
facade.streaming_world_loader_path = NodePath("../StreamingWorldLoaderDouble")
|
||||
facade.streaming_focus_source_path = NodePath("../FocusSource")
|
||||
facade.world_environment_controller_path = NodePath("../WorldEnvironmentControllerDouble")
|
||||
facade.world_entity_presenter_path = NodePath("../WorldEntityPresenterDouble")
|
||||
test_root.add_child(loader)
|
||||
test_root.add_child(source)
|
||||
test_root.add_child(environment_controller)
|
||||
test_root.add_child(entity_presenter)
|
||||
test_root.add_child(facade)
|
||||
get_root().add_child(test_root)
|
||||
await process_frame
|
||||
@@ -181,6 +204,21 @@ func _verify_delegation_and_snapshot_isolation(failures: Array[String]) -> void:
|
||||
"invalid environment snapshot rejected",
|
||||
failures
|
||||
)
|
||||
|
||||
var entity_id: RefCounted = ENTITY_ID_SCRIPT.new(7)
|
||||
var entity_snapshot: RefCounted = ENTITY_PRESENTATION_SNAPSHOT_SCRIPT.create(
|
||||
entity_id,
|
||||
GODOT_WORLD_POSITION_SCRIPT.new(1.0, 2.0, 3.0),
|
||||
"res://src/tests/scenes/character_presentation_model_fixture.tscn"
|
||||
)
|
||||
_expect_true(facade.present_entity(entity_snapshot), "entity presentation delegates", failures)
|
||||
_expect_true(entity_presenter.presented_snapshot == entity_snapshot, "entity snapshot reference delegates", failures)
|
||||
_expect_true(facade.remove_entity(entity_id), "entity removal delegates", failures)
|
||||
_expect_true(entity_presenter.removed_entity_id == entity_id, "entity identity reference delegates", failures)
|
||||
var entity_diagnostics: Dictionary = facade.entity_presentation_snapshot()
|
||||
(entity_diagnostics.get("entities") as Array).clear()
|
||||
_expect_true(entity_presenter.diagnostics.entities.size() == 1, "entity diagnostics detached", failures)
|
||||
_expect_true(not facade.present_entity(ENTITY_PRESENTATION_SNAPSHOT_SCRIPT.create(ENTITY_ID_SCRIPT.new(0), GODOT_WORLD_POSITION_SCRIPT.new(0.0, 0.0, 0.0), "res://fixture.tscn")), "invalid entity snapshot rejected", failures)
|
||||
test_root.queue_free()
|
||||
|
||||
|
||||
@@ -191,6 +229,8 @@ func _verify_repository_wiring(failures: Array[String]) -> void:
|
||||
_expect_true(scene_source.contains('[node name="WorldRenderFacade" type="Node" parent="."]'), "%s owns facade node" % scene_path, failures)
|
||||
_expect_true(scene_source.contains('streaming_focus_source_path = NodePath("../ThirdPersonPlayer")'), "%s facade uses player focus" % scene_path, failures)
|
||||
_expect_true(scene_source.contains('world_environment_controller_path = NodePath("../WowSkyController")'), "%s facade uses sky controller" % scene_path, failures)
|
||||
_expect_true(scene_source.contains('world_entity_presenter_path = NodePath("../WorldEntityPresenter")'), "%s facade uses entity presenter" % scene_path, failures)
|
||||
_expect_true(scene_source.contains('[node name="WorldEntityPresenter" type="Node3D" parent="."]'), "%s owns entity presenter service" % scene_path, failures)
|
||||
|
||||
for tool_path in TOOL_PATHS:
|
||||
var tool_source := _read_text(tool_path, failures)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## Steps
|
||||
|
||||
- [ ] Ввести `WorldRenderFacade` для focus, environment, entity visuals, ground query и metrics.
|
||||
- [x] Ввести `WorldRenderFacade` для focus, environment, entity visuals, ground query и metrics.
|
||||
- [x] Запретить gameplay/editor доступ к внутренним очередям streamer.
|
||||
- [x] Извлечь pure `StreamingTargetPlanner`.
|
||||
- [x] Извлечь `RenderBudgetScheduler` с bounded queues/cancellation.
|
||||
@@ -27,9 +27,14 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
## Evidence
|
||||
|
||||
- Date: 2026-07-16
|
||||
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`;
|
||||
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`, `f88bf97`;
|
||||
packages `M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-001`,
|
||||
`M03-RND-FACADE-GROUND-QUERY-001`, `M03-RND-FACADE-ENVIRONMENT-001`
|
||||
`M03-RND-FACADE-GROUND-QUERY-001`, `M03-RND-FACADE-ENVIRONMENT-001`,
|
||||
`M03-RND-FACADE-ENTITY-001`, `M03-RND-TERRAIN-CACHE-SERVICE-001`,
|
||||
`M03-RND-TERRAIN-LOD-PLANNER-001`, `M03-RND-TERRAIN-CHUNK-QUEUE-001`,
|
||||
`M03-RND-M2-UNIQUE-REGISTRY-001`,
|
||||
`M03-RND-M2-PLACEMENT-TRANSFORM-001`,
|
||||
`M03-RND-M2-PLACEMENT-GROUPER-001`
|
||||
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
|
||||
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
|
||||
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner
|
||||
@@ -41,7 +46,26 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
`delegation=5 ground_contract=2`; cold `RenderedGroundSample contract=3`;
|
||||
environment integration passed cold snapshot `contract=5` and facade
|
||||
`delegation=6 environment_contract=7`; dry-run retained all `7/7` plans and
|
||||
applied the dusk checkpoint at `time=19.00`. M02 terrain-query regression
|
||||
applied the dusk checkpoint at `time=19.00`. Entity presentation passed
|
||||
`snapshot_cases=8 lifecycle_cases=14 runtime_scenes=2`; facade expanded to
|
||||
`delegation=9 entity_contract=6`, while character presentation remained `9/10/3/1`.
|
||||
Terrain quality cache passed `contract_cases=15 loader_calls=3`; internal-access
|
||||
inventory decreased to `42` because the loader no longer owns the LRU-order field.
|
||||
Terrain chunk LOD planner passed `cases=7 iterations=250 elapsed_ms=12.630`;
|
||||
post-merge facade, internal-access and `7/7` manifest gates remained green.
|
||||
Terrain chunk geometry queue planner passed `cases=8 iterations=100
|
||||
elapsed_ms=229.324`; internal-access remained `42` and post-merge facade and
|
||||
`7/7` manifest gates stayed green.
|
||||
M2 unique placement registry passed `cases=10 iterations=100 elapsed_ms=35.513`;
|
||||
the historical `uid:11785` cross-tile smoke, internal-access `42`, facade and
|
||||
`7/7` manifest gates remained green after merge.
|
||||
M2 placement transform resolver passed `cases=9 iterations=10000
|
||||
elapsed_ms=27.409`; post-merge M2 registry, internal-access `42`, facade,
|
||||
`7/7` manifest, documentation and coordination gates remained green.
|
||||
M2 placement grouper passed `cases=9 iterations=100 placements=256
|
||||
elapsed_ms=67.104`; post-merge transform resolver, registry, internal-access
|
||||
`42`, facade, `7/7` manifest, documentation and coordination gates remained green.
|
||||
M02 terrain-query regression
|
||||
remained green (13 pre-existing expired M00 claim warnings).
|
||||
- Fidelity comparison: all 16 historical operation limits and drain sites were
|
||||
preserved; chunk removal/create retain their shared removal-first budget.
|
||||
@@ -50,14 +74,48 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
`2/5/10/20/40`-unit nearby fallback while hiding mutable renderer state.
|
||||
Environment snapshots preserve the prior fixed-clock assignments while DBC
|
||||
profile/area/skybox selection, interpolation, fog, sun and shader rules remain unchanged.
|
||||
Runtime produces no world-entity commands yet, so player/world output and all
|
||||
baseline plans remain unchanged by the new present/remove ownership boundary.
|
||||
Terrain revisit cache retains the same admitted/excluded sources, LRU touch/trim,
|
||||
profile capacities and three loader adapter sites; no cache format changed.
|
||||
Chunk LOD keeps horizontal XZ chunk-center distance, inclusive squared thresholds,
|
||||
sparse-index omission and raw negative-radius squaring at both loader call sites.
|
||||
Chunk operation planning preserves stable removals, nearest-first create sorting,
|
||||
tile-LOD remove-all takeover, sparse tile-origin fallback and XZ-only distance;
|
||||
loader still drains removals first through the shared `chunk_geometry` budget.
|
||||
M2 positive `MDDF.uniqueId` retains `uid:<decimal>` first-owner semantics,
|
||||
within-call duplicate suppression, cross-tile skip/retry and unkeyed pass-through.
|
||||
M2 regular Euler, Elwynn cliff-rock shell correction, waterfall yaw/local-axis
|
||||
twist, tall-waterfall anchor compensation, path normalization and caller scale
|
||||
clamps are unchanged across worker grouping, placeholders and direct instances.
|
||||
M2 worker grouping retains invalid-record filtering, case-sensitive lowercase
|
||||
`.mdx/.mdl` conversion, first-seen group/per-group placement order, tile-local
|
||||
origin subtraction and the `0.0001` scale clamp.
|
||||
No new build-12340 parity claim is made; asset-backed visual/p95/p99 comparison remains pending.
|
||||
- Changed files: scene-free scheduler and verifier; streamer budget composition/
|
||||
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes;
|
||||
renderer-owned ground sample, facade/loader ground query, migrated terrain probe,
|
||||
immutable environment snapshot, facade/sky adapter, migrated checkpoint time input,
|
||||
contract-v1 entity snapshot, renderer-owned entity presenter, runtime composition,
|
||||
scene-free terrain quality Mesh cache and loader store/restore/clear adapters,
|
||||
immutable terrain chunk LOD policy, pure planner and both loader plan adapters,
|
||||
pure terrain chunk geometry queue planner and loader queue-adoption adapter,
|
||||
stateful M2 unique placement registry, loader reserve/release/clear adapters and
|
||||
migrated historical dedupe smoke, stateless M2 placement transform resolver and
|
||||
three transform consumers, stateless M2 placement grouper and worker adapter,
|
||||
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
|
||||
- Remaining risks: queue storage length and worker concurrency/stale-result logic
|
||||
remain streamer-owned; cancellation stops new permits but does not interrupt
|
||||
in-flight work; the environment snapshot currently covers world time but not weather
|
||||
or indoor state; M03 still needs entity facade commands, service extraction
|
||||
or indoor state; entity PackedScene loading is synchronous and not accepted for
|
||||
network-scale hot paths; terrain cache is entry-count bounded without Mesh-byte
|
||||
metrics and the remaining terrain build/tasks/state/finalization stay monolithic;
|
||||
chunk LOD asset-backed p95/p99 and visual acceptance remain pending;
|
||||
chunk queue planning adds a second tile-state pass whose asset-backed p95/p99
|
||||
still needs acceptance, while queue drains and geometry finalization remain monolithic;
|
||||
private Azeroth data was absent for the asset-backed ADT placement probe; M2
|
||||
asset-backed visual transform recheck, grouping/build/tasks/caches/finalization
|
||||
and boundary traversal p95/p99 remain pending; path grouping is not yet
|
||||
culling-driven spatial-cell grouping;
|
||||
M03 still needs further terrain/M2/WMO/liquid service extraction
|
||||
and dependency/performance acceptance.
|
||||
|
||||
Reference in New Issue
Block a user