Architecture¶
SimVX is a game engine in pure Python: node-based scene hierarchy, Vulkan GPU-driven rendering, and NumPy-based math. This page covers the key internal pipelines.
Node Lifecycle¶
Every node progresses through these stages:
construction --> enter_tree --> ready --> [update / fixed_update] --> exit_tree
Construction –
__init__()sets properties andPropertydefaults. No tree access yet.Enter tree –
add_child()inserts the node.Childdescriptors are instantiated. The node receives a reference to itsSceneTree.on_enter_tree()fires.Ready –
on_ready()runs bottom-up (children first, then parent).OnReadydescriptors resolve. Safe to access children by path:self["Camera"].Process –
on_update(dt)runs every frame with wall-clock delta.on_fixed_update(dt)runs at a fixed rate (default 60 Hz) with accumulator-based catch-up.Exit tree –
destroy()triggerson_exit_tree(). Signals are disconnected, children are recursively removed, and the node is dequeued from theSceneTree.
Children added during on_ready() go through the full lifecycle before the parent’s on_ready() returns. This guarantees that self["Player/Camera"] is valid inside on_ready().
Scene Tree¶
SceneTree owns the root node and drives the frame loop:
SceneTree.tick(dt) # walk tree, call on_update(dt) on every node
SceneTree.physics_tick(dt) # walk tree, call on_fixed_update(dt)
SceneTree.render(Draw2D) # 2D draw pass -- re-runs on_draw only for dirty nodes (retained)
Driver (SceneTree.tick / physics_tick / render) and handler (Node.on_update / on_fixed_update / on_draw) deliberately use different verbs so a misspelt override on a Node never silently shadows the engine’s per-frame loop.
Groups allow batch queries: tree.get_group("enemies") returns all nodes tagged with that group. The tree also manages deferred deletions – destroy() queues removal until the end of the current frame to avoid mutation during iteration.
Render Pipeline¶
SimVX uses a GPU-driven forward renderer. The per-frame work driven from
Python is bounded: scene state goes into flat NumPy arrays once, then a
single multi-draw-indirect call submits every visible instance. There are
no per-object vkCmdDraw loops in Python.
The public contract is: GPU-driven, one indirect submission for opaque
geometry, a separate sorted pass for transparent. Buffer sizes, descriptor
counts, culling strategy, and pass ordering are implementation details
that may change without notice: read the code in
packages/graphics/src/simvx/graphics/renderer/ if you need the current
specifics.
2D is retained. A node’s on_draw(renderer) is not re-run every frame:
the renderer caches the geometry it produced and re-runs on_draw only when
the node is marked dirty. A Property or transform write auto-dirties the
node; per-frame animation reading non-Property state opts in with
dynamic = True; a discrete non-Property change calls queue_redraw() at
the mutation site. See Your First 2D Game.
Input Flow¶
GLFW key/mouse event --> input_adapter --> Input singleton --> InputMap --> game code
GLFW callback –
key_callback_with_uireceives raw key events from GLFW.Input adapter – Translates GLFW key codes to
Key/MouseButtonenums, updates theInputsingleton’s pressed/released state, and routes events to the UI system.Input singleton – Stores per-frame state.
Input.is_key_pressed(Key.W)checks instantaneous state;Input.is_action_just_pressed("jump")checks action bindings.InputMap – Maps named actions to typed key bindings:
InputMap.add_action("jump", [Key.SPACE, JoyButton.A]). Actions are queried by name inon_update(dt).
UI widgets receive input first. If a focused widget consumes the event, it does not propagate to game nodes.
Performance Notes¶
Node counts: The tree walk is pure Python, so keep node counts under ~5,000 for 60 fps process ticks. Use groups and
find()to avoid deep recursive searches.Draw call batching: All opaque geometry is drawn in a single indirect draw call regardless of material count. Only transparent objects require sorting.
SSBO capacity: Default buffers support 16,384 instances and 256 lights. Exceeding these triggers a buffer resize (one-time stall).
Physics tick: Fixed at 60 Hz by default. Multiple physics steps per frame occur when the frame rate drops below 60 fps (capped at 100 ms accumulation to prevent spiral-of-death).
Headless mode:
App(visible=False)creates a real Vulkan surface with an invisible GLFW window. Rendering is identical to visible mode – useful for automated visual tests in CI.