# 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)