Qt and Direct3D 12 - First Encounter
The landscape of graphics APIs is changing. Qt Quick 2, released with Qt 5.0 in 2012, was betting heavily on OpenGL and OpenGL ES 2.0. There have been changes and improvements since then - the Qt Quick 2D Renderer was introduced, experiments with software rasterizers were made, and enablers for newer OpenGL versions got added all over the Qt graphics stack. However, as Lars mentioned in his Qt World Summit 2015 keynote, the situation is changing: new, low-level, more efficient APIs like Vulkan, Metal and Direct3D 12 are about to become widely available. Some of these are tied to certain platforms, making them the best choice when targeting the platform in question, while others are expected to be present on a wider variety of systems. At the same time betting solely on a complex, accelerated API is not always the best choice: traditional, desktop user interfaces running on older hardware are sometimes better served with plain old CPU-based rendering.
Therefore, unsurprisingly enough, one of the research areas for upcoming Qt versions is making the graphics stack, and in particular, Qt Quick, more flexible, with support for different graphics APIs as well as software rendering.
Such research work often leads to useful side effects: this post is about one of these, a simple Qt module enabling easy integration of Direct3D 12 rendering into a standalone Qt window - or alternatively QWidget-based applications - and making it easier to get started and experiment with modern D3D techniques while continuing to enjoy the familiar APIs, tools, and development environment Qt and its ecosystem offer.
What it is
The QtD3D12Window module, living in a qt-labs repository, is a Qt 5.6 module providing a QD3D12Window class similar to QOpenGLWindow, a handy qmake rule for offline HLSL shader compilation via fxc, and a comprehensive set of examples for a number of basic use cases (inspired by Microsoft's own Hello World samples).
What it is not
Before going further, let's reiterate what this module is not: it is not a way to run existing Qt applications on Direct3D (that is exactly what ANGLE provides when it comes to D3D9 and 11), nor does it support Qt Quick in any way. It is also not something that will get added to Qt in its current form, and it is not a complete engine or framework of any kind (the interface QD3D12Window offers is likely insufficient for more complex, multi-threaded approaches).
The first step for integrating custom rendering code using a new graphics API is to make the rendering target a QWindow's underlying native window. Currently OpenGL is the only choice and it is deeply integrated into Qt both internally (QPA interfaces, platform plugins) and externally (public APIs, see the QOpenGL classes in QtGui for instance). This is expected to change in the future, but for now, for our standalone D3D experiment, we will just do everything on our own. Qt's windows platform plugin makes it easy to obtain the native window handle (HWND) as that is exactly what QWindow::winId() returns in Qt applications running on Windows. We can then use DXGI to enumerate the available adapters, pick either a hardware one or WARP, and create a swap chain. QD3D12Window handles all this, with defaults that are good enough for the typical basic applications.
Speaking of DXGI, this marks our first foray into graphics adapter and device management: while with OpenGL and its usual windowing system interfaces such capabilities are limited, other APIs (especially the ones tied to platforms with a suitable driver model) may offer a lot more when it comes to discovering graphics adapters and managing device changes, removals and resets during the lifetime of the application. Such functionality, on platforms and APIs where available, is expected to get more focus in Qt in the future.
While implementing QD3D12Window, it turned out there was no need to do much generic plumbing - thanks to the way QOpenGLWindow and QRasterWindow had been introduced back in Qt 5.4: the common base class QPaintDeviceWindow (somewhat misleading in the D3D case as our window here is not suitable for QPainter-based painting) provides all the necessary infrastructure so subclasses can focus on the graphics API specifics while getting the basic, consistent Qt functionality - like update() - out of the box. This is good news since it allows easy experimenting with other APIs as well in the future (QMetalWindow, QVulkanWindow, you name it).
QD3D12Window mirrors QOpenGLWindow which in turn is based on QOpenGLWidget/QGLWidget's well-known initializeGL - resizeGL - paintGL interface, with GL becoming D3D, naturally. There are two new functions subclasses may reimplement: releaseD3D and afterPresent. The latter is invoked every time after issuing a Present call: most simple applications will wait for the GPU via a fence here. The former is used to make applications able to survive device removals: when the graphics device becomes unavailable, this function is invoked and is expected to release all resources it has created during initialize/resize/paint. QD3D12Window will then then take care of starting over and invoking initializeD3D again. This way the application can remain functional even when a driver update or a timeout in shader execution occurs.
The best is probably to dive straight into the examples, looking at hellotriangle for instance shows that if you haved used QOpenGLWindow or QOpenGLWidget before, QD3D12Window will present no unexpected surprises either.
So now we can have a top-level window with nothing but our awesome D3D12-rendered content in it. This is excellent but what about having some traditional user interface controls in there? For now, the approach that is usable with QD3D12Window is to make it a native child window via QWidget::createWindowContainer(). This comes with the usual limitations so be sure to read the documentation first. Nonetheless it will work fine for most simple purposes.
The handling of shader code and compilation is another very interesting topic. With OpenGL, Qt and Qt Quick bet on runtime compilation due to that being the only universally available solution not involving vendor and platform specifics. With other graphics APIs in the future, it is expected that Qt will focus more on offline compilation, integrating it into the build system as much as possible. Besides the obvious performance benefits, this drastically improves the development workflow as well: getting a proper compiler error right when hitting Ctrl+B in Qt Creator can easily feel infinitely superior to the "old" way of browsing the debug output while running the application.
The QtD3D12Window module comes with a simple qmake rule that allows invoking fxc.exe during build time. All the examples use this to generate header files where the compiled bytecode is provided as a plain char array. Take a look at the the .pro file for one of them and the shader setup for the pipeline state. Simple and easy, isn't it?
All this is not completely new to Qt: the bundled ANGLE in qtbase performs shader compilation in the same manner. However, that is hidden to and not usable by application-level D3D code, while the hlsl.prf file here can be copied over to the Qt SDK's mkspecs/features folder, making it available to any Qt application.
QD3D12Window automatically enables the D3D12 debug layer. This is extremely useful in practice as many of the common mistakes made while getting started with D3D12 result in human readable, verbose and actually helpful debug messages. However, you may want to disable this when doing performance testing or overhead comparison.
Another helpful tool is the graphics debugger included in Visual Studio. One way to launch this for Qt apps is doing devenv /debugexe qtapplication.exe from a developer command prompt and hitting Alt+F5. (alternatively, generating Visual Studio project files with qmake -tp vc may work too) This proved to be quite useful while developing even our simple examples - for instance the ability to inspect graphics resources and see if we managed to correctly generate all mipmap levels is immensely helpful.
As mentioned before, the module comes with a set of examples that cover the basics and may be useful to anyone getting started with D3D12 development. See the readme for an overview.
That's it for now, hope you find our little labs module useful. Happy hacking!
Subscribe to our newsletter
Try Qt 6.6 Now!
Download the latest release here: www.qt.io/download.
Qt 6.6. is a feature release with focus on improving UX capabilities including responsive UI technology and the Qt Graph module.
Check out all our open positions here and follow us on Instagram to see what it's like to be #QtPeople.