/**************************************************************************** ** ** Copyright (C) 2018 The Qt Company Ltd. ** Contact: http://www.qt.io/licensing/ ** ** This file is part of Qt 3D Studio. ** ** $QT_BEGIN_LICENSE:GPL$ ** 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 General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 or (at your option) 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.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-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "q3dsstudio3dengine_p.h" #include "q3dslayer3d_p.h" #include "q3dslayer3dsgnode_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE /*! \qmltype Studio3DEngine //! \instantiates Q3DSImGuiItem \inqmlmodule Q3DSStudio3DEngine \ingroup 3dstudioruntime2 \inherits Item \since Qt 3D Studio 2.1 \internal \brief A non-visual item representing a Qt 3D Studio + Qt 3D engine. Unlike \c Scene3D or \l Studio3D where the output of the 3D renderer is shown in a single rectangular area, more advanced 3D integration approaches follow a split engine-views model. The enabler for this is Studio3DEngine which, while an Item, does not render anything. Instead, QML scenes must contain \l Layer3D visual items in addition to Studio3DEngine. Each \l Layer3D instance is connected to a Studio3DEngine and renders the contents of a single \c layer from the 3D scene. They are lightweight and pose no limitations on where or how they can be placed, transformed or blended in the Qt Quick scene. \note Applications should never have more than one instance of Studio3DEngine because running multiple graphics engine stacks in parallel leads to performance degradation and a number of subtle problems. \sa Layer3D */ #define DUMMY_W 64 #define DUMMY_H 64 static bool engineCleanerRegistered = false; static QSet engineTracker; static void engineCleaner() { // We cannot go down with engines alive, mainly because some Qt 3D stuff // uses threads which need proper shutdown. QSet strayEngines = std::move(engineTracker); for (Q3DSEngine *engine : strayEngines) delete engine; } Q3DSStudio3DEngine::Q3DSStudio3DEngine(QQuickItem *parent) : QQuickItem(parent) { if (!engineCleanerRegistered) { qAddPostRoutine(engineCleaner); engineCleanerRegistered = true; } // Unlike Studio3D, Studio3DEngine is a non-visual item. It is still an // item since we absolutely need to be associated with a QQuickWindow for // various reasons. By being non-visual, there is no update() and // updatePaintNode mechanism. Yet the usual item-stuff-on-main-thread / // sg-stuff-on-render-thread split applies here as well due to the // lifecycle issues (the item may go away long before the rendering on the // render thread etc.), so most of this below is similar to Studio3D, // except we tie ourselves to the beforeSynchronizing signal for doing stuff // on the render thread with the main thread locked. setFlag(QQuickItem::ItemHasContents, false); Q3DSUtils::setDialogsEnabled(false); } Q3DSStudio3DEngine::~Q3DSStudio3DEngine() { // No cleanup here. m_engine cannot just be destroyed since the renderer // may be happily marching on on the render thread at this point. Hence the // releaseResources / sceneGraphInvalidated / qAddPostRoutine trio. } // Let's do resource handling correctly, which means handling releaseResources // on the item and connecting to the sceneGraphInvalidated signal. Care must be // taken to support the case of moving the item from window to another as well. void Q3DSStudio3DEngine::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &changeData) { if (change == QQuickItem::ItemSceneChange) { if (changeData.window) { connect(changeData.window, &QQuickWindow::sceneGraphInvalidated, this, [this]() { qCDebug(lcStudio3D, "[R] sceneGraphInvalidated"); delete m_renderer; m_renderer = nullptr; QMetaObject::invokeMethod(this, "destroyEngine"); }, Qt::DirectConnection); // let the magic begin createEngine(); // our updatePaintNode alternative connect(changeData.window, &QQuickWindow::beforeSynchronizing, this, &Q3DSStudio3DEngine::updateViews, Qt::DirectConnection); } } } class EngineReleaser : public QObject { public: EngineReleaser(Q3DSEngine *engine) : m_engine(engine) { } ~EngineReleaser() { if (engineTracker.contains(m_engine)) { qCDebug(lcStudio3D, "async release: destroying engine %p", m_engine); engineTracker.remove(m_engine); delete m_engine; } // Note: Here the destruction of the old engine and the creation of a // new one will overlap (if the item survives, that is). } private: Q3DSEngine *m_engine; }; class RendererReleaser : public QRunnable { public: RendererReleaser(QObject *r, EngineReleaser *er) : m_renderer(r), m_engineReleaser(er) { } void run() override { delete m_renderer; // now, if this is on the render thread (Qt Quick with threaded render // loop) and the application is exiting, the deleteLater may not // actually be executed ever. Hence the need for the post routine and // engineTracker. However, if the application stays alive and we are // cleaning up for another reason, this is just fine since the engine // will eventually get deleted fine by the main thread. m_engineReleaser->deleteLater(); } private: QObject *m_renderer; EngineReleaser *m_engineReleaser; }; void Q3DSStudio3DEngine::releaseResources() { qCDebug(lcStudio3D, "releaseResources"); // this may not be an application exit; if this is just a window change then allow continuing // by eventually creating new engine and renderer objects releaseEngineAndRenderer(); } void Q3DSStudio3DEngine::releaseEngineAndRenderer() { if (!window() || !m_renderer) return; // renderer must be destroyed first (on the Quick render thread) if (m_renderer->thread() == QThread::currentThread()) { delete m_renderer; m_renderer = nullptr; destroyEngine(); } else { // by the time the runnable runs we (the item) may already be gone; that's fine, just pass the renderer ref EngineReleaser *er = new EngineReleaser(m_engine); RendererReleaser *rr = new RendererReleaser(m_renderer, er); // Yes, this could be partly merged into our beforeSynchronizing // handler but it would not be any more lightweight since the // EngineReleaser needs to be sent over, which then needs // synchronization, etc. So stick with a render job. window()->scheduleRenderJob(rr, QQuickWindow::BeforeSynchronizingStage); m_renderer->invalidateEngine(); m_renderer = nullptr; m_engine = nullptr; } } void Q3DSStudio3DEngine::startEngine() { if (m_engine) { qCDebug(lcStudio3D, "Engine start (setting Qt3D root entity)"); Q_ASSERT(m_renderer); if (m_engine->start()) m_renderer->notifyEngineStart(); } else { qWarning("No engine (no window for item?), cannot start"); } } void Q3DSStudio3DEngine::destroyEngine() { if (m_engine) { Q_ASSERT(!m_renderer); qCDebug(lcStudio3D, "destroying engine %p", m_engine); engineTracker.remove(m_engine); delete m_engine; // recreate on next window change - if we are still around, that is m_engine = nullptr; } } void Q3DSStudio3DEngine::sendResizeToQt3D(const QSize &size, qreal dpr) { Qt3DCore::QEntity *rootEntity = m_engine->rootEntity(); if (rootEntity) { Qt3DRender::QRenderSurfaceSelector *surfaceSelector = Qt3DRender::QRenderSurfaceSelectorPrivate::find(rootEntity); qCDebug(lcStudio3D, "Setting external render target size on surface selector %p", surfaceSelector); if (surfaceSelector) { surfaceSelector->setExternalRenderTargetSize(size); surfaceSelector->setSurfacePixelRatio(float(dpr)); } } } void Q3DSStudio3DEngine::createEngine() { if (m_engine) return; QQuickWindow *w = window(); Q_ASSERT(w); const qreal dpr = w->effectiveDevicePixelRatio(); const QSize size = QSize(DUMMY_W, DUMMY_H) * dpr; m_engine = new Q3DSEngine; engineTracker.insert(m_engine); Q3DSEngine::Flags flags = Q3DSEngine::WithoutRenderAspect | Q3DSEngine::EnableProfiling; m_engine->setFlags(flags); // Create the implicit presentation, root object and slides. m_presentation = new Q3DSUipPresentation; m_presentation->setPresentationWidth(size.width()); m_presentation->setPresentationHeight(size.height()); m_scene = m_presentation->newObject("Scene"); m_scene->setName(QLatin1String("Scene")); m_presentation->setScene(m_scene); m_masterSlide = m_presentation->newObject("MasterSlide"); m_masterSlide->setName(QLatin1String("MasterSlide")); m_slide = m_presentation->newObject("Slide"); m_slide->setName(QLatin1String("Slide")); m_masterSlide->appendChildNode(m_slide); m_presentation->setMasterSlide(m_masterSlide); // Qt 3D needs a surface always, keep it happy. if (QWindow *rw = QQuickRenderControl::renderWindowFor(w)) { // rw is the top-level window that is backed by a native window. Do // not use that though since we must not clash with e.g. the widget // backingstore compositor in the main thread. QOffscreenSurface *dummySurface = new QOffscreenSurface; dummySurface->setParent(qGuiApp); // parent to something suitably long-living dummySurface->setFormat(rw->format()); dummySurface->create(); m_engine->setSurface(dummySurface); } else { m_engine->setSurface(w); } // Create scenemanager and build the initial Qt 3D scene. m_engine->setPresentation(m_presentation); m_engine->resize(size, dpr); sendResizeToQt3D(size, dpr); // don't bother composing the layers via Qt 3D m_engine->setMainLayerComposition(false); } void Q3DSStudio3DEngine::updateViews() { // called on the render thread with main thread blocked QQuickWindow *w = window(); if (!w) return; if (!m_renderer) m_renderer = new Renderer(this, m_engine->aspectEngine()); if (m_pendingViewSend) { m_pendingViewSend = false; QVector layers; // Gather the layer names (or ids) that need to be exposed. for (Q3DSLayer3D *layerItem : m_views) { Q3DSLayerNode *layer3DS = layerItem->layerNode(); if (!layer3DS) continue; Qt3DCore::QNodeId nodeId = m_engine->layerTextureNodeId(layer3DS); if (!nodeId.isNull()) { Renderer::Layer layer; layer.item = layerItem; layer.textureNodeId = nodeId; Q3DSLayerAttached *layerData = layer3DS->attached(); layer.pixelSize = layerData->layerSize * layerData->ssaaScaleFactor; layer.sampleCount = layerData->msaaSampleCount; layer.itemSizeWithoutDpr = layerItem->size().toSize(); layers.append(layer); } else { qWarning("No QNodeId for layer %s texture?", layer3DS->id().constData()); } } m_renderer->setLayers(layers); } } class ContextSaver { public: ContextSaver() { m_context = QOpenGLContext::currentContext(); m_surface = m_context ? m_context->surface() : nullptr; } ~ContextSaver() { if (m_context && m_context->surface() != m_surface) m_context->makeCurrent(m_surface); } QOpenGLContext *context() const { return m_context; } QSurface *surface() const { return m_surface; } private: QOpenGLContext *m_context; QSurface *m_surface; }; // There is one renderer object for each Studio3DEngine. It lives on the Qt // Quick render thread (which may also be the main thread with the basic // render loop). Q3DSStudio3DEngine::Renderer::Renderer(Q3DSStudio3DEngine *engine, Qt3DCore::QAspectEngine *aspectEngine) : m_engine(engine), m_aspectEngine(aspectEngine) { qCDebug(lcStudio3D, "[R] new renderer %p, window is %p, aspect engine %p", this, m_engine->window(), m_aspectEngine); connect(m_engine->window(), &QQuickWindow::beforeRendering, this, &Q3DSStudio3DEngine::Renderer::render, Qt::DirectConnection); m_usingRenderThread = QThread::currentThread() != qGuiApp->thread(); // there is no OpenGL context guaranteed to be current here so defer everything else } Q3DSStudio3DEngine::Renderer::~Renderer() { qCDebug(lcStudio3D, "[R] renderer %p dtor, QOpenGLContext %p", this, QOpenGLContext::currentContext()); { ContextSaver saver; m_renderAspectD->renderShutdown(); } delete m_fbo; } void Q3DSStudio3DEngine::Renderer::invalidateEngine() { // must be called from the main thread (or not at all if Quick render thread == main) Q_ASSERT(thread() != QThread::currentThread()); QMutexLocker lock(&m_engineInvalidLock); m_engineInvalid = true; } void Q3DSStudio3DEngine::Renderer::notifyEngineStart() { m_engineStarted.store(1); } static bool sampleCountEquals(int a, int b) { return a == b || (a <= 1 && b <= 1); } void Q3DSStudio3DEngine::Renderer::render() { // m_engine may be destroyed already QMutexLocker lock(&m_engineInvalidLock); if (m_engineInvalid) return; // the end is nigh - sit back and relax til the RendererReleaser comes for us QQuickWindow *w = m_engine->window(); if (!w) return; if (!m_renderAspect) { QOpenGLContext *ctx = QOpenGLContext::currentContext(); qCDebug(lcStudio3D, "[R] got first render(), QOpenGLContext %p", ctx); { ContextSaver saver; m_renderAspect = new Qt3DRender::QRenderAspect(Qt3DRender::QRenderAspect::Synchronous); m_aspectEngine->registerAspect(m_renderAspect); m_renderAspectD = static_cast(Qt3DRender::QRenderAspectPrivate::get(m_renderAspect)); m_renderAspectD->renderInitialize(ctx); } // off we go QMetaObject::invokeMethod(m_engine, "startEngine"); m_renderTimer.start(); } // With the threaded render loop we may skip 1 or 2 frames since there is // no point in doing anything until Qt 3D can render the actual scene, and // there is no telling when startEngine() is actually invoked on the main // thread... if (!m_engineStarted.load()) { w->update(); return; } // make sure there is a "default" render target even though nothing is output to it if (!m_fbo) { const QSize size = QSize(DUMMY_W, DUMMY_H) * w->effectiveDevicePixelRatio(); m_fbo = new QOpenGLFramebufferObject(size, QOpenGLFramebufferObject::CombinedDepthStencil); } { ContextSaver saver; w->resetOpenGLState(); m_fbo->bind(); m_renderAspectD->renderSynchronous(); } w->resetOpenGLState(); QSharedPointer ra = m_renderAspectD->m_renderer->nodeManagers()->resourceAccessor(); if (ra) { for (const Layer &layer : m_layers) { QOpenGLTexture *tex = nullptr; ra->accessResource(Qt3DRender::Render::ResourceAccessor::OGLTextureRead, layer.textureNodeId, (void**) &tex, nullptr); if (tex) { uint id = tex->textureId(); Q3DSLayer3DSGNode *viewNode = layer.item->node(); if (viewNode) { if (uint(viewNode->texture()->textureId()) != id || viewNode->texture()->textureSize() != layer.pixelSize || !sampleCountEquals(viewNode->sampleCount(), layer.sampleCount) || viewNode->rect().size() != layer.itemSizeWithoutDpr) { QSGTexture *t = w->createTextureFromId(id, layer.pixelSize, QQuickWindow::TextureHasAlphaChannel); Q_ASSERT(t); t->setFiltering(QSGTexture::Linear); viewNode->setTexture(t, layer.sampleCount); // owns, so previous texture is destroyed layer.item->notifyTextureChange(t, layer.sampleCount); viewNode->setRect(QRectF(QPointF(0, 0), layer.itemSizeWithoutDpr)); } viewNode->markDirty(QSGNode::DirtyMaterial); } } } } if (m_engine->engine()) m_engine->engine()->reportQuickRenderLoopStats(m_renderTimer.restart(), m_usingRenderThread); // ### hmm... like Scene3D and Studio3D, this also suffers from the problem // of forcing continuous updates on Qt Quick level since there is no way to // know if a QQuickWindow render round is necessary as there is no telling // if the Qt 3D frame tied to beforeRendering will generate output // different than before. w->update(); } void Q3DSStudio3DEngine::registerView(Q3DSLayer3D *view) { m_views.append(view); m_pendingViewSend = true; } void Q3DSStudio3DEngine::unregisterView(Q3DSLayer3D *view) { m_views.removeOne(view); m_pendingViewSend = true; } void Q3DSStudio3DEngine::handleViewGeometryChange(Q3DSLayer3D *view, const QSize &size) { if (!m_engine) return; Q3DSLayerNode *layer3DS = view->layerNode(); if (!layer3DS) return; if (!layer3DS->hasExplicitSize() || layer3DS->explicitSize() != size) { qCDebug(lcScene, "Explicit size for layer %s is %dx%d", layer3DS->id().constData(), size.width(), size.height()); // Set the desired size and generate a property change. The // property name may be something bogus but that's enough to get // the scenemanager to recalculate size-related things in the // layer. Eventually it will lead to emitting // Q3DSEngine::layerResized. Note that this is all asynchronous and // may only happen on the next frame action. layer3DS->notifyPropertyChanges({ layer3DS->setExplicitSize(true, size) }); } } QT_END_NAMESPACE