diff options
author | Laszlo Agocs <[email protected]> | 2023-08-10 13:05:29 +0200 |
---|---|---|
committer | Laszlo Agocs <[email protected]> | 2023-08-15 14:13:32 +0200 |
commit | 8024e182f6eac62b36c4474f43f81a98693743d1 (patch) | |
tree | daec7be4ead4a81e9630895fc032482212c178f1 /examples/quick | |
parent | 9b6943a0b887449dd0e294ad2c5ef9779fe0f6f3 (diff) |
Add a QRhi-based QQuickRenderControl example
Task-number: QTBUG-113331
Change-Id: I638e0f2483bf88d6fff3ad929ea6714e64fa199b
Reviewed-by: Qt CI Bot <[email protected]>
Reviewed-by: Andy Nichols <[email protected]>
Diffstat (limited to 'examples/quick')
8 files changed, 942 insertions, 0 deletions
diff --git a/examples/quick/rendercontrol/CMakeLists.txt b/examples/quick/rendercontrol/CMakeLists.txt index b0255dda6f..6adbd31b95 100644 --- a/examples/quick/rendercontrol/CMakeLists.txt +++ b/examples/quick/rendercontrol/CMakeLists.txt @@ -5,3 +5,6 @@ qt_internal_add_example(rendercontrol_opengl) if(WIN32) qt_internal_add_example(rendercontrol_d3d11) endif() +if(TARGET Qt6::Widgets) + qt_internal_add_example(rendercontrol_rhi) +endif() diff --git a/examples/quick/rendercontrol/rendercontrol_rhi/CMakeLists.txt b/examples/quick/rendercontrol/rendercontrol_rhi/CMakeLists.txt new file mode 100644 index 0000000000..fa138d2860 --- /dev/null +++ b/examples/quick/rendercontrol/rendercontrol_rhi/CMakeLists.txt @@ -0,0 +1,41 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) +project(rendercontrol_rhi LANGUAGES CXX) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/quick/rendercontrol_rhi") + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Quick Widgets) + +qt_standard_project_setup(REQUIRES 6.5) + +qt_add_executable(rendercontrol_rhi WIN32 MACOSX_BUNDLE + main.cpp +) + +target_link_libraries(rendercontrol_rhi PRIVATE + Qt6::Core + Qt6::GuiPrivate + Qt6::Quick + Qt6::Widgets +) + +qt_add_qml_module(rendercontrol_rhi + URI rendercontrolrhiexample + QML_FILES + "demo.qml" + RESOURCE_PREFIX + "/" + NO_RESOURCE_TARGET_PATH +) + +install(TARGETS rendercontrol_rhi + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/examples/quick/rendercontrol/rendercontrol_rhi/demo.qml b/examples/quick/rendercontrol/rendercontrol_rhi/demo.qml new file mode 100644 index 0000000000..707d9cb3a4 --- /dev/null +++ b/examples/quick/rendercontrol/rendercontrol_rhi/demo.qml @@ -0,0 +1,156 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import QtQuick +import QtQuick.Particles + +Rectangle { + id: root + + gradient: Gradient { + GradientStop { position: 0; color: "steelblue" } + GradientStop { position: 1; color: "black" } + } + + Text { + anchors.centerIn: parent + text: "Qt Quick in a texture" + font.pointSize: 40 + color: "white" + + SequentialAnimation on rotation { + PauseAnimation { duration: 2500 } + NumberAnimation { from: 0; to: 360; duration: 5000; easing.type: Easing.InOutCubic } + loops: Animation.Infinite + } + } + + ParticleSystem { + id: particles + anchors.fill: parent + + ImageParticle { + id: smoke + system: particles + anchors.fill: parent + groups: ["A", "B"] + source: "qrc:///particleresources/glowdot.png" + colorVariation: 0 + color: "#00111111" + } + ImageParticle { + id: flame + anchors.fill: parent + system: particles + groups: ["C", "D"] + source: "qrc:///particleresources/glowdot.png" + colorVariation: 0.1 + color: "#00ff400f" + } + + Emitter { + id: fire + system: particles + group: "C" + + y: parent.height + width: parent.width + + emitRate: 350 + lifeSpan: 3500 + + acceleration: PointDirection { y: -17; xVariation: 3 } + velocity: PointDirection {xVariation: 3} + + size: 24 + sizeVariation: 8 + endSize: 4 + } + + TrailEmitter { + id: fireSmoke + group: "B" + system: particles + follow: "C" + width: root.width + height: root.height - 68 + + emitRatePerParticle: 1 + lifeSpan: 2000 + + velocity: PointDirection {y:-17*6; yVariation: -17; xVariation: 3} + acceleration: PointDirection {xVariation: 3} + + size: 36 + sizeVariation: 8 + endSize: 16 + } + + TrailEmitter { + id: fireballFlame + anchors.fill: parent + system: particles + group: "D" + follow: "E" + + emitRatePerParticle: 120 + lifeSpan: 180 + emitWidth: TrailEmitter.ParticleSize + emitHeight: TrailEmitter.ParticleSize + emitShape: EllipseShape{} + + size: 16 + sizeVariation: 4 + endSize: 4 + } + + TrailEmitter { + id: fireballSmoke + anchors.fill: parent + system: particles + group: "A" + follow: "E" + + emitRatePerParticle: 128 + lifeSpan: 2400 + emitWidth: TrailEmitter.ParticleSize + emitHeight: TrailEmitter.ParticleSize + emitShape: EllipseShape{} + + velocity: PointDirection {yVariation: 16; xVariation: 16} + acceleration: PointDirection {y: -16} + + size: 24 + sizeVariation: 8 + endSize: 8 + } + + Emitter { + id: balls + system: particles + group: "E" + + y: parent.height + width: parent.width + + emitRate: 2 + lifeSpan: 7000 + + velocity: PointDirection {y:-17*4*2; xVariation: 6*6} + acceleration: PointDirection {y: 17*2; xVariation: 6*6} + + size: 8 + sizeVariation: 4 + } + + Turbulence { //A bit of turbulence makes the smoke look better + anchors.fill: parent + groups: ["A","B"] + strength: 32 + system: particles + } + } + + onWidthChanged: particles.reset() + onHeightChanged: particles.reset() +} diff --git a/examples/quick/rendercontrol/rendercontrol_rhi/doc/images/rendercontrol-rhi-example.jpg b/examples/quick/rendercontrol/rendercontrol_rhi/doc/images/rendercontrol-rhi-example.jpg Binary files differnew file mode 100644 index 0000000000..369f1edf84 --- /dev/null +++ b/examples/quick/rendercontrol/rendercontrol_rhi/doc/images/rendercontrol-rhi-example.jpg diff --git a/examples/quick/rendercontrol/rendercontrol_rhi/doc/src/rendercontrol_rhi.qdoc b/examples/quick/rendercontrol/rendercontrol_rhi/doc/src/rendercontrol_rhi.qdoc new file mode 100644 index 0000000000..cad4a878eb --- /dev/null +++ b/examples/quick/rendercontrol/rendercontrol_rhi/doc/src/rendercontrol_rhi.qdoc @@ -0,0 +1,129 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! + \title QQuickRenderControl RHI Example + \example rendercontrol/rendercontrol_rhi + \brief Shows how to render a Qt Quick scene into a QRhiTexture. + \image rendercontrol-rhi-example.jpg + + This example demonstrates how to set up a Qt Quick scene that has its + rendering redirected into a \l QRhiTexture. The application is then free to + do whatever it wants with the resulting texture from each frame. + This example is a QWidget-based application that performs a readback of the + image data, and then displays the collected per-frame renders with CPU and + GPU-based timing information for each. + + By using Qt's 3D graphics API abstraction, this example is not tied to any + particular graphics API. At startup, a dialog is shown with the platforms' + potentially supported 3D APIs. + + \snippet rendercontrol/rendercontrol_rhi/main.cpp apiselect + + \note It is not guaranteed that all selections will be functional on a given + platform. + + Once a selection is made, a QML file is loaded. However, we will not simply + create a \l QQuickView instance and \l{QQuickView::show()}{show()} it. + Rather, the \l QQuickWindow that manages the Qt Quick scene is never shown + on-screen. Instead, the application takes control over when and to where + render, via \l QQuickRenderControl. + + \snippet rendercontrol/rendercontrol_rhi/main.cpp load-1 + + Once the object tree is instantiated, the root item (a \l Rectangle) is + queried, its size is ensured to be valid and then propagated. + + \note Scenes that use the \l Window element within the object tree are not + supported. + + \snippet rendercontrol/rendercontrol_rhi/main.cpp load-instantiate + + At this point there are no rendering resources initialized, i.e., nothing has + been done with the native 3D graphics API yet. A \l QRhi is instantiated + only in the next step, and that is what triggers setting up the Vulkan, + Metal, Direct 3D, etc. rendering system under the hood. + + \snippet rendercontrol/rendercontrol_rhi/main.cpp load-graphicsinit + + \note This application uses a model where Qt creates an instance of QRhi. + This is not the only possible approach: if the application maintains its own + QRhi (and so OpenGL context, Vulkan device, etc.), then Qt Quick can be + requested to adopt and use that existing QRhi. That is done via passing a \l + QQuickGraphicsDevice created by \l{QQuickGraphicsDevice::fromRhi()} to + \l QQuickWindow, similarly to how \l QQuickGraphicsConfiguration is set in + the snippet above. Consider for example the case of wanting to use the Qt + Quick rendered textures in a QRhiWidget: in that case the QRhiWidget's QRhi + will need to passed on to Qt Quick, instead of letting Qt Quick create its + own. + + Once \l QQuickRenderControl::initialize() succeeds, the renderer is live and + ready to go. For that, we need a color buffer to render into. + + \l QQuickRenderTarget is a lightweight implicitly-shared class that carries + (but those not own) various sets of native or QRhi objects that describe + textures, render targets, or similar. Calling + \l{QQuickWindow::setRenderTarget()}{setRenderTarget()} on the \l + QQuickWindow (remember that we have a QQuickWindow that is not visible + on-screen) is what triggers redirecting the Qt Quick scene graph's rendering + into the texture provided by the application. When working with \l QRhi (and + not with native 3D API objects such as OpenGL texture IDs or VkImage + objects), the application should set up a \l QRhiTextureRenderTarget and + then pass it to Qt Quick via \l QQuickRenderTarget::fromRhiRenderTarget(). + + \snippet rendercontrol/rendercontrol_rhi/main.cpp texture-setup + + \note Always provide a depth-stencil buffer for Qt Quick since both of these + buffers and the depth and stencil test may get utilized by the Qt Quick + scenegraph when rendering. + + The main render loop is the following. This also shows how to perform + GPU->CPU readbacks of images. Once a QImage is available, the \l + QWidget-based user interface updates accordingly. We will omit diving + into the details for that here. + + The example also demonstrates a simple way of measuring the cost of + rendering a frame on the CPU and the GPU. Offscreen-rendered frames are well + suited for this due to certain internal QRhi behavior, which implies that + operations that otherwise are asynchronous (in the sense that they complete + only when rendering a subsequent frame), are guaranteed to be ready once \l + QRhi::endOffscreenFrame() (i.e., QQuickRenderControl::endFrame()) returns. + We use this knowledge when reading back the texture, and it applies also to + GPU timestamps as well. That is why the application can display the GPU time + for each frame, while guaranteeing that the time actually refers to that + particular frame (not an earlier one). See + \l{QRhiCommandBuffer::lastCompletedGpuTime()}{lastCompletedGpuTime()} for + details around GPU timings. The CPU side timings are taken using + \l QElapsedTimer. + + \snippet rendercontrol/rendercontrol_rhi/main.cpp render-core + + One important piece is the stepping of Qt Quick animations. As we do not + have an on-screen window that can drive the animation system either via + measuring elapsed time, an ordinary timer, or presentation rate-based + throttling, redirecting the Qt Quick rendering often implies that the + driving of animations needs to be taken over by the application. Otherwise, + animations function based on a plain system timer, but the actual elapsed + time will often have nothing to do with what the offscreen-rendered scene is + expected to perceive. Consider rendering 5 frames in a row, in a tight loop. + How the animations in those 5 frames move depends on the speed with which + the CPU executes the loop iterations. That is almost never ideal. To ensure + consistent animations, install a custom \l QAnimationDriver. While this is + an undocumented (but public) API meant for advanced users, the example here + provides a simple example of using it. + + \snippet rendercontrol/rendercontrol_rhi/main.cpp anim-driver + + The application has a \l QSlider that can be used to change the animation + step value from the default 16 milliseconds to something else. Note the call + to the setStep() function of our QAnimationDriver subclass. + + \snippet rendercontrol/rendercontrol_rhi/main.cpp anim-slider + + Advancing the animations is done before each frame (i.e., before the + QQuickRenderControl::beginFrame() call) by simply calling advance(): + + \snippet rendercontrol/rendercontrol_rhi/main.cpp anim-step + + \sa QRhi, QQuickRenderControl, QQuickWindow +*/ diff --git a/examples/quick/rendercontrol/rendercontrol_rhi/main.cpp b/examples/quick/rendercontrol/rendercontrol_rhi/main.cpp new file mode 100644 index 0000000000..3bc40400e0 --- /dev/null +++ b/examples/quick/rendercontrol/rendercontrol_rhi/main.cpp @@ -0,0 +1,598 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include <QMainWindow> +#include <QApplication> +#include <QVBoxLayout> +#include <QHBoxLayout> +#include <QMenuBar> +#include <QStatusBar> +#include <QFileDialog> +#include <QMessageBox> +#include <QLabel> +#include <QScrollArea> +#include <QGridLayout> +#include <QPushButton> +#include <QSlider> +#include <QScrollBar> +#include <QListWidget> +#include <QPainter> + +#include <QQuickWindow> +#include <QQuickRenderControl> +#include <QQuickRenderTarget> +#include <QQuickGraphicsDevice> +#include <QQuickGraphicsConfiguration> +#include <QQuickItem> +#include <QQmlEngine> +#include <QQmlComponent> + +#include <QAnimationDriver> +#include <QElapsedTimer> + +#include <rhi/qrhi.h> + +//! [anim-driver] +class AnimationDriver : public QAnimationDriver +{ +public: + AnimationDriver() : m_step(16) { } + + void setStep(int milliseconds) { m_step = milliseconds; } + + void advance() override + { + m_elapsed += m_step; + advanceAnimation(); + } + + qint64 elapsed() const override + { + return m_elapsed; + } + +private: + int m_step; + qint64 m_elapsed = 0; +}; +//! [anim-driver] + +class ImageLabel; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(AnimationDriver *animationDriver); + ~MainWindow(); + + void load(const QString &filename); + void render(); + +private slots: + void openRequested(); + +private: + void reset(); + void stepAnimations(); + + AnimationDriver *m_animationDriver; + + std::unique_ptr<QQuickRenderControl> m_renderControl; + std::unique_ptr<QQuickWindow> m_scene; + std::unique_ptr<QQmlEngine> m_qmlEngine; + std::unique_ptr<QQmlComponent> m_qmlComponent; + + std::unique_ptr<QRhiTexture> m_texture; + std::unique_ptr<QRhiRenderBuffer> m_ds; + std::unique_ptr<QRhiTextureRenderTarget> m_rt; + std::unique_ptr<QRhiRenderPassDescriptor> m_rpDesc; + + qint64 m_frameCount = 0; + QSize m_thumbnailSize; + QVector<double> m_cpuTimes; + QVector<double> m_gpuTimes; + + QLabel *m_apiMsg; + QLabel *m_statusMsg; + QLabel *m_avgCpuMsg; + QLabel *m_avgGpuMsg; + QLabel *m_driverInfoMsg; + + QScrollArea *m_scrollArea; + QGridLayout *m_grid; + QVector<QWidget *> m_gridWidgets; + +public: +#if QT_CONFIG(vulkan) + QVulkanInstance *m_vulkanInstance = nullptr; +#endif + + ImageLabel *m_focus = nullptr; + QLabel *m_fullSizeViewerWindow = nullptr; + QVector<QImage> m_frames; +}; + +MainWindow::MainWindow(AnimationDriver *animationDriver) + : m_animationDriver(animationDriver) +{ + QWidget *centralWidget = new QWidget(this); + QVBoxLayout *vlayout = new QVBoxLayout(centralWidget); + QHBoxLayout *controlLayout = new QHBoxLayout; + vlayout->addLayout(controlLayout); + + QPushButton *btn = new QPushButton(tr("Next frame")); + QObject::connect(btn, &QPushButton::clicked, btn, [this] { + render(); + }); + controlLayout->addWidget(btn); + + QPushButton *btnMulti = new QPushButton(tr("Next 10 frames")); + QObject::connect(btnMulti, &QPushButton::clicked, btn, [this] { + for (int i = 0; i < 10; ++i) + QMetaObject::invokeMethod(this, &MainWindow::render, Qt::QueuedConnection); + }); + controlLayout->addWidget(btnMulti); + +//! [anim-slider] + QSlider *animSlider = new QSlider; + animSlider->setOrientation(Qt::Horizontal); + animSlider->setMinimum(1); + animSlider->setMaximum(1000); + QLabel *animLabel = new QLabel; + QObject::connect(animSlider, &QSlider::valueChanged, animSlider, [this, animLabel, animSlider] { + m_animationDriver->setStep(animSlider->value()); + animLabel->setText(tr("Simulated elapsed time per frame: %1 ms").arg(animSlider->value())); + }); + animSlider->setValue(16); +//! [anim-slider] + controlLayout->addWidget(animLabel); + controlLayout->addWidget(animSlider); + + QGridLayout *gridLayout = new QGridLayout; + vlayout->addLayout(gridLayout); + + QWidget *viewport = new QWidget; + m_grid = new QGridLayout(viewport); + m_scrollArea = new QScrollArea; + m_scrollArea->setWidgetResizable(true); + m_scrollArea->setWidget(viewport); + QObject::connect(m_scrollArea->verticalScrollBar(), &QScrollBar::rangeChanged, this, [this] { + m_scrollArea->verticalScrollBar()->setSliderPosition(m_scrollArea->verticalScrollBar()->maximum()); + }); + + gridLayout->addWidget(m_scrollArea); + setCentralWidget(centralWidget); + + QMenu *fileMenu = menuBar()->addMenu(tr("&File")); + fileMenu->addAction(tr("&Open"), this, &MainWindow::openRequested); + fileMenu->addAction(tr("E&xit"), qApp, &QCoreApplication::quit); + + m_statusMsg = new QLabel; + statusBar()->addWidget(m_statusMsg); + m_avgCpuMsg = new QLabel; + statusBar()->addWidget(m_avgCpuMsg); + m_avgGpuMsg = new QLabel; + statusBar()->addWidget(m_avgGpuMsg); + m_apiMsg = new QLabel; + statusBar()->addWidget(m_apiMsg); + m_driverInfoMsg = new QLabel; + statusBar()->addWidget(m_driverInfoMsg); +} + +MainWindow::~MainWindow() +{ + delete m_fullSizeViewerWindow; +} + +void MainWindow::openRequested() +{ + QString filename = QFileDialog::getOpenFileName(this, tr("Open file"), QString(), tr("QML files (*.qml);;All files (*.*)")); + if (!filename.isEmpty()) + load(filename); +} + +void MainWindow::reset() +{ + m_rpDesc.reset(); + m_rt.reset(); + m_ds.reset(); + m_texture.reset(); + + m_qmlComponent.reset(); + m_qmlEngine.reset(); + m_scene.reset(); + m_renderControl.reset(); + + m_apiMsg->setText(QString()); + m_statusMsg->setText(QString()); + m_avgCpuMsg->setText(QString()); + m_avgGpuMsg->setText(QString()); + m_driverInfoMsg->setText(QString()); + + m_frameCount = 0; + + qDeleteAll(m_gridWidgets); + m_gridWidgets.clear(); + + delete m_fullSizeViewerWindow; + m_fullSizeViewerWindow = nullptr; + m_focus = nullptr; +} + +//! [load-1] +void MainWindow::load(const QString &filename) +{ + reset(); + + m_renderControl.reset(new QQuickRenderControl); + m_scene.reset(new QQuickWindow(m_renderControl.get())); + + // enable lastCompletedGpuTime() on QRhiCommandBuffer, if supported by the underlying 3D API + QQuickGraphicsConfiguration config; + config.setTimestamps(true); + m_scene->setGraphicsConfiguration(config); + +#if QT_CONFIG(vulkan) + if (m_scene->graphicsApi() == QSGRendererInterface::Vulkan) + m_scene->setVulkanInstance(m_vulkanInstance); +#endif + + m_qmlEngine.reset(new QQmlEngine); + m_qmlComponent.reset(new QQmlComponent(m_qmlEngine.get(), QUrl::fromLocalFile(filename))); + if (m_qmlComponent->isError()) { + for (const QQmlError &error : m_qmlComponent->errors()) + qWarning() << error.url() << error.line() << error; + QMessageBox::critical(this, tr("Cannot load QML scene"), tr("Failed to load %1").arg(filename)); + reset(); + return; + } +//! [load-1] + +//! [load-instantiate] + QObject *rootObject = m_qmlComponent->create(); + if (m_qmlComponent->isError()) { + for (const QQmlError &error : m_qmlComponent->errors()) + qWarning() << error.url() << error.line() << error; + QMessageBox::critical(this, tr("Cannot load QML scene"), tr("Failed to create component")); + reset(); + return; + } + + QQuickItem *rootItem = qobject_cast<QQuickItem *>(rootObject); + if (!rootItem) { + // Get rid of the on-screen window, if the root object was a Window + if (QQuickWindow *w = qobject_cast<QQuickWindow *>(rootObject)) + delete w; + QMessageBox::critical(this, + tr("Invalid root item in QML scene"), + tr("Root object is not a QQuickItem. If this is a scene with Window in it, note that such scenes are not supported.")); + reset(); + return; + } + + if (rootItem->size().width() < 16) + rootItem->setSize(QSizeF(640, 360)); + + m_scene->contentItem()->setSize(rootItem->size()); + m_scene->setGeometry(0, 0, rootItem->width(), rootItem->height()); + + rootItem->setParentItem(m_scene->contentItem()); + + m_statusMsg->setText(tr("QML scene loaded")); +//! [load-instantiate] + +//! [load-graphicsinit] + const bool initSuccess = m_renderControl->initialize(); + if (!initSuccess) { + QMessageBox::critical(this, tr("Cannot initialize renderer"), tr("QQuickRenderControl::initialize() failed")); + reset(); + return; + } + + const QSGRendererInterface::GraphicsApi api = m_scene->rendererInterface()->graphicsApi(); + switch (api) { + case QSGRendererInterface::OpenGL: + m_apiMsg->setText(tr("OpenGL")); + break; + case QSGRendererInterface::Direct3D11: + m_apiMsg->setText(tr("D3D11")); + break; + case QSGRendererInterface::Direct3D12: + m_apiMsg->setText(tr("D3D12")); + break; + case QSGRendererInterface::Vulkan: + m_apiMsg->setText(tr("Vulkan")); + break; + case QSGRendererInterface::Metal: + m_apiMsg->setText(tr("Metal")); + break; + default: + m_apiMsg->setText(tr("Unknown 3D API")); + break; + } + + QRhi *rhi = m_renderControl->rhi(); + if (!rhi) { + QMessageBox::critical(this, tr("Cannot render"), tr("No QRhi from QQuickRenderControl")); + reset(); + return; + } + + m_driverInfoMsg->setText(QString::fromUtf8(rhi->driverInfo().deviceName)); +//! [load-graphicsinit] + +//! [texture-setup] + const QSize pixelSize = rootItem->size().toSize(); // no scaling, i.e. the item size is in pixels + + m_texture.reset(rhi->newTexture(QRhiTexture::RGBA8, pixelSize, 1, + QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource)); + if (!m_texture->create()) { + QMessageBox::critical(this, tr("Cannot render"), tr("Cannot create texture object")); + reset(); + return; + } + + m_ds.reset(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, pixelSize, 1)); + if (!m_ds->create()) { + QMessageBox::critical(this, tr("Cannot render"), tr("Cannot create depth-stencil buffer")); + reset(); + return; + } + + QRhiTextureRenderTargetDescription rtDesc(QRhiColorAttachment(m_texture.get())); + rtDesc.setDepthStencilBuffer(m_ds.get()); + m_rt.reset(rhi->newTextureRenderTarget(rtDesc)); + m_rpDesc.reset(m_rt->newCompatibleRenderPassDescriptor()); + m_rt->setRenderPassDescriptor(m_rpDesc.get()); + if (!m_rt->create()) { + QMessageBox::critical(this, tr("Cannot render"), tr("Cannot create render target")); + reset(); + return; + } + + m_scene->setRenderTarget(QQuickRenderTarget::fromRhiRenderTarget(m_rt.get())); +//! [texture-setup] + + render(); +} + +class ImageLabel : public QLabel +{ +public: + ImageLabel(MainWindow *mw, int frameIndex) + : m_mainWindow(mw), + m_frameIndex(frameIndex) + { + } + + void mousePressEvent(QMouseEvent *) override { + if (m_mainWindow->m_focus != this) { + ImageLabel *oldFocus = m_mainWindow->m_focus; + m_mainWindow->m_focus = this; + if (oldFocus) + oldFocus->update(); + update(); + } + } + + void paintEvent(QPaintEvent *e) override { + + if (m_mainWindow->m_focus == this) { + QPainter p(this); + p.fillRect(0, 0, width(), height(), Qt::blue); + p.end(); + QLabel::paintEvent(e); + p.begin(this); + p.setOpacity(0.5); + p.fillRect(0, 0, width(), height(), Qt::blue); + } else { + QLabel::paintEvent(e); + } + } + + void mouseDoubleClickEvent(QMouseEvent *) override { + QLabel *&w(m_mainWindow->m_fullSizeViewerWindow); + if (!w) + w = new QLabel; + const QImage &image(m_mainWindow->m_frames[m_frameIndex]); + w->resize(image.width(), image.height()); + w->setPixmap(QPixmap::fromImage(image)); + w->show(); + w->activateWindow(); + } + + MainWindow *m_mainWindow; + int m_frameIndex; +}; + +void MainWindow::render() +{ + if (!m_renderControl) + return; + + if (m_frameCount > 0) + stepAnimations(); + +//! [render-core] + QElapsedTimer cpuTimer; + cpuTimer.start(); + + m_renderControl->polishItems(); + + m_renderControl->beginFrame(); + + m_renderControl->sync(); + m_renderControl->render(); + + QRhi *rhi = m_renderControl->rhi(); + QRhiReadbackResult readResult; + QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch(); + readbackBatch->readBackTexture(m_texture.get(), &readResult); + m_renderControl->commandBuffer()->resourceUpdate(readbackBatch); + + m_renderControl->endFrame(); + + const double gpuRenderTimeMs = m_renderControl->commandBuffer()->lastCompletedGpuTime() * 1000.0; + const double cpuRenderTimeMs = cpuTimer.nsecsElapsed() / 1000000.0; + + // m_renderControl->begin/endFrame() is based on QRhi's + // begin/endOffscreenFrame() under the hood, meaning it does not do + // pipelining, unlike swapchain-based frames, and therefore the readback is + // guaranteed to complete once endFrame() returns. + QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()), + readResult.pixelSize.width(), readResult.pixelSize.height(), + QImage::Format_RGBA8888_Premultiplied); + QImage result; +if (rhi->isYUpInFramebuffer()) + result = wrapperImage.mirrored(); +else + result = wrapperImage.copy(); + +//! [render-core] + + m_gpuTimes.append(gpuRenderTimeMs); + m_cpuTimes.append(cpuRenderTimeMs); + + m_frames.append(result); + + if (m_thumbnailSize.isEmpty()) + m_thumbnailSize = size() / 4; + + const QImage thumbnail = result.scaled(m_thumbnailSize, Qt::KeepAspectRatio); + + ImageLabel *image = new ImageLabel(this, m_frames.count() - 1); + image->setPixmap(QPixmap::fromImage(thumbnail)); + image->setAlignment(Qt::AlignCenter); + + QLabel *label = new QLabel(tr("Frame %1\nCPU: %2 ms GPU: %3 ms").arg(m_frameCount).arg(cpuRenderTimeMs, 0, 'f', 4).arg(gpuRenderTimeMs, 0, 'f', 4)); + label->setAlignment(Qt::AlignCenter); + QWidget *container = new QWidget; + QVBoxLayout *layout = new QVBoxLayout; + layout->addWidget(image); + layout->addWidget(label); + container->setLayout(layout); + m_grid->addWidget(container, m_frameCount / 3, m_frameCount % 3); + m_gridWidgets.append(container); + + m_scrollArea->verticalScrollBar()->setSliderPosition(m_scrollArea->verticalScrollBar()->maximum()); + + m_frameCount += 1; + + double v = 0; + for (double t : m_cpuTimes) + v += t; + v /= m_cpuTimes.count(); + m_avgCpuMsg->setText(tr("Avg. CPU render time: %1 ms").arg(v, 0, 'f', 4)); + if (m_cpuTimes.count() > 64) { + m_cpuTimes.clear(); + m_cpuTimes.append(v); + } + v = 0; + for (double t : m_gpuTimes) + v += t; + v /= m_gpuTimes.count(); + m_avgGpuMsg->setText(tr("Avg. GPU render time: %1 ms").arg(v, 0, 'f', 4)); + if (m_gpuTimes.count() > 64) { + m_gpuTimes.clear(); + m_gpuTimes.append(v); + } +} + +//! [anim-step] +void MainWindow::stepAnimations() +{ + // Now the Qt Quick scene will think that <slider value> milliseconds have + // elapsed and update animations accordingly when doing the next frame. + m_animationDriver->advance(); +} +//! [anim-step] + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + AnimationDriver animDriver; + animDriver.install(); + +#if QT_CONFIG(vulkan) + QVulkanInstance vulkanInstance; +#endif + + MainWindow mainWindow(&animDriver); + +//! [apiselect] + QDialog apiSelect; + QVBoxLayout *selLayout = new QVBoxLayout; + selLayout->addWidget(new QLabel(QObject::tr("Select graphics API to use"))); + QListWidget *apiList = new QListWidget; + QVarLengthArray<QSGRendererInterface::GraphicsApi, 5> apiValues; +#ifdef Q_OS_WIN + apiList->addItem("Direct3D 11"); + apiValues.append(QSGRendererInterface::Direct3D11); + apiList->addItem("Direct3D 12"); + apiValues.append(QSGRendererInterface::Direct3D12); +#endif +#if defined(Q_OS_MACOS) || defined(Q_OS_IOS) + apiList->addItem("Metal"); + apiValues.append(QSGRendererInterface::Metal); +#endif +#if QT_CONFIG(vulkan) + apiList->addItem("Vulkan"); + apiValues.append(QSGRendererInterface::Vulkan); +#endif +#if QT_CONFIG(opengl) + apiList->addItem("OpenGL / OpenGL ES"); + apiValues.append(QSGRendererInterface::OpenGL); +#endif + if (apiValues.isEmpty()) { + QMessageBox::critical(nullptr, QObject::tr("No 3D graphics API"), QObject::tr("No 3D graphics APIs are supported in this Qt build")); + return 1; + } +//! [apiselect] + apiList->setCurrentRow(0); + selLayout->addWidget(apiList); + QPushButton *okBtn = new QPushButton("Ok"); + okBtn->setDefault(true); + selLayout->addWidget(okBtn); + apiSelect.setLayout(selLayout); + apiSelect.resize(320, 200); + apiSelect.show(); + + QObject::connect(okBtn, &QPushButton::clicked, okBtn, [apiList, &apiSelect, &apiValues, &mainWindow +#if QT_CONFIG(vulkan) + , &vulkanInstance +#endif + ] { + const QSGRendererInterface::GraphicsApi api = apiValues[apiList->currentRow()]; + QQuickWindow::setGraphicsApi(api); + +#if QT_CONFIG(vulkan) + if (api == QSGRendererInterface::Vulkan) { + vulkanInstance.setExtensions(QQuickGraphicsConfiguration::preferredInstanceExtensions()); + if (!vulkanInstance.create()) { + QMessageBox::critical(nullptr, QObject::tr("Cannot initialize Vulkan"), QObject::tr("Failed to create VkInstance")); + return; + } + mainWindow.m_vulkanInstance = &vulkanInstance; + } +#endif + + mainWindow.resize(1280, 720); + mainWindow.show(); + mainWindow.load(QLatin1String(":/demo.qml")); + // load() renders one frame, add 19 more + for (int i = 1; i <= 19; ++i) { + mainWindow.render(); + // have to process events, e.g. to get queued metacalls delivered + QCoreApplication::processEvents(); + } + + apiSelect.close(); + }); + + return app.exec(); +} + +#include "main.moc" diff --git a/examples/quick/rendercontrol/rendercontrol_rhi/rendercontrol_rhi.pro b/examples/quick/rendercontrol/rendercontrol_rhi/rendercontrol_rhi.pro new file mode 100644 index 0000000000..cd397f4451 --- /dev/null +++ b/examples/quick/rendercontrol/rendercontrol_rhi/rendercontrol_rhi.pro @@ -0,0 +1,10 @@ +TEMPLATE = app + +QT += gui-private widgets qml quick + +SOURCES += main.cpp + +RESOURCES += rendercontrol_rhi.qrc + +target.path = $$[QT_INSTALL_EXAMPLES]/quick/rendercontrol/rendercontrol_rhi +INSTALLS += target diff --git a/examples/quick/rendercontrol/rendercontrol_rhi/rendercontrol_rhi.qrc b/examples/quick/rendercontrol/rendercontrol_rhi/rendercontrol_rhi.qrc new file mode 100644 index 0000000000..6d9c32d786 --- /dev/null +++ b/examples/quick/rendercontrol/rendercontrol_rhi/rendercontrol_rhi.qrc @@ -0,0 +1,5 @@ +<RCC> + <qresource prefix="/"> + <file>demo.qml</file> + </qresource> +</RCC> |