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.

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

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

  5. Exit tree – Triggered by destroy(). Signals are disconnected, children are recursively removed, and the node is dequeued from the SceneTree.

Children added during ready() go through the full lifecycle before the parent’s ready() returns. This guarantees that self["Player/Camera"] is valid inside ready().

Scene Tree

SceneTree owns the root node and drives the frame loop:

SceneTree.process(dt)          # walk tree, call process(dt) on every node
SceneTree.physics_process(dt)  # walk tree, call physics_process(dt)
SceneTree.draw(Draw2D)         # 2D draw pass -- nodes emit draw commands

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. No per-object draw calls from Python.

Scene submission --> SSBO upload --> Multi-draw indirect --> Swapchain present

Frame flow

  1. Scene submissionSceneAdapter walks the node tree, collecting MeshInstance3D transforms, materials, and lights into flat NumPy arrays.

  2. Frustum culling – CPU-side bounding-sphere test per viewport. Culled instances are excluded from the draw command buffer.

  3. SSBO upload – Transform, material, and light arrays are memcpy’d to Shader Storage Buffer Objects. One upload per frame.

  4. Draw command build – A VkDrawIndexedIndirectCommand array is built (one entry per visible mesh instance).

  5. Pre-render – Shadow map passes and SSAO compute dispatch run before the main render pass.

  6. Main render pass – A single vkCmdDrawIndexedIndirect call draws all opaque geometry. Transparent objects follow in a separate sorted pass.

  7. 2D overlay – Text, UI widgets, and Draw2D geometry are rendered in an orthographic pass over the 3D scene.

  8. Present – The swapchain image is presented. Frame capture (for headless testing) happens here if requested.

Key data structures

Buffer

Contents

Update frequency

Transform SSBO

4x4 model matrices (float32)

Every frame

Material SSBO

Color, roughness, metallic, texture IDs

On change

Light SSBO

Position, color, intensity, type

Every frame

Indirect draw buffer

VkDrawIndexedIndirectCommand per instance

Every frame

Bindless textures are stored in a single descriptor array (up to 4096 slots). Materials reference textures by integer index – no per-material descriptor set switching.

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 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.