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