Vulkan C++ Bindings reloaded

A few months have passed since we’ve wrote about the Vulkan C++ bindings on the developer 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

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.

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_EXCEPTIONSResultValue<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:

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:

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.