# M2 Prototype Cache State

## Metadata

| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-PROTOTYPE-CACHE-001` |
| Owners | Detached static/animated prototypes and missing/static-animation outcomes |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-prototype-cache`, 2026-07-17 |
| Profiles/capabilities | Static, GLB animated and native animated M2 prototype reuse |

## Purpose

Own the four shutdown-lifetime M2 prototype lookup states previously stored in
`StreamingWorldLoader`: static and animated `Node3D` references, missing model
paths and paths whose animation fallback is static-only.

## Non-goals

- Normalize paths, choose cache files, call ResourceLoader or parse raw M2 data.
- Build, repair or instantiate prototypes, Meshes, materials or MultiMeshes.
- Own animation/Mesh request queues, permits or tile/build state.
- Add eviction, persistence, retries, profile rules or format changes.

## Context and boundaries

```mermaid
flowchart LR
    Loader[StreamingWorldLoader] --> State[M2PrototypeCacheState]
    Observer[M2 resource observers] --> State
    Resource[ResourceLoader / raw builders] --> Loader
    Resource --> Observer
    State --> Static[Static prototype Node3D]
    State --> Animated[Animated prototype Node3D]
    State --> Missing[Missing-model path set]
    State --> StaticOnly[Static-animation path set]
    Loader --> Instances[Duplicate/animated instance materialization]
```

The state may depend on `Node3D` lifetime APIs and value-only path containers.
Filesystem/native/ResourceLoader APIs, builders, worker pools, render scheduling,
Mesh traversal and other application layers are forbidden.

## Public API

| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `has_static_prototype(path)` | Query | Test for one valid retained static prototype | Renderer main thread; shutdown lifetime | Empty/invalid returns false |
| `find_static_prototype(path)` | Query | Return exact retained static Node reference | Renderer main thread; borrowed | Empty/unknown/invalid returns null |
| `adopt_static_prototype(path, prototype)` | Command/query | Retain first valid static prototype and return canonical reference | Renderer main thread; until shutdown | Invalid input returns null; later candidate released |
| `has_animated_prototype(path)` | Query | Test for one valid retained animated prototype | Renderer main thread; shutdown lifetime | Empty/invalid returns false |
| `find_animated_prototype(path)` | Query | Return exact retained animated Node reference | Renderer main thread; borrowed | Empty/unknown/invalid returns null |
| `adopt_animated_prototype(path, prototype)` | Command/query | Retain first valid animated prototype and return canonical reference | Renderer main thread; until shutdown | Invalid input returns null; later candidate released |
| `mark_model_missing(path)` / `is_model_missing(path)` | Command/query | Store/query a missing-model outcome | Renderer main thread; until shutdown | Empty mark rejected |
| `mark_animation_static(path)` / `is_animation_static(path)` | Command/query | Store/query static-only animation outcome | Renderer main thread; until shutdown | Empty mark rejected |
| `diagnostic_snapshot()` | Diagnostic query | Return detached sorted path arrays for all four states | Caller-owned result | Never exposes Nodes |
| `clear_and_release()` | Lifecycle command | Release prototypes and clear all state | Final shutdown after async drain | Idempotent |

## Inputs and outputs

| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Normalized non-empty M2 path | Loader normalization | Cache state | Copied String key | Shutdown lifetime |
| Input | Detached static Node3D | Cached scene/raw builder adapter | Cache state | Adopted on success | Until final shutdown |
| Input | Detached animated Node3D | GLB/native animation adapter | Cache state | Adopted on success | Until final shutdown |
| Input | Missing/static-only outcome | Resource observers/finalizers | Cache state | Boolean set entry | Until final shutdown |
| Output | Canonical prototype Node3D | Cache state | Loader instance/material adapter | Borrowed exact reference | One lookup/use |
| Output | Detached path-only snapshot | Cache state | Tests/diagnostics | Fresh caller-owned arrays | One query |

Side effects are strong Node retention, release of rejected duplicate candidates,
final Node destruction/queueing and mutation of runtime-only path state.

## Data flow

```mermaid
flowchart TD
    Request[Normalized prototype request] --> Positive{Valid prototype cached?}
    Positive -->|yes| Return[Return exact prototype]
    Positive -->|no| Negative{Missing or static-only outcome?}
    Negative -->|yes| Fallback[Return caller fallback]
    Negative -->|no| Load[Loader performs existing I/O/build]
    Load --> Success{Valid prototype?}
    Success -->|yes| Adopt[Adopt first candidate]
    Adopt --> Return
    Success -->|no| Mark[Mark relevant negative outcome]
    Mark --> Fallback
```

## Lifecycle/state

```mermaid
stateDiagram-v2
    [*] --> Empty
    Empty --> Positive: adopt static/animated prototype
    Empty --> Negative: mark missing/static-only path
    Positive --> Positive: lookup or same-reference adoption
    Positive --> Positive: duplicate candidate released; first wins
    Negative --> Negative: repeated mark/query
    Positive --> Released: final shutdown
    Negative --> Released: final shutdown
    Released --> Released: repeated clear
    Released --> [*]
```

Map refresh does not transition or clear this service. Final scene shutdown calls
`clear_and_release` only after worker and ResourceLoader work is drained.

## Main sequence

```mermaid
sequenceDiagram
    participant Loader as StreamingWorldLoader
    participant State as M2PrototypeCacheState
    participant Source as ResourceLoader/raw builder
    Loader->>State: find static/animated(path)
    alt positive hit
        State-->>Loader: exact retained Node3D
    else negative hit
        Loader->>State: is missing/static-only(path)
        State-->>Loader: true; use historical fallback
    else uncached
        Loader->>Source: existing load/build operation
        alt valid prototype
            Loader->>State: adopt(path, prototype)
            State-->>Loader: canonical Node3D
        else failure
            Loader->>State: mark missing/static-only(path)
        end
    end
    Loader->>State: shutdown clear_and_release()
    State-->>Loader: all paths empty; Nodes released
```

