render: extract M2 animation resource finalizer

This commit is contained in:
2026-07-18 13:30:24 +04:00
parent ff952da7d8
commit e7cd967dce
17 changed files with 1066 additions and 121 deletions
+14
View File
@@ -46,6 +46,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
- `src/render/m2/m2_static_build_resource_observer.gd` - static Mesh cache lookup, threaded request selection and missing transition. - `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_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_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_animation_resource_finalizer.gd` - cached animated M2 terminal status polling, Resource load, scene finalization and prototype/static-only outcome.
- `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_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. - `src/render/m2/m2_mesh_resource_cache_state.gd` - normalized-path prepared static M2 Mesh references with final-shutdown lifetime.
- `src/render/m2/m2_mesh_resource_extractor.gd` - first-Mesh selection from direct/PackedScene/Node inputs with temporary instance cleanup. - `src/render/m2/m2_mesh_resource_extractor.gd` - first-Mesh selection from direct/PackedScene/Node inputs with temporary instance cleanup.
@@ -1155,6 +1156,19 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
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 Animation Resource Finalizer Extraction
- `M2AnimationResourceFinalizer` now owns cached animated M2 terminal status
polling, completion-FIFO pops, terminal Resource retrieval, candidate
instantiation, repair/validation, prototype/static-only adoption and success log.
- The two-phase preparation/completion API preserves the prior ordering: the
loader resolves a material prototype only after a loaded PackedScene produces
a detached Node3D candidate. One scheduler permit still pops one record.
- `StreamingWorldLoader` retains the `M2_ANIMATION_FINALIZE` permit loop,
material-prototype lookup, build dispatch, materialization and SceneTree lifetime.
- No request order, status rule, cache format, profile or visible behavior changed.
Synthetic fixtures are not asset-backed animation/leak/p95/p99 evidence.
## 2026-07-18 M2 Native Animation Resource Observer Extraction ## 2026-07-18 M2 Native Animation Resource Observer Extraction
- `M2NativeAnimationResourceObserver` now owns the exact case-insensitive - `M2NativeAnimationResourceObserver` now owns the exact case-insensitive
+1
View File
@@ -33,6 +33,7 @@
| M2 animation playback controller | Implemented extraction | [`m2-animation-playback-controller.md`](m2-animation-playback-controller.md) | | M2 animation playback controller | Implemented extraction | [`m2-animation-playback-controller.md`](m2-animation-playback-controller.md) |
| M2 animated instance materializer | Implemented extraction | [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.md) | | M2 animated instance materializer | Implemented extraction | [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.md) |
| M2 animation load pipeline state | Implemented extraction | [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md) | | M2 animation load pipeline state | Implemented extraction | [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md) |
| M2 animation resource finalizer | Implemented extraction | [`m2-animation-resource-finalizer.md`](m2-animation-resource-finalizer.md) |
| M2 mesh load pipeline state | Implemented extraction | [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md) | | M2 mesh load pipeline state | Implemented extraction | [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md) |
| M2 mesh resource cache state | Implemented extraction | [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md) | | M2 mesh resource cache state | Implemented extraction | [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md) |
| M2 mesh resource extractor | Implemented extraction | [`m2-mesh-resource-extractor.md`](m2-mesh-resource-extractor.md) | | M2 mesh resource extractor | Implemented extraction | [`m2-mesh-resource-extractor.md`](m2-mesh-resource-extractor.md) |
+22 -22
View File
@@ -27,9 +27,9 @@ and accept only candidates containing AnimationPlayer descendants.
```mermaid ```mermaid
flowchart LR flowchart LR
Loader[StreamingWorldLoader] -->|loaded Resource and material source| Finalizer[M2AnimatedSceneFinalizer] ResourceFinalizer[M2AnimationResourceFinalizer] -->|loaded Resource and material source| Finalizer[M2AnimatedSceneFinalizer]
Finalizer -->|accepted Node3D and player count| Loader Finalizer -->|accepted Node3D and player count| ResourceFinalizer
Loader --> Cache[M2PrototypeCacheState] ResourceFinalizer --> Cache[M2PrototypeCacheState]
``` ```
Allowed dependencies are Godot scene/resource/material types. ResourceLoader, Allowed dependencies are Godot scene/resource/material types. ResourceLoader,
@@ -50,10 +50,10 @@ other application layers are forbidden.
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | | Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| Input | Loaded Resource | Loader I/O adapter | Candidate instantiator | Borrowed Resource | One finalize permit | | Input | Loaded Resource | Animation resource finalizer | Candidate instantiator | Borrowed Resource | One finalize permit |
| Input | Static material prototype Node3D | Loader cache/build adapter | Material repair | Borrowed Node | One repair call | | Input | Static material prototype Node3D | Loader cache/build adapter | Material repair | Borrowed Node | One repair call |
| Input | Detached animated candidate | Instantiator | Repair/final validation | Finalizer then caller/release | One attempt | | Input | Detached animated candidate | Instantiator | Repair/final validation | Finalizer then caller/release | One attempt |
| Output | Accepted prototype and player count | Finalizer | Loader adoption/log adapter | Exact Node transferred | Shutdown cache lifetime | | Output | Accepted prototype and player count | Finalizer | Resource-finalizer adoption/log adapter | Exact Node transferred | Shutdown cache lifetime |
| Output | Depth-first engine-node arrays | Traversal | Loader preparation/playback | Borrowed references | One call | | Output | Depth-first engine-node arrays | Traversal | Loader preparation/playback | Borrowed references | One call |
Side effects are PackedScene instantiation, surface override assignment and Side effects are PackedScene instantiation, surface override assignment and
@@ -91,20 +91,19 @@ stateDiagram-v2
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant L as StreamingWorldLoader participant R as M2AnimationResourceFinalizer
participant F as M2AnimatedSceneFinalizer participant F as M2AnimatedSceneFinalizer
participant C as M2PrototypeCacheState participant C as M2PrototypeCacheState
L->>F: instantiate_candidate(Resource) R->>F: instantiate_candidate(Resource)
F-->>L: detached Node3D or null F-->>R: detached Node3D or null
L->>L: get static material prototype R->>F: repair_materials(candidate, source)
L->>F: repair_materials(candidate, source) R->>F: finalize_candidate(candidate)
L->>F: finalize_candidate(candidate)
alt accepted alt accepted
F-->>L: exact Node3D and player count F-->>R: exact Node3D and player count
L->>C: adopt animated prototype R->>C: adopt animated prototype
else rejected else rejected
F-->>L: empty; candidate freed F-->>R: empty; candidate freed
L->>C: mark animation static R->>C: mark animation static
end end
``` ```
@@ -112,11 +111,11 @@ sequenceDiagram
```mermaid ```mermaid
flowchart TB flowchart TB
Loader[StreamingWorldLoader] --> Finalizer[M2AnimatedSceneFinalizer] ResourceFinalizer[M2AnimationResourceFinalizer] --> Finalizer[M2AnimatedSceneFinalizer]
Finalizer --> Engine[PackedScene / Node3D / Mesh / Material / AnimationPlayer] Finalizer --> Engine[PackedScene / Node3D / Mesh / Material / AnimationPlayer]
Loader --> Resource[ResourceLoader] ResourceFinalizer --> Resource[ResourceLoader]
Loader --> Budget[RenderBudgetScheduler] Loader --> Budget[RenderBudgetScheduler]
Loader --> Prototype[M2PrototypeCacheState] ResourceFinalizer --> Prototype[M2PrototypeCacheState]
Finalizer -. no dependency .-> Resource Finalizer -. no dependency .-> Resource
Finalizer -. no dependency .-> Budget Finalizer -. no dependency .-> Budget
Finalizer -. no dependency .-> Prototype Finalizer -. no dependency .-> Prototype
@@ -126,7 +125,7 @@ flowchart TB
- Every method runs synchronously on the renderer main thread. - Every method runs synchronously on the renderer main thread.
- A valid candidate is detached and finalizer-owned until acceptance. - A valid candidate is detached and finalizer-owned until acceptance.
- Acceptance transfers the exact Node3D to the loader/prototype cache path. - Acceptance transfers the exact Node3D to the resource-finalizer/cache path.
- Rejection frees the candidate synchronously, including wrong-type roots. - Rejection frees the candidate synchronously, including wrong-type roots.
- Traversal results borrow Nodes; source materials remain Resource-owned. - Traversal results borrow Nodes; source materials remain Resource-owned.
@@ -134,11 +133,11 @@ flowchart TB
| Failure | Detection | Behavior | Diagnostic | Recovery | | Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---| |---|---|---|---|---|
| Null/unsupported Resource | Type guard | Return null | Dedicated verifier | Loader marks static | | Null/unsupported Resource | Type guard | Return null | Dedicated verifier | Resource finalizer marks static |
| Wrong root type | Instantiated type guard | Free and return null | Node-count regression | Rebuild cache | | Wrong root type | Instantiated type guard | Free and return null | Node-count regression | Rebuild cache |
| No material source/meshes | Null/empty traversal | Leave materials unchanged | Material fixture | Imported materials remain | | No material source/meshes | Null/empty traversal | Leave materials unchanged | Material fixture | Imported materials remain |
| Missing source surface | Material lookup | First source material fallback | Mapping fixture | Repair static source | | Missing source surface | Material lookup | First source material fallback | Mapping fixture | Repair static source |
| No AnimationPlayer | Descendant inventory | Free and return empty | Lifetime regression | Loader marks static | | No AnimationPlayer | Descendant inventory | Free and return empty | Lifetime regression | Resource finalizer marks static |
| Shutdown/cancellation | Not owned | No retained state | N/A | Loader drains first | | Shutdown/cancellation | Not owned | No retained state | N/A | Loader drains first |
## Configuration and capabilities ## Configuration and capabilities
@@ -199,7 +198,8 @@ by `M2AnimatedInstanceMaterializer`.
| `src/render/m2/m2_animated_instance_materializer.gd` | Per-duplicate player inventory consumer and batch owner | | `src/render/m2/m2_animated_instance_materializer.gd` | Per-duplicate player inventory consumer and batch owner |
| `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending/terminal records before finalization | | `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending/terminal records before finalization |
| `src/render/m2/m2_prototype_cache_state.gd` | Accepted prototype/static-only outcomes | | `src/render/m2/m2_prototype_cache_state.gd` | Accepted prototype/static-only outcomes |
| `src/scenes/streaming/streaming_world_loader.gd` | I/O, permits, material source, adoption and logs | | `src/render/m2/m2_animation_resource_finalizer.gd` | I/O, candidate composition, adoption and logs |
| `src/scenes/streaming/streaming_world_loader.gd` | Permits and material-source lookup |
| `src/tools/verify_m2_animated_scene_finalizer.gd` | Scene/material/lifetime/boundary/timing regression | | `src/tools/verify_m2_animated_scene_finalizer.gd` | Scene/material/lifetime/boundary/timing regression |
## Related decisions and references ## Related decisions and references
@@ -15,9 +15,9 @@
Own cross-frame bookkeeping between a successful animated M2 ResourceLoader Own cross-frame bookkeeping between a successful animated M2 ResourceLoader
request, terminal polling and budgeted main-thread scene finalization. This is request, terminal polling and budgeted main-thread scene finalization. This is
an exact state extraction; cached animation eligibility and request admission an exact state extraction; cached animation eligibility and request admission
belong to `M2CachedAnimationResourceObserver`, while scene finalization and belong to `M2CachedAnimationResourceObserver`; terminal I/O/outcomes belong to
retained Node lifecycle belong to `M2AnimatedSceneFinalizer` and `M2AnimationResourceFinalizer`, while validation and retained Node lifetime
`M2PrototypeCacheState`. belong to `M2AnimatedSceneFinalizer` and `M2PrototypeCacheState`.
## Non-goals ## Non-goals
@@ -32,11 +32,12 @@ retained Node lifecycle belong to `M2AnimatedSceneFinalizer` and
flowchart LR flowchart LR
Observer[M2CachedAnimationResourceObserver] --> IO[ResourceLoader request] Observer[M2CachedAnimationResourceObserver] --> IO[ResourceLoader request]
Observer --> State[M2AnimationLoadPipelineState] Observer --> State[M2AnimationLoadPipelineState]
Loader[StreamingWorldLoader] --> IO Finalizer[M2AnimationResourceFinalizer] --> IO
Loader --> State Finalizer --> State
State -->|detached pending records| Loader State -->|detached pending records| Finalizer
Loader -->|opaque terminal status| State Finalizer -->|opaque terminal status| State
State -->|completion FIFO| Loader State -->|completion FIFO| Finalizer
Loader[StreamingWorldLoader] --> Finalizer
Loader --> Budget[M2_ANIMATION_FINALIZE permit] Loader --> Budget[M2_ANIMATION_FINALIZE permit]
Loader --> Prototype[M2PrototypeCacheState] Loader --> Prototype[M2PrototypeCacheState]
``` ```
@@ -65,9 +66,9 @@ renderer services are forbidden.
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | | Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| Input | Normalized M2 path and GLB Resource path | Loader request adapter | Pending map | Copied Strings | Until completion/discard/clear | | Input | Normalized M2 path and GLB Resource path | Loader request adapter | Pending map | Copied Strings | Until completion/discard/clear |
| Input | Opaque terminal status | Loader polling adapter | Finalize FIFO | Integer value | Until pop/clear | | Input | Opaque terminal status | Resource finalizer | Finalize FIFO | Integer value | Until pop/clear |
| Output | Detached pending records | State | Loader poll/shutdown adapter | Caller-owned copies | One pass | | Output | Detached pending records | State | Resource finalizer/shutdown adapter | Caller-owned copies | One pass |
| Output | Oldest completion record | State | Loader finalizer | Transferred Dictionary | One finalize attempt | | Output | Oldest completion record | State | Resource finalizer | Transferred Dictionary | One finalize attempt |
| Output | Detached diagnostics | State | Verifier/future metrics | Caller-owned copies | Snapshot lifetime | | Output | Detached diagnostics | State | Verifier/future metrics | Caller-owned copies | Snapshot lifetime |
Side effects are limited to collection mutation and retaining String/integer values. Side effects are limited to collection mutation and retaining String/integer values.
@@ -77,7 +78,7 @@ Side effects are limited to collection mutation and retaining String/integer val
```mermaid ```mermaid
flowchart TD flowchart TD
Start[Successful threaded request] --> Remember[Remember request] Start[Successful threaded request] --> Remember[Remember request]
Remember --> Poll[Loader polls detached snapshot] Remember --> Poll[Resource finalizer polls detached snapshot]
Poll --> Terminal{Loaded or failed?} Poll --> Terminal{Loaded or failed?}
Terminal -->|no| Poll Terminal -->|no| Poll
Terminal -->|yes| Complete[Complete with opaque status] Terminal -->|yes| Complete[Complete with opaque status]
@@ -85,7 +86,7 @@ flowchart TD
FIFO --> Permit{Permit available?} FIFO --> Permit{Permit available?}
Permit -->|no| FIFO Permit -->|no| FIFO
Permit -->|yes| Pop[Pop oldest record] Permit -->|yes| Pop[Pop oldest record]
Pop --> Finalize[Loader loads/instantiates or marks static] Pop --> Finalize[Resource finalizer loads/instantiates or marks static]
``` ```
## Lifecycle/state ## Lifecycle/state
@@ -105,19 +106,22 @@ stateDiagram-v2
sequenceDiagram sequenceDiagram
participant O as CachedAnimationObserver participant O as CachedAnimationObserver
participant L as StreamingWorldLoader participant L as StreamingWorldLoader
participant F as AnimationResourceFinalizer
participant R as ResourceLoader participant R as ResourceLoader
participant S as M2AnimationLoadPipelineState participant S as M2AnimationLoadPipelineState
participant P as M2PrototypeCacheState participant P as M2PrototypeCacheState
O->>R: load_threaded_request(GLB) O->>R: load_threaded_request(GLB)
O->>S: remember_request(path, GLB) O->>S: remember_request(path, GLB)
loop frames loop frames
L->>S: request_records_snapshot() L->>F: poll terminal requests
L->>R: load_threaded_get_status(GLB) F->>S: request_records_snapshot()
F->>R: load_threaded_get_status(GLB)
end end
L->>S: complete_request(path, status) F->>S: complete_request(path, status)
L->>S: pop_finalize_record() after permit L->>F: prepare after permit
L->>R: load_threaded_get(GLB) F->>S: pop_finalize_record()
L->>P: adopt animated prototype or mark static F->>R: load_threaded_get(GLB)
F->>P: adopt animated prototype or mark static
``` ```
## Ownership, threading and resources ## Ownership, threading and resources
@@ -125,8 +129,8 @@ sequenceDiagram
- Main thread serializes all mutation. - Main thread serializes all mutation.
- State owns only request/finalize Dictionaries with copied paths and statuses. - State owns only request/finalize Dictionaries with copied paths and statuses.
- Loader drains pending ResourceLoader paths before orderly shutdown clear. - Loader drains pending ResourceLoader paths before orderly shutdown clear.
- Loader owns PackedScene instantiation and material repair; prototype state owns - Resource finalizer owns terminal I/O/outcomes and composes scene validation;
accepted detached Node references and static-only outcomes. prototype state owns accepted detached Nodes and static-only outcomes.
## Dependency diagram ## Dependency diagram
@@ -134,8 +138,9 @@ sequenceDiagram
flowchart TB flowchart TB
Observer[M2CachedAnimationResourceObserver] --> State[M2AnimationLoadPipelineState] Observer[M2CachedAnimationResourceObserver] --> State[M2AnimationLoadPipelineState]
Observer --> Resource[ResourceLoader request] Observer --> Resource[ResourceLoader request]
Loader[StreamingWorldLoader] --> State[M2AnimationLoadPipelineState] Finalizer[M2AnimationResourceFinalizer] --> State[M2AnimationLoadPipelineState]
Loader --> Resource Finalizer --> Resource
Loader[StreamingWorldLoader] --> Finalizer
Loader --> Budget[RenderBudgetScheduler] Loader --> Budget[RenderBudgetScheduler]
Loader --> Prototype[M2PrototypeCacheState] Loader --> Prototype[M2PrototypeCacheState]
State -. no dependency .-> Resource State -. no dependency .-> Resource
@@ -185,9 +190,8 @@ no rebake or migration is required.
## Extension points ## Extension points
ResourceLoader polling may later move behind a separate adapter without ResourceLoader polling/finalization now belongs to a sibling service without
changing this value-only state contract. Animated-scene finalization is now a changing this value-only state contract.
sibling service.
## Capability status ## Capability status
@@ -195,7 +199,7 @@ sibling service.
|---|---|---|---| |---|---|---|---|
| Animated request/finalize state | Implemented extraction | Synthetic contract/source/timing verifier | Asset-backed traversal/leak/p95/p99 pending | | Animated request/finalize state | Implemented extraction | Synthetic contract/source/timing verifier | Asset-backed traversal/leak/p95/p99 pending |
| Cached request admission and GLB selection | Implemented in observer | Policy/GLB/source verifier | Asset-backed traversal pending | | 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 | | Terminal ResourceLoader polling | Implemented finalizer extraction | Status/order/source regressions | Asset-backed traversal pending |
| Animated prototype outcomes | Implemented extraction | Prototype cache verifier | Asset-backed animation fidelity pending | | Animated prototype outcomes | Implemented extraction | Prototype cache verifier | Asset-backed animation fidelity pending |
## Known gaps and risks ## Known gaps and risks
@@ -209,15 +213,17 @@ sibling service.
| Path | Responsibility | | Path | Responsibility |
|---|---| |---|---|
| `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending records, completion FIFO and metrics | | `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending records, completion FIFO and metrics |
| `src/render/m2/m2_animation_resource_finalizer.gd` | Terminal polling, Resource load and prototype outcome |
| `src/render/m2/m2_cached_animation_resource_observer.gd` | Eligibility, GLB selection and request admission | | `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_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/render/m2/m2_prototype_cache_state.gd` | Animated prototype/static-only outcomes |
| `src/scenes/streaming/streaming_world_loader.gd` | Native eligibility/build, polling, permits and adoption | | `src/scenes/streaming/streaming_world_loader.gd` | Permit loop, material lookup and composition |
| `src/tools/verify_m2_animation_load_pipeline_state.gd` | Lifecycle/boundary/timing regression | | `src/tools/verify_m2_animation_load_pipeline_state.gd` | Lifecycle/boundary/timing regression |
## Related decisions and references ## Related decisions and references
- [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md) - [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md)
- [`m2-animation-resource-finalizer.md`](m2-animation-resource-finalizer.md)
- [`m2-prototype-cache-state.md`](m2-prototype-cache-state.md) - [`m2-prototype-cache-state.md`](m2-prototype-cache-state.md)
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md) - [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
- [`world-renderer.md`](world-renderer.md) - [`world-renderer.md`](world-renderer.md)
@@ -0,0 +1,249 @@
# M2 Animation Resource Finalizer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-ANIMATION-RESOURCE-FINALIZER-001` |
| Owners | Cached animated Resource polling, terminal load and prototype outcome |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animation-resource-finalizer`, 2026-07-18 |
| Profiles/capabilities | Existing optional cached-GLB animated M2 path |
## Purpose
Drain cached animated M2 ResourceLoader work without keeping terminal I/O and
prototype outcome logic in `StreamingWorldLoader`. The service polls pending
requests, prepares a detached scene candidate and completes material repair,
validation, cache adoption or static-only fallback.
## Non-goals
- Select cached GLB paths, apply allow/deny policy or admit threaded requests.
- Select/build native animated M2 prototypes.
- Look up the static material prototype used for repair.
- Own render permits, build jobs, playback, instances or SceneTree roots.
- Change ResourceLoader ordering, cache formats, profiles or visible output.
## Context and boundaries
```mermaid
flowchart LR
Observer[M2CachedAnimationResourceObserver] --> Pipeline[M2AnimationLoadPipelineState]
Loader[StreamingWorldLoader] --> Finalizer[M2AnimationResourceFinalizer]
Pipeline --> Finalizer
ResourceLoader --> Finalizer
Finalizer --> SceneFinalizer[M2AnimatedSceneFinalizer]
Finalizer --> Cache[M2PrototypeCacheState]
Loader --> Material[Material prototype lookup]
Material --> Finalizer
```
The two-phase API preserves the historical ordering: a loaded Resource must
instantiate a candidate before the loader performs material-prototype lookup.
## Public API
| Symbol | Kind | Purpose | Failure behavior |
|---|---|---|---|
| `poll_terminal_requests(pipeline, cache)` | I/O command | Move LOADED/FAILED requests to completion FIFO | Invalid composition returns zero |
| `prepare_next_candidate(pipeline, cache)` | I/O command/query | Pop one record, load Resource and instantiate candidate | Skip/failed/invalid outcome returns empty Dictionary |
| `finalize_prepared_candidate(preparation, material_source, cache, debug)` | Command/query | Repair, validate, adopt/log or mark static-only | Invalid/finalization failure returns null |
| `load_threaded_get_status(path)` | I/O adapter | Production status query with injectable test seam | Returns ResourceLoader status |
| `load_threaded_get(path)` | I/O adapter | Production terminal Resource retrieval with injectable test seam | May return null |
## Inputs and outputs
| Direction | Data | Producer | Consumer | Ownership/lifetime |
|---|---|---|---|---|
| Input | Pending/finalize records | Animation pipeline | Finalizer | Pipeline-owned copied Dictionaries |
| Input | ResourceLoader status/Resource | Godot ResourceLoader | Finalizer | Resource reference for one operation |
| Input | Material source root | Loader lookup | Scene finalizer | Borrowed exact Node3D |
| Input | Prototype/static-only state | Prototype cache | Finalizer | Borrowed service; loader session |
| Output | Prepared candidate Dictionary | Finalizer | Loader/finalizer completion | Caller-owned, immediate operation |
| Output | Canonical animated prototype | Prototype cache | Loader build path | Borrowed exact Node3D |
| Output | Static-only outcome | Finalizer | Prototype cache | Copied path; loader session |
| Output | Success diagnostic | Finalizer | Runtime log | Debug-gated one-line record |
## Data flow
```mermaid
flowchart TD
Poll[Poll pending records in insertion order] --> Path{Resource path empty?}
Path -->|yes| Discard[Discard and mark static-only]
Path -->|no| Status{LOADED or FAILED?}
Status -->|no| Pending[Keep pending]
Status -->|yes| Complete[Append completion FIFO]
Complete --> Permit[Loader consumes one finalize permit]
Permit --> Pop[Pop oldest terminal record]
Pop --> Existing{Empty/cached/static-only?}
Existing -->|yes| Skip[Finish permit operation]
Existing -->|no| Loaded{Status LOADED?}
Loaded -->|no| Mark[Mark static-only]
Loaded -->|yes| Get[Get terminal Resource]
Get --> Instantiate[Instantiate detached candidate]
Instantiate --> Valid{Candidate exists?}
Valid -->|no| Mark
Valid -->|yes| Material[Loader resolves material source]
Material --> Repair[Repair and validate]
Repair --> Prototype{Prototype accepted?}
Prototype -->|no| Mark
Prototype -->|yes| Adopt[Adopt and optionally log]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Polling
Polling --> Pending: nonterminal status
Polling --> TerminalQueued: LOADED or FAILED
Polling --> StaticOnly: empty Resource path
TerminalQueued --> Skipped: cached/static/invalid record
TerminalQueued --> StaticOnly: failed load or invalid candidate
TerminalQueued --> Prepared: detached candidate
Prepared --> Adopted: repair/finalize succeeds
Prepared --> StaticOnly: validation fails
Pending --> [*]
Skipped --> [*]
StaticOnly --> [*]
Adopted --> [*]
```
## Main sequence
```mermaid
sequenceDiagram
participant L as StreamingWorldLoader
participant F as AnimationResourceFinalizer
participant P as AnimationLoadPipelineState
participant R as ResourceLoader
participant S as AnimatedSceneFinalizer
participant C as PrototypeCacheState
L->>F: poll_terminal_requests(P, C)
F->>R: load_threaded_get_status(path)
F->>P: complete terminal request
L->>L: consume M2_ANIMATION_FINALIZE permit
L->>F: prepare_next_candidate(P, C)
F->>P: pop oldest finalize record
F->>R: load_threaded_get(path)
F->>S: instantiate_candidate(Resource)
alt candidate available
F-->>L: normalized/path/candidate
L->>L: resolve material prototype
L->>F: finalize_prepared_candidate(...)
F->>S: repair_materials + finalize_candidate
F->>C: adopt prototype or mark static-only
else rejected
F->>C: mark static-only
end
```
## Dependency diagram
```mermaid
flowchart TB
Finalizer[M2AnimationResourceFinalizer] --> ResourceLoader
Finalizer --> Pipeline[M2AnimationLoadPipelineState]
Finalizer --> SceneFinalizer[M2AnimatedSceneFinalizer]
Finalizer --> Cache[M2PrototypeCacheState]
Loader[StreamingWorldLoader] --> Finalizer
Loader --> Scheduler[RenderBudgetScheduler]
Loader --> MaterialLookup[Material prototype lookup]
Finalizer -. no dependency .-> Scheduler
Finalizer -. no dependency .-> BuildQueue[M2BuildQueue]
Finalizer -. no dependency .-> Playback[M2AnimationPlaybackController]
```
## Ownership, threading and resources
- All methods run synchronously on the renderer main thread.
- Pipeline owns pending and completion records; one loader permit pops one record.
- The prepared Dictionary temporarily owns the detached candidate reference. The
loader must call completion synchronously after material lookup.
- Existing scene finalizer frees rejected candidate roots. Prototype cache owns
accepted detached prototypes until final shutdown.
- Service retains only scene-finalizer and optional test-adapter references. The
production ResourceLoader path creates no self-reference or retained Resource.
- Loader retains scheduler permits, material lookup and all instance/SceneTree work.
## Errors, cancellation and recovery
| Failure/state | Behavior | Recovery |
|---|---|---|
| Missing service dependency | Return zero/empty/null | Correct renderer composition |
| Empty Resource path | Discard pending request and mark static-only | Repair cache and start new session |
| Nonterminal status | Keep request pending | Poll next tick |
| FAILED terminal status | Pop one record and mark static-only | Repair cache and start new session |
| Existing cached/static state | Pop and skip without terminal Resource get | None |
| Null/unsupported Resource | Mark static-only | Repair imported cache |
| Candidate validation failure | Scene finalizer frees candidate; mark static-only | Repair animation/cache |
| Tile cancellation | No direct transition; shared cache work continues | Loader queue remains authoritative |
| Shutdown | Loader drains ResourceLoader before pipeline/cache clear | New loader starts empty |
## Configuration and capabilities
The module adds no setting. Existing cached-animation allow/deny policy,
`m2_animation_finalize_ops_per_tick`, debug flag and scheduler lane remain exact.
## Persistence, cache and migration
No persistence, schema or cache path changes are introduced. Existing `.glb`
imports and `pivot_prefix_v1` eligibility remain owned by the cached observer.
## Diagnostics and observability
Successful debug-enabled adoption emits the unchanged `M2_ANIM_CACHE` record
with normalized path, Resource cache path and AnimationPlayer count. Pipeline
work metrics remain pending plus completion FIFO size.
## Verification
- Dedicated verifier covers pending/loaded/failed polling order, empty path,
completion FIFO, cached/static skips, terminal Resource get, candidate identity,
material repair arguments, adoption, rejection, source ownership and timing.
- Pipeline, scene-finalizer, prototype-cache, cached-observer, shutdown, facade,
internal-access and baseline regressions protect adjacent behavior.
- Fidelity evidence is exact lifecycle/I/O extraction only; no private asset,
original-client animation, visual, leak-pressure or p95/p99 claim is made.
## Extension points
Material-prototype lookup may later become a dedicated resource observer, which
would allow the entire two-phase operation to compose outside the loader. Static
Mesh Resource finalization remains separate to avoid premature generalization.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Animated request terminal polling | Implemented extraction | Status/order/source verifier | Asset-backed long traversal pending |
| Animated candidate finalization/outcome | Implemented extraction | Identity/repair/adoption verifier | Private visual/leak/p95/p99 pending |
| Material prototype lookup | Loader-owned | Existing renderer regressions | Dedicated observer optional |
## Known gaps and risks
- Terminal Resource get, instantiation and repair remain synchronous main-thread work.
- Prepared candidate ownership relies on immediate completion by the loader adapter.
- Private asset traversal, descriptor pressure, leak and p95/p99 evidence is absent.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_animation_resource_finalizer.gd` | Poll/load/prepare/finalize/cache/log service |
| `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending and completion FIFO ownership |
| `src/render/m2/m2_animated_scene_finalizer.gd` | Candidate instantiation/repair/validation/rejection cleanup |
| `src/render/m2/m2_prototype_cache_state.gd` | Accepted prototype/static-only ownership |
| `src/scenes/streaming/streaming_world_loader.gd` | Permits, material lookup and synchronous composition |
| `src/tools/verify_m2_animation_resource_finalizer.gd` | Polling/finalization/boundary/timing regression |
## Related decisions and references
- [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md)
- [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md)
- [`m2-cached-animation-resource-observer.md`](m2-cached-animation-resource-observer.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)
@@ -151,8 +151,9 @@ 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.
- The sibling native observer owns the native build attempt. Loader remains owner - The sibling native observer owns the native build attempt and the animation
of terminal polling/finalize, permits, materialization and SceneTree mutation. resource finalizer owns terminal polling/finalize. Loader retains permits,
material lookup, materialization and SceneTree mutation.
## Errors, cancellation and recovery ## Errors, cancellation and recovery
@@ -197,9 +198,8 @@ queue and hitch metrics are unchanged.
## Extension points ## Extension points
Native animation observation can become a sibling service without changing this Native and cached observation remain sibling services. Terminal ResourceLoader
cached-GLB contract. Terminal ResourceLoader polling/finalization may later move polling/finalization uses the dedicated finalizer while retaining pipeline state.
behind a dedicated adapter while retaining the pipeline state.
## Capability status ## Capability status
@@ -207,7 +207,7 @@ behind a dedicated adapter while retaining the pipeline state.
|---|---|---|---| |---|---|---|---|
| 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 | Implemented sibling extraction | Native observer lifecycle/source verifier | Asset-backed traversal pending | | 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 | Implemented finalizer extraction | Pipeline/finalizer regressions | Asset-backed traversal pending |
## Known gaps and risks ## Known gaps and risks
@@ -224,6 +224,7 @@ behind a dedicated adapter while retaining the pipeline state.
| `src/render/m2/m2_cached_animation_resource_observer.gd` | Cached GLB policy, request and snapshot production | | `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_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_animation_resource_finalizer.gd` | Terminal polling/load/finalize outcomes |
| `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/render/m2/m2_native_animation_resource_observer.gd` | Native-first candidate/read/build/cache observation | | `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/scenes/streaming/streaming_world_loader.gd` | Composition, fallback order, finalize and execution |
+4 -2
View File
@@ -64,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 | 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 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 | Detached animated Node3D | GLB/native animation adapter | Cache state | Adopted on success | Until final shutdown |
| Input | Missing/static-only outcome | Loader/observer failure adapter | Cache state | Boolean set entry | Until final shutdown | | Input | Missing/static-only outcome | Resource observers/finalizers | 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 | 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 | | Output | Detached path-only snapshot | Cache state | Tests/diagnostics | Fresh caller-owned arrays | One query |
@@ -192,7 +192,8 @@ 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 success logging belongs to the native observer. success logging belongs to the native observer and cached terminal success
logging belongs to the animation resource finalizer.
- 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.
@@ -238,6 +239,7 @@ this state without moving ResourceLoader or builder ownership into cache state.
|---|---| |---|---|
| `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/render/m2/m2_native_animation_resource_observer.gd` | Animated raw/build and positive/static-only transitions | | `src/render/m2/m2_native_animation_resource_observer.gd` | Animated raw/build and positive/static-only transitions |
| `src/render/m2/m2_animation_resource_finalizer.gd` | Cached terminal positive/static-only transitions |
| `src/scenes/streaming/streaming_world_loader.gd` | Normalization, observer order, remaining I/O/fallback adapters | | `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 |
+7 -2
View File
@@ -148,6 +148,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
| `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 | | `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 | | `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 |
| `M2AnimationLoadPipelineState` | Internal M2 async-state service | Owns animated scene pending request records and terminal finalize FIFO | Renderer main thread; map/session | Empty/duplicate/unknown transitions rejected | | `M2AnimationLoadPipelineState` | Internal M2 async-state service | Owns animated scene pending request records and terminal finalize FIFO | Renderer main thread; map/session | Empty/duplicate/unknown transitions rejected |
| `M2AnimationResourceFinalizer` | Internal M2 terminal-I/O service | Polls cached animation requests, loads/finalizes scenes and publishes prototype outcome | Renderer main thread; stateless across calls | Invalid/failed candidates mark static-only |
| `M2AnimatedSceneFinalizer` | Internal M2 scene-finalization service | Instantiates candidates, repairs materials and requires AnimationPlayer descendants | Renderer main thread; stateless after each call | Invalid/rejected detached roots are freed | | `M2AnimatedSceneFinalizer` | Internal M2 scene-finalization service | Instantiates candidates, repairs materials and requires AnimationPlayer descendants | Renderer main thread; stateless after each call | Invalid/rejected detached roots are freed |
| `M2AnimationPlaybackController` | Internal M2 playback service | Applies stable phase, default imported animation and native animator startup | Renderer main thread; stateless after each call | Missing inventories/names no-op through historical fallbacks | | `M2AnimationPlaybackController` | Internal M2 playback service | Applies stable phase, default imported animation and native animator startup | Renderer main thread; stateless after each call | Missing inventories/names no-op through historical fallbacks |
| `M2AnimatedInstanceMaterializer` | Internal M2 scene-materialization service | Duplicates ordered animated instances, applies render settings, starts playback and attaches a non-empty batch | Renderer main thread; stateless after each call | Invalid/empty input or all failed duplicates returns empty | | `M2AnimatedInstanceMaterializer` | Internal M2 scene-materialization service | Duplicates ordered animated instances, applies render settings, starts playback and attaches a non-empty batch | Renderer main thread; stateless after each call | Invalid/empty input or all failed duplicates returns empty |
@@ -432,7 +433,8 @@ sequenceDiagram
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 native observer owns synchronous native build; the snapshot production. The native observer owns synchronous native build; the
loader retains ResourceLoader polling/finalize, permits and terminal adoption. animation resource finalizer owns cached terminal polling/load/finalize and
adoption. Loader retains permits and material-prototype lookup.
- `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.
@@ -595,8 +597,9 @@ 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 | Asset-backed traversal and terminal polling extraction pending | | M2 cached animation resource observer | Implemented extraction | Cache/pending/policy/GLB/request/source/timing contract | Asset-backed traversal pending |
| M2 native animation resource observer | Implemented extraction | Candidate/cache/raw/build/adoption/source/timing contract | Asset-backed traversal/leak/p95/p99 pending | | M2 native animation resource observer | Implemented extraction | Candidate/cache/raw/build/adoption/source/timing contract | Asset-backed traversal/leak/p95/p99 pending |
| M2 animation resource finalizer | Implemented extraction | Status/FIFO/load/candidate/repair/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 |
@@ -664,6 +667,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/render/liquid/adt_water_scene_finalizer.gd` | Main-thread ADT water build, tile attachment and optional editor ownership | | `src/render/liquid/adt_water_scene_finalizer.gd` | Main-thread ADT water build, tile attachment and optional editor ownership |
| `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Billboard/UV-rotation stale cached-mesh rebuild decision and memoization | | `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Billboard/UV-rotation stale cached-mesh rebuild decision and memoization |
| `src/render/m2/m2_animation_load_pipeline_state.gd` | Animated M2 pending Resource paths, terminal statuses and finalize FIFO | | `src/render/m2/m2_animation_load_pipeline_state.gd` | Animated M2 pending Resource paths, terminal statuses and finalize FIFO |
| `src/render/m2/m2_animation_resource_finalizer.gd` | Cached animation terminal polling/load/finalize outcomes |
| `src/render/m2/m2_animated_scene_finalizer.gd` | Animated scene candidate ownership, material repair and player validation | | `src/render/m2/m2_animated_scene_finalizer.gd` | Animated scene candidate ownership, material repair and player validation |
| `src/render/m2/m2_animation_playback_controller.gd` | Per-instance phase, selection, native copy/start and imported playback | | `src/render/m2/m2_animation_playback_controller.gd` | Per-instance phase, selection, native copy/start and imported playback |
| `src/render/m2/m2_animated_instance_materializer.gd` | Animated duplicate/render/playback startup and non-empty batch attachment | | `src/render/m2/m2_animated_instance_materializer.gd` | Animated duplicate/render/playback startup and non-empty batch attachment |
@@ -697,6 +701,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/tools/verify_adt_water_scene_finalizer.gd` | Synthetic ADT water scene/ownership/boundary/timing regression | | `src/tools/verify_adt_water_scene_finalizer.gd` | Synthetic ADT water scene/ownership/boundary/timing regression |
| `src/tools/verify_m2_runtime_mesh_rebuild_classifier.gd` | M2 rebuild predicate/cache/boundary/timing regression | | `src/tools/verify_m2_runtime_mesh_rebuild_classifier.gd` | M2 rebuild predicate/cache/boundary/timing regression |
| `src/tools/verify_m2_animation_load_pipeline_state.gd` | Animated M2 request/terminal/FIFO/boundary/timing regression | | `src/tools/verify_m2_animation_load_pipeline_state.gd` | Animated M2 request/terminal/FIFO/boundary/timing regression |
| `src/tools/verify_m2_animation_resource_finalizer.gd` | Animated M2 polling/load/finalize/adoption/boundary/timing regression |
| `src/tools/verify_m2_animated_scene_finalizer.gd` | Animated M2 candidate/material/player/lifetime/boundary/timing regression | | `src/tools/verify_m2_animated_scene_finalizer.gd` | Animated M2 candidate/material/player/lifetime/boundary/timing regression |
| `src/tools/verify_m2_animation_playback_controller.gd` | Animated M2 phase/selection/playback/native/boundary/timing regression | | `src/tools/verify_m2_animation_playback_controller.gd` | Animated M2 phase/selection/playback/native/boundary/timing regression |
| `src/tools/verify_m2_animated_instance_materializer.gd` | Animated M2 order/render/playback/attachment/boundary/timing regression | | `src/tools/verify_m2_animated_instance_materializer.gd` | Animated M2 order/render/playback/attachment/boundary/timing regression |
@@ -0,0 +1,187 @@
class_name M2AnimationResourceFinalizer
extends RefCounted
## Polls cached animated M2 ResourceLoader requests and finalizes terminal scenes.
## The caller owns permits, material-source lookup and SceneTree materialization.
const M2_ANIMATED_SCENE_FINALIZER_SCRIPT := preload(
"res://src/render/m2/m2_animated_scene_finalizer.gd"
)
var _animated_scene_finalizer: RefCounted
var _resource_loader_adapter: Object
func _init(
animated_scene_finalizer: RefCounted = null,
resource_loader_adapter: Object = null
) -> void:
_animated_scene_finalizer = animated_scene_finalizer
if _animated_scene_finalizer == null:
_animated_scene_finalizer = M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new()
_resource_loader_adapter = resource_loader_adapter
## Moves terminal threaded requests into the pipeline finalize FIFO.
## Empty resource paths retain the historical immediate static-only outcome.
func poll_terminal_requests(
load_pipeline_state: RefCounted,
prototype_cache_state: RefCounted
) -> int:
if load_pipeline_state == null or prototype_cache_state == null:
return 0
var completed_request_count := 0
var request_records: Array = load_pipeline_state.call(
"request_records_snapshot"
)
for request_variant in request_records:
if not (request_variant is Dictionary):
continue
var request: Dictionary = request_variant
var normalized_relative_path := String(request.get("normalized", ""))
var resource_path := String(request.get("path", ""))
if resource_path.is_empty():
load_pipeline_state.call(
"discard_request",
normalized_relative_path
)
prototype_cache_state.call(
"mark_animation_static",
normalized_relative_path
)
completed_request_count += 1
continue
var load_status := load_threaded_get_status(resource_path)
if (
load_status != ResourceLoader.THREAD_LOAD_LOADED
and load_status != ResourceLoader.THREAD_LOAD_FAILED
):
continue
load_pipeline_state.call(
"complete_request",
normalized_relative_path,
load_status
)
completed_request_count += 1
return completed_request_count
## Pops one terminal record and prepares a detached animated scene candidate.
## An empty result means the record was skipped or resolved as static-only.
func prepare_next_candidate(
load_pipeline_state: RefCounted,
prototype_cache_state: RefCounted
) -> Dictionary:
if load_pipeline_state == null or prototype_cache_state == null:
return {}
var terminal_record: Dictionary = load_pipeline_state.call(
"pop_finalize_record"
)
if terminal_record.is_empty():
return {}
var normalized_relative_path := String(
terminal_record.get("normalized", "")
)
if (
normalized_relative_path.is_empty()
or bool(prototype_cache_state.call(
"has_animated_prototype",
normalized_relative_path
))
or bool(prototype_cache_state.call(
"is_animation_static",
normalized_relative_path
))
):
return {}
if int(terminal_record.get(
"status",
ResourceLoader.THREAD_LOAD_FAILED
)) != ResourceLoader.THREAD_LOAD_LOADED:
prototype_cache_state.call(
"mark_animation_static",
normalized_relative_path
)
return {}
var resource_path := String(terminal_record.get("path", ""))
var loaded_resource := load_threaded_get(resource_path)
var candidate := _animated_scene_finalizer.call(
"instantiate_candidate",
loaded_resource
) as Node3D
if candidate == null:
prototype_cache_state.call(
"mark_animation_static",
normalized_relative_path
)
return {}
return {
"normalized": normalized_relative_path,
"path": resource_path,
"candidate": candidate,
}
## Repairs, validates and adopts one prepared candidate. The preparation must be
## completed synchronously so its detached candidate always receives an owner.
func finalize_prepared_candidate(
preparation: Dictionary,
material_source_root: Node3D,
prototype_cache_state: RefCounted,
debug_logging_enabled: bool = false
) -> Node3D:
if prototype_cache_state == null:
return null
var normalized_relative_path := String(preparation.get("normalized", ""))
var candidate := preparation.get("candidate", null) as Node3D
if normalized_relative_path.is_empty() or candidate == null:
return null
_animated_scene_finalizer.call(
"repair_materials",
candidate,
material_source_root
)
var finalization: Dictionary = _animated_scene_finalizer.call(
"finalize_candidate",
candidate
)
var prototype := finalization.get("prototype", null) as Node3D
if prototype == null:
prototype_cache_state.call(
"mark_animation_static",
normalized_relative_path
)
return null
var canonical_prototype := prototype_cache_state.call(
"adopt_animated_prototype",
normalized_relative_path,
prototype
) as Node3D
if debug_logging_enabled:
print("M2_ANIM_CACHE path=%s cache=%s players=%d" % [
normalized_relative_path,
String(preparation.get("path", "")),
int(finalization.get("animation_player_count", 0)),
])
return canonical_prototype
## Production ResourceLoader status adapter; injectable in synthetic tests.
func load_threaded_get_status(resource_path: String) -> int:
if _resource_loader_adapter != null:
return int(_resource_loader_adapter.call(
"load_threaded_get_status",
resource_path
))
return ResourceLoader.load_threaded_get_status(resource_path)
## Production ResourceLoader terminal-result adapter; injectable in tests.
func load_threaded_get(resource_path: String) -> Resource:
if _resource_loader_adapter != null:
return _resource_loader_adapter.call(
"load_threaded_get",
resource_path
) as Resource
return ResourceLoader.load_threaded_get(resource_path)
@@ -0,0 +1 @@
uid://2rnsjucg3dul
+22 -39
View File
@@ -96,6 +96,9 @@ const M2_ANIMATION_LOAD_PIPELINE_STATE_SCRIPT := preload(
const M2_ANIMATED_SCENE_FINALIZER_SCRIPT := preload( const M2_ANIMATED_SCENE_FINALIZER_SCRIPT := preload(
"res://src/render/m2/m2_animated_scene_finalizer.gd" "res://src/render/m2/m2_animated_scene_finalizer.gd"
) )
const M2_ANIMATION_RESOURCE_FINALIZER_SCRIPT := preload(
"res://src/render/m2/m2_animation_resource_finalizer.gd"
)
const M2_ANIMATED_INSTANCE_MATERIALIZER_SCRIPT := preload( const M2_ANIMATED_INSTANCE_MATERIALIZER_SCRIPT := preload(
"res://src/render/m2/m2_animated_instance_materializer.gd" "res://src/render/m2/m2_animated_instance_materializer.gd"
) )
@@ -309,6 +312,9 @@ var _m2_mesh_resource_extractor := M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new()
var _m2_mesh_load_pipeline_state := M2_MESH_LOAD_PIPELINE_STATE_SCRIPT.new() var _m2_mesh_load_pipeline_state := M2_MESH_LOAD_PIPELINE_STATE_SCRIPT.new()
var _m2_animation_load_pipeline_state := M2_ANIMATION_LOAD_PIPELINE_STATE_SCRIPT.new() var _m2_animation_load_pipeline_state := M2_ANIMATION_LOAD_PIPELINE_STATE_SCRIPT.new()
var _m2_animated_scene_finalizer := M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new() var _m2_animated_scene_finalizer := M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new()
var _m2_animation_resource_finalizer := M2_ANIMATION_RESOURCE_FINALIZER_SCRIPT.new(
_m2_animated_scene_finalizer
)
var _m2_animated_instance_materializer := M2_ANIMATED_INSTANCE_MATERIALIZER_SCRIPT.new() var _m2_animated_instance_materializer := M2_ANIMATED_INSTANCE_MATERIALIZER_SCRIPT.new()
var _wmo_build_jobs: Dictionary = {} var _wmo_build_jobs: Dictionary = {}
var _wmo_build_queue: Array = [] var _wmo_build_queue: Array = []
@@ -4238,50 +4244,27 @@ func _drain_m2_group_results() -> void:
func _drain_m2_animation_loads() -> void: func _drain_m2_animation_loads() -> void:
for pending in _m2_animation_load_pipeline_state.request_records_snapshot(): _m2_animation_resource_finalizer.poll_terminal_requests(
var normalized_rel := String(pending.get("normalized", "")) _m2_animation_load_pipeline_state,
var path := String(pending.get("path", "")) _m2_prototype_cache_state
if path.is_empty(): )
_m2_animation_load_pipeline_state.discard_request(normalized_rel)
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
continue
_m2_animation_load_pipeline_state.complete_request(normalized_rel, status)
while _m2_animation_load_pipeline_state.has_finalize_record() and _render_budget_scheduler.try_consume_permit( while _m2_animation_load_pipeline_state.has_finalize_record() and _render_budget_scheduler.try_consume_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE): RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE):
var pending: Dictionary = _m2_animation_load_pipeline_state.pop_finalize_record() var preparation := _m2_animation_resource_finalizer.prepare_next_candidate(
var normalized_rel := String(pending.get("normalized", "")) _m2_animation_load_pipeline_state,
if ( _m2_prototype_cache_state
normalized_rel.is_empty() )
or _m2_prototype_cache_state.has_animated_prototype(normalized_rel) if preparation.is_empty():
or _m2_prototype_cache_state.is_animation_static(normalized_rel)
):
continue continue
if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED: var normalized_rel := String(preparation.get("normalized", ""))
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
continue
var path := String(pending.get("path", ""))
var resource: Resource = ResourceLoader.load_threaded_get(path)
var candidate := _m2_animated_scene_finalizer.instantiate_candidate(resource)
if candidate != null:
var material_source := _get_or_load_m2_material_prototype(normalized_rel) var material_source := _get_or_load_m2_material_prototype(normalized_rel)
_m2_animated_scene_finalizer.repair_materials(candidate, material_source) _m2_animation_resource_finalizer.finalize_prepared_candidate(
var finalization := _m2_animated_scene_finalizer.finalize_candidate(candidate) preparation,
var prototype: Node3D = finalization.get("prototype", null) material_source,
if prototype != null: _m2_prototype_cache_state,
_m2_prototype_cache_state.adopt_animated_prototype(normalized_rel, prototype) debug_streaming
if debug_streaming: )
print("M2_ANIM_CACHE path=%s cache=%s players=%d" % [
normalized_rel,
path,
int(finalization.get("animation_player_count", 0)),
])
continue
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
func _drain_m2_mesh_loads() -> void: func _drain_m2_mesh_loads() -> void:
+30 -10
View File
@@ -5,6 +5,9 @@ extends SceneTree
const FINALIZER_SCRIPT := preload("res://src/render/m2/m2_animated_scene_finalizer.gd") const FINALIZER_SCRIPT := preload("res://src/render/m2/m2_animated_scene_finalizer.gd")
const FINALIZER_PATH := "res://src/render/m2/m2_animated_scene_finalizer.gd" const FINALIZER_PATH := "res://src/render/m2/m2_animated_scene_finalizer.gd"
const RESOURCE_FINALIZER_PATH := (
"res://src/render/m2/m2_animation_resource_finalizer.gd"
)
const MATERIALIZER_PATH := "res://src/render/m2/m2_animated_instance_materializer.gd" const MATERIALIZER_PATH := "res://src/render/m2/m2_animated_instance_materializer.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
@@ -116,6 +119,9 @@ func _verify_material_mapping(failures: Array[String]) -> void:
func _verify_ownership_boundaries(failures: Array[String]) -> void: func _verify_ownership_boundaries(failures: Array[String]) -> void:
var finalizer_source := FileAccess.get_file_as_string(FINALIZER_PATH) var finalizer_source := FileAccess.get_file_as_string(FINALIZER_PATH)
var resource_finalizer_source := FileAccess.get_file_as_string(
RESOURCE_FINALIZER_PATH
)
var materializer_source := FileAccess.get_file_as_string(MATERIALIZER_PATH) var materializer_source := FileAccess.get_file_as_string(MATERIALIZER_PATH)
var loader_source := FileAccess.get_file_as_string(LOADER_PATH) var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
_expect_true(loader_source.contains("M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new()"), "loader composes finalizer", failures) _expect_true(loader_source.contains("M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new()"), "loader composes finalizer", failures)
@@ -126,27 +132,41 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
]: ]:
_expect_false(loader_source.contains(removed_loader_function), "legacy helper removed: %s" % removed_loader_function, failures) _expect_false(loader_source.contains(removed_loader_function), "legacy helper removed: %s" % removed_loader_function, failures)
for delegated_call in [ for delegated_call in [
"_m2_animated_scene_finalizer.instantiate_candidate(resource)", "\"instantiate_candidate\"",
"_m2_animated_scene_finalizer.repair_materials(candidate, material_source)", "\"repair_materials\"",
"_m2_animated_scene_finalizer.finalize_candidate(candidate)", "\"finalize_candidate\"",
"_m2_animated_scene_finalizer.mesh_instances_in_subtree(root)",
]: ]:
_expect_equal(loader_source.count(delegated_call), 1, "single loader delegation: %s" % delegated_call, failures) _expect_equal(
resource_finalizer_source.count(delegated_call),
1,
"single resource-finalizer delegation: %s" % delegated_call,
failures
)
_expect_equal(
loader_source.count("_m2_animated_scene_finalizer.mesh_instances_in_subtree(root)"),
1,
"single loader mesh traversal delegation",
failures
)
_expect_equal( _expect_equal(
materializer_source.count("_animated_scene_finalizer.animation_players_in_subtree("), materializer_source.count("_animated_scene_finalizer.animation_players_in_subtree("),
1, 1,
"single materializer player-inventory delegation", "single materializer player-inventory delegation",
failures failures
) )
for retained_loader_rule in [ for retained_renderer_rule in [
"ResourceLoader.load_threaded_get(path)", "ResourceLoader.load_threaded_get(resource_path)",
"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE", "RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE",
"_get_or_load_m2_material_prototype(normalized_rel)", "_get_or_load_m2_material_prototype(normalized_rel)",
"_m2_prototype_cache_state.adopt_animated_prototype(", "\"adopt_animated_prototype\"",
"_m2_prototype_cache_state.mark_animation_static(normalized_rel)", "\"mark_animation_static\"",
"M2_ANIM_CACHE path=%s cache=%s players=%d", "M2_ANIM_CACHE path=%s cache=%s players=%d",
]: ]:
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures) _expect_true(
(loader_source + resource_finalizer_source).contains(retained_renderer_rule),
"renderer retains %s" % retained_renderer_rule,
failures
)
for forbidden_dependency in [ for forbidden_dependency in [
"ResourceLoader.", "ResourceLoader.",
"FileAccess.", "FileAccess.",
@@ -8,6 +8,9 @@ const PIPELINE_PATH := "res://src/render/m2/m2_animation_load_pipeline_state.gd"
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 RESOURCE_FINALIZER_PATH := (
"res://src/render/m2/m2_animation_resource_finalizer.gd"
)
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
@@ -84,6 +87,9 @@ func _verify_discard_metrics_clear_and_diagnostics(failures: Array[String]) -> v
func _verify_ownership_boundaries(failures: Array[String]) -> void: func _verify_ownership_boundaries(failures: Array[String]) -> void:
var pipeline_source := FileAccess.get_file_as_string(PIPELINE_PATH) var pipeline_source := FileAccess.get_file_as_string(PIPELINE_PATH)
var observer_source := FileAccess.get_file_as_string(OBSERVER_PATH) var observer_source := FileAccess.get_file_as_string(OBSERVER_PATH)
var resource_finalizer_source := FileAccess.get_file_as_string(
RESOURCE_FINALIZER_PATH
)
var loader_source := FileAccess.get_file_as_string(LOADER_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_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_load_requests:"), "legacy request field removed", failures)
@@ -95,14 +101,18 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
"cached observer owns request admission", "cached observer owns request admission",
failures failures
) )
for retained_loader_rule in [ for retained_renderer_rule in [
"ResourceLoader.load_threaded_get_status(path)", "ResourceLoader.load_threaded_get_status(resource_path)",
"ResourceLoader.load_threaded_get(path)", "ResourceLoader.load_threaded_get(resource_path)",
"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE", "RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE",
"_m2_prototype_cache_state.adopt_animated_prototype(", "\"adopt_animated_prototype\"",
"_m2_prototype_cache_state.mark_animation_static(", "\"mark_animation_static\"",
]: ]:
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures) _expect_true(
(loader_source + resource_finalizer_source).contains(retained_renderer_rule),
"renderer retains %s" % retained_renderer_rule,
failures
)
for forbidden_dependency in [ for forbidden_dependency in [
"ResourceLoader.", "ResourceLoader.",
"WorkerThreadPool.", "WorkerThreadPool.",
@@ -0,0 +1,448 @@
extends SceneTree
const FINALIZER_SCRIPT := preload(
"res://src/render/m2/m2_animation_resource_finalizer.gd"
)
const PIPELINE_SCRIPT := preload(
"res://src/render/m2/m2_animation_load_pipeline_state.gd"
)
const PROTOTYPE_CACHE_SCRIPT := preload(
"res://src/render/m2/m2_prototype_cache_state.gd"
)
const FINALIZER_PATH := "res://src/render/m2/m2_animation_resource_finalizer.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
class FakeResourceLoaderAdapter extends RefCounted:
var statuses: Dictionary = {}
var resources: Dictionary = {}
var status_paths: Array[String] = []
var load_paths: Array[String] = []
func load_threaded_get_status(resource_path: String) -> int:
status_paths.append(resource_path)
return int(statuses.get(
resource_path,
ResourceLoader.THREAD_LOAD_IN_PROGRESS
))
func load_threaded_get(resource_path: String) -> Resource:
load_paths.append(resource_path)
return resources.get(resource_path, null) as Resource
class FakeAnimatedSceneFinalizer extends RefCounted:
var candidate_by_resource: Dictionary = {}
var finalization_by_candidate: Dictionary = {}
var instantiated_resources: Array[Resource] = []
var repaired_candidates: Array[Node3D] = []
var repaired_material_sources: Array[Node3D] = []
func instantiate_candidate(resource: Resource) -> Node3D:
instantiated_resources.append(resource)
return candidate_by_resource.get(resource, null) as Node3D
func repair_materials(candidate: Node3D, material_source: Node3D) -> void:
repaired_candidates.append(candidate)
repaired_material_sources.append(material_source)
func finalize_candidate(candidate: Node3D) -> Dictionary:
return finalization_by_candidate.get(candidate, {}) as Dictionary
class EmptyPathPipeline extends RefCounted:
var discarded_path: String = ""
func request_records_snapshot() -> Array[Dictionary]:
return [{"normalized": "world/empty.m2", "path": ""}]
func discard_request(normalized_relative_path: String) -> bool:
discarded_path = normalized_relative_path
return true
func _initialize() -> void:
var failures: Array[String] = []
_verify_polling(failures)
_verify_empty_path_polling(failures)
_verify_terminal_rejections(failures)
_verify_preparation_and_adoption(failures)
_verify_finalize_rejection(failures)
_verify_source_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("M2_ANIMATION_RESOURCE_FINALIZER: %s" % failure)
quit(1)
return
print(
"M2_ANIMATION_RESOURCE_FINALIZER PASS "
+ "cases=28 iterations=1000 elapsed_ms=%.3f" % elapsed_milliseconds
)
quit(0)
func _verify_polling(failures: Array[String]) -> void:
var resource_loader := FakeResourceLoaderAdapter.new()
var scene_finalizer := FakeAnimatedSceneFinalizer.new()
var service: RefCounted = FINALIZER_SCRIPT.new(scene_finalizer, resource_loader)
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
for request in [
["world/pending.m2", "res://pending.tscn"],
["world/loaded.m2", "res://loaded.tscn"],
["world/failed.m2", "res://failed.tscn"],
]:
pipeline.call("remember_request", request[0], request[1])
resource_loader.statuses = {
"res://pending.tscn": ResourceLoader.THREAD_LOAD_IN_PROGRESS,
"res://loaded.tscn": ResourceLoader.THREAD_LOAD_LOADED,
"res://failed.tscn": ResourceLoader.THREAD_LOAD_FAILED,
}
_expect_equal(
int(service.call("poll_terminal_requests", pipeline, prototype_cache)),
2,
"two terminal requests completed",
failures
)
_expect_equal(
int(pipeline.call("pending_request_count")),
1,
"pending request retained",
failures
)
_expect_equal(
int(pipeline.call("finalize_record_count")),
2,
"terminal FIFO receives two records",
failures
)
_expect_string_array(
resource_loader.status_paths,
["res://pending.tscn", "res://loaded.tscn", "res://failed.tscn"],
"status polling insertion order",
failures
)
prototype_cache.call("clear_and_release")
func _verify_empty_path_polling(failures: Array[String]) -> void:
var resource_loader := FakeResourceLoaderAdapter.new()
var service: RefCounted = FINALIZER_SCRIPT.new(
FakeAnimatedSceneFinalizer.new(),
resource_loader
)
var pipeline := EmptyPathPipeline.new()
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
_expect_equal(
int(service.call("poll_terminal_requests", pipeline, prototype_cache)),
1,
"empty resource path completed",
failures
)
_expect_string_equal(
pipeline.discarded_path,
"world/empty.m2",
"empty resource path discarded",
failures
)
_expect_true(
bool(prototype_cache.call("is_animation_static", "world/empty.m2")),
"empty resource path marks static",
failures
)
_expect_equal(resource_loader.status_paths.size(), 0, "empty path skips I/O", failures)
prototype_cache.call("clear_and_release")
func _verify_terminal_rejections(failures: Array[String]) -> void:
var resource_loader := FakeResourceLoaderAdapter.new()
var scene_finalizer := FakeAnimatedSceneFinalizer.new()
var service: RefCounted = FINALIZER_SCRIPT.new(scene_finalizer, resource_loader)
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
_expect_dictionary_empty(
service.call("prepare_next_candidate", pipeline, prototype_cache),
"empty finalize FIFO",
failures
)
_enqueue_terminal(
pipeline,
"world/failed.m2",
"res://failed.tscn",
ResourceLoader.THREAD_LOAD_FAILED
)
_expect_dictionary_empty(
service.call("prepare_next_candidate", pipeline, prototype_cache),
"failed terminal record rejects",
failures
)
_expect_true(
bool(prototype_cache.call("is_animation_static", "world/failed.m2")),
"failed terminal record marks static",
failures
)
var cached := Node3D.new()
prototype_cache.call("adopt_animated_prototype", "world/cached.m2", cached)
_enqueue_terminal(
pipeline,
"world/cached.m2",
"res://cached.tscn",
ResourceLoader.THREAD_LOAD_LOADED
)
_expect_dictionary_empty(
service.call("prepare_next_candidate", pipeline, prototype_cache),
"cached terminal record skipped",
failures
)
prototype_cache.call("mark_animation_static", "world/static.m2")
_enqueue_terminal(
pipeline,
"world/static.m2",
"res://static.tscn",
ResourceLoader.THREAD_LOAD_LOADED
)
_expect_dictionary_empty(
service.call("prepare_next_candidate", pipeline, prototype_cache),
"static terminal record skipped",
failures
)
_enqueue_terminal(pipeline, "world/null.m2", "res://null.tscn", ResourceLoader.THREAD_LOAD_LOADED)
_expect_dictionary_empty(
service.call("prepare_next_candidate", pipeline, prototype_cache),
"null Resource candidate rejects",
failures
)
_expect_true(
bool(prototype_cache.call("is_animation_static", "world/null.m2")),
"null Resource candidate marks static",
failures
)
_expect_string_array(
resource_loader.load_paths,
["res://null.tscn"],
"only uncached loaded record performs terminal get",
failures
)
prototype_cache.call("clear_and_release")
func _verify_preparation_and_adoption(failures: Array[String]) -> void:
var resource_loader := FakeResourceLoaderAdapter.new()
var scene_finalizer := FakeAnimatedSceneFinalizer.new()
var service: RefCounted = FINALIZER_SCRIPT.new(scene_finalizer, resource_loader)
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
var packed_scene := PackedScene.new()
var candidate := Node3D.new()
var prototype := Node3D.new()
var material_source := Node3D.new()
resource_loader.resources["res://animated.tscn"] = packed_scene
scene_finalizer.candidate_by_resource[packed_scene] = candidate
scene_finalizer.finalization_by_candidate[candidate] = {
"prototype": prototype,
"animation_player_count": 3,
}
_enqueue_terminal(
pipeline,
"world/animated.m2",
"res://animated.tscn",
ResourceLoader.THREAD_LOAD_LOADED
)
var preparation: Dictionary = service.call(
"prepare_next_candidate",
pipeline,
prototype_cache
)
_expect_same(preparation.get("candidate"), candidate, "candidate identity", failures)
_expect_string_equal(
String(preparation.get("normalized", "")),
"world/animated.m2",
"prepared normalized path",
failures
)
_expect_string_equal(
String(preparation.get("path", "")),
"res://animated.tscn",
"prepared Resource path",
failures
)
var adopted: Node3D = service.call(
"finalize_prepared_candidate",
preparation,
material_source,
prototype_cache,
false
)
_expect_same(adopted, prototype, "adopted prototype identity", failures)
_expect_same(
prototype_cache.call("find_animated_prototype", "world/animated.m2"),
prototype,
"prototype cache adoption",
failures
)
_expect_same(scene_finalizer.repaired_candidates[0], candidate, "repair candidate", failures)
_expect_same(
scene_finalizer.repaired_material_sources[0],
material_source,
"repair material source",
failures
)
prototype_cache.call("clear_and_release")
material_source.free()
candidate.free()
func _verify_finalize_rejection(failures: Array[String]) -> void:
var scene_finalizer := FakeAnimatedSceneFinalizer.new()
var service: RefCounted = FINALIZER_SCRIPT.new(
scene_finalizer,
FakeResourceLoaderAdapter.new()
)
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
var candidate := Node3D.new()
_expect_null(
service.call(
"finalize_prepared_candidate",
{"normalized": "world/rejected.m2", "candidate": candidate},
null,
prototype_cache
),
"rejected finalization returns null",
failures
)
_expect_true(
bool(prototype_cache.call("is_animation_static", "world/rejected.m2")),
"rejected finalization marks static",
failures
)
candidate.free()
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 service_source := FileAccess.get_file_as_string(FINALIZER_PATH)
var drain_start := loader_source.find("func _drain_m2_animation_loads()")
var drain_end := loader_source.find("func _drain_m2_mesh_loads()", drain_start)
var drain_source := loader_source.substr(drain_start, drain_end - drain_start)
_expect_true(
drain_source.contains("_m2_animation_resource_finalizer.poll_terminal_requests("),
"loader delegates polling",
failures
)
_expect_true(
drain_source.contains("_m2_animation_resource_finalizer.prepare_next_candidate("),
"loader delegates candidate preparation",
failures
)
_expect_true(
drain_source.contains("_m2_animation_resource_finalizer.finalize_prepared_candidate("),
"loader delegates finalization",
failures
)
for removed_token in [
"ResourceLoader.load_threaded_get_status",
"ResourceLoader.load_threaded_get(",
"_m2_animated_scene_finalizer.instantiate_candidate",
"_m2_animated_scene_finalizer.repair_materials",
"_m2_animated_scene_finalizer.finalize_candidate",
"M2_ANIM_CACHE path=",
]:
_expect_false(drain_source.contains(removed_token), "loader omits %s" % removed_token, failures)
for required_token in [
"ResourceLoader.load_threaded_get_status",
"ResourceLoader.load_threaded_get(",
"\"instantiate_candidate\"",
"\"repair_materials\"",
"\"finalize_candidate\"",
"M2_ANIM_CACHE path=%s cache=%s players=%d",
]:
_expect_true(
service_source.contains(required_token),
"service owns %s" % required_token,
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var service: RefCounted = FINALIZER_SCRIPT.new(
FakeAnimatedSceneFinalizer.new(),
FakeResourceLoaderAdapter.new()
)
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
var started_microseconds := Time.get_ticks_usec()
for iteration in range(1000):
service.call("poll_terminal_requests", pipeline, prototype_cache)
var elapsed_milliseconds := float(
Time.get_ticks_usec() - started_microseconds
) / 1000.0
_expect_true(elapsed_milliseconds < 1000.0, "empty polling remains bounded", failures)
prototype_cache.call("clear_and_release")
return elapsed_milliseconds
func _enqueue_terminal(
pipeline: RefCounted,
normalized_relative_path: String,
resource_path: String,
status: int
) -> void:
pipeline.call("remember_request", normalized_relative_path, resource_path)
pipeline.call("complete_request", normalized_relative_path, status)
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_dictionary_empty(
actual: Variant,
label: String,
failures: Array[String]
) -> void:
_expect_true(actual is Dictionary and (actual as Dictionary).is_empty(), 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])
func _expect_string_array(
actual: Array[String],
expected: Array[String],
label: String,
failures: Array[String]
) -> void:
if actual != expected:
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
@@ -0,0 +1 @@
uid://dh16lkxak8dan
+6 -2
View File
@@ -10,6 +10,9 @@ const NATIVE_OBSERVER_PATH := (
const CACHED_OBSERVER_PATH := ( const CACHED_OBSERVER_PATH := (
"res://src/render/m2/m2_cached_animation_resource_observer.gd" "res://src/render/m2/m2_cached_animation_resource_observer.gd"
) )
const RESOURCE_FINALIZER_PATH := (
"res://src/render/m2/m2_animation_resource_finalizer.gd"
)
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
@@ -102,6 +105,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
var observer_sources := ( var observer_sources := (
FileAccess.get_file_as_string(NATIVE_OBSERVER_PATH) FileAccess.get_file_as_string(NATIVE_OBSERVER_PATH)
+ FileAccess.get_file_as_string(CACHED_OBSERVER_PATH) + FileAccess.get_file_as_string(CACHED_OBSERVER_PATH)
+ FileAccess.get_file_as_string(RESOURCE_FINALIZER_PATH)
) )
_expect_true( _expect_true(
loader_source.contains("M2_PROTOTYPE_CACHE_STATE_SCRIPT.new()"), loader_source.contains("M2_PROTOTYPE_CACHE_STATE_SCRIPT.new()"),
@@ -119,9 +123,9 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
"_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(",
"\"find_animated_prototype\"", "\"find_animated_prototype\"",
"_m2_prototype_cache_state.adopt_animated_prototype(", "\"adopt_animated_prototype\"",
"_m2_prototype_cache_state.mark_model_missing(", "_m2_prototype_cache_state.mark_model_missing(",
"_m2_prototype_cache_state.mark_animation_static(", "\"mark_animation_static\"",
"_m2_prototype_cache_state.clear_and_release()", "_m2_prototype_cache_state.clear_and_release()",
]: ]:
_expect_true( _expect_true(
+16 -3
View File
@@ -61,7 +61,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
`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` `M03-RND-M2-NATIVE-ANIMATION-RESOURCE-OBSERVER-001`,
`M03-RND-M2-ANIMATION-RESOURCE-FINALIZER-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
@@ -305,6 +306,13 @@ Runtime и Editor используют facade; planner/scheduler тестиру
observer, repository, prototype cache, snapshot, dispatch, shutdown, facade, observer, repository, prototype cache, snapshot, dispatch, shutdown, facade,
internal-access `30`, manifest `7/7`, documentation `44`, coordination and internal-access `30`, manifest `7/7`, documentation `44`, coordination and
checkpoint dry-run `7/7`. checkpoint dry-run `7/7`.
M2 animation resource finalizer passed `cases=28 iterations=1000
elapsed_ms=0.673` with exact
pending/loaded/failed polling order, empty-path discard, completion FIFO,
cache/static skips, Resource/candidate identity, repair, adoption and source cases.
All 58 autonomous headless regressions passed; internal-access remained `30`,
documentation covered `45` 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
@@ -350,6 +358,9 @@ Runtime и Editor используют facade; planner/scheduler тестиру
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 through its observer, so selection and visible behavior are unchanged. first through its observer, so selection and visible behavior are unchanged.
Cached animation terminal work retains pending insertion-order polling,
completion-order FIFO, one popped record per permit, candidate-before-material
lookup, exact repair/validation/adoption and `M2_ANIM_CACHE` fields.
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.
@@ -472,6 +483,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
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 native M2 animation resource observer and loader/raw-repository/prototype-cache
adapters plus synthetic lifecycle and exact-dependency regression fixtures, adapters plus synthetic lifecycle and exact-dependency regression fixtures,
cached M2 animation resource finalizer and loader/pipeline/scene-finalizer/
prototype-cache adapters plus synthetic terminal-I/O 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
@@ -501,8 +514,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
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, including native observation; observers produce typed per-operation state, including native observation;
action execution and root cleanup remain in loader; both ResourceLoader finalize animation terminal ResourceLoader drain is extracted, while action execution,
drains remain loader-owned; material-prototype lookup, root cleanup and static Mesh finalize drain 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;
asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending; asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending;