# M2 Static Batch Materializer ## Metadata | Field | Value | |---|---| | Status | Implemented | | Target/work package | M03 / `M03-RND-M2-STATIC-BATCH-MATERIALIZER-001` | | Owners | Static M2 MultiMesh construction, render settings and attachment | | Last verified | Worktree `work/sindo-main-codex/m03-m2-static-batch-materializer`, 2026-07-18 | | Profiles/capabilities | Existing static M2 placement path | ## Purpose Build and attach one static M2 `MultiMeshInstance3D` from an already prepared Mesh and an ordered transform slice. The service preserves the existing transform format, Mesh identity, batch name, visibility and shadow behavior. ## Non-goals - Group placements, size batches or advance build-job cursors. - Load, refresh, build or cache Mesh resources. - Consume render permits, rotate queues or decide retries/cancellation. - Assign Editor owners or release tile roots. - Materialize animated instances or introduce spatial-cell batching. ## Context and boundaries ```mermaid flowchart LR Groups[Ordered transform group] --> Loader[StreamingWorldLoader build adapter] MeshCache[Prepared Mesh cache] --> Loader Planner[M2BuildBatchPlanner] --> Slice[Start and count] Slice --> Loader Loader --> Materializer[M2StaticBatchMaterializer] Materializer --> Batch[Attached MultiMeshInstance3D] Loader --> EditorOwner[Editor owner adapter] ``` Allowed dependencies are Godot `Mesh`, `MultiMesh`, `MultiMeshInstance3D`, `GeometryInstance3D` and main-thread SceneTree APIs. Files, ResourceLoader, workers, RenderingServer calls, caches, build jobs, scheduler policy, gameplay, network and Editor ownership are forbidden. ## Public API | Symbol | Kind | Purpose | Thread/lifetime | Errors | |---|---|---|---|---| | `materialize_batch(parent, path, mesh, transforms, start, count, serial, visibility_end, visibility_margin, cast_shadows)` | Command/query | Create, configure and attach one static MultiMesh batch | Main thread; borrowed inputs, parent owns result | Invalid/empty input returns null; transform bounds are a caller precondition | ## Inputs and outputs | Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | |---|---|---|---|---|---| | Input | M2 parent root | Loader build job | Batch attachment | Borrowed Node3D | Tile/job lifetime | | Input | Relative model path and serial | Loader build adapter | Batch node name | Copied values | One call | | Input | Prepared static Mesh | M2 Mesh resource cache/finalizer | MultiMesh | Borrowed Resource; exact reference retained by MultiMesh | Cache and batch lifetimes | | Input | Ordered transforms, start and count | Placement group and batch plan | MultiMesh transform buffer | Borrowed Array/scalars | One call | | Input | Visibility end/margin and shadow flag | Renderer configuration | MultiMeshInstance3D properties | Copied scalars | One call | | Output | Attached `MultiMeshInstance3D` | Materializer | Parent SceneTree and loader Editor-owner adapter | Parent-owned node/resource | Tile lifetime | Side effects are MultiMesh allocation, transform-buffer mutation, MultiMeshInstance3D allocation/configuration and SceneTree attachment. The service retains no direct reference after return. ## Data flow ```mermaid flowchart TD Validate[Validate parent, Mesh, transforms and count] --> MultiMesh[Allocate TRANSFORM_3D MultiMesh] MultiMesh --> MeshIdentity[Assign exact prepared Mesh] MeshIdentity --> Count[Set instance count] Count --> Upload[Upload transforms start plus ordered batch offset] Upload --> Node[Create named MultiMeshInstance3D] Node --> Render[Apply shadow and optional visibility end/margin] Render --> Attach[Attach once to supplied parent] Attach --> Return[Return attached node] Validate -->|invalid| Null[Return null without attachment] ``` ## Lifecycle/state ```mermaid stateDiagram-v2 [*] --> Validating Validating --> Rejected: invalid or empty input Validating --> Building: valid slice Building --> Configured: transforms and render properties set Configured --> Attached: parent add_child Rejected --> [*] Attached --> [*]: parent/tile owner releases subtree ``` The `RefCounted` service is stateless; states describe a single call. ## Main sequence ```mermaid sequenceDiagram participant L as StreamingWorldLoader participant P as M2BuildBatchPlanner participant M as M2StaticBatchMaterializer participant R as M2 parent root L->>P: plan_batch(transform count, offset, static limits) P-->>L: start/count and cursor transition L->>L: resolve prepared Mesh or rotate pending job L->>M: materialize_batch(parent, Mesh, transform slice, render settings) M->>M: allocate MultiMesh and upload ordered transforms M->>R: add_child(MultiMeshInstance3D) M-->>L: attached node L->>L: assign optional Editor owner ``` ## Dependency diagram ```mermaid flowchart TB Loader[StreamingWorldLoader] --> Materializer[M2StaticBatchMaterializer] Planner[M2BuildBatchPlanner] --> Loader MeshState[M2MeshResourceCacheState] --> Loader Materializer --> Engine[Mesh / MultiMesh / MultiMeshInstance3D] Loader --> EditorOwner[Editor ownership] Loader --> Scheduler[RenderBudgetScheduler] Materializer -. no dependency .-> ResourceIO[ResourceLoader / files] Materializer -. no dependency .-> BuildState[Build jobs / queues / caches] ``` ## Ownership, threading and resources - The caller borrows a prepared Mesh and transform Array for the synchronous call. - The new MultiMesh retains the exact Mesh Resource reference. - The new MultiMeshInstance3D retains the MultiMesh and transfers to the parent. - Parent/tile teardown owns node and Resource release through the SceneTree. - Loader assigns an Editor owner after successful attachment when configured. - All Resource and SceneTree mutation is main-thread-only. - No RID is acquired or released directly by this service. ## Errors, cancellation and recovery | Failure | Detection | Behavior | Diagnostic | Recovery | |---|---|---|---|---| | Null parent or Mesh | Entry guard | Return null without allocation/attachment | Synthetic contract fixture | Correct build composition/cache state | | Empty transforms or non-positive count | Entry guard | Return null | Synthetic contract fixture | Planner supplies non-empty slice | | Invalid transform bounds | Caller precondition | Historical indexed access behavior remains | Build planner/source regression | Repair job cursor/group state | | Mesh pending | Outside service | Loader rotates job without calling service | Existing build/cache diagnostics | Async completion retries job | | Tile cancelled | Outside service | Loader cancels before materialization or releases parent later | Shutdown regression | Eligible tile may requeue | The service does not swallow invalid bounds or silently clamp/reorder transforms, because doing so would change the accepted build-planner contract. ## Configuration and capabilities | Setting/capability | Default | Profile | Runtime mutable | Effect | |---|---|---|---|---| | Transform format | `MultiMesh.TRANSFORM_3D` | All | No | Preserves full 3D M2 placements | | Static batch count | Loader `m2_multimesh_batch_size` through planner | Quality preset | Yes, between plans | Controls per-batch transform count | | Visibility end/margin | Loader M2 range / chunk size | Quality preset | Yes, between calls | Applied only when end is positive | | Shadow mode | Loader `m2_cast_shadows` | Quality preset | Yes, between calls | Selects ON/OFF casting setting | ## Persistence, cache and migration No state is serialized and no cache/schema version changes. The service consumes the accepted prepared Mesh cache contract; no rebake or migration is required. ## Diagnostics and observability - The service emits no logs, metrics or debug views. - Existing loader queue/hitch metrics continue to account for `m2build` work. - Path/serial remain visible through the unchanged batch node name. - Editor ownership remains observable through the existing loader adapter. ## Verification - `verify_m2_static_batch_materializer.gd` covers invalid inputs, node naming, exact Mesh identity, transform format/count, ordered slice source contract, visibility, shadows, single attachment, ownership boundaries and timing. - Build planner, Mesh cache/extractor/finalizer, prototype, animated materializer, shutdown, materials, facade and internal-access regressions protect neighbors. - The headless dummy renderer does not round-trip per-instance transforms through `MultiMesh.get_instance_transform`; exact ordered upload is protected by the source contract and render checkpoints remain the observable regression gate. - Fidelity evidence is exact extraction only; no new build-12340 parity claim. - Performance budget: 10,000 transform API writes under one second in headless. ## Extension points Measured spatial-cell batching may later provide smaller transform slices while reusing this materializer. A generic static/animated base class, async mutation and callback framework are intentionally excluded. ## Capability status | Capability | Status | Evidence | Gap/next step | |---|---|---|---| | Static M2 MultiMesh materialization | Implemented extraction | Synthetic node/resource/render/source/timing verifier | Asset-backed traversal pending | | Transform ordering | Implemented extraction | Exact source contract and existing checkpoints | Headless per-instance getter unavailable | | Editor persistence | Existing loader-owned | Source boundary and editor helper | Editor integration fixture remains separate | | Spatial-cell batching | Planned | Renderer roadmap | Culling/performance design and evidence pending | ## Known gaps and risks - Materialization remains synchronous main-thread work under the existing permit. - MultiMesh transform upload timing in a headless dummy renderer does not measure GPU cost. - No private asset traversal, visual comparison, descriptor-pressure, leak or p95/p99 run exists. - Current model-path batching is not culling-driven spatial-cell batching. ## Source map | Path | Responsibility | |---|---| | `src/render/m2/m2_static_batch_materializer.gd` | Static MultiMesh allocation, transform upload, render setup and attachment | | `src/render/m2/m2_build_batch_planner.gd` | Static/animated batch count and cursor planning | | `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared static Mesh reference ownership | | `src/scenes/streaming/streaming_world_loader.gd` | Resource readiness, job/budget adapter and Editor ownership | | `src/tools/verify_m2_static_batch_materializer.gd` | Node/resource/render/source/timing regression | ## Related decisions and references - [`m2-build-batch-planner.md`](m2-build-batch-planner.md) - [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md) - [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.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)