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