Files
open-wc/docs/modules/m2-unique-placement-registry.md

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