> ## Content Index
> Fetch the complete content index at: https://ozzadar.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Lights: A High-Level Tour of My Game Engine (and the Weird Stuff Inside)
- URL: https://ozzadar.com/blog/lights-a-high-level-tour-of-my-game-engine-and-the-weird-stuff-inside/
- Published: 2026-09-17T02:42:44.000Z
- Updated: 2026-09-17T02:42:44.000Z
- Author: Paul Mauviel
- Tags: C++, Game Engines, Lights

I've been heads down on **Lights**, the C++ engine underneath my current game projects, for a while now. Long enough that it's grown some genuinely unusual corners. This is the first in a series about how it actually works. We'll start high level: what the engine looks like from the outside, how it's put together, and a few of the weirder decisions I've made along the way. Later posts will dig into specific systems in more depth.

## The shape of it

Lights is a C++23 static library, built with CMake, windowing handled entirely through SDL3\. It's split into two layers: **core** (audio, platform, rendering, text, a small generic algorithms library) and **framework** (the game loop, scene management, input, prebuilt layers built on top of core). It's not consumed as a submodule. It gets pulled into my games' superrepo via CMake's `FetchContent` and compiled as a static lib alongside everything else.

![Lights layering diagram: consumer game with entt registry, Lights framework layer, Lights core layer, and the ozz_rendering RHI repo](https://ozzadar.com/content/images/2026/09/lights-layers.svg)

## Rendering lives in its own repo

The actual GPU backend isn't part of Lights at all. It's a sibling project, **ozz\_rendering**, pulled in the same way, with a CMake flag (`LOCAL_RENDERING_DIR`) to swap in a local working copy when I'm iterating on both at once. It exposes a device centric RHI: an `RHIDevice` owns every GPU resource, and callers only ever hold opaque handles, never raw GPU objects. Backends currently implemented: Vulkan, OpenGL, and WebGPU, plus an `Auto` mode that resolves to whatever's appropriate for the platform.

## The engine doesn't force an ECS on you

`SceneObject` is a flat struct. A transform, a mesh (vertex/index buffer handles), a material. That's it. Lights only knows how to draw one, through the graph based renderer described below. It has no opinion about how you organize game logic above that.

idkwtfim2D, one of the games built on Lights, glues in its own `entt::registry` at the game layer for that. `GameLayer` inherits `SceneLayer` and `Renderable` from Lights, but owns an `entt::registry` directly as a member, plus a couple of composite `SceneObject`s. Systems walk the registry each frame and feed their results into those scene objects, which are what Lights actually renders. The engine and the ECS stay fully decoupled. You bring whichever object model actually fits the game.

## Most of this is handrolled

It's worth being upfront about what's actually mine here versus what's a library doing the work. The exceptions are a short list: SDL3 for windowing and input plumbing, glm for math, freetype2 for font rasterization, rtaudio for the actual audio I/O, spdlog for logging, toml11 for config parsing, Crypto++ for hashing primitives, Clay for UI layout, and (on desktop builds) Boost.Beast and Asio for the WebSocket transport behind the networking layer. There's also a Tracy profiler hook in debug builds, and native builds can use the Slang shader compiler (the web build compiles shaders to WGSL offline instead, since Slang has no wasm distribution).

Everything else is built from scratch: the graph system, the frame graph orchestration that runs on top of it, the audio DSP nodes, scene management, the input system, the schema/codegen tooling, collision primitives. That's the part worth digging into.

## The weird and interesting stuff

### One graph class runs both my modular synthesizer and my renderer

This is the one I most wanted to write about. Deep in `core/algo` there's a `GraphNode`, a completely generic directed graph primitive. Connect nodes, disconnect them, topologically sort from a root, flatten the graph. It knows nothing about audio or rendering. It's maybe 40 lines of domain free plumbing.

Two completely unrelated systems build on top of it.

![Diagram showing GraphNode as a shared base class for both AudioGraphNode (a modular-synth style DSP graph) and Renderable (a hand rolled frame graph)](https://ozzadar.com/content/images/2026/09/graph-node.svg)

The audio system's `AudioGraphNode` inherits it directly, and there are real node types running on it: mixers, processors, and a `saw_tooth_node` oscillator. It's a modular synthesis style DSP graph, not just a sample player.

The renderer's `Renderable` also inherits it. Each renderable declares named inputs it needs and produces named render targets other nodes can pull from, and `Renderer::ExecuteSceneGraph()` runs the whole thing as a topologically sorted dependency graph. A hand rolled frame graph.

Most engines would never share infrastructure between a synthesizer and a renderer. Mine uses the exact same 40-line class for both, because a dependency graph is a dependency graph regardless of what's actually flowing through it.

### The input system does chords and sequences with one struct

I'm fairly proud of this one. Input mappings bind a named action to a list of `InputChord`s, and each chord can work two completely different ways depending on one flag.

With `bIsSequence` off, it's a simultaneous chord: every key in the list has to be down at once, Ctrl+Shift+S style. With it on, the same struct becomes a timed sequence: press the keys in order within a configurable window (`TimeBetweenKeys`) and it fires, Konami code style. Miss a step or run out of time and it resets, but there's a specific carve out in `ReceiveEvent` so that pressing the first key of the sequence again always restarts it cleanly, even mid-attempt, instead of leaving you stuck.

One data structure, one function, two genuinely different input patterns, and action mappings, axis mappings, and text input listeners all sit on top of the same subsystem.

### Clay UI renders through the same pipeline as everything else

The UI toolkit is Clay, a small immediate mode layout library, wrapped in a `ClayUILayer` that is itself a `SceneLayer` and a `Renderable`. Nothing about it is special cased in the renderer. Each frame it builds a Clay layout, gets back a stream of render commands (rects, borders, images, text, scissor regions), and converts those commands into scene objects with generated or refreshed materials and meshes, which then render into that layer's target like any other renderable in the graph.

The nice part is that UI composes with gameplay layers through the exact same render target plumbing everything else uses. It's not a separate overlay system bolted on afterward.

### I wrote my own protobuf, then I didn't

Networking needs a wire format, and at the time I didn't want to drag a compiled toolchain into my CMake build just to get one. So I built `ozz_typegen`: a small schema compiler that parses a custom `.ozz` schema language and generates C++ structs and enums, paired with a hand rolled binary format (`ozz_binarypacking`).

Its own README was upfront about what it was and wasn't:

> "A crack at doing type generation based on schema similar to protocol buffers. I wanted a low-friction way of doing this in CMake without having to rely on compiled libraries or anything like that. This will obviously be a lot less powerful than protobufs, but for my purposes it should do quite well."

It worked, but the limits were real: C++ output only, no nested types, generated files couldn't reference each other. Eventually those limits caught up with me on simple-mmo's connection and character schemas, and I switched to FlatBuffers instead. Same low-friction CMake integration I was originally chasing, none of the corners I'd cut to get there. `ozz_typegen` is still sitting in the repo, but the networking layer has moved on.

### I wrote my own password hashing, then I didn't

Same pattern, different subsystem. Lights' `core/util` has a `crypto.h` that wraps Crypto++ directly: `GenerateSalt()` and `HashPassword()`, SHA-512 under the hood. It sits right next to things like ring buffers and memory-size literals, which tells you this was never meant to be a generic "engine for any game." It's built with a networked, account-having game in mind.

But that's not what actually protects real users. simple-mmo's server doesn't call into `crypto.h` at all. It uses libsodium's `crypto_pwhash_str`, which hashes with Argon2id and bakes the salt directly into the output string, verified on the other end with `crypto_pwhash_str_verify`. There's even a precomputed dummy hash the server checks against when a user doesn't exist, so a login attempt takes the same amount of time either way and doesn't leak whether an email is registered.

The engine-level version was a reasonable first pass. The real implementation used the properly vetted library where it actually mattered.

## What's next

This was the tour. Future posts will go deeper on specific pieces: the frame graph renderer and how `ExecuteSceneGraph()` actually schedules work, the FlatBuffers based networking layer, and the Clay UI integration in more detail. If there's a particular one you want first, let me know.