From 17f5cc0faa376c8ac84a7ac72d5b8c0366866980 Mon Sep 17 00:00:00 2001 From: sindoring Date: Sat, 18 Jul 2026 02:34:18 +0400 Subject: [PATCH 1/2] render: extract M2 build dispatch planner --- RENDER.md | 13 + docs/modules/README.md | 1 + docs/modules/m2-build-batch-planner.md | 15 +- docs/modules/m2-build-dispatch-planner.md | 231 ++++++++++++++++++ docs/modules/m2-build-queue.md | 8 +- docs/modules/world-renderer.md | 18 +- src/render/m2/m2_build_dispatch_planner.gd | 48 ++++ .../m2/m2_build_dispatch_planner.gd.uid | 1 + .../streaming/streaming_world_loader.gd | 90 +++++-- src/tools/verify_m2_build_dispatch_planner.gd | 225 +++++++++++++++++ .../verify_m2_build_dispatch_planner.gd.uid | 1 + targets/03-renderer-facade.md | 17 +- 12 files changed, 632 insertions(+), 36 deletions(-) create mode 100644 docs/modules/m2-build-dispatch-planner.md create mode 100644 src/render/m2/m2_build_dispatch_planner.gd create mode 100644 src/render/m2/m2_build_dispatch_planner.gd.uid create mode 100644 src/tools/verify_m2_build_dispatch_planner.gd create mode 100644 src/tools/verify_m2_build_dispatch_planner.gd.uid diff --git a/RENDER.md b/RENDER.md index 4f74da3..abf9dd0 100644 --- a/RENDER.md +++ b/RENDER.md @@ -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 diff --git a/docs/modules/README.md b/docs/modules/README.md index f44ba4e..ff04e3d 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -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) | diff --git a/docs/modules/m2-build-batch-planner.md b/docs/modules/m2-build-batch-planner.md index efb36af..5125ae4 100644 --- a/docs/modules/m2-build-batch-planner.md +++ b/docs/modules/m2-build-batch-planner.md @@ -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 | diff --git a/docs/modules/m2-build-dispatch-planner.md b/docs/modules/m2-build-dispatch-planner.md new file mode 100644 index 0000000..e4682e5 --- /dev/null +++ b/docs/modules/m2-build-dispatch-planner.md @@ -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) diff --git a/docs/modules/m2-build-queue.md b/docs/modules/m2-build-queue.md index c867c33..5b18455 100644 --- a/docs/modules/m2-build-queue.md +++ b/docs/modules/m2-build-queue.md @@ -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) diff --git a/docs/modules/world-renderer.md b/docs/modules/world-renderer.md index 037487e..80baa60 100644 --- a/docs/modules/world-renderer.md +++ b/docs/modules/world-renderer.md @@ -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 | diff --git a/src/render/m2/m2_build_dispatch_planner.gd b/src/render/m2/m2_build_dispatch_planner.gd new file mode 100644 index 0000000..c683241 --- /dev/null +++ b/src/render/m2/m2_build_dispatch_planner.gd @@ -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, + } diff --git a/src/render/m2/m2_build_dispatch_planner.gd.uid b/src/render/m2/m2_build_dispatch_planner.gd.uid new file mode 100644 index 0000000..8ad7d4d --- /dev/null +++ b/src/render/m2/m2_build_dispatch_planner.gd.uid @@ -0,0 +1 @@ +uid://df0bv1x2x4j2x diff --git a/src/scenes/streaming/streaming_world_loader.gd b/src/scenes/streaming/streaming_world_loader.gd index cf82e2f..c19cc3e 100644 --- a/src/scenes/streaming/streaming_world_loader.gd +++ b/src/scenes/streaming/streaming_world_loader.gd @@ -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 diff --git a/src/tools/verify_m2_build_dispatch_planner.gd b/src/tools/verify_m2_build_dispatch_planner.gd new file mode 100644 index 0000000..a47fbc9 --- /dev/null +++ b/src/tools/verify_m2_build_dispatch_planner.gd @@ -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) diff --git a/src/tools/verify_m2_build_dispatch_planner.gd.uid b/src/tools/verify_m2_build_dispatch_planner.gd.uid new file mode 100644 index 0000000..ef388a7 --- /dev/null +++ b/src/tools/verify_m2_build_dispatch_planner.gd.uid @@ -0,0 +1 @@ +uid://ck4mypi4djlmy diff --git a/targets/03-renderer-facade.md b/targets/03-renderer-facade.md index cb40f41..2b6f7f2 100644 --- a/targets/03-renderer-facade.md +++ b/targets/03-renderer-facade.md @@ -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; From ed71b36aeceeca9393c4c0558cd0f94b0bd93c9b Mon Sep 17 00:00:00 2001 From: sindoring Date: Sat, 18 Jul 2026 02:34:31 +0400 Subject: [PATCH 2/2] coordination: hand off M2 build dispatch planner --- .../M03-RND-M2-BUILD-DISPATCH-PLANNER-001.md | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/coordination/claims/M03-RND-M2-BUILD-DISPATCH-PLANNER-001.md b/coordination/claims/M03-RND-M2-BUILD-DISPATCH-PLANNER-001.md index a900751..5567fa9 100644 --- a/coordination/claims/M03-RND-M2-BUILD-DISPATCH-PLANNER-001.md +++ b/coordination/claims/M03-RND-M2-BUILD-DISPATCH-PLANNER-001.md @@ -73,14 +73,21 @@ exact permit/cursor behavior. ## Status -- State: active -- Done: contract and ownership boundary -- Next: implementation, verification and documentation +- State: ready for integration +- Done: implementation, verification, documentation and worktree handoff +- Next: no-ff merge and post-merge acceptance on master - Blocked by: ## Handoff -- Commit: -- Results: -- Remaining risks: -- Documentation updated: +- Commit: `17f5cc0faa376c8ac84a7ac72d5b8c0366866980` +- Results: dispatch planner `cases=10 iterations=20000 elapsed_ms=11.933`; + all autonomous headless regressions `53/53`; internal access + `private_symbols=30`; baseline manifest/dry-run `7/7`; documentation + `module_specs=40`; coordination passed with 30 historical expired warnings. +- Remaining risks: proprietary ADT visual/p95/p99 evidence is unavailable; + resource observation/requests, action execution, permits and engine lifetime + intentionally remain loader-owned. +- Documentation updated: new `m2-build-dispatch-planner.md` with API/I/O, + data-flow, lifecycle, sequence, dependency and ownership diagrams; batch/queue/ + world renderer specs, module registry, RENDER and M03 Evidence updated.