Skip to content

Instantly share code, notes, and snippets.

@PolarNick239
Created March 7, 2024 10:33
Show Gist options
  • Save PolarNick239/d4e8cf6645ccb84804afe8d864be3f22 to your computer and use it in GitHub Desktop.
Save PolarNick239/d4e8cf6645ccb84804afe8d864be3f22 to your computer and use it in GitHub Desktop.
Vulkan C API vs C++ RAII API examples comparison

Vulkan C API vs C++ RAII API examples comparison

Example 1:

// Vulkan C++ API:
vk::BufferCopy region_cpp;
command_buffer.copyBuffer(staging_buffer, vk_data->buffer, region_cpp);
// Vulkan C API:
VkBufferCopy region = {0, 0, size};
VKF.vkCmdCopyBuffer(vk::CommandBuffer(command_buffer), staging_buffer, vk_data->buffer, 1, &region);

Note that VkBufferCopy is a struct, so it is easy to make a mistake and initialize it partially (leaving some fields with uninitialized trash):

In such case we initialized size field, but all other fields (offsets) were left uninitialized:

VkBufferCopy region;
region.size = size;

Improvement 1: This is not the case with C++ API, because this code vk::BufferCopy region_cpp; will call default constructor initializing all fields with zeros.

Note that in case of C API it is enough to write VkBufferCopy region = {}; (but it is still more error-prone).

Example 2:

// Vulkan C++ API:
vk::raii::Instance temporary_instance = vk::raii::Instance(context, instance_create_info);
std::vector<vk::raii::PhysicalDevice> devices = temporary_instance.enumeratePhysicalDevices();
// Vulkan C API:
VkInstance temporary_instance;
assert(VKF.vkCreateInstance(&instance_create_info, nullptr, &temporary_instance) == VK_SUCCESS);
unsigned int ndevices = 0;
assert(VKF.vkEnumeratePhysicalDevices(vk::Instance(temporary_instance), &ndevices, nullptr) == VK_SUCCESS);
std::vector<VkPhysicalDevice> devices(ndevices);
assert(VKF.vkEnumeratePhysicalDevices(vk::Instance(temporary_instance), &ndevices, devices.data()) == VK_SUCCESS);
VKF.vkDestroyInstance(temporary_instance);

Improvement 2: In case of C++ RAII API we don't need to call vkDestroyInstance(...) manually. Also it supports std containers like std::vector - see devices result from vkEnumeratePhysicalDevices.

Interesting implementation details:

These three lines are equal thanks to syntax sugar inside Vulkan C++ API:

vk::raii::DescriptorSet descriptor_sets;
...
descriptor_writes[i] = vk::WriteDescriptorSet(descriptor_sets, binding, 0, 1, vk::DescriptorType::eStorageBuffer);
descriptor_writes[i] = vk::WriteDescriptorSet(descriptor_sets.operator vk::DescriptorSet(), binding, 0, 1, vk::DescriptorType::eStorageBuffer);
descriptor_writes[i] = vk::WriteDescriptorSet(vk::DescriptorSet(descriptor_sets), binding, 0, 1, vk::DescriptorType::eStorageBuffer);

I.e. vk::raii::DescriptorSet will be implicitly converted into vk::DescriptorSet. So RAII boxes are-just-works! (without need of manual conversions)

Improvement 3: More pretty-looking and expressive enums: VkDescriptorType::VK_DESCRIPTOR_TYPE_STORAGE_BUFFER -> vk::DescriptorType::eStorageBuffer.

But there are also a case when some parameter is a pointer to multiple values, f.e.:

// C API function:
void vkCmdBindDescriptorSets(
    VkCommandBuffer                             commandBuffer,
    VkPipelineBindPoint                         pipelineBindPoint,
    VkPipelineLayout                            layout,
    uint32_t                                    firstSet,
    uint32_t                                    descriptorSetCount,
    const VkDescriptorSet*                      pDescriptorSets, // <- this argument is a pointer to multiple VkDescriptorSet values
    uint32_t                                    dynamicOffsetCount,
    const uint32_t*                             pDynamicOffsets);

Improvement 4: In such cases you can even pass std::vector<vk::DescriptorSet> as an argument:

vk::raii::DescriptorSet descriptor_sets_a = ...;
vk::raii::DescriptorSet descriptor_sets_b = ...;
std::vector<vk::DescriptorSet> descriptors;
descriptors.push_back(descriptor_sets_a);
descriptors.push_back(descriptor_sets_b);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eCompute, pipeline_layout, 0, descriptors, nullptr);

And you can pass even a single (non-RAII) descriptor (it will be automatically boxed into vk::ArrayProxy):

vk::raii::DescriptorSet descriptor_sets;
vk::DescriptorSet descriptor_sets_non_raii = descriptor_sets;
// These four lines are equal, but the 4-th one DOESN'T COMPILE
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eCompute, vk::PipelineLayout(kernel->pipelineLayout()), 0, descriptor_sets_non_raii, nullptr);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eCompute, vk::PipelineLayout(kernel->pipelineLayout()), 0, vk::ArrayProxy<vk::DescriptorSet>(descriptor_sets_non_raii), nullptr);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eCompute, vk::PipelineLayout(kernel->pipelineLayout()), 0, vk::ArrayProxy<vk::DescriptorSet>(descriptor_sets), nullptr);
command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eCompute, vk::PipelineLayout(kernel->pipelineLayout()), 0, descriptor_sets, nullptr); // this line does not compile!

The last one line doesn't compile, because it requires two implicit conversions: vk::raii::DescriptorSet -> vk::DescriptorSet -> vk::ArrayProxy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment