render: extract M2 build dispatch planner

This commit is contained in:
2026-07-18 02:34:18 +04:00
parent 1fb566f9bc
commit 17f5cc0faa
12 changed files with 632 additions and 36 deletions
+13
View File
@@ -41,6 +41,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
- `src/render/m2/m2_animated_instance_materializer.gd` - main-thread animated instance duplication, render settings, playback startup and non-empty batch attachment.
- `src/render/m2/m2_static_batch_materializer.gd` - main-thread static M2 MultiMesh construction, render settings and attachment.
- `src/render/m2/m2_build_queue.gd` / `m2_build_job.gd` - typed pending M2 jobs, FIFO/stale tile keys, grouped-transform references and progress cursors without engine destruction.
- `src/render/m2/m2_build_dispatch_planner.gd` - pure animation/static wait, materialization and missing-model advance decision.
- `src/render/m2/m2_animation_load_pipeline_state.gd` - animated M2 threaded-load request records and completion-order finalize FIFO without I/O or Node ownership.
- `src/render/m2/m2_mesh_load_pipeline_state.gd` - static M2 threaded-load request records, terminal statuses and completion-order finalize FIFO without I/O or Mesh ownership.
- `src/render/m2/m2_mesh_resource_cache_state.gd` - normalized-path prepared static M2 Mesh references with final-shutdown lifetime.
@@ -1058,6 +1059,18 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- Cache formats, quality profiles, batching output and visible rules are unchanged.
Asset-backed p95/p99 and spatial-cell batching evidence remain pending.
## 2026-07-18 M2 Build Dispatch Planner Extraction
- `M2BuildDispatchPlanner` now owns the pure action priority between pending
animation, animated materialization, pending/ready static Mesh and terminally
missing-model advancement.
- Pending animation still wins before batch planning. Unresolved static Meshes
still rotate and consume one `M2_BUILD` permit without advancing progress.
- Empty batches advance without serial change; animated, static and terminally
missing positive batches retain the historical serial increment.
- Resource lookup/request order, queue/cursor adoption, materialization, permits,
Node lifetime, cache formats, profiles and visible rules remain unchanged.
## 2026-07-17 M2 Runtime Mesh Rebuild Classifier Extraction
- `M2RuntimeMeshRebuildClassifier` now owns the memoized decision used when a
+1
View File
@@ -21,6 +21,7 @@
| M2 placement transform resolver | Implemented | [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.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 dispatch planner | Implemented extraction | [`m2-build-dispatch-planner.md`](m2-build-dispatch-planner.md) |
| M2 build queue | Implemented extraction | [`m2-build-queue.md`](m2-build-queue.md) |
| M2 static batch materializer | Implemented extraction | [`m2-static-batch-materializer.md`](m2-static-batch-materializer.md) |
| M2 runtime mesh rebuild classifier | Implemented extraction | [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md) |
+9 -6
View File
@@ -32,8 +32,8 @@ flowchart LR
Loader --> Planner
Planner --> Plan[Detached batch plan]
Plan --> Loader
Loader --> Ready{Resource ready?}
Ready --> Materialize[Animated or MultiMesh materialization]
Loader --> Dispatch[M2BuildDispatchPlanner]
Dispatch --> Materialize[Animated or MultiMesh materialization]
Loader --> Budget[RenderBudgetScheduler permit]
```
@@ -103,7 +103,8 @@ sequenceDiagram
- The planner owns only call-local scalar values and the returned Dictionary.
- `M2BuildQueue` owns typed jobs, FIFO ordering, serial numbers and group/offset
cursors. The loader owns tile checks, resource readiness/retry and adoption calls.
cursors. `M2BuildDispatchPlanner` owns the pure resource-state action decision;
the loader owns tile checks, resource observation/retry and adoption calls.
- Materializers own main-thread Node/MultiMesh construction under loader roots.
- The scheduler owns the frame-local `M2_BUILD` counter.
- Pure planning is thread-safe, though the current adapter calls it on main thread.
@@ -149,8 +150,8 @@ queue depth, build activity and hitch observability.
## Extension points
- A later package may extract resource readiness/dispatch while retaining the
typed build-job and FIFO contracts defined by `M2BuildQueue`.
- A later package may extract resource observation/request orchestration while
retaining the dispatch and typed build-job/FIFO contracts.
- Spatial-cell batching must use measured culling/performance evidence and must
not silently change this model-path batch cursor.
@@ -160,7 +161,8 @@ queue depth, build activity and hitch observability.
|---|---|---|---|
| Static/animated batch cursor planning | Implemented extraction | Contract/source/timing verifier | Asset-backed p95/p99 pending |
| Typed build queue/cursor state | Implemented extraction | M2 build queue lifecycle verifier | Asset-backed traversal pending |
| Resource readiness/dispatch | Remains in loader | Existing lifecycle regressions | Stateful extraction pending |
| Resource dispatch decision | Implemented extraction | M2 build dispatch planner verifier | Asset-backed traversal pending |
| Resource observation/requests | Remains in loader | Existing lifecycle regressions | Stateful extraction pending |
| Spatial-cell batching | Planned | Renderer roadmap | Culling evidence/design pending |
## Known gaps and risks
@@ -176,6 +178,7 @@ queue depth, build activity and hitch observability.
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_build_batch_planner.gd` | Pure limit/count/cursor planning |
| `src/render/m2/m2_build_dispatch_planner.gd` | Pure observed-state action/transition planning |
| `src/render/m2/m2_build_queue.gd` | Typed pending jobs, FIFO order and cursor ownership |
| `src/scenes/streaming/streaming_world_loader.gd` | Tile checks, resource readiness, progress adoption, materializer adapters and budgets |
| `src/render/m2/m2_static_batch_materializer.gd` | Planned static-slice MultiMesh construction and attachment |
+231
View File
@@ -0,0 +1,231 @@
# M2 Build Dispatch Planner
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-BUILD-DISPATCH-PLANNER-001` |
| Owners | Pure M2 resource-state to build-action decision |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-build-dispatch-planner`, 2026-07-18 |
| Profiles/capabilities | Existing static MultiMesh and animated-instance build paths |
## Purpose
Select one M2 build action after the loader observes animation/static resource
state. The planner makes wait priority, materializer selection and the historical
missing-model serial transition explicit without owning resources or engine work.
## Non-goals
- Locate, request, load, cache or validate M2 resources.
- Size batches, own build jobs/queues or consume renderer permits.
- Create, attach or free Nodes, Meshes, MultiMeshes or animated instances.
- Change transforms, ordering, render settings, profiles or visible output.
- Add spatial-cell batching or M2 format/fidelity behavior.
## Context and boundaries
```mermaid
flowchart LR
Loader[StreamingWorldLoader resource observations] --> Planner[M2BuildDispatchPlanner]
Planner --> Action[Detached action and transition plan]
Action --> Loader
Loader --> Queue[M2BuildQueue rotate/progress]
Loader --> Animated[M2AnimatedInstanceMaterializer]
Loader --> Static[M2StaticBatchMaterializer]
Loader --> Budget[RenderBudgetScheduler]
```
The planner depends only on `RefCounted`, scalar values, `StringName` constants
and a fresh Dictionary. ResourceLoader, caches, Nodes, Meshes, RenderingServer,
SceneTree, files, workers, mutexes, gameplay, network and Editor APIs are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `ACTION_WAIT_FOR_ANIMATION` | Constant | Pending animation request blocks all other actions | Immutable | None |
| `ACTION_MATERIALIZE_ANIMATED` | Constant | Build the selected slice from animated prototype | 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_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 |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Planned batch count | `M2BuildBatchPlanner` through loader | Dispatch planner | Integer copy | One call |
| Input | Animation request pending | Animation pipeline through loader | Dispatch planner | Boolean copy | One call |
| Input | Animated prototype available | Prototype cache/native loader adapter | Dispatch planner | Boolean copy; no Node reference | One call |
| Input | Static Mesh available | Mesh cache/request adapter | Dispatch planner | Boolean copy; no Mesh reference | One call |
| Input | Static model terminally missing | Prototype cache through loader | Dispatch planner | Boolean copy | One call |
| Output | Action | Dispatch planner | Loader materializer/wait branch | Immutable StringName | One operation |
| Output | `rotate_queue` | Dispatch planner | Loader queue adapter | Detached boolean | One operation |
| Output | `increment_batch_serial` | Dispatch planner | Loader progress adapter | Detached boolean | One operation |
The output Dictionary is newly allocated and caller-owned. The planner retains
no state, resource or engine-object reference.
## Data flow
```mermaid
flowchart TD
Input[Observed state] --> AnimationPending{Animation request pending?}
AnimationPending -->|yes| WaitAnimation[Wait animation; rotate]
AnimationPending -->|no| Positive{Batch count positive?}
Positive -->|no| AdvanceEmpty[Advance; keep serial]
Positive -->|yes| Animated{Animated prototype?}
Animated -->|yes| BuildAnimated[Materialize animated; increment serial]
Animated -->|no| StaticReady{Static Mesh ready?}
StaticReady -->|yes| BuildStatic[Materialize static; increment serial]
StaticReady -->|no| Missing{Model terminally missing?}
Missing -->|yes| AdvanceMissing[Advance without Node; increment serial]
Missing -->|no| WaitStatic[Wait static Mesh; rotate]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Observe
Observe --> Waiting: animation/static unresolved
Observe --> Materializing: animated/static resource ready
Observe --> Advancing: empty or terminally missing batch
Waiting --> [*]: detached wait plan
Materializing --> [*]: detached materialization plan
Advancing --> [*]: detached advance plan
```
The planner itself is stateless. Queue rotation, cursor adoption, cancellation,
map reset and shutdown remain external lifecycle transitions.
## Main sequence
```mermaid
sequenceDiagram
participant L as StreamingWorldLoader
participant B as M2BuildBatchPlanner
participant D as M2BuildDispatchPlanner
participant Q as M2BuildQueue
participant M as M2 materializer
participant S as RenderBudgetScheduler
L->>L: observe animation request/prototype
alt animation not pending
L->>B: plan_batch
L->>L: lookup/request static Mesh when required
end
L->>D: plan_step(observed scalar state)
D-->>L: action, rotate flag, serial flag
alt wait action
L->>Q: rotate_front
else materialize action
L->>M: materialize selected slice
L->>Q: adopt progress and serial
else advance action
L->>Q: adopt progress and optional serial
end
L->>S: consume one M2_BUILD permit
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Dispatch[M2BuildDispatchPlanner]
Loader --> Batch[M2BuildBatchPlanner]
Loader --> Queue[M2BuildQueue]
Loader --> Caches[M2 cache/pipeline state]
Loader --> Materializers[M2 materializers]
Dispatch --> Values[Integers + booleans + StringName]
Dispatch -. no dependency .-> Engine[Node / Mesh / ResourceLoader]
Dispatch -. no dependency .-> State[Queue / cache / scheduler state]
```
## Ownership, threading and resources
- Planner owns call-local comparisons and the returned Dictionary only.
- Loader owns resource lookup/request order, action execution and permit use.
- `M2BuildQueue` owns pending jobs, FIFO keys and progress cursors.
- Materializers own main-thread scene construction under loader-owned roots.
- Pure calls are thread-safe; the current loader adapter calls on main thread.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Animation request pending | Boolean observation | Wait wins even if other flags are true | Priority fixture | Retry when queue returns front |
| Non-positive batch | Count comparison | Advance without serial increment | Zero/negative fixtures | Next group/cursor operation |
| Static Mesh unresolved | No Mesh and not missing | Rotate without serial increment | Retry fixture | Loader request/cache completes |
| Static model missing | Terminal missing flag | Advance without Node and increment serial | Missing fixture | Later group continues |
| Contradictory static ready/missing | Both true | Ready Mesh wins | Priority fixture | Loader normally prevents contradiction |
| Tile cancellation/root invalid | Outside planner | Loader cancels before dispatch | Queue/shutdown regressions | Eligible tile may requeue |
## Configuration and capabilities
The planner adds no setting. Existing animated/static batch limits, animation
enable flag, visibility/shadow settings and `M2_BUILD` permits remain external.
## Persistence, cache and migration
No state is serialized. No ADT/M2 cache format or version changes; no rebake or
migration is required.
## Diagnostics and observability
The planner emits no logs. Its verifier reports the action matrix, source
boundaries and bounded 20,000-call timing. Existing loader queue/hitch metrics
remain unchanged.
## Verification
- `verify_m2_build_dispatch_planner.gd` covers wait priority, zero/negative
counts, animated/static priority, unresolved retry, terminal missing, detached
results, loader/dependency boundaries and bounded timing.
- Queue, batch planner, materializers, caches, shutdown, facade, internal-access
and checkpoint tests protect adjacent behavior.
- Fidelity evidence is exact branch/transition extraction only. No private asset
or original-client visual comparison is claimed.
## Extension points
A later package may move resource observation/request execution behind a typed
service while preserving this scalar action contract. Generic callbacks, signals
and a shared state-machine framework remain intentionally excluded.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| M2 build action selection | Implemented extraction | Priority/matrix/source/timing verifier | Asset-backed traversal pending |
| Resource observation and requests | Loader-owned | Cache/pipeline regressions | Typed orchestration extraction pending |
| Queue/cursor state | Implemented separately | M2 build queue verifier | Asset-backed traversal pending |
| Materialization | Implemented separately | Static/animated materializer verifiers | GPU/p95/p99 evidence pending |
## Known gaps and risks
- Planner consumes raw booleans rather than a typed resource snapshot.
- Loader still performs synchronous/native prototype attempts and static request I/O.
- Synthetic timing does not measure ResourceLoader, Node or GPU work.
- Private traversal, leak, visual and p95/p99 evidence remains unavailable.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_build_dispatch_planner.gd` | Pure action priority and transition plan |
| `src/scenes/streaming/streaming_world_loader.gd` | Resource observation, action execution, permits and engine lifetime |
| `src/render/m2/m2_build_batch_planner.gd` | Batch count and cursor plan |
| `src/render/m2/m2_build_queue.gd` | Pending jobs, FIFO and cursor ownership |
| `src/tools/verify_m2_build_dispatch_planner.gd` | Matrix, boundary and timing regression |
## Related decisions and references
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
- [`m2-build-queue.md`](m2-build-queue.md)
- [`m2-static-batch-materializer.md`](m2-static-batch-materializer.md)
- [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+7 -1
View File
@@ -33,6 +33,7 @@ flowchart LR
Queue --> Job[M2BuildJob]
Queue --> Loader
Loader --> Planner[M2BuildBatchPlanner]
Loader --> Dispatch[M2BuildDispatchPlanner]
Loader --> Static[M2StaticBatchMaterializer]
Loader --> Animated[M2AnimatedInstanceMaterializer]
Loader --> Scheduler[RenderBudgetScheduler]
@@ -171,6 +172,8 @@ flowchart TB
- Job retains the exact groups Dictionary, a fresh group-key snapshot and root.
- Root ownership remains with the loader/tile SceneTree; queue release never frees it.
- Loader validates `is_instance_valid`, frees empty/aborted roots and mutates tile state.
- `M2BuildDispatchPlanner` selects wait/materializer/advance actions without
borrowing queue-owned engine references.
- All current operations run on the renderer main thread; no mutex is required.
- Group worker results cross their existing mutex mailbox before enqueue.
@@ -229,7 +232,8 @@ queue base, signals and callbacks are intentionally excluded.
| Typed keyed M2 jobs and FIFO | Implemented extraction | Lifecycle/order/source/timing verifier | Asset-backed traversal pending |
| Cursor/serial ownership | Implemented extraction | Atomic progress fixtures | Typed batch-plan result remains Dictionary |
| Root destruction | Existing loader-owned | Lifetime/source/shutdown regressions | Keep outside state service |
| Resource readiness/dispatch | Existing loader-owned | Adjacent cache/build tests | Safe extraction remains |
| Resource dispatch decision | Implemented separately | Dispatch planner verifier | Asset-backed traversal pending |
| Resource observation/requests | Existing loader-owned | Adjacent cache/build tests | Safe extraction remains |
## Known gaps and risks
@@ -246,11 +250,13 @@ queue base, signals and callbacks are intentionally excluded.
| `src/render/m2/m2_build_queue.gd` | Keyed job ownership, FIFO/stale/rotation lifecycle and diagnostics |
| `src/scenes/streaming/streaming_world_loader.gd` | Eligibility, readiness, permits, materialization, tile state and root cleanup |
| `src/render/m2/m2_build_batch_planner.gd` | Batch count and cursor-plan calculation |
| `src/render/m2/m2_build_dispatch_planner.gd` | Resource-state action and transition planning |
| `src/tools/verify_m2_build_queue.gd` | Lifecycle/order/lifetime/boundary/timing regression |
## Related decisions and references
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
- [`m2-build-dispatch-planner.md`](m2-build-dispatch-planner.md)
- [`m2-static-batch-materializer.md`](m2-static-batch-materializer.md)
- [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.md)
- [`world-renderer.md`](world-renderer.md)
+14 -4
View File
@@ -54,6 +54,8 @@ flowchart LR
M2Transform --> Loader
Loader --> M2Batch[M2BuildBatchPlanner]
M2Batch --> Loader
Loader --> M2Dispatch[M2BuildDispatchPlanner]
M2Dispatch --> Loader
Loader --> M2Queue[M2BuildQueue]
M2Queue --> Loader
Loader --> M2Static[M2StaticBatchMaterializer]
@@ -136,6 +138,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
| `M2PlacementTransformResolver.resolve_basis/resolve_origin_offset` | Internal pure M2 service | Resolves regular and calibrated model-specific ADT placement transforms | Worker/main thread; stateless | Unknown paths use regular basis and zero offset |
| `M2PlacementGrouper.group_placements` | Internal pure M2 service | Validates and groups ordered tile-local placement transforms by normalized path | Worker/main thread; stateless | Invalid variants/name IDs/empty paths are skipped |
| `M2BuildBatchPlanner.plan_batch` | Internal pure M2 service | Selects static/animated batch count and next group cursor | Main/any thread; stateless | Non-positive selected limit clamps to one; empty range completes |
| `M2BuildDispatchPlanner.plan_step` | Internal pure M2 service | Selects wait, animated/static materialization or no-Node advancement from observed resource state | Main/any thread; stateless | Pending animation has priority; unresolved static Mesh waits |
| `M2BuildQueue` / `M2BuildJob` | Internal M2 pending-state service | Own typed root/groups/cursor jobs and FIFO/stale tile keys | Renderer main thread; map session | Invalid enqueue rejected; stale keys drain independently of jobs |
| `M2StaticBatchMaterializer.materialize_batch` | Internal M2 scene-materialization service | Builds and attaches one prepared-Mesh MultiMesh transform slice | Renderer main thread; stateless after each call | Invalid/empty input returns null; bounds are caller precondition |
| `M2RuntimeMeshRebuildClassifier` | Internal memoized M2 service | Detects billboard/UV-rotation metadata requiring stale cached-mesh rebuild | Renderer main thread; cached until reset | Invalid variants/indices skipped; first path decision wins |
@@ -185,6 +188,7 @@ loader configuration remains transitional composition data, not a caller API.
| Internal transform | Rotation/path/scale | Loader or grouper / `M2PlacementTransformResolver` | Group/placeholder/instance transforms | Value-only Basis/Vector3 | One placement |
| Internal grouping | Tile origin, M2 names and placements | Loader / `M2PlacementGrouper` | Loader worker result/build job | Fresh Dictionary/Transform3D arrays | One grouping task |
| Internal batch plan | Transform count/offset, path kind and limits | Loader / `M2BuildBatchPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One build operation |
| Internal M2 dispatch plan | Batch count and observed animation/static resource state | Loader / `M2BuildDispatchPlanner` | Loader queue/materializer/progress adapter | Fresh scalar/action Dictionary | One build operation |
| Internal M2 pending build | Tile key, M2 root, grouped transforms and cursors | Loader / `M2BuildQueue` | Loader readiness, planner and materializer adapters | Queue-owned job/keys and strong references | Until finish/cancel/clear/replacement |
| Internal static M2 materialization | Parent, prepared Mesh, ordered transform slice and render settings | Loader / `M2StaticBatchMaterializer` | Attached MultiMeshInstance3D | Parent owns node/MultiMesh; exact Mesh reference retained | One main-thread build batch |
| Internal WMO placement | Path, MODF placement, tile/index | Loader / `WmoPlacementResolver` | WMO caches, registry and three instance adapters | Value-only String/Transform3D | Lookup/placement lifetime |
@@ -249,7 +253,8 @@ flowchart TD
M2Grouper --> M2Batch[M2BuildBatchPlanner]
M2Grouper --> M2Queue[M2BuildQueue]
M2Queue --> M2Batch
M2Batch --> M2Static[M2StaticBatchMaterializer]
M2Batch --> M2Dispatch[M2BuildDispatchPlanner]
M2Dispatch --> M2Static[M2StaticBatchMaterializer]
M2Static --> M2
R --> WmoPlacement[WmoPlacementResolver]
WmoPlacement --> WmoRegistry[WmoPlacementRegistry]
@@ -377,11 +382,13 @@ sequenceDiagram
The loader retains tasks, mutex/result queues and stale-result checks; accepted
groups enter `M2BuildQueue` as typed pending jobs.
- `M2BuildBatchPlanner` is stateless and owns only call-local scalar plans.
`M2BuildDispatchPlanner` is stateless and owns only call-local action/transition
plans after the loader observes resource availability.
`M2BuildQueue` owns typed pending jobs, FIFO/stale keys, grouped-transform
references and group/offset/serial cursors without freeing engine objects.
`M2StaticBatchMaterializer` owns static MultiMesh construction/attachment;
the loader retains resource transitions, cursor-adoption decisions,
animated/static dispatch, root cleanup, budgets and Editor ownership.
the loader retains resource observation/requests, action execution,
cursor adoption, root cleanup, budgets and Editor ownership.
- `WmoPlacementResolver` is stateless and owns only call-local cache-key,
identity and transform values. `WmoPlacementRegistry` owns only placement-key
reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation
@@ -562,7 +569,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| M2 unique placement registry | Implemented extraction | Scene-free ownership/lifecycle/timing contract and historical `uid:11785` smoke | Group/build/tasks/finalization and asset-backed p95/p99 remain pending |
| M2 placement transform resolver | Implemented extraction | Scene-free formula/source/timing contract across three consumers | Asset-backed visual recheck and general placement parity 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 | Queue/resource state, spatial cells and asset-backed p95/p99 remain pending |
| M2 build batch planner | Implemented extraction | Scene-free limit/count/cursor/source/timing contract | Spatial cells and asset-backed p95/p99 remain pending |
| M2 build dispatch planner | Implemented extraction | Scene-free priority/action/transition/source/timing contract | Resource observation/orchestration and asset-backed traversal remain pending |
| M2 build queue | Implemented extraction | Typed lifecycle/FIFO/progress/lifetime/source/timing contract | Resource dispatch and asset-backed traversal remain pending |
| M2 animation load pipeline state | Implemented extraction | Synthetic lifecycle/FIFO/source/timing contract | Asset-backed traversal/leak/animation-fidelity/p95/p99 pending |
| M2 animated scene finalizer | Implemented extraction | Synthetic type/lifetime/material/player/source/timing contract | Asset-backed GLB traversal/material/animation comparison pending |
@@ -634,6 +642,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/render/m2/m2_animation_playback_controller.gd` | Per-instance phase, selection, native copy/start and imported playback |
| `src/render/m2/m2_animated_instance_materializer.gd` | Animated duplicate/render/playback startup and non-empty batch attachment |
| `src/render/m2/m2_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_job.gd` | M2 root/groups references and group/offset/serial progress |
| `src/render/m2/m2_build_queue.gd` | Keyed pending jobs and FIFO/stale tile-key lifecycle |
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Static M2 pending Resource paths, terminal statuses and finalize FIFO |
@@ -662,6 +671,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/tools/verify_m2_animation_playback_controller.gd` | Animated M2 phase/selection/playback/native/boundary/timing regression |
| `src/tools/verify_m2_animated_instance_materializer.gd` | Animated M2 order/render/playback/attachment/boundary/timing regression |
| `src/tools/verify_m2_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_queue.gd` | M2 job/FIFO/progress/lifetime/boundary/timing regression |
| `src/tools/verify_m2_mesh_load_pipeline_state.gd` | Static M2 request/terminal/FIFO/boundary/timing regression |
| `src/tools/verify_m2_mesh_resource_cache_state.gd` | Static M2 Mesh cache ownership/lifetime/boundary/timing regression |
@@ -0,0 +1,48 @@
class_name M2BuildDispatchPlanner
extends RefCounted
## Selects the next M2 build action from already observed resource state.
## The planner retains no state or engine-object references.
const ACTION_WAIT_FOR_ANIMATION := &"wait_for_animation"
const ACTION_MATERIALIZE_ANIMATED := &"materialize_animated"
const ACTION_WAIT_FOR_STATIC_MESH := &"wait_for_static_mesh"
const ACTION_MATERIALIZE_STATIC := &"materialize_static"
const ACTION_ADVANCE_WITHOUT_MATERIALIZATION := &"advance_without_materialization"
## Returns a detached action/transition plan for one M2 build operation.
func plan_step(
batch_count: int,
animation_request_pending: bool,
has_animated_prototype: bool,
has_static_mesh: bool,
static_model_missing: bool
) -> Dictionary:
if animation_request_pending:
return _wait_plan(ACTION_WAIT_FOR_ANIMATION)
if batch_count <= 0:
return _advance_plan(ACTION_ADVANCE_WITHOUT_MATERIALIZATION, false)
if has_animated_prototype:
return _advance_plan(ACTION_MATERIALIZE_ANIMATED, true)
if has_static_mesh:
return _advance_plan(ACTION_MATERIALIZE_STATIC, true)
if static_model_missing:
return _advance_plan(ACTION_ADVANCE_WITHOUT_MATERIALIZATION, true)
return _wait_plan(ACTION_WAIT_FOR_STATIC_MESH)
func _wait_plan(action: StringName) -> Dictionary:
return {
"action": action,
"rotate_queue": true,
"increment_batch_serial": false,
}
func _advance_plan(action: StringName, increment_batch_serial: bool) -> Dictionary:
return {
"action": action,
"rotate_queue": false,
"increment_batch_serial": increment_batch_serial,
}
@@ -0,0 +1 @@
uid://df0bv1x2x4j2x
+67 -23
View File
@@ -60,6 +60,9 @@ const M2_PLACEMENT_GROUPER_SCRIPT := preload(
const M2_BUILD_BATCH_PLANNER_SCRIPT := preload(
"res://src/render/m2/m2_build_batch_planner.gd"
)
const M2_BUILD_DISPATCH_PLANNER_SCRIPT := preload(
"res://src/render/m2/m2_build_dispatch_planner.gd"
)
const M2_BUILD_QUEUE_SCRIPT := preload("res://src/render/m2/m2_build_queue.gd")
const M2_STATIC_BATCH_MATERIALIZER_SCRIPT := preload(
"res://src/render/m2/m2_static_batch_materializer.gd"
@@ -270,6 +273,7 @@ var _m2_group_tasks: Dictionary = {}
var _m2_group_result_mutex := Mutex.new()
var _m2_group_result_queue: Array = []
var _m2_build_queue_state := M2_BUILD_QUEUE_SCRIPT.new()
var _m2_build_dispatch_planner := M2_BUILD_DISPATCH_PLANNER_SCRIPT.new()
var _m2_unique_placement_registry := (
M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT.new()
)
@@ -4342,32 +4346,72 @@ func _process_m2_build_jobs() -> void:
animated_prototype = _get_or_load_m2_native_animated_prototype(rel_path)
if animated_prototype == null:
animated_prototype = _get_or_load_m2_animated_prototype(rel_path)
if enable_m2_animated_instances and _m2_animation_load_pipeline_state.has_request(normalized_rel):
_m2_build_queue_state.rotate_front()
_render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.M2_BUILD)
continue
var batch_plan: Dictionary = _m2_build_batch_planner.plan_batch(
transforms.size(),
offset,
animated_prototype != null,
m2_animated_instances_per_tick,
m2_multimesh_batch_size
var animation_request_pending := (
enable_m2_animated_instances
and _m2_animation_load_pipeline_state.has_request(normalized_rel)
)
var batch_count: int = int(batch_plan["batch_count"])
var batch_plan: Dictionary = {}
var batch_count := 0
var static_mesh: Mesh = null
var static_model_missing := false
if not animation_request_pending:
batch_plan = _m2_build_batch_planner.plan_batch(
transforms.size(),
offset,
animated_prototype != null,
m2_animated_instances_per_tick,
m2_multimesh_batch_size
)
batch_count = int(batch_plan["batch_count"])
if batch_count > 0 and animated_prototype == null:
static_mesh = _get_m2_mesh_or_request(rel_path)
static_model_missing = (
static_mesh == null
and _m2_prototype_cache_state.is_model_missing(normalized_rel)
)
var dispatch_plan: Dictionary = _m2_build_dispatch_planner.plan_step(
batch_count,
animation_request_pending,
animated_prototype != null,
static_mesh != null,
static_model_missing
)
if bool(dispatch_plan["rotate_queue"]):
_m2_build_queue_state.rotate_front()
_render_budget_scheduler.try_consume_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.M2_BUILD
)
continue
var serial := _m2_build_queue_state.batch_serial_for(key)
var next_serial := serial
if batch_count > 0:
if animated_prototype != null:
_materialize_m2_animated_batch(root as Node3D, rel_path, animated_prototype, transforms, offset, batch_count, serial)
else:
var mesh := _get_m2_mesh_or_request(rel_path)
if mesh == null:
if not _m2_prototype_cache_state.is_model_missing(normalized_rel):
_m2_build_queue_state.rotate_front()
_render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.M2_BUILD)
continue
else:
_materialize_m2_group_batch(root as Node3D, rel_path, mesh, transforms, offset, batch_count, serial)
var dispatch_action: StringName = dispatch_plan["action"]
if (
dispatch_action
== M2_BUILD_DISPATCH_PLANNER_SCRIPT.ACTION_MATERIALIZE_ANIMATED
):
_materialize_m2_animated_batch(
root as Node3D,
rel_path,
animated_prototype,
transforms,
offset,
batch_count,
serial
)
elif (
dispatch_action
== M2_BUILD_DISPATCH_PLANNER_SCRIPT.ACTION_MATERIALIZE_STATIC
):
_materialize_m2_group_batch(
root as Node3D,
rel_path,
static_mesh,
transforms,
offset,
batch_count,
serial
)
if bool(dispatch_plan["increment_batch_serial"]):
next_serial = serial + 1
var next_group_index := index
var next_transform_offset := offset
@@ -0,0 +1,225 @@
extends SceneTree
## Asset-free M2 build dispatch priority, transition, source-boundary and
## bounded-timing regression.
const PLANNER_SCRIPT := preload(
"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"
func _initialize() -> void:
var failures: Array[String] = []
_verify_wait_and_empty_actions(failures)
_verify_materialization_priority(failures)
_verify_static_retry_and_missing_actions(failures)
_verify_fresh_results(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_DISPATCH_PLANNER: %s" % failure)
quit(1)
return
print(
"M2_BUILD_DISPATCH_PLANNER PASS cases=10 iterations=20000 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_wait_and_empty_actions(failures: Array[String]) -> void:
var planner: RefCounted = PLANNER_SCRIPT.new()
var animation_wait: Dictionary = planner.call(
"plan_step", 4, true, true, true, true
)
_expect_plan(
animation_wait,
PLANNER_SCRIPT.ACTION_WAIT_FOR_ANIMATION,
true,
false,
"animation wait has absolute priority",
failures
)
var empty_batch: Dictionary = planner.call(
"plan_step", 0, false, true, true, true
)
_expect_plan(
empty_batch,
PLANNER_SCRIPT.ACTION_ADVANCE_WITHOUT_MATERIALIZATION,
false,
false,
"zero batch advances without serial",
failures
)
var negative_batch: Dictionary = planner.call(
"plan_step", -3, false, false, false, false
)
_expect_plan(
negative_batch,
PLANNER_SCRIPT.ACTION_ADVANCE_WITHOUT_MATERIALIZATION,
false,
false,
"negative batch advances without serial",
failures
)
func _verify_materialization_priority(failures: Array[String]) -> void:
var planner: RefCounted = PLANNER_SCRIPT.new()
var animated: Dictionary = planner.call(
"plan_step", 3, false, true, true, true
)
_expect_plan(
animated,
PLANNER_SCRIPT.ACTION_MATERIALIZE_ANIMATED,
false,
true,
"animated prototype wins over static state",
failures
)
var static_mesh: Dictionary = planner.call(
"plan_step", 3, false, false, true, true
)
_expect_plan(
static_mesh,
PLANNER_SCRIPT.ACTION_MATERIALIZE_STATIC,
false,
true,
"prepared static mesh wins over missing flag",
failures
)
func _verify_static_retry_and_missing_actions(failures: Array[String]) -> void:
var planner: RefCounted = PLANNER_SCRIPT.new()
var static_wait: Dictionary = planner.call(
"plan_step", 2, false, false, false, false
)
_expect_plan(
static_wait,
PLANNER_SCRIPT.ACTION_WAIT_FOR_STATIC_MESH,
true,
false,
"unresolved static mesh rotates without serial",
failures
)
var missing: Dictionary = planner.call(
"plan_step", 2, false, false, false, true
)
_expect_plan(
missing,
PLANNER_SCRIPT.ACTION_ADVANCE_WITHOUT_MATERIALIZATION,
false,
true,
"terminal missing model advances serial",
failures
)
func _verify_fresh_results(failures: Array[String]) -> void:
var planner: RefCounted = PLANNER_SCRIPT.new()
var first: Dictionary = planner.call("plan_step", 1, false, true, false, false)
first["action"] = &"mutated"
first["rotate_queue"] = true
var second: Dictionary = planner.call("plan_step", 1, false, true, false, false)
_expect_plan(
second,
PLANNER_SCRIPT.ACTION_MATERIALIZE_ANIMATED,
false,
true,
"plan results are detached",
failures
)
func _verify_loader_and_dependency_boundaries(failures: Array[String]) -> void:
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
var planner_source := FileAccess.get_file_as_string(PLANNER_PATH)
_expect_true(
loader_source.contains("M2_BUILD_DISPATCH_PLANNER_SCRIPT.new()"),
"loader composes dispatch planner",
failures
)
_expect_true(
loader_source.contains("_m2_build_dispatch_planner.plan_step("),
"loader delegates dispatch decision",
failures
)
_expect_true(
loader_source.contains("_get_m2_mesh_or_request(rel_path)")
and loader_source.contains("_materialize_m2_animated_batch(")
and loader_source.contains("_materialize_m2_group_batch(")
and loader_source.contains("try_consume_permit("),
"loader retains readiness, materialization and permits",
failures
)
for forbidden_token in [
"preload(",
"ResourceLoader",
"RenderingServer",
"FileAccess",
"queue_free",
"Node3D",
"WorkerThreadPool",
"Thread",
"Mutex",
]:
_expect_false(
planner_source.contains(forbidden_token),
"planner omits %s ownership" % forbidden_token,
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var planner: RefCounted = PLANNER_SCRIPT.new()
var started_microseconds := Time.get_ticks_usec()
for iteration in range(20000):
planner.call(
"plan_step",
iteration % 5,
iteration % 11 == 0,
iteration % 3 == 0,
iteration % 3 == 1,
iteration % 7 == 0
)
var elapsed_milliseconds := (
float(Time.get_ticks_usec() - started_microseconds) / 1000.0
)
_expect_true(
elapsed_milliseconds < 1000.0,
"20,000 dispatch plans remain bounded",
failures
)
return elapsed_milliseconds
func _expect_plan(
plan: Dictionary,
expected_action: StringName,
expected_rotation: bool,
expected_serial_increment: bool,
label: String,
failures: Array[String]
) -> void:
if plan.get("action", &"") != expected_action:
failures.append("%s action" % label)
if bool(plan.get("rotate_queue", false)) != expected_rotation:
failures.append("%s rotation" % label)
if (
bool(plan.get("increment_batch_serial", false))
!= expected_serial_increment
):
failures.append("%s serial" % 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://ck4mypi4djlmy
+15 -2
View File
@@ -56,7 +56,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
`M03-RND-M2-ANIMATION-PLAYBACK-001`,
`M03-RND-M2-ANIMATED-INSTANCE-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`
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner
@@ -251,6 +252,13 @@ Runtime и Editor используют facade; planner/scheduler тестиру
Post-merge queue (`65.998ms`), planner, static/animated materializers, Mesh/
prototype state, shutdown, materials, facade, internal-access `30`, manifest
`7/7`, documentation and coordination gates remained green.
M2 build dispatch planner passed `cases=10 iterations=20000
elapsed_ms=11.933` with animation-wait priority, zero/negative batch advance,
animated/static selection, unresolved static retry, terminal missing-model
serial advancement, detached results and source/dependency boundaries. All 53
autonomous headless regressions and checkpoint dry-run `7/7` passed; the
proprietary ADT probe remained unavailable. Internal-access inventory remains
`30`; resource observation/requests and action execution remain loader-owned.
M02 terrain-query regression
remained green (13 pre-existing expired M00 claim warnings).
- Fidelity comparison: all 16 historical operation limits and drain sites were
@@ -282,6 +290,9 @@ Runtime и Editor используют facade; planner/scheduler тестиру
Pending M2 builds retain keyed latest-job replacement, duplicate/stale FIFO
entries, one-key rotation, strong root/group references and exact group/offset/
serial progression; releasing queue records still never frees scene nodes.
M2 build dispatch retains pending-animation priority before batch planning,
animated-before-static selection, unresolved-static rotation without progress,
and the historical serial increment for terminally missing positive batches.
WMO cache-key normalization, positive MODF identity with tile/index fallback
and world position/Euler/unclamped-scale transforms are unchanged across
lightweight render, cached-scene and live-prototype paths.
@@ -397,6 +408,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
M2 animated instance materializer and loader log/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,
stateless M2 build dispatch planner and loader resource-observation/action adapter,
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
- Remaining risks: worker concurrency and stale-result validation remain
streamer-owned; cancellation stops new permits but does not interrupt
@@ -420,7 +432,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
static MultiMesh materialization is also separated but remains synchronous;
headless timing does not measure transform upload GPU cost and spatial-cell
batching still needs culling/performance evidence; typed M2 pending state is
separated, while resource readiness/dispatch and root cleanup remain in loader;
separated and dispatch decisions are explicit, while resource observation/
requests, action execution and root cleanup remain in loader;
WMO ResourceLoader/FileAccess I/O, live fallback and group materialization
remain in the loader;
asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending;