merge M03 M2 animated scene finalizer
This commit is contained in:
@@ -36,6 +36,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
|
||||
- `src/render/liquid/adt_water_load_pipeline_state.gd` - ADT water pending FIFO/dedupe, active task IDs and worker-safe parsed-result mailbox without parser or Node ownership.
|
||||
- `src/render/liquid/adt_water_scene_finalizer.gd` - stateless main-thread ADT water build/attach and optional persisted Editor ownership through the existing ADTBuilder.
|
||||
- `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` - memoized billboard/UV-rotation decision for stale cached M2 runtime mesh refresh.
|
||||
- `src/render/m2/m2_animated_scene_finalizer.gd` - animated PackedScene candidate ownership, historical material repair and AnimationPlayer validation.
|
||||
- `src/render/m2/m2_animation_load_pipeline_state.gd` - animated M2 threaded-load request records and completion-order finalize FIFO without I/O or Node ownership.
|
||||
- `src/render/m2/m2_mesh_load_pipeline_state.gd` - static M2 threaded-load request records, terminal statuses and completion-order finalize FIFO without I/O or Mesh ownership.
|
||||
- `src/render/m2/m2_mesh_resource_cache_state.gd` - normalized-path prepared static M2 Mesh references with final-shutdown lifetime.
|
||||
@@ -1098,6 +1099,22 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
||||
- Cache formats, animation behavior and visible output are unchanged. Synthetic
|
||||
timing is not asset-backed I/O, leak, animation-fidelity or p95/p99 evidence.
|
||||
|
||||
## 2026-07-18 M2 Animated Scene Finalizer Extraction
|
||||
|
||||
- `M2AnimatedSceneFinalizer` now owns terminal animated PackedScene candidate
|
||||
instantiation, depth-first Mesh/AnimationPlayer traversal, static-prototype
|
||||
material override mapping and rejected detached-root destruction.
|
||||
- Material repair retains source/target depth-first ordering, source-index clamp,
|
||||
per-surface override priority and first discovered material fallback.
|
||||
- A candidate still requires at least one AnimationPlayer. Accepted exact Node3D
|
||||
and player count return to the loader; invalid candidates are freed, including
|
||||
the previously unhandled non-Node3D imported-root case.
|
||||
- `StreamingWorldLoader` retains ResourceLoader I/O/statuses, finalize permits,
|
||||
material-prototype selection/loading, prototype adoption/static fallback,
|
||||
animation playback policy and diagnostic logging.
|
||||
- Cache formats, profiles and visible rules are unchanged. Synthetic scene and
|
||||
material fixtures are not asset-backed animation-fidelity or p95/p99 evidence.
|
||||
|
||||
## 2026-07-17 M2 Mesh Resource Cache State Extraction
|
||||
|
||||
- `M2MeshResourceCacheState` now owns normalized-path references to prepared
|
||||
|
||||
@@ -78,14 +78,24 @@ main-thread finalizer.
|
||||
|
||||
## Status
|
||||
|
||||
- State: claimed
|
||||
- Done: candidate/material/player ownership boundary audited on current master
|
||||
- Next: implement finalizer, migrate adapters, verify and document
|
||||
- State: ready for integration
|
||||
- Done: finalizer, loader adapters, lifecycle/material verifier and required documentation
|
||||
- Next: integrate on master and run post-merge acceptance gates
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit:
|
||||
- Results:
|
||||
- Remaining risks:
|
||||
- Documentation updated:
|
||||
- Commit: `1b450dc`
|
||||
- Results: animated scene finalizer passes 13 invalid/type/lifetime/traversal/
|
||||
material/transfer/source cases and 10,000 depth-eight queries in 40.126 ms;
|
||||
all 48 autonomous headless verifiers pass, while the proprietary ADT placement
|
||||
probe is unavailable without `data/extracted`; checkpoint dry-run passes 7/7;
|
||||
documentation and coordination gates pass; loader-private inventory remains 30.
|
||||
- Remaining risks: proprietary GLB traversal, material/animation comparison,
|
||||
descriptor pressure, leaks and p95/p99 remain unavailable; no paired original-
|
||||
client visual comparison is claimed. Positional material mapping is unchanged.
|
||||
- Documentation updated: inline API; `m2-animated-scene-finalizer.md` with
|
||||
data-flow, lifecycle, sequence and dependency diagrams; animation pipeline,
|
||||
prototype/world-renderer specs, module registry and `RENDER.md`.
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-ANIMATED-SCENE-FINALIZER-001:1b450dc -->
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
| M2 placement grouper | Implemented extraction | [`m2-placement-grouper.md`](m2-placement-grouper.md) |
|
||||
| M2 build batch planner | Implemented extraction | [`m2-build-batch-planner.md`](m2-build-batch-planner.md) |
|
||||
| M2 runtime mesh rebuild classifier | Implemented extraction | [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md) |
|
||||
| M2 animated scene finalizer | Implemented extraction | [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md) |
|
||||
| M2 animation load pipeline state | Implemented extraction | [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md) |
|
||||
| M2 mesh load pipeline state | Implemented extraction | [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md) |
|
||||
| M2 mesh resource cache state | Implemented extraction | [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md) |
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
# M2 Animated Scene Finalizer
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented extraction |
|
||||
| Target/work package | M03 / `M03-RND-M2-ANIMATED-SCENE-FINALIZER-001` |
|
||||
| Owners | Detached animated candidate, material repair and player validation |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animated-scene-finalizer`, 2026-07-18 |
|
||||
| Profiles/capabilities | Existing optional cached-GLB animated M2 path |
|
||||
|
||||
## Purpose
|
||||
|
||||
Finalize one loaded animated M2 scene on the main thread: instantiate a valid
|
||||
detached Node3D candidate, copy the historical static-prototype material mapping
|
||||
and accept only candidates containing AnimationPlayer descendants.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Call ResourceLoader or interpret threaded-load statuses.
|
||||
- Choose animation eligibility, cache paths, permits or material prototypes.
|
||||
- Adopt prototypes, mark static fallback or select/play animations.
|
||||
- Process native M2 animators or static Mesh rebuilds.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Loader[StreamingWorldLoader] -->|loaded Resource and material source| Finalizer[M2AnimatedSceneFinalizer]
|
||||
Finalizer -->|accepted Node3D and player count| Loader
|
||||
Loader --> Cache[M2PrototypeCacheState]
|
||||
```
|
||||
|
||||
Allowed dependencies are Godot scene/resource/material types. ResourceLoader,
|
||||
filesystem, workers, scheduler, builders, raw repositories, prototype cache and
|
||||
other application layers are forbidden.
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
| `instantiate_candidate(resource)` | Ownership query | Instantiate PackedScene as detached Node3D | Main thread; caller receives valid candidate | Unsupported/invalid null; created wrong-type root freed |
|
||||
| `repair_materials(animated_root, material_source_root)` | Command | Apply historical material overrides | Main thread; borrowed roots | Null/empty roots no-op |
|
||||
| `finalize_candidate(candidate_root)` | Ownership command/query | Require AnimationPlayer and transfer root/count | Main thread; detached candidate | Null empty; rejected candidate freed |
|
||||
| `mesh_instances_in_subtree(root)` | Query | Depth-first MeshInstance3D inventory | Main thread; borrowed subtree | Null returns empty |
|
||||
| `animation_players_in_subtree(root)` | Query | Depth-first AnimationPlayer inventory | Main thread; borrowed subtree | Null returns empty |
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Loaded Resource | Loader I/O adapter | 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 | Depth-first engine-node arrays | Traversal | Loader preparation/playback | Borrowed references | One call |
|
||||
|
||||
Side effects are PackedScene instantiation, surface override assignment and
|
||||
synchronous destruction of rejected detached roots.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Resource[Loaded Resource] --> Packed{PackedScene?}
|
||||
Packed -->|no| Reject[Return null]
|
||||
Packed -->|yes| Instantiate[Instantiate]
|
||||
Instantiate --> Root{Node3D?}
|
||||
Root -->|no| FreeWrong[Free root and reject]
|
||||
Root -->|yes| Repair[Repair materials]
|
||||
Repair --> Players{AnimationPlayer exists?}
|
||||
Players -->|no| FreeCandidate[Free and reject]
|
||||
Players -->|yes| Transfer[Transfer exact root and count]
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Absent
|
||||
Absent --> Candidate: instantiate valid scene
|
||||
Candidate --> Candidate: repair materials
|
||||
Candidate --> Accepted: player found
|
||||
Candidate --> Released: no player
|
||||
Accepted --> [*]: ownership transferred
|
||||
Released --> [*]
|
||||
```
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant L as StreamingWorldLoader
|
||||
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)
|
||||
alt accepted
|
||||
F-->>L: exact Node3D and player count
|
||||
L->>C: adopt animated prototype
|
||||
else rejected
|
||||
F-->>L: empty; candidate freed
|
||||
L->>C: mark animation static
|
||||
end
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> Finalizer[M2AnimatedSceneFinalizer]
|
||||
Finalizer --> Engine[PackedScene / Node3D / Mesh / Material / AnimationPlayer]
|
||||
Loader --> Resource[ResourceLoader]
|
||||
Loader --> Budget[RenderBudgetScheduler]
|
||||
Loader --> Prototype[M2PrototypeCacheState]
|
||||
Finalizer -. no dependency .-> Resource
|
||||
Finalizer -. no dependency .-> Budget
|
||||
Finalizer -. no dependency .-> Prototype
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- 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.
|
||||
- Rejection frees the candidate synchronously, including wrong-type roots.
|
||||
- Traversal results borrow Nodes; source materials remain Resource-owned.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Null/unsupported Resource | Type guard | Return null | Dedicated verifier | Loader 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 |
|
||||
| Shutdown/cancellation | Not owned | No retained state | N/A | Loader drains first |
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||
|---|---|---|---|---|
|
||||
| Finalize permit | `m2_animation_finalize_ops_per_tick = 1` | Existing profiles | Yes | Caller bounds calls |
|
||||
| Material mapping | Depth-first/index-clamped | All | No | Preserves current appearance |
|
||||
| Required players | At least one | All | No | Rejects non-animated scenes |
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
The service retains no state and serializes nothing. GLB/cache formats, material
|
||||
versions and prototype lifetimes are unchanged; no rebake is required.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
- Accepted result reports player count for the existing `M2_ANIM_CACHE` log.
|
||||
- Traversal order is deterministic depth-first preorder.
|
||||
- The service owns no logging or metrics.
|
||||
|
||||
## Verification
|
||||
|
||||
- `verify_m2_animated_scene_finalizer.gd` covers invalid Resources, wrong-root
|
||||
release, exact transfer, player traversal/count, override priority, fallback,
|
||||
source-index mapping, source boundaries and timing.
|
||||
- Adjacent animation pipeline/prototype/shutdown/material/build tests protect callers.
|
||||
- Fidelity evidence is exact mapping/lifecycle extraction; no asset-backed or
|
||||
original-client visual parity claim is made.
|
||||
- Performance budget: 10,000 depth-eight player inventories under one second.
|
||||
|
||||
## Extension points
|
||||
|
||||
Animation playback policy and native animator cloning remain separate services.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| PackedScene candidate | Implemented extraction | Type/lifetime fixture | Proprietary corrupt corpus pending |
|
||||
| Material repair | Implemented extraction | Exact-reference mapping | Asset-backed comparison pending |
|
||||
| Player validation | Implemented extraction | Nested traversal/transfer | Multi-player GLB corpus pending |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- Material matching remains positional rather than semantic.
|
||||
- Accepted prototypes remain unbounded until final loader shutdown.
|
||||
- No private traversal, descriptor-pressure, p95/p99 or paired-client run exists.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/m2/m2_animated_scene_finalizer.gd` | Candidate ownership, traversal, material repair and validation |
|
||||
| `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/tools/verify_m2_animated_scene_finalizer.gd` | Scene/material/lifetime/boundary/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.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)
|
||||
@@ -14,8 +14,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; animation eligibility, loading and Node lifecycle
|
||||
remain in `StreamingWorldLoader` and `M2PrototypeCacheState`.
|
||||
an exact state extraction; animation eligibility and loading remain in
|
||||
`StreamingWorldLoader`, while scene finalization and retained Node lifecycle
|
||||
belong to `M2AnimatedSceneFinalizer` and `M2PrototypeCacheState`.
|
||||
|
||||
## Non-goals
|
||||
|
||||
@@ -178,8 +179,9 @@ no rebake or migration is required.
|
||||
|
||||
## Extension points
|
||||
|
||||
ResourceLoader polling or animated-scene finalization may later move behind
|
||||
separate adapters without changing this value-only state contract.
|
||||
ResourceLoader polling may later move behind a separate adapter without
|
||||
changing this value-only state contract. Animated-scene finalization is now a
|
||||
sibling service.
|
||||
|
||||
## Capability status
|
||||
|
||||
@@ -200,12 +202,14 @@ separate adapters without changing this value-only state contract.
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending records, completion FIFO and metrics |
|
||||
| `src/render/m2/m2_animated_scene_finalizer.gd` | Candidate instantiation, material repair and player validation |
|
||||
| `src/render/m2/m2_prototype_cache_state.gd` | Animated prototype/static-only outcomes |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Eligibility, I/O, permits, instantiation and adoption |
|
||||
| `src/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-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)
|
||||
|
||||
@@ -240,6 +240,7 @@ this service without moving ResourceLoader or builder ownership into it.
|
||||
## Related decisions and references
|
||||
|
||||
- [`m2-raw-model-repository.md`](m2-raw-model-repository.md)
|
||||
- [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md)
|
||||
- [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md)
|
||||
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
|
||||
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
| Status | Partial |
|
||||
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
|
||||
| Owners | Renderer workstream / milestone integrator |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animation-load-pipeline`, 2026-07-17 |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animated-scene-finalizer`, 2026-07-18 |
|
||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||
|
||||
## Purpose
|
||||
@@ -134,6 +134,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
||||
| `M2BuildBatchPlanner.plan_batch` | Internal pure M2 service | Selects static/animated batch count and next group cursor | Main/any thread; stateless | Non-positive selected limit clamps to one; empty range completes |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `M2MeshLoadPipelineState` | Internal M2 async-state service | Owns static Mesh pending request records and terminal finalize FIFO | Renderer main thread; map/session | Empty/duplicate/unknown transitions rejected |
|
||||
| `M2MeshResourceCacheState` | Internal M2 Resource cache | Owns prepared static Mesh references by normalized path | Renderer main thread; final shutdown | Empty path/null Mesh rejected; same path replaces |
|
||||
| `M2MeshResourceExtractor` | Internal M2 scene/resource service | Selects first Mesh from direct Resource, PackedScene or Node subtree | Renderer main thread; stateless except temporary instance | Invalid/no Mesh returns null; temporary PackedScene root freed |
|
||||
@@ -185,6 +186,7 @@ loader configuration remains transitional composition data, not a caller API.
|
||||
| Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset |
|
||||
| Internal raw M2 read | Extracted directory and normalized relative path | Loader / `M2RawModelRepository` | Finalizer, static or animated builder adapters | Fresh Dictionary; repository retains nothing | One synchronous native call |
|
||||
| Internal animated M2 load | Normalized path, cached GLB path and opaque terminal status | Loader / `M2AnimationLoadPipelineState` | Loader poll/finalize adapters | State-owned String/status records; detached pending snapshots | Request through terminal finalize/reset |
|
||||
| Internal animated M2 candidate | Loaded PackedScene, static material root and detached candidate | Loader / `M2AnimatedSceneFinalizer` | Loader prototype adoption/static fallback | Finalizer owns until rejection or exact-root transfer | One main-thread finalize permit |
|
||||
| Internal M2 prototype state | Normalized path, adopted static/animated Node or negative outcome | Loader / `M2PrototypeCacheState` | Loader reuse/fallback adapters | State-owned strong Node refs and Strings; detached diagnostics | Until final shutdown |
|
||||
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
|
||||
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
|
||||
@@ -541,6 +543,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| M2 placement grouper | Implemented extraction | Scene-free validation/order/transform/source/timing contract | Worker/build state, spatial cells and asset-backed p95/p99 remain pending |
|
||||
| M2 build batch planner | Implemented extraction | Scene-free limit/count/cursor/source/timing contract | Queue/resource state, spatial cells and asset-backed p95/p99 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 |
|
||||
| M2 prototype cache state | Implemented extraction | Synthetic identity/negative/lifecycle/source/timing plus shutdown contract | Asset-backed traversal/leak/p95/p99 pending |
|
||||
| WMO placement resolver | Implemented extraction | Scene-free path/identity/transform/source/timing contract | Asset-backed comparison pending |
|
||||
| WMO placement registry | Implemented extraction | Scene-free ownership/lifecycle/source/timing contract | Build/resource state and asset-backed cross-tile corpus pending |
|
||||
@@ -603,6 +606,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_animated_scene_finalizer.gd` | Animated scene candidate ownership, material repair and player validation |
|
||||
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Static M2 pending Resource paths, terminal statuses and finalize FIFO |
|
||||
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared static M2 Mesh references and final-shutdown release |
|
||||
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene instance lifetime |
|
||||
@@ -625,6 +629,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_animated_scene_finalizer.gd` | Animated M2 candidate/material/player/lifetime/boundary/timing regression |
|
||||
| `src/tools/verify_m2_mesh_load_pipeline_state.gd` | Static M2 request/terminal/FIFO/boundary/timing regression |
|
||||
| `src/tools/verify_m2_mesh_resource_cache_state.gd` | Static M2 Mesh cache ownership/lifetime/boundary/timing regression |
|
||||
| `src/tools/verify_m2_mesh_resource_extractor.gd` | M2 direct/PackedScene/subtree order/lifetime/boundary/timing regression |
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
class_name M2AnimatedSceneFinalizer
|
||||
extends RefCounted
|
||||
|
||||
## Instantiates and validates detached animated M2 scene candidates and repairs
|
||||
## their surface materials from an existing static material prototype.
|
||||
## The caller owns ResourceLoader I/O, permits and prototype cache adoption.
|
||||
|
||||
|
||||
## Instantiates one PackedScene as a detached Node3D candidate. Unsupported
|
||||
## Resources return null; a created non-Node3D root is freed before return.
|
||||
func instantiate_candidate(resource: Resource) -> Node3D:
|
||||
if not (resource is PackedScene):
|
||||
return null
|
||||
var candidate_root := (resource as PackedScene).instantiate()
|
||||
if candidate_root is Node3D:
|
||||
return candidate_root as Node3D
|
||||
if candidate_root != null:
|
||||
candidate_root.free()
|
||||
return null
|
||||
|
||||
|
||||
## Applies the historical depth-first source-to-target material mapping. A null
|
||||
## source or target leaves the candidate unchanged.
|
||||
func repair_materials(animated_root: Node3D, material_source_root: Node3D) -> void:
|
||||
if animated_root == null or material_source_root == null:
|
||||
return
|
||||
var source_meshes := mesh_instances_in_subtree(material_source_root)
|
||||
var target_meshes := mesh_instances_in_subtree(animated_root)
|
||||
if source_meshes.is_empty() or target_meshes.is_empty():
|
||||
return
|
||||
|
||||
var fallback_material := _first_mesh_material(source_meshes)
|
||||
for target_index in target_meshes.size():
|
||||
var target := target_meshes[target_index]
|
||||
if target == null or target.mesh == null:
|
||||
continue
|
||||
var source := source_meshes[mini(target_index, source_meshes.size() - 1)]
|
||||
for surface_index in target.mesh.get_surface_count():
|
||||
var material := _mesh_instance_surface_material(source, surface_index)
|
||||
if material == null:
|
||||
material = fallback_material
|
||||
if material != null:
|
||||
target.set_surface_override_material(surface_index, material)
|
||||
|
||||
|
||||
## Accepts a detached candidate only when it contains an AnimationPlayer.
|
||||
## Rejected candidates are freed synchronously. A successful result transfers
|
||||
## the exact prototype reference and its AnimationPlayer count to the caller.
|
||||
func finalize_candidate(candidate_root: Node3D) -> Dictionary:
|
||||
if candidate_root == null:
|
||||
return {}
|
||||
var animation_players := animation_players_in_subtree(candidate_root)
|
||||
if animation_players.is_empty():
|
||||
candidate_root.free()
|
||||
return {}
|
||||
return {
|
||||
"prototype": candidate_root,
|
||||
"animation_player_count": animation_players.size(),
|
||||
}
|
||||
|
||||
|
||||
## Returns MeshInstance3D descendants in depth-first preorder, including root.
|
||||
func mesh_instances_in_subtree(root: Node) -> Array[MeshInstance3D]:
|
||||
var mesh_instances: Array[MeshInstance3D] = []
|
||||
if root != null:
|
||||
_collect_mesh_instances(root, mesh_instances)
|
||||
return mesh_instances
|
||||
|
||||
|
||||
## Returns AnimationPlayer descendants in depth-first preorder, including root.
|
||||
func animation_players_in_subtree(root: Node) -> Array[AnimationPlayer]:
|
||||
var animation_players: Array[AnimationPlayer] = []
|
||||
if root != null:
|
||||
_collect_animation_players(root, animation_players)
|
||||
return animation_players
|
||||
|
||||
|
||||
func _collect_mesh_instances(node: Node, result: Array[MeshInstance3D]) -> void:
|
||||
if node is MeshInstance3D:
|
||||
result.append(node as MeshInstance3D)
|
||||
for child in node.get_children():
|
||||
_collect_mesh_instances(child, result)
|
||||
|
||||
|
||||
func _collect_animation_players(node: Node, result: Array[AnimationPlayer]) -> void:
|
||||
if node is AnimationPlayer:
|
||||
result.append(node as AnimationPlayer)
|
||||
for child in node.get_children():
|
||||
_collect_animation_players(child, result)
|
||||
|
||||
|
||||
func _first_mesh_material(meshes: Array[MeshInstance3D]) -> Material:
|
||||
for mesh_instance in meshes:
|
||||
if mesh_instance == null or mesh_instance.mesh == null:
|
||||
continue
|
||||
for surface_index in mesh_instance.mesh.get_surface_count():
|
||||
var material := _mesh_instance_surface_material(mesh_instance, surface_index)
|
||||
if material != null:
|
||||
return material
|
||||
return null
|
||||
|
||||
|
||||
func _mesh_instance_surface_material(
|
||||
mesh_instance: MeshInstance3D,
|
||||
surface_index: int
|
||||
) -> Material:
|
||||
if mesh_instance == null or mesh_instance.mesh == null:
|
||||
return null
|
||||
if surface_index < mesh_instance.get_surface_override_material_count():
|
||||
var override_material := mesh_instance.get_surface_override_material(surface_index)
|
||||
if override_material != null:
|
||||
return override_material
|
||||
if surface_index < mesh_instance.mesh.get_surface_count():
|
||||
return mesh_instance.mesh.surface_get_material(surface_index)
|
||||
return null
|
||||
@@ -0,0 +1 @@
|
||||
uid://cjjim3v675w66
|
||||
@@ -75,6 +75,9 @@ const M2_MESH_LOAD_PIPELINE_STATE_SCRIPT := preload(
|
||||
const M2_ANIMATION_LOAD_PIPELINE_STATE_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_animation_load_pipeline_state.gd"
|
||||
)
|
||||
const M2_ANIMATED_SCENE_FINALIZER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_animated_scene_finalizer.gd"
|
||||
)
|
||||
const M2_MESH_RESOURCE_CACHE_STATE_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_mesh_resource_cache_state.gd"
|
||||
)
|
||||
@@ -274,6 +277,7 @@ var _m2_mesh_resource_cache_state := M2_MESH_RESOURCE_CACHE_STATE_SCRIPT.new()
|
||||
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 _wmo_build_jobs: Dictionary = {}
|
||||
var _wmo_build_queue: Array = []
|
||||
var _tile_loading_tasks: Dictionary = {}
|
||||
@@ -4238,20 +4242,21 @@ func _drain_m2_animation_loads() -> void:
|
||||
|
||||
var path := String(pending.get("path", ""))
|
||||
var resource: Resource = ResourceLoader.load_threaded_get(path)
|
||||
if resource is PackedScene:
|
||||
var node = (resource as PackedScene).instantiate()
|
||||
if node is Node3D:
|
||||
_repair_m2_animated_materials(normalized_rel, node as Node3D)
|
||||
var players := _find_animation_players_recursive(node)
|
||||
if not players.is_empty():
|
||||
_m2_prototype_cache_state.adopt_animated_prototype(
|
||||
normalized_rel,
|
||||
node as Node3D
|
||||
)
|
||||
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, players.size()])
|
||||
print("M2_ANIM_CACHE path=%s cache=%s players=%d" % [
|
||||
normalized_rel,
|
||||
path,
|
||||
int(finalization.get("animation_player_count", 0)),
|
||||
])
|
||||
continue
|
||||
node.free()
|
||||
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||
|
||||
|
||||
@@ -4464,7 +4469,7 @@ func _start_m2_animations(root: Node, rel_path: String, instance_index: int) ->
|
||||
float(state.get("length", 0.0)),
|
||||
])
|
||||
|
||||
var players := _find_animation_players_recursive(root)
|
||||
var players := _m2_animated_scene_finalizer.animation_players_in_subtree(root)
|
||||
for player in players:
|
||||
var animation_name := _choose_default_m2_animation(player, rel_path)
|
||||
if animation_name.is_empty():
|
||||
@@ -4523,19 +4528,6 @@ func _choose_default_m2_animation(player: AnimationPlayer, rel_path: String = ""
|
||||
return String(names[0]) if not names.is_empty() else ""
|
||||
|
||||
|
||||
func _find_animation_players_recursive(root: Node) -> Array[AnimationPlayer]:
|
||||
var result: Array[AnimationPlayer] = []
|
||||
_collect_animation_players(root, result)
|
||||
return result
|
||||
|
||||
|
||||
func _collect_animation_players(node: Node, result: Array[AnimationPlayer]) -> void:
|
||||
if node is AnimationPlayer:
|
||||
result.append(node as AnimationPlayer)
|
||||
for child in node.get_children():
|
||||
_collect_animation_players(child, result)
|
||||
|
||||
|
||||
func _finish_m2_build_job(key: String) -> void:
|
||||
if not _m2_build_jobs.has(key):
|
||||
return
|
||||
@@ -4673,7 +4665,7 @@ func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh:
|
||||
func _prepare_m2_node_for_runtime(normalized_rel: String, root: Node3D) -> void:
|
||||
if root == null:
|
||||
return
|
||||
for mesh_instance in _find_mesh_instances_recursive(root):
|
||||
for mesh_instance in _m2_animated_scene_finalizer.mesh_instances_in_subtree(root):
|
||||
if mesh_instance == null or mesh_instance.mesh == null:
|
||||
continue
|
||||
var prepared := _prepare_m2_mesh_for_runtime(normalized_rel, mesh_instance.mesh)
|
||||
@@ -5000,32 +4992,6 @@ func _is_m2_animation_denied(normalized_rel: String) -> bool:
|
||||
return false
|
||||
|
||||
|
||||
func _repair_m2_animated_materials(normalized_rel: String, animated_root: Node3D) -> void:
|
||||
if animated_root == null:
|
||||
return
|
||||
var material_root := _get_or_load_m2_material_prototype(normalized_rel)
|
||||
if material_root == null:
|
||||
return
|
||||
|
||||
var source_meshes := _find_mesh_instances_recursive(material_root)
|
||||
var target_meshes := _find_mesh_instances_recursive(animated_root)
|
||||
if source_meshes.is_empty() or target_meshes.is_empty():
|
||||
return
|
||||
|
||||
var fallback_material := _first_mesh_material(source_meshes)
|
||||
for i in target_meshes.size():
|
||||
var target := target_meshes[i]
|
||||
if target == null or target.mesh == null:
|
||||
continue
|
||||
var source := source_meshes[mini(i, source_meshes.size() - 1)]
|
||||
for surface_idx in target.mesh.get_surface_count():
|
||||
var material := _mesh_instance_surface_material(source, surface_idx)
|
||||
if material == null:
|
||||
material = fallback_material
|
||||
if material != null:
|
||||
target.set_surface_override_material(surface_idx, material)
|
||||
|
||||
|
||||
func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
|
||||
if normalized_rel.is_empty():
|
||||
return null
|
||||
@@ -5049,42 +5015,6 @@ func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
|
||||
return null
|
||||
|
||||
|
||||
func _find_mesh_instances_recursive(root: Node) -> Array[MeshInstance3D]:
|
||||
var result: Array[MeshInstance3D] = []
|
||||
_collect_mesh_instances(root, result)
|
||||
return result
|
||||
|
||||
|
||||
func _collect_mesh_instances(node: Node, result: Array[MeshInstance3D]) -> void:
|
||||
if node is MeshInstance3D:
|
||||
result.append(node as MeshInstance3D)
|
||||
for child in node.get_children():
|
||||
_collect_mesh_instances(child, result)
|
||||
|
||||
|
||||
func _first_mesh_material(meshes: Array[MeshInstance3D]) -> Material:
|
||||
for mesh_instance in meshes:
|
||||
if mesh_instance == null or mesh_instance.mesh == null:
|
||||
continue
|
||||
for surface_idx in mesh_instance.mesh.get_surface_count():
|
||||
var material := _mesh_instance_surface_material(mesh_instance, surface_idx)
|
||||
if material != null:
|
||||
return material
|
||||
return null
|
||||
|
||||
|
||||
func _mesh_instance_surface_material(mesh_instance: MeshInstance3D, surface_idx: int) -> Material:
|
||||
if mesh_instance == null or mesh_instance.mesh == null:
|
||||
return null
|
||||
if surface_idx < mesh_instance.get_surface_override_material_count():
|
||||
var override_material := mesh_instance.get_surface_override_material(surface_idx)
|
||||
if override_material != null:
|
||||
return override_material
|
||||
if surface_idx < mesh_instance.mesh.get_surface_count():
|
||||
return mesh_instance.mesh.surface_get_material(surface_idx)
|
||||
return null
|
||||
|
||||
|
||||
func _glb_cache_is_safe_for_runtime_animation(cache_res_path: String) -> bool:
|
||||
var abs_path := ProjectSettings.globalize_path(cache_res_path)
|
||||
if not FileAccess.file_exists(abs_path):
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
extends SceneTree
|
||||
|
||||
## Synthetic candidate, material, ownership and timing regression for animated
|
||||
## M2 scene finalization.
|
||||
|
||||
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 LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_candidate_instantiation_and_invalid_lifetime(failures)
|
||||
_verify_animation_validation_and_transfer(failures)
|
||||
_verify_material_mapping(failures)
|
||||
_verify_ownership_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_ANIMATED_SCENE_FINALIZER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_ANIMATED_SCENE_FINALIZER PASS cases=13 iterations=10000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_candidate_instantiation_and_invalid_lifetime(failures: Array[String]) -> void:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
_expect_same(finalizer.call("instantiate_candidate", null), null, "null Resource rejected", failures)
|
||||
_expect_same(finalizer.call("instantiate_candidate", Resource.new()), null, "unsupported Resource rejected", failures)
|
||||
|
||||
var invalid_source := Node.new()
|
||||
var invalid_scene := PackedScene.new()
|
||||
_expect_equal(invalid_scene.pack(invalid_source), OK, "non-Node3D scene packs", failures)
|
||||
invalid_source.free()
|
||||
var node_count_before := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
|
||||
_expect_same(finalizer.call("instantiate_candidate", invalid_scene), null, "non-Node3D root rejected", failures)
|
||||
var node_count_after := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
|
||||
_expect_equal(node_count_after, node_count_before, "invalid instantiated root freed", failures)
|
||||
|
||||
var valid_source := Node3D.new()
|
||||
var valid_scene := PackedScene.new()
|
||||
_expect_equal(valid_scene.pack(valid_source), OK, "Node3D scene packs", failures)
|
||||
valid_source.free()
|
||||
var candidate: Node3D = finalizer.call("instantiate_candidate", valid_scene)
|
||||
_expect_true(candidate != null, "Node3D candidate returned", failures)
|
||||
if candidate != null:
|
||||
candidate.free()
|
||||
|
||||
|
||||
func _verify_animation_validation_and_transfer(failures: Array[String]) -> void:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
var empty_candidate := Node3D.new()
|
||||
var empty_candidate_id := empty_candidate.get_instance_id()
|
||||
var rejected: Dictionary = finalizer.call("finalize_candidate", empty_candidate)
|
||||
_expect_true(rejected.is_empty(), "candidate without AnimationPlayer rejected", failures)
|
||||
_expect_false(is_instance_id_valid(empty_candidate_id), "rejected candidate freed", failures)
|
||||
|
||||
var animated_candidate := Node3D.new()
|
||||
var branch := Node3D.new()
|
||||
var first_player := AnimationPlayer.new()
|
||||
var second_player := AnimationPlayer.new()
|
||||
animated_candidate.add_child(branch)
|
||||
branch.add_child(first_player)
|
||||
animated_candidate.add_child(second_player)
|
||||
var players: Array[AnimationPlayer] = finalizer.call(
|
||||
"animation_players_in_subtree",
|
||||
animated_candidate
|
||||
)
|
||||
_expect_equal(players.size(), 2, "nested players discovered", failures)
|
||||
_expect_same(players[0], first_player, "player depth-first order retained", failures)
|
||||
var accepted: Dictionary = finalizer.call("finalize_candidate", animated_candidate)
|
||||
_expect_same(accepted.get("prototype"), animated_candidate, "exact candidate transferred", failures)
|
||||
_expect_equal(int(accepted.get("animation_player_count", 0)), 2, "player count transferred", failures)
|
||||
animated_candidate.free()
|
||||
|
||||
|
||||
func _verify_material_mapping(failures: Array[String]) -> void:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
var material_source_root := Node3D.new()
|
||||
var source_first := MeshInstance3D.new()
|
||||
var source_second := MeshInstance3D.new()
|
||||
source_first.mesh = _make_triangle_mesh(2)
|
||||
source_second.mesh = _make_triangle_mesh(1)
|
||||
var first_override := StandardMaterial3D.new()
|
||||
var second_surface_material := StandardMaterial3D.new()
|
||||
source_first.set_surface_override_material(0, first_override)
|
||||
source_second.mesh.surface_set_material(0, second_surface_material)
|
||||
material_source_root.add_child(source_first)
|
||||
material_source_root.add_child(source_second)
|
||||
|
||||
var animated_root := Node3D.new()
|
||||
var target_first := MeshInstance3D.new()
|
||||
var target_second := MeshInstance3D.new()
|
||||
target_first.mesh = _make_triangle_mesh(2)
|
||||
target_second.mesh = _make_triangle_mesh(2)
|
||||
animated_root.add_child(target_first)
|
||||
animated_root.add_child(target_second)
|
||||
var target_meshes: Array[MeshInstance3D] = finalizer.call(
|
||||
"mesh_instances_in_subtree",
|
||||
animated_root
|
||||
)
|
||||
_expect_same(target_meshes[0], target_first, "mesh depth-first order retained", failures)
|
||||
finalizer.call("repair_materials", animated_root, material_source_root)
|
||||
_expect_same(target_first.get_surface_override_material(0), first_override, "source override has priority", failures)
|
||||
_expect_same(target_first.get_surface_override_material(1), first_override, "missing source surface uses first fallback", failures)
|
||||
_expect_same(target_second.get_surface_override_material(0), second_surface_material, "source index maps to target index", failures)
|
||||
_expect_same(target_second.get_surface_override_material(1), first_override, "clamped source missing surface uses fallback", failures)
|
||||
animated_root.free()
|
||||
material_source_root.free()
|
||||
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var finalizer_source := FileAccess.get_file_as_string(FINALIZER_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)
|
||||
for removed_loader_function in [
|
||||
"func _repair_m2_animated_materials(",
|
||||
"func _find_animation_players_recursive(",
|
||||
"func _find_mesh_instances_recursive(",
|
||||
]:
|
||||
_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)",
|
||||
"_m2_animated_scene_finalizer.animation_players_in_subtree(root)",
|
||||
]:
|
||||
_expect_equal(loader_source.count(delegated_call), 1, "single loader delegation: %s" % delegated_call, failures)
|
||||
for retained_loader_rule in [
|
||||
"ResourceLoader.load_threaded_get(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)",
|
||||
"M2_ANIM_CACHE path=%s cache=%s players=%d",
|
||||
]:
|
||||
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||
for forbidden_dependency in [
|
||||
"ResourceLoader.",
|
||||
"FileAccess.",
|
||||
"WorkerThreadPool.",
|
||||
"_m2_prototype_cache_state",
|
||||
"_render_budget_scheduler",
|
||||
"M2Builder",
|
||||
"M2RawModelRepository",
|
||||
]:
|
||||
_expect_false(finalizer_source.contains(forbidden_dependency), "finalizer omits %s ownership" % forbidden_dependency, failures)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
var root := Node3D.new()
|
||||
var branch := root
|
||||
for _depth_index in range(8):
|
||||
var child_branch := Node3D.new()
|
||||
branch.add_child(child_branch)
|
||||
branch = child_branch
|
||||
branch.add_child(AnimationPlayer.new())
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for _iteration in range(10000):
|
||||
finalizer.call("animation_players_in_subtree", root)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
root.free()
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "10000 subtree queries under 1 second", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _make_triangle_mesh(surface_count: int) -> ArrayMesh:
|
||||
var mesh := ArrayMesh.new()
|
||||
for _surface_index in range(surface_count):
|
||||
var arrays := []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = PackedVector3Array([
|
||||
Vector3.ZERO,
|
||||
Vector3.RIGHT,
|
||||
Vector3.UP,
|
||||
])
|
||||
arrays[Mesh.ARRAY_INDEX] = PackedInt32Array([0, 1, 2])
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return mesh
|
||||
|
||||
|
||||
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
if not condition:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(not condition, label, failures)
|
||||
|
||||
|
||||
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
|
||||
if not is_same(actual, expected):
|
||||
failures.append(label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bx51wu1f6o5je
|
||||
Reference in New Issue
Block a user