11 KiB
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
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
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
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
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
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
m2buildwork. - 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.gdcovers 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 |