Build & Compilation

Makefile Targets

TargetDescription
make viewerBuild shaders + C++ dylib + C# exe
make runBuild everything and run the viewer
make devBuild and run with hot reload (edit game logic live)
make appBuild macOS .app bundle
make shadersCompile GLSL shaders to SPIR-V only
make allBuild hello demo (basic P/Invoke test)
make cleanRemove all build artifacts (build/, compile_commands.json)

make run is the primary development command. make dev enables hot reload for game logic.

Shader Compilation

GLSL shaders are compiled to SPIR-V using glslc (Vulkan SDK tool):

$(VERT_SPV): native/shaders/shader.vert | $(SHADER_DIR)
    glslc $< -o $@

$(FRAG_SPV): native/shaders/shader.frag | $(SHADER_DIR)
    glslc $< -o $@

$(UI_VERT_SPV): native/shaders/ui.vert | $(SHADER_DIR)
    glslc $< -o $@

$(UI_FRAG_SPV): native/shaders/ui.frag | $(SHADER_DIR)
    glslc $< -o $@

Input files in native/shaders/, output in build/shaders/:

SourceOutputPurpose
shader.vertbuild/shaders/vert.spv3D scene vertex shader
shader.fragbuild/shaders/frag.spv3D scene fragment shader (Blinn-Phong + shadows)
ui.vertbuild/shaders/ui_vert.spvUI overlay vertex shader
ui.fragbuild/shaders/ui_frag.spvUI overlay fragment shader
shadow.vertbuild/shaders/shadow_vert.spvShadow map depth-only vertex shader
shadow_point.vertbuild/shaders/shadow_point_vert.spvPoint light cubemap shadow vertex shader
shadow_point.fragbuild/shaders/shadow_point_frag.spvPoint light cubemap shadow fragment shader

CMake Configuration

native/CMakeLists.txt builds the shared library:

cmake_minimum_required(VERSION 3.20)
project(renderer LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)

find_package(Vulkan REQUIRED)
find_package(glfw3 REQUIRED)
find_package(glm REQUIRED)

add_library(renderer SHARED
    renderer/renderer.cpp
    renderer/vulkan_init.cpp
    renderer/vulkan_util.cpp
    scene/mesh.cpp
    scene/entity.cpp
    scene/material.cpp
    ui/ui.cpp
    bridge.cpp
)

target_include_directories(renderer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/vendor)

target_link_libraries(renderer PRIVATE
    Vulkan::Vulkan
    glfw
    glm::glm
    "-framework Cocoa"
    "-framework IOKit"
)

Output: build/librenderer.dylib

The Makefile runs CMake with CMAKE_EXPORT_COMPILE_COMMANDS=ON and symlinks compile_commands.json to the repo root for IDE intellisense.

Dependencies (installed via Homebrew):

  • Vulkan SDK (includes glslc, Vulkan headers, MoltenVK)
  • GLFW 3
  • GLM

