render: extract M2 animation resource finalizer
This commit is contained in:
@@ -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_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_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_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.
|
||||
@@ -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
|
||||
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
|
||||
|
||||
- `M2NativeAnimationResourceObserver` now owns the exact case-insensitive
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
| 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 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 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) |
|
||||
|
||||
@@ -27,9 +27,9 @@ and accept only candidates containing AnimationPlayer descendants.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Loader[StreamingWorldLoader] -->|loaded Resource and material source| Finalizer[M2AnimatedSceneFinalizer]
|
||||
Finalizer -->|accepted Node3D and player count| Loader
|
||||
Loader --> Cache[M2PrototypeCacheState]
|
||||
ResourceFinalizer[M2AnimationResourceFinalizer] -->|loaded Resource and material source| Finalizer[M2AnimatedSceneFinalizer]
|
||||
Finalizer -->|accepted Node3D and player count| ResourceFinalizer
|
||||
ResourceFinalizer --> Cache[M2PrototypeCacheState]
|
||||
```
|
||||
|
||||
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 |
|
||||
|---|---|---|---|---|---|
|
||||
| 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 | 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 |
|
||||
|
||||
Side effects are PackedScene instantiation, surface override assignment and
|
||||
@@ -91,20 +91,19 @@ stateDiagram-v2
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant L as StreamingWorldLoader
|
||||
participant R as M2AnimationResourceFinalizer
|
||||
participant F as M2AnimatedSceneFinalizer
|
||||
participant C as M2PrototypeCacheState
|
||||
L->>F: instantiate_candidate(Resource)
|
||||
F-->>L: detached Node3D or null
|
||||
L->>L: get static material prototype
|
||||
L->>F: repair_materials(candidate, source)
|
||||
L->>F: finalize_candidate(candidate)
|
||||
R->>F: instantiate_candidate(Resource)
|
||||
F-->>R: detached Node3D or null
|
||||
R->>F: repair_materials(candidate, source)
|
||||
R->>F: finalize_candidate(candidate)
|
||||
alt accepted
|
||||
F-->>L: exact Node3D and player count
|
||||
L->>C: adopt animated prototype
|
||||
F-->>R: exact Node3D and player count
|
||||
R->>C: adopt animated prototype
|
||||
else rejected
|
||||
F-->>L: empty; candidate freed
|
||||
L->>C: mark animation static
|
||||
F-->>R: empty; candidate freed
|
||||
R->>C: mark animation static
|
||||
end
|
||||
```
|
||||
|
||||
@@ -112,11 +111,11 @@ sequenceDiagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> Finalizer[M2AnimatedSceneFinalizer]
|
||||
ResourceFinalizer[M2AnimationResourceFinalizer] --> Finalizer[M2AnimatedSceneFinalizer]
|
||||
Finalizer --> Engine[PackedScene / Node3D / Mesh / Material / AnimationPlayer]
|
||||
Loader --> Resource[ResourceLoader]
|
||||
ResourceFinalizer --> Resource[ResourceLoader]
|
||||
Loader --> Budget[RenderBudgetScheduler]
|
||||
Loader --> Prototype[M2PrototypeCacheState]
|
||||
ResourceFinalizer --> Prototype[M2PrototypeCacheState]
|
||||
Finalizer -. no dependency .-> Resource
|
||||
Finalizer -. no dependency .-> Budget
|
||||
Finalizer -. no dependency .-> Prototype
|
||||
@@ -126,7 +125,7 @@ flowchart TB
|
||||
|
||||
- Every method runs synchronously on the renderer main thread.
|
||||
- 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.
|
||||
- Traversal results borrow Nodes; source materials remain Resource-owned.
|
||||
|
||||
@@ -134,11 +133,11 @@ flowchart TB
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
## 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_animation_load_pipeline_state.gd` | Pending/terminal records before finalization |
|
||||
| `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 |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
Own cross-frame bookkeeping between a successful animated M2 ResourceLoader
|
||||
request, terminal polling and budgeted main-thread scene finalization. This is
|
||||
an exact state extraction; cached animation eligibility and request admission
|
||||
belong to `M2CachedAnimationResourceObserver`, while scene finalization and
|
||||
retained Node lifecycle belong to `M2AnimatedSceneFinalizer` and
|
||||
`M2PrototypeCacheState`.
|
||||
belong to `M2CachedAnimationResourceObserver`; terminal I/O/outcomes belong to
|
||||
`M2AnimationResourceFinalizer`, while validation and retained Node lifetime
|
||||
belong to `M2AnimatedSceneFinalizer` and `M2PrototypeCacheState`.
|
||||
|
||||
## Non-goals
|
||||
|
||||
@@ -32,11 +32,12 @@ retained Node lifecycle belong to `M2AnimatedSceneFinalizer` and
|
||||
flowchart LR
|
||||
Observer[M2CachedAnimationResourceObserver] --> IO[ResourceLoader request]
|
||||
Observer --> State[M2AnimationLoadPipelineState]
|
||||
Loader[StreamingWorldLoader] --> IO
|
||||
Loader --> State
|
||||
State -->|detached pending records| Loader
|
||||
Loader -->|opaque terminal status| State
|
||||
State -->|completion FIFO| Loader
|
||||
Finalizer[M2AnimationResourceFinalizer] --> IO
|
||||
Finalizer --> State
|
||||
State -->|detached pending records| Finalizer
|
||||
Finalizer -->|opaque terminal status| State
|
||||
State -->|completion FIFO| Finalizer
|
||||
Loader[StreamingWorldLoader] --> Finalizer
|
||||
Loader --> Budget[M2_ANIMATION_FINALIZE permit]
|
||||
Loader --> Prototype[M2PrototypeCacheState]
|
||||
```
|
||||
@@ -65,9 +66,9 @@ renderer services are forbidden.
|
||||
| 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 | Opaque terminal status | Loader polling adapter | Finalize FIFO | Integer value | Until pop/clear |
|
||||
| Output | Detached pending records | State | Loader poll/shutdown adapter | Caller-owned copies | One pass |
|
||||
| Output | Oldest completion record | State | Loader finalizer | Transferred Dictionary | One finalize attempt |
|
||||
| Input | Opaque terminal status | Resource finalizer | Finalize FIFO | Integer value | Until pop/clear |
|
||||
| Output | Detached pending records | State | Resource finalizer/shutdown adapter | Caller-owned copies | One pass |
|
||||
| Output | Oldest completion record | State | Resource finalizer | Transferred Dictionary | One finalize attempt |
|
||||
| Output | Detached diagnostics | State | Verifier/future metrics | Caller-owned copies | Snapshot lifetime |
|
||||
|
||||
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
|
||||
flowchart TD
|
||||
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?}
|
||||
Terminal -->|no| Poll
|
||||
Terminal -->|yes| Complete[Complete with opaque status]
|
||||
@@ -85,7 +86,7 @@ flowchart TD
|
||||
FIFO --> Permit{Permit available?}
|
||||
Permit -->|no| FIFO
|
||||
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
|
||||
@@ -105,19 +106,22 @@ stateDiagram-v2
|
||||
sequenceDiagram
|
||||
participant O as CachedAnimationObserver
|
||||
participant L as StreamingWorldLoader
|
||||
participant F as AnimationResourceFinalizer
|
||||
participant R as ResourceLoader
|
||||
participant S as M2AnimationLoadPipelineState
|
||||
participant P as M2PrototypeCacheState
|
||||
O->>R: load_threaded_request(GLB)
|
||||
O->>S: remember_request(path, GLB)
|
||||
loop frames
|
||||
L->>S: request_records_snapshot()
|
||||
L->>R: load_threaded_get_status(GLB)
|
||||
L->>F: poll terminal requests
|
||||
F->>S: request_records_snapshot()
|
||||
F->>R: load_threaded_get_status(GLB)
|
||||
end
|
||||
L->>S: complete_request(path, status)
|
||||
L->>S: pop_finalize_record() after permit
|
||||
L->>R: load_threaded_get(GLB)
|
||||
L->>P: adopt animated prototype or mark static
|
||||
F->>S: complete_request(path, status)
|
||||
L->>F: prepare after permit
|
||||
F->>S: pop_finalize_record()
|
||||
F->>R: load_threaded_get(GLB)
|
||||
F->>P: adopt animated prototype or mark static
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
@@ -125,8 +129,8 @@ sequenceDiagram
|
||||
- Main thread serializes all mutation.
|
||||
- State owns only request/finalize Dictionaries with copied paths and statuses.
|
||||
- Loader drains pending ResourceLoader paths before orderly shutdown clear.
|
||||
- Loader owns PackedScene instantiation and material repair; prototype state owns
|
||||
accepted detached Node references and static-only outcomes.
|
||||
- Resource finalizer owns terminal I/O/outcomes and composes scene validation;
|
||||
prototype state owns accepted detached Nodes and static-only outcomes.
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
@@ -134,8 +138,9 @@ sequenceDiagram
|
||||
flowchart TB
|
||||
Observer[M2CachedAnimationResourceObserver] --> State[M2AnimationLoadPipelineState]
|
||||
Observer --> Resource[ResourceLoader request]
|
||||
Loader[StreamingWorldLoader] --> State[M2AnimationLoadPipelineState]
|
||||
Loader --> Resource
|
||||
Finalizer[M2AnimationResourceFinalizer] --> State[M2AnimationLoadPipelineState]
|
||||
Finalizer --> Resource
|
||||
Loader[StreamingWorldLoader] --> Finalizer
|
||||
Loader --> Budget[RenderBudgetScheduler]
|
||||
Loader --> Prototype[M2PrototypeCacheState]
|
||||
State -. no dependency .-> Resource
|
||||
@@ -185,9 +190,8 @@ no rebake or migration is required.
|
||||
|
||||
## Extension points
|
||||
|
||||
ResourceLoader polling may later move behind a separate adapter without
|
||||
changing this value-only state contract. Animated-scene finalization is now a
|
||||
sibling service.
|
||||
ResourceLoader polling/finalization now belongs to a sibling service without
|
||||
changing this value-only state contract.
|
||||
|
||||
## 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 |
|
||||
| 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 |
|
||||
|
||||
## Known gaps and risks
|
||||
@@ -209,15 +213,17 @@ sibling service.
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `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_animated_scene_finalizer.gd` | Candidate instantiation, material repair and player validation |
|
||||
| `src/render/m2/m2_prototype_cache_state.gd` | Animated prototype/static-only outcomes |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | 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 |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`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-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.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.
|
||||
- Resource existence/request and synchronous GLB JSON inspection run on the
|
||||
renderer main thread, matching the previous loader behavior.
|
||||
- The sibling native observer owns the native build attempt. Loader remains owner
|
||||
of terminal polling/finalize, permits, materialization and SceneTree mutation.
|
||||
- The sibling native observer owns the native build attempt and the animation
|
||||
resource finalizer owns terminal polling/finalize. Loader retains permits,
|
||||
material lookup, materialization and SceneTree mutation.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
@@ -197,9 +198,8 @@ queue and hitch metrics are unchanged.
|
||||
|
||||
## Extension points
|
||||
|
||||
Native animation observation can become a sibling service without changing this
|
||||
cached-GLB contract. Terminal ResourceLoader polling/finalization may later move
|
||||
behind a dedicated adapter while retaining the pipeline state.
|
||||
Native and cached observation remain sibling services. Terminal ResourceLoader
|
||||
polling/finalization uses the dedicated finalizer while retaining pipeline state.
|
||||
|
||||
## 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 |
|
||||
| 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
|
||||
|
||||
@@ -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_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_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_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 |
|
||||
|
||||
@@ -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 | Detached static Node3D | Cached scene/raw builder adapter | Cache state | Adopted on success | Until final shutdown |
|
||||
| Input | Detached animated Node3D | GLB/native animation adapter | Cache state | Adopted on success | Until final shutdown |
|
||||
| Input | Missing/static-only outcome | Loader/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 | 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.
|
||||
- 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
|
||||
contributed work counts.
|
||||
- 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_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/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 |
|
||||
|
||||
@@ -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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
@@ -432,7 +433,8 @@ sequenceDiagram
|
||||
terminal statuses and completion-order finalize FIFO. The cached animation
|
||||
observer owns allow/deny/path/GLB selection, request admission and initial
|
||||
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
|
||||
them at the existing final-shutdown site. Prototype state and materialization
|
||||
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 resource snapshot | Implemented extraction | Typed identity/adoption/lifetime/source/timing contract | Resource observation service and asset-backed traversal remain pending |
|
||||
| M2 static build resource observer | Implemented extraction | Cache/pending/path/request/source/timing contract | Asset-backed traversal and finalize extraction pending |
|
||||
| M2 cached animation resource observer | Implemented extraction | Cache/pending/policy/GLB/request/source/timing contract | 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 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 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 |
|
||||
@@ -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/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_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_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 |
|
||||
@@ -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_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_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_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 |
|
||||
|
||||
@@ -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
|
||||
@@ -96,6 +96,9 @@ const M2_ANIMATION_LOAD_PIPELINE_STATE_SCRIPT := preload(
|
||||
const M2_ANIMATED_SCENE_FINALIZER_SCRIPT := preload(
|
||||
"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(
|
||||
"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_animation_load_pipeline_state := M2_ANIMATION_LOAD_PIPELINE_STATE_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 _wmo_build_jobs: Dictionary = {}
|
||||
var _wmo_build_queue: Array = []
|
||||
@@ -4238,50 +4244,27 @@ func _drain_m2_group_results() -> void:
|
||||
|
||||
|
||||
func _drain_m2_animation_loads() -> void:
|
||||
for pending in _m2_animation_load_pipeline_state.request_records_snapshot():
|
||||
var normalized_rel := String(pending.get("normalized", ""))
|
||||
var path := String(pending.get("path", ""))
|
||||
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)
|
||||
_m2_animation_resource_finalizer.poll_terminal_requests(
|
||||
_m2_animation_load_pipeline_state,
|
||||
_m2_prototype_cache_state
|
||||
)
|
||||
|
||||
while _m2_animation_load_pipeline_state.has_finalize_record() and _render_budget_scheduler.try_consume_permit(
|
||||
RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE):
|
||||
var pending: Dictionary = _m2_animation_load_pipeline_state.pop_finalize_record()
|
||||
var normalized_rel := String(pending.get("normalized", ""))
|
||||
if (
|
||||
normalized_rel.is_empty()
|
||||
or _m2_prototype_cache_state.has_animated_prototype(normalized_rel)
|
||||
or _m2_prototype_cache_state.is_animation_static(normalized_rel)
|
||||
):
|
||||
var preparation := _m2_animation_resource_finalizer.prepare_next_candidate(
|
||||
_m2_animation_load_pipeline_state,
|
||||
_m2_prototype_cache_state
|
||||
)
|
||||
if preparation.is_empty():
|
||||
continue
|
||||
if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED:
|
||||
_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)
|
||||
_m2_animated_scene_finalizer.repair_materials(candidate, material_source)
|
||||
var finalization := _m2_animated_scene_finalizer.finalize_candidate(candidate)
|
||||
var prototype: Node3D = finalization.get("prototype", null)
|
||||
if prototype != null:
|
||||
_m2_prototype_cache_state.adopt_animated_prototype(normalized_rel, prototype)
|
||||
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)
|
||||
var normalized_rel := String(preparation.get("normalized", ""))
|
||||
var material_source := _get_or_load_m2_material_prototype(normalized_rel)
|
||||
_m2_animation_resource_finalizer.finalize_prepared_candidate(
|
||||
preparation,
|
||||
material_source,
|
||||
_m2_prototype_cache_state,
|
||||
debug_streaming
|
||||
)
|
||||
|
||||
|
||||
func _drain_m2_mesh_loads() -> void:
|
||||
|
||||
@@ -5,6 +5,9 @@ extends SceneTree
|
||||
|
||||
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 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 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:
|
||||
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 loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_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)
|
||||
for delegated_call in [
|
||||
"_m2_animated_scene_finalizer.instantiate_candidate(resource)",
|
||||
"_m2_animated_scene_finalizer.repair_materials(candidate, material_source)",
|
||||
"_m2_animated_scene_finalizer.finalize_candidate(candidate)",
|
||||
"_m2_animated_scene_finalizer.mesh_instances_in_subtree(root)",
|
||||
"\"instantiate_candidate\"",
|
||||
"\"repair_materials\"",
|
||||
"\"finalize_candidate\"",
|
||||
]:
|
||||
_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(
|
||||
materializer_source.count("_animated_scene_finalizer.animation_players_in_subtree("),
|
||||
1,
|
||||
"single materializer player-inventory delegation",
|
||||
failures
|
||||
)
|
||||
for retained_loader_rule in [
|
||||
"ResourceLoader.load_threaded_get(path)",
|
||||
for retained_renderer_rule in [
|
||||
"ResourceLoader.load_threaded_get(resource_path)",
|
||||
"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE",
|
||||
"_get_or_load_m2_material_prototype(normalized_rel)",
|
||||
"_m2_prototype_cache_state.adopt_animated_prototype(",
|
||||
"_m2_prototype_cache_state.mark_animation_static(normalized_rel)",
|
||||
"\"adopt_animated_prototype\"",
|
||||
"\"mark_animation_static\"",
|
||||
"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 [
|
||||
"ResourceLoader.",
|
||||
"FileAccess.",
|
||||
|
||||
@@ -8,6 +8,9 @@ const PIPELINE_PATH := "res://src/render/m2/m2_animation_load_pipeline_state.gd"
|
||||
const OBSERVER_PATH := (
|
||||
"res://src/render/m2/m2_cached_animation_resource_observer.gd"
|
||||
)
|
||||
const RESOURCE_FINALIZER_PATH := (
|
||||
"res://src/render/m2/m2_animation_resource_finalizer.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:
|
||||
var pipeline_source := FileAccess.get_file_as_string(PIPELINE_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)
|
||||
_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)
|
||||
@@ -95,14 +101,18 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
"cached observer owns request admission",
|
||||
failures
|
||||
)
|
||||
for retained_loader_rule in [
|
||||
"ResourceLoader.load_threaded_get_status(path)",
|
||||
"ResourceLoader.load_threaded_get(path)",
|
||||
for retained_renderer_rule in [
|
||||
"ResourceLoader.load_threaded_get_status(resource_path)",
|
||||
"ResourceLoader.load_threaded_get(resource_path)",
|
||||
"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE",
|
||||
"_m2_prototype_cache_state.adopt_animated_prototype(",
|
||||
"_m2_prototype_cache_state.mark_animation_static(",
|
||||
"\"adopt_animated_prototype\"",
|
||||
"\"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 [
|
||||
"ResourceLoader.",
|
||||
"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
|
||||
@@ -10,6 +10,9 @@ const NATIVE_OBSERVER_PATH := (
|
||||
const CACHED_OBSERVER_PATH := (
|
||||
"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"
|
||||
|
||||
|
||||
@@ -102,6 +105,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var observer_sources := (
|
||||
FileAccess.get_file_as_string(NATIVE_OBSERVER_PATH)
|
||||
+ FileAccess.get_file_as_string(CACHED_OBSERVER_PATH)
|
||||
+ FileAccess.get_file_as_string(RESOURCE_FINALIZER_PATH)
|
||||
)
|
||||
_expect_true(
|
||||
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.adopt_static_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_animation_static(",
|
||||
"\"mark_animation_static\"",
|
||||
"_m2_prototype_cache_state.clear_and_release()",
|
||||
]:
|
||||
_expect_true(
|
||||
|
||||
@@ -61,7 +61,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
`M03-RND-M2-BUILD-RESOURCE-SNAPSHOT-001`,
|
||||
`M03-RND-M2-STATIC-BUILD-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
|
||||
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
|
||||
- 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,
|
||||
internal-access `30`, manifest `7/7`, documentation `44`, coordination and
|
||||
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
|
||||
remained green (13 pre-existing expired M00 claim warnings).
|
||||
- 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`
|
||||
schema rules and request/static-only transitions. Native GryphonRoost remains
|
||||
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
|
||||
and world position/Euler/unclamped-scale transforms are unchanged across
|
||||
lightweight render, cached-scene and live-prototype paths.
|
||||
@@ -472,6 +483,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
adapters plus generated GLB metadata regression fixtures,
|
||||
native M2 animation resource observer and loader/raw-repository/prototype-cache
|
||||
adapters plus synthetic lifecycle and exact-dependency regression fixtures,
|
||||
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.
|
||||
- Remaining risks: worker concurrency and stale-result validation remain
|
||||
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
|
||||
separated and dispatch decisions are explicit; static and cached-animation
|
||||
observers produce typed per-operation state, including native observation;
|
||||
action execution and root cleanup remain in loader; both ResourceLoader finalize
|
||||
drains remain loader-owned;
|
||||
animation terminal ResourceLoader drain is extracted, while action execution,
|
||||
material-prototype lookup, root cleanup and static Mesh finalize drain remain loader-owned;
|
||||
WMO ResourceLoader/FileAccess I/O, live fallback and group materialization
|
||||
remain in the loader;
|
||||
asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending;
|
||||
|
||||
Reference in New Issue
Block a user