blob: 79ffee76893407331f633fea5287a8ecafff77c3 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtQuick module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qsgd3d12renderloop_p.h"
#include "qsgd3d12engine_p.h"
#include "qsgd3d12context_p.h"
#include "qsgd3d12rendercontext_p.h"
#include "qsgd3d12shadereffectnode_p.h"
#include <private/qquickwindow_p.h>
#include <private/qquickprofiler_p.h>
#include <private/qquickanimatorcontroller_p.h>
#include <QElapsedTimer>
#include <QGuiApplication>
#include <QScreen>
#include <qtquick_tracepoints_p.h>
QT_BEGIN_NAMESPACE
// NOTE: Avoid categorized logging. It is slow.
#define DECLARE_DEBUG_VAR(variable) \
static bool debug_ ## variable() \
{ static bool value = qgetenv("QSG_RENDERER_DEBUG").contains(QT_STRINGIFY(variable)); return value; }
DECLARE_DEBUG_VAR(loop)
DECLARE_DEBUG_VAR(time)
// This render loop operates on the gui (main) thread.
// Conceptually it matches the OpenGL 'windows' render loop.
static inline int qsgrl_animation_interval()
{
const qreal refreshRate = QGuiApplication::primaryScreen() ? QGuiApplication::primaryScreen()->refreshRate() : 0;
return refreshRate < 1 ? 16 : int(1000 / refreshRate);
}
QSGD3D12RenderLoop::QSGD3D12RenderLoop()
{
if (Q_UNLIKELY(debug_loop()))
qDebug("new d3d12 render loop");
sg = new QSGD3D12Context;
m_anims = sg->createAnimationDriver(this);
connect(m_anims, &QAnimationDriver::started, this, &QSGD3D12RenderLoop::onAnimationStarted);
connect(m_anims, &QAnimationDriver::stopped, this, &QSGD3D12RenderLoop::onAnimationStopped);
m_anims->install();
m_vsyncDelta = qsgrl_animation_interval();
}
QSGD3D12RenderLoop::~QSGD3D12RenderLoop()
{
delete sg;
}
void QSGD3D12RenderLoop::show(QQuickWindow *window)
{
if (Q_UNLIKELY(debug_loop()))
qDebug() << "show" << window;
}
void QSGD3D12RenderLoop::hide(QQuickWindow *window)
{
if (Q_UNLIKELY(debug_loop()))
qDebug() << "hide" << window;
}
void QSGD3D12RenderLoop::resize(QQuickWindow *window)
{
if (!m_windows.contains(window) || window->size().isEmpty())
return;
if (Q_UNLIKELY(debug_loop()))
qDebug() << "resize" << window;
const WindowData &data(m_windows[window]);
if (!data.exposed)
return;
if (data.engine)
data.engine->setWindowSize(window->size(), window->effectiveDevicePixelRatio());
}
void QSGD3D12RenderLoop::windowDestroyed(QQuickWindow *window)
{
if (Q_UNLIKELY(debug_loop()))
qDebug() << "window destroyed" << window;
if (!m_windows.contains(window))
return;
QQuickWindowPrivate *wd = QQuickWindowPrivate::get(window);
wd->fireAboutToStop();
WindowData &data(m_windows[window]);
QSGD3D12Engine *engine = data.engine;
QSGD3D12RenderContext *rc = data.rc;
m_windows.remove(window);
// QSGNode destruction may release graphics resources in use so wait first.
engine->waitGPU();
// Bye bye nodes...
wd->cleanupNodesOnShutdown();
QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache();
rc->invalidate();
delete rc;
delete engine;
wd->animationController.reset();
}
void QSGD3D12RenderLoop::exposeWindow(QQuickWindow *window)
{
WindowData data;
data.exposed = true;
data.engine = new QSGD3D12Engine;
data.rc = static_cast<QSGD3D12RenderContext *>(QQuickWindowPrivate::get(window)->context);
data.rc->setEngine(data.engine);
m_windows[window] = data;
const int samples = window->format().samples();
const bool alpha = window->format().alphaBufferSize() > 0;
const qreal dpr = window->effectiveDevicePixelRatio();
if (Q_UNLIKELY(debug_loop()))
qDebug() << "initializing D3D12 engine" << window << window->size() << dpr << samples << alpha;
data.engine->attachToWindow(window->winId(), window->size(), dpr, samples, alpha);
}
void QSGD3D12RenderLoop::obscureWindow(QQuickWindow *window)
{
m_windows[window].exposed = false;
QQuickWindowPrivate *wd = QQuickWindowPrivate::get(window);
wd->fireAboutToStop();
}
void QSGD3D12RenderLoop::exposureChanged(QQuickWindow *window)
{
if (Q_UNLIKELY(debug_loop()))
qDebug() << "exposure changed" << window << window->isExposed();
if (window->isExposed()) {
if (!m_windows.contains(window))
exposeWindow(window);
// Stop non-visual animation timer as we now have a window rendering.
if (m_animationTimer && somethingVisible()) {
killTimer(m_animationTimer);
m_animationTimer = 0;
}
// If we have a pending timer and we get an expose, we need to stop it.
// Otherwise we get two frames and two animation ticks in the same time interval.
if (m_updateTimer) {
killTimer(m_updateTimer);
m_updateTimer = 0;
}
WindowData &data(m_windows[window]);
data.exposed = true;
data.updatePending = true;
render();
} else if (m_windows.contains(window)) {
obscureWindow(window);
// Potentially start the non-visual animation timer if nobody is rendering.
if (m_anims->isRunning() && !somethingVisible() && !m_animationTimer)
m_animationTimer = startTimer(m_vsyncDelta);
}
}
QImage QSGD3D12RenderLoop::grab(QQuickWindow *window)
{
const bool tempExpose = !m_windows.contains(window);
if (tempExpose)
exposeWindow(window);
m_windows[window].grabOnly = true;
renderWindow(window);
QImage grabbed = m_grabContent;
m_grabContent = QImage();
if (tempExpose)
obscureWindow(window);
return grabbed;
}
bool QSGD3D12RenderLoop::somethingVisible() const
{
for (auto it = m_windows.constBegin(), itEnd = m_windows.constEnd(); it != itEnd; ++it) {
if (it.key()->isVisible() && it.key()->isExposed())
return true;
}
return false;
}
void QSGD3D12RenderLoop::maybePostUpdateTimer()
{
if (!m_updateTimer) {
if (Q_UNLIKELY(debug_loop()))
qDebug("starting update timer");
m_updateTimer = startTimer(m_vsyncDelta / 3);
}
}
void QSGD3D12RenderLoop::update(QQuickWindow *window)
{
maybeUpdate(window);
}
void QSGD3D12RenderLoop::maybeUpdate(QQuickWindow *window)
{
if (!m_windows.contains(window) || !somethingVisible())
return;
m_windows[window].updatePending = true;
maybePostUpdateTimer();
}
QAnimationDriver *QSGD3D12RenderLoop::animationDriver() const
{
return m_anims;
}
QSGContext *QSGD3D12RenderLoop::sceneGraphContext() const
{
return sg;
}
QSGRenderContext *QSGD3D12RenderLoop::createRenderContext(QSGContext *) const
{
// The rendercontext and engine are per-window, like with the threaded
// loop, but unlike the non-threaded OpenGL variants.
return sg->createRenderContext();
}
void QSGD3D12RenderLoop::releaseResources(QQuickWindow *window)
{
if (Q_UNLIKELY(debug_loop()))
qDebug() << "releaseResources" << window;
}
void QSGD3D12RenderLoop::postJob(QQuickWindow *window, QRunnable *job)
{
Q_UNUSED(window);
Q_ASSERT(job);
Q_ASSERT(window);
job->run();
delete job;
}
QSurface::SurfaceType QSGD3D12RenderLoop::windowSurfaceType() const
{
return QSurface::OpenGLSurface;
}
bool QSGD3D12RenderLoop::interleaveIncubation() const
{
return m_anims->isRunning() && somethingVisible();
}
void QSGD3D12RenderLoop::onAnimationStarted()
{
if (!somethingVisible()) {
if (!m_animationTimer) {
if (Q_UNLIKELY(debug_loop()))
qDebug("starting non-visual animation timer");
m_animationTimer = startTimer(m_vsyncDelta);
}
} else {
maybePostUpdateTimer();
}
}
void QSGD3D12RenderLoop::onAnimationStopped()
{
if (m_animationTimer) {
if (Q_UNLIKELY(debug_loop()))
qDebug("stopping non-visual animation timer");
killTimer(m_animationTimer);
m_animationTimer = 0;
}
}
bool QSGD3D12RenderLoop::event(QEvent *event)
{
switch (event->type()) {
case QEvent::Timer:
{
QTimerEvent *te = static_cast<QTimerEvent *>(event);
if (te->timerId() == m_animationTimer) {
if (Q_UNLIKELY(debug_loop()))
qDebug("animation tick while no windows exposed");
m_anims->advance();
} else if (te->timerId() == m_updateTimer) {
if (Q_UNLIKELY(debug_loop()))
qDebug("update timeout - rendering");
killTimer(m_updateTimer);
m_updateTimer = 0;
render();
}
return true;
}
default:
break;
}
return QObject::event(event);
}
void QSGD3D12RenderLoop::render()
{
bool rendered = false;
for (auto it = m_windows.begin(), itEnd = m_windows.end(); it != itEnd; ++it) {
if (it->updatePending) {
it->updatePending = false;
renderWindow(it.key());
rendered = true;
}
}
if (!rendered) {
if (Q_UNLIKELY(debug_loop()))
qDebug("render - no changes, sleep");
QThread::msleep(m_vsyncDelta);
}
if (m_anims->isRunning()) {
if (Q_UNLIKELY(debug_loop()))
qDebug("render - advancing animations");
m_anims->advance();
// It is not given that animations triggered another maybeUpdate()
// and thus another render pass, so to keep things running,
// make sure there is another frame pending.
maybePostUpdateTimer();
emit timeToIncubate();
}
}
void QSGD3D12RenderLoop::renderWindow(QQuickWindow *window)
{
if (Q_UNLIKELY(debug_loop()))
qDebug() << "renderWindow" << window;
QQuickWindowPrivate *wd = QQuickWindowPrivate::get(window);
if (!m_windows.contains(window) || !window->geometry().isValid())
return;
WindowData &data(m_windows[window]);
if (!data.exposed) { // not the same as window->isExposed(), when grabbing invisible windows for instance
if (Q_UNLIKELY(debug_loop()))
qDebug("renderWindow - not exposed, abort");
return;
}
Q_TRACE_SCOPE(QSG_renderWindow);
if (!data.grabOnly)
wd->flushFrameSynchronousEvents();
QElapsedTimer renderTimer;
qint64 renderTime = 0, syncTime = 0, polishTime = 0;
const bool profileFrames = debug_time();
if (profileFrames)
renderTimer.start();
Q_QUICK_SG_PROFILE_START(QQuickProfiler::SceneGraphPolishFrame);
Q_TRACE(QSG_polishItems_entry);
wd->polishItems();
if (profileFrames)
polishTime = renderTimer.nsecsElapsed();
Q_TRACE(QSG_polishItems_exit);
Q_QUICK_SG_PROFILE_SWITCH(QQuickProfiler::SceneGraphPolishFrame,
QQuickProfiler::SceneGraphRenderLoopFrame,
QQuickProfiler::SceneGraphPolishPolish);
Q_TRACE(QSG_sync_entry);
emit window->afterAnimating();
// The native window may change in some (quite artificial) cases, e.g. due
// to a hide - destroy - show on the QWindow.
bool needsWindow = !data.engine->window();
if (data.engine->window() && data.engine->window() != window->winId()) {
if (Q_UNLIKELY(debug_loop()))
qDebug("sync - native window handle changes for active engine");
data.engine->waitGPU();
wd->cleanupNodesOnShutdown();
QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache();
data.rc->invalidate();
data.engine->releaseResources();
needsWindow = true;
}
if (needsWindow) {
// Must only ever get here when there is no window or releaseResources() has been called.
const int samples = window->format().samples();
const bool alpha = window->format().alphaBufferSize() > 0;
const qreal dpr = window->effectiveDevicePixelRatio();
if (Q_UNLIKELY(debug_loop()))
qDebug() << "sync - reinitializing D3D12 engine" << window << window->size() << dpr << samples << alpha;
data.engine->attachToWindow(window->winId(), window->size(), dpr, samples, alpha);
}
// Recover from device loss.
if (!data.engine->hasResources()) {
if (Q_UNLIKELY(debug_loop()))
qDebug("sync - device was lost, resetting scenegraph");
wd->cleanupNodesOnShutdown();
QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache();
data.rc->invalidate();
}
data.rc->initialize(nullptr);
wd->syncSceneGraph();
data.rc->endSync();
if (profileFrames)
syncTime = renderTimer.nsecsElapsed();
Q_TRACE(QSG_sync_exit);
Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame,
QQuickProfiler::SceneGraphRenderLoopSync);
Q_TRACE(QSG_render_entry);
wd->renderSceneGraph(window->size());
if (profileFrames)
renderTime = renderTimer.nsecsElapsed();
Q_TRACE(QSG_render_exit);
Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame,
QQuickProfiler::SceneGraphRenderLoopRender);
Q_TRACE(QSG_swap_entry);
if (!data.grabOnly) {
// The engine is able to have multiple frames in flight. This in effect is
// similar to BufferQueueingOpenGL. Provide an env var to force the
// traditional blocking swap behavior, just in case.
static bool blockOnEachFrame = qEnvironmentVariableIntValue("QT_D3D_BLOCKING_PRESENT") != 0;
if (window->isVisible()) {
data.engine->present();
if (blockOnEachFrame)
data.engine->waitGPU();
// The concept of "frame swaps" is quite misleading by default, when
// blockOnEachFrame is not used, but emit it for compatibility.
wd->fireFrameSwapped();
} else {
if (blockOnEachFrame)
data.engine->waitGPU();
}
} else {
m_grabContent = data.engine->executeAndWaitReadbackRenderTarget();
data.grabOnly = false;
}
qint64 swapTime = 0;
if (profileFrames)
swapTime = renderTimer.nsecsElapsed();
Q_TRACE(QSG_swap_exit);
Q_QUICK_SG_PROFILE_END(QQuickProfiler::SceneGraphRenderLoopFrame,
QQuickProfiler::SceneGraphRenderLoopSwap);
if (Q_UNLIKELY(debug_time())) {
static QTime lastFrameTime = QTime::currentTime();
qDebug("Frame rendered with 'd3d12' renderloop in %dms, polish=%d, sync=%d, render=%d, swap=%d, frameDelta=%d",
int(swapTime / 1000000),
int(polishTime / 1000000),
int((syncTime - polishTime) / 1000000),
int((renderTime - syncTime) / 1000000),
int((swapTime - renderTime) / 10000000),
int(lastFrameTime.msecsTo(QTime::currentTime())));
lastFrameTime = QTime::currentTime();
}
// Simulate device loss if requested.
static int devLossTest = qEnvironmentVariableIntValue("QT_D3D_TEST_DEVICE_LOSS");
if (devLossTest > 0) {
static QElapsedTimer kt;
static bool timerRunning = false;
if (!timerRunning) {
kt.start();
timerRunning = true;
} else if (kt.elapsed() > 5000) {
--devLossTest;
kt.restart();
data.engine->simulateDeviceLoss();
}
}
}
int QSGD3D12RenderLoop::flags() const
{
return SupportsGrabWithoutExpose;
}
QT_END_NAMESPACE