Direct3D 12 Support in Qt 6.6

Qt 6.6 introduces a new QRhi backend, for Direct3D 12. This means that the number of supported 3D APIs is now up to five: Vulkan, Metal, OpenGL / OpenGL ES, Direct3D 11, and Direct3D 12. Applications using Qt Quick and Qt Quick 3D can now choose to use D3D12 to render the contents of a QQuickWindow or QQuickView. In addition, building on the architectural improvements introduced earlier in this blog post, QQuickWidget is also fully supported.

Let's take a quick look at what this means in practice.

What does this mean for me?

In many cases, nothing. This is because for Qt Quick applications the default rendering backend continues to be Direct3D 11 on Windows, and this is not expected to change in the foreseeable future. Using Direct3D 12 needs to be an explicit choice now, similarly to how applications can also request using Vulkan or OpenGL when running on Windows.

While the D3D12 backend is expected to be quite robust already, some shortcoming can always surface. In addition, compatibility for existing applications, preventing surprises and unexpected behavioral changes, and avoiding deprecations with little practical value are important goals for the Qt graphics stack. Continuing with the existing defaults helps minimizing surprises when deploying and shipping applications to a wide range of systems (including virtual machines and old systems with a less-than-ideal driver situation), while those who want to or have to, can always opt-in to the new code paths.

How to use it in Qt Quick applications?

When working a QQuickWindow (or the Window QML element), QQuickView, QQuickWidget, call QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D12). This is typically done early on in main(). The setting is application global and applies to all Qt Quick windows.

Alternatively, set the QSG_RHI_BACKEND environment variable to d3d12 before launching the application.

When troubleshooting, or just to see if the request is correctly accepted, enabling the common scenegraph logs by setting QSG_INFO=1 (or by enabling the qt.scenegraph.general/qt.rhi.general logging categories) is useful, as always.

The D3D debug layer can be enabled either by launching with QSG_RHI_DEBUG_LAYER=1 or by setting the appropriate flag in QQuickGraphicsConfiguration and setting it on the QQuickWindow/View. (i.e., the exact same way one requests enabling the Vulkan validation layer when using Vulkan.) This requires that the debug layer is available at run time. Look for a line stating "Enabling D3D12 debug layer" in the logs when in doubt. This is probably less interesting for the typical Qt Quick application (as we do not expect standard Qt rendering to generate debug layer errors, do we), but may become useful when integrating external D3D12-based graphics, compute, or machine learning code into a Qt Quick application.

Speaking of external rendering engines, QQuickRenderTarget gets an appropriate fromD3D12Texture() function to allow wrapping an existing D3D12 texture and redirecting Qt Quick rendering into it when driving the QQuickWindow via QQuickRenderControl.  In QQuickGraphicsDevice, used to get QQuickWindow to adopt existing native physical device (adapter), device, context, etc. objects, fromAdapter() serves both D3D11 and D3D12 from now on.

How to use it in applications working directly with QRhi?

Qt 6.6 opens up the QRhi family of APIs to a large degree, meaning the documentation for these classes is now part of the standard Qt documentation set, and the APIs are lifted to a "semi-public" level similarly to the QPA classes. Applications that wish to use the same cross-platform rendering facilities Qt itself uses to implement Qt Quick and Qt Quick 3D, can now do so without having to deal with fully private APIs with no generated documentations.

When working directly with QRhi, one can pass QRhi::D3D12 to QRhi::create() and specify a QRhiD3D12InitParams. This is currently only used to control if the debug layer should be enabled. To use a specific adapter, or adopt an existing device+command queue combo, QRhiD3D12NativeHandles can be used. PreferSoftwareRenderer is honored by this backend as well, similarly to D3D11; this will request choosing the adapter with DXGI_ADAPTER_FLAG_SOFTWARE.  (e.g. WARP)

When pulling out native objects from the QRhi instance, QRhi::nativeHandles() provides a QRhiD3D12NativeHandles that reports the used adapter, ID3D12Device, and ID3D12CommandQueue. Unsurprisingly, querying the native texture from a QRhiTexture returns a ID3D12Resource and a D3D12_RESOURCE_STATES.

Some internal aspects

Internally, the D3D12 backend is similar in many ways to the Vulkan backend. In some aspects it is simpler (no render pass and subpass nightmares), while in some other aspects it has to deal with a number of annoying problems, esp. in the area of shader resource bindings and when it comes to managing the  shader visible descriptor heaps.

Regarding shaders, the same pipeline is used as with everything else: the Vulkan-compatible GLSL source code is compiled to SPIR-V and then translated further via SPIRV-Cross to HLSL. The catch here is that certain constructs do not quite map as-is between SPIR-V and HLSL, so the translation process generates additional metadata, for example mapping tables that say, for example, to map SPIR-V combined image sampler binding X to HLSL shader registers tY and sZ. This metadata is then consumed by the QRhi backend. This then presents some interesting challenges when building root signatures and laying out descriptor tables, consider for instance that the mentioned mapping tables are per-shader and completely independent (i.e. the binding-register mappings may differ between the vertex and fragment shaders, ouch), which means not everything as optimal yet as it could be. (this applies to D3D11 as well, but that APIs higher-level nature conveniently hides some of these issues)

