diff --git a/RENDER.md b/RENDER.md index 64acdbe..061214e 100644 --- a/RENDER.md +++ b/RENDER.md @@ -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 diff --git a/coordination/claims/M03-RND-M2-ANIMATION-RESOURCE-FINALIZER-001.md b/coordination/claims/M03-RND-M2-ANIMATION-RESOURCE-FINALIZER-001.md index 278f946..f3f73d3 100644 --- a/coordination/claims/M03-RND-M2-ANIMATION-RESOURCE-FINALIZER-001.md +++ b/coordination/claims/M03-RND-M2-ANIMATION-RESOURCE-FINALIZER-001.md @@ -58,14 +58,20 @@ from the loader into a dedicated two-phase finalization service. ## Status -- State: active -- Done: ownership/compatibility contract -- Next: implementation, verification and documentation +- State: ready +- Done: implementation, verification and documentation +- Next: integrator no-ff merge and post-merge acceptance - Blocked by: ## Handoff -- Commit: -- Results: -- Remaining risks: -- Documentation updated: +- Commit: `e7cd967` +- Results: finalizer `28` cases / `1,000` timing iterations; all `58/58` + autonomous headless regressions; facade/internal-access/shutdown/manifest; + documentation `45`; coordination; checkpoint dry-run `7/7`. +- Remaining risks: private asset animation/visual/leak/p95/p99 evidence is absent; + terminal Resource get/instantiation/repair stays synchronous; immediate prepared- + candidate completion is a composition precondition; material lookup stays loader-owned. +- Documentation updated: new finalizer API/I/O/data-flow/state/sequence/dependency/ + ownership spec; pipeline, scene finalizer, cached observer, prototype cache, + world renderer, module registry, `RENDER.md` and M03 Evidence. diff --git a/docs/modules/README.md b/docs/modules/README.md index b8ead11..a0ef71d 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -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) | diff --git a/docs/modules/m2-animated-scene-finalizer.md b/docs/modules/m2-animated-scene-finalizer.md index 0654d7f..b6576c5 100644 --- a/docs/modules/m2-animated-scene-finalizer.md +++ b/docs/modules/m2-animated-scene-finalizer.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 diff --git a/docs/modules/m2-animation-load-pipeline-state.md b/docs/modules/m2-animation-load-pipeline-state.md index 8cca0d8..aa3f4c4 100644 --- a/docs/modules/m2-animation-load-pipeline-state.md +++ b/docs/modules/m2-animation-load-pipeline-state.md @@ -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) diff --git a/docs/modules/m2-animation-resource-finalizer.md b/docs/modules/m2-animation-resource-finalizer.md new file mode 100644 index 0000000..d8e2f68 --- /dev/null +++ b/docs/modules/m2-animation-resource-finalizer.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) diff --git a/docs/modules/m2-cached-animation-resource-observer.md b/docs/modules/m2-cached-animation-resource-observer.md index 9748cad..694e585 100644 --- a/docs/modules/m2-cached-animation-resource-observer.md +++ b/docs/modules/m2-cached-animation-resource-observer.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 | diff --git a/docs/modules/m2-prototype-cache-state.md b/docs/modules/m2-prototype-cache-state.md index 4a54bc4..805fd71 100644 --- a/docs/modules/m2-prototype-cache-state.md +++ b/docs/modules/m2-prototype-cache-state.md @@ -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 | diff --git a/docs/modules/world-renderer.md b/docs/modules/world-renderer.md index 4fcafc0..bd63a7d 100644 --- a/docs/modules/world-renderer.md +++ b/docs/modules/world-renderer.md @@ -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 | diff --git a/src/render/m2/m2_animation_resource_finalizer.gd b/src/render/m2/m2_animation_resource_finalizer.gd new file mode 100644 index 0000000..f89655d --- /dev/null +++ b/src/render/m2/m2_animation_resource_finalizer.gd @@ -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) diff --git a/src/render/m2/m2_animation_resource_finalizer.gd.uid b/src/render/m2/m2_animation_resource_finalizer.gd.uid new file mode 100644 index 0000000..10ef6c1 --- /dev/null +++ b/src/render/m2/m2_animation_resource_finalizer.gd.uid @@ -0,0 +1 @@ +uid://2rnsjucg3dul diff --git a/src/scenes/streaming/streaming_world_loader.gd b/src/scenes/streaming/streaming_world_loader.gd index 7c780c2..49c920c 100644 --- a/src/scenes/streaming/streaming_world_loader.gd +++ b/src/scenes/streaming/streaming_world_loader.gd @@ -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: diff --git a/src/tools/verify_m2_animated_scene_finalizer.gd b/src/tools/verify_m2_animated_scene_finalizer.gd index 3ccb171..1b892f8 100644 --- a/src/tools/verify_m2_animated_scene_finalizer.gd +++ b/src/tools/verify_m2_animated_scene_finalizer.gd @@ -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.", diff --git a/src/tools/verify_m2_animation_load_pipeline_state.gd b/src/tools/verify_m2_animation_load_pipeline_state.gd index 4c20d2a..9c9c9a3 100644 --- a/src/tools/verify_m2_animation_load_pipeline_state.gd +++ b/src/tools/verify_m2_animation_load_pipeline_state.gd @@ -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.", diff --git a/src/tools/verify_m2_animation_resource_finalizer.gd b/src/tools/verify_m2_animation_resource_finalizer.gd new file mode 100644 index 0000000..66bb2c5 --- /dev/null +++ b/src/tools/verify_m2_animation_resource_finalizer.gd @@ -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]) diff --git a/src/tools/verify_m2_animation_resource_finalizer.gd.uid b/src/tools/verify_m2_animation_resource_finalizer.gd.uid new file mode 100644 index 0000000..948eadc --- /dev/null +++ b/src/tools/verify_m2_animation_resource_finalizer.gd.uid @@ -0,0 +1 @@ +uid://dh16lkxak8dan diff --git a/src/tools/verify_m2_prototype_cache_state.gd b/src/tools/verify_m2_prototype_cache_state.gd index dcb40e3..e18f324 100644 --- a/src/tools/verify_m2_prototype_cache_state.gd +++ b/src/tools/verify_m2_prototype_cache_state.gd @@ -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( diff --git a/targets/03-renderer-facade.md b/targets/03-renderer-facade.md index 2000e22..c40b7ac 100644 --- a/targets/03-renderer-facade.md +++ b/targets/03-renderer-facade.md @@ -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;