render: add M2 build resource snapshot
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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) |
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user