render: extract native M2 animation resource observer

This commit is contained in:
2026-07-18 10:55:47 +04:00
parent d22a9cd743
commit 1cb0101a73
18 changed files with 932 additions and 104 deletions
+23 -7
View File
@@ -329,7 +329,7 @@ Native M2 animation first pass for composite doodads:
- `M2NativeAnimator` evaluates the selected Stand sequence and applies WoW-style bone matrices: - `M2NativeAnimator` evaluates the selected Stand sequence and applies WoW-style bone matrices:
`T(pivot + translation) * R * S * T(-pivot) * parent`. `T(pivot + translation) * R * S * T(-pivot) * parent`.
- Vertex influences are resolved through `.skin` local bone indices and the M2 `boneCombos` palette, matching the original section/batch renderer model used by WoW/whoa. - Vertex influences are resolved through `.skin` local bone indices and the M2 `boneCombos` palette, matching the original section/batch renderer model used by WoW/whoa.
- `StreamingWorldLoader` routes only `gryphonroost` through this native path; simple critters still use the existing GLB allowlist and most M2 world props stay on static MultiMesh. - `M2NativeAnimationResourceObserver` routes only `gryphonroost` through this native path; simple critters still use the existing GLB allowlist and most M2 world props stay on static MultiMesh.
- Current implementation rebuilds the animated `ArrayMesh` on CPU. This is correct enough for the problematic rare doodads and gives us the same data layout that can later move to shader/GPU skinning. - Current implementation rebuilds the animated `ArrayMesh` on CPU. This is correct enough for the problematic rare doodads and gives us the same data layout that can later move to shader/GPU skinning.
Причина: композитные doodad вроде `GryphonRoost` снова ломают визуал при GLB-анимации. До M2-native renderer все world placement M2 должны оставаться статическими. GLB-анимация оставлена только как вручную включаемый debug experiment через allowlist. Причина: композитные doodad вроде `GryphonRoost` снова ломают визуал при GLB-анимации. До M2-native renderer все world placement M2 должны оставаться статическими. GLB-анимация оставлена только как вручную включаемый debug experiment через allowlist.
@@ -1148,12 +1148,28 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
primitive/schema safety validation, request admission and static-only fallback. primitive/schema safety validation, request admission and static-only fallback.
- The observer produces `M2BuildResourceSnapshot` with the exact borrowed cached - The observer produces `M2BuildResourceSnapshot` with the exact borrowed cached
prototype or pending state and retains/frees no engine object. prototype or pending state and retains/frees no engine object.
- Native GryphonRoost raw-data build/debug logging and all terminal polling, - Native GryphonRoost raw-data build/debug logging belongs to the sibling native
finalization, permits and SceneTree mutation remain loader-owned. observer; terminal polling, finalization, permits and SceneTree mutation remain
loader-owned.
- Defaults, path order, accepted empty/`pivot_prefix_v1` schemas, cache format, - Defaults, path order, accepted empty/`pivot_prefix_v1` schemas, cache format,
metrics and visible behavior are unchanged. Generated GLB metadata fixtures metrics and visible behavior are unchanged. Generated GLB metadata fixtures
are not private asset, leak, p95/p99 or original-client animation evidence. are not private asset, leak, p95/p99 or original-client animation evidence.
## 2026-07-18 M2 Native Animation Resource Observer Extraction
- `M2NativeAnimationResourceObserver` now owns the exact case-insensitive
`gryphonroost` candidate rule, animated prototype/static-only cache checks,
synchronous raw animated read, native builder call, adoption and success log.
- The native attempt remains first. Empty raw data/surfaces and null/childless
builds retain the same static-only fallback; accepted Nodes remain owned by
`M2PrototypeCacheState` until final shutdown.
- `StreamingWorldLoader` retains observer order, typed snapshot construction,
cached-GLB fallback, terminal ResourceLoader polling/finalize, permits,
materialization and SceneTree lifetime.
- The historical unfreed childless builder result is deliberately preserved and
documented as a leak risk. No parser, cache, profile or visible rule changed;
synthetic fixtures are not asset-backed animation or performance evidence.
## 2026-07-18 M2 Animated Scene Finalizer Extraction ## 2026-07-18 M2 Animated Scene Finalizer Extraction
- `M2AnimatedSceneFinalizer` now owns terminal animated PackedScene candidate - `M2AnimatedSceneFinalizer` now owns terminal animated PackedScene candidate
@@ -1228,10 +1244,10 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- `M2RawModelRepository` now owns the repeated extracted-file and optional - `M2RawModelRepository` now owns the repeated extracted-file and optional
native `M2Loader` boundary for static `load_m2` and animated native `M2Loader` boundary for static `load_m2` and animated
`load_m2_animated` raw Dictionaries. `load_m2_animated` raw Dictionaries.
- `StreamingWorldLoader` delegates the stale-Mesh refresh, static prototype and - `StreamingWorldLoader` delegates stale-Mesh refresh and static reads directly;
native animated prototype reads. It retains normalization, `.tscn/.glb` `M2NativeAnimationResourceObserver` delegates native animated reads. The loader
fallback order, builders, permits and Node/Mesh use; prototype/negative state retains normalization, `.tscn/.glb` fallback order, permits and Node/Mesh use;
is now isolated in `M2PrototypeCacheState`. prototype/negative state is isolated in `M2PrototypeCacheState`.
- The repository retains no path, native object or parsed data. Empty paths, - The repository retains no path, native object or parsed data. Empty paths,
absent files/classes/methods and invalid results produce the same empty-value absent files/classes/methods and invalid results produce the same empty-value
fallback contract; path join/globalization and native method names are exact. fallback contract; path join/globalization and native method names are exact.
+1
View File
@@ -25,6 +25,7 @@
| M2 build resource snapshot | Implemented extraction | [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md) | | M2 build resource snapshot | Implemented extraction | [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md) |
| M2 static build resource observer | Implemented extraction | [`m2-static-build-resource-observer.md`](m2-static-build-resource-observer.md) | | M2 static build resource observer | Implemented extraction | [`m2-static-build-resource-observer.md`](m2-static-build-resource-observer.md) |
| M2 cached animation resource observer | Implemented extraction | [`m2-cached-animation-resource-observer.md`](m2-cached-animation-resource-observer.md) | | M2 cached animation resource observer | Implemented extraction | [`m2-cached-animation-resource-observer.md`](m2-cached-animation-resource-observer.md) |
| M2 native animation resource observer | Implemented extraction | [`m2-native-animation-resource-observer.md`](m2-native-animation-resource-observer.md) |
| M2 build queue | Implemented extraction | [`m2-build-queue.md`](m2-build-queue.md) | | M2 build queue | Implemented extraction | [`m2-build-queue.md`](m2-build-queue.md) |
| M2 static batch materializer | Implemented extraction | [`m2-static-batch-materializer.md`](m2-static-batch-materializer.md) | | M2 static batch materializer | Implemented extraction | [`m2-static-batch-materializer.md`](m2-static-batch-materializer.md) |
| M2 runtime mesh rebuild classifier | Implemented extraction | [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md) | | M2 runtime mesh rebuild classifier | Implemented extraction | [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md) |
+8 -7
View File
@@ -148,7 +148,7 @@ flowchart TB
- `M2BuildResourceSnapshot` owns the typed call-local observation contract; - `M2BuildResourceSnapshot` owns the typed call-local observation contract;
dispatch reads availability/pending/missing accessors but no engine references. dispatch reads availability/pending/missing accessors but no engine references.
- Loader owns native-first observation order, action execution and permit use; - Loader owns native-first observation order, action execution and permit use;
static/cached-animation observers own their lookup/request phases. native/static/cached-animation observers own their resource phases.
- `M2BuildQueue` owns pending jobs, FIFO keys and progress cursors. - `M2BuildQueue` owns pending jobs, FIFO keys and progress cursors.
- Materializers own main-thread scene construction under loader-owned roots. - Materializers own main-thread scene construction under loader-owned roots.
- Pure calls are thread-safe; the current loader adapter calls on main thread. - Pure calls are thread-safe; the current loader adapter calls on main thread.
@@ -192,9 +192,8 @@ remain unchanged.
## Extension points ## Extension points
Static and cached animated observation/request execution now live behind sibling Static, cached animated and native animated observation now live behind sibling
services that produce `M2BuildResourceSnapshot`. Native animation remains an services that produce `M2BuildResourceSnapshot`. Generic callbacks, signals and a shared state-machine
orchestrator-owned phase. Generic callbacks, signals and a shared state-machine
framework remain intentionally excluded. framework remain intentionally excluded.
## Capability status ## Capability status
@@ -202,13 +201,14 @@ framework remain intentionally excluded.
| Capability | Status | Evidence | Gap/next step | | Capability | Status | Evidence | Gap/next step |
|---|---|---|---| |---|---|---|---|
| M2 build action selection | Implemented extraction | Priority/matrix/source/timing verifier | Asset-backed traversal pending | | M2 build action selection | Implemented extraction | Priority/matrix/source/timing verifier | Asset-backed traversal pending |
| Static/cached animation observation and requests | Implemented separately | Observer/cache/pipeline regressions | Native observation remains loader-owned | | Static/cached/native animation observation | Implemented separately | Observer/cache/pipeline regressions | Asset-backed traversal pending |
| Queue/cursor state | Implemented separately | M2 build queue verifier | Asset-backed traversal pending | | Queue/cursor state | Implemented separately | M2 build queue verifier | Asset-backed traversal pending |
| Materialization | Implemented separately | Static/animated materializer verifiers | GPU/p95/p99 evidence pending | | Materialization | Implemented separately | Static/animated materializer verifiers | GPU/p95/p99 evidence pending |
## Known gaps and risks ## Known gaps and risks
- Loader still performs synchronous/native prototype attempts and static request I/O. - Native prototype attempts remain synchronous through the observer; loader still
owns static request I/O and terminal animated ResourceLoader polling.
- Synthetic timing does not measure ResourceLoader, Node or GPU work. - Synthetic timing does not measure ResourceLoader, Node or GPU work.
- Private traversal, leak, visual and p95/p99 evidence remains unavailable. - Private traversal, leak, visual and p95/p99 evidence remains unavailable.
@@ -219,7 +219,8 @@ framework remain intentionally excluded.
| `src/render/m2/m2_build_dispatch_planner.gd` | Pure action priority and transition plan | | `src/render/m2/m2_build_dispatch_planner.gd` | Pure action priority and transition plan |
| `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step resource observation contract | | `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step resource observation contract |
| `src/render/m2/m2_cached_animation_resource_observer.gd` | Cached animation observation/request phase | | `src/render/m2/m2_cached_animation_resource_observer.gd` | Cached animation observation/request phase |
| `src/scenes/streaming/streaming_world_loader.gd` | Native observation order, action execution, permits and engine lifetime | | `src/render/m2/m2_native_animation_resource_observer.gd` | Native candidate/read/build/cache observation |
| `src/scenes/streaming/streaming_world_loader.gd` | Observation order, action execution, permits and engine lifetime |
| `src/render/m2/m2_build_batch_planner.gd` | Batch count and cursor plan | | `src/render/m2/m2_build_batch_planner.gd` | Batch count and cursor plan |
| `src/render/m2/m2_build_queue.gd` | Pending jobs, FIFO and cursor ownership | | `src/render/m2/m2_build_queue.gd` | Pending jobs, FIFO and cursor ownership |
| `src/tools/verify_m2_build_dispatch_planner.gd` | Matrix, boundary and timing regression | | `src/tools/verify_m2_build_dispatch_planner.gd` | Matrix, boundary and timing regression |
+10 -9
View File
@@ -28,7 +28,7 @@ static Mesh and terminal missing-model state. The snapshot never owns engine lif
```mermaid ```mermaid
flowchart LR flowchart LR
Native[StreamingWorldLoader native observation] --> Snapshot[M2BuildResourceSnapshot] Native[M2NativeAnimationResourceObserver] --> Snapshot[M2BuildResourceSnapshot]
Cached[M2CachedAnimationResourceObserver] --> Snapshot Cached[M2CachedAnimationResourceObserver] --> Snapshot
Static[M2StaticBuildResourceObserver] --> Snapshot Static[M2StaticBuildResourceObserver] --> Snapshot
Snapshot --> Dispatch[M2BuildDispatchPlanner] Snapshot --> Dispatch[M2BuildDispatchPlanner]
@@ -112,7 +112,7 @@ sequenceDiagram
participant B as M2BuildBatchPlanner participant B as M2BuildBatchPlanner
participant D as M2BuildDispatchPlanner participant D as M2BuildDispatchPlanner
participant M as M2 materializer participant M as M2 materializer
L->>L: resolve native animated prototype L->>L: delegate native animated prototype observation
L->>S: construct native result L->>S: construct native result
opt no native prototype opt no native prototype
L->>L: delegate cached animation observer L->>L: delegate cached animation observer
@@ -149,8 +149,8 @@ flowchart TB
## Ownership, threading and resources ## Ownership, threading and resources
- Snapshot owns only copied scalar state and temporary references. - Snapshot owns only copied scalar state and temporary references.
- Prototype/cache services retain resource state; loader owns native-first - Prototype/cache services retain resource state; the native observer owns its
observation order while cached/static observers own their phases. phase while loader owns native-first ordering among resource observers.
- Scene roots remain loader/tile-owned; Mesh lifetime follows cache/resource refs. - Scene roots remain loader/tile-owned; Mesh lifetime follows cache/resource refs.
- Dispatch planner reads only snapshot accessors and never mutates the snapshot. - Dispatch planner reads only snapshot accessors and never mutates the snapshot.
- Current construction/adoption occurs on renderer main thread. - Current construction/adoption occurs on renderer main thread.
@@ -195,8 +195,8 @@ queue/hitch metrics remain unchanged.
## Extension points ## Extension points
`M2CachedAnimationResourceObserver` produces the cached animated phase and `M2CachedAnimationResourceObserver` produces the cached animated phase and
`M2StaticBuildResourceObserver` produces the static phase. Native animation `M2StaticBuildResourceObserver` produces the static phase. Native animation is
observation remains loader-owned. The value contract stays independent of all produced by `M2NativeAnimationResourceObserver`. The value stays independent of all
producers and of generic callback frameworks. producers and of generic callback frameworks.
## Capability status ## Capability status
@@ -207,14 +207,14 @@ producers and of generic callback frameworks.
| Dispatch decision | Implemented separately | Dispatch planner verifier | Asset-backed traversal pending | | Dispatch decision | Implemented separately | Dispatch planner verifier | Asset-backed traversal pending |
| Static lookup/request execution | Implemented separately | Static observer verifier | Asset-backed traversal pending | | Static lookup/request execution | Implemented separately | Static observer verifier | Asset-backed traversal pending |
| Cached animated lookup/request execution | Implemented separately | Cached observer verifier | Asset-backed traversal pending | | Cached animated lookup/request execution | Implemented separately | Cached observer verifier | Asset-backed traversal pending |
| Native animated lookup/build execution | Loader-owned | Native renderer regression | Service extraction pending | | Native animated lookup/build execution | Implemented observer extraction | Native observer lifecycle/source verifier | Asset-backed traversal pending |
| Engine lifetime/materialization | Loader/materializer-owned | Lifetime/materializer regressions | GPU/p95/p99 evidence pending | | Engine lifetime/materialization | Loader/materializer-owned | Lifetime/materializer regressions | GPU/p95/p99 evidence pending |
## Known gaps and risks ## Known gaps and risks
- Static observation is mutable during its short two-phase construction. - Static observation is mutable during its short two-phase construction.
- Raw Node3D/Mesh references remain necessary for current materializer APIs. - Raw Node3D/Mesh references remain necessary for current materializer APIs.
- Loader still owns synchronous native prototype attempts and terminal polling. - Native parsing/build remains synchronous; loader still owns terminal polling.
- Synthetic timing does not measure resource, Node or GPU work. - Synthetic timing does not measure resource, Node or GPU work.
## Source map ## Source map
@@ -225,7 +225,8 @@ producers and of generic callback frameworks.
| `src/render/m2/m2_build_dispatch_planner.gd` | Snapshot-to-action planning | | `src/render/m2/m2_build_dispatch_planner.gd` | Snapshot-to-action planning |
| `src/render/m2/m2_static_build_resource_observer.gd` | Static snapshot production and requests | | `src/render/m2/m2_static_build_resource_observer.gd` | Static snapshot production and requests |
| `src/render/m2/m2_cached_animation_resource_observer.gd` | Cached animated snapshot production and requests | | `src/render/m2/m2_cached_animation_resource_observer.gd` | Cached animated snapshot production and requests |
| `src/scenes/streaming/streaming_world_loader.gd` | Native animation observation and materializer borrowing | | `src/render/m2/m2_native_animation_resource_observer.gd` | Native prototype observation producer |
| `src/scenes/streaming/streaming_world_loader.gd` | Observer ordering and materializer borrowing |
| `src/tools/verify_m2_build_resource_snapshot.gd` | Identity/adoption/lifetime/boundary/timing regression | | `src/tools/verify_m2_build_resource_snapshot.gd` | Identity/adoption/lifetime/boundary/timing regression |
## Related decisions and references ## Related decisions and references
@@ -28,7 +28,7 @@ or record a terminal static-only animation outcome without changing behavior.
```mermaid ```mermaid
flowchart LR flowchart LR
Loader[StreamingWorldLoader] --> Native[Native animation attempt] Loader[StreamingWorldLoader] --> Native[M2NativeAnimationResourceObserver]
Native -->|no prototype| Observer[M2CachedAnimationResourceObserver] Native -->|no prototype| Observer[M2CachedAnimationResourceObserver]
Prototype[M2PrototypeCacheState] --> Observer Prototype[M2PrototypeCacheState] --> Observer
Pipeline[M2AnimationLoadPipelineState] --> Observer Pipeline[M2AnimationLoadPipelineState] --> Observer
@@ -151,8 +151,8 @@ flowchart TB
- Snapshot borrows the exact cached prototype; observer never frees it. - Snapshot borrows the exact cached prototype; observer never frees it.
- Resource existence/request and synchronous GLB JSON inspection run on the - Resource existence/request and synchronous GLB JSON inspection run on the
renderer main thread, matching the previous loader behavior. renderer main thread, matching the previous loader behavior.
- Loader remains owner of native animation build, terminal polling/finalize, - The sibling native observer owns the native build attempt. Loader remains owner
permits, materialization and every SceneTree mutation. of terminal polling/finalize, permits, materialization and SceneTree mutation.
## Errors, cancellation and recovery ## Errors, cancellation and recovery
@@ -206,7 +206,7 @@ behind a dedicated adapter while retaining the pipeline state.
| Capability | Status | Evidence | Gap/next step | | Capability | Status | Evidence | Gap/next step |
|---|---|---|---| |---|---|---|---|
| Cached animation observation/request | Implemented extraction | Lifecycle/policy/GLB/source/timing verifier | Asset-backed traversal pending | | Cached animation observation/request | Implemented extraction | Lifecycle/policy/GLB/source/timing verifier | Asset-backed traversal pending |
| Native animation observation | Existing loader-owned | Native renderer regression | Separate extraction | | Native animation observation | Implemented sibling extraction | Native observer lifecycle/source verifier | Asset-backed traversal pending |
| Animated finalize/preparation | Loader/finalizer split | Pipeline/finalizer regressions | Polling adapter optional | | Animated finalize/preparation | Loader/finalizer split | Pipeline/finalizer regressions | Polling adapter optional |
## Known gaps and risks ## Known gaps and risks
@@ -225,12 +225,14 @@ behind a dedicated adapter while retaining the pipeline state.
| `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step observation value | | `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step observation value |
| `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending request/finalize state | | `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending request/finalize state |
| `src/render/m2/m2_prototype_cache_state.gd` | Prototype and static-only outcomes | | `src/render/m2/m2_prototype_cache_state.gd` | Prototype and static-only outcomes |
| `src/scenes/streaming/streaming_world_loader.gd` | Native attempt, composition, finalize and execution | | `src/render/m2/m2_native_animation_resource_observer.gd` | Native-first candidate/read/build/cache observation |
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, fallback order, finalize and execution |
| `src/tools/verify_m2_cached_animation_resource_observer.gd` | Lifecycle/policy/GLB/boundary/timing regression | | `src/tools/verify_m2_cached_animation_resource_observer.gd` | Lifecycle/policy/GLB/boundary/timing regression |
## Related decisions and references ## Related decisions and references
- [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md) - [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md)
- [`m2-native-animation-resource-observer.md`](m2-native-animation-resource-observer.md)
- [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md) - [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md)
- [`m2-prototype-cache-state.md`](m2-prototype-cache-state.md) - [`m2-prototype-cache-state.md`](m2-prototype-cache-state.md)
- [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md) - [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md)
@@ -0,0 +1,245 @@
# M2 Native Animation Resource Observer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-NATIVE-ANIMATION-RESOURCE-OBSERVER-001` |
| Owners | Native animation candidate policy, raw read, build and cache outcome |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-native-animation-resource-observer`, 2026-07-18 |
| Profiles/capabilities | Existing native GryphonRoost animated M2 path |
## Purpose
Resolve the historical native animated M2 path as one synchronous observation:
select a GryphonRoost candidate, reuse cached state, read animated raw data,
build a detached prototype and publish either the prototype or static-only result.
## Non-goals
- Add native animation candidates or change the `gryphonroost` predicate.
- Observe/request cached GLB animation or poll/finalize ResourceLoader work.
- Duplicate, attach, animate or free accepted instances in the SceneTree.
- Consume render permits or change build queue, batching and dispatch policy.
- Change native parsing/building, cache formats, profiles or visible output.
## Context and boundaries
```mermaid
flowchart LR
Loader[StreamingWorldLoader] --> Observer[M2NativeAnimationResourceObserver]
Observer --> Repository[M2RawModelRepository]
Observer --> Builder[M2NativeAnimatedBuilder]
Observer --> Cache[M2PrototypeCacheState]
Observer --> Prototype[Detached animated prototype]
Prototype --> Snapshot[M2BuildResourceSnapshot]
Observer -->|no prototype| Cached[M2CachedAnimationResourceObserver]
```
The observer is the native-first resource boundary. The loader supplies an
already-normalized path and remains responsible for fallback ordering and for
constructing the snapshot consumed by dispatch.
## Public API
| Symbol | Kind | Purpose | Failure behavior |
|---|---|---|---|
| `is_native_animation_candidate(path)` | Pure query | Apply the historical case-insensitive GryphonRoost substring policy | Empty/unmatched path returns false |
| `observe(path, extracted_dir, repository, cache, debug)` | Command/query | Return an exact cached/new native prototype or record static-only fallback | Invalid/non-candidate input returns null without mutation |
| `build_animated_prototype(data, extracted_dir)` | Adapter command | Call the retained native animated builder dependency | Missing builder or invalid result returns null |
The optional constructor dependency exists for deterministic tests; production
uses `M2NativeAnimatedBuilder` without a new abstraction or runtime setting.
## Inputs and outputs
| Direction | Data | Producer | Consumer | Ownership/lifetime |
|---|---|---|---|---|
| Input | Normalized relative M2 path | Loader path adapter | Observer | Copied String; one call |
| Input | Extracted directory | Loader configuration | Repository/builder | Copied String; one call |
| Input | Raw animated Dictionary | Repository | Observer/builder | Call-local value |
| Input | Prototype cache state | Loader composition | Observer | Borrowed service; loader session |
| Output | Cached or adopted prototype | Cache/observer | Loader snapshot | Borrowed exact Node3D reference |
| Output | Static-only transition | Observer | Prototype cache | Copied path; loader session |
| Output | Debug line | Observer | Runtime log | Emitted only when enabled and build succeeds |
## Data flow
```mermaid
flowchart TD
Start[Observe normalized path] --> Candidate{Valid native candidate?}
Candidate -->|no| None[Return null]
Candidate -->|yes| Cached{Cached prototype?}
Cached -->|yes| Return[Return exact prototype]
Cached -->|no| Static{Marked static-only?}
Static -->|yes| None
Static -->|no| Read[Read animated raw data]
Read --> Surfaces{Data and animated surfaces?}
Surfaces -->|no| Mark[Mark static-only]
Surfaces -->|yes| Build[Build detached prototype]
Build --> Children{Prototype has children?}
Children -->|no| Mark
Children -->|yes| Adopt[Adopt first cache prototype]
Adopt --> Log{Debug enabled?}
Log -->|yes| Emit[Emit exact native cache fields]
Log -->|no| Return
Emit --> Return
Mark --> None
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Observing
Observing --> Rejected: invalid or non-candidate
Observing --> Cached: positive cache hit
Observing --> StaticOnly: negative cache hit
Observing --> Reading: uncached candidate
Reading --> StaticOnly: no animated surfaces
Reading --> Building: usable raw data
Building --> StaticOnly: null or childless result
Building --> Adopted: valid result
Adopted --> [*]
Cached --> [*]
StaticOnly --> [*]
Rejected --> [*]
```
## Main sequence
```mermaid
sequenceDiagram
participant L as StreamingWorldLoader
participant O as NativeAnimationObserver
participant C as M2PrototypeCacheState
participant R as M2RawModelRepository
participant B as M2NativeAnimatedBuilder
L->>O: observe(path, directory, repository, cache, debug)
O->>C: find animated / is static-only
alt uncached native candidate
O->>R: load_animated_model_data(directory, path)
R-->>O: raw Dictionary
O->>B: build(raw, directory)
alt valid prototype
O->>C: adopt_animated_prototype(path, prototype)
O-->>L: canonical prototype
else unavailable
O->>C: mark_animation_static(path)
O-->>L: null
end
else cached result
O-->>L: prototype or null
end
```
## Dependency diagram
```mermaid
flowchart TB
Observer[M2NativeAnimationResourceObserver] --> Repository[M2RawModelRepository]
Observer --> Builder[M2NativeAnimatedBuilder]
Observer --> Cache[M2PrototypeCacheState]
Loader[StreamingWorldLoader] --> Observer
Observer -. no dependency .-> ResourceLoader
Observer -. no dependency .-> Pipeline[M2AnimationLoadPipelineState]
Observer -. no dependency .-> Scheduler[RenderBudgetScheduler]
Observer -. no dependency .-> SceneTree
```
## Ownership, threading and resources
- Observation and native parsing/building remain synchronous on the renderer
main thread, exactly as before extraction; there is no mid-call cancellation.
- The observer retains no path, raw Dictionary, Node or service reference.
- Prototype cache owns an accepted detached Node until final shutdown; callers
only borrow the canonical reference.
- The historical childless-builder rejection does not free its detached Node.
This deliberately preserved lifetime edge is documented as a remaining leak
risk rather than silently changed during architectural extraction.
- Loader owns snapshots, fallback sequencing, permits, materialization and every
SceneTree mutation.
## Errors, cancellation and recovery
| Failure/state | Behavior | Recovery |
|---|---|---|
| Empty/non-candidate path or missing dependency | Return null without state mutation | Correct composition/path |
| Cached prototype | Return exact canonical Node immediately | None |
| Existing static-only outcome | Return null without repeated I/O | New loader session after source repair |
| Empty raw data or animated surfaces | Mark static-only and return null | Repair source/parser and start new session |
| Null/childless build | Mark static-only and return null | Repair builder/data and start new session |
| Duplicate successful build | Cache releases later candidate and returns first | None |
| Cancellation during native call | Not supported; call completes | Loader controls whether the call starts |
| Shutdown | Observer retains nothing | Cache releases adopted Nodes after drains |
## Configuration and capabilities
The observer consumes the existing extracted directory and `debug_streaming`
flag. Candidate policy remains the case-insensitive substring `gryphonroost`.
No profile, cache, budget or animation setting is added.
## Persistence, cache and migration
No persistence or format changes are introduced. Positive and static-only state
continues to use the shutdown-lifetime prototype cache; no asset rebake is needed.
## Diagnostics and observability
A successful debug-enabled build emits the unchanged `M2_NATIVE_ANIM_CACHE`
record with path, surface count, bone count, animation id, sequence index,
activity score and length. Rejections remain silent as before.
## Verification
- Dedicated synthetic verifier covers candidate policy, invalid dependencies,
cache identity, static suppression, raw/surface/build failures, preserved
childless lifetime, exact repository/builder arguments, adoption, source
ownership and bounded candidate checks.
- Cached observer, raw repository, prototype cache, snapshot, dispatch, shutdown,
facade, internal-access and render-baseline checks protect adjacent behavior.
- Fidelity evidence is exact control-flow and diagnostic extraction. It is not
original-client animation, visual, memory or asset-backed performance evidence.
## Extension points
Terminal cached-GLB polling/finalization can move behind a separate adapter
without changing this native-first contract. Candidate expansion requires its
own fidelity evidence and must not silently change `Blizzlike335` behavior.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Native candidate selection | Implemented extraction | Policy/source/timing verifier | Additional candidates require fidelity work |
| Raw read/build/cache outcome | Implemented extraction | Failure/identity/adoption verifier | Asset-backed traversal pending |
| Terminal cached ResourceLoader polling | Loader-owned | Existing pipeline regressions | Dedicated finalize adapter |
## Known gaps and risks
- Native parsing/building is synchronous and not bounded by a render permit.
- Rejected childless builder Nodes retain the historical unowned lifetime edge.
- Private asset traversal, descriptor/leak pressure, p95/p99 and paired visual/
animation evidence remain unavailable in this package.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_native_animation_resource_observer.gd` | Candidate/cache/raw/build/outcome/logging observation |
| `src/render/m2/m2_raw_model_repository.gd` | Optional native file/parser boundary |
| `src/render/m2/m2_prototype_cache_state.gd` | Positive prototype and static-only ownership |
| `addons/mpq_extractor/loaders/m2_native_animated_builder.gd` | Existing animated prototype construction |
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, native-first fallback order and execution |
| `src/tools/verify_m2_native_animation_resource_observer.gd` | Lifecycle/identity/boundary/timing regression |
## Related decisions and references
- [`m2-cached-animation-resource-observer.md`](m2-cached-animation-resource-observer.md)
- [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md)
- [`m2-raw-model-repository.md`](m2-raw-model-repository.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)
+6 -5
View File
@@ -142,7 +142,7 @@ flowchart TB
Loader --> Raw[M2RawModelRepository] Loader --> Raw[M2RawModelRepository]
Observer --> ResourceLoader Observer --> ResourceLoader
Loader --> StaticBuilder[M2Builder] Loader --> StaticBuilder[M2Builder]
Loader --> AnimatedBuilder[M2NativeAnimatedBuilder] Observer --> AnimatedBuilder[M2NativeAnimatedBuilder]
State -. no dependency .-> Raw State -. no dependency .-> Raw
State -. no dependency .-> ResourceLoader State -. no dependency .-> ResourceLoader
State -. no dependency .-> StaticBuilder State -. no dependency .-> StaticBuilder
@@ -192,7 +192,7 @@ and native M2 formats are unchanged; no migration or rebake is introduced.
- `diagnostic_snapshot` exposes four sorted path arrays without Node references. - `diagnostic_snapshot` exposes four sorted path arrays without Node references.
- Cached eligibility rejection logging belongs to the cached observer; native - Cached eligibility rejection logging belongs to the cached observer; native
animation logs remain loader-owned. animation success logging belongs to the native observer.
- Existing renderer queue metrics remain unchanged because these tables never - Existing renderer queue metrics remain unchanged because these tables never
contributed work counts. contributed work counts.
- Normalized relative path remains the correlation key. - Normalized relative path remains the correlation key.
@@ -212,8 +212,8 @@ and native M2 formats are unchanged; no migration or rebake is introduced.
## Extension points ## Extension points
Eviction or byte/count budgets require measured memory evidence and explicit Eviction or byte/count budgets require measured memory evidence and explicit
prototype-user lifetime rules. Cached animation observation consumes this prototype-user lifetime rules. Cached and native animation observers consume
service without moving ResourceLoader or builder ownership into prototype state. this state without moving ResourceLoader or builder ownership into cache state.
## Capability status ## Capability status
@@ -237,7 +237,8 @@ service without moving ResourceLoader or builder ownership into prototype state.
| Path | Responsibility | | Path | Responsibility |
|---|---| |---|---|
| `src/render/m2/m2_prototype_cache_state.gd` | Positive Node ownership, negative path state and shutdown release | | `src/render/m2/m2_prototype_cache_state.gd` | Positive Node ownership, negative path state and shutdown release |
| `src/scenes/streaming/streaming_world_loader.gd` | Normalization, I/O/build/fallback decisions and cache adapters | | `src/render/m2/m2_native_animation_resource_observer.gd` | Animated raw/build and positive/static-only 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_m2_prototype_cache_state.gd` | Admission/identity/lifecycle/source/timing regression |
| `src/tools/verify_render_runtime_cache_shutdown.gd` | Integrated final-shutdown release regression | | `src/tools/verify_render_runtime_cache_shutdown.gd` | Integrated final-shutdown release regression |
+12 -7
View File
@@ -28,10 +28,12 @@ static or animated Dictionary consumed by existing builders and classifiers.
```mermaid ```mermaid
flowchart LR flowchart LR
Loader[StreamingWorldLoader] --> Repository[M2RawModelRepository] Loader[StreamingWorldLoader] --> Repository[M2RawModelRepository]
Observer[M2NativeAnimationResourceObserver] --> Repository
Repository --> File[Extracted M2 file] Repository --> File[Extracted M2 file]
Repository --> Native[ClassDB M2Loader] Repository --> Native[ClassDB M2Loader]
Native --> Raw[Raw Dictionary] Native --> Raw[Raw Dictionary]
Raw --> Loader Raw --> Loader
Raw --> Observer
Loader --> Builder[Existing M2 builders/finalizer] Loader --> Builder[Existing M2 builders/finalizer]
``` ```
@@ -54,7 +56,7 @@ renderer policy and other application layers are forbidden.
| Input | Already-normalized relative M2 path String | Loader normalization | Repository | Copied value | One call | | Input | Already-normalized relative M2 path String | Loader normalization | Repository | Copied value | One call |
| Input | Extracted `.m2` bytes | Local legal extraction | Native M2Loader | File-owned | Native call | | Input | Extracted `.m2` bytes | Local legal extraction | Native M2Loader | File-owned | Native call |
| Output | Static raw M2 Dictionary | Native `load_m2` | Loader/finalizer/M2Builder | Fresh native result | One caller operation | | Output | Static raw M2 Dictionary | Native `load_m2` | Loader/finalizer/M2Builder | Fresh native result | One caller operation |
| Output | Animated raw M2 Dictionary | Native `load_m2_animated` | Loader/animated builder | Fresh native result | One caller operation | | Output | Animated raw M2 Dictionary | Native `load_m2_animated` | Native animation observer/builder | Fresh native result | One caller operation |
| Output | Empty Dictionary | Repository guards | Loader fallback and prototype outcome adapter | Fresh value | One failed call | | Output | Empty Dictionary | Repository guards | Loader fallback and prototype outcome adapter | Fresh value | One failed call |
Side effects are limited to file-existence inspection, synchronous native file Side effects are limited to file-existence inspection, synchronous native file
@@ -98,18 +100,18 @@ No request, result or failure state survives a call.
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant Loader as StreamingWorldLoader participant Caller as Loader or NativeAnimationObserver
participant Repo as M2RawModelRepository participant Repo as M2RawModelRepository
participant File as FileAccess participant File as FileAccess
participant Native as M2Loader participant Native as M2Loader
Loader->>Repo: load static/animated(directory, path) Caller->>Repo: load static/animated(directory, path)
Repo->>File: file_exists(globalized joined path) Repo->>File: file_exists(globalized joined path)
alt dependency or file unavailable alt dependency or file unavailable
Repo-->>Loader: empty Dictionary Repo-->>Caller: empty Dictionary
else available else available
Repo->>Native: instantiate and call exact native method Repo->>Native: instantiate and call exact native method
Native-->>Repo: Variant Native-->>Repo: Variant
Repo-->>Loader: Dictionary or empty Dictionary Repo-->>Caller: Dictionary or empty Dictionary
end end
``` ```
@@ -118,13 +120,14 @@ sequenceDiagram
```mermaid ```mermaid
flowchart TB flowchart TB
Loader[StreamingWorldLoader] --> Repository[M2RawModelRepository] Loader[StreamingWorldLoader] --> Repository[M2RawModelRepository]
Observer[M2NativeAnimationResourceObserver] --> Repository
Repository --> ProjectSettings Repository --> ProjectSettings
Repository --> FileAccess Repository --> FileAccess
Repository --> ClassDB Repository --> ClassDB
ClassDB --> Native[M2Loader extension] ClassDB --> Native[M2Loader extension]
Loader --> Finalizer[M2RuntimeMeshFinalizer] Loader --> Finalizer[M2RuntimeMeshFinalizer]
Loader --> StaticBuilder[M2Builder] Loader --> StaticBuilder[M2Builder]
Loader --> AnimatedBuilder[M2NativeAnimatedBuilder] Observer --> AnimatedBuilder[M2NativeAnimatedBuilder]
Repository -. no dependency .-> Finalizer Repository -. no dependency .-> Finalizer
Repository -. no dependency .-> StaticBuilder Repository -. no dependency .-> StaticBuilder
Repository -. no dependency .-> Cache[Renderer caches/queues] Repository -. no dependency .-> Cache[Renderer caches/queues]
@@ -133,7 +136,8 @@ flowchart TB
## Ownership, threading and resources ## Ownership, threading and resources
- The repository owns only call-local path, native instance and result values. - The repository owns only call-local path, native instance and result values.
- The loader owns path normalization and fallback selection; - The loader owns path normalization and fallback selection; the native observer
owns the animated raw-read/build decision;
`M2PrototypeCacheState` owns prototype/negative adoption. `M2PrototypeCacheState` owns prototype/negative adoption.
- Native `M2Loader` owns parsing behavior and returns a new Dictionary value. - Native `M2Loader` owns parsing behavior and returns a new Dictionary value.
- Calls are synchronous on the caller's thread; current renderer callers use the - Calls are synchronous on the caller's thread; current renderer callers use the
@@ -211,6 +215,7 @@ measured work packages rather than expansion of this repository.
| Path | Responsibility | | Path | Responsibility |
|---|---| |---|---|
| `src/render/m2/m2_raw_model_repository.gd` | Stateless native class/file/method boundary | | `src/render/m2/m2_raw_model_repository.gd` | Stateless native class/file/method boundary |
| `src/render/m2/m2_native_animation_resource_observer.gd` | Animated raw-data consumer and builder adapter |
| `src/render/m2/m2_prototype_cache_state.gd` | Prototype and negative-result retention | | `src/render/m2/m2_prototype_cache_state.gd` | Prototype and negative-result retention |
| `src/scenes/streaming/streaming_world_loader.gd` | Path normalization, fallback decisions and result consumers | | `src/scenes/streaming/streaming_world_loader.gd` | Path normalization, fallback decisions and result consumers |
| `src/native/src/m2_loader.cpp` | Native static/animated parsing implementation | | `src/native/src/m2_loader.cpp` | Native static/animated parsing implementation |
+12 -6
View File
@@ -157,6 +157,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
| `M2RuntimeMeshFinalizer` | Internal M2 preparation service | Owns refresh version, rebuild classification, M2Builder rebuild and fallback | Renderer main thread; decisions cached until reset | Null returns null; missing/failed rebuild marks and reuses original Mesh | | `M2RuntimeMeshFinalizer` | Internal M2 preparation service | Owns refresh version, rebuild classification, M2Builder rebuild and fallback | Renderer main thread; decisions cached until reset | Null returns null; missing/failed rebuild marks and reuses original Mesh |
| `M2RawModelRepository` | Internal M2 native repository | Reads static/animated raw Dictionaries through exact M2Loader methods | Synchronous; stateless | Invalid/unavailable/non-Dictionary result returns empty Dictionary | | `M2RawModelRepository` | Internal M2 native repository | Reads static/animated raw Dictionaries through exact M2Loader methods | Synchronous; stateless | Invalid/unavailable/non-Dictionary result returns empty Dictionary |
| `M2PrototypeCacheState` | Internal M2 prototype cache | Owns detached static/animated Nodes and missing/static-only outcomes | Renderer main thread; final shutdown | Invalid admission rejected; first valid prototype wins | | `M2PrototypeCacheState` | Internal M2 prototype cache | Owns detached static/animated Nodes and missing/static-only outcomes | Renderer main thread; final shutdown | Invalid admission rejected; first valid prototype wins |
| `M2NativeAnimationResourceObserver` | Internal native M2 resource observer | Selects GryphonRoost, reads/builds and publishes prototype/static-only outcome | Synchronous renderer main thread; stateless | Invalid/unavailable candidates return null; failures mark static-only |
| `WmoPlacementResolver.normalize_relative_path/resolve_unique_key/resolve_world_transform` | Internal pure WMO service | Resolves cache key, registry identity and world transform | Main/any thread; stateless | Missing UID uses tile/index fallback; transform fields use historical defaults | | `WmoPlacementResolver.normalize_relative_path/resolve_unique_key/resolve_world_transform` | Internal pure WMO service | Resolves cache key, registry identity and world transform | Main/any thread; stateless | Missing UID uses tile/index fallback; transform fields use historical defaults |
| `WmoPlacementRegistry.add_reference/release_reference/contains/active_count/diagnostic_snapshot/clear` | Internal WMO service | Owns placement-key to tile/global reference sets | Renderer main thread; map session | Empty/unknown/non-owner input is rejected without mutation | | `WmoPlacementRegistry.add_reference/release_reference/contains/active_count/diagnostic_snapshot/clear` | Internal WMO service | Owns placement-key to tile/global reference sets | Renderer main thread; map session | Empty/unknown/non-owner input is rejected without mutation |
| `WmoRenderBuildStepPlanner.plan_step` | Internal pure WMO service | Selects one mesh-first lightweight render-group operation and next cursors | Main/any thread; stateless | Raw integer comparisons are preserved without clamping | | `WmoRenderBuildStepPlanner.plan_step` | Internal pure WMO service | Selects one mesh-first lightweight render-group operation and next cursors | Main/any thread; stateless | Raw integer comparisons are preserved without clamping |
@@ -204,7 +205,7 @@ loader configuration remains transitional composition data, not a caller API.
| Internal WMO render cache | Normalized path, cache path and validated Resource | Loader / `WmoRenderResourceCacheState` | Loader lookup, ResourceLoader poll and build queue | State-owned Resource/path references; detached request snapshots | Until transient/full clear | | Internal WMO render cache | Normalized path, cache path and validated Resource | Loader / `WmoRenderResourceCacheState` | Loader lookup, ResourceLoader poll and build queue | State-owned Resource/path references; detached request snapshots | Until transient/full clear |
| Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear | | Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear |
| Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset | | Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset |
| Internal raw M2 read | Extracted directory and normalized relative path | Loader / `M2RawModelRepository` | Finalizer, static or animated builder adapters | Fresh Dictionary; repository retains nothing | One synchronous native call | | Internal raw M2 read | Extracted directory and normalized relative path | Loader/native observer via `M2RawModelRepository` | Finalizer, static or animated builder adapters | Fresh Dictionary; repository retains nothing | One synchronous native call |
| Internal animated M2 load | Normalized path, cached GLB path and opaque terminal status | Cached observer / loader / `M2AnimationLoadPipelineState` | Loader poll/finalize adapters | State-owned String/status records; detached pending snapshots | Request through terminal finalize/reset | | Internal animated M2 load | Normalized path, cached GLB path and opaque terminal status | Cached observer / loader / `M2AnimationLoadPipelineState` | Loader poll/finalize adapters | State-owned String/status records; detached pending snapshots | Request through terminal finalize/reset |
| Internal animated M2 candidate | Loaded PackedScene, static material root and detached candidate | Loader / `M2AnimatedSceneFinalizer` | Loader prototype adoption/static fallback | Finalizer owns until rejection or exact-root transfer | One main-thread finalize permit | | Internal animated M2 candidate | Loaded PackedScene, static material root and detached candidate | Loader / `M2AnimatedSceneFinalizer` | Loader prototype adoption/static fallback | Finalizer owns until rejection or exact-root transfer | One main-thread finalize permit |
| Internal animated M2 playback | Prototype/instance native animators, path/index and player inventory | Materializer/finalizer / `M2AnimationPlaybackController` | Native and imported animation state | Borrowed Nodes; detached optional diagnostics | One duplicated instance startup | | Internal animated M2 playback | Prototype/instance native animators, path/index and player inventory | Materializer/finalizer / `M2AnimationPlaybackController` | Native and imported animation state | Borrowed Nodes; detached optional diagnostics | One duplicated instance startup |
@@ -398,9 +399,11 @@ sequenceDiagram
`M2StaticBuildResourceObserver` owns static Mesh lookup/request admission and `M2StaticBuildResourceObserver` owns static Mesh lookup/request admission and
snapshot adoption. `M2CachedAnimationResourceObserver` owns cached animated snapshot adoption. `M2CachedAnimationResourceObserver` owns cached animated
prototype lookup, GLB policy/request admission and snapshot production. prototype lookup, GLB policy/request admission and snapshot production.
`M2NativeAnimationResourceObserver` owns native candidate selection, raw read,
detached build and prototype/static-only cache outcome.
`M2StaticBatchMaterializer` owns static MultiMesh construction/attachment; the `M2StaticBatchMaterializer` owns static MultiMesh construction/attachment; the
loader retains native animation observation, action execution, cursor adoption, loader retains action execution, cursor adoption, root cleanup, budgets and
root cleanup, budgets and Editor ownership. Editor ownership.
- `WmoPlacementResolver` is stateless and owns only call-local cache-key, - `WmoPlacementResolver` is stateless and owns only call-local cache-key,
identity and transform values. `WmoPlacementRegistry` owns only placement-key identity and transform values. `WmoPlacementRegistry` owns only placement-key
reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation
@@ -428,8 +431,8 @@ sequenceDiagram
- `M2AnimationLoadPipelineState` owns animated M2 pending Resource paths, opaque - `M2AnimationLoadPipelineState` owns animated M2 pending Resource paths, opaque
terminal statuses and completion-order finalize FIFO. The cached animation terminal statuses and completion-order finalize FIFO. The cached animation
observer owns allow/deny/path/GLB selection, request admission and initial observer owns allow/deny/path/GLB selection, request admission and initial
snapshot production. The loader retains native animation build, ResourceLoader snapshot production. The native observer owns synchronous native build; the
polling/finalize, permits and terminal adoption. loader retains ResourceLoader polling/finalize, permits and terminal adoption.
- `M2MeshResourceCacheState` owns prepared static Mesh references and releases - `M2MeshResourceCacheState` owns prepared static Mesh references and releases
them at the existing final-shutdown site. Prototype state and materialization them at the existing final-shutdown site. Prototype state and materialization
belong to the sibling cache service and loader respectively. belong to the sibling cache service and loader respectively.
@@ -592,7 +595,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| M2 build dispatch planner | Implemented extraction | Scene-free priority/action/transition/source/timing contract | Resource observation/orchestration and asset-backed traversal remain pending | | M2 build dispatch planner | Implemented extraction | Scene-free priority/action/transition/source/timing contract | Resource observation/orchestration and asset-backed traversal remain pending |
| M2 build resource snapshot | Implemented extraction | Typed identity/adoption/lifetime/source/timing contract | Resource observation service and asset-backed traversal remain pending | | M2 build resource snapshot | Implemented extraction | Typed identity/adoption/lifetime/source/timing contract | Resource observation service and asset-backed traversal remain pending |
| M2 static build resource observer | Implemented extraction | Cache/pending/path/request/source/timing contract | Asset-backed traversal and finalize extraction pending | | M2 static build resource observer | Implemented extraction | Cache/pending/path/request/source/timing contract | Asset-backed traversal and finalize extraction pending |
| M2 cached animation resource observer | Implemented extraction | Cache/pending/policy/GLB/request/source/timing contract | Native observation and asset-backed traversal pending | | M2 cached animation resource observer | Implemented extraction | Cache/pending/policy/GLB/request/source/timing contract | Asset-backed traversal and terminal polling extraction pending |
| M2 native animation resource observer | Implemented extraction | Candidate/cache/raw/build/adoption/source/timing contract | Asset-backed traversal/leak/p95/p99 pending |
| M2 build queue | Implemented extraction | Typed lifecycle/FIFO/progress/lifetime/source/timing contract | Resource dispatch and asset-backed traversal remain pending | | M2 build queue | Implemented extraction | Typed lifecycle/FIFO/progress/lifetime/source/timing contract | Resource dispatch and asset-backed traversal remain pending |
| M2 animation load pipeline state | Implemented extraction | Synthetic lifecycle/FIFO/source/timing contract | Asset-backed traversal/leak/animation-fidelity/p95/p99 pending | | M2 animation load pipeline state | Implemented extraction | Synthetic lifecycle/FIFO/source/timing contract | Asset-backed traversal/leak/animation-fidelity/p95/p99 pending |
| M2 animated scene finalizer | Implemented extraction | Synthetic type/lifetime/material/player/source/timing contract | Asset-backed GLB traversal/material/animation comparison pending | | M2 animated scene finalizer | Implemented extraction | Synthetic type/lifetime/material/player/source/timing contract | Asset-backed GLB traversal/material/animation comparison pending |
@@ -675,6 +679,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene instance lifetime | | `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene instance lifetime |
| `src/render/m2/m2_runtime_mesh_finalizer.gd` | Runtime refresh version, rebuild classification/build and fallback | | `src/render/m2/m2_runtime_mesh_finalizer.gd` | Runtime refresh version, rebuild classification/build and fallback |
| `src/render/m2/m2_raw_model_repository.gd` | Stateless static/animated native M2 file boundary | | `src/render/m2/m2_raw_model_repository.gd` | Stateless static/animated native M2 file boundary |
| `src/render/m2/m2_native_animation_resource_observer.gd` | Native candidate/read/build/cache observation |
| `src/render/m2/m2_prototype_cache_state.gd` | Detached prototype ownership and negative lookup outcomes | | `src/render/m2/m2_prototype_cache_state.gd` | Detached prototype ownership and negative lookup outcomes |
| `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation | | `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy | | `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
@@ -706,6 +711,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/tools/verify_m2_mesh_resource_extractor.gd` | M2 direct/PackedScene/subtree order/lifetime/boundary/timing regression | | `src/tools/verify_m2_mesh_resource_extractor.gd` | M2 direct/PackedScene/subtree order/lifetime/boundary/timing regression |
| `src/tools/verify_m2_runtime_mesh_finalizer.gd` | M2 current/stale/rebuild/fallback/boundary/timing regression | | `src/tools/verify_m2_runtime_mesh_finalizer.gd` | M2 current/stale/rebuild/fallback/boundary/timing regression |
| `src/tools/verify_m2_raw_model_repository.gd` | M2 invalid/missing/native-boundary/dependency/timing regression | | `src/tools/verify_m2_raw_model_repository.gd` | M2 invalid/missing/native-boundary/dependency/timing regression |
| `src/tools/verify_m2_native_animation_resource_observer.gd` | Native candidate/cache/raw/build/adoption/boundary/timing regression |
| `src/tools/verify_m2_prototype_cache_state.gd` | M2 prototype identity/negative/lifecycle/boundary/timing regression | | `src/tools/verify_m2_prototype_cache_state.gd` | M2 prototype identity/negative/lifecycle/boundary/timing regression |
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression | | `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression | | `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
@@ -0,0 +1,113 @@
class_name M2NativeAnimationResourceObserver
extends RefCounted
## Resolves the existing native GryphonRoost animated M2 prototype path.
const M2_NATIVE_ANIMATED_BUILDER_SCRIPT := preload(
"res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd"
)
var _native_animated_builder: Object
func _init(native_animated_builder: Object = M2_NATIVE_ANIMATED_BUILDER_SCRIPT) -> void:
_native_animated_builder = native_animated_builder
## Returns whether the normalized path uses the historical native animation path.
func is_native_animation_candidate(normalized_relative_path: String) -> bool:
return normalized_relative_path.to_lower().contains("gryphonroost")
## Returns the exact cached or newly adopted native animated prototype.
## Rejected native candidates are marked static-only for the renderer session.
func observe(
normalized_relative_path: String,
extracted_directory: String,
raw_model_repository: RefCounted,
prototype_cache_state: RefCounted,
debug_logging_enabled: bool = false
) -> Node3D:
if (
normalized_relative_path.is_empty()
or not is_native_animation_candidate(normalized_relative_path)
or raw_model_repository == null
or prototype_cache_state == null
):
return null
var cached_prototype := prototype_cache_state.call(
"find_animated_prototype",
normalized_relative_path
) as Node3D
if cached_prototype != null:
return cached_prototype
if bool(prototype_cache_state.call(
"is_animation_static",
normalized_relative_path
)):
return null
var animated_model_data: Dictionary = raw_model_repository.call(
"load_animated_model_data",
extracted_directory,
normalized_relative_path
)
var animated_surfaces: Array = animated_model_data.get(
"animated_surfaces",
[]
)
if animated_model_data.is_empty() or animated_surfaces.is_empty():
prototype_cache_state.call(
"mark_animation_static",
normalized_relative_path
)
return null
var prototype := build_animated_prototype(
animated_model_data,
extracted_directory
)
if prototype == null or prototype.get_child_count() <= 0:
prototype_cache_state.call(
"mark_animation_static",
normalized_relative_path
)
return null
prototype = prototype_cache_state.call(
"adopt_animated_prototype",
normalized_relative_path,
prototype
) as Node3D
if debug_logging_enabled:
print(
(
"M2_NATIVE_ANIM_CACHE path=%s surfaces=%d bones=%d "
+ "anim_id=%d seq=%d score=%d length=%.2f"
)
% [
normalized_relative_path,
animated_surfaces.size(),
(animated_model_data.get("bones", []) as Array).size(),
int(animated_model_data.get("animation_id", -1)),
int(animated_model_data.get("animation_sequence_index", -1)),
int(animated_model_data.get("animation_activity_score", 0)),
float(animated_model_data.get("animation_length", 0.0)),
]
)
return prototype
## Builds one prototype through the exact native animated builder dependency.
func build_animated_prototype(
animated_model_data: Dictionary,
extracted_directory: String
) -> Node3D:
if _native_animated_builder == null:
return null
return _native_animated_builder.call(
"build",
animated_model_data,
extracted_directory
) as Node3D
@@ -0,0 +1 @@
uid://bc3kgi23usncx
+13 -48
View File
@@ -28,7 +28,6 @@ const WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT := preload(
"res://src/render/wmo/wmo_scene_resource_cache_state.gd" "res://src/render/wmo/wmo_scene_resource_cache_state.gd"
) )
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd") const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
const M2_NATIVE_ANIMATED_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd")
const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd") const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd") const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd") const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
@@ -76,6 +75,9 @@ const M2_STATIC_BUILD_RESOURCE_OBSERVER_SCRIPT := preload(
const M2_CACHED_ANIMATION_RESOURCE_OBSERVER_SCRIPT := preload( const M2_CACHED_ANIMATION_RESOURCE_OBSERVER_SCRIPT := preload(
"res://src/render/m2/m2_cached_animation_resource_observer.gd" "res://src/render/m2/m2_cached_animation_resource_observer.gd"
) )
const M2_NATIVE_ANIMATION_RESOURCE_OBSERVER_SCRIPT := preload(
"res://src/render/m2/m2_native_animation_resource_observer.gd"
)
const M2_RUNTIME_MESH_FINALIZER_SCRIPT := preload( const M2_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
"res://src/render/m2/m2_runtime_mesh_finalizer.gd" "res://src/render/m2/m2_runtime_mesh_finalizer.gd"
) )
@@ -289,6 +291,9 @@ var _m2_static_build_resource_observer := (
var _m2_animation_resource_observer := ( var _m2_animation_resource_observer := (
M2_CACHED_ANIMATION_RESOURCE_OBSERVER_SCRIPT.new() M2_CACHED_ANIMATION_RESOURCE_OBSERVER_SCRIPT.new()
) )
var _m2_native_animation_resource_observer := (
M2_NATIVE_ANIMATION_RESOURCE_OBSERVER_SCRIPT.new()
)
var _m2_unique_placement_registry := ( var _m2_unique_placement_registry := (
M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT.new() M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT.new()
) )
@@ -4359,7 +4364,13 @@ func _process_m2_build_jobs() -> void:
var resource_snapshot: RefCounted var resource_snapshot: RefCounted
if enable_m2_animated_instances: if enable_m2_animated_instances:
var native_animated_prototype := ( var native_animated_prototype := (
_get_or_load_m2_native_animated_prototype(rel_path) _m2_native_animation_resource_observer.observe(
normalized_rel,
extracted_dir,
_m2_raw_model_repository,
_m2_prototype_cache_state,
debug_streaming
)
) )
if native_animated_prototype != null: if native_animated_prototype != null:
resource_snapshot = M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new( resource_snapshot = M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
@@ -4849,52 +4860,6 @@ func _get_or_load_m2_prototype(rel_path: String) -> Node3D:
) )
func _get_or_load_m2_native_animated_prototype(rel_path: String) -> Node3D:
var normalized_rel := _normalize_m2_rel_path(rel_path)
if normalized_rel.is_empty() or not _is_m2_native_animation_candidate(normalized_rel):
return null
var cached_animated := _m2_prototype_cache_state.find_animated_prototype(
normalized_rel
)
if cached_animated != null:
return cached_animated
if _m2_prototype_cache_state.is_animation_static(normalized_rel):
return null
var data := _m2_raw_model_repository.load_animated_model_data(
extracted_dir,
normalized_rel
)
var animated_surfaces: Array = data.get("animated_surfaces", [])
if data.is_empty() or animated_surfaces.is_empty():
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
return null
var prototype: Node3D = M2_NATIVE_ANIMATED_BUILDER_SCRIPT.build(data, extracted_dir)
if prototype == null or prototype.get_child_count() <= 0:
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
return null
prototype = _m2_prototype_cache_state.adopt_animated_prototype(
normalized_rel,
prototype
)
if debug_streaming:
print("M2_NATIVE_ANIM_CACHE path=%s surfaces=%d bones=%d anim_id=%d seq=%d score=%d length=%.2f" % [
normalized_rel,
animated_surfaces.size(),
(data.get("bones", []) as Array).size(),
int(data.get("animation_id", -1)),
int(data.get("animation_sequence_index", -1)),
int(data.get("animation_activity_score", 0)),
float(data.get("animation_length", 0.0)),
])
return prototype
func _is_m2_native_animation_candidate(normalized_rel: String) -> bool:
return normalized_rel.to_lower().contains("gryphonroost")
func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D: func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
if normalized_rel.is_empty(): if normalized_rel.is_empty():
return null return null
@@ -12,6 +12,9 @@ const PIPELINE_SCRIPT := preload(
const OBSERVER_PATH := ( const OBSERVER_PATH := (
"res://src/render/m2/m2_cached_animation_resource_observer.gd" "res://src/render/m2/m2_cached_animation_resource_observer.gd"
) )
const NATIVE_OBSERVER_PATH := (
"res://src/render/m2/m2_native_animation_resource_observer.gd"
)
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
const FIXTURE_DIRECTORY := "user://verify_m2_cached_animation_observer" const FIXTURE_DIRECTORY := "user://verify_m2_cached_animation_observer"
@@ -252,6 +255,7 @@ func _verify_glb_safety(failures: Array[String]) -> void:
func _verify_source_boundaries(failures: Array[String]) -> void: func _verify_source_boundaries(failures: Array[String]) -> void:
var loader_source := FileAccess.get_file_as_string(LOADER_PATH) var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
var observer_source := FileAccess.get_file_as_string(OBSERVER_PATH) var observer_source := FileAccess.get_file_as_string(OBSERVER_PATH)
var native_observer_source := FileAccess.get_file_as_string(NATIVE_OBSERVER_PATH)
_expect_true( _expect_true(
loader_source.contains("_m2_animation_resource_observer.observe("), loader_source.contains("_m2_animation_resource_observer.observe("),
"loader delegates cached observation", "loader delegates cached observation",
@@ -269,8 +273,13 @@ func _verify_source_boundaries(failures: Array[String]) -> void:
failures failures
) )
_expect_true( _expect_true(
loader_source.contains("func _get_or_load_m2_native_animated_prototype"), loader_source.contains("_m2_native_animation_resource_observer.observe("),
"loader retains native observation", "loader delegates native observation",
failures
)
_expect_true(
native_observer_source.contains("func is_native_animation_candidate("),
"native observer retains native candidate policy",
failures failures
) )
_expect_true( _expect_true(
@@ -0,0 +1,426 @@
extends SceneTree
const OBSERVER_SCRIPT := preload(
"res://src/render/m2/m2_native_animation_resource_observer.gd"
)
const PROTOTYPE_CACHE_SCRIPT := preload(
"res://src/render/m2/m2_prototype_cache_state.gd"
)
const OBSERVER_PATH := (
"res://src/render/m2/m2_native_animation_resource_observer.gd"
)
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
class FakeRawModelRepository extends RefCounted:
var result: Dictionary = {}
var call_count: int = 0
var last_extracted_directory: String = ""
var last_normalized_relative_path: String = ""
func load_animated_model_data(
extracted_directory: String,
normalized_relative_path: String
) -> Dictionary:
call_count += 1
last_extracted_directory = extracted_directory
last_normalized_relative_path = normalized_relative_path
return result
class FakeNativeAnimatedBuilder extends RefCounted:
var result: Node3D = null
var call_count: int = 0
var last_model_data: Dictionary = {}
var last_extracted_directory: String = ""
func build(
animated_model_data: Dictionary,
extracted_directory: String
) -> Node3D:
call_count += 1
last_model_data = animated_model_data
last_extracted_directory = extracted_directory
return result
func _initialize() -> void:
var failures: Array[String] = []
_verify_candidate_policy(failures)
_verify_missing_dependencies(failures)
_verify_cache_and_negative_lifecycle(failures)
_verify_raw_and_builder_failures(failures)
_verify_successful_adoption(failures)
_verify_source_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("M2_NATIVE_ANIMATION_RESOURCE_OBSERVER: %s" % failure)
quit(1)
return
print(
"M2_NATIVE_ANIMATION_RESOURCE_OBSERVER PASS "
+ "cases=33 iterations=20000 elapsed_ms=%.3f" % elapsed_milliseconds
)
quit(0)
func _verify_candidate_policy(failures: Array[String]) -> void:
var observer: RefCounted = OBSERVER_SCRIPT.new()
_expect_false(
bool(observer.call("is_native_animation_candidate", "")),
"empty path is not candidate",
failures
)
_expect_true(
bool(observer.call(
"is_native_animation_candidate",
"World/Generic/GryphonRoost/GryphonRoost.M2"
)),
"candidate match ignores case",
failures
)
_expect_false(
bool(observer.call(
"is_native_animation_candidate",
"creature/gryphon/gryphon.m2"
)),
"unlisted gryphon is not candidate",
failures
)
func _verify_missing_dependencies(failures: Array[String]) -> void:
var path := "world/gryphonroost/dependency.m2"
var builder := FakeNativeAnimatedBuilder.new()
var observer: RefCounted = OBSERVER_SCRIPT.new(builder)
var repository := FakeRawModelRepository.new()
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
_expect_null(
observer.call("observe", path, "res://data/extracted", null, prototype_cache),
"missing repository rejected",
failures
)
_expect_null(
observer.call("observe", path, "res://data/extracted", repository, null),
"missing cache rejected",
failures
)
repository.result = _animated_model_data()
observer = OBSERVER_SCRIPT.new(null)
_expect_null(
observer.call(
"observe", path, "res://data/extracted", repository, prototype_cache
),
"missing builder rejected",
failures
)
prototype_cache.call("clear_and_release")
func _verify_cache_and_negative_lifecycle(failures: Array[String]) -> void:
var builder := FakeNativeAnimatedBuilder.new()
var observer: RefCounted = OBSERVER_SCRIPT.new(builder)
var repository := FakeRawModelRepository.new()
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
_expect_null(
observer.call(
"observe", "", "res://data/extracted", repository,
prototype_cache
),
"empty observation rejected",
failures
)
_expect_null(
observer.call(
"observe", "world/not_native.m2", "res://data/extracted",
repository, prototype_cache
),
"non-candidate rejected",
failures
)
_expect_equal(repository.call_count, 0, "rejected paths skip repository", failures)
var cached_prototype := Node3D.new()
prototype_cache.call(
"adopt_animated_prototype",
"world/gryphonroost/cached.m2",
cached_prototype
)
_expect_same(
observer.call(
"observe", "world/gryphonroost/cached.m2",
"res://data/extracted", repository, prototype_cache
),
cached_prototype,
"cached prototype identity",
failures
)
prototype_cache.call(
"mark_animation_static",
"world/gryphonroost/static.m2"
)
_expect_null(
observer.call(
"observe", "world/gryphonroost/static.m2",
"res://data/extracted", repository, prototype_cache
),
"static-only outcome suppresses load",
failures
)
_expect_equal(repository.call_count, 0, "cache outcomes skip repository", failures)
prototype_cache.call("clear_and_release")
func _verify_raw_and_builder_failures(failures: Array[String]) -> void:
var builder := FakeNativeAnimatedBuilder.new()
var observer: RefCounted = OBSERVER_SCRIPT.new(builder)
var repository := FakeRawModelRepository.new()
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
var empty_path := "world/gryphonroost/empty.m2"
_expect_null(
observer.call(
"observe", empty_path, "res://data/extracted", repository,
prototype_cache
),
"empty raw data rejects",
failures
)
_expect_true(
bool(prototype_cache.call("is_animation_static", empty_path)),
"empty raw data marks static",
failures
)
repository.result = {"animated_surfaces": []}
var surfaces_path := "world/gryphonroost/no_surfaces.m2"
_expect_null(
observer.call(
"observe", surfaces_path, "res://data/extracted", repository,
prototype_cache
),
"empty animated surfaces reject",
failures
)
_expect_true(
bool(prototype_cache.call("is_animation_static", surfaces_path)),
"empty surfaces mark static",
failures
)
repository.result = _animated_model_data()
var null_path := "world/gryphonroost/null_build.m2"
_expect_null(
observer.call(
"observe", null_path, "res://data/extracted", repository,
prototype_cache
),
"null build rejects",
failures
)
_expect_true(
bool(prototype_cache.call("is_animation_static", null_path)),
"null build marks static",
failures
)
var childless_prototype := Node3D.new()
builder.result = childless_prototype
var childless_path := "world/gryphonroost/childless.m2"
_expect_null(
observer.call(
"observe", childless_path, "res://data/extracted", repository,
prototype_cache
),
"childless build rejects",
failures
)
_expect_true(
bool(prototype_cache.call("is_animation_static", childless_path)),
"childless build marks static",
failures
)
_expect_true(
is_instance_valid(childless_prototype),
"historical childless candidate lifetime retained",
failures
)
childless_prototype.free()
prototype_cache.call("clear_and_release")
func _verify_successful_adoption(failures: Array[String]) -> void:
var builder := FakeNativeAnimatedBuilder.new()
var prototype := Node3D.new()
prototype.add_child(Node3D.new())
builder.result = prototype
var observer: RefCounted = OBSERVER_SCRIPT.new(builder)
var repository := FakeRawModelRepository.new()
repository.result = _animated_model_data()
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
var path := "World/Generic/GryphonRoost/GryphonRoost.M2"
var observed: Node3D = observer.call(
"observe",
path,
"res://custom/extracted",
repository,
prototype_cache
)
_expect_same(observed, prototype, "successful build identity", failures)
_expect_same(
prototype_cache.call("find_animated_prototype", path),
prototype,
"successful build adopted",
failures
)
_expect_equal(repository.call_count, 1, "one repository read", failures)
_expect_string_equal(
repository.last_extracted_directory,
"res://custom/extracted",
"repository extracted directory",
failures
)
_expect_string_equal(
repository.last_normalized_relative_path,
path,
"repository normalized path",
failures
)
_expect_equal(builder.call_count, 1, "one builder call", failures)
_expect_true(
builder.last_model_data == repository.result,
"builder receives exact raw data",
failures
)
_expect_string_equal(
builder.last_extracted_directory,
"res://custom/extracted",
"builder extracted directory",
failures
)
prototype_cache.call("clear_and_release")
func _verify_source_boundaries(failures: Array[String]) -> void:
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
var observer_source := FileAccess.get_file_as_string(OBSERVER_PATH)
_expect_true(
loader_source.contains("_m2_native_animation_resource_observer.observe("),
"loader delegates native observation",
failures
)
for removed_token in [
"func _get_or_load_m2_native_animated_prototype",
"func _is_m2_native_animation_candidate",
"const M2_NATIVE_ANIMATED_BUILDER_SCRIPT",
]:
_expect_false(
loader_source.contains(removed_token),
"loader omits %s" % removed_token,
failures
)
for required_token in [
"M2_NATIVE_ANIMATED_BUILDER_SCRIPT",
"\"load_animated_model_data\"",
"\"adopt_animated_prototype\"",
"\"mark_animation_static\"",
"M2_NATIVE_ANIM_CACHE path=%s surfaces=%d bones=%d",
]:
_expect_true(
observer_source.contains(required_token),
"observer owns %s" % required_token,
failures
)
for forbidden_token in [
"ResourceLoader",
"queue_free",
"RenderingServer",
"WorkerThreadPool",
"RenderBudgetScheduler",
"add_child",
]:
_expect_false(
observer_source.contains(forbidden_token),
"observer omits %s" % forbidden_token,
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var observer: RefCounted = OBSERVER_SCRIPT.new()
var started_microseconds := Time.get_ticks_usec()
for iteration in range(20000):
observer.call(
"is_native_animation_candidate",
"world/model_%d/gryphonroost.m2" % (iteration % 64)
)
var elapsed_milliseconds := (
float(Time.get_ticks_usec() - started_microseconds) / 1000.0
)
_expect_true(
elapsed_milliseconds < 1000.0,
"20,000 candidate checks remain bounded",
failures
)
return elapsed_milliseconds
func _animated_model_data() -> Dictionary:
return {
"animated_surfaces": [{"surface": 1}],
"bones": [{"bone": 1}, {"bone": 2}],
"animation_id": 7,
"animation_sequence_index": 3,
"animation_activity_score": 9,
"animation_length": 1.25,
}
func _expect_true(
condition: bool,
label: String,
failures: Array[String]
) -> void:
if not condition:
failures.append(label)
func _expect_false(
condition: bool,
label: String,
failures: Array[String]
) -> void:
_expect_true(not condition, label, failures)
func _expect_null(
actual: Variant,
label: String,
failures: Array[String]
) -> void:
_expect_true(actual == null, label, failures)
func _expect_same(
actual: Variant,
expected: Variant,
label: String,
failures: Array[String]
) -> void:
_expect_true(is_same(actual, expected), label, failures)
func _expect_equal(
actual: int,
expected: int,
label: String,
failures: Array[String]
) -> void:
if actual != expected:
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
func _expect_string_equal(
actual: String,
expected: String,
label: String,
failures: Array[String]
) -> void:
if actual != expected:
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
@@ -0,0 +1 @@
uid://cto5qa1vh221v
+16 -2
View File
@@ -4,6 +4,12 @@ extends SceneTree
const CACHE_STATE_SCRIPT := preload("res://src/render/m2/m2_prototype_cache_state.gd") const CACHE_STATE_SCRIPT := preload("res://src/render/m2/m2_prototype_cache_state.gd")
const CACHE_STATE_PATH := "res://src/render/m2/m2_prototype_cache_state.gd" const CACHE_STATE_PATH := "res://src/render/m2/m2_prototype_cache_state.gd"
const NATIVE_OBSERVER_PATH := (
"res://src/render/m2/m2_native_animation_resource_observer.gd"
)
const CACHED_OBSERVER_PATH := (
"res://src/render/m2/m2_cached_animation_resource_observer.gd"
)
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
@@ -93,6 +99,10 @@ func _verify_release_ownership(failures: Array[String]) -> void:
func _verify_ownership_boundaries(failures: Array[String]) -> void: func _verify_ownership_boundaries(failures: Array[String]) -> void:
var state_source := FileAccess.get_file_as_string(CACHE_STATE_PATH) var state_source := FileAccess.get_file_as_string(CACHE_STATE_PATH)
var loader_source := FileAccess.get_file_as_string(LOADER_PATH) var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
var observer_sources := (
FileAccess.get_file_as_string(NATIVE_OBSERVER_PATH)
+ FileAccess.get_file_as_string(CACHED_OBSERVER_PATH)
)
_expect_true( _expect_true(
loader_source.contains("M2_PROTOTYPE_CACHE_STATE_SCRIPT.new()"), loader_source.contains("M2_PROTOTYPE_CACHE_STATE_SCRIPT.new()"),
"loader composes prototype cache state", "loader composes prototype cache state",
@@ -108,13 +118,17 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
for required_loader_rule in [ for required_loader_rule in [
"_m2_prototype_cache_state.find_static_prototype(", "_m2_prototype_cache_state.find_static_prototype(",
"_m2_prototype_cache_state.adopt_static_prototype(", "_m2_prototype_cache_state.adopt_static_prototype(",
"_m2_prototype_cache_state.find_animated_prototype(", "\"find_animated_prototype\"",
"_m2_prototype_cache_state.adopt_animated_prototype(", "_m2_prototype_cache_state.adopt_animated_prototype(",
"_m2_prototype_cache_state.mark_model_missing(", "_m2_prototype_cache_state.mark_model_missing(",
"_m2_prototype_cache_state.mark_animation_static(", "_m2_prototype_cache_state.mark_animation_static(",
"_m2_prototype_cache_state.clear_and_release()", "_m2_prototype_cache_state.clear_and_release()",
]: ]:
_expect_true(loader_source.contains(required_loader_rule), "loader delegates %s" % required_loader_rule, failures) _expect_true(
(loader_source + observer_sources).contains(required_loader_rule),
"renderer delegates %s" % required_loader_rule,
failures
)
for forbidden_dependency in [ for forbidden_dependency in [
"ResourceLoader.", "ResourceLoader.",
"FileAccess.", "FileAccess.",
+8 -1
View File
@@ -4,6 +4,9 @@ extends SceneTree
const REPOSITORY_SCRIPT := preload("res://src/render/m2/m2_raw_model_repository.gd") const REPOSITORY_SCRIPT := preload("res://src/render/m2/m2_raw_model_repository.gd")
const REPOSITORY_PATH := "res://src/render/m2/m2_raw_model_repository.gd" const REPOSITORY_PATH := "res://src/render/m2/m2_raw_model_repository.gd"
const NATIVE_OBSERVER_PATH := (
"res://src/render/m2/m2_native_animation_resource_observer.gd"
)
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
@@ -56,6 +59,7 @@ func _verify_rejected_inputs(failures: Array[String]) -> void:
func _verify_ownership_boundaries(failures: Array[String]) -> void: func _verify_ownership_boundaries(failures: Array[String]) -> void:
var repository_source := FileAccess.get_file_as_string(REPOSITORY_PATH) var repository_source := FileAccess.get_file_as_string(REPOSITORY_PATH)
var native_observer_source := FileAccess.get_file_as_string(NATIVE_OBSERVER_PATH)
var loader_source := FileAccess.get_file_as_string(LOADER_PATH) var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
_expect_true( _expect_true(
loader_source.contains("M2_RAW_MODEL_REPOSITORY_SCRIPT.new()"), loader_source.contains("M2_RAW_MODEL_REPOSITORY_SCRIPT.new()"),
@@ -69,7 +73,10 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
failures failures
) )
_expect_equal( _expect_equal(
loader_source.count("_m2_raw_model_repository.load_animated_model_data("), (
loader_source.count("_m2_raw_model_repository.load_animated_model_data(")
+ native_observer_source.count("\"load_animated_model_data\"")
),
1, 1,
"animated caller delegates", "animated caller delegates",
failures failures
+19 -5
View File
@@ -60,7 +60,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
`M03-RND-M2-BUILD-DISPATCH-PLANNER-001`, `M03-RND-M2-BUILD-DISPATCH-PLANNER-001`,
`M03-RND-M2-BUILD-RESOURCE-SNAPSHOT-001`, `M03-RND-M2-BUILD-RESOURCE-SNAPSHOT-001`,
`M03-RND-M2-STATIC-BUILD-RESOURCE-OBSERVER-001`, `M03-RND-M2-STATIC-BUILD-RESOURCE-OBSERVER-001`,
`M03-RND-M2-CACHED-ANIMATION-RESOURCE-OBSERVER-001` `M03-RND-M2-CACHED-ANIMATION-RESOURCE-OBSERVER-001`,
`M03-RND-M2-NATIVE-ANIMATION-RESOURCE-OBSERVER-001`
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown - Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates. headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner - Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner
@@ -294,6 +295,12 @@ Runtime и Editor используют facade; planner/scheduler тестиру
Merge `f79e064` passed post-merge observer (`61.514ms`), animation pipeline, Merge `f79e064` passed post-merge observer (`61.514ms`), animation pipeline,
snapshot, dispatch, prototype cache, shutdown, facade, internal-access `30`, snapshot, dispatch, prototype cache, shutdown, facade, internal-access `30`,
manifest `7/7`, documentation `43`, coordination and checkpoint dry-run `7/7`. manifest `7/7`, documentation `43`, coordination and checkpoint dry-run `7/7`.
M2 native animation resource observer passed `cases=33 iterations=20000
elapsed_ms=16.881` with exact candidate, dependency, cached/static, raw/surface/build
failure, preserved childless lifetime, adoption, dependency and source cases.
All 57 autonomous headless regressions passed; internal-access remained `30`,
documentation covered `44` module specifications, coordination retained `34`
historical expired-claim warnings and checkpoint dry-run kept all `7/7` plans.
M02 terrain-query regression M02 terrain-query regression
remained green (13 pre-existing expired M00 claim warnings). remained green (13 pre-existing expired M00 claim warnings).
- Fidelity comparison: all 16 historical operation limits and drain sites were - Fidelity comparison: all 16 historical operation limits and drain sites were
@@ -331,11 +338,14 @@ Runtime и Editor используют facade; planner/scheduler тестиру
One build operation now carries the exact normalized path, optional animated One build operation now carries the exact normalized path, optional animated
prototype, pending-animation state, optional static Mesh and missing outcome in prototype, pending-animation state, optional static Mesh and missing outcome in
a typed snapshot; releasing it never frees either borrowed engine resource. a typed snapshot; releasing it never frees either borrowed engine resource.
Cached animated lookup now preserves cached/static/pending priority, empty- Native GryphonRoost observation now preserves case-insensitive substring
selection, cached/static priority, synchronous animated raw read, non-empty
surface and child requirements, first-wins adoption and exact success-log fields.
Cached animated lookup preserves cached/static/pending priority, empty-
allowlist rejection, denylist precedence, normalized/lowercase/basename path allowlist rejection, denylist precedence, normalized/lowercase/basename path
order, animation presence, primitive cap, accepted empty/`pivot_prefix_v1` order, animation presence, primitive cap, accepted empty/`pivot_prefix_v1`
schema rules and request/static-only transitions. Native GryphonRoost remains schema rules and request/static-only transitions. Native GryphonRoost remains
first and loader-owned, so selection and visible behavior are unchanged. first through its observer, so selection and visible behavior are unchanged.
WMO cache-key normalization, positive MODF identity with tile/index fallback WMO cache-key normalization, positive MODF identity with tile/index fallback
and world position/Euler/unclamped-scale transforms are unchanged across and world position/Euler/unclamped-scale transforms are unchanged across
lightweight render, cached-scene and live-prototype paths. lightweight render, cached-scene and live-prototype paths.
@@ -456,6 +466,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
static M2 build resource observer and loader/cache/pipeline/snapshot adapters, static M2 build resource observer and loader/cache/pipeline/snapshot adapters,
cached M2 animation resource observer and loader/prototype/pipeline/snapshot cached M2 animation resource observer and loader/prototype/pipeline/snapshot
adapters plus generated GLB metadata regression fixtures, adapters plus generated GLB metadata regression fixtures,
native M2 animation resource observer and loader/raw-repository/prototype-cache
adapters plus synthetic lifecycle and exact-dependency regression fixtures,
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence. expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
- Remaining risks: worker concurrency and stale-result validation remain - Remaining risks: worker concurrency and stale-result validation remain
streamer-owned; cancellation stops new permits but does not interrupt streamer-owned; cancellation stops new permits but does not interrupt
@@ -478,12 +490,14 @@ Runtime и Editor используют facade; planner/scheduler тестиру
raw raw
animated-instance materialization is now separated but remains synchronous animated-instance materialization is now separated but remains synchronous
main-thread work; asset-backed traversal/visual/leak/p95/p99 evidence is pending; main-thread work; asset-backed traversal/visual/leak/p95/p99 evidence is pending;
native parsing/build also remains synchronous and the historically unfreed
childless builder result remains a documented leak edge;
static MultiMesh materialization is also separated but remains synchronous; static MultiMesh materialization is also separated but remains synchronous;
headless timing does not measure transform upload GPU cost and spatial-cell headless timing does not measure transform upload GPU cost and spatial-cell
batching still needs culling/performance evidence; typed M2 pending state is batching still needs culling/performance evidence; typed M2 pending state is
separated and dispatch decisions are explicit; static and cached-animation separated and dispatch decisions are explicit; static and cached-animation
observers produce typed per-operation state, while native observation, action observers produce typed per-operation state, including native observation;
execution and root cleanup remain in loader; both ResourceLoader finalize action execution and root cleanup remain in loader; both ResourceLoader finalize
drains remain loader-owned; drains remain loader-owned;
WMO ResourceLoader/FileAccess I/O, live fallback and group materialization WMO ResourceLoader/FileAccess I/O, live fallback and group materialization
remain in the loader; remain in the loader;