Skip to content
Waxed Display Server
← Back to Docs

Vulkan RAII

Vulkan-Hpp RAII Usage Guide

The Mandatory Rule

CRITICAL RULE - ABSOLUTELY NO EXCEPTIONS

ALL Vulkan code in the Waxed project MUST use Vulkan-Hpp RAII (vulkan_raii.hpp).

If you write Vulkan code without vk::raii::, you are violating the PRIMARY RULE of this project.


Table of Contents

  1. Why RAII is Mandatory
  2. The Correct Include
  3. RAII Types Reference
  4. Correct Usage Patterns
  5. Forbidden Patterns
  6. Common Patterns in Waxed
  7. Memory Management
  8. Command Buffers
  9. Extension Functions
  10. Migration Guide
  11. Ownership Semantics

Why RAII is Mandatory

Automatic Cleanup

The RAII (Resource Acquisition Is Initialization) pattern guarantees that Vulkan resources are automatically cleaned up when they go out of scope. This eliminates:

  • Memory leaks: No forgotten vkDestroy* calls
  • Use-after-free bugs: Resources live exactly as long as their owning object
  • Destructor boilerplate: ~500+ lines of cleanup code eliminated

Exception Safety

Even though Waxed uses std::expected instead of exceptions, RAII provides exception-safe semantics. If code paths change in the future, resources are still guaranteed cleanup.

Type Safety

The vk::raii::* wrappers are strongly-typed C++ classes that prevent:

  • Passing wrong handle types to functions
  • Confusion between similar handle types
  • Accidental integer conversions

Less Code

Compare the boilerplate:

// Raw Vulkan (FORBIDDEN)
VkImage image;
VkDeviceMemory memory;
VkImageView view;

// ... many lines of create calls ...

if (error) {
    vkDestroyImageView(device, view, nullptr);
    vkFreeMemory(device, memory, nullptr);
    vkDestroyImage(device, image, nullptr);
    return error;
}

// At end of function:
vkDestroyImageView(device, view, nullptr);
vkFreeMemory(device, memory, nullptr);
vkDestroyImage(device, image, nullptr);
// RAII (CORRECT)
vk::raii::Image image{device, imageInfo};
vk::raii::DeviceMemory memory{device, allocInfo};
vk::raii::ImageView view{device, viewInfo};

// That's it! Automatic cleanup on scope exit.

Industry Standard

Vulkan-Hpp RAII is the industry standard for modern C++ Vulkan development. It’s developed by the Khronos Group and shipped with the Vulkan SDK.


The Correct Include

Required Include

#include <vulkan/vulkan_raii.hpp>

Forbidden Includes

// ❌ FORBIDDEN - Do not use these
#include <vulkan/vulkan.h>
#include <vulkan/vulkan.hpp>

Include Order

Vulkan headers should be included FIRST to avoid conflicts with other waxed types:

// In headers:
#pragma once
#include <vulkan/vulkan_raii.hpp>  // FIRST
#include <memory>
#include <vector>
// In source files:
#include <vulkan/vulkan_raii.hpp>  // FIRST
#include "my_header.h"
#include <other_headers>

Header Location

The header is installed at /usr/include/vulkan/vulkan_raii.hpp on Arch Linux systems.


RAII Types Reference

Core Object Types

RAII TypeRaw Handle (Forbidden)Purpose
vk::raii::ContextN/AEntry point for creating Instance
vk::raii::InstanceVkInstanceVulkan instance connection
vk::raii::PhysicalDeviceVkPhysicalDeviceGPU device (handle only)
vk::raii::DeviceVkDeviceLogical device
vk::raii::QueueVkQueueCommand queue

Resource Types

RAII TypeRaw Handle (Forbidden)Purpose
vk::raii::ImageVkImageImage resource
vk::raii::ImageViewVkImageViewImage view
vk::raii::BufferVkBufferBuffer resource
vk::raii::BufferViewVkBufferViewBuffer view
vk::raii::DeviceMemoryVkDeviceMemoryDevice memory allocation
vk::raii::SamplerVkSamplerTexture sampler

Rendering Types

RAII TypeRaw Handle (Forbidden)Purpose
vk::raii::RenderPassVkRenderPassRender pass
vk::raii::FramebufferVkFramebufferFramebuffer
vk::raii::CommandPoolVkCommandPoolCommand buffer pool
vk::raii::CommandBufferVkCommandBufferCommand buffer
vk::raii::PipelineVkPipelineGraphics/compute pipeline
vk::raii::PipelineLayoutVkPipelineLayoutPipeline layout

Descriptor Types

RAII TypeRaw Handle (Forbidden)Purpose
vk::raii::DescriptorPoolVkDescriptorPoolDescriptor pool
vk::raii::DescriptorSetLayoutVkDescriptorSetLayoutDescriptor set layout
vk::raii::DescriptorSetsVkDescriptorSetDescriptor sets (plural!)

Shader Types

RAII TypeRaw Handle (Forbidden)Purpose
vk::raii::ShaderModuleVkShaderModuleShader module

Synchronization Types

RAII TypeRaw Handle (Forbidden)Purpose
vk::raii::FenceVkFenceFence
vk::raii::SemaphoreVkSemaphoreSemaphore
vk::raii::EventVkEventEvent

Query Types

RAII TypeRaw Handle (Forbidden)Purpose
vk::raii::QueryPoolVkQueryPoolQuery pool

Correct Usage Patterns

Basic Object Creation

#include <vulkan/vulkan_raii.hpp>

// Create context and instance
vk::raii::Context context;
vk::raii::Instance instance{context, instanceCreateInfo};

// Physical device is not owned, but wrapped for convenience
vk::raii::PhysicalDevice physicalDevice{instance, physicalDevice};

// Create logical device
vk::raii::Device device{physicalDevice, deviceCreateInfo};

// Get queue from device
vk::raii::Queue queue{device, queueFamilyIndex, queueIndex};

Creating and Binding Resources

// Create image
vk::ImageCreateInfo imageInfo{};
imageInfo.setImageType(vk::ImageType::e2D);
imageInfo.setExtent({width, height, 1});
imageInfo.setFormat(vk::Format::eR8G8B8A8Unorm);
// ... set other properties

vk::raii::Image image{device, imageInfo};

// Get memory requirements
auto memReqs = image.getMemoryRequirements();

// Find memory type (helper function)
uint32_t memType = find_memory_type(
    physicalDevice.getMemoryProperties(),
    memReqs.memoryTypeBits,
    vk::MemoryPropertyFlagBits::eDeviceLocal
);

// Allocate and bind memory
vk::MemoryAllocateInfo allocInfo{};
allocInfo.setAllocationSize(memReqs.size);
allocInfo.setMemoryTypeIndex(memType);

vk::raii::DeviceMemory memory{device, allocInfo};
image.bindMemory(*memory, 0);

Creating Image Views

vk::ImageViewCreateInfo viewInfo{};
viewInfo.setImage(*image);
viewInfo.setViewType(vk::ImageViewType::e2D);
viewInfo.setFormat(vk::Format::eR8G8B8A8Unorm);
viewInfo.setSubresourceRange({
    vk::ImageAspectFlagBits::eColor,
    0, 1, 0, 1
});

vk::raii::ImageView view{device, viewInfo};

Command Buffers

// Create command pool
vk::CommandPoolCreateInfo poolInfo{};
poolInfo.setFlags(vk::CommandPoolCreateFlagBits::eResetCommandBuffer);
poolInfo.setQueueFamilyIndex(queueFamilyIndex);

vk::raii::CommandPool commandPool{device, poolInfo};

// Allocate command buffers
vk::CommandBufferAllocateInfo allocInfo{};
allocInfo.setCommandPool(*commandPool);
allocInfo.setLevel(vk::CommandBufferLevel::ePrimary);
allocInfo.setCommandBufferCount(1);

auto commandBuffers = device.allocateCommandBuffers(allocInfo);
vk::raii::CommandBuffer cmdBuffer{std::move(commandBuffers[0])};

// Record commands
vk::CommandBufferBeginInfo beginInfo{};
beginInfo.setFlags(vk::CommandBufferUsageFlagBits::eOneTimeSubmit);

cmdBuffer.begin(beginInfo);

// ... record commands ...

cmdBuffer.end();

Pipelines

// Shader modules
vk::raii::ShaderModule vertShader{device, vertCreateInfo};
vk::raii::ShaderModule fragShader{device, fragCreateInfo};

// Pipeline layout
vk::raii::PipelineLayout pipelineLayout{device, pipelineLayoutInfo};

// Graphics pipeline
vk::raii::Pipeline graphicsPipeline{
    device,
    VK_NULL_HANDLE,  // pipeline cache
    graphicsPipelineCreateInfo
};

Descriptor Sets

// Create descriptor pool
vk::DescriptorPoolCreateInfo poolInfo{};
poolInfo.setFlags(vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet);
poolInfo.setMaxSets(1);
poolInfo.setPoolSizes(poolSizes);

vk::raii::DescriptorPool descriptorPool{device, poolInfo};

// Create descriptor set layout
vk::raii::DescriptorSetLayout layout{device, layoutInfo};

// Allocate descriptor sets (note: plural type)
vk::DescriptorSetAllocateInfo allocInfo{};
allocInfo.setDescriptorPool(*descriptorPool);
allocInfo.setSetLayouts(*layout);

vk::raii::DescriptorSets descriptorSets{device, allocInfo};

// Update descriptor sets
vk::WriteDescriptorSet write{};
write.setDstSet(*descriptorSets[0]);
write.setDstBinding(0);
write.setDescriptorCount(1);
write.setDescriptorType(vk::DescriptorType::eCombinedImageSampler);
write.setPImageInfo(&imageInfo);

device.updateDescriptorSets(write, nullptr);

Synchronization

// Create fence
vk::FenceCreateInfo fenceInfo{};
fenceInfo.setFlags(vk::FenceCreateFlagBits::eSignaled);

vk::raii::Fence fence{device, fenceInfo};

// Wait for fence
device.waitForFences(*fence, VK_TRUE, UINT64_MAX);
device.resetFences(*fence);

// Create semaphore
vk::SemaphoreCreateInfo semInfo{};
vk::raii::Semaphore semaphore{device, semInfo};

Forbidden Patterns

Manual Cleanup Calls

// ❌ FORBIDDEN - Manual vkDestroy* calls
vk::raii::Image image{device, imageInfo};
vkDestroyImage(device, *image, nullptr);  // WRONG!

// ✅ CORRECT - Let RAII handle cleanup
vk::raii::Image image{device, imageInfo};
// Destructor cleans up automatically

Manual Create Calls

// ❌ FORBIDDEN - Manual vkCreate* calls
VkImage image;
vkCreateImage(device, &imageInfo, nullptr, &image);

// ✅ CORRECT - Use RAII constructor
vk::raii::Image image{device, imageInfo};

Mixing Raw and RAII Handles

// ❌ FORBIDDEN - Mixing types
VkImage rawImage;
vk::raii::ImageView view{device, rawImage, viewInfo};  // Works, but discouraged

// ✅ CORRECT - Consistent RAII usage
vk::raii::Image image{device, imageInfo};
vk::raii::ImageView view{device, *image, viewInfo};

Storing Pointers to RAII Objects Carelessly

// ❌ DANGEROUS - Storing pointer to temporary
vk::raii::Image* img = &(vk::raii::Image{device, info});  // TEMPORARY!
// Use of 'img' here is use-after-free

// ✅ CORRECT - Proper lifetime management
auto image = std::make_unique<vk::raii::Image>(device, info);
vk::raii::Image* img = image.get();

Common Patterns in Waxed

Pattern 1: Shared Vulkan Device

In include/waxed/vulkan_handles.h, the SharedVulkanDevice structure:

struct SharedVulkanDevice {
    std::unique_ptr<vk::raii::Context> context;
    std::unique_ptr<vk::raii::Instance> instance;
    std::optional<vk::raii::PhysicalDevice> physical_device;  // Note: optional
    std::unique_ptr<vk::raii::Device> device;
    std::unique_ptr<vk::raii::Queue> graphics_queue;
    uint32_t graphics_queue_family = 0;

