simvx.graphics.app

Graphics application layer: engine wrapper with node tree support.

Module Contents

Classes

App

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". None picks 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 with vsync=False to make the cap govern on high-refresh displays. Writable at runtime through :attr:App.target_fps. visible: Open a visible window. False for headless runs. vsync: Wait for vertical sync when presenting. bg_colour: Background clear colour: "transparent", an RGBA tuple, or None for the theme background. render_thread: Force the pipelined render thread on for this process. False (default) renders synchronously unless a WorldEnvironment sets render_mode='pipelined'.

Run a scene, a Node subclass with on_ready and on_update hooks::

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_size and active_backend report the live state.

Initialization

classmethod current() → simvx.graphics.app.App | None

Return the running app on any backend, or None if none is alive.

Prefer node.app from inside Node subclasses (after on_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:RenderCapabilities snapshot (host + GPU).

Forwards to app.engine.capabilities. None until run() has initialised the Vulkan engine. Read-only; rendering config flows through WorldEnvironment / App construction, never through this object.

property texture_manager

The renderer’s :class:TextureManager (available after run() starts).

Convenience accessor: forwards to app.engine.texture_manager so 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 via WorldEnvironment.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"), or None if 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_size carries and that node.viewport_size reports. Not the framebuffer’s, which is twice as many pixels per axis on a 2x display; Engine.width is 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. After run(), 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 None for 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 an int() accepts that lands >= 1; a cap below one raises ValueError.

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 / end path (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.WebApp carries 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 dt before delivery to tree.process() and tree.physics_process(). 1.0 is real-time, 0.3 is 30% speed (slow-motion / hitstop), 2.0 is double speed, 0.0 is paused (still ticks, with zero dt). Default 1.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.now for 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. None when 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 for frames frames.

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_headless does, then returns. An exception raised by the game propagates out of run() as it would in a window. simvx run FILE --headless is built on it. Code that builds its own scene calls

Meth:

run_headless directly instead.

Args: frames: How many frames each run lasts. At least 1. screenshot: Where to write the last frame as a PNG. None writes 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.exit which 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, or None to 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 StreamingServer instance (from simvx.graphics.streaming).