A Vulkan rendering engine built while following the Vulkan Tutorial, refactored from a single-file application into a modular engine structure.
Tutorial progress: swap chain recreation (end of the "Drawing a triangle" chapter).
The application opens a window and renders a hardcoded triangle using dynamic rendering (no render passes). Window resize and minimize are handled correctly.
src/
main.cpp ← Entry point; constructs Engine and runs it
core/
Config.hpp ← EngineConfig struct (window size, shader paths, etc.)
Engine.hpp/.cpp ← Top-level composer; owns all subsystems and runs the main loop
Window.hpp/.cpp ← GLFW window wrapper; creates the VkSurfaceKHR
vk/
Instance.hpp/.cpp ← Vulkan instance + debug messenger + validation layers
Device.hpp/.cpp ← Physical device selection + logical device + queue
SwapChain.hpp/.cpp ← Swapchain + image views + recreation logic
Pipeline.hpp/.cpp ← Graphics pipeline + pipeline layout + shader module loading
Renderer.hpp/.cpp ← Command pool/buffers, sync objects, per-frame draw loop
io/
FileIO.hpp/.cpp ← Generic binary file reading (used for SPIR-V)
shaders/
shader.slang ← Source shader (Slang language, compiled to SPIR-V)
CMakeLists.txt
The engine is organized as a composition of single-responsibility classes, owned by Engine.
Engine
├── Window
├── Instance
├── SurfaceKHR (raw, from Window + Instance)
├── Device ──────────── needs Instance + Surface
├── SwapChain ───────── needs Device + Window + Surface
├── Pipeline ────────── needs Device + swapchain format
└── Renderer ────────── needs Device + SwapChain + Pipeline
Construction flows top-down; destruction happens in reverse via RAII. Member declaration order in Engine.hpp
determines both.
- RAII everywhere. All Vulkan handles are
vk::raii::*types. No manualvkDestroy*calls. Destruction order is controlled by member declaration order. - Dependencies passed by
const&in constructors. Subsystems store references to their dependencies. This makes lifetimes explicit and surfaces ordering bugs at compile time. - No singletons or globals. The only global state is the Vulkan-Hpp dynamic dispatch loader, which lives in
Instance.cpp. - Classes don't know about things above them.
Rendererdoesn't know aboutWindow;SwapChaindoesn't know aboutEngine. This keeps the graph acyclic. - Configuration lives in
EngineConfig. Window size, shader paths, and future engine-wide settings go there, not scattered as constants.
Engine::mainLoop is the simplest possible driver:
while (!window.shouldClose()) {
window.pollEvents();
const bool resized = window.wasResized();
if (resized) window.resetResizedFlag();
renderer.drawFrame(resized);
}
device.waitIdle();Renderer::drawFrame handles fences, semaphores, command buffer recording, submission, and presentation. It calls
swapChain.recreate() internally when the swapchain becomes out-of-date or when the caller signals a resize.
- CMake ≥ 3.29
- C++20-capable compiler (GCC 13+, Clang 17+, MSVC 19.34+)
- vcpkg with:
vulkanglfw3glm
- Vulkan SDK (for
slangcshader compiler and validation layers)
The CMakeLists auto-detects vcpkg via $VCPKG_ROOT, a local vcpkg/ directory, or C:/vcpkg.
cmake -B build
cmake --build buildOr open the project in CLion / Visual Studio and build from there.
Slang shaders in shaders/*.slang are compiled to SPIR-V at build time via slangc (from the Vulkan SDK). The
resulting .spv files are copied next to the executable.
If slangc is not found, the shaders/ directory is copied as-is — you'll need to pre-compile your shaders manually in
that case.
./build/vulkan_tutorial_appThe executable expects shaders/ to be adjacent to it (the CMake post-build step handles this).
The project uses vulkan_raii.hpp (header-only path) rather than the C++20 vulkan_hpp module. This requires:
VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGEin exactly one TU (Instance.cpp).- Explicit
VULKAN_HPP_DEFAULT_DISPATCHER.init()calls inInstance::createInstance(pre-instance) and after the instance is created (instance-level functions).Deviceinitializes device-level dispatch in its constructor.
These are set up in CMakeLists.txt:
target_compile_definitions(vulkan_tutorial_app PRIVATE
VULKAN_HPP_NO_STRUCT_CONSTRUCTORS
VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1
)The device selection in Device::isDeviceSuitable requires:
- Vulkan 1.3
- Graphics queue family with present support for the surface
VK_KHR_swapchainextension- Features:
shaderDrawParameters,dynamicRendering,synchronization2,extendedDynamicState
Most desktop GPUs from the last few years meet these requirements. Integrated graphics may need driver updates.
Following the Vulkan Tutorial, the next topics are:
- Vertex buffers
- Index buffers
- Uniform buffers + descriptor sets
- Texture mapping
- Depth buffering
- Model loading
- Mipmaps
- Multisampling
Each chapter will likely require extending existing classes (e.g., Pipeline grows to accept descriptor set layouts) or adding new ones (e.g., a Buffer class wrapping vk::raii::Buffer + vk::raii::DeviceMemory).