Integrating custom OpenGL rendering with Qt Quick via QQuickFramebufferObject

Integrating custom OpenGL rendering code, for example to show 3D models, with Qt Quick is a popular topic with Qt users. With the release of Qt 5.0 the standard approach was to use the beforeRendering and afterRendering signals to issue the custom OpenGL commands before or after the rendering of the rest of the scene, thus providing an over- or underlay type of setup.

With Qt 5.2 a new, additional approach has been introduced: QQuickFramebufferObject. This allows placing and transforming the custom OpenGL rendering like any other Quick item, providing the most flexible solution at the expense of rendering via OpenGL framebuffer objects. While it was little known at the time of its original release, it is becoming more and more popular in applications built with Qt 5.4 and 5.5. It is also one of the building blocks for Qt 3D, where the Scene3D element is in fact nothing but a QQuickFramebufferObject under the hood. We will now take a look at this powerful class, look at some examples and discuss the issues that tend to come up quite often when getting started.

First things first: why is this great?

textureinsgnode

textureinsgnode is the standard example of QQuickFramebufferObject. The Qt logo is rendered directly via OpenGL while having animated transformations applied to the QQuickFramebufferObject-derived item.

qquickviewcomparison

This QQuickWidget example also features QQuickFramebufferObject: the Qt logo is a real 3D mesh with a basic phong material rendered via a framebuffer object. The textured quad, rendered using the framebuffer's associated texture, becomes a genuine item in the scene with other standard Qt Quick elements above and below it. It also demonstrates the usage of custom properties to control the camera.

The API

QQuickFramebufferObject is a QQuickItem-derived class that is meant to be subclassed further. That subclass is then registered to QML and used like any other standard Quick element.

However, having the OpenGL rendering moved to a separate thread, as Qt Quick does on many platforms, introduces certain restrictions on OpenGL resource management. To make this as painless as possible, the design of QQuickFramebufferObject follows the well-known split: subclasses are expected to reimplement a createRenderer() virtual function. This factory function is always called on the scenegraph's render thread (which is either the main (GUI) thread, or a separate, dedicated one). The returned QQuickFramebufferObject::Renderer subclass belongs to this render thread. All its functions, including the constructor and destructor, are guaranteed to be invoked on the render thread with the OpenGL context bound and therefore it is always safe to create, destroy, or access OpenGL resources.

In practice this is no different than the QQuickItem - QSGNode or the QVideoFilter - QVideoFilterRunnable pattern.

The basic outline of the C++ code usually becomes something like the following:


class FbItemRenderer : public QQuickFramebufferObject::Renderer
{
    QOpenGLFramebufferObject *createFramebufferObject(const QSize &size)
    {
        QOpenGLFramebufferObjectFormat format;
        format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
        // optionally enable multisampling by doing format.setSamples(4);
        return new QOpenGLFramebufferObject(size, format);
    }

void render() { // Called with the FBO bound and the viewport set. ... // Issue OpenGL commands. } ... }

class FbItem : public QQuickFramebufferObject { QQuickFramebufferObject::Renderer *createRenderer() const { return new FbItemRenderer; } ... }

int main(int argc, char **argv) { ... qmlRegisterType("fbitem", 1, 0, "FbItem"); ... }

In the QML code we can import and take the custom item into use:


import fbitem 1.0

FbItem { anchors.fill: parent ... // transform, animate like any other Item }

This is in fact all you need to get started and have your custom OpenGL rendering shown in a Qt Quick item.

Pitfalls: context and state

When getting started with QQuickFramebufferObject, the common issue is not getting anything rendered, or getting incomplete rendering. This can almost always be attributed to the fact that QQuickFramebufferObject::Renderer subclasses use the same OpenGL context as the Qt Quick scenegraph's renderer. This is very efficient due to avoiding context switches, and prevents unexpected issues due to sharing and using resources from multiple contexts, but it comes at the cost of having to take care of resetting the OpenGL state the rendering code relies on.

In earlier Qt versions the documentation was not quite clear on this point. Fortunately in Qt 5.5 this is now corrected, and the documentation of render() includes the following notes:

Do not assume that the OpenGL state is all set to the defaults when this function is invoked, or that it is maintained between calls. Both the Qt Quick renderer and the custom rendering code uses the same OpenGL context. This means that the state might have been modified by Quick before invoking this function.

It is recommended to call QQuickWindow::resetOpenGLState() before returning. This resets OpenGL state used by the Qt Quick renderer and thus avoids interference from the state changes made by the rendering code in this function.

What does this mean in practice?

As an example, say you have some old OpenGL 1.x fixed pipeline code rendering a not-very-3D triangle. Even in this simplest possible case you may need to ensure that certain state is disabled: we must not have a shader program active and depth testing must be disabled. glDisable(GL_DEPTH_TEST); glUseProgram(0); does the job. For rendering that is more 3D oriented, a good example of a very common issue is forgetting to re-enable depth writes via glDepthMask(GL_TRUE).

Although not necessary in many cases, it is good practice to reset all the state the Qt Quick renderer potentially relies on when leaving render(). Since Qt 5.2 a helper function is available to do this: resetOpenGLState(). To access this member function, store a pointer to the corresponding item in the Renderer and do the following just before returning from render():


    m_item->window()->resetOpenGLState();

Note that this must be the last thing we do in render() because it also alters the framebuffer binding. Issuing further rendering commands afterwards will lead to unexpected results.

Custom properties and data synchronization

Accessing the Renderer object's associated item looks easy. All we need is to store a pointer, right? Be aware however that this can be very dangerous: accessing data at arbitrary times from the Renderer subclass is not safe because the functions there execute on the scenegraph's render thread while ordinary QML data (like the associated QQuickFramebufferObject subclass) live on the main (GUI) thread of the application.

As an example, say that we want to expose the rotation of some model in our 3D scene to QML. This is extremely handy since we can apply bindings and animations to it, utilizing the full power of QML.


class FbItem : public QQuickFramebufferObject
{
    Q_OBJECT
    Q_PROPERTY(QVector3D rotation READ rotation WRITE setRotation NOTIFY rotationChanged)

public: QVector3D rotation() const { return m_rotation; } void setRotation(const QVector3D &v); ... private: QVector3D m_rotation; };

So far so good. Now, how do we access this vector in the FbItemRenderer's render() implementation? At first, simply using m_item->rotation() may look like the way to go. Unfortunately it is unsafe and therefore wrong.

Instead, we must maintain a copy of the relevant data in the renderer object. The synchronization must happen at a well-defined point, where we know that the main (GUI) thread will not access the data. The render() function is not suitable for this. Fortunately, QQuickFramebufferObject makes all this pretty easy with the Renderer's synchronize() function:


class FbItemRenderer : public QQuickFramebufferObject::Renderer
{
public:
    void synchronize(QQuickFramebufferObject *item) {
        FbItem *fbitem = static_cast<FbItem *>(item);
        m_rotation = fbitem->rotation();
    }
    ...
private:
    QVector3D m_rotation; // render thread's copy of the data, this is what we will use in render()
}

When synchronize() is called, the main (GUI) thread is guaranteed to be blocked and therefore it cannot read or write the FbItem's data. Accessing those on the render thread is thus safe.

Triggering updates

To schedule a new call to render(), applications have two options. When on the main (GUI) thread, or inside JavaScript code, use QQuickItem::update(). In addition, QQuickFramebufferObject::Renderer adds its own update() function, which is only usable from the render thread, for example inside render() to schedule a re-rendering of the FBO content on the next frame.

In practice the most common case is to schedule a refresh for the FBO contents when some property changes. For example, the setRotation() function from the above example could look like this:


void FbItem::setRotation(const QVector3D &v)
{
    if (m_rotation != v) {
        m_rotation = v;
        emit rotationChanged();
        update();
    }
}

Internals

Finally, it is worth noting that QQuickFramebufferObject is also prepared to handle multisampling and screen changes.

Whenever the QOpenGLFramebufferObject returned from createFramebufferObject() has multisampling enabled (the QOpenGLFramebufferObjectFormat has a sample count greater than 0 and multisampled framebuffers are supported by the OpenGL implementation in use), the samples will be resolved with a glBlitFramebuffer call into a non-multisampled FBO after each invocation of render(). This is done transparently to the applications so they do not have to worry about it.

Resizing the window with the Qt Quick content will lead to recreating the FBO because its size should change too. The application will see this as a repeated call to createFramebufferObject() with the new size. It is possible to opt out of this by setting the textureFollowsItemSize property to false.

However, there is a special case when the FBO has to get recreated regardless: when moving the window to a screen with a different device pixel ratio. For example moving a window between a retina and non-retina screen on OS X systems will inherently need a new, double or half sized, framebuffer, even when the window dimensions are the same in device independent units. Just like with ordinary resizes, Qt is prepared to handle this by requesting a new framebuffer object with a different size when necessary. One possible pitfall here is the applications' caching of the results of the factory functions: avoid this. createFramebufferObject() and createRenderer() must never cache their return value. Just create a new instance and return it. Keep it simple. Qt takes care of managing the returned instance and destroying it when the time comes. Happy hacking!


Blog Topics:

Comments