A tiny top-down shooter on a hand-written Vulkan renderer. Drive a red circle ("the car") with WASD, aim with the mouse, hold left-click to shoot yellow bullets at a blue horde that chases you and respawns from the edges.
Modern, bindless Vulkan — deliberately small:
- Vulkan 1.3: dynamic rendering + synchronization2 (no render passes / framebuffers).
- Bindless: ONE descriptor set (an array of storage buffers). Every pipeline shares one layout; a buffer is handed to shaders by an index in a push constant. Adding a buffer never touches a descriptor layout.
- GPU sim: the horde + bullets run entirely in a GLSL compute shader
(
shaders/physics.comp) over a bucket grid; the CPU only drives the player. - Data-driven: buffers live in
BUF_SPECS, pipelines inPIPE_SPECS(render.odin). - Separate build step:
tools/build.odincopies the GPU structs from render.odin's@glslblock intotools/gen, which reflects them →shaders/gen.glsl, then compiles + validates each GLSL shader (glslc -Werror→spirv-val) intoshaders/spv/. The game only reads the compiled.spv— no compiler in the game. - Odin → GLSL: the CPU↔GPU contract (
Body/Pushstructs, buffers, push block, gameplay constants) is defined once in Odin@glslblocks and reflected intogen.glsl— one source of truth. (Vertex→fragment varyings stay shader-side, in each pipeline's.vert/.frag.) - Hot reload: the game watches its compiled
.spvand reloads pipelines when they change. Under./run.sh watchthe watcher recompiles GLSL → SPIR-V (+ naga) on save, so editing a.glslreloads in-app, no restart. Plus Vulkan validation layers at runtime.
| control | action |
|---|---|
| W A S D | drive the car |
| mouse | aim |
| left-click | shoot (hold to fire) |
| R | restart |
| ESC | quit |
| file | what it is |
|---|---|
main.odin |
window + loop + input |
vk.odin |
Vulkan backend: bootstrap, bindless, buffers, pipelines, frame |
render.odin |
high-level surface: buffer list, pipeline list, init, render() |
shaders.odin |
loads compiled .spv + hot-reload trigger |
tools/gen/ |
reflects the @glsl blocks → shaders/gen.glsl |
tools/build.odin |
build step: GLSL → validated SPIR-V + game; watch = live-reload loop |
car.odin |
CPU game: player movement + bullet spawning |
shaders/common.glsl |
shared shader contract (bindless decls, push constant, consts) |
shaders/physics.comp |
GPU sim: bucket grid + chase/separate/shoot/respawn |
shaders/circle.{vert,frag} |
instanced circle-SDF render (own vertex→fragment varyings) |
./run.sh # build + run (Vulkan validation ON)
./run.sh watch # watcher: recompile shaders (+naga) on .glsl, rebuild binary on .odin
./run.sh shot # headless drive + screenshot → .debug_screenshots/
Requires Odin, the Vulkan SDK / libvulkan, glslc + spirv-tools, and
vendor:sdl3 / vendor:vulkan / vendor:stb. Run from the project root (shaders are
read from shaders/). For runtime validation: sudo pacman -S vulkan-validation-layers.
./run.sh dist # all three targets
./run.sh dist linux # (or windows | mac)
Godot-style: one Linux host builds every native bundle. Odin emits the game as an
object per target and zig cc links it — zig carries the glibc / mingw-w64 / macOS
sysroots, so there's no MSVC, no osxcross, no second machine. Only SDL3 is linked;
Vulkan is loaded at runtime, and on macOS that driver is MoltenVK (Vulkan-on-Metal),
bundled into the .app and selected via SDL_VULKAN_LIBRARY. Linux targets an old
glibc (2.17) so the binary runs on ancient distros; the Mac build is arm64 (Apple
Silicon). Prebuilt SDL3 + MoltenVK are fetched from conda-forge into a gitignored
libs/ cache. Outputs land in dist/ as ready-to-ship archives:
| target | artifact |
|---|---|
| Linux | toomanymachines-linux-x86_64.tar.gz |
| Windows | toomanymachines-windows-x86_64.zip (GUI) |
| macOS | toomanymachines-macos-arm64.zip (.app) |
Extra host tools: zig, curl, unzip, zstd, tar, zip, and — for the mac
target only — llvm-install-name-tool plus a code signer (rcodesign, from
cargo install apple-codesign).
- Gatekeeper: the
.appis ad-hoc signed (required so Apple Silicon will run it at all) but not notarized, so a downloaded copy is quarantined. First launch: right- click → Open, or clear the flag:xattr -dr com.apple.quarantine TooManyMachines.app. - MoltenVK: Vulkan runs on Metal via the bundled MoltenVK. The game points SDL at it and
forces
MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS=1in-process on startup (loop.odin) — the renderer is bindless (unsized runtime descriptor arrays), which MoltenVK can only honor through Metal tier-2 argument buffers (standard on Apple Silicon). Doing it in code means it holds whether launched from Finder or a terminal; the.appInfo.plistsets the same env as a fallback.