Decouple rate-limiting of paint-on-screen widgets from top level widget

As part of eacd58d4e78e7238ba5fcca90ba960aaf3ebd263, a mechanism was
added to prevent posting redundant UpdateRequest events to the top
level widget, managed by QWidgetRepaintManager. The mechanism relied
on a boolean that was set when posting an update request event, and
reset when processing the event for the top level widget, as part
of QWidgetRepaintManager::sync().

However, for paint-on-screen widgets, we don't post an update request
to the top level, we post it to the paint-on-screen widget directly.
And when processing that event, we don't paint the widget though the
normal QWidgetRepaintManager machinery, but instead call the paint
event via QWidgetPrivate::paintOnScreen().

As a result, an update() on a paint-on-screen widget would result
in never receiving updates for non-paint-on-screen widgets, as
we assumed there was no reason to send further update requests.

We could fix this by clearing the updateRequestSent flag as part
of the paintOnScreen() code path, but that's incorrect as the flag
represents whether the top level QWidgetRepaintManager needs an
update request event or not, and would lead to missed updates
to normal widgets until the paint-on-screen widget finishes its
update.

Instead, we only set updateRequestSent if we're posting update
requests for non-paint-on-screen widgets, which in practice
means the top level widget.

The paint on screen logic in QWidgetRepaintManager::markDirty
still takes care of rate-limiting the update requests to the
paint-on-screen widget, by comparing the dirty area of the
widget.

There is definite room for improvement here, especially in the
direction of handling update requests via QWindow::requestUpdate
instead of manually posted events, but for now this will have to
do.

Fixes: QTBUG-80167
Pick-to: 6.6 6.5 6.2 5.15
Change-Id: Ib5685de7ca2fd7cd7883a25bb7bc0255ea242d30
Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
(cherry picked from commit 697e1b0397259959e3f555296f87a0d9d923e4b5)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
Tor Arne Vestbø 2024-02-28 20:44:58 +01:00 committed by Qt Cherry-pick Bot
parent 007507b576
commit c198f7124c
2 changed files with 94 additions and 1 deletions

View File

@ -355,7 +355,11 @@ void QWidgetRepaintManager::sendUpdateRequest(QWidget *widget, UpdateTime update
switch (updateTime) {
case UpdateLater:
updateRequestSent = true;
// Prevent redundant update request events, unless it's a
// paint on screen widget, as these don't go through the
// normal backingstore sync machinery.
if (!widget->d_func()->shouldPaintOnScreen())
updateRequestSent = true;
QCoreApplication::postEvent(widget, new QEvent(QEvent::UpdateRequest), Qt::LowEventPriority);
break;
case UpdateNow: {

View File

@ -77,8 +77,11 @@ public:
const auto type = event->type();
if (type == QEvent::WindowActivate || type == QEvent::WindowDeactivate)
return true;
if (type == QEvent::UpdateRequest)
++updateRequests;
return QWidget::event(event);
}
int updateRequests = 0;
protected:
void paintEvent(QPaintEvent *event) override
@ -256,6 +259,8 @@ private slots:
void opaqueChildren();
void staticContents();
void scroll();
void paintOnScreenUpdates();
#if defined(QT_BUILD_INTERNAL)
void scrollWithOverlap();
void overlappedRegion();
@ -491,6 +496,90 @@ void tst_QWidgetRepaintManager::scroll()
QCOMPARE(widget.takePaintedRegions(), QRegion());
}
class PaintOnScreenWidget : public TestWidget
{
public:
using TestWidget::TestWidget;
// Explicit override to prevent noPaintOnScreen on Windows
QPaintEngine *paintEngine() const override
{
return nullptr;
}
};
void tst_QWidgetRepaintManager::paintOnScreenUpdates()
{
{
TestWidget topLevel;
topLevel.setObjectName("TopLevel");
topLevel.resize(500, 500);
TestWidget renderToTextureWidget(&topLevel);
renderToTextureWidget.setObjectName("RenderToTexture");
renderToTextureWidget.setGeometry(0, 0, 200, 200);
QWidgetPrivate::get(&renderToTextureWidget)->setRenderToTexture();
PaintOnScreenWidget paintOnScreenWidget(&topLevel);
paintOnScreenWidget.setObjectName("PaintOnScreen");
paintOnScreenWidget.setGeometry(200, 200, 300, 300);
topLevel.initialShow();
// Updating before toggling WA_PaintOnScreen should work fine
paintOnScreenWidget.update();
paintOnScreenWidget.waitForPainted();
QVERIFY(paintOnScreenWidget.waitForPainted());
#ifdef Q_OS_ANDROID
QEXPECT_FAIL("", "This test fails on Android", Abort);
#endif
QCOMPARE(paintOnScreenWidget.takePaintedRegions(), paintOnScreenWidget.rect());
renderToTextureWidget.update();
QVERIFY(renderToTextureWidget.waitForPainted());
QCOMPARE(renderToTextureWidget.takePaintedRegions(), renderToTextureWidget.rect());
// Then toggle WA_PaintOnScreen
paintOnScreenWidget.setAttribute(Qt::WA_PaintOnScreen);
// The render-to-texture widget updates fine
renderToTextureWidget.update();
QVERIFY(renderToTextureWidget.waitForPainted());
QCOMPARE(renderToTextureWidget.takePaintedRegions(), renderToTextureWidget.rect());
// Updating the paint-on-screen texture widget will not result
// in a paint event, but should result in an update request.
paintOnScreenWidget.updateRequests = 0;
paintOnScreenWidget.update();
QVERIFY(QTest::qWaitFor([&]{ return paintOnScreenWidget.updateRequests > 0; }));
// And should not prevent the render-to-texture widget from receiving updates
renderToTextureWidget.update();
QVERIFY(renderToTextureWidget.waitForPainted());
QCOMPARE(renderToTextureWidget.takePaintedRegions(), renderToTextureWidget.rect());
}
{
TestWidget paintOnScreenTopLevel;
paintOnScreenTopLevel.setObjectName("PaintOnScreenTopLevel");
paintOnScreenTopLevel.setAttribute(Qt::WA_PaintOnScreen);
paintOnScreenTopLevel.initialShow();
paintOnScreenTopLevel.updateRequests = 0;
paintOnScreenTopLevel.update();
QVERIFY(QTest::qWaitFor([&]{ return paintOnScreenTopLevel.updateRequests > 0; }));
// Turn off paint on screen and make it a render-to-texture widget.
// This will lead us into a QWidgetRepaintManager::markDirty() code
// path that checks updateRequestSent, which is still set from the
// update above since paint-on-screen handling doesn't reset it.
paintOnScreenTopLevel.setAttribute(Qt::WA_PaintOnScreen, false);
QWidgetPrivate::get(&paintOnScreenTopLevel)->setRenderToTexture();
paintOnScreenTopLevel.update();
QVERIFY(QTest::qWaitFor([&]{ return paintOnScreenTopLevel.updateRequests > 1; }));
}
}
#if defined(QT_BUILD_INTERNAL)