TheDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/VKSFY/keel/llms.txt
Use this file to discover all available pages before exploring further.
Scheduler owns all registered system functions and invokes them in Phase order every tick. App and World share a single Scheduler instance — systems registered through either @app.system(...) or @world.system(...) land in the same registry and run in the same order. You rarely construct a Scheduler directly; access the shared one through app.scheduler or world.scheduler.
Resource Injection
Beyond the mandatoryworld and dt arguments, a system may declare additional parameters with type annotations. The Scheduler resolves each annotated parameter by looking up the corresponding type in world._resources at call time.
Every extra parameter must have a type annotation. A parameter without an annotation raises
TypeError at registration time — not at runtime — so misconfigured systems fail immediately when the decorator is applied.Methods
Scheduler.register(phase, fn, after=None)
Register fn to run during phase. The scheduler inspects the function signature to discover resource types, then topologically re-sorts the phase list to respect after dependencies.
The execution phase. One of
PRE_UPDATE, UPDATE, POST_UPDATE, PRE_RENDER, RENDER, POST_RENDER.The system function. Its first two parameters should be
world (World) and dt (float). Any additional annotated parameters are treated as resource injections.One system function or a list/tuple of functions that must run before
fn within the same phase. Raises ValueError if:- Any listed function is registered in a different phase (cross-phase
afteris forbidden). - Any listed function has not been registered yet.
- Adding the dependency would create a cycle.
after is rejected:
Scheduler.systems(phase) -> list[Callable]
Return the ordered list of registered system functions for phase. The order reflects the topological sort derived from after dependencies, stable on registration order for independent systems.
The phase to query.
Functions registered in
phase, in execution order.Scheduler.clear()
Remove all registered systems from every phase and clear all ordering metadata. Useful in tests that build and tear down multiple schedulers within the same process.
Scheduler.run(world, dt)
Invoke every system once, iterating phases in IntEnum order (PRE_UPDATE → UPDATE → POST_UPDATE → PRE_RENDER → RENDER → POST_RENDER). Within each phase, systems run in topological (then registration) order.
The world to pass as the first argument to each system.
Delta time to pass as the second argument to each system.
Scheduler.tick(world, dt)
Alias for run(). Provided for symmetry with world.tick().
Scheduler.tick_simulation(world, dt)
Run only the simulation phases: PRE_UPDATE, UPDATE, POST_UPDATE. Used by the run loop’s FixedStepDriver to advance the simulation at a fixed 60 Hz rate independently of the display frame rate.
The world passed to each system.
Fixed simulation delta — always
keel.FIXED_DT (1/60 s) from the run loop.Scheduler.tick_render(world, dt)
Run only the render phases: PRE_RENDER, RENDER, POST_RENDER. Called once per visual frame with the real elapsed wall-clock time so render systems can interpolate smoothly between simulation ticks.
The world passed to each system.
Real elapsed frame time — may differ from
FIXED_DT depending on display refresh rate.Scheduler.tick_phases(world, dt, phases)
Invoke systems for an arbitrary ordered sequence of phases. Powers both tick_simulation and tick_render internally. Use this for custom execution splits (e.g. running only RENDER and POST_RENDER in a secondary render thread).
The world passed to each system.
Delta time passed to each system.
The phases to run, in the order you want them invoked.
Scheduler.attach_profiler(profiler)
Wrap every system invocation in profiler.begin(name) / profiler.end(name) timing calls. The profiler is also notified for the special "__frame__" marker from the run loop.
An object with
begin(name: str) and end(name: str) methods. Use keel.setup_profiler(app) to get the built-in frame profiler.Scheduler.detach_profiler()
Stop forwarding system timings. Useful when toggling the profiler at runtime.
Execution Model
The diagram below shows how the run loop orchestratesFixedStepDriver and the Scheduler each visual frame:
Poll Input
input_state.begin_frame() then window.swap_and_poll() — GLFW callbacks fire here, pushing new KeyEvent, MouseMoveEvent, etc. into the event bus.Simulation Ticks (0..N)
FixedStepDriver.step() drains the accumulator by calling scheduler.tick_simulation(world, FIXED_DT) and world.flush() for each 1/60 s interval that has elapsed since the last frame.