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