# M2 Animated Instance Materializer ## Metadata | Field | Value | |---|---| | Status | Implemented extraction | | Target/work package | M03 / `M03-RND-M2-ANIMATED-INSTANCE-MATERIALIZER-001` | | Owners | Animated M2 duplicate/batch construction, render settings, playback startup and attachment | | Last verified | Worktree `work/sindo-main-codex/m03-m2-animated-instance-materializer`, 2026-07-18 | | Profiles/capabilities | Imported GLB and native experimental animated M2 instances | ## Purpose Materialize one ordered animated M2 build batch on the main thread. The service duplicates an accepted prototype, preserves historical names and transforms, applies visibility/shadow settings recursively, starts native/imported playback, attaches a non-empty batch and returns detached native diagnostic entries. ## Non-goals - Plan build batches, advance job cursors or consume render budgets. - Load, finalize, select or cache animated prototypes. - Select animations or implement native animator deformation. - Format logs, normalize paths, assign Editor owners or clean up tile roots. - Materialize static M2 `MultiMesh` batches. ## Context and boundaries ```mermaid flowchart LR Loader[StreamingWorldLoader adapter] --> Materializer[M2AnimatedInstanceMaterializer] Prototype[Accepted animated prototype] --> Materializer Plan[Transforms and batch slice] --> Materializer Materializer --> Playback[M2AnimationPlaybackController] Materializer --> Finalizer[M2AnimatedSceneFinalizer traversal] Materializer --> Batch[Attached animated batch] Materializer --> Diagnostics[Detached diagnostic entries] ``` Allowed dependencies are Godot Node/Node3D/GeometryInstance3D APIs and the accepted animated finalizer and playback services. Resource loading, files, workers, caches, scheduler policy, Editor ownership and `MultiMesh` are forbidden. ## Public API | Symbol | Kind | Purpose | Thread/lifetime | Errors | |---|---|---|---|---| | `materialize_batch(parent, path, prototype, transforms, start, count, serial, visibility_end, visibility_margin, cast_shadows, native_script, collect_diagnostics)` | Command/query | Build, start and attach one ordered animated batch | Main thread; borrowed inputs, parent owns result | Invalid/empty input or all failed duplicates returns `{}` | ## 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 path, start/count and serial | Batch planner/loader adapter | Names, phase and slice | Copied scalars | One call | | Input | Accepted animated prototype | Prototype cache | Duplicate source | Borrowed Node3D | Cache-owned | | Input | Ordered transforms | Placement grouping/build job | Instance transforms | Borrowed Array | Job lifetime | | Input | Visibility/shadow settings | Renderer configuration | Recursive render mutation | Copied scalars | One call | | Input | Native animator Script identity | Loader composition | Playback controller | Borrowed Script | Application lifetime | | Output | `batch_root` | Materializer | Parent SceneTree and loader Editor-owner adapter | Parent-owned Node3D | Tile lifetime | | Output | `native_diagnostics` entries | Playback controller/materializer | Loader log adapter | Caller-owned Array/Dictionaries | Debug call | Side effects are Node duplication, instance naming/transform assignment, recursive geometry mutation, playback mutation and SceneTree attachment. The service retains no batch, prototype, transform or diagnostic references. ## Data flow ```mermaid flowchart TD Slice[Ordered transform slice] --> Duplicate[Duplicate prototype with signals, groups and scripts] Duplicate --> Identity[Assign historical name and transform] Identity --> Render[Apply visibility margin and shadow recursively] Render --> NativeCopy[Copy native animator runtime fields] NativeCopy --> AttachInstance[Attach duplicate to detached batch] AttachInstance --> Players[Inventory AnimationPlayers] Players --> Start[Start deterministic native/imported playback] Start --> More{More slice entries?} More -->|yes| Duplicate More -->|no, non-empty| AttachBatch[Attach batch to supplied parent] Start --> Diagnostics[Tag detached states with instance index] ``` ## Lifecycle/state ```mermaid stateDiagram-v2 [*] --> Validating Validating --> Empty: invalid input Validating --> Building: valid slice Building --> Building: next successful duplicate Building --> Empty: no successful duplicates Building --> Attached: non-empty batch Empty --> [*] Attached --> [*]: parent/tile owner releases subtree ``` The `RefCounted` service is stateless; lifecycle labels describe each batch call. ## Main sequence ```mermaid sequenceDiagram participant L as StreamingWorldLoader participant M as M2AnimatedInstanceMaterializer participant F as M2AnimatedSceneFinalizer participant P as M2AnimationPlaybackController participant R as M2 parent root L->>M: materialize_batch(slice, prototype, render settings) loop ordered instance M->>M: duplicate, name, transform, render settings M->>P: copy native animator fields M->>F: animation_players_in_subtree(duplicate) M->>P: start_instance_playback(path, index, players, debug) end M->>R: add_child(non-empty batch) M-->>L: batch root and indexed diagnostics L->>L: format logs and assign Editor owner ``` ## Dependency diagram ```mermaid flowchart TB Loader[StreamingWorldLoader] --> Materializer[M2AnimatedInstanceMaterializer] Materializer --> Playback[M2AnimationPlaybackController] Materializer --> Finalizer[M2AnimatedSceneFinalizer] Materializer --> Engine[Node3D / GeometryInstance3D / SceneTree APIs] Loader --> EditorOwner[Editor ownership adapter] Loader --> BuildJob[Build cursor and budget] Materializer -. no dependency .-> Cache[Prototype/cache/load state] Materializer -. no dependency .-> MultiMesh[Static M2 materialization] ``` ## Ownership, threading and resources - Loader/build state borrows the accepted prototype and ordered transform Array. - Each successful duplicate becomes a child of the detached batch immediately. - A non-empty batch transfers to the supplied parent; the parent owns its lifetime. - A batch with no successful duplicates is queued for deletion and no reference returns. - Playback/finalizer services borrow each duplicate only during synchronous calls. - Returned diagnostic entries contain the stable instance index and a detached state. - All Node duplication, mutation and attachment is main-thread-only. ## Errors, cancellation and recovery | Failure | Detection | Behavior | Diagnostic | Recovery | |---|---|---|---|---| | Null parent/prototype | Entry guard | Return `{}` without mutation | Contract verifier | Correct composition/cache state | | Empty transforms or non-positive count | Entry guard | Return `{}` | Contract verifier | Planner supplies a non-empty slice | | Duplicate is not Node3D | Cast result | Skip that entry and preserve order of successes | Child-count contract | Repair prototype scene root | | Every duplicate fails | Empty batch check | Queue empty batch, return `{}` | Empty-result contract | Rebuild/import prototype | | Shutdown during/after call | Outside service | Call is synchronous; loader owns cancellation and subtree release | Shutdown regression | Loader cancels job/releases parent | Transform bounds remain a planner/build-job precondition exactly as before the extraction; this service does not clamp or silently reorder an invalid slice. ## Configuration and capabilities | Setting/capability | Default | Profile | Runtime mutable | Effect | |---|---|---|---|---| | Duplicate flags | Signals + groups + scripts | All | No | Preserves historical instance behavior | | Visibility end/margin | Loader renderer settings / chunk size | All | Yes, between calls | Applied to every GeometryInstance3D descendant | | Shadow mode | Loader `m2_cast_shadows` | All | Yes, between calls | Enables/disables descendant shadow casting | | Native diagnostics | Loader `debug_streaming` | Debug | Yes | Collects indexed state only when requested | ## Persistence, cache and migration No data is serialized and no cache/schema version changes. Editor owner assignment remains a loader concern after attachment, so generated-scene persistence behavior and migration remain unchanged. ## Diagnostics and observability - The materializer emits no logs, metrics or debug views. - Native state remains detached and is tagged with the absolute instance index. - Loader preserves the existing normalized-path `M2_NATIVE_ANIMATOR` log text. - No correlation ID is introduced; path/index remains the existing identity. ## Verification - `verify_m2_animated_instance_materializer.gd` covers invalid input, names, order, transforms, recursive visibility/margin/shadows, playback, native field references, indexed diagnostics, prototype immutability and ownership boundaries. - Playback/finalizer/pipeline/build/prototype/shutdown/material/facade/internal- access regressions protect adjacent behavior. - Fidelity evidence is exact extraction of existing flags/order/settings/calls; no original-client visual comparison or proprietary asset result is claimed. - Performance budget: 1,000 synthetic Node3D instances under one second. ## Extension points A later facade-owned animated presentation command may supply the same explicit batch contract. Async SceneTree mutation, callbacks and a generic scene factory are intentionally excluded until measurements show a need. ## Capability status | Capability | Status | Evidence | Gap/next step | |---|---|---|---| | Ordered animated instance/batch materialization | Implemented extraction | Synthetic batch contract verifier | Proprietary asset traversal pending | | Recursive visibility/shadow settings | Implemented extraction | Nested geometry fixture | Visual distance/shadow comparison pending | | Native/imported playback startup | Delegated to implemented controller | Playback + materializer verifiers | Full WoW animation-state mapping pending | | Editor persistence | Existing loader-owned | Source boundary and editor helper | Editor integration fixture remains separate | ## Known gaps and risks - The existing build planner must continue to guarantee valid transform bounds. - Default animation selection and native deformation retain their documented gaps. - No private asset, paired original-client, descriptor-pressure, leak or p95/p99 run exists. - Recursive render mutation remains proportional to duplicate subtree size. ## Source map | Path | Responsibility | |---|---| | `src/render/m2/m2_animated_instance_materializer.gd` | Duplicate, render mutation, playback startup and non-empty batch attachment | | `src/render/m2/m2_animation_playback_controller.gd` | Phase, selection and native/imported playback mutation | | `src/render/m2/m2_animated_scene_finalizer.gd` | AnimationPlayer inventory for each duplicate | | `src/scenes/streaming/streaming_world_loader.gd` | Build adapter, diagnostic formatting and Editor ownership | | `src/tools/verify_m2_animated_instance_materializer.gd` | Batch/render/playback/boundary/timing regression | ## Related decisions and references - [`m2-animation-playback-controller.md`](m2-animation-playback-controller.md) - [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md) - [`m2-build-batch-planner.md`](m2-build-batch-planner.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)