Velvet and the QML Scene Graph

First of all, let me start with a bit of clarification. I was at DevDays this year and met a lot of people and I came to understand that we had done a poor job naming the scene graph project. Because of name similarity, it gives the indication that it is similar to projects like Open Scene Graph, which is not the case and was never the intention. Our scene graph is a compact and small 2D scene graph for rendering QML files. So, from now on, we will refer to it as the QML Scene Graph. Its sole purpose for existence is to make QML better.

Moving on...

Animations should feel like velvet - silky smooth and pleasing. From the technical perspective this requires a few things:

  • Draw one frame for every frame the display can draw. Modern displays, such as the LCD or LED screens you are using to read this, are almost always clocked at 60Hz. It depends on the resolution (dpi) of the display of course, but some magic happens around 60Hz. If you update 2D graphics at 30Hz, you can really see every single frame as individual frames. As you get closer to 60Hz, the frames start to blend together and the eyes perceive fluid motion rather than frames and that is the key difference between velvet and sand paper.
  • Be done on time. To reach 60 Hz, you need to be done in no more than 16.66 ms. If you ever shoot above, you missed your mark. That means that while doing animations, you cannot do anything else than updating a few properties and then draw the stuff. If it takes time, it either needs to be ready beforehand, or you do the work in a background thread as described in my previous post. Until recently, I was convinced that missing the mark once now and again, would not be disastrous, but it really makes the difference between velvet and sand paper.
  • Do not draw in the middle of vertical refresh. That leads to Screen Tearing, basically chopping your nice frame in two and ruining what should be a moment of visual pleasure. The solution is to use some form of synchronization that ties you to the screen's vertical refresh rate. Out of the box, Qt does not always help you here. On Mac OS X and on the Symbian N8, we are locked to vertical refresh and if you try to draw more often than that, the windowing system will block your rendering thread. On Linux / X11, Maemo and Windows, we are not locked and you can typically see tearing. Fortunately, there is a pretty simple solution. QGLWidget combined with QGLFormat::setSwapInterval set to 1, will enable the QGLWidget to be synchronized to vertical refresh. In the QML Scene Graph, we require OpenGL and we set the swap interval to 1 by default.
  • Advance the animation relative to the time between frames. If your object is moving at 1 pixel per millisecond, then it needs to move 16.66 pixels pr frame. If you missed a frame, then it needs to move 33.33 pixels the next one. If you always hit your 16.66ms mark, then this problem goes away, of course.

Yet with all these things in place, Qt cannot yet guarantee me velvet...

The missing ingredient is where the animation "tick" comes from. Qt's animation framework uses a timer, which advances the animations and updates properties in all the objects. This sends off several update requests and eventually the frame gets repainted. There are two faults in this setup. Firstly the timer is started at 1000 / 60, which rounds down to 16, so it's not 16.66 as it should have been. In addition, the timer is not accurate. It can fire at 15 or 17, and if it fires at 17, then the animation misses a frame.

This had been bothering me for a while, so while in San Francisco for DevDays, I had some "time off" to start digging. The result is that in the QML Scene Graph we drive the animations a bit differently. We do something along the lines of:

while (animationIsRunning) {

The result is that we are always progressing the animation in sync with the vertical refresh, so once every 16.66 ms, and exactly once pr frame. I said that I was initially not convinced that missing the occational frame was that bad, but it took me ONE look at the result and I realized we finally had it. Velvet!

We cannot do this generally in Qt because the method we have for vertical synchronization is only through OpenGL's swapBuffers(), so we can only tie it to one window. With Wayland or through custom OpenGL extensions, we can potentially get the vertical synchronization without going through swap, which means we could in theory advance animations across multiple windows, but that is out of scope for me right now. For now, it is fixed for that single window running the QML Scene Graph.

The repository is here:

Blog Topics: