QQuickPaintedItem updating slowly using QPainter

Viewed 720

So I have been toying arpund with QQuickPaintedItem to make a simple scribbling app. Taking insipiration from this Tablet Example, I have created a simply QML item based on QQuickPaintedItem which gets mouse events and paints a path on the screen based on the input received. However, upon testing I realised that my implementation is slow, more specifically when the mouse moves across the scene the paint lags behind the movement. I have to create the same example using a custom QWidget (which uses the same technique) and the result much better, with practically no lag in the painting. I've recorded the issue (slowed down at 0.5x): Video QQuickPaintedItem vs Video QWidget.

Here is the code for the QQuickPaintedItem implementation:

DrawingCanvas.h

#ifndef DRAWINGCANVAS_H
#define DRAWINGCANVAS_H

#include <QObject>
#include <QQuickPaintedItem>
#include <QImage>
#include <QPainter>

class DrawingCanvas : public QQuickPaintedItem
{
    Q_OBJECT
    Q_PROPERTY(bool drawing READ drawing WRITE setDrawing NOTIFY drawingChanged)

public:
    explicit DrawingCanvas(QQuickItem *parent = nullptr);
    bool drawing() const;

    Q_INVOKABLE void initiateBuffer();
    QString penColor() const;

public slots:
    void setDrawing(bool drawing);

protected:
    void mousePressEvent(QMouseEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;
    void mouseReleaseEvent(QMouseEvent *event) override;
    void paint(QPainter *painter);

signals:
    void drawingChanged(bool drawing);
    void penWidthChanged(int penWidth);
    void penColorChanged(QString penColor);

private:
    void drawOnBuffer(QPointF pos);

    bool m_drawing;
    QPixmap m_buffer;
    QPointF m_lastPoint;
    QRect m_updateRect;

};

#endif // DRAWINGCANVAS_H

DrawingCanvas.cpp

#include "drawingcanvas.h"

#include <QPainter>

DrawingCanvas::DrawingCanvas(QQuickItem *parent) : QQuickPaintedItem(parent)
{
    setRenderTarget(FramebufferObject);
    setAcceptedMouseButtons(Qt::AllButtons);
}

bool DrawingCanvas::drawing() const
{
    return m_drawing;
}

void DrawingCanvas::mousePressEvent(QMouseEvent *event)
{
    if (!m_drawing) {
        m_drawing = true;
        m_lastPoint = event->pos();
    }
}

void DrawingCanvas::mouseMoveEvent(QMouseEvent *event)
{
    if (m_drawing) {
        drawOnBuffer(event->pos());
        m_lastPoint = event->pos();
    }
}

void DrawingCanvas::mouseReleaseEvent(QMouseEvent *event)
{
    if (m_drawing && event->buttons() == Qt::NoButton)
        m_drawing = false;
}

void DrawingCanvas::paint(QPainter *painter)
{
    painter->drawPixmap(m_updateRect, m_buffer, m_updateRect);
    m_updateRect = QRect();
}

void DrawingCanvas::setDrawing(bool drawing)
{
    if (m_drawing == drawing)
        return;

    m_drawing = drawing;
    emit drawingChanged(m_drawing);
}

void DrawingCanvas::initiateBuffer()
{
    m_buffer = QPixmap(width(), height());
    m_buffer.fill(Qt::transparent);
}

void DrawingCanvas::drawOnBuffer(QPointF pos)
{
    QPainter bufferPainter;
    int rad = 2;

    if(bufferPainter.begin(&m_buffer)){

        bufferPainter.drawLine(m_lastPoint, pos);

        auto dirtyRect = QRect(m_lastPoint.toPoint(), pos.toPoint()).normalized()
                .adjusted(-rad, -rad, rad, rad);


        //        // change the canvas dirty region
        if(m_updateRect.isNull()){
            m_updateRect = dirtyRect;
        }
        else{
            m_updateRect = m_updateRect.united(dirtyRect);
        }
        update(m_updateRect);
    }
}

And here is the QWidget implementation:

DrawingWidget.h

#ifndef DRAWINGWIDGET_H
#define DRAWINGWIDGET_H

#include <QWidget>

class DrawingWidget : public QWidget
{
    Q_OBJECT
public:
    DrawingWidget();
protected:
    void mousePressEvent(QMouseEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;
    void mouseReleaseEvent(QMouseEvent *event) override;
    void paintEvent(QPaintEvent *event) override;
    void resizeEvent(QResizeEvent *event) override;

private:
    void initPixmap();
    void paintPixmap(QPainter &painter, QMouseEvent *event);

    QPixmap m_pixmap;
    bool m_deviceDown = false;

    struct Point {
        QPointF pos;
    } lastPoint;
};

#endif // DRAWINGWIDGET_H

DrawingWidget.cpp


#include "drawingwidget.h"

#include <QCoreApplication>
#include <QPainter>
#include <QtMath>
#include <cstdlib>
#include <QMouseEvent>

DrawingWidget::DrawingWidget()
{
    resize(500, 500);
    setAutoFillBackground(true);
}

void DrawingWidget::mousePressEvent(QMouseEvent *event)
{
    if (!m_deviceDown) {
        m_deviceDown = true;
        lastPoint.pos = event->pos();
    }
}

void DrawingWidget::mouseMoveEvent(QMouseEvent *event)
{
    if (m_deviceDown) {
        QPainter painter(&m_pixmap);
        paintPixmap(painter, event);
        lastPoint.pos = event->pos();

    }
}

void DrawingWidget::mouseReleaseEvent(QMouseEvent *event)
{
    if (m_deviceDown && event->buttons() == Qt::NoButton)
        m_deviceDown = false;
    update();
}


void DrawingWidget::initPixmap()
{
    qreal dpr = devicePixelRatioF();
    QPixmap newPixmap = QPixmap(qRound(width() * dpr), qRound(height() * dpr));
    newPixmap.setDevicePixelRatio(dpr);
    newPixmap.fill(Qt::white);
    QPainter painter(&newPixmap);
    if (!m_pixmap.isNull())
        painter.drawPixmap(0, 0, m_pixmap);
    painter.end();
    m_pixmap = newPixmap;
}

void DrawingWidget::paintEvent(QPaintEvent *event)
{
    if (m_pixmap.isNull())
        initPixmap();
    QPainter painter(this);
    QRect pixmapPortion = QRect(event->rect().topLeft() * devicePixelRatioF(),
                                event->rect().size() * devicePixelRatioF());
    painter.drawPixmap(event->rect().topLeft(), m_pixmap, pixmapPortion);
}

void DrawingWidget::paintPixmap(QPainter &painter, QMouseEvent *event)
{
    static qreal maxPenRadius = 1.0;
    painter.setRenderHint(QPainter::Antialiasing);


    painter.drawLine(lastPoint.pos, event->pos());
    update(QRect(lastPoint.pos.toPoint(), event->pos()).normalized()
           .adjusted(-maxPenRadius, -maxPenRadius, maxPenRadius, maxPenRadius));
}

void DrawingWidget::resizeEvent(QResizeEvent *)
{
    initPixmap();
}

EDIT: I have also tried to implement this using the new Shapes APIs in QML, but the results were similar to QQuickPaintedItem:

    Shape {
        id: myShape
        anchors.fill: parent
        ShapePath {
            id: shapePath
            strokeColor: "black"
            strokeWidth: 2
            capStyle: ShapePath.RoundCap
            fillColor: "transparent"
        }
    }
    MouseArea {
        anchors.fill: parent
        onPressed: {
            shapePath.startX = mouse.x
            shapePath.startY = mouse.y
        }

        onPositionChanged: {
            var pathcurve = Qt.createQmlObject(
                        'import QtQuick 2.12; PathCurve {}', shapePath)
            pathcurve.x = mouse.x
            pathcurve.y = mouse.y
            shapePath.pathElements.push(pathcurve)
        }
    }
1 Answers

Ok so after playing around with my code for more than a month I came to realised that this is because of V-Sync enabled in QML. By default QML automatically syncs the GL drawing to the vertical refresh of the screen. The solution to this problem is described here which mentions:

To minimize latency, your best bet would be to:

  1. disable vsync, either using QSurfaceFormat::setSwapInterval(0) (since 5.3) or in the system's control panel.

  2. Run your application with QSG_RENDER_LOOP=basic to turn off the threaded/window render loops. The windows/threaded render loops will rely on vsync to be throttled so if you don't set 'basic' animations will spin at 100% CPU.

Qt's mouse input handling is done by posting an even to the GUI thread when the mouse/touch event comes in. This will be present in the next rendered frame. With vsync and double/triple buffering this means that frame comes to screen 0-33 ms after the event occured. Up to another 16ms delay will be added if the application is already throttled (and blocked in for instance its swapBuffer() call).

Then add in any latency added by the system compositor which may or may not be another few vsyncs worth of latency.

For step 1, one can use the very convenient QSurfaceFormat::setDefaultFormat before the loading of the QML engine. Step 2 is very important since if the rendering loop is not set to basic, the QML animations system goes crazy hard on the CPU and the animations become way too fast. To setup an environment variable in Qt it is sufficient to call:

qputenv("QSG_RENDER_LOOP", "basic");

before the Qt Application object is instantiated.

Related