Our company has developed a game client based on Qt5. Since a few weeks we got reports of our players that after a certain amount of time the game stutters for a few seconds and then continues without that effect happening again.
We could narrow it down to a minimal example. It seems to occur only on newer NVIDIA drivers (we tried 416 and 418, under 3xx.xx the bug didn’t happen).
We contacted the Qt company and the could reproduce it but they think its a bug in the NVIDIA driver since it didn’t happen in older versions of the driver. So I hope somebody here can clarify what exactly happens.
- You need to have the Qt5 framework installed (we tried 5.5.1, 5.11 and the newest 5.12.1 versions which are all affected). The open source version is enough to try this. We have tried the msvc2017 32-bit version and the mingw 64-bit version.
- In the bin-directory there exists a executable named "qmlscene.exe" which can be used to execute a so called QML-file which is some kind of declarative UI language with javascript
- Before you start it you should set two environment variables wich set the size of the used texture atlas. The bigger the atlas the longer the stutter. You should set at least 4096x4096 pixels ``` set QSG_ATLAS_WIDTH=4096 set QSG_ATLAS_HEIGHT=4096 ```
- Save the example below as test.qml and run it with qmlscene.exe ``` qmlscene.exe test.qml ```
- After running smooth for a few seconds you will see the animation stuttering heavily for a short while. Then it's running normally again. The stutter seems to appear somewhere within the NVIDIA OpenGL driver (nvoglv32.dll). At least thats what the profiler stack trace shows which can't be narrowed down because of missing debug symbols.
Here is the QML-file to run:
import QtQuick 2.5
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
import QtQuick.Layouts 1.2
import QtQuick.Dialogs 1.0
Window {
id: launcherWindow
color: "grey"
width: 800
height: 600
property int drawnFrames: 0
// this is important since it triggers the bug
// when an image element is loaded directly at the beginning that has the property
// smooth set to "false" the effect happens. If smooth is set to "true" the bug won't appear
Image {
source: "background.png"
smooth: false
}
Text {
text: "Drawn Frames: " + drawnFrames
color: "white"
font.pixelSize: 20
}
Rectangle {
width: 400
height: 200
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
Canvas {
id: canvas
property real progress: 0.0
anchors.fill: parent
Timer {
id: animationTimer
property int direction: 1
interval: 25
repeat: true
running: true
onTriggered: {
canvas.progress += 0.002 * direction;
if (canvas.progress > 1.0 || canvas.progress < 0.0) {
direction = -1 * direction;
}
// this is the other important part. You need a canvas element which is periodically redrawn via
// "requestPaint". The bigger the canvas element (which should mean more pixels redrawn) the sooner
// the bug occurs
canvas.requestPaint();
}
}
onPaint: {
var ctx = getContext("2d");
ctx.reset();
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, progress * width, height);
drawnFrames += 1;
}
}
}
}
A bit of technical background:
Qt5 uses a scenegraph to render scenes described in QML files. The base technology is called QtQuick. It has an OpenGL backend which will be driven by the Qt5 framework. The developer only declares simple UI elements like in the example above. All the resulting OpenGL calls will be done directly by the QtQuick backend.
For images an automatically managed texture atlas is used. Qt uploads images into this singleton texture atlas to have less context switches between draw calls.
The effect only occurs when an image element exists that has “smooth” set to “false” which actually means draw it without filtering. This image element must be loaded at application start (so I guess it has something to do with initializing the graphics system and the texture atlas). Image elements that are loaded dynamically at a later time won’t trigger the effect.
Also the size of the texture atlas has a direct impact on the length of the stutter. It seems like the texture is maybe reinitialized.
The bug itself is triggered by using a Canvas-element (like the javascript canvas element in browsers) which has to be redrawn periodically. There seems to be a direct relation with the number of redrawn pixels. If I double the size of the canvas element the effect will happen after half the drawn frames.
I can’t say if the bug occurs because the Qt5 framework does something strange in OpenGL or that it’s really a driver bug which is new in 41x versions.