    std::unique_ptr<vk::raii::DescriptorPool> descriptor_pool;
    std::unique_ptr<vk::raii::DescriptorSetLayout> descriptor_layout;

    // Extension function pointers
    PFN_vkGetSemaphoreFdKHR vkGetSemaphoreFdKHR = nullptr;

    // Device properties
    vk::PhysicalDeviceProperties device_properties{};
    vk::PhysicalDeviceMemoryProperties memory_properties{};

    // Extension support flags
    bool supports_dma_buf = false;
    bool supports_timeline_semaphore = false;

    std::atomic<bool> initialized{false};
};

Pattern 2: Plugin Initialization

From plugins/streaming/texture_streamer.cpp:

TextureStreamer::TextureStreamer(vk::raii::Instance& instance,
                                 vk::raii::PhysicalDevice& physical_device,
                                 vk::raii::Device& device,
                                 vk::raii::Queue& transfer_queue,
                                 uint32_t transfer_queue_family,
                                 const Config& config)
    : instance_(instance)
    , physical_device_(physical_device)
    , device_(device)
    , transfer_queue_(transfer_queue)
    , transfer_queue_family_(transfer_queue_family)
{
    // Create command pool
    vk::CommandPoolCreateInfo pool_info(
        vk::CommandPoolCreateFlagBits::eResetCommandBuffer,
        transfer_queue_family_
    );
    cmd_pool_.emplace(device_, pool_info);

    // ... rest of initialization
}

Pattern 3: Staging Buffer with Dynamic Resize

auto TextureStreamer::ensure_staging_buffer(size_t required_size) -> bool {
    // Destroy old buffer if exists
    if (staging_buffer_) {
        staging_buffer_.reset();
        staging_memory_.reset();
        staging_ptr_ = nullptr;
        staging_size_ = 0;
    }

    // Create new buffer
    vk::BufferCreateInfo buffer_info{};
    buffer_info.setSize(required_size);
    buffer_info.setUsage(vk::BufferUsageFlagBits::eTransferSrc);
    staging_buffer_.emplace(device_, buffer_info);

    // Allocate memory
    auto mem_reqs = staging_buffer_->getMemoryRequirements();
    vk::MemoryAllocateInfo alloc_info{};
    alloc_info.setAllocationSize(mem_reqs.size);
    alloc_info.setMemoryTypeIndex(mem_type);
    staging_memory_.emplace(device_, alloc_info);

    // Bind and map
    staging_buffer_->bindMemory(**staging_memory_, 0);
    staging_ptr_ = staging_memory_->mapMemory(0, required_size);
    staging_size_ = required_size;

    return true;
}

Pattern 4: Texture Slot Structure

struct TextureSlot {
    std::optional<vk::raii::Image> image;
    std::optional<vk::raii::DeviceMemory> memory;
    std::optional<vk::raii::ImageView> view;

    uint32_t width = 0;
    uint32_t height = 0;
    uint32_t stride = 0;
    VkFormat format = VK_FORMAT_UNDEFINED;

    UniqueFd dma_buf_fd;  // Not a Vulkan object
    uint64_t dma_buf_modifier = 0;

    enum State { Empty, Loading, Ready, Error };
    std::atomic<State> state{Empty};
    uint64_t sequence = 0;
};

Memory Management

Finding Memory Type

static auto find_memory_type(vk::PhysicalDeviceMemoryProperties const& mem_props,
                             uint32_t type_bits,
                             vk::MemoryPropertyFlags properties) -> uint32_t {
    for (uint32_t i = 0; i < mem_props.memoryTypeCount; ++i) {
        if ((type_bits & (1 << i)) &&
            (mem_props.memoryTypes[i].propertyFlags & properties) == properties) {
            return i;
        }
    }
    return UINT32_MAX;
}

// Usage:
auto mem_props = physicalDevice.getMemoryProperties();
auto mem_reqs = image.getMemoryRequirements();
uint32_t mem_type = find_memory_type(
    mem_props,
    mem_reqs.memoryTypeBits,
    vk::MemoryPropertyFlagBits::eDeviceLocal
);

Mapping Memory

