Architecture

SimVX is a Godot-inspired 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 --> [process / physics_process] --> exit_tree
  1. Construction__init__() sets properties and Property defaults. No tree access yet.

  2. Enter treeadd_child() inserts the node. Child descriptors are instantiated. The node receives a reference to its SceneTree. on_enter_tree() fires.

  3. Readyon_ready() runs bottom-up (children first, then parent). OnReady descriptors resolve. Safe to access children by path: self["Camera"].

  4. Processon_process(dt) runs every frame with wall-clock delta. on_physics_process(dt) runs at a fixed rate (default 60 Hz) with accumulator-based catch-up.

  5. Exit treedestroy() triggers on_exit_tree(). Signals are disconnected, children are recursively removed, and the node is dequeued from the SceneTree.

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_process(dt) on every node
SceneTree.physics_tick(dt)     # walk tree, call on_physics_process(dt)
SceneTree.render(Draw2D)       # 2D draw pass -- nodes' on_draw(renderer) emits commands

Driver (SceneTree.tick / physics_tick / render) and handler (Node.on_process / on_physics_process / 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.

Input Flow

GLFW key/mouse event --> input_adapter --> Input singleton --> InputMap --> game code
  1. GLFW callbackkey_callback_with_ui receives raw key events from GLFW.

  2. Input adapter – Translates GLFW key codes to Key/MouseButton enums, updates the Input singleton’s pressed/released state, and routes events to the UI system.

  3. Input singleton – Stores per-frame state. Input.is_key_pressed(Key.W) checks instantaneous state; Input.is_action_just_pressed("jump") checks action bindings.

  4. InputMap – Maps named actions to typed key bindings: InputMap.add_action("jump", [Key.SPACE, JoyButton.A]). Actions are queried by name in on_process(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.