## Dependency diagram

```mermaid
flowchart TB
    Loader[StreamingWorldLoader] --> State[M2PrototypeCacheState]
    Observer[M2 resource observers] --> State
    State --> Node3D
    Loader --> Raw[M2RawModelRepository]
    Observer --> ResourceLoader
    Loader --> StaticBuilder[M2Builder]
    Observer --> AnimatedBuilder[M2NativeAnimatedBuilder]
    State -. no dependency .-> Raw
    State -. no dependency .-> ResourceLoader
    State -. no dependency .-> StaticBuilder
    State -. no dependency .-> MeshCache[M2MeshResourceCacheState]
```

## Ownership, threading and resources

- Renderer main-thread adapters serialize all operations.
- The state owns strong references to adopted static and animated prototype Nodes.
- A duplicate detached candidate is released immediately; the first valid Node
  stays canonical for that path.
- Lookups borrow exact Node references without transferring ownership.
- Detached Nodes are synchronously `free()`d at shutdown. Defensive in-tree
  Nodes use `queue_free()` to preserve SceneTree rules.
- Negative entries own only copied normalized-path Strings.

## Errors, cancellation and recovery

| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty path/null/invalid Node | Admission guard | Reject/null/false | Dedicated verifier | Correct caller input |
| Unknown positive lookup | Map miss | Return null | Path snapshot | Loader continues existing load/fallback |
| Duplicate candidate | Occupied valid path | Release candidate; return first | Identity fixture | None required |
| Model source/load failure | Loader result | Mark missing outcome | Existing fallback behavior | New loader session/source repair |
| Animation unavailable/unsafe | Observer/native result | Mark static-only outcome | Existing fallback behavior | New loader session/cache repair |
| Shutdown | Loader lifecycle | Release positive and clear all state | Shutdown verifier | New loader begins empty |
| Cancellation | Not owned | No state transition inside this service | N/A | Loader drains/cancels before shutdown clear |

## Configuration and capabilities

| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Positive cache lifetime | Final loader shutdown | All | No | Reuses detached prototypes across map refresh |
| Negative cache lifetime | Final loader shutdown | All | No | Preserves historical fallback suppression |
| Eviction capacity | Unbounded historical behavior | All | No | No mid-session Node destruction |

Animation enablement, candidate/allow/deny rules and cache paths remain loader
configuration consumed by the cached observer. Per-frame permits remain loader-owned.

## Persistence, cache and migration

This is runtime-only state and serializes nothing. `.tscn/.glb`, material refresh
and native M2 formats are unchanged; no migration or rebake is introduced.

## Diagnostics and observability

- `diagnostic_snapshot` exposes four sorted path arrays without Node references.
- Cached eligibility rejection logging belongs to the cached observer; native
  success logging belongs to the native observer and cached terminal success
  logging belongs to the animation resource finalizer. Static terminal missing
  outcomes are produced by `M2MeshResourceFinalizer`.
- Existing renderer queue metrics remain unchanged because these tables never
  contributed work counts.
- Normalized relative path remains the correlation key.

## Verification

- `verify_m2_prototype_cache_state.gd`: invalid admission, exact static/animated
  identity, independent positive/negative states, first-wins duplicate release,
  detached/in-tree shutdown ownership, idempotent clear, detached diagnostics,
  loader source boundaries and 100-by-256 bounded path-state timing.
- Runtime-cache shutdown regression verifies final release through the real loader.
- Raw repository/finalizer/extractor/Mesh cache/pipeline/material/animation/M2
  build regressions protect adjacent behavior.
- Fidelity evidence is exact cache transition and lifetime extraction. No new
  proprietary asset or original-client visual comparison is claimed.

## Extension points

Eviction or byte/count budgets require measured memory evidence and explicit
prototype-user lifetime rules. Cached and native animation observers consume
this state without moving ResourceLoader or builder ownership into cache state.

## Capability status

| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Static prototype retention | Implemented extraction | Identity/source/shutdown verifier | Asset-backed traversal/leak run pending |
| Animated prototype retention | Implemented extraction | Independent identity/shutdown verifier | Legal animated success fixture pending |
| Missing/static-only outcomes | Implemented extraction | Independent negative-state verifier | Recovery still requires new loader session |
| Bounded eviction | Historical unbounded behavior | Documented lifetime | Add only after memory profiling |

## Known gaps and risks

- Strong Node references intentionally live until final shutdown.
- Negative outcomes do not retain reason codes and cannot be individually retried,
  preserving historical behavior.
- No proprietary long traversal, descriptor pressure, leak, p95/p99 or paired
  original-client evidence is included.

## Source map

| Path | Responsibility |
|---|---|
| `src/render/m2/m2_prototype_cache_state.gd` | Positive Node ownership, negative path state and shutdown release |
| `src/render/m2/m2_native_animation_resource_observer.gd` | Animated raw/build and positive/static-only transitions |
| `src/render/m2/m2_animation_resource_finalizer.gd` | Cached terminal positive/static-only transitions |
| `src/render/m2/m2_mesh_resource_finalizer.gd` | Static terminal missing transitions |
| `src/scenes/streaming/streaming_world_loader.gd` | Normalization, observer order, remaining I/O/fallback adapters |
| `src/tools/verify_m2_prototype_cache_state.gd` | Admission/identity/lifecycle/source/timing regression |
| `src/tools/verify_render_runtime_cache_shutdown.gd` | Integrated final-shutdown release regression |

## Related decisions and references

- [`m2-raw-model-repository.md`](m2-raw-model-repository.md)
- [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md)
- [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md)
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-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)
