Lysa  0.0
Lysa 3D Engine
Architecture overiew

Overview

Lysa is organized as a set of loosely coupled subsystems, each exposed through a C++23 module interface (.ixx) paired with a .cpp implementation. The top-level module lysa re-exports all public interfaces and serves as the single include point for downstream code.

All GPU interaction is delegated to Vireo RHI : an external graphics abstraction layer. The engine never calls a graphics API (Vulkan, DirectX 12) directly

Entry Point & Context

lysa::Lysa is the main entry class. It owns the application context, starts the main loop (run()), and coordinates the fixed-timestep physics update with the variable-timestep render frame.

lysa::Context is a singleton that aggregates every engine-wide service:

Field Role
vireo Vireo backend : device, instance, GPU resource factory
fs Virtual File System
events Centralized event dispatcher
defer Deferred command buffer (main-thread deferred tasks)
threads Async task pool (multi-threaded work)
res Resources registry (live resource tracking by type)
samplers Global GPU samplers
graphicQueue Submit queue for rendering work
transferQueue Submit queue for DMA transfers
asyncQueue Async transfer command submission
physicsEngine Pluggable physics backend (Jolt or PhysX, optional)
lua Lua scripting environment (optional)

The context is accessed globally through the lysa::ctx() free function.

Main Loop

The main loop in lysa::Lysa::run follows a fixed-timestep accumulator pattern:

  1. Platform events : window, input, OS messages consumed by processPlatformEvents().
  2. Physics update : MainLoopEvent::PHYSICS_PROCESS fired at a fixed delta time (default 1/60 s), potentially multiple times per frame to drain the accumulator.
  3. Game update : MainLoopEvent::PROCESS fired once with the remaining frame time.
  4. GPU upload : new/modified resources are uploaded to the GPU via uploadData().
  5. Render : each active lysa::RenderTarget drives its lysa::Renderer.
  6. Quit : MainLoopEvent::QUIT fired before resource teardown when Context::exit is set.

Platform Layer

Platform-specific code is isolated under src/platform/:

  • win32/ : Win32 API window, input, DirectoryWatcher, VFS root resolution.
  • posix/ : POSIX file I/O and directory watching for Linux.
  • sdl/ : SDL3 windowing and input (Linux only, controlled by USE_SDL3 CMake option).

Engine code above this layer is platform-agnostic. Platform backends are selected at compile time through CMake.

Virtual File System

src/VirtualFS.ixx provides URI-based path resolution for portable asset loading:

  • app:// : read-only application assets (typically bundled with the executable).
  • user:// : user-writable data directory.

lysa::ResourcesPack and lysa::AssetsPack layer packed file support on top of the VFS, allowing assets to be shipped as single archive files without changing any loading code.

Resource Management

All engine resources (meshes, images, materials, textures, samplers…) live in fixed-capacity pools managed by lysa::ResourcesManager :

  • Resources are created into available slots and addressed by a small integral unique_id.
  • All accessors are O(1) by direct index.
  • Capacity is fixed at engine initialization via lysa::ResourcesCapacity.
  • GPU upload is triggered automatically on resource creation.

The lysa::ResourcesRegistry tracks all live resources by type and provides a single discovery point for cross-subsystem queries.

Dedicated manager types : ImageManager, MaterialManager, MeshManager : are instantiated by lysa::Lysa and own the corresponding GPU-side buffers.

Event System

src/Event.ixx provides a centralized observer-based dispatcher (lysa::EventManager).

All engine subsystems route their notifications through this system:

  • Input events (keyboard, mouse, gamepad).
  • Physics contact and collision callbacks.
  • Application lifecycle events (MainLoopEvent::PROCESS, PHYSICS_PROCESS, QUIT).
  • Custom user events.

Lua callbacks are registered through the same dispatcher, unifying C++ and script-side event handling.

Rendering Architecture

Render passes

The rendering stack is built from composable lysa::Renderpass objects. Each pass owns its attachments, pipeline state, and descriptor sets. Passes are assembled into a lysa::Renderer instance that drives the full frame.

Both rendering paths share the following passes:

Pass Description
DepthPrepass Early depth fill to reduce overdraw
ShadowMapPass Cascaded/cube shadow maps for directional & point lights
TransparencyPass Weighted Blended Order-Independent Transparency (WBOIT)
ShaderMaterialPass User-defined Slang shader materials
BloomPass Separable Gaussian bloom
SMAAPass Subpixel Morphological Anti-Aliasing
FXAAPass Fast Approximate Anti-Aliasing
GammaCorrectionPass Linear → sRGB conversion
PostProcessingCompute Compute-based post-processing chain (FXAA, SMAA, ACES, FSR…)

