# M2 Runtime Mesh Rebuild Classifier

## Metadata

| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001` |
| Owners | Billboard/UV-rotation rebuild decision and per-path memoization |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-runtime-rebuild-classifier`, 2026-07-17 |
| Profiles/capabilities | Existing M2 material-refresh path; profile-independent |

## Purpose

Determine whether a stale cached M2 mesh must be rebuilt in memory from raw M2
data to restore billboard custom attributes or UV-rotation material inputs. The
first decision is memoized by normalized relative path for the current map/
runtime-cache lifetime.

## Non-goals

- Read M2 files or invoke `M2Loader`/`M2Builder`.
- Own cached Meshes, scenes, materials, Nodes or RIDs.
- Select cache paths, perform ResourceLoader transitions or consume permits.
- Change refresh/cache format versions or shader behavior.
- Classify animation, particles, ribbons or general visual fidelity.

## Context and boundaries

```mermaid
flowchart LR
    Cache[Stale cached M2 Mesh] --> Loader[StreamingWorldLoader]
    Loader --> Raw[M2Loader raw Dictionary]
    Raw --> Classifier[M2RuntimeMeshRebuildClassifier]
    Classifier --> Decision[Memoized rebuild boolean]
    Decision --> Loader
    Loader -->|true| Builder[M2Builder rebuild]
    Loader -->|false| Refresh[Mark material refresh version]
```

Allowed dependencies are Dictionary/Array, `PackedInt32Array`, `Vector4` and
scalar math. Files, ResourceLoader, parser/builder classes, SceneTree,
WorkerThreadPool, Mesh/Node/RID ownership and other layers are forbidden.

## Public API

| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `needs_runtime_mesh_rebuild(normalized_relative_path, raw_m2_data)` | Memoized query | Detect billboard or referenced UV rotation requirements | Renderer main thread; cached until clear | Invalid variants/indices are skipped; first path decision wins |
| `clear()` | Command | Drop all memoized path decisions | Main thread; map reset/shutdown | Idempotent |
| `cached_path_count()` | Query | Return memoized path count | Main thread | None |
| `diagnostic_snapshot()` | Query | Return detached path/decision map | Main thread; caller-owned | None |

## Inputs and outputs

| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Normalized M2 relative path | Loader cache lookup | Classifier cache key | String value copied | Until clear |
| Input | Raw M2 batches/transforms/combos Dictionary | Native `M2Loader` adapter | Classifier | Borrowed; not mutated | First classification call per path |
| Output | Rebuild-required boolean | Classifier | Loader material-refresh adapter | Scalar | Memoized until clear |
| Output | Detached decision Dictionary | Classifier | Verifier/future diagnostics | Caller-owned copy | Snapshot lifetime |

Side effects are limited to inserting and clearing boolean memoization entries.

## Data flow

```mermaid
flowchart TD
    Query[path + raw M2 data] --> Cached{Path cached?}
    Cached -->|yes| Return[Return first decision]
    Cached -->|no| Billboard{Any billboard flag or positive vertex count?}
    Billboard -->|yes| StoreTrue[Store true]
    Billboard -->|no| Inputs{Transforms and combos present?}
    Inputs -->|no| StoreFalse[Store false]
    Inputs -->|yes| Batch[For each Dictionary batch]
    Batch --> Stage[Inspect at most four referenced stages]
    Stage --> Rotation{Non-identity rotation or speed above epsilon?}
    Rotation -->|yes| StoreTrue
    Rotation -->|no| StoreFalse
    StoreTrue --> Return
    StoreFalse --> Return
```

## Lifecycle/state

Each path moves from unknown to one immutable cached boolean. Clear returns all
paths to unknown; no resource lifecycle participates.

```mermaid
stateDiagram-v2
    [*] --> Unknown
    Unknown --> RebuildRequired: billboard or referenced UV rotation
    Unknown --> RebuildNotRequired: no relevant metadata
    RebuildRequired --> RebuildRequired: repeated query
    RebuildNotRequired --> RebuildNotRequired: repeated query
    RebuildRequired --> Unknown: clear
    RebuildNotRequired --> Unknown: clear
```

## Main sequence

```mermaid
sequenceDiagram
    participant Loader as StreamingWorldLoader
    participant Native as M2Loader
    participant Classifier as M2RuntimeMeshRebuildClassifier
    participant Builder as M2Builder
    Loader->>Native: load_m2(normalized path)
    Native-->>Loader: raw Dictionary
    Loader->>Classifier: needs_runtime_mesh_rebuild(path, data)
    Classifier-->>Loader: memoized boolean
    alt rebuild required
        Loader->>Builder: rebuild mesh from raw data
    else no rebuild required
        Loader->>Loader: stamp material refresh version
    end
```

