Architecture Overview
This page describes the high-level architecture of the Safi Engine: how C# game logic communicates with the C++ Vulkan renderer, what each file is responsible for, and how the build pipeline produces a running application.
High-Level Architecture
The engine is split into two language domains. C# (running on Mono) owns the main loop, ECS world, and all gameplay logic. C++ owns the Vulkan rendering backend. The two sides communicate exclusively through P/Invoke -- a foreign-function interface where C# calls extern "C" functions exported from a shared library.
Data flows one direction: C# tells C++ what to render. The native side never calls back into managed code. Every frame, C# systems iterate over ECS components and issue bridge calls to set transforms, update lights, submit UI vertices, and trigger the draw. The C++ side translates these into Vulkan command buffers.
A separate P/Invoke boundary exists for physics: PhysicsBridge.cs calls into libjoltc.dylib (built from the native/joltc/ submodule). The PhysicsWorld singleton manages the Jolt lifecycle, and PhysicsSystem (in game logic) creates bodies, steps the simulation, and syncs transforms back to ECS.
File Map
Build Pipeline
Running make run executes four steps in sequence:
1. Shader Compilation
Each .vert and .frag file is compiled from GLSL to SPIR-V using Google's glslc compiler. The SPIR-V binaries land in build/shaders/ where the renderer loads them at init.
2. Native Library Build
CMake configures and builds the C++ code into build/librenderer.dylib. A symlink to compile_commands.json is placed at the repo root for IDE support.
3. C# Compilation
All managed source files listed in the Makefile's MANAGED_CS and GAMELOGIC_CS_FILES variables are compiled into a single Mono executable.
4. Run
DYLD_LIBRARY_PATH tells Mono where to find librenderer.dylib (and GLFW/MoltenVK from Homebrew). VK_ICD_FILENAMES points Vulkan's loader to the MoltenVK ICD so that Vulkan runs on top of Metal.
Triple-Pipeline Architecture
The renderer runs three graphics pipelines within a single Vulkan render pass. All pipelines share the same swapchain framebuffers and command buffers, but they have different pipeline states.
3D Scene Pipeline
The 3D pipeline renders all opaque scene geometry first.
Debug Wireframe Pipeline
The debug wireframe pipeline renders after 3D geometry and before the UI overlay, but only when the debug overlay is enabled (debugOverlayEnabled_ == true). Debug entities are stored in a separate debugEntities_ list and use meshes from the shared combined vertex/index buffers. Created alongside the 3D pipeline in createGraphicsPipeline() by modifying rasterizer and depth-stencil state.
UI Overlay Pipeline
The UI pipeline renders after all 3D geometry but before vkCmdEndRenderPass. It uses host-visible, persistently-mapped vertex buffers (one per frame-in-flight, max 4096 vertices) so that C# can write UI vertices directly without staging copies. The font atlas is baked once at init from assets/fonts/RobotoMono-Regular.ttf using stb_truetype.
Where to Edit
Every new function exposed to C# requires changes in three files:
native/renderer.h+ the appropriate.cppfile innative/renderer/,native/scene/, ornative/ui/-- Add the method to theVulkanRendererclass and implement it.native/bridge.cpp-- Add anextern "C"wrapper that calls the new method ong_renderer, wrapped in a try/catch.managed/rendering/NativeBridge.cs-- Add a[DllImport("renderer")]static extern declaration matching the C function signature. :::
:::tip Where to Edit -- Adding a New Shader
- Create the
.vertand/or.fragfile innative/shaders/. - In the
Makefile, add a new SPV variable (e.g.,MY_VERT_SPV = $(SHADER_DIR)/my_vert.spv) and a corresponding compilation rule (glslc $< -o $@). - Add the new SPV target to the
shaders:dependency list so it gets built automatically. - In the appropriate
.cppfile (e.g.,renderer/vulkan_init.cppfor pipelines,renderer/renderer.cppfor loading), load the new SPIR-V binary and create the pipeline that uses it. :::
:::tip Where to Edit -- Adding a New C# File The project uses two folders for C# code:
- Engine files (
managed/) -- Add to the appropriate subdirectory (core/,rendering/,physics/, orruntime/) and add the path toMANAGED_CSin the Makefile. These are stable engine internals. - Game files (
game_logic/) -- Add the path toGAMELOGIC_CS_FILESin the Makefile. Systems go ingame_logic/systems/using thepartial class Systemspattern. These are user-editable game logic files and are hot-reloadable withmake dev.
The .csproj uses EnableDefaultCompileItems so it discovers new files automatically — no project file changes needed. Only the Makefile needs updating.