Vulkan C++ Bindings reloaded
A few months have passed since we’ve wrote about the Vulkan C++ bindings on the technical blog. Since then we’ve constantly been improving the API to simplify the usage to make it even easier to use Vulkan. Most of the improvements in this update are related to array handling and error handling, though we’ve also changed the basic interface a little bit.
Member Variables
In the early versions of Vulkan-Hpp we had getter and setter functions for each member variable of the vulkan structs to emulate C99 designated initializers. It turned out that this pattern made code porting from the C-API to the C++-API more complicated than required and coding using this pattern was also not very fluent. Due to this we decided to make the member variables public, remove the getters and rename the setter for the value Foo from MyStruct::Foo(Bar value) to MyStruct::setFoo(bar). This way there are three different ways to initialize a struct
Constructor style: vk::Offset2D offset(0,0); C-style: Vk::Offset2D offset; offset.x = 0; offset.y = 0; InitializerList style: Vk::Offset2D offset = Vk::Offset2D().setX(0).setY(0);
ArrayProxy
The Vulkan API has several places where the user has to pass a pair of (count,pointer) as function argument. C++ has a few containers which map perfectly to to this pair. To simpify development we decided to support those types which map to an C-array without additional copies. As a result we added a new proxy class vk::ArrayProxy<T> which supports empty arrays, and a single value as well as the STL containers std::initializer_list, std:.array und std::vector. The proxy object is required to avoid an exponential explosion of functions, especially for the case of more than one array in a function signature.
Here are some code samples for each constructor type:
vk::CommandBuffer c; // pass an empty array c.setScissor(0, nullptr); // pass a single value. Value is passed as reference vk::Rect2D scissorRect = { {0, 0}, {640, 480} }; c.setScissor(0, scissorRect); // pass a temporary value. c.setScissor(0, { { 0, 0 },{ 0, 0 } }); // generate a std::initializer_list using two rectangles from the stack. This might generate a copy of the rectangles. vk::Rect2D scissorRect1 = { { 0, 0 },{ 320, 240 } }; vk::Rect2D scissorRect2 = { { 320, 240 },{ 320, 240 } }; c.setScissor(0, { scissorRect, scissorRect2 }); // construct a std::initializer_list using two temporary rectangles. c.setScissor(0, { { { 0, 0 },{ 320, 240 } }, { { 320, 240 },{ 320, 240 } } } ); // pass an std::array std::array<vk::Rect2D, 2> arr{ scissorRect1, scissorRect2 }; c.setScissor(0, arr); // pass an std::vector of dynamic size std::vector<vk::Rect2D> vec; vec.push_back(scissorRect1); vec.push_back(scissorRect2); c.setScissor(0, vec);
People have been wondering if we could use the ArrayProxy in the constructors of structs too. Unfortunately, this has a high risk for errors. When passing a temporary into the ArrayProxy, it would be destroyed at the end of the line while the struct continues to exist with a dangling pointer. When passing an STL container like a std::vector into the ArrayProxy, the struct cannot take the ownership of the data to ensure that it stays alive as long as the struct. If the passed-in std::vector is destroyed before the struct, it would hold a dangling pointer.
Return Values
Vulkan requires you to pass in pointers for a single return value or (count,pointer) tuples for array based return values. This API design is required since Vulkan functions which return a value usually also have to return an error code. For the C++ API we found a nice way to replace the return pointer by keeping the ability to return error codes.
A function
VkResult vkSomeFunction(..., SomeType *output)
Becomes
ResultValue<SomeType>::type someFunction(...);
Error codes & Exceptions
By default Vulkan-Hpp has exceptions enabled. This means that Vulkan-Hpp checks the return code of each function call which returns a Vk::Result
. If Vk::Result is a failure a std::runtime_error will be thrown. Since there’s no need to return the error code anymore the C++ bindings can now use the return value to return the actual desired return value, i.e. a vulkan handle. In those cases ResultValue <SomeType>::type
is defined as SomeType
. To create a device you can now just write:
vk::Device device = physicalDevice.createDevice(createInfo);
If exception handling is disabled by defining VULKAN_HPP_NO_EXCEPTIONS
ResultValue<SomeType>::type
is a struct which contains the return value and the error code in the fields result
and value
.
In case you don’t want to use the vk::ArrayProxy and return value transformation you can still call the plain C-style function. Below are three examples showing the 3 ways to use the API:
The first snippet shows how to use the API without exceptions and the return value transformation:
// No exceptions, no return value transformation ShaderModuleCreateInfo createInfo(...); ShaderModule shader1; Result result = device.createShaderModule(&createInfo, allocator, &shader1); if (result.result != VK_SUCCESS) { handle error code; cleanup? return? } ShaderModule shader2; Result result = device.createShaderModule(&createInfo, allocator, &shader2); if (result != VK_SUCCESS) { handle error code; cleanup? return? }
The second snipped shows how to use the API using return value transformation, but without exceptions. It’s already a little bit shorter than the original code:
ResultValue<ShaderModule> shaderResult1 = device.createShaderModule({...} /* createInfo temporary */); if (shaderResult1.result != VK_SUCCESS) { handle error code; cleanup? return? } ResultValue<ShaderModule> shaderResult2 = device.createShaderModule({...} /* createInfo temporary */); if (shaderResult2.result != VK_SUCCESS) { handle error code; cleanup? return? }
Finally, the last code example is using exceptions and return value transformation. This is the default mode of the API.
ShaderModule shader1; ShaderModule shader2; try { myHandle = device.createShaderModule({...}); myHandle2 = device.createShaderModule({...}); } catch(std::exception const &e) { // handle error and free resources }
Keep in mind that Vulkan-Hpp does not support RAII style handles and that you have to cleanup your resources in the error handler!
RAII
Adding RAII to Vulkan-Hpp would require refcounting support of Vulkan API handles. Those refcounts have to be stored somewhere and be incremented/decremented across Vulkan-Hpp boundaries. In addition to this storing the refcounts means that structs which hold handles wouldn’t be binary compatible to Vulkan anymore and thus passing a struct from C++ to C would require a copy. It’s also not clear who owns a handle after a transition between the C-API and C++-API. Due to this we decided that refcounting is not the correct place for the Vulkan C++ bindings.
Enumerations
Finally, Vulkan has a few enumeration functions which require the developer to write copy/paste code pieces like this:
std::vector<LayerProperties,Allocator> properties; uint32_t propertyCount; Result result; do { // determine number of elements to query result = static_cast<Result>( vkEnumerateDeviceLayerProperties( m_physicalDevice, &propertyCount, nullptr ) ); if ( ( result == Result::eSuccess ) && propertyCount ) { // allocate memory & query again properties.resize( propertyCount ); result = static_cast<Result>( vkEnumerateDeviceLayerProperties( m_physicalDevice, &propertyCount, reinterpret_cast <VkLayerProperties*>( properties.data() ) ) ); } } while ( result == Result::eIncomplete ); // it's possible that the count has changed, start again if properties was not big enough properties.resize(propertyCount);
Since writing this loop over and over again is tedious and error prone the C++ binding takes care of the enumeration so that you can just write:
std::vector<LayerProperties> properties = physicalDevice.enumerateDeviceLayerProperties();
Custom allocators for std::vectors
As seen before, some functions return a std::vector<>, maybe as part of a ResultValue<>. For those functions, you can specify the custom allocator to be used by the allocator as template argument to the function.
std::vector<LayerProperties, CustomAllocator> properties = physicalDevice.enumerateDeviceLayerProperties<CustomAllocator>();
Vulkan-Hpp is a part of the Vulkan SDK starting with version 1.0.24.0. All you need to do to use the C++ bindings is add #include <vulkan/vulkan.hpp> to your includes and you’re good to go.
If you would like to learn more about Vulkan take a look at our product page, you will find links to tradeshow presentations, news articles and support at the bottom of the page.
If you’re interested in the project you can find it at: https://github.com/KhronosGroup/Vulkan-Hpp
Feel free to fork the code-generator and customize it for your own needs and let us know what other features you are interested in.