// Allocate memory
vk::raii::DeviceMemory memory{device, allocInfo};

// Map memory
void* ptr = memory.mapMemory(0, size);

// Use mapped memory
std::memcpy(ptr, data, size);

// Unmap (optional, RAII handles on destruction)
memory.unmapMemory();

DMA-BUF Export (External Memory)

// Image with external memory
vk::ExternalMemoryImageCreateInfo extMemInfo{};
extMemInfo.setHandleTypes(vk::ExternalMemoryHandleTypeFlagBits::eDmaBufEXT);

vk::ImageCreateInfo imageInfo{};
imageInfo.setPNext(&extMemInfo);
// ... set other properties

vk::raii::Image image{device, imageInfo};

// Memory with export
vk::ExportMemoryAllocateInfo exportAllocInfo{};
exportAllocInfo.setHandleTypes(vk::ExternalMemoryHandleTypeFlagBits::eDmaBufEXT);

vk::MemoryAllocateInfo allocInfo{};
allocInfo.setPNext(&exportAllocInfo);
allocInfo.setAllocationSize(size);
allocInfo.setMemoryTypeIndex(mem_type);

vk::raii::DeviceMemory memory{device, allocInfo};

// Export to DMA-BUF FD
PFN_vkGetMemoryFdKHR vkGetMemoryFdKHR = // ... load function pointer
VkMemoryGetFdInfoKHR getFdInfo{};
getFdInfo.sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR;
getFdInfo.memory = *memory;
getFdInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT;

int fd = -1;
VkResult result = vkGetMemoryFdKHR(*device, &getFdInfo, &fd);

Command Buffers

Recording Commands

vk::CommandBufferBeginInfo beginInfo{};
beginInfo.setFlags(vk::CommandBufferUsageFlagBits::eOneTimeSubmit);

cmdBuffer.begin(beginInfo);

// Image memory barrier
vk::ImageMemoryBarrier barrier{};
barrier.setOldLayout(vk::ImageLayout::eUndefined);
barrier.setNewLayout(vk::ImageLayout::eTransferDstOptimal);
barrier.setSrcAccessMask(vk::AccessFlagBits::eNone);
barrier.setDstAccessMask(vk::AccessFlagBits::eTransferWrite);
barrier.setImage(*image);
barrier.setSubresourceRange({vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1});

cmdBuffer.pipelineBarrier(
    vk::PipelineStageFlagBits::eTopOfPipe,
    vk::PipelineStageFlagBits::eTransfer,
    {}, {}, {}, barrier
);

// Copy buffer to image
vk::BufferImageCopy copyRegion{};
copyRegion.setBufferOffset(0);
copyRegion.setImageSubresource({vk::ImageAspectFlagBits::eColor, 0, 0, 1});
copyRegion.setImageExtent({width, height, 1});

cmdBuffer.copyBufferToImage(*buffer, *image,
    vk::ImageLayout::eTransferDstOptimal, copyRegion);

cmdBuffer.end();

Submitting Commands

vk::SubmitInfo submitInfo{};
submitInfo.setCommandBuffers(*cmdBuffer);

queue.submit(submitInfo);

// Wait for completion
queue.waitIdle();
// Or use fences for async

One-Time Submit Helper Pattern

auto execute_one_time_commands(vk::raii::Device& device,
                               vk::raii::Queue& queue,
                               vk::raii::CommandPool& pool,
                               auto&& record_func) -> void {
    vk::CommandBufferAllocateInfo allocInfo{};
    allocInfo.setCommandPool(*pool);
    allocInfo.setLevel(vk::CommandBufferLevel::ePrimary);
    allocInfo.setCommandBufferCount(1);

    auto cmdBuffers = device.allocateCommandBuffers(allocInfo);
    vk::raii::CommandBuffer cmd{std::move(cmdBuffers[0])};

    vk::CommandBufferBeginInfo beginInfo{};
    beginInfo.setFlags(vk::CommandBufferUsageFlagBits::eOneTimeSubmit);
    cmd.begin(beginInfo);

    record_func(cmd);

    cmd.end();

    vk::SubmitInfo submitInfo{};
    submitInfo.setCommandBuffers(*cmd);
    queue.submit(submitInfo);
    queue.waitIdle();
}

