Cinder — a Vulkan renderer in pure C
A from-scratch Vulkan 1.3 renderer written in C99, built to drive a competitive tycoon sim — kit-of-parts buildings, a few thousand animated characters, and a deterministic simulation.
Overview
Cinder is the rendering layer for an unannounced tycoon sim. Written in C99 against Vulkan 1.3, with a Slang shader pipeline and an asset cook that produces a flat pack format. No engine-side C++, no middleware, and a deliberately small API surface: describe pipelines programmatically, then render.
Problem
Commercial engines are built around a per-instance transform matrix and a general-purpose scene graph. The game needs neither. Buildings are a kit of parts deformed into the cells of an irregular skewed grid, so the per-instance data is cell corner positions rather than a matrix — the shape most engines assume is the wrong shape here. The simulation is also deterministic, which constrains the toolchain in ways middleware does not let you control.
Constraints
- Solo developer — every subsystem has to earn its place
- Deterministic simulation: no -ffast-math, no -march, plain -O0/-O3
- The crowd is the frame budget — a few thousand animated characters, not the city
- Vulkan 1.3 minimum, enforced at device selection
- No engine-side C++, which rules out most middleware
Approach
Build up from the Vulkan primitives with a fixed-size binding model rather than a general renderer. Keep the engine notes as a running decision log, so choices that are expensive to reverse get recorded with their reasoning at the time. Two example programs — a model viewer and a particle system — act as the working smoke tests.
Key Decisions
Pure C99 with no engine-side C++
Keeps the ABI simple and the build fast, and forces an explicit design where C++ would hide allocation and lifetime behind abstractions. The cost is accepted deliberately: it rules out most middleware, nearly all of which assumes a C++ ABI.
- C++ with a light abstraction layer
- Rust with ash or wgpu
Require Vulkan 1.3 rather than supporting 1.0+
Descriptor indexing has been core since 1.2, so requiring 1.3 guarantees it is present on every device the game will ever run on. That single guarantee removes a whole class of fallback paths and unlocks the bindless approach below.
- Vulkan 1.0 baseline with feature detection and fallbacks
- Target 1.1 for wider hardware coverage
No texture atlases for 3D content — bindless descriptor arrays instead
Atlases exist to solve draw-call batching under one-texture-per-descriptor binding. With Vulkan 1.3 guaranteed, descriptor indexing solves batching directly without constraining content. Atlases would also break the full mip chain (neighbouring tiles bleed at every level past the first) and make REPEAT wrapping impossible, since wrap mode is per-image. Better to make a contained renderer change than bake a constraint into the cook pipeline and every UV in the project.
- Texture atlases with gutters sized to mip depth
- Capped mips per tile
Per-instance data is cell corner positions, not a transform matrix
Buildings are assembled from a kit of pieces deformed into the cells of an irregular grid, so the vertex shader interpolates mesh-local coordinates into the cell corners. Anything assuming a per-instance matrix is structurally wrong for this game, which is a large part of why an off-the-shelf engine was not the answer.
- Rigid per-instance transforms with pre-deformed mesh variants
- Regular grid, avoiding deformation entirely
Tech Stack
- C99
- Vulkan 1.3
- Slang
- stb_image
Result & Impact
- C99 — no engine-side C++Written in
- Vulkan 1.3, enforced at device selectionGraphics API
In active development. Graphics and compute paths both work, with a fixed binding model (≤4 sets, ≤4 bindings per set, 2 frames in flight) and a Slang shader pipeline feeding a flat pack format. The engine notes double as a decision log — each entry records why a choice was made and what would have to change to reverse it, which is what makes a solo project of this size navigable a year later.
Learnings
- Requiring a higher API baseline can remove more complexity than it costs in hardware coverage — one guaranteed feature deleted an entire category of fallback code
- Determinism is a build-system property as much as a code property; compiler flags are part of the design, not an afterthought
- Writing the decision log while deciding, rather than afterwards, is what keeps a solo codebase legible
Why not an engine
The usual answer for a solo developer is Unreal, Unity, or Godot, and for most projects it is the right answer. It is not the right answer here for one structural reason: the game’s buildings are deformed into their grid cells rather than rigidly transformed into place.
That single fact means the per-instance data is four corner positions, not a matrix — and a scene graph built around per-instance transforms fights that at every level. Add a deterministic simulation, which wants control over compiler flags that middleware does not expose, and the case for owning the stack stops being a preference and starts being a requirement.