simvx.graphics.app¶
Graphics application layer: engine wrapper with node tree support.
Module Contents¶
Classes¶
The window and frame loop that runs a scene. |
Data¶
API¶
- simvx.graphics.app.log¶
‘getLogger(…)’
- simvx.graphics.app.__all__¶
[‘App’, ‘AVAILABLE_BACKENDS’]
- class simvx.graphics.app.App(title: str = 'SimVX', width: int = 1280, height: int = 720, backend: str | None = None, physics_fps: int = 60, target_fps: int | None = None, visible: bool = True, vsync: bool = True, bg_colour: tuple[float, float, float, float] | str | None = None, audio_sample_rate: int = 48000, audio_channels: int = 2, render_thread: bool = False, multi_gpu: bool = False, splash: Any = None, splash_min_time: float | None = None, unbranded: bool = False, **_kwargs)¶
The window and frame loop that runs a scene.
Args: title: Window title. width: Initial window width in engine units (at least 1). height: Initial window height in engine units (at least 1). backend: Windowing backend:
"glfw","sdl3"or"qt".Nonepicks one that is installed. physics_fps: Fixed-timestep physics rate in Hz. Independent of the display rate. target_fps: Maximum display frame rate.None(default) is uncapped, limited only by vsync. Combine withvsync=Falseto make the cap govern on high-refresh displays. Writable at runtime through :attr:App.target_fps. visible: Open a visible window.Falsefor headless runs. vsync: Wait for vertical sync when presenting. bg_colour: Background clear colour:"transparent", an RGBA tuple, orNonefor the theme background. render_thread: Force the pipelined render thread on for this process.False(default) renders synchronously unless aWorldEnvironmentsetsrender_mode='pipelined'.Run a scene, a
Nodesubclass withon_readyandon_updatehooks::App(title="My Game").run(MyGame())
Or drive the loop with plain callbacks::
App(title="My Game").run(update=my_update, render=my_render)
Once
run()has opened the window,window_title,window_sizeandactive_backendreport the live state.Initialization
- classmethod current() simvx.graphics.app.App | None¶
Return the running app on any backend, or
Noneif none is alive.Prefer
node.appfrom inside Node subclasses (afteron_enter_tree); use this from non-Node helpers like audio callbacks, utility modules, or tools that don’t have a node handle.
- toggle_fullscreen() None¶
Toggle fullscreen mode.
- property is_fullscreen: bool¶
- property engine: simvx.graphics.engine.Engine | None¶
Access the graphics engine (available after run() starts).
- property capabilities¶
Immutable :class:
RenderCapabilitiessnapshot (host + GPU).Forwards to
app.engine.capabilities.Noneuntilrun()has initialised the Vulkan engine. Read-only; rendering config flows throughWorldEnvironment/Appconstruction, never through this object.
- property texture_manager¶
The renderer’s :class:
TextureManager(available after run() starts).Convenience accessor: forwards to
app.engine.texture_managerso scenes that upload runtime-generated pixels (e.g. rasterised text) don’t need to reach into_engine.
- property scene_adapter¶
Scene adapter bridging SceneTree to the renderer (available after run() starts).
- register_lut(tex_id: int, lut_data) int¶
Register a 3D colour-grading LUT under
tex_id(available after run() starts).Forwards to :meth:
Engine.register_lut. Drive the LUT from a scene viaWorldEnvironment.lut_tex_id+lut_enabled; the engine binds the registered 3D LUT in the tonemap pass.
- property active_backend: str | None¶
Return the resolved backend name (e.g.
"glfw"), orNoneif the engine has not yet initialised a window.
- property window_title: str¶
Current window title. Writable once the window exists.
- property window_size: tuple[int, int]¶
The window’s
(width, height)right now, in engine units.Read from the window every time, so a drag-resize, a fullscreen toggle and a browser canvas resize all reach it. Before a window exists this is the size one will be asked for.
In engine units, which is to say the window’s logical size: the same number :attr:
~simvx.core.scene_tree.SceneTree.screen_sizecarries and thatnode.viewport_sizereports. Not the framebuffer’s, which is twice as many pixels per axis on a 2x display;Engine.widthis that one, and it belongs to the swapchain rather than to layout.Writable: assigning resizes the window, or sets the size one will be created at.
- property width: int¶
The window’s width right now, in engine units. See :attr:
window_size.A report rather than a setting: resize through
window_size, which is the one door, so that this always answers what the window is rather than what it was last told.
- property height: int¶
The window’s height right now, in engine units. See :attr:
window_size.
- property cursor_pos: tuple[float, float]¶
Current cursor position in screen coordinates,
(0.0, 0.0)before run().
- property vsync: bool¶
Whether vertical sync is currently enabled.
- set_vsync(value: bool) None¶
Toggle vsync at runtime.
Before
run()this just stores the boot-time value. Afterrun(), the engine recreates the swapchain with the new present mode (FIFO when on, MAILBOX/IMMEDIATE when off).
- property target_fps: int | None¶
Maximum display frame rate in Hz, or
Nonefor uncapped.Read/write at any time, including mid-run, so a frame-cap row in an options menu applies on the next frame instead of the next launch. Accepts
None, or anything anint()accepts that lands>= 1; a cap below one raisesValueError.Four limits are worth knowing:
Under vsync a cap above the display’s refresh rate does nothing: the budget sleep sits on top of a blocking present. Pair a cap that must dominate with
vsync=False.The externally driven
begin/step/endpath (the editor viewport and the agent live session) is caller-clocked and ignores the cap by design.An invisible run is built uncapped so headless work goes flat out. Writing this property during one does apply the cap; reading it back always reports what the app was asked for, never what that run chose.
On web the exported page follows writes too: :class:
simvx.web.WebAppcarries the same property and the page’s loop reads it every frame.
- property time_scale: float¶
Global time-scale multiplier for the scene-tree tick.
Applied to
dtbefore delivery totree.process()andtree.physics_process().1.0is real-time,0.3is 30% speed (slow-motion / hitstop),2.0is double speed,0.0is paused (still ticks, with zero dt). Default1.0.Scope is intentionally narrow: only the scene-tree dt is scaled. Audio playback rate, coroutine pacing, and the wall-clock physics accumulator are NOT touched: slowing audio + coroutines requires a separate design pass. Read :attr:
simvx.core.SceneTree.nowfor the scaled scene clock.
- property last_draw2d_draw_count: int | None¶
The 2D draw count (bindless item submit) of the last telemetry refresh.
Convenience read of
last_telemetry["draw2d_draw_count"], so it follows that attribute’s refresh contract: the final rendered frame of a headless run, the previous frame of a windowed synchronous one.Nonewhen no refresh has happened yet.
- run(root_or_update: Any = None, *, update: collections.abc.Callable[[], None] | None = None, render: collections.abc.Callable[[object, tuple[int, int]], None] | None = None) None¶
Run the graphics engine.
Args: root_or_update: Either a Node (scene root) or a per-frame update callback. When a Node is passed, SceneTree and input are set up automatically. update: Per-frame callback (alternative to the positional arg). render: Custom render callback (cmd, extent). Only used in callback mode.
- classmethod force_headless(frames: int, *, screenshot: str | pathlib.Path | None = None) collections.abc.Iterator[None]¶
Make every
App.run(root)inside the block run headless forframesframes.This runs a program that was written to open a window, unmodified: its own
App(...).run(root)keeps the size and settings it chose and renders offscreen on a fixed timestep, as :meth:run_headlessdoes, then returns. An exception raised by the game propagates out ofrun()as it would in a window.simvx run FILE --headlessis built on it. Code that builds its own scene calls- Meth:
run_headlessdirectly instead.
Args: frames: How many frames each run lasts. At least 1. screenshot: Where to write the last frame as a PNG.
Nonewrites nothing.
- quit() None¶
Request a clean shutdown of the running app.
Safe to call from process/physics_process or input handlers. The main loop exits at the end of the current frame; audio and Vulkan resources are torn down in the usual finally-paths. Prefer this over
sys.exitwhich can leave background threads (e.g. miniaudio) alive and stall process exit.
- run_headless(root_node: Any, *, frames: int = 1, on_frame: collections.abc.Callable[[int, float], bool | None] | None = None, capture_frames: list[int] | None = (), capture_fn: collections.abc.Callable[[int], bool] | None = None) list¶
Run the engine headlessly for frames frames and return captured pixels.
Args: root_node: Scene root node. frames: Total number of frames to simulate. on_frame: Optional callback invoked with (frame_index, time) before each frame. Return False to stop early. capture_frames: Which frame indices to capture. Defaults to
(): no capture. Per-frame swapchain readback costs ~4 ms/frame (vkQueueWaitIdle+vkDeviceWaitIdle+ host copy), so the default is no-capture so that tests/CI/benchmarks measure real frame work, not host-side readback. Pass an explicit list of frame indices (e.g.[frames - 1]for the final frame) to opt in, orNoneto capture every frame. capture_fn: Dynamic capture predicate: called with frame index, captures if True. Takes precedence over capture_frames when provided.Returns: List of (H, W, 4) uint8 RGBA numpy arrays, one per captured frame (empty when the default
capture_frames=()is used).
- run_streaming(root_node: Any, server: Any) None¶
Run the engine headlessly and stream frames to browsers over WebSocket.
Args: root_node: Scene root node. server: A
StreamingServerinstance (fromsimvx.graphics.streaming).