Magnum::Vk::Device class new in Git master

Device.

Wraps a VkDevice and stores device-specific Vulkan function pointers. A device provides an abstraction over a physical GPU or a CPU with Vulkan capabilities.

Device creation

With an Instance ready, a device has to be picked first. Commonly it's done by calling pickDevice() and letting the library choose. This selection is affected by the --magnum-device command-line option, giving the end users an ability to pick a particular device, choose a discrete or integrated GPU or even a software implementation. If the application needs something specific, you can use enumerateDevices() instead, pick a device from the list manually, provide the users with a list to choose from etc.

The picked device is then passed to DeviceCreateInfo. At the very least you'll also need to set up queues, as every Vulkan device needs at least one. That's done by creating an empty Queue instance and then referencing it from DeviceCreateInfo::addQueues(). After the device is constructed, the queue gets populated and is ready to be used.

#include <Magnum/Vk/DeviceCreateInfo.h>



Vk::Queue queue{NoCreate};
Vk::Device device{instance, Vk::DeviceCreateInfo{Vk::pickDevice(instance)}
    .addQueues(Vk::QueueFlag::Graphics, {0.0f}, {queue})
};

In the above snippet, we requested a graphics queue via a convenience API. The information about available queues and other device properties is stored in a DeviceProperties that got returned from pickDevice() and DeviceCreateInfo called DeviceProperties::pickQueueFamily() for us. As with device picking, you can also iterate through all DeviceProperties::queueFamilyCount() and choose one manually.

Same as with Instance, the above won't enable any additional extensions except for what the engine itself needs or what's supplied on the command line. Use DeviceCreateInfo::addEnabledExtensions() to enable them, you can use both string names as well as predefined device extensions from the Extensions namespace. Later on, presence of predefined extensions can be checked with isExtensionEnabled().

Vk::Device device{instance, Vk::DeviceCreateInfo{}
    
    .addEnabledExtensions<                         // predefined extensions
        Vk::Extensions::EXT::index_type_uint8,
        Vk::Extensions::KHR::device_group>()
    .addEnabledExtensions({"VK_NV_mesh_shader"_s}) // can be plain strings too
};

In addition to extensions, you'll be usually enabling features as well. These are all exposed in a giant DeviceFeatures enum and you can simply OR them together. Internally, those get translated to VkPhysicalDeviceFeatures2 and related structures, features that are not exposed in the enum can be enabled by adding a corresponding structure to the pNext chain. As with extensions, the set of enabled features can be later checked with enabledFeatures().

Vk::Device device{instance, Vk::DeviceCreateInfo{}
    
    .setEnabledFeatures(
        Vk::DeviceFeature::IndexTypeUnsignedByte|
        Vk::DeviceFeature::SamplerAnisotropy|
        Vk::DeviceFeature::GeometryShader|
        )
};

However, usually you'll be checking for extension and feature availability first, which is doable through DeviceProperties::enumerateExtensionProperties() and ExtensionProperties::isSupported() for extensions, and DeviceProperties::features() for features. In case of features you can make use of the enum set operations and simply mask away features that are not available — however note that some features also require an extension to be explicitly enabled.

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

Vk::DeviceCreateInfo info{properties};
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});

info.setEnabledFeatures(properties.features() & // mask away unsupported ones
    (Vk::DeviceFeature::IndexTypeUnsignedByte|
     Vk::DeviceFeature::SamplerAnisotropy|
     Vk::DeviceFeature::GeometryShader|
     ));

With both Instance and Device created, you can proceed to setting up a CommandPool and a Pipeline, which will then need a ShaderSet and a PipelineLayout. For rasterization pipelines, you'll additionally need a MeshLayout and a RenderPass.

Vulkan portability subset

To simplify porting to platforms with the Portability Subset, Magnum implicitly enables the KHR_portability_subset extension on all devices that advertise it, as required by the spec, so you don't need to handle that part. This behavior can be disabled with DeviceCreateInfo::Flag::NoImplicitExtensions.

