217 lines
11 KiB
Markdown
217 lines
11 KiB
Markdown
# 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)
|