# M2 Placement Grouper ## Metadata | Field | Value | |---|---| | Status | Implemented extraction | | Target/work package | M03 / `M03-RND-M2-PLACEMENT-GROUPER-001` | | Owners | Pure ADT M2 placement validation, normalization and transform grouping | | Last verified | Worktree `work/sindo-main-codex/m03-m2-placement-grouper`, 2026-07-16 | | Profiles/capabilities | Existing static M2 worker-grouping path | ## Purpose Convert valid raw ADT M2 placement records into ordered tile-local transforms grouped by the historically normalized relative model path. ## Non-goals - Reserve cross-tile unique IDs or choose placement ownership. - Own WorkerThreadPool tasks, mutexes, result queues or tile lifecycle. - Load models, choose animated/static rendering or build MultiMesh batches. - Create Node, Mesh, material, Resource or RID objects. - Change path case, transform rules, batching or visual fidelity. ## Context and boundaries ```mermaid flowchart LR ADT[ADT names + placements] --> Registry[M2UniquePlacementRegistry] Registry --> Grouper[M2PlacementGrouper] Grouper --> Resolver[M2PlacementTransformResolver] Resolver --> Grouper Grouper --> Groups[Path to ordered Transform3D arrays] Groups --> Worker[Loader worker result adapter] Worker --> Build[Loader-owned build queue] ``` Allowed dependencies are value containers, `Vector3`, `Basis`, `Transform3D` and `M2PlacementTransformResolver`. SceneTree, Node, RenderingServer, WorkerThreadPool, mutexes, ResourceLoader, files, gameplay, network and editor UI are forbidden. ## Public API | Symbol | Kind | Purpose | Thread/lifetime | Errors | |---|---|---|---|---| | `group_placements(tile_origin, m2_names, m2_placements)` | Pure query | Return valid placements grouped as ordered tile-local transforms | Worker/main thread; call-local output | Invalid variants/name IDs/empty paths are skipped | ## Inputs and outputs | Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | |---|---|---|---|---|---| | Input | Godot-space tile origin | Loader tile state | Local-position calculation | Value copy | One call | | Input | ADT MMDX name table | Parsed ADT tile | Path lookup | Read-only PackedStringArray | One call | | Input | Raw placement dictionaries | Unique registry result | Validation/transforms | Read-only shallow values | One call | | Output | Relative path to ordered `Transform3D` arrays | Grouper | Loader worker/build adapter | Fresh Dictionary and arrays | One call/build job | Output retains first-seen group-key order through Godot Dictionary insertion order and placement order inside each array. Inputs are not retained or returned. ## Data flow ```mermaid flowchart TD Call[group_placements] --> Next[Read placement] Next --> Dict{Dictionary?} Dict -->|no| Next Dict -->|yes| Name{Valid name_id?} Name -->|no| Next Name -->|yes| Normalize[Backslash to slash; lowercase MDX/MDL to M2] Normalize --> Empty{Path empty?} Empty -->|yes| Next Empty -->|no| Local[world position - tile origin] Local --> Resolve[Resolve basis and origin offset] Resolve --> Scale[Apply max scale 0.0001] Scale --> Append[Append Transform3D to path group] Append --> Next Next -->|complete| Return[Return fresh groups] ``` ## Lifecycle/state The grouper retains no state. Every call constructs one stateless transform resolver and fresh output containers. Reset, cancellation and shutdown require no grouper action. ## Main sequence ```mermaid sequenceDiagram participant Loader as Loader worker callback participant Grouper as M2PlacementGrouper participant Resolver as M2PlacementTransformResolver Loader->>Grouper: group_placements(origin, names, placements) loop valid placement Grouper->>Resolver: resolve basis and origin offset Resolver-->>Grouper: value-only basis and offset Grouper->>Grouper: append local Transform3D end Grouper-->>Loader: fresh path groups Loader->>Loader: mutex-protected result enqueue ``` ## Ownership, threading and resources - The grouper owns only call-local containers and transforms. - The loader owns tasks, mutex/result queue, tile/build state, caches, model loading, Nodes, MultiMesh, Mesh, materials and RIDs. - Worker use is safe because the grouper and resolver mutate no shared state. - The caller owns returned containers; mutation cannot affect later calls. ## Errors, cancellation and recovery | Failure | Detection | Behavior | Diagnostic | Recovery | |---|---|---|---|---| | Non-Dictionary placement | Type guard | Skip | Contract fixture | Repair parser input | | Invalid name ID | Bounds guard | Skip | Contract fixture | Repair name table/placement | | Empty path | Empty guard | Skip | Contract fixture | Supply valid MMDX entry | | Missing position/rotation/scale | Defaults | Zero position/rotation and scale 1 | Contract fixture | Expected fallback | | Zero/negative scale | Historical clamp | Basis scale uses `0.0001` | Contract fixture | Supply valid ADT scale | | Cancellation after grouping | Loader state check | Discard stale result | Loader regressions | Re-request eligible detail | Cancellation stays loader-owned because grouping has no task or partial state. ## Configuration and capabilities No setting or profile branch is introduced. Preserved behavior includes backslash replacement without lowercasing, case-sensitive lowercase `.mdx/.mdl` conversion, first-seen ordering, tile-local subtraction and scale clamp `0.0001`. ## Persistence, cache and migration No persistence or cache format participates. ADT and M2 cache versions are unchanged, so no rebuild or migration is required. ## Diagnostics and observability The service emits no logs. Its verifier records contract results, source boundaries and bounded synthetic 100-by-256 placement timing. Loader metrics continue to own task/build/cache observability. ## Verification - `verify_m2_placement_grouper.gd`: invalid input, path behavior, ordering, defaults, local origin, regular/waterfall transforms, scale clamp, fresh output, loader/source boundary and timing. - `verify_m2_placement_transform_resolver.gd`: three transform consumers remain covered as two direct loader adapters plus the grouper adapter. - Registry/dedupe, facade, internal-access, manifest, shutdown, documentation and coordination regressions remain required. - Fidelity evidence is exact extraction; no new asset-backed parity claim is made. ## Extension points - A later package may extract the M2 build-job state machine behind explicit budgets while retaining this pure grouping input. - Spatial cells require measured culling evidence and are outside this contract. ## Capability status | Capability | Status | Evidence | Gap/next step | |---|---|---|---| | Pure path/transform grouping | Implemented extraction | Contract/source/timing verifier | Asset-backed traversal timing pending | | Worker/build orchestration | Remains in loader | Existing regressions | Separate stateful extraction required | | Spatial MultiMesh cells | Partial | Current path batches | Culling-driven design/evidence pending | ## Known gaps and risks - Raw placement dictionaries remain the ADT parser boundary. - Uppercase extensions remain unchanged even though transform exception matching is case-insensitive. - Grouping is by model path, not spatial culling cell. - Asset-backed p95/p99 and visual comparison remain unavailable. ## Source map | Path | Responsibility | |---|---| | `src/render/m2/m2_placement_grouper.gd` | Validation, normalization and grouping | | `src/render/m2/m2_placement_transform_resolver.gd` | Basis and origin rules | | `src/scenes/streaming/streaming_world_loader.gd` | Worker/result/build lifecycle and resources | | `src/tools/verify_m2_placement_grouper.gd` | Contract, dependency and timing regression | ## Related decisions and references - [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md) - [`m2-unique-placement-registry.md`](m2-unique-placement-registry.md) - [`world-renderer.md`](world-renderer.md) - [`../../RENDER.md`](../../RENDER.md) - [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)