diff options
author | Laszlo Agocs <laszlo.agocs@theqtcompany.com> | 2016-06-09 16:26:05 +0200 |
---|---|---|
committer | Laszlo Agocs <laszlo.agocs@qt.io> | 2016-06-24 17:56:00 +0000 |
commit | 886480688dbebf428eaa387a9f52ceaf7b0ae9d9 (patch) | |
tree | 5d4fb385012153419c93a17ba0a7b38648358bf0 | |
parent | 4412d22c26ba4a187837b0565c66e5b51de85b3c (diff) |
D3D12: Reintroduce the single threaded render loop
...and use it by default. Use QSG_RENDER_LOOP=threaded to request the
threaded one (but prepare for potential DXGI deadlocks, usually when
multiple windows are involved).
Change-Id: I1372b4a8f2388e08515899792e2a9c347b31faf8
Reviewed-by: Andy Nichols <andy.nichols@qt.io>
-rw-r--r-- | src/plugins/scenegraph/d3d12/d3d12.pro | 2 | ||||
-rw-r--r-- | src/plugins/scenegraph/d3d12/qsgd3d12adaptation.cpp | 11 | ||||
-rw-r--r-- | src/plugins/scenegraph/d3d12/qsgd3d12renderloop.cpp | 1143 | ||||
-rw-r--r-- | src/plugins/scenegraph/d3d12/qsgd3d12renderloop_p.h | 35 | ||||
-rw-r--r-- | src/plugins/scenegraph/d3d12/qsgd3d12threadedrenderloop.cpp | 1187 | ||||
-rw-r--r-- | src/plugins/scenegraph/d3d12/qsgd3d12threadedrenderloop_p.h | 129 |
6 files changed, 1516 insertions, 991 deletions
diff --git a/src/plugins/scenegraph/d3d12/d3d12.pro b/src/plugins/scenegraph/d3d12/d3d12.pro index c814c11de0..5d1b7a4946 100644 --- a/src/plugins/scenegraph/d3d12/d3d12.pro +++ b/src/plugins/scenegraph/d3d12/d3d12.pro @@ -12,6 +12,7 @@ QMAKE_TARGET_DESCRIPTION = "Quick D3D12 Renderer for Qt." SOURCES += \ $$PWD/qsgd3d12adaptation.cpp \ $$PWD/qsgd3d12renderloop.cpp \ + $$PWD/qsgd3d12threadedrenderloop.cpp \ $$PWD/qsgd3d12renderer.cpp \ $$PWD/qsgd3d12context.cpp \ $$PWD/qsgd3d12rendercontext.cpp \ @@ -33,6 +34,7 @@ NO_PCH_SOURCES += \ HEADERS += \ $$PWD/qsgd3d12adaptation_p.h \ $$PWD/qsgd3d12renderloop_p.h \ + $$PWD/qsgd3d12threadedrenderloop_p.h \ $$PWD/qsgd3d12renderer_p.h \ $$PWD/qsgd3d12context_p.h \ $$PWD/qsgd3d12rendercontext_p.h \ diff --git a/src/plugins/scenegraph/d3d12/qsgd3d12adaptation.cpp b/src/plugins/scenegraph/d3d12/qsgd3d12adaptation.cpp index 2762177e5d..b93da0ae01 100644 --- a/src/plugins/scenegraph/d3d12/qsgd3d12adaptation.cpp +++ b/src/plugins/scenegraph/d3d12/qsgd3d12adaptation.cpp @@ -39,6 +39,7 @@ #include "qsgd3d12adaptation_p.h" #include "qsgd3d12renderloop_p.h" +#include "qsgd3d12threadedrenderloop_p.h" #include "qsgd3d12context_p.h" QT_BEGIN_NAMESPACE @@ -68,6 +69,16 @@ QSGContextFactoryInterface::Flags QSGD3D12Adaptation::flags(const QString &) con QSGRenderLoop *QSGD3D12Adaptation::createWindowManager() { + static bool threaded = false; + static bool envChecked = false; + if (!envChecked) { + envChecked = true; + threaded = qgetenv("QSG_RENDER_LOOP") == QByteArrayLiteral("threaded"); + } + + if (threaded) + return new QSGD3D12ThreadedRenderLoop; + return new QSGD3D12RenderLoop; } diff --git a/src/plugins/scenegraph/d3d12/qsgd3d12renderloop.cpp b/src/plugins/scenegraph/d3d12/qsgd3d12renderloop.cpp index 067b0d35f6..288a7ce4ad 100644 --- a/src/plugins/scenegraph/d3d12/qsgd3d12renderloop.cpp +++ b/src/plugins/scenegraph/d3d12/qsgd3d12renderloop.cpp @@ -42,16 +42,9 @@ #include "qsgd3d12context_p.h" #include "qsgd3d12rendercontext_p.h" #include "qsgd3d12shadereffectnode_p.h" -#include <private/qsgrenderer_p.h> #include <private/qquickwindow_p.h> #include <private/qquickprofiler_p.h> -#include <private/qquickanimatorcontroller_p.h> -#include <private/qquickprofiler_p.h> -#include <private/qqmldebugserviceinterfaces_p.h> -#include <private/qqmldebugconnector_p.h> #include <QElapsedTimer> -#include <QQueue> -#include <QGuiApplication> QT_BEGIN_NAMESPACE @@ -64,641 +57,16 @@ QT_BEGIN_NAMESPACE DECLARE_DEBUG_VAR(loop) DECLARE_DEBUG_VAR(time) -/* - The D3D render loop mostly mirrors the threaded OpenGL render loop. - - There are two classes here. QSGD3D12RenderLoop and QSGD3D12RenderThread. All - communication between the two is based on event passing and we have a number - of custom events. - - Render loop is per process, render thread is per window. The - QSGD3D12RenderContext and QSGD3D12Engine are per window as well. The former - is created (but not owned) by QQuickWindow. The D3D device is per process. - - In this implementation, the render thread is never blocked and the GUI - thread will initiate a polishAndSync which will block and wait for the - render thread to pick it up and release the block only after the render - thread is done syncing. The reason for this is: - - 1. Clear blocking paradigm. We only have one real "block" point - (polishAndSync()) and all blocking is initiated by GUI and picked up by - Render at specific times based on events. This makes the execution - deterministic. - - 2. Render does not have to interact with GUI. This is done so that the - render thread can run its own animation system which stays alive even when - the GUI thread is blocked doing I/O, object instantiation, QPainter-painting - or any other non-trivial task. - - The render thread has affinity to the GUI thread until a window is shown. - From that moment and until the window is destroyed, it will have affinity to - the render thread. (moved back at the end of run for cleanup). - */ - -// Passed from the RL to the RT when a window is removed obscured and should be -// removed from the render loop. -const QEvent::Type WM_Obscure = QEvent::Type(QEvent::User + 1); - -// Passed from the RL to RT when GUI has been locked, waiting for sync. -const QEvent::Type WM_RequestSync = QEvent::Type(QEvent::User + 2); - -// Passed by the RT to itself to trigger another render pass. This is typically -// a result of QQuickWindow::update(). -const QEvent::Type WM_RequestRepaint = QEvent::Type(QEvent::User + 3); - -// Passed by the RL to the RT to maybe release resource if no windows are -// rendering. -const QEvent::Type WM_TryRelease = QEvent::Type(QEvent::User + 4); - -// Passed by the RL to the RT when a QQuickWindow::grabWindow() is called. -const QEvent::Type WM_Grab = QEvent::Type(QEvent::User + 5); - -// Passed by the window when there is a render job to run. -const QEvent::Type WM_PostJob = QEvent::Type(QEvent::User + 6); - -class QSGD3D12WindowEvent : public QEvent -{ -public: - QSGD3D12WindowEvent(QQuickWindow *c, QEvent::Type type) : QEvent(type), window(c) { } - QQuickWindow *window; -}; - -class QSGD3D12TryReleaseEvent : public QSGD3D12WindowEvent -{ -public: - QSGD3D12TryReleaseEvent(QQuickWindow *win, bool destroy) - : QSGD3D12WindowEvent(win, WM_TryRelease), destroying(destroy) { } - bool destroying; -}; - -class QSGD3D12SyncEvent : public QSGD3D12WindowEvent -{ -public: - QSGD3D12SyncEvent(QQuickWindow *c, bool inExpose, bool force) - : QSGD3D12WindowEvent(c, WM_RequestSync) - , size(c->size()) - , dpr(c->effectiveDevicePixelRatio()) - , syncInExpose(inExpose) - , forceRenderPass(force) { } - QSize size; - float dpr; - bool syncInExpose; - bool forceRenderPass; -}; - -class QSGD3D12GrabEvent : public QSGD3D12WindowEvent -{ -public: - QSGD3D12GrabEvent(QQuickWindow *c, QImage *result) - : QSGD3D12WindowEvent(c, WM_Grab), image(result) { } - QImage *image; -}; - -class QSGD3D12JobEvent : public QSGD3D12WindowEvent -{ -public: - QSGD3D12JobEvent(QQuickWindow *c, QRunnable *postedJob) - : QSGD3D12WindowEvent(c, WM_PostJob), job(postedJob) { } - ~QSGD3D12JobEvent() { delete job; } - QRunnable *job; -}; - -class QSGD3D12EventQueue : public QQueue<QEvent *> -{ -public: - void addEvent(QEvent *e) { - mutex.lock(); - enqueue(e); - if (waiting) - condition.wakeOne(); - mutex.unlock(); - } - - QEvent *takeEvent(bool wait) { - mutex.lock(); - if (isEmpty() && wait) { - waiting = true; - condition.wait(&mutex); - waiting = false; - } - QEvent *e = dequeue(); - mutex.unlock(); - return e; - } - - bool hasMoreEvents() { - mutex.lock(); - bool has = !isEmpty(); - mutex.unlock(); - return has; - } - -private: - QMutex mutex; - QWaitCondition condition; - bool waiting = false; -}; - -static inline int qsgrl_animation_interval() -{ - const qreal refreshRate = QGuiApplication::primaryScreen() ? QGuiApplication::primaryScreen()->refreshRate() : 0; - return refreshRate < 1 ? 16 : int(1000 / refreshRate); -} - -class QSGD3D12RenderThread : public QThread -{ - Q_OBJECT - -public: - QSGD3D12RenderThread(QSGD3D12RenderLoop *rl, QSGRenderContext *renderContext) - : renderLoop(rl) - { - rc = static_cast<QSGD3D12RenderContext *>(renderContext); - vsyncDelta = qsgrl_animation_interval(); - } - - ~QSGD3D12RenderThread() - { - delete rc; - } - - bool event(QEvent *e); - void run(); - - void syncAndRender(); - void sync(bool inExpose); - - void requestRepaint() - { - if (sleeping) - stopEventProcessing = true; - if (exposedWindow) - pendingUpdate |= RepaintRequest; - } - - void processEventsAndWaitForMore(); - void processEvents(); - void postEvent(QEvent *e); - - enum UpdateRequest { - SyncRequest = 0x01, - RepaintRequest = 0x02, - ExposeRequest = 0x04 | RepaintRequest | SyncRequest - }; - - QSGD3D12Engine *engine = nullptr; - QSGD3D12RenderLoop *renderLoop; - QSGD3D12RenderContext *rc; - QAnimationDriver *rtAnim = nullptr; - volatile bool active = false; - uint pendingUpdate = 0; - bool sleeping = false; - bool syncResultedInChanges = false; - float vsyncDelta; - QMutex mutex; - QWaitCondition waitCondition; - QQuickWindow *exposedWindow = nullptr; - bool stopEventProcessing = false; - QSGD3D12EventQueue eventQueue; - QElapsedTimer threadTimer; - qint64 syncTime; - qint64 renderTime; - qint64 sinceLastTime; - -public slots: - void onSceneGraphChanged() { - syncResultedInChanges = true; - } -}; - -bool QSGD3D12RenderThread::event(QEvent *e) -{ - switch (e->type()) { - - case WM_Obscure: - Q_ASSERT(!exposedWindow || exposedWindow == static_cast<QSGD3D12WindowEvent *>(e)->window); - if (Q_UNLIKELY(debug_loop())) - qDebug() << "RT - WM_Obscure" << exposedWindow; - mutex.lock(); - if (exposedWindow) { - QQuickWindowPrivate::get(exposedWindow)->fireAboutToStop(); - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - WM_Obscure - window removed"); - exposedWindow = nullptr; - } - waitCondition.wakeOne(); - mutex.unlock(); - return true; - - case WM_RequestSync: { - QSGD3D12SyncEvent *wme = static_cast<QSGD3D12SyncEvent *>(e); - if (sleeping) - stopEventProcessing = true; - // One thread+engine for each window. However, the native window may - // change in some (quite artificial) cases, e.g. due to a hide - - // destroy - show on the QWindow. - bool needsWindow = !engine->window(); - if (engine->window() && engine->window() != wme->window->winId()) { - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - WM_RequestSync - native window handle changes for active engine"); - engine->waitGPU(); - QQuickWindowPrivate::get(wme->window)->cleanupNodesOnShutdown(); - QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache(); - rc->invalidate(); - engine->releaseResources(); - needsWindow = true; - // Be nice and emit the rendercontext's initialized() later on at - // some point so that QQuickWindow::sceneGraphInitialized() behaves - // in a manner similar to GL. - rc->setInitializedPending(); - } - if (needsWindow) { - // Must only ever get here when there is no window or releaseResources() has been called. - const int samples = wme->window->format().samples(); - if (Q_UNLIKELY(debug_loop())) - qDebug() << "RT - WM_RequestSync - initializing D3D12 engine" << wme->window - << wme->size << wme->dpr << samples; - engine->attachToWindow(wme->window->winId(), wme->size, wme->dpr, samples); - } - exposedWindow = wme->window; - engine->setWindowSize(wme->size, wme->dpr); - if (Q_UNLIKELY(debug_loop())) - qDebug() << "RT - WM_RequestSync" << exposedWindow; - pendingUpdate |= SyncRequest; - if (wme->syncInExpose) { - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - WM_RequestSync - triggered from expose"); - pendingUpdate |= ExposeRequest; - } - if (wme->forceRenderPass) { - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - WM_RequestSync - repaint regardless"); - pendingUpdate |= RepaintRequest; - } - return true; - } - - case WM_TryRelease: { - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - WM_TryRelease"); - mutex.lock(); - renderLoop->lockedForSync = true; - QSGD3D12TryReleaseEvent *wme = static_cast<QSGD3D12TryReleaseEvent *>(e); - // Only when no windows are exposed anymore or we are shutting down. - if (!exposedWindow || wme->destroying) { - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - WM_TryRelease - invalidating rc"); - if (wme->window) { - QQuickWindowPrivate *wd = QQuickWindowPrivate::get(wme->window); - if (wme->destroying) { - // QSGNode destruction may release graphics resources in use so wait first. - engine->waitGPU(); - // Bye bye nodes... - wd->cleanupNodesOnShutdown(); - QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache(); - } - rc->invalidate(); - QCoreApplication::processEvents(); - QCoreApplication::sendPostedEvents(0, QEvent::DeferredDelete); - if (wme->destroying) - delete wd->animationController; - } - if (wme->destroying) - active = false; - if (sleeping) - stopEventProcessing = true; - } else { - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - WM_TryRelease - not releasing because window is still active"); - } - waitCondition.wakeOne(); - renderLoop->lockedForSync = false; - mutex.unlock(); - return true; - } - - case WM_Grab: { - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - WM_Grab"); - QSGD3D12GrabEvent *wme = static_cast<QSGD3D12GrabEvent *>(e); - Q_ASSERT(wme->window); - Q_ASSERT(wme->window == exposedWindow || !exposedWindow); - mutex.lock(); - if (wme->window) { - // Grabbing is generally done by rendering a frame and reading the - // color buffer contents back, without presenting, and then - // creating a QImage from the returned data. It is terribly - // inefficient since it involves a full blocking wait for the GPU. - // However, our hands are tied by the existing, synchronous APIs of - // QQuickWindow and such. - QQuickWindowPrivate *wd = QQuickWindowPrivate::get(wme->window); - rc->ensureInitializedEmitted(); - wd->syncSceneGraph(); - wd->renderSceneGraph(wme->window->size()); - *wme->image = engine->executeAndWaitReadbackRenderTarget(); - } - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - WM_Grab - waking gui to handle result"); - waitCondition.wakeOne(); - mutex.unlock(); - return true; - } - - case WM_PostJob: { - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - WM_PostJob"); - QSGD3D12JobEvent *wme = static_cast<QSGD3D12JobEvent *>(e); - Q_ASSERT(wme->window == exposedWindow); - if (exposedWindow) { - wme->job->run(); - delete wme->job; - wme->job = nullptr; - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - WM_PostJob - job done"); - } - return true; - } - - case WM_RequestRepaint: - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - WM_RequestPaint"); - // When GUI posts this event, it is followed by a polishAndSync, so we - // must not exit the event loop yet. - pendingUpdate |= RepaintRequest; - break; - - default: - break; - } - - return QThread::event(e); -} - -void QSGD3D12RenderThread::postEvent(QEvent *e) -{ - eventQueue.addEvent(e); -} - -void QSGD3D12RenderThread::processEvents() -{ - while (eventQueue.hasMoreEvents()) { - QEvent *e = eventQueue.takeEvent(false); - event(e); - delete e; - } -} - -void QSGD3D12RenderThread::processEventsAndWaitForMore() -{ - stopEventProcessing = false; - while (!stopEventProcessing) { - QEvent *e = eventQueue.takeEvent(true); - event(e); - delete e; - } -} - -void QSGD3D12RenderThread::run() -{ - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - run()"); - - engine = new QSGD3D12Engine; - rc->setEngine(engine); - - rtAnim = rc->sceneGraphContext()->createAnimationDriver(nullptr); - rtAnim->install(); - - if (QQmlDebugConnector::service<QQmlProfilerService>()) - QQuickProfiler::registerAnimationCallback(); - - while (active) { - if (exposedWindow) - syncAndRender(); - - processEvents(); - QCoreApplication::processEvents(); - - if (pendingUpdate == 0 || !exposedWindow) { - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - done drawing, sleep"); - sleeping = true; - processEventsAndWaitForMore(); - sleeping = false; - } - } - - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - run() exiting"); - - delete rtAnim; - rtAnim = nullptr; - - rc->moveToThread(renderLoop->thread()); - moveToThread(renderLoop->thread()); - - rc->setEngine(nullptr); - delete engine; - engine = nullptr; -} - -void QSGD3D12RenderThread::sync(bool inExpose) -{ - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - sync"); - - mutex.lock(); - Q_ASSERT_X(renderLoop->lockedForSync, "QSGD3D12RenderThread::sync()", "sync triggered with gui not locked"); - - // Recover from device loss. - if (!engine->hasResources()) { - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - sync - device was lost, resetting scenegraph"); - QQuickWindowPrivate::get(exposedWindow)->cleanupNodesOnShutdown(); - QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache(); - rc->invalidate(); - } - - if (engine->window()) { - QQuickWindowPrivate *wd = QQuickWindowPrivate::get(exposedWindow); - bool hadRenderer = wd->renderer != nullptr; - // If the scene graph was touched since the last sync() make sure it sends the - // changed signal. - if (wd->renderer) - wd->renderer->clearChangedFlag(); - - rc->ensureInitializedEmitted(); - wd->syncSceneGraph(); - - if (!hadRenderer && wd->renderer) { - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - created renderer"); - syncResultedInChanges = true; - connect(wd->renderer, &QSGRenderer::sceneGraphChanged, this, - &QSGD3D12RenderThread::onSceneGraphChanged, Qt::DirectConnection); - } - - // Process deferred deletes now, directly after the sync as deleteLater - // on the GUI must now also have resulted in SG changes and the delete - // is a safe operation. - QCoreApplication::sendPostedEvents(0, QEvent::DeferredDelete); - } - - if (!inExpose) { - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - sync complete, waking gui"); - waitCondition.wakeOne(); - mutex.unlock(); - } -} - -void QSGD3D12RenderThread::syncAndRender() -{ - if (Q_UNLIKELY(debug_time())) { - sinceLastTime = threadTimer.nsecsElapsed(); - threadTimer.start(); - } - Q_QUICK_SG_PROFILE_START(QQuickProfiler::SceneGraphRenderLoopFrame); - - QElapsedTimer waitTimer; - waitTimer.start(); - - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - syncAndRender()"); - - syncResultedInChanges = false; - QQuickWindowPrivate *wd = QQuickWindowPrivate::get(exposedWindow); - - const bool repaintRequested = (pendingUpdate & RepaintRequest) || wd->customRenderStage; - const bool syncRequested = pendingUpdate & SyncRequest; - const bool exposeRequested = (pendingUpdate & ExposeRequest) == ExposeRequest; - pendingUpdate = 0; - - if (syncRequested) - sync(exposeRequested); - -#ifndef QSG_NO_RENDER_TIMING - if (Q_UNLIKELY(debug_time())) - syncTime = threadTimer.nsecsElapsed(); -#endif - Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame); - - if (!syncResultedInChanges && !repaintRequested) { - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - no changes, render aborted"); - int waitTime = vsyncDelta - (int) waitTimer.elapsed(); - if (waitTime > 0) - msleep(waitTime); - return; - } - - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - rendering started"); - - if (rtAnim->isRunning()) { - wd->animationController->lock(); - rtAnim->advance(); - wd->animationController->unlock(); - } - - bool canRender = wd->renderer != nullptr; - // Recover from device loss. - if (!engine->hasResources()) { - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - syncAndRender - device was lost, posting FullUpdateRequest"); - // Cannot do anything here because gui is not locked. Request a new - // sync+render round on the gui thread and let the sync handle it. - QCoreApplication::postEvent(exposedWindow, new QEvent(QEvent::Type(QQuickWindowPrivate::FullUpdateRequest))); - canRender = false; - } - - if (canRender) { - wd->renderSceneGraph(engine->windowSize()); - if (Q_UNLIKELY(debug_time())) - renderTime = threadTimer.nsecsElapsed(); - Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame); - - // 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 (!wd->customRenderStage || !wd->customRenderStage->swap()) - engine->present(); - - if (blockOnEachFrame) - 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 { - Q_QUICK_SG_PROFILE_SKIP(QQuickProfiler::SceneGraphRenderLoopFrame, 1); - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - window not ready, skipping render"); - } - - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - rendering done"); - - if (exposeRequested) { - if (Q_UNLIKELY(debug_loop())) - qDebug("RT - wake gui after initial expose"); - waitCondition.wakeOne(); - mutex.unlock(); - } - - if (Q_UNLIKELY(debug_time())) - qDebug("Frame rendered with 'd3d12' renderloop in %dms, sync=%d, render=%d, swap=%d - (on render thread)", - int(threadTimer.elapsed()), - int((syncTime/1000000)), - int((renderTime - syncTime) / 1000000), - int(threadTimer.elapsed() - renderTime / 1000000)); - - Q_QUICK_SG_PROFILE_END(QQuickProfiler::SceneGraphRenderLoopFrame); - - 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(); - engine->simulateDeviceLoss(); - } - } -} - -template<class T> T *windowFor(const QVector<T> &list, QQuickWindow *window) -{ - for (const T &t : list) { - if (t.window == window) - return const_cast<T *>(&t); - } - return nullptr; -} - QSGD3D12RenderLoop::QSGD3D12RenderLoop() { if (Q_UNLIKELY(debug_loop())) - qDebug("new d3d12 render loop ctor"); + qDebug("new d3d12 render loop"); sg = new QSGD3D12Context; - - anim = sg->createAnimationDriver(this); - connect(anim, &QAnimationDriver::started, this, &QSGD3D12RenderLoop::onAnimationStarted); - connect(anim, &QAnimationDriver::stopped, this, &QSGD3D12RenderLoop::onAnimationStopped); - anim->install(); } QSGD3D12RenderLoop::~QSGD3D12RenderLoop() { - if (Q_UNLIKELY(debug_loop())) - qDebug("new d3d12 render loop dtor"); - delete sg; } @@ -712,11 +80,6 @@ void QSGD3D12RenderLoop::hide(QQuickWindow *window) { if (Q_UNLIKELY(debug_loop())) qDebug() << "hide" << window; - - if (window->isExposed()) - handleObscurity(windowFor(windows, window)); - - releaseResources(window); } void QSGD3D12RenderLoop::resize(QQuickWindow *window) @@ -725,7 +88,11 @@ void QSGD3D12RenderLoop::resize(QQuickWindow *window) return; if (Q_UNLIKELY(debug_loop())) - qDebug() << "resize" << window << window->size(); + qDebug() << "resize" << window; + + WindowData &data(m_windows[window]); + if (data.engine) + data.engine->setWindowSize(window->size(), window->effectiveDevicePixelRatio()); } void QSGD3D12RenderLoop::windowDestroyed(QQuickWindow *window) @@ -733,26 +100,32 @@ void QSGD3D12RenderLoop::windowDestroyed(QQuickWindow *window) if (Q_UNLIKELY(debug_loop())) qDebug() << "window destroyed" << window; - WindowData *w = windowFor(windows, window); - if (!w) + if (!m_windows.contains(window)) return; - handleObscurity(w); - handleResourceRelease(w, true); + QQuickWindowPrivate *wd = QQuickWindowPrivate::get(window); + wd->fireAboutToStop(); - QSGD3D12RenderThread *thread = w->thread; - while (thread->isRunning()) - QThread::yieldCurrentThread(); + WindowData &data(m_windows[window]); + QSGD3D12Engine *engine = data.engine; + QSGD3D12RenderContext *rc = data.rc; + m_windows.remove(window); - Q_ASSERT(thread->thread() == QThread::currentThread()); - delete thread; + // QSGNode destruction may release graphics resources in use so wait first. + engine->waitGPU(); - for (int i = 0; i < windows.size(); ++i) { - if (windows.at(i).window == window) { - windows.removeAt(i); - break; - } - } + // Bye bye nodes... + wd->cleanupNodesOnShutdown(); + + QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache(); + + rc->invalidate(); + + if (m_windows.isEmpty()) + QCoreApplication::sendPostedEvents(0, QEvent::DeferredDelete); + + delete rc; + delete engine; } void QSGD3D12RenderLoop::exposureChanged(QQuickWindow *window) @@ -761,76 +134,55 @@ void QSGD3D12RenderLoop::exposureChanged(QQuickWindow *window) qDebug() << "exposure changed" << window; if (window->isExposed()) { - handleExposure(window); - } else { - WindowData *w = windowFor(windows, window); - if (w) - handleObscurity(w); + if (!m_windows.contains(window)) { + WindowData data; + 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 qreal dpr = window->effectiveDevicePixelRatio(); + + if (debug_loop()) + qDebug() << "initializing D3D12 engine" << window << window->size() << dpr << samples; + + data.engine->attachToWindow(window->winId(), window->size(), dpr, samples); + } + m_windows[window].updatePending = true; + renderWindow(window); + } else if (m_windows.contains(window)) { + QQuickWindowPrivate *wd = QQuickWindowPrivate::get(window); + wd->fireAboutToStop(); } } QImage QSGD3D12RenderLoop::grab(QQuickWindow *window) { - if (Q_UNLIKELY(debug_loop())) - qDebug() << "grab" << window; - - WindowData *w = windowFor(windows, window); - // Have to support invisible (but created()'ed) windows as well. - // Unlike with GL, leaving that case for QQuickWindow to handle is not feasible. - const bool tempExpose = !w; - if (tempExpose) { - handleExposure(window); - w = windowFor(windows, window); - Q_ASSERT(w); - } - - if (!w->thread->isRunning()) + if (!m_windows.contains(window)) return QImage(); - if (!window->handle()) - window->create(); - - QQuickWindowPrivate *wd = QQuickWindowPrivate::get(window); - wd->polishItems(); - - QImage result; - w->thread->mutex.lock(); - lockedForSync = true; - w->thread->postEvent(new QSGD3D12GrabEvent(window, &result)); - w->thread->waitCondition.wait(&w->thread->mutex); - lockedForSync = false; - w->thread->mutex.unlock(); + m_windows[window].grabOnly = true; - result.setDevicePixelRatio(window->effectiveDevicePixelRatio()); + renderWindow(window); - if (tempExpose) - handleObscurity(w); - - return result; + QImage grabbed = m_grabContent; + m_grabContent = QImage(); + return grabbed; } void QSGD3D12RenderLoop::update(QQuickWindow *window) { - WindowData *w = windowFor(windows, window); - if (!w) - return; - - if (w->thread == QThread::currentThread()) { - w->thread->requestRepaint(); + if (!m_windows.contains(window)) return; - } - // We set forceRenderPass because we want to make sure the QQuickWindow - // actually does a full render pass after the next sync. - w->forceRenderPass = true; - scheduleUpdate(w); + m_windows[window].updatePending = true; + window->requestUpdate(); } void QSGD3D12RenderLoop::maybeUpdate(QQuickWindow *window) { - WindowData *w = windowFor(windows, window); - if (w) - scheduleUpdate(w); + update(window); } // called in response to window->requestUpdate() @@ -839,14 +191,12 @@ void QSGD3D12RenderLoop::handleUpdateRequest(QQuickWindow *window) if (Q_UNLIKELY(debug_loop())) qDebug() << "handleUpdateRequest" << window; - WindowData *w = windowFor(windows, window); - if (w) - polishAndSync(w, false); + renderWindow(window); } QAnimationDriver *QSGD3D12RenderLoop::animationDriver() const { - return anim; + return nullptr; } QSGContext *QSGD3D12RenderLoop::sceneGraphContext() const @@ -863,19 +213,14 @@ void QSGD3D12RenderLoop::releaseResources(QQuickWindow *window) { if (Q_UNLIKELY(debug_loop())) qDebug() << "releaseResources" << window; - - WindowData *w = windowFor(windows, window); - if (w) - handleResourceRelease(w, false); } void QSGD3D12RenderLoop::postJob(QQuickWindow *window, QRunnable *job) { - WindowData *w = windowFor(windows, window); - if (w && w->thread && w->thread->exposedWindow) - w->thread->postEvent(new QSGD3D12JobEvent(window, job)); - else - delete job; + Q_ASSERT(job); + Q_ASSERT(window); + job->run(); + delete job; } QSurface::SurfaceType QSGD3D12RenderLoop::windowSurfaceType() const @@ -883,285 +228,153 @@ QSurface::SurfaceType QSGD3D12RenderLoop::windowSurfaceType() const return QSurface::OpenGLSurface; } -bool QSGD3D12RenderLoop::interleaveIncubation() const -{ - bool somethingVisible = false; - for (const WindowData &w : windows) { - if (w.window->isVisible() && w.window->isExposed()) { - somethingVisible = true; - break; - } - } - return somethingVisible && anim->isRunning(); -} - -int QSGD3D12RenderLoop::flags() const +void QSGD3D12RenderLoop::renderWindow(QQuickWindow *window) { - return SupportsGrabWithoutExpose; -} + if (Q_UNLIKELY(debug_loop())) + qDebug() << "renderWindow" << window; -bool QSGD3D12RenderLoop::event(QEvent *e) -{ - if (e->type() == QEvent::Timer) { - QTimerEvent *te = static_cast<QTimerEvent *>(e); - if (te->timerId() == animationTimer) { - anim->advance(); - emit timeToIncubate(); - return true; - } - } + QQuickWindowPrivate *wd = QQuickWindowPrivate::get(window); + if (!wd->isRenderable() || !m_windows.contains(window)) + return; - return QObject::event(e); -} + WindowData &data(m_windows[window]); -void QSGD3D12RenderLoop::onAnimationStarted() -{ - startOrStopAnimationTimer(); + const bool needsSwap = data.updatePending; + data.updatePending = false; - for (const WindowData &w : qAsConst(windows)) - w.window->requestUpdate(); -} + if (!data.grabOnly) { + wd->flushFrameSynchronousEvents(); + if (!m_windows.contains(window)) + return; + } -void QSGD3D12RenderLoop::onAnimationStopped() -{ - startOrStopAnimationTimer(); -} + 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); -void QSGD3D12RenderLoop::startOrStopAnimationTimer() -{ - int exposedWindowCount = 0; - const WindowData *exposed = nullptr; - - for (int i = 0; i < windows.size(); ++i) { - const WindowData &w(windows[i]); - if (w.window->isVisible() && w.window->isExposed()) { - ++exposedWindowCount; - exposed = &w; - } - } + wd->polishItems(); - if (animationTimer && (exposedWindowCount == 1 || !anim->isRunning())) { - killTimer(animationTimer); - animationTimer = 0; - // If animations are running, make sure we keep on animating - if (anim->isRunning()) - exposed->window->requestUpdate(); - } else if (!animationTimer && exposedWindowCount != 1 && anim->isRunning()) { - animationTimer = startTimer(qsgrl_animation_interval()); - } -} + if (profileFrames) + polishTime = renderTimer.nsecsElapsed(); + Q_QUICK_SG_PROFILE_SWITCH(QQuickProfiler::SceneGraphPolishFrame, + QQuickProfiler::SceneGraphRenderLoopFrame); -void QSGD3D12RenderLoop::handleExposure(QQuickWindow *window) -{ - if (Q_UNLIKELY(debug_loop())) - qDebug() << "handleExposure" << window; + emit window->afterAnimating(); - WindowData *w = windowFor(windows, window); - if (!w) { + // 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("adding window to list"); - WindowData win; - win.window = window; - QSGRenderContext *rc = QQuickWindowPrivate::get(window)->context; // will transfer ownership - win.thread = new QSGD3D12RenderThread(this, rc); - win.updateDuringSync = false; - win.forceRenderPass = true; // also covered by polishAndSync(inExpose=true), but doesn't hurt - windows.append(win); - w = &windows.last(); - } - - // set this early as we'll be rendering shortly anyway and this avoids - // special casing exposure in polishAndSync. - w->thread->exposedWindow = window; - - if (w->window->size().isEmpty() - || (w->window->isTopLevel() && !w->window->geometry().intersects(w->window->screen()->availableGeometry()))) { -#ifndef QT_NO_DEBUG - qWarning().noquote().nospace() << "QSGD3D12RenderLoop: expose event received for window " - << w->window << " with invalid geometry: " << w->window->geometry() - << " on " << w->window->screen(); -#endif + qDebug("sync - native window handle changes for active engine"); + data.engine->waitGPU(); + wd->cleanupNodesOnShutdown(); + QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache(); + data.rc->invalidate(); + data.engine->releaseResources(); + needsWindow = true; + // Be nice and emit the rendercontext's initialized() later on so that + // QQuickWindow::sceneGraphInitialized() behaves in a manner similar to GL. + data.rc->setInitializedPending(); + } + if (needsWindow) { + // Must only ever get here when there is no window or releaseResources() has been called. + const int samples = window->format().samples(); + const qreal dpr = window->effectiveDevicePixelRatio(); + if (Q_UNLIKELY(debug_loop())) + qDebug() << "sync - reinitializing D3D12 engine" << window << window->size() << dpr << samples; + data.engine->attachToWindow(window->winId(), window->size(), dpr, samples); } - if (!w->window->handle()) - w->window->create(); - - // Start render thread if it is not running - if (!w->thread->isRunning()) { + // Recover from device loss. + if (!data.engine->hasResources()) { if (Q_UNLIKELY(debug_loop())) - qDebug("starting render thread"); - // Push a few things to the render thread. - QQuickAnimatorController *controller = QQuickWindowPrivate::get(w->window)->animationController; - if (controller->thread() != w->thread) - controller->moveToThread(w->thread); - if (w->thread->thread() == QThread::currentThread()) { - w->thread->rc->moveToThread(w->thread); - w->thread->moveToThread(w->thread); - } - - w->thread->active = true; - w->thread->start(); - - if (!w->thread->isRunning()) - qFatal("Render thread failed to start, aborting application."); + qDebug("sync - device was lost, resetting scenegraph"); + wd->cleanupNodesOnShutdown(); + QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache(); + data.rc->invalidate(); + data.rc->setInitializedPending(); } - polishAndSync(w, true); - - startOrStopAnimationTimer(); -} - -void QSGD3D12RenderLoop::handleObscurity(WindowData *w) -{ - if (Q_UNLIKELY(debug_loop())) - qDebug() << "handleObscurity" << w->window; + data.rc->ensureInitializedEmitted(); - if (w->thread->isRunning()) { - w->thread->mutex.lock(); - w->thread->postEvent(new QSGD3D12WindowEvent(w->window, WM_Obscure)); - w->thread->waitCondition.wait(&w->thread->mutex); - w->thread->mutex.unlock(); - } - - startOrStopAnimationTimer(); -} + wd->syncSceneGraph(); -void QSGD3D12RenderLoop::scheduleUpdate(WindowData *w) -{ - if (!QCoreApplication::instance()) - return; + if (profileFrames) + syncTime = renderTimer.nsecsElapsed(); + Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame); - if (!w || !w->thread->isRunning()) - return; + wd->renderSceneGraph(window->size()); - QThread *current = QThread::currentThread(); - if (current != QCoreApplication::instance()->thread() && (current != w->thread || !lockedForSync)) { - qWarning() << "Updates can only be scheduled from GUI thread or from QQuickItem::updatePaintNode()"; - return; - } + if (profileFrames) + renderTime = renderTimer.nsecsElapsed(); + Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame); - if (current == w->thread) { - w->updateDuringSync = true; - return; + if (data.grabOnly) { + m_grabContent = data.engine->executeAndWaitReadbackRenderTarget(); + data.grabOnly = false; } - w->window->requestUpdate(); -} + // 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; -void QSGD3D12RenderLoop::handleResourceRelease(WindowData *w, bool destroying) -{ - if (Q_UNLIKELY(debug_loop())) - qDebug() << "handleResourceRelease" << (destroying ? "destroying" : "hide/releaseResources") << w->window; - - w->thread->mutex.lock(); - if (w->thread->isRunning() && w->thread->active) { - QQuickWindow *window = w->window; - - // Note that window->handle() is typically null by this time because - // the platform window is already destroyed. This should not be a - // problem for the D3D cleanup. - - w->thread->postEvent(new QSGD3D12TryReleaseEvent(window, destroying)); - w->thread->waitCondition.wait(&w->thread->mutex); - - // Avoid a shutdown race condition. - // If SG is invalidated and 'active' becomes false, the thread's run() - // method will exit. handleExposure() relies on QThread::isRunning() (because it - // potentially needs to start the thread again) and our mutex cannot be used to - // track the thread stopping, so we wait a few nanoseconds extra so the thread - // can exit properly. - if (!w->thread->active) - w->thread->wait(); + if (needsSwap && 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(); } - w->thread->mutex.unlock(); -} -void QSGD3D12RenderLoop::polishAndSync(WindowData *w, bool inExpose) -{ - if (Q_UNLIKELY(debug_loop())) - qDebug() << "polishAndSync" << (inExpose ? "(in expose)" : "(normal)") << w->window; - - QQuickWindow *window = w->window; - if (!w->thread || !w->thread->exposedWindow) { - if (Q_UNLIKELY(debug_loop())) - qDebug("polishAndSync - not exposed, abort"); - return; - } + qint64 swapTime = 0; + if (profileFrames) + swapTime = renderTimer.nsecsElapsed(); + Q_QUICK_SG_PROFILE_END(QQuickProfiler::SceneGraphRenderLoopFrame); - // Flush pending touch events. - QQuickWindowPrivate::get(window)->flushFrameSynchronousEvents(); - // The delivery of the event might have caused the window to stop rendering - w = windowFor(windows, window); - if (!w || !w->thread || !w->thread->exposedWindow) { - if (Q_UNLIKELY(debug_loop())) - qDebug("polishAndSync - removed after touch event flushing, abort"); - return; + 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(); } - QElapsedTimer timer; - qint64 polishTime = 0; - qint64 waitTime = 0; - qint64 syncTime = 0; - if (Q_UNLIKELY(debug_time())) - timer.start(); - Q_QUICK_SG_PROFILE_START(QQuickProfiler::SceneGraphPolishAndSync); - - QQuickWindowPrivate *wd = QQuickWindowPrivate::get(window); - wd->polishItems(); - - if (Q_UNLIKELY(debug_time())) - polishTime = timer.nsecsElapsed(); - Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphPolishAndSync); - - w->updateDuringSync = false; - - emit window->afterAnimating(); - - if (Q_UNLIKELY(debug_loop())) - qDebug("polishAndSync - lock for sync"); - w->thread->mutex.lock(); - lockedForSync = true; - w->thread->postEvent(new QSGD3D12SyncEvent(window, inExpose, w->forceRenderPass)); - w->forceRenderPass = false; - - if (Q_UNLIKELY(debug_loop())) - qDebug("polishAndSync - wait for sync"); - if (Q_UNLIKELY(debug_time())) - waitTime = timer.nsecsElapsed(); - Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphPolishAndSync); - w->thread->waitCondition.wait(&w->thread->mutex); - lockedForSync = false; - w->thread->mutex.unlock(); - if (Q_UNLIKELY(debug_loop())) - qDebug("polishAndSync - unlock after sync"); - - if (Q_UNLIKELY(debug_time())) - syncTime = timer.nsecsElapsed(); - Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphPolishAndSync); + // Might have been set during syncSceneGraph() + if (data.updatePending) + maybeUpdate(window); - if (!animationTimer && anim->isRunning()) { - if (Q_UNLIKELY(debug_loop())) - qDebug("polishAndSync - advancing animations"); - anim->advance(); - // We need to trigger another sync to keep animations running... - w->window->requestUpdate(); - emit timeToIncubate(); - } else if (w->updateDuringSync) { - w->window->requestUpdate(); + // 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(); + } } - - if (Q_UNLIKELY(debug_time())) - qDebug().nospace() - << "Frame prepared with 'd3d12' renderloop" - << ", polish=" << (polishTime / 1000000) - << ", lock=" << (waitTime - polishTime) / 1000000 - << ", blockedForSync=" << (syncTime - waitTime) / 1000000 - << ", animations=" << (timer.nsecsElapsed() - syncTime) / 1000000 - << " - (on gui thread) " << window; - - Q_QUICK_SG_PROFILE_END(QQuickProfiler::SceneGraphPolishAndSync); } -#include "qsgd3d12renderloop.moc" +int QSGD3D12RenderLoop::flags() const +{ + return SupportsGrabWithoutExpose; +} QT_END_NAMESPACE diff --git a/src/plugins/scenegraph/d3d12/qsgd3d12renderloop_p.h b/src/plugins/scenegraph/d3d12/qsgd3d12renderloop_p.h index 732f8dd5d2..fbd9d66d4a 100644 --- a/src/plugins/scenegraph/d3d12/qsgd3d12renderloop_p.h +++ b/src/plugins/scenegraph/d3d12/qsgd3d12renderloop_p.h @@ -58,12 +58,9 @@ QT_BEGIN_NAMESPACE class QSGD3D12Engine; class QSGD3D12Context; class QSGD3D12RenderContext; -class QSGD3D12RenderThread; class QSGD3D12RenderLoop : public QSGRenderLoop { - Q_OBJECT - public: QSGD3D12RenderLoop(); ~QSGD3D12RenderLoop(); @@ -91,37 +88,23 @@ public: void postJob(QQuickWindow *window, QRunnable *job) override; QSurface::SurfaceType windowSurfaceType() const override; - bool interleaveIncubation() const override; int flags() const override; - bool event(QEvent *e) override; +private: + void renderWindow(QQuickWindow *window); -public Q_SLOTS: - void onAnimationStarted(); - void onAnimationStopped(); + QSGD3D12Context *sg; -private: struct WindowData { - QQuickWindow *window; - QSGD3D12RenderThread *thread; - uint updateDuringSync : 1; - uint forceRenderPass : 1; + QSGD3D12RenderContext *rc = nullptr; + QSGD3D12Engine *engine = nullptr; + bool updatePending = false; + bool grabOnly = false; }; - void startOrStopAnimationTimer(); - void handleExposure(QQuickWindow *window); - void handleObscurity(WindowData *w); - void scheduleUpdate(WindowData *w); - void handleResourceRelease(WindowData *w, bool destroying); - void polishAndSync(WindowData *w, bool inExpose); - - QSGD3D12Context *sg; - QAnimationDriver *anim; - int animationTimer = 0; - bool lockedForSync = false; - QVector<WindowData> windows; + QHash<QQuickWindow *, WindowData> m_windows; - friend class QSGD3D12RenderThread; + QImage m_grabContent; }; QT_END_NAMESPACE diff --git a/src/plugins/scenegraph/d3d12/qsgd3d12threadedrenderloop.cpp b/src/plugins/scenegraph/d3d12/qsgd3d12threadedrenderloop.cpp new file mode 100644 index 0000000000..9f32a6d8bf --- /dev/null +++ b/src/plugins/scenegraph/d3d12/qsgd3d12threadedrenderloop.cpp @@ -0,0 +1,1187 @@ +/**************************************************************************** +** +** 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 "qsgd3d12threadedrenderloop_p.h" +#include "qsgd3d12engine_p.h" +#include "qsgd3d12context_p.h" +#include "qsgd3d12rendercontext_p.h" +#include "qsgd3d12shadereffectnode_p.h" +#include <private/qsgrenderer_p.h> +#include <private/qquickwindow_p.h> +#include <private/qquickprofiler_p.h> +#include <private/qquickanimatorcontroller_p.h> +#include <private/qquickprofiler_p.h> +#include <private/qqmldebugserviceinterfaces_p.h> +#include <private/qqmldebugconnector_p.h> +#include <QElapsedTimer> +#include <QQueue> +#include <QGuiApplication> + +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) + + +// NOTE: The threaded renderloop is not currently safe to use in practice as it +// is prone to deadlocks, in particular when multiple windows are active. This +// is because DXGI's limitation of relying on the gui message pump in certain +// cases. See +// https://msdn.microsoft.com/en-us/library/windows/desktop/ee417025(v=vs.85).aspx#multithreading_and_dxgi +// +// This means that if swap chain functions like create, release, and +// potentially even Present, are called outside the gui thread, then the +// application must ensure the gui thread does not ever block and wait for the +// render thread - since on the render thread a DXGI call may be in turn +// waiting for the gui thread to deliver a window message... +// +// Ensuring this is impossible with the current design where the gui thread +// must block at certain points, waiting for the render thread. Qt moves out +// rendering from the main thread, in order to make application's life easier, +// whereas the typical DXGI-compatible model would require moving work, but not +// windowing and presenting, out to additional threads. + + +/* + The D3D render loop mostly mirrors the threaded OpenGL render loop. + + There are two classes here. QSGD3D12ThreadedRenderLoop and + QSGD3D12RenderThread. All communication between the two is based on event + passing and we have a number of custom events. + + Render loop is per process, render thread is per window. The + QSGD3D12RenderContext and QSGD3D12Engine are per window as well. The former + is created (but not owned) by QQuickWindow. The D3D device is per process. + + In this implementation, the render thread is never blocked and the GUI + thread will initiate a polishAndSync which will block and wait for the + render thread to pick it up and release the block only after the render + thread is done syncing. The reason for this is: + + 1. Clear blocking paradigm. We only have one real "block" point + (polishAndSync()) and all blocking is initiated by GUI and picked up by + Render at specific times based on events. This makes the execution + deterministic. + + 2. Render does not have to interact with GUI. This is done so that the + render thread can run its own animation system which stays alive even when + the GUI thread is blocked doing I/O, object instantiation, QPainter-painting + or any other non-trivial task. + + The render thread has affinity to the GUI thread until a window is shown. + From that moment and until the window is destroyed, it will have affinity to + the render thread. (moved back at the end of run for cleanup). + */ + +// Passed from the RL to the RT when a window is removed obscured and should be +// removed from the render loop. +const QEvent::Type WM_Obscure = QEvent::Type(QEvent::User + 1); + +// Passed from the RL to RT when GUI has been locked, waiting for sync. +const QEvent::Type WM_RequestSync = QEvent::Type(QEvent::User + 2); + +// Passed by the RT to itself to trigger another render pass. This is typically +// a result of QQuickWindow::update(). +const QEvent::Type WM_RequestRepaint = QEvent::Type(QEvent::User + 3); + +// Passed by the RL to the RT to maybe release resource if no windows are +// rendering. +const QEvent::Type WM_TryRelease = QEvent::Type(QEvent::User + 4); + +// Passed by the RL to the RT when a QQuickWindow::grabWindow() is called. +const QEvent::Type WM_Grab = QEvent::Type(QEvent::User + 5); + +// Passed by the window when there is a render job to run. +const QEvent::Type WM_PostJob = QEvent::Type(QEvent::User + 6); + +class QSGD3D12WindowEvent : public QEvent +{ +public: + QSGD3D12WindowEvent(QQuickWindow *c, QEvent::Type type) : QEvent(type), window(c) { } + QQuickWindow *window; +}; + +class QSGD3D12TryReleaseEvent : public QSGD3D12WindowEvent +{ +public: + QSGD3D12TryReleaseEvent(QQuickWindow *win, bool destroy) + : QSGD3D12WindowEvent(win, WM_TryRelease), destroying(destroy) { } + bool destroying; +}; + +class QSGD3D12SyncEvent : public QSGD3D12WindowEvent +{ +public: + QSGD3D12SyncEvent(QQuickWindow *c, bool inExpose, bool force) + : QSGD3D12WindowEvent(c, WM_RequestSync) + , size(c->size()) + , dpr(c->effectiveDevicePixelRatio()) + , syncInExpose(inExpose) + , forceRenderPass(force) { } + QSize size; + float dpr; + bool syncInExpose; + bool forceRenderPass; +}; + +class QSGD3D12GrabEvent : public QSGD3D12WindowEvent +{ +public: + QSGD3D12GrabEvent(QQuickWindow *c, QImage *result) + : QSGD3D12WindowEvent(c, WM_Grab), image(result) { } + QImage *image; +}; + +class QSGD3D12JobEvent : public QSGD3D12WindowEvent +{ +public: + QSGD3D12JobEvent(QQuickWindow *c, QRunnable *postedJob) + : QSGD3D12WindowEvent(c, WM_PostJob), job(postedJob) { } + ~QSGD3D12JobEvent() { delete job; } + QRunnable *job; +}; + +class QSGD3D12EventQueue : public QQueue<QEvent *> +{ +public: + void addEvent(QEvent *e) { + mutex.lock(); + enqueue(e); + if (waiting) + condition.wakeOne(); + mutex.unlock(); + } + + QEvent *takeEvent(bool wait) { + mutex.lock(); + if (isEmpty() && wait) { + waiting = true; + condition.wait(&mutex); + waiting = false; + } + QEvent *e = dequeue(); + mutex.unlock(); + return e; + } + + bool hasMoreEvents() { + mutex.lock(); + bool has = !isEmpty(); + mutex.unlock(); + return has; + } + +private: + QMutex mutex; + QWaitCondition condition; + bool waiting = false; +}; + +static inline int qsgrl_animation_interval() +{ + const qreal refreshRate = QGuiApplication::primaryScreen() ? QGuiApplication::primaryScreen()->refreshRate() : 0; + return refreshRate < 1 ? 16 : int(1000 / refreshRate); +} + +class QSGD3D12RenderThread : public QThread +{ + Q_OBJECT + +public: + QSGD3D12RenderThread(QSGD3D12ThreadedRenderLoop *rl, QSGRenderContext *renderContext) + : renderLoop(rl) + { + rc = static_cast<QSGD3D12RenderContext *>(renderContext); + vsyncDelta = qsgrl_animation_interval(); + } + + ~QSGD3D12RenderThread() + { + delete rc; + } + + bool event(QEvent *e); + void run(); + + void syncAndRender(); + void sync(bool inExpose); + + void requestRepaint() + { + if (sleeping) + stopEventProcessing = true; + if (exposedWindow) + pendingUpdate |= RepaintRequest; + } + + void processEventsAndWaitForMore(); + void processEvents(); + void postEvent(QEvent *e); + + enum UpdateRequest { + SyncRequest = 0x01, + RepaintRequest = 0x02, + ExposeRequest = 0x04 | RepaintRequest | SyncRequest + }; + + QSGD3D12Engine *engine = nullptr; + QSGD3D12ThreadedRenderLoop *renderLoop; + QSGD3D12RenderContext *rc; + QAnimationDriver *rtAnim = nullptr; + volatile bool active = false; + uint pendingUpdate = 0; + bool sleeping = false; + bool syncResultedInChanges = false; + float vsyncDelta; + QMutex mutex; + QWaitCondition waitCondition; + QQuickWindow *exposedWindow = nullptr; + bool stopEventProcessing = false; + QSGD3D12EventQueue eventQueue; + QElapsedTimer threadTimer; + qint64 syncTime; + qint64 renderTime; + qint64 sinceLastTime; + +public slots: + void onSceneGraphChanged() { + syncResultedInChanges = true; + } +}; + +bool QSGD3D12RenderThread::event(QEvent *e) +{ + switch (e->type()) { + + case WM_Obscure: + Q_ASSERT(!exposedWindow || exposedWindow == static_cast<QSGD3D12WindowEvent *>(e)->window); + if (Q_UNLIKELY(debug_loop())) + qDebug() << "RT - WM_Obscure" << exposedWindow; + mutex.lock(); + if (exposedWindow) { + QQuickWindowPrivate::get(exposedWindow)->fireAboutToStop(); + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - WM_Obscure - window removed"); + exposedWindow = nullptr; + } + waitCondition.wakeOne(); + mutex.unlock(); + return true; + + case WM_RequestSync: { + QSGD3D12SyncEvent *wme = static_cast<QSGD3D12SyncEvent *>(e); + if (sleeping) + stopEventProcessing = true; + // One thread+engine for each window. However, the native window may + // change in some (quite artificial) cases, e.g. due to a hide - + // destroy - show on the QWindow. + bool needsWindow = !engine->window(); + if (engine->window() && engine->window() != wme->window->winId()) { + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - WM_RequestSync - native window handle changes for active engine"); + engine->waitGPU(); + QQuickWindowPrivate::get(wme->window)->cleanupNodesOnShutdown(); + QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache(); + rc->invalidate(); + engine->releaseResources(); + needsWindow = true; + // Be nice and emit the rendercontext's initialized() later on at + // some point so that QQuickWindow::sceneGraphInitialized() behaves + // in a manner similar to GL. + rc->setInitializedPending(); + } + if (needsWindow) { + // Must only ever get here when there is no window or releaseResources() has been called. + const int samples = wme->window->format().samples(); + if (Q_UNLIKELY(debug_loop())) + qDebug() << "RT - WM_RequestSync - initializing D3D12 engine" << wme->window + << wme->size << wme->dpr << samples; + engine->attachToWindow(wme->window->winId(), wme->size, wme->dpr, samples); + } + exposedWindow = wme->window; + engine->setWindowSize(wme->size, wme->dpr); + if (Q_UNLIKELY(debug_loop())) + qDebug() << "RT - WM_RequestSync" << exposedWindow; + pendingUpdate |= SyncRequest; + if (wme->syncInExpose) { + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - WM_RequestSync - triggered from expose"); + pendingUpdate |= ExposeRequest; + } + if (wme->forceRenderPass) { + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - WM_RequestSync - repaint regardless"); + pendingUpdate |= RepaintRequest; + } + return true; + } + + case WM_TryRelease: { + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - WM_TryRelease"); + mutex.lock(); + renderLoop->lockedForSync = true; + QSGD3D12TryReleaseEvent *wme = static_cast<QSGD3D12TryReleaseEvent *>(e); + // Only when no windows are exposed anymore or we are shutting down. + if (!exposedWindow || wme->destroying) { + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - WM_TryRelease - invalidating rc"); + if (wme->window) { + QQuickWindowPrivate *wd = QQuickWindowPrivate::get(wme->window); + if (wme->destroying) { + // QSGNode destruction may release graphics resources in use so wait first. + engine->waitGPU(); + // Bye bye nodes... + wd->cleanupNodesOnShutdown(); + QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache(); + } + rc->invalidate(); + QCoreApplication::processEvents(); + QCoreApplication::sendPostedEvents(0, QEvent::DeferredDelete); + if (wme->destroying) + delete wd->animationController; + } + if (wme->destroying) + active = false; + if (sleeping) + stopEventProcessing = true; + } else { + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - WM_TryRelease - not releasing because window is still active"); + } + waitCondition.wakeOne(); + renderLoop->lockedForSync = false; + mutex.unlock(); + return true; + } + + case WM_Grab: { + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - WM_Grab"); + QSGD3D12GrabEvent *wme = static_cast<QSGD3D12GrabEvent *>(e); + Q_ASSERT(wme->window); + Q_ASSERT(wme->window == exposedWindow || !exposedWindow); + mutex.lock(); + if (wme->window) { + // Grabbing is generally done by rendering a frame and reading the + // color buffer contents back, without presenting, and then + // creating a QImage from the returned data. It is terribly + // inefficient since it involves a full blocking wait for the GPU. + // However, our hands are tied by the existing, synchronous APIs of + // QQuickWindow and such. + QQuickWindowPrivate *wd = QQuickWindowPrivate::get(wme->window); + rc->ensureInitializedEmitted(); + wd->syncSceneGraph(); + wd->renderSceneGraph(wme->window->size()); + *wme->image = engine->executeAndWaitReadbackRenderTarget(); + } + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - WM_Grab - waking gui to handle result"); + waitCondition.wakeOne(); + mutex.unlock(); + return true; + } + + case WM_PostJob: { + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - WM_PostJob"); + QSGD3D12JobEvent *wme = static_cast<QSGD3D12JobEvent *>(e); + Q_ASSERT(wme->window == exposedWindow); + if (exposedWindow) { + wme->job->run(); + delete wme->job; + wme->job = nullptr; + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - WM_PostJob - job done"); + } + return true; + } + + case WM_RequestRepaint: + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - WM_RequestPaint"); + // When GUI posts this event, it is followed by a polishAndSync, so we + // must not exit the event loop yet. + pendingUpdate |= RepaintRequest; + break; + + default: + break; + } + + return QThread::event(e); +} + +void QSGD3D12RenderThread::postEvent(QEvent *e) +{ + eventQueue.addEvent(e); +} + +void QSGD3D12RenderThread::processEvents() +{ + while (eventQueue.hasMoreEvents()) { + QEvent *e = eventQueue.takeEvent(false); + event(e); + delete e; + } +} + +void QSGD3D12RenderThread::processEventsAndWaitForMore() +{ + stopEventProcessing = false; + while (!stopEventProcessing) { + QEvent *e = eventQueue.takeEvent(true); + event(e); + delete e; + } +} + +void QSGD3D12RenderThread::run() +{ + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - run()"); + + engine = new QSGD3D12Engine; + rc->setEngine(engine); + + rtAnim = rc->sceneGraphContext()->createAnimationDriver(nullptr); + rtAnim->install(); + + if (QQmlDebugConnector::service<QQmlProfilerService>()) + QQuickProfiler::registerAnimationCallback(); + + while (active) { + if (exposedWindow) + syncAndRender(); + + processEvents(); + QCoreApplication::processEvents(); + + if (pendingUpdate == 0 || !exposedWindow) { + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - done drawing, sleep"); + sleeping = true; + processEventsAndWaitForMore(); + sleeping = false; + } + } + + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - run() exiting"); + + delete rtAnim; + rtAnim = nullptr; + + rc->moveToThread(renderLoop->thread()); + moveToThread(renderLoop->thread()); + + rc->setEngine(nullptr); + delete engine; + engine = nullptr; +} + +void QSGD3D12RenderThread::sync(bool inExpose) +{ + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - sync"); + + mutex.lock(); + Q_ASSERT_X(renderLoop->lockedForSync, "QSGD3D12RenderThread::sync()", "sync triggered with gui not locked"); + + // Recover from device loss. + if (!engine->hasResources()) { + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - sync - device was lost, resetting scenegraph"); + QQuickWindowPrivate::get(exposedWindow)->cleanupNodesOnShutdown(); + QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache(); + rc->invalidate(); + } + + if (engine->window()) { + QQuickWindowPrivate *wd = QQuickWindowPrivate::get(exposedWindow); + bool hadRenderer = wd->renderer != nullptr; + // If the scene graph was touched since the last sync() make sure it sends the + // changed signal. + if (wd->renderer) + wd->renderer->clearChangedFlag(); + + rc->ensureInitializedEmitted(); + wd->syncSceneGraph(); + + if (!hadRenderer && wd->renderer) { + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - created renderer"); + syncResultedInChanges = true; + connect(wd->renderer, &QSGRenderer::sceneGraphChanged, this, + &QSGD3D12RenderThread::onSceneGraphChanged, Qt::DirectConnection); + } + + // Process deferred deletes now, directly after the sync as deleteLater + // on the GUI must now also have resulted in SG changes and the delete + // is a safe operation. + QCoreApplication::sendPostedEvents(0, QEvent::DeferredDelete); + } + + if (!inExpose) { + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - sync complete, waking gui"); + waitCondition.wakeOne(); + mutex.unlock(); + } +} + +void QSGD3D12RenderThread::syncAndRender() +{ + if (Q_UNLIKELY(debug_time())) { + sinceLastTime = threadTimer.nsecsElapsed(); + threadTimer.start(); + } + Q_QUICK_SG_PROFILE_START(QQuickProfiler::SceneGraphRenderLoopFrame); + + QElapsedTimer waitTimer; + waitTimer.start(); + + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - syncAndRender()"); + + syncResultedInChanges = false; + QQuickWindowPrivate *wd = QQuickWindowPrivate::get(exposedWindow); + + const bool repaintRequested = (pendingUpdate & RepaintRequest) || wd->customRenderStage; + const bool syncRequested = pendingUpdate & SyncRequest; + const bool exposeRequested = (pendingUpdate & ExposeRequest) == ExposeRequest; + pendingUpdate = 0; + + if (syncRequested) + sync(exposeRequested); + +#ifndef QSG_NO_RENDER_TIMING + if (Q_UNLIKELY(debug_time())) + syncTime = threadTimer.nsecsElapsed(); +#endif + Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame); + + if (!syncResultedInChanges && !repaintRequested) { + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - no changes, render aborted"); + int waitTime = vsyncDelta - (int) waitTimer.elapsed(); + if (waitTime > 0) + msleep(waitTime); + return; + } + + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - rendering started"); + + if (rtAnim->isRunning()) { + wd->animationController->lock(); + rtAnim->advance(); + wd->animationController->unlock(); + } + + bool canRender = wd->renderer != nullptr; + // Recover from device loss. + if (!engine->hasResources()) { + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - syncAndRender - device was lost, posting FullUpdateRequest"); + // Cannot do anything here because gui is not locked. Request a new + // sync+render round on the gui thread and let the sync handle it. + QCoreApplication::postEvent(exposedWindow, new QEvent(QEvent::Type(QQuickWindowPrivate::FullUpdateRequest))); + canRender = false; + } + + if (canRender) { + wd->renderSceneGraph(engine->windowSize()); + if (Q_UNLIKELY(debug_time())) + renderTime = threadTimer.nsecsElapsed(); + Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphRenderLoopFrame); + + // 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 (!wd->customRenderStage || !wd->customRenderStage->swap()) + engine->present(); + + if (blockOnEachFrame) + 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 { + Q_QUICK_SG_PROFILE_SKIP(QQuickProfiler::SceneGraphRenderLoopFrame, 1); + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - window not ready, skipping render"); + } + + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - rendering done"); + + if (exposeRequested) { + if (Q_UNLIKELY(debug_loop())) + qDebug("RT - wake gui after initial expose"); + waitCondition.wakeOne(); + mutex.unlock(); + } + + if (Q_UNLIKELY(debug_time())) + qDebug("Frame rendered with 'd3d12' renderloop in %dms, sync=%d, render=%d, swap=%d - (on render thread)", + int(threadTimer.elapsed()), + int((syncTime/1000000)), + int((renderTime - syncTime) / 1000000), + int(threadTimer.elapsed() - renderTime / 1000000)); + + Q_QUICK_SG_PROFILE_END(QQuickProfiler::SceneGraphRenderLoopFrame); + + 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(); + engine->simulateDeviceLoss(); + } + } +} + +template<class T> T *windowFor(const QVector<T> &list, QQuickWindow *window) +{ + for (const T &t : list) { + if (t.window == window) + return const_cast<T *>(&t); + } + return nullptr; +} + +QSGD3D12ThreadedRenderLoop::QSGD3D12ThreadedRenderLoop() +{ + if (Q_UNLIKELY(debug_loop())) + qDebug("d3d12 THREADED render loop ctor"); + + sg = new QSGD3D12Context; + + anim = sg->createAnimationDriver(this); + connect(anim, &QAnimationDriver::started, this, &QSGD3D12ThreadedRenderLoop::onAnimationStarted); + connect(anim, &QAnimationDriver::stopped, this, &QSGD3D12ThreadedRenderLoop::onAnimationStopped); + anim->install(); +} + +QSGD3D12ThreadedRenderLoop::~QSGD3D12ThreadedRenderLoop() +{ + if (Q_UNLIKELY(debug_loop())) + qDebug("d3d12 THREADED render loop dtor"); + + delete sg; +} + +void QSGD3D12ThreadedRenderLoop::show(QQuickWindow *window) +{ + if (Q_UNLIKELY(debug_loop())) + qDebug() << "show" << window; +} + +void QSGD3D12ThreadedRenderLoop::hide(QQuickWindow *window) +{ + if (Q_UNLIKELY(debug_loop())) + qDebug() << "hide" << window; + + if (window->isExposed()) + handleObscurity(windowFor(windows, window)); + + releaseResources(window); +} + +void QSGD3D12ThreadedRenderLoop::resize(QQuickWindow *window) +{ + if (!window->isExposed() || window->size().isEmpty()) + return; + + if (Q_UNLIKELY(debug_loop())) + qDebug() << "resize" << window << window->size(); +} + +void QSGD3D12ThreadedRenderLoop::windowDestroyed(QQuickWindow *window) +{ + if (Q_UNLIKELY(debug_loop())) + qDebug() << "window destroyed" << window; + + WindowData *w = windowFor(windows, window); + if (!w) + return; + + handleObscurity(w); + handleResourceRelease(w, true); + + QSGD3D12RenderThread *thread = w->thread; + while (thread->isRunning()) + QThread::yieldCurrentThread(); + + Q_ASSERT(thread->thread() == QThread::currentThread()); + delete thread; + + for (int i = 0; i < windows.size(); ++i) { + if (windows.at(i).window == window) { + windows.removeAt(i); + break; + } + } +} + +void QSGD3D12ThreadedRenderLoop::exposureChanged(QQuickWindow *window) +{ + if (Q_UNLIKELY(debug_loop())) + qDebug() << "exposure changed" << window; + + if (window->isExposed()) { + handleExposure(window); + } else { + WindowData *w = windowFor(windows, window); + if (w) + handleObscurity(w); + } +} + +QImage QSGD3D12ThreadedRenderLoop::grab(QQuickWindow *window) +{ + if (Q_UNLIKELY(debug_loop())) + qDebug() << "grab" << window; + + WindowData *w = windowFor(windows, window); + // Have to support invisible (but created()'ed) windows as well. + // Unlike with GL, leaving that case for QQuickWindow to handle is not feasible. + const bool tempExpose = !w; + if (tempExpose) { + handleExposure(window); + w = windowFor(windows, window); + Q_ASSERT(w); + } + + if (!w->thread->isRunning()) + return QImage(); + + if (!window->handle()) + window->create(); + + QQuickWindowPrivate *wd = QQuickWindowPrivate::get(window); + wd->polishItems(); + + QImage result; + w->thread->mutex.lock(); + lockedForSync = true; + w->thread->postEvent(new QSGD3D12GrabEvent(window, &result)); + w->thread->waitCondition.wait(&w->thread->mutex); + lockedForSync = false; + w->thread->mutex.unlock(); + + result.setDevicePixelRatio(window->effectiveDevicePixelRatio()); + + if (tempExpose) + handleObscurity(w); + + return result; +} + +void QSGD3D12ThreadedRenderLoop::update(QQuickWindow *window) +{ + WindowData *w = windowFor(windows, window); + if (!w) + return; + + if (w->thread == QThread::currentThread()) { + w->thread->requestRepaint(); + return; + } + + // We set forceRenderPass because we want to make sure the QQuickWindow + // actually does a full render pass after the next sync. + w->forceRenderPass = true; + scheduleUpdate(w); +} + +void QSGD3D12ThreadedRenderLoop::maybeUpdate(QQuickWindow *window) +{ + WindowData *w = windowFor(windows, window); + if (w) + scheduleUpdate(w); +} + +// called in response to window->requestUpdate() +void QSGD3D12ThreadedRenderLoop::handleUpdateRequest(QQuickWindow *window) +{ + if (Q_UNLIKELY(debug_loop())) + qDebug() << "handleUpdateRequest" << window; + + WindowData *w = windowFor(windows, window); + if (w) + polishAndSync(w, false); +} + +QAnimationDriver *QSGD3D12ThreadedRenderLoop::animationDriver() const +{ + return anim; +} + +QSGContext *QSGD3D12ThreadedRenderLoop::sceneGraphContext() const +{ + return sg; +} + +QSGRenderContext *QSGD3D12ThreadedRenderLoop::createRenderContext(QSGContext *) const +{ + return sg->createRenderContext(); +} + +void QSGD3D12ThreadedRenderLoop::releaseResources(QQuickWindow *window) +{ + if (Q_UNLIKELY(debug_loop())) + qDebug() << "releaseResources" << window; + + WindowData *w = windowFor(windows, window); + if (w) + handleResourceRelease(w, false); +} + +void QSGD3D12ThreadedRenderLoop::postJob(QQuickWindow *window, QRunnable *job) +{ + WindowData *w = windowFor(windows, window); + if (w && w->thread && w->thread->exposedWindow) + w->thread->postEvent(new QSGD3D12JobEvent(window, job)); + else + delete job; +} + +QSurface::SurfaceType QSGD3D12ThreadedRenderLoop::windowSurfaceType() const +{ + return QSurface::OpenGLSurface; +} + +bool QSGD3D12ThreadedRenderLoop::interleaveIncubation() const +{ + bool somethingVisible = false; + for (const WindowData &w : windows) { + if (w.window->isVisible() && w.window->isExposed()) { + somethingVisible = true; + break; + } + } + return somethingVisible && anim->isRunning(); +} + +int QSGD3D12ThreadedRenderLoop::flags() const +{ + return SupportsGrabWithoutExpose; +} + +bool QSGD3D12ThreadedRenderLoop::event(QEvent *e) +{ + if (e->type() == QEvent::Timer) { + QTimerEvent *te = static_cast<QTimerEvent *>(e); + if (te->timerId() == animationTimer) { + anim->advance(); + emit timeToIncubate(); + return true; + } + } + + return QObject::event(e); +} + +void QSGD3D12ThreadedRenderLoop::onAnimationStarted() +{ + startOrStopAnimationTimer(); + + for (const WindowData &w : qAsConst(windows)) + w.window->requestUpdate(); +} + +void QSGD3D12ThreadedRenderLoop::onAnimationStopped() +{ + startOrStopAnimationTimer(); +} + +void QSGD3D12ThreadedRenderLoop::startOrStopAnimationTimer() +{ + int exposedWindowCount = 0; + const WindowData *exposed = nullptr; + + for (int i = 0; i < windows.size(); ++i) { + const WindowData &w(windows[i]); + if (w.window->isVisible() && w.window->isExposed()) { + ++exposedWindowCount; + exposed = &w; + } + } + + if (animationTimer && (exposedWindowCount == 1 || !anim->isRunning())) { + killTimer(animationTimer); + animationTimer = 0; + // If animations are running, make sure we keep on animating + if (anim->isRunning()) + exposed->window->requestUpdate(); + } else if (!animationTimer && exposedWindowCount != 1 && anim->isRunning()) { + animationTimer = startTimer(qsgrl_animation_interval()); + } +} + +void QSGD3D12ThreadedRenderLoop::handleExposure(QQuickWindow *window) +{ + if (Q_UNLIKELY(debug_loop())) + qDebug() << "handleExposure" << window; + + WindowData *w = windowFor(windows, window); + if (!w) { + if (Q_UNLIKELY(debug_loop())) + qDebug("adding window to list"); + WindowData win; + win.window = window; + QSGRenderContext *rc = QQuickWindowPrivate::get(window)->context; // will transfer ownership + win.thread = new QSGD3D12RenderThread(this, rc); + win.updateDuringSync = false; + win.forceRenderPass = true; // also covered by polishAndSync(inExpose=true), but doesn't hurt + windows.append(win); + w = &windows.last(); + } + + // set this early as we'll be rendering shortly anyway and this avoids + // special casing exposure in polishAndSync. + w->thread->exposedWindow = window; + + if (w->window->size().isEmpty() + || (w->window->isTopLevel() && !w->window->geometry().intersects(w->window->screen()->availableGeometry()))) { +#ifndef QT_NO_DEBUG + qWarning().noquote().nospace() << "QSGD3D12ThreadedRenderLoop: expose event received for window " + << w->window << " with invalid geometry: " << w->window->geometry() + << " on " << w->window->screen(); +#endif + } + + if (!w->window->handle()) + w->window->create(); + + // Start render thread if it is not running + if (!w->thread->isRunning()) { + if (Q_UNLIKELY(debug_loop())) + qDebug("starting render thread"); + // Push a few things to the render thread. + QQuickAnimatorController *controller = QQuickWindowPrivate::get(w->window)->animationController; + if (controller->thread() != w->thread) + controller->moveToThread(w->thread); + if (w->thread->thread() == QThread::currentThread()) { + w->thread->rc->moveToThread(w->thread); + w->thread->moveToThread(w->thread); + } + + w->thread->active = true; + w->thread->start(); + + if (!w->thread->isRunning()) + qFatal("Render thread failed to start, aborting application."); + } + + polishAndSync(w, true); + + startOrStopAnimationTimer(); +} + +void QSGD3D12ThreadedRenderLoop::handleObscurity(WindowData *w) +{ + if (Q_UNLIKELY(debug_loop())) + qDebug() << "handleObscurity" << w->window; + + if (w->thread->isRunning()) { + w->thread->mutex.lock(); + w->thread->postEvent(new QSGD3D12WindowEvent(w->window, WM_Obscure)); + w->thread->waitCondition.wait(&w->thread->mutex); + w->thread->mutex.unlock(); + } + + startOrStopAnimationTimer(); +} + +void QSGD3D12ThreadedRenderLoop::scheduleUpdate(WindowData *w) +{ + if (!QCoreApplication::instance()) + return; + + if (!w || !w->thread->isRunning()) + return; + + QThread *current = QThread::currentThread(); + if (current != QCoreApplication::instance()->thread() && (current != w->thread || !lockedForSync)) { + qWarning() << "Updates can only be scheduled from GUI thread or from QQuickItem::updatePaintNode()"; + return; + } + + if (current == w->thread) { + w->updateDuringSync = true; + return; + } + + w->window->requestUpdate(); +} + +void QSGD3D12ThreadedRenderLoop::handleResourceRelease(WindowData *w, bool destroying) +{ + if (Q_UNLIKELY(debug_loop())) + qDebug() << "handleResourceRelease" << (destroying ? "destroying" : "hide/releaseResources") << w->window; + + w->thread->mutex.lock(); + if (w->thread->isRunning() && w->thread->active) { + QQuickWindow *window = w->window; + + // Note that window->handle() is typically null by this time because + // the platform window is already destroyed. This should not be a + // problem for the D3D cleanup. + + w->thread->postEvent(new QSGD3D12TryReleaseEvent(window, destroying)); + w->thread->waitCondition.wait(&w->thread->mutex); + + // Avoid a shutdown race condition. + // If SG is invalidated and 'active' becomes false, the thread's run() + // method will exit. handleExposure() relies on QThread::isRunning() (because it + // potentially needs to start the thread again) and our mutex cannot be used to + // track the thread stopping, so we wait a few nanoseconds extra so the thread + // can exit properly. + if (!w->thread->active) + w->thread->wait(); + } + w->thread->mutex.unlock(); +} + +void QSGD3D12ThreadedRenderLoop::polishAndSync(WindowData *w, bool inExpose) +{ + if (Q_UNLIKELY(debug_loop())) + qDebug() << "polishAndSync" << (inExpose ? "(in expose)" : "(normal)") << w->window; + + QQuickWindow *window = w->window; + if (!w->thread || !w->thread->exposedWindow) { + if (Q_UNLIKELY(debug_loop())) + qDebug("polishAndSync - not exposed, abort"); + return; + } + + // Flush pending touch events. + QQuickWindowPrivate::get(window)->flushFrameSynchronousEvents(); + // The delivery of the event might have caused the window to stop rendering + w = windowFor(windows, window); + if (!w || !w->thread || !w->thread->exposedWindow) { + if (Q_UNLIKELY(debug_loop())) + qDebug("polishAndSync - removed after touch event flushing, abort"); + return; + } + + QElapsedTimer timer; + qint64 polishTime = 0; + qint64 waitTime = 0; + qint64 syncTime = 0; + if (Q_UNLIKELY(debug_time())) + timer.start(); + Q_QUICK_SG_PROFILE_START(QQuickProfiler::SceneGraphPolishAndSync); + + QQuickWindowPrivate *wd = QQuickWindowPrivate::get(window); + wd->polishItems(); + + if (Q_UNLIKELY(debug_time())) + polishTime = timer.nsecsElapsed(); + Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphPolishAndSync); + + w->updateDuringSync = false; + + emit window->afterAnimating(); + + if (Q_UNLIKELY(debug_loop())) + qDebug("polishAndSync - lock for sync"); + w->thread->mutex.lock(); + lockedForSync = true; + w->thread->postEvent(new QSGD3D12SyncEvent(window, inExpose, w->forceRenderPass)); + w->forceRenderPass = false; + + if (Q_UNLIKELY(debug_loop())) + qDebug("polishAndSync - wait for sync"); + if (Q_UNLIKELY(debug_time())) + waitTime = timer.nsecsElapsed(); + Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphPolishAndSync); + w->thread->waitCondition.wait(&w->thread->mutex); + lockedForSync = false; + w->thread->mutex.unlock(); + if (Q_UNLIKELY(debug_loop())) + qDebug("polishAndSync - unlock after sync"); + + if (Q_UNLIKELY(debug_time())) + syncTime = timer.nsecsElapsed(); + Q_QUICK_SG_PROFILE_RECORD(QQuickProfiler::SceneGraphPolishAndSync); + + if (!animationTimer && anim->isRunning()) { + if (Q_UNLIKELY(debug_loop())) + qDebug("polishAndSync - advancing animations"); + anim->advance(); + // We need to trigger another sync to keep animations running... + w->window->requestUpdate(); + emit timeToIncubate(); + } else if (w->updateDuringSync) { + w->window->requestUpdate(); + } + + if (Q_UNLIKELY(debug_time())) + qDebug().nospace() + << "Frame prepared with 'd3d12' renderloop" + << ", polish=" << (polishTime / 1000000) + << ", lock=" << (waitTime - polishTime) / 1000000 + << ", blockedForSync=" << (syncTime - waitTime) / 1000000 + << ", animations=" << (timer.nsecsElapsed() - syncTime) / 1000000 + << " - (on gui thread) " << window; + + Q_QUICK_SG_PROFILE_END(QQuickProfiler::SceneGraphPolishAndSync); +} + +#include "qsgd3d12threadedrenderloop.moc" + +QT_END_NAMESPACE diff --git a/src/plugins/scenegraph/d3d12/qsgd3d12threadedrenderloop_p.h b/src/plugins/scenegraph/d3d12/qsgd3d12threadedrenderloop_p.h new file mode 100644 index 0000000000..46f62948f1 --- /dev/null +++ b/src/plugins/scenegraph/d3d12/qsgd3d12threadedrenderloop_p.h @@ -0,0 +1,129 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef QSGD3D12THREADEDRENDERLOOP_P_H +#define QSGD3D12THREADEDRENDERLOOP_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qsgrenderloop_p.h> + +QT_BEGIN_NAMESPACE + +class QSGD3D12Engine; +class QSGD3D12Context; +class QSGD3D12RenderContext; +class QSGD3D12RenderThread; + +class QSGD3D12ThreadedRenderLoop : public QSGRenderLoop +{ + Q_OBJECT + +public: + QSGD3D12ThreadedRenderLoop(); + ~QSGD3D12ThreadedRenderLoop(); + + void show(QQuickWindow *window) override; + void hide(QQuickWindow *window) override; + void resize(QQuickWindow *window) override; + + void windowDestroyed(QQuickWindow *window) override; + + void exposureChanged(QQuickWindow *window) override; + + QImage grab(QQuickWindow *window) override; + + void update(QQuickWindow *window) override; + void maybeUpdate(QQuickWindow *window) override; + void handleUpdateRequest(QQuickWindow *window) override; + + QAnimationDriver *animationDriver() const override; + + QSGContext *sceneGraphContext() const override; + QSGRenderContext *createRenderContext(QSGContext *) const override; + + void releaseResources(QQuickWindow *window) override; + void postJob(QQuickWindow *window, QRunnable *job) override; + + QSurface::SurfaceType windowSurfaceType() const override; + bool interleaveIncubation() const override; + int flags() const override; + + bool event(QEvent *e) override; + +public Q_SLOTS: + void onAnimationStarted(); + void onAnimationStopped(); + +private: + struct WindowData { + QQuickWindow *window; + QSGD3D12RenderThread *thread; + uint updateDuringSync : 1; + uint forceRenderPass : 1; + }; + + void startOrStopAnimationTimer(); + void handleExposure(QQuickWindow *window); + void handleObscurity(WindowData *w); + void scheduleUpdate(WindowData *w); + void handleResourceRelease(WindowData *w, bool destroying); + void polishAndSync(WindowData *w, bool inExpose); + + QSGD3D12Context *sg; + QAnimationDriver *anim; + int animationTimer = 0; + bool lockedForSync = false; + QVector<WindowData> windows; + + friend class QSGD3D12RenderThread; +}; + +QT_END_NAMESPACE + +#endif // QSGD3D12THREADEDRENDERLOOP_P_H |