1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qsgrenderloop_p.h" |
5 | #include "qsgthreadedrenderloop_p.h" |
6 | #include "qsgrhisupport_p.h" |
7 | #include <private/qquickanimatorcontroller_p.h> |
8 | |
9 | #include <QtCore/QCoreApplication> |
10 | #include <QtCore/QElapsedTimer> |
11 | #include <QtCore/QLibraryInfo> |
12 | #include <QtCore/private/qabstractanimation_p.h> |
13 | |
14 | #include <QtGui/QOffscreenSurface> |
15 | #include <QtGui/private/qguiapplication_p.h> |
16 | #include <qpa/qplatformintegration.h> |
17 | #include <QPlatformSurfaceEvent> |
18 | |
19 | #include <QtQml/private/qqmlglobal_p.h> |
20 | |
21 | #include <QtQuick/QQuickWindow> |
22 | #include <QtQuick/private/qquickwindow_p.h> |
23 | #include <QtQuick/private/qquickitem_p.h> |
24 | #include <QtQuick/private/qsgcontext_p.h> |
25 | #include <QtQuick/private/qsgrenderer_p.h> |
26 | #include <private/qquickprofiler_p.h> |
27 | #include <qtquick_tracepoints_p.h> |
28 | |
29 | #include <private/qsgrhishadereffectnode_p.h> |
30 | |
31 | #include <private/qsgdefaultrendercontext_p.h> |
32 | |
33 | #ifdef Q_OS_WIN |
34 | #include <QtCore/qt_windows.h> |
35 | #endif |
36 | |
37 | QT_BEGIN_NAMESPACE |
38 | |
39 | extern bool qsg_useConsistentTiming(); |
40 | |
41 | #define ENABLE_DEFAULT_BACKEND |
42 | |
43 | Q_TRACE_POINT(qtquick, QSG_renderWindow_entry) |
44 | Q_TRACE_POINT(qtquick, QSG_renderWindow_exit) |
45 | Q_TRACE_POINT(qtquick, QSG_polishItems_entry) |
46 | Q_TRACE_POINT(qtquick, QSG_polishItems_exit) |
47 | Q_TRACE_POINT(qtquick, QSG_sync_entry) |
48 | Q_TRACE_POINT(qtquick, QSG_sync_exit) |
49 | Q_TRACE_POINT(qtquick, QSG_render_entry) |
50 | Q_TRACE_POINT(qtquick, QSG_render_exit) |
51 | Q_TRACE_POINT(qtquick, QSG_swap_entry) |
52 | Q_TRACE_POINT(qtquick, QSG_swap_exit) |
53 | |
54 | |
55 | /* |
56 | - Uses one QRhi per window. (and so each window has its own QOpenGLContext or ID3D11Device(Context) etc.) |
57 | - Animations are advanced using the standard timer (no custom animation |
58 | driver is installed), so QML animations run as expected even when vsync |
59 | throttling is broken. |
60 | */ |
61 | |
62 | DEFINE_BOOL_CONFIG_OPTION(qmlNoThreadedRenderer, QML_BAD_GUI_RENDER_LOOP); |
63 | DEFINE_BOOL_CONFIG_OPTION(qmlForceThreadedRenderer, QML_FORCE_THREADED_RENDERER); // Might trigger graphics driver threading bugs, use at own risk |
64 | |
65 | QSGRenderLoop *QSGRenderLoop::s_instance = nullptr; |
66 | |
67 | QSGRenderLoop::~QSGRenderLoop() |
68 | { |
69 | } |
70 | |
71 | void QSGRenderLoop::cleanup() |
72 | { |
73 | if (!s_instance) |
74 | return; |
75 | for (QQuickWindow *w : s_instance->windows()) { |
76 | QQuickWindowPrivate *wd = QQuickWindowPrivate::get(c: w); |
77 | if (wd->windowManager == s_instance) { |
78 | s_instance->windowDestroyed(window: w); |
79 | wd->windowManager = nullptr; |
80 | } |
81 | } |
82 | delete s_instance; |
83 | s_instance = nullptr; |
84 | } |
85 | |
86 | QSurface::SurfaceType QSGRenderLoop::windowSurfaceType() const |
87 | { |
88 | #ifdef ENABLE_DEFAULT_BACKEND |
89 | return QSGRhiSupport::instance()->windowSurfaceType(); |
90 | #else |
91 | return QSurface::RasterSurface; |
92 | #endif |
93 | } |
94 | |
95 | void QSGRenderLoop::postJob(QQuickWindow *window, QRunnable *job) |
96 | { |
97 | Q_ASSERT(job); |
98 | #ifdef ENABLE_DEFAULT_BACKEND |
99 | Q_ASSERT(window); |
100 | QQuickWindowPrivate *cd = QQuickWindowPrivate::get(c: window); |
101 | if (cd->rhi) |
102 | cd->rhi->makeThreadLocalNativeContextCurrent(); |
103 | job->run(); |
104 | #else |
105 | Q_UNUSED(window); |
106 | job->run(); |
107 | #endif |
108 | delete job; |
109 | } |
110 | |
111 | #ifdef ENABLE_DEFAULT_BACKEND |
112 | class QSGGuiThreadRenderLoop : public QSGRenderLoop |
113 | { |
114 | Q_OBJECT |
115 | public: |
116 | QSGGuiThreadRenderLoop(); |
117 | ~QSGGuiThreadRenderLoop(); |
118 | |
119 | void show(QQuickWindow *window) override; |
120 | void hide(QQuickWindow *window) override; |
121 | |
122 | void windowDestroyed(QQuickWindow *window) override; |
123 | |
124 | void renderWindow(QQuickWindow *window); |
125 | void exposureChanged(QQuickWindow *window) override; |
126 | QImage grab(QQuickWindow *window) override; |
127 | |
128 | void maybeUpdate(QQuickWindow *window) override; |
129 | void update(QQuickWindow *window) override { maybeUpdate(window); } // identical for this implementation. |
130 | void handleUpdateRequest(QQuickWindow *) override; |
131 | |
132 | void releaseResources(QQuickWindow *) override; |
133 | |
134 | QAnimationDriver *animationDriver() const override { return nullptr; } |
135 | |
136 | QSGContext *sceneGraphContext() const override; |
137 | QSGRenderContext *createRenderContext(QSGContext *) const override; |
138 | |
139 | void releaseSwapchain(QQuickWindow *window); |
140 | void handleDeviceLoss(); |
141 | |
142 | bool eventFilter(QObject *watched, QEvent *event) override; |
143 | |
144 | struct WindowData { |
145 | WindowData() |
146 | : updatePending(false), |
147 | rhiDeviceLost(false), |
148 | rhiDoomed(false) |
149 | { } |
150 | QRhi *rhi = nullptr; |
151 | bool ownRhi = true; |
152 | QSGRenderContext *rc = nullptr; |
153 | QElapsedTimer timeBetweenRenders; |
154 | int sampleCount = 1; |
155 | bool updatePending : 1; |
156 | bool rhiDeviceLost : 1; |
157 | bool rhiDoomed : 1; |
158 | }; |
159 | |
160 | bool ensureRhi(QQuickWindow *window, WindowData &data); |
161 | |
162 | QHash<QQuickWindow *, WindowData> m_windows; |
163 | |
164 | QOffscreenSurface *offscreenSurface = nullptr; |
165 | QSGContext *sg; |
166 | mutable QSet<QSGRenderContext *> pendingRenderContexts; |
167 | |
168 | bool m_inPolish = false; |
169 | }; |
170 | #endif |
171 | |
172 | QSGRenderLoop *QSGRenderLoop::instance() |
173 | { |
174 | if (!s_instance) { |
175 | |
176 | QSGRhiSupport::checkEnvQSgInfo(); |
177 | |
178 | s_instance = QSGContext::createWindowManager(); |
179 | #ifdef ENABLE_DEFAULT_BACKEND |
180 | if (!s_instance) { |
181 | QSGRhiSupport *rhiSupport = QSGRhiSupport::instance(); |
182 | |
183 | QSGRenderLoopType loopType; |
184 | if (rhiSupport->rhiBackend() != QRhi::OpenGLES2) { |
185 | loopType = ThreadedRenderLoop; |
186 | } else { |
187 | if (QGuiApplicationPrivate::platformIntegration()->hasCapability(cap: QPlatformIntegration::ThreadedOpenGL)) |
188 | loopType = ThreadedRenderLoop; |
189 | else |
190 | loopType = BasicRenderLoop; |
191 | } |
192 | |
193 | switch (rhiSupport->rhiBackend()) { |
194 | case QRhi::Null: |
195 | loopType = BasicRenderLoop; |
196 | break; |
197 | |
198 | case QRhi::D3D11: |
199 | // The threaded loop's model may not be suitable for DXGI |
200 | // due to the possibility of having the main thread (with |
201 | // the Windows message pump) blocked while issuing a |
202 | // Present on the render thread. However, according to the |
203 | // docs this can be a problem for fullscreen swapchains |
204 | // only. So leave threaded enabled by default for now and |
205 | // revisit later if there are problems. |
206 | break; |
207 | |
208 | default: |
209 | break; |
210 | } |
211 | |
212 | // The environment variables can always override. This is good |
213 | // because in some situations it makes perfect sense to try out a |
214 | // render loop that is otherwise disabled by default. |
215 | |
216 | if (qmlNoThreadedRenderer()) |
217 | loopType = BasicRenderLoop; |
218 | else if (qmlForceThreadedRenderer()) |
219 | loopType = ThreadedRenderLoop; |
220 | |
221 | if (Q_UNLIKELY(qEnvironmentVariableIsSet("QSG_RENDER_LOOP" ))) { |
222 | const QByteArray loopName = qgetenv(varName: "QSG_RENDER_LOOP" ); |
223 | if (loopName == "windows" ) { |
224 | qWarning(msg: "The 'windows' render loop is no longer supported. Using 'basic' instead." ); |
225 | loopType = BasicRenderLoop; |
226 | } else if (loopName == "basic" ) { |
227 | loopType = BasicRenderLoop; |
228 | } else if (loopName == "threaded" ) { |
229 | loopType = ThreadedRenderLoop; |
230 | } |
231 | } |
232 | |
233 | switch (loopType) { |
234 | #if QT_CONFIG(thread) |
235 | case ThreadedRenderLoop: |
236 | qCDebug(QSG_LOG_INFO, "threaded render loop" ); |
237 | s_instance = new QSGThreadedRenderLoop(); |
238 | break; |
239 | #endif |
240 | default: |
241 | qCDebug(QSG_LOG_INFO, "basic render loop" ); |
242 | s_instance = new QSGGuiThreadRenderLoop(); |
243 | break; |
244 | } |
245 | } |
246 | #endif |
247 | qAddPostRoutine(QSGRenderLoop::cleanup); |
248 | } |
249 | |
250 | return s_instance; |
251 | } |
252 | |
253 | void QSGRenderLoop::setInstance(QSGRenderLoop *instance) |
254 | { |
255 | Q_ASSERT(!s_instance); |
256 | s_instance = instance; |
257 | } |
258 | |
259 | void QSGRenderLoop::handleContextCreationFailure(QQuickWindow *window) |
260 | { |
261 | QString translatedMessage; |
262 | QString untranslatedMessage; |
263 | QQuickWindowPrivate::rhiCreationFailureMessage(backendName: QSGRhiSupport::instance()->rhiBackendName(), |
264 | translatedMessage: &translatedMessage, |
265 | untranslatedMessage: &untranslatedMessage); |
266 | // If there is a slot connected to the error signal, emit it and leave it to |
267 | // the application to do something with the message. If nothing is connected, |
268 | // show a message on our own and terminate. |
269 | const bool signalEmitted = |
270 | QQuickWindowPrivate::get(c: window)->emitError(error: QQuickWindow::ContextNotAvailable, |
271 | msg: translatedMessage); |
272 | #if defined(Q_OS_WIN) |
273 | if (!signalEmitted && !QLibraryInfo::isDebugBuild() && !GetConsoleWindow()) { |
274 | MessageBox(0, (LPCTSTR) translatedMessage.utf16(), |
275 | (LPCTSTR)(QCoreApplication::applicationName().utf16()), |
276 | MB_OK | MB_ICONERROR); |
277 | } |
278 | #endif // Q_OS_WIN |
279 | if (!signalEmitted) |
280 | qFatal(msg: "%s" , qPrintable(untranslatedMessage)); |
281 | } |
282 | |
283 | #ifdef ENABLE_DEFAULT_BACKEND |
284 | QSGGuiThreadRenderLoop::QSGGuiThreadRenderLoop() |
285 | { |
286 | if (qsg_useConsistentTiming()) { |
287 | QUnifiedTimer::instance(create: true)->setConsistentTiming(true); |
288 | qCDebug(QSG_LOG_INFO, "using fixed animation steps" ); |
289 | } |
290 | |
291 | sg = QSGContext::createDefaultContext(); |
292 | } |
293 | |
294 | QSGGuiThreadRenderLoop::~QSGGuiThreadRenderLoop() |
295 | { |
296 | qDeleteAll(c: pendingRenderContexts); |
297 | delete sg; |
298 | } |
299 | |
300 | void QSGGuiThreadRenderLoop::show(QQuickWindow *window) |
301 | { |
302 | if (!m_windows.contains(key: window)) |
303 | m_windows.insert(key: window, value: {}); |
304 | |
305 | m_windows[window].timeBetweenRenders.start(); |
306 | maybeUpdate(window); |
307 | } |
308 | |
309 | void QSGGuiThreadRenderLoop::hide(QQuickWindow *window) |
310 | { |
311 | QQuickWindowPrivate *cd = QQuickWindowPrivate::get(c: window); |
312 | cd->fireAboutToStop(); |
313 | auto it = m_windows.find(key: window); |
314 | if (it != m_windows.end()) |
315 | it->updatePending = false; |
316 | } |
317 | |
318 | void QSGGuiThreadRenderLoop::windowDestroyed(QQuickWindow *window) |
319 | { |
320 | hide(window); |
321 | |
322 | WindowData data = m_windows.value(key: window, defaultValue: {}); |
323 | m_windows.remove(key: window); |
324 | |
325 | QQuickWindowPrivate *d = QQuickWindowPrivate::get(c: window); |
326 | |
327 | if (data.rhi) { |
328 | // Direct OpenGL calls in user code need a current context, like |
329 | // when rendering; ensure this (no-op when not running on GL). |
330 | // Also works when there is no handle() anymore. |
331 | data.rhi->makeThreadLocalNativeContextCurrent(); |
332 | } |
333 | |
334 | if (d->swapchain) { |
335 | if (window->handle()) { |
336 | // We get here when exiting via QCoreApplication::quit() instead of |
337 | // through QWindow::close(). |
338 | releaseSwapchain(window); |
339 | } else { |
340 | qWarning(msg: "QSGGuiThreadRenderLoop cleanup with QQuickWindow %p swapchain %p still alive, this should not happen." , |
341 | window, d->swapchain); |
342 | } |
343 | } |
344 | |
345 | d->cleanupNodesOnShutdown(); |
346 | |
347 | #if QT_CONFIG(quick_shadereffect) |
348 | QSGRhiShaderEffectNode::resetMaterialTypeCache(materialTypeCacheKey: window); |
349 | #endif |
350 | |
351 | if (data.rc) { |
352 | data.rc->invalidate(); |
353 | delete data.rc; |
354 | } |
355 | |
356 | if (data.ownRhi) |
357 | QSGRhiSupport::instance()->destroyRhi(rhi: data.rhi, config: d->graphicsConfig); |
358 | |
359 | d->rhi = nullptr; |
360 | |
361 | d->animationController.reset(); |
362 | |
363 | if (m_windows.isEmpty()) { |
364 | delete offscreenSurface; |
365 | offscreenSurface = nullptr; |
366 | } |
367 | } |
368 | |
369 | void QSGGuiThreadRenderLoop::handleDeviceLoss() |
370 | { |
371 | qWarning(msg: "Graphics device lost, cleaning up scenegraph and releasing RHIs" ); |
372 | |
373 | for (auto it = m_windows.begin(), itEnd = m_windows.end(); it != itEnd; ++it) { |
374 | if (!it->rhi || !it->rhi->isDeviceLost()) |
375 | return; |
376 | |
377 | QQuickWindowPrivate::get(c: it.key())->cleanupNodesOnShutdown(); |
378 | |
379 | if (it->rc) |
380 | it->rc->invalidate(); |
381 | |
382 | releaseSwapchain(window: it.key()); |
383 | it->rhiDeviceLost = true; |
384 | |
385 | if (it->ownRhi) |
386 | QSGRhiSupport::instance()->destroyRhi(rhi: it->rhi, config: {}); |
387 | it->rhi = nullptr; |
388 | } |
389 | } |
390 | |
391 | void QSGGuiThreadRenderLoop::releaseSwapchain(QQuickWindow *window) |
392 | { |
393 | QQuickWindowPrivate *wd = QQuickWindowPrivate::get(c: window); |
394 | delete wd->rpDescForSwapchain; |
395 | wd->rpDescForSwapchain = nullptr; |
396 | delete wd->swapchain; |
397 | wd->swapchain = nullptr; |
398 | delete wd->depthStencilForSwapchain; |
399 | wd->depthStencilForSwapchain = nullptr; |
400 | wd->hasActiveSwapchain = wd->hasRenderableSwapchain = wd->swapchainJustBecameRenderable = false; |
401 | } |
402 | |
403 | bool QSGGuiThreadRenderLoop::eventFilter(QObject *watched, QEvent *event) |
404 | { |
405 | switch (event->type()) { |
406 | case QEvent::PlatformSurface: |
407 | // this is the proper time to tear down the swapchain (while the native window and surface are still around) |
408 | if (static_cast<QPlatformSurfaceEvent *>(event)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) { |
409 | QQuickWindow *w = qobject_cast<QQuickWindow *>(object: watched); |
410 | if (w) |
411 | releaseSwapchain(window: w); |
412 | // keep this filter on the window - needed for uncommon but valid |
413 | // sequences of calls like window->destroy(); window->show(); |
414 | } |
415 | break; |
416 | default: |
417 | break; |
418 | } |
419 | return QObject::eventFilter(watched, event); |
420 | } |
421 | |
422 | bool QSGGuiThreadRenderLoop::ensureRhi(QQuickWindow *window, WindowData &data) |
423 | { |
424 | QQuickWindowPrivate *cd = QQuickWindowPrivate::get(c: window); |
425 | QSGRhiSupport *rhiSupport = QSGRhiSupport::instance(); |
426 | bool ok = data.rhi != nullptr; |
427 | |
428 | if (!data.rhi) { |
429 | // This block below handles both the initial QRhi initialization and |
430 | // also the subsequent reinitialization attempts after a device lost |
431 | // (reset) situation. |
432 | |
433 | if (data.rhiDoomed) // no repeated attempts if the initial attempt failed |
434 | return false; |
435 | |
436 | if (!offscreenSurface) |
437 | offscreenSurface = rhiSupport->maybeCreateOffscreenSurface(window); |
438 | |
439 | QSGRhiSupport::RhiCreateResult rhiResult = rhiSupport->createRhi(window, offscreenSurface); |
440 | data.rhi = rhiResult.rhi; |
441 | data.ownRhi = rhiResult.own; |
442 | |
443 | if (data.rhi) { |
444 | data.rhiDeviceLost = false; |
445 | |
446 | ok = true; |
447 | // We need to guarantee that sceneGraphInitialized is |
448 | // emitted with a context current, if running with OpenGL. |
449 | data.rhi->makeThreadLocalNativeContextCurrent(); |
450 | |
451 | // The sample count cannot vary between windows as we use the same |
452 | // rendercontext for all of them. Decide it here and now. |
453 | data.sampleCount = rhiSupport->chooseSampleCountForWindowWithRhi(window, rhi: data.rhi); |
454 | |
455 | cd->rhi = data.rhi; // set this early in case something hooked up to rc initialized() accesses it |
456 | |
457 | QSGDefaultRenderContext::InitParams rcParams; |
458 | rcParams.rhi = data.rhi; |
459 | rcParams.sampleCount = data.sampleCount; |
460 | rcParams.initialSurfacePixelSize = window->size() * window->effectiveDevicePixelRatio(); |
461 | rcParams.maybeSurface = window; |
462 | cd->context->initialize(params: &rcParams); |
463 | } else { |
464 | if (!data.rhiDeviceLost) { |
465 | data.rhiDoomed = true; |
466 | handleContextCreationFailure(window); |
467 | } |
468 | // otherwise no error, just return false so that we will retry on a subsequent rendering attempt |
469 | } |
470 | } |
471 | |
472 | if (data.rhi && !cd->swapchain) { |
473 | // if it's not the first window then the rhi is not yet stored to |
474 | // QQuickWindowPrivate, do it now |
475 | cd->rhi = data.rhi; |
476 | |
477 | // Unlike the threaded render loop, we use the same rhi for all windows |
478 | // and so createRhi() is called only once. Certain initialization may |
479 | // need to be done on a per window basis still, so make sure it is done. |
480 | rhiSupport->prepareWindowForRhi(window); |
481 | |
482 | QRhiSwapChain::Flags flags = QRhiSwapChain::UsedAsTransferSource; // may be used in a grab |
483 | const QSurfaceFormat requestedFormat = window->requestedFormat(); |
484 | |
485 | // QQ is always premul alpha. Decide based on alphaBufferSize in |
486 | // requestedFormat(). (the platform plugin can override format() but |
487 | // what matters here is what the application wanted, hence using the |
488 | // requested one) |
489 | const bool alpha = requestedFormat.alphaBufferSize() > 0; |
490 | if (alpha) |
491 | flags |= QRhiSwapChain::SurfaceHasPreMulAlpha; |
492 | |
493 | // Request NoVSync if swap interval was set to 0 (either by the app or |
494 | // by QSG_NO_VSYNC). What this means in practice is another question, |
495 | // but at least we tried. |
496 | if (requestedFormat.swapInterval() == 0) { |
497 | qCDebug(QSG_LOG_INFO, "Swap interval is 0, attempting to disable vsync when presenting." ); |
498 | flags |= QRhiSwapChain::NoVSync; |
499 | } |
500 | |
501 | cd->swapchain = data.rhi->newSwapChain(); |
502 | static bool depthBufferEnabled = qEnvironmentVariableIsEmpty(varName: "QSG_NO_DEPTH_BUFFER" ); |
503 | if (depthBufferEnabled) { |
504 | cd->depthStencilForSwapchain = data.rhi->newRenderBuffer(type: QRhiRenderBuffer::DepthStencil, |
505 | pixelSize: QSize(), |
506 | sampleCount: data.sampleCount, |
507 | flags: QRhiRenderBuffer::UsedWithSwapChainOnly); |
508 | cd->swapchain->setDepthStencil(cd->depthStencilForSwapchain); |
509 | } |
510 | cd->swapchain->setWindow(window); |
511 | rhiSupport->applySwapChainFormat(scWithWindowSet: cd->swapchain, window); |
512 | qCDebug(QSG_LOG_INFO, "MSAA sample count for the swapchain is %d. Alpha channel requested = %s" , |
513 | data.sampleCount, alpha ? "yes" : "no" ); |
514 | cd->swapchain->setSampleCount(data.sampleCount); |
515 | cd->swapchain->setFlags(flags); |
516 | cd->rpDescForSwapchain = cd->swapchain->newCompatibleRenderPassDescriptor(); |
517 | cd->swapchain->setRenderPassDescriptor(cd->rpDescForSwapchain); |
518 | |
519 | window->installEventFilter(filterObj: this); |
520 | } |
521 | |
522 | if (!data.rc) { |
523 | QSGRenderContext *rc = cd->context; |
524 | pendingRenderContexts.remove(value: rc); |
525 | data.rc = rc; |
526 | if (!data.rc) |
527 | qWarning(msg: "No QSGRenderContext for window %p, this should not happen" , window); |
528 | } |
529 | |
530 | return ok; |
531 | } |
532 | |
533 | void QSGGuiThreadRenderLoop::renderWindow(QQuickWindow *window) |
534 | { |
535 | auto winDataIt = m_windows.find(key: window); |
536 | if (winDataIt == m_windows.end()) |
537 | return; |
538 | |
539 | WindowData &data(*winDataIt); |
540 | bool alsoSwap = data.updatePending; |
541 | data.updatePending = false; |
542 | |
543 | QQuickWindowPrivate *cd = QQuickWindowPrivate::get(c: window); |
544 | if (!cd->isRenderable()) |
545 | return; |
546 | |
547 | if (!cd->updatesEnabled) |
548 | return; |
549 | |
550 | if (!ensureRhi(window, data)) |
551 | return; |
552 | |
553 | bool lastDirtyWindow = true; |
554 | for (auto it = m_windows.cbegin(), end = m_windows.cend(); it != end; ++it) { |
555 | if (it->updatePending) { |
556 | lastDirtyWindow = false; |
557 | break; |
558 | } |
559 | } |
560 | |
561 | cd->deliveryAgentPrivate()->flushFrameSynchronousEvents(win: window); |
562 | // Event delivery/processing triggered the window to be deleted or stop rendering. |
563 | if (!m_windows.contains(key: window)) |
564 | return; |
565 | |
566 | QSize effectiveOutputSize; // always prefer what the surface tells us, not the QWindow |
567 | if (cd->swapchain) { |
568 | effectiveOutputSize = cd->swapchain->surfacePixelSize(); |
569 | // An update request could still be delivered right before we get an |
570 | // unexpose. With Vulkan on Windows for example attempting to render |
571 | // leads to failures at this stage since the surface size is already 0. |
572 | if (effectiveOutputSize.isEmpty()) |
573 | return; |
574 | } |
575 | |
576 | Q_TRACE_SCOPE(QSG_renderWindow); |
577 | QElapsedTimer renderTimer; |
578 | qint64 renderTime = 0, syncTime = 0, polishTime = 0; |
579 | const bool profileFrames = QSG_LOG_TIME_RENDERLOOP().isDebugEnabled(); |
580 | if (profileFrames) |
581 | renderTimer.start(); |
582 | Q_TRACE(QSG_polishItems_entry); |
583 | Q_QUICK_SG_PROFILE_START(QQuickProfiler::SceneGraphPolishFrame); |
584 | |
585 | m_inPolish = true; |
586 | cd->polishItems(); |
587 | m_inPolish = false; |
588 | |
589 | if (profileFrames) |
590 | polishTime = renderTimer.nsecsElapsed(); |
591 | |
592 | Q_TRACE(QSG_polishItems_exit); |
593 | Q_QUICK_SG_PROFILE_SWITCH(QQuickProfiler::SceneGraphPolishFrame, |
594 | QQuickProfiler::SceneGraphRenderLoopFrame, |
595 | QQuickProfiler::SceneGraphPolishPolish); |
596 | Q_TRACE(QSG_sync_entry); |
597 | |
598 | emit window->afterAnimating(); |
599 | |
600 | // Begin the frame before syncing -> sync is where we may invoke |
601 | // updatePaintNode() on the items and they may want to do resource updates. |
602 | // Also relevant for applications that connect to the before/afterSynchronizing |
603 | // signals and want to do graphics stuff already there. |
604 | if (cd->swapchain) { |
605 | Q_ASSERT(!effectiveOutputSize.isEmpty()); |
606 | const QSize previousOutputSize = cd->swapchain->currentPixelSize(); |
607 | if (previousOutputSize != effectiveOutputSize || cd->swapchainJustBecameRenderable) { |
608 | if (cd->swapchainJustBecameRenderable) |
609 | qCDebug(QSG_LOG_RENDERLOOP, "just became exposed" ); |
610 | |
611 | cd->hasActiveSwapchain = cd->swapchain->createOrResize(); |
612 | if (!cd->hasActiveSwapchain && data.rhi->isDeviceLost()) { |
613 | handleDeviceLoss(); |
614 | return; |
615 | } |
616 | |
617 | cd->swapchainJustBecameRenderable = false; |
618 | cd->hasRenderableSwapchain = cd->hasActiveSwapchain; |
619 | |
620 | if (cd->hasActiveSwapchain) { |
621 | // surface size atomicity: now that buildOrResize() succeeded, |
622 | // query the size that was used in there by the swapchain, and |
623 | // that is the size we will use while preparing the next frame. |
624 | effectiveOutputSize = cd->swapchain->currentPixelSize(); |
625 | qCDebug(QSG_LOG_RENDERLOOP) << "rhi swapchain size" << effectiveOutputSize; |
626 | } else { |
627 | qWarning(msg: "Failed to build or resize swapchain" ); |
628 | } |
629 | } |
630 | |
631 | emit window->beforeFrameBegin(); |
632 | |
633 | Q_ASSERT(data.rhi == cd->rhi); |
634 | QRhi::FrameOpResult frameResult = data.rhi->beginFrame(swapChain: cd->swapchain); |
635 | if (frameResult != QRhi::FrameOpSuccess) { |
636 | if (frameResult == QRhi::FrameOpDeviceLost) |
637 | handleDeviceLoss(); |
638 | else if (frameResult == QRhi::FrameOpError) |
639 | qWarning(msg: "Failed to start frame" ); |
640 | // out of date is not worth warning about - it may happen even during resizing on some platforms |
641 | emit window->afterFrameEnd(); |
642 | return; |
643 | } |
644 | } |
645 | |
646 | // Enable external OpenGL rendering connected to one of the |
647 | // QQuickWindow signals (beforeSynchronizing, beforeRendering, |
648 | // etc.) to function like it did on the direct OpenGL path, |
649 | // i.e. ensure there is a context current, just in case. |
650 | data.rhi->makeThreadLocalNativeContextCurrent(); |
651 | |
652 | cd->syncSceneGraph(); |
653 | if (lastDirtyWindow) |
654 | data.rc->endSync(); |
655 | |
656 | if (profileFrames) |
657 | syncTime = renderTimer.nsecsElapsed(); |
658 | |
659 | Q_TRACE(QSG_sync_exit); |
660 | Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame, |
661 | QQuickProfiler::SceneGraphRenderLoopSync); |
662 | Q_TRACE(QSG_render_entry); |
663 | |
664 | cd->renderSceneGraph(); |
665 | |
666 | if (profileFrames) |
667 | renderTime = renderTimer.nsecsElapsed(); |
668 | Q_TRACE(QSG_render_exit); |
669 | Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame, |
670 | QQuickProfiler::SceneGraphRenderLoopRender); |
671 | Q_TRACE(QSG_swap_entry); |
672 | |
673 | const bool needsPresent = alsoSwap && window->isVisible(); |
674 | double lastCompletedGpuTime = 0; |
675 | if (cd->swapchain) { |
676 | QRhi::EndFrameFlags flags; |
677 | if (!needsPresent) |
678 | flags |= QRhi::SkipPresent; |
679 | QRhi::FrameOpResult frameResult = data.rhi->endFrame(swapChain: cd->swapchain, flags); |
680 | if (frameResult != QRhi::FrameOpSuccess) { |
681 | if (frameResult == QRhi::FrameOpDeviceLost) |
682 | handleDeviceLoss(); |
683 | else if (frameResult == QRhi::FrameOpError) |
684 | qWarning(msg: "Failed to end frame" ); |
685 | } else { |
686 | lastCompletedGpuTime = cd->swapchain->currentFrameCommandBuffer()->lastCompletedGpuTime(); |
687 | } |
688 | } |
689 | if (needsPresent) |
690 | cd->fireFrameSwapped(); |
691 | |
692 | emit window->afterFrameEnd(); |
693 | |
694 | qint64 swapTime = 0; |
695 | if (profileFrames) |
696 | swapTime = renderTimer.nsecsElapsed(); |
697 | |
698 | Q_TRACE(QSG_swap_exit); |
699 | Q_QUICK_SG_PROFILE_END(QQuickProfiler::SceneGraphRenderLoopFrame, |
700 | QQuickProfiler::SceneGraphRenderLoopSwap); |
701 | |
702 | if (profileFrames) { |
703 | qCDebug(QSG_LOG_TIME_RENDERLOOP, |
704 | "[window %p][gui thread] syncAndRender: frame rendered in %dms, polish=%d, sync=%d, render=%d, swap=%d, perWindowFrameDelta=%d" , |
705 | window, |
706 | int(swapTime / 1000000), |
707 | int(polishTime / 1000000), |
708 | int((syncTime - polishTime) / 1000000), |
709 | int((renderTime - syncTime) / 1000000), |
710 | int((swapTime - renderTime) / 1000000), |
711 | int(data.timeBetweenRenders.restart())); |
712 | if (!qFuzzyIsNull(d: lastCompletedGpuTime) && cd->graphicsConfig.timestampsEnabled()) { |
713 | qCDebug(QSG_LOG_TIME_RENDERLOOP, "[window %p][gui thread] syncAndRender: last retrieved GPU frame time was %.4f ms" , |
714 | window, |
715 | lastCompletedGpuTime * 1000.0); |
716 | } |
717 | } |
718 | |
719 | // Might have been set during syncSceneGraph() |
720 | if (data.updatePending) |
721 | maybeUpdate(window); |
722 | } |
723 | |
724 | void QSGGuiThreadRenderLoop::exposureChanged(QQuickWindow *window) |
725 | { |
726 | QQuickWindowPrivate *wd = QQuickWindowPrivate::get(c: window); |
727 | |
728 | // This is tricker than used to be. We want to detect having an empty |
729 | // surface size (which may be the case even when window->size() is |
730 | // non-empty, on some platforms with some graphics APIs!) as well as the |
731 | // case when the window just became "newly exposed" (e.g. after a |
732 | // minimize-restore on Windows, or when switching between fully obscured - |
733 | // not fully obscured on macOS) |
734 | |
735 | if (!window->isExposed() || (wd->hasActiveSwapchain && wd->swapchain->surfacePixelSize().isEmpty())) |
736 | wd->hasRenderableSwapchain = false; |
737 | |
738 | if (window->isExposed() && !wd->hasRenderableSwapchain && wd->hasActiveSwapchain |
739 | && !wd->swapchain->surfacePixelSize().isEmpty()) |
740 | { |
741 | wd->hasRenderableSwapchain = true; |
742 | wd->swapchainJustBecameRenderable = true; |
743 | } |
744 | |
745 | auto winDataIt = m_windows.find(key: window); |
746 | if (winDataIt != m_windows.end()) { |
747 | if (window->isExposed() && (!winDataIt->rhi || !wd->hasActiveSwapchain || wd->hasRenderableSwapchain)) { |
748 | winDataIt->updatePending = true; |
749 | renderWindow(window); |
750 | } |
751 | } |
752 | } |
753 | |
754 | QImage QSGGuiThreadRenderLoop::grab(QQuickWindow *window) |
755 | { |
756 | auto winDataIt = m_windows.find(key: window); |
757 | if (winDataIt == m_windows.end()) |
758 | return QImage(); |
759 | |
760 | if (!ensureRhi(window, data&: *winDataIt)) |
761 | return QImage(); |
762 | |
763 | QQuickWindowPrivate *cd = QQuickWindowPrivate::get(c: window); |
764 | m_inPolish = true; |
765 | cd->polishItems(); |
766 | m_inPolish = false; |
767 | |
768 | // The assumption is that the swapchain is usable since on expose we do a |
769 | // renderWindow() so one cannot get to grab() without having done at least |
770 | // one on-screen frame. |
771 | cd->rhi->beginFrame(swapChain: cd->swapchain); |
772 | cd->rhi->makeThreadLocalNativeContextCurrent(); // for custom GL rendering before/during/after sync |
773 | cd->syncSceneGraph(); |
774 | cd->renderSceneGraph(); |
775 | QImage image = QSGRhiSupport::instance()->grabAndBlockInCurrentFrame(rhi: cd->rhi, cb: cd->swapchain->currentFrameCommandBuffer()); |
776 | cd->rhi->endFrame(swapChain: cd->swapchain, flags: QRhi::SkipPresent); |
777 | |
778 | image.setDevicePixelRatio(window->effectiveDevicePixelRatio()); |
779 | return image; |
780 | } |
781 | |
782 | void QSGGuiThreadRenderLoop::maybeUpdate(QQuickWindow *window) |
783 | { |
784 | auto winDataIt = m_windows.find(key: window); |
785 | if (winDataIt == m_windows.end()) |
786 | return; |
787 | |
788 | // Even if the window is not renderable, |
789 | // renderWindow() called on different window |
790 | // should not delete QSGTexture's |
791 | // from this unrenderable window. |
792 | winDataIt->updatePending = true; |
793 | |
794 | QQuickWindowPrivate *cd = QQuickWindowPrivate::get(c: window); |
795 | if (!cd->isRenderable()) |
796 | return; |
797 | |
798 | // An updatePolish() implementation may call update() to get the QQuickItem |
799 | // dirtied. That's fine but it also leads to calling this function. |
800 | // Requesting another update is a waste then since the updatePolish() call |
801 | // will be followed up with a round of sync and render. |
802 | if (m_inPolish) |
803 | return; |
804 | |
805 | window->requestUpdate(); |
806 | } |
807 | |
808 | QSGContext *QSGGuiThreadRenderLoop::sceneGraphContext() const |
809 | { |
810 | return sg; |
811 | } |
812 | |
813 | QSGRenderContext *QSGGuiThreadRenderLoop::createRenderContext(QSGContext *sg) const |
814 | { |
815 | QSGRenderContext *rc = sg->createRenderContext(); |
816 | pendingRenderContexts.insert(value: rc); |
817 | return rc; |
818 | } |
819 | |
820 | void QSGGuiThreadRenderLoop::releaseResources(QQuickWindow *w) |
821 | { |
822 | // No full invalidation of the rendercontext, just clear some caches. |
823 | QQuickWindowPrivate *d = QQuickWindowPrivate::get(c: w); |
824 | emit d->context->releaseCachedResourcesRequested(); |
825 | if (d->renderer) |
826 | d->renderer->releaseCachedResources(); |
827 | #if QT_CONFIG(quick_shadereffect) |
828 | QSGRhiShaderEffectNode::garbageCollectMaterialTypeCache(materialTypeCacheKey: w); |
829 | #endif |
830 | } |
831 | |
832 | void QSGGuiThreadRenderLoop::handleUpdateRequest(QQuickWindow *window) |
833 | { |
834 | renderWindow(window); |
835 | } |
836 | |
837 | #endif // ENABLE_DEFAULT_BACKEND |
838 | |
839 | QT_END_NAMESPACE |
840 | |
841 | #include "qsgrenderloop.moc" |
842 | #include "moc_qsgrenderloop_p.cpp" |
843 | |