A 3D first-person shooter engine for Windows, built on Direct3D 12.
Algebra Engine is a from-scratch runtime written in C++20, with an offline asset pipeline in Rust and gameplay scripting in LuaJIT. It targets a single platform and a single rendering path deliberately: the goal is a small, legible, measurable engine rather than a portable one.
Status: in active development. The renderer, physics, object model, scripting runtime, asset pipeline and editor shell are implemented and tested. Lighting is partially complete. See Roadmap for what remains.
- Design principles
- Feature overview
- Architecture
- Scripting with Lua
- Building
- Repository layout
- Verification
- Roadmap
- License
Single target, single path. Windows and Direct3D 12 only. One lighting path (clustered forward), one binding model (classic descriptor binding), one scripting language. Every alternative that was rejected was rejected because maintaining two paths costs more than the flexibility is worth at this scale.
No exceptions, no RTTI. The engine compiles with /EHs-c- and /GR-. Error
handling is explicit: functions return status, invalid input is rejected loudly rather
than clamped silently, and every capacity limit is counted rather than truncated in
silence.
No STL in hot paths or game state. std::vector, std::string and std::iostream
do not appear in engine code. Game state lives in fixed-capacity arenas so it can be
snapshotted with a single memcpy.
Frozen binary layouts. Every format shared between C++ and Rust (mesh, texture,
level, material, reflection) is locked by static_assert on the C++ side, a
compile-time assertion on the Rust side, and a test that reads real compiler output.
The first two lock each side independently; only the third proves they agree.
Fixed simulation timestep. Simulation runs at a fixed 60 Hz and rendering interpolates. Coroutine waits are expressed in ticks, never in frames, so gameplay timing is identical at any display refresh rate.
| Coordinate system | Right-handed, +Y up, -Z forward |
| Units | 1 unit = 1 metre |
| Matrices | Column vectors, column-major storage, M * v |
| Translation | Fourth column |
| Vector layout | Vector3 is 12 bytes with no padding; Vector4 and Quaternion are 16 |
| Naming | Types and functions PascalCase, locals camelCase, members m_, constants kPascalCase |
Vector layouts are frozen because the Lua FFI declarations, the save format and the network snapshot format all depend on them.
Rendering
- Direct3D 12 with a render graph that derives resource barriers from declared usage
- Clustered forward lighting: 16 x 9 x 24 cluster grid, exponential depth slicing
- Shader variant permutation with a generated variant manifest
- Material assets that select shader variants by feature name
- Transient resource pooling with automatic aliasing of compatible descriptions
- Dedicated render thread running one frame behind simulation
- Frame capture and pixel-level verification tooling
Physics
- Jolt Physics 5.3.0, driven from the fixed timestep
- 32 collision layers with a symmetric collision matrix, configured from a text file
- Physics materials as separate assets with surface type classification
- Continuous collision detection, enabled per body
- Ray, sphere sweep and overlap queries exposed to Lua
- Character movement with slide resolution, step-up and ground snapping
- Bone-attached hitboxes and joint-driven ragdolls sharing the same bodies
- Contact recording and wireframe debug drawing
Object model
- Handle-based object world with generation counters; stale handles are detectable
- Component data is plain old data, stored in the engine arena
- Ordered transform hierarchy resolved in a single linear pass
- Deferred destruction and an event bus
Reflection
ALGEBRA_TYPE()andALGEBRA_FIELD()markers scanned by an offline Rust generator- One declaration drives four consumers: editor inspector, save system, hot reload and the Lua binding surface
- Field offsets are emitted as
offsetofexpressions so the C++ compiler, not the generator, decides layout
Asset pipeline
- Rust tools producing compiled formats: meshes, textures, shaders, levels, materials
- Content-hash incremental builds with dependency tracking through shader includes
- Stable asset identity through sidecar metadata files
Editor
- Dear ImGui shell with dockable panels, scene tree, inspector and layer matrix
- Viewport rendering into an offscreen target
- Layout persistence
Diagnostics
- Structured logging with categories and throttling
- Tracy profiler integration
- Crash handler with minidump capture
- Memory tracking by category
The runtime is C++. The offline tools are Rust. They are separate processes that communicate through files, not a foreign function interface, and neither links against the other. This keeps the tool chain free to use crates that would be unacceptable in a shipping runtime, and keeps the runtime free of a Rust dependency.
Rust tools C++ runtime
---------- -----------
algebra-assets -> .amsh/.atex/.dxil/.aphysmat/.armat -> asset loaders
algebra-level -> .alevel -> level instantiation
algebra-bindgen -> Reflection.generated.h -> reflection tables
-> algebra.lua -> LuaJIT FFI bindings
A persistent arena backs game state. The arena exists for five reasons at once, which is why it is a foundational system rather than an optimisation:
- Stable addresses for the Lua FFI
- Snapshot and restore for network rollback
- Double buffering for the render thread
- Save and load
- Editor play mode entry and exit
Game state contains no owning pointers. Objects reference each other through handles carrying a generation counter, so a snapshot is a byte copy and a stale reference is detectable rather than undefined.
Main thread message pump, input, simulation, present
Render thread command list recording, one frame behind
Task system job graph for parallel work
The render thread records frame N while the main thread advances to frame N+1. Submission and presentation stay on the main thread, because DXGI delivers window events through the thread that owns the message pump. The only data crossing the boundary is a sealed per-frame view, copied by value.
The render graph takes pass declarations and resource accesses, then computes transitions, culls passes whose outputs are never read, and pools transient textures by description. Passes declare intent; they never write barriers by hand.
Lighting uses a clustered forward path. The view frustum is divided into a fixed 16 x 9 x 24 grid, independent of resolution so that a window resize touches no GPU resource. Depth slices are distributed exponentially, which gives each slice roughly equal screen area; a linear distribution would place everything within the first twenty metres into a single slice.
Light assignment currently runs on the CPU. This is a deliberate staging decision rather than a final one: verifying a compute-shader assignment requires GPU readback plus a CPU reference implementation, which means writing this code anyway. The CPU path will remain as the reference against which a compute implementation is checked.
Shader variants are produced by permutation over feature keywords declared inside the shader source. The pipeline emits a manifest mapping feature masks to compiled files, and the engine reads that manifest rather than recomputing the content hash. Materials name the features they want as strings; the mask is resolved through the manifest, so adding a feature to a shader cannot silently shift every existing material onto a different variant.
Physics is a member of the game runtime rather than a global, so a headless server can run two worlds in one process. Jolt types do not appear in any engine header; the implementation is held behind an opaque pointer. Collision layers are encoded so that 32 game layers map onto Jolt's broad-phase model, and the collision matrix is written symmetrically to make asymmetric rules impossible to express by accident.
Sources live in assets/ and shaders/. Compiled outputs go to build/assets/ and to
each build preset's binary directory. Incremental builds are driven by content hashes
that include tool settings and tracked dependencies, so changing a shader include
rebuilds every shader that includes it.
Compiled formats carry a magic number and a version. A version mismatch is rejected rather than read, because reading an older layout with a newer reader produces shifted data and no diagnostic.
Gameplay logic is written in Lua and executed by LuaJIT. The division is explicit: the engine owns simulation, rendering, physics and asset management; Lua owns game rules, character feel, weapon behaviour, UI logic and state machines.
Bindings are generated, not hand-written. The reflection generator reads the engine's
C ABI header and emits an ffi.cdef block plus a typed wrapper module. Regenerating
is a build step; the generated module carries the ABI version and refuses to load
against a mismatched engine, which turns a silent memory corruption into an explicit
error at startup.
local algebra = require('algebra')
local hit = algebra.physics.raycast(origin, direction, algebra.layer.Default)
if hit.valid then
print(string.format('hit %s at %.2f m', hit.body, hit.distance))
endA Lua component declares its fields; the values are stored in the engine arena, not in a Lua table. This is what allows the editor to inspect a Lua component, the save system to serialise it, hot reload to preserve it, and network rollback to restore it, all through the same mechanism used by C++ components.
local Spinner = algebra.component('Spinner', {
angle = { type = 'float', default = 0.0 },
speed = { type = 'float', default = 120.0 },
})
function Spinner:fixed_update(dt)
self.angle = self.angle + self.speed * dt
endCoroutine waits are expressed in ticks, not seconds and not frames:
algebra.schedule(function()
algebra.wait_ticks(30)
weapon.reload_complete()
end)A frame-based wait would run at a different rate on a 144 Hz display than on a 60 Hz one. Tick-based waiting is identical everywhere and survives rollback.
The FFI boundary is cheap but not free. Query functions write into caller-provided buffers so that a per-frame query allocates nothing. Where a shared result table is reused across calls, the guide documents it and an escape hatch is provided for callers that need to retain a result.
Hot reload watches script files and reloads changed chunks, cancelling coroutines that belong to replaced code. Component data survives the reload because it lives in the arena.
- Windows 10 or later, x64
- Visual Studio 2022 or newer with the C++ workload
- Windows SDK 10.0.26100 or newer, which provides
dxc.exefor shader compilation - CMake 3.24 or newer and Ninja, both supplied by the Visual Studio installation
- Rust 1.90 or newer for the offline tools
- A GPU supporting Direct3D 12 feature level 12_0
cmake --preset debug
cmake --build build/debug
Presets: debug, release (Ninja), and vs for Visual Studio project generation.
Outputs are placed in build/<preset>/bin: AlgebraGame.exe, AlgebraEditor.exe
and AlgebraTests.exe.
The Rust tree is built separately and has no CMake integration.
cd tools
cargo build --release
cargo clippy --all-targets
cargo test
powershell -ExecutionPolicy Bypass -File scripts/BuildAssets.ps1
powershell -ExecutionPolicy Bypass -File scripts/BuildBindings.ps1
BuildAssets.ps1 compiles assets, levels and shaders, then stages everything into each
build preset's binary directory. BuildBindings.ps1 regenerates the reflection table
and the Lua bindings; it must be re-run whenever a reflected type or the C ABI changes.
assets/ Source assets, level definitions, engine configuration
bench/ Micro-benchmarks for allocators and the task system
cmake/ Compiler settings and ABI flags
editor/ Editor application
engine/ Runtime library
animation/ Skeleton and pose data
asset/ Compiled asset readers
audio/ Audio interfaces
core/ Types, math, memory, tasks, logging, reflection
physics/ Jolt integration, layers, character movement, hitboxes
platform/ Window, input, crash handling
render/ Direct3D 12 renderer, render graph, lighting
scene/ Object world, transform hierarchy, camera, events
script/ LuaJIT virtual machine, C ABI, hot reload
systems/ Cross-cutting runtime systems
game/ Game executable
scripts/ Lua runtime scripts and build scripts
shaders/ HLSL sources
tests/ Unit tests and out-of-process verification probes
thirdparty/ Vendored dependencies
tools/ Rust offline tools
| Dependency | Purpose |
|---|---|
| Jolt Physics | Rigid body simulation |
| LuaJIT | Gameplay scripting |
| Dear ImGui | Editor user interface |
| D3D12 Memory Allocator | GPU memory management |
| Tracy | Frame profiling |
The engine is verified at two levels.
Unit tests cover allocators, handles, math, reflection, serialisation, the object world, scripting, physics and the lighting data path. They run without a GPU.
build/debug/bin/AlgebraTests.exe
Out-of-process probes launch the real executables and inspect their output, including captured framebuffer pixels. They exist because a renderer that compiles, runs and produces no validation warnings can still be wrong; the probes assert on content rather than on absence of errors.
powershell -ExecutionPolicy Bypass -File tests/probes/RenderProbe.ps1
powershell -ExecutionPolicy Bypass -File tests/probes/CrashProbe.ps1
powershell -ExecutionPolicy Bypass -File tests/probes/AssetProbe.ps1
powershell -ExecutionPolicy Bypass -File tests/probes/EditorProbe.ps1
Probe assertions are written as contracts rather than as fixed constants, and every assertion that a behaviour is prevented is paired with a control run in which it is not prevented. A test that only observes the guarded case cannot distinguish a working guard from a broken measurement.
The following work is planned. Items are listed in intended order of implementation.
Lighting
- Shader variant budget and stripping of unused variants
- Cascaded shadow maps
- Cascade caching, with distant cascades updated in rotation
- Motion vectors and camera jitter
- Temporal anti-aliasing
- Physically based sky and atmosphere
- Probe volumes with runtime sampling
- Offline probe baking, one scenario per time of day
- Scenario blending for the day and night cycle
- Reflection probes and screen-space reflections
- High dynamic range rendering and tone mapping
Crowd systems and culling
- GPU instancing and a material property buffer
- CPU particle system
- Particle effect definition format
- Projectile system with ballistic simulation
- Voronoi fracturing in the offline pipeline
- Debris system for fractured geometry
- CPU software occlusion culling
- Frustum and occlusion culling distributed across the task graph
Animation and audio
- Skeleton and pose evaluation
- GPU skinning
- Blending and transitions
- Animation layers, so the lower body can walk while the upper body aims
- Root motion extraction
- Inverse kinematics solvers for foot placement and hand positioning
- Animation scripting interface, with state machines authored in Lua
- Audio backend integration
- Audio buses and mixing
- Three-dimensional positioning, distance curves and directionality
- Reverb zones
- Sound occlusion driven by the existing occlusion system
- Footstep and impact sound selection by surface type
Editor completion
- Command pattern infrastructure for undo
- Translate, rotate and scale gizmos
- Property editing written back to the scene file as data
- Scene saving and loading
- Play mode entered and exited through snapshots
- Undo and redo across all editor operations
- Multiple selection
- Copy, paste and duplicate
Game user interface and persistence
- Two-dimensional drawing primitives: rectangles, textures, nine-slice
- Signed distance field font atlas generation
- Signed distance field text rendering with outline and shadow
- Clipping regions and layer ordering
- Mouse events exposed to Lua
- Lua user interface layout library
- Save and load built on snapshots and field names
- Settings system with persistence
Iteration tools and packaging
- Asset server with file watching, running as a separate process
- Socket notification protocol
- Shader hot reload
- Texture hot reload
- Mesh hot reload, updating collision shapes, hitboxes and skeletons
- Package builder producing compressed, indexed archives
- Lua bytecode compilation for release builds
- Navigation mesh baking
- Release packaging
- Crash dump symbolisation and symbol archiving
Algebra Engine is released under the GNU General Public License, version 3. See LICENSE for the full text.
Vendored dependencies in thirdparty/ remain under their own licenses.
Copyright (C) 2026 sl4de