221 lines
10 KiB
Markdown
221 lines
10 KiB
Markdown
# 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)
|