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
- Why RAII is Mandatory
- The Correct Include
- RAII Types Reference
- Correct Usage Patterns
- Forbidden Patterns
- Common Patterns in Waxed
- Memory Management
- Command Buffers
- Extension Functions
- Migration Guide
- 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 Type | Raw Handle (Forbidden) | Purpose |
|---|---|---|
vk::raii::Context | N/A | Entry point for creating Instance |
vk::raii::Instance | VkInstance | Vulkan instance connection |
vk::raii::PhysicalDevice | VkPhysicalDevice | GPU device (handle only) |
vk::raii::Device | VkDevice | Logical device |
vk::raii::Queue | VkQueue | Command queue |
Resource Types
| RAII Type | Raw Handle (Forbidden) | Purpose |
|---|---|---|
vk::raii::Image | VkImage | Image resource |
vk::raii::ImageView | VkImageView | Image view |
vk::raii::Buffer | VkBuffer | Buffer resource |
vk::raii::BufferView | VkBufferView | Buffer view |
vk::raii::DeviceMemory | VkDeviceMemory | Device memory allocation |
vk::raii::Sampler | VkSampler | Texture sampler |
Rendering Types
| RAII Type | Raw Handle (Forbidden) | Purpose |
|---|---|---|
vk::raii::RenderPass | VkRenderPass | Render pass |
vk::raii::Framebuffer | VkFramebuffer | Framebuffer |
vk::raii::CommandPool | VkCommandPool | Command buffer pool |
vk::raii::CommandBuffer | VkCommandBuffer | Command buffer |
vk::raii::Pipeline | VkPipeline | Graphics/compute pipeline |
vk::raii::PipelineLayout | VkPipelineLayout | Pipeline layout |
Descriptor Types
| RAII Type | Raw Handle (Forbidden) | Purpose |
|---|---|---|
vk::raii::DescriptorPool | VkDescriptorPool | Descriptor pool |
vk::raii::DescriptorSetLayout | VkDescriptorSetLayout | Descriptor set layout |
vk::raii::DescriptorSets | VkDescriptorSet | Descriptor sets (plural!) |
Shader Types
| RAII Type | Raw Handle (Forbidden) | Purpose |
|---|---|---|
vk::raii::ShaderModule | VkShaderModule | Shader module |
Synchronization Types
| RAII Type | Raw Handle (Forbidden) | Purpose |
|---|---|---|
vk::raii::Fence | VkFence | Fence |
vk::raii::Semaphore | VkSemaphore | Semaphore |
vk::raii::Event | VkEvent | Event |
Query Types
| RAII Type | Raw Handle (Forbidden) | Purpose |
|---|---|---|
vk::raii::QueryPool | VkQueryPool | Query 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 Vulkan | RAII 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
- DescriptorSets is plural:
vk::raii::DescriptorSets - PhysicalDevice is not owned: Keep raw or wrapped
- Queue is returned by value, not pointer:
vk::raii::Queue queue = device.getQueue(...) - CommandBuffers returns vector:
auto bufs = device.allocateCommandBuffers(...) - 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*orvkDestroy*calls - No manual cleanup in destructors
- Command buffer calls are member functions
- Raw handles only obtained with
*operator -
VkPhysicalDeviceis 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
- Vulkan-Hpp GitHub: https://github.com/KhronosGroup/Vulkan-Hpp
- Vulkan Specification: https://registry.khronos.org/vulkan/
- DMA-BUF Extensions: VK_EXT_external_memory_dma_buf
- Timeline Semaphores: VK_KHR_timeline_semaphore