Vendor headers (in native/vendor/, no separate build):

  • cgltf.h — glTF 2.0 parser (single-header, needs #define CGLTF_IMPLEMENTATION in one .cpp)
  • stb_truetype.h — TrueType font rasterizer (needs #define STB_TRUETYPE_IMPLEMENTATION)
  • stb_image.h — Image decoder (needs #define STB_IMAGE_IMPLEMENTATION)

Each #define lives in a separate .cpp file: CGLTF_IMPLEMENTATION in scene/mesh.cpp, STB_IMAGE_IMPLEMENTATION in scene/material.cpp, STB_TRUETYPE_IMPLEMENTATION in ui/ui.cpp.

C# Compilation

The Makefile uses two separate file lists — engine code and game logic — combined into VIEWER_CS:

MANAGED_CS = managed/runtime/Viewer.cs managed/core/World.cs managed/core/Components.cs \
             managed/rendering/NativeBridge.cs managed/core/FreeCameraState.cs \
             managed/physics/PhysicsBridge.cs managed/physics/PhysicsWorld.cs
GAMELOGIC_CS_FILES = game_logic/Game.cs game_logic/GameConstants.cs \
    game_logic/systems/InputMovementSystem.cs \
    game_logic/systems/TimerSystem.cs \
    game_logic/systems/PhysicsSystem.cs \
    game_logic/systems/CameraSystems.cs \
    game_logic/systems/LightSyncSystem.cs \
    game_logic/systems/HierarchyTransformSystem.cs \
    game_logic/systems/DebugSystems.cs \
    game_logic/systems/RenderSyncSystem.cs
VIEWER_CS = $(MANAGED_CS) $(GAMELOGIC_CS_FILES)

$(VIEWER_EXE): $(VIEWER_CS)
    mcs -out:$@ $(VIEWER_CS)

Uses the Mono C# compiler (mcs). All source files are listed explicitly in MANAGED_CS (engine) and GAMELOGIC_CS_FILES (game logic) — there is no automatic file discovery.

IDE IntelliSense

The repo includes SaFiEngine.sln (repo root) and managed/SaFiEngine.csproj for C# language server support (autocomplete, go-to-definition, error checking). These files are not used by the build — the Makefile drives compilation via mcs. After cloning, run dotnet restore once to generate managed/obj/project.assets.json, which the language server needs to resolve System.* types.

Dev Mode (Hot Reload)

make dev splits C# into three assemblies for live code reloading:

# Engine (stable) — World, Components, NativeBridge, FreeCameraState, PhysicsBridge, PhysicsWorld
ENGINE_CS = managed/core/World.cs managed/core/Components.cs \
            managed/rendering/NativeBridge.cs managed/core/FreeCameraState.cs \
            managed/physics/PhysicsBridge.cs managed/physics/PhysicsWorld.cs
ENGINE_DLL = $(BUILD_DIR)/Engine.dll

# Game logic (hot-reloadable) — Game.cs, GameConstants.cs, systems/*.cs
GAMELOGIC_CS = game_logic/Game.cs game_logic/GameConstants.cs \
    game_logic/systems/InputMovementSystem.cs \
    game_logic/systems/TimerSystem.cs \
    game_logic/systems/PhysicsSystem.cs \
    game_logic/systems/CameraSystems.cs \
    game_logic/systems/LightSyncSystem.cs \
    game_logic/systems/HierarchyTransformSystem.cs \
    game_logic/systems/DebugSystems.cs \
    game_logic/systems/RenderSyncSystem.cs
GAMELOGIC_DLL = $(BUILD_DIR)/GameLogic.dll

# Viewer with hot reload support
VIEWERDEV_CS = managed/runtime/Viewer.cs managed/runtime/HotReload.cs
VIEWERDEV_EXE = $(BUILD_DIR)/ViewerDev.exe
AssemblyContentsReloadable?
Engine.dllWorld, Components, NativeBridge, FreeCameraState, PhysicsBridge, PhysicsWorldNo
GameLogic.dllGame.cs, GameConstants.cs, systems/*.csYes
ViewerDev.exeViewer.cs + HotReload.cs (compiled with -define:HOT_RELOAD)No

A FileSystemWatcher in HotReload.cs monitors game_logic/ for .cs changes. On save, it auto-discovers all .cs files in the directory, recompiles GameLogic.dll, and loads the new assembly via reflection. The reload then looks for a Game.Setup(World) method in the new assembly:

  • If found — calls world.Reset() (despawns all entities, clears component stores, clears systems, clears all 8 light slots on the native side, resets entity IDs) then re-invokes Game.Setup(world) from the new assembly. This makes scene changes (new entities, lights, system registration order) take effect immediately.
  • If not found — falls back to swapping system delegates by name (systems-only reload).

make run and make app are unaffected — they compile everything into a single Viewer.exe with no hot reload code (#if HOT_RELOAD guards).

Runtime Environment

make run sets environment variables before executing:

DYLD_LIBRARY_PATH=build:/opt/homebrew/lib \
VK_ICD_FILENAMES=/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json \
    mono build/Viewer.exe
  • DYLD_LIBRARY_PATH — locates librenderer.dylib and libjoltc.dylib (in build/) and MoltenVK/GLFW (in /opt/homebrew/lib)
  • VK_ICD_FILENAMES — tells the Vulkan loader where to find the MoltenVK ICD. Note: the path is under /etc/ not /share/ (Homebrew-specific)
  • mono — runs the compiled .NET executable
Where to Edit

Adding a new shader: Create the .vert/.frag file in native/shaders/. In the Makefile, add a new SPV variable (e.g., NEW_VERT_SPV), add a compilation rule, and add it to the shaders: dependency list.

Adding a new C++ source file: Add it to add_library(renderer SHARED ...) in native/CMakeLists.txt. CMake will pick it up on next build.

Adding a new C# file: Engine files go in the appropriate managed/ subdirectory (core/, rendering/, physics/, or runtime/) — add the path to MANAGED_CS in the Makefile. Game files go in game_logic/ (systems in game_logic/systems/) — add the path to GAMELOGIC_CS_FILES. Game files are automatically hot-reloadable with make dev. The .csproj discovers files automatically, so no project file update is needed.