Vulkan without the 800 lines of boilerplate.

vksdl wraps the ceremony — instance creation, device selection, swapchain management, synchronization — and leaves the actual rendering to you. One #include, raw VkCommandBuffer inside.

~17k lines C++20 54 public headers 20 examples 41 tests
// 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();

800 lines vs 15.

Drag the slider. Left is raw Vulkan. Right is vksdl. Same result.

Raw Vulkan
// 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...
vksdl
// 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);

What It Wraps vs What Stays Raw

Every RAII object exposes its raw Vulkan handle. Full escape hatches everywhere.

vksdl handles thisYou write this
Instance + validation + debug messengerNothing -- one builder call
GPU selection, queue families, feature chainsneedSwapchain(), needRayTracingPipeline()
Swapchain format/present mode, resizerecreate() on window resize
Fences, semaphores, round-robin acquireNothing -- acquireFrame() / presentFrame()
SPIR-V loading, pipeline layout, blend defaultsRecord commands, bind, draw
VMA allocation, typed buffer/image buildersChoose usage, upload data
BLAS/TLAS, SBT layout, RT pipelineTrace rays, write shaders
Render graph: barriers, lifetime, toposortDeclare passes, record in callbacks

Drop to raw Vulkan anywhere: vkDevice(), vkPipeline(), vkBuffer(), vkImage()

What's Inside

Core

Instance, device, surface, swapchain builders. RAII, move-only, Result<T> everywhere. No exceptions.

Ray Tracing

BLAS/TLAS construction with compaction. SBT layout. RT pipeline builder. From triangle to 475-sphere path tracer.

Render Graph

Automatic barriers, toposort, transient resources. Three-layer API: raw, render targets, pipeline-aware. ~8μs compile for 40 passes.

Pipeline Model

Graphics Pipeline Library fast-link + async background optimize. Disk-persistent cache. Atomic swap on readiness.

Memory (VMA)

Typed buffer/image builders. Staging uploads. Dedicated transfer queue. Zero per-frame allocations in the hot path.

Descriptors

Bump allocator, fluent writer, growable pool. Bindless tables. Push descriptors. Deduplicating sampler cache.

Shader Reflection

SPIRV-Reflect based. Auto-generate descriptor set layouts from SPIR-V. Layer 2 render graph auto-bind.

Error Handling

Result<T> on every fallible call. Human-readable messages with actionable suggestions. Device fault diagnostics.

The Triangle Example

120 lines including the render loop and resize handling. The raw Vulkan equivalent is 800+.

examples/triangle/main.cpp View on GitHub →
#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();
}

20 Working Examples

From first triangle to 40-pass deferred renderer. Each is a self-contained main.cpp.

Design Philosophy

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.

Get Started

Vulkan SDK 1.3+ CMake 3.21+ C++20 compiler
# 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

Dependencies

All fetched automatically via vcpkg. No manual downloads.