simvx.core.scene_step

The single canonical per-frame logic-phase sequence, shared by every embedder.

Consumers that drive a :class:~simvx.core.scene_tree.SceneTree one frame at a time (the windowed/headless/streaming :class:FrameLoop, the editor’s PlayMode and the headless :class:~simvx.core.testing.SceneRunner today; the AI rendered session and future embedders next) re-spell the same load-bearing order: pump polled input, step physics, dispatch queued UI events, run logic. The order is pure SceneTree logic (physics_tick / interpolate_physics / tick are core methods with zero Vulkan), so it lives in core where every embedder, including the ones that cannot import graphics, can share one definition. (The web runtime owns a deliberately different policy and is not routed through here yet: see its notes.)

func:

step_scene_logic runs exactly that sequence and stops BEFORE the GPU half (Draw2D reset + tree.render + submit/present) and BEFORE the windowed Input frame boundary (Input._end_frame / Input._new_frame): those are the caller’s job, because they diverge per embedder (swapchain present vs offscreen render-to-target vs nothing). This module imports nothing from graphics.

Per-phase variation is expressed through :class:ScenePhases: a frozen set of optional hooks. phases=None runs the engine-default behaviour (one fixed physics step + interpolate to the step boundary, then one logic tick). A caller that owns a different physics-stepping policy (the windowed accumulator clock) or a different per-node walk (the editor’s error-tolerant / profiled walkers) supplies node_walk; a caller that polls a device the platform does not deliver by callback supplies input_pump; a caller with a queued UI-event drain or a between-physics-and-logic window-sync supplies ui_events; a profiler supplies on_phase.

Module Contents

Classes

ScenePhases

Optional per-phase hooks for :func:step_scene_logic.

Functions

step_scene_logic

Run one frame of the canonical logic-phase sequence on tree.

Data

API

simvx.core.scene_step.__all__

[‘ScenePhases’, ‘step_scene_logic’]

class simvx.core.scene_step.ScenePhases[source]

Optional per-phase hooks for :func:step_scene_logic.

Every field defaults to None, which selects the engine-default behaviour for that phase. Supply a hook only to override one phase; the canonical order (input pump, then physics, then UI events, then logic) is fixed and not overridable. The fields are declared in that order and are keyword-only: four interchangeable callables are not something to pass by position.

Attributes: input_pump: called once at the very top of the frame, before physics, to publish input the platform does not deliver by callback. The frame’s coalesced pointer motion is delivered just before it, by the engine, for every caller. Controllers are polled, so this is where the poll and the flush of its edges belong: a pad edge published after the physics phase reaches a fixed tick a whole frame later than the same action bound to a key, which the window callback delivers before the frame starts. node_walk: (root, dt, phase) where phase is "physics" or "process". When supplied it replaces the default physics step and/or the default logic tick: the caller owns how the tree is walked for that phase (its own physics-stepping policy, an error-tolerant or profiled per-node walk, etc.). root is the scene root, or None when the tree has no root. ui_events: called once, after physics and before logic, to drain queued UI / input events into the tree. This is the same slot the windowed loop uses for window-size and theme sync and the editor uses to flush its viewport UI-event queue. on_phase: (name, edge) with edge in {"begin", "end"}, fired around each of "input", "physics", "ui", "process" so a profiler can bracket per-phase timings.

input_pump: collections.abc.Callable[[], None] | None

None

node_walk: collections.abc.Callable[[simvx.core.node.Node | None, float, str], None] | None

None

ui_events: collections.abc.Callable[[], None] | None

None

on_phase: collections.abc.Callable[[str, str], None] | None

None

simvx.core.scene_step.step_scene_logic(tree: simvx.core.scene_tree.SceneTree, dt: float, *, time_scale: float = 1.0, phases: simvx.core.scene_step.ScenePhases | None = None) → None[source]

Run one frame of the canonical logic-phase sequence on tree.

Order (load-bearing):

  1. input – tree.flush_pointer_motion(), then phases.input_pump() if supplied, so the frame’s pointer motion and a polled device’s edges are both published before any tick can read them.

  2. physics – tree.physics_tick(dt * time_scale) then tree.interpolate_physics(1.0), OR phases.node_walk(root, dt, "physics").

  3. ui – phases.ui_events() if supplied (no engine default).

  4. process – tree.tick(dt * time_scale), OR phases.node_walk(root, dt, "process").

Stops before Draw2D / submit / present and before the Input frame boundary: those stay with the caller. Imports nothing from graphics.

With phases=None this is byte-identical to a hand-rolled tree.physics_tick(dt * time_scale); tree.interpolate_physics(1.0); tree.tick(dt * time_scale).

Args: tree: the scene tree to advance. dt: frame delta in seconds (unscaled). time_scale: multiplier applied to dt for the default physics and logic steps (slow-mo / fast-forward). When node_walk is supplied the caller owns scaling, so dt is passed through unscaled. phases: optional per-phase overrides; None selects engine defaults.