// Usage:
execute_one_time_commands(device, queue, pool, [&](vk::raii::CommandBuffer& cmd) {
    // Record commands here
    vk::ImageMemoryBarrier barrier{/*...*/};
    cmd.pipelineBarrier(/*...*/);
});

Extension Functions

Loading Extension Functions

// Method 1: Via device dispatch (preferred)
auto vkGetMemoryFdKHR = device.getProcAddr<vk::GetMemoryFdKHR>("vkGetMemoryFdKHR");

// Method 2: Manual loading (fallback for some extensions)
PFN_vkVoidFunction proc_addr = device.getProcAddr("vkGetMemoryFdKHR");
auto vkGetMemoryFdKHR = reinterpret_cast<PFN_vkGetMemoryFdKHR>(proc_addr);

Using Extension Functions

// For vkGetMemoryFdKHR (DMA-BUF export)
VkMemoryGetFdInfoKHR getFdInfo{};
getFdInfo.sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR;
getFdInfo.memory = *memory;
getFdInfo.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT;

int fd = -1;
VkResult result = vkGetMemoryFdKHR(*device, &getFdInfo, &fd);

Timeline Semaphores

// Create timeline semaphore
vk::SemaphoreTypeCreateInfo timelineInfo{};
timelineInfo.setSemaphoreType(vk::SemaphoreType::eTimeline);
timelineInfo.setInitialValue(0);

vk::SemaphoreCreateInfo semInfo{};
semInfo.setPNext(&timelineInfo);

vk::raii::Semaphore semaphore{device, semInfo};

// Signal timeline semaphore
vk::TimelineSemaphoreSubmitInfo timelineSubmitInfo{};
timelineSubmitInfo.setSignalSemaphoreValues(1);

vk::SubmitInfo submitInfo{};
submitInfo.setSignalSemaphores(*semaphore);
submitInfo.setPNext(&timelineSubmitInfo);

queue.submit(submitInfo);

// Wait for specific value
vk::SemaphoreWaitInfo waitInfo{};
waitInfo.setSemaphores(*semaphore);
waitInfo.setValues(1);

device.waitSemaphores(waitInfo, UINT64_MAX);

Migration Guide

Step 1: Replace the Include

// Before:
#include <vulkan/vulkan.h>

// After:
#include <vulkan/vulkan_raii.hpp>

Step 2: Replace Raw Types with RAII

// Before:
VkInstance instance;
VkDevice device;
VkImage image;
VkDeviceMemory memory;

// After:
vk::raii::Instance instance{nullptr};
vk::raii::Device device{nullptr};
vk::raii::Image image{nullptr};
vk::raii::DeviceMemory memory{nullptr};

Step 3: Replace Creation Calls

// Before:
VkInstance instance;
VkResult result = vkCreateInstance(&createInfo, nullptr, &instance);
if (result != VK_SUCCESS) { /* error */ }
// Later:
vkDestroyInstance(instance, nullptr);

// After:
vk::raii::Instance instance{context, createInfo};
// That's it! Automatic cleanup.

Step 4: Convert Function Calls to Member Functions

// Before:
vkCmdPipelineBarrier(cmdBuffer, ...);
vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE);
vkDeviceWaitIdle(device);
vkGetImageMemoryRequirements(device, image, &memReqs);

// After:
cmdBuffer.pipelineBarrier(...);
queue.submit(submitInfo);
device.waitIdle();
auto memReqs = image.getMemoryRequirements();

Step 5: Delete Manual Cleanup

// Before (DELETE ALL OF THIS):
void cleanup() {
    vkDestroyImageView(device, imageView, nullptr);
    vkDestroyImage(device, image, nullptr);
    vkFreeMemory(device, memory, nullptr);
    vkDestroyDescriptorPool(device, descriptorPool, nullptr);
    vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
    // ... many more lines
}

// After:
// Nothing! RAII handles cleanup automatically.

Step 6: Update Struct/Class Members

// Before:
class MyRenderer {
    VkDevice device_;
    VkImage image_;
    VkDeviceMemory memory_;
    VkImageView view_;
};