## Dependency diagram

```mermaid
flowchart TB
    Loader[StreamingWorldLoader] --> Classifier[M2RuntimeMeshRebuildClassifier]
    Loader --> Native[M2Loader]
    Loader --> Builder[M2Builder]
    Loader --> Cache[M2 Mesh/Scene caches]
    Classifier -. no dependency .-> Native
    Classifier -. no dependency .-> Builder
    Classifier -. no dependency .-> Cache
```

## Ownership, threading and resources

- The classifier owns only normalized-path boolean entries.
- The loader owns raw loading, cache/resource selection, Mesh replacement and
  material refresh metadata.
- Raw Dictionaries are borrowed and never retained; only the computed boolean is cached.
- Current calls and clears are serialized on the renderer main thread.
- No Node, Resource, Mesh, material or RID reference crosses into the classifier.

## Errors, cancellation and recovery

| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Non-Dictionary batch/transform | Type guard | Skip entry or use identity defaults | Synthetic verifier | Correct parser data on next cleared lifetime |
| Missing transforms/combos | Empty guard | No UV-driven rebuild | Synthetic verifier | Billboard classification still applies |
| Invalid combo/transform index | Bounds guard | Skip stage | Synthetic verifier | Correct raw data and clear |
| Repeated path with changed data | Cache hit | Preserve first decision | Memoization fixture | Map/reset clear recomputes |
| Load/build failure | Outside classifier | Existing mesh fallback remains | Loader diagnostics | Existing reload/fallback path |

## Configuration and capabilities

| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Maximum texture stages inspected | `4` | Existing M2 material path | No | Preserves historical four-stage shader boundary |
| UV rotation-speed epsilon | `0.000001` exclusive | Existing M2 material path | No | Larger absolute speed requires rebuild |
| `M2_MATERIAL_REFRESH_VERSION` | Loader constant `2` | All | Code version | Gates whether classifier is called |

## Persistence, cache and migration

Memoized booleans are runtime-only and are cleared at the two historical loader
reset/shutdown sites. No persisted cache version or rebuild instruction changes.

## Diagnostics and observability

- Logs: none.
- Metrics: `cached_path_count` is contract-level only and not added to runtime logs.
- Debug views: detached path-to-boolean snapshot.
- Correlation IDs: normalized M2 relative path.

## Verification

- `verify_m2_runtime_mesh_rebuild_classifier.gd`: billboard flag/count, invalid
  variants, identity/non-identity rotation, positive/negative speed, exact
  epsilon, four-stage cap, invalid indices, memoization, clear, source boundary
  and 100-by-256 timing.
- Existing M2 builder/material verifiers cover downstream shader/cache behavior.
- Fidelity evidence is exact predicate/cache extraction. No new asset-backed or
  original-client parity claim is made.
- Performance budget: 25,600 classifications and clears under one second.

## Extension points

- A typed raw M2 material descriptor may replace the Dictionary only with parser
  fixtures and a compatibility adapter.
- Mesh request/cache/finalization state can be extracted separately without
  changing this rebuild predicate.

## Capability status

| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Billboard rebuild classification | Implemented extraction | Flag/count fixtures | Asset-backed cache refresh timing pending |
| UV-rotation rebuild classification | Implemented extraction | Combo/stage/threshold fixtures | More build-12340 material fixtures pending |
| Runtime memoization lifetime | Implemented extraction | First-decision/clear/source fixtures | Byte/count runtime metric not exposed |
| M2 mesh loading/finalization | Existing loader-owned | Material/shutdown regressions | Separate state extraction pending |

## Known gaps and risks

- Raw parser data remains Dictionary-based.
- The first decision wins even if different raw data is passed for the same path;
  this intentionally preserves prior cache behavior.
- Only four texture stages are inspected, matching current material support.
- No private asset traversal, descriptor-pressure, p95/p99 or paired visual run is included.

## Source map

| Path | Responsibility |
|---|---|
| `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Predicate, thresholds and boolean memoization |
| `src/scenes/streaming/streaming_world_loader.gd` | Raw loading, Mesh rebuild/adoption, refresh version and clear calls |
| `addons/mpq_extractor/loaders/m2_builder.gd` | Mesh/material rebuild implementation |
| `src/tools/verify_m2_runtime_mesh_rebuild_classifier.gd` | Synthetic predicate/cache/boundary/timing regression |

## Related decisions and references

- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
- [`m2-placement-grouper.md`](m2-placement-grouper.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)
