How I learned Vulkan and wrote a small game engine with it
Elias Daler built a focused Vulkan game engine and two small demos in roughly three months by prioritizing practical game development over premature abstraction. The write-up distills the renderer architecture, modern Vulkan techniques, and hard-won implementation advice.
How I learned Vulkan and wrote a small game engine with it
Author: Elias Daler | Published: 2024-06-05 | Generated: 2025-11-22 | Domain: edw.is
Tags: ‘#vulkan’ ‘#graphics-programming’ ‘#game-engine’ ‘#rendering’ ‘#cpp’ ‘#gamedev’
TLDR
Elias Daler learned Vulkan and built EDBR, a purpose-built engine for small desktop games, plus a 3D cat game and 2D platformer, in about three months. His central advice is to start with a small game rather than a general-purpose engine, use modern Vulkan features and focused helper libraries to reduce boilerplate, and defer abstractions or optimizations until profiling proves they are necessary. EDBR relies on dynamic rendering, compute skinning, buffer device addresses, bindless descriptors, explicit synchronization, and a command-based renderer/game boundary.
Key Takeaways
- A constrained scope made the project achievable: EDBR has roughly 19k engine lines of code—6.7k graphics-related and 2k of light Vulkan abstractions—alongside a 4.6k-line 3D game and a 1.2k-line platformer. It was developed around actual game needs rather than as a universal engine.
- Modern Vulkan lowers the entry barrier: Dynamic rendering,
vk-bootstrap, Vulkan Memory Allocator (VMA), and volk eliminate substantial setup and extension-loading boilerplate. Daler recommends vkguide as the primary starting resource. - The renderer minimizes descriptor-set complexity: Buffer device address (BDA) and programmable vertex pulling pass buffer addresses through push constants, while a single bindless descriptor set stores textures and samplers. Materials can therefore reference textures by integer IDs rather than requiring per-material descriptor bindings.
- The frame is organized as explicit passes: Compute skinning produces reusable animated vertex buffers; cascaded shadow mapping uses a 4096×4096, three-slice depth texture; geometry is rendered with MSAA, followed by manual depth resolve, fog/post-processing, and UI.
- Batching produces efficient 2D rendering: The sprite renderer generates quad vertices from
gl_VertexIndex, stores per-sprite data in a GPU buffer, and draws all sprites in one instanced call. It renders 10,000 sprites in approximately 315 microseconds. - Manual synchronization is acceptable at small scale: The engine inserts explicit
vkCmdPipelineBarrier2barriers between passes, such as compute skinning and shadow-map rendering. A render graph is deferred as future work rather than introduced prematurely.
Images & Media
- Final rendered frame — The completed frame after geometry, post-processing, and dialogue UI rendering.
- Frame geometry resolve — Resolved multisampled geometry buffer used to illustrate MSAA output.
- Cascaded shadow-map slice — One slice of the 4096×4096 three-cascade shadow map.
- Sprite quad diagram — Shows the two triangles generated procedurally for each sprite quad.
- Many-sprites benchmark — Profiling capture for rendering 10,000 sprites.
- UI layout examples — Diagrams demonstrating relative sizing, positioning, origins, and fixed-size UI placement.
Referenced Links
- EDBR source code — Open-source repository for the Vulkan engine and game demos.
- vkguide — Primary Vulkan learning guide recommended by the author.
- Vulkan Memory Allocator — Library used to simplify Vulkan memory allocation.
- vk-bootstrap — Initialization helper for device selection, swapchain setup, and other Vulkan boilerplate.
- Vulkan synchronization examples — Khronos reference material for explicit Vulkan synchronization.
- Bindless textures in Vulkan — Technical guide for descriptor indexing and bindless texture workflows.