Textures, buffers, and command buffers work similarly to Vulkan. Texture uploads go through a staging buffer, although the D3D12 backend tries something new by reserving a small staging area per frame (slot), currently 16 MB and use that for all non-Dynamic buffer and texture uploads as long as they fit. Dynamic (host visible, i.e. upload heap) QRhiBuffers are backed by multiple ID3D12Resources, and the usual frame slot model is used to allow having potentially more than one frame in flight. (i.e., like other well-behaving backends, the D3D12 one also does not simply block and wait for completion when submitting the commands for a frame). Same goes for command buffers, these will cycle through FRAMES_IN_FLIGHT ID3D12GraphicsCommandLists in consecutive QRhi::beginFrame() calls. FRAMES_IN_FLIGHT is set to 2 at the moment.

Compute shader support and storage images and buffers are implemented similarly to the other backends. In the unlikely event of using readonly SSBOs in the GLSL shader (e.g., layout (binding = 0, std430) readonly buffer someBuffer { ... }), it needs to be noted that Qt 6.6.0 has a bug where this gets translated to a HLSL ByteAddressBuffer (and that involves using an SRV), whereas the QRhi backend is only prepared to deal with RWByteAddressBuffer (and an UAV).

As with other backends, there are a number of environment variables that control backend-specific behavior or can be used to override some settings without changing the application:

  • QT_D3D_ADAPTER_INDEX - As with D3D11, this is the manual, environment variable-based override for the adapter to use. To get the index, check the logs printed from QSG_INFO=1 (or qt.rhi.general).
  • QT_D3D_STABLE_POWER_STATE - Calls SetStablePowerState(), with all the consequences and implications described in the D3D documentation. This can become relevant when working with timestamps via QRhiCommandBuffer::lastCompletedGpuTime().  GPU timings are in fact another new feature in Qt 6.6, and are a topic for a future blog post.
  • QT_RHI_LEAK_CHECK - Relevant in Release (or RelWithDebInfo) builds since Debug has this enabled automatically. When set to a non-zero value, the QRhi will print some helpful reminders if there are any resources (buffers, textures, etc.) not yet released at the time of destroying the QRhi instance. With the D3D12 backend this is extended to do more, as it checks for unreleased descriptors (RTVs, DSVs, SRVs, etc.) as well.
  • QT_D3D_DEBUG_BREAK - Enables breaking on Corruption, Error, and Warning severity messages from the debug layer.
  • QT_D3D_NO_SUBALLOC - Disables using D3D12MemoryAllocator. Doing so means every buffer and texture does its own CreateCommittedResource().

Why is this useful?

The D3D12 backend has two main benefits:

One is mentioned above already: applications that have to integrate with non-Qt-based D3D12 code can now do this in a much more natural manner since they can get the QQuickWindow to also use D3D12. No need for going through translation layers such as D3D11On12 anymore. Examples of this are rendering/compute engines using features that are not currently used or do not apply to Qt's own 2D/3D renderers (e.g., raytracing, some sophisticated compute, work graphs, etc.) or machine learning code using DirectML.

The other, which benefits Qt itself in the long run, is to enable using new and upcoming features that are no longer made available in D3D11. A good example of this is in the (currently in-development) 6.7 branch of Qt. This already has the lowest-level enablers for multiview rendering integrated. (think GL_OVR_multiview and the equivalents in the other 3D APIs; having multiview support is going to be important for having efficient VR/AR support in Qt Quick 3D in the future) While there were (and still are) no plans to enable this in the D3D11 backend, since multiview is only available as vendor-specific extensions there, it turned out that having a D3D12 backend was great news because there we could use view instancing as-is.

(plus one: being able to run Qt applications on top of D3D12 allows using tools, such as PIX that were not possible before, or at least not without using a translation layer)

What does this mean for graphics and compute shaders?

For now pretty much nothing, because targeting Shader Model 5.0 is going to be sufficient. There are no changes around this in Qt Quick or Qt Quick 3D: all built-in material shaders are conditioned with --hlsl 50 when running qsb (either directly or via CMake). Applications should need no changes either: switching the QRhi backend to D3D12 is expected to just work without touching anything related to the shader conditioning settings.

When thinking of some new features that may be taken into use in the future, Shader Model 5.0 may not be sufficient. For example, with the above-mentioned multiview work in Qt 6.7, shaders involved in multiview will need to use Shader Model 6.1 because SV_ViewID (to which gl_ViewIndex is translated) is only available from that version on. And the old DXBC-based tools (fxc, D3DCompile()) are no longer sufficient for that, one needs to switch to the DXIL-based toolchain. This is why in Qt 6.7 we will transparently introduce running the modern shader compiler, dxc, and use the dxcapi.h interfaces at run time whenever the requested Shader Model is 6.0 or higher. For 5.0 nothing changes, that will continue to use the old tools.

Blog Topics: