Files
open-wc/docs/modules/render-budget-scheduler.md

11 KiB

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

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

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

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

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