render: extract native M2 animation resource observer
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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) |
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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 |
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user