Laszlo Agocs

Vulkan Support in Qt 5.10 – Part 2

Published Friday June 16th, 2017
Leave a comment
Posted in Android, Dev Loop, Graphics, OpenGL, QPA, Windows

In the previous instalment we looked at the background for Qt 5.10’s upcoming Vulkan support. Let’s now start digging out into the details.

Obtaining a Vulkan-Enabled Qt Build

When building Qt from sources on Windows, Linux or Android, Vulkan support is enabled automatically whenever a Vulkan header is detected in the build environment. Windows is handled specially in the sense that the environment variable VULKAN_SDK – set by the LunarG Vulkan SDK – is picked up automatically.

Check the output of configure (also available afterwards in config.summary):

Qt Gui:
  ...
  Vulkan ................................. yes
  ...

If it says no, go to qtbase/config.tests/qpa/vulkan, run make, and see why it did not compile.

As mentioned in part 1, neither the QtGui library nor the platform plugins link directly to libvulkan or similar. Same applies to Qt applications by default. This comes very handy here: a Vulkan-enabled Qt build is perfectly fine for deployment also to systems without any Vulkan libraries. No headache with missing DLLs and such. Naturally, once it turns out Vulkan is not available at runtime, QVulkanInstance::create() will fail and will return false always. It must also be noted that the applications themselves can choose to link to a Vulkan (loader) library, if they have a reason to do so: all it takes is adding LIBS+=-lvulkan or similar to the .pro file.

Getting a Vulkan Instance

In Vulkan all per-application state is stored in a VkInstance object, see the the specification for a detailed overview. In Qt, Vulkan instances are represented by QVulkanInstance. This is backed by a QPlatformVulkanInstance following the usual QPA patterns. The platform plugins, at least the ones that are interested in providing Vulkan support, are expected to provide an implementation for it under the hood. As described earlier, this currently covers windows, xcb and android.

Following the familiar pattern from QWindow and the QOpenGL* classes, QVulkanInstance performs no initialization until create() is called. The loading of the Vulkan library (or the loader library which in turn routes to a vendor implementation) happens only at this point. (with a few exceptions, see below)

The resulting VkInstance can be retrieved via vkInstance().

Quite unsurprisingly, QVulkanInstance allows specifying the usual instance configuration options, like the desired API version, and, most importantly, the list of layers and extensions to enable.

While the Qt APIs allow including unsupported layers and extensions too – since it filters them out automatically – it may still be necessary in some cases to examine the names and versions of all supported layers and extensions. This can be done at any time – even before calling create() – via supportedExtensions() and supportedLayers(). These will naturally trigger an early loading of the Vulkan implementation when needed.

It is worth knowing that the surface-related extensions that are required for basic operation, such as VK_KHR_surface or VK_KHR_win32_surface, are automatically added to the list by Qt, and applications do not have to worry about these.

Typical main() Patterns

In the end the main() function for a Qt application with a Vulkan-capable window (or a Vulkan-capable window embedded into a QWidget hierarchy) will typically look like the following:

int main(int argc, char **argv)
{
    QGuiApplication app(argc, argv); // or QApplication when widgets are involved

    const bool enableLogsAndValidation = ...

    QVulkanInstance inst;

    if (enableLogsAndValidation) {
        QLoggingCategory::setFilterRules(QStringLiteral("qt.vulkan=true"));

#ifndef Q_OS_ANDROID
        inst.setLayers(QByteArrayList() << "VK_LAYER_LUNARG_standard_validation");
#else // see Android-specifics at https://developer.android.com/ndk/guides/graphics/validation-layer.html
        inst.setLayers(QByteArrayList()
                       << "VK_LAYER_GOOGLE_threading"
                       << "VK_LAYER_LUNARG_parameter_validation"
                       << "VK_LAYER_LUNARG_object_tracker"
                       << "VK_LAYER_LUNARG_core_validation"
                       << "VK_LAYER_LUNARG_image"
                       << "VK_LAYER_LUNARG_swapchain"
                       << "VK_LAYER_GOOGLE_unique_objects");
#endif
    }

    if (!inst.create())
        qFatal("Failed to create Vulkan instance: %d", inst.errorCode());

    MyVulkanWindow w;
    w.setVulkanInstance(&inst);
    w.resize(1024, 768);
    w.show();

    return app.exec();
}

