Invoke Canvas onPaint exactly once per update?

2019-09-12 05:38发布

问题:

AKA: Canvas requestPaint() too slow; requestAnimationFrame() too fast

I'm trying to create a QML Canvas that repaints as fast as possible—once per update in the main UI render loop—in order to create an FPS timer.

I initially wrote this simple test:

import QtQuick 2.7
import QtQuick.Window 2.2

Window {
    visible:true; width:100; height:100
    Canvas {
        anchors.fill:parent
        onPaint: console.log(+new Date)
    }
}

I only get the callback once. So I added requestPaint():

onPaint: {
    console.log(+new Date)
    requestPaint()
}

No change: I still only get one callback. Same if I use markDirty(). Same if I actually paint something on the canvas each callback.

So I moved to requestAnimationFrame():

import QtQuick 2.7
import QtQuick.Window 2.2
Window {
    visible:true; width:100; height:100
    Canvas {
        anchors.fill:parent
        Component.onCompleted: crank()
        function crank(){
            console.log(+new Date)
            requestAnimationFrame(crank)
        }
    }
}

Now I get callbacks, but way too many. On average, I get 77 callbacks per millisecond, some times as many as 127 callbacks in a single millisecond. So many callbacks that nothing else in the application displays, not even initially. Even if I remove the console.log(), to prove that I'm not i/o bound).

How can I get my canvas to repaint once "per frame", so that I can measure the FPS semi-accurately? Any why does requestPaint() not actually work? And why is requestAnimationFrame() apparently useless?

回答1:

The problem with your approach is that you are requesting paint from onPaint, this is not going to work, because onPaint event is triggered from within QQuickItem::polish()

void QQuickItem::polish()
{
    Q_D(QQuickItem);
    if (!d->polishScheduled) {
        d->polishScheduled = true;
        if (d->window) {
            QQuickWindowPrivate *p = QQuickWindowPrivate::get(d->window);
            bool maybeupdate = p->itemsToPolish.isEmpty();
            p->itemsToPolish.append(this);
            if (maybeupdate) d->window->maybeUpdate();
        }
    }
}

During this call d->polishScheduled is set to true and if you call requestPaint() again, nothing happens. You need to trigger it asynchronously. For example, use Timer with interval 0.

import QtQuick 2.0

Canvas {
  id: canvas
  width: 200
  height: 200
  property real angle
  property int fps

  Timer {
    id: repaintTimer
    running: false
    interval: 0
    onTriggered: {
      angle += 0.01
      canvas.requestPaint()
    }
  }

  Timer {
    interval: 1000
    running: true
    repeat: true
    onTriggered: {
      console.log(fps)
      fps = 0
    }
  }

  onPaint: {
    var ctx = getContext("2d")
    ctx.save()
    ctx.clearRect(0, 0, width, height)
    ctx.moveTo(100, 100)
    ctx.translate(100,100)
    ctx.rotate(angle)
    ctx.beginPath()
    ctx.lineTo(40, 10)
    ctx.lineTo(40, 40)
    ctx.lineTo(10, 40)
    ctx.lineTo(10, 10)
    ctx.closePath()
    ctx.stroke()
    ctx.restore()
    fps += 1
    repaintTimer.start()
  }
}

Another Timer is here to record fps. When I run this code in qmlscene, I get 60 fps.



回答2:

There was a bug with requestAnimationFrame() prior to Qt 5.9. This bug has been fixed.

The following code works as expected and desired to keep the canvas continuously redrawing:

Canvas {
    width:100; height:100;
    property var ctx
    onAvailableChanged: if (available) ctx = getContext('2d');
    onPaint: {
        if (!ctx) return;
        ctx.clearRect(0, 0, width, height);
        // draw here
        requestAnimationFrame(paint);
    }
}


标签: qt qml qt-quick