Levain
A 3D game engine in C++23 built with Claude Code: I make the architecture and roadmap calls and set the review rules, and the agent writes the code, the tests and the documentation.
My role
- Levain is built with Claude Code. The agent writes the code, the tests, the CI and the documentation. I decide the architecture and the scope, answer its design questions, and review. The commits carry Claude as co-author; the merge commits, made through my GitHub account, carry my name alone.
- Architecture decisions: every structural choice is an ADR first, validated by me before it is implemented. More than thirty of them, from NVRHI and flecs to the code shape I hold the agent to (logic in free functions that take all their dependencies as parameters, ECS glue on a single line).
- Roadmap and milestones: nine phases (0 to 8), planned in hours of my own attention rather than in agent sessions. Each milestone has a measurable acceptance criterion and closes with a tagged release whose notes record the measured result, usually with the command behind it.
- Review and verification rules: a pull request must be reviewable in 30 minutes; zero validation errors in Debug (NVRHI, Vulkan layers, Direct3D 12 debug layer); no test, warning or sanitizer is ever disabled to make something pass; every figure comes with the command or versioned script that produced it.
- The agent workflow: versioned skills in the repository (session, build, autonomous pull request, milestone close, questions), each with a log of the pitfalls already hit. In autonomous mode, a review subagent reviews each pull request before the agent merges it; I make the calls through short polls and review afterwards.
- Engine and game split: the game, Rando, a short exploration game, lives in its own public repository and pulls the engine at a pinned commit. Choosing it reshaped the roadmap (terrain, water and grass, skeletal animation, third-person camera, swimming and gliding), and I arbitrated each addition.
What the engine contains
- A WebGPU backend for NVRHI, written in the repository (NVRHI has none upstream) next to NVRHI’s own Vulkan and Direct3D 12 backends. The engine compiles to WebAssembly: the live demo is a fox you steer through a valley, in a browser with WebGPU enabled (Firefox still needs a flag on Linux).
- A Windows build cross-compiled from Linux with clang-cl, run in Vulkan and Direct3D 12 on an RTX 4070 laptop, and tested in CI on WARP and lavapipe.
- Rendering: PBR with clustered forward+ lighting, cascaded shadow maps (4 cascades, PCF), HDR and tonemapping, image-based lighting, frustum culling, and heightmap terrain, water and GPU-instanced grass as engine plugins.
- Simulation: flecs ECS, fixed-step loop with render interpolation, action-based input, Jolt physics with a character controller.
- Animation: ozz-animation behind a glTF bridge, skinning in a compute shader, clip blending.
- Assets: glTF import, GUIDs in
.metafiles that survive renames, offline cooking (KTX2 with a BC7 cache), hot reload of shaders and textures. - Editor: Dear ImGui panels, with a hierarchy and an inspector driven by component reflection: one declaration per component, the fields read from the struct itself, no code generator in the build.
Why it was interesting
Most of the work is deciding, not typing. The agent lays out options with their trade-offs, I choose, and the choice is written down where the next session will find it. Before the engine went to the browser, a throwaway prototype measured WebGPU in Firefox; before Windows returned to the roadmap, another one measured clang-cl from Linux. Decisions are cheap to make and expensive to undo, so they get evidence first.
Another rule came out of phase 0: three safeguards had stayed green without checking anything. Since then, a check whose condition is not met has to fail loudly instead of quietly not running. The agent now applies it to every check it writes.
The process is engineered like the code. When CI ran for every pull request of a stack and the
waits stretched into hours, I decided it would run once per feature, on the pull request into
main, and that pull requests would stop evicting the caches of main: a run went from an hour or
more to under half an hour. Phases 0 to 6 closed at about 54 hours of my time for
53 estimated.