simvx.graphics.frame_loop¶
FrameLoop: the single per-frame engine drive seam (clock + sink + step).
Historically App._run_with_tree (visible), App.run_headless,
App.run_streaming, and the editor’s PlayMode each hand-rolled the same
physics -> interpolate -> tick -> Draw2D -> submit_scene -> present/capture
core plus an identical pipelined frame_driver. FrameLoop owns that core once;
the per-loop differences are expressed as three pluggable pieces:
Clock: how wall time advances and the physics-stepping strategy (
RealTimeClockaccumulator vsFixedStepClocksingle-step).Sink: where the rendered frame goes and how capture is wired (
SwapchainSinkpresent,OffscreenSinkreadback,StreamSinkpush).Integration: input source + windowed extras (callbacks, gamepad, resize, picking) – supplied per loop.
The LLM agent live-session and the editor viewport are just FixedStepClock
(externally driven) + an offscreen or swapchain sink. This module is migrated
one consumer at a time; run_headless is the first to land on it.
Module Contents¶
Classes¶
Decides per-frame dt and how physics/logic advance. |
|
Fixed timestep: one physics step per frame, interpolate at the step boundary. |
|
Wall-clock dt with a fixed-timestep physics accumulator (windowed / streaming). |
|
Where the frame goes; supplies the render-thread capture + sync readback. |
|
Present to the window; no readback. Windowed games. |
|
Read frames back into |
|
Push each drawn frame to connected browser clients over WebSocket. |
|
Per-loop input source and windowed extras. |
|
Batch headless: InputSimulator drives Input; capture + telemetry at the end. |
|
Browser streaming: drain queued client input, push readback to clients. |
|
Visible game window: platform input callbacks, gamepad, resize, picking. |
|
A unit of work drained once per frame on the loop (main) thread. |
|
Ordered registry of :class: |
|
One per-frame engine drive seam, parameterised by clock + sink + integration. |
Functions¶
Snapshot of currently-running :class: |
Data¶
API¶
- simvx.graphics.frame_loop.log¶
‘getLogger(…)’
- simvx.graphics.frame_loop.active_loops() list[FrameLoop][source]¶
Snapshot of currently-running :class:
FrameLoopinstances (most recent last).
- class simvx.graphics.frame_loop.FrameClock[source]¶
Decides per-frame dt and how physics/logic advance.
- on_begin(app: simvx.graphics.app.App) None[source]¶
Called once after setup, before the first frame.
- abstractmethod advance(app: simvx.graphics.app.App) float[source]¶
- abstractmethod step_physics(app: simvx.graphics.app.App, tree: Any, frame_dt: float) None[source]¶
- tick_logic(app: simvx.graphics.app.App, tree: Any, frame_dt: float) None[source]¶
- class simvx.graphics.frame_loop.FixedStepClock(*, frames: int | None = None, dt: float | None = None)[source]¶
Bases:
simvx.graphics.frame_loop.FrameClockFixed timestep: one physics step per frame, interpolate at the step boundary.
With
framesset this is the headless batch clock; withframes=Noneand an explicitdtit is the externally-driven clock the agent / editor use (the caller advances frames, no internal cap).Initialization
- advance(app: simvx.graphics.app.App) float[source]¶
- step_physics(app: simvx.graphics.app.App, tree: Any, frame_dt: float) None[source]¶
- tick_logic(app: simvx.graphics.app.App, tree: Any, frame_dt: float) None[source]¶
- on_begin(app: simvx.graphics.app.App) None¶
- class simvx.graphics.frame_loop.RealTimeClock[source]¶
Bases:
simvx.graphics.frame_loop.FrameClockWall-clock dt with a fixed-timestep physics accumulator (windowed / streaming).
Initialization
- on_begin(app: simvx.graphics.app.App) None[source]¶
- advance(app: simvx.graphics.app.App) float[source]¶
- step_physics(app: simvx.graphics.app.App, tree: Any, frame_dt: float) None[source]¶
- should_stop(frame_idx: int) bool¶
- tick_logic(app: simvx.graphics.app.App, tree: Any, frame_dt: float) None¶
- class simvx.graphics.frame_loop.FrameSink[source]¶
Where the frame goes; supplies the render-thread capture + sync readback.
- ring_capacity: int¶
2
- render_thread_capture() collections.abc.Callable[[int, Any], None] | None[source]¶
Callback the pipelined RenderThread runs after present (or None).
- capture_synchronous(loop: simvx.graphics.frame_loop.FrameLoop, frame_idx: int) None[source]¶
Synchronous-mode readback of the just-drawn frame (no-op by default).
- class simvx.graphics.frame_loop.SwapchainSink[source]¶
Bases:
simvx.graphics.frame_loop.FrameSinkPresent to the window; no readback. Windowed games.
- ring_capacity¶
2
- render_thread_capture() collections.abc.Callable[[int, Any], None] | None¶
- capture_synchronous(loop: simvx.graphics.frame_loop.FrameLoop, frame_idx: int) None¶
- class simvx.graphics.frame_loop.OffscreenSink(*, capture_frames: collections.abc.Sequence[int] | None = (), capture_fn: collections.abc.Callable[[int], bool] | None = None)[source]¶
Bases:
simvx.graphics.frame_loop.FrameSinkRead frames back into
captured(headless tests, agent capture).Initialization
- ring_capacity¶
1
- capture_synchronous(loop: simvx.graphics.frame_loop.FrameLoop, frame_idx: int) None[source]¶
- class simvx.graphics.frame_loop.StreamSink(server: Any)[source]¶
Bases:
simvx.graphics.frame_loop.FrameSinkPush each drawn frame to connected browser clients over WebSocket.
Initialization
- ring_capacity¶
2
- capture_synchronous(loop: simvx.graphics.frame_loop.FrameLoop, frame_idx: int) None[source]¶
- class simvx.graphics.frame_loop.FrameIntegration[source]¶
Per-loop input source and windowed extras.
- reset_globals: bool¶
False
- on_run_begin(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
After the engine + tree exist, before the loop (e.g. start a stream server).
- on_run_end(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
In the run() finally, before audio shutdown (e.g. stop a stream server).
- on_setup(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
Extra setup after the adapter exists (input callbacks, clipboard).
- pre_tick(loop: simvx.graphics.frame_loop.FrameLoop) bool[source]¶
Input drain / window sync before the tick. Return False to stop the loop.
- mid_tick(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
Between physics and logic tick (theme sync on the windowed path).
- post_tick(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
After Draw2D submit (mouse picking on the windowed path).
- per_frame_telemetry(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
Live per-frame telemetry (windowed path).
- on_loop_end(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
At the stop boundary (headless captures final telemetry here).
- pre_render(loop: simvx.graphics.frame_loop.FrameLoop, cmd: Any) None[source]¶
Offscreen passes before the main render pass (SubViewports, probes).
- class simvx.graphics.frame_loop.HeadlessIntegration(on_frame: collections.abc.Callable[[int, float], bool | None] | None = None)[source]¶
Bases:
simvx.graphics.frame_loop.FrameIntegrationBatch headless: InputSimulator drives Input; capture + telemetry at the end.
Initialization
- reset_globals¶
True
- pre_tick(loop: simvx.graphics.frame_loop.FrameLoop) bool[source]¶
- on_loop_end(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
- on_run_begin(loop: simvx.graphics.frame_loop.FrameLoop) None¶
- on_run_end(loop: simvx.graphics.frame_loop.FrameLoop) None¶
- on_setup(loop: simvx.graphics.frame_loop.FrameLoop) None¶
- mid_tick(loop: simvx.graphics.frame_loop.FrameLoop) None¶
- post_tick(loop: simvx.graphics.frame_loop.FrameLoop) None¶
- per_frame_telemetry(loop: simvx.graphics.frame_loop.FrameLoop) None¶
- pre_render(loop: simvx.graphics.frame_loop.FrameLoop, cmd: Any) None¶
- class simvx.graphics.frame_loop.StreamingIntegration(server: Any)[source]¶
Bases:
simvx.graphics.frame_loop.FrameIntegrationBrowser streaming: drain queued client input, push readback to clients.
Initialization
- on_run_begin(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
- on_run_end(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
- pre_tick(loop: simvx.graphics.frame_loop.FrameLoop) bool[source]¶
- pre_render(loop: simvx.graphics.frame_loop.FrameLoop, cmd: Any) None[source]¶
- reset_globals: bool¶
False
- on_setup(loop: simvx.graphics.frame_loop.FrameLoop) None¶
- mid_tick(loop: simvx.graphics.frame_loop.FrameLoop) None¶
- post_tick(loop: simvx.graphics.frame_loop.FrameLoop) None¶
- per_frame_telemetry(loop: simvx.graphics.frame_loop.FrameLoop) None¶
- on_loop_end(loop: simvx.graphics.frame_loop.FrameLoop) None¶
- class simvx.graphics.frame_loop.WindowedIntegration[source]¶
Bases:
simvx.graphics.frame_loop.FrameIntegrationVisible game window: platform input callbacks, gamepad, resize, picking.
Gamepad poll / window-resize / theme sync run in
mid_tick(after physics, before the logic tick) to match the original_run_with_treeordering.Initialization
- on_run_end(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
- on_setup(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
- mid_tick(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
- post_tick(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
- per_frame_telemetry(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
- reset_globals: bool¶
False
- on_run_begin(loop: simvx.graphics.frame_loop.FrameLoop) None¶
- pre_tick(loop: simvx.graphics.frame_loop.FrameLoop) bool¶
- on_loop_end(loop: simvx.graphics.frame_loop.FrameLoop) None¶
- pre_render(loop: simvx.graphics.frame_loop.FrameLoop, cmd: Any) None¶
- class simvx.graphics.frame_loop.FrameService[source]¶
A unit of work drained once per frame on the loop (main) thread.
The contract is the marshalling pattern external drivers (PEP 768 socket bridge, editor tooling) need: a producer thread enqueues work, and this runs on the one thread that owns the SceneTree, AT the post-tick barrier (tree fully ticked + Draw2D built, GPU handoff not yet started). Implement
- Meth:
on_frame_service; keep it bounded and non-blocking.
- on_frame_service(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
Drain queued work on the loop thread. Must not block the frame.
- on_loop_end(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
Release resources when the owning loop tears down (close sockets, join threads).
- class simvx.graphics.frame_loop.FrameServices[source]¶
Ordered registry of :class:
FrameServiceobjects drained each frame.Loop-level (not integration-level) so every clock/sink/integration combo – windowed, headless, streaming, and the externally-driven agent/editor path – reaches the same barrier through
FrameLoop._updatewith zero per-consumer wiring. Registration is the canonical way to attach a per-frame drain to a running loop; it is dependency-free (pure stdlib list).Initialization
- add(service: simvx.graphics.frame_loop.FrameService) simvx.graphics.frame_loop.FrameService[source]¶
Register a service. Returns it for
svc = loop.services.add(MyService()).
- remove(service: simvx.graphics.frame_loop.FrameService) None[source]¶
Deregister a service (idempotent).
- run(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
Drain every registered service on the loop thread, in registration order.
A failing service is fully logged and the remaining services still run; the loop must never be left half-drained or silently killed by one bad service.
- close(loop: simvx.graphics.frame_loop.FrameLoop) None[source]¶
Tell every service the loop is ending, then clear the registry.
Called from FrameLoop._teardown so a registered bridge (its accept thread + bound socket) is shut down with the loop instead of leaking until process exit.
- class simvx.graphics.frame_loop.FrameLoop(app: simvx.graphics.app.App, root_node: Any, *, clock: simvx.graphics.frame_loop.FrameClock, sink: simvx.graphics.frame_loop.FrameSink, integration: simvx.graphics.frame_loop.FrameIntegration, visible: bool, vsync: bool)[source]¶
One per-frame engine drive seam, parameterised by clock + sink + integration.
Initialization
- engine: simvx.graphics.engine.Engine¶
None
- begin() None[source]¶
Externally-driven setup: build the engine, create the window + device, run setup. Pair with :meth:
step_frame/ :meth:capture/ :meth:end. For an External (FixedStepClock) clock – the agent live-session and editor viewport.
- step_frame(dt: float | None = None) bool[source]¶
Advance exactly one frame; returns False when the loop should stop.