Forward renderer

lysa::ForwardRenderer with a single ForwardColorPass that writes directly to the color/depth attachments. Suitable for scenes with many unique materials. Shading (PBR, alpha-test) is computed per draw call in the fragment shader.

Deferred renderer

lysa::DeferredRenderer with a G-Buffer pipeline:

Pass Output
GBufferPass Position, Normal, Albedo, Emissive into separate attachments
GTAOPass Ground-Truth Ambient Occlusion
SSAOPass Screen-Space Ambient Occlusion
LightingPass Full-screen lighting resolve (PBR, shadow reads, AO)
TAAPass Temporal Anti-Aliasing
FrameSharpeningPass CAS sharpening after TAA

Deferred rendering is selected by setting the DEFERRED_RENDERER CMake option (default ON).

GPU-driven culling and draw commands

Two compute passes run before the main color pass:

  • lysa::GenerateDrawCommands : reads the instances visibility mask and writes VkDrawIndexedIndirectCommand / D3D12 indirect draw arguments into a GPU buffer for use by the frustum culling passes.
  • lysa::FrustumCulling : culls mesh instances against the camera frustum on the GPU, writing culled draw commands into a second GPU buffer.

The main color passes consume these culled indirect draw buffers, keeping the CPU out of the per-draw loop.

Scene & SceneFrameData

lysa::Scene owns the list of lysa::MeshInstance objects, lights, and the environment settings. It computes per-frame lysa::SceneFrameData : GPU-ready view/projection matrices, instance transforms, material bindings, shadow map parameters : consumed by every render pass.

Shader pipeline

Shaders are written in Slang and compiled to both DXIL (DirectX 12) and SPIR-V (Vulkan) at build time by cmake/shaders.cmake. Compiled binaries ship under shaders/.

The math library in CPU code (src/depends/hlslpp/) uses the same HLSL-compatible types (float3, float4x4, …) as the shaders, eliminating type-conversion boilerplate at the CPU–GPU boundary.

Physics

The physics layer exposes a common interface (lysa::PhysicsEngine, src/physics/PhysicsEngine.ixx) implemented by two pluggable backends, selected exclusively at compile time:

  • Jolt Physics (src/physics/jolt/) : default backend (PHYSIC_ENGINE_JOLT=ON).
  • NVIDIA PhysX (src/physics/physx/) : alternative backend (PHYSIC_ENGINE_PHYSX=ON).

Both backends expose:

  • lysa::CollisionObject : rigid body, kinematic body, static trigger.
  • lysa::CollisionShapes : box, sphere, capsule, convex hull, triangle mesh.
  • lysa::RayCast : layer-filtered ray queries.
  • Contact/collision callbacks routed through the event system.

A lysa::PhysicsDebugRenderer visualizes physics shapes and constraints in real time (enabled via DebugConfiguration in lysa::ContextConfiguration).

Scripting : Lua Bindings

When compiled with LUA_BINDINGS=ON, the engine exposes its full public API to Lua via LuaBridge.

Lua callbacks integrate with the engine event system using the same EventManager::on() mechanism as C++ listeners, allowing hybrid C++/Lua classes.

Async & Threading

Component Description
AsyncTasksPool Fixed thread pool for fire-and-forget parallel work
AsyncQueue Serialized async submission of GPU transfer commands
DeferredTasksBuffer Main-thread deferred command queue (execute-next-frame tasks)

These three primitives cover the main concurrency patterns in the engine: CPU parallel work, background GPU uploads, and safe main-thread-deferred mutations of renderer state.

Tooling

  • Blender add-on (tools/blender/blender_lysa_addon.py) : exports scenes, meshes, materials, lights, and cameras directly into a Lysa-compatible format.
  • Resource Pack list generator (tools/gen_pack_list.py) : produces asset pack manifests for lysa::ResourcesPack.

Dependencies

Library Role Location
Vireo RHI Graphics abstraction (Vulkan / DX12) External (VIREO_RHI_PROJECT_DIR)
Jolt Physics Default physics backend CMake FetchContent
NVIDIA PhysX Optional physics backend External (PHYSX_INCLUDE)
Slang Shader language & compiler System PATH
LuaBridge Lua ↔ C++ bindings CMake FetchContent
hlslpp HLSL-compatible CPU math Vendored (src/depends/hlslpp/)
nlohmann/json JSON parsing Vendored (src/depends/json/)
stb Image loading/writing, TrueType Vendored (src/depends/stb/)
SDL3 Windowing on Linux System / CMake FetchContent