Feature guide » Vulkan wrapping layer new in Git master

Overview of the base Vulkan wrapper API.

The Magnum::Vk library is a thin but high-level abstraction of the Vulkan GPU API, providing sane defaults with ability to opt-in for greater control and performance.

CreateInfo structure wrappers

The *CreateInfo / *AllocateInfo structures tend to be rather complex compared to the created object and because they're only needed once when constructing the object, their definition is put in a separate header. This should help compile times in larger codebases where Vulkan objects are constructed in one place and then passed around to functions, without needing to know anything about the CreateInfo-related definitions anymore. Then, for convenience, each ThingCreateInfo.h header is guaranteed to include the Thing.h as well, so if you want both you can do for example just

#include <Magnum/Vk/RenderPassCreateInfo.h>

instead of having to verbosely include both:

#include <Magnum/Vk/RenderPass.h>
#include <Magnum/Vk/RenderPassCreateInfo.h>

Unless said otherwise in the particular constructor docs, a Vk::*CreateInfo instance has all required fields set to valid values upon construction, with everything else optional. One exception is for example Vk::DeviceCreateInfo, where the user is expected to call addQueues() as well.

To completely mitigate the overhead from instantiating wrapper *CreateInfo classes, each of them can also be constructed using the NoInit tag, which will skip all initialization and leave the contents unspecified to be filled later. In case the structure contains some heap-allocated state, the NoInit constructor is guaranteed to not allocate. Note that with NoInit constructors you have the full responsibility to correctly set up all members.

Instance and device wrappers

Compared to OpenGL, which has a concept of "current context", Vulkan doesn't have any implicit globals. The Vk library follows that, with each object carrying a reference to a corresponding instance or device along. This was chosen as a reasonable tradeoff between requiring an explicit instance/device parameter in each API (which would be too error-prone and annoying to use) and having an implicit thread-local instance/device (which would repeat the well-known pain points of OpenGL).

Vulkan API entrypoints aren't global either because each instance and device can have a different set of enabled layers and extensions, and thus different instance- and device-local function pointers. While the Vulkan specification allows device-level functions to be queried on an instance and thus use the same function pointers on a variety of devices, such workflow implies additional dispatch overhead, and thus isn't recommended. Magnum instead stores instance- and device-level function pointers locally in each Vk::Instance and Vk::Device to avoid this overhead — these are then accessible through operator->() on both:

Vk::Instance instance{};

VkPhysicalDeviceGroupPropertiesKHR properties[10];
UnsignedInt count = Containers::arraySize(properties);
instance->EnumeratePhysicalDeviceGroupsKHR(instance, &count, properties);

For convenience and for easier interaction with 3rd party code, such pointers can be made global by calling Vk::Instance::populateGlobalFunctionPointers() and Vk::Device::populateGlobalFunctionPointers(), after which you can use the vk* functions as usual. However, all implications coming from these being tied to a particular instance/device still apply:

#include <MagnumExternal/Vulkan/flextVkGlobal.h>



instance.populateGlobalFunctionPointers();

VkPhysicalDeviceGroupPropertiesKHR properties[10];
UnsignedInt count = Containers::arraySize(properties);
vkEnumeratePhysicalDeviceGroupsKHR(instance, &count, properties);

Host memory allocation

As opposed to device memory allocation, which is exposed through Vk::Memory and related APIs, overriding host memory allocation via VkAllocationCallbacks is not possible as use cases for overriding host memory allocators are quite rare. This pointer is thus always set to nullptr in any vkCreate*() calls. If you want to supply your own callbacks, you have to call these functions directly — ideally through the instance- and device-level function pointers available through Vk::Instance::operator->() and Vk::Device::operator->().

Common interfaces for interaction with raw Vulkan code

Each wrapped Vulkan object has a handle() getter, giving back the underlying Vulkan handle such as VkInstance. In addition it's also implicitly convertible to that handle type, which means you can pass it as-is to raw Vulkan APIs. You can also use release() to release its ownership and continue to use it as a regular handle. Conversely, any Vulkan handle can be wrapped into a first-class Magnum object using a corresponding wrap() function.

Similarly, all Vk::*CreateInfo wrapper classes are convertible to a Vk*CreateInfo pointer in order to be easily passable directly to Vulkan APIs. You can create them from an existing Vk*CreateInfo instances as well, and use operator->() to access the wrapped structure to supply additional parameters not exposed by Magnum. However take care to not clash with values and pointers already set:

Vk::InstanceCreateInfo info{};

/* Add a custom validation features setup */
VkValidationFeaturesEXT validationFeatures{};
validationFeatures.sType = VK_STRUCTURE_TYPE_VALIDATION_FEATURES_EXT;
validationFeatures.enabledValidationFeatureCount = 1;
constexpr auto bestPractices = VK_VALIDATION_FEATURE_ENABLE_BEST_PRACTICES_EXT;
validationFeatures.pEnabledValidationFeatures = &bestPractices;
CORRADE_INTERNAL_ASSERT(!info->pNext); // or find the end of the pNext chain
info->pNext = &validationFeatures;

Similarly to the NoInit constructors, constructing a Vk::*CreateInfo from the underlying Vulkan structure is guaranteed to not allocate as well — only a shallow copy of the top-level structure is made and internal pointers, if any, keep pointing to the originating data. That approach has some implications:

  • The user is responsible for ensuring those stay in scope for as long as the structure is needed
  • When calling Magnum's own APIs on the instance that was constructed from a raw Vulkan structure, the pointed-to-data aren't touched in any way (which means they can be const), however they might get abandoned and the top-level structure pointed elsewhere.
  • Existing pNext chains are preserved. Calling Magnum's own APIs may result in new entries added to the chain, but always at the front (again in order to allow the pointed-to-data be const). It's a responsibility of the user to ensure no conflicting or duplicate entries are present in the original chain when mixing it with Magnum's APIs.

It's also possible to add new structures to the pNext chain of an existing instance. However, to prevent conflicts with Magnum which inserts at the front, new raw structures should be always appended at the end of the chain.

Optimizing instance and device property retrieval

Retrieving layer and extension info as well as device properties involves allocations, string operations, sorting and other potentially expensive work both on the application side and in the driver. While the Vk::LayerProperties, Vk::InstanceExtensionProperties / Vk::ExtensionProperties and Vk::DeviceProperties APIs are made in a way to optimize subsequent queries as much as possible, care should be taken to avoid constructing and initializing them more times than necessary.

For Vk::Instance creation, the Vk::InstanceCreateInfo constructor is able to take pointers to existing Vk::LayerProperties and Vk::InstanceExtensionProperties instances and reuse them to query availability of implicitly enabled layers and extensions. If they're not passed, the class may (but also might not) create its own instances internally:

Vk::LayerProperties layers = ;
Vk::InstanceExtensionProperties extensions = ;

/* Pass the layer and extension properties for use by InstanceCreateInfo */
Vk::InstanceCreateInfo info{argc, argv, &layers, &extensions};
if(layers.isSupported("VK_LAYER_KHRONOS_validation"_s))
    info.addEnabledLayers({"VK_LAYER_KHRONOS_validation"_s});
if(extensions.isSupported<Vk::Extensions::EXT::debug_report>())
    info.addEnabledExtensions<Vk::Extensions::EXT::debug_report>();


Vk::Instance instance{info};

For Vk::Device creation, the Vk::DeviceProperties should ideally be moved all the way to the Vk::Device constructor, at which point it's made available through Vk::Device::properties() to any code that needs it. If you have Vk::pickDevice(), Vk::DeviceCreateInfo and Vk::Device constructor all in a single expression, the optimal operation is done implicitly:

Vk::Device device{instance, Vk::DeviceCreateInfo{Vk::pickDevice(instance)}
    .addQueues()
    
};

However, if you instantiate Vk::DeviceProperties and/or Vk::DeviceCreateInfo separately, you have to std::move() them to achieve the desired effect. An existing Vk::ExtensionProperties instance can be also passed to Vk::DeviceCreateInfo to allow reuse:

Vk::DeviceProperties properties = Vk::pickDevice(instance);
Vk::ExtensionProperties extensions = properties.enumerateExtensionProperties();

/* Move the device properties to the info structure, pass extension properties
   to allow reuse as well */
Vk::DeviceCreateInfo info{std::move(properties), &extensions};
if(extensions.isSupported<Vk::Extensions::EXT::index_type_uint8>())
    info.addEnabledExtensions<Vk::Extensions::EXT::index_type_uint8>();
if(extensions.isSupported("VK_NV_mesh_shader"_s))
    info.addEnabledExtensions({"VK_NV_mesh_shader"_s});


/* Finally, be sure to move the info structure to the device as well */
Vk::Device device{instance, std::move(info)};