1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the QtQuick module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39
40 #include "qsgd3d12renderloop_p.h"
41 #include "qsgd3d12engine_p.h"
42 #include "qsgd3d12context_p.h"
43 #include "qsgd3d12rendercontext_p.h"
44 #include "qsgd3d12shadereffectnode_p.h"
45 #include <private/qquickwindow_p.h>
46 #include <private/qquickprofiler_p.h>
47 #include <private/qquickanimatorcontroller_p.h>
48 #include <QElapsedTimer>
49 #include <QGuiApplication>
50 #include <QScreen>
51
52 #include <qtquick_tracepoints_p.h>
53
54 QT_BEGIN_NAMESPACE
55
56 // NOTE: Avoid categorized logging. It is slow.
57
58 #define DECLARE_DEBUG_VAR(variable) \
59 static bool debug_ ## variable() \
60 { static bool value = qgetenv("QSG_RENDERER_DEBUG").contains(QT_STRINGIFY(variable)); return value; }
61
DECLARE_DEBUG_VAR(loop)62 DECLARE_DEBUG_VAR(loop)
63 DECLARE_DEBUG_VAR(time)
64
65
66 // This render loop operates on the gui (main) thread.
67 // Conceptually it matches the OpenGL 'windows' render loop.
68
69 static inline int qsgrl_animation_interval()
70 {
71 const qreal refreshRate = QGuiApplication::primaryScreen() ? QGuiApplication::primaryScreen()->refreshRate() : 0;
72 return refreshRate < 1 ? 16 : int(1000 / refreshRate);
73 }
74
QSGD3D12RenderLoop()75 QSGD3D12RenderLoop::QSGD3D12RenderLoop()
76 {
77 if (Q_UNLIKELY(debug_loop()))
78 qDebug("new d3d12 render loop");
79
80 sg = new QSGD3D12Context;
81
82 m_anims = sg->createAnimationDriver(this);
83 connect(m_anims, &QAnimationDriver::started, this, &QSGD3D12RenderLoop::onAnimationStarted);
84 connect(m_anims, &QAnimationDriver::stopped, this, &QSGD3D12RenderLoop::onAnimationStopped);
85 m_anims->install();
86
87 m_vsyncDelta = qsgrl_animation_interval();
88 }
89
~QSGD3D12RenderLoop()90 QSGD3D12RenderLoop::~QSGD3D12RenderLoop()
91 {
92 delete sg;
93 }
94
show(QQuickWindow * window)95 void QSGD3D12RenderLoop::show(QQuickWindow *window)
96 {
97 if (Q_UNLIKELY(debug_loop()))
98 qDebug() << "show" << window;
99 }
100
hide(QQuickWindow * window)101 void QSGD3D12RenderLoop::hide(QQuickWindow *window)
102 {
103 if (Q_UNLIKELY(debug_loop()))
104 qDebug() << "hide" << window;
105 }
106
resize(QQuickWindow * window)107 void QSGD3D12RenderLoop::resize(QQuickWindow *window)
108 {
109 if (!m_windows.contains(window) || window->size().isEmpty())
110 return;
111
112 if (Q_UNLIKELY(debug_loop()))
113 qDebug() << "resize" << window;
114
115 const WindowData &data(m_windows[window]);
116
117 if (!data.exposed)
118 return;
119
120 if (data.engine)
121 data.engine->setWindowSize(window->size(), window->effectiveDevicePixelRatio());
122 }
123
windowDestroyed(QQuickWindow * window)124 void QSGD3D12RenderLoop::windowDestroyed(QQuickWindow *window)
125 {
126 if (Q_UNLIKELY(debug_loop()))
127 qDebug() << "window destroyed" << window;
128
129 if (!m_windows.contains(window))
130 return;
131
132 QQuickWindowPrivate *wd = QQuickWindowPrivate::get(window);
133 wd->fireAboutToStop();
134
135 WindowData &data(m_windows[window]);
136 QSGD3D12Engine *engine = data.engine;
137 QSGD3D12RenderContext *rc = data.rc;
138 m_windows.remove(window);
139
140 // QSGNode destruction may release graphics resources in use so wait first.
141 engine->waitGPU();
142
143 // Bye bye nodes...
144 wd->cleanupNodesOnShutdown();
145
146 QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache();
147
148 rc->invalidate();
149
150 delete rc;
151 delete engine;
152
153 wd->animationController.reset();
154 }
155
exposeWindow(QQuickWindow * window)156 void QSGD3D12RenderLoop::exposeWindow(QQuickWindow *window)
157 {
158 WindowData data;
159 data.exposed = true;
160 data.engine = new QSGD3D12Engine;
161 data.rc = static_cast<QSGD3D12RenderContext *>(QQuickWindowPrivate::get(window)->context);
162 data.rc->setEngine(data.engine);
163 m_windows[window] = data;
164
165 const int samples = window->format().samples();
166 const bool alpha = window->format().alphaBufferSize() > 0;
167 const qreal dpr = window->effectiveDevicePixelRatio();
168
169 if (Q_UNLIKELY(debug_loop()))
170 qDebug() << "initializing D3D12 engine" << window << window->size() << dpr << samples << alpha;
171
172 data.engine->attachToWindow(window->winId(), window->size(), dpr, samples, alpha);
173 }
174
obscureWindow(QQuickWindow * window)175 void QSGD3D12RenderLoop::obscureWindow(QQuickWindow *window)
176 {
177 m_windows[window].exposed = false;
178 QQuickWindowPrivate *wd = QQuickWindowPrivate::get(window);
179 wd->fireAboutToStop();
180 }
181
exposureChanged(QQuickWindow * window)182 void QSGD3D12RenderLoop::exposureChanged(QQuickWindow *window)
183 {
184 if (Q_UNLIKELY(debug_loop()))
185 qDebug() << "exposure changed" << window << window->isExposed();
186
187 if (window->isExposed()) {
188 if (!m_windows.contains(window))
189 exposeWindow(window);
190
191 // Stop non-visual animation timer as we now have a window rendering.
192 if (m_animationTimer && somethingVisible()) {
193 killTimer(m_animationTimer);
194 m_animationTimer = 0;
195 }
196 // If we have a pending timer and we get an expose, we need to stop it.
197 // Otherwise we get two frames and two animation ticks in the same time interval.
198 if (m_updateTimer) {
199 killTimer(m_updateTimer);
200 m_updateTimer = 0;
201 }
202
203 WindowData &data(m_windows[window]);
204 data.exposed = true;
205 data.updatePending = true;
206
207 render();
208
209 } else if (m_windows.contains(window)) {
210 obscureWindow(window);
211
212 // Potentially start the non-visual animation timer if nobody is rendering.
213 if (m_anims->isRunning() && !somethingVisible() && !m_animationTimer)
214 m_animationTimer = startTimer(m_vsyncDelta);
215 }
216 }
217
grab(QQuickWindow * window)218 QImage QSGD3D12RenderLoop::grab(QQuickWindow *window)
219 {
220 const bool tempExpose = !m_windows.contains(window);
221 if (tempExpose)
222 exposeWindow(window);
223
224 m_windows[window].grabOnly = true;
225
226 renderWindow(window);
227
228 QImage grabbed = m_grabContent;
229 m_grabContent = QImage();
230
231 if (tempExpose)
232 obscureWindow(window);
233
234 return grabbed;
235 }
236
somethingVisible() const237 bool QSGD3D12RenderLoop::somethingVisible() const
238 {
239 for (auto it = m_windows.constBegin(), itEnd = m_windows.constEnd(); it != itEnd; ++it) {
240 if (it.key()->isVisible() && it.key()->isExposed())
241 return true;
242 }
243 return false;
244 }
245
maybePostUpdateTimer()246 void QSGD3D12RenderLoop::maybePostUpdateTimer()
247 {
248 if (!m_updateTimer) {
249 if (Q_UNLIKELY(debug_loop()))
250 qDebug("starting update timer");
251 m_updateTimer = startTimer(m_vsyncDelta / 3);
252 }
253 }
254
update(QQuickWindow * window)255 void QSGD3D12RenderLoop::update(QQuickWindow *window)
256 {
257 maybeUpdate(window);
258 }
259
maybeUpdate(QQuickWindow * window)260 void QSGD3D12RenderLoop::maybeUpdate(QQuickWindow *window)
261 {
262 if (!m_windows.contains(window) || !somethingVisible())
263 return;
264
265 m_windows[window].updatePending = true;
266 maybePostUpdateTimer();
267 }
268
animationDriver() const269 QAnimationDriver *QSGD3D12RenderLoop::animationDriver() const
270 {
271 return m_anims;
272 }
273
sceneGraphContext() const274 QSGContext *QSGD3D12RenderLoop::sceneGraphContext() const
275 {
276 return sg;
277 }
278
createRenderContext(QSGContext *) const279 QSGRenderContext *QSGD3D12RenderLoop::createRenderContext(QSGContext *) const
280 {
281 // The rendercontext and engine are per-window, like with the threaded
282 // loop, but unlike the non-threaded OpenGL variants.
283 return sg->createRenderContext();
284 }
285
releaseResources(QQuickWindow * window)286 void QSGD3D12RenderLoop::releaseResources(QQuickWindow *window)
287 {
288 if (Q_UNLIKELY(debug_loop()))
289 qDebug() << "releaseResources" << window;
290 }
291
postJob(QQuickWindow * window,QRunnable * job)292 void QSGD3D12RenderLoop::postJob(QQuickWindow *window, QRunnable *job)
293 {
294 Q_UNUSED(window);
295 Q_ASSERT(job);
296 Q_ASSERT(window);
297 job->run();
298 delete job;
299 }
300
windowSurfaceType() const301 QSurface::SurfaceType QSGD3D12RenderLoop::windowSurfaceType() const
302 {
303 return QSurface::OpenGLSurface;
304 }
305
interleaveIncubation() const306 bool QSGD3D12RenderLoop::interleaveIncubation() const
307 {
308 return m_anims->isRunning() && somethingVisible();
309 }
310
onAnimationStarted()311 void QSGD3D12RenderLoop::onAnimationStarted()
312 {
313 if (!somethingVisible()) {
314 if (!m_animationTimer) {
315 if (Q_UNLIKELY(debug_loop()))
316 qDebug("starting non-visual animation timer");
317 m_animationTimer = startTimer(m_vsyncDelta);
318 }
319 } else {
320 maybePostUpdateTimer();
321 }
322 }
323
onAnimationStopped()324 void QSGD3D12RenderLoop::onAnimationStopped()
325 {
326 if (m_animationTimer) {
327 if (Q_UNLIKELY(debug_loop()))
328 qDebug("stopping non-visual animation timer");
329 killTimer(m_animationTimer);
330 m_animationTimer = 0;
331 }
332 }
333
event(QEvent * event)334 bool QSGD3D12RenderLoop::event(QEvent *event)
335 {
336 switch (event->type()) {
337 case QEvent::Timer:
338 {
339 QTimerEvent *te = static_cast<QTimerEvent *>(event);
340 if (te->timerId() == m_animationTimer) {
341 if (Q_UNLIKELY(debug_loop()))
342 qDebug("animation tick while no windows exposed");
343 m_anims->advance();
344 } else if (te->timerId() == m_updateTimer) {
345 if (Q_UNLIKELY(debug_loop()))
346 qDebug("update timeout - rendering");
347 killTimer(m_updateTimer);
348 m_updateTimer = 0;
349 render();
350 }
351 return true;
352 }
353 default:
354 break;
355 }
356
357 return QObject::event(event);
358 }
359
render()360 void QSGD3D12RenderLoop::render()
361 {
362 bool rendered = false;
363 for (auto it = m_windows.begin(), itEnd = m_windows.end(); it != itEnd; ++it) {
364 if (it->updatePending) {
365 it->updatePending = false;
366 renderWindow(it.key());
367 rendered = true;
368 }
369 }
370
371 if (!rendered) {
372 if (Q_UNLIKELY(debug_loop()))
373 qDebug("render - no changes, sleep");
374 QThread::msleep(m_vsyncDelta);
375 }
376
377 if (m_anims->isRunning()) {
378 if (Q_UNLIKELY(debug_loop()))
379 qDebug("render - advancing animations");
380
381 m_anims->advance();
382
383 // It is not given that animations triggered another maybeUpdate()
384 // and thus another render pass, so to keep things running,
385 // make sure there is another frame pending.
386 maybePostUpdateTimer();
387
388 emit timeToIncubate();
389 }
390 }
391
renderWindow(QQuickWindow * window)392 void QSGD3D12RenderLoop::renderWindow(QQuickWindow *window)
393 {
394 if (Q_UNLIKELY(debug_loop()))
395 qDebug() << "renderWindow" << window;
396
397 QQuickWindowPrivate *wd = QQuickWindowPrivate::get(window);
398 if (!m_windows.contains(window) || !window->geometry().isValid())
399 return;
400
401 WindowData &data(m_windows[window]);
402 if (!data.exposed) { // not the same as window->isExposed(), when grabbing invisible windows for instance
403 if (Q_UNLIKELY(debug_loop()))
404 qDebug("renderWindow - not exposed, abort");
405 return;
406 }
407
408 Q_TRACE_SCOPE(QSG_renderWindow);
409
410 if (!data.grabOnly)
411 wd->flushFrameSynchronousEvents();
412
413 QElapsedTimer renderTimer;
414 qint64 renderTime = 0, syncTime = 0, polishTime = 0;
415 const bool profileFrames = debug_time();
416 if (profileFrames)
417 renderTimer.start();
418 Q_QUICK_SG_PROFILE_START(QQuickProfiler::SceneGraphPolishFrame);
419 Q_TRACE(QSG_polishItems_entry);
420
421 wd->polishItems();
422
423 if (profileFrames)
424 polishTime = renderTimer.nsecsElapsed();
425 Q_TRACE(QSG_polishItems_exit);
426 Q_QUICK_SG_PROFILE_SWITCH(QQuickProfiler::SceneGraphPolishFrame,
427 QQuickProfiler::SceneGraphRenderLoopFrame,
428 QQuickProfiler::SceneGraphPolishPolish);
429 Q_TRACE(QSG_sync_entry);
430
431 emit window->afterAnimating();
432
433 // The native window may change in some (quite artificial) cases, e.g. due
434 // to a hide - destroy - show on the QWindow.
435 bool needsWindow = !data.engine->window();
436 if (data.engine->window() && data.engine->window() != window->winId()) {
437 if (Q_UNLIKELY(debug_loop()))
438 qDebug("sync - native window handle changes for active engine");
439 data.engine->waitGPU();
440 wd->cleanupNodesOnShutdown();
441 QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache();
442 data.rc->invalidate();
443 data.engine->releaseResources();
444 needsWindow = true;
445 }
446 if (needsWindow) {
447 // Must only ever get here when there is no window or releaseResources() has been called.
448 const int samples = window->format().samples();
449 const bool alpha = window->format().alphaBufferSize() > 0;
450 const qreal dpr = window->effectiveDevicePixelRatio();
451 if (Q_UNLIKELY(debug_loop()))
452 qDebug() << "sync - reinitializing D3D12 engine" << window << window->size() << dpr << samples << alpha;
453 data.engine->attachToWindow(window->winId(), window->size(), dpr, samples, alpha);
454 }
455
456 // Recover from device loss.
457 if (!data.engine->hasResources()) {
458 if (Q_UNLIKELY(debug_loop()))
459 qDebug("sync - device was lost, resetting scenegraph");
460 wd->cleanupNodesOnShutdown();
461 QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache();
462 data.rc->invalidate();
463 }
464
465 data.rc->initialize(nullptr);
466
467 wd->syncSceneGraph();
468 data.rc->endSync();
469
470 if (profileFrames)
471 syncTime = renderTimer.nsecsElapsed();
472 Q_TRACE(QSG_sync_exit);
473 Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame,
474 QQuickProfiler::SceneGraphRenderLoopSync);
475 Q_TRACE(QSG_render_entry);
476
477 wd->renderSceneGraph(window->size());
478
479 if (profileFrames)
480 renderTime = renderTimer.nsecsElapsed();
481 Q_TRACE(QSG_render_exit);
482 Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame,
483 QQuickProfiler::SceneGraphRenderLoopRender);
484 Q_TRACE(QSG_swap_entry);
485
486 if (!data.grabOnly) {
487 // The engine is able to have multiple frames in flight. This in effect is
488 // similar to BufferQueueingOpenGL. Provide an env var to force the
489 // traditional blocking swap behavior, just in case.
490 static bool blockOnEachFrame = qEnvironmentVariableIntValue("QT_D3D_BLOCKING_PRESENT") != 0;
491
492 if (window->isVisible()) {
493 data.engine->present();
494 if (blockOnEachFrame)
495 data.engine->waitGPU();
496 // The concept of "frame swaps" is quite misleading by default, when
497 // blockOnEachFrame is not used, but emit it for compatibility.
498 wd->fireFrameSwapped();
499 } else {
500 if (blockOnEachFrame)
501 data.engine->waitGPU();
502 }
503 } else {
504 m_grabContent = data.engine->executeAndWaitReadbackRenderTarget();
505 data.grabOnly = false;
506 }
507
508 qint64 swapTime = 0;
509 if (profileFrames)
510 swapTime = renderTimer.nsecsElapsed();
511 Q_TRACE(QSG_swap_exit);
512 Q_QUICK_SG_PROFILE_END(QQuickProfiler::SceneGraphRenderLoopFrame,
513 QQuickProfiler::SceneGraphRenderLoopSwap);
514
515 if (Q_UNLIKELY(debug_time())) {
516 static QTime lastFrameTime = QTime::currentTime();
517 qDebug("Frame rendered with 'd3d12' renderloop in %dms, polish=%d, sync=%d, render=%d, swap=%d, frameDelta=%d",
518 int(swapTime / 1000000),
519 int(polishTime / 1000000),
520 int((syncTime - polishTime) / 1000000),
521 int((renderTime - syncTime) / 1000000),
522 int((swapTime - renderTime) / 10000000),
523 int(lastFrameTime.msecsTo(QTime::currentTime())));
524 lastFrameTime = QTime::currentTime();
525 }
526
527 // Simulate device loss if requested.
528 static int devLossTest = qEnvironmentVariableIntValue("QT_D3D_TEST_DEVICE_LOSS");
529 if (devLossTest > 0) {
530 static QElapsedTimer kt;
531 static bool timerRunning = false;
532 if (!timerRunning) {
533 kt.start();
534 timerRunning = true;
535 } else if (kt.elapsed() > 5000) {
536 --devLossTest;
537 kt.restart();
538 data.engine->simulateDeviceLoss();
539 }
540 }
541 }
542
flags() const543 int QSGD3D12RenderLoop::flags() const
544 {
545 return SupportsGrabWithoutExpose;
546 }
547
548 QT_END_NAMESPACE
549