10 KiB
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
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
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
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
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 feedm2_active_unique_idsbaseline 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: existinguid:11785cross-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 |