Qt 3D Renderer changes and improvements in Qt 6
With Qt 6 well on its way, it’s about time we go over some of the internal changes and optimizations made to Qt 3D for the upcoming release.
In a separate article, my colleague Mike Krus has already highlighted the API changes we’ve made in Qt 3D for Qt 6. This post will dive into the internal changes.
Goodbye Render Thread!
Back in 5.14, we got rid of the Aspect Thread. Yet we still had a Render Thread. On paper, having a dedicated thread would allow you to send drawing commands to the GPU while preparing commands for the next frame. This could potentially help with maintaining a high frame rate, if command submission took a long time. In practice, this worked only in the case that Qt 3D was used as standalone (without QtQuick).
The Qt Quick renderer has blocking synching points. Synching Qt Quick and Qt 3D content forces us to wait for all drawing commands to have been performed before proceeding to the next frame. Not only does this make having a RenderThread pointless, but it also results in separate code paths to handle Qt 3D driven by QtQuick.
Moreover, having an extra thread also adds a cost with general synchronization, scheduling, and memory allocations. In addition, the more threads you have, the more things get complex and hard to troubleshoot. For these reasons, we decided that whatever little speed gain we obtained from having a Render Thread didn’t justify keeping it.
In essence, this means that Qt 3D for Qt 6 only uses the main thread and a thread pool to spawn jobs in every frame. The rendering either takes place in the main thread or in the QtQuick SceneGraph thread, if using QtQuick.
Always favor the std containers
The backend of Qt 3D made heavy use of the Qt containers, like QVector and QHash. Much to our surprise, it appeared that QVector was adding a little bit of overhead everywhere we used it (mostly because of copy on write checks). Lots of something small can add up to something big. We were actually paying a high cost simply by using QVector.
Therefore, we’ve switched to using std::vectors in most of the Qt 3D backend. This work started with 5.15 and was pursued for Qt 6, as well. The API is not as nice as that of Qt’s vector but we have seen noticeable improvements in critical code paths on complex scenes. In contrast with the backend, the public API still relies on QVector (well QList in Qt6).
Use Rendering Abstractions
The Qt 5 architecture is tightly tied to OpenGL. Hence, our efforts with Qt 3D were primarily focused on providing an efficient OpenGL renderer. Towards the end of the Qt 5 series, The Qt Company started working on abstracting access to various graphics APIs with RHI (Rendering Hardware Abstraction).
In Qt 6, the tight coupling with OpenGL in the code will be removed in favor of calls made through the RHI abstraction layer. This signifies that Qt and, more specifically, Qt Quick will be able to work with different rendering backends (DirectX, Vulkan, OpenGL, Metal), depending on the target, the platform, and what’s available.
Qt 3D Render Plugins
Over the past couple of Qt releases, under the hood Qt 3D has slowly been reworked. The aim being to decouple the rendering from the processing code. In that sense, a plugin mechanism was introduced to load the renderer as a plugin. This has been finalized for Qt 6 and the existing OpenGL renderer is now a dedicated plugin. In parallel, we’ve started working on a new RHI-based render plugin.
The plan for Qt 3D for Qt 6 is to have the RHI-based renderer become the default. However, you will still be able to use the OpenGL render plugin if you wish to. A lot of work has been put into the OpenGL renderer and it is currently more mature and feature complete that what can be done with the RHI plugin. In reality, for most use cases, the RHI renderer will probably be enough.
API-wise this has little impact on code. The only changes required to have Qt 3D work with the RHI backend is to define a RHI compatible QTechnique on custom QMaterial classes. The default materials provided in the Qt 3D Extra modules have been updated.
Of course, this plugin mechanism will also allow you to have dedicated renderers for Metal or Vulkan, if the RHI plugin is deemed insufficient.
Selecting a Render Plugin and a RHI backend
By default, Qt 3D will try to load the RHI plugin and let RHI decide which backend to use. However, that behavior can be overridden by using the following environment variables and values:
Note: When using Scene3D, the OpenGL plugin should only be selected if you’re forcing RHI to use its OpenGL backend.
The development of RHI is driven with the use cases of Qt Quick and Qt Quick 3D in mind. This implies that not all the features required for more advanced use cases are available.
When it comes to Qt 3D, this means that what the FrameGraph API allows you to describe might not be translatable to existing RHI commands.
- No way to explicitly Blit (you have to blit manually by rendering a quad into a framebuffer)
- MemoryBarrier cannot be set explicitly
- Not all Texture Formats are available
- Draw Indirect is currently not supported
- Geometry Shaders are currently not supported.
- Different RHI backends support might support different feature set.
No Multithreading Support
One of the interesting features of modern graphics APIs, such as Vulkan, is the ability to use several threads to build the list of rendering commands. This maps perfectly to Qt 3D’s multithreaded architecture. Unfortunately, RHI does not currently support multithreading. Due to this limitation, while Qt 3D still leverages multiple threads to prepare commands, it needs to serially generate the RHI commands from a single thread. As a result, things are not likely to be any faster than what was achieved with the OpenGL renderer when it comes to command generation and submission. Arguably, there might still be a gain if the driver works better with whichever RHI backend gets used.
Limited feature set available with OpenGL backend
The OpenGL backend maps only to the ES 2 / GL 2 feature set.
Limitations on partial clears of a render target
Another hurdle with the current implementation is it limits each render target to a single clear call. For instance, you cannot clear the depth buffer after having drawn something. Given that, the workaround is generating more render targets and attachments than would otherwise be required and manually blitting between these.
As can be seen, a fair amount of work has been ongoing these past few months. From new performance optimizations in the backend to new decoupled rendering plugins, Qt 3D is ready for the next major Qt release. While most of these changes will be transparent for Qt 3D users, they still represent nice improvements.
KDAB provides a number of services around Qt 3D, including mentoring your team and embedding Qt 3D code into your application, among others. You can find out more about these services here.