Compare commits
5 Commits
57ae1b8f16
...
1b6c46870d
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b6c46870d | |||
| 2342430439 | |||
| c41e8886a6 | |||
| acb3a809bb | |||
| c13c993645 |
@@ -977,6 +977,19 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
|||||||
- Cache formats, quality profiles and visual rules are unchanged. Asset-backed
|
- Cache formats, quality profiles and visual rules are unchanged. Asset-backed
|
||||||
p95/p99 and visual acceptance remain required.
|
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 World Environment Snapshot Facade
|
## 2026-07-16 World Environment Snapshot Facade
|
||||||
|
|
||||||
- `WorldEnvironmentSnapshot` carries one immutable finite time-of-day value,
|
- `WorldEnvironmentSnapshot` carries one immutable finite time-of-day value,
|
||||||
|
|||||||
@@ -0,0 +1,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
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
| Terrain quality Mesh cache | Implemented extraction | [`terrain-quality-mesh-cache.md`](terrain-quality-mesh-cache.md) |
|
| Terrain quality Mesh cache | Implemented extraction | [`terrain-quality-mesh-cache.md`](terrain-quality-mesh-cache.md) |
|
||||||
| Terrain chunk LOD planner | Implemented extraction | [`terrain-chunk-lod-planner.md`](terrain-chunk-lod-planner.md) |
|
| Terrain chunk LOD planner | Implemented extraction | [`terrain-chunk-lod-planner.md`](terrain-chunk-lod-planner.md) |
|
||||||
| Terrain chunk geometry queue planner | Implemented extraction | [`terrain-chunk-geometry-queue-planner.md`](terrain-chunk-geometry-queue-planner.md) |
|
| Terrain chunk geometry queue planner | Implemented extraction | [`terrain-chunk-geometry-queue-planner.md`](terrain-chunk-geometry-queue-planner.md) |
|
||||||
|
| M2 unique placement registry | Implemented extraction | [`m2-unique-placement-registry.md`](m2-unique-placement-registry.md) |
|
||||||
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
|
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
|
||||||
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
|
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
|
||||||
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
||||||
|
|||||||
@@ -0,0 +1,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)
|
||||||
@@ -5,9 +5,9 @@
|
|||||||
| Field | Value |
|
| Field | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Status | Partial |
|
| Status | Partial |
|
||||||
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain cache/LOD/geometry-queue packages |
|
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; `M03-RND-M2-UNIQUE-REGISTRY-001` |
|
||||||
| Owners | Renderer workstream / milestone integrator |
|
| Owners | Renderer workstream / milestone integrator |
|
||||||
| Last verified | Worktree `work/sindo-main-codex/m03-terrain-chunk-queue`, 2026-07-16 |
|
| Last verified | Worktree `work/sindo-main-codex/m03-m2-unique-registry`, 2026-07-16 |
|
||||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
@@ -46,6 +46,8 @@ flowchart LR
|
|||||||
ChunkLod --> Loader
|
ChunkLod --> Loader
|
||||||
Loader --> ChunkQueue[TerrainChunkGeometryQueuePlanner]
|
Loader --> ChunkQueue[TerrainChunkGeometryQueuePlanner]
|
||||||
ChunkQueue --> Loader
|
ChunkQueue --> Loader
|
||||||
|
Loader --> M2Registry[M2UniquePlacementRegistry]
|
||||||
|
M2Registry --> Loader
|
||||||
Native --> Parsed[Parsed tile/model data]
|
Native --> Parsed[Parsed tile/model data]
|
||||||
Parsed --> Loader
|
Parsed --> Loader
|
||||||
Loader --> Scene[SceneTree nodes]
|
Loader --> Scene[SceneTree nodes]
|
||||||
@@ -112,6 +114,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
|||||||
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
||||||
|
|
||||||
Публичным contract не считаются `StreamingWorldLoader` methods/properties,
|
Публичным contract не считаются `StreamingWorldLoader` methods/properties,
|
||||||
внутренние dictionaries, queues, job records и generated node names. Scene-owned
|
внутренние dictionaries, queues, job records и generated node names. Scene-owned
|
||||||
@@ -136,6 +139,7 @@ loader configuration remains transitional composition data, not a caller API.
|
|||||||
| Internal cache | Full-quality terrain Mesh/source | Loader terrain upgrade / `TerrainQualityMeshCache` | Revisited tile state | Cache-retained Mesh reference | Map session until eviction/reset |
|
| 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 | 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 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 |
|
||||||
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
|
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
|
||||||
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
|
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
|
||||||
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
|
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
|
||||||
@@ -179,6 +183,8 @@ flowchart TD
|
|||||||
ChunkLod --> DesiredChunkLod[Desired chunk LOD state]
|
ChunkLod --> DesiredChunkLod[Desired chunk LOD state]
|
||||||
DesiredChunkLod --> ChunkQueue[TerrainChunkGeometryQueuePlanner]
|
DesiredChunkLod --> ChunkQueue[TerrainChunkGeometryQueuePlanner]
|
||||||
ChunkQueue --> B
|
ChunkQueue --> B
|
||||||
|
R --> M2Registry[M2UniquePlacementRegistry reserve]
|
||||||
|
M2Registry --> M2
|
||||||
R --> B[RenderBudgetScheduler permits]
|
R --> B[RenderBudgetScheduler permits]
|
||||||
B --> Terrain[Terrain attach/upgrade]
|
B --> Terrain[Terrain attach/upgrade]
|
||||||
B --> M2[M2 group/MultiMesh attach]
|
B --> M2[M2 group/MultiMesh attach]
|
||||||
@@ -290,6 +296,9 @@ sequenceDiagram
|
|||||||
- `TerrainChunkGeometryQueuePlanner` is stateless and owns only call-local
|
- `TerrainChunkGeometryQueuePlanner` is stateless and owns only call-local
|
||||||
create/remove request calculation and sorting. The loader adopts the fresh
|
create/remove request calculation and sorting. The loader adopts the fresh
|
||||||
arrays and retains removal-first drains, budgets and render side effects.
|
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.
|
||||||
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
|
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
|
||||||
the facade never returns Mesh, Node, tile-state or queue references.
|
the facade never returns Mesh, Node, tile-state or queue references.
|
||||||
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
|
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
|
||||||
@@ -372,6 +381,9 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
- Terrain chunk geometry queue contract: current/desired comparison, tile-LOD
|
- Terrain chunk geometry queue contract: current/desired comparison, tile-LOD
|
||||||
takeover, sparse origin fallback, stable removals, nearest creates, loader
|
takeover, sparse origin fallback, stable removals, nearest creates, loader
|
||||||
drain boundary and bounded multi-tile timing without a world scene.
|
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.
|
||||||
- Budget scheduler contract: exact lane exhaustion, shared chunk removal/create
|
- Budget scheduler contract: exact lane exhaustion, shared chunk removal/create
|
||||||
priority, independent lanes, frame reset, invalid limits, terminal cancellation,
|
priority, independent lanes, frame reset, invalid limits, terminal cancellation,
|
||||||
dependency boundaries and bounded permit timing without loading a world scene.
|
dependency boundaries and bounded permit timing without loading a world scene.
|
||||||
@@ -401,6 +413,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| ADT streaming/terrain | Partial | M00 checkpoints and current scenes | Fidelity/performance gaps remain |
|
| ADT streaming/terrain | Partial | M00 checkpoints and current scenes | Fidelity/performance gaps remain |
|
||||||
| Static M2 placement | Partial | MultiMesh/dedupe probes | Full materials/animation/effects |
|
| 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 |
|
||||||
| WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity |
|
| WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity |
|
||||||
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
|
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
|
||||||
| Sky/light | Partial | DBC controller/logs | Zone transition paired validation |
|
| Sky/light | Partial | DBC controller/logs | Zone transition paired validation |
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -24,6 +24,9 @@ const TERRAIN_CHUNK_LOD_PLANNER_SCRIPT := preload("res://src/render/terrain/terr
|
|||||||
const TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER_SCRIPT := preload(
|
const TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER_SCRIPT := preload(
|
||||||
"res://src/render/terrain/terrain_chunk_geometry_queue_planner.gd"
|
"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 STREAMING_TARGET_PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
|
const STREAMING_TARGET_PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
|
||||||
const STREAMING_TARGET_POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
|
const 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")
|
const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd")
|
||||||
@@ -207,7 +210,9 @@ var _m2_group_result_mutex := Mutex.new()
|
|||||||
var _m2_group_result_queue: Array = []
|
var _m2_group_result_queue: Array = []
|
||||||
var _m2_build_jobs: Dictionary = {}
|
var _m2_build_jobs: Dictionary = {}
|
||||||
var _m2_build_queue: Array = []
|
var _m2_build_queue: Array = []
|
||||||
var _m2_unique_registry: Dictionary = {}
|
var _m2_unique_placement_registry := (
|
||||||
|
M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT.new()
|
||||||
|
)
|
||||||
var _m2_mesh_cache: Dictionary = {}
|
var _m2_mesh_cache: Dictionary = {}
|
||||||
var _m2_mesh_load_requests: Dictionary = {}
|
var _m2_mesh_load_requests: Dictionary = {}
|
||||||
var _m2_mesh_finalize_queue: Array = []
|
var _m2_mesh_finalize_queue: Array = []
|
||||||
@@ -1011,7 +1016,7 @@ func render_baseline_snapshot() -> Dictionary:
|
|||||||
"chunk_remove": _chunk_remove_queue.size(),
|
"chunk_remove": _chunk_remove_queue.size(),
|
||||||
},
|
},
|
||||||
"cache_entries": _tile_mesh_cache.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(),
|
"wmo_instances": _wmo_registry.size(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2965,7 +2970,7 @@ func _clear_streamed_world() -> void:
|
|||||||
for key in _m2_build_jobs.keys():
|
for key in _m2_build_jobs.keys():
|
||||||
_cancel_m2_build_job(String(key))
|
_cancel_m2_build_job(String(key))
|
||||||
_m2_build_queue.clear()
|
_m2_build_queue.clear()
|
||||||
_m2_unique_registry.clear()
|
_m2_unique_placement_registry.clear()
|
||||||
_m2_mesh_load_requests.clear()
|
_m2_mesh_load_requests.clear()
|
||||||
_m2_mesh_finalize_queue.clear()
|
_m2_mesh_finalize_queue.clear()
|
||||||
_m2_runtime_rebuild_required_cache.clear()
|
_m2_runtime_rebuild_required_cache.clear()
|
||||||
@@ -4129,7 +4134,10 @@ func _request_tile_m2_assets(
|
|||||||
if _m2_group_tasks.has(key) or _m2_build_jobs.has(key):
|
if _m2_group_tasks.has(key) or _m2_build_jobs.has(key):
|
||||||
return result
|
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", [])
|
var filtered_placements: Array = filtered.get("placements", [])
|
||||||
result["unique_keys"] = filtered.get("unique_keys", [])
|
result["unique_keys"] = filtered.get("unique_keys", [])
|
||||||
result["skipped_unique_keys"] = filtered.get("skipped_unique_keys", [])
|
result["skipped_unique_keys"] = filtered.get("skipped_unique_keys", [])
|
||||||
@@ -4143,46 +4151,6 @@ func _request_tile_m2_assets(
|
|||||||
return result
|
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:
|
func _release_tile_m2_unique_keys(state: Dictionary, notify_candidates: bool = true) -> void:
|
||||||
var tile_key := String(state.get("key", ""))
|
var tile_key := String(state.get("key", ""))
|
||||||
var unique_keys: Array = state.get("m2_unique_keys", [])
|
var unique_keys: Array = state.get("m2_unique_keys", [])
|
||||||
@@ -4191,16 +4159,7 @@ func _release_tile_m2_unique_keys(state: Dictionary, notify_candidates: bool = t
|
|||||||
state["m2_skipped_unique_keys"] = []
|
state["m2_skipped_unique_keys"] = []
|
||||||
return
|
return
|
||||||
|
|
||||||
var released_keys: Array = []
|
var released_keys: Array = _m2_unique_placement_registry.release(tile_key, unique_keys)
|
||||||
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)
|
|
||||||
|
|
||||||
state["m2_unique_keys"] = []
|
state["m2_unique_keys"] = []
|
||||||
state["m2_skipped_unique_keys"] = []
|
state["m2_skipped_unique_keys"] = []
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
extends SceneTree
|
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
|
var _failures := 0
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
var loader = STREAMING_WORLD_LOADER.new()
|
var registry: RefCounted = M2_UNIQUE_PLACEMENT_REGISTRY.new()
|
||||||
var placement := {
|
var placement := {
|
||||||
"name_id": 0,
|
"name_id": 0,
|
||||||
"unique_id": 11785,
|
"unique_id": 11785,
|
||||||
@@ -15,30 +17,24 @@ func _initialize() -> void:
|
|||||||
"scale": 1.0,
|
"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("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("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")
|
_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("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")
|
_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", {
|
registry.call("release", "30_49", ["uid:11785"])
|
||||||
"key": "30_49",
|
var after_release: Dictionary = registry.call("reserve", "31_49", [placement])
|
||||||
"m2_unique_keys": ["uid:11785"],
|
|
||||||
"m2_skipped_unique_keys": [],
|
|
||||||
}, false)
|
|
||||||
var after_release: Dictionary = loader.call("_reserve_tile_m2_placements", "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("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")
|
_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_registry: RefCounted = M2_UNIQUE_PLACEMENT_REGISTRY.new()
|
||||||
var same_tile_duplicate: Dictionary = same_tile_loader.call("_reserve_tile_m2_placements", "31_49", [placement, placement])
|
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")
|
_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:
|
if _failures > 0:
|
||||||
push_error("M2 unique dedupe verification failed: %d issue(s)" % _failures)
|
push_error("M2 unique dedupe verification failed: %d issue(s)" % _failures)
|
||||||
quit(1)
|
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
|
||||||
@@ -27,11 +27,12 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
## Evidence
|
## Evidence
|
||||||
|
|
||||||
- Date: 2026-07-16
|
- Date: 2026-07-16
|
||||||
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`;
|
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`;
|
||||||
packages `M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-001`,
|
packages `M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-001`,
|
||||||
`M03-RND-FACADE-GROUND-QUERY-001`, `M03-RND-FACADE-ENVIRONMENT-001`,
|
`M03-RND-FACADE-GROUND-QUERY-001`, `M03-RND-FACADE-ENVIRONMENT-001`,
|
||||||
`M03-RND-FACADE-ENTITY-001`, `M03-RND-TERRAIN-CACHE-SERVICE-001`,
|
`M03-RND-FACADE-ENTITY-001`, `M03-RND-TERRAIN-CACHE-SERVICE-001`,
|
||||||
`M03-RND-TERRAIN-LOD-PLANNER-001`, `M03-RND-TERRAIN-CHUNK-QUEUE-001`
|
`M03-RND-TERRAIN-LOD-PLANNER-001`, `M03-RND-TERRAIN-CHUNK-QUEUE-001`,
|
||||||
|
`M03-RND-M2-UNIQUE-REGISTRY-001`
|
||||||
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
|
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
|
||||||
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
|
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
|
||||||
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner
|
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner
|
||||||
@@ -53,6 +54,9 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
Terrain chunk geometry queue planner passed `cases=8 iterations=100
|
Terrain chunk geometry queue planner passed `cases=8 iterations=100
|
||||||
elapsed_ms=229.324`; internal-access remained `42` and post-merge facade and
|
elapsed_ms=229.324`; internal-access remained `42` and post-merge facade and
|
||||||
`7/7` manifest gates stayed green.
|
`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.
|
||||||
M02 terrain-query regression
|
M02 terrain-query regression
|
||||||
remained green (13 pre-existing expired M00 claim warnings).
|
remained green (13 pre-existing expired M00 claim warnings).
|
||||||
- Fidelity comparison: all 16 historical operation limits and drain sites were
|
- Fidelity comparison: all 16 historical operation limits and drain sites were
|
||||||
@@ -71,6 +75,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
Chunk operation planning preserves stable removals, nearest-first create sorting,
|
Chunk operation planning preserves stable removals, nearest-first create sorting,
|
||||||
tile-LOD remove-all takeover, sparse tile-origin fallback and XZ-only distance;
|
tile-LOD remove-all takeover, sparse tile-origin fallback and XZ-only distance;
|
||||||
loader still drains removals first through the shared `chunk_geometry` budget.
|
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.
|
||||||
No new build-12340 parity claim is made; asset-backed visual/p95/p99 comparison remains pending.
|
No new build-12340 parity claim is made; asset-backed visual/p95/p99 comparison remains pending.
|
||||||
- Changed files: scene-free scheduler and verifier; streamer budget composition/
|
- Changed files: scene-free scheduler and verifier; streamer budget composition/
|
||||||
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes;
|
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes;
|
||||||
@@ -80,6 +86,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
scene-free terrain quality Mesh cache and loader store/restore/clear adapters,
|
scene-free terrain quality Mesh cache and loader store/restore/clear adapters,
|
||||||
immutable terrain chunk LOD policy, pure planner and both loader plan adapters,
|
immutable terrain chunk LOD policy, pure planner and both loader plan adapters,
|
||||||
pure terrain chunk geometry queue planner and loader queue-adoption adapter,
|
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,
|
||||||
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
|
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
|
||||||
- Remaining risks: queue storage length and worker concurrency/stale-result logic
|
- Remaining risks: queue storage length and worker concurrency/stale-result logic
|
||||||
remain streamer-owned; cancellation stops new permits but does not interrupt
|
remain streamer-owned; cancellation stops new permits but does not interrupt
|
||||||
@@ -90,5 +98,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
chunk LOD asset-backed p95/p99 and visual acceptance remain pending;
|
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
|
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;
|
still needs acceptance, while queue drains and geometry finalization remain monolithic;
|
||||||
|
private Azeroth data was absent for the asset-backed ADT placement probe; M2
|
||||||
|
grouping/build/tasks/caches/finalization and boundary traversal p95/p99 remain pending;
|
||||||
M03 still needs further terrain/M2/WMO/liquid service extraction
|
M03 still needs further terrain/M2/WMO/liquid service extraction
|
||||||
and dependency/performance acceptance.
|
and dependency/performance acceptance.
|
||||||
|
|||||||
Reference in New Issue
Block a user