diff options
| author | JiDe Zhang <zhangjide@uniontech.com> | 2022-04-05 16:01:19 +0800 |
|---|---|---|
| committer | JiDe Zhang <zhangjide@uniontech.com> | 2025-12-12 15:35:42 +0800 |
| commit | 2e0e3000db2c76d6b20affc95498ee2bf5bdbc43 (patch) | |
| tree | ea7a204d385290a08185f91aea8dfe36fefcdda8 /examples/quick/rendercontrol/rendercontrol_opengl | |
| parent | 712dbe34bc7c10d8503f0bc352eced2d890fb7af (diff) | |
Restore multi-threaded OpenGL path in rendercontrol_opengl example
Porting from the Qt5.
Pick-to: 6.11
Task-number: QTBUG-102301
Change-Id: Ic4aa3e4c3f759ccd6e04ef77ed5c2f888e7f9aa0
Reviewed-by: JiDe Zhang <zhangjide@uniontech.com>
Reviewed-by: Liang Qi <liang.qi@qt.io>
Diffstat (limited to 'examples/quick/rendercontrol/rendercontrol_opengl')
5 files changed, 591 insertions, 5 deletions
diff --git a/examples/quick/rendercontrol/rendercontrol_opengl/CMakeLists.txt b/examples/quick/rendercontrol/rendercontrol_opengl/CMakeLists.txt index ac277f533c..0c5021f7a8 100644 --- a/examples/quick/rendercontrol/rendercontrol_opengl/CMakeLists.txt +++ b/examples/quick/rendercontrol/rendercontrol_opengl/CMakeLists.txt @@ -4,7 +4,7 @@ cmake_minimum_required(VERSION 3.16) project(rendercontrol_opengl LANGUAGES CXX) -find_package(Qt6 REQUIRED COMPONENTS Core Gui OpenGL Qml Quick) +find_package(Qt6 REQUIRED COMPONENTS Core Gui GuiPrivate OpenGL Qml Quick) qt_standard_project_setup(REQUIRES 6.8) @@ -12,11 +12,13 @@ qt_add_executable(rendercontrol_openglexample WIN32 MACOSX_BUNDLE cuberenderer.cpp cuberenderer.h main.cpp window_singlethreaded.cpp window_singlethreaded.h + window_multithreaded.cpp window_multithreaded.h ) target_link_libraries(rendercontrol_openglexample PRIVATE Qt6::Core Qt6::Gui + Qt6::GuiPrivate Qt6::OpenGL Qt6::Qml Qt6::Quick diff --git a/examples/quick/rendercontrol/rendercontrol_opengl/main.cpp b/examples/quick/rendercontrol/rendercontrol_opengl/main.cpp index 849d9d38d6..f882109545 100644 --- a/examples/quick/rendercontrol/rendercontrol_opengl/main.cpp +++ b/examples/quick/rendercontrol/rendercontrol_opengl/main.cpp @@ -3,7 +3,9 @@ #include <QGuiApplication> #include <QQuickWindow> +#include <QCommandLineParser> #include "window_singlethreaded.h" +#include "window_multithreaded.h" int main(int argc, char **argv) { @@ -12,9 +14,29 @@ int main(int argc, char **argv) // only functional when Qt Quick is also using OpenGL QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL); - WindowSingleThreaded window; - window.resize(1024, 768); - window.show(); + QCoreApplication::setApplicationName("Qt Render Control Example"); + QCoreApplication::setOrganizationName("QtProject"); + QCoreApplication::setApplicationVersion(QT_VERSION_STR); + QCommandLineParser parser; + parser.setApplicationDescription(QCoreApplication::applicationName()); + parser.addHelpOption(); + parser.addVersionOption(); + QCommandLineOption threadedOption("threaded", "Threaded Rendering"); + parser.addOption(threadedOption); + + parser.process(app); + + QScopedPointer<QWindow> window; + if (parser.isSet(threadedOption)) { + qWarning("Using separate Qt Quick render thread"); + window.reset(new WindowMultiThreaded); + } else { + qWarning("Using single-threaded rendering"); + window.reset(new WindowSingleThreaded); + } + + window->resize(1024, 768); + window->show(); return app.exec(); } diff --git a/examples/quick/rendercontrol/rendercontrol_opengl/rendercontrol_opengl.pro b/examples/quick/rendercontrol/rendercontrol_opengl/rendercontrol_opengl.pro index 3f32a0dff4..783fdf096d 100644 --- a/examples/quick/rendercontrol/rendercontrol_opengl/rendercontrol_opengl.pro +++ b/examples/quick/rendercontrol/rendercontrol_opengl/rendercontrol_opengl.pro @@ -1,12 +1,14 @@ TEMPLATE = app -QT += quick qml opengl +QT += quick qml opengl gui-private SOURCES += main.cpp \ window_singlethreaded.cpp \ + window_multithreaded.cpp \ cuberenderer.cpp HEADERS += window_singlethreaded.h \ + window_multithreaded.h \ cuberenderer.h RESOURCES += rendercontrol.qrc diff --git a/examples/quick/rendercontrol/rendercontrol_opengl/window_multithreaded.cpp b/examples/quick/rendercontrol/rendercontrol_opengl/window_multithreaded.cpp new file mode 100644 index 0000000000..f858e20e8f --- /dev/null +++ b/examples/quick/rendercontrol/rendercontrol_opengl/window_multithreaded.cpp @@ -0,0 +1,452 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include "window_multithreaded.h" +#include "cuberenderer.h" +#include <QOpenGLContext> +#include <QOpenGLFunctions> +#include <QOpenGLFramebufferObject> +#include <QOpenGLShaderProgram> +#include <QOpenGLVertexArrayObject> +#include <QOpenGLBuffer> +#include <QOpenGLVertexArrayObject> +#include <QOffscreenSurface> +#include <QQmlEngine> +#include <QQmlComponent> +#include <QQuickItem> +#include <QQuickWindow> +#include <QQuickRenderControl> +#include <QQuickRenderTarget> +#include <QCoreApplication> +#include <QQuickGraphicsDevice> +#include <QQuickGraphicsConfiguration> +#include <rhi/qrhi.h> + +/* + This implementation runs the Qt Quick scenegraph's sync and render phases on a + separate, dedicated thread. Rendering the cube using our custom OpenGL engine + happens on that thread as well. This is similar to the built-in threaded + render loop, but does not support all the features. There is no support for + getting Animators running on the render thread for example. + + We choose to use QObject's event mechanism to communicate with the QObject + living on the render thread. An alternative would be to subclass QThread and + reimplement run() with a custom event handling approach, like + QSGThreadedRenderLoop does. That would potentially lead to better results but + is also more complex. +*/ + +static const QEvent::Type INIT = QEvent::Type(QEvent::User + 1); +static const QEvent::Type RENDER = QEvent::Type(QEvent::User + 2); +static const QEvent::Type RESIZE = QEvent::Type(QEvent::User + 3); +static const QEvent::Type CLEANUP = QEvent::Type(QEvent::User + 4); +static const QEvent::Type STOP = QEvent::Type(QEvent::User + 5); + +static const QEvent::Type UPDATE = QEvent::Type(QEvent::User + 6); + +QuickRenderer::QuickRenderer() + : m_context(nullptr), + m_surface(nullptr), + m_window(nullptr), + m_quickWindow(nullptr), + m_renderControl(nullptr), + m_rhi(nullptr), + m_textureId(0), + m_cubeRenderer(nullptr), + m_quit(false) +{ +} + +void QuickRenderer::requestInit() +{ + QCoreApplication::postEvent(this, new QEvent(INIT)); +} + +void QuickRenderer::requestRender() +{ + QCoreApplication::postEvent(this, new QEvent(RENDER)); +} + +void QuickRenderer::requestResize() +{ + QCoreApplication::postEvent(this, new QEvent(RESIZE)); +} + +void QuickRenderer::requestCleanup() +{ + QCoreApplication::postEvent(this, new QEvent(CLEANUP)); +} + +void QuickRenderer::requestStop() +{ + QCoreApplication::postEvent(this, new QEvent(STOP)); +} + +bool QuickRenderer::event(QEvent *e) +{ + QMutexLocker lock(&m_mutex); + + switch (int(e->type())) { + case INIT: + init(); + return true; + case RENDER: + render(&lock); + return true; + case RESIZE: + if (m_cubeRenderer) + m_cubeRenderer->resize(m_window->width(), m_window->height()); + return true; + case CLEANUP: + cleanup(); + return true; + case STOP: + cleanupRhi(); + return true; + default: + return QObject::event(e); + } +} + +void QuickRenderer::init() +{ + m_context->makeCurrent(m_surface); + + // Pass our offscreen surface to the cube renderer just so that it will + // have something is can make current during cleanup. QOffscreenSurface, + // just like QWindow, must always be created on the gui thread (as it might + // be backed by an actual QWindow). + m_cubeRenderer = new CubeRenderer(m_surface); + m_cubeRenderer->resize(m_window->width(), m_window->height()); + + const QSurfaceFormat format = m_quickWindow->requestedFormat(); + QRhiGles2InitParams rhiParams; + rhiParams.format = format; + rhiParams.fallbackSurface = m_surface; + rhiParams.window = m_quickWindow; + QRhiGles2NativeHandles importDev; + importDev.context = m_context; + + m_rhi = QRhi::create(QRhi::OpenGLES2, &rhiParams, {}, &importDev); + m_quickWindow->setGraphicsDevice(QQuickGraphicsDevice::fromRhi(m_rhi)); + + m_renderControl->initialize(); +} + +void QuickRenderer::cleanup() +{ + m_context->makeCurrent(m_surface); + + m_renderControl->invalidate(); + + if (m_textureId) + m_context->functions()->glDeleteTextures(1, &m_textureId); + + delete m_cubeRenderer; + m_cubeRenderer = nullptr; + + m_quickWindow->setGraphicsDevice({}); + m_quickWindow->setRenderTarget({}); + + m_context->doneCurrent(); + + m_cond.wakeOne(); +} + +void QuickRenderer::cleanupRhi() +{ + delete m_rhi; + m_rhi = nullptr; + m_context->moveToThread(QCoreApplication::instance()->thread()); + + m_cond.wakeOne(); +} + +void QuickRenderer::ensureTexture() +{ + qreal dpr = m_quickWindow->devicePixelRatio(); + QSize textureSize = m_quickWindow->size() * dpr; + QOpenGLFunctions *f = m_context->functions(); + f->glGenTextures(1, &m_textureId); + f->glBindTexture(GL_TEXTURE_2D, m_textureId); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + f->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, textureSize.width(), textureSize.height(), 0, + GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + m_quickWindow->setRenderTarget(QQuickRenderTarget::fromOpenGLTexture(m_textureId, textureSize)); +} + +void QuickRenderer::render(QMutexLocker<QMutex> *lock) +{ + Q_ASSERT(QThread::currentThread() != m_window->thread()); + + if (!m_context->makeCurrent(m_surface)) { + qWarning("Failed to make context current on render thread"); + return; + } + + ensureTexture(); + + m_renderControl->beginFrame(); + // Synchronization and rendering happens here on the render thread. + m_renderControl->sync(); + + // The gui thread can now continue. + m_cond.wakeOne(); + lock->unlock(); + + // Meanwhile on this thread continue with the actual rendering (into the FBO first). + m_renderControl->render(); + m_renderControl->endFrame(); + m_context->functions()->glFlush(); + + // The cube renderer uses its own context, no need to bother with the state here. + + // Get something onto the screen using our custom OpenGL engine. + QMutexLocker quitLock(&m_quitMutex); + if (!m_quit) + m_cubeRenderer->render(m_window, m_context, m_textureId); +} + +void QuickRenderer::aboutToQuit() +{ + QMutexLocker lock(&m_quitMutex); + m_quit = true; +} + +class RenderControl : public QQuickRenderControl +{ +public: + RenderControl(QWindow *w) : m_window(w) { } + QWindow *renderWindow(QPoint *offset) override; + +private: + QWindow *m_window; +}; + +WindowMultiThreaded::WindowMultiThreaded() + : m_qmlComponent(nullptr), + m_rootItem(nullptr), + m_quickInitialized(false), + m_psrRequested(false) +{ + setSurfaceType(QSurface::OpenGLSurface); + + QSurfaceFormat format; + // Qt Quick may need a depth and stencil buffer. Always make sure these are available. + format.setDepthBufferSize(16); + format.setStencilBufferSize(8); + setFormat(format); + + m_context = new QOpenGLContext; + m_context->setFormat(format); + m_context->create(); + + m_offscreenSurface = new QOffscreenSurface; + // Pass m_context->format(), not format. Format does not specify and color buffer + // sizes, while the context, that has just been created, reports a format that has + // these values filled in. Pass this to the offscreen surface to make sure it will be + // compatible with the context's configuration. + m_offscreenSurface->setFormat(m_context->format()); + m_offscreenSurface->create(); + + m_renderControl = new RenderControl(this); + + // Create a QQuickWindow that is associated with out render control. Note that this + // window never gets created or shown, meaning that it will never get an underlying + // native (platform) window. + m_quickWindow = new QQuickWindow(m_renderControl); + + // Create a QML engine. + m_qmlEngine = new QQmlEngine; + if (!m_qmlEngine->incubationController()) + m_qmlEngine->setIncubationController(m_quickWindow->incubationController()); + + m_quickRenderer = new QuickRenderer; + m_quickRenderer->setContext(m_context); + + // These live on the gui thread. Just give access to them on the render thread. + m_quickRenderer->setSurface(m_offscreenSurface); + m_quickRenderer->setWindow(this); + m_quickRenderer->setQuickWindow(m_quickWindow); + m_quickRenderer->setRenderControl(m_renderControl); + + m_quickRendererThread = new QThread; + + // Notify the render control that some scenegraph internals have to live on + // m_quickRenderThread. + m_renderControl->prepareThread(m_quickRendererThread); + + // The QOpenGLContext and the QObject representing the rendering logic on + // the render thread must live on that thread. + m_context->moveToThread(m_quickRendererThread); + m_quickRenderer->moveToThread(m_quickRendererThread); + + m_quickRendererThread->start(); + + // Now hook up the signals. For simplicy we don't differentiate + // between renderRequested (only render is needed, no sync) and + // sceneChanged (polish and sync is needed too). + connect(m_renderControl, &QQuickRenderControl::renderRequested, this, &WindowMultiThreaded::requestUpdate); + connect(m_renderControl, &QQuickRenderControl::sceneChanged, this, &WindowMultiThreaded::requestUpdate); +} + +WindowMultiThreaded::~WindowMultiThreaded() +{ + // Release resources and move the context ownership back to this thread. + m_quickRenderer->mutex()->lock(); + m_quickRenderer->requestCleanup(); + m_quickRenderer->cond()->wait(m_quickRenderer->mutex()); + m_quickRenderer->mutex()->unlock(); + + delete m_renderControl; + delete m_qmlComponent; + delete m_quickWindow; + delete m_qmlEngine; + + // Release rhi + m_quickRenderer->mutex()->lock(); + m_quickRenderer->requestStop(); + m_quickRenderer->cond()->wait(m_quickRenderer->mutex()); + m_quickRenderer->mutex()->unlock(); + + m_quickRendererThread->quit(); + m_quickRendererThread->wait(); + delete m_quickRenderer; + + delete m_offscreenSurface; + delete m_context; +} + +void WindowMultiThreaded::requestUpdate() +{ + if (m_quickInitialized && !m_psrRequested) { + m_psrRequested = true; + QCoreApplication::postEvent(this, new QEvent(UPDATE)); + } +} + +bool WindowMultiThreaded::event(QEvent *e) +{ + if (e->type() == UPDATE) { + polishSyncAndRender(); + m_psrRequested = false; + return true; + } else if (e->type() == QEvent::Close) { + // Avoid rendering on the render thread when the window is about to + // close. Once a QWindow is closed, the underlying platform window will + // go away, even though the QWindow instance itself is still + // valid. Operations like swapBuffers() are futile and only result in + // warnings afterwards. Prevent this. + m_quickRenderer->aboutToQuit(); + } + return QWindow::event(e); +} + +void WindowMultiThreaded::polishSyncAndRender() +{ + Q_ASSERT(QThread::currentThread() == thread()); + + // Polishing happens on the gui thread. + m_renderControl->polishItems(); + // Sync happens on the render thread with the gui thread (this one) blocked. + QMutexLocker lock(m_quickRenderer->mutex()); + m_quickRenderer->requestRender(); + // Wait until sync is complete. + m_quickRenderer->cond()->wait(m_quickRenderer->mutex()); + // Rendering happens on the render thread without blocking the gui (main) + // thread. This is good because the blocking swap (waiting for vsync) + // happens on the render thread, not blocking other work. +} + +void WindowMultiThreaded::run() +{ + disconnect(m_qmlComponent, &QQmlComponent::statusChanged, this, &WindowMultiThreaded::run); + + if (m_qmlComponent->isError()) { + const QList<QQmlError> errorList = m_qmlComponent->errors(); + for (const QQmlError &error : errorList) + qWarning() << error.url() << error.line() << error; + return; + } + + QObject *rootObject = m_qmlComponent->create(); + if (m_qmlComponent->isError()) { + const QList<QQmlError> errorList = m_qmlComponent->errors(); + for (const QQmlError &error : errorList) + qWarning() << error.url() << error.line() << error; + return; + } + + m_rootItem = qobject_cast<QQuickItem *>(rootObject); + if (!m_rootItem) { + qWarning("run: Not a QQuickItem"); + delete rootObject; + return; + } + + // The root item is ready. Associate it with the window. + m_rootItem->setParentItem(m_quickWindow->contentItem()); + + // Update item and rendering related geometries. + updateSizes(); + + m_quickInitialized = true; + + // Initialize the render thread and perform the first polish/sync/render. + m_quickRenderer->requestInit(); + polishSyncAndRender(); +} + +void WindowMultiThreaded::updateSizes() +{ + // Behave like SizeRootObjectToView. + m_rootItem->setWidth(width()); + m_rootItem->setHeight(height()); + + m_quickWindow->setGeometry(0, 0, width(), height()); +} + +void WindowMultiThreaded::startQuick(const QString &filename) +{ + m_qmlComponent = new QQmlComponent(m_qmlEngine, QUrl(filename)); + if (m_qmlComponent->isLoading()) + connect(m_qmlComponent, &QQmlComponent::statusChanged, this, &WindowMultiThreaded::run); + else + run(); +} + +void WindowMultiThreaded::exposeEvent(QExposeEvent *) +{ + if (isExposed()) { + if (!m_quickInitialized) + startQuick(QStringLiteral("qrc:/qt/qml/rendercontrol/demo.qml")); + } +} + +void WindowMultiThreaded::resizeEvent(QResizeEvent *) +{ + // If this is a resize after the scene is up and running, recreate the fbo and the + // Quick item and scene. + if (m_rootItem) { + updateSizes(); + m_quickRenderer->requestResize(); + polishSyncAndRender(); + } +} + +void WindowMultiThreaded::mousePressEvent(QMouseEvent *e) +{ + // Use the constructor taking localPos and screenPos. That puts localPos into the + // event's localPos and windowPos, and screenPos into the event's screenPos. This way + // the windowPos in e is ignored and is replaced by localPos. This is necessary + // because QQuickWindow thinks of itself as a top-level window always. + QMouseEvent mappedEvent(e->type(), e->position(), e->globalPosition(), e->button(), e->buttons(), e->modifiers()); + QCoreApplication::sendEvent(m_quickWindow, &mappedEvent); +} + +void WindowMultiThreaded::mouseReleaseEvent(QMouseEvent *e) +{ + QMouseEvent mappedEvent(e->type(), e->position(), e->globalPosition(), e->button(), e->buttons(), e->modifiers()); + QCoreApplication::sendEvent(m_quickWindow, &mappedEvent); +} diff --git a/examples/quick/rendercontrol/rendercontrol_opengl/window_multithreaded.h b/examples/quick/rendercontrol/rendercontrol_opengl/window_multithreaded.h new file mode 100644 index 0000000000..e505080081 --- /dev/null +++ b/examples/quick/rendercontrol/rendercontrol_opengl/window_multithreaded.h @@ -0,0 +1,108 @@ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#ifndef WINDOW_MULTITHREADED_H +#define WINDOW_MULTITHREADED_H + +#include <QWindow> +#include <QMatrix4x4> +#include <QThread> +#include <QWaitCondition> +#include <QMutex> + +QT_FORWARD_DECLARE_CLASS(QOpenGLContext) +QT_FORWARD_DECLARE_CLASS(QOffscreenSurface) +QT_FORWARD_DECLARE_CLASS(QQuickRenderControl) +QT_FORWARD_DECLARE_CLASS(QQuickWindow) +QT_FORWARD_DECLARE_CLASS(QQmlEngine) +QT_FORWARD_DECLARE_CLASS(QQmlComponent) +QT_FORWARD_DECLARE_CLASS(QQuickItem) +QT_FORWARD_DECLARE_CLASS(QRhi) + +class CubeRenderer; + +class QuickRenderer : public QObject +{ + Q_OBJECT + +public: + QuickRenderer(); + + void requestInit(); + void requestRender(); + void requestResize(); + void requestCleanup(); + void requestStop(); + + QWaitCondition *cond() { return &m_cond; } + QMutex *mutex() { return &m_mutex; } + + void setContext(QOpenGLContext *ctx) { m_context = ctx; } + void setSurface(QOffscreenSurface *s) { m_surface = s; } + void setWindow(QWindow *w) { m_window = w; } + void setQuickWindow(QQuickWindow *w) { m_quickWindow = w; } + void setRenderControl(QQuickRenderControl *r) { m_renderControl = r; } + + void aboutToQuit(); + +private: + bool event(QEvent *e) override; + void init(); + void cleanup(); + void cleanupRhi(); + void ensureTexture(); + void render(QMutexLocker<QMutex> *lock); + + QWaitCondition m_cond; + QMutex m_mutex; + QOpenGLContext *m_context; + QOffscreenSurface *m_surface; + QWindow *m_window; + QQuickWindow *m_quickWindow; + QQuickRenderControl *m_renderControl; + QRhi *m_rhi; + uint m_textureId; + CubeRenderer *m_cubeRenderer; + QMutex m_quitMutex; + bool m_quit; +}; + +class WindowMultiThreaded : public QWindow +{ + Q_OBJECT + +public: + WindowMultiThreaded(); + ~WindowMultiThreaded(); + +protected: + void exposeEvent(QExposeEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + bool event(QEvent *e) override; + +private slots: + void run(); + void requestUpdate(); + void polishSyncAndRender(); + +private: + void startQuick(const QString &filename); + void updateSizes(); + + QuickRenderer *m_quickRenderer; + QThread *m_quickRendererThread; + + QOpenGLContext *m_context; + QOffscreenSurface *m_offscreenSurface; + QQuickRenderControl *m_renderControl; + QQuickWindow *m_quickWindow; + QQmlEngine *m_qmlEngine; + QQmlComponent *m_qmlComponent; + QQuickItem *m_rootItem; + bool m_quickInitialized; + bool m_psrRequested; +}; + +#endif |
