# M2 Animated Scene Finalizer

## Metadata

| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-ANIMATED-SCENE-FINALIZER-001` |
| Owners | Detached animated candidate, material repair and player validation |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animated-scene-finalizer`, 2026-07-18 |
| Profiles/capabilities | Existing optional cached-GLB animated M2 path |

## Purpose

Finalize one loaded animated M2 scene on the main thread: instantiate a valid
detached Node3D candidate, copy the historical static-prototype material mapping
and accept only candidates containing AnimationPlayer descendants.

## Non-goals

- Call ResourceLoader or interpret threaded-load statuses.
- Choose animation eligibility, cache paths, permits or material prototypes.
- Adopt prototypes, mark static fallback or select/play animations.
- Process native M2 animators or static Mesh rebuilds.

## Context and boundaries

```mermaid
flowchart LR
    ResourceFinalizer[M2AnimationResourceFinalizer] -->|loaded Resource and material source| Finalizer[M2AnimatedSceneFinalizer]
    Finalizer -->|accepted Node3D and player count| ResourceFinalizer
    ResourceFinalizer --> Cache[M2PrototypeCacheState]
```

Allowed dependencies are Godot scene/resource/material types. ResourceLoader,
filesystem, workers, scheduler, builders, raw repositories, prototype cache and
other application layers are forbidden.

## Public API

| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `instantiate_candidate(resource)` | Ownership query | Instantiate PackedScene as detached Node3D | Main thread; caller receives valid candidate | Unsupported/invalid null; created wrong-type root freed |
| `repair_materials(animated_root, material_source_root)` | Command | Apply historical material overrides | Main thread; borrowed roots | Null/empty roots no-op |
| `finalize_candidate(candidate_root)` | Ownership command/query | Require AnimationPlayer and transfer root/count | Main thread; detached candidate | Null empty; rejected candidate freed |
| `mesh_instances_in_subtree(root)` | Query | Depth-first MeshInstance3D inventory | Main thread; borrowed subtree | Null returns empty |
| `animation_players_in_subtree(root)` | Query | Depth-first AnimationPlayer inventory | Main thread; borrowed subtree | Null returns empty |

## Inputs and outputs

| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Loaded Resource | Animation resource finalizer | Candidate instantiator | Borrowed Resource | One finalize permit |
| Input | Static material prototype Node3D | Loader cache/build adapter | Material repair | Borrowed Node | One repair call |
| Input | Detached animated candidate | Instantiator | Repair/final validation | Finalizer then caller/release | One attempt |
| Output | Accepted prototype and player count | Finalizer | Resource-finalizer adoption/log adapter | Exact Node transferred | Shutdown cache lifetime |
| Output | Depth-first engine-node arrays | Traversal | Loader preparation/playback | Borrowed references | One call |

Side effects are PackedScene instantiation, surface override assignment and
synchronous destruction of rejected detached roots.

## Data flow

```mermaid
flowchart TD
    Resource[Loaded Resource] --> Packed{PackedScene?}
    Packed -->|no| Reject[Return null]
    Packed -->|yes| Instantiate[Instantiate]
    Instantiate --> Root{Node3D?}
    Root -->|no| FreeWrong[Free root and reject]
    Root -->|yes| Repair[Repair materials]
    Repair --> Players{AnimationPlayer exists?}
    Players -->|no| FreeCandidate[Free and reject]
    Players -->|yes| Transfer[Transfer exact root and count]
```

## Lifecycle/state

```mermaid
stateDiagram-v2
    [*] --> Absent
    Absent --> Candidate: instantiate valid scene
    Candidate --> Candidate: repair materials
    Candidate --> Accepted: player found
    Candidate --> Released: no player
    Accepted --> [*]: ownership transferred
    Released --> [*]
```

## Main sequence

```mermaid
sequenceDiagram
    participant R as M2AnimationResourceFinalizer
    participant F as M2AnimatedSceneFinalizer
    participant C as M2PrototypeCacheState
    R->>F: instantiate_candidate(Resource)
    F-->>R: detached Node3D or null
    R->>F: repair_materials(candidate, source)
    R->>F: finalize_candidate(candidate)
    alt accepted
        F-->>R: exact Node3D and player count
        R->>C: adopt animated prototype
    else rejected
        F-->>R: empty; candidate freed
        R->>C: mark animation static
    end
```

## Dependency diagram