// After:
class MyRenderer {
    vk::raii::Device device_{nullptr};
    vk::raii::Image image_{nullptr};
    vk::raii::DeviceMemory memory_{nullptr};
    vk::raii::ImageView view_{nullptr};
};

Step 7: Handle Move Semantics

// RAII types use move semantics
vk::raii::Image create_image(vk::raii::Device& device) {
    vk::ImageCreateInfo info{/*...*/};
    return vk::raii::Image{device, info};  // Move return
}

// Accept by reference when not transferring ownership
void use_image(const vk::raii::Image& image) {
    // Use *image to get raw handle when needed
}

Ownership Semantics

Non-Owned Handles

Some Vulkan handles are not owned and should remain as raw types:

// VkPhysicalDevice is NOT owned, keep as raw handle
VkPhysicalDevice physical_device = VK_NULL_HANDLE;

// But you can wrap it for convenience (doesn't take ownership)
vk::raii::PhysicalDevice physical_device_raii{instance, physical_device};

Owned Handles

All created resources MUST be RAII:

// ✅ CORRECT - All owned resources are RAII
vk::raii::Device device{physical_device, device_info};
vk::raii::Image image{device, image_info};
vk::raii::DeviceMemory memory{device, memory_info};
vk::raii::ImageView view{device, view_info};

Reference Semantics

When passing RAII objects to functions:

// Pass by reference when not transferring ownership
void init_buffer(vk::raii::Device& device, vk::raii::Buffer& buffer) {
    // device and buffer remain owned by caller
}

// Pass by std::unique_ptr for shared ownership
void store_buffer(std::unique_ptr<vk::raii::Buffer> buffer) {
    // Takes ownership of the buffer
}

Dereferencing to Raw Handles

When a raw handle is required (e.g., for external APIs):

vk::raii::Image image{device, imageInfo};

// Get raw handle with dereference operator
VkImage raw_handle = *image;

// For pointers to RAII objects
vk::raii::Device* device_ptr = &device;
VkDevice raw_device = **device_ptr;

Optional RAII Members

Use std::optional for RAII members that may not be initialized:

struct TextureSlot {
    std::optional<vk::raii::Image> image;
    std::optional<vk::raii::DeviceMemory> memory;
    std::optional<vk::raii::ImageView> view;

    bool is_allocated() const {
        return image.has_value() && memory.has_value() && view.has_value();
    }
};

Quick Reference Card

Common Conversions

Raw VulkanRAII Equivalent
vkCreate*(&info, nullptr, &handle)vk::raii::Type object{device, info}
vkDestroy*(handle, nullptr)DELETE - automatic
vkCmd*(cmd, ...)cmd.memberFunction(...)
vkQueueSubmit(queue, ...)queue.submit(...)
vkDeviceWaitIdle(device)device.waitIdle()
vkGet*Properties(device, &props)auto props = device.getProperties()
vkAllocate*(&allocInfo, &handles)vk::raii::Type objects{device, allocInfo}

Namespace Prefixes

vk::raii::         // All RAII types
vk::               // Enums, flags, structs
vk::FlagTraits<>   // Flag traits templates

Common Gotchas

  1. DescriptorSets is plural: vk::raii::DescriptorSets
  2. PhysicalDevice is not owned: Keep raw or wrapped
  3. Queue is returned by value, not pointer: vk::raii::Queue queue = device.getQueue(...)
  4. CommandBuffers returns vector: auto bufs = device.allocateCommandBuffers(...)
  5. Use * to dereference: Most APIs taking raw handles need *raii_object

Verification Checklist

Before committing Vulkan code, verify:

  • Include is #include <vulkan/vulkan_raii.hpp>
  • All owned resources use vk::raii::* types
  • No vkCreate* or vkDestroy* calls
  • No manual cleanup in destructors
  • Command buffer calls are member functions
  • Raw handles only obtained with * operator
  • VkPhysicalDevice is the only non-owned Vulkan handle
  • Extension functions loaded via getProcAddr()
  • DMA-BUF exports use proper external memory setup
  • Code compiles with zero warnings

Further Reading