aboutsummaryrefslogtreecommitdiffstats
path: root/examples/quick
diff options
context:
space:
mode:
authorLaszlo Agocs <[email protected]>2023-08-10 13:05:29 +0200
committerLaszlo Agocs <[email protected]>2023-08-15 14:13:32 +0200
commit8024e182f6eac62b36c4474f43f81a98693743d1 (patch)
treedaec7be4ead4a81e9630895fc032482212c178f1 /examples/quick
parent9b6943a0b887449dd0e294ad2c5ef9779fe0f6f3 (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')
-rw-r--r--examples/quick/rendercontrol/CMakeLists.txt3
-rw-r--r--examples/quick/rendercontrol/rendercontrol_rhi/CMakeLists.txt41
-rw-r--r--examples/quick/rendercontrol/rendercontrol_rhi/demo.qml156
-rw-r--r--examples/quick/rendercontrol/rendercontrol_rhi/doc/images/rendercontrol-rhi-example.jpgbin0 -> 117091 bytes
-rw-r--r--examples/quick/rendercontrol/rendercontrol_rhi/doc/src/rendercontrol_rhi.qdoc129
-rw-r--r--examples/quick/rendercontrol/rendercontrol_rhi/main.cpp598
-rw-r--r--examples/quick/rendercontrol/rendercontrol_rhi/rendercontrol_rhi.pro10
-rw-r--r--examples/quick/rendercontrol/rendercontrol_rhi/rendercontrol_rhi.qrc5
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
new file mode 100644
index 0000000000..369f1edf84
--- /dev/null
+++ b/examples/quick/rendercontrol/rendercontrol_rhi/doc/images/rendercontrol-rhi-example.jpg
Binary files differ
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>