# M2 Build Batch Planner

## Metadata

| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-M2-BUILD-BATCH-PLANNER-001` |
| Owners | Pure static/animated M2 batch sizing and group cursor progression |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-build-batch-planner`, 2026-07-17 |
| Profiles/capabilities | Existing static MultiMesh and animated-instance build paths |

## Purpose

Select the effective per-tick M2 batch limit, cap it to remaining transforms and
plan the next group offset without accessing loader state or renderer resources.

## Non-goals

- Own M2 build jobs, queues, tile state, retries or cancellation.
- Decide whether an animated prototype or static mesh is ready.
- Rotate queues or consume `RenderBudgetScheduler` permits.
- Create or modify Nodes, MultiMesh, Mesh, materials or RIDs.
- Introduce spatial cells or change profile batch limits.

## Context and boundaries

```mermaid
flowchart LR
    Groups[M2PlacementGrouper transform arrays] --> Loader[Loader build loop]
    Limits[Animated/static batch limits] --> Planner[M2BuildBatchPlanner]
    Loader --> Planner
    Planner --> Plan[Detached batch plan]
    Plan --> Loader
    Loader --> Dispatch[M2BuildDispatchPlanner]
    Dispatch --> Materialize[Animated or MultiMesh materialization]
    Loader --> Budget[RenderBudgetScheduler permit]
```

Allowed dependencies are integer/boolean scalar math and a fresh Dictionary.
Node, SceneTree, RenderingServer, ResourceLoader, WorkerThreadPool, mutexes,
renderer caches, gameplay, network, editor UI and files are forbidden.

## Public API

| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `plan_batch(transform_count, current_offset, has_animated_prototype, animated_batch_limit, static_batch_limit)` | Pure query | Return the selected batch size/count and next group cursor | Any thread; call-local result | Non-positive selected limit clamps to one; empty/past-end range completes |

## Inputs and outputs

| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Transform count | Grouped transform array | Planner remaining-count formula | Scalar copy | One plan |
| Input | Current group offset | Loader build job | Planner cursor formula | Scalar copy | One plan |
| Input | Animated prototype availability | Loader resource selection | Planner limit selector | Scalar copy | One plan |
| Input | Animated/static raw limits | Renderer profile/exports | Planner clamp | Scalar copy | One plan |
| Output | `effective_batch_size` | Planner | Tests/diagnostics | Detached integer | One plan |
| Output | `batch_count` | Planner | Loader materialization adapter | Detached integer | One plan |
| Output | `next_offset` | Planner | Loader build-job cursor | Detached integer | Until next batch |
| Output | `group_complete` | Planner | Loader group-index progression | Detached boolean | One plan |

The output Dictionary is newly allocated and caller-owned. The planner retains
no reference to it or any input.

## Data flow

```mermaid
flowchart TD
    Select{Animated prototype?} -->|yes| Animated[animated_batch_limit]
    Select -->|no| Static[static_batch_limit]
    Animated --> Clamp[max 1]
    Static --> Clamp
    Clamp --> Remaining[max 0 of transform_count - current_offset]
    Remaining --> Count[min effective limit, remaining]
    Count --> Complete{count <= 0 or offset + count >= total?}
    Complete -->|yes| Done[group_complete true; next_offset 0]
    Complete -->|no| Continue[group_complete false; next_offset offset + count]
```

## Lifecycle/state

The planner is stateless. It has no initialization, reset, cancellation,
recovery or shutdown work. Each call returns a complete detached value plan.

## Main sequence

```mermaid
sequenceDiagram
    participant Loader as StreamingWorldLoader build loop
    participant Planner as M2BuildBatchPlanner
    participant Materializer as Animated/MultiMesh adapter
    participant Scheduler as RenderBudgetScheduler
    Loader->>Planner: plan_batch(count, offset, path kind, limits)
    Planner-->>Loader: batch_count, next_offset, group_complete
    Loader->>Loader: resolve/request prototype or mesh
    Loader->>Materializer: materialize selected transform slice
    Loader->>Loader: adopt cursor/group completion
    Loader->>Scheduler: consume M2_BUILD permit
```

## Ownership, threading and resources

- The planner owns only call-local scalar values and the returned Dictionary.
- `M2BuildQueue` owns typed jobs, FIFO ordering, serial numbers and group/offset
  cursors. `M2BuildDispatchPlanner` owns the pure resource-state action decision;
  the loader owns tile checks and native-first orchestration, while static and
  cached animation observers own their observation/retry phases.
- 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.

## Errors, cancellation and recovery

| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Zero/negative selected limit | Scalar clamp | Effective size becomes one | Contract fixture | Correct profile value later |
| Empty transform group | Remaining count zero | Count zero; group completes | Contract fixture | Loader advances group |
| Offset at/past end | Remaining count clamp | Count zero; offset resets | Contract fixture | Loader advances group |
| Negative offset | No new validation | Historical arithmetic is preserved | Contract fixture | Loader inputs should remain valid |
| Resource pending/missing | Outside planner | Loader rotates or advances as before | Existing cache/build diagnostics | Resource loader retry/fallback |
| Tile cancelled | Outside planner | Loader cancels job before planning/materialization | Existing lifecycle gates | Eligible tile may requeue |

## Configuration and capabilities

The planner introduces no setting. It consumes existing
`m2_animated_instances_per_tick` and `m2_multimesh_batch_size` values after the
loader determines whether an animated prototype is available.

## Persistence, cache and migration

No persistence, schema or cache format participates. ADT/M2 cache versions,
quality presets and rebuild instructions are unchanged.

## Diagnostics and observability

The planner emits no logs. Its verifier covers deterministic formula/source
boundaries and records bounded 20,000-plan timing. Existing loader metrics retain
queue depth, build activity and hitch observability.

## Verification

- `verify_m2_build_batch_planner.gd`: static/animated selection, invalid-limit
  clamp, remaining cap, exact completion, empty/past-end and negative-offset
  behavior, loader/dependency source boundaries and bounded timing.
- Grouper, transform resolver, unique registry/dedupe, facade, internal-access,
  manifest, shutdown, scheduler, streaming, coordinate, documentation and
  coordination regressions remain required.
- Fidelity evidence is formula extraction only. Asset-backed frame pacing and
  visual output require the renderer checkpoint/performance workflow.

## Extension points

- Remaining native resource observation may be extracted 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.

## Capability status

| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| 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 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

- The planner result remains a Dictionary; `M2BuildQueue` adopts its scalar
  cursor values through one explicit progress call.
- Negative offsets are preserved rather than rejected to avoid a hidden behavior change.
- Planning timing does not measure resource loading or materialization.
- Private asset-backed p95/p99 and visual comparison remain unavailable.

## Source map

| 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 |
| `src/render/m2/m2_animation_playback_controller.gd` | Animated-instance phase/selection/native/imported playback |
| `src/render/m2/m2_animated_instance_materializer.gd` | Planned animated-slice duplication, playback startup and attachment |
| `src/render/m2/m2_placement_grouper.gd` | Produces grouped transform arrays |
| `src/tools/verify_m2_build_batch_planner.gd` | Formula, source and timing regression |

## Related decisions and references

- [`m2-placement-grouper.md`](m2-placement-grouper.md)
- [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md)
- [`m2-static-batch-materializer.md`](m2-static-batch-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)
