refactor(M03): extract M2 runtime rebuild classifier

This commit is contained in:
2026-07-17 10:01:29 +04:00
parent 324e64dd6e
commit 19ca8f48e8
9 changed files with 600 additions and 52 deletions
@@ -0,0 +1,221 @@
# 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)