render: extract cached M2 animation resource observer
This commit is contained in:
@@ -44,6 +44,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
|
||||
- `src/render/m2/m2_build_dispatch_planner.gd` - pure animation/static wait, materialization and missing-model advance decision.
|
||||
- `src/render/m2/m2_build_resource_snapshot.gd` - typed per-step animated/static resource observations without engine destruction.
|
||||
- `src/render/m2/m2_static_build_resource_observer.gd` - static Mesh cache lookup, threaded request selection and missing transition.
|
||||
- `src/render/m2/m2_cached_animation_resource_observer.gd` - cached animated GLB eligibility, threaded request admission and snapshot production.
|
||||
- `src/render/m2/m2_animation_load_pipeline_state.gd` - animated M2 threaded-load request records and completion-order finalize FIFO without I/O or Node ownership.
|
||||
- `src/render/m2/m2_mesh_load_pipeline_state.gd` - static M2 threaded-load request records, terminal statuses and completion-order finalize FIFO without I/O or Mesh ownership.
|
||||
- `src/render/m2/m2_mesh_resource_cache_state.gd` - normalized-path prepared static M2 Mesh references with final-shutdown lifetime.
|
||||
@@ -1134,12 +1135,25 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
||||
order. All three historical `m2_animation` metrics still count both stages.
|
||||
- Shutdown still drains pending ResourceLoader paths before clear; map reset and
|
||||
shutdown preserve the same two clear sites.
|
||||
- `StreamingWorldLoader` retains GLB eligibility/cache selection, every
|
||||
- At this extraction stage, `StreamingWorldLoader` retained GLB eligibility/cache selection, every
|
||||
ResourceLoader call, `M2_ANIMATION_FINALIZE` permits, scene instantiation,
|
||||
material repair and prototype/static-fallback decisions.
|
||||
- Cache formats, animation behavior and visible output are unchanged. Synthetic
|
||||
timing is not asset-backed I/O, leak, animation-fidelity or p95/p99 evidence.
|
||||
|
||||
## 2026-07-18 M2 Cached Animation Resource Observer Extraction
|
||||
|
||||
- `M2CachedAnimationResourceObserver` now owns cached animated-prototype lookup,
|
||||
allow/deny matching, historical `.glb` candidate selection, GLB animation/
|
||||
primitive/schema safety validation, request admission and static-only fallback.
|
||||
- The observer produces `M2BuildResourceSnapshot` with the exact borrowed cached
|
||||
prototype or pending state and retains/frees no engine object.
|
||||
- Native GryphonRoost raw-data build/debug logging and all terminal polling,
|
||||
finalization, permits and SceneTree mutation remain loader-owned.
|
||||
- Defaults, path order, accepted empty/`pivot_prefix_v1` schemas, cache format,
|
||||
metrics and visible behavior are unchanged. Generated GLB metadata fixtures
|
||||
are not private asset, leak, p95/p99 or original-client animation evidence.
|
||||
|
||||
## 2026-07-18 M2 Animated Scene Finalizer Extraction
|
||||
|
||||
- `M2AnimatedSceneFinalizer` now owns terminal animated PackedScene candidate
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
| M2 build dispatch planner | Implemented extraction | [`m2-build-dispatch-planner.md`](m2-build-dispatch-planner.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 cached animation resource observer | Implemented extraction | [`m2-cached-animation-resource-observer.md`](m2-cached-animation-resource-observer.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 runtime mesh rebuild classifier | Implemented extraction | [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md) |
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
|
||||
Own cross-frame bookkeeping between a successful animated M2 ResourceLoader
|
||||
request, terminal polling and budgeted main-thread scene finalization. This is
|
||||
an exact state extraction; animation eligibility and loading remain in
|
||||
`StreamingWorldLoader`, while scene finalization and retained Node lifecycle
|
||||
belong to `M2AnimatedSceneFinalizer` and `M2PrototypeCacheState`.
|
||||
an exact state extraction; cached animation eligibility and request admission
|
||||
belong to `M2CachedAnimationResourceObserver`, while scene finalization and
|
||||
retained Node lifecycle belong to `M2AnimatedSceneFinalizer` and
|
||||
`M2PrototypeCacheState`.
|
||||
|
||||
## Non-goals
|
||||
|
||||
@@ -29,8 +30,10 @@ belong to `M2AnimatedSceneFinalizer` and `M2PrototypeCacheState`.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Loader[StreamingWorldLoader] --> IO[ResourceLoader]
|
||||
Loader --> State[M2AnimationLoadPipelineState]
|
||||
Observer[M2CachedAnimationResourceObserver] --> IO[ResourceLoader request]
|
||||
Observer --> State[M2AnimationLoadPipelineState]
|
||||
Loader[StreamingWorldLoader] --> IO
|
||||
Loader --> State
|
||||
State -->|detached pending records| Loader
|
||||
Loader -->|opaque terminal status| State
|
||||
State -->|completion FIFO| Loader
|
||||
@@ -100,12 +103,13 @@ stateDiagram-v2
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant O as CachedAnimationObserver
|
||||
participant L as StreamingWorldLoader
|
||||
participant R as ResourceLoader
|
||||
participant S as M2AnimationLoadPipelineState
|
||||
participant P as M2PrototypeCacheState
|
||||
L->>R: load_threaded_request(GLB)
|
||||
L->>S: remember_request(path, GLB)
|
||||
O->>R: load_threaded_request(GLB)
|
||||
O->>S: remember_request(path, GLB)
|
||||
loop frames
|
||||
L->>S: request_records_snapshot()
|
||||
L->>R: load_threaded_get_status(GLB)
|
||||
@@ -128,8 +132,10 @@ sequenceDiagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Observer[M2CachedAnimationResourceObserver] --> State[M2AnimationLoadPipelineState]
|
||||
Observer --> Resource[ResourceLoader request]
|
||||
Loader[StreamingWorldLoader] --> State[M2AnimationLoadPipelineState]
|
||||
Loader --> Resource[ResourceLoader]
|
||||
Loader --> Resource
|
||||
Loader --> Budget[RenderBudgetScheduler]
|
||||
Loader --> Prototype[M2PrototypeCacheState]
|
||||
State -. no dependency .-> Resource
|
||||
@@ -142,7 +148,7 @@ flowchart TB
|
||||
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Empty/duplicate request | State guard | Reject unchanged | Contract verifier | Correct caller or request later |
|
||||
| Request start/cache miss | Loader | No insertion; mark static | Existing loader path | Cache correction/reload |
|
||||
| Request start/cache miss | Cached observer | No insertion; mark static | Observer contract | Cache correction/reload |
|
||||
| Non-terminal status | Loader | Keep pending | Existing metric | Poll next frame |
|
||||
| Failed terminal load | Popped status | Loader marks static | Existing behavior | Future map/session reload |
|
||||
| Empty defensive path | Loader poll | Discard and mark static | Source contract | Correct producer |
|
||||
@@ -154,7 +160,7 @@ flowchart TB
|
||||
|---|---|---|---|---|
|
||||
| `enable_m2_animated_instances` | `true` | Existing renderer profile | Yes | Enables caller request path |
|
||||
| `m2_animation_finalize_ops_per_tick` | `1` | Quality/custom | Yes | Bounds caller FIFO drain |
|
||||
| Animated allow/deny/primitive rules | Existing values | Existing renderer profile | Yes | Filter before state insertion |
|
||||
| Animated allow/deny/primitive rules | Existing values | Existing renderer profile | Yes | Observer filters before state insertion |
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
@@ -188,7 +194,8 @@ sibling service.
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Animated request/finalize state | Implemented extraction | Synthetic contract/source/timing verifier | Asset-backed traversal/leak/p95/p99 pending |
|
||||
| ResourceLoader and GLB selection | Existing loader-owned | Adjacent renderer tests | I/O adapter extraction optional |
|
||||
| Cached request admission and GLB selection | Implemented in observer | Policy/GLB/source verifier | Asset-backed traversal pending |
|
||||
| Terminal ResourceLoader polling | Existing loader-owned | Shutdown/finalizer regressions | I/O adapter extraction optional |
|
||||
| Animated prototype outcomes | Implemented extraction | Prototype cache verifier | Asset-backed animation fidelity pending |
|
||||
|
||||
## Known gaps and risks
|
||||
@@ -202,9 +209,10 @@ sibling service.
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending records, completion FIFO and metrics |
|
||||
| `src/render/m2/m2_cached_animation_resource_observer.gd` | Eligibility, GLB selection and request admission |
|
||||
| `src/render/m2/m2_animated_scene_finalizer.gd` | Candidate instantiation, material repair and player validation |
|
||||
| `src/render/m2/m2_prototype_cache_state.gd` | Animated prototype/static-only outcomes |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Eligibility, I/O, permits, instantiation and adoption |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Native eligibility/build, polling, permits and adoption |
|
||||
| `src/tools/verify_m2_animation_load_pipeline_state.gd` | Lifecycle/boundary/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
@@ -104,7 +104,8 @@ sequenceDiagram
|
||||
- The planner owns only call-local scalar values and the returned Dictionary.
|
||||
- `M2BuildQueue` owns typed jobs, FIFO ordering, serial numbers and group/offset
|
||||
cursors. `M2BuildDispatchPlanner` owns the pure resource-state action decision;
|
||||
the loader owns tile checks, resource observation/retry and adoption calls.
|
||||
the loader owns tile checks and native-first orchestration, while static and
|
||||
cached animation observers own their observation/retry phases.
|
||||
- Materializers own main-thread Node/MultiMesh construction under loader roots.
|
||||
- The scheduler owns the frame-local `M2_BUILD` counter.
|
||||
- Pure planning is thread-safe, though the current adapter calls it on main thread.
|
||||
@@ -150,8 +151,8 @@ queue depth, build activity and hitch observability.
|
||||
|
||||
## Extension points
|
||||
|
||||
- A later package may extract resource observation/request orchestration while
|
||||
retaining the dispatch and typed build-job/FIFO contracts.
|
||||
- Remaining native resource observation may be extracted while retaining the
|
||||
dispatch and typed build-job/FIFO contracts.
|
||||
- Spatial-cell batching must use measured culling/performance evidence and must
|
||||
not silently change this model-path batch cursor.
|
||||
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
Select one M2 build action after the loader observes animation/static resource
|
||||
state. The planner makes wait priority, materializer selection and the historical
|
||||
missing-model serial transition explicit without owning resources or engine work.
|
||||
Select one M2 build action after renderer observers produce animation/static
|
||||
resource state. The planner makes wait priority, materializer selection and the
|
||||
historical missing-model serial transition explicit without owning resources or
|
||||
engine work.
|
||||
|
||||
## Non-goals
|
||||
|
||||
@@ -146,7 +147,8 @@ flowchart TB
|
||||
- Planner owns call-local comparisons and the returned Dictionary only.
|
||||
- `M2BuildResourceSnapshot` owns the typed call-local observation contract;
|
||||
dispatch reads availability/pending/missing accessors but no engine references.
|
||||
- Loader owns resource lookup/request 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.
|
||||
- `M2BuildQueue` owns pending jobs, FIFO keys and progress cursors.
|
||||
- Materializers own main-thread scene construction under loader-owned roots.
|
||||
- Pure calls are thread-safe; the current loader adapter calls on main thread.
|
||||
@@ -190,16 +192,17 @@ remain unchanged.
|
||||
|
||||
## Extension points
|
||||
|
||||
A later package may move resource observation/request execution behind a service
|
||||
that produces `M2BuildResourceSnapshot`. Generic callbacks, signals and a shared
|
||||
state-machine framework remain intentionally excluded.
|
||||
Static and cached animated observation/request execution now live behind sibling
|
||||
services that produce `M2BuildResourceSnapshot`. Native animation remains an
|
||||
orchestrator-owned phase. Generic callbacks, signals and a shared state-machine
|
||||
framework remain intentionally excluded.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| M2 build action selection | Implemented extraction | Priority/matrix/source/timing verifier | Asset-backed traversal pending |
|
||||
| Resource observation and requests | Loader-owned | Cache/pipeline regressions | Typed orchestration extraction pending |
|
||||
| Static/cached animation observation and requests | Implemented separately | Observer/cache/pipeline regressions | Native observation remains loader-owned |
|
||||
| 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 |
|
||||
|
||||
@@ -215,7 +218,8 @@ state-machine framework remain intentionally excluded.
|
||||
|---|---|
|
||||
| `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/scenes/streaming/streaming_world_loader.gd` | Resource observation, action execution, permits and engine lifetime |
|
||||
| `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_build_batch_planner.gd` | Batch count and cursor plan |
|
||||
| `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 |
|
||||
|
||||
@@ -28,7 +28,9 @@ static Mesh and terminal missing-model state. The snapshot never owns engine lif
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Loader[StreamingWorldLoader observations] --> Snapshot[M2BuildResourceSnapshot]
|
||||
Native[StreamingWorldLoader native observation] --> Snapshot[M2BuildResourceSnapshot]
|
||||
Cached[M2CachedAnimationResourceObserver] --> Snapshot
|
||||
Static[M2StaticBuildResourceObserver] --> Snapshot
|
||||
Snapshot --> Dispatch[M2BuildDispatchPlanner]
|
||||
Snapshot --> Loader
|
||||
Loader --> Animated[M2AnimatedInstanceMaterializer]
|
||||
@@ -60,8 +62,8 @@ are forbidden.
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Normalized relative path | Loader path adapter | Snapshot/diagnostics | Copied String | Snapshot lifetime |
|
||||
| Input | Optional animated prototype | Prototype/native animation path | Snapshot/materializer adapter | Borrowed exact Node3D reference | One build operation |
|
||||
| Input | Animation request pending | Animation pipeline state | Snapshot/dispatch planner | Boolean copy | One build operation |
|
||||
| Input | Optional animated prototype | Native/cached animation observer | Snapshot/materializer adapter | Borrowed exact Node3D reference | One build operation |
|
||||
| Input | Animation request pending | Cached animation observer | Snapshot/dispatch planner | Boolean copy | One build operation |
|
||||
| Input | Optional prepared static Mesh | Mesh cache/request path | Snapshot/materializer adapter | Borrowed exact Mesh reference | One build operation |
|
||||
| Input | Static model terminally missing | Prototype cache state | Snapshot/dispatch planner | Boolean copy | One build operation |
|
||||
| Output | Availability/pending/missing values | Snapshot | Dispatch planner | Scalar reads | One dispatch plan |
|
||||
@@ -110,8 +112,12 @@ sequenceDiagram
|
||||
participant B as M2BuildBatchPlanner
|
||||
participant D as M2BuildDispatchPlanner
|
||||
participant M as M2 materializer
|
||||
L->>L: resolve/request animated prototype
|
||||
L->>S: construct(path, prototype, pending)
|
||||
L->>L: resolve native animated prototype
|
||||
L->>S: construct native result
|
||||
opt no native prototype
|
||||
L->>L: delegate cached animation observer
|
||||
L->>S: receive cached/pending snapshot
|
||||
end
|
||||
alt animation not pending
|
||||
L->>B: plan batch using snapshot availability
|
||||
opt positive static batch
|
||||
@@ -143,7 +149,8 @@ flowchart TB
|
||||
## Ownership, threading and resources
|
||||
|
||||
- Snapshot owns only copied scalar state and temporary references.
|
||||
- Prototype/cache services retain resource state; loader owns observation order.
|
||||
- Prototype/cache services retain resource state; loader owns native-first
|
||||
observation order while cached/static observers own their phases.
|
||||
- Scene roots remain loader/tile-owned; Mesh lifetime follows cache/resource refs.
|
||||
- Dispatch planner reads only snapshot accessors and never mutates the snapshot.
|
||||
- Current construction/adoption occurs on renderer main thread.
|
||||
@@ -187,9 +194,10 @@ queue/hitch metrics remain unchanged.
|
||||
|
||||
## Extension points
|
||||
|
||||
`M2StaticBuildResourceObserver` now produces the static phase. A later animated
|
||||
observer may produce the first phase. The value contract remains independent of
|
||||
both producers and of generic callback frameworks.
|
||||
`M2CachedAnimationResourceObserver` produces the cached animated phase and
|
||||
`M2StaticBuildResourceObserver` produces the static phase. Native animation
|
||||
observation remains loader-owned. The value contract stays independent of all
|
||||
producers and of generic callback frameworks.
|
||||
|
||||
## Capability status
|
||||
|
||||
@@ -198,14 +206,15 @@ both producers and of generic callback frameworks.
|
||||
| Typed per-step M2 resource observation | Implemented extraction | Identity/adoption/lifetime/source/timing 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 |
|
||||
| Animated lookup/request execution | Loader-owned | Animation regressions | Service extraction 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 |
|
||||
| Engine lifetime/materialization | Loader/materializer-owned | Lifetime/materializer regressions | GPU/p95/p99 evidence pending |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- Static observation is mutable during its short two-phase construction.
|
||||
- Raw Node3D/Mesh references remain necessary for current materializer APIs.
|
||||
- Loader still owns synchronous/native prototype attempts and ResourceLoader requests.
|
||||
- Loader still owns synchronous native prototype attempts and terminal polling.
|
||||
- Synthetic timing does not measure resource, Node or GPU work.
|
||||
|
||||
## Source map
|
||||
@@ -215,7 +224,8 @@ both producers and of generic callback frameworks.
|
||||
| `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step resource observations and diagnostics |
|
||||
| `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/scenes/streaming/streaming_world_loader.gd` | Animated observation/request execution and materializer borrowing |
|
||||
| `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/tools/verify_m2_build_resource_snapshot.gd` | Identity/adoption/lifetime/boundary/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
# M2 Cached Animation Resource Observer
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented extraction |
|
||||
| Target/work package | M03 / `M03-RND-M2-CACHED-ANIMATION-RESOURCE-OBSERVER-001` |
|
||||
| Owners | Cached animated M2 eligibility, request admission and initial snapshot |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-cached-animation-resource-observer`, 2026-07-18 |
|
||||
| Profiles/capabilities | Existing optional cached-GLB animated M2 path |
|
||||
|
||||
## Purpose
|
||||
|
||||
Produce the cached-animation phase of `M2BuildResourceSnapshot`: reuse an exact
|
||||
animated prototype, wait for a pending request, admit the first safe GLB request,
|
||||
or record a terminal static-only animation outcome without changing behavior.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Load/build native GryphonRoost animation or emit its diagnostics.
|
||||
- Poll/finalize threaded Resources or instantiate/fix imported scenes.
|
||||
- Own/free prototype Nodes, snapshots, pipeline entries or cache state.
|
||||
- Materialize instances, consume permits or change animation policy defaults.
|
||||
- Change GLB layout, cache format, batching, profiles or visible output.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Loader[StreamingWorldLoader] --> Native[Native animation attempt]
|
||||
Native -->|no prototype| Observer[M2CachedAnimationResourceObserver]
|
||||
Prototype[M2PrototypeCacheState] --> Observer
|
||||
Pipeline[M2AnimationLoadPipelineState] --> Observer
|
||||
Resource[ResourceLoader and GLB JSON] --> Observer
|
||||
Observer --> Snapshot[M2BuildResourceSnapshot]
|
||||
Snapshot --> Dispatch[M2BuildDispatchPlanner]
|
||||
```
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Failure behavior |
|
||||
|---|---|---|---|
|
||||
| `observe(path, cache_dir, max_primitives, allow, deny, prototype_state, pipeline_state, debug)` | Command/query | Produce cached prototype/pending/static-only snapshot | Invalid input returns empty non-pending snapshot |
|
||||
| `find_eligible_glb_cache_path(...)` | I/O query | Select first existing safe historical candidate | Returns empty String |
|
||||
| `is_animation_path_allowed(path, allow, deny)` | Pure query | Apply stripped case-insensitive substring policy | Empty path/allowlist rejects; deny wins |
|
||||
| `cache_resource_paths(cache_dir, path)` | Pure query | Return nested/lowercase/basename GLB candidates | Empty values yield fewer candidates |
|
||||
| `glb_cache_is_safe_for_runtime_animation(path, max, debug)` | I/O query | Validate animations, primitive limit and schema | Invalid/missing GLB rejects |
|
||||
| `read_glb_json(path)` | I/O query | Read version-2 GLB JSON chunk | Returns empty Dictionary |
|
||||
| `glb_primitive_count(gltf)` | Pure query | Count primitives across meshes | Invalid entries contribute zero |
|
||||
| `glb_animation_schema(gltf)` | Pure query | Read OpenWC schema marker | Missing marker returns empty String |
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Data | Producer | Consumer | Ownership/lifetime |
|
||||
|---|---|---|---|---|
|
||||
| Input | Normalized M2 path/cache directory | Loader | Observer | Copied Strings; one observation |
|
||||
| Input | Primitive cap and allow/deny patterns | Loader exports | Observer | Copied/read-only call values |
|
||||
| Input | Prototype and request states | M2 state services | Observer | Borrowed services; map/session |
|
||||
| Input | Existing GLB cache file | Offline cache pipeline | Observer | Read-only file access |
|
||||
| Output | Resource snapshot | Observer | Loader/dispatch/materializer | Caller-owned snapshot; one build step |
|
||||
| Output | Borrowed exact prototype | Prototype state | Snapshot/materializer | No ownership transfer |
|
||||
| Output | Threaded request record | Observer | Animation pipeline | Pipeline-owned copied paths |
|
||||
| Output | Static-only transition | Observer | Prototype state | State-owned copied path |
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[Observe normalized path] --> Valid{Valid composition?}
|
||||
Valid -->|no| Empty[Empty non-pending snapshot]
|
||||
Valid -->|yes| Cached{Prototype cached?}
|
||||
Cached -->|yes| Ready[Snapshot with exact prototype]
|
||||
Cached -->|no| Static{Already static-only?}
|
||||
Static -->|yes| Empty
|
||||
Static -->|no| Pending{Request exists?}
|
||||
Pending -->|yes| Wait[Pending snapshot]
|
||||
Pending -->|no| Policy{Allow and not deny?}
|
||||
Policy -->|no| Mark[Mark static-only]
|
||||
Policy -->|yes| Candidate[Historical GLB candidates]
|
||||
Candidate --> Safe{Animations, primitive cap, schema safe?}
|
||||
Safe -->|no usable| Mark
|
||||
Safe -->|yes| Request[Threaded request]
|
||||
Request -->|OK or busy| Remember[Remember and return pending snapshot]
|
||||
Request -->|error| Mark
|
||||
Mark --> Empty
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Observing
|
||||
Observing --> Cached
|
||||
Observing --> Pending
|
||||
Observing --> Requested
|
||||
Observing --> StaticOnly
|
||||
Observing --> Rejected
|
||||
Cached --> [*]
|
||||
Pending --> [*]
|
||||
Requested --> [*]
|
||||
StaticOnly --> [*]
|
||||
Rejected --> [*]
|
||||
```
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant L as StreamingWorldLoader
|
||||
participant O as CachedAnimationObserver
|
||||
participant C as PrototypeCacheState
|
||||
participant P as AnimationLoadPipelineState
|
||||
participant R as ResourceLoader
|
||||
participant S as M2BuildResourceSnapshot
|
||||
L->>L: attempt native candidate
|
||||
alt no native prototype
|
||||
L->>O: observe(path, cache/policy/state)
|
||||
O->>C: find animated / static-only
|
||||
O->>P: has request
|
||||
opt eligible cache candidate
|
||||
O->>R: exists + GLB JSON + threaded request
|
||||
O->>P: remember request
|
||||
end
|
||||
O-->>L: snapshot
|
||||
else native prototype
|
||||
L->>S: construct native snapshot
|
||||
end
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Observer[M2CachedAnimationResourceObserver] --> Snapshot[M2BuildResourceSnapshot]
|
||||
Observer --> Prototype[M2PrototypeCacheState]
|
||||
Observer --> Pipeline[M2AnimationLoadPipelineState]
|
||||
Observer --> ResourceLoader
|
||||
Observer --> FileAccess[GLB header and JSON]
|
||||
Observer -. no dependency .-> Native[M2 native builder/repository]
|
||||
Observer -. no dependency .-> Finalizer[M2AnimatedSceneFinalizer]
|
||||
Observer -. no dependency .-> Scheduler[RenderBudgetScheduler]
|
||||
Observer -. no dependency .-> SceneTree
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- Observer retains no service, request, snapshot, Resource or Node reference.
|
||||
- Prototype state retains accepted Nodes and static-only paths until shutdown.
|
||||
- Pipeline state retains copied request paths until terminal polling/finalize.
|
||||
- Snapshot borrows the exact cached prototype; observer never frees it.
|
||||
- Resource existence/request and synchronous GLB JSON inspection run on the
|
||||
renderer main thread, matching the previous loader behavior.
|
||||
- Loader remains owner of native animation build, terminal polling/finalize,
|
||||
permits, materialization and every SceneTree mutation.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure/state | Behavior | Recovery |
|
||||
|---|---|---|
|
||||
| Empty path/dependency | Return empty non-pending snapshot without mutation | Correct composition |
|
||||
| Cached prototype | Return exact reference immediately | None |
|
||||
| Existing request | Return pending without duplicate admission | Retry after terminal drain |
|
||||
| Disallowed/denied path | Mark static-only | New loader session/configuration |
|
||||
| Missing/invalid/unsafe GLB | Continue candidates, then mark static-only | Repair/rebake cache and restart session |
|
||||
| Request error | Mark static-only | New loader session/cache repair |
|
||||
| Tile cancellation | Observer retains nothing | Loader queue remains authoritative |
|
||||
| Shutdown | Loader drains requests before state clear | New loader starts empty |
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
The service consumes existing `m2_cache_dir`, `m2_animated_max_primitives`,
|
||||
`m2_animated_allowlist_patterns`, `m2_animated_denylist_patterns` and debug flag.
|
||||
It adds no setting and preserves runtime mutability and defaults.
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
No persistence or cache format changes are introduced. Existing `.glb` candidate
|
||||
layout and OpenWC schema markers are read unchanged; no rebake is required.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
Unsupported non-empty schemas preserve the historical optional
|
||||
`M2_ANIM_REJECT` debug line. Snapshot and pipeline diagnostics remain detached;
|
||||
queue and hitch metrics are unchanged.
|
||||
|
||||
## Verification
|
||||
|
||||
- Dedicated verifier covers invalid/cached identity/static/pending/missing
|
||||
lifecycle, allow/deny semantics, exact path order, generated GLB header/JSON,
|
||||
animation presence, primitive cap, accepted/rejected schemas, source boundaries
|
||||
and bounded timing.
|
||||
- Snapshot, dispatch, pipeline, prototype, shutdown, facade, internal-access and
|
||||
checkpoint regressions protect adjacent behavior.
|
||||
- Fidelity evidence is exact policy/I/O extraction. No original-client visual or
|
||||
animation parity claim is made.
|
||||
|
||||
## Extension points
|
||||
|
||||
Native animation observation can become a sibling service without changing this
|
||||
cached-GLB contract. Terminal ResourceLoader polling/finalization may later move
|
||||
behind a dedicated adapter while retaining the pipeline state.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| 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 |
|
||||
| Animated finalize/preparation | Loader/finalizer split | Pipeline/finalizer regressions | Polling adapter optional |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- A successful asynchronous request is not started by the unit fixture because
|
||||
leaving it undrained leaks process work; source and pipeline tests cover admission.
|
||||
- Generated GLB metadata fixtures validate policy, not Godot import fidelity.
|
||||
- Private traversal, leak/descriptor pressure, p95/p99 and paired visuals remain
|
||||
unavailable without private assets.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/m2/m2_cached_animation_resource_observer.gd` | Cached GLB policy, request and snapshot production |
|
||||
| `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_prototype_cache_state.gd` | Prototype and static-only outcomes |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Native attempt, composition, finalize and execution |
|
||||
| `src/tools/verify_m2_cached_animation_resource_observer.gd` | Lifecycle/policy/GLB/boundary/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md)
|
||||
- [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md)
|
||||
- [`m2-prototype-cache-state.md`](m2-prototype-cache-state.md)
|
||||
- [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.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)
|
||||
@@ -28,7 +28,9 @@ paths and paths whose animation fallback is static-only.
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Loader[StreamingWorldLoader] --> State[M2PrototypeCacheState]
|
||||
Observer[M2 resource observers] --> State
|
||||
Resource[ResourceLoader / raw builders] --> Loader
|
||||
Resource --> Observer
|
||||
State --> Static[Static prototype Node3D]
|
||||
State --> Animated[Animated prototype Node3D]
|
||||
State --> Missing[Missing-model path set]
|
||||
@@ -62,7 +64,7 @@ Mesh traversal and other application layers are forbidden.
|
||||
| Input | Normalized non-empty M2 path | Loader normalization | Cache state | Copied String key | Shutdown lifetime |
|
||||
| Input | Detached static Node3D | Cached scene/raw builder adapter | Cache state | Adopted on success | Until final shutdown |
|
||||
| Input | Detached animated Node3D | GLB/native animation adapter | Cache state | Adopted on success | Until final shutdown |
|
||||
| Input | Missing/static-only outcome | Loader failure/fallback adapter | Cache state | Boolean set entry | Until final shutdown |
|
||||
| Input | Missing/static-only outcome | Loader/observer failure adapter | Cache state | Boolean set entry | Until final shutdown |
|
||||
| Output | Canonical prototype Node3D | Cache state | Loader instance/material adapter | Borrowed exact reference | One lookup/use |
|
||||
| Output | Detached path-only snapshot | Cache state | Tests/diagnostics | Fresh caller-owned arrays | One query |
|
||||
|
||||
@@ -135,9 +137,10 @@ sequenceDiagram
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> State[M2PrototypeCacheState]
|
||||
Observer[M2 resource observers] --> State
|
||||
State --> Node3D
|
||||
Loader --> Raw[M2RawModelRepository]
|
||||
Loader --> ResourceLoader
|
||||
Observer --> ResourceLoader
|
||||
Loader --> StaticBuilder[M2Builder]
|
||||
Loader --> AnimatedBuilder[M2NativeAnimatedBuilder]
|
||||
State -. no dependency .-> Raw
|
||||
@@ -165,7 +168,7 @@ flowchart TB
|
||||
| Unknown positive lookup | Map miss | Return null | Path snapshot | Loader continues existing load/fallback |
|
||||
| Duplicate candidate | Occupied valid path | Release candidate; return first | Identity fixture | None required |
|
||||
| Model source/load failure | Loader result | Mark missing outcome | Existing fallback behavior | New loader session/source repair |
|
||||
| Animation unavailable/unsafe | Loader policy/result | Mark static-only outcome | Existing fallback behavior | New loader session/cache repair |
|
||||
| Animation unavailable/unsafe | Observer/native result | Mark static-only outcome | Existing fallback behavior | New loader session/cache repair |
|
||||
| Shutdown | Loader lifecycle | Release positive and clear all state | Shutdown verifier | New loader begins empty |
|
||||
| Cancellation | Not owned | No state transition inside this service | N/A | Loader drains/cancels before shutdown clear |
|
||||
|
||||
@@ -177,8 +180,8 @@ flowchart TB
|
||||
| Negative cache lifetime | Final loader shutdown | All | No | Preserves historical fallback suppression |
|
||||
| Eviction capacity | Unbounded historical behavior | All | No | No mid-session Node destruction |
|
||||
|
||||
Animation enablement, candidate/allow/deny rules, cache paths and per-frame
|
||||
permits remain loader configuration.
|
||||
Animation enablement, candidate/allow/deny rules and cache paths remain loader
|
||||
configuration consumed by the cached observer. Per-frame permits remain loader-owned.
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
@@ -188,7 +191,8 @@ and native M2 formats are unchanged; no migration or rebake is introduced.
|
||||
## Diagnostics and observability
|
||||
|
||||
- `diagnostic_snapshot` exposes four sorted path arrays without Node references.
|
||||
- Existing `M2_ANIM_CACHE` and native animation logs remain loader-owned.
|
||||
- Cached eligibility rejection logging belongs to the cached observer; native
|
||||
animation logs remain loader-owned.
|
||||
- Existing renderer queue metrics remain unchanged because these tables never
|
||||
contributed work counts.
|
||||
- Normalized relative path remains the correlation key.
|
||||
@@ -208,8 +212,8 @@ and native M2 formats are unchanged; no migration or rebake is introduced.
|
||||
## Extension points
|
||||
|
||||
Eviction or byte/count budgets require measured memory evidence and explicit
|
||||
prototype-user lifetime rules. Animation request-state extraction can consume
|
||||
this service without moving ResourceLoader or builder ownership into it.
|
||||
prototype-user lifetime rules. Cached animation observation consumes this
|
||||
service without moving ResourceLoader or builder ownership into prototype state.
|
||||
|
||||
## Capability status
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
||||
| `M2BuildDispatchPlanner.plan_step` | Internal pure M2 service | Selects wait, animated/static materialization or no-Node advancement from observed resource state | Main/any thread; stateless | Pending animation has priority; unresolved static Mesh waits |
|
||||
| `M2BuildResourceSnapshot` | Internal M2 value contract | Carries per-step normalized path, animated/static references and pending/missing observations | Renderer main thread; one build operation | Values retained exactly; release never frees resources |
|
||||
| `M2StaticBuildResourceObserver.observe` | Internal M2 I/O service | Produces cached/pending/requested/missing static snapshot phase | Renderer main thread; stateless | Invalid composition rejected; no candidate marks missing |
|
||||
| `M2CachedAnimationResourceObserver.observe` | Internal M2 I/O service | Produces cached/pending/static-only animated snapshot phase | Renderer main thread; stateless | Invalid composition returns empty snapshot; no safe candidate marks static-only |
|
||||
| `M2BuildQueue` / `M2BuildJob` | Internal M2 pending-state service | Own typed root/groups/cursor jobs and FIFO/stale tile keys | Renderer main thread; map session | Invalid enqueue rejected; stale keys drain independently of jobs |
|
||||
| `M2StaticBatchMaterializer.materialize_batch` | Internal M2 scene-materialization service | Builds and attaches one prepared-Mesh MultiMesh transform slice | Renderer main thread; stateless after each call | Invalid/empty input returns null; bounds are caller precondition |
|
||||
| `M2RuntimeMeshRebuildClassifier` | Internal memoized M2 service | Detects billboard/UV-rotation metadata requiring stale cached-mesh rebuild | Renderer main thread; cached until reset | Invalid variants/indices skipped; first path decision wins |
|
||||
@@ -193,7 +194,7 @@ loader configuration remains transitional composition data, not a caller API.
|
||||
| Internal grouping | Tile origin, M2 names and placements | Loader / `M2PlacementGrouper` | Loader worker result/build job | Fresh Dictionary/Transform3D arrays | One grouping task |
|
||||
| Internal batch plan | Transform count/offset, path kind and limits | Loader / `M2BuildBatchPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One build operation |
|
||||
| Internal M2 dispatch plan | Batch count and observed animation/static resource state | Loader / `M2BuildDispatchPlanner` | Loader queue/materializer/progress adapter | Fresh scalar/action Dictionary | One build operation |
|
||||
| Internal M2 resource observation | Normalized path, optional prototype/Mesh and pending/missing flags | Loader / `M2BuildResourceSnapshot` | Dispatch planner and loader materializer adapter | Snapshot borrows exact engine references | One build operation |
|
||||
| Internal M2 resource observation | Normalized path, optional prototype/Mesh and pending/missing flags | Loader / M2 observers / `M2BuildResourceSnapshot` | Dispatch planner and loader materializer adapter | Snapshot borrows exact engine references | One build operation |
|
||||
| Internal M2 pending build | Tile key, M2 root, grouped transforms and cursors | Loader / `M2BuildQueue` | Loader readiness, planner and materializer adapters | Queue-owned job/keys and strong references | Until finish/cancel/clear/replacement |
|
||||
| Internal static M2 materialization | Parent, prepared Mesh, ordered transform slice and render settings | Loader / `M2StaticBatchMaterializer` | Attached MultiMeshInstance3D | Parent owns node/MultiMesh; exact Mesh reference retained | One main-thread build batch |
|
||||
| Internal WMO placement | Path, MODF placement, tile/index | Loader / `WmoPlacementResolver` | WMO caches, registry and three instance adapters | Value-only String/Transform3D | Lookup/placement lifetime |
|
||||
@@ -204,7 +205,7 @@ loader configuration remains transitional composition data, not a caller API.
|
||||
| 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 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 animated M2 load | Normalized path, cached GLB path and opaque terminal status | 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 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 materialization | Parent, prototype, ordered transform slice and render settings | Loader / `M2AnimatedInstanceMaterializer` | Attached animated batch and indexed diagnostics | Parent owns batch; caller owns detached diagnostics | One main-thread build batch |
|
||||
@@ -395,9 +396,11 @@ sequenceDiagram
|
||||
`M2BuildQueue` owns typed pending jobs, FIFO/stale keys, grouped-transform
|
||||
references and group/offset/serial cursors without freeing engine objects.
|
||||
`M2StaticBuildResourceObserver` owns static Mesh lookup/request admission and
|
||||
snapshot adoption. `M2StaticBatchMaterializer` owns static MultiMesh
|
||||
construction/attachment; the loader retains animated observation/requests,
|
||||
action execution, cursor adoption, root cleanup, budgets and Editor ownership.
|
||||
snapshot adoption. `M2CachedAnimationResourceObserver` owns cached animated
|
||||
prototype lookup, GLB policy/request admission and snapshot production.
|
||||
`M2StaticBatchMaterializer` owns static MultiMesh construction/attachment; the
|
||||
loader retains native animation observation, action execution, cursor adoption,
|
||||
root cleanup, budgets and Editor ownership.
|
||||
- `WmoPlacementResolver` is stateless and owns only call-local cache-key,
|
||||
identity and transform values. `WmoPlacementRegistry` owns only placement-key
|
||||
reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation
|
||||
@@ -422,6 +425,11 @@ sequenceDiagram
|
||||
cache path selection, request admission and initial snapshot adoption. The
|
||||
loader retains ResourceLoader polling/finalize, permits and terminal adoption;
|
||||
prototype cache state owns shared missing outcomes.
|
||||
- `M2AnimationLoadPipelineState` owns animated M2 pending Resource paths, opaque
|
||||
terminal statuses and completion-order finalize FIFO. The cached animation
|
||||
observer owns allow/deny/path/GLB selection, request admission and initial
|
||||
snapshot production. The loader retains native animation build, ResourceLoader
|
||||
polling/finalize, permits and terminal adoption.
|
||||
- `M2MeshResourceCacheState` owns prepared static Mesh references and releases
|
||||
them at the existing final-shutdown site. Prototype state and materialization
|
||||
belong to the sibling cache service and loader respectively.
|
||||
@@ -584,6 +592,7 @@ 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 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 cached animation resource observer | Implemented extraction | Cache/pending/policy/GLB/request/source/timing contract | Native observation and asset-backed traversal 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 animated scene finalizer | Implemented extraction | Synthetic type/lifetime/material/player/source/timing contract | Asset-backed GLB traversal/material/animation comparison pending |
|
||||
@@ -658,6 +667,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| `src/render/m2/m2_build_dispatch_planner.gd` | M2 wait/materializer/advance action priority and transition plan |
|
||||
| `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step animated/static observations and borrowed references |
|
||||
| `src/render/m2/m2_static_build_resource_observer.gd` | Static Mesh lookup/request/missing snapshot production |
|
||||
| `src/render/m2/m2_cached_animation_resource_observer.gd` | Cached animated GLB policy/request/snapshot production |
|
||||
| `src/render/m2/m2_build_job.gd` | M2 root/groups references and group/offset/serial progress |
|
||||
| `src/render/m2/m2_build_queue.gd` | Keyed pending jobs and FIFO/stale tile-key lifecycle |
|
||||
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Static M2 pending Resource paths, terminal statuses and finalize FIFO |
|
||||
@@ -689,6 +699,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| `src/tools/verify_m2_build_dispatch_planner.gd` | M2 dispatch priority/action/transition/boundary/timing regression |
|
||||
| `src/tools/verify_m2_build_resource_snapshot.gd` | M2 observation identity/adoption/lifetime/boundary/timing regression |
|
||||
| `src/tools/verify_m2_static_build_resource_observer.gd` | Static lookup/request/path/boundary/timing regression |
|
||||
| `src/tools/verify_m2_cached_animation_resource_observer.gd` | Cached animation lifecycle/policy/GLB/boundary/timing regression |
|
||||
| `src/tools/verify_m2_build_queue.gd` | M2 job/FIFO/progress/lifetime/boundary/timing regression |
|
||||
| `src/tools/verify_m2_mesh_load_pipeline_state.gd` | Static M2 request/terminal/FIFO/boundary/timing regression |
|
||||
| `src/tools/verify_m2_mesh_resource_cache_state.gd` | Static M2 Mesh cache ownership/lifetime/boundary/timing regression |
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
class_name M2CachedAnimationResourceObserver
|
||||
extends RefCounted
|
||||
|
||||
## Observes or requests the cached-GLB animated phase of one M2 build step.
|
||||
|
||||
const M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_build_resource_snapshot.gd"
|
||||
)
|
||||
|
||||
|
||||
## Returns a snapshot containing the exact cached prototype or pending state.
|
||||
## Invalid composition and terminal failures produce a non-pending snapshot.
|
||||
func observe(
|
||||
normalized_relative_path: String,
|
||||
cache_directory: String,
|
||||
maximum_primitive_count: int,
|
||||
allowlist_patterns: PackedStringArray,
|
||||
denylist_patterns: PackedStringArray,
|
||||
prototype_cache_state: RefCounted,
|
||||
load_pipeline_state: RefCounted,
|
||||
debug_logging_enabled: bool = false
|
||||
) -> RefCounted:
|
||||
var empty_snapshot: RefCounted = M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
|
||||
normalized_relative_path,
|
||||
null,
|
||||
false
|
||||
)
|
||||
if (
|
||||
normalized_relative_path.is_empty()
|
||||
or prototype_cache_state == null
|
||||
or load_pipeline_state == null
|
||||
):
|
||||
return empty_snapshot
|
||||
|
||||
var cached_prototype := prototype_cache_state.call(
|
||||
"find_animated_prototype",
|
||||
normalized_relative_path
|
||||
) as Node3D
|
||||
if cached_prototype != null:
|
||||
return M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
|
||||
normalized_relative_path,
|
||||
cached_prototype,
|
||||
false
|
||||
)
|
||||
if bool(prototype_cache_state.call(
|
||||
"is_animation_static",
|
||||
normalized_relative_path
|
||||
)):
|
||||
return empty_snapshot
|
||||
if bool(load_pipeline_state.call("has_request", normalized_relative_path)):
|
||||
return M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
|
||||
normalized_relative_path,
|
||||
null,
|
||||
true
|
||||
)
|
||||
|
||||
var cache_resource_path := find_eligible_glb_cache_path(
|
||||
normalized_relative_path,
|
||||
cache_directory,
|
||||
maximum_primitive_count,
|
||||
allowlist_patterns,
|
||||
denylist_patterns,
|
||||
debug_logging_enabled
|
||||
)
|
||||
if cache_resource_path.is_empty():
|
||||
prototype_cache_state.call(
|
||||
"mark_animation_static",
|
||||
normalized_relative_path
|
||||
)
|
||||
return empty_snapshot
|
||||
|
||||
var request_error := ResourceLoader.load_threaded_request(
|
||||
cache_resource_path,
|
||||
"",
|
||||
false,
|
||||
ResourceLoader.CACHE_MODE_REUSE
|
||||
)
|
||||
if request_error == OK or request_error == ERR_BUSY:
|
||||
load_pipeline_state.call(
|
||||
"remember_request",
|
||||
normalized_relative_path,
|
||||
cache_resource_path
|
||||
)
|
||||
return M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
|
||||
normalized_relative_path,
|
||||
null,
|
||||
true
|
||||
)
|
||||
|
||||
prototype_cache_state.call("mark_animation_static", normalized_relative_path)
|
||||
return empty_snapshot
|
||||
|
||||
|
||||
## Returns the first eligible historical `.glb` cache candidate.
|
||||
func find_eligible_glb_cache_path(
|
||||
normalized_relative_path: String,
|
||||
cache_directory: String,
|
||||
maximum_primitive_count: int,
|
||||
allowlist_patterns: PackedStringArray,
|
||||
denylist_patterns: PackedStringArray,
|
||||
debug_logging_enabled: bool = false
|
||||
) -> String:
|
||||
if not is_animation_path_allowed(
|
||||
normalized_relative_path,
|
||||
allowlist_patterns,
|
||||
denylist_patterns
|
||||
):
|
||||
return ""
|
||||
for cache_resource_path in cache_resource_paths(
|
||||
cache_directory,
|
||||
normalized_relative_path
|
||||
):
|
||||
if not ResourceLoader.exists(cache_resource_path):
|
||||
continue
|
||||
if glb_cache_is_safe_for_runtime_animation(
|
||||
cache_resource_path,
|
||||
maximum_primitive_count,
|
||||
debug_logging_enabled
|
||||
):
|
||||
return cache_resource_path
|
||||
return ""
|
||||
|
||||
|
||||
## Applies the historical stripped, case-insensitive allow/deny substring policy.
|
||||
func is_animation_path_allowed(
|
||||
normalized_relative_path: String,
|
||||
allowlist_patterns: PackedStringArray,
|
||||
denylist_patterns: PackedStringArray
|
||||
) -> bool:
|
||||
if normalized_relative_path.is_empty() or allowlist_patterns.is_empty():
|
||||
return false
|
||||
var lowercase_path := normalized_relative_path.to_lower()
|
||||
var allowed := false
|
||||
for pattern in allowlist_patterns:
|
||||
var needle := String(pattern).strip_edges().to_lower()
|
||||
if not needle.is_empty() and lowercase_path.contains(needle):
|
||||
allowed = true
|
||||
break
|
||||
if not allowed:
|
||||
return false
|
||||
for pattern in denylist_patterns:
|
||||
var needle := String(pattern).strip_edges().to_lower()
|
||||
if not needle.is_empty() and lowercase_path.contains(needle):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
## Returns historical nested/lowercase/basename `.glb` candidates without I/O.
|
||||
func cache_resource_paths(
|
||||
cache_directory: String,
|
||||
relative_path: String
|
||||
) -> PackedStringArray:
|
||||
var normalized := relative_path.replace("\\", "/")
|
||||
var lowercase := normalized.to_lower()
|
||||
var stems := [
|
||||
normalized.get_basename(),
|
||||
lowercase.get_basename(),
|
||||
normalized.get_file().get_basename(),
|
||||
lowercase.get_file().get_basename(),
|
||||
]
|
||||
var result := PackedStringArray()
|
||||
for stem in stems:
|
||||
if stem.is_empty():
|
||||
continue
|
||||
var cache_resource_path := cache_directory.path_join(stem + ".glb")
|
||||
if not result.has(cache_resource_path):
|
||||
result.append(cache_resource_path)
|
||||
return result
|
||||
|
||||
|
||||
## Checks animation presence, primitive limit and accepted OpenWC schema.
|
||||
func glb_cache_is_safe_for_runtime_animation(
|
||||
cache_resource_path: String,
|
||||
maximum_primitive_count: int,
|
||||
debug_logging_enabled: bool = false
|
||||
) -> bool:
|
||||
var gltf := read_glb_json(cache_resource_path)
|
||||
if gltf.is_empty():
|
||||
return false
|
||||
var animations: Array = gltf.get("animations", [])
|
||||
if animations.is_empty():
|
||||
return false
|
||||
var primitive_count := glb_primitive_count(gltf)
|
||||
if maximum_primitive_count > 0 and primitive_count > maximum_primitive_count:
|
||||
return false
|
||||
var schema := glb_animation_schema(gltf)
|
||||
if schema == "pivot_prefix_v1" or schema.is_empty():
|
||||
return true
|
||||
if debug_logging_enabled:
|
||||
print(
|
||||
"M2_ANIM_REJECT schema=%s primitives=%d cache=%s"
|
||||
% [schema, primitive_count, cache_resource_path]
|
||||
)
|
||||
return false
|
||||
|
||||
|
||||
## Reads the JSON chunk of a version-2 GLB or returns an empty Dictionary.
|
||||
func read_glb_json(cache_resource_path: String) -> Dictionary:
|
||||
var absolute_path := ProjectSettings.globalize_path(cache_resource_path)
|
||||
if not FileAccess.file_exists(absolute_path):
|
||||
return {}
|
||||
var file := FileAccess.open(absolute_path, FileAccess.READ)
|
||||
if file == null or file.get_length() < 20:
|
||||
return {}
|
||||
var magic := file.get_32()
|
||||
var version := file.get_32()
|
||||
file.get_32()
|
||||
if magic != 0x46546c67 or version != 2:
|
||||
return {}
|
||||
var json_length := int(file.get_32())
|
||||
var chunk_type := file.get_32()
|
||||
if json_length <= 0 or chunk_type != 0x4e4f534a:
|
||||
return {}
|
||||
var parsed: Variant = JSON.parse_string(
|
||||
file.get_buffer(json_length).get_string_from_utf8()
|
||||
)
|
||||
return parsed as Dictionary if parsed is Dictionary else {}
|
||||
|
||||
|
||||
## Counts all glTF Mesh primitives using the historical safety rule.
|
||||
func glb_primitive_count(gltf: Dictionary) -> int:
|
||||
var count := 0
|
||||
var meshes: Array = gltf.get("meshes", [])
|
||||
for mesh_variant in meshes:
|
||||
if mesh_variant is Dictionary:
|
||||
var primitives: Array = (mesh_variant as Dictionary).get(
|
||||
"primitives",
|
||||
[]
|
||||
)
|
||||
count += primitives.size()
|
||||
return count
|
||||
|
||||
|
||||
## Returns the OpenWC animation schema marker from parsed glTF metadata.
|
||||
func glb_animation_schema(gltf: Dictionary) -> String:
|
||||
var asset: Dictionary = gltf.get("asset", {})
|
||||
var extras: Dictionary = asset.get("extras", {})
|
||||
return String(extras.get("openwc_m2_anim_schema", ""))
|
||||
@@ -0,0 +1 @@
|
||||
uid://d0ptej41k2smt
|
||||
@@ -73,6 +73,9 @@ const M2_STATIC_BATCH_MATERIALIZER_SCRIPT := preload(
|
||||
const M2_STATIC_BUILD_RESOURCE_OBSERVER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_static_build_resource_observer.gd"
|
||||
)
|
||||
const M2_CACHED_ANIMATION_RESOURCE_OBSERVER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_cached_animation_resource_observer.gd"
|
||||
)
|
||||
const M2_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_runtime_mesh_finalizer.gd"
|
||||
)
|
||||
@@ -283,6 +286,9 @@ var _m2_build_dispatch_planner := M2_BUILD_DISPATCH_PLANNER_SCRIPT.new()
|
||||
var _m2_static_build_resource_observer := (
|
||||
M2_STATIC_BUILD_RESOURCE_OBSERVER_SCRIPT.new()
|
||||
)
|
||||
var _m2_animation_resource_observer := (
|
||||
M2_CACHED_ANIMATION_RESOURCE_OBSERVER_SCRIPT.new()
|
||||
)
|
||||
var _m2_unique_placement_registry := (
|
||||
M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT.new()
|
||||
)
|
||||
@@ -4350,19 +4356,33 @@ func _process_m2_build_jobs() -> void:
|
||||
var groups := _m2_build_queue_state.groups_for(key)
|
||||
var transforms: Array = groups.get(rel_path, [])
|
||||
var offset := _m2_build_queue_state.transform_offset_for(key)
|
||||
var animated_prototype: Node3D = null
|
||||
var resource_snapshot: RefCounted
|
||||
if enable_m2_animated_instances:
|
||||
animated_prototype = _get_or_load_m2_native_animated_prototype(rel_path)
|
||||
if animated_prototype == null:
|
||||
animated_prototype = _get_or_load_m2_animated_prototype(rel_path)
|
||||
var animation_request_pending := (
|
||||
enable_m2_animated_instances
|
||||
and _m2_animation_load_pipeline_state.has_request(normalized_rel)
|
||||
var native_animated_prototype := (
|
||||
_get_or_load_m2_native_animated_prototype(rel_path)
|
||||
)
|
||||
var resource_snapshot: RefCounted = M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
|
||||
if native_animated_prototype != null:
|
||||
resource_snapshot = M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
|
||||
normalized_rel,
|
||||
animated_prototype,
|
||||
animation_request_pending
|
||||
native_animated_prototype,
|
||||
false
|
||||
)
|
||||
else:
|
||||
resource_snapshot = _m2_animation_resource_observer.observe(
|
||||
normalized_rel,
|
||||
m2_cache_dir,
|
||||
m2_animated_max_primitives,
|
||||
m2_animated_allowlist_patterns,
|
||||
m2_animated_denylist_patterns,
|
||||
_m2_prototype_cache_state,
|
||||
_m2_animation_load_pipeline_state,
|
||||
debug_streaming
|
||||
)
|
||||
else:
|
||||
resource_snapshot = M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
|
||||
normalized_rel,
|
||||
null,
|
||||
false
|
||||
)
|
||||
var batch_plan: Dictionary = {}
|
||||
var batch_count := 0
|
||||
@@ -4875,75 +4895,6 @@ func _is_m2_native_animation_candidate(normalized_rel: String) -> bool:
|
||||
return normalized_rel.to_lower().contains("gryphonroost")
|
||||
|
||||
|
||||
func _get_or_load_m2_animated_prototype(rel_path: String) -> Node3D:
|
||||
var normalized_rel := _normalize_m2_rel_path(rel_path)
|
||||
if normalized_rel.is_empty():
|
||||
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
|
||||
if _m2_animation_load_pipeline_state.has_request(normalized_rel):
|
||||
return null
|
||||
|
||||
_request_m2_animation_load(normalized_rel)
|
||||
return null
|
||||
|
||||
|
||||
func _request_m2_animation_load(normalized_rel: String) -> void:
|
||||
if normalized_rel.is_empty() or _m2_animation_load_pipeline_state.has_request(normalized_rel):
|
||||
return
|
||||
var cache_res_path := _find_m2_animated_glb_cache_path(normalized_rel)
|
||||
if cache_res_path.is_empty():
|
||||
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||
return
|
||||
var err := ResourceLoader.load_threaded_request(
|
||||
cache_res_path,
|
||||
"",
|
||||
false,
|
||||
ResourceLoader.CACHE_MODE_REUSE)
|
||||
if err == OK or err == ERR_BUSY:
|
||||
_m2_animation_load_pipeline_state.remember_request(normalized_rel, cache_res_path)
|
||||
else:
|
||||
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||
|
||||
|
||||
func _find_m2_animated_glb_cache_path(normalized_rel: String) -> String:
|
||||
if not _is_m2_animation_allowed(normalized_rel):
|
||||
return ""
|
||||
if _is_m2_animation_denied(normalized_rel):
|
||||
return ""
|
||||
for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".glb"]):
|
||||
if not ResourceLoader.exists(cache_res_path):
|
||||
continue
|
||||
if _glb_cache_is_safe_for_runtime_animation(cache_res_path):
|
||||
return cache_res_path
|
||||
return ""
|
||||
|
||||
|
||||
func _is_m2_animation_allowed(normalized_rel: String) -> bool:
|
||||
if m2_animated_allowlist_patterns.is_empty():
|
||||
return false
|
||||
var lower := normalized_rel.to_lower()
|
||||
for pattern in m2_animated_allowlist_patterns:
|
||||
var needle := String(pattern).strip_edges().to_lower()
|
||||
if not needle.is_empty() and lower.contains(needle):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _is_m2_animation_denied(normalized_rel: String) -> bool:
|
||||
var lower := normalized_rel.to_lower()
|
||||
for pattern in m2_animated_denylist_patterns:
|
||||
var needle := String(pattern).strip_edges().to_lower()
|
||||
if not needle.is_empty() and lower.contains(needle):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
|
||||
if normalized_rel.is_empty():
|
||||
return null
|
||||
@@ -4967,62 +4918,6 @@ func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
|
||||
return null
|
||||
|
||||
|
||||
func _glb_cache_is_safe_for_runtime_animation(cache_res_path: String) -> bool:
|
||||
var abs_path := ProjectSettings.globalize_path(cache_res_path)
|
||||
if not FileAccess.file_exists(abs_path):
|
||||
return false
|
||||
var file := FileAccess.open(abs_path, FileAccess.READ)
|
||||
if file == null or file.get_length() < 20:
|
||||
return false
|
||||
var magic := file.get_32()
|
||||
var version := file.get_32()
|
||||
file.get_32()
|
||||
if magic != 0x46546c67 or version != 2:
|
||||
return false
|
||||
var json_len := int(file.get_32())
|
||||
var chunk_type := file.get_32()
|
||||
if json_len <= 0 or chunk_type != 0x4e4f534a:
|
||||
return false
|
||||
var json_text := file.get_buffer(json_len).get_string_from_utf8()
|
||||
var parsed = JSON.parse_string(json_text)
|
||||
if not (parsed is Dictionary):
|
||||
return false
|
||||
var gltf := parsed as Dictionary
|
||||
var animations: Array = gltf.get("animations", [])
|
||||
if animations.is_empty():
|
||||
return false
|
||||
var primitive_count := _glb_primitive_count(gltf)
|
||||
if m2_animated_max_primitives > 0 and primitive_count > m2_animated_max_primitives:
|
||||
return false
|
||||
var schema := _glb_animation_schema(gltf)
|
||||
if schema == "pivot_prefix_v1":
|
||||
return true
|
||||
# Older simple critter GLBs were generated before the schema marker existed.
|
||||
# Accept them only after allowlist/denylist filtering and primitive-count checks.
|
||||
if schema.is_empty():
|
||||
return true
|
||||
if debug_streaming:
|
||||
print("M2_ANIM_REJECT schema=%s primitives=%d cache=%s" % [schema, primitive_count, cache_res_path])
|
||||
return false
|
||||
|
||||
|
||||
func _glb_primitive_count(gltf: Dictionary) -> int:
|
||||
var count := 0
|
||||
var meshes: Array = gltf.get("meshes", [])
|
||||
for mesh_variant in meshes:
|
||||
if not (mesh_variant is Dictionary):
|
||||
continue
|
||||
var primitives: Array = (mesh_variant as Dictionary).get("primitives", [])
|
||||
count += primitives.size()
|
||||
return count
|
||||
|
||||
|
||||
func _glb_animation_schema(gltf: Dictionary) -> String:
|
||||
var asset: Dictionary = gltf.get("asset", {})
|
||||
var extras: Dictionary = asset.get("extras", {})
|
||||
return String(extras.get("openwc_m2_anim_schema", ""))
|
||||
|
||||
|
||||
func _get_or_load_wmo_prototype(rel_path: String) -> Node3D:
|
||||
var normalized_rel := rel_path.replace("\\", "/")
|
||||
if normalized_rel.is_empty():
|
||||
|
||||
@@ -5,6 +5,9 @@ extends SceneTree
|
||||
|
||||
const PIPELINE_SCRIPT := preload("res://src/render/m2/m2_animation_load_pipeline_state.gd")
|
||||
const PIPELINE_PATH := "res://src/render/m2/m2_animation_load_pipeline_state.gd"
|
||||
const OBSERVER_PATH := (
|
||||
"res://src/render/m2/m2_cached_animation_resource_observer.gd"
|
||||
)
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
@@ -80,14 +83,19 @@ func _verify_discard_metrics_clear_and_diagnostics(failures: Array[String]) -> v
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var pipeline_source := FileAccess.get_file_as_string(PIPELINE_PATH)
|
||||
var observer_source := FileAccess.get_file_as_string(OBSERVER_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_expect_true(loader_source.contains("M2_ANIMATION_LOAD_PIPELINE_STATE_SCRIPT.new()"), "loader composes pipeline state", failures)
|
||||
_expect_false(loader_source.contains("var _m2_animation_load_requests:"), "legacy request field removed", failures)
|
||||
_expect_false(loader_source.contains("var _m2_animation_finalize_queue:"), "legacy finalize field removed", failures)
|
||||
_expect_equal(loader_source.count("_m2_animation_load_pipeline_state.total_work_count()"), 3, "three existing metrics delegate", failures)
|
||||
_expect_equal(loader_source.count("_m2_animation_load_pipeline_state.clear()"), 2, "two existing clear sites delegate", failures)
|
||||
_expect_true(
|
||||
observer_source.contains("ResourceLoader.load_threaded_request("),
|
||||
"cached observer owns request admission",
|
||||
failures
|
||||
)
|
||||
for retained_loader_rule in [
|
||||
"ResourceLoader.load_threaded_request(",
|
||||
"ResourceLoader.load_threaded_get_status(path)",
|
||||
"ResourceLoader.load_threaded_get(path)",
|
||||
"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE",
|
||||
|
||||
@@ -9,6 +9,9 @@ const SNAPSHOT_SCRIPT := preload(
|
||||
const SNAPSHOT_PATH := "res://src/render/m2/m2_build_resource_snapshot.gd"
|
||||
const DISPATCH_PATH := "res://src/render/m2/m2_build_dispatch_planner.gd"
|
||||
const STATIC_OBSERVER_PATH := "res://src/render/m2/m2_static_build_resource_observer.gd"
|
||||
const CACHED_ANIMATION_OBSERVER_PATH := (
|
||||
"res://src/render/m2/m2_cached_animation_resource_observer.gd"
|
||||
)
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
@@ -136,6 +139,9 @@ func _verify_loader_and_dependency_boundaries(failures: Array[String]) -> void:
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
var dispatch_source := FileAccess.get_file_as_string(DISPATCH_PATH)
|
||||
var static_observer_source := FileAccess.get_file_as_string(STATIC_OBSERVER_PATH)
|
||||
var cached_observer_source := FileAccess.get_file_as_string(
|
||||
CACHED_ANIMATION_OBSERVER_PATH
|
||||
)
|
||||
var snapshot_source := FileAccess.get_file_as_string(SNAPSHOT_PATH)
|
||||
for delegated_token in [
|
||||
"M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(",
|
||||
@@ -152,6 +158,11 @@ func _verify_loader_and_dependency_boundaries(failures: Array[String]) -> void:
|
||||
"static observer adopts snapshot",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
cached_observer_source.contains("M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new("),
|
||||
"cached animation observer produces snapshot",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
dispatch_source.contains("resource_snapshot.call(\"animation_request_pending\")")
|
||||
and dispatch_source.contains("resource_snapshot.call(\"has_static_mesh\")"),
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
extends SceneTree
|
||||
|
||||
const OBSERVER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_cached_animation_resource_observer.gd"
|
||||
)
|
||||
const PROTOTYPE_CACHE_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_prototype_cache_state.gd"
|
||||
)
|
||||
const PIPELINE_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_animation_load_pipeline_state.gd"
|
||||
)
|
||||
const OBSERVER_PATH := (
|
||||
"res://src/render/m2/m2_cached_animation_resource_observer.gd"
|
||||
)
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
const FIXTURE_DIRECTORY := "user://verify_m2_cached_animation_observer"
|
||||
|
||||
var _fixture_paths: PackedStringArray = PackedStringArray()
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_observation_lifecycle(failures)
|
||||
_verify_path_policy_and_order(failures)
|
||||
_verify_glb_safety(failures)
|
||||
_verify_source_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
_remove_fixtures()
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_CACHED_ANIMATION_RESOURCE_OBSERVER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_CACHED_ANIMATION_RESOURCE_OBSERVER PASS "
|
||||
+ "cases=33 iterations=20000 elapsed_ms=%.3f" % elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_observation_lifecycle(failures: Array[String]) -> void:
|
||||
var observer: RefCounted = OBSERVER_SCRIPT.new()
|
||||
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
var invalid_snapshot: RefCounted = observer.call(
|
||||
"observe", "", "res://none", 3, PackedStringArray(),
|
||||
PackedStringArray(), prototype_cache, pipeline
|
||||
)
|
||||
_expect_false(
|
||||
bool(invalid_snapshot.call("animation_request_pending")),
|
||||
"invalid path is not pending",
|
||||
failures
|
||||
)
|
||||
var prototype := Node3D.new()
|
||||
prototype_cache.call(
|
||||
"adopt_animated_prototype",
|
||||
"creature/fish/a.m2",
|
||||
prototype
|
||||
)
|
||||
var cached_snapshot: RefCounted = observer.call(
|
||||
"observe", "creature/fish/a.m2", "res://none", 3,
|
||||
PackedStringArray(["creature/fish/"]), PackedStringArray(),
|
||||
prototype_cache, pipeline
|
||||
)
|
||||
_expect_true(
|
||||
is_same(cached_snapshot.call("animated_prototype"), prototype),
|
||||
"cached prototype identity",
|
||||
failures
|
||||
)
|
||||
prototype_cache.call("mark_animation_static", "creature/fish/static.m2")
|
||||
var static_snapshot: RefCounted = observer.call(
|
||||
"observe", "creature/fish/static.m2", "res://none", 3,
|
||||
PackedStringArray(["creature/fish/"]), PackedStringArray(),
|
||||
prototype_cache, pipeline
|
||||
)
|
||||
_expect_false(
|
||||
bool(static_snapshot.call("animation_request_pending")),
|
||||
"static outcome is not pending",
|
||||
failures
|
||||
)
|
||||
pipeline.call(
|
||||
"remember_request",
|
||||
"creature/fish/pending.m2",
|
||||
"res://pending.glb"
|
||||
)
|
||||
var pending_snapshot: RefCounted = observer.call(
|
||||
"observe", "creature/fish/pending.m2", "res://none", 3,
|
||||
PackedStringArray(["creature/fish/"]), PackedStringArray(),
|
||||
prototype_cache, pipeline
|
||||
)
|
||||
_expect_true(
|
||||
bool(pending_snapshot.call("animation_request_pending")),
|
||||
"existing request remains pending",
|
||||
failures
|
||||
)
|
||||
var unavailable_snapshot: RefCounted = observer.call(
|
||||
"observe", "creature/fish/unavailable.m2", "res://none", 3,
|
||||
PackedStringArray(["creature/fish/"]), PackedStringArray(),
|
||||
prototype_cache, pipeline
|
||||
)
|
||||
_expect_false(
|
||||
bool(unavailable_snapshot.call("animation_request_pending")),
|
||||
"missing candidate is not pending",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(prototype_cache.call(
|
||||
"is_animation_static",
|
||||
"creature/fish/unavailable.m2"
|
||||
)),
|
||||
"missing candidate marks static",
|
||||
failures
|
||||
)
|
||||
pipeline.call("clear")
|
||||
prototype_cache.call("clear_and_release")
|
||||
|
||||
|
||||
func _verify_path_policy_and_order(failures: Array[String]) -> void:
|
||||
var observer: RefCounted = OBSERVER_SCRIPT.new()
|
||||
_expect_false(
|
||||
bool(observer.call(
|
||||
"is_animation_path_allowed", "creature/fish/a.m2",
|
||||
PackedStringArray(), PackedStringArray()
|
||||
)),
|
||||
"empty allowlist rejects",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(observer.call(
|
||||
"is_animation_path_allowed", "Creature/Fish/A.M2",
|
||||
PackedStringArray([" creature/fish/ "]), PackedStringArray()
|
||||
)),
|
||||
"allowlist strips and ignores case",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
bool(observer.call(
|
||||
"is_animation_path_allowed", "creature/fish/boss/a.m2",
|
||||
PackedStringArray(["creature/fish/"]),
|
||||
PackedStringArray([" FISH/BOSS "])
|
||||
)),
|
||||
"denylist wins",
|
||||
failures
|
||||
)
|
||||
var paths: PackedStringArray = observer.call(
|
||||
"cache_resource_paths",
|
||||
"res://cache",
|
||||
"World/Critter/Foo.M2"
|
||||
)
|
||||
_expect_true(
|
||||
paths == PackedStringArray([
|
||||
"res://cache/World/Critter/Foo.glb",
|
||||
"res://cache/world/critter/foo.glb",
|
||||
"res://cache/Foo.glb",
|
||||
"res://cache/foo.glb",
|
||||
]),
|
||||
"historical GLB candidate order",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_glb_safety(failures: Array[String]) -> void:
|
||||
var observer: RefCounted = OBSERVER_SCRIPT.new()
|
||||
_expect_false(
|
||||
bool(observer.call(
|
||||
"glb_cache_is_safe_for_runtime_animation",
|
||||
"user://missing.glb",
|
||||
3
|
||||
)),
|
||||
"missing GLB rejects",
|
||||
failures
|
||||
)
|
||||
var pivot_path := _write_glb_fixture(
|
||||
"pivot.glb",
|
||||
_make_gltf(2, "pivot_prefix_v1", true)
|
||||
)
|
||||
_expect_true(
|
||||
bool(observer.call(
|
||||
"glb_cache_is_safe_for_runtime_animation", pivot_path, 3
|
||||
)),
|
||||
"pivot-prefix schema accepts",
|
||||
failures
|
||||
)
|
||||
var legacy_path := _write_glb_fixture(
|
||||
"legacy.glb",
|
||||
_make_gltf(1, "", true)
|
||||
)
|
||||
_expect_true(
|
||||
bool(observer.call(
|
||||
"glb_cache_is_safe_for_runtime_animation", legacy_path, 3
|
||||
)),
|
||||
"legacy empty schema accepts",
|
||||
failures
|
||||
)
|
||||
var unknown_path := _write_glb_fixture(
|
||||
"unknown.glb",
|
||||
_make_gltf(1, "future_schema", true)
|
||||
)
|
||||
_expect_false(
|
||||
bool(observer.call(
|
||||
"glb_cache_is_safe_for_runtime_animation", unknown_path, 3
|
||||
)),
|
||||
"unknown schema rejects",
|
||||
failures
|
||||
)
|
||||
var complex_path := _write_glb_fixture(
|
||||
"complex.glb",
|
||||
_make_gltf(4, "pivot_prefix_v1", true)
|
||||
)
|
||||
_expect_false(
|
||||
bool(observer.call(
|
||||
"glb_cache_is_safe_for_runtime_animation", complex_path, 3
|
||||
)),
|
||||
"primitive limit rejects",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(observer.call(
|
||||
"glb_cache_is_safe_for_runtime_animation", complex_path, 0
|
||||
)),
|
||||
"non-positive primitive limit disables cap",
|
||||
failures
|
||||
)
|
||||
var static_path := _write_glb_fixture(
|
||||
"static.glb",
|
||||
_make_gltf(1, "pivot_prefix_v1", false)
|
||||
)
|
||||
_expect_false(
|
||||
bool(observer.call(
|
||||
"glb_cache_is_safe_for_runtime_animation", static_path, 3
|
||||
)),
|
||||
"GLB without animations rejects",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
int(observer.call("glb_primitive_count", _make_gltf(5, "", true))),
|
||||
5,
|
||||
"primitive count retained",
|
||||
failures
|
||||
)
|
||||
_expect_string_equal(
|
||||
String(observer.call(
|
||||
"glb_animation_schema",
|
||||
_make_gltf(1, "pivot_prefix_v1", true)
|
||||
)),
|
||||
"pivot_prefix_v1",
|
||||
"schema retained",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
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_animation_resource_observer.observe("),
|
||||
"loader delegates cached observation",
|
||||
failures
|
||||
)
|
||||
for removed_function in [
|
||||
"func _get_or_load_m2_animated_prototype",
|
||||
"func _request_m2_animation_load",
|
||||
"func _find_m2_animated_glb_cache_path",
|
||||
"func _glb_cache_is_safe_for_runtime_animation",
|
||||
]:
|
||||
_expect_false(
|
||||
loader_source.contains(removed_function),
|
||||
"loader omits %s" % removed_function,
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("func _get_or_load_m2_native_animated_prototype"),
|
||||
"loader retains native observation",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
observer_source.contains("ResourceLoader.load_threaded_request("),
|
||||
"observer owns request admission",
|
||||
failures
|
||||
)
|
||||
for forbidden_token in [
|
||||
"queue_free",
|
||||
"RenderingServer",
|
||||
"WorkerThreadPool",
|
||||
"M2_NATIVE_ANIMATED_BUILDER",
|
||||
"M2RawModelRepository",
|
||||
"RenderBudgetScheduler",
|
||||
]:
|
||||
_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 allowlist := PackedStringArray(["creature/fish/", "world/critter/"])
|
||||
var denylist := PackedStringArray(["boss"])
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in range(20000):
|
||||
var path := "Creature/Fish/model_%d.m2" % (iteration % 64)
|
||||
observer.call(
|
||||
"is_animation_path_allowed",
|
||||
path,
|
||||
allowlist,
|
||||
denylist
|
||||
)
|
||||
observer.call("cache_resource_paths", "res://cache", path)
|
||||
var elapsed_milliseconds := (
|
||||
float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
)
|
||||
_expect_true(
|
||||
elapsed_milliseconds < 1000.0,
|
||||
"20,000 policy/path observations remain bounded",
|
||||
failures
|
||||
)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _make_gltf(
|
||||
primitive_count: int,
|
||||
schema: String,
|
||||
has_animation: bool
|
||||
) -> Dictionary:
|
||||
var primitives: Array[Dictionary] = []
|
||||
for _index in range(primitive_count):
|
||||
primitives.append({})
|
||||
var gltf := {
|
||||
"asset": {"version": "2.0", "extras": {}},
|
||||
"meshes": [{"primitives": primitives}],
|
||||
"animations": [{}] if has_animation else [],
|
||||
}
|
||||
if not schema.is_empty():
|
||||
(gltf["asset"]["extras"] as Dictionary)[
|
||||
"openwc_m2_anim_schema"
|
||||
] = schema
|
||||
return gltf
|
||||
|
||||
|
||||
func _write_glb_fixture(file_name: String, gltf: Dictionary) -> String:
|
||||
var absolute_directory := ProjectSettings.globalize_path(FIXTURE_DIRECTORY)
|
||||
DirAccess.make_dir_recursive_absolute(absolute_directory)
|
||||
var resource_path := FIXTURE_DIRECTORY.path_join(file_name)
|
||||
var absolute_path := ProjectSettings.globalize_path(resource_path)
|
||||
var json_bytes := JSON.stringify(gltf).to_utf8_buffer()
|
||||
while json_bytes.size() % 4 != 0:
|
||||
json_bytes.append(0x20)
|
||||
var file := FileAccess.open(absolute_path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return resource_path
|
||||
file.store_32(0x46546c67)
|
||||
file.store_32(2)
|
||||
file.store_32(20 + json_bytes.size())
|
||||
file.store_32(json_bytes.size())
|
||||
file.store_32(0x4e4f534a)
|
||||
file.store_buffer(json_bytes)
|
||||
file.close()
|
||||
_fixture_paths.append(absolute_path)
|
||||
return resource_path
|
||||
|
||||
|
||||
func _remove_fixtures() -> void:
|
||||
for absolute_path in _fixture_paths:
|
||||
if FileAccess.file_exists(absolute_path):
|
||||
DirAccess.remove_absolute(absolute_path)
|
||||
var absolute_directory := ProjectSettings.globalize_path(FIXTURE_DIRECTORY)
|
||||
if DirAccess.dir_exists_absolute(absolute_directory):
|
||||
DirAccess.remove_absolute(absolute_directory)
|
||||
|
||||
|
||||
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_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://cbviicmix3kb0
|
||||
@@ -59,7 +59,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
`M03-RND-M2-BUILD-QUEUE-001`,
|
||||
`M03-RND-M2-BUILD-DISPATCH-PLANNER-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`
|
||||
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
|
||||
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
|
||||
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner
|
||||
@@ -283,6 +284,13 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
Merge `a043c79` passed post-merge observer (`64.202ms`), snapshot, dispatch,
|
||||
Mesh pipeline/cache, prototype cache, shutdown, facade, internal-access `30`,
|
||||
manifest `7/7`, documentation `42`, coordination and checkpoint dry-run `7/7`.
|
||||
M2 cached animation resource observer passed `cases=33 iterations=20000
|
||||
elapsed_ms=61.745` with cached-prototype identity, static/pending/missing
|
||||
lifecycle, stripped case-insensitive allow/deny precedence, exact historical
|
||||
GLB path order and generated GLB animation/primitive/schema safety fixtures.
|
||||
All 56 autonomous headless regressions passed; internal-access remained `30`,
|
||||
documentation covered `43` module specifications and checkpoint dry-run kept
|
||||
all `7/7` plans. Coordination passed with `34` historical expired-claim warnings.
|
||||
M02 terrain-query regression
|
||||
remained green (13 pre-existing expired M00 claim warnings).
|
||||
- Fidelity comparison: all 16 historical operation limits and drain sites were
|
||||
@@ -320,6 +328,11 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
One build operation now carries the exact normalized path, optional animated
|
||||
prototype, pending-animation state, optional static Mesh and missing outcome in
|
||||
a typed snapshot; releasing it never frees either borrowed engine resource.
|
||||
Cached animated lookup now preserves cached/static/pending priority, empty-
|
||||
allowlist rejection, denylist precedence, normalized/lowercase/basename path
|
||||
order, animation presence, primitive cap, accepted empty/`pivot_prefix_v1`
|
||||
schema rules and request/static-only transitions. Native GryphonRoost remains
|
||||
first and loader-owned, so selection and visible behavior are unchanged.
|
||||
WMO cache-key normalization, positive MODF identity with tile/index fallback
|
||||
and world position/Euler/unclamped-scale transforms are unchanged across
|
||||
lightweight render, cached-scene and live-prototype paths.
|
||||
@@ -438,6 +451,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
stateless M2 build dispatch planner and loader resource-observation/action adapter,
|
||||
typed M2 build resource snapshot and loader/dispatch/materializer adapters,
|
||||
static M2 build resource observer and loader/cache/pipeline/snapshot adapters,
|
||||
cached M2 animation resource observer and loader/prototype/pipeline/snapshot
|
||||
adapters plus generated GLB metadata regression fixtures,
|
||||
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
|
||||
- Remaining risks: worker concurrency and stale-result validation remain
|
||||
streamer-owned; cancellation stops new permits but does not interrupt
|
||||
@@ -454,18 +469,19 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
culling-driven spatial-cell grouping; M2 rebuild classification and static
|
||||
request/finalize bookkeeping, prepared Mesh cache ownership and first-Mesh
|
||||
traversal plus refresh/rebuild/fallback, raw native M2 I/O and prototype/
|
||||
negative cache state are separated, but synchronous parsing, ResourceLoader,
|
||||
animation request state and Mesh adoption remain in the loader call path; raw
|
||||
negative cache state are separated, but synchronous native parsing/build and
|
||||
Mesh adoption remain in the loader call path; cached animation eligibility/
|
||||
request admission is extracted while terminal ResourceLoader polling remains;
|
||||
raw
|
||||
animated-instance materialization is now separated but remains synchronous
|
||||
main-thread work; asset-backed traversal/visual/leak/p95/p99 evidence is pending;
|
||||
static MultiMesh materialization is also separated but remains synchronous;
|
||||
headless timing does not measure transform upload GPU cost and spatial-cell
|
||||
batching still needs culling/performance evidence; typed M2 pending state is
|
||||
separated and dispatch decisions are explicit, while resource observation/
|
||||
requests, action execution and root cleanup remain in loader; resource state is
|
||||
typed per operation but its producing service is not yet extracted;
|
||||
static lookup/request production is extracted, while animated observation and
|
||||
both ResourceLoader finalize drains remain loader-owned;
|
||||
separated and dispatch decisions are explicit; static and cached-animation
|
||||
observers produce typed per-operation state, while native observation, action
|
||||
execution and root cleanup remain in loader; both ResourceLoader finalize
|
||||
drains remain loader-owned;
|
||||
WMO ResourceLoader/FileAccess I/O, live fallback and group materialization
|
||||
remain in the loader;
|
||||
asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending;
|
||||
|
||||
Reference in New Issue
Block a user