1// Copyright (C) 2016 The Qt Company Ltd.
2// Copyright (C) 2016 Jolla Ltd, author: <gunnar.sletta@jollamobile.com>
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4
5
6#include <QtCore/QMutex>
7#include <QtCore/QWaitCondition>
8#include <QtCore/QAnimationDriver>
9#include <QtCore/QQueue>
10#include <QtCore/QTime>
11
12#include <QtGui/QGuiApplication>
13#include <QtGui/QScreen>
14#include <QtGui/QOffscreenSurface>
15
16#include <qpa/qwindowsysteminterface.h>
17
18#include <QtQuick/QQuickWindow>
19#include <private/qquickwindow_p.h>
20#include <private/qquickitem_p.h>
21
22#include <QtQuick/private/qsgrenderer_p.h>
23
24#include "qsgthreadedrenderloop_p.h"
25#include "qsgrhisupport_p.h"
26#include <private/qquickanimatorcontroller_p.h>
27
28#include <private/qquickprofiler_p.h>
29#include <private/qqmldebugserviceinterfaces_p.h>
30#include <private/qqmldebugconnector_p.h>
31
32#include <private/qsgrhishadereffectnode_p.h>
33#include <private/qsgdefaultrendercontext_p.h>
34
35#include <qtquick_tracepoints_p.h>
36
37#ifdef Q_OS_DARWIN
38#include <QtCore/private/qcore_mac_p.h>
39#endif
40
41/*
42 Overall design:
43
44 There are two classes here. QSGThreadedRenderLoop and
45 QSGRenderThread. All communication between the two is based on
46 event passing and we have a number of custom events.
47
48 In this implementation, the render thread is never blocked and the
49 GUI thread will initiate a polishAndSync which will block and wait
50 for the render thread to pick it up and release the block only
51 after the render thread is done syncing. The reason for this
52 is:
53
54 1. Clear blocking paradigm. We only have one real "block" point
55 (polishAndSync()) and all blocking is initiated by GUI and picked
56 up by Render at specific times based on events. This makes the
57 execution deterministic.
58
59 2. Render does not have to interact with GUI. This is done so that
60 the render thread can run its own animation system which stays
61 alive even when the GUI thread is blocked doing i/o, object
62 instantiation, QPainter-painting or any other non-trivial task.
63
64 ---
65
66 There is one thread per window and one QRhi instance per thread.
67
68 ---
69
70 The render thread has affinity to the GUI thread until a window
71 is shown. From that moment and until the window is destroyed, it
72 will have affinity to the render thread. (moved back at the end
73 of run for cleanup).
74
75 ---
76
77 The render loop is active while any window is exposed. All visible
78 windows are tracked, but only exposed windows are actually added to
79 the render thread and rendered. That means that if all windows are
80 obscured, we might end up cleaning up the SG and GL context (if all
81 windows have disabled persistency). Especially for multiprocess,
82 low-end systems, this should be quite important.
83
84 */
85
86QT_BEGIN_NAMESPACE
87
88Q_TRACE_POINT(qtquick, QSG_polishAndSync_entry)
89Q_TRACE_POINT(qtquick, QSG_polishAndSync_exit)
90Q_TRACE_POINT(qtquick, QSG_wait_entry)
91Q_TRACE_POINT(qtquick, QSG_wait_exit)
92Q_TRACE_POINT(qtquick, QSG_syncAndRender_entry)
93Q_TRACE_POINT(qtquick, QSG_syncAndRender_exit)
94Q_TRACE_POINT(qtquick, QSG_animations_entry)
95Q_TRACE_POINT(qtquick, QSG_animations_exit)
96
97#define QSG_RT_PAD " (RT) %s"
98
99extern Q_GUI_EXPORT QImage qt_gl_read_framebuffer(const QSize &size, bool alpha_format, bool include_alpha);
100
101// RL: Render Loop
102// RT: Render Thread
103
104
105QSGThreadedRenderLoop::Window *QSGThreadedRenderLoop::windowFor(QQuickWindow *window)
106{
107 for (const auto &t : std::as_const(t&: m_windows)) {
108 if (t.window == window)
109 return const_cast<Window *>(&t);
110 }
111 return nullptr;
112}
113
114class WMWindowEvent : public QEvent
115{
116public:
117 WMWindowEvent(QQuickWindow *c, QEvent::Type type) : QEvent(type), window(c) { }
118 QQuickWindow *window;
119};
120
121class WMTryReleaseEvent : public WMWindowEvent
122{
123public:
124 WMTryReleaseEvent(QQuickWindow *win, bool destroy, bool needsFallbackSurface)
125 : WMWindowEvent(win, QEvent::Type(WM_TryRelease))
126 , inDestructor(destroy)
127 , needsFallback(needsFallbackSurface)
128 {}
129
130 bool inDestructor;
131 bool needsFallback;
132};
133
134class WMSyncEvent : public WMWindowEvent
135{
136public:
137 WMSyncEvent(QQuickWindow *c, bool inExpose, bool force, const QRhiSwapChainProxyData &scProxyData)
138 : WMWindowEvent(c, QEvent::Type(WM_RequestSync))
139 , size(c->size())
140 , dpr(float(c->effectiveDevicePixelRatio()))
141 , syncInExpose(inExpose)
142 , forceRenderPass(force)
143 , scProxyData(scProxyData)
144 {}
145 QSize size;
146 float dpr;
147 bool syncInExpose;
148 bool forceRenderPass;
149 QRhiSwapChainProxyData scProxyData;
150};
151
152
153class WMGrabEvent : public WMWindowEvent
154{
155public:
156 WMGrabEvent(QQuickWindow *c, QImage *result) :
157 WMWindowEvent(c, QEvent::Type(WM_Grab)), image(result) {}
158 QImage *image;
159};
160
161class WMJobEvent : public WMWindowEvent
162{
163public:
164 WMJobEvent(QQuickWindow *c, QRunnable *postedJob)
165 : WMWindowEvent(c, QEvent::Type(WM_PostJob)), job(postedJob) {}
166 ~WMJobEvent() { delete job; }
167 QRunnable *job;
168};
169
170class WMReleaseSwapchainEvent : public WMWindowEvent
171{
172public:
173 WMReleaseSwapchainEvent(QQuickWindow *c) :
174 WMWindowEvent(c, QEvent::Type(WM_ReleaseSwapchain)) { }
175};
176
177class QSGRenderThreadEventQueue : public QQueue<QEvent *>
178{
179public:
180 QSGRenderThreadEventQueue()
181 : waiting(false)
182 {
183 }
184
185 void addEvent(QEvent *e) {
186 mutex.lock();
187 enqueue(t: e);
188 if (waiting)
189 condition.wakeOne();
190 mutex.unlock();
191 }
192
193 QEvent *takeEvent(bool wait) {
194 mutex.lock();
195 if (size() == 0 && wait) {
196 waiting = true;
197 condition.wait(lockedMutex: &mutex);
198 waiting = false;
199 }
200 QEvent *e = dequeue();
201 mutex.unlock();
202 return e;
203 }
204
205 bool hasMoreEvents() {
206 mutex.lock();
207 bool has = !isEmpty();
208 mutex.unlock();
209 return has;
210 }
211
212private:
213 QMutex mutex;
214 QWaitCondition condition;
215 bool waiting;
216};
217
218
219class QSGRenderThread : public QThread
220{
221 Q_OBJECT
222public:
223 QSGRenderThread(QSGThreadedRenderLoop *w, QSGRenderContext *renderContext)
224 : wm(w)
225 , rhi(nullptr)
226 , ownRhi(true)
227 , offscreenSurface(nullptr)
228 , animatorDriver(nullptr)
229 , pendingUpdate(0)
230 , sleeping(false)
231 , syncResultedInChanges(false)
232 , active(false)
233 , window(nullptr)
234 , stopEventProcessing(false)
235 {
236 sgrc = static_cast<QSGDefaultRenderContext *>(renderContext);
237#if (defined(Q_OS_QNX) && defined(Q_PROCESSOR_X86)) || defined(Q_OS_INTEGRITY)
238 // The SDP 6.6.0 x86 MESA driver requires a larger stack than the default.
239 setStackSize(1024 * 1024);
240#endif
241 }
242
243 ~QSGRenderThread()
244 {
245 delete sgrc;
246 delete offscreenSurface;
247 }
248
249 void invalidateGraphics(QQuickWindow *window, bool inDestructor);
250
251 bool event(QEvent *) override;
252 void run() override;
253
254 void syncAndRender();
255 void sync(bool inExpose);
256
257 void requestRepaint()
258 {
259 if (sleeping)
260 stopEventProcessing = true;
261 if (window)
262 pendingUpdate |= RepaintRequest;
263 }
264
265 void processEventsAndWaitForMore();
266 void processEvents();
267 void postEvent(QEvent *e);
268
269public slots:
270 void sceneGraphChanged() {
271 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "sceneGraphChanged");
272 syncResultedInChanges = true;
273 }
274
275public:
276 enum {
277 SyncRequest = 0x01,
278 RepaintRequest = 0x02,
279 ExposeRequest = 0x04 | RepaintRequest | SyncRequest
280 };
281
282 void ensureRhi();
283 void handleDeviceLoss();
284
285 QSGThreadedRenderLoop *wm;
286 QRhi *rhi;
287 bool ownRhi;
288 QSGDefaultRenderContext *sgrc;
289 QOffscreenSurface *offscreenSurface;
290
291 QAnimationDriver *animatorDriver;
292
293 uint pendingUpdate;
294 bool sleeping;
295 bool syncResultedInChanges;
296
297 volatile bool active;
298
299 QMutex mutex;
300 QWaitCondition waitCondition;
301
302 QElapsedTimer m_threadTimeBetweenRenders;
303
304 QQuickWindow *window; // Will be 0 when window is not exposed
305 QSize windowSize;
306 float dpr = 1;
307 QRhiSwapChainProxyData scProxyData;
308 int rhiSampleCount = 1;
309 bool rhiDeviceLost = false;
310 bool rhiDoomed = false;
311 bool guiNotifiedAboutRhiFailure = false;
312
313 // Local event queue stuff...
314 bool stopEventProcessing;
315 QSGRenderThreadEventQueue eventQueue;
316};
317
318bool QSGRenderThread::event(QEvent *e)
319{
320 switch ((int) e->type()) {
321
322 case WM_Obscure: {
323 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "WM_Obscure");
324
325 Q_ASSERT(!window || window == static_cast<WMWindowEvent *>(e)->window);
326
327 mutex.lock();
328 if (window) {
329 QQuickWindowPrivate::get(c: window)->fireAboutToStop();
330 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- window removed");
331 window = nullptr;
332 }
333 waitCondition.wakeOne();
334 mutex.unlock();
335
336 return true; }
337
338 case WM_RequestSync: {
339 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "WM_RequestSync");
340 WMSyncEvent *se = static_cast<WMSyncEvent *>(e);
341 if (sleeping)
342 stopEventProcessing = true;
343 window = se->window;
344 windowSize = se->size;
345 dpr = se->dpr;
346 scProxyData = se->scProxyData;
347
348 pendingUpdate |= SyncRequest;
349 if (se->syncInExpose) {
350 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- triggered from expose");
351 pendingUpdate |= ExposeRequest;
352 }
353 if (se->forceRenderPass) {
354 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- repaint regardless");
355 pendingUpdate |= RepaintRequest;
356 }
357 return true; }
358
359 case WM_TryRelease: {
360 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "WM_TryRelease");
361 mutex.lock();
362 wm->m_lockedForSync = true;
363 WMTryReleaseEvent *wme = static_cast<WMTryReleaseEvent *>(e);
364 if (!window || wme->inDestructor) {
365 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- setting exit flag and invalidating");
366 invalidateGraphics(window: wme->window, inDestructor: wme->inDestructor);
367 active = rhi != nullptr;
368 Q_ASSERT_X(!wme->inDestructor || !active, "QSGRenderThread::invalidateGraphics()", "Thread's active state is not set to false when shutting down");
369 if (sleeping)
370 stopEventProcessing = true;
371 } else {
372 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- not releasing because window is still active");
373 if (window) {
374 QQuickWindowPrivate *d = QQuickWindowPrivate::get(c: window);
375 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- requesting external renderers such as Quick 3D to release cached resources");
376 emit d->context->releaseCachedResourcesRequested();
377 if (d->renderer) {
378 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- requesting renderer to release cached resources");
379 d->renderer->releaseCachedResources();
380 }
381#if QT_CONFIG(quick_shadereffect)
382 QSGRhiShaderEffectNode::garbageCollectMaterialTypeCache(materialTypeCacheKey: window);
383#endif
384 }
385 }
386 waitCondition.wakeOne();
387 wm->m_lockedForSync = false;
388 mutex.unlock();
389 return true;
390 }
391
392 case WM_Grab: {
393 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "WM_Grab");
394 WMGrabEvent *ce = static_cast<WMGrabEvent *>(e);
395 Q_ASSERT(ce->window);
396 Q_ASSERT(ce->window == window || !window);
397 mutex.lock();
398 if (ce->window) {
399 if (rhi) {
400 QQuickWindowPrivate *cd = QQuickWindowPrivate::get(c: ce->window);
401 // The assumption is that the swapchain is usable, because on
402 // expose the thread starts up and renders a frame so one cannot
403 // get here without having done at least one on-screen frame.
404 cd->rhi->beginFrame(swapChain: cd->swapchain);
405 cd->rhi->makeThreadLocalNativeContextCurrent(); // for custom GL rendering before/during/after sync
406 cd->syncSceneGraph();
407 sgrc->endSync();
408 cd->renderSceneGraph();
409 *ce->image = QSGRhiSupport::instance()->grabAndBlockInCurrentFrame(rhi, cb: cd->swapchain->currentFrameCommandBuffer());
410 cd->rhi->endFrame(swapChain: cd->swapchain, flags: QRhi::SkipPresent);
411 }
412 ce->image->setDevicePixelRatio(ce->window->effectiveDevicePixelRatio());
413 }
414 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- waking gui to handle result");
415 waitCondition.wakeOne();
416 mutex.unlock();
417 return true;
418 }
419
420 case WM_PostJob: {
421 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "WM_PostJob");
422 WMJobEvent *ce = static_cast<WMJobEvent *>(e);
423 Q_ASSERT(ce->window == window);
424 if (window) {
425 if (rhi)
426 rhi->makeThreadLocalNativeContextCurrent();
427 ce->job->run();
428 delete ce->job;
429 ce->job = nullptr;
430 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- job done");
431 }
432 return true;
433 }
434
435 case WM_ReleaseSwapchain: {
436 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "WM_ReleaseSwapchain");
437 WMReleaseSwapchainEvent *ce = static_cast<WMReleaseSwapchainEvent *>(e);
438 // forget about 'window' here that may be null when already unexposed
439 Q_ASSERT(ce->window);
440 mutex.lock();
441 if (ce->window) {
442 wm->releaseSwapchain(window: ce->window);
443 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- swapchain released");
444 }
445 waitCondition.wakeOne();
446 mutex.unlock();
447 return true;
448 }
449
450 default:
451 break;
452 }
453 return QThread::event(event: e);
454}
455
456void QSGRenderThread::invalidateGraphics(QQuickWindow *window, bool inDestructor)
457{
458 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "invalidateGraphics()");
459
460 if (!rhi)
461 return;
462
463 if (!window) {
464 qCWarning(QSG_LOG_RENDERLOOP, "QSGThreadedRenderLoop:QSGRenderThread: no window to make current...");
465 return;
466 }
467
468 bool wipeSG = inDestructor || !window->isPersistentSceneGraph();
469 bool wipeGraphics = inDestructor || (wipeSG && !window->isPersistentGraphics());
470
471 rhi->makeThreadLocalNativeContextCurrent();
472
473 QQuickWindowPrivate *dd = QQuickWindowPrivate::get(c: window);
474
475 // The canvas nodes must be cleaned up regardless if we are in the destructor..
476 if (wipeSG) {
477 dd->cleanupNodesOnShutdown();
478#if QT_CONFIG(quick_shadereffect)
479 QSGRhiShaderEffectNode::resetMaterialTypeCache(materialTypeCacheKey: window);
480#endif
481 } else {
482 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- persistent SG, avoiding cleanup");
483 return;
484 }
485
486 sgrc->invalidate();
487 QCoreApplication::processEvents();
488 QCoreApplication::sendPostedEvents(receiver: nullptr, event_type: QEvent::DeferredDelete);
489 if (inDestructor)
490 dd->animationController.reset();
491
492 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- invalidating scene graph");
493
494 if (wipeGraphics) {
495 if (dd->swapchain) {
496 if (window->handle()) {
497 // We get here when exiting via QCoreApplication::quit() instead of
498 // through QWindow::close().
499 wm->releaseSwapchain(window);
500 } else {
501 qWarning(msg: "QSGThreadedRenderLoop cleanup with QQuickWindow %p swapchain %p still alive, this should not happen.",
502 window, dd->swapchain);
503 }
504 }
505 if (ownRhi)
506 QSGRhiSupport::instance()->destroyRhi(rhi, config: dd->graphicsConfig);
507 rhi = nullptr;
508 dd->rhi = nullptr;
509 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- QRhi destroyed");
510 } else {
511 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- persistent GL, avoiding cleanup");
512 }
513}
514
515/*
516 Enters the mutex lock to make sure GUI is blocking and performs
517 sync, then wakes GUI.
518 */
519void QSGRenderThread::sync(bool inExpose)
520{
521 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "sync()");
522 mutex.lock();
523
524 Q_ASSERT_X(wm->m_lockedForSync, "QSGRenderThread::sync()", "sync triggered on bad terms as gui is not already locked...");
525
526 bool canSync = true;
527 if (rhi) {
528 if (windowSize.width() > 0 && windowSize.height() > 0) {
529 // With the rhi making the (OpenGL) context current serves only one
530 // purpose: to enable external OpenGL rendering connected to one of
531 // the QQuickWindow signals (beforeSynchronizing, beforeRendering,
532 // etc.) to function like it did on the direct OpenGL path. For our
533 // own rendering this call would not be necessary.
534 rhi->makeThreadLocalNativeContextCurrent();
535 } else {
536 // Zero size windows do not initialize a swapchain and
537 // rendercontext. So no sync or render can be done then.
538 canSync = false;
539 }
540 } else {
541 canSync = false;
542 }
543 if (canSync) {
544 QQuickWindowPrivate *d = QQuickWindowPrivate::get(c: window);
545 bool hadRenderer = d->renderer != nullptr;
546 // If the scene graph was touched since the last sync() make sure it sends the
547 // changed signal.
548 if (d->renderer)
549 d->renderer->clearChangedFlag();
550 d->syncSceneGraph();
551 sgrc->endSync();
552 if (!hadRenderer && d->renderer) {
553 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- renderer was created");
554 syncResultedInChanges = true;
555 connect(sender: d->renderer, SIGNAL(sceneGraphChanged()), receiver: this, SLOT(sceneGraphChanged()), Qt::DirectConnection);
556 }
557
558 // Process deferred deletes now, directly after the sync as
559 // deleteLater on the GUI must now also have resulted in SG changes
560 // and the delete is a safe operation.
561 QCoreApplication::sendPostedEvents(receiver: nullptr, event_type: QEvent::DeferredDelete);
562 } else {
563 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- window has bad size, sync aborted");
564 }
565
566 // Two special cases: For grabs we do not care about blocking the gui
567 // (main) thread. When this is from an expose, we will keep locked until
568 // the frame is rendered (submitted), so in that case waking happens later
569 // in syncAndRender(). Otherwise, wake now and let the main thread go on
570 // while we render.
571 if (!inExpose) {
572 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- sync complete, waking Gui");
573 waitCondition.wakeOne();
574 mutex.unlock();
575 }
576}
577
578void QSGRenderThread::handleDeviceLoss()
579{
580 if (!rhi || !rhi->isDeviceLost())
581 return;
582
583 qWarning(msg: "Graphics device lost, cleaning up scenegraph and releasing RHI");
584 QQuickWindowPrivate *wd = QQuickWindowPrivate::get(c: window);
585 wd->cleanupNodesOnShutdown();
586 sgrc->invalidate();
587 wm->releaseSwapchain(window);
588 rhiDeviceLost = true;
589 if (ownRhi)
590 QSGRhiSupport::instance()->destroyRhi(rhi, config: {});
591 rhi = nullptr;
592}
593
594void QSGRenderThread::syncAndRender()
595{
596 const bool profileFrames = QSG_LOG_TIME_RENDERLOOP().isDebugEnabled();
597 QElapsedTimer threadTimer;
598 qint64 syncTime = 0, renderTime = 0;
599 if (profileFrames)
600 threadTimer.start();
601 Q_TRACE_SCOPE(QSG_syncAndRender);
602 Q_QUICK_SG_PROFILE_START(QQuickProfiler::SceneGraphRenderLoopFrame);
603 Q_TRACE(QSG_sync_entry);
604
605 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "syncAndRender()");
606
607 if (profileFrames) {
608 const qint64 elapsedSinceLastMs = m_threadTimeBetweenRenders.restart();
609 qCDebug(QSG_LOG_TIME_RENDERLOOP, "[window %p][render thread %p] syncAndRender: start, elapsed since last call: %d ms",
610 window,
611 QThread::currentThread(),
612 int(elapsedSinceLastMs));
613 }
614
615 syncResultedInChanges = false;
616 QQuickWindowPrivate *d = QQuickWindowPrivate::get(c: window);
617
618 const bool syncRequested = (pendingUpdate & SyncRequest);
619 const bool exposeRequested = (pendingUpdate & ExposeRequest) == ExposeRequest;
620 pendingUpdate = 0;
621
622 QQuickWindowPrivate *cd = QQuickWindowPrivate::get(c: window);
623 // Begin the frame before syncing -> sync is where we may invoke
624 // updatePaintNode() on the items and they may want to do resource updates.
625 // Also relevant for applications that connect to the before/afterSynchronizing
626 // signals and want to do graphics stuff already there.
627 const bool hasValidSwapChain = (cd->swapchain && windowSize.width() > 0 && windowSize.height() > 0);
628 if (hasValidSwapChain) {
629 cd->swapchain->setProxyData(scProxyData);
630 // always prefer what the surface tells us, not the QWindow
631 const QSize effectiveOutputSize = cd->swapchain->surfacePixelSize();
632 // An update request could still be delivered right before we get an
633 // unexpose. With Vulkan on Windows for example attempting to render
634 // leads to failures at this stage since the surface size is already 0.
635 if (effectiveOutputSize.isEmpty())
636 return;
637
638 const QSize previousOutputSize = cd->swapchain->currentPixelSize();
639 if (previousOutputSize != effectiveOutputSize || cd->swapchainJustBecameRenderable) {
640 if (cd->swapchainJustBecameRenderable)
641 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "just became exposed");
642
643 cd->hasActiveSwapchain = cd->swapchain->createOrResize();
644 if (!cd->hasActiveSwapchain && rhi->isDeviceLost()) {
645 handleDeviceLoss();
646 QCoreApplication::postEvent(receiver: window, event: new QEvent(QEvent::Type(QQuickWindowPrivate::FullUpdateRequest)));
647 return;
648 }
649
650 cd->swapchainJustBecameRenderable = false;
651 cd->hasRenderableSwapchain = cd->hasActiveSwapchain;
652
653 if (!cd->hasActiveSwapchain)
654 qWarning(msg: "Failed to build or resize swapchain");
655 else
656 qCDebug(QSG_LOG_RENDERLOOP) << "rhi swapchain size" << cd->swapchain->currentPixelSize();
657 }
658
659 emit window->beforeFrameBegin();
660
661 Q_ASSERT(rhi == cd->rhi);
662 QRhi::FrameOpResult frameResult = rhi->beginFrame(swapChain: cd->swapchain);
663 if (frameResult != QRhi::FrameOpSuccess) {
664 if (frameResult == QRhi::FrameOpDeviceLost)
665 handleDeviceLoss();
666 else if (frameResult == QRhi::FrameOpError)
667 qWarning(msg: "Failed to start frame");
668 // try again later
669 if (frameResult == QRhi::FrameOpDeviceLost || frameResult == QRhi::FrameOpSwapChainOutOfDate)
670 QCoreApplication::postEvent(receiver: window, event: new QEvent(QEvent::Type(QQuickWindowPrivate::FullUpdateRequest)));
671 // Before returning we need to ensure the same wake up logic that
672 // would have happened if beginFrame() had suceeded.
673 if (syncRequested) {
674 // Lock like sync() would do. Note that exposeRequested always includes syncRequested.
675 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- bailing out due to failed beginFrame, wake Gui");
676 mutex.lock();
677 // Go ahead with waking because we will return right after this.
678 waitCondition.wakeOne();
679 mutex.unlock();
680 }
681 emit window->afterFrameEnd();
682 return;
683 }
684 }
685
686 if (syncRequested) {
687 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- updatePending, doing sync");
688 sync(inExpose: exposeRequested);
689 }
690#ifndef QSG_NO_RENDER_TIMING
691 if (profileFrames)
692 syncTime = threadTimer.nsecsElapsed();
693#endif
694 Q_TRACE(QSG_sync_exit);
695 Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame,
696 QQuickProfiler::SceneGraphRenderLoopSync);
697
698 // Qt 6 no longer aborts when !syncResultedInChanges && !RepaintRequest,
699 // meaning this function always completes and presents a frame. This is
700 // more compatible with what the basic render loop (or a custom loop with
701 // QQuickRenderControl) would do, is more accurate due to not having to do
702 // an msleep() with an inaccurate interval, and avoids misunderstandings
703 // for signals like frameSwapped(). (in Qt 5 a continuously "updating"
704 // window is continuously presenting frames with the basic loop, but not
705 // with threaded due to aborting when sync() finds there are no relevant
706 // visual changes in the scene graph; this system proved to be simply too
707 // confusing in practice)
708
709 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- rendering started");
710
711 Q_TRACE(QSG_render_entry);
712
713 // RepaintRequest may have been set in pendingUpdate in an
714 // updatePaintNode() invoked from sync(). We are about to do a repaint
715 // right now, so reset the flag. (bits other than RepaintRequest cannot
716 // be set in pendingUpdate at this point)
717 pendingUpdate = 0;
718
719 // Advance render thread animations (from the QQuickAnimator subclasses).
720 if (animatorDriver->isRunning()) {
721 d->animationController->lock();
722 animatorDriver->advance();
723 d->animationController->unlock();
724 }
725
726 // Zero size windows do not initialize a swapchain and
727 // rendercontext. So no sync or render can be done then.
728 const bool canRender = d->renderer && hasValidSwapChain;
729 double lastCompletedGpuTime = 0;
730 if (canRender) {
731 if (!syncRequested) // else this was already done in sync()
732 rhi->makeThreadLocalNativeContextCurrent();
733
734 d->renderSceneGraph();
735
736 if (profileFrames)
737 renderTime = threadTimer.nsecsElapsed();
738 Q_TRACE(QSG_render_exit);
739 Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame,
740 QQuickProfiler::SceneGraphRenderLoopRender);
741 Q_TRACE(QSG_swap_entry);
742
743 QRhi::FrameOpResult frameResult = rhi->endFrame(swapChain: cd->swapchain);
744 if (frameResult != QRhi::FrameOpSuccess) {
745 if (frameResult == QRhi::FrameOpDeviceLost)
746 handleDeviceLoss();
747 else if (frameResult == QRhi::FrameOpError)
748 qWarning(msg: "Failed to end frame");
749 if (frameResult == QRhi::FrameOpDeviceLost || frameResult == QRhi::FrameOpSwapChainOutOfDate)
750 QCoreApplication::postEvent(receiver: window, event: new QEvent(QEvent::Type(QQuickWindowPrivate::FullUpdateRequest)));
751 } else {
752 lastCompletedGpuTime = cd->swapchain->currentFrameCommandBuffer()->lastCompletedGpuTime();
753 }
754 d->fireFrameSwapped();
755 } else {
756 Q_TRACE(QSG_render_exit);
757 Q_QUICK_SG_PROFILE_SKIP(QQuickProfiler::SceneGraphRenderLoopFrame,
758 QQuickProfiler::SceneGraphRenderLoopSync, 1);
759 Q_TRACE(QSG_swap_entry);
760 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- window not ready, skipping render");
761 // Make sure a beginFrame() always gets an endFrame(). We could have
762 // started a frame but then not have a valid renderer (if there was no
763 // sync). So gracefully handle that.
764 if (cd->swapchain && rhi->isRecordingFrame())
765 rhi->endFrame(swapChain: cd->swapchain, flags: QRhi::SkipPresent);
766 }
767
768 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- rendering done");
769
770 // beforeFrameBegin - afterFrameEnd must always come in pairs; if there was
771 // no before due to 0 size then there shouldn't be an after either
772 if (hasValidSwapChain)
773 emit window->afterFrameEnd();
774
775 // Though it would be more correct to put this block directly after
776 // fireFrameSwapped in the if (current) branch above, we don't do
777 // that to avoid blocking the GUI thread in the case where it
778 // has started rendering with a bad window, causing makeCurrent to
779 // fail or if the window has a bad size.
780 if (exposeRequested) {
781 // With expose sync() did not wake gui, do it now.
782 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "- wake Gui after expose");
783 waitCondition.wakeOne();
784 mutex.unlock();
785 }
786
787 if (profileFrames) {
788 // Beware that there is no guarantee the graphics stack always
789 // blocks for a full vsync in beginFrame() or endFrame(). (because
790 // e.g. there is no guarantee that OpenGL blocks in swapBuffers(),
791 // it may block elsewhere; also strategies may change once there
792 // are multiple windows) So process the results printed here with
793 // caution and pay attention to the elapsed-since-last-call time
794 // printed at the beginning of the function too.
795 qCDebug(QSG_LOG_TIME_RENDERLOOP,
796 "[window %p][render thread %p] syncAndRender: frame rendered in %dms, sync=%d, render=%d, swap=%d",
797 window,
798 QThread::currentThread(),
799 int(threadTimer.elapsed()),
800 int((syncTime/1000000)),
801 int((renderTime - syncTime) / 1000000),
802 int((threadTimer.nsecsElapsed() - renderTime) / 1000000));
803 if (!qFuzzyIsNull(d: lastCompletedGpuTime) && cd->graphicsConfig.timestampsEnabled()) {
804 qCDebug(QSG_LOG_TIME_RENDERLOOP, "[window %p][render thread %p] syncAndRender: last retrieved GPU frame time was %.4f ms",
805 window,
806 QThread::currentThread(),
807 lastCompletedGpuTime * 1000.0);
808 }
809 }
810
811 Q_TRACE(QSG_swap_exit);
812 Q_QUICK_SG_PROFILE_END(QQuickProfiler::SceneGraphRenderLoopFrame,
813 QQuickProfiler::SceneGraphRenderLoopSwap);
814}
815
816
817
818void QSGRenderThread::postEvent(QEvent *e)
819{
820 eventQueue.addEvent(e);
821}
822
823
824
825void QSGRenderThread::processEvents()
826{
827 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "--- begin processEvents()");
828 while (eventQueue.hasMoreEvents()) {
829 QEvent *e = eventQueue.takeEvent(wait: false);
830 event(e);
831 delete e;
832 }
833 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "--- done processEvents()");
834}
835
836void QSGRenderThread::processEventsAndWaitForMore()
837{
838 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "--- begin processEventsAndWaitForMore()");
839 stopEventProcessing = false;
840 while (!stopEventProcessing) {
841 QEvent *e = eventQueue.takeEvent(wait: true);
842 event(e);
843 delete e;
844 }
845 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "--- done processEventsAndWaitForMore()");
846}
847
848void QSGRenderThread::ensureRhi()
849{
850 if (!rhi) {
851 if (rhiDoomed) // no repeated attempts if the initial attempt failed
852 return;
853 QSGRhiSupport *rhiSupport = QSGRhiSupport::instance();
854 QSGRhiSupport::RhiCreateResult rhiResult = rhiSupport->createRhi(window, offscreenSurface);
855 rhi = rhiResult.rhi;
856 ownRhi = rhiResult.own;
857 if (rhi) {
858 rhiDeviceLost = false;
859 rhiSampleCount = rhiSupport->chooseSampleCountForWindowWithRhi(window, rhi);
860 } else {
861 if (!rhiDeviceLost) {
862 rhiDoomed = true;
863 qWarning(msg: "Failed to create QRhi on the render thread; scenegraph is not functional");
864 }
865 // otherwise no error, will retry on a subsequent rendering attempt
866 return;
867 }
868 }
869 if (!sgrc->rhi() && windowSize.width() > 0 && windowSize.height() > 0) {
870 // We need to guarantee that sceneGraphInitialized is emitted
871 // with a context current, if running with OpenGL.
872 rhi->makeThreadLocalNativeContextCurrent();
873 QSGDefaultRenderContext::InitParams rcParams;
874 rcParams.rhi = rhi;
875 rcParams.sampleCount = rhiSampleCount;
876 rcParams.initialSurfacePixelSize = windowSize * qreal(dpr);
877 rcParams.maybeSurface = window;
878 sgrc->initialize(params: &rcParams);
879 }
880 QQuickWindowPrivate *cd = QQuickWindowPrivate::get(c: window);
881 if (rhi && !cd->swapchain) {
882 cd->rhi = rhi;
883 QRhiSwapChain::Flags flags = QRhiSwapChain::UsedAsTransferSource; // may be used in a grab
884 const QSurfaceFormat requestedFormat = window->requestedFormat();
885
886 // QQ is always premul alpha. Decide based on alphaBufferSize in
887 // requestedFormat(). (the platform plugin can override format() but
888 // what matters here is what the application wanted, hence using the
889 // requested one)
890 const bool alpha = requestedFormat.alphaBufferSize() > 0;
891 if (alpha)
892 flags |= QRhiSwapChain::SurfaceHasPreMulAlpha;
893
894 // Request NoVSync if swap interval was set to 0 (either by the app or
895 // by QSG_NO_VSYNC). What this means in practice is another question,
896 // but at least we tried.
897 if (requestedFormat.swapInterval() == 0) {
898 qCDebug(QSG_LOG_INFO, "Swap interval is 0, attempting to disable vsync when presenting.");
899 flags |= QRhiSwapChain::NoVSync;
900 }
901
902 cd->swapchain = rhi->newSwapChain();
903 static bool depthBufferEnabled = qEnvironmentVariableIsEmpty(varName: "QSG_NO_DEPTH_BUFFER");
904 if (depthBufferEnabled) {
905 cd->depthStencilForSwapchain = rhi->newRenderBuffer(type: QRhiRenderBuffer::DepthStencil,
906 pixelSize: QSize(),
907 sampleCount: rhiSampleCount,
908 flags: QRhiRenderBuffer::UsedWithSwapChainOnly);
909 cd->swapchain->setDepthStencil(cd->depthStencilForSwapchain);
910 }
911 cd->swapchain->setWindow(window);
912 cd->swapchain->setProxyData(scProxyData);
913 QSGRhiSupport::instance()->applySwapChainFormat(scWithWindowSet: cd->swapchain, window);
914 qCDebug(QSG_LOG_INFO, "MSAA sample count for the swapchain is %d. Alpha channel requested = %s.",
915 rhiSampleCount, alpha ? "yes" : "no");
916 cd->swapchain->setSampleCount(rhiSampleCount);
917 cd->swapchain->setFlags(flags);
918 cd->rpDescForSwapchain = cd->swapchain->newCompatibleRenderPassDescriptor();
919 cd->swapchain->setRenderPassDescriptor(cd->rpDescForSwapchain);
920 }
921}
922
923void QSGRenderThread::run()
924{
925 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "run()");
926 animatorDriver = sgrc->sceneGraphContext()->createAnimationDriver(parent: nullptr);
927 animatorDriver->install();
928 if (QQmlDebugConnector::service<QQmlProfilerService>())
929 QQuickProfiler::registerAnimationCallback();
930
931 m_threadTimeBetweenRenders.start();
932
933 while (active) {
934#ifdef Q_OS_DARWIN
935 QMacAutoReleasePool frameReleasePool;
936#endif
937
938 if (window) {
939 ensureRhi();
940
941 // We absolutely have to syncAndRender() here, even when QRhi
942 // failed to initialize otherwise the gui thread will be left
943 // in a blocked state. It is up to syncAndRender() to
944 // gracefully skip all graphics stuff when rhi is null.
945
946 syncAndRender();
947
948 // Now we can do something about rhi init failures. (reinit
949 // failure after device reset does not count)
950 if (rhiDoomed && !guiNotifiedAboutRhiFailure) {
951 guiNotifiedAboutRhiFailure = true;
952 QEvent *e = new QEvent(QEvent::Type(QQuickWindowPrivate::TriggerContextCreationFailure));
953 QCoreApplication::postEvent(receiver: window, event: e);
954 }
955 }
956
957 processEvents();
958 QCoreApplication::processEvents();
959
960 if (active && (pendingUpdate == 0 || !window)) {
961 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "done drawing, sleep...");
962 sleeping = true;
963 processEventsAndWaitForMore();
964 sleeping = false;
965 }
966 }
967
968 Q_ASSERT_X(!rhi, "QSGRenderThread::run()", "The graphics context should be cleaned up before exiting the render thread...");
969
970 qCDebug(QSG_LOG_RENDERLOOP, QSG_RT_PAD, "run() completed");
971
972 delete animatorDriver;
973 animatorDriver = nullptr;
974
975 sgrc->moveToThread(thread: wm->thread());
976 moveToThread(thread: wm->thread());
977}
978
979QSGThreadedRenderLoop::QSGThreadedRenderLoop()
980 : sg(QSGContext::createDefaultContext())
981 , m_animation_timer(0)
982{
983 m_animation_driver = sg->createAnimationDriver(parent: this);
984
985 connect(sender: m_animation_driver, SIGNAL(started()), receiver: this, SLOT(animationStarted()));
986 connect(sender: m_animation_driver, SIGNAL(stopped()), receiver: this, SLOT(animationStopped()));
987
988 m_animation_driver->install();
989}
990
991QSGThreadedRenderLoop::~QSGThreadedRenderLoop()
992{
993 qDeleteAll(c: pendingRenderContexts);
994 delete sg;
995}
996
997QSGRenderContext *QSGThreadedRenderLoop::createRenderContext(QSGContext *sg) const
998{
999 auto context = sg->createRenderContext();
1000 pendingRenderContexts.insert(value: context);
1001 return context;
1002}
1003
1004void QSGThreadedRenderLoop::postUpdateRequest(Window *w)
1005{
1006 w->window->requestUpdate();
1007}
1008
1009QAnimationDriver *QSGThreadedRenderLoop::animationDriver() const
1010{
1011 return m_animation_driver;
1012}
1013
1014QSGContext *QSGThreadedRenderLoop::sceneGraphContext() const
1015{
1016 return sg;
1017}
1018
1019bool QSGThreadedRenderLoop::anyoneShowing() const
1020{
1021 for (int i=0; i<m_windows.size(); ++i) {
1022 QQuickWindow *c = m_windows.at(i).window;
1023 if (c->isVisible() && c->isExposed())
1024 return true;
1025 }
1026 return false;
1027}
1028
1029bool QSGThreadedRenderLoop::interleaveIncubation() const
1030{
1031 return m_animation_driver->isRunning() && anyoneShowing();
1032}
1033
1034void QSGThreadedRenderLoop::animationStarted()
1035{
1036 qCDebug(QSG_LOG_RENDERLOOP, "- animationStarted()");
1037 startOrStopAnimationTimer();
1038
1039 for (int i=0; i<m_windows.size(); ++i)
1040 postUpdateRequest(w: const_cast<Window *>(&m_windows.at(i)));
1041}
1042
1043void QSGThreadedRenderLoop::animationStopped()
1044{
1045 qCDebug(QSG_LOG_RENDERLOOP, "- animationStopped()");
1046 startOrStopAnimationTimer();
1047}
1048
1049
1050void QSGThreadedRenderLoop::startOrStopAnimationTimer()
1051{
1052 if (!sg->isVSyncDependent(driver: m_animation_driver))
1053 return;
1054
1055 int exposedWindows = 0;
1056 int unthrottledWindows = 0;
1057 int badVSync = 0;
1058 const Window *theOne = nullptr;
1059 for (int i=0; i<m_windows.size(); ++i) {
1060 const Window &w = m_windows.at(i);
1061 if (w.window->isVisible() && w.window->isExposed()) {
1062 ++exposedWindows;
1063 theOne = &w;
1064 if (w.actualWindowFormat.swapInterval() == 0)
1065 ++unthrottledWindows;
1066 if (w.badVSync)
1067 ++badVSync;
1068 }
1069 }
1070
1071 // Best case: with 1 exposed windows we can advance regular animations in
1072 // polishAndSync() and rely on being throttled to vsync. (no normal system
1073 // timer needed)
1074 //
1075 // Special case: with no windows exposed (e.g. on Windows: all of them are
1076 // minimized) run a normal system timer to make non-visual animation
1077 // functional still.
1078 //
1079 // Not so ideal case: with more than one window exposed we have to use the
1080 // same path as the no-windows case since polishAndSync() is now called
1081 // potentially for multiple windows over time so it cannot take care of
1082 // advancing the animation driver anymore.
1083 //
1084 // On top, another case: a window with vsync disabled should disable all the
1085 // good stuff and go with the system timer.
1086 //
1087 // Similarly, if there is at least one window where we determined that
1088 // vsync based blocking is not working as expected, that should make us
1089 // choose the timer based way.
1090
1091 const bool canUseVSyncBasedAnimation = exposedWindows == 1 && unthrottledWindows == 0 && badVSync == 0;
1092
1093 if (m_animation_timer != 0 && (canUseVSyncBasedAnimation || !m_animation_driver->isRunning())) {
1094 qCDebug(QSG_LOG_RENDERLOOP, "*** Stopping system (not vsync-based) animation timer (exposedWindows=%d unthrottledWindows=%d badVSync=%d)",
1095 exposedWindows, unthrottledWindows, badVSync);
1096 killTimer(id: m_animation_timer);
1097 m_animation_timer = 0;
1098 // If animations are running, make sure we keep on animating
1099 if (m_animation_driver->isRunning())
1100 postUpdateRequest(w: const_cast<Window *>(theOne));
1101 } else if (m_animation_timer == 0 && !canUseVSyncBasedAnimation && m_animation_driver->isRunning()) {
1102 qCDebug(QSG_LOG_RENDERLOOP, "*** Starting system (not vsync-based) animation timer (exposedWindows=%d unthrottledWindows=%d badVSync=%d)",
1103 exposedWindows, unthrottledWindows, badVSync);
1104 m_animation_timer = startTimer(interval: int(sg->vsyncIntervalForAnimationDriver(driver: m_animation_driver)));
1105 }
1106}
1107
1108/*
1109 Removes this window from the list of tracked windowes in this
1110 window manager. hide() will trigger obscure, which in turn will
1111 stop rendering.
1112
1113 This function will be called during QWindow::close() which will
1114 also destroy the QPlatformWindow so it is important that this
1115 triggers handleObscurity() and that rendering for that window
1116 is fully done and over with by the time this function exits.
1117 */
1118
1119void QSGThreadedRenderLoop::hide(QQuickWindow *window)
1120{
1121 qCDebug(QSG_LOG_RENDERLOOP) << "hide()" << window;
1122
1123 if (window->isExposed())
1124 handleObscurity(w: windowFor(window));
1125
1126 releaseResources(window);
1127}
1128
1129void QSGThreadedRenderLoop::resize(QQuickWindow *window)
1130{
1131 qCDebug(QSG_LOG_RENDERLOOP) << "reisze()" << window;
1132
1133 Window *w = windowFor(window);
1134 if (!w)
1135 return;
1136
1137 w->psTimeAccumulator = 0.0f;
1138 w->psTimeSampleCount = 0;
1139}
1140
1141/*
1142 If the window is first hide it, then perform a complete cleanup
1143 with releaseResources which will take down the GL context and
1144 exit the rendering thread.
1145 */
1146void QSGThreadedRenderLoop::windowDestroyed(QQuickWindow *window)
1147{
1148 qCDebug(QSG_LOG_RENDERLOOP) << "begin windowDestroyed()" << window;
1149
1150 Window *w = windowFor(window);
1151 if (!w)
1152 return;
1153
1154 handleObscurity(w);
1155 releaseResources(window: w, inDestructor: true);
1156
1157 QSGRenderThread *thread = w->thread;
1158 while (thread->isRunning())
1159 QThread::yieldCurrentThread();
1160 Q_ASSERT(thread->thread() == QThread::currentThread());
1161 delete thread;
1162
1163 for (int i=0; i<m_windows.size(); ++i) {
1164 if (m_windows.at(i).window == window) {
1165 m_windows.removeAt(i);
1166 break;
1167 }
1168 }
1169
1170 // Now that we altered the window list, we may need to stop the animation
1171 // timer even if we didn't via handleObscurity. This covers the case where
1172 // we destroy a visible & exposed QQuickWindow.
1173 startOrStopAnimationTimer();
1174
1175 qCDebug(QSG_LOG_RENDERLOOP) << "done windowDestroyed()" << window;
1176}
1177
1178void QSGThreadedRenderLoop::releaseSwapchain(QQuickWindow *window)
1179{
1180 QQuickWindowPrivate *wd = QQuickWindowPrivate::get(c: window);
1181 delete wd->rpDescForSwapchain;
1182 wd->rpDescForSwapchain = nullptr;
1183 delete wd->swapchain;
1184 wd->swapchain = nullptr;
1185 delete wd->depthStencilForSwapchain;
1186 wd->depthStencilForSwapchain = nullptr;
1187 wd->hasActiveSwapchain = wd->hasRenderableSwapchain = wd->swapchainJustBecameRenderable = false;
1188}
1189
1190void QSGThreadedRenderLoop::exposureChanged(QQuickWindow *window)
1191{
1192 qCDebug(QSG_LOG_RENDERLOOP) << "exposureChanged()" << window;
1193
1194 // This is tricker than used to be. We want to detect having an empty
1195 // surface size (which may be the case even when window->size() is
1196 // non-empty, on some platforms with some graphics APIs!) as well as the
1197 // case when the window just became "newly exposed" (e.g. after a
1198 // minimize-restore on Windows, or when switching between fully obscured -
1199 // not fully obscured on macOS)
1200 QQuickWindowPrivate *wd = QQuickWindowPrivate::get(c: window);
1201 if (!window->isExposed())
1202 wd->hasRenderableSwapchain = false;
1203
1204 bool skipThisExpose = false;
1205 if (window->isExposed() && wd->hasActiveSwapchain && wd->swapchain->surfacePixelSize().isEmpty()) {
1206 wd->hasRenderableSwapchain = false;
1207 skipThisExpose = true;
1208 }
1209
1210 if (window->isExposed() && !wd->hasRenderableSwapchain && wd->hasActiveSwapchain
1211 && !wd->swapchain->surfacePixelSize().isEmpty())
1212 {
1213 wd->hasRenderableSwapchain = true;
1214 wd->swapchainJustBecameRenderable = true;
1215 }
1216
1217 if (window->isExposed()) {
1218 if (!skipThisExpose)
1219 handleExposure(w: window);
1220 } else {
1221 Window *w = windowFor(window);
1222 if (w)
1223 handleObscurity(w);
1224 }
1225}
1226
1227/*
1228 Will post an event to the render thread that this window should
1229 start to render.
1230 */
1231void QSGThreadedRenderLoop::handleExposure(QQuickWindow *window)
1232{
1233 qCDebug(QSG_LOG_RENDERLOOP) << "handleExposure()" << window;
1234
1235 Window *w = windowFor(window);
1236 if (!w) {
1237 qCDebug(QSG_LOG_RENDERLOOP, "- adding window to list");
1238 Window win;
1239 win.window = window;
1240 win.actualWindowFormat = window->format();
1241 auto renderContext = QQuickWindowPrivate::get(c: window)->context;
1242 // The thread assumes ownership, so we don't need to delete it later.
1243 pendingRenderContexts.remove(value: renderContext);
1244 win.thread = new QSGRenderThread(this, renderContext);
1245 win.updateDuringSync = false;
1246 win.forceRenderPass = true; // also covered by polishAndSync(inExpose=true), but doesn't hurt
1247 win.badVSync = false;
1248 win.timeBetweenPolishAndSyncs.start();
1249 win.psTimeAccumulator = 0.0f;
1250 win.psTimeSampleCount = 0;
1251 m_windows << win;
1252 w = &m_windows.last();
1253 } else {
1254 if (!QQuickWindowPrivate::get(c: window)->updatesEnabled) {
1255 qCDebug(QSG_LOG_RENDERLOOP, "- updatesEnabled is false, abort");
1256 return;
1257 }
1258 }
1259
1260 // set this early as we'll be rendering shortly anyway and this avoids
1261 // specialcasing exposure in polishAndSync.
1262 w->thread->window = window;
1263
1264#ifndef QT_NO_DEBUG
1265 if (w->window->width() <= 0 || w->window->height() <= 0
1266 || (w->window->isTopLevel() && !w->window->geometry().intersects(r: w->window->screen()->availableGeometry()))) {
1267 qWarning().noquote().nospace() << "QSGThreadedRenderLoop: expose event received for window "
1268 << w->window << " with invalid geometry: " << w->window->geometry()
1269 << " on " << w->window->screen();
1270 }
1271#endif
1272
1273 // Because we are going to bind a GL context to it, make sure it
1274 // is created.
1275 if (!w->window->handle())
1276 w->window->create();
1277
1278 // Start render thread if it is not running
1279 if (!w->thread->isRunning()) {
1280 qCDebug(QSG_LOG_RENDERLOOP, "- starting render thread");
1281
1282 if (!w->thread->rhi) {
1283 QSGRhiSupport *rhiSupport = QSGRhiSupport::instance();
1284 if (!w->thread->offscreenSurface)
1285 w->thread->offscreenSurface = rhiSupport->maybeCreateOffscreenSurface(window);
1286 w->thread->scProxyData = QRhi::updateSwapChainProxyData(impl: rhiSupport->rhiBackend(), window);
1287 window->installEventFilter(filterObj: this);
1288 }
1289
1290 QQuickAnimatorController *controller
1291 = QQuickWindowPrivate::get(c: w->window)->animationController.get();
1292 if (controller->thread() != w->thread)
1293 controller->moveToThread(thread: w->thread);
1294
1295 w->thread->active = true;
1296 if (w->thread->thread() == QThread::currentThread()) {
1297 w->thread->sgrc->moveToThread(thread: w->thread);
1298 w->thread->moveToThread(thread: w->thread);
1299 }
1300 w->thread->start();
1301 if (!w->thread->isRunning())
1302 qFatal(msg: "Render thread failed to start, aborting application.");
1303
1304 } else {
1305 qCDebug(QSG_LOG_RENDERLOOP, "- render thread already running");
1306 }
1307
1308 polishAndSync(w, inExpose: true);
1309 qCDebug(QSG_LOG_RENDERLOOP, "- done with handleExposure()");
1310
1311 startOrStopAnimationTimer();
1312}
1313
1314/*
1315 This function posts an event to the render thread to remove the window
1316 from the list of windowses to render.
1317
1318 It also starts up the non-vsync animation tick if no more windows
1319 are showing.
1320 */
1321void QSGThreadedRenderLoop::handleObscurity(Window *w)
1322{
1323 if (!w)
1324 return;
1325
1326 qCDebug(QSG_LOG_RENDERLOOP) << "handleObscurity()" << w->window;
1327 if (w->thread->isRunning()) {
1328 if (!QQuickWindowPrivate::get(c: w->window)->updatesEnabled) {
1329 qCDebug(QSG_LOG_RENDERLOOP, "- updatesEnabled is false, abort");
1330 return;
1331 }
1332 w->thread->mutex.lock();
1333 w->thread->postEvent(e: new WMWindowEvent(w->window, QEvent::Type(WM_Obscure)));
1334 w->thread->waitCondition.wait(lockedMutex: &w->thread->mutex);
1335 w->thread->mutex.unlock();
1336 }
1337 startOrStopAnimationTimer();
1338}
1339
1340bool QSGThreadedRenderLoop::eventFilter(QObject *watched, QEvent *event)
1341{
1342 switch (event->type()) {
1343 case QEvent::PlatformSurface:
1344 // this is the proper time to tear down the swapchain (while the native window and surface are still around)
1345 if (static_cast<QPlatformSurfaceEvent *>(event)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) {
1346 QQuickWindow *window = qobject_cast<QQuickWindow *>(object: watched);
1347 if (window) {
1348 Window *w = windowFor(window);
1349 if (w && w->thread->isRunning()) {
1350 w->thread->mutex.lock();
1351 w->thread->postEvent(e: new WMReleaseSwapchainEvent(window));
1352 w->thread->waitCondition.wait(lockedMutex: &w->thread->mutex);
1353 w->thread->mutex.unlock();
1354 }
1355 }
1356 // keep this filter on the window - needed for uncommon but valid
1357 // sequences of calls like window->destroy(); window->show();
1358 }
1359 break;
1360 default:
1361 break;
1362 }
1363 return QObject::eventFilter(watched, event);
1364}
1365
1366void QSGThreadedRenderLoop::handleUpdateRequest(QQuickWindow *window)
1367{
1368 qCDebug(QSG_LOG_RENDERLOOP) << "- update request" << window;
1369 if (!QQuickWindowPrivate::get(c: window)->updatesEnabled) {
1370 qCDebug(QSG_LOG_RENDERLOOP, "- updatesEnabled is false, abort");
1371 return;
1372 }
1373 Window *w = windowFor(window);
1374 if (w)
1375 polishAndSync(w);
1376}
1377
1378void QSGThreadedRenderLoop::maybeUpdate(QQuickWindow *window)
1379{
1380 Window *w = windowFor(window);
1381 if (w)
1382 maybeUpdate(window: w);
1383}
1384
1385/*
1386 Called whenever the QML scene has changed. Will post an event to
1387 ourselves that a sync is needed.
1388 */
1389void QSGThreadedRenderLoop::maybeUpdate(Window *w)
1390{
1391 if (!QCoreApplication::instance())
1392 return;
1393
1394 if (!w || !w->thread->isRunning())
1395 return;
1396
1397 QThread *current = QThread::currentThread();
1398 if (current == w->thread && w->thread->rhi && w->thread->rhi->isDeviceLost())
1399 return;
1400 if (current != QCoreApplication::instance()->thread() && (current != w->thread || !m_lockedForSync)) {
1401 qWarning() << "Updates can only be scheduled from GUI thread or from QQuickItem::updatePaintNode()";
1402 return;
1403 }
1404
1405 qCDebug(QSG_LOG_RENDERLOOP) << "update from item" << w->window;
1406
1407 // Call this function from the Gui thread later as startTimer cannot be
1408 // called from the render thread.
1409 if (current == w->thread) {
1410 qCDebug(QSG_LOG_RENDERLOOP, "- on render thread");
1411 w->updateDuringSync = true;
1412 return;
1413 }
1414
1415 // An updatePolish() implementation may call update() to get the QQuickItem
1416 // dirtied. That's fine but it also leads to calling this function.
1417 // Requesting another update is a waste then since the updatePolish() call
1418 // will be followed up with a round of sync and render.
1419 if (m_inPolish)
1420 return;
1421
1422 postUpdateRequest(w);
1423}
1424
1425/*
1426 Called when the QQuickWindow should be explicitly repainted. This function
1427 can also be called on the render thread when the GUI thread is blocked to
1428 keep render thread animations alive.
1429 */
1430void QSGThreadedRenderLoop::update(QQuickWindow *window)
1431{
1432 Window *w = windowFor(window);
1433 if (!w)
1434 return;
1435
1436 if (w->thread == QThread::currentThread()) {
1437 qCDebug(QSG_LOG_RENDERLOOP) << "update on window - on render thread" << w->window;
1438 w->thread->requestRepaint();
1439 return;
1440 }
1441
1442 qCDebug(QSG_LOG_RENDERLOOP) << "update on window" << w->window;
1443 // We set forceRenderPass because we want to make sure the QQuickWindow
1444 // actually does a full render pass after the next sync.
1445 w->forceRenderPass = true;
1446 maybeUpdate(w);
1447}
1448
1449
1450void QSGThreadedRenderLoop::releaseResources(QQuickWindow *window)
1451{
1452 Window *w = windowFor(window);
1453 if (w)
1454 releaseResources(window: w, inDestructor: false);
1455}
1456
1457/*
1458 * Release resources will post an event to the render thread to
1459 * free up the SG and GL resources and exists the render thread.
1460 */
1461void QSGThreadedRenderLoop::releaseResources(Window *w, bool inDestructor)
1462{
1463 qCDebug(QSG_LOG_RENDERLOOP) << "releaseResources()" << (inDestructor ? "in destructor" : "in api-call") << w->window;
1464
1465 w->thread->mutex.lock();
1466 if (w->thread->isRunning() && w->thread->active) {
1467 QQuickWindow *window = w->window;
1468
1469 // The platform window might have been destroyed before
1470 // hide/release/windowDestroyed is called, so we may need to have a
1471 // fallback surface to perform the cleanup of the scene graph and the
1472 // RHI resources.
1473
1474 qCDebug(QSG_LOG_RENDERLOOP, "- posting release request to render thread");
1475 w->thread->postEvent(e: new WMTryReleaseEvent(window, inDestructor, window->handle() == nullptr));
1476 w->thread->waitCondition.wait(lockedMutex: &w->thread->mutex);
1477
1478 // Avoid a shutdown race condition.
1479 // If SG is invalidated and 'active' becomes false, the thread's run()
1480 // method will exit. handleExposure() relies on QThread::isRunning() (because it
1481 // potentially needs to start the thread again) and our mutex cannot be used to
1482 // track the thread stopping, so we wait a few nanoseconds extra so the thread
1483 // can exit properly.
1484 if (!w->thread->active) {
1485 qCDebug(QSG_LOG_RENDERLOOP) << " - waiting for render thread to exit" << w->window;
1486 w->thread->wait();
1487 qCDebug(QSG_LOG_RENDERLOOP) << " - render thread finished" << w->window;
1488 }
1489 }
1490 w->thread->mutex.unlock();
1491}
1492
1493
1494/* Calls polish on all items, then requests synchronization with the render thread
1495 * and blocks until that is complete. Returns false if it aborted; otherwise true.
1496 */
1497void QSGThreadedRenderLoop::polishAndSync(Window *w, bool inExpose)
1498{
1499 qCDebug(QSG_LOG_RENDERLOOP) << "polishAndSync" << (inExpose ? "(in expose)" : "(normal)") << w->window;
1500
1501 QQuickWindow *window = w->window;
1502 if (!w->thread || !w->thread->window) {
1503 qCDebug(QSG_LOG_RENDERLOOP, "- not exposed, abort");
1504 return;
1505 }
1506
1507 // Flush pending touch events.
1508 QQuickWindowPrivate::get(c: window)->deliveryAgentPrivate()->flushFrameSynchronousEvents(win: window);
1509 // The delivery of the event might have caused the window to stop rendering
1510 w = windowFor(window);
1511 if (!w || !w->thread || !w->thread->window) {
1512 qCDebug(QSG_LOG_RENDERLOOP, "- removed after event flushing, abort");
1513 return;
1514 }
1515
1516 Q_TRACE_SCOPE(QSG_polishAndSync);
1517 QElapsedTimer timer;
1518 qint64 polishTime = 0;
1519 qint64 waitTime = 0;
1520 qint64 syncTime = 0;
1521
1522 const qint64 elapsedSinceLastMs = w->timeBetweenPolishAndSyncs.restart();
1523
1524 if (w->actualWindowFormat.swapInterval() != 0 && sg->isVSyncDependent(driver: m_animation_driver)) {
1525 w->psTimeAccumulator += elapsedSinceLastMs;
1526 w->psTimeSampleCount += 1;
1527 // cannot be too high because we'd then delay recognition of broken vsync at start
1528 static const int PS_TIME_SAMPLE_LENGTH = 20;
1529 if (w->psTimeSampleCount > PS_TIME_SAMPLE_LENGTH) {
1530 const float t = w->psTimeAccumulator / w->psTimeSampleCount;
1531 const float vsyncRate = sg->vsyncIntervalForAnimationDriver(driver: m_animation_driver);
1532
1533 // What this means is that the last PS_TIME_SAMPLE_LENGTH frames
1534 // average to an elapsed time of t milliseconds, whereas the animation
1535 // driver (assuming a single window, vsync-based advancing) assumes a
1536 // vsyncRate milliseconds for a frame. If now we see that the elapsed
1537 // time is way too low (less than half of the approx. expected value),
1538 // then we assume that something is wrong with vsync.
1539 //
1540 // This will not capture everything. Consider a 144 Hz screen with 6.9
1541 // ms vsync rate, the half of that is below the default 5 ms timer of
1542 // QWindow::requestUpdate(), so this will not trigger even if the
1543 // graphics stack does not throttle. But then the whole workaround is
1544 // not that important because the animations advance anyway closer to
1545 // what's expected (e.g. advancing as if 6-7 ms passed after ca. 5 ms),
1546 // the gap is a lot smaller than with the 60 Hz case (animations
1547 // advancing as if 16 ms passed after just ca. 5 ms) The workaround
1548 // here is present mainly for virtual machines and other broken
1549 // environments, most of which will persumably report a 60 Hz screen.
1550
1551 const float threshold = vsyncRate * 0.5f;
1552 const bool badVSync = t < threshold;
1553 if (badVSync && !w->badVSync) {
1554 // Once we determine something is wrong with the frame rate, set
1555 // the flag for the rest of the lifetime of the window. This is
1556 // saner and more deterministic than allowing it to be turned on
1557 // and off. (a window resize can take up time, leading to higher
1558 // elapsed times, thus unnecessarily starting to switch modes,
1559 // while some platforms seem to have advanced logic (and adaptive
1560 // refresh rates an whatnot) that can eventually start throttling
1561 // an unthrottled window, potentially leading to a continuous
1562 // switching of modes back and forth which is not desirable.
1563 w->badVSync = true;
1564 qCDebug(QSG_LOG_INFO, "Window %p is determined to have broken vsync throttling (%f < %f) "
1565 "switching to system timer to drive gui thread animations to remedy this "
1566 "(however, render thread animators will likely advance at an incorrect rate).",
1567 w->window, t, threshold);
1568 startOrStopAnimationTimer();
1569 }
1570
1571 w->psTimeAccumulator = 0.0f;
1572 w->psTimeSampleCount = 0;
1573 }
1574 }
1575
1576 const bool profileFrames = QSG_LOG_TIME_RENDERLOOP().isDebugEnabled();
1577 if (profileFrames) {
1578 timer.start();
1579 qCDebug(QSG_LOG_TIME_RENDERLOOP, "[window %p][gui thread] polishAndSync: start, elapsed since last call: %d ms",
1580 window,
1581 int(elapsedSinceLastMs));
1582 }
1583 Q_QUICK_SG_PROFILE_START(QQuickProfiler::SceneGraphPolishAndSync);
1584 Q_TRACE(QSG_polishItems_entry);
1585
1586 QQuickWindowPrivate *d = QQuickWindowPrivate::get(c: window);
1587 m_inPolish = true;
1588 d->polishItems();
1589 m_inPolish = false;
1590
1591 if (profileFrames)
1592 polishTime = timer.nsecsElapsed();
1593 Q_TRACE(QSG_polishItems_exit);
1594 Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphPolishAndSync,
1595 QQuickProfiler::SceneGraphPolishAndSyncPolish);
1596 Q_TRACE(QSG_wait_entry);
1597
1598 w->updateDuringSync = false;
1599
1600 emit window->afterAnimating();
1601
1602 const QRhiSwapChainProxyData scProxyData =
1603 QRhi::updateSwapChainProxyData(impl: QSGRhiSupport::instance()->rhiBackend(), window);
1604
1605 qCDebug(QSG_LOG_RENDERLOOP, "- lock for sync");
1606 w->thread->mutex.lock();
1607 m_lockedForSync = true;
1608 w->thread->postEvent(e: new WMSyncEvent(window, inExpose, w->forceRenderPass, scProxyData));
1609 w->forceRenderPass = false;
1610
1611 qCDebug(QSG_LOG_RENDERLOOP, "- wait for sync");
1612 if (profileFrames)
1613 waitTime = timer.nsecsElapsed();
1614 Q_TRACE(QSG_wait_exit);
1615 Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphPolishAndSync,
1616 QQuickProfiler::SceneGraphPolishAndSyncWait);
1617 Q_TRACE(QSG_sync_entry);
1618
1619 w->thread->waitCondition.wait(lockedMutex: &w->thread->mutex);
1620 m_lockedForSync = false;
1621 w->thread->mutex.unlock();
1622 qCDebug(QSG_LOG_RENDERLOOP, "- unlock after sync");
1623
1624 if (profileFrames)
1625 syncTime = timer.nsecsElapsed();
1626 Q_TRACE(QSG_sync_exit);
1627 Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphPolishAndSync,
1628 QQuickProfiler::SceneGraphPolishAndSyncSync);
1629 Q_TRACE(QSG_animations_entry);
1630
1631 // Now is the time to advance the regular animations (as we are throttled
1632 // to vsync due to the wait above), but this is only relevant when there is
1633 // one single window. With multiple windows m_animation_timer is active,
1634 // and advance() happens instead in response to a good old timer event, not
1635 // here. (the above applies only when the QSGAnimationDriver reports
1636 // isVSyncDependent() == true, if not then we always use the driver and
1637 // just advance here)
1638 if (m_animation_timer == 0 && m_animation_driver->isRunning()) {
1639 qCDebug(QSG_LOG_RENDERLOOP, "- advancing animations");
1640 m_animation_driver->advance();
1641 qCDebug(QSG_LOG_RENDERLOOP, "- animations done..");
1642
1643 // We need to trigger another update round to keep all animations
1644 // running correctly. For animations that lead to a visual change (a
1645 // property change in some item leading to dirtying the item and so
1646 // ending up in maybeUpdate()) this would not be needed, but other
1647 // animations would then stop functioning since there is nothing
1648 // advancing the animation system if we do not call postUpdateRequest()
1649 // here and nothing else leads to it either. This has an unfortunate
1650 // side effect in multi window cases: one can end up in a situation
1651 // where a non-animating window gets updates continuously because there
1652 // is an animation running in some other window that is non-exposed or
1653 // even closed already (if it was exposed we would not hit this branch,
1654 // however). Sadly, there is nothing that can be done about it.
1655 postUpdateRequest(w);
1656
1657 emit timeToIncubate();
1658 } else if (w->updateDuringSync) {
1659 postUpdateRequest(w);
1660 }
1661
1662 if (profileFrames) {
1663 qCDebug(QSG_LOG_TIME_RENDERLOOP, "[window %p][gui thread] Frame prepared, polish=%d ms, lock=%d ms, blockedForSync=%d ms, animations=%d ms",
1664 window,
1665 int(polishTime / 1000000),
1666 int((waitTime - polishTime) / 1000000),
1667 int((syncTime - waitTime) / 1000000),
1668 int((timer.nsecsElapsed() - syncTime) / 1000000));
1669 }
1670
1671 Q_TRACE(QSG_animations_exit);
1672 Q_QUICK_SG_PROFILE_END(QQuickProfiler::SceneGraphPolishAndSync,
1673 QQuickProfiler::SceneGraphPolishAndSyncAnimations);
1674}
1675
1676bool QSGThreadedRenderLoop::event(QEvent *e)
1677{
1678 switch ((int) e->type()) {
1679
1680 case QEvent::Timer: {
1681 Q_ASSERT(sg->isVSyncDependent(m_animation_driver));
1682 QTimerEvent *te = static_cast<QTimerEvent *>(e);
1683 if (te->timerId() == m_animation_timer) {
1684 qCDebug(QSG_LOG_RENDERLOOP, "- ticking non-render thread timer");
1685 m_animation_driver->advance();
1686 emit timeToIncubate();
1687 return true;
1688 }
1689 }
1690
1691 default:
1692 break;
1693 }
1694
1695 return QObject::event(event: e);
1696}
1697
1698
1699
1700/*
1701 Locks down GUI and performs a grab the scene graph, then returns the result.
1702
1703 Since the QML scene could have changed since the last time it was rendered,
1704 we need to polish and sync the scene graph. This might seem superfluous, but
1705 - QML changes could have triggered deleteLater() which could have removed
1706 textures or other objects from the scene graph, causing render to crash.
1707 - Autotests rely on grab(), setProperty(), grab(), compare behavior.
1708 */
1709
1710QImage QSGThreadedRenderLoop::grab(QQuickWindow *window)
1711{
1712 qCDebug(QSG_LOG_RENDERLOOP) << "grab()" << window;
1713
1714 Window *w = windowFor(window);
1715 Q_ASSERT(w);
1716
1717 if (!w->thread->isRunning())
1718 return QImage();
1719
1720 if (!window->handle())
1721 window->create();
1722
1723 qCDebug(QSG_LOG_RENDERLOOP, "- polishing items");
1724 QQuickWindowPrivate *d = QQuickWindowPrivate::get(c: window);
1725 m_inPolish = true;
1726 d->polishItems();
1727 m_inPolish = false;
1728
1729 QImage result;
1730 w->thread->mutex.lock();
1731 m_lockedForSync = true;
1732 qCDebug(QSG_LOG_RENDERLOOP, "- posting grab event");
1733 w->thread->postEvent(e: new WMGrabEvent(window, &result));
1734 w->thread->waitCondition.wait(lockedMutex: &w->thread->mutex);
1735 m_lockedForSync = false;
1736 w->thread->mutex.unlock();
1737
1738 qCDebug(QSG_LOG_RENDERLOOP, "- grab complete");
1739
1740 return result;
1741}
1742
1743/*
1744 * Posts a new job event to the render thread.
1745 * Returns true if posting succeeded.
1746 */
1747void QSGThreadedRenderLoop::postJob(QQuickWindow *window, QRunnable *job)
1748{
1749 Window *w = windowFor(window);
1750 if (w && w->thread && w->thread->window)
1751 w->thread->postEvent(e: new WMJobEvent(window, job));
1752 else
1753 delete job;
1754}
1755
1756QT_END_NAMESPACE
1757
1758#include "qsgthreadedrenderloop.moc"
1759#include "moc_qsgthreadedrenderloop_p.cpp"
1760

source code of qtdeclarative/src/quick/scenegraph/qsgthreadedrenderloop.cpp