Ongoing

Cinder — a Vulkan renderer in pure C

Designer & Sole Engineer·2025·4 min read

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++

Reasoning:

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.

Alternatives considered:
  • C++ with a light abstraction layer
  • Rust with ash or wgpu

Require Vulkan 1.3 rather than supporting 1.0+

Reasoning:

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.

Alternatives considered:
  • 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

Reasoning:

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.

Alternatives considered:
  • Texture atlases with gutters sized to mip depth
  • Capped mips per tile

Per-instance data is cell corner positions, not a transform matrix

Reasoning:

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.

Alternatives considered:
  • 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 selection
    Graphics 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.