In most cases there will be a single QVulkanInstance. This can live on the stack, but has to be ready before creating the QWindow or QVulkanWindow-derived window objects since they will need to be associated with a QVulkanInstance. (more on this and other window-related topics in part 3)

The logging category qt.vulkan can be very helpful for troubleshooting. When enabled, both QVulkanInstance and, if used, QVulkanWindow will print a number of interesting things on the debug output, during initialization in particular. The hard-coded setFilerRules() call in the code snippet above is not necessarily the best approach always, but works well for platforms where environment variables (QT_LOGGING_RULES) are problematic. On Windows and Linux it is better to control this via the environment or configuration files.

When it comes to output from Vulkan and, first and foremost, the validation layers, QVulkanInstance offers the convenience of automatically redirecting these messages to qDebug. By default VK_EXT_debug_report gets enabled and redirection is active. If this is not desired, set the corresponding flag before calling create().

For example, the output from the hellovulkancubes example running on an NVIDIA Shield TV with Android 7.0 will look something like the following. If there were validation errors, they would show up too in a similar manner.

qt.vulkan: Supported Vulkan instance layers: QVector()
qt.vulkan: Supported Vulkan instance extensions: QVector(QVulkanExtension("VK_KHR_surface" 25), QVulkanExtension("VK_KHR_android_surface" 6), QVulkanExtension("VK_EXT_debug_report" 2))
qt.vulkan: Enabling Vulkan instance layers: ()
qt.vulkan: Enabling Vulkan instance extensions: ("VK_EXT_debug_report", "VK_KHR_surface", "VK_KHR_android_surface")
qt.vulkan: QVulkanWindow init
qt.vulkan: 1 physical devices
qt.vulkan: Physical device [0]: name 'NVIDIA Tegra X1' version 361.0.0
qt.vulkan: Using physical device [0]
Supported sample counts: QVector(1, 2, 4, 8)
Requesting 4x MSAA
qt.vulkan: queue family 0: flags=0xf count=16 supportsPresent=1
qt.vulkan: Using queue families: graphics = 0 present = 0
qt.vulkan: Supported device extensions: QVector(QVulkanExtension("VK_KHR_swapchain" 68), QVulkanExtension("VK_KHR_sampler_mirror_clamp_to_edge" 1), QVulkanExtension("VK_NV_dedicated_allocation" 1), QVulkanExtension("VK_NV_glsl_shader" 1))
qt.vulkan: Enabling device extensions: QVector(VK_KHR_swapchain)
qt.vulkan: memtype 0: flags=0x1
qt.vulkan: memtype 1: flags=0x1
qt.vulkan: memtype 2: flags=0x7
qt.vulkan: memtype 3: flags=0xb
qt.vulkan: Picked memtype 2 for host visible memory
qt.vulkan: Picked memtype 0 for device local memory
qt.vulkan: Color format: 37 Depth-stencil format: 129
Renderer init
qt.vulkan: Creating new swap chain of 2 buffers, size 1920x1080
qt.vulkan: Actual swap chain buffer count: 2 (supportsReadback=1)
qt.vulkan: Allocating 33423360 bytes for transient image (memtype 0)
qt.vulkan: Allocating 66846720 bytes for transient image (memtype 0)

Working with External Graphics Engines

Our final topic for this part is the question of integrating with existing, external engines.

During the lifetime of the Qt 5.x series, there has been a growing focus on making Qt Quick and the underlying OpenGL enablers more interoperable with foreign engines. This led to productizing QQuickRenderControl, the enhancements to QOpenGLContext for adopting existing native contexts, and similar improvements all over the stack.

In the same spirit QVulkanInstance allows adopting an existing VkInstance. All this takes is calling setVkInstance() before create(). This way every aspect of the VkInstance creation is up to the application or some other framework, and QVulkanInstance will merely wrap the provided VkInstance object instead of constructing a new one from scratch.

That’s all for now, stay tuned for part 3!

Do you like this? Share it
Share on LinkedInGoogle+Share on FacebookTweet about this on Twitter

Posted in Android, Dev Loop, Graphics, OpenGL, QPA, Windows

Leave a Reply

Your email address will not be published.

Get started today with Qt Download now