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