vksdl wraps the ceremony — instance creation, device selection,
swapchain management, synchronization — and leaves the actual
rendering to you. One #include, raw VkCommandBuffer
inside.
// Setup is 15 lines. The rest is your Vulkan.
auto app = vksdl::App::create().value();
auto window = app.createWindow("Triangle", 1280, 720).value();
auto instance = vksdl::InstanceBuilder{}
.appName("tri").requireVulkan(1, 3)
.enableWindowSupport().build().value();
auto surface = vksdl::Surface::create(instance, window).value();
auto device = vksdl::DeviceBuilder(instance, surface)
.needSwapchain().needDynamicRendering().needSync2()
.preferDiscreteGpu().build().value();
auto swapchain = vksdl::SwapchainBuilder(device, surface)
.size(window.pixelSize()).build().value();
auto pipeline = vksdl::PipelineBuilder(device)
.vertexShader("shaders/tri.vert.spv")
.fragmentShader("shaders/tri.frag.spv")
.colorFormat(swapchain).build().value();
Drag the slider. Left is raw Vulkan. Right is vksdl. Same result.
// Instance creation -- raw Vulkan (partial)
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "Triangle";
appInfo.applicationVersion = VK_MAKE_VERSION(1,0,0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1,0,0);
appInfo.apiVersion = VK_API_VERSION_1_3;
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
uint32_t sdlExtCount = 0;
const char* const* sdlExts =
SDL_Vulkan_GetInstanceExtensions(&sdlExtCount);
std::vector<const char*> extensions(sdlExts,
sdlExts + sdlExtCount);
extensions.push_back(
VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
createInfo.enabledExtensionCount =
static_cast<uint32_t>(extensions.size());
createInfo.ppEnabledExtensionNames = extensions.data();
const char* validationLayer =
"VK_LAYER_KHRONOS_validation";
createInfo.enabledLayerCount = 1;
createInfo.ppEnabledLayerNames = &validationLayer;
VkDebugUtilsMessengerCreateInfoEXT debugCI{};
debugCI.sType =
VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
debugCI.messageSeverity =
VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
debugCI.messageType =
VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT |
VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
debugCI.pfnUserCallback = debugCallback;
createInfo.pNext = &debugCI;
VkInstance instance;
if (vkCreateInstance(&createInfo, nullptr,
&instance) != VK_SUCCESS) {
throw std::runtime_error(
"failed to create instance!");
}
// ... then 200+ more lines for device selection,
// queue creation, swapchain, image views,
// render pass, framebuffers, command pool,
// sync objects, pipeline layout, shaders...
// Same result. Setup is 15 lines.
auto app = vksdl::App::create().value();
auto window = app.createWindow("Triangle", 1280, 720).value();
auto instance = vksdl::InstanceBuilder{}
.appName("tri")
.requireVulkan(1, 3)
.enableWindowSupport()
.build().value();
auto surface = vksdl::Surface::create(instance, window).value();
auto device = vksdl::DeviceBuilder(instance, surface)
.needSwapchain()
.needDynamicRendering()
.needSync2()
.preferDiscreteGpu()
.build().value();
auto swapchain = vksdl::SwapchainBuilder(device, surface)
.size(window.pixelSize())
.build().value();
auto frames = vksdl::FrameSync::create(device,
swapchain.imageCount()).value();
auto pipeline = vksdl::PipelineBuilder(device)
.vertexShader("shaders/triangle.vert.spv")
.fragmentShader("shaders/triangle.frag.spv")
.colorFormat(swapchain)
.build().value();
// Your render loop is standard Vulkan.
// You own the command buffer.
auto [frame, img] = vksdl::acquireFrame(
swapchain, frames, device, window).value();
vksdl::beginOneTimeCommands(frame.cmd);
// vkCmdBeginRendering, vkCmdDraw, vkCmdEndRendering
vksdl::endCommands(frame.cmd);
vksdl::presentFrame(device, swapchain, window,
frame, img, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);
Every RAII object exposes its raw Vulkan handle. Full escape hatches everywhere.
| vksdl handles this | You write this |
|---|---|
| Instance + validation + debug messenger | Nothing -- one builder call |
| GPU selection, queue families, feature chains | needSwapchain(), needRayTracingPipeline() |
| Swapchain format/present mode, resize | recreate() on window resize |
| Fences, semaphores, round-robin acquire | Nothing -- acquireFrame() / presentFrame() |
| SPIR-V loading, pipeline layout, blend defaults | Record commands, bind, draw |
| VMA allocation, typed buffer/image builders | Choose usage, upload data |
| BLAS/TLAS, SBT layout, RT pipeline | Trace rays, write shaders |
| Render graph: barriers, lifetime, toposort | Declare passes, record in callbacks |
Drop to raw Vulkan anywhere:
vkDevice(), vkPipeline(), vkBuffer(), vkImage()
Instance, device, surface, swapchain builders. RAII, move-only, Result<T> everywhere. No exceptions.
BLAS/TLAS construction with compaction. SBT layout. RT pipeline builder. From triangle to 475-sphere path tracer.
Automatic barriers, toposort, transient resources. Three-layer API: raw, render targets, pipeline-aware. ~8μs compile for 40 passes.
Graphics Pipeline Library fast-link + async background optimize. Disk-persistent cache. Atomic swap on readiness.
Typed buffer/image builders. Staging uploads. Dedicated transfer queue. Zero per-frame allocations in the hot path.
Bump allocator, fluent writer, growable pool. Bindless tables. Push descriptors. Deduplicating sampler cache.
SPIRV-Reflect based. Auto-generate descriptor set layouts from SPIR-V. Layer 2 render graph auto-bind.
Result<T> on every fallible call. Human-readable messages with actionable suggestions. Device fault diagnostics.
120 lines including the render loop and resize handling. The raw Vulkan equivalent is 800+.
#include <vksdl/vksdl.hpp>
int main() {
// --- Bootstrap: 15 lines, all RAII, all Result<T> ---
auto app = vksdl::App::create().value();
auto window = app.createWindow("Triangle", 1280, 720).value();
auto instance = vksdl::InstanceBuilder{}.appName("tri")
.requireVulkan(1, 3).enableWindowSupport().build().value();
auto surface = vksdl::Surface::create(instance, window).value();
auto device = vksdl::DeviceBuilder(instance, surface)
.needSwapchain().needDynamicRendering().needSync2()
.preferDiscreteGpu().build().value();
auto swapchain = vksdl::SwapchainBuilder(device, surface)
.size(window.pixelSize()).build().value();
auto frames = vksdl::FrameSync::create(device,
swapchain.imageCount()).value();
auto pipeline = vksdl::PipelineBuilder(device)
.vertexShader("shaders/triangle.vert.spv")
.fragmentShader("shaders/triangle.frag.spv")
.colorFormat(swapchain).build().value();
// --- Render loop: standard Vulkan, you own the command buffer ---
while (!window.closeRequested()) {
app.pollEvents();
if (window.consumeResize()) {
device.waitIdle();
swapchain.recreate(window.pixelSize());
}
auto [frame, img] = vksdl::acquireFrame(
swapchain, frames, device, window).value();
vksdl::beginOneTimeCommands(frame.cmd);
vksdl::transitionToColorAttachment(frame.cmd, img.image);
// Your rendering code -- vkCmdBeginRendering, bind, draw
// ...
vksdl::transitionToPresent(frame.cmd, img.image);
vksdl::endCommands(frame.cmd);
vksdl::presentFrame(device, swapchain, window, frame, img,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);
}
device.waitIdle();
}
From first triangle to 40-pass deferred renderer. Each is a self-contained main.cpp.
Wrap ceremony. Leave intent raw.
Ceremony is code with one correct answer: creating an instance, selecting a GPU, destroying objects in the right order. vksdl wraps that.
Intent is code where you're making real choices: recording commands, choosing wait stages, structuring submissions. vksdl leaves that alone.
The test: if two experienced Vulkan developers would write the same boilerplate identically, vksdl should eliminate it. If they'd write it differently, vksdl stays out of the way.
# Clone with vcpkg submodule
git clone --recursive https://github.com/MrMartyK/vksdl.git
cd vksdl
# Configure
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake
# Build
cmake --build build
# Run tests
cd build && ctest --output-on-failure
All fetched automatically via vcpkg. No manual downloads.