diff --git a/RENDER.md b/RENDER.md index abf9dd0..6e3c752 100644 --- a/RENDER.md +++ b/RENDER.md @@ -42,6 +42,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g - `src/render/m2/m2_static_batch_materializer.gd` - main-thread static M2 MultiMesh construction, render settings and attachment. - `src/render/m2/m2_build_queue.gd` / `m2_build_job.gd` - typed pending M2 jobs, FIFO/stale tile keys, grouped-transform references and progress cursors without engine destruction. - `src/render/m2/m2_build_dispatch_planner.gd` - pure animation/static wait, materialization and missing-model advance decision. +- `src/render/m2/m2_build_resource_snapshot.gd` - typed per-step animated/static resource observations without engine destruction. - `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. @@ -1071,6 +1072,18 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe' - Resource lookup/request order, queue/cursor adoption, materialization, permits, Node lifetime, cache formats, profiles and visible rules remain unchanged. +## 2026-07-18 M2 Build Resource Snapshot Extraction + +- `M2BuildResourceSnapshot` now carries one build-step normalized path, optional + animated prototype, pending-animation state, optional static Mesh and terminal + missing-model state through typed accessors. +- Animation observation is captured first. Static observation is adopted only + for a positive non-animated batch after animation is no longer pending. +- The dispatch planner consumes the snapshot; the loader borrows the selected + prototype/Mesh for the existing materializer calls. +- ResourceLoader/cache requests, permit/cursor transitions, engine lifetime, + cache formats, profiles and visible rules remain unchanged. + ## 2026-07-17 M2 Runtime Mesh Rebuild Classifier Extraction - `M2RuntimeMeshRebuildClassifier` now owns the memoized decision used when a diff --git a/docs/modules/README.md b/docs/modules/README.md index ff04e3d..375f0a7 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -22,6 +22,7 @@ | M2 placement grouper | Implemented extraction | [`m2-placement-grouper.md`](m2-placement-grouper.md) | | M2 build batch planner | Implemented extraction | [`m2-build-batch-planner.md`](m2-build-batch-planner.md) | | M2 build dispatch planner | Implemented extraction | [`m2-build-dispatch-planner.md`](m2-build-dispatch-planner.md) | +| M2 build resource snapshot | Implemented extraction | [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md) | | M2 build queue | Implemented extraction | [`m2-build-queue.md`](m2-build-queue.md) | | M2 static batch materializer | Implemented extraction | [`m2-static-batch-materializer.md`](m2-static-batch-materializer.md) | | M2 runtime mesh rebuild classifier | Implemented extraction | [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md) | diff --git a/docs/modules/m2-build-dispatch-planner.md b/docs/modules/m2-build-dispatch-planner.md index e4682e5..e685b37 100644 --- a/docs/modules/m2-build-dispatch-planner.md +++ b/docs/modules/m2-build-dispatch-planner.md @@ -50,17 +50,14 @@ SceneTree, files, workers, mutexes, gameplay, network and Editor APIs are forbid | `ACTION_WAIT_FOR_STATIC_MESH` | Constant | Static resource is unresolved and queue must rotate | Immutable | None | | `ACTION_MATERIALIZE_STATIC` | Constant | Build the selected slice from prepared static Mesh | Immutable | None | | `ACTION_ADVANCE_WITHOUT_MATERIALIZATION` | Constant | Empty or terminally missing slice advances without a Node | Immutable | None | -| `plan_step(batch_count, animation_pending, has_animated, has_static_mesh, static_missing)` | Pure query | Return action, queue-rotation and serial-transition values | Any thread; call-local result | Raw booleans/count are accepted; non-positive count advances | +| `plan_step(batch_count, resource_snapshot)` | Pure query | Return action, queue-rotation and serial-transition values | Any thread; call-local result | Null snapshot waits for static Mesh; non-positive count advances | ## Inputs and outputs | Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | |---|---|---|---|---|---| | Input | Planned batch count | `M2BuildBatchPlanner` through loader | Dispatch planner | Integer copy | One call | -| Input | Animation request pending | Animation pipeline through loader | Dispatch planner | Boolean copy | One call | -| Input | Animated prototype available | Prototype cache/native loader adapter | Dispatch planner | Boolean copy; no Node reference | One call | -| Input | Static Mesh available | Mesh cache/request adapter | Dispatch planner | Boolean copy; no Mesh reference | One call | -| Input | Static model terminally missing | Prototype cache through loader | Dispatch planner | Boolean copy | One call | +| Input | Typed animated/static availability, pending and missing observations | Loader / `M2BuildResourceSnapshot` | Dispatch planner | Borrowed snapshot; engine references are not read | One call | | Output | Action | Dispatch planner | Loader materializer/wait branch | Immutable StringName | One operation | | Output | `rotate_queue` | Dispatch planner | Loader queue adapter | Detached boolean | One operation | | Output | `increment_batch_serial` | Dispatch planner | Loader progress adapter | Detached boolean | One operation | @@ -116,7 +113,7 @@ sequenceDiagram L->>B: plan_batch L->>L: lookup/request static Mesh when required end - L->>D: plan_step(observed scalar state) + L->>D: plan_step(batch count, resource snapshot) D-->>L: action, rotate flag, serial flag alt wait action L->>Q: rotate_front @@ -138,6 +135,7 @@ flowchart TB Loader --> Queue[M2BuildQueue] Loader --> Caches[M2 cache/pipeline state] Loader --> Materializers[M2 materializers] + Dispatch --> Snapshot[M2BuildResourceSnapshot accessors] Dispatch --> Values[Integers + booleans + StringName] Dispatch -. no dependency .-> Engine[Node / Mesh / ResourceLoader] Dispatch -. no dependency .-> State[Queue / cache / scheduler state] @@ -146,6 +144,8 @@ flowchart TB ## Ownership, threading and resources - Planner owns call-local comparisons and the returned Dictionary only. +- `M2BuildResourceSnapshot` owns the typed call-local observation contract; + dispatch reads availability/pending/missing accessors but no engine references. - Loader owns resource lookup/request order, action execution and permit use. - `M2BuildQueue` owns pending jobs, FIFO keys and progress cursors. - Materializers own main-thread scene construction under loader-owned roots. @@ -190,9 +190,9 @@ remain unchanged. ## Extension points -A later package may move resource observation/request execution behind a typed -service while preserving this scalar action contract. Generic callbacks, signals -and a shared state-machine framework remain intentionally excluded. +A later package may move resource observation/request execution behind a service +that produces `M2BuildResourceSnapshot`. Generic callbacks, signals and a shared +state-machine framework remain intentionally excluded. ## Capability status @@ -205,7 +205,6 @@ and a shared state-machine framework remain intentionally excluded. ## Known gaps and risks -- Planner consumes raw booleans rather than a typed resource snapshot. - Loader still performs synchronous/native prototype attempts and static request I/O. - Synthetic timing does not measure ResourceLoader, Node or GPU work. - Private traversal, leak, visual and p95/p99 evidence remains unavailable. @@ -215,6 +214,7 @@ and a shared state-machine framework remain intentionally excluded. | Path | Responsibility | |---|---| | `src/render/m2/m2_build_dispatch_planner.gd` | Pure action priority and transition plan | +| `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step resource observation contract | | `src/scenes/streaming/streaming_world_loader.gd` | Resource observation, action execution, permits and engine lifetime | | `src/render/m2/m2_build_batch_planner.gd` | Batch count and cursor plan | | `src/render/m2/m2_build_queue.gd` | Pending jobs, FIFO and cursor ownership | @@ -223,6 +223,7 @@ and a shared state-machine framework remain intentionally excluded. ## Related decisions and references - [`m2-build-batch-planner.md`](m2-build-batch-planner.md) +- [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md) - [`m2-build-queue.md`](m2-build-queue.md) - [`m2-static-batch-materializer.md`](m2-static-batch-materializer.md) - [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.md) diff --git a/docs/modules/m2-build-queue.md b/docs/modules/m2-build-queue.md index 5b18455..3398e80 100644 --- a/docs/modules/m2-build-queue.md +++ b/docs/modules/m2-build-queue.md @@ -257,6 +257,7 @@ queue base, signals and callbacks are intentionally excluded. - [`m2-build-batch-planner.md`](m2-build-batch-planner.md) - [`m2-build-dispatch-planner.md`](m2-build-dispatch-planner.md) +- [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md) - [`m2-static-batch-materializer.md`](m2-static-batch-materializer.md) - [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.md) - [`world-renderer.md`](world-renderer.md) diff --git a/docs/modules/m2-build-resource-snapshot.md b/docs/modules/m2-build-resource-snapshot.md new file mode 100644 index 0000000..e27bd1d --- /dev/null +++ b/docs/modules/m2-build-resource-snapshot.md @@ -0,0 +1,226 @@ +# M2 Build Resource Snapshot + +## Metadata + +| Field | Value | +|---|---| +| Status | Implemented extraction | +| Target/work package | M03 / `M03-RND-M2-BUILD-RESOURCE-SNAPSHOT-001` | +| Owners | Per-build-step observed M2 resource references and outcome flags | +| Last verified | Worktree `work/sindo-main-codex/m03-m2-build-resource-snapshot`, 2026-07-18 | +| Profiles/capabilities | Existing static MultiMesh and animated-instance build paths | + +## Purpose + +Carry the resource observations for one M2 build operation as a typed contract: +normalized path, optional animated prototype, pending-animation state, optional +static Mesh and terminal missing-model state. The snapshot never owns engine lifetime. + +## Non-goals + +- Locate, request, load, validate or cache M2 resources. +- Decide dispatch action, batch count, queue order or permit consumption. +- Create, attach or free Nodes, Meshes, MultiMeshes or animated instances. +- Change animation allowlists, native candidate rules or cache paths/formats. +- Persist observations beyond one build operation. + +## Context and boundaries + +```mermaid +flowchart LR + Loader[StreamingWorldLoader observations] --> Snapshot[M2BuildResourceSnapshot] + Snapshot --> Dispatch[M2BuildDispatchPlanner] + Snapshot --> Loader + Loader --> Animated[M2AnimatedInstanceMaterializer] + Loader --> Static[M2StaticBatchMaterializer] +``` + +The snapshot depends only on `RefCounted`, String, booleans and borrowed Node3D/ +Mesh references. ResourceLoader, cache/pipeline state, SceneTree mutation, +RenderingServer/RIDs, files, workers, mutexes, gameplay, network and Editor APIs +are forbidden. + +## Public API + +| Symbol | Kind | Purpose | Thread/lifetime | Errors | +|---|---|---|---|---| +| Constructor `(normalized_path, animated_prototype, animation_pending)` | Command | Capture first-phase animation observation | Main thread; one build operation | Values retained exactly | +| `normalized_relative_path()` | Query | Return observed normalized path | Snapshot lifetime | None | +| `animated_prototype()` | Query | Borrow exact animated prototype | Main thread; no ownership transfer | May be null | +| `has_animated_prototype()` | Query | Report prototype availability | Any serialized caller | None | +| `animation_request_pending()` | Query | Report pending animation request | Any serialized caller | None | +| `adopt_static_observation(mesh, missing)` | Command | Replace second-phase static observation | Main thread | Values retained exactly, including contradictory inputs | +| `static_mesh()` | Query | Borrow exact prepared Mesh | Main thread; no ownership transfer | May be null | +| `has_static_mesh()` | Query | Report static Mesh availability | Any serialized caller | None | +| `static_model_missing()` | Query | Report terminal missing outcome | Any serialized caller | None | +| `diagnostic_snapshot()` | Query | Return detached path/availability flags | Any serialized caller | Omits engine references | + +## Inputs and outputs + +| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | +|---|---|---|---|---|---| +| Input | Normalized relative path | Loader path adapter | Snapshot/diagnostics | Copied String | Snapshot lifetime | +| Input | Optional animated prototype | Prototype/native animation path | Snapshot/materializer adapter | Borrowed exact Node3D reference | One build operation | +| Input | Animation request pending | Animation pipeline state | Snapshot/dispatch planner | Boolean copy | One build operation | +| Input | Optional prepared static Mesh | Mesh cache/request path | Snapshot/materializer adapter | Borrowed exact Mesh reference | One build operation | +| Input | Static model terminally missing | Prototype cache state | Snapshot/dispatch planner | Boolean copy | One build operation | +| Output | Availability/pending/missing values | Snapshot | Dispatch planner | Scalar reads | One dispatch plan | +| Output | Borrowed prototype or Mesh | Snapshot | Loader materializer adapter | No ownership transfer | One materialization call | +| Output | Detached diagnostics | Snapshot | Tests/future metrics | Caller-owned Dictionary | Call result | + +The snapshot is released after the build-loop operation. Releasing or replacing +it never frees the borrowed Node3D or Mesh. + +## Data flow + +```mermaid +flowchart TD + Path[Normalized M2 path] --> Construct[Create snapshot] + Animated[Animated prototype or null] --> Construct + Pending[Animation request pending] --> Construct + Construct --> PendingCheck{Animation pending?} + PendingCheck -->|yes| Dispatch[Dispatch planner] + PendingCheck -->|no| Batch[Batch planner] + Batch --> StaticNeeded{Positive static batch?} + StaticNeeded -->|yes| Adopt[Adopt Mesh and missing observation] + StaticNeeded -->|no| Dispatch + Adopt --> Dispatch + Dispatch --> Borrow[Loader borrows selected resource] +``` + +## Lifecycle/state + +```mermaid +stateDiagram-v2 + [*] --> AnimationObserved: construct + AnimationObserved --> AnimationObserved: animation pending or animated path + AnimationObserved --> StaticObserved: adopt static result + StaticObserved --> StaticObserved: replace static result + AnimationObserved --> Released: operation ends + StaticObserved --> Released: operation ends + Released --> [*] +``` + +## Main sequence + +```mermaid +sequenceDiagram + participant L as StreamingWorldLoader + participant S as M2BuildResourceSnapshot + participant B as M2BuildBatchPlanner + participant D as M2BuildDispatchPlanner + participant M as M2 materializer + L->>L: resolve/request animated prototype + L->>S: construct(path, prototype, pending) + alt animation not pending + L->>B: plan batch using snapshot availability + opt positive static batch + L->>L: lookup/request static Mesh + L->>S: adopt_static_observation + end + end + L->>D: plan_step(batch count, snapshot) + D-->>L: action plan + opt materialization action + L->>S: borrow prototype or Mesh + L->>M: materialize selected slice + end +``` + +## Dependency diagram + +```mermaid +flowchart TB + Loader[StreamingWorldLoader] --> Snapshot[M2BuildResourceSnapshot] + Dispatch[M2BuildDispatchPlanner] --> Snapshot + Snapshot --> Values[String and booleans] + Snapshot --> Borrowed[Borrowed Node3D and Mesh] + Snapshot -. no dependency .-> IO[ResourceLoader / FileAccess] + Snapshot -. no dependency .-> State[Cache / pipeline / queue / scheduler] + Snapshot -. no dependency .-> Mutation[SceneTree / RenderingServer] +``` + +## Ownership, threading and resources + +- Snapshot owns only copied scalar state and temporary references. +- Prototype/cache services retain resource state; loader owns observation order. +- Scene roots remain loader/tile-owned; Mesh lifetime follows cache/resource refs. +- Dispatch planner reads only snapshot accessors and never mutates the snapshot. +- Current construction/adoption occurs on renderer main thread. + +## Errors, cancellation and recovery + +| Failure/state | Detection | Behavior | Diagnostic | Recovery | +|---|---|---|---|---| +| Empty path | Exact String retained | No validation or hidden normalization | Contract fixture | Loader already normalizes input | +| Null prototype/Mesh | Availability accessor false | Dispatch uses pending/missing state | Contract fixture | Loader request/cache path retries | +| Mesh and missing both true | Exact adoption | Both retained; dispatch gives ready Mesh priority | Contradiction fixture | Loader normally prevents combination | +| Static observation replaced | Explicit adoption | Latest Mesh/missing values win | Replacement fixture | None required | +| Borrowed Node externally freed | Outside snapshot | Loader validity/lifecycle gates remain authoritative | Shutdown regressions | Tile may cancel/requeue | +| Tile cancellation | Outside snapshot | Ephemeral snapshot releases references | Queue/shutdown regressions | Eligible tile may rebuild | + +## Configuration and capabilities + +The snapshot adds no setting. Existing animation enable/allow/deny rules, batch +limits, cache paths, render settings and scheduler permits remain external. + +## Persistence, cache and migration + +No observation is serialized. No ADT/M2 cache version changes and no rebake or +migration are required. + +## Diagnostics and observability + +`diagnostic_snapshot()` exposes normalized path and four availability/outcome +flags without Node/Mesh references. The module emits no logs. Existing loader +queue/hitch metrics remain unchanged. + +## Verification + +- `verify_m2_build_resource_snapshot.gd` covers exact path/reference identity, + defaults, static adoption/replacement/clear, contradictory flags, detached + diagnostics, engine lifetime, loader/dispatch boundaries and bounded timing. +- Dispatch, queue, batch, materializer, cache/pipeline, shutdown, facade, + internal-access and checkpoint regressions protect adjacent behavior. +- Fidelity evidence is exact observation transfer only; no private asset or + original-client visual claim is made. + +## Extension points + +A later resource-observation service may produce this snapshot while moving +ResourceLoader/cache request ownership out of the loader. The value contract +must remain independent of that service and of generic callback frameworks. + +## Capability status + +| Capability | Status | Evidence | Gap/next step | +|---|---|---|---| +| Typed per-step M2 resource observation | Implemented extraction | Identity/adoption/lifetime/source/timing verifier | Asset-backed traversal pending | +| Dispatch decision | Implemented separately | Dispatch planner verifier | Asset-backed traversal pending | +| Resource lookup/request execution | Loader-owned | Cache/pipeline regressions | Service extraction pending | +| Engine lifetime/materialization | Loader/materializer-owned | Lifetime/materializer regressions | GPU/p95/p99 evidence pending | + +## Known gaps and risks + +- Static observation is mutable during its short two-phase construction. +- Raw Node3D/Mesh references remain necessary for current materializer APIs. +- Loader still owns synchronous/native prototype attempts and ResourceLoader requests. +- Synthetic timing does not measure resource, Node or GPU work. + +## Source map + +| Path | Responsibility | +|---|---| +| `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step resource observations and diagnostics | +| `src/render/m2/m2_build_dispatch_planner.gd` | Snapshot-to-action planning | +| `src/scenes/streaming/streaming_world_loader.gd` | Resource observation/request execution and materializer borrowing | +| `src/tools/verify_m2_build_resource_snapshot.gd` | Identity/adoption/lifetime/boundary/timing regression | + +## Related decisions and references + +- [`m2-build-dispatch-planner.md`](m2-build-dispatch-planner.md) +- [`m2-build-batch-planner.md`](m2-build-batch-planner.md) +- [`m2-build-queue.md`](m2-build-queue.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/world-renderer.md b/docs/modules/world-renderer.md index 80baa60..cca6440 100644 --- a/docs/modules/world-renderer.md +++ b/docs/modules/world-renderer.md @@ -56,6 +56,8 @@ flowchart LR M2Batch --> Loader Loader --> M2Dispatch[M2BuildDispatchPlanner] M2Dispatch --> Loader + Loader --> M2Resources[M2BuildResourceSnapshot] + M2Resources --> M2Dispatch Loader --> M2Queue[M2BuildQueue] M2Queue --> Loader Loader --> M2Static[M2StaticBatchMaterializer] @@ -139,6 +141,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state | `M2PlacementGrouper.group_placements` | Internal pure M2 service | Validates and groups ordered tile-local placement transforms by normalized path | Worker/main thread; stateless | Invalid variants/name IDs/empty paths are skipped | | `M2BuildBatchPlanner.plan_batch` | Internal pure M2 service | Selects static/animated batch count and next group cursor | Main/any thread; stateless | Non-positive selected limit clamps to one; empty range completes | | `M2BuildDispatchPlanner.plan_step` | Internal pure M2 service | Selects wait, animated/static materialization or no-Node advancement from observed resource state | Main/any thread; stateless | Pending animation has priority; unresolved static Mesh waits | +| `M2BuildResourceSnapshot` | Internal M2 value contract | Carries per-step normalized path, animated/static references and pending/missing observations | Renderer main thread; one build operation | Values retained exactly; release never frees resources | | `M2BuildQueue` / `M2BuildJob` | Internal M2 pending-state service | Own typed root/groups/cursor jobs and FIFO/stale tile keys | Renderer main thread; map session | Invalid enqueue rejected; stale keys drain independently of jobs | | `M2StaticBatchMaterializer.materialize_batch` | Internal M2 scene-materialization service | Builds and attaches one prepared-Mesh MultiMesh transform slice | Renderer main thread; stateless after each call | Invalid/empty input returns null; bounds are caller precondition | | `M2RuntimeMeshRebuildClassifier` | Internal memoized M2 service | Detects billboard/UV-rotation metadata requiring stale cached-mesh rebuild | Renderer main thread; cached until reset | Invalid variants/indices skipped; first path decision wins | @@ -189,6 +192,7 @@ loader configuration remains transitional composition data, not a caller API. | Internal grouping | Tile origin, M2 names and placements | Loader / `M2PlacementGrouper` | Loader worker result/build job | Fresh Dictionary/Transform3D arrays | One grouping task | | Internal batch plan | Transform count/offset, path kind and limits | Loader / `M2BuildBatchPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One build operation | | Internal M2 dispatch plan | Batch count and observed animation/static resource state | Loader / `M2BuildDispatchPlanner` | Loader queue/materializer/progress adapter | Fresh scalar/action Dictionary | One build operation | +| Internal M2 resource observation | Normalized path, optional prototype/Mesh and pending/missing flags | Loader / `M2BuildResourceSnapshot` | Dispatch planner and loader materializer adapter | Snapshot borrows exact engine references | One build operation | | Internal M2 pending build | Tile key, M2 root, grouped transforms and cursors | Loader / `M2BuildQueue` | Loader readiness, planner and materializer adapters | Queue-owned job/keys and strong references | Until finish/cancel/clear/replacement | | Internal static M2 materialization | Parent, prepared Mesh, ordered transform slice and render settings | Loader / `M2StaticBatchMaterializer` | Attached MultiMeshInstance3D | Parent owns node/MultiMesh; exact Mesh reference retained | One main-thread build batch | | Internal WMO placement | Path, MODF placement, tile/index | Loader / `WmoPlacementResolver` | WMO caches, registry and three instance adapters | Value-only String/Transform3D | Lookup/placement lifetime | @@ -253,7 +257,8 @@ flowchart TD M2Grouper --> M2Batch[M2BuildBatchPlanner] M2Grouper --> M2Queue[M2BuildQueue] M2Queue --> M2Batch - M2Batch --> M2Dispatch[M2BuildDispatchPlanner] + M2Batch --> M2Resources[M2BuildResourceSnapshot] + M2Resources --> M2Dispatch[M2BuildDispatchPlanner] M2Dispatch --> M2Static[M2StaticBatchMaterializer] M2Static --> M2 R --> WmoPlacement[WmoPlacementResolver] @@ -383,7 +388,9 @@ sequenceDiagram groups enter `M2BuildQueue` as typed pending jobs. - `M2BuildBatchPlanner` is stateless and owns only call-local scalar plans. `M2BuildDispatchPlanner` is stateless and owns only call-local action/transition - plans after the loader observes resource availability. + plans after the loader observes resource availability. `M2BuildResourceSnapshot` + carries those per-step observations and borrows exact prototype/Mesh references + without controlling engine lifetime. `M2BuildQueue` owns typed pending jobs, FIFO/stale keys, grouped-transform references and group/offset/serial cursors without freeing engine objects. `M2StaticBatchMaterializer` owns static MultiMesh construction/attachment; @@ -571,6 +578,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | M2 placement grouper | Implemented extraction | Scene-free validation/order/transform/source/timing contract | Worker/build state, spatial cells and asset-backed p95/p99 remain pending | | M2 build batch planner | Implemented extraction | Scene-free limit/count/cursor/source/timing contract | Spatial cells and asset-backed p95/p99 remain pending | | M2 build dispatch planner | Implemented extraction | Scene-free priority/action/transition/source/timing contract | Resource observation/orchestration and asset-backed traversal remain pending | +| M2 build resource snapshot | Implemented extraction | Typed identity/adoption/lifetime/source/timing contract | Resource observation service and asset-backed traversal remain pending | | M2 build queue | Implemented extraction | Typed lifecycle/FIFO/progress/lifetime/source/timing contract | Resource dispatch and asset-backed traversal remain pending | | M2 animation load pipeline state | Implemented extraction | Synthetic lifecycle/FIFO/source/timing contract | Asset-backed traversal/leak/animation-fidelity/p95/p99 pending | | M2 animated scene finalizer | Implemented extraction | Synthetic type/lifetime/material/player/source/timing contract | Asset-backed GLB traversal/material/animation comparison pending | @@ -643,6 +651,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | `src/render/m2/m2_animated_instance_materializer.gd` | Animated duplicate/render/playback startup and non-empty batch attachment | | `src/render/m2/m2_static_batch_materializer.gd` | Static MultiMesh construction, render setup and attachment | | `src/render/m2/m2_build_dispatch_planner.gd` | M2 wait/materializer/advance action priority and transition plan | +| `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step animated/static observations and borrowed references | | `src/render/m2/m2_build_job.gd` | M2 root/groups references and group/offset/serial progress | | `src/render/m2/m2_build_queue.gd` | Keyed pending jobs and FIFO/stale tile-key lifecycle | | `src/render/m2/m2_mesh_load_pipeline_state.gd` | Static M2 pending Resource paths, terminal statuses and finalize FIFO | @@ -672,6 +681,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | `src/tools/verify_m2_animated_instance_materializer.gd` | Animated M2 order/render/playback/attachment/boundary/timing regression | | `src/tools/verify_m2_static_batch_materializer.gd` | Static M2 node/Mesh/render/attachment/boundary/timing regression | | `src/tools/verify_m2_build_dispatch_planner.gd` | M2 dispatch priority/action/transition/boundary/timing regression | +| `src/tools/verify_m2_build_resource_snapshot.gd` | M2 observation identity/adoption/lifetime/boundary/timing regression | | `src/tools/verify_m2_build_queue.gd` | M2 job/FIFO/progress/lifetime/boundary/timing regression | | `src/tools/verify_m2_mesh_load_pipeline_state.gd` | Static M2 request/terminal/FIFO/boundary/timing regression | | `src/tools/verify_m2_mesh_resource_cache_state.gd` | Static M2 Mesh cache ownership/lifetime/boundary/timing regression | diff --git a/src/render/m2/m2_build_dispatch_planner.gd b/src/render/m2/m2_build_dispatch_planner.gd index c683241..94097a9 100644 --- a/src/render/m2/m2_build_dispatch_planner.gd +++ b/src/render/m2/m2_build_dispatch_planner.gd @@ -14,20 +14,19 @@ const ACTION_ADVANCE_WITHOUT_MATERIALIZATION := &"advance_without_materializatio ## Returns a detached action/transition plan for one M2 build operation. func plan_step( batch_count: int, - animation_request_pending: bool, - has_animated_prototype: bool, - has_static_mesh: bool, - static_model_missing: bool + resource_snapshot: RefCounted ) -> Dictionary: - if animation_request_pending: + if resource_snapshot == null: + return _wait_plan(ACTION_WAIT_FOR_STATIC_MESH) + if bool(resource_snapshot.call("animation_request_pending")): return _wait_plan(ACTION_WAIT_FOR_ANIMATION) if batch_count <= 0: return _advance_plan(ACTION_ADVANCE_WITHOUT_MATERIALIZATION, false) - if has_animated_prototype: + if bool(resource_snapshot.call("has_animated_prototype")): return _advance_plan(ACTION_MATERIALIZE_ANIMATED, true) - if has_static_mesh: + if bool(resource_snapshot.call("has_static_mesh")): return _advance_plan(ACTION_MATERIALIZE_STATIC, true) - if static_model_missing: + if bool(resource_snapshot.call("static_model_missing")): return _advance_plan(ACTION_ADVANCE_WITHOUT_MATERIALIZATION, true) return _wait_plan(ACTION_WAIT_FOR_STATIC_MESH) diff --git a/src/render/m2/m2_build_resource_snapshot.gd b/src/render/m2/m2_build_resource_snapshot.gd new file mode 100644 index 0000000..7d5798b --- /dev/null +++ b/src/render/m2/m2_build_resource_snapshot.gd @@ -0,0 +1,72 @@ +class_name M2BuildResourceSnapshot +extends RefCounted + +## Holds one M2 build-step resource observation without owning engine lifetime. + +var _normalized_relative_path: String +var _animated_prototype: Node3D +var _animation_request_pending: bool +var _static_mesh: Mesh = null +var _static_model_missing: bool = false + + +func _init( + normalized_relative_path: String, + animated_prototype: Node3D, + animation_request_pending: bool +) -> void: + _normalized_relative_path = normalized_relative_path + _animated_prototype = animated_prototype + _animation_request_pending = animation_request_pending + + +## Returns the normalized M2 path observed by the loader. +func normalized_relative_path() -> String: + return _normalized_relative_path + + +## Returns the borrowed animated prototype without transferring ownership. +func animated_prototype() -> Node3D: + return _animated_prototype + + +## Returns whether an animated prototype was observed. +func has_animated_prototype() -> bool: + return _animated_prototype != null + + +## Returns whether the animation request remains pending. +func animation_request_pending() -> bool: + return _animation_request_pending + + +## Adopts the optional static Mesh and terminal missing-model observation. +func adopt_static_observation(static_mesh: Mesh, static_model_missing: bool) -> void: + _static_mesh = static_mesh + _static_model_missing = static_model_missing + + +## Returns the borrowed static Mesh without transferring ownership. +func static_mesh() -> Mesh: + return _static_mesh + + +## Returns whether a prepared static Mesh was observed. +func has_static_mesh() -> bool: + return _static_mesh != null + + +## Returns whether the static model lookup reached a terminal missing outcome. +func static_model_missing() -> bool: + return _static_model_missing + + +## Returns detached path/availability diagnostics without engine references. +func diagnostic_snapshot() -> Dictionary: + return { + "normalized_relative_path": _normalized_relative_path, + "has_animated_prototype": has_animated_prototype(), + "animation_request_pending": _animation_request_pending, + "has_static_mesh": has_static_mesh(), + "static_model_missing": _static_model_missing, + } diff --git a/src/render/m2/m2_build_resource_snapshot.gd.uid b/src/render/m2/m2_build_resource_snapshot.gd.uid new file mode 100644 index 0000000..40058c3 --- /dev/null +++ b/src/render/m2/m2_build_resource_snapshot.gd.uid @@ -0,0 +1 @@ +uid://cgleoy15rtmby diff --git a/src/scenes/streaming/streaming_world_loader.gd b/src/scenes/streaming/streaming_world_loader.gd index c19cc3e..961a09a 100644 --- a/src/scenes/streaming/streaming_world_loader.gd +++ b/src/scenes/streaming/streaming_world_loader.gd @@ -63,6 +63,9 @@ const M2_BUILD_BATCH_PLANNER_SCRIPT := preload( const M2_BUILD_DISPATCH_PLANNER_SCRIPT := preload( "res://src/render/m2/m2_build_dispatch_planner.gd" ) +const M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT := preload( + "res://src/render/m2/m2_build_resource_snapshot.gd" +) const M2_BUILD_QUEUE_SCRIPT := preload("res://src/render/m2/m2_build_queue.gd") const M2_STATIC_BATCH_MATERIALIZER_SCRIPT := preload( "res://src/render/m2/m2_static_batch_materializer.gd" @@ -4350,31 +4353,38 @@ func _process_m2_build_jobs() -> void: enable_m2_animated_instances and _m2_animation_load_pipeline_state.has_request(normalized_rel) ) + var resource_snapshot: RefCounted = M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new( + normalized_rel, + animated_prototype, + animation_request_pending + ) var batch_plan: Dictionary = {} var batch_count := 0 - var static_mesh: Mesh = null - var static_model_missing := false - if not animation_request_pending: + if not bool(resource_snapshot.call("animation_request_pending")): batch_plan = _m2_build_batch_planner.plan_batch( transforms.size(), offset, - animated_prototype != null, + bool(resource_snapshot.call("has_animated_prototype")), m2_animated_instances_per_tick, m2_multimesh_batch_size ) batch_count = int(batch_plan["batch_count"]) - if batch_count > 0 and animated_prototype == null: - static_mesh = _get_m2_mesh_or_request(rel_path) - static_model_missing = ( - static_mesh == null - and _m2_prototype_cache_state.is_model_missing(normalized_rel) + if ( + batch_count > 0 + and not bool(resource_snapshot.call("has_animated_prototype")) + ): + var static_mesh := _get_m2_mesh_or_request(rel_path) + resource_snapshot.call( + "adopt_static_observation", + static_mesh, + ( + static_mesh == null + and _m2_prototype_cache_state.is_model_missing(normalized_rel) + ) ) var dispatch_plan: Dictionary = _m2_build_dispatch_planner.plan_step( batch_count, - animation_request_pending, - animated_prototype != null, - static_mesh != null, - static_model_missing + resource_snapshot ) if bool(dispatch_plan["rotate_queue"]): _m2_build_queue_state.rotate_front() @@ -4392,7 +4402,7 @@ func _process_m2_build_jobs() -> void: _materialize_m2_animated_batch( root as Node3D, rel_path, - animated_prototype, + resource_snapshot.call("animated_prototype") as Node3D, transforms, offset, batch_count, @@ -4405,7 +4415,7 @@ func _process_m2_build_jobs() -> void: _materialize_m2_group_batch( root as Node3D, rel_path, - static_mesh, + resource_snapshot.call("static_mesh") as Mesh, transforms, offset, batch_count, @@ -4431,8 +4441,6 @@ func _process_m2_build_jobs() -> void: if next_group_index >= group_keys.size(): _finish_m2_build_job(key) _m2_build_queue_state.pop_front() - - func _materialize_m2_group_batch( m2_root: Node3D, rel_path: String, diff --git a/src/tools/verify_m2_build_dispatch_planner.gd b/src/tools/verify_m2_build_dispatch_planner.gd index a47fbc9..a2d8513 100644 --- a/src/tools/verify_m2_build_dispatch_planner.gd +++ b/src/tools/verify_m2_build_dispatch_planner.gd @@ -6,6 +6,9 @@ extends SceneTree const PLANNER_SCRIPT := preload( "res://src/render/m2/m2_build_dispatch_planner.gd" ) +const SNAPSHOT_SCRIPT := preload( + "res://src/render/m2/m2_build_resource_snapshot.gd" +) const PLANNER_PATH := "res://src/render/m2/m2_build_dispatch_planner.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" @@ -32,8 +35,10 @@ func _initialize() -> void: func _verify_wait_and_empty_actions(failures: Array[String]) -> void: var planner: RefCounted = PLANNER_SCRIPT.new() + var prototype := Node3D.new() + var snapshot := _make_snapshot(prototype, true, ArrayMesh.new(), true) var animation_wait: Dictionary = planner.call( - "plan_step", 4, true, true, true, true + "plan_step", 4, snapshot ) _expect_plan( animation_wait, @@ -44,7 +49,7 @@ func _verify_wait_and_empty_actions(failures: Array[String]) -> void: failures ) var empty_batch: Dictionary = planner.call( - "plan_step", 0, false, true, true, true + "plan_step", 0, _make_snapshot(prototype, false, ArrayMesh.new(), true) ) _expect_plan( empty_batch, @@ -55,7 +60,7 @@ func _verify_wait_and_empty_actions(failures: Array[String]) -> void: failures ) var negative_batch: Dictionary = planner.call( - "plan_step", -3, false, false, false, false + "plan_step", -3, _make_snapshot(null, false, null, false) ) _expect_plan( negative_batch, @@ -65,12 +70,14 @@ func _verify_wait_and_empty_actions(failures: Array[String]) -> void: "negative batch advances without serial", failures ) + prototype.free() func _verify_materialization_priority(failures: Array[String]) -> void: var planner: RefCounted = PLANNER_SCRIPT.new() + var prototype := Node3D.new() var animated: Dictionary = planner.call( - "plan_step", 3, false, true, true, true + "plan_step", 3, _make_snapshot(prototype, false, ArrayMesh.new(), true) ) _expect_plan( animated, @@ -81,7 +88,7 @@ func _verify_materialization_priority(failures: Array[String]) -> void: failures ) var static_mesh: Dictionary = planner.call( - "plan_step", 3, false, false, true, true + "plan_step", 3, _make_snapshot(null, false, ArrayMesh.new(), true) ) _expect_plan( static_mesh, @@ -91,12 +98,13 @@ func _verify_materialization_priority(failures: Array[String]) -> void: "prepared static mesh wins over missing flag", failures ) + prototype.free() func _verify_static_retry_and_missing_actions(failures: Array[String]) -> void: var planner: RefCounted = PLANNER_SCRIPT.new() var static_wait: Dictionary = planner.call( - "plan_step", 2, false, false, false, false + "plan_step", 2, _make_snapshot(null, false, null, false) ) _expect_plan( static_wait, @@ -107,7 +115,7 @@ func _verify_static_retry_and_missing_actions(failures: Array[String]) -> void: failures ) var missing: Dictionary = planner.call( - "plan_step", 2, false, false, false, true + "plan_step", 2, _make_snapshot(null, false, null, true) ) _expect_plan( missing, @@ -121,10 +129,12 @@ func _verify_static_retry_and_missing_actions(failures: Array[String]) -> void: func _verify_fresh_results(failures: Array[String]) -> void: var planner: RefCounted = PLANNER_SCRIPT.new() - var first: Dictionary = planner.call("plan_step", 1, false, true, false, false) + var prototype := Node3D.new() + var snapshot := _make_snapshot(prototype, false, null, false) + var first: Dictionary = planner.call("plan_step", 1, snapshot) first["action"] = &"mutated" first["rotate_queue"] = true - var second: Dictionary = planner.call("plan_step", 1, false, true, false, false) + var second: Dictionary = planner.call("plan_step", 1, snapshot) _expect_plan( second, PLANNER_SCRIPT.ACTION_MATERIALIZE_ANIMATED, @@ -133,6 +143,7 @@ func _verify_fresh_results(failures: Array[String]) -> void: "plan results are detached", failures ) + prototype.free() func _verify_loader_and_dependency_boundaries(failures: Array[String]) -> void: @@ -176,15 +187,18 @@ func _verify_loader_and_dependency_boundaries(failures: Array[String]) -> void: func _verify_bounded_timing(failures: Array[String]) -> float: var planner: RefCounted = PLANNER_SCRIPT.new() + var snapshots := [ + _make_snapshot(null, true, null, false), + _make_snapshot(null, false, null, false), + _make_snapshot(null, false, ArrayMesh.new(), false), + _make_snapshot(null, false, null, true), + ] var started_microseconds := Time.get_ticks_usec() for iteration in range(20000): planner.call( "plan_step", iteration % 5, - iteration % 11 == 0, - iteration % 3 == 0, - iteration % 3 == 1, - iteration % 7 == 0 + snapshots[iteration % snapshots.size()] ) var elapsed_milliseconds := ( float(Time.get_ticks_usec() - started_microseconds) / 1000.0 @@ -197,6 +211,25 @@ func _verify_bounded_timing(failures: Array[String]) -> float: return elapsed_milliseconds +func _make_snapshot( + animated_prototype: Node3D, + animation_request_pending: bool, + static_mesh: Mesh, + static_model_missing: bool +) -> RefCounted: + var snapshot: RefCounted = SNAPSHOT_SCRIPT.new( + "world/test.m2", + animated_prototype, + animation_request_pending + ) + snapshot.call( + "adopt_static_observation", + static_mesh, + static_model_missing + ) + return snapshot + + func _expect_plan( plan: Dictionary, expected_action: StringName, diff --git a/src/tools/verify_m2_build_resource_snapshot.gd b/src/tools/verify_m2_build_resource_snapshot.gd new file mode 100644 index 0000000..b68e005 --- /dev/null +++ b/src/tools/verify_m2_build_resource_snapshot.gd @@ -0,0 +1,224 @@ +extends SceneTree + +## Asset-free M2 resource observation identity, adoption, lifetime, boundary and +## bounded-timing regression. + +const SNAPSHOT_SCRIPT := preload( + "res://src/render/m2/m2_build_resource_snapshot.gd" +) +const SNAPSHOT_PATH := "res://src/render/m2/m2_build_resource_snapshot.gd" +const DISPATCH_PATH := "res://src/render/m2/m2_build_dispatch_planner.gd" +const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" + + +func _initialize() -> void: + var failures: Array[String] = [] + _verify_initial_observation(failures) + _verify_static_adoption_and_replacement(failures) + _verify_detached_diagnostics(failures) + _verify_engine_lifetime(failures) + _verify_loader_and_dependency_boundaries(failures) + var elapsed_milliseconds := _verify_bounded_timing(failures) + if not failures.is_empty(): + for failure in failures: + push_error("M2_BUILD_RESOURCE_SNAPSHOT: %s" % failure) + quit(1) + return + print( + "M2_BUILD_RESOURCE_SNAPSHOT PASS cases=12 iterations=20000 elapsed_ms=%.3f" + % elapsed_milliseconds + ) + quit(0) + + +func _verify_initial_observation(failures: Array[String]) -> void: + var prototype := Node3D.new() + var snapshot: RefCounted = SNAPSHOT_SCRIPT.new( + "world/example.m2", + prototype, + true + ) + _expect_string( + String(snapshot.call("normalized_relative_path")), + "world/example.m2", + "normalized path retained", + failures + ) + _expect_same( + snapshot.call("animated_prototype"), + prototype, + "animated prototype identity retained", + failures + ) + _expect_true( + bool(snapshot.call("has_animated_prototype")), + "animated availability", + failures + ) + _expect_true( + bool(snapshot.call("animation_request_pending")), + "animation pending retained", + failures + ) + _expect_false(bool(snapshot.call("has_static_mesh")), "static defaults empty", failures) + _expect_false( + bool(snapshot.call("static_model_missing")), + "missing defaults false", + failures + ) + prototype.free() + + +func _verify_static_adoption_and_replacement(failures: Array[String]) -> void: + var snapshot: RefCounted = SNAPSHOT_SCRIPT.new("world/static.m2", null, false) + var first_mesh := ArrayMesh.new() + var second_mesh := ArrayMesh.new() + snapshot.call("adopt_static_observation", first_mesh, true) + _expect_same(snapshot.call("static_mesh"), first_mesh, "first Mesh identity", failures) + _expect_true(bool(snapshot.call("has_static_mesh")), "static available", failures) + _expect_true( + bool(snapshot.call("static_model_missing")), + "contradictory missing flag retained", + failures + ) + snapshot.call("adopt_static_observation", second_mesh, false) + _expect_same( + snapshot.call("static_mesh"), + second_mesh, + "replacement Mesh identity", + failures + ) + _expect_false( + bool(snapshot.call("static_model_missing")), + "replacement missing flag", + failures + ) + snapshot.call("adopt_static_observation", null, true) + _expect_false(bool(snapshot.call("has_static_mesh")), "Mesh may clear", failures) + _expect_true(bool(snapshot.call("static_model_missing")), "terminal missing adopted", failures) + + +func _verify_detached_diagnostics(failures: Array[String]) -> void: + var prototype := Node3D.new() + var mesh := ArrayMesh.new() + var snapshot: RefCounted = SNAPSHOT_SCRIPT.new("world/diag.m2", prototype, false) + snapshot.call("adopt_static_observation", mesh, true) + var diagnostics: Dictionary = snapshot.call("diagnostic_snapshot") + _expect_true( + diagnostics == { + "normalized_relative_path": "world/diag.m2", + "has_animated_prototype": true, + "animation_request_pending": false, + "has_static_mesh": true, + "static_model_missing": true, + }, + "diagnostic scalar contract", + failures + ) + _expect_false(diagnostics.values().has(prototype), "diagnostics omit prototype", failures) + _expect_false(diagnostics.values().has(mesh), "diagnostics omit Mesh", failures) + diagnostics["has_static_mesh"] = false + var fresh: Dictionary = snapshot.call("diagnostic_snapshot") + _expect_true(bool(fresh["has_static_mesh"]), "diagnostics detached", failures) + prototype.free() + + +func _verify_engine_lifetime(failures: Array[String]) -> void: + var prototype := Node3D.new() + var snapshot: RefCounted = SNAPSHOT_SCRIPT.new("world/lifetime.m2", prototype, false) + snapshot = null + _expect_true(is_instance_valid(prototype), "snapshot release keeps Node valid", failures) + prototype.free() + + +func _verify_loader_and_dependency_boundaries(failures: Array[String]) -> void: + var loader_source := FileAccess.get_file_as_string(LOADER_PATH) + var dispatch_source := FileAccess.get_file_as_string(DISPATCH_PATH) + var snapshot_source := FileAccess.get_file_as_string(SNAPSHOT_PATH) + for delegated_token in [ + "M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(", + "\"adopt_static_observation\"", + "resource_snapshot.call(\"animated_prototype\")", + "resource_snapshot.call(\"static_mesh\")", + ]: + _expect_true( + loader_source.contains(delegated_token), + "loader uses %s" % delegated_token, + failures + ) + _expect_true( + dispatch_source.contains("resource_snapshot.call(\"animation_request_pending\")") + and dispatch_source.contains("resource_snapshot.call(\"has_static_mesh\")"), + "dispatch planner consumes typed snapshot", + failures + ) + for forbidden_token in [ + "queue_free", + "free(", + "ResourceLoader", + "RenderingServer", + "FileAccess", + "WorkerThreadPool", + "Thread", + "Mutex", + ]: + _expect_false( + snapshot_source.contains(forbidden_token), + "snapshot omits %s ownership" % forbidden_token, + failures + ) + + +func _verify_bounded_timing(failures: Array[String]) -> float: + var mesh := ArrayMesh.new() + var started_microseconds := Time.get_ticks_usec() + for iteration in range(20000): + var snapshot: RefCounted = SNAPSHOT_SCRIPT.new( + "world/%d.m2" % (iteration % 64), + null, + iteration % 5 == 0 + ) + snapshot.call( + "adopt_static_observation", + mesh if iteration % 3 == 0 else null, + iteration % 7 == 0 + ) + snapshot.call("diagnostic_snapshot") + var elapsed_milliseconds := ( + float(Time.get_ticks_usec() - started_microseconds) / 1000.0 + ) + _expect_true( + elapsed_milliseconds < 1000.0, + "20,000 snapshot cycles remain bounded", + failures + ) + return elapsed_milliseconds + + +func _expect_string( + 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) + + +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) diff --git a/src/tools/verify_m2_build_resource_snapshot.gd.uid b/src/tools/verify_m2_build_resource_snapshot.gd.uid new file mode 100644 index 0000000..a068674 --- /dev/null +++ b/src/tools/verify_m2_build_resource_snapshot.gd.uid @@ -0,0 +1 @@ +uid://cgx3uaof21ubs diff --git a/targets/03-renderer-facade.md b/targets/03-renderer-facade.md index 6c6c781..162717b 100644 --- a/targets/03-renderer-facade.md +++ b/targets/03-renderer-facade.md @@ -57,7 +57,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру `M03-RND-M2-ANIMATED-INSTANCE-MATERIALIZER-001`, `M03-RND-M2-STATIC-BATCH-MATERIALIZER-001`, `M03-RND-M2-BUILD-QUEUE-001`, - `M03-RND-M2-BUILD-DISPATCH-PLANNER-001` + `M03-RND-M2-BUILD-DISPATCH-PLANNER-001`, + `M03-RND-M2-BUILD-RESOURCE-SNAPSHOT-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 @@ -263,6 +264,13 @@ Runtime и Editor используют facade; planner/scheduler тестиру materializers, animation/Mesh pipelines, Mesh/prototype state, shutdown, facade, internal-access `30`, manifest and checkpoint dry-run `7/7`, documentation and coordination remained green. + M2 build resource snapshot passed `cases=12 iterations=20000 + elapsed_ms=42.995` with exact path/prototype/Mesh identity, animation defaults, + two-phase static adoption/replacement/clear, contradictory-state preservation, + detached diagnostics, engine lifetime and loader/dispatch boundaries. All 54 + autonomous headless regressions and checkpoint dry-run `7/7` passed; the + proprietary ADT probe remained unavailable. Internal-access inventory remains + `30`; resource lookup/request execution remains loader-owned. M02 terrain-query regression remained green (13 pre-existing expired M00 claim warnings). - Fidelity comparison: all 16 historical operation limits and drain sites were @@ -297,6 +305,9 @@ Runtime и Editor используют facade; planner/scheduler тестиру M2 build dispatch retains pending-animation priority before batch planning, animated-before-static selection, unresolved-static rotation without progress, and the historical serial increment for terminally missing positive batches. + One build operation now carries the exact normalized path, optional animated + prototype, pending-animation state, optional static Mesh and missing outcome in + a typed snapshot; releasing it never frees either borrowed engine resource. WMO cache-key normalization, positive MODF identity with tile/index fallback and world position/Euler/unclamped-scale transforms are unchanged across lightweight render, cached-scene and live-prototype paths. @@ -413,6 +424,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру M2 static batch materializer and loader readiness/cursor/Editor-owner adapter, typed M2 build job/queue and loader enqueue/drain/progress/cancel/clear adapters, stateless M2 build dispatch planner and loader resource-observation/action adapter, + typed M2 build resource snapshot and loader/dispatch/materializer 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 @@ -437,7 +449,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру headless timing does not measure transform upload GPU cost and spatial-cell batching still needs culling/performance evidence; typed M2 pending state is separated and dispatch decisions are explicit, while resource observation/ - requests, action execution and root cleanup remain in loader; + requests, action execution and root cleanup remain in loader; resource state is + typed per operation but its producing service is not yet extracted; 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;