```mermaid
flowchart TB
    ResourceFinalizer[M2AnimationResourceFinalizer] --> Finalizer[M2AnimatedSceneFinalizer]
    Finalizer --> Engine[PackedScene / Node3D / Mesh / Material / AnimationPlayer]
    ResourceFinalizer --> Resource[ResourceLoader]
    Loader --> Budget[RenderBudgetScheduler]
    ResourceFinalizer --> Prototype[M2PrototypeCacheState]
    Finalizer -. no dependency .-> Resource
    Finalizer -. no dependency .-> Budget
    Finalizer -. no dependency .-> Prototype
```

## Ownership, threading and resources

- Every method runs synchronously on the renderer main thread.
- A valid candidate is detached and finalizer-owned until acceptance.
- Acceptance transfers the exact Node3D to the resource-finalizer/cache path.
- Rejection frees the candidate synchronously, including wrong-type roots.
- Traversal results borrow Nodes; source materials remain Resource-owned.

## Errors, cancellation and recovery

| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Null/unsupported Resource | Type guard | Return null | Dedicated verifier | Resource finalizer marks static |
| Wrong root type | Instantiated type guard | Free and return null | Node-count regression | Rebuild cache |
| No material source/meshes | Null/empty traversal | Leave materials unchanged | Material fixture | Imported materials remain |
| Missing source surface | Material lookup | First source material fallback | Mapping fixture | Repair static source |
| No AnimationPlayer | Descendant inventory | Free and return empty | Lifetime regression | Resource finalizer marks static |
| Shutdown/cancellation | Not owned | No retained state | N/A | Loader drains first |

## Configuration and capabilities

| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Finalize permit | `m2_animation_finalize_ops_per_tick = 1` | Existing profiles | Yes | Caller bounds calls |
| Material mapping | Depth-first/index-clamped | All | No | Preserves current appearance |
| Required players | At least one | All | No | Rejects non-animated scenes |

## Persistence, cache and migration

The service retains no state and serializes nothing. GLB/cache formats, material
versions and prototype lifetimes are unchanged; no rebake is required.

## Diagnostics and observability

- Accepted result reports player count for the existing `M2_ANIM_CACHE` log.
- Traversal order is deterministic depth-first preorder.
- The service owns no logging or metrics.

## Verification

- `verify_m2_animated_scene_finalizer.gd` covers invalid Resources, wrong-root
  release, exact transfer, player traversal/count, override priority, fallback,
  source-index mapping, source boundaries and timing.
- Adjacent animation pipeline/prototype/shutdown/material/build tests protect callers.
- Fidelity evidence is exact mapping/lifecycle extraction; no asset-backed or
  original-client visual parity claim is made.
- Performance budget: 10,000 depth-eight player inventories under one second.

## Extension points

Animation playback policy and native animator field copying now belong to
`M2AnimationPlaybackController`; accepted instance/batch materialization is owned
by `M2AnimatedInstanceMaterializer`.

## Capability status

| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| PackedScene candidate | Implemented extraction | Type/lifetime fixture | Proprietary corrupt corpus pending |
| Material repair | Implemented extraction | Exact-reference mapping | Asset-backed comparison pending |
| Player validation | Implemented extraction | Nested traversal/transfer | Multi-player GLB corpus pending |

## Known gaps and risks

- Material matching remains positional rather than semantic.
- Accepted prototypes remain unbounded until final loader shutdown.
- No private traversal, descriptor-pressure, p95/p99 or paired-client run exists.

## Source map

| Path | Responsibility |
|---|---|
| `src/render/m2/m2_animated_scene_finalizer.gd` | Candidate ownership, traversal, material repair and validation |
| `src/render/m2/m2_animation_playback_controller.gd` | Accepted-instance playback and native animator startup |
| `src/render/m2/m2_animated_instance_materializer.gd` | Per-duplicate player inventory consumer and batch owner |
| `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending/terminal records before finalization |
| `src/render/m2/m2_prototype_cache_state.gd` | Accepted prototype/static-only outcomes |
| `src/render/m2/m2_animation_resource_finalizer.gd` | I/O, candidate composition, adoption and logs |
| `src/scenes/streaming/streaming_world_loader.gd` | Permits and material-source lookup |
| `src/tools/verify_m2_animated_scene_finalizer.gd` | Scene/material/lifetime/boundary/timing regression |

## Related decisions and references

- [`m2-animation-playback-controller.md`](m2-animation-playback-controller.md)
- [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md)
- [`m2-prototype-cache-state.md`](m2-prototype-cache-state.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)
