Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b2d9f23fe | |||
| c0fc191bcd | |||
| 15c24692f0 | |||
| 9b73571dcb | |||
| 5be36376c2 |
@@ -37,6 +37,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
|
||||
- `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_playback_controller.gd` - deterministic per-instance phase, default animation selection and native/imported playback mutation.
|
||||
- `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.
|
||||
@@ -1115,6 +1116,20 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
||||
- 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-18 M2 Animation Playback Controller Extraction
|
||||
|
||||
- `M2AnimationPlaybackController` now owns the stable path/index phase formula,
|
||||
ordinary/fish/bird animation-name priority, substring/first-name fallbacks and
|
||||
AnimationPlayer linear-loop/play/seek mutation.
|
||||
- Exact-script native animators retain depth-first pairing, the same five copied
|
||||
fields, optional `prepare_runtime`, phase assignment and debug-state sampling.
|
||||
- Native debug records are detached and sampled only when `debug_streaming` is
|
||||
enabled; `StreamingWorldLoader` retains path normalization and exact log text.
|
||||
- Loader still duplicates/transforms/attaches instances, applies visibility,
|
||||
shadows and Editor ownership, owns build cursors/permits and obtains player inventory.
|
||||
- Cache formats, playback priorities, phase behavior and visible rules are unchanged.
|
||||
Synthetic timing is 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
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# M03-RND-M2-ANIMATION-PLAYBACK-001
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-M2-ANIMATION-PLAYBACK-001:sindo-main-codex:2026-07-20 -->
|
||||
|
||||
## Owner
|
||||
|
||||
- Agent ID: `sindo-main-codex`
|
||||
- Target: M03 Renderer Facade and Safe Extraction
|
||||
- Branch: `work/sindo-main-codex/m03-m2-animation-playback`
|
||||
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-m2-animation-playback`
|
||||
|
||||
## Outcome
|
||||
|
||||
Extract deterministic animated M2 playback selection/phase, AnimationPlayer
|
||||
mutation and native animator data/start behavior from `StreamingWorldLoader`.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Duplicate or attach animated instances/batch roots.
|
||||
- Own visibility, shadow, Editor owner or build-job cursor rules.
|
||||
- Load/finalize/cache scenes or change animation eligibility.
|
||||
- Change candidate priority, phase formula, logging text or visible playback.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/m2/m2_animation_playback_controller.gd`,
|
||||
`src/tools/verify_m2_animation_playback_controller.gd`,
|
||||
`docs/modules/m2-animation-playback-controller.md`, this claim
|
||||
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`, M2 animated
|
||||
finalizer/build/prototype/material/facade/internal-access/manifest verifiers and
|
||||
specs, `docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
|
||||
`targets/03-renderer-facade.md`
|
||||
- Generated/ignored: `.godot`, native DLL, generated resources/caches and proprietary corpus
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Phase remains `abs(hash("path:index")) % 1000 / 1000.0`.
|
||||
- Exact animation-name priority remains ordinary Stand/Idle/Run/Walk, fish
|
||||
Run/Walk/Swim/Stand/Idle/Death, birds Run/Walk/Swim/Stand/Idle, then
|
||||
case-insensitive substring fallback and first-name fallback.
|
||||
- Every AnimationPlayer animation becomes linear-looping before selected play;
|
||||
positive-length selection seeks to deterministic phase.
|
||||
- Native animators match exact script identity in depth-first order, copy the
|
||||
same five fields, call prepare_runtime when available and set phase.
|
||||
- Runtime debug state is sampled only when existing debug mode requests it;
|
||||
loader retains final log formatting/path normalization.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: accepted animated scene finalizer package on master `c0a7bfb`
|
||||
- Blocks: animated M2 instance materializer extraction
|
||||
- External state: AnimationPlayer and M2NativeAnimator APIs remain engine/scene contracts
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: dedicated phase/selection/loop/play/seek/native-copy/start/diagnostic/
|
||||
source/timing verifier; animated finalizer/pipeline/prototype/build/shutdown/
|
||||
material/facade/internal-access/manifest regressions; docs/coordination/dry-run gates
|
||||
- Fixtures: synthetic AnimationPlayers and real M2NativeAnimator nodes
|
||||
- Fidelity evidence: exact priority, phase and mutation/reference contracts
|
||||
- Performance budget: 20,000 selection/phase operations under 1 second
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline API docs
|
||||
- Module spec with inputs/outputs, ownership and source map
|
||||
- Data-flow, lifecycle/state, sequence and dependency diagrams
|
||||
- Adjacent finalizer/build/world-renderer/module-registry/RENDER updates
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important name introduced: `M2AnimationPlaybackController`
|
||||
- Simplest solution: stateless main-thread RefCounted over supplied Nodes/players
|
||||
- Rejected complexity: signals, playback state machine, generic animation framework
|
||||
- Unavoidable complexity: native and imported AnimationPlayer paths coexist
|
||||
- Measured optimization evidence: bounded selection/phase loop
|
||||
|
||||
## Status
|
||||
|
||||
- State: accepted
|
||||
- Done: playback controller, loader adapters, verifier and required documentation
|
||||
- Next: extract animated M2 instance/batch materialization
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `9b73571`
|
||||
- Results: playback controller passes 15 phase/selection/loop/play/seek/native-
|
||||
copy/start/diagnostic/source cases and 20,000 phase-selection pairs in
|
||||
23.415 ms; all 49 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: private animation-name/timing/native visual comparison,
|
||||
descriptor pressure, leaks and p95/p99 remain unavailable; default selection
|
||||
is still heuristic and no paired original-client comparison is claimed.
|
||||
- Documentation updated: inline API; `m2-animation-playback-controller.md` with
|
||||
data-flow, lifecycle, sequence and dependency diagrams; finalizer/build/world-
|
||||
renderer specs, module registry and `RENDER.md`.
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-ANIMATION-PLAYBACK-001:9b73571 -->
|
||||
|
||||
## Integration
|
||||
|
||||
- Merge commit: `c0fc191`
|
||||
- Post-merge results: playback `cases=15 iterations=20000 elapsed_ms=23.008`;
|
||||
animated finalizer/pipeline, prototype, build, shutdown, materials, facade,
|
||||
internal-access `30`, manifest `7/7`, documentation and coordination passed.
|
||||
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-ANIMATION-PLAYBACK-001:c0fc191 -->
|
||||
@@ -23,6 +23,7 @@
|
||||
| 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 playback controller | Implemented extraction | [`m2-animation-playback-controller.md`](m2-animation-playback-controller.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) |
|
||||
|
||||
@@ -172,7 +172,8 @@ versions and prototype lifetimes are unchanged; no rebake is required.
|
||||
|
||||
## Extension points
|
||||
|
||||
Animation playback policy and native animator cloning remain separate services.
|
||||
Animation playback policy and native animator field copying now belong to
|
||||
`M2AnimationPlaybackController`; instance/batch materialization remains loader-owned.
|
||||
|
||||
## Capability status
|
||||
|
||||
@@ -193,6 +194,7 @@ Animation playback policy and native animator cloning remain separate services.
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/m2/m2_animated_scene_finalizer.gd` | Candidate ownership, traversal, material repair and validation |
|
||||
| `src/render/m2/m2_animation_playback_controller.gd` | Accepted-instance playback and native animator startup |
|
||||
| `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 |
|
||||
@@ -200,6 +202,7 @@ Animation playback policy and native animator cloning remain separate services.
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`m2-animation-playback-controller.md`](m2-animation-playback-controller.md)
|
||||
- [`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)
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
# M2 Animation Playback Controller
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented extraction |
|
||||
| Target/work package | M03 / `M03-RND-M2-ANIMATION-PLAYBACK-001` |
|
||||
| Owners | Per-instance AnimationPlayer/native animator playback mutation |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animation-playback`, 2026-07-18 |
|
||||
| Profiles/capabilities | Imported GLB and native experimental animated M2 instances |
|
||||
|
||||
## Purpose
|
||||
|
||||
Apply deterministic playback to one already duplicated M2 instance: copy native
|
||||
animator runtime fields, derive a stable phase, prepare/phase native animators,
|
||||
select an imported animation and configure linear looping/playback/seek.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Duplicate, name, transform or attach instances/batch roots.
|
||||
- Apply visibility, shadows or Editor ownership.
|
||||
- Load/finalize/cache animated scenes or decide eligibility.
|
||||
- Change animation priorities, phase formula or native animator implementation.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Prototype[Animated prototype] --> Loader[StreamingWorldLoader materializer]
|
||||
Loader -->|source and duplicate| Playback[M2AnimationPlaybackController]
|
||||
Finalizer[M2AnimatedSceneFinalizer player inventory] --> Playback
|
||||
Playback --> Native[M2NativeAnimator mutation]
|
||||
Playback --> Imported[AnimationPlayer mutation]
|
||||
```
|
||||
|
||||
Allowed dependencies are supplied Nodes, Script identity, AnimationPlayer and
|
||||
Animation resources. ResourceLoader, files, workers, caches, scheduler,
|
||||
MultiMesh, SceneTree attachment and application layers are forbidden.
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
| `copy_native_animator_data(source_root, target_root, script)` | Command | Copy five runtime fields in depth-first pair order | Main thread; borrowed nodes | Null/script mismatch yields no pairs |
|
||||
| `start_instance_playback(root, path, index, script, players, collect_diagnostics)` | Command/query | Start native/imported playback and optionally return native state | Main thread; one instance | Null/empty inventories no-op |
|
||||
| `native_animators_in_subtree(root, script)` | Query | Exact-script depth-first inventory | Main thread; borrowed nodes | Null root/script returns empty |
|
||||
| `phase_for_instance(path, index)` | Pure query | Stable phase in `[0, 1)` | Any thread; scalar | Historical hash behavior retained |
|
||||
| `choose_default_animation(player, path)` | Pure engine query | Apply exact ordinary/fish/bird/fallback priority | Main thread; borrowed player | Null/no animations returns empty |
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Prototype and duplicate subtrees | Loader materializer | Native field copy | Borrowed Nodes | One duplicate |
|
||||
| Input | Exact native animator Script | Loader composition | Native inventory | Borrowed Script | Call-local |
|
||||
| Input | Relative path and instance index | Build batch adapter | Phase/selection | Copied values | One start |
|
||||
| Input | AnimationPlayer inventory | Animated scene finalizer traversal | Imported playback | Borrowed players | One start |
|
||||
| Output | Mutated native/imported playback | Controller | Rendered instance | Nodes retain state/resources | Instance lifetime |
|
||||
| Output | Detached native diagnostic records | Controller | Loader log adapter | Caller-owned Dictionaries | Debug call |
|
||||
|
||||
Side effects are native field assignment, prepare/phase calls, animation loop
|
||||
mutation, play and seek. The service retains no inputs.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Identity[Path and index] --> Phase[Stable hash phase]
|
||||
NativeInventory[Exact-script native inventory] --> Prepare[prepare runtime if available]
|
||||
Phase --> NativePhase[set native phase]
|
||||
Players[AnimationPlayers] --> Select[Choose path-specific default]
|
||||
Select --> Loop[Set every animation LOOP_LINEAR]
|
||||
Loop --> Play[Play selected name]
|
||||
Phase --> Seek[Seek positive-length selection]
|
||||
NativePhase --> Diagnostics{Debug requested?}
|
||||
Diagnostics -->|yes| Snapshot[Detached runtime state]
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Unstarted
|
||||
Unstarted --> Prepared: copy native data and start
|
||||
Prepared --> Playing: native phase and/or AnimationPlayer play
|
||||
Playing --> Playing: repeated deterministic start
|
||||
Playing --> [*]: instance owner releases subtree
|
||||
```
|
||||
|
||||
The controller itself is stateless; lifecycle labels describe borrowed instance state.
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant L as StreamingWorldLoader
|
||||
participant F as M2AnimatedSceneFinalizer
|
||||
participant P as M2AnimationPlaybackController
|
||||
participant N as M2NativeAnimator
|
||||
participant A as AnimationPlayer
|
||||
L->>P: copy_native_animator_data(prototype, duplicate, script)
|
||||
L->>F: animation_players_in_subtree(duplicate)
|
||||
F-->>L: ordered players
|
||||
L->>P: start_instance_playback(path, index, players, debug)
|
||||
P->>N: prepare_runtime and set_phase
|
||||
P->>A: choose, loop, play and seek
|
||||
P-->>L: optional detached native diagnostics
|
||||
L->>L: preserve existing log format
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> Playback[M2AnimationPlaybackController]
|
||||
Finalizer[M2AnimatedSceneFinalizer] --> Loader
|
||||
Playback --> Engine[Node / Script / AnimationPlayer / Animation]
|
||||
Loader --> Batch[SceneTree batch materialization]
|
||||
Loader --> NativeScript[M2NativeAnimator script]
|
||||
Playback -. no dependency .-> ResourceLoader
|
||||
Playback -. no dependency .-> Cache[M2PrototypeCacheState]
|
||||
Playback -. no dependency .-> Scheduler[RenderBudgetScheduler]
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- Loader owns prototype duplication, instance/batch roots and SceneTree attachment.
|
||||
- Finalizer traversal supplies borrowed AnimationPlayer references.
|
||||
- Controller borrows Nodes/Script/players only for the synchronous call.
|
||||
- Native arrays are assigned by reference exactly as before extraction.
|
||||
- Diagnostic Dictionaries are deep-duplicated before return.
|
||||
- Main thread performs all engine-object mutation; pure phase math is thread-safe.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Missing roots/script | Inventory guard | No native copy/start | Contract fixture | Correct composition |
|
||||
| Fewer target animators | Pair-count minimum | Copy available pairs only | Native copy fixture | Rebuild import/prototype |
|
||||
| No matching name | Ordered fallback | Substring, first name, then empty | Selection fixtures | Add compatible animation |
|
||||
| Empty animation selection | Empty result | Skip loop/play/seek for that player | Selection fixture | Static/native path continues |
|
||||
| Zero-length selected animation | Length guard | Play without seek | Playback contract | Imported timing remains engine-owned |
|
||||
| Cancellation/shutdown | Outside service | No retained state | N/A | Loader owns subtree release |
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||
|---|---|---|---|---|
|
||||
| Phase buckets | 1000 | All | No | Stable per path/index desynchronization |
|
||||
| Loop mode | `LOOP_LINEAR` | All | No | Every available imported animation loops |
|
||||
| Native diagnostics | `debug_streaming` | Debug | Yes | Samples state only when requested |
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
No state or format is serialized. GLB/native cache formats, prototype lifetime
|
||||
and material versions are unchanged; no rebake is required.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
- Optional native states remain detached and contain existing prepared,
|
||||
processing, mesh, bone, surface and length fields.
|
||||
- Loader retains `M2_NATIVE_ANIMATOR` log formatting and path normalization.
|
||||
- Controller emits no logs or metrics.
|
||||
|
||||
## Verification
|
||||
|
||||
- `verify_m2_animation_playback_controller.gd` covers exact phase, ordinary/
|
||||
fish/bird priorities, substring/first fallback, loop/play/seek, native exact-
|
||||
script order, five-field copy, phase, detached diagnostics and boundaries.
|
||||
- Finalizer/build/prototype/material/shutdown regressions protect adjacent behavior.
|
||||
- Fidelity evidence is exact policy/mutation extraction; no private asset or
|
||||
original-client animation comparison is claimed.
|
||||
- Performance budget: 20,000 phase-and-selection pairs under one second.
|
||||
|
||||
## Extension points
|
||||
|
||||
Server-driven animation state may later replace default selection through a
|
||||
separate presentation contract; this fallback controller must remain available
|
||||
for world doodads and compatibility fixtures.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Imported default selection/playback | Implemented extraction | Synthetic priority/play/seek verifier | Asset-backed animation-name corpus pending |
|
||||
| Native data/phase startup | Implemented extraction | Exact-reference/native state verifier | Native visual fidelity remains experimental |
|
||||
| Instance/batch materialization | Existing loader-owned | Adjacent build tests | Safe extraction next |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- Hash phase intentionally depends on existing Godot String hashing behavior.
|
||||
- Default-name heuristics are not a complete WoW animation-state mapping.
|
||||
- No proprietary traversal, animation timing comparison, p95/p99 or paired-client run exists.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/m2/m2_animation_playback_controller.gd` | Phase, selection, native copy/start and imported playback |
|
||||
| `src/render/m2/m2_animated_scene_finalizer.gd` | Accepted prototype/player inventory |
|
||||
| `src/scenes/streaming/m2_native_animator.gd` | Experimental native deformation runtime |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Instance duplication/attachment and diagnostic formatting |
|
||||
| `src/tools/verify_m2_animation_playback_controller.gd` | Policy/mutation/boundary/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md)
|
||||
- [`m2-build-batch-planner.md`](m2-build-batch-planner.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)
|
||||
@@ -174,7 +174,8 @@ queue depth, build activity and hitch observability.
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/m2/m2_build_batch_planner.gd` | Pure limit/count/cursor planning |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Build job, queue, resources, materialization and budgets |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Build job, queue, resources, instance materialization and budgets |
|
||||
| `src/render/m2/m2_animation_playback_controller.gd` | Animated-instance phase/selection/native/imported playback |
|
||||
| `src/render/m2/m2_placement_grouper.gd` | Produces grouped transform arrays |
|
||||
| `src/tools/verify_m2_build_batch_planner.gd` | Formula, source and timing regression |
|
||||
|
||||
|
||||
@@ -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-animated-scene-finalizer`, 2026-07-18 |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animation-playback`, 2026-07-18 |
|
||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||
|
||||
## Purpose
|
||||
@@ -135,6 +135,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
@@ -187,6 +188,7 @@ loader configuration remains transitional composition data, not a caller API.
|
||||
| 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 animated M2 playback | Prototype/instance native animators, path/index and player inventory | Loader/finalizer / `M2AnimationPlaybackController` | Native and imported animation state | Borrowed Nodes; detached optional diagnostics | One duplicated instance startup |
|
||||
| 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 |
|
||||
@@ -544,6 +546,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| 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 animation playback controller | Implemented extraction | Synthetic phase/selection/loop/native-copy/source/timing contract | Asset-backed animation timing/name/native 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 |
|
||||
@@ -607,6 +610,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| `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_animation_playback_controller.gd` | Per-instance phase, selection, native copy/start and imported playback |
|
||||
| `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 |
|
||||
@@ -630,6 +634,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| `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_animation_playback_controller.gd` | Animated M2 phase/selection/playback/native/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,114 @@
|
||||
class_name M2AnimationPlaybackController
|
||||
extends RefCounted
|
||||
|
||||
## Applies deterministic playback to one animated M2 instance and copies the
|
||||
## runtime fields of exact-script native animators. The caller owns instance
|
||||
## duplication, SceneTree attachment and diagnostic log formatting.
|
||||
|
||||
|
||||
## Copies the five historical runtime fields between matching native animators
|
||||
## in depth-first order. References are intentionally retained exactly.
|
||||
func copy_native_animator_data(
|
||||
source_root: Node,
|
||||
target_root: Node,
|
||||
native_animator_script: Script
|
||||
) -> void:
|
||||
var source_animators := native_animators_in_subtree(source_root, native_animator_script)
|
||||
var target_animators := native_animators_in_subtree(target_root, native_animator_script)
|
||||
for animator_index in range(mini(source_animators.size(), target_animators.size())):
|
||||
var source := source_animators[animator_index]
|
||||
var target := target_animators[animator_index]
|
||||
target.mesh_instance_path = source.mesh_instance_path
|
||||
target.bones = source.bones
|
||||
target.surfaces = source.surfaces
|
||||
target.animation_length = source.animation_length
|
||||
target.playback_speed = source.playback_speed
|
||||
|
||||
|
||||
## Prepares native animators, applies deterministic phase, and starts supplied
|
||||
## AnimationPlayers with the historical loop/name/seek rules. Runtime debug
|
||||
## state is sampled only when requested and returned as detached Dictionaries.
|
||||
func start_instance_playback(
|
||||
root: Node,
|
||||
relative_path: String,
|
||||
instance_index: int,
|
||||
native_animator_script: Script,
|
||||
animation_players: Array[AnimationPlayer],
|
||||
collect_native_diagnostics: bool
|
||||
) -> Array[Dictionary]:
|
||||
var phase := phase_for_instance(relative_path, instance_index)
|
||||
var native_diagnostics: Array[Dictionary] = []
|
||||
for animator in native_animators_in_subtree(root, native_animator_script):
|
||||
if animator.has_method("prepare_runtime"):
|
||||
animator.prepare_runtime()
|
||||
animator.set_phase(phase)
|
||||
if collect_native_diagnostics and animator.has_method("runtime_debug_state"):
|
||||
var diagnostic_variant = animator.runtime_debug_state()
|
||||
if diagnostic_variant is Dictionary:
|
||||
native_diagnostics.append((diagnostic_variant as Dictionary).duplicate(true))
|
||||
|
||||
for player in animation_players:
|
||||
if player == null:
|
||||
continue
|
||||
var animation_name := choose_default_animation(player, relative_path)
|
||||
if animation_name.is_empty():
|
||||
continue
|
||||
for available_name in player.get_animation_list():
|
||||
var available_animation := player.get_animation(available_name)
|
||||
if available_animation != null:
|
||||
available_animation.loop_mode = Animation.LOOP_LINEAR
|
||||
player.play(animation_name)
|
||||
var selected_animation := player.get_animation(animation_name)
|
||||
if selected_animation != null and selected_animation.length > 0.0:
|
||||
player.seek(selected_animation.length * phase, true)
|
||||
return native_diagnostics
|
||||
|
||||
|
||||
## Returns exact-script native animators in depth-first preorder.
|
||||
func native_animators_in_subtree(
|
||||
root: Node,
|
||||
native_animator_script: Script
|
||||
) -> Array[Node]:
|
||||
var native_animators: Array[Node] = []
|
||||
if root != null and native_animator_script != null:
|
||||
_collect_native_animators(root, native_animator_script, native_animators)
|
||||
return native_animators
|
||||
|
||||
|
||||
## Returns the existing stable path/index phase in the inclusive-lower,
|
||||
## exclusive-upper range [0, 1).
|
||||
func phase_for_instance(relative_path: String, instance_index: int) -> float:
|
||||
return float(abs(("%s:%d" % [relative_path, instance_index]).hash()) % 1000) / 1000.0
|
||||
|
||||
|
||||
## Chooses the historical default animation name for ordinary, fish and bird
|
||||
## paths, then case-insensitive substring and first-name fallbacks.
|
||||
func choose_default_animation(player: AnimationPlayer, relative_path: String = "") -> String:
|
||||
if player == null:
|
||||
return ""
|
||||
var lower_path := relative_path.to_lower()
|
||||
var candidates: Array[String] = ["Stand", "Idle", "Run", "Walk"]
|
||||
if lower_path.contains("fish"):
|
||||
candidates = ["Run", "Walk", "Swim", "Stand", "Idle", "Death"]
|
||||
elif lower_path.contains("eagle") or lower_path.contains("bird") or lower_path.contains("gull"):
|
||||
candidates = ["Run", "Walk", "Swim", "Stand", "Idle"]
|
||||
for candidate in candidates:
|
||||
if player.has_animation(candidate):
|
||||
return candidate
|
||||
var available_names := player.get_animation_list()
|
||||
for candidate in ["run", "walk", "swim", "stand", "idle", "loop"]:
|
||||
for available_name in available_names:
|
||||
if String(available_name).to_lower().contains(candidate):
|
||||
return String(available_name)
|
||||
return String(available_names[0]) if not available_names.is_empty() else ""
|
||||
|
||||
|
||||
func _collect_native_animators(
|
||||
node: Node,
|
||||
native_animator_script: Script,
|
||||
result: Array[Node]
|
||||
) -> void:
|
||||
if node.get_script() == native_animator_script:
|
||||
result.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_native_animators(child, native_animator_script, result)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c6oln217sml80
|
||||
@@ -78,6 +78,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_PLAYBACK_CONTROLLER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_animation_playback_controller.gd"
|
||||
)
|
||||
const M2_MESH_RESOURCE_CACHE_STATE_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_mesh_resource_cache_state.gd"
|
||||
)
|
||||
@@ -278,6 +281,7 @@ 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_playback_controller := M2_ANIMATION_PLAYBACK_CONTROLLER_SCRIPT.new()
|
||||
var _wmo_build_jobs: Dictionary = {}
|
||||
var _wmo_build_queue: Array = []
|
||||
var _tile_loading_tasks: Dictionary = {}
|
||||
@@ -4438,9 +4442,35 @@ func _materialize_m2_animated_batch(
|
||||
instance.transform = transforms[start + i]
|
||||
_apply_visibility_range_recursive(instance, m2_visibility_range)
|
||||
_apply_shadow_cast_recursive(instance, m2_cast_shadows)
|
||||
_copy_m2_native_animator_data(prototype, instance)
|
||||
_m2_animation_playback_controller.copy_native_animator_data(
|
||||
prototype,
|
||||
instance,
|
||||
M2_NATIVE_ANIMATOR_SCRIPT
|
||||
)
|
||||
batch_root.add_child(instance)
|
||||
_start_m2_animations(instance, rel_path, start + i)
|
||||
var animation_players := _m2_animated_scene_finalizer.animation_players_in_subtree(
|
||||
instance
|
||||
)
|
||||
var native_diagnostics := _m2_animation_playback_controller.start_instance_playback(
|
||||
instance,
|
||||
rel_path,
|
||||
start + i,
|
||||
M2_NATIVE_ANIMATOR_SCRIPT,
|
||||
animation_players,
|
||||
debug_streaming
|
||||
)
|
||||
if debug_streaming:
|
||||
for state in native_diagnostics:
|
||||
print("M2_NATIVE_ANIMATOR path=%s instance=%d prepared=%s processing=%s mesh=%s bones=%d surfaces=%d length=%.2f" % [
|
||||
_normalize_m2_rel_path(rel_path),
|
||||
start + i,
|
||||
str(state.get("prepared", false)),
|
||||
str(state.get("processing", false)),
|
||||
str(state.get("has_mesh", false)),
|
||||
int(state.get("bones", 0)),
|
||||
int(state.get("surfaces", 0)),
|
||||
float(state.get("length", 0.0)),
|
||||
])
|
||||
|
||||
if batch_root.get_child_count() <= 0:
|
||||
batch_root.queue_free()
|
||||
@@ -4449,85 +4479,6 @@ func _materialize_m2_animated_batch(
|
||||
_set_editor_owner_recursive(batch_root)
|
||||
|
||||
|
||||
func _start_m2_animations(root: Node, rel_path: String, instance_index: int) -> void:
|
||||
var phase := float(abs(("%s:%d" % [rel_path, instance_index]).hash()) % 1000) / 1000.0
|
||||
var native_animators := _find_m2_native_animators_recursive(root)
|
||||
for animator in native_animators:
|
||||
if animator.has_method("prepare_runtime"):
|
||||
animator.prepare_runtime()
|
||||
animator.set_phase(phase)
|
||||
if debug_streaming and animator.has_method("runtime_debug_state"):
|
||||
var state: Dictionary = animator.runtime_debug_state()
|
||||
print("M2_NATIVE_ANIMATOR path=%s instance=%d prepared=%s processing=%s mesh=%s bones=%d surfaces=%d length=%.2f" % [
|
||||
_normalize_m2_rel_path(rel_path),
|
||||
instance_index,
|
||||
str(state.get("prepared", false)),
|
||||
str(state.get("processing", false)),
|
||||
str(state.get("has_mesh", false)),
|
||||
int(state.get("bones", 0)),
|
||||
int(state.get("surfaces", 0)),
|
||||
float(state.get("length", 0.0)),
|
||||
])
|
||||
|
||||
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():
|
||||
continue
|
||||
for name in player.get_animation_list():
|
||||
var animation := player.get_animation(name)
|
||||
if animation != null:
|
||||
animation.loop_mode = Animation.LOOP_LINEAR
|
||||
player.play(animation_name)
|
||||
var animation := player.get_animation(animation_name)
|
||||
if animation != null and animation.length > 0.0:
|
||||
player.seek(animation.length * phase, true)
|
||||
|
||||
|
||||
func _copy_m2_native_animator_data(source_root: Node, target_root: Node) -> void:
|
||||
var source_animators := _find_m2_native_animators_recursive(source_root)
|
||||
var target_animators := _find_m2_native_animators_recursive(target_root)
|
||||
for i in range(mini(source_animators.size(), target_animators.size())):
|
||||
var source := source_animators[i]
|
||||
var target := target_animators[i]
|
||||
target.mesh_instance_path = source.mesh_instance_path
|
||||
target.bones = source.bones
|
||||
target.surfaces = source.surfaces
|
||||
target.animation_length = source.animation_length
|
||||
target.playback_speed = source.playback_speed
|
||||
|
||||
|
||||
func _find_m2_native_animators_recursive(root: Node) -> Array[Node]:
|
||||
var result: Array[Node] = []
|
||||
_collect_m2_native_animators(root, result)
|
||||
return result
|
||||
|
||||
|
||||
func _collect_m2_native_animators(node: Node, result: Array[Node]) -> void:
|
||||
if node.get_script() == M2_NATIVE_ANIMATOR_SCRIPT:
|
||||
result.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_m2_native_animators(child, result)
|
||||
|
||||
|
||||
func _choose_default_m2_animation(player: AnimationPlayer, rel_path: String = "") -> String:
|
||||
var lower := rel_path.to_lower()
|
||||
var candidates: Array[String] = ["Stand", "Idle", "Run", "Walk"]
|
||||
if lower.contains("fish"):
|
||||
candidates = ["Run", "Walk", "Swim", "Stand", "Idle", "Death"]
|
||||
elif lower.contains("eagle") or lower.contains("bird") or lower.contains("gull"):
|
||||
candidates = ["Run", "Walk", "Swim", "Stand", "Idle"]
|
||||
for candidate in candidates:
|
||||
if player.has_animation(candidate):
|
||||
return candidate
|
||||
var names := player.get_animation_list()
|
||||
for candidate in ["run", "walk", "swim", "stand", "idle", "loop"]:
|
||||
for name in names:
|
||||
if String(name).to_lower().contains(candidate):
|
||||
return String(name)
|
||||
return String(names[0]) if not names.is_empty() else ""
|
||||
|
||||
|
||||
func _finish_m2_build_job(key: String) -> void:
|
||||
if not _m2_build_jobs.has(key):
|
||||
return
|
||||
|
||||
@@ -128,7 +128,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
"_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)",
|
||||
"_m2_animated_scene_finalizer.animation_players_in_subtree(",
|
||||
]:
|
||||
_expect_equal(loader_source.count(delegated_call), 1, "single loader delegation: %s" % delegated_call, failures)
|
||||
for retained_loader_rule in [
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
extends SceneTree
|
||||
|
||||
## Synthetic phase, selection, playback, native-copy and boundary regression for
|
||||
## animated M2 instance playback.
|
||||
|
||||
const CONTROLLER_SCRIPT := preload("res://src/render/m2/m2_animation_playback_controller.gd")
|
||||
const NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
|
||||
const CONTROLLER_PATH := "res://src/render/m2/m2_animation_playback_controller.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_phase_contract(failures)
|
||||
_verify_animation_selection_priority(failures)
|
||||
_verify_player_loop_play_and_seek(failures)
|
||||
_verify_native_copy_start_and_diagnostics(failures)
|
||||
_verify_ownership_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_ANIMATION_PLAYBACK_CONTROLLER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_ANIMATION_PLAYBACK_CONTROLLER PASS cases=15 iterations=20000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_phase_contract(failures: Array[String]) -> void:
|
||||
var controller: RefCounted = CONTROLLER_SCRIPT.new()
|
||||
for fixture in [
|
||||
{"path": "creature/fish/fish.m2", "index": 0},
|
||||
{"path": "world/critter/eagle.m2", "index": 17},
|
||||
{"path": "World\\Critter\\Bird.m2", "index": -3},
|
||||
]:
|
||||
var path := String(fixture["path"])
|
||||
var index := int(fixture["index"])
|
||||
var expected := float(abs(("%s:%d" % [path, index]).hash()) % 1000) / 1000.0
|
||||
var actual := float(controller.call("phase_for_instance", path, index))
|
||||
_expect_float_equal(actual, expected, "exact phase formula", failures)
|
||||
_expect_true(actual >= 0.0 and actual < 1.0, "phase in unit interval", failures)
|
||||
|
||||
|
||||
func _verify_animation_selection_priority(failures: Array[String]) -> void:
|
||||
var controller: RefCounted = CONTROLLER_SCRIPT.new()
|
||||
var ordinary := _make_player(["Idle", "Stand", "Run"])
|
||||
_expect_string_equal(String(controller.call("choose_default_animation", ordinary, "world/tree.m2")), "Stand", "ordinary priority", failures)
|
||||
var fish := _make_player(["Stand", "Swim", "Death"])
|
||||
_expect_string_equal(String(controller.call("choose_default_animation", fish, "creature/fish/a.m2")), "Swim", "fish priority", failures)
|
||||
var bird := _make_player(["Idle", "Walk", "Swim"])
|
||||
_expect_string_equal(String(controller.call("choose_default_animation", bird, "creature/eagle/a.m2")), "Walk", "bird priority", failures)
|
||||
var substring := _make_player(["Custom_Run_Loop"])
|
||||
_expect_string_equal(String(controller.call("choose_default_animation", substring, "world/a.m2")), "Custom_Run_Loop", "substring fallback", failures)
|
||||
var arbitrary := _make_player(["Zeta"])
|
||||
_expect_string_equal(String(controller.call("choose_default_animation", arbitrary, "world/a.m2")), "Zeta", "first-name fallback", failures)
|
||||
var empty_player := AnimationPlayer.new()
|
||||
_expect_string_equal(String(controller.call("choose_default_animation", empty_player, "")), "", "empty player rejected", failures)
|
||||
ordinary.free()
|
||||
fish.free()
|
||||
bird.free()
|
||||
substring.free()
|
||||
arbitrary.free()
|
||||
empty_player.free()
|
||||
|
||||
|
||||
func _verify_player_loop_play_and_seek(failures: Array[String]) -> void:
|
||||
var controller: RefCounted = CONTROLLER_SCRIPT.new()
|
||||
var fixture_root := Node3D.new()
|
||||
var player := _make_player(["Stand", "Other"])
|
||||
var stand := player.get_animation("Stand")
|
||||
var other := player.get_animation("Other")
|
||||
stand.length = 2.0
|
||||
other.length = 1.0
|
||||
stand.loop_mode = Animation.LOOP_NONE
|
||||
other.loop_mode = Animation.LOOP_NONE
|
||||
fixture_root.add_child(player)
|
||||
get_root().add_child(fixture_root)
|
||||
var phase := float(controller.call("phase_for_instance", "world/test.m2", 5))
|
||||
var diagnostics: Array[Dictionary] = controller.call(
|
||||
"start_instance_playback",
|
||||
fixture_root,
|
||||
"world/test.m2",
|
||||
5,
|
||||
NATIVE_ANIMATOR_SCRIPT,
|
||||
[player] as Array[AnimationPlayer],
|
||||
false
|
||||
)
|
||||
_expect_true(diagnostics.is_empty(), "no native diagnostics without animators", failures)
|
||||
_expect_equal(stand.loop_mode, Animation.LOOP_LINEAR, "selected loop linear", failures)
|
||||
_expect_equal(other.loop_mode, Animation.LOOP_LINEAR, "all loops linear", failures)
|
||||
_expect_string_equal(player.current_animation, "Stand", "selected animation plays", failures)
|
||||
_expect_float_equal(player.current_animation_position, stand.length * phase, "selected animation seeks to phase", failures)
|
||||
fixture_root.free()
|
||||
|
||||
|
||||
func _verify_native_copy_start_and_diagnostics(failures: Array[String]) -> void:
|
||||
var controller: RefCounted = CONTROLLER_SCRIPT.new()
|
||||
var source_root := Node3D.new()
|
||||
var source_branch := Node.new()
|
||||
var source_animator: Node = NATIVE_ANIMATOR_SCRIPT.new()
|
||||
source_root.add_child(source_branch)
|
||||
source_branch.add_child(source_animator)
|
||||
source_animator.mesh_instance_path = NodePath("../SourceMesh")
|
||||
source_animator.bones = [{"parent": -1}]
|
||||
source_animator.surfaces = [{"vertices": PackedVector3Array()}]
|
||||
source_animator.animation_length = 8.0
|
||||
source_animator.playback_speed = 1.5
|
||||
|
||||
var target_root := Node3D.new()
|
||||
var target_branch := Node.new()
|
||||
var target_animator: Node = NATIVE_ANIMATOR_SCRIPT.new()
|
||||
target_root.add_child(target_branch)
|
||||
target_branch.add_child(target_animator)
|
||||
controller.call("copy_native_animator_data", source_root, target_root, NATIVE_ANIMATOR_SCRIPT)
|
||||
_expect_true(target_animator.mesh_instance_path == source_animator.mesh_instance_path, "native path copied", failures)
|
||||
_expect_same(target_animator.bones, source_animator.bones, "native bones reference copied", failures)
|
||||
_expect_same(target_animator.surfaces, source_animator.surfaces, "native surfaces reference copied", failures)
|
||||
_expect_float_equal(target_animator.animation_length, 8.0, "native length copied", failures)
|
||||
_expect_float_equal(target_animator.playback_speed, 1.5, "native speed copied", failures)
|
||||
|
||||
var native_inventory: Array[Node] = controller.call("native_animators_in_subtree", target_root, NATIVE_ANIMATOR_SCRIPT)
|
||||
_expect_equal(native_inventory.size(), 1, "exact-script native animator discovered", failures)
|
||||
var phase := float(controller.call("phase_for_instance", "world/native.m2", 9))
|
||||
var diagnostics: Array[Dictionary] = controller.call(
|
||||
"start_instance_playback",
|
||||
target_root,
|
||||
"world/native.m2",
|
||||
9,
|
||||
NATIVE_ANIMATOR_SCRIPT,
|
||||
[] as Array[AnimationPlayer],
|
||||
true
|
||||
)
|
||||
_expect_equal(diagnostics.size(), 1, "requested native diagnostic returned", failures)
|
||||
_expect_float_equal(float(target_animator.get("_time")), 8.0 * phase, "native phase applied", failures)
|
||||
diagnostics[0]["bones"] = 999
|
||||
_expect_equal(int(target_animator.runtime_debug_state()["bones"]), 1, "native diagnostic detached", failures)
|
||||
source_root.free()
|
||||
target_root.free()
|
||||
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var controller_source := FileAccess.get_file_as_string(CONTROLLER_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_expect_true(loader_source.contains("M2_ANIMATION_PLAYBACK_CONTROLLER_SCRIPT.new()"), "loader composes playback controller", failures)
|
||||
for removed_loader_function in [
|
||||
"func _start_m2_animations(",
|
||||
"func _copy_m2_native_animator_data(",
|
||||
"func _find_m2_native_animators_recursive(",
|
||||
"func _choose_default_m2_animation(",
|
||||
]:
|
||||
_expect_false(loader_source.contains(removed_loader_function), "legacy helper removed: %s" % removed_loader_function, failures)
|
||||
_expect_equal(loader_source.count("_m2_animation_playback_controller.copy_native_animator_data("), 1, "native copy delegates once", failures)
|
||||
_expect_equal(loader_source.count("_m2_animation_playback_controller.start_instance_playback("), 1, "playback delegates once", failures)
|
||||
for retained_loader_rule in [
|
||||
"prototype.duplicate(Node.DUPLICATE_SIGNALS | Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS)",
|
||||
"batch_root.add_child(instance)",
|
||||
"_apply_visibility_range_recursive(instance, m2_visibility_range)",
|
||||
"_apply_shadow_cast_recursive(instance, m2_cast_shadows)",
|
||||
"_set_editor_owner_recursive(batch_root)",
|
||||
"M2_NATIVE_ANIMATOR path=%s instance=%d",
|
||||
]:
|
||||
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||
for forbidden_dependency in [
|
||||
"ResourceLoader.",
|
||||
"FileAccess.",
|
||||
"WorkerThreadPool.",
|
||||
"MultiMesh",
|
||||
"add_child(",
|
||||
"_m2_prototype_cache_state",
|
||||
"_render_budget_scheduler",
|
||||
]:
|
||||
_expect_false(controller_source.contains(forbidden_dependency), "controller omits %s ownership" % forbidden_dependency, failures)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var controller: RefCounted = CONTROLLER_SCRIPT.new()
|
||||
var player := _make_player(["Stand", "Custom_Run"])
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in range(20000):
|
||||
controller.call("phase_for_instance", "world/model.m2", iteration)
|
||||
controller.call("choose_default_animation", player, "world/model.m2")
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
player.free()
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "20000 phase/selection pairs under 1 second", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _make_player(animation_names: Array[String]) -> AnimationPlayer:
|
||||
var player := AnimationPlayer.new()
|
||||
var library := AnimationLibrary.new()
|
||||
for animation_name in animation_names:
|
||||
library.add_animation(animation_name, Animation.new())
|
||||
player.add_animation_library("", library)
|
||||
return player
|
||||
|
||||
|
||||
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_float_equal(actual: float, expected: float, label: String, failures: Array[String]) -> void:
|
||||
if not is_equal_approx(actual, expected):
|
||||
failures.append("%s expected=%.6f actual=%.6f" % [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_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
|
||||
if not is_same(actual, expected):
|
||||
failures.append(label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ceurdk7w0kpbd
|
||||
@@ -27,7 +27,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
## Evidence
|
||||
|
||||
- Date: 2026-07-18
|
||||
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`, `f88bf97`, `3f84717`, `d2cb52f`, `f9e5a4f`, `c06aed5`, `a878e7c`, `60603e1`, `b4955d6`, `ada0fd9`, `f470775`, `874fd0f`, `38f831e`, `83b3618`, `3094561`, `fa85985`, `1fafdde`, `7cd07dd`, `1de50f9`, `3dccd3e`, `e7a7c67`;
|
||||
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`, `f88bf97`, `3f84717`, `d2cb52f`, `f9e5a4f`, `c06aed5`, `a878e7c`, `60603e1`, `b4955d6`, `ada0fd9`, `f470775`, `874fd0f`, `38f831e`, `83b3618`, `3094561`, `fa85985`, `1fafdde`, `7cd07dd`, `1de50f9`, `3dccd3e`, `e7a7c67`, `c0fc191`;
|
||||
packages `M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-001`,
|
||||
`M03-RND-FACADE-GROUND-QUERY-001`, `M03-RND-FACADE-ENVIRONMENT-001`,
|
||||
`M03-RND-FACADE-ENTITY-001`, `M03-RND-TERRAIN-CACHE-SERVICE-001`,
|
||||
@@ -52,7 +52,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
`M03-RND-M2-RAW-MODEL-REPOSITORY-001`,
|
||||
`M03-RND-M2-PROTOTYPE-CACHE-001`,
|
||||
`M03-RND-M2-ANIMATION-LOAD-PIPELINE-001`,
|
||||
`M03-RND-M2-ANIMATED-SCENE-FINALIZER-001`
|
||||
`M03-RND-M2-ANIMATED-SCENE-FINALIZER-001`,
|
||||
`M03-RND-M2-ANIMATION-PLAYBACK-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
|
||||
@@ -207,6 +208,14 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
finalizer (`40.314ms`), animation pipeline, prototype state, shutdown,
|
||||
materials, facade, internal-access `30`, `7/7` manifest, documentation and
|
||||
coordination gates remained green.
|
||||
M2 animation playback controller passed `cases=15 iterations=20000
|
||||
elapsed_ms=23.415` with exact phase, ordinary/fish/bird priorities,
|
||||
substring/first-name fallback, loop/play/seek, native exact-script order,
|
||||
five-field reference copy, phase and detached diagnostics. All 49 autonomous
|
||||
headless regressions and checkpoint dry-run `7/7` passed; the proprietary ADT
|
||||
probe remained unavailable. Post-merge playback (`23.008ms`), animated
|
||||
finalizer/pipeline, prototype, build, shutdown, materials, facade,
|
||||
internal-access `30`, `7/7` manifest, documentation and coordination passed.
|
||||
M02 terrain-query regression
|
||||
remained green (13 pre-existing expired M00 claim warnings).
|
||||
- Fidelity comparison: all 16 historical operation limits and drain sites were
|
||||
@@ -291,6 +300,13 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
rejected roots are synchronously freed, including the previously leaked
|
||||
non-Node3D imported-root edge case. I/O, permits, material-source selection,
|
||||
prototype/static outcome and playback policy remain loader-owned.
|
||||
Animated instance playback retains the exact path/index hash phase, ordinary/
|
||||
fish/bird priority lists, case-insensitive substring and first-name fallbacks,
|
||||
linear looping of every imported animation, selected play and positive-length
|
||||
phase seek. Native animators retain exact-script depth-first pairing, five
|
||||
reference/value assignments, optional prepare, phase and debug sampling.
|
||||
Instance duplication/attachment, visibility, shadows, Editor ownership and
|
||||
diagnostic log formatting remain loader-owned.
|
||||
Prepared static M2 Mesh lookups retain exact Resource identity, same-path
|
||||
replacement and map-refresh lifetime; the only clear remains final shutdown
|
||||
after asynchronous drain. Shared missing/static/animated prototype state now
|
||||
@@ -339,6 +355,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
shutdown-lifetime M2 prototype cache state and all positive/negative adapters,
|
||||
M2 animation load pipeline state and loader request/poll/finalize/metric/clear adapters,
|
||||
M2 animated scene finalizer and loader candidate/material/player traversal adapters,
|
||||
M2 animation playback controller and loader native/imported playback adapters,
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user