For portability-related DeviceFeatures, on conformant Vulkan implementations (which don't advertise KHR_portability_subset) these are all implicitly marked as supported in DeviceProperties::features() and then implicitly marked as enabled in Device::enabledFeatures(), independently of whether you enable them or not. On devices having only the Portability Subset, the supported features are listed in DeviceProperties::features() but you're expected to manually enable them on device creation — that part is not done implicitly by the engine.

A workflow that supports both conformant and Portability Subset devices with a single code path is outlined in the following snippet — on device creation you request features that you want (which is a no-op on conformant implementations), and at runtime you query those features in appropriate cases (which will be always true on conformant implementations). As with other features, all APIs that require a particular Portability Subset feature are marked as such and also listed among others at Functionality requiring a specific Vulkan feature.

Vk::DeviceProperties properties = ;
Vk::Device device{instance, Vk::DeviceCreateInfo{properties}
    /* enable triangle fans only if actually supported */
    .setEnabledFeatures(properties.features() & Vk::DeviceFeature::TriangleFans)
    
};



if(device.enabledFeatures() & Vk::DeviceFeature::TriangleFans) {
    // draw a triangle fan mesh
} else {
    // indexed draw fallback
}

Command-line options

The Device inherits a subset of the Instance command-line options, in particular the following. If the Instance didn't get argc / argv passed, only the environment variables are used.

  • --magnum-disable-workarounds LIST — Vulkan driver workarounds to disable (see Driver workarounds for detailed info) (environment: MAGNUM_DISABLE_WORKAROUNDS)
  • --magnum-disable-extensions LIST — Vulkan instance or device extensions to disable, meaning DeviceCreateInfo::addEnabledExtensions() will skip them (environment: MAGNUM_DISABLE_EXTENSIONS)
  • --magnum-enable-extensions LIST — Vulkan device extensions to enable in addition to DeviceCreateInfo defaults and what the application requests (environment: MAGNUM_ENABLE_EXTENSIONS)
  • --magnum-vulkan-version X.Y — force Device Vulkan version instead of using what the device reports as supported, affecting what entrypoints and extensions get used (environment: MAGNUM_VULKAN_VERSION)
  • --magnum-log default|quiet|verbose — console logging (environment: MAGNUM_LOG) (default: default)
  • --magnum-device ID|integrated|discrete|virtual|cpu — device ID or kind to pick in pickDevice(); if a device is selected through enumerateDevices() or any other way, this option has no effect (environment: MAGNUM_DEVICE)

Interaction with raw Vulkan code

In addition to the common properties explained in Common interfaces for interaction with raw Vulkan code, the Device contains device-level Vulkan function pointers, accessible through operator->():

Vk::Device device{};

// ...
device->ResetQueryPoolEXT(device, );

These functions are by default not accessible globally (and neither there is a global "current instance"), which is done in order to avoid multiple independent instances affecting each other. Sometimes it is however desirable to have global function pointers — for example when a 3rd party code needs to operate on the same instance, or when writing quick prototype code — and then it's possible to populate those using populateGlobalFunctionPointers(). Compared to the above, the same custom code would then look like this:

#include <MagnumExternal/Vulkan/flextVkGlobal.h>



Vk::Device device{};
device.populateGlobalFunctionPointers();


vkResetQueryPoolEXT(device, );

Similarly you can use Instance::populateGlobalFunctionPointers() to populate instance-level global function pointers.

Disabled move and delayed device creation

Due to the way Queue instances are populated on device creation, and for safety reasons as all device-dependent objects internally have to keep a pointer to the originating Device to access Vulkan function pointers, the Device class is not movable. This leads to a difference compared to other Vulkan object wrappers, where you can use the NoCreate tag to construct an empty instance (for example as a class member) and do a delayed creation by moving a new instance over the empty one. Here you have to use the create() function instead:

class MyApplication {
    public:
        explicit MyApplication();

    private:
        Vk::Device _device{NoCreate};
};

MyApplication::MyApplication() {
    // decide on extensions, features, ...

    _device.create(instance, Vk::DeviceCreateInfo{}
        
    );
}

Similar case is with wrap() — instead of being static, you have to call it on a NoCreate'd instance. The Instance class behaves equivalently.

Constructors, destructors, conversion operators

Device(Instance& instance, const DeviceCreateInfo& info) explicit
Constructor.
Device(Instance& instance, DeviceCreateInfo&& info) explicit
Construct, reusing already populated device properties.
Device(NoCreateT) explicit
Construct without creating the device.
Device(const Device&) deleted
Copying is not allowed.
Device(Device&& other) deleted
Moving is not allowed.
~Device()
Destructor.
operator VkDevice()

Public functions

void wrap(Instance& instance, VkPhysicalDevice physicalDevice, VkDevice handle, Version version, const Containers::StringIterable& enabledExtensions, const DeviceFeatures& enabledFeatures, HandleFlags flags = {})
Wrap existing Vulkan handle.
auto operator=(const Device&) -> Device& deleted
Copying is not allowed.
auto operator=(Device&&) -> Device& deleted
Moving is not allowed.
auto handle() -> VkDevice
Underlying VkDevice handle.
auto handleFlags() const -> HandleFlags
Handle flags.
void create(Instance& instance, const DeviceCreateInfo& info)
Create a device.
void create(Instance& instance, DeviceCreateInfo&& info)
Create a device, reusing already populated device properties.
auto tryCreate(Instance& instance, const DeviceCreateInfo& info) -> Result
Try to create a device.
auto tryCreate(Instance& instance, DeviceCreateInfo&& info) -> Result
Try to create a device, reusing already populated device properties.
auto properties() -> DeviceProperties&
Device properties.
auto version() const -> Version
Version supported by the device.
auto isVersionSupported(Version version) const -> bool
Whether given version is supported on the device.
template<class E>
auto isExtensionEnabled() const -> bool
Whether given extension is enabled.
auto isExtensionEnabled(const Extension& extension) const -> bool
auto enabledFeatures() const -> const DeviceFeatures&
Features enabled on the device.
auto operator*() const -> const FlextVkDevice&
Device-specific Vulkan function pointers.
auto operator->() const -> const FlextVkDevice*
auto release() -> VkDevice
Release the underlying Vulkan device.
void populateGlobalFunctionPointers()
Populate global device-level function pointers to be used with third-party code.

Function documentation

Magnum::Vk::Device::Device(Instance& instance, const DeviceCreateInfo& info) explicit

Constructor.

Equivalent to calling Device(NoCreateT) followed by create(Instance&, const DeviceCreateInfo&).

Magnum::Vk::Device::Device(Instance& instance, DeviceCreateInfo&& info) explicit

Construct, reusing already populated device properties.

Equivalent to calling Device(NoCreateT) followed by create(Instance&, DeviceCreateInfo&&).

Magnum::Vk::Device::Device(NoCreateT) explicit

Construct without creating the device.

Use create() or tryCreate() to create the device.

Magnum::Vk::Device::Device(Device&& other) deleted

Moving is not allowed.

See Disabled move and delayed device creation for more information.

Magnum::Vk::Device::~Device()

Destructor.

Destroys associated VkDevice handle, unless the instance was created using wrap() without HandleFlag::DestroyOnDestruction specified.

Magnum::Vk::Device::operator VkDevice()

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

void Magnum::Vk::Device::wrap(Instance& instance, VkPhysicalDevice physicalDevice, VkDevice handle, Version version, const Containers::StringIterable& enabledExtensions, const DeviceFeatures& enabledFeatures, HandleFlags flags = {})

Wrap existing Vulkan handle.

Parameters
instance Vulkan instance the device is created on
physicalDevice Physical device the VkVkDevice was created from. Used to populate properties().
handle The VkDevice handle
version Vulkan version that's assumed to be used on the device
enabledExtensions Extensions that are assumed to be enabled on the device
enabledFeatures Features that are assumed to be enabled on the device
flags Handle flags

The handle is expected to be originating from instance and physicalDevice. The version, enabledExtensions and enabledFeatures parameters populate internal info about supported version, enabled extensions and enabled features and will be reflected in isVersionSupported(), isExtensionEnabled() and enabledFeatures(), among other things. If enabledExtensions / enabledFeatures is empty, the device will behave as if no extensions / no features were enabled.

Note that this function retrieves all device-specific Vulkan function pointers, which is a relatively costly operation. It's thus not recommended to call this function repeatedly for creating short-lived device instances, even though it's technically correct.

Unlike a device created using the constructor or create(), the Vulkan device is by default not deleted on destruction. Use flags for different behavior.

Device& Magnum::Vk::Device::operator=(Device&&) deleted

Moving is not allowed.

See Disabled move and delayed device creation for more information.

void Magnum::Vk::Device::create(Instance& instance, const DeviceCreateInfo& info)

Create a device.

Parameters
instance Vulkan instance to create the device on
info Device creation info

Meant to be called on a NoCreate'd instance. After creating the device populates device-level function pointers and runtime information about enabled extensions and features based on info, and finally requests device queues added via DeviceCreateInfo::addQueues(), populating the Queue references.

If device creation fails, a message is printed to error output and the application exits — if you need a different behavior, use tryCreate() instead.

void Magnum::Vk::Device::create(Instance& instance, DeviceCreateInfo&& info)

Create a device, reusing already populated device properties.

Compared to create(Instance&, const DeviceCreateInfo&), it can take ownership of the DeviceProperties added to info earlier via DeviceCreateInfo::DeviceCreateInfo(DeviceProperties&&, const ExtensionProperties*, Flags) or any of the other r-value-taking constructors.

With that, the properties() getter and any APIs relying on it can reuse what was possibly already queried without having to repeat the potentially complex queries second time.

Result Magnum::Vk::Device::tryCreate(Instance& instance, const DeviceCreateInfo& info)

Try to create a device.

Unlike create(Instance&, const DeviceCreateInfo&), instead of exiting on error, prints a message to error output and returns a corresponding result value. On success returns Result::Success.

Result Magnum::Vk::Device::tryCreate(Instance& instance, DeviceCreateInfo&& info)

Try to create a device, reusing already populated device properties.

Unlike create(Instance&, DeviceCreateInfo&&), instead of exiting on error, prints a message to error output and returns a corresponding result value. On success returns Result::Success.

DeviceProperties& Magnum::Vk::Device::properties()

Device properties.

If a r-value DeviceProperties instance was propagated to DeviceCreateInfo and then to Device, it's reused here. Otherwise the contents are populated on first use.

Version Magnum::Vk::Device::version() const

Version supported by the device.

Unless overridden using --magnum-vulkan-version on the command line, corresponds to DeviceProperties::version().

bool Magnum::Vk::Device::isVersionSupported(Version version) const

Whether given version is supported on the device.

Compares version against version().

template<class E>
bool Magnum::Vk::Device::isExtensionEnabled() const

Whether given extension is enabled.

Accepts device extensions from the Extensions namespace, listed also in the Vulkan support tables. Search complexity is $ \mathcal{O}(1) $ . Example usage:

if(device.isExtensionEnabled<Vk::Extensions::EXT::index_type_uint8>()) {
    // keep mesh indices 8bit
} else {
    // convert them to 16bit
}

Note that this returns true only if given extension is supported by the driver and it was enabled via DeviceCreateInfo::addEnabledExtensions(). For querying extension support before creating a device use ExtensionProperties::isSupported().

bool Magnum::Vk::Device::isExtensionEnabled(const Extension& extension) const

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

const DeviceFeatures& Magnum::Vk::Device::enabledFeatures() const

Features enabled on the device.

const FlextVkDevice& Magnum::Vk::Device::operator*() const

Device-specific Vulkan function pointers.

Function pointers are implicitly stored per-device, use populateGlobalFunctionPointers() to populate the global vk* functions.

const FlextVkDevice* Magnum::Vk::Device::operator->() const

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

VkDevice Magnum::Vk::Device::release()

Release the underlying Vulkan device.

Releases ownership of the Vulkan device and returns its handle so vkDestroyDevice() is not called on destruction. The internal state is then equivalent to moved-from state.

void Magnum::Vk::Device::populateGlobalFunctionPointers()

Populate global device-level function pointers to be used with third-party code.

Populates device-level global function pointers so third-party code is able to call global device-level vk* functions. See Interaction with raw Vulkan code for more information.