diff --git a/RENDER.md b/RENDER.md index 64c1b2a..5d9d717 100644 --- a/RENDER.md +++ b/RENDER.md @@ -38,6 +38,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g - `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_animated_instance_materializer.gd` - main-thread animated instance duplication, render settings, playback startup and non-empty batch attachment. - `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. diff --git a/coordination/claims/M03-RND-M2-ANIMATED-INSTANCE-MATERIALIZER-001.md b/coordination/claims/M03-RND-M2-ANIMATED-INSTANCE-MATERIALIZER-001.md index 7c26af1..060a36b 100644 --- a/coordination/claims/M03-RND-M2-ANIMATED-INSTANCE-MATERIALIZER-001.md +++ b/coordination/claims/M03-RND-M2-ANIMATED-INSTANCE-MATERIALIZER-001.md @@ -72,14 +72,24 @@ playback startup and batch attachment from `StreamingWorldLoader`. ## Status -- State: active -- Done: contract and ownership boundary -- Next: implementation, verification and documentation +- State: ready for integration +- Done: materializer, loader adapter, verifier and required documentation +- Next: integrator merge and post-merge acceptance - Blocked by: ## Handoff -- Commit: -- Results: -- Remaining risks: -- Documentation updated: +- Commit: `456f3e2` +- Results: materializer passes 12 duplication/order/render/playback/diagnostic/ + source cases and 1,000 instances in 9.267 ms; all 50 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 asset traversal/visual comparison, descriptor + pressure, leaks and p95/p99 remain unavailable; materialization is still + synchronous main-thread work and no original-client comparison is claimed. +- Documentation updated: inline API; `m2-animated-instance-materializer.md` + with data-flow, lifecycle, sequence and dependency diagrams; playback, + finalizer, build, world-renderer specs, module registry and `RENDER.md`. + + diff --git a/docs/modules/README.md b/docs/modules/README.md index 8c6fedf..3efaf78 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -24,6 +24,7 @@ | 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 animated instance materializer | Implemented extraction | [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.md) | | M2 animation load pipeline state | Implemented extraction | [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md) | | M2 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) | diff --git a/docs/modules/m2-animated-instance-materializer.md b/docs/modules/m2-animated-instance-materializer.md new file mode 100644 index 0000000..068488b --- /dev/null +++ b/docs/modules/m2-animated-instance-materializer.md @@ -0,0 +1,231 @@ +# M2 Animated Instance Materializer + +## Metadata + +| Field | Value | +|---|---| +| Status | Implemented extraction | +| Target/work package | M03 / `M03-RND-M2-ANIMATED-INSTANCE-MATERIALIZER-001` | +| Owners | Animated M2 duplicate/batch construction, render settings, playback startup and attachment | +| Last verified | Worktree `work/sindo-main-codex/m03-m2-animated-instance-materializer`, 2026-07-18 | +| Profiles/capabilities | Imported GLB and native experimental animated M2 instances | + +## Purpose + +Materialize one ordered animated M2 build batch on the main thread. The service +duplicates an accepted prototype, preserves historical names and transforms, +applies visibility/shadow settings recursively, starts native/imported playback, +attaches a non-empty batch and returns detached native diagnostic entries. + +## Non-goals + +- Plan build batches, advance job cursors or consume render budgets. +- Load, finalize, select or cache animated prototypes. +- Select animations or implement native animator deformation. +- Format logs, normalize paths, assign Editor owners or clean up tile roots. +- Materialize static M2 `MultiMesh` batches. + +## Context and boundaries + +```mermaid +flowchart LR + Loader[StreamingWorldLoader adapter] --> Materializer[M2AnimatedInstanceMaterializer] + Prototype[Accepted animated prototype] --> Materializer + Plan[Transforms and batch slice] --> Materializer + Materializer --> Playback[M2AnimationPlaybackController] + Materializer --> Finalizer[M2AnimatedSceneFinalizer traversal] + Materializer --> Batch[Attached animated batch] + Materializer --> Diagnostics[Detached diagnostic entries] +``` + +Allowed dependencies are Godot Node/Node3D/GeometryInstance3D APIs and the +accepted animated finalizer and playback services. Resource loading, files, +workers, caches, scheduler policy, Editor ownership and `MultiMesh` are forbidden. + +## Public API + +| Symbol | Kind | Purpose | Thread/lifetime | Errors | +|---|---|---|---|---| +| `materialize_batch(parent, path, prototype, transforms, start, count, serial, visibility_end, visibility_margin, cast_shadows, native_script, collect_diagnostics)` | Command/query | Build, start and attach one ordered animated batch | Main thread; borrowed inputs, parent owns result | Invalid/empty input or all failed duplicates returns `{}` | + +## Inputs and outputs + +| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | +|---|---|---|---|---|---| +| Input | M2 parent root | Loader build job | Batch attachment | Borrowed Node3D | Tile/job lifetime | +| Input | Relative path, start/count and serial | Batch planner/loader adapter | Names, phase and slice | Copied scalars | One call | +| Input | Accepted animated prototype | Prototype cache | Duplicate source | Borrowed Node3D | Cache-owned | +| Input | Ordered transforms | Placement grouping/build job | Instance transforms | Borrowed Array | Job lifetime | +| Input | Visibility/shadow settings | Renderer configuration | Recursive render mutation | Copied scalars | One call | +| Input | Native animator Script identity | Loader composition | Playback controller | Borrowed Script | Application lifetime | +| Output | `batch_root` | Materializer | Parent SceneTree and loader Editor-owner adapter | Parent-owned Node3D | Tile lifetime | +| Output | `native_diagnostics` entries | Playback controller/materializer | Loader log adapter | Caller-owned Array/Dictionaries | Debug call | + +Side effects are Node duplication, instance naming/transform assignment, +recursive geometry mutation, playback mutation and SceneTree attachment. The +service retains no batch, prototype, transform or diagnostic references. + +## Data flow + +```mermaid +flowchart TD + Slice[Ordered transform slice] --> Duplicate[Duplicate prototype with signals, groups and scripts] + Duplicate --> Identity[Assign historical name and transform] + Identity --> Render[Apply visibility margin and shadow recursively] + Render --> NativeCopy[Copy native animator runtime fields] + NativeCopy --> AttachInstance[Attach duplicate to detached batch] + AttachInstance --> Players[Inventory AnimationPlayers] + Players --> Start[Start deterministic native/imported playback] + Start --> More{More slice entries?} + More -->|yes| Duplicate + More -->|no, non-empty| AttachBatch[Attach batch to supplied parent] + Start --> Diagnostics[Tag detached states with instance index] +``` + +## Lifecycle/state + +```mermaid +stateDiagram-v2 + [*] --> Validating + Validating --> Empty: invalid input + Validating --> Building: valid slice + Building --> Building: next successful duplicate + Building --> Empty: no successful duplicates + Building --> Attached: non-empty batch + Empty --> [*] + Attached --> [*]: parent/tile owner releases subtree +``` + +The `RefCounted` service is stateless; lifecycle labels describe each batch call. + +## Main sequence + +```mermaid +sequenceDiagram + participant L as StreamingWorldLoader + participant M as M2AnimatedInstanceMaterializer + participant F as M2AnimatedSceneFinalizer + participant P as M2AnimationPlaybackController + participant R as M2 parent root + L->>M: materialize_batch(slice, prototype, render settings) + loop ordered instance + M->>M: duplicate, name, transform, render settings + M->>P: copy native animator fields + M->>F: animation_players_in_subtree(duplicate) + M->>P: start_instance_playback(path, index, players, debug) + end + M->>R: add_child(non-empty batch) + M-->>L: batch root and indexed diagnostics + L->>L: format logs and assign Editor owner +``` + +## Dependency diagram + +```mermaid +flowchart TB + Loader[StreamingWorldLoader] --> Materializer[M2AnimatedInstanceMaterializer] + Materializer --> Playback[M2AnimationPlaybackController] + Materializer --> Finalizer[M2AnimatedSceneFinalizer] + Materializer --> Engine[Node3D / GeometryInstance3D / SceneTree APIs] + Loader --> EditorOwner[Editor ownership adapter] + Loader --> BuildJob[Build cursor and budget] + Materializer -. no dependency .-> Cache[Prototype/cache/load state] + Materializer -. no dependency .-> MultiMesh[Static M2 materialization] +``` + +## Ownership, threading and resources + +- Loader/build state borrows the accepted prototype and ordered transform Array. +- Each successful duplicate becomes a child of the detached batch immediately. +- A non-empty batch transfers to the supplied parent; the parent owns its lifetime. +- A batch with no successful duplicates is queued for deletion and no reference returns. +- Playback/finalizer services borrow each duplicate only during synchronous calls. +- Returned diagnostic entries contain the stable instance index and a detached state. +- All Node duplication, mutation and attachment is main-thread-only. + +## Errors, cancellation and recovery + +| Failure | Detection | Behavior | Diagnostic | Recovery | +|---|---|---|---|---| +| Null parent/prototype | Entry guard | Return `{}` without mutation | Contract verifier | Correct composition/cache state | +| Empty transforms or non-positive count | Entry guard | Return `{}` | Contract verifier | Planner supplies a non-empty slice | +| Duplicate is not Node3D | Cast result | Skip that entry and preserve order of successes | Child-count contract | Repair prototype scene root | +| Every duplicate fails | Empty batch check | Queue empty batch, return `{}` | Empty-result contract | Rebuild/import prototype | +| Shutdown during/after call | Outside service | Call is synchronous; loader owns cancellation and subtree release | Shutdown regression | Loader cancels job/releases parent | + +Transform bounds remain a planner/build-job precondition exactly as before the +extraction; this service does not clamp or silently reorder an invalid slice. + +## Configuration and capabilities + +| Setting/capability | Default | Profile | Runtime mutable | Effect | +|---|---|---|---|---| +| Duplicate flags | Signals + groups + scripts | All | No | Preserves historical instance behavior | +| Visibility end/margin | Loader renderer settings / chunk size | All | Yes, between calls | Applied to every GeometryInstance3D descendant | +| Shadow mode | Loader `m2_cast_shadows` | All | Yes, between calls | Enables/disables descendant shadow casting | +| Native diagnostics | Loader `debug_streaming` | Debug | Yes | Collects indexed state only when requested | + +## Persistence, cache and migration + +No data is serialized and no cache/schema version changes. Editor owner +assignment remains a loader concern after attachment, so generated-scene +persistence behavior and migration remain unchanged. + +## Diagnostics and observability + +- The materializer emits no logs, metrics or debug views. +- Native state remains detached and is tagged with the absolute instance index. +- Loader preserves the existing normalized-path `M2_NATIVE_ANIMATOR` log text. +- No correlation ID is introduced; path/index remains the existing identity. + +## Verification + +- `verify_m2_animated_instance_materializer.gd` covers invalid input, names, + order, transforms, recursive visibility/margin/shadows, playback, native field + references, indexed diagnostics, prototype immutability and ownership boundaries. +- Playback/finalizer/pipeline/build/prototype/shutdown/material/facade/internal- + access regressions protect adjacent behavior. +- Fidelity evidence is exact extraction of existing flags/order/settings/calls; + no original-client visual comparison or proprietary asset result is claimed. +- Performance budget: 1,000 synthetic Node3D instances under one second. + +## Extension points + +A later facade-owned animated presentation command may supply the same explicit +batch contract. Async SceneTree mutation, callbacks and a generic scene factory +are intentionally excluded until measurements show a need. + +## Capability status + +| Capability | Status | Evidence | Gap/next step | +|---|---|---|---| +| Ordered animated instance/batch materialization | Implemented extraction | Synthetic batch contract verifier | Proprietary asset traversal pending | +| Recursive visibility/shadow settings | Implemented extraction | Nested geometry fixture | Visual distance/shadow comparison pending | +| Native/imported playback startup | Delegated to implemented controller | Playback + materializer verifiers | Full WoW animation-state mapping pending | +| Editor persistence | Existing loader-owned | Source boundary and editor helper | Editor integration fixture remains separate | + +## Known gaps and risks + +- The existing build planner must continue to guarantee valid transform bounds. +- Default animation selection and native deformation retain their documented gaps. +- No private asset, paired original-client, descriptor-pressure, leak or p95/p99 run exists. +- Recursive render mutation remains proportional to duplicate subtree size. + +## Source map + +| Path | Responsibility | +|---|---| +| `src/render/m2/m2_animated_instance_materializer.gd` | Duplicate, render mutation, playback startup and non-empty batch attachment | +| `src/render/m2/m2_animation_playback_controller.gd` | Phase, selection and native/imported playback mutation | +| `src/render/m2/m2_animated_scene_finalizer.gd` | AnimationPlayer inventory for each duplicate | +| `src/scenes/streaming/streaming_world_loader.gd` | Build adapter, diagnostic formatting and Editor ownership | +| `src/tools/verify_m2_animated_instance_materializer.gd` | Batch/render/playback/boundary/timing regression | + +## Related decisions and references + +- [`m2-animation-playback-controller.md`](m2-animation-playback-controller.md) +- [`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) diff --git a/docs/modules/m2-animated-scene-finalizer.md b/docs/modules/m2-animated-scene-finalizer.md index aed3719..0654d7f 100644 --- a/docs/modules/m2-animated-scene-finalizer.md +++ b/docs/modules/m2-animated-scene-finalizer.md @@ -173,7 +173,8 @@ versions and prototype lifetimes are unchanged; no rebake is required. ## Extension points Animation playback policy and native animator field copying now belong to -`M2AnimationPlaybackController`; instance/batch materialization remains loader-owned. +`M2AnimationPlaybackController`; accepted instance/batch materialization is owned +by `M2AnimatedInstanceMaterializer`. ## Capability status @@ -195,6 +196,7 @@ Animation playback policy and native animator field copying now belong to |---|---| | `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_animated_instance_materializer.gd` | Per-duplicate player inventory consumer and batch owner | | `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending/terminal records before finalization | | `src/render/m2/m2_prototype_cache_state.gd` | Accepted prototype/static-only outcomes | | `src/scenes/streaming/streaming_world_loader.gd` | I/O, permits, material source, adoption and logs | diff --git a/docs/modules/m2-animation-playback-controller.md b/docs/modules/m2-animation-playback-controller.md index 8927c74..cb6dde2 100644 --- a/docs/modules/m2-animation-playback-controller.md +++ b/docs/modules/m2-animation-playback-controller.md @@ -27,8 +27,8 @@ select an imported animation and configure linear looping/playback/seek. ```mermaid flowchart LR - Prototype[Animated prototype] --> Loader[StreamingWorldLoader materializer] - Loader -->|source and duplicate| Playback[M2AnimationPlaybackController] + Prototype[Animated prototype] --> Materializer[M2AnimatedInstanceMaterializer] + Materializer -->|source and duplicate| Playback[M2AnimationPlaybackController] Finalizer[M2AnimatedSceneFinalizer player inventory] --> Playback Playback --> Native[M2NativeAnimator mutation] Playback --> Imported[AnimationPlayer mutation] @@ -52,7 +52,7 @@ MultiMesh, SceneTree attachment and application layers are forbidden. | Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | |---|---|---|---|---|---| -| Input | Prototype and duplicate subtrees | Loader materializer | Native field copy | Borrowed Nodes | One duplicate | +| Input | Prototype and duplicate subtrees | M2 animated instance 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 | @@ -94,29 +94,30 @@ The controller itself is stateless; lifecycle labels describe borrowed instance ```mermaid sequenceDiagram - participant L as StreamingWorldLoader + participant M as M2AnimatedInstanceMaterializer 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) + M->>P: copy_native_animator_data(prototype, duplicate, script) + M->>F: animation_players_in_subtree(duplicate) + F-->>M: ordered players + M->>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 + P-->>M: optional detached native diagnostics + M-->>M: tag states with instance index ``` ## Dependency diagram ```mermaid flowchart TB - Loader[StreamingWorldLoader] --> Playback[M2AnimationPlaybackController] - Finalizer[M2AnimatedSceneFinalizer] --> Loader + Loader[StreamingWorldLoader] --> Materializer[M2AnimatedInstanceMaterializer] + Materializer --> Playback[M2AnimationPlaybackController] + Materializer --> Finalizer[M2AnimatedSceneFinalizer] Playback --> Engine[Node / Script / AnimationPlayer / Animation] - Loader --> Batch[SceneTree batch materialization] + Materializer --> Batch[SceneTree batch materialization] Loader --> NativeScript[M2NativeAnimator script] Playback -. no dependency .-> ResourceLoader Playback -. no dependency .-> Cache[M2PrototypeCacheState] @@ -125,8 +126,8 @@ flowchart TB ## Ownership, threading and resources -- Loader owns prototype duplication, instance/batch roots and SceneTree attachment. -- Finalizer traversal supplies borrowed AnimationPlayer references. +- Materializer owns prototype duplication, instance/batch roots and SceneTree attachment. +- Materializer uses finalizer traversal for 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. @@ -185,7 +186,7 @@ for world doodads and compatibility fixtures. |---|---|---|---| | 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 | +| Instance/batch materialization | Implemented extraction | Materializer contract verifier | Asset-backed traversal pending | ## Known gaps and risks @@ -200,7 +201,8 @@ for world doodads and compatibility fixtures. | `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/render/m2/m2_animated_instance_materializer.gd` | Instance duplication/attachment and playback composition | +| `src/scenes/streaming/streaming_world_loader.gd` | Build adapter, diagnostic formatting and Editor ownership | | `src/tools/verify_m2_animation_playback_controller.gd` | Policy/mutation/boundary/timing regression | ## Related decisions and references diff --git a/docs/modules/m2-build-batch-planner.md b/docs/modules/m2-build-batch-planner.md index f631213..87ce0eb 100644 --- a/docs/modules/m2-build-batch-planner.md +++ b/docs/modules/m2-build-batch-planner.md @@ -176,6 +176,7 @@ queue depth, build activity and hitch observability. | `src/render/m2/m2_build_batch_planner.gd` | Pure limit/count/cursor planning | | `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_animated_instance_materializer.gd` | Planned animated-slice duplication, playback startup and attachment | | `src/render/m2/m2_placement_grouper.gd` | Produces grouped transform arrays | | `src/tools/verify_m2_build_batch_planner.gd` | Formula, source and timing regression | diff --git a/docs/modules/world-renderer.md b/docs/modules/world-renderer.md index 5072bef..7f79f97 100644 --- a/docs/modules/world-renderer.md +++ b/docs/modules/world-renderer.md @@ -136,6 +136,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state | `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 | +| `M2AnimatedInstanceMaterializer` | Internal M2 scene-materialization service | Duplicates ordered animated instances, applies render settings, starts playback and attaches a non-empty batch | Renderer main thread; stateless after each call | Invalid/empty input or all failed duplicates returns empty | | `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 | @@ -188,7 +189,8 @@ 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 animated M2 playback | Prototype/instance native animators, path/index and player inventory | Materializer/finalizer / `M2AnimationPlaybackController` | Native and imported animation state | Borrowed Nodes; detached optional diagnostics | One duplicated instance startup | +| Internal animated M2 materialization | Parent, prototype, ordered transform slice and render settings | Loader / `M2AnimatedInstanceMaterializer` | Attached animated batch and indexed diagnostics | Parent owns batch; caller owns detached diagnostics | One main-thread build batch | | 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 | @@ -547,6 +549,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | 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 animated instance materializer | Implemented extraction | Synthetic order/transform/render/playback/attachment/source/timing contract | Asset-backed traversal/visual/leak/p95/p99 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 | @@ -611,6 +614,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | `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_animated_instance_materializer.gd` | Animated duplicate/render/playback startup and non-empty batch attachment | | `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 | @@ -635,6 +639,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | `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_animated_instance_materializer.gd` | Animated M2 order/render/playback/attachment/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 | diff --git a/src/render/m2/m2_animated_instance_materializer.gd b/src/render/m2/m2_animated_instance_materializer.gd new file mode 100644 index 0000000..ff7f145 --- /dev/null +++ b/src/render/m2/m2_animated_instance_materializer.gd @@ -0,0 +1,120 @@ +class_name M2AnimatedInstanceMaterializer +extends RefCounted + +## Duplicates one ordered animated M2 batch, applies instance render settings, +## starts playback and attaches a non-empty batch to the supplied parent. +## All calls mutate Godot scene objects and therefore belong on the main thread. + +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" +) + +var _animated_scene_finalizer := M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new() +var _animation_playback_controller := M2_ANIMATION_PLAYBACK_CONTROLLER_SCRIPT.new() + + +## Returns the attached batch root and detached diagnostic entries. Invalid or +## empty input, and a batch whose duplicates all fail, returns an empty result. +## The caller owns the returned/attached batch through `m2_parent_root`. +func materialize_batch( + m2_parent_root: Node3D, + relative_path: String, + prototype: Node3D, + transforms: Array, + start_index: int, + instance_count: int, + batch_serial: int, + visibility_range_end: float, + visibility_range_margin: float, + cast_shadows: bool, + native_animator_script: Script, + collect_native_diagnostics: bool +) -> Dictionary: + if ( + m2_parent_root == null + or transforms.is_empty() + or instance_count <= 0 + or prototype == null + ): + return {} + + var model_name := relative_path.get_file().get_basename() + var batch_root := Node3D.new() + batch_root.name = "%s_anim_%d" % [model_name, batch_serial] + var diagnostic_entries: Array[Dictionary] = [] + for batch_offset in instance_count: + var instance_index := start_index + batch_offset + var instance := prototype.duplicate( + Node.DUPLICATE_SIGNALS | Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS + ) as Node3D + if instance == null: + continue + instance.name = "%s_%d" % [model_name, instance_index] + instance.transform = transforms[instance_index] + _apply_visibility_range_recursive( + instance, + visibility_range_end, + visibility_range_margin + ) + _apply_shadow_cast_recursive(instance, cast_shadows) + _animation_playback_controller.copy_native_animator_data( + prototype, + instance, + native_animator_script + ) + batch_root.add_child(instance) + var animation_players := _animated_scene_finalizer.animation_players_in_subtree( + instance + ) + var native_diagnostics := _animation_playback_controller.start_instance_playback( + instance, + relative_path, + instance_index, + native_animator_script, + animation_players, + collect_native_diagnostics + ) + for native_diagnostic in native_diagnostics: + diagnostic_entries.append({ + "instance_index": instance_index, + "state": native_diagnostic, + }) + + if batch_root.get_child_count() <= 0: + batch_root.queue_free() + return {} + m2_parent_root.add_child(batch_root) + return { + "batch_root": batch_root, + "native_diagnostics": diagnostic_entries, + } + + +func _apply_visibility_range_recursive( + node: Node, + range_end: float, + range_end_margin: float +) -> void: + if range_end <= 0.0: + return + if node is GeometryInstance3D: + var geometry := node as GeometryInstance3D + geometry.visibility_range_end = range_end + geometry.visibility_range_end_margin = range_end_margin + for child in node.get_children(): + _apply_visibility_range_recursive(child, range_end, range_end_margin) + + +func _apply_shadow_cast_recursive(node: Node, cast_shadows: bool) -> void: + if node is GeometryInstance3D: + var geometry := node as GeometryInstance3D + geometry.cast_shadow = ( + GeometryInstance3D.SHADOW_CASTING_SETTING_ON + if cast_shadows + else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF + ) + for child in node.get_children(): + _apply_shadow_cast_recursive(child, cast_shadows) diff --git a/src/render/m2/m2_animated_instance_materializer.gd.uid b/src/render/m2/m2_animated_instance_materializer.gd.uid new file mode 100644 index 0000000..1cf42cb --- /dev/null +++ b/src/render/m2/m2_animated_instance_materializer.gd.uid @@ -0,0 +1 @@ +uid://dkgy3rvsmlkvm diff --git a/src/render/m2/m2_animation_playback_controller.gd b/src/render/m2/m2_animation_playback_controller.gd index 21e45fd..7901f61 100644 --- a/src/render/m2/m2_animation_playback_controller.gd +++ b/src/render/m2/m2_animation_playback_controller.gd @@ -2,8 +2,8 @@ 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. +## runtime fields of exact-script native animators. The materializer owns +## instance duplication/attachment; its caller owns diagnostic log formatting. ## Copies the five historical runtime fields between matching native animators diff --git a/src/scenes/streaming/streaming_world_loader.gd b/src/scenes/streaming/streaming_world_loader.gd index adc2806..510535b 100644 --- a/src/scenes/streaming/streaming_world_loader.gd +++ b/src/scenes/streaming/streaming_world_loader.gd @@ -78,8 +78,8 @@ 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_ANIMATED_INSTANCE_MATERIALIZER_SCRIPT := preload( + "res://src/render/m2/m2_animated_instance_materializer.gd" ) const M2_MESH_RESOURCE_CACHE_STATE_SCRIPT := preload( "res://src/render/m2/m2_mesh_resource_cache_state.gd" @@ -281,7 +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 _m2_animated_instance_materializer := M2_ANIMATED_INSTANCE_MATERIALIZER_SCRIPT.new() var _wmo_build_jobs: Dictionary = {} var _wmo_build_queue: Array = [] var _tile_loading_tasks: Dictionary = {} @@ -4429,53 +4429,36 @@ func _materialize_m2_animated_batch( start: int, count: int, serial: int) -> void: - if transforms.is_empty() or count <= 0 or prototype == null: + var materialization := _m2_animated_instance_materializer.materialize_batch( + m2_root, + rel_path, + prototype, + transforms, + start, + count, + serial, + m2_visibility_range, + CHUNK_SIZE, + m2_cast_shadows, + M2_NATIVE_ANIMATOR_SCRIPT, + debug_streaming + ) + if materialization.is_empty(): return - - var batch_root := Node3D.new() - batch_root.name = "%s_anim_%d" % [rel_path.get_file().get_basename(), serial] - for i in count: - var instance := prototype.duplicate(Node.DUPLICATE_SIGNALS | Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS) as Node3D - if instance == null: - continue - instance.name = "%s_%d" % [rel_path.get_file().get_basename(), start + i] - instance.transform = transforms[start + i] - _apply_visibility_range_recursive(instance, m2_visibility_range) - _apply_shadow_cast_recursive(instance, m2_cast_shadows) - _m2_animation_playback_controller.copy_native_animator_data( - prototype, - instance, - M2_NATIVE_ANIMATOR_SCRIPT - ) - batch_root.add_child(instance) - 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() - return - m2_root.add_child(batch_root) + if debug_streaming: + for diagnostic_entry in materialization.get("native_diagnostics", []): + var state: Dictionary = diagnostic_entry.get("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), + int(diagnostic_entry.get("instance_index", -1)), + 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 batch_root: Node3D = materialization.get("batch_root", null) _set_editor_owner_recursive(batch_root) diff --git a/src/tools/verify_m2_animated_instance_materializer.gd b/src/tools/verify_m2_animated_instance_materializer.gd new file mode 100644 index 0000000..acecf44 --- /dev/null +++ b/src/tools/verify_m2_animated_instance_materializer.gd @@ -0,0 +1,277 @@ +extends SceneTree + +## Synthetic batch duplication, render-property, playback, ownership-boundary +## and bounded-timing regression for animated M2 instance materialization. + +const MATERIALIZER_SCRIPT := preload( + "res://src/render/m2/m2_animated_instance_materializer.gd" +) +const NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd") +const MATERIALIZER_PATH := "res://src/render/m2/m2_animated_instance_materializer.gd" +const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" + + +func _initialize() -> void: + var failures: Array[String] = [] + _verify_batch_contract(failures) + _verify_invalid_inputs(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_INSTANCE_MATERIALIZER: %s" % failure) + quit(1) + return + print( + "M2_ANIMATED_INSTANCE_MATERIALIZER PASS cases=12 instances=1000 elapsed_ms=%.3f" + % elapsed_milliseconds + ) + quit(0) + + +func _verify_batch_contract(failures: Array[String]) -> void: + var materializer = MATERIALIZER_SCRIPT.new() + var parent_root := Node3D.new() + get_root().add_child(parent_root) + var prototype := _make_animated_prototype() + var transforms: Array = [ + Transform3D(Basis.IDENTITY, Vector3.ZERO), + Transform3D(Basis.IDENTITY, Vector3(1.0, 2.0, 3.0)), + Transform3D(Basis.IDENTITY, Vector3(4.0, 5.0, 6.0)), + ] + var result: Dictionary = materializer.materialize_batch( + parent_root, + "World\\Doodads\\AnimatedTree.m2", + prototype, + transforms, + 1, + 2, + 7, + 345.0, + 16.0, + false, + NATIVE_ANIMATOR_SCRIPT, + true + ) + _expect_false(result.is_empty(), "valid batch returned", failures) + var batch_root: Node3D = result.get("batch_root", null) + _expect_same(batch_root.get_parent(), parent_root, "batch attached to supplied parent", failures) + _expect_string_equal(batch_root.name, "AnimatedTree_anim_7", "batch name", failures) + _expect_equal(batch_root.get_child_count(), 2, "ordered duplicate count", failures) + for batch_offset in 2: + var instance := batch_root.get_child(batch_offset) as Node3D + var instance_index := batch_offset + 1 + _expect_string_equal(instance.name, "AnimatedTree_%d" % instance_index, "instance name", failures) + _expect_true(instance.transform == transforms[instance_index], "instance transform", failures) + var geometry := instance.get_node("Branch/Geometry") as GeometryInstance3D + _expect_float_equal(geometry.visibility_range_end, 345.0, "recursive visibility end", failures) + _expect_float_equal(geometry.visibility_range_end_margin, 16.0, "recursive visibility margin", failures) + _expect_equal( + geometry.cast_shadow, + GeometryInstance3D.SHADOW_CASTING_SETTING_OFF, + "recursive shadow mode", + failures + ) + var player := instance.get_node("AnimationPlayer") as AnimationPlayer + _expect_string_equal(player.current_animation, "Stand", "playback started", failures) + var animator: Node = instance.get_node("Branch/NativeAnimator") + _expect_same( + animator.bones, + prototype.get_node("Branch/NativeAnimator").bones, + "native fields copied by reference", + failures + ) + + var diagnostic_entries: Array = result.get("native_diagnostics", []) + _expect_equal(diagnostic_entries.size(), 2, "one native diagnostic per instance", failures) + for diagnostic_offset in diagnostic_entries.size(): + var entry: Dictionary = diagnostic_entries[diagnostic_offset] + _expect_equal( + int(entry.get("instance_index", -1)), + diagnostic_offset + 1, + "diagnostic index", + failures + ) + _expect_equal( + int((entry.get("state", {}) as Dictionary).get("bones", 0)), + 1, + "diagnostic state", + failures + ) + + var source_geometry := prototype.get_node("Branch/Geometry") as GeometryInstance3D + _expect_float_equal(source_geometry.visibility_range_end, 0.0, "prototype render state unchanged", failures) + prototype.free() + parent_root.free() + + +func _verify_invalid_inputs(failures: Array[String]) -> void: + var materializer = MATERIALIZER_SCRIPT.new() + var prototype := Node3D.new() + var parent_root := Node3D.new() + _expect_true( + materializer.materialize_batch( + null, "a.m2", prototype, [Transform3D.IDENTITY], 0, 1, 0, + 0.0, 0.0, true, NATIVE_ANIMATOR_SCRIPT, false + ).is_empty(), + "null parent rejected", + failures + ) + _expect_true( + materializer.materialize_batch( + parent_root, "a.m2", prototype, [], 0, 1, 0, + 0.0, 0.0, true, NATIVE_ANIMATOR_SCRIPT, false + ).is_empty(), + "empty transforms rejected", + failures + ) + _expect_true( + materializer.materialize_batch( + parent_root, "a.m2", prototype, [Transform3D.IDENTITY], 0, 0, 0, + 0.0, 0.0, true, NATIVE_ANIMATOR_SCRIPT, false + ).is_empty(), + "zero count rejected", + failures + ) + _expect_equal(parent_root.get_child_count(), 0, "invalid input has no attachment", failures) + prototype.free() + parent_root.free() + + +func _verify_ownership_boundaries(failures: Array[String]) -> void: + var materializer_source := FileAccess.get_file_as_string(MATERIALIZER_PATH) + var loader_source := FileAccess.get_file_as_string(LOADER_PATH) + _expect_true( + loader_source.contains("M2_ANIMATED_INSTANCE_MATERIALIZER_SCRIPT.new()"), + "loader composes materializer", + failures + ) + _expect_equal( + loader_source.count("_m2_animated_instance_materializer.materialize_batch("), + 1, + "loader delegates once", + failures + ) + for retained_loader_rule in [ + "_set_editor_owner_recursive(batch_root)", + "M2_NATIVE_ANIMATOR path=%s instance=%d", + "_normalize_m2_rel_path(rel_path)", + ]: + _expect_true( + loader_source.contains(retained_loader_rule), + "loader retains %s" % retained_loader_rule, + failures + ) + for removed_loader_rule in [ + "prototype.duplicate(Node.DUPLICATE_SIGNALS | Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS)", + "_m2_animation_playback_controller", + ]: + _expect_false( + loader_source.contains(removed_loader_rule), + "loader releases %s" % removed_loader_rule, + failures + ) + for required_materializer_rule in [ + "Node.DUPLICATE_SIGNALS | Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS", + "_animation_playback_controller.copy_native_animator_data(", + "_animation_playback_controller.start_instance_playback(", + "m2_parent_root.add_child(batch_root)", + ]: + _expect_true( + materializer_source.contains(required_materializer_rule), + "materializer owns %s" % required_materializer_rule, + failures + ) + for forbidden_dependency in [ + "ResourceLoader.", + "FileAccess.", + "WorkerThreadPool.", + "MultiMesh", + ".owner =", + "_m2_build_jobs", + "_render_budget_scheduler", + ]: + _expect_false( + materializer_source.contains(forbidden_dependency), + "materializer omits %s ownership" % forbidden_dependency, + failures + ) + + +func _verify_bounded_timing(failures: Array[String]) -> float: + var materializer = MATERIALIZER_SCRIPT.new() + var parent_root := Node3D.new() + get_root().add_child(parent_root) + var prototype := Node3D.new() + var transforms: Array = [] + for instance_index in 1000: + transforms.append( + Transform3D(Basis.IDENTITY, Vector3(float(instance_index), 0.0, 0.0)) + ) + var started_microseconds := Time.get_ticks_usec() + var result: Dictionary = materializer.materialize_batch( + parent_root, "world/timing.m2", prototype, transforms, 0, 1000, 1, + 0.0, 0.0, true, NATIVE_ANIMATOR_SCRIPT, false + ) + var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0 + var batch_root := result.get("batch_root", null) as Node3D + _expect_equal(batch_root.get_child_count(), 1000, "timing batch complete", failures) + _expect_true(elapsed_milliseconds < 1000.0, "1000 instances under 1 second", failures) + prototype.free() + parent_root.free() + return elapsed_milliseconds + + +func _make_animated_prototype() -> Node3D: + var prototype := Node3D.new() + var branch := Node3D.new() + branch.name = "Branch" + prototype.add_child(branch) + var geometry := MeshInstance3D.new() + geometry.name = "Geometry" + branch.add_child(geometry) + var native_animator: Node = NATIVE_ANIMATOR_SCRIPT.new() + native_animator.name = "NativeAnimator" + native_animator.mesh_instance_path = NodePath("../Geometry") + native_animator.bones = [{"parent": -1}] + native_animator.surfaces = [{"vertices": PackedVector3Array()}] + native_animator.animation_length = 8.0 + branch.add_child(native_animator) + var player := AnimationPlayer.new() + player.name = "AnimationPlayer" + var library := AnimationLibrary.new() + var stand := Animation.new() + stand.length = 2.0 + library.add_animation("Stand", stand) + player.add_animation_library("", library) + prototype.add_child(player) + return prototype + + +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) diff --git a/src/tools/verify_m2_animated_instance_materializer.gd.uid b/src/tools/verify_m2_animated_instance_materializer.gd.uid new file mode 100644 index 0000000..6db7cb5 --- /dev/null +++ b/src/tools/verify_m2_animated_instance_materializer.gd.uid @@ -0,0 +1 @@ +uid://kxr85qkrfnhb diff --git a/src/tools/verify_m2_animated_scene_finalizer.gd b/src/tools/verify_m2_animated_scene_finalizer.gd index 60ea222..3ccb171 100644 --- a/src/tools/verify_m2_animated_scene_finalizer.gd +++ b/src/tools/verify_m2_animated_scene_finalizer.gd @@ -5,6 +5,7 @@ extends SceneTree const FINALIZER_SCRIPT := preload("res://src/render/m2/m2_animated_scene_finalizer.gd") const FINALIZER_PATH := "res://src/render/m2/m2_animated_scene_finalizer.gd" +const MATERIALIZER_PATH := "res://src/render/m2/m2_animated_instance_materializer.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" @@ -115,6 +116,7 @@ func _verify_material_mapping(failures: Array[String]) -> void: func _verify_ownership_boundaries(failures: Array[String]) -> void: var finalizer_source := FileAccess.get_file_as_string(FINALIZER_PATH) + var materializer_source := FileAccess.get_file_as_string(MATERIALIZER_PATH) var loader_source := FileAccess.get_file_as_string(LOADER_PATH) _expect_true(loader_source.contains("M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new()"), "loader composes finalizer", failures) for removed_loader_function in [ @@ -128,9 +130,14 @@ 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(", ]: _expect_equal(loader_source.count(delegated_call), 1, "single loader delegation: %s" % delegated_call, failures) + _expect_equal( + materializer_source.count("_animated_scene_finalizer.animation_players_in_subtree("), + 1, + "single materializer player-inventory delegation", + failures + ) for retained_loader_rule in [ "ResourceLoader.load_threaded_get(path)", "RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE", diff --git a/src/tools/verify_m2_animation_playback_controller.gd b/src/tools/verify_m2_animation_playback_controller.gd index bfd594b..8a900b7 100644 --- a/src/tools/verify_m2_animation_playback_controller.gd +++ b/src/tools/verify_m2_animation_playback_controller.gd @@ -6,6 +6,7 @@ extends SceneTree 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 MATERIALIZER_PATH := "res://src/render/m2/m2_animated_instance_materializer.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" @@ -143,8 +144,9 @@ func _verify_native_copy_start_and_diagnostics(failures: Array[String]) -> void: func _verify_ownership_boundaries(failures: Array[String]) -> void: var controller_source := FileAccess.get_file_as_string(CONTROLLER_PATH) + var materializer_source := FileAccess.get_file_as_string(MATERIALIZER_PATH) var loader_source := FileAccess.get_file_as_string(LOADER_PATH) - _expect_true(loader_source.contains("M2_ANIMATION_PLAYBACK_CONTROLLER_SCRIPT.new()"), "loader composes playback controller", failures) + _expect_true(materializer_source.contains("M2_ANIMATION_PLAYBACK_CONTROLLER_SCRIPT.new()"), "materializer composes playback controller", failures) for removed_loader_function in [ "func _start_m2_animations(", "func _copy_m2_native_animator_data(", @@ -152,13 +154,15 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void: "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)", + _expect_equal(materializer_source.count("_animation_playback_controller.copy_native_animator_data("), 1, "native copy delegates once", failures) + _expect_equal(materializer_source.count("_animation_playback_controller.start_instance_playback("), 1, "playback delegates once", failures) + for retained_materializer_rule in [ + "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)", + "_apply_shadow_cast_recursive(instance, cast_shadows)", + ]: + _expect_true(materializer_source.contains(retained_materializer_rule), "materializer retains %s" % retained_materializer_rule, failures) + for retained_loader_rule in [ "_set_editor_owner_recursive(batch_root)", "M2_NATIVE_ANIMATOR path=%s instance=%d", ]: diff --git a/targets/03-renderer-facade.md b/targets/03-renderer-facade.md index 8f278d3..824928c 100644 --- a/targets/03-renderer-facade.md +++ b/targets/03-renderer-facade.md @@ -216,6 +216,14 @@ Runtime и Editor используют facade; planner/scheduler тестиру 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. + M2 animated instance materializer passed `cases=12 instances=1000 + elapsed_ms=9.267` with exact duplicate flags, ordered names/transforms, + recursive visibility margin/shadows, imported playback, native reference copy, + indexed diagnostics, prototype isolation and attachment/source boundaries. All + 50 autonomous headless regressions and checkpoint dry-run `7/7` passed; the + proprietary ADT probe remained unavailable. Documentation and coordination + gates passed; internal-access inventory remains `30` while the loader adapter + now retains only diagnostic formatting and Editor ownership for this batch. M02 terrain-query regression remained green (13 pre-existing expired M00 claim warnings). - Fidelity comparison: all 16 historical operation limits and drain sites were @@ -356,6 +364,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру 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, + M2 animated instance materializer and loader log/Editor-owner adapter, 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 @@ -374,6 +383,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру traversal plus refresh/rebuild/fallback, raw native M2 I/O and prototype/ negative cache state are separated, but synchronous parsing, ResourceLoader, animation request state and Mesh adoption remain in the loader call path; raw + animated-instance materialization is now separated but remains synchronous + main-thread work; asset-backed traversal/visual/leak/p95/p99 evidence is pending; WMO ResourceLoader/FileAccess I/O, live fallback and group materialization remain in the loader; asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending;