Do grabs out of order

...while extending the autotest to cover more complicated cases, such as
grabbing again after show-hide and doing show-grab-hide-grab-show-grab.

In fact some of these cases have not been working in Qt 5. Now the basic
render loop is fixed up to support the all the combinations threaded
does.

Task-number: QTBUG-87399
Change-Id: Id01995bc3a2660b16cfb2f8bedc84becea0be1bb
Reviewed-by: Andy Nichols <andy.nichols@qt.io>
This commit is contained in:
Laszlo Agocs 2020-10-14 19:20:22 +02:00
parent 8103b8ce12
commit c5ac1b8a06
3 changed files with 107 additions and 77 deletions

View File

@ -172,18 +172,20 @@ public:
struct WindowData {
WindowData()
: updatePending(false),
grabOnly(false),
: sampleCount(1),
updatePending(false),
rhiDeviceLost(false),
rhiDoomed(false)
{ }
QElapsedTimer timeBetweenRenders;
int sampleCount;
bool updatePending : 1;
bool grabOnly : 1;
bool rhiDeviceLost : 1;
bool rhiDoomed : 1;
};
bool ensureRhi(QQuickWindow *window, WindowData &data);
QHash<QQuickWindow *, WindowData> m_windows;
QOffscreenSurface *offscreenSurface = nullptr;
@ -191,7 +193,6 @@ public:
QSGContext *sg;
QSGRenderContext *rc;
QImage grabContent;
bool m_inPolish = false;
};
#endif
@ -447,22 +448,11 @@ bool QSGGuiThreadRenderLoop::eventFilter(QObject *watched, QEvent *event)
return QObject::eventFilter(watched, event);
}
void QSGGuiThreadRenderLoop::renderWindow(QQuickWindow *window)
bool QSGGuiThreadRenderLoop::ensureRhi(QQuickWindow *window, WindowData &data)
{
if (!m_windows.contains(window))
return;
WindowData &data = const_cast<WindowData &>(m_windows[window]);
bool alsoSwap = data.updatePending;
data.updatePending = false;
QQuickWindowPrivate *cd = QQuickWindowPrivate::get(window);
if (!cd->isRenderable())
return;
bool current = false;
QSGRhiSupport *rhiSupport = QSGRhiSupport::instance();
int rhiSampleCount = 1;
bool current = false;
if (!rhi) {
// This block below handles both the initial QRhi initialization and
@ -470,7 +460,7 @@ void QSGGuiThreadRenderLoop::renderWindow(QQuickWindow *window)
// (reset) situation.
if (data.rhiDoomed) // no repeated attempts if the initial attempt failed
return;
return false;
if (!offscreenSurface)
offscreenSurface = rhiSupport->maybeCreateOffscreenSurface(window);
@ -488,13 +478,13 @@ void QSGGuiThreadRenderLoop::renderWindow(QQuickWindow *window)
// The sample count cannot vary between windows as we use the same
// rendercontext for all of them. Decide it here and now.
rhiSampleCount = rhiSupport->chooseSampleCountForWindowWithRhi(window, rhi);
data.sampleCount = rhiSupport->chooseSampleCountForWindowWithRhi(window, rhi);
cd->rhi = rhi; // set this early in case something hooked up to rc initialized() accesses it
QSGDefaultRenderContext::InitParams rcParams;
rcParams.rhi = rhi;
rcParams.sampleCount = rhiSampleCount;
rcParams.sampleCount = data.sampleCount;
rcParams.initialSurfacePixelSize = window->size() * window->effectiveDevicePixelRatio();
rcParams.maybeSurface = window;
cd->context->initialize(&rcParams);
@ -546,14 +536,14 @@ void QSGGuiThreadRenderLoop::renderWindow(QQuickWindow *window)
if (depthBufferEnabled) {
cd->depthStencilForSwapchain = rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil,
QSize(),
rhiSampleCount,
data.sampleCount,
QRhiRenderBuffer::UsedWithSwapChainOnly);
cd->swapchain->setDepthStencil(cd->depthStencilForSwapchain);
}
cd->swapchain->setWindow(window);
qCDebug(QSG_LOG_INFO, "MSAA sample count for the swapchain is %d. Alpha channel requested = %s",
rhiSampleCount, alpha ? "yes" : "no");
cd->swapchain->setSampleCount(rhiSampleCount);
data.sampleCount, alpha ? "yes" : "no");
cd->swapchain->setSampleCount(data.sampleCount);
cd->swapchain->setFlags(flags);
cd->rpDescForSwapchain = cd->swapchain->newCompatibleRenderPassDescriptor();
cd->swapchain->setRenderPassDescriptor(cd->rpDescForSwapchain);
@ -561,6 +551,25 @@ void QSGGuiThreadRenderLoop::renderWindow(QQuickWindow *window)
window->installEventFilter(this);
}
return current;
}
void QSGGuiThreadRenderLoop::renderWindow(QQuickWindow *window)
{
if (!m_windows.contains(window))
return;
WindowData &data = const_cast<WindowData &>(m_windows[window]);
bool alsoSwap = data.updatePending;
data.updatePending = false;
QQuickWindowPrivate *cd = QQuickWindowPrivate::get(window);
if (!cd->isRenderable())
return;
if (!ensureRhi(window, data))
return;
bool lastDirtyWindow = true;
auto i = m_windows.constBegin();
while (i != m_windows.constEnd()) {
@ -571,16 +580,11 @@ void QSGGuiThreadRenderLoop::renderWindow(QQuickWindow *window)
i++;
}
if (!current)
cd->flushFrameSynchronousEvents();
// Event delivery/processing triggered the window to be deleted or stop rendering.
if (!m_windows.contains(window))
return;
if (!data.grabOnly) {
cd->flushFrameSynchronousEvents();
// Event delivery/processing triggered the window to be deleted or stop rendering.
if (!m_windows.contains(window))
return;
}
QSize effectiveOutputSize; // always prefer what the surface tells us, not the QWindow
if (cd->swapchain) {
effectiveOutputSize = cd->swapchain->surfacePixelSize();
@ -682,13 +686,6 @@ void QSGGuiThreadRenderLoop::renderWindow(QQuickWindow *window)
QQuickProfiler::SceneGraphRenderLoopRender);
Q_TRACE(QSG_swap_entry);
if (data.grabOnly) {
if (cd->swapchain)
grabContent = rhiSupport->grabAndBlockInCurrentFrame(rhi, cd->swapchain->currentFrameCommandBuffer());
grabContent.setDevicePixelRatio(window->effectiveDevicePixelRatio());
data.grabOnly = false;
}
const bool needsPresent = alsoSwap && window->isVisible();
if (cd->swapchain) {
QRhi::EndFrameFlags flags;
@ -766,13 +763,25 @@ QImage QSGGuiThreadRenderLoop::grab(QQuickWindow *window)
if (!m_windows.contains(window))
return QImage();
m_windows[window].grabOnly = true;
if (!ensureRhi(window, m_windows[window]))
return QImage();
renderWindow(window);
QQuickWindowPrivate *cd = QQuickWindowPrivate::get(window);
m_inPolish = true;
cd->polishItems();
m_inPolish = false;
QImage grabbed = grabContent;
grabContent = QImage();
return grabbed;
// The assumption is that the swapchain is usable since on expose we do a
// renderWindow() so one cannot get to grab() without having done at least
// one on-screen frame.
cd->rhi->beginFrame(cd->swapchain);
cd->syncSceneGraph();
cd->renderSceneGraph(window->size());
QImage image = QSGRhiSupport::instance()->grabAndBlockInCurrentFrame(rhi, cd->swapchain->currentFrameCommandBuffer());
cd->rhi->endFrame(cd->swapchain, QRhi::SkipPresent);
image.setDevicePixelRatio(window->effectiveDevicePixelRatio());
return image;
}
void QSGGuiThreadRenderLoop::maybeUpdate(QQuickWindow *window)

View File

@ -303,8 +303,8 @@ public:
bool event(QEvent *) override;
void run() override;
void syncAndRender(QImage *grabImage = nullptr);
void sync(bool inExpose, bool inGrab);
void syncAndRender();
void sync(bool inExpose);
void requestRepaint()
{
@ -443,8 +443,17 @@ bool QSGRenderThread::event(QEvent *e)
mutex.lock();
if (ce->window) {
if (rhi) {
rhi->makeThreadLocalNativeContextCurrent();
syncAndRender(ce->image);
QQuickWindowPrivate *cd = QQuickWindowPrivate::get(ce->window);
cd->rhi->makeThreadLocalNativeContextCurrent();
// The assumption is that the swapchain is usable, because on
// expose the thread starts up and renders a frame so one cannot
// get here without having done at least one on-screen frame.
cd->rhi->beginFrame(cd->swapchain);
cd->syncSceneGraph();
sgrc->endSync();
cd->renderSceneGraph(ce->window->size());
*ce->image = QSGRhiSupport::instance()->grabAndBlockInCurrentFrame(rhi, cd->swapchain->currentFrameCommandBuffer());
cd->rhi->endFrame(cd->swapchain, QRhi::SkipPresent);
}
ce->image->setDevicePixelRatio(ce->window->effectiveDevicePixelRatio());
}
@ -559,11 +568,10 @@ void QSGRenderThread::invalidateGraphics(QQuickWindow *window, bool inDestructor
Enters the mutex lock to make sure GUI is blocking and performs
sync, then wakes GUI.
*/
void QSGRenderThread::sync(bool inExpose, bool inGrab)
void QSGRenderThread::sync(bool inExpose)
{
qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "sync()");
if (!inGrab)
mutex.lock();
mutex.lock();
Q_ASSERT_X(wm->m_lockedForSync, "QSGRenderThread::sync()", "sync triggered on bad terms as gui is not already locked...");
@ -612,7 +620,7 @@ void QSGRenderThread::sync(bool inExpose, bool inGrab)
// the frame is rendered (submitted), so in that case waking happens later
// in syncAndRender(). Otherwise, wake now and let the main thread go on
// while we render.
if (!inExpose && !inGrab) {
if (!inExpose) {
qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- sync complete, waking Gui");
waitCondition.wakeOne();
mutex.unlock();
@ -633,7 +641,7 @@ void QSGRenderThread::handleDeviceLoss()
rhi = nullptr;
}
void QSGRenderThread::syncAndRender(QImage *grabImage)
void QSGRenderThread::syncAndRender()
{
const bool profileFrames = QSG_LOG_TIME_RENDERLOOP().isDebugEnabled();
QElapsedTimer threadTimer;
@ -660,11 +668,9 @@ void QSGRenderThread::syncAndRender(QImage *grabImage)
syncResultedInChanges = false;
QQuickWindowPrivate *d = QQuickWindowPrivate::get(window);
const bool syncRequested = (pendingUpdate & SyncRequest) || grabImage;
const bool syncRequested = (pendingUpdate & SyncRequest);
const bool exposeRequested = (pendingUpdate & ExposeRequest) == ExposeRequest;
const bool grabRequested = grabImage != nullptr;
if (!grabRequested)
pendingUpdate = 0;
pendingUpdate = 0;
QQuickWindowPrivate *cd = QQuickWindowPrivate::get(window);
// Begin the frame before syncing -> sync is where we may invoke
@ -715,7 +721,7 @@ void QSGRenderThread::syncAndRender(QImage *grabImage)
QCoreApplication::postEvent(window, new QEvent(QEvent::Type(QQuickWindowPrivate::FullUpdateRequest)));
// Before returning we need to ensure the same wake up logic that
// would have happened if beginFrame() had suceeded.
if (syncRequested && !grabRequested) {
if (syncRequested) {
// Lock like sync() would do. Note that exposeRequested always includes syncRequested.
qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- bailing out due to failed beginFrame, wake Gui");
mutex.lock();
@ -730,7 +736,7 @@ void QSGRenderThread::syncAndRender(QImage *grabImage)
if (syncRequested) {
qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- updatePending, doing sync");
sync(exposeRequested, grabRequested);
sync(exposeRequested);
}
#ifndef QSG_NO_RENDER_TIMING
if (profileFrames)
@ -759,11 +765,10 @@ void QSGRenderThread::syncAndRender(QImage *grabImage)
// updatePaintNode() invoked from sync(). We are about to do a repaint
// right now, so reset the flag. (bits other than RepaintRequest cannot
// be set in pendingUpdate at this point)
if (!grabRequested)
pendingUpdate = 0;
pendingUpdate = 0;
// Advance render thread animations (from the QQuickAnimator subclasses).
if (animatorDriver->isRunning() && !grabRequested) {
if (animatorDriver->isRunning()) {
d->animationController->lock();
animatorDriver->advance();
d->animationController->unlock();
@ -787,20 +792,8 @@ void QSGRenderThread::syncAndRender(QImage *grabImage)
QQuickProfiler::SceneGraphRenderLoopRender);
Q_TRACE(QSG_swap_entry);
// With the rhi grabs can only be done by adding a readback and then
// blocking in a real frame. The legacy GL path never gets here with
// grabs as it rather invokes sync/render directly without going
// through syncAndRender().
if (grabRequested) {
Q_ASSERT(rhi && cd->swapchain);
*grabImage = QSGRhiSupport::instance()->grabAndBlockInCurrentFrame(rhi, cd->swapchain->currentFrameCommandBuffer());
}
if (cd->swapchain) {
QRhi::EndFrameFlags flags;
if (grabRequested)
flags |= QRhi::SkipPresent;
QRhi::FrameOpResult frameResult = rhi->endFrame(cd->swapchain, flags);
QRhi::FrameOpResult frameResult = rhi->endFrame(cd->swapchain);
if (frameResult != QRhi::FrameOpSuccess) {
if (frameResult == QRhi::FrameOpDeviceLost)
handleDeviceLoss();
@ -810,9 +803,7 @@ void QSGRenderThread::syncAndRender(QImage *grabImage)
QCoreApplication::postEvent(window, new QEvent(QEvent::Type(QQuickWindowPrivate::FullUpdateRequest)));
}
}
if (!grabRequested)
d->fireFrameSwapped();
d->fireFrameSwapped();
} else {
Q_TRACE(QSG_render_exit);
Q_QUICK_SG_PROFILE_SKIP(QQuickProfiler::SceneGraphRenderLoopFrame,
@ -1673,7 +1664,9 @@ QImage QSGThreadedRenderLoop::grab(QQuickWindow *window)
qCDebug(QSG_LOG_RENDERLOOP, "- polishing items");
QQuickWindowPrivate *d = QQuickWindowPrivate::get(window);
m_inPolish = true;
d->polishItems();
m_inPolish = false;
QImage result;
w->thread->mutex.lock();

View File

@ -1528,6 +1528,34 @@ void tst_qquickwindow::grab()
} else {
QCOMPARE((uint) content.convertToFormat(QImage::Format_RGB32).pixel(0, 0), (uint) 0xffff0000);
}
if (visible) {
// Now hide() and regrab to exercise the case of a window that is
// renderable and then becomes non-exposed (non-renderable). This is not
// the same as the visible==false case which starts with a window that
// never was renderable before grabbing.
window.hide();
QImage content = window.grabWindow();
QCOMPARE(content.width(), int(window.width() * window.devicePixelRatio()));
QCOMPARE(content.height(), int(window.height() * window.devicePixelRatio()));
if (alpha) {
QCOMPARE((uint) content.convertToFormat(QImage::Format_ARGB32_Premultiplied).pixel(0, 0), (uint) 0x00000000);
} else {
QCOMPARE((uint) content.convertToFormat(QImage::Format_RGB32).pixel(0, 0), (uint) 0xffff0000);
}
// now make it visible and exercise the main grab path again to see if it still works
window.show();
QVERIFY(QTest::qWaitForWindowExposed(&window));
content = window.grabWindow();
QCOMPARE(content.width(), int(window.width() * window.devicePixelRatio()));
QCOMPARE(content.height(), int(window.height() * window.devicePixelRatio()));
if (alpha) {
QCOMPARE((uint) content.convertToFormat(QImage::Format_ARGB32_Premultiplied).pixel(0, 0), (uint) 0x00000000);
} else {
QCOMPARE((uint) content.convertToFormat(QImage::Format_RGB32).pixel(0, 0), (uint) 0xffff0000);
}
}
}
void tst_qquickwindow::multipleWindows()