# 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)
