# 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 tree** -- `add_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. **Process** -- `process(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 submission** -- `SceneAdapter` 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 callback** -- `key_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.