Files
open-wc/docs/modules/m2-static-batch-materializer.md
T

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