vulkan_raii.hpp is a C++ layer on top of vulkan.hpp that follows the RAII-principle (RAII: Resource Acquisition Is Initialization, see [https://en.cppreference.com/w/cpp/language/raii#:~:text=Resource%20Acquisition%20Is%20Initialization%20or,in%20limited%20supply](https://en.cppreference.com/w/cpp/language/raii#:~:text=Resource%20Acquisition%20Is%20Initialization%20or,in%20limited%20supply)). This header-only library uses all the enums and structure wrappers from vulkan.hpp and provides a new set of wrapper classes for the Vulkan handle types. Instead of creating Vulkan handles with vk*Allocate or vk*Create a constructor of the corresponding Vulkan handle wrapper class is used. And instead of destroying Vulkan handles with vk*Free or vk*Destroy, the destructor of that handle class is called.
## General Usage
As a simple example, instead of creating a `vk::Device`
// create a vk::Device, given a vk::PhysicalDevice physicalDevice and a vk::DeviceCreateInfo deviceCreateInfo
That `vk::raii::Device` is automatically destroyed, when its scope is left.
Other than the `vk::Device`, you can assign the `vk::raii::Device` to a smart pointer:
// create a smart-pointer to a vk::raii::Device, given a smart-pointer to a vk::raii::PhysicalDevice pPhysicalDevice and a vk::DeviceCreateInfo deviceCreateInfo
Note that the vk::raii objects own the actual Vulkan resource. Therefore, all vk::raii objects that own destructable resources are just movable, but not copyable. Therefore, a few vk::raii objects, like vk::raii::PhysicalDevice are copyable as well.
For simplicity, in the rest of this document a vk::raii object is always directly instantiated on the stack. Obviously, that's not essential. You could assign them as well to a std::unique_ptr, a std::shared_ptr, or any other smart pointer or object managing data structure. And you can even assign them to a dumb pointer by using the new operator.
Similar to a `vk::Device`, a `vk::raii::Device` provides the functions related to that class. But other than the `vk::Device`, you don't need to provide a device-specific dispatcher to those functions to get multi-device functionality. That's already managed by the `vk::raii::Device`.
That is, calling a device-related function is identical for both cases:
// call waitIdle from a vk::Device
device.waitIdle();
// call waitIdle from a vk::raii::Device
device.waitIdle();
vk::raii goes one step further. In the vk namespace, most of the functions are members of `vk::Device`. In the vk::raii namespace functions strongly related to a non-dispatchable handle are members of the corresponding vi::raii object. For example, to bind memory to a buffer, in vk namespace you write
// bind vk::DeviceMemory memory to a vk::Buffer buffer, given vk::DeviceSize memoryOffset
// bind vk::raii::DeviceMemory memory to a vk::raii::Buffer buffer, given vk::DeviceSize memoryOffset
buffer.bindMemory( *memory, memoryOffset );
Note that `vk::raii::Buffer::bindMemory()`takes a `vk::DeviceMemory` as its first argument, not a `vk::raii::DeviceMemory`. From a vk::raii object you get to the corresponding vk object by just dereferencing the vk::raii object.
## First Steps
### 00 Create a vk::raii::Context
The very first step when using classes from the vk::raii namespace is to instantiate a `vk::raii::Context`. This class has no counterpart in either the vk namespace or the pure C-API of Vulkan. It is the handle to the few functions that are not bound to a `VkInstance` or a `VkDevice`:
// instantiate a vk::raii::Context
vk::raii::Context context;
To use any of those "global" functions, your code would look like that:
The `vk::raii::Instance` now holds all the instance-related functions. For example, to get all the `vk::PhysicalDeviceGroupProperties` for an instance, your call would look like this:
// get all vk::PhysicalDeviceGroupProperties from a vk::raii::Instance instance
Actually, "all the instance-related functions", as stated above, might be a bit misleading. There are just very few public functions available with the `vk::raii::Instance`, as most of the instance-related functions are creation functions that are not wrapped publicly but used internally when instantiating other vk::raii-objects.
### 02 Enumerate the vk::raii::PhysicalDevices
Enumerating the physical devices of an instance is slightly different in vk::raii namespace as you might be used to from the vk-namespace or the pure C-API. As there might be multiple physical devices attached, you would instantiate a `vk::raii::PhysicalDevices` (note the trailing 's' here!), which essentially is a `std::vector` of `vk::raii::PhysicalDevice`s (note the trailing 's' here!):
// enumerate the vk::raii::PhysicalDevices, given a vk::raii::Instance instance
As vk::raii::PhysicalDevices is just a `std::vector<vk::raii::PhysicalDevice>`, you can access any specific `vk::raii:PhysicalDevice` by indexing into that `std::vector`:
// get the vk::LayerProperties of the vk::raii::PhysicalDevice with index physicalDeviceIndex, given a vk::raii::PhysicalDevices physicalDevices
Note, that even though the actual `VkPhysicalDevice` owned by a `vk::raii::PhysicalDevice` is not a destructible resource, for consistency reasons a `vk::raii::PhysicalDevice` is a movable but not copyable object just like all the other vk::raii objects.
### 03 Create a vk::raii::Device
To create a `vk::raii::Device`, you just instantiate an object of that class:
// create a vk::raii::Device, given a vk::raii::PhysicalDevice physicalDevice and a vk::DeviceCreateInfo deviceCreateInfo
For each instantiated `vk::raii::Device`, the device-specific Vulkan function pointers are resolved. That is, for multi-device programs, you automatically use the correct device-specific function pointers, and organizing a multi-device program is simplified:
// create a vk::raii::Device per vk::raii::PhysicalDevice, given a vk::raii::PhysicalDevices physicalDevices, and a corresponding array of vk::DeviceCreateInfo deviceCreateInfos
std::vector<vk::raii::Device> devices;
for ( size_t i = 0; i <physicalDevices.size();i++)
As the number of `vk::raii::CommandBuffer`s to allocate from a `vk::raii::CommandPool` is given by the member `commandBufferCount` of a `vk::CommandBufferAllocateInfo` structure, it can't be instantiated as a single object. Instead you get a `vk::raii::CommandBuffers` (note the trailing 's' here!), which essentially is a `std::vector` of `vk::raii::CommandBuffer`s (note the trailing 's' here!).
// create a vk::raii::CommandBuffers, given a vk::raii::Device device and a vk::CommandBufferAllocateInfo commandBufferAllocateInfo
Note, that the `vk::CommandBufferAllocateInfo` holds a `vk::CommandPool` member `commandPool`. To assign that from a `vk::raii::CommandPool` you can use the `operator*()`:
// assign vk::CommandBufferAllocateInfo::commandPool, given a vk::raii::CommandPool commandPool
As a `vk::raii::CommandBuffers` is just a `std::vector<vk::raii::CommandBuffer>`, you can access any specific `vk::raii:CommandBuffer` by indexing into that `std::vector`:
// start recording of the vk::raii::CommandBuffer with index commandBufferIndex, given a vk::raii::CommandBuffers commandBuffers
commandBuffers[commandBufferIndex].begin();
You can as well get one `vk::raii::CommandBuffer` out of a `vk::raii::CommandBuffers` like this:
// get the vk::raii::CommandBuffer with index commandBufferIndex, given a vk::raii::CommandBuffers commandBuffers
There is one important thing to note, regarding command pool and command buffer handling. When you destroy a `VkCommandPool`, all `VkCommandBuffer`s allocated from that pool are implicitly freed. That automatism does not work well with the raii-approach. As the `vk::raii::CommandBuffers` are independent objects, they are not automatically destroyed when the `vk::raii::CommandPool` they are created from is destroyed. Instead, their destructor would try to use an invalid `vk::raii::CommandPool`, which obviously is an error.
To handle that correctly, you have to make sure, that all `vk::raii::CommandBuffers` generated from a `vk::raii::CommandPool` are explicitly destroyed before that `vk::raii::CommandPool` is destroyed!
### 05 Create a vk::raii::SwapchainKHR
To initialize a swap chain, you first instantiate a `vk::raii::SwapchainKHR`:
// create a vk::raii::SwapchainKHR, given a vk::raii::Device device and a vk::SwapchainCreateInfoKHR swapChainCreateInfo
Note, that you don't get `vk::raii::Image`s here, but plain `VkImage`s. They are controlled by the swap chain, and you should not destroy them.
But you can create `vk::raii::ImageView`s out of them:
// create a vk::raii::ImageView per VkImage, given a vk::raii::Device sevice, a vector of VkImages images and a vk::ImageViewCreateInfo imageViewCreateInfo
To create the corresponding vk::raii::DeviceMemory, you should determine appropriate values for the vk::MemoryAllocateInfo. That is, get the memory requirements from the pDepthImage, and determine some memoryTypeIndex from the pPhysicalDevice's memory properties, requiring vk::MemoryPropertyFlagBits::eDeviceLocal.
// get the vk::MemoryRequirements of the pDepthImage
Initializing a uniform buffer is very similar to initializing a depth buffer as described above. You just instantiate a `vk::raii::Buffer` instead of a `vk::raii::Image`, and a `vk::raii::DeviceMemory`, and bind the memory to the buffer:
// create a vk::raii::Buffer, given a vk::raii::Device device and a vk::BufferCreateInfo bufferCreateInfo
To initialize a Pipeline Layout you just have to instantiate a `vk::raii::DescriptorSetLayout` and a `vk::raii::PipelineLayout` using that `vk::raii::DescriptorSetLayout`:
// create a vk::raii::DescriptorSetLayout, given a vk::raii::Device device and a vk::DescriptorSetLayoutCreateInfo descriptorSetLayoutCreateInfo
### 09 Create a vk::raii::DescriptorPool and vk::raii::DescriptorSets
The Descriptor Set handling with `vk::raii` requires some special handling that is not needed when using the pure C-API or the vk-namespace!
As a `vk::raii::DescriptorSet` object destroys itself in the destructor, you have to instantiate the corresponding `vk::raii::DescriptorPool` with the `vk::DescriptorPoolCreateInfo::flags` set to (at least) `vk::DescriptorPoolCreateFlagBits::eFreeDescriptorSet`. Otherwise, such individual destruction of a `vk::raii::DescriptorSet` would not be allowed!
That is, an instantiation of a `vk::raii::DescriptorPool` would look like this:
// create a vk::raii::DescriptorPool, given a vk::raii::Device device and a vk::DescriptorPoolCreateInfo descriptorPoolCreateInfo
To actually instantiate a `vk::raii::DescriptorSet`, you need a `vk::raii::DescriptorPool`, as just described, and a `vk::raii::DescriptorSetLayout`, similar to the one described in the previous section.
Moreover, as the number of `vk::raii::DescriptorSet`s to allocate from a `vk::raii::DescriptorPool` is given by the number of `vk::DescriptorSetLayouts` held by a `vk::DescriptorSetAllocateInfo`, it can't be instantiated as a single object. Instead you get a `vk::raii::DescriptorSets` (note the trailing 's' here!), which essentially is a `std::vector` of `vk::raii::DescriptorSet`s (note the trailing 's' here!).
When you want to create just one `vk::raii::DescriptorSet`, using just one `vk::raii::DescriptorSetLayout`, your code might look like this:
// create a vk::raii::DescriptorSets, holding a single vk::raii::DescriptorSet, given a vk::raii::Device device, a vk::raii::DescriptorPool descriptorPool, and a single vk::raii::DescriptorSetLayout descriptorSetLayout
And, again similar to the vk::raii::CommandBuffers handling described above, you can get one `vk::raii::DescriptorSet` out of a `vk::raii::DescriptorSets` like this:
// get the vk::raii::DescriptorSet with index descriptorSetIndex, given a vk::raii::DescriptorSets descriptorSets
If you have a `std::vector<vk::raii::ImageView>` as described in chapter 05 above, with one view per `VkImage` that you got from a `vk::raii::SwapchainKHR`; and one `vk::raii::ImageView` as described in chapter 06 above, which is a view on a `vk::raii::Image`, that is supposed to be a depth buffer, you can create a `vk::raii::Framebuffer` per swapchain image.
// create a vector of vk::raii::Framebuffer, given a vk::raii::ImageView depthImageView, a vector of vk::raii::ImageView swapchainImageViews, a vk::raii::RenderPass renderPass, a vk::raii::Devie device, and some width and height
// use the depth image view as the second attachment for each vk::raii::Framebuffer
std::array<vk::ImageView,2> attachments;
attachments[1] = *depthImageView;
std::vector<vk::raii::Framebuffer> framebuffers;
for ( auto const & imageView : swapchainImageViews )
{
// use each image view from the swapchain as the first attachment
To initialize a vertex buffer, you essentially have to combine some of the pieces described in the chapters before. First, you need to create a `vk::raii::Buffer` and a `vk::raii::DeviceMemory` and bind them:
// create a vk::raii::Buffer vertexBuffer, given a vk::raii::Device device and some vertexData in host memory
The only thing to keep in mind here is the dereferencing of raii handles, like `pipelineLayout` or `renderPass` in the `vk::GraphicsPipelineCreateInfo`:
Note, `vk::raii::SwapchainKHR::acquireNextImage` returns a `std::pair<vk::Result, uint32_t>`, that can nicely be assigned onto two separate values using std::tie().
And also note, the returned `vk::Result` can not only be `vk::Result::eSuccess`, but also `vk::Result::eTimeout`, `vk::Result::eNotReady`, or `vk::Result::eSuboptimalKHR`, which should be handled here accordingly!
Next, you can record some commands into a `vk::raii::CommandBuffer`:
// open the commandBuffer for recording
commandBuffer.begin( {} );
// initialize a vk::RenderPassBeginInfo with the current imageIndex and some appropriate renderArea and clearValues
result = presentQueue.presentKHR( presentInfoKHR );
Note here, again, that `result` can not only be `vk::Result::eSuccess`, but also `vk::Result::eSuboptimalKHR`, which should be handled accordingly.
## Conclusion
With the vk::raii namespace you've got a complete set of Vulkan handle wrapper classes following the RAII-paradigm. That is, they can easily be assigned to a smart pointer. And you can't miss their destruction.
Moreover, the actual function pointer handling is done automatically by `vk::raii::Context`, `vk::raii::Instance`, and `vk::raii::Device`. That is, you always use the correct device-specific functions, no matter how many devices you're using.
Note, though, that there are a few classes, like `vk::raii::CommandPool` and `vk::raii::DescriptorSet`, that need some special handling that deviates from what you can do with the pure C-API or the wrapper classes in the vk-namespace.