merge: M2 build resource snapshot

This commit is contained in:
2026-07-18 02:47:35 +04:00
15 changed files with 669 additions and 59 deletions
+13
View File
@@ -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_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_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_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_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_load_pipeline_state.gd` - static M2 threaded-load request records, terminal statuses and completion-order finalize FIFO without I/O or Mesh ownership.
- `src/render/m2/m2_mesh_resource_cache_state.gd` - normalized-path prepared static M2 Mesh references with final-shutdown lifetime. - `src/render/m2/m2_mesh_resource_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, - Resource lookup/request order, queue/cursor adoption, materialization, permits,
Node lifetime, cache formats, profiles and visible rules remain unchanged. 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 ## 2026-07-17 M2 Runtime Mesh Rebuild Classifier Extraction
- `M2RuntimeMeshRebuildClassifier` now owns the memoized decision used when a - `M2RuntimeMeshRebuildClassifier` now owns the memoized decision used when a
@@ -73,14 +73,21 @@ observation snapshot consumed by the dispatch planner and materializer adapter.
## Status ## Status
- State: active - State: ready for integration
- Done: contract and ownership boundary - Done: implementation, verification, documentation and worktree handoff
- Next: implementation, verification and documentation - Next: no-ff merge and post-merge acceptance on master
- Blocked by: - Blocked by:
## Handoff ## Handoff
- Commit: - Commit: `ec1b90f1e44037572e3e5052e6dedc016c29859a`
- Results: - Results: resource snapshot `cases=12 iterations=20000 elapsed_ms=42.995`;
- Remaining risks: dispatch planner remained green; all autonomous headless regressions `54/54`;
- Documentation updated: internal access `private_symbols=30`; baseline manifest/dry-run `7/7`;
documentation `module_specs=41`; coordination passed with 30 historical warnings.
- Remaining risks: proprietary ADT visual/p95/p99 evidence is unavailable;
resource lookup/request execution, action execution, permits and engine lifetime
intentionally remain loader-owned.
- Documentation updated: new `m2-build-resource-snapshot.md` with API/I/O,
data-flow, lifecycle, sequence, dependency and ownership diagrams; dispatch/
queue/world renderer specs, module registry, RENDER and M03 Evidence updated.
+1
View File
@@ -22,6 +22,7 @@
| M2 placement grouper | Implemented extraction | [`m2-placement-grouper.md`](m2-placement-grouper.md) | | 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 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 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 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 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) | | M2 runtime mesh rebuild classifier | Implemented extraction | [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md) |
+11 -10
View File
@@ -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_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_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 | | `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 ## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | | Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| Input | Planned batch count | `M2BuildBatchPlanner` through loader | Dispatch planner | Integer copy | One call | | 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 | Typed animated/static availability, pending and missing observations | Loader / `M2BuildResourceSnapshot` | Dispatch planner | Borrowed snapshot; engine references are not read | 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 |
| Output | Action | Dispatch planner | Loader materializer/wait branch | Immutable StringName | One operation | | 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 | `rotate_queue` | Dispatch planner | Loader queue adapter | Detached boolean | One operation |
| Output | `increment_batch_serial` | Dispatch planner | Loader progress 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->>B: plan_batch
L->>L: lookup/request static Mesh when required L->>L: lookup/request static Mesh when required
end end
L->>D: plan_step(observed scalar state) L->>D: plan_step(batch count, resource snapshot)
D-->>L: action, rotate flag, serial flag D-->>L: action, rotate flag, serial flag
alt wait action alt wait action
L->>Q: rotate_front L->>Q: rotate_front
@@ -138,6 +135,7 @@ flowchart TB
Loader --> Queue[M2BuildQueue] Loader --> Queue[M2BuildQueue]
Loader --> Caches[M2 cache/pipeline state] Loader --> Caches[M2 cache/pipeline state]
Loader --> Materializers[M2 materializers] Loader --> Materializers[M2 materializers]
Dispatch --> Snapshot[M2BuildResourceSnapshot accessors]
Dispatch --> Values[Integers + booleans + StringName] Dispatch --> Values[Integers + booleans + StringName]
Dispatch -. no dependency .-> Engine[Node / Mesh / ResourceLoader] Dispatch -. no dependency .-> Engine[Node / Mesh / ResourceLoader]
Dispatch -. no dependency .-> State[Queue / cache / scheduler state] Dispatch -. no dependency .-> State[Queue / cache / scheduler state]
@@ -146,6 +144,8 @@ flowchart TB
## Ownership, threading and resources ## Ownership, threading and resources
- Planner owns call-local comparisons and the returned Dictionary only. - 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. - Loader owns resource lookup/request order, action execution and permit use.
- `M2BuildQueue` owns pending jobs, FIFO keys and progress cursors. - `M2BuildQueue` owns pending jobs, FIFO keys and progress cursors.
- Materializers own main-thread scene construction under loader-owned roots. - Materializers own main-thread scene construction under loader-owned roots.
@@ -190,9 +190,9 @@ remain unchanged.
## Extension points ## Extension points
A later package may move resource observation/request execution behind a typed A later package may move resource observation/request execution behind a service
service while preserving this scalar action contract. Generic callbacks, signals that produces `M2BuildResourceSnapshot`. Generic callbacks, signals and a shared
and a shared state-machine framework remain intentionally excluded. state-machine framework remain intentionally excluded.
## Capability status ## Capability status
@@ -205,7 +205,6 @@ and a shared state-machine framework remain intentionally excluded.
## Known gaps and risks ## 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. - Loader still performs synchronous/native prototype attempts and static request I/O.
- Synthetic timing does not measure ResourceLoader, Node or GPU work. - Synthetic timing does not measure ResourceLoader, Node or GPU work.
- Private traversal, leak, visual and p95/p99 evidence remains unavailable. - 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 | | Path | Responsibility |
|---|---| |---|---|
| `src/render/m2/m2_build_dispatch_planner.gd` | Pure action priority and transition plan | | `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/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_batch_planner.gd` | Batch count and cursor plan |
| `src/render/m2/m2_build_queue.gd` | Pending jobs, FIFO and cursor ownership | | `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 ## Related decisions and references
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md) - [`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-build-queue.md`](m2-build-queue.md)
- [`m2-static-batch-materializer.md`](m2-static-batch-materializer.md) - [`m2-static-batch-materializer.md`](m2-static-batch-materializer.md)
- [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.md) - [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.md)
+1
View File
@@ -257,6 +257,7 @@ queue base, signals and callbacks are intentionally excluded.
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md) - [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
- [`m2-build-dispatch-planner.md`](m2-build-dispatch-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-static-batch-materializer.md`](m2-static-batch-materializer.md)
- [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.md) - [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.md)
- [`world-renderer.md`](world-renderer.md) - [`world-renderer.md`](world-renderer.md)
+226
View File
@@ -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)
+12 -2
View File
@@ -56,6 +56,8 @@ flowchart LR
M2Batch --> Loader M2Batch --> Loader
Loader --> M2Dispatch[M2BuildDispatchPlanner] Loader --> M2Dispatch[M2BuildDispatchPlanner]
M2Dispatch --> Loader M2Dispatch --> Loader
Loader --> M2Resources[M2BuildResourceSnapshot]
M2Resources --> M2Dispatch
Loader --> M2Queue[M2BuildQueue] Loader --> M2Queue[M2BuildQueue]
M2Queue --> Loader M2Queue --> Loader
Loader --> M2Static[M2StaticBatchMaterializer] 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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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 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 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 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 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 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 | | 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 --> M2Batch[M2BuildBatchPlanner]
M2Grouper --> M2Queue[M2BuildQueue] M2Grouper --> M2Queue[M2BuildQueue]
M2Queue --> M2Batch M2Queue --> M2Batch
M2Batch --> M2Dispatch[M2BuildDispatchPlanner] M2Batch --> M2Resources[M2BuildResourceSnapshot]
M2Resources --> M2Dispatch[M2BuildDispatchPlanner]
M2Dispatch --> M2Static[M2StaticBatchMaterializer] M2Dispatch --> M2Static[M2StaticBatchMaterializer]
M2Static --> M2 M2Static --> M2
R --> WmoPlacement[WmoPlacementResolver] R --> WmoPlacement[WmoPlacementResolver]
@@ -383,7 +388,9 @@ sequenceDiagram
groups enter `M2BuildQueue` as typed pending jobs. groups enter `M2BuildQueue` as typed pending jobs.
- `M2BuildBatchPlanner` is stateless and owns only call-local scalar plans. - `M2BuildBatchPlanner` is stateless and owns only call-local scalar plans.
`M2BuildDispatchPlanner` is stateless and owns only call-local action/transition `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 `M2BuildQueue` owns typed pending jobs, FIFO/stale keys, grouped-transform
references and group/offset/serial cursors without freeing engine objects. references and group/offset/serial cursors without freeing engine objects.
`M2StaticBatchMaterializer` owns static MultiMesh construction/attachment; `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 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 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 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 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 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 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_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_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_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_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_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 | | `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_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_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_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_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_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_cache_state.gd` | Static M2 Mesh cache ownership/lifetime/boundary/timing regression |
+7 -8
View File
@@ -14,20 +14,19 @@ const ACTION_ADVANCE_WITHOUT_MATERIALIZATION := &"advance_without_materializatio
## Returns a detached action/transition plan for one M2 build operation. ## Returns a detached action/transition plan for one M2 build operation.
func plan_step( func plan_step(
batch_count: int, batch_count: int,
animation_request_pending: bool, resource_snapshot: RefCounted
has_animated_prototype: bool,
has_static_mesh: bool,
static_model_missing: bool
) -> Dictionary: ) -> 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) return _wait_plan(ACTION_WAIT_FOR_ANIMATION)
if batch_count <= 0: if batch_count <= 0:
return _advance_plan(ACTION_ADVANCE_WITHOUT_MATERIALIZATION, false) 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) 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) 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 _advance_plan(ACTION_ADVANCE_WITHOUT_MATERIALIZATION, true)
return _wait_plan(ACTION_WAIT_FOR_STATIC_MESH) return _wait_plan(ACTION_WAIT_FOR_STATIC_MESH)
@@ -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,
}
@@ -0,0 +1 @@
uid://cgleoy15rtmby
+23 -15
View File
@@ -63,6 +63,9 @@ const M2_BUILD_BATCH_PLANNER_SCRIPT := preload(
const M2_BUILD_DISPATCH_PLANNER_SCRIPT := preload( const M2_BUILD_DISPATCH_PLANNER_SCRIPT := preload(
"res://src/render/m2/m2_build_dispatch_planner.gd" "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_BUILD_QUEUE_SCRIPT := preload("res://src/render/m2/m2_build_queue.gd")
const M2_STATIC_BATCH_MATERIALIZER_SCRIPT := preload( const M2_STATIC_BATCH_MATERIALIZER_SCRIPT := preload(
"res://src/render/m2/m2_static_batch_materializer.gd" "res://src/render/m2/m2_static_batch_materializer.gd"
@@ -4350,31 +4353,38 @@ func _process_m2_build_jobs() -> void:
enable_m2_animated_instances enable_m2_animated_instances
and _m2_animation_load_pipeline_state.has_request(normalized_rel) 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_plan: Dictionary = {}
var batch_count := 0 var batch_count := 0
var static_mesh: Mesh = null if not bool(resource_snapshot.call("animation_request_pending")):
var static_model_missing := false
if not animation_request_pending:
batch_plan = _m2_build_batch_planner.plan_batch( batch_plan = _m2_build_batch_planner.plan_batch(
transforms.size(), transforms.size(),
offset, offset,
animated_prototype != null, bool(resource_snapshot.call("has_animated_prototype")),
m2_animated_instances_per_tick, m2_animated_instances_per_tick,
m2_multimesh_batch_size m2_multimesh_batch_size
) )
batch_count = int(batch_plan["batch_count"]) batch_count = int(batch_plan["batch_count"])
if batch_count > 0 and animated_prototype == null: if (
static_mesh = _get_m2_mesh_or_request(rel_path) batch_count > 0
static_model_missing = ( 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 static_mesh == null
and _m2_prototype_cache_state.is_model_missing(normalized_rel) and _m2_prototype_cache_state.is_model_missing(normalized_rel)
) )
)
var dispatch_plan: Dictionary = _m2_build_dispatch_planner.plan_step( var dispatch_plan: Dictionary = _m2_build_dispatch_planner.plan_step(
batch_count, batch_count,
animation_request_pending, resource_snapshot
animated_prototype != null,
static_mesh != null,
static_model_missing
) )
if bool(dispatch_plan["rotate_queue"]): if bool(dispatch_plan["rotate_queue"]):
_m2_build_queue_state.rotate_front() _m2_build_queue_state.rotate_front()
@@ -4392,7 +4402,7 @@ func _process_m2_build_jobs() -> void:
_materialize_m2_animated_batch( _materialize_m2_animated_batch(
root as Node3D, root as Node3D,
rel_path, rel_path,
animated_prototype, resource_snapshot.call("animated_prototype") as Node3D,
transforms, transforms,
offset, offset,
batch_count, batch_count,
@@ -4405,7 +4415,7 @@ func _process_m2_build_jobs() -> void:
_materialize_m2_group_batch( _materialize_m2_group_batch(
root as Node3D, root as Node3D,
rel_path, rel_path,
static_mesh, resource_snapshot.call("static_mesh") as Mesh,
transforms, transforms,
offset, offset,
batch_count, batch_count,
@@ -4431,8 +4441,6 @@ func _process_m2_build_jobs() -> void:
if next_group_index >= group_keys.size(): if next_group_index >= group_keys.size():
_finish_m2_build_job(key) _finish_m2_build_job(key)
_m2_build_queue_state.pop_front() _m2_build_queue_state.pop_front()
func _materialize_m2_group_batch( func _materialize_m2_group_batch(
m2_root: Node3D, m2_root: Node3D,
rel_path: String, rel_path: String,
+46 -13
View File
@@ -6,6 +6,9 @@ extends SceneTree
const PLANNER_SCRIPT := preload( const PLANNER_SCRIPT := preload(
"res://src/render/m2/m2_build_dispatch_planner.gd" "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 PLANNER_PATH := "res://src/render/m2/m2_build_dispatch_planner.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.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: func _verify_wait_and_empty_actions(failures: Array[String]) -> void:
var planner: RefCounted = PLANNER_SCRIPT.new() 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( var animation_wait: Dictionary = planner.call(
"plan_step", 4, true, true, true, true "plan_step", 4, snapshot
) )
_expect_plan( _expect_plan(
animation_wait, animation_wait,
@@ -44,7 +49,7 @@ func _verify_wait_and_empty_actions(failures: Array[String]) -> void:
failures failures
) )
var empty_batch: Dictionary = planner.call( 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( _expect_plan(
empty_batch, empty_batch,
@@ -55,7 +60,7 @@ func _verify_wait_and_empty_actions(failures: Array[String]) -> void:
failures failures
) )
var negative_batch: Dictionary = planner.call( var negative_batch: Dictionary = planner.call(
"plan_step", -3, false, false, false, false "plan_step", -3, _make_snapshot(null, false, null, false)
) )
_expect_plan( _expect_plan(
negative_batch, negative_batch,
@@ -65,12 +70,14 @@ func _verify_wait_and_empty_actions(failures: Array[String]) -> void:
"negative batch advances without serial", "negative batch advances without serial",
failures failures
) )
prototype.free()
func _verify_materialization_priority(failures: Array[String]) -> void: func _verify_materialization_priority(failures: Array[String]) -> void:
var planner: RefCounted = PLANNER_SCRIPT.new() var planner: RefCounted = PLANNER_SCRIPT.new()
var prototype := Node3D.new()
var animated: Dictionary = planner.call( var animated: Dictionary = planner.call(
"plan_step", 3, false, true, true, true "plan_step", 3, _make_snapshot(prototype, false, ArrayMesh.new(), true)
) )
_expect_plan( _expect_plan(
animated, animated,
@@ -81,7 +88,7 @@ func _verify_materialization_priority(failures: Array[String]) -> void:
failures failures
) )
var static_mesh: Dictionary = planner.call( 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( _expect_plan(
static_mesh, static_mesh,
@@ -91,12 +98,13 @@ func _verify_materialization_priority(failures: Array[String]) -> void:
"prepared static mesh wins over missing flag", "prepared static mesh wins over missing flag",
failures failures
) )
prototype.free()
func _verify_static_retry_and_missing_actions(failures: Array[String]) -> void: func _verify_static_retry_and_missing_actions(failures: Array[String]) -> void:
var planner: RefCounted = PLANNER_SCRIPT.new() var planner: RefCounted = PLANNER_SCRIPT.new()
var static_wait: Dictionary = planner.call( var static_wait: Dictionary = planner.call(
"plan_step", 2, false, false, false, false "plan_step", 2, _make_snapshot(null, false, null, false)
) )
_expect_plan( _expect_plan(
static_wait, static_wait,
@@ -107,7 +115,7 @@ func _verify_static_retry_and_missing_actions(failures: Array[String]) -> void:
failures failures
) )
var missing: Dictionary = planner.call( var missing: Dictionary = planner.call(
"plan_step", 2, false, false, false, true "plan_step", 2, _make_snapshot(null, false, null, true)
) )
_expect_plan( _expect_plan(
missing, missing,
@@ -121,10 +129,12 @@ func _verify_static_retry_and_missing_actions(failures: Array[String]) -> void:
func _verify_fresh_results(failures: Array[String]) -> void: func _verify_fresh_results(failures: Array[String]) -> void:
var planner: RefCounted = PLANNER_SCRIPT.new() 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["action"] = &"mutated"
first["rotate_queue"] = true 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( _expect_plan(
second, second,
PLANNER_SCRIPT.ACTION_MATERIALIZE_ANIMATED, PLANNER_SCRIPT.ACTION_MATERIALIZE_ANIMATED,
@@ -133,6 +143,7 @@ func _verify_fresh_results(failures: Array[String]) -> void:
"plan results are detached", "plan results are detached",
failures failures
) )
prototype.free()
func _verify_loader_and_dependency_boundaries(failures: Array[String]) -> void: 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: func _verify_bounded_timing(failures: Array[String]) -> float:
var planner: RefCounted = PLANNER_SCRIPT.new() 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() var started_microseconds := Time.get_ticks_usec()
for iteration in range(20000): for iteration in range(20000):
planner.call( planner.call(
"plan_step", "plan_step",
iteration % 5, iteration % 5,
iteration % 11 == 0, snapshots[iteration % snapshots.size()]
iteration % 3 == 0,
iteration % 3 == 1,
iteration % 7 == 0
) )
var elapsed_milliseconds := ( var elapsed_milliseconds := (
float(Time.get_ticks_usec() - started_microseconds) / 1000.0 float(Time.get_ticks_usec() - started_microseconds) / 1000.0
@@ -197,6 +211,25 @@ func _verify_bounded_timing(failures: Array[String]) -> float:
return elapsed_milliseconds 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( func _expect_plan(
plan: Dictionary, plan: Dictionary,
expected_action: StringName, expected_action: StringName,
@@ -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)
@@ -0,0 +1 @@
uid://cgx3uaof21ubs
+15 -2
View File
@@ -57,7 +57,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
`M03-RND-M2-ANIMATED-INSTANCE-MATERIALIZER-001`, `M03-RND-M2-ANIMATED-INSTANCE-MATERIALIZER-001`,
`M03-RND-M2-STATIC-BATCH-MATERIALIZER-001`, `M03-RND-M2-STATIC-BATCH-MATERIALIZER-001`,
`M03-RND-M2-BUILD-QUEUE-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 - Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates. headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner - 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, materializers, animation/Mesh pipelines, Mesh/prototype state, shutdown, facade,
internal-access `30`, manifest and checkpoint dry-run `7/7`, documentation and internal-access `30`, manifest and checkpoint dry-run `7/7`, documentation and
coordination remained green. 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 M02 terrain-query regression
remained green (13 pre-existing expired M00 claim warnings). remained green (13 pre-existing expired M00 claim warnings).
- Fidelity comparison: all 16 historical operation limits and drain sites were - 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, M2 build dispatch retains pending-animation priority before batch planning,
animated-before-static selection, unresolved-static rotation without progress, animated-before-static selection, unresolved-static rotation without progress,
and the historical serial increment for terminally missing positive batches. 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 WMO cache-key normalization, positive MODF identity with tile/index fallback
and world position/Euler/unclamped-scale transforms are unchanged across and world position/Euler/unclamped-scale transforms are unchanged across
lightweight render, cached-scene and live-prototype paths. 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, M2 static batch materializer and loader readiness/cursor/Editor-owner adapter,
typed M2 build job/queue and loader enqueue/drain/progress/cancel/clear adapters, typed M2 build job/queue and loader enqueue/drain/progress/cancel/clear adapters,
stateless M2 build dispatch planner and loader resource-observation/action adapter, 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. expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
- Remaining risks: worker concurrency and stale-result validation remain - Remaining risks: worker concurrency and stale-result validation remain
streamer-owned; cancellation stops new permits but does not interrupt 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 headless timing does not measure transform upload GPU cost and spatial-cell
batching still needs culling/performance evidence; typed M2 pending state is batching still needs culling/performance evidence; typed M2 pending state is
separated and dispatch decisions are explicit, while resource observation/ 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 WMO ResourceLoader/FileAccess I/O, live fallback and group materialization
remain in the loader; remain in the loader;
asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending; asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending;