QML Threaded Animation: Scene Graph vs. Property Animations

QML provides two distinct approaches for implementing animations: the standard Qt Quick property animation system and OpenGL-based scene graph rendering. This article examines a demo that illustrates the performence differences between these methods under CPU load.

Visual Comparison

The demo displays two spinning icons (see Figure 1). The left spinner uses QML's built-in NumberAnimation, while the right one leverages a custom OpenGL-based implementation via the scene graph. When a simulated blocking operation occurs (indicated by "Blocked!" text), the left spinner visibly stutters every ~400ms, whereas the right spinner remains smooth—demonstrating thread separation in rendering.

Scene Graph Implementation (Right Spinner)

A custom Spinner class inherits from QQuickItem to integrate with QML:

#ifndef SPINNER_H
#define SPINNER_H

#include <QtQuick/QQuickItem>

class Spinner : public QQuickItem
{
    Q_OBJECT
    Q_PROPERTY(bool spinning READ spinning WRITE setSpinning NOTIFY spinningChanged)

public:
    Spinner();
    bool spinning() const { return m_spinning; }
    void setSpinning(bool spinning);

protected:
    QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *) override;

signals:
    void spinningChanged();

private:
    bool m_spinning = false;
};

The implementation creates a specialized scene graph node (SpinnerNode) that handles rendering:

QSGNode *Spinner::updatePaintNode(QSGNode *old, UpdatePaintNodeData *)
{
    auto node = static_cast<SpinnerNode*>(old);
    if (!node)
        node = new SpinnerNode(window());
    node->setSpinning(m_spinning);
    return node;
}

The SpinnerNode class manages texture loading and rotation:

class SpinnerNode : public QObject, public QSGTransformNode
{
    Q_OBJECT
public:
    explicit SpinnerNode(QQuickWindow *window) 
        : m_window(window)
    {
        connect(window, &QQuickWindow::beforeRendering, this, &SpinnerNode::rotateIfNeeded);
        connect(window, &QQuickWindow::frameSwapped, this, &SpinnerNode::requestUpdate);

        auto texture = window->createTextureFromImage(QImage(":/spinner.png"));
        auto textureNode = new QSGSimpleTextureNode();
        textureNode->setTexture(texture);
        textureNode->setRect(0, 0, 64, 64);
        appendChildNode(textureNode);
        m_texture.reset(texture);
    }

private slots:
    void rotateIfNeeded() {
        if (!m_spinning) return;
        m_rotation += 360.0 / m_window->screen()->refreshRate();
        QMatrix4x4 transform;
        transform.translate(32, 32);
        transform.rotate(m_rotation, 0, 0, 1);
        transform.translate(-32, -32);
        setMatrix(transform);
    }

    void requestUpdate() {
        if (m_spinning) m_window->update();
    }

private:
    qreal m_rotation = 0;
    bool m_spinning = false;
    QScopedPointer<QSGTexture> m_texture;
    QQuickWindow *m_window;
};

Property Animation (Left Spinner)

The left spinner uses declarative QML animation:

Image {
    source: "spinner.png"
    anchors.centerIn: parent
    anchors.horizontalCenterOffset: -80
    
    NumberAnimation on rotation {
        from: 0; to: 360
        duration: 1000
        loops: Animation.Infinite
    }
}

Bolcking Simulation

A JavaScript timer simulates CPU-intensive work:

Timer {
    id: blocker
    interval: 357
    repeat: true
    onTriggered: {
        const start = Date.now();
        while (Date.now() - start < 100) {
            // Busy-wait to block main thread
        }
    }
}

This blocking operation affects only the main-thread animation (left spinner), while the scene graph renderer (right spinner) continues smoothly since it operates on a separate render thread when using threaded render loops.

Tags: QML qtquick SceneGraph OpenGL threading

Posted on Tue, 20 Oct 2026 16:56:50 +0000 by AshtrayWaterloo