# Render Budget Scheduler

## Metadata

| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | `M03-RND-SCHEDULER-001` |
| Owners | Renderer workstream / M03 integrator |
| Last verified | Worktree `work/sindo-main-codex/m03-render-budget-scheduler`, 2026-07-16 |
| Profiles/capabilities | Runtime and Editor main-thread operation quotas; profile values supplied by streamer composition |

## Purpose

Issue a bounded number of permits for each renderer operation lane during one
process frame. The scheduler centralizes quota accounting and teardown
cancellation without taking ownership of queues or render work.

## Non-goals

- Own, sort, drain or cap the stored length of a renderer queue.
- Submit, cancel or await worker and `ResourceLoader` tasks.
- Execute callbacks or mutate the SceneTree, resources, caches or RIDs.
- Adapt limits from elapsed time or create a general job/dependency framework.
- Select renderer quality or compatibility profiles.

## Context and boundaries

```mermaid
flowchart LR
    Config[Streamer exported per-frame limits] --> Frame[operation limit snapshot]
    Frame --> Scheduler[RenderBudgetScheduler]
    Queues[StreamingWorldLoader owned queues] --> Drains[Ordered queue drains]
    Scheduler -->|boolean permits| Drains
    Drains --> Render[Main-thread finalize/attach/evict]
    Shutdown[StreamingWorldLoader exit] -->|cancel| Scheduler
    Scheduler -. does not own .-> Queues
```

Allowed dependencies are Godot value containers and `RefCounted`. The module
must not depend on `Node`, `WorkerThreadPool`, `ResourceLoader`, `RenderingServer`,
mutexes, renderer assets or streamer queue fields.

## Public API

| Symbol | Kind | Purpose | Thread/lifetime | Failure behavior |
|---|---|---|---|---|
| Lane constants | `StringName` constants | Stable operation categories used in the frame limit map | Process lifetime | Unknown lanes have zero permits |
| `begin_frame` | Method | Replaces all limits and clears consumption diagnostics for one frame | Main thread; once before drains | Negative limits clamp to zero; cannot revive a cancelled instance |
| `has_remaining_permit` | Query | Tests a lane without consuming | Main thread; current frame | Unknown/exhausted/cancelled returns `false` |
| `try_consume_permit` | Mutation | Atomically checks and consumes one caller operation permit | Main thread; current frame | Unknown/exhausted/cancelled returns `false` |
| `remaining_permits` | Query | Returns a lane's current unconsumed count | Main thread; current frame | Unknown/cancelled returns zero |
| `consumed_permits_snapshot` | Query | Returns detached per-lane diagnostics | Caller owns returned dictionary | Mutation cannot affect scheduler state |
| `cancel` | Lifecycle method | Permanently stops new permits and clears remaining limits | Main thread; renderer teardown | Idempotent; does not interrupt in-flight caller work |
| `is_cancelled` | Query | Reports terminal lifecycle state | Scheduler lifetime | No failure |

## Inputs and outputs

| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | `Dictionary[StringName, int]` operation limits | `StreamingWorldLoader` configuration snapshot | Scheduler | Copied by scheduler | One process frame |
| Input | Lane ID | Ordered streamer drain | Scheduler | Constant value | One permit check |
| Input | Cancellation | Streamer `_exit_tree` | Scheduler | Terminal flag | Renderer instance lifetime |
| Output | Permit boolean | Scheduler | Streamer drain loop | Scalar | One attempted operation |
| Output | Remaining count | Scheduler | Streamer/tests | Scalar | Current frame |
| Output | Consumed snapshot | Scheduler | Diagnostics/tests | Detached caller-owned copy | Current frame snapshot |

The scheduler has no I/O, logging, SceneTree, task, cache or GPU side effects.

## Data flow

```mermaid
flowchart TD
    Exports[Exported operation limits] --> Limits[_render_operation_limits_for_frame]
    Limits --> Begin[begin_frame]
    Begin --> Remaining[remaining permits by lane]
    Queue[Non-empty caller-owned queue] --> Check[try_consume_permit]
    Remaining --> Check
    Check -->|true| Execute[Caller performs one operation]
    Check -->|false| Defer[Operation remains queued]
    Execute --> Consumed[consumed permits diagnostics]
```

## Lifecycle and state

```mermaid
stateDiagram-v2
    [*] --> Ready
    Ready --> FrameActive: begin_frame
    FrameActive --> FrameActive: consume/check
    FrameActive --> FrameActive: begin_frame resets limits
    Ready --> Cancelled: cancel
    FrameActive --> Cancelled: cancel
    Cancelled --> Cancelled: begin_frame/check/cancel
    Cancelled --> [*]
```

Cancellation is intentionally terminal. A new renderer scene creates a new
scheduler instance; an old instance cannot accidentally resume after teardown.

## Main sequence

```mermaid
sequenceDiagram
    participant Stream as StreamingWorldLoader
    participant Budget as RenderBudgetScheduler
    participant Queue as Loader-owned queues
    participant Render as SceneTree/RenderingServer
    Stream->>Budget: begin_frame(operation limits)
    loop existing fixed drain order
        Stream->>Queue: inspect next operation
        Stream->>Budget: try_consume_permit(lane)
        Budget-->>Stream: true or false
        Stream->>Render: execute one operation when true
    end
    Stream->>Budget: cancel() during _exit_tree
    Budget-->>Stream: later permits rejected
    Stream->>Stream: await tasks and release owned resources
```

## Ownership, threading and resources

- `StreamingWorldLoader` owns every queue, task registry, cache, Node, Resource and RID.
- The scheduler owns two small per-frame counter dictionaries and one terminal flag.
- The streamer preserves operation order. In particular, chunk removals and
  creations share `CHUNK_GEOMETRY`, with removals drained first.
- All current scheduler calls occur on the Godot main thread. The service has no
  mutex and is not safe for concurrent mutation.
- A scheduler instance lives exactly as long as its loader instance.

## Errors, cancellation and recovery

| Condition | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Negative/zero limit | `begin_frame` clamp | Lane issues no permits | Contract verifier | Correct configuration or accept disabled lane |
| Unknown lane | Permit lookup miss | Returns `false`/zero | Contract verifier | Add the lane to the frame snapshot |
| Exhausted lane | Remaining count is zero | Work remains in caller queue for a later frame | Queue-depth/hitch metrics | Next `begin_frame` replenishes permits |
| Renderer teardown | Streamer calls `cancel` before task waits | New permits stop immediately | Shutdown ownership regression | New scene creates a new scheduler |
| In-flight operation at cancel | Caller already consumed permit | Scheduler does not interrupt it | Ownership docs | Caller completes/cleans it through existing shutdown path |

This cancellation scope is permit issuance, not worker cancellation. Existing
task/result staleness and shutdown waits remain streamer responsibilities.

## Configuration and capabilities

The streamer's existing exported values populate 16 lanes: tile finalization,
terrain upgrade/control-splat/splat-cache/splat-build, water finalization, shared
chunk geometry, tile-load starts, tile LOD create/remove, M2 animation/mesh
finalization and build, WMO instance/group build, and detail synchronization.
Defaults and quality-preset assignments are unchanged. `tile_finalize` retains
its historical minimum of one; other negative values behave as zero.

## Persistence, cache and migration

The module is runtime-only and serializes nothing. It changes no cache, asset,
scene, protocol or database format and requires no version bump or migration.

## Diagnostics and observability

- `consumed_permits_snapshot` exposes detached per-frame counts for tests and
  future facade metrics without exposing mutable scheduler state.
- Existing queue depths, named hitch sections and `HITCH`/`PERF` logs remain in
  `StreamingWorldLoader`.
- No scheduler logging occurs on the hot permit path.

## Verification

- `src/tools/verify_render_budget_scheduler.gd` covers exact exhaustion,
  independent lanes, shared removal/create priority, frame reset, invalid/unknown
  lanes, detached diagnostics, terminal cancellation and source boundaries.
- The verifier performs 20,000 permit checks with a generous 150 ms regression
  ceiling on the headless runner; this is a guard, not a renderer p95 claim.
- Integration regressions must retain the seven renderer checkpoint plans and
  existing focus/planner/facade contracts.
- Fidelity evidence is behavior-preserving: exported values, process order,
  cache versions and visual operations are unchanged. No build-12340 parity
  claim follows from this extraction.

## Extension points

- Add facade consumption metrics using detached snapshots.
- Move a service's queue drain behind its own boundary while retaining these lane IDs.
- Add measured time-aware policy only after asset-backed p95/p99 evidence proves
  fixed operation counts insufficient; do not embed callbacks in this scheduler.

## Capability status

| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Per-lane frame bounds | Implemented | Exact exhaustion and independent-lane contracts | Asset-backed p95/p99 baseline remains |
| Shared removal/create priority | Implemented | Synthetic shared `CHUNK_GEOMETRY` contract | Other service priorities remain streamer-owned |
| Teardown permit cancellation | Implemented | Terminal cancellation contract and loader source gate | Worker stale-result cancellation remains streamer-owned |
| Detached consumption diagnostics | Implemented | Snapshot isolation contract | Not yet exposed through facade metrics |

## Known gaps and risks

- Queue storage length is not capped; only per-frame draining is bounded.
- Worker concurrency and stale-result cancellation remain existing streamer logic.
- Exact p95/p99 impact requires local extracted assets and paired baseline runs.
- Lane IDs are dynamic dictionary keys; the verifier guards the current contract.

## Source map

| Path | Responsibility |
|---|---|
| `src/render/streaming/render_budget_scheduler.gd` | Scene-free lane counters, permits, diagnostics and cancellation |
| `src/scenes/streaming/streaming_world_loader.gd` | Limit composition, fixed drain order, queue/resource ownership and operation execution |
| `src/tools/verify_render_budget_scheduler.gd` | Asset-free behavior, dependency and bounded timing regression |

## Related decisions and references

- [`world-renderer.md`](world-renderer.md)
- [`streaming-target-planner.md`](streaming-target-planner.md)
- [`../../targets/03-renderer-facade.md`](../../targets/03-renderer-facade.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
- [`../CODING_STANDARD.md`](../CODING_STANDARD.md)
