summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--examples/widgets/doc/src/cuberhiwidget.qdoc169
-rw-r--r--examples/widgets/doc/src/simplerhiwidget.qdoc203
-rw-r--r--examples/widgets/rhi/CMakeLists.txt8
-rw-r--r--examples/widgets/rhi/cuberhiwidget/CMakeLists.txt47
-rw-r--r--examples/widgets/rhi/cuberhiwidget/cube.h139
-rw-r--r--examples/widgets/rhi/cuberhiwidget/cuberhiwidget.pro12
-rw-r--r--examples/widgets/rhi/cuberhiwidget/cuberhiwidget.qrc6
-rw-r--r--examples/widgets/rhi/cuberhiwidget/examplewidget.cpp172
-rw-r--r--examples/widgets/rhi/cuberhiwidget/examplewidget.h72
-rw-r--r--examples/widgets/rhi/cuberhiwidget/main.cpp166
-rw-r--r--examples/widgets/rhi/cuberhiwidget/shader_assets/texture.frag.qsbbin0 -> 1042 bytes
-rw-r--r--examples/widgets/rhi/cuberhiwidget/shader_assets/texture.vert.qsbbin0 -> 1161 bytes
-rw-r--r--examples/widgets/rhi/cuberhiwidget/shaders/texture.frag12
-rw-r--r--examples/widgets/rhi/cuberhiwidget/shaders/texture.vert15
-rw-r--r--examples/widgets/rhi/rhi.pro4
-rw-r--r--examples/widgets/rhi/simplerhiwidget/CMakeLists.txt47
-rw-r--r--examples/widgets/rhi/simplerhiwidget/examplewidget.cpp102
-rw-r--r--examples/widgets/rhi/simplerhiwidget/examplewidget.h30
-rw-r--r--examples/widgets/rhi/simplerhiwidget/main.cpp26
-rw-r--r--examples/widgets/rhi/simplerhiwidget/shader_assets/color.frag.qsbbin0 -> 738 bytes
-rw-r--r--examples/widgets/rhi/simplerhiwidget/shader_assets/color.vert.qsbbin0 -> 1091 bytes
-rw-r--r--examples/widgets/rhi/simplerhiwidget/shaders/color.frag10
-rw-r--r--examples/widgets/rhi/simplerhiwidget/shaders/color.vert16
-rw-r--r--examples/widgets/rhi/simplerhiwidget/simplerhiwidget.pro12
-rw-r--r--examples/widgets/rhi/simplerhiwidget/simplerhiwidget.qrc6
-rw-r--r--examples/widgets/widgets.pro1
-rw-r--r--src/gui/doc/src/qtgui-overview.qdoc12
-rw-r--r--src/gui/painting/qbackingstoredefaultcompositor.cpp5
-rw-r--r--src/gui/painting/qplatformbackingstore.h3
-rw-r--r--src/widgets/CMakeLists.txt1
-rw-r--r--src/widgets/doc/images/cuberhiwidget-example.jpgbin0 -> 70232 bytes
-rw-r--r--src/widgets/doc/images/qrhiwidget-intro.jpgbin0 -> 9508 bytes
-rw-r--r--src/widgets/doc/images/simplerhiwidget-example.jpgbin0 -> 12489 bytes
-rw-r--r--src/widgets/doc/snippets/qrhiwidget/rhiwidgetintro.cpp109
-rw-r--r--src/widgets/doc/snippets/qrhiwidget/rhiwidgetintro.frag10
-rw-r--r--src/widgets/doc/snippets/qrhiwidget/rhiwidgetintro.vert15
-rw-r--r--src/widgets/kernel/qrhiwidget.cpp1283
-rw-r--r--src/widgets/kernel/qrhiwidget.h102
-rw-r--r--src/widgets/kernel/qrhiwidget_p.h63
-rw-r--r--src/widgets/kernel/qwidget.cpp3
-rw-r--r--tests/auto/widgets/widgets/CMakeLists.txt1
-rw-r--r--tests/auto/widgets/widgets/qrhiwidget/CMakeLists.txt25
-rw-r--r--tests/auto/widgets/widgets/qrhiwidget/data/simple.frag8
-rw-r--r--tests/auto/widgets/widgets/qrhiwidget/data/simple.frag.qsbbin0 -> 724 bytes
-rw-r--r--tests/auto/widgets/widgets/qrhiwidget/data/simple.vert8
-rw-r--r--tests/auto/widgets/widgets/qrhiwidget/data/simple.vert.qsbbin0 -> 783 bytes
-rw-r--r--tests/auto/widgets/widgets/qrhiwidget/tst_qrhiwidget.cpp792
-rw-r--r--tests/manual/rhi/CMakeLists.txt2
-rw-r--r--tests/manual/rhi/rhiwidgetproto/CMakeLists.txt (renamed from tests/manual/rhi/rhiwidget/CMakeLists.txt)8
-rw-r--r--tests/manual/rhi/rhiwidgetproto/examplewidget.cpp (renamed from tests/manual/rhi/rhiwidget/examplewidget.cpp)0
-rw-r--r--tests/manual/rhi/rhiwidgetproto/examplewidget.h (renamed from tests/manual/rhi/rhiwidget/examplewidget.h)0
-rw-r--r--tests/manual/rhi/rhiwidgetproto/main.cpp (renamed from tests/manual/rhi/rhiwidget/main.cpp)0
-rw-r--r--tests/manual/rhi/rhiwidgetproto/rhiwidget.cpp (renamed from tests/manual/rhi/rhiwidget/rhiwidget.cpp)0
-rw-r--r--tests/manual/rhi/rhiwidgetproto/rhiwidget.h (renamed from tests/manual/rhi/rhiwidget/rhiwidget.h)0
-rw-r--r--tests/manual/rhi/rhiwidgetproto/rhiwidget_p.h (renamed from tests/manual/rhi/rhiwidget/rhiwidget_p.h)0
55 files changed, 3717 insertions, 8 deletions
diff --git a/examples/widgets/doc/src/cuberhiwidget.qdoc b/examples/widgets/doc/src/cuberhiwidget.qdoc
new file mode 100644
index 0000000000..84be1d0942
--- /dev/null
+++ b/examples/widgets/doc/src/cuberhiwidget.qdoc
@@ -0,0 +1,169 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
+
+/*!
+ \example rhi/cuberhiwidget
+ \title Cube RHI Widget Example
+ \ingroup examples-widgets
+ \brief Shows how to render a textured cube and integrate with QPainter and widgets, using QRhi Qt's 3D API and shading language abstraction layer.
+
+ \image cuberhiwidget-example.jpg
+ \caption Screenshot of the Cube RHI Widget example
+
+ This example builds on the \l{Simple RHI Widget Example}. While the simple
+ example is intentionally minimal and as compact as possible, rendering only
+ a single triangle with no additional widgets in the window, this
+ application demonstrates:
+
+ \list
+
+ \li Having various widgets in the window, some of them controlling data
+ that is consumed by the QRhiWidget subclass.
+
+ \li Instead of continuously requesting updates, the QRhiWidget here only
+ updates the content in its backing texture when some related data changes.
+
+ \li The cube is textured using a \l QRhiTexture that sources its content
+ from a \l QImage that contains software-based rendering performed with
+ \l QPainter.
+
+ \li The contents of the QRhiWidget \l{QRhiWidget::grab()}{can be
+ read back} and saved to an image file (e.g. a PNG file).
+
+ \li 4x multisample antialiasing \l{QRhiWidget::sampleConut}{can be toggled}
+ at run time. The QRhiWidget subclass is prepared to handle the changing
+ sample count correctly.
+
+ \li Forcing an \l{QRhiWidget::explicitSize}{explicitly specified backing
+ texture size} can be toggled dynamically and controlled with a slider
+ between 16x16 up to 512x512 pixels.
+
+ \li The QRhiWidget subclass deals with a changing \l QRhi correctly. This
+ can be seen in action when making the widget top-level (no parent; becomes
+ a separate window) and then reparenting it again into the main window's
+ child hierarchy.
+
+ \li Most importantly, some widgets, with semi-transparency even, can be
+ placed on top of the QRhiWidget, proving that correct stacking and blending
+ is feasible. This is a case where QRhiWidget is superior to embedding a
+ native window, i.e. a QRhi-based QWindow using
+ QWidget::createWindowContainer(), because it allows stacking and clipping
+ the same way as any ordinary, software-rendered QWidget, whereas native
+ window embedding may, depending on the platform, have various limitations,
+ e.g. often it can be difficult or inefficient to place additional controls
+ on top.
+
+ \endlist
+
+ In the reimplementation of \l{QRhiWidget::initialize()}{initialize()}, the
+ first thing to do is to check if the QRhi we last worked with is still
+ up-to-date, and if the sample count (for multisample antialiasing) has
+ changed. The former is important because all graphics resources must be
+ released when the QRhi changes, whereas with a dynamically changing sample
+ count a similar problem arises specifically for QRhiGraphicsPipeline
+ objects as those bake the sample count in. For simplicity, the application
+ handles all such changes the same way, by resetting its \c scene struct to
+ a default constructed one, which conveniently drops all graphics resources.
+ All resources are then recreated.
+
+ When the backing texture size (so the render target size) changes, no
+ special action is needed, but a signal is emitted for convenience, just so
+ that main() can reposition the overlay label. The 3D API name is also
+ exposed via a signal by querying \l QRhi::backendName() whenever the QRhi
+ changes.
+
+ The implementation has to be aware that multisample antialiasing implies
+ that \l{QRhiWidget::colorTexture()}{colorTexture()} is \nullptr, while
+ \l{QRhiWidget::msaaColorBuffer()}{msaaColorBuffer()} is valid. This is
+ the opposite of when MSAA is not in use. The reason for differentiating
+ and using different types (QRhiTexture, QRhiRenderBuffer) is to allow
+ using MSAA with 3D graphics APIs that do not have support for
+ multisample textures, but have support for multisample renderbuffers.
+ An example of this is OpenGL ES 3.0.
+
+ When checking the up-to-date pixel size and sample count, a convenient and
+ compact solution is to query via the QRhiRenderTarget, because this way one
+ does not need to check which of colorTexture() and msaaColorBuffer() are
+ valid.
+
+ \snippet rhi/cuberhiwidget/examplewidget.cpp init-1
+
+ The rest is quite self-explanatory. The buffers and pipelines are
+ (re)created, if necessary. The contents of the texture that is used to
+ texture the cube mesh is updated. The scene is rendered using a perspective
+ projection. The view is just a simple translation for now.
+
+ \snippet rhi/cuberhiwidget/examplewidget.cpp init-2
+
+ The function that performs the actual enqueuing of the uniform buffer write
+ is also taking the user-provided rotation into account, thus generating the
+ final modelview-projection matrix.
+
+ \snippet rhi/cuberhiwidget/examplewidget.cpp rotation-update
+
+ Updating the \l QRhiTexture that is sampled in the fragment shader when
+ rendering the cube, is quite simple, even though a lot is happening in
+ there: first a QPainter-based drawing is generated within a QImage. This
+ uses the user-provided text. Then the CPU-side pixel data is uploaded to a
+ texture (more precisely, the upload operation is recorded on a \l
+ QRhiResourceUpdateBatch, which is then submitted later in render()).
+
+ \snippet rhi/cuberhiwidget/examplewidget.cpp texture-update
+
+ The graphics resource initialization is simple. There is only a vertex
+ buffer, no index buffer, and a uniform buffer with only a 4x4 matrix in it
+ (16 floats).
+
+ The texture that contains the QPainter-generated drawing has a size of
+ 512x512. Note that all sizes (texture sizes, viewports, scissors, texture
+ upload regions, etc.) are always in pixels when working with QRhi. To
+ sample this texture in the shader, a \l{QRhiSampler}{sampler object} is
+ needed (irrespective of the fact that QRhi-based applications will
+ typically use combined image samplers in the GLSL shader code, which then
+ may be transpiled to separate texture and sampler objects with some shading
+ languages, or may stay a combined texture-sampler object with others,
+ meaning there may not actually be a native sampler object under the hood at
+ run time, depending on the 3D API, but this is all transparent to the
+ application)
+
+ The vertex shader reads from the uniform buffer at binding point 0,
+ therefore
+ \c{scene.ubuf} is exposed at that binding location. The fragment shader
+ samples a texture provided at binding point 1,
+ therefore a combined texture-sampler pair is specified for that binding location.
+
+ The QRhiGraphicsPipeline enables depth test/write, and culls backfaces. It
+ also relies on a number of defaults, e.g. the depth comparison function
+ defaults to \c Less, which is fine for us, and the front face mode is
+ counter-clockwise, which is also good as-is so does not need to be set
+ again.
+
+ \snippet rhi/cuberhiwidget/examplewidget.cpp setup-scene
+
+ In the reimplementation of \l{QRhiWidget::render()}{render()}, first the
+ user-provided data is checked. If the \l QSlider controlling the rotation
+ has provided a new value, or the \l QTextEdit with the cube text has
+ changed its text, the graphics resources the contents of which depend on
+ such data get updated.
+
+ Then, a single render pass with a single draw call is recorded. The cube
+ mesh data is provided in a non-interleaved format, hence the need for two
+ vertex input bindings, one is the positions (x, y, z) the other is the UVs
+ (u, v), with a start offset that corresponds to 36 x-y-z float pairs.
+
+ \snippet rhi/cuberhiwidget/examplewidget.cpp render
+
+ How is the user-provided data sent? Take the rotation for example. main()
+ connects to the QSlider's \l{QSlider::valueChanged}{valueChanged} signal.
+ When emitted, the connected lamda calls setCubeRotation() on the
+ ExampleRhiWidget. Here, if the value is different from before, it is
+ stored, and a dirty flag is set. Then, most importantly,
+ \l{QWidget::update()}{update()} is called on the ExampleRhiWidget. This is
+ what triggers rendering a new frame into the QRhiWidget's backing texture.
+ Without this the content of the ExampleRhiWidget would not update when
+ dragging the slider.
+
+ \snippet rhi/cuberhiwidget/examplewidget.h data-setters
+
+ \sa QRhi, {Simple RHI Widget Example}, {RHI Window Example}
+*/
diff --git a/examples/widgets/doc/src/simplerhiwidget.qdoc b/examples/widgets/doc/src/simplerhiwidget.qdoc
new file mode 100644
index 0000000000..5db4f2d583
--- /dev/null
+++ b/examples/widgets/doc/src/simplerhiwidget.qdoc
@@ -0,0 +1,203 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
+
+/*!
+ \example rhi/simplerhiwidget
+ \title Simple RHI Widget Example
+ \ingroup examples-widgets
+ \brief Shows how to render a triangle using QRhi, Qt's 3D API and shading language abstraction layer.
+
+ \image simplerhiwidget-example.jpg
+ \caption Screenshot of the Simple RHI Widget example
+
+ This example is, in many ways, the counterpart of the \l{RHI Window
+ Example} in the \l QWidget world. The \l QRhiWidget subclass in this
+ applications renders a single triangle, using a simple graphics pipeline
+ with basic vertex and fragment shaders. Unlike the plain QWindow-based
+ application, this example does not need to worry about lower level details,
+ such as setting up the window and the QRhi, or dealing with swapchain and
+ window events, as that is taken care of by the QWidget framework here. The
+ instance of the \l QRhiWidget subclass is added to a QVBoxLayout. To keep
+ the example minimal and compact, there are no further widgets or 3D content
+ introduced.
+
+ Once an instance of \c ExampleRhiWidget, a \l QRhiWidget subclass, is added
+ to a top-level widget's child hierarchy, the corresponding window
+ automatically becomes a Direct 3D, Vulkan, Metal, or OpenGL-rendered
+ window. The QPainter-rendered widget content, i.e. everything that is not a
+ QRhiWidget, QOpenGLWidget, or QQuickWidget, is then uploaded to a texture,
+ whereas the mentioned special widgets each render to a texture. The
+ resulting set of \l{QRhiTexture}{textures} is composited together by the
+ top-level widget's backingstore.
+
+ \section1 Structure and main()
+
+ The \c{main()} function is quite simple. The top-level widget defaults to a
+ size of 720p (this size is in logical units, the actual pixel size may be
+ different, depending on the \l{QWidget::devicePixelRatio()}{scale factor}.
+ The window is resizable. QRhiWidget makes it simple to implement subclasses
+ that correctly deal with the resizing of the widget due to window size or
+ layout changes.
+
+ \snippet rhi/simplerhiwidget/main.cpp 0
+
+ The QRhiWidget subclass reimplements the two virtuals:
+ \l{QRhiWidget::initialize()}{initialize()} and
+ \l{QRhiWidget::render()}{render()}.
+ initialize() is called at least once before render(),
+ but is also invoked upon a number of important changes, such as when the
+ widget's backing texture is recreated due to a changing widget size, when
+ render target parameters change, or when the widget changes to a new QRhi
+ due to moving to a new top-level window.
+
+ \note Unlike QOpenGLWidget's legacy \c initializeGL - \c resizeGL - \c
+ paintGL model, there are only two virtuals in QRhiWidget. This is because
+ there are more special events that possible need taking care of than just
+ resizing, e.g. when reparenting to a different top-level window. (robust
+ QOpenGLWidget implementations had to deal with this by performing
+ additional bookkeeping, e.g. by tracking the associated QOpenGLContext
+ lifetime, meaning the three virtuals were not actually sufficient) A
+ simpler pair of \c initialize - \c render, where \c initialize is
+ re-invoked upon important changes is better suited for this.
+
+ The \l QRhi instance is not owned by the widget. It is going to be queried
+ in \c initialize() \l{QRhiWidget::rhi()}{from the base class}. Storing it
+ as a member allows recognizing changes when \c initialize() is invoked
+ again. Graphics resources, such as the vertex and uniform buffers, or the
+ graphics pipeline are however under the control of \c ExampleRhiWidget.
+
+ \snippet rhi/simplerhiwidget/examplewidget.h 0
+
+ For the \c{#include <rhi/qrhi.h>} statement to work, the application must
+ link to \c GuiPrivate (or \c{gui-private} with qmake). See \l QRhi for more
+ details about the compatibility promise of the QRhi family of APIs.
+
+ \c CMakeLists.txt
+
+ \badcode
+ target_link_libraries(simplerhiwidget PRIVATE
+ Qt6::Core
+ Qt6::Gui
+ Qt6::GuiPrivate
+ Qt6::Widgets
+ )
+ \endcode
+
+ \section1 Rendering Setup
+
+ In \c examplewidget.cpp the widget implementation uses a helper function to
+ load up a \l QShader object from a \c{.qsb} file. This application ships
+ pre-conditioned \c{.qsb} files embedded in to the executable via the Qt
+ Resource System. Due to module dependencies (and due to still supporting
+ qmake), this example does not use the convenient CMake function
+ \c{qt_add_shaders()}, but rather comes with the \c{.qsb} files as part of
+ the source tree. Real world applications are encouraged to avoid this and
+ rather use the Qt Shader Tools module's CMake integration features (\c
+ qt_add_shaders). Regardless of the approach, in the C++ code the loading
+ of the bundled/generated \c{.qsb} files is the same.
+
+ \snippet rhi/simplerhiwidget/examplewidget.cpp get-shader
+
+ Let's look at the initialize() implementation. First, the \l QRhi object is
+ queried and stored for later use, and also to allow comparison in future
+ invocations of the function. When there is a mismatch (e.g. when the widget
+ is moved between windows), recreation of graphics resources need to be
+ recreated is triggered by destroying and nulling out a suitable object, in
+ this case the \c m_pipeline. The example does not actively demonstrate
+ reparenting between windows, but it is prepared to handle it. It is also
+ prepared to handle a changing widget size that can happen when resizing the
+ window. That needs no special handling since \c{initialize()} is invoked
+ every time that happens, and so querying
+ \c{renderTarget()->pixelSize()} or \c{colorTexture()->pixelSize()}
+ always gives the latest, up-to-date size in pixels. What this example is
+ not prepared for is changing
+ \l{QRhiWidget::textureFormat}{texture formats} and
+ \l{QRhiWidget::sampleCount}{multisample settings}
+ since it only ever uses the defaults (RGBA8 and no multisample antialiasing).
+
+ \snippet rhi/simplerhiwidget/examplewidget.cpp init-1
+
+ When the graphics resources need to be (re)created, \c{initialize()} does
+ this using quite typical QRhi-based code. A single vertex buffer with the
+ interleaved position - color vertex data is sufficient, whereas the
+ modelview-projection matrix is exposed via a uniform buffer of 64 bytes (16
+ floats). The uniform buffer is the only shader visible resource, and it is
+ only used in the vertex shader. The graphics pipeline relies on a lot of
+ defaults (for example, depth test off, blending disabled, color write
+ enabled, face culling disabled, the default topology of triangles, etc.)
+ The vertex data layout is \c x, \c y, \c r, \c g, \c b, hence the stride is
+ 5 floats, whereas the second vertex input attribute (the color) has an
+ offset of 2 floats (skipping \c x and \c y). Each graphics pipeline has to
+ be associated with a \l QRhiRenderPassDescriptor. This can be retrieved
+ from the \l QRhiRenderTarget managed by the base class.
+
+ \note This example relies on the QRhiWidget's default of
+ \l{QRhiWidget::autoRenderTarget}{autoRenderTarget} set to \c true.
+ That is why it does not need to manage the render target, but can just
+ query the existing one by calling
+ \l{QRhiWidget::renderTarget()}{renderTarget()}.
+
+ \snippet rhi/simplerhiwidget/examplewidget.cpp init-pipeline
+
+ Finally, the projection matrix is calculated. This depends on the widget
+ size and is thus done unconditionally in every invocation of the functions.
+
+ \note Any size and viewport calculations should only ever rely on the pixel
+ size queried from the resource serving as the color buffer since that is
+ the actual render target. Avoid manually calculating sizes, viewports,
+ scissors, etc. based on the QWidget-reported size or device pixel ratio.
+
+ \note The projection matrix includes the
+ \l{QRhi::clipSpaceCorrMatrix()}{correction matrix} from QRhi in order to
+ cater for 3D API differences in normalized device coordinates.
+ (for example, Y down vs. Y up)
+
+ A translation of \c{-4} is applied just to make sure the triangle with \c z
+ values of 0 will be visible.
+
+ \snippet rhi/simplerhiwidget/examplewidget.cpp init-matrix
+
+ \section1 Rendering
+
+ The widget records a single render pass, which contains a single draw call.
+
+ The view-projection matrix calculated in the initialize step gets combined
+ with the model matrix, which in this case happens to be a simple rotation.
+ The resulting matrix is then written to the uniform buffer. Note how
+ \c resourceUpdates is passed to
+ \l{QRhiCommandBuffer::beginPass()}{beginPass()}, which is a shortcut to not
+ having to invoke \l{QRhiCommandBuffer::resourceUpdate()}{resourceUpdate()}
+ manually.
+
+ \snippet rhi/simplerhiwidget/examplewidget.cpp render-1
+
+ In the render pass, a single draw call with 3 vertices is recorded. The
+ graphics pipeline created in the initialize step is bound on the command
+ buffer, and the viewport is set to cover the entire widget. To make the
+ uniform buffer visible to the (vertex) shader,
+ \l{QRhiCommandBuffer::setShaderResources()}{setShaderResources()} is called
+ with no argument, which means using the \c m_srb since that was associated
+ with the pipeline at pipeline creation time. In more complex renderers it
+ is not unusual to pass in a different \l QRhiShaderResourceBindings object,
+ as long as that is
+ \l{QRhiShaderResourceBindings::isLayoutCompatible()}{layout-compatible}
+ with the one given at pipeline creation time.
+ There is no index buffer, and there is a single vertex buffer binding (the
+ single element in \c vbufBinding refers to the single entry in the binding
+ list of the \l QRhiVertexInputLayout that was specified when creating
+ pipeline).
+
+ \snippet rhi/simplerhiwidget/examplewidget.cpp render-pass
+
+ Once the render pass is recorded, \l{QWidget::update()}{update()} is
+ called. This requests a new frame, and is used to ensure the widget
+ continuously updates, and the triangle appears rotating. The rendering
+ thread (the main thread in this case) is throttled by the presentation rate
+ by default. There is no proper animation system in this example, and so the
+ rotation will increase in every frame, meaning the triangle will rotate at
+ different speeds on displays with different refresh rates.
+
+ \snippet rhi/simplerhiwidget/examplewidget.cpp render-2
+
+ \sa QRhi, {Cube RHI Widget Example}, {RHI Window Example}
+*/
diff --git a/examples/widgets/rhi/CMakeLists.txt b/examples/widgets/rhi/CMakeLists.txt
new file mode 100644
index 0000000000..bb106dfd40
--- /dev/null
+++ b/examples/widgets/rhi/CMakeLists.txt
@@ -0,0 +1,8 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+if(NOT TARGET Qt6::Widgets)
+ return()
+endif()
+qt_internal_add_example(simplerhiwidget)
+qt_internal_add_example(cuberhiwidget)
diff --git a/examples/widgets/rhi/cuberhiwidget/CMakeLists.txt b/examples/widgets/rhi/cuberhiwidget/CMakeLists.txt
new file mode 100644
index 0000000000..be4bde25e6
--- /dev/null
+++ b/examples/widgets/rhi/cuberhiwidget/CMakeLists.txt
@@ -0,0 +1,47 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+cmake_minimum_required(VERSION 3.16)
+project(cuberhiwidget LANGUAGES CXX)
+
+if(NOT DEFINED INSTALL_EXAMPLESDIR)
+ set(INSTALL_EXAMPLESDIR "examples")
+endif()
+
+set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/widgets/rhi/cuberhiwidget")
+
+find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)
+
+qt_standard_project_setup()
+
+qt_add_executable(cuberhiwidget
+ examplewidget.cpp examplewidget.h cube.h
+ main.cpp
+)
+
+set_target_properties(cuberhiwidget PROPERTIES
+ WIN32_EXECUTABLE TRUE
+ MACOSX_BUNDLE TRUE
+)
+
+# needs GuiPrivate to be able to include <rhi/qrhi.h>
+target_link_libraries(cuberhiwidget PRIVATE
+ Qt6::Core
+ Qt6::Gui
+ Qt6::GuiPrivate
+ Qt6::Widgets
+)
+
+qt_add_resources(cuberhiwidget "cuberhiwidget"
+ PREFIX
+ "/"
+ FILES
+ "shader_assets/texture.vert.qsb"
+ "shader_assets/texture.frag.qsb"
+)
+
+install(TARGETS cuberhiwidget
+ RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
+ BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
+ LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
+)
diff --git a/examples/widgets/rhi/cuberhiwidget/cube.h b/examples/widgets/rhi/cuberhiwidget/cube.h
new file mode 100644
index 0000000000..9d55eede92
--- /dev/null
+++ b/examples/widgets/rhi/cuberhiwidget/cube.h
@@ -0,0 +1,139 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef CUBE_H
+#define CUBE_H
+
+// clang-format off
+static const float cube[] = {
+ -1.0f, -1.0f, -1.0f, // -X
+ -1.0f, -1.0f, 1.0f,
+ -1.0f, 1.0f, 1.0f,
+ -1.0f, 1.0f, 1.0f,
+ -1.0f, 1.0f, -1.0f,
+ -1.0f, -1.0f, -1.0f,
+
+ -1.0f, -1.0f, -1.0f, // -Z
+ 1.0f, 1.0f, -1.0f,
+ 1.0f, -1.0f, -1.0f,
+ -1.0f, -1.0f, -1.0f,
+ -1.0f, 1.0f, -1.0f,
+ 1.0f, 1.0f, -1.0f,
+
+ -1.0f, -1.0f, -1.0f, // -Y
+ 1.0f, -1.0f, -1.0f,
+ 1.0f, -1.0f, 1.0f,
+ -1.0f, -1.0f, -1.0f,
+ 1.0f, -1.0f, 1.0f,
+ -1.0f, -1.0f, 1.0f,
+
+ -1.0f, 1.0f, -1.0f, // +Y
+ -1.0f, 1.0f, 1.0f,
+ 1.0f, 1.0f, 1.0f,
+ -1.0f, 1.0f, -1.0f,
+ 1.0f, 1.0f, 1.0f,
+ 1.0f, 1.0f, -1.0f,
+
+ 1.0f, 1.0f, -1.0f, // +X
+ 1.0f, 1.0f, 1.0f,
+ 1.0f, -1.0f, 1.0f,
+ 1.0f, -1.0f, 1.0f,
+ 1.0f, -1.0f, -1.0f,
+ 1.0f, 1.0f, -1.0f,
+
+ -1.0f, 1.0f, 1.0f, // +Z
+ -1.0f, -1.0f, 1.0f,
+ 1.0f, 1.0f, 1.0f,
+ -1.0f, -1.0f, 1.0f,
+ 1.0f, -1.0f, 1.0f,
+ 1.0f, 1.0f, 1.0f,
+
+ // UVs
+ 0.0f, 1.0f, // -X
+ 1.0f, 1.0f,
+ 1.0f, 0.0f,
+ 1.0f, 0.0f,
+ 0.0f, 0.0f,
+ 0.0f, 1.0f,
+
+ 1.0f, 1.0f, // -Z
+ 0.0f, 0.0f,
+ 0.0f, 1.0f,
+ 1.0f, 1.0f,
+ 1.0f, 0.0f,
+ 0.0f, 0.0f,
+
+ 1.0f, 0.0f, // -Y
+ 1.0f, 1.0f,
+ 0.0f, 1.0f,
+ 1.0f, 0.0f,
+ 0.0f, 1.0f,
+ 0.0f, 0.0f,
+
+ 1.0f, 0.0f, // +Y
+ 0.0f, 0.0f,
+ 0.0f, 1.0f,
+ 1.0f, 0.0f,
+ 0.0f, 1.0f,
+ 1.0f, 1.0f,
+
+ 1.0f, 0.0f, // +X
+ 0.0f, 0.0f,
+ 0.0f, 1.0f,
+ 0.0f, 1.0f,
+ 1.0f, 1.0f,
+ 1.0f, 0.0f,
+
+ 0.0f, 0.0f, // +Z
+ 0.0f, 1.0f,
+ 1.0f, 0.0f,
+ 0.0f, 1.0f,
+ 1.0f, 1.0f,
+ 1.0f, 0.0f,
+
+ // normals
+ -1.0, 0.0, 0.0, // -X
+ -1.0, 0.0, 0.0,
+ -1.0, 0.0, 0.0,
+ -1.0, 0.0, 0.0,
+ -1.0, 0.0, 0.0,
+ -1.0, 0.0, 0.0,
+
+ 0.0, 0.0, -1.0, // -Z
+ 0.0, 0.0, -1.0,
+ 0.0, 0.0, -1.0,
+ 0.0, 0.0, -1.0,
+ 0.0, 0.0, -1.0,
+ 0.0, 0.0, -1.0,
+
+ 0.0, -1.0, 0.0, // -Y
+ 0.0, -1.0, 0.0,
+ 0.0, -1.0, 0.0,
+ 0.0, -1.0, 0.0,
+ 0.0, -1.0, 0.0,
+ 0.0, -1.0, 0.0,
+
+ 0.0, 1.0, 0.0, // +Y
+ 0.0, 1.0, 0.0,
+ 0.0, 1.0, 0.0,
+ 0.0, 1.0, 0.0,
+ 0.0, 1.0, 0.0,
+ 0.0, 1.0, 0.0,
+
+ 1.0, 0.0, 0.0, // +X
+ 1.0, 0.0, 0.0,
+ 1.0, 0.0, 0.0,
+ 1.0, 0.0, 0.0,
+ 1.0, 0.0, 0.0,
+ 1.0, 0.0, 0.0,
+
+ 0.0, 0.0, 1.0, // +Z
+ 0.0, 0.0, 1.0,
+ 0.0, 0.0, 1.0,
+ 0.0, 0.0, 1.0,
+ 0.0, 0.0, 1.0,
+ 0.0, 0.0, 1.0
+};
+// clang-format on
+
+#endif
diff --git a/examples/widgets/rhi/cuberhiwidget/cuberhiwidget.pro b/examples/widgets/rhi/cuberhiwidget/cuberhiwidget.pro
new file mode 100644
index 0000000000..94abd29e08
--- /dev/null
+++ b/examples/widgets/rhi/cuberhiwidget/cuberhiwidget.pro
@@ -0,0 +1,12 @@
+TEMPLATE = app
+
+# needs gui-private to be able to include <rhi/qrhi.h>
+QT += gui-private widgets
+
+HEADERS += examplewidget.h
+SOURCES += examplewidget.cpp main.cpp
+
+RESOURCES += cuberhiwidget.qrc
+
+target.path = $$[QT_INSTALL_EXAMPLES]/widgets/rhi/cuberhiwidget
+INSTALLS += target
diff --git a/examples/widgets/rhi/cuberhiwidget/cuberhiwidget.qrc b/examples/widgets/rhi/cuberhiwidget/cuberhiwidget.qrc
new file mode 100644
index 0000000000..33ca81dbde
--- /dev/null
+++ b/examples/widgets/rhi/cuberhiwidget/cuberhiwidget.qrc
@@ -0,0 +1,6 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource prefix="/">
+ <file>shader_assets/texture.vert.qsb</file>
+ <file>shader_assets/texture.frag.qsb</file>
+</qresource>
+</RCC>
diff --git a/examples/widgets/rhi/cuberhiwidget/examplewidget.cpp b/examples/widgets/rhi/cuberhiwidget/examplewidget.cpp
new file mode 100644
index 0000000000..fe39d904dd
--- /dev/null
+++ b/examples/widgets/rhi/cuberhiwidget/examplewidget.cpp
@@ -0,0 +1,172 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include "examplewidget.h"
+#include "cube.h"
+#include <QFile>
+#include <QPainter>
+
+static const QSize CUBE_TEX_SIZE(512, 512);
+
+ExampleRhiWidget::ExampleRhiWidget(QWidget *parent)
+ : QRhiWidget(parent)
+{
+}
+
+//![init-1]
+void ExampleRhiWidget::initialize(QRhiCommandBuffer *)
+{
+ if (m_rhi != rhi()) {
+ m_rhi = rhi();
+ scene = {};
+ emit rhiChanged(QString::fromUtf8(m_rhi->backendName()));
+ }
+ if (m_pixelSize != renderTarget()->pixelSize()) {
+ m_pixelSize = renderTarget()->pixelSize();
+ emit resized();
+ }
+ if (m_sampleCount != renderTarget()->sampleCount()) {
+ m_sampleCount = renderTarget()->sampleCount();
+ scene = {};
+ }
+//![init-1]
+
+//![init-2]
+ if (!scene.vbuf) {
+ initScene();
+ updateCubeTexture();
+ }
+
+ scene.mvp = m_rhi->clipSpaceCorrMatrix();
+ scene.mvp.perspective(45.0f, m_pixelSize.width() / (float) m_pixelSize.height(), 0.01f, 1000.0f);
+ scene.mvp.translate(0, 0, -4);
+ updateMvp();
+}
+//![init-2]
+
+//![rotation-update]
+void ExampleRhiWidget::updateMvp()
+{
+ QMatrix4x4 mvp = scene.mvp * QMatrix4x4(QQuaternion::fromEulerAngles(QVector3D(30, itemData.cubeRotation, 0)).toRotationMatrix());
+ if (!scene.resourceUpdates)
+ scene.resourceUpdates = m_rhi->nextResourceUpdateBatch();
+ scene.resourceUpdates->updateDynamicBuffer(scene.ubuf.get(), 0, 64, mvp.constData());
+}
+//![rotation-update]
+
+//![texture-update]
+void ExampleRhiWidget::updateCubeTexture()
+{
+ QImage image(CUBE_TEX_SIZE, QImage::Format_RGBA8888);
+ const QRect r(QPoint(0, 0), CUBE_TEX_SIZE);
+ QPainter p(&image);
+ p.fillRect(r, QGradient::DeepBlue);
+ QFont font;
+ font.setPointSize(24);
+ p.setFont(font);
+ p.drawText(r, itemData.cubeText);
+ p.end();
+
+ if (!scene.resourceUpdates)
+ scene.resourceUpdates = m_rhi->nextResourceUpdateBatch();
+ scene.resourceUpdates->uploadTexture(scene.cubeTex.get(), image);
+}
+//![texture-update]
+
+static QShader getShader(const QString &name)
+{
+ QFile f(name);
+ return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader();
+}
+
+void ExampleRhiWidget::initScene()
+{
+//![setup-scene]
+ scene.vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube)));
+ scene.vbuf->create();
+
+ scene.resourceUpdates = m_rhi->nextResourceUpdateBatch();
+ scene.resourceUpdates->uploadStaticBuffer(scene.vbuf.get(), cube);
+
+ scene.ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
+ scene.ubuf->create();
+
+ scene.cubeTex.reset(m_rhi->newTexture(QRhiTexture::RGBA8, CUBE_TEX_SIZE));
+ scene.cubeTex->create();
+
+ scene.sampler.reset(m_rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
+ QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
+ scene.sampler->create();
+
+ scene.srb.reset(m_rhi->newShaderResourceBindings());
+ scene.srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, scene.ubuf.get()),
+ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, scene.cubeTex.get(), scene.sampler.get())
+ });
+ scene.srb->create();
+
+ scene.ps.reset(m_rhi->newGraphicsPipeline());
+ scene.ps->setDepthTest(true);
+ scene.ps->setDepthWrite(true);
+ scene.ps->setCullMode(QRhiGraphicsPipeline::Back);
+ scene.ps->setShaderStages({
+ { QRhiShaderStage::Vertex, getShader(QLatin1String(":/shader_assets/texture.vert.qsb")) },
+ { QRhiShaderStage::Fragment, getShader(QLatin1String(":/shader_assets/texture.frag.qsb")) }
+ });
+ QRhiVertexInputLayout inputLayout;
+ // The cube is provided as non-interleaved sets of positions, UVs, normals.
+ // Normals are not interesting here, only need the positions and UVs.
+ inputLayout.setBindings({
+ { 3 * sizeof(float) },
+ { 2 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float3, 0 },
+ { 1, 1, QRhiVertexInputAttribute::Float2, 0 }
+ });
+ scene.ps->setSampleCount(m_sampleCount);
+ scene.ps->setVertexInputLayout(inputLayout);
+ scene.ps->setShaderResourceBindings(scene.srb.get());
+ scene.ps->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
+ scene.ps->create();
+//![setup-scene]
+}
+
+//![render]
+void ExampleRhiWidget::render(QRhiCommandBuffer *cb)
+{
+ if (itemData.cubeRotationDirty) {
+ itemData.cubeRotationDirty = false;
+ updateMvp();
+ }
+
+ if (itemData.cubeTextDirty) {
+ itemData.cubeTextDirty = false;
+ updateCubeTexture();
+ }
+
+ QRhiResourceUpdateBatch *resourceUpdates = scene.resourceUpdates;
+ if (resourceUpdates)
+ scene.resourceUpdates = nullptr;
+
+ const QColor clearColor = QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f);
+ cb->beginPass(renderTarget(), clearColor, { 1.0f, 0 }, resourceUpdates);
+
+ cb->setGraphicsPipeline(scene.ps.get());
+ cb->setViewport(QRhiViewport(0, 0, m_pixelSize.width(), m_pixelSize.height()));
+ cb->setShaderResources();
+ const QRhiCommandBuffer::VertexInput vbufBindings[] = {
+ { scene.vbuf.get(), 0 },
+ { scene.vbuf.get(), quint32(36 * 3 * sizeof(float)) }
+ };
+ cb->setVertexInput(0, 2, vbufBindings);
+ cb->draw(36);
+
+ cb->endPass();
+}
+//![render]
+
+void ExampleRhiWidget::releaseResources()
+{
+ scene = {}; // a subsequent initialize() will recreate everything
+}
diff --git a/examples/widgets/rhi/cuberhiwidget/examplewidget.h b/examples/widgets/rhi/cuberhiwidget/examplewidget.h
new file mode 100644
index 0000000000..9cc554b3fb
--- /dev/null
+++ b/examples/widgets/rhi/cuberhiwidget/examplewidget.h
@@ -0,0 +1,72 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef EXAMPLEWIDGET_H
+#define EXAMPLEWIDGET_H
+
+#include <QRhiWidget>
+#include <rhi/qrhi.h>
+
+class ExampleRhiWidget : public QRhiWidget
+{
+ Q_OBJECT
+
+public:
+ ExampleRhiWidget(QWidget *parent = nullptr);
+
+ void initialize(QRhiCommandBuffer *cb) override;
+ void render(QRhiCommandBuffer *cb) override;
+ void releaseResources() override;
+//![data-setters]
+ void setCubeTextureText(const QString &s)
+ {
+ if (itemData.cubeText == s)
+ return;
+ itemData.cubeText = s;
+ itemData.cubeTextDirty = true;
+ update();
+ }
+
+ void setCubeRotation(float r)
+ {
+ if (itemData.cubeRotation == r)
+ return;
+ itemData.cubeRotation = r;
+ itemData.cubeRotationDirty = true;
+ update();
+ }
+//![data-setters]
+
+signals:
+ void resized();
+ void rhiChanged(const QString &apiName);
+
+private:
+ QRhi *m_rhi = nullptr;
+ int m_sampleCount = 1;
+ QSize m_pixelSize;
+
+ struct {
+ QRhiResourceUpdateBatch *resourceUpdates = nullptr;
+ std::unique_ptr<QRhiBuffer> vbuf;
+ std::unique_ptr<QRhiBuffer> ubuf;
+ std::unique_ptr<QRhiShaderResourceBindings> srb;
+ std::unique_ptr<QRhiGraphicsPipeline> ps;
+ std::unique_ptr<QRhiSampler> sampler;
+ std::unique_ptr<QRhiTexture> cubeTex;
+ QMatrix4x4 mvp;
+ } scene;
+
+ struct {
+ QString cubeText;
+ bool cubeTextDirty = false;
+ float cubeRotation = 0.0f;
+ bool cubeRotationDirty = false;
+ } itemData;
+
+ void initScene();
+ void updateMvp();
+ void updateCubeTexture();
+};
+
+#endif
diff --git a/examples/widgets/rhi/cuberhiwidget/main.cpp b/examples/widgets/rhi/cuberhiwidget/main.cpp
new file mode 100644
index 0000000000..03882aef24
--- /dev/null
+++ b/examples/widgets/rhi/cuberhiwidget/main.cpp
@@ -0,0 +1,166 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include <QApplication>
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QSlider>
+#include <QTextEdit>
+#include <QPushButton>
+#include <QLabel>
+#include <QCheckBox>
+#include <QFileDialog>
+#include <QFontInfo>
+#include <QMouseEvent>
+#include <QDrag>
+#include <QMimeData>
+#include "examplewidget.h"
+
+int main(int argc, char **argv)
+{
+ QApplication app(argc, argv);
+
+ QVBoxLayout *layout = new QVBoxLayout;
+
+ ExampleRhiWidget *rhiWidget = new ExampleRhiWidget;
+ QLabel *overlayLabel = new QLabel(rhiWidget);
+ overlayLabel->setText(QLatin1String("This is a\nsemi-transparent\n overlay widget\n"
+ "placed on top of\nthe QRhiWidget."));
+ overlayLabel->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
+ overlayLabel->setAutoFillBackground(true);
+ QPalette semiTransparent(QColor(255, 0, 0, 64));
+ semiTransparent.setBrush(QPalette::Text, Qt::white);
+ semiTransparent.setBrush(QPalette::WindowText, Qt::white);
+ overlayLabel->setPalette(semiTransparent);
+ QFont f = overlayLabel->font();
+ f.setPixelSize(QFontInfo(f).pixelSize() * 2);
+ f.setWeight(QFont::Bold);
+ overlayLabel->setFont(f);
+ overlayLabel->resize(320, 320);
+ overlayLabel->hide();
+ QObject::connect(rhiWidget, &ExampleRhiWidget::resized, rhiWidget, [rhiWidget, overlayLabel] {
+ const int w = overlayLabel->width();
+ const int h = overlayLabel->height();
+ overlayLabel->setGeometry(rhiWidget->width() / 2 - w / 2, rhiWidget->height() / 2 - h / 2, w, h);
+ });
+
+ QTextEdit *edit = new QTextEdit(QLatin1String("QRhiWidget!<br><br>"
+ "The cube is textured with QPainter-generated content.<br><br>"
+ "Regular, non-native widgets on top work just fine."));
+ QObject::connect(edit, &QTextEdit::textChanged, edit, [edit, rhiWidget] {
+ rhiWidget->setCubeTextureText(edit->toPlainText());
+ });
+ edit->setMaximumHeight(100);
+ layout->addWidget(edit);
+
+ QSlider *slider = new QSlider(Qt::Horizontal);
+ slider->setMinimum(0);
+ slider->setMaximum(360);
+ QObject::connect(slider, &QSlider::valueChanged, slider, [slider, rhiWidget] {
+ rhiWidget->setCubeRotation(slider->value());
+ });
+
+ QHBoxLayout *sliderLayout = new QHBoxLayout;
+ sliderLayout->addWidget(new QLabel(QLatin1String("Cube rotation")));
+ sliderLayout->addWidget(slider);
+ layout->addLayout(sliderLayout);
+
+ QHBoxLayout *btnLayout = new QHBoxLayout;
+
+ QLabel *apiLabel = new QLabel;
+ btnLayout->addWidget(apiLabel);
+ QObject::connect(rhiWidget, &ExampleRhiWidget::rhiChanged, rhiWidget, [apiLabel](const QString &apiName) {
+ apiLabel->setText(QLatin1String("Using QRhi on ") + apiName);
+ });
+
+ QPushButton *btnMakeWindow = new QPushButton(QLatin1String("Make top-level window"));
+ QObject::connect(btnMakeWindow, &QPushButton::clicked, btnMakeWindow, [rhiWidget, btnMakeWindow, layout] {
+ if (rhiWidget->parentWidget()) {
+ rhiWidget->setParent(nullptr);
+ rhiWidget->setAttribute(Qt::WA_DeleteOnClose, true);
+ rhiWidget->show();
+ btnMakeWindow->setText(QLatin1String("Make child widget"));
+ } else {
+ rhiWidget->setAttribute(Qt::WA_DeleteOnClose, false);
+ layout->addWidget(rhiWidget);
+ btnMakeWindow->setText(QLatin1String("Make top-level window"));
+ }
+ });
+ btnLayout->addWidget(btnMakeWindow);
+
+ QPushButton *btn = new QPushButton(QLatin1String("Grab to image"));
+ QObject::connect(btn, &QPushButton::clicked, btn, [rhiWidget] {
+ QImage image = rhiWidget->grab();
+ qDebug() << "Got image" << image;
+ if (!image.isNull()) {
+ QFileDialog fd(rhiWidget->parentWidget());
+ fd.setAcceptMode(QFileDialog::AcceptSave);
+ fd.setDefaultSuffix("png");
+ fd.selectFile("test.png");
+ if (fd.exec() == QDialog::Accepted)
+ image.save(fd.selectedFiles().first());
+ }
+ });
+ btnLayout->addWidget(btn);
+
+ QCheckBox *cbMsaa = new QCheckBox(QLatin1String("Use 4x MSAA"));
+ QObject::connect(cbMsaa, &QCheckBox::stateChanged, cbMsaa, [cbMsaa, rhiWidget] {
+ if (cbMsaa->isChecked())
+ rhiWidget->setSampleCount(4);
+ else
+ rhiWidget->setSampleCount(1);
+ });
+ btnLayout->addWidget(cbMsaa);
+
+ QCheckBox *cbOvberlay = new QCheckBox(QLatin1String("Show overlay widget"));
+ QObject::connect(cbOvberlay, &QCheckBox::stateChanged, cbOvberlay, [cbOvberlay, overlayLabel] {
+ if (cbOvberlay->isChecked())
+ overlayLabel->setVisible(true);
+ else
+ overlayLabel->setVisible(false);
+ });
+ btnLayout->addWidget(cbOvberlay);
+
+ QCheckBox *cbFlip = new QCheckBox(QLatin1String("Flip"));
+ QObject::connect(cbFlip, &QCheckBox::stateChanged, cbOvberlay, [cbFlip, rhiWidget] {
+ rhiWidget->setMirrorVertically(cbFlip->isChecked());
+ });
+ btnLayout->addWidget(cbFlip);
+
+ QCheckBox *cbExplicitSize = new QCheckBox(QLatin1String("Use explicit size"));
+ btnLayout->addWidget(cbExplicitSize);
+ QSlider *explicitSizeSlider = new QSlider(Qt::Horizontal);
+ explicitSizeSlider->setMinimum(16);
+ explicitSizeSlider->setMaximum(512);
+ btnLayout->addWidget(explicitSizeSlider);
+
+ QObject::connect(cbExplicitSize, &QCheckBox::stateChanged, cbExplicitSize, [cbExplicitSize, explicitSizeSlider, rhiWidget] {
+ if (cbExplicitSize->isChecked())
+ rhiWidget->setExplicitSize(QSize(explicitSizeSlider->value(), explicitSizeSlider->value()));
+ else
+ rhiWidget->setExplicitSize(QSize());
+ });
+ QObject::connect(explicitSizeSlider, &QSlider::valueChanged, explicitSizeSlider, [explicitSizeSlider, cbExplicitSize, rhiWidget] {
+ if (cbExplicitSize->isChecked())
+ rhiWidget->setExplicitSize(QSize(explicitSizeSlider->value(), explicitSizeSlider->value()));
+ });
+
+ // Exit when the detached window is closed; there is not much we can do
+ // with the controls in the main window then.
+ QObject::connect(rhiWidget, &QObject::destroyed, rhiWidget, [rhiWidget] {
+ if (!rhiWidget->parentWidget())
+ qGuiApp->quit();
+ });
+
+ layout->addLayout(btnLayout);
+ layout->addWidget(rhiWidget);
+
+ rhiWidget->setCubeTextureText(edit->toPlainText());
+
+ QWidget w;
+ w.setLayout(layout);
+ w.resize(1280, 720);
+ w.show();
+
+ return app.exec();
+}
diff --git a/examples/widgets/rhi/cuberhiwidget/shader_assets/texture.frag.qsb b/examples/widgets/rhi/cuberhiwidget/shader_assets/texture.frag.qsb
new file mode 100644
index 0000000000..dc440d8067
--- /dev/null
+++ b/examples/widgets/rhi/cuberhiwidget/shader_assets/texture.frag.qsb
Binary files differ
diff --git a/examples/widgets/rhi/cuberhiwidget/shader_assets/texture.vert.qsb b/examples/widgets/rhi/cuberhiwidget/shader_assets/texture.vert.qsb
new file mode 100644
index 0000000000..84aed7fee2
--- /dev/null
+++ b/examples/widgets/rhi/cuberhiwidget/shader_assets/texture.vert.qsb
Binary files differ
diff --git a/examples/widgets/rhi/cuberhiwidget/shaders/texture.frag b/examples/widgets/rhi/cuberhiwidget/shaders/texture.frag
new file mode 100644
index 0000000000..9a14dc6eeb
--- /dev/null
+++ b/examples/widgets/rhi/cuberhiwidget/shaders/texture.frag
@@ -0,0 +1,12 @@
+#version 440
+
+layout(location = 0) in vec2 v_texcoord;
+layout(location = 0) out vec4 fragColor;
+
+layout(binding = 1) uniform sampler2D tex;
+
+void main()
+{
+ vec4 c = texture(tex, v_texcoord);
+ fragColor = vec4(c.rgb * c.a, c.a);
+}
diff --git a/examples/widgets/rhi/cuberhiwidget/shaders/texture.vert b/examples/widgets/rhi/cuberhiwidget/shaders/texture.vert
new file mode 100644
index 0000000000..a58a932abc
--- /dev/null
+++ b/examples/widgets/rhi/cuberhiwidget/shaders/texture.vert
@@ -0,0 +1,15 @@
+#version 440
+
+layout(location = 0) in vec4 position;
+layout(location = 1) in vec2 texcoord;
+layout(location = 0) out vec2 v_texcoord;
+
+layout(std140, binding = 0) uniform buf {
+ mat4 mvp;
+};
+
+void main()
+{
+ v_texcoord = vec2(texcoord.x, texcoord.y);
+ gl_Position = mvp * position;
+}
diff --git a/examples/widgets/rhi/rhi.pro b/examples/widgets/rhi/rhi.pro
new file mode 100644
index 0000000000..9248e5e0e3
--- /dev/null
+++ b/examples/widgets/rhi/rhi.pro
@@ -0,0 +1,4 @@
+requires(qtHaveModule(widgets))
+TEMPLATE = subdirs
+SUBDIRS = simplerhiwidget \
+ cuberhiwidget
diff --git a/examples/widgets/rhi/simplerhiwidget/CMakeLists.txt b/examples/widgets/rhi/simplerhiwidget/CMakeLists.txt
new file mode 100644
index 0000000000..c1e11e14c4
--- /dev/null
+++ b/examples/widgets/rhi/simplerhiwidget/CMakeLists.txt
@@ -0,0 +1,47 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+cmake_minimum_required(VERSION 3.16)
+project(simplerhiwidget LANGUAGES CXX)
+
+if(NOT DEFINED INSTALL_EXAMPLESDIR)
+ set(INSTALL_EXAMPLESDIR "examples")
+endif()
+
+set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/widgets/rhi/simplerhiwidget")
+
+find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)
+
+qt_standard_project_setup()
+
+qt_add_executable(simplerhiwidget
+ examplewidget.cpp examplewidget.h
+ main.cpp
+)
+
+set_target_properties(simplerhiwidget PROPERTIES
+ WIN32_EXECUTABLE TRUE
+ MACOSX_BUNDLE TRUE
+)
+
+# needs GuiPrivate to be able to include <rhi/qrhi.h>
+target_link_libraries(simplerhiwidget PRIVATE
+ Qt6::Core
+ Qt6::Gui
+ Qt6::GuiPrivate
+ Qt6::Widgets
+)
+
+qt_add_resources(simplerhiwidget "simplerhiwidget"
+ PREFIX
+ "/"
+ FILES
+ "shader_assets/color.vert.qsb"
+ "shader_assets/color.frag.qsb"
+)
+
+install(TARGETS simplerhiwidget
+ RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
+ BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
+ LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
+)
diff --git a/examples/widgets/rhi/simplerhiwidget/examplewidget.cpp b/examples/widgets/rhi/simplerhiwidget/examplewidget.cpp
new file mode 100644
index 0000000000..7de33059a3
--- /dev/null
+++ b/examples/widgets/rhi/simplerhiwidget/examplewidget.cpp
@@ -0,0 +1,102 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include "examplewidget.h"
+#include <QFile>
+
+static float vertexData[] = {
+ 0.0f, 0.5f, 1.0f, 0.0f, 0.0f,
+ -0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
+ 0.5f, -0.5f, 0.0f, 0.0f, 1.0f,
+};
+
+//![get-shader]
+static QShader getShader(const QString &name)
+{
+ QFile f(name);
+ return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader();
+}
+//![get-shader]
+
+//![init-1]
+void ExampleRhiWidget::initialize(QRhiCommandBuffer *cb)
+{
+ if (m_rhi != rhi()) {
+ m_pipeline.reset();
+ m_rhi = rhi();
+ }
+//![init-1]
+//![init-pipeline]
+ if (!m_pipeline) {
+ m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData)));
+ m_vbuf->create();
+
+ m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
+ m_ubuf->create();
+
+ m_srb.reset(m_rhi->newShaderResourceBindings());
+ m_srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, m_ubuf.get()),
+ });
+ m_srb->create();
+
+ m_pipeline.reset(m_rhi->newGraphicsPipeline());
+ m_pipeline->setShaderStages({
+ { QRhiShaderStage::Vertex, getShader(QLatin1String(":/shader_assets/color.vert.qsb")) },
+ { QRhiShaderStage::Fragment, getShader(QLatin1String(":/shader_assets/color.frag.qsb")) }
+ });
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 5 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) }
+ });
+ m_pipeline->setVertexInputLayout(inputLayout);
+ m_pipeline->setShaderResourceBindings(m_srb.get());
+ m_pipeline->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
+ m_pipeline->create();
+
+ QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch();
+ resourceUpdates->uploadStaticBuffer(m_vbuf.get(), vertexData);
+ cb->resourceUpdate(resourceUpdates);
+ }
+//![init-pipeline]
+
+//![init-matrix]
+ const QSize outputSize = renderTarget()->pixelSize();
+ m_viewProjection = m_rhi->clipSpaceCorrMatrix();
+ m_viewProjection.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 1000.0f);
+ m_viewProjection.translate(0, 0, -4);
+}
+//![init-matrix]
+
+//![render-1]
+void ExampleRhiWidget::render(QRhiCommandBuffer *cb)
+{
+ QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch();
+ m_rotation += 1.0f;
+ QMatrix4x4 modelViewProjection = m_viewProjection;
+ modelViewProjection.rotate(m_rotation, 0, 1, 0);
+ resourceUpdates->updateDynamicBuffer(m_ubuf.get(), 0, 64, modelViewProjection.constData());
+//![render-1]
+//![render-pass]
+ const QColor clearColor = QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f);
+ cb->beginPass(renderTarget(), clearColor, { 1.0f, 0 }, resourceUpdates);
+
+ cb->setGraphicsPipeline(m_pipeline.get());
+ const QSize outputSize = renderTarget()->pixelSize();
+ cb->setViewport(QRhiViewport(0, 0, outputSize.width(), outputSize.height()));
+ cb->setShaderResources();
+ const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0);
+ cb->setVertexInput(0, 1, &vbufBinding);
+ cb->draw(3);
+
+ cb->endPass();
+//![render-pass]
+
+//![render-2]
+ update();
+}
+//![render-2]
diff --git a/examples/widgets/rhi/simplerhiwidget/examplewidget.h b/examples/widgets/rhi/simplerhiwidget/examplewidget.h
new file mode 100644
index 0000000000..efd3b90d91
--- /dev/null
+++ b/examples/widgets/rhi/simplerhiwidget/examplewidget.h
@@ -0,0 +1,30 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef EXAMPLEWIDGET_H
+#define EXAMPLEWIDGET_H
+
+//![0]
+#include <QRhiWidget>
+#include <rhi/qrhi.h>
+
+class ExampleRhiWidget : public QRhiWidget
+{
+public:
+ ExampleRhiWidget(QWidget *parent = nullptr) : QRhiWidget(parent) { }
+
+ void initialize(QRhiCommandBuffer *cb) override;
+ void render(QRhiCommandBuffer *cb) override;
+
+private:
+ QRhi *m_rhi = nullptr;
+ std::unique_ptr<QRhiBuffer> m_vbuf;
+ std::unique_ptr<QRhiBuffer> m_ubuf;
+ std::unique_ptr<QRhiShaderResourceBindings> m_srb;
+ std::unique_ptr<QRhiGraphicsPipeline> m_pipeline;
+ QMatrix4x4 m_viewProjection;
+ float m_rotation = 0.0f;
+};
+//![0]
+
+#endif
diff --git a/examples/widgets/rhi/simplerhiwidget/main.cpp b/examples/widgets/rhi/simplerhiwidget/main.cpp
new file mode 100644
index 0000000000..b9cf848125
--- /dev/null
+++ b/examples/widgets/rhi/simplerhiwidget/main.cpp
@@ -0,0 +1,26 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include <QApplication>
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include "examplewidget.h"
+
+//![0]
+int main(int argc, char **argv)
+{
+ QApplication app(argc, argv);
+
+ ExampleRhiWidget *rhiWidget = new ExampleRhiWidget;
+
+ QVBoxLayout *layout = new QVBoxLayout;
+ layout->addWidget(rhiWidget);
+
+ QWidget w;
+ w.setLayout(layout);
+ w.resize(1280, 720);
+ w.show();
+
+ return app.exec();
+}
+//![0]
diff --git a/examples/widgets/rhi/simplerhiwidget/shader_assets/color.frag.qsb b/examples/widgets/rhi/simplerhiwidget/shader_assets/color.frag.qsb
new file mode 100644
index 0000000000..32bd2d5953
--- /dev/null
+++ b/examples/widgets/rhi/simplerhiwidget/shader_assets/color.frag.qsb
Binary files differ
diff --git a/examples/widgets/rhi/simplerhiwidget/shader_assets/color.vert.qsb b/examples/widgets/rhi/simplerhiwidget/shader_assets/color.vert.qsb
new file mode 100644
index 0000000000..bf97035d7e
--- /dev/null
+++ b/examples/widgets/rhi/simplerhiwidget/shader_assets/color.vert.qsb
Binary files differ
diff --git a/examples/widgets/rhi/simplerhiwidget/shaders/color.frag b/examples/widgets/rhi/simplerhiwidget/shaders/color.frag
new file mode 100644
index 0000000000..375587662f
--- /dev/null
+++ b/examples/widgets/rhi/simplerhiwidget/shaders/color.frag
@@ -0,0 +1,10 @@
+#version 440
+
+layout(location = 0) in vec3 v_color;
+
+layout(location = 0) out vec4 fragColor;
+
+void main()
+{
+ fragColor = vec4(v_color, 1.0);
+}
diff --git a/examples/widgets/rhi/simplerhiwidget/shaders/color.vert b/examples/widgets/rhi/simplerhiwidget/shaders/color.vert
new file mode 100644
index 0000000000..e876f290e7
--- /dev/null
+++ b/examples/widgets/rhi/simplerhiwidget/shaders/color.vert
@@ -0,0 +1,16 @@
+#version 440
+
+layout(location = 0) in vec4 position;
+layout(location = 1) in vec3 color;
+
+layout(location = 0) out vec3 v_color;
+
+layout(std140, binding = 0) uniform buf {
+ mat4 mvp;
+};
+
+void main()
+{
+ v_color = color;
+ gl_Position = mvp * position;
+}
diff --git a/examples/widgets/rhi/simplerhiwidget/simplerhiwidget.pro b/examples/widgets/rhi/simplerhiwidget/simplerhiwidget.pro
new file mode 100644
index 0000000000..2477d7f368
--- /dev/null
+++ b/examples/widgets/rhi/simplerhiwidget/simplerhiwidget.pro
@@ -0,0 +1,12 @@
+TEMPLATE = app
+
+# needs gui-private to be able to include <rhi/qrhi.h>
+QT += gui-private widgets
+
+HEADERS += examplewidget.h
+SOURCES += examplewidget.cpp main.cpp
+
+RESOURCES += simplerhiwidget.qrc
+
+target.path = $$[QT_INSTALL_EXAMPLES]/widgets/rhi/simplerhiwidget
+INSTALLS += target
diff --git a/examples/widgets/rhi/simplerhiwidget/simplerhiwidget.qrc b/examples/widgets/rhi/simplerhiwidget/simplerhiwidget.qrc
new file mode 100644
index 0000000000..ddc6dfbe5a
--- /dev/null
+++ b/examples/widgets/rhi/simplerhiwidget/simplerhiwidget.qrc
@@ -0,0 +1,6 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource prefix="/">
+ <file>shader_assets/color.vert.qsb</file>
+ <file>shader_assets/color.frag.qsb</file>
+</qresource>
+</RCC>
diff --git a/examples/widgets/widgets.pro b/examples/widgets/widgets.pro
index 8818582105..e8ac9d83bd 100644
--- a/examples/widgets/widgets.pro
+++ b/examples/widgets/widgets.pro
@@ -15,6 +15,7 @@ SUBDIRS = \
layouts \
mainwindows \
painting \
+ rhi \
richtext \
tools \
touch \
diff --git a/src/gui/doc/src/qtgui-overview.qdoc b/src/gui/doc/src/qtgui-overview.qdoc
index 8ba191d7f0..446479c9be 100644
--- a/src/gui/doc/src/qtgui-overview.qdoc
+++ b/src/gui/doc/src/qtgui-overview.qdoc
@@ -90,6 +90,18 @@
portable, cross-platform application that performs accelerated 3D rendering
onto a QWindow using QRhi.
+ Working directly with QWindow is the most advanced and often the most
+ flexible way of rendering with the QRhi API. It is the most low-level
+ approach, however, and limited in the sense that Qt's UI technologies,
+ widgets and Qt Quick, are not utilized at all. In many cases applications
+ will rather want to integrate QRhi-based rendering into a widget or Qt
+ Quick-based user interface. QWidget-based applications may choose to embed
+ the window as a native child into the widget hierarchy via
+ QWidget::createWindowContainer(), but in many cases \l QRhiWidget will
+ offer a more convenient enabler to integrate QRhi-based rendering into a
+ widget UI. Qt Quick provides its own set of enablers for extending the
+ 2D/3D scene with QRhi-based custom rendering.
+
\note The RHI family of APIs are currently offered with a limited
compatibility guarantee, as opposed to regular Qt public APIs. See \l QRhi
for details.
diff --git a/src/gui/painting/qbackingstoredefaultcompositor.cpp b/src/gui/painting/qbackingstoredefaultcompositor.cpp
index 96df95c7f9..cd984ae0e9 100644
--- a/src/gui/painting/qbackingstoredefaultcompositor.cpp
+++ b/src/gui/painting/qbackingstoredefaultcompositor.cpp
@@ -551,10 +551,13 @@ QPlatformBackingStore::FlushResult QBackingStoreDefaultCompositor::flush(QPlatfo
}
for (int i = 0; i < textureWidgetCount; ++i) {
+ const bool invertSourceForTextureWidget = textures->flags(i).testFlag(QPlatformTextureList::MirrorVertically)
+ ? !invertSource : invertSource;
QMatrix4x4 target;
QMatrix3x3 source;
if (!prepareDrawForRenderToTextureWidget(textures, i, window, deviceWindowRect,
- offset, invertTargetY, invertSource, &target, &source))
+ offset, invertTargetY, invertSourceForTextureWidget,
+ &target, &source))
{
m_textureQuadData[i].reset();
continue;
diff --git a/src/gui/painting/qplatformbackingstore.h b/src/gui/painting/qplatformbackingstore.h
index d928af650a..e39515b16f 100644
--- a/src/gui/painting/qplatformbackingstore.h
+++ b/src/gui/painting/qplatformbackingstore.h
@@ -94,7 +94,8 @@ public:
enum Flag {
StacksOnTop = 0x01,
TextureIsSrgb = 0x02,
- NeedsPremultipliedAlphaBlending = 0x04
+ NeedsPremultipliedAlphaBlending = 0x04,
+ MirrorVertically = 0x08
};
Q_DECLARE_FLAGS(Flags, Flag)
diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt
index e6092060d7..d2de475fbd 100644
--- a/src/widgets/CMakeLists.txt
+++ b/src/widgets/CMakeLists.txt
@@ -20,6 +20,7 @@ qt_internal_add_module(Widgets
kernel/qlayout.cpp kernel/qlayout.h kernel/qlayout_p.h
kernel/qlayoutengine.cpp kernel/qlayoutengine_p.h
kernel/qlayoutitem.cpp kernel/qlayoutitem.h
+ kernel/qrhiwidget.cpp kernel/qrhiwidget.h kernel/qrhiwidget_p.h
kernel/qsizepolicy.cpp kernel/qsizepolicy.h
kernel/qstackedlayout.cpp kernel/qstackedlayout.h
kernel/qstandardgestures.cpp kernel/qstandardgestures_p.h
diff --git a/src/widgets/doc/images/cuberhiwidget-example.jpg b/src/widgets/doc/images/cuberhiwidget-example.jpg
new file mode 100644
index 0000000000..70baab8beb
--- /dev/null
+++ b/src/widgets/doc/images/cuberhiwidget-example.jpg
Binary files differ
diff --git a/src/widgets/doc/images/qrhiwidget-intro.jpg b/src/widgets/doc/images/qrhiwidget-intro.jpg
new file mode 100644
index 0000000000..20f931a723
--- /dev/null
+++ b/src/widgets/doc/images/qrhiwidget-intro.jpg
Binary files differ
diff --git a/src/widgets/doc/images/simplerhiwidget-example.jpg b/src/widgets/doc/images/simplerhiwidget-example.jpg
new file mode 100644
index 0000000000..3f0a1b355c
--- /dev/null
+++ b/src/widgets/doc/images/simplerhiwidget-example.jpg
Binary files differ
diff --git a/src/widgets/doc/snippets/qrhiwidget/rhiwidgetintro.cpp b/src/widgets/doc/snippets/qrhiwidget/rhiwidgetintro.cpp
new file mode 100644
index 0000000000..1cd583e294
--- /dev/null
+++ b/src/widgets/doc/snippets/qrhiwidget/rhiwidgetintro.cpp
@@ -0,0 +1,109 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include <QRhiWidget>
+#include <QFile>
+#include <rhi/qrhi.h>
+
+//![0]
+class ExampleRhiWidget : public QRhiWidget
+{
+public:
+ ExampleRhiWidget(QWidget *parent = nullptr) : QRhiWidget(parent) { }
+ void initialize(QRhiCommandBuffer *cb) override;
+ void render(QRhiCommandBuffer *cb) override;
+private:
+ QRhi *m_rhi = nullptr;
+ std::unique_ptr<QRhiBuffer> m_vbuf;
+ std::unique_ptr<QRhiBuffer> m_ubuf;
+ std::unique_ptr<QRhiShaderResourceBindings> m_srb;
+ std::unique_ptr<QRhiGraphicsPipeline> m_pipeline;
+ QMatrix4x4 m_viewProjection;
+ float m_rotation = 0.0f;
+};
+
+float vertexData[] = {
+ 0.0f, 0.5f, 1.0f, 0.0f, 0.0f,
+ -0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
+ 0.5f, -0.5f, 0.0f, 0.0f, 1.0f,
+};
+
+QShader getShader(const QString &name)
+{
+ QFile f(name);
+ return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader();
+}
+
+void ExampleRhiWidget::initialize(QRhiCommandBuffer *cb)
+{
+ if (m_rhi != rhi()) {
+ m_pipeline.reset();
+ m_rhi = rhi();
+ }
+
+ if (!m_pipeline) {
+ m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData)));
+ m_vbuf->create();
+
+ m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
+ m_ubuf->create();
+
+ m_srb.reset(m_rhi->newShaderResourceBindings());
+ m_srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, m_ubuf.get()),
+ });
+ m_srb->create();
+
+ m_pipeline.reset(m_rhi->newGraphicsPipeline());
+ m_pipeline->setShaderStages({
+ { QRhiShaderStage::Vertex, getShader(QLatin1String(":/shader_assets/color.vert.qsb")) },
+ { QRhiShaderStage::Fragment, getShader(QLatin1String(":/shader_assets/color.frag.qsb")) }
+ });
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 5 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) }
+ });
+ m_pipeline->setVertexInputLayout(inputLayout);
+ m_pipeline->setShaderResourceBindings(m_srb.get());
+ m_pipeline->setRenderPassDescriptor(renderTarget()->renderPassDescriptor());
+ m_pipeline->create();
+
+ QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch();
+ resourceUpdates->uploadStaticBuffer(m_vbuf.get(), vertexData);
+ cb->resourceUpdate(resourceUpdates);
+ }
+
+ const QSize outputSize = colorTexture()->pixelSize();
+ m_viewProjection = m_rhi->clipSpaceCorrMatrix();
+ m_viewProjection.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 1000.0f);
+ m_viewProjection.translate(0, 0, -4);
+}
+
+void ExampleRhiWidget::render(QRhiCommandBuffer *cb)
+{
+ QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch();
+ m_rotation += 1.0f;
+ QMatrix4x4 modelViewProjection = m_viewProjection;
+ modelViewProjection.rotate(m_rotation, 0, 1, 0);
+ resourceUpdates->updateDynamicBuffer(m_ubuf.get(), 0, 64, modelViewProjection.constData());
+
+ const QColor clearColor = QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f);
+ cb->beginPass(renderTarget(), clearColor, { 1.0f, 0 }, resourceUpdates);
+
+ cb->setGraphicsPipeline(m_pipeline.get());
+ const QSize outputSize = colorTexture()->pixelSize();
+ cb->setViewport(QRhiViewport(0, 0, outputSize.width(), outputSize.height()));
+ cb->setShaderResources();
+ const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0);
+ cb->setVertexInput(0, 1, &vbufBinding);
+ cb->draw(3);
+
+ cb->endPass();
+
+ update();
+}
+//![0]
diff --git a/src/widgets/doc/snippets/qrhiwidget/rhiwidgetintro.frag b/src/widgets/doc/snippets/qrhiwidget/rhiwidgetintro.frag
new file mode 100644
index 0000000000..d86bcf7386
--- /dev/null
+++ b/src/widgets/doc/snippets/qrhiwidget/rhiwidgetintro.frag
@@ -0,0 +1,10 @@
+//![0]
+#version 440
+layout(location = 0) in vec3 v_color;
+layout(location = 0) out vec4 fragColor;
+
+void main()
+{
+ fragColor = vec4(v_color, 1.0);
+}
+//![0]
diff --git a/src/widgets/doc/snippets/qrhiwidget/rhiwidgetintro.vert b/src/widgets/doc/snippets/qrhiwidget/rhiwidgetintro.vert
new file mode 100644
index 0000000000..610df304b1
--- /dev/null
+++ b/src/widgets/doc/snippets/qrhiwidget/rhiwidgetintro.vert
@@ -0,0 +1,15 @@
+//![0]
+#version 440
+layout(location = 0) in vec4 position;
+layout(location = 1) in vec3 color;
+layout(location = 0) out vec3 v_color;
+layout(std140, binding = 0) uniform buf {
+ mat4 mvp;
+};
+
+void main()
+{
+ v_color = color;
+ gl_Position = mvp * position;
+}
+//![0]
diff --git a/src/widgets/kernel/qrhiwidget.cpp b/src/widgets/kernel/qrhiwidget.cpp
new file mode 100644
index 0000000000..0b49e7465d
--- /dev/null
+++ b/src/widgets/kernel/qrhiwidget.cpp
@@ -0,0 +1,1283 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#include "qrhiwidget_p.h"
+#include <private/qguiapplication_p.h>
+#include <qpa/qplatformintegration.h>
+#include <private/qwidgetrepaintmanager_p.h>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \class QRhiWidget
+ \inmodule QtWidgets
+ \since 6.7
+
+ \brief The QRhiWidget class is a widget for rendering 3D graphics via an
+ accelerated grapics API, such as Vulkan, Metal, or Direct 3D.
+
+ \preliminary
+
+ \note QRhiWidget is in tech preview in Qt 6.7. \b {The API is under
+ development and subject to change.}
+
+ QRhiWidget provides functionality for displaying 3D content rendered
+ through the \l QRhi APIs within a QWidget-based application. In many ways
+ it is the portable equivalent of \l QOpenGLWidget that is not tied to a
+ single 3D graphics API, but rather can function with all the APIs QRhi
+ supports (such as, Direct 3D 11/12, Vulkan, Metal, and OpenGL).
+
+ QRhiWidget is expected to be subclassed. To render into the 2D texture that
+ is implicitly created and managed by the QRhiWidget, subclasses should
+ reimplement the virtual functions initialize() and render().
+
+ The size of the texture will by default adapt to the size of the item. If a
+ fixed size is preferred, set an explicit size specified in pixels by
+ calling setExplicitSize().
+
+ In addition to the texture serving as the color buffer, a depth/stencil
+ buffer and a render target binding these together is maintained implicitly
+ as well.
+
+ The QRhi for the widget's top-level window is configured to use a platform
+ specific backend and graphics API by default: Metal on macOS and iOS,
+ Direct 3D 11 on Windows, OpenGL otherwise. Call setApi() to override this.
+
+ \note A single widget window can only use one QRhi backend, and so graphics
+ API. If two QRhiWidget or QQuickWidget widgets in the window's widget
+ hierarchy request different APIs, only one of them will function correctly.
+
+ \note While QRhiWidget is a public Qt API, the QRhi family of classes in
+ the Qt Gui module, including QShader and QShaderDescription, offer limited
+ compatibility guarantees. There are no source or binary compatibility
+ guarantees for these classes, meaning the API is only guaranteed to work
+ with the Qt version the application was developed against. Source
+ incompatible changes are however aimed to be kept at a minimum and will
+ only be made in minor releases (6.7, 6.8, and so on). \c{qrhiwidget.h} does
+ not directly include any QRhi-related headers. To use those classes when
+ implementing a QRhiWidget subclass, link to
+ \c{Qt::GuiPrivate} (if using CMake), and include the appropriate headers
+ with the \c rhi prefix, for example \c{#include <rhi/qrhi.h>}.
+
+ An example of a simple QRhiWidget subclass rendering a triangle is the
+ following:
+
+ \snippet qrhiwidget/rhiwidgetintro.cpp 0
+
+ This is a widget that continuously requests updates, throttled by the
+ presentation rate (vsync, depending on the screen refresh rate). If
+ continuously rendering is not desired, the update() call in render() should
+ be removed and rather issued when updating the rendered content is
+ necessary. For example, if the rotation should be tied to the value of a
+ QSlider, then connecting the slider's value change signal to a slot or
+ lambda that forwards the new value and calls update() is sufficient.
+
+ The vertex and fragment shaders are provided as Vulkan-style GLSL and must
+ be processed first by the Qt shader infrastructure first. This is achieved
+ either by running the \c qsb command-line tool manually, or by using the
+ qt_add_shaders() function in CMake. The QRhiWidget implementation loads
+ these pre-processed \c{.qsb} files that are shipped with the application.
+
+ The source code for these shaders could be the following:
+
+ \c{color.vert}
+
+ \snippet qrhiwidget/rhiwidgetintro.vert 0
+
+ \c{color.frag}
+
+ \snippet qrhiwidget/rhiwidgetintro.frag 0
+
+ The result is a widget that shows the following:
+
+ \image qrhiwidget-intro.jpg
+
+ For a complete, minimal, introductory example check out the \l{Simple RHI
+ Widget Example}.
+
+ For an example with more functionality and demonstration of further
+ concepts, check the \l{Cube RHI Widget Example}.
+
+ QRhiWidget always involves rendering into a backing texture, not
+ directly to the window (the surface or layer provided by the windowing
+ system for the native window). This allows properly compositing the content
+ with the rest of the widget-based UI, and offering a simple and compact
+ API, making it easy to get started. All this comes at the expense of
+ additional resources and a potential effect on performance. This is often
+ perfectly acceptable in practice, but advanced users should keep in mind
+ the pros and cons of the different approaches. Refer to the \l{RHI Window
+ Example} and compare it with the \l{Simple RHI Widget Example} for details
+ about the two approaches.
+
+ Reparenting a QRhiWidget into a widget hierarchy that belongs to a
+ different window (top-level widget), or making the QRhiWidget itself a
+ top-level (by setting the parent to \nullptr), involves changing the
+ associated QRhi (and potentially destroying the old one) while the
+ QRhiWidget continues to stay alive and well. To support this, robust
+ QRhiWidget implementations are expected to reimplement the
+ releaseResources() virtual function as well, and drop their QRhi resources
+ just as they do in the destructor. The \l{Cube RHI Widget Example}
+ demonstrates this in practice.
+
+ While not a primary use case, QRhiWidget also allows incorporating
+ rendering code that directly uses a 3D graphics API such as Vulkan, Metal,
+ Direct 3D, or OpenGL. See \l QRhiCommandBuffer::beginExternal() for details
+ on recording native commands within a QRhi render pass, as well as
+ \l QRhiTexture::createFrom() for a way to wrap an existing native texture and
+ then use it with QRhi in a subsequent render pass. Note however that the
+ configurability of the underlying graphics API (its device or context
+ features, layers, extensions, etc.) is going to be limited since
+ QRhiWidget's primary goal is to provide an environment suitable for
+ QRhi-based rendering code, not to enable arbitrary, potentially complex,
+ foreign rendering engines.
+
+ \since 6.7
+
+ \sa QRhi, QShader, QOpenGLWidget, {Simple RHI Widget Example}, {Cube RHI Widget Example}
+ */
+
+/*!
+ \enum QRhiWidget::Api
+ Specifies the 3D API and QRhi backend to use
+
+ \value OpenGL
+ \value Metal
+ \value Vulkan
+ \value D3D11
+ \value D3D12
+ \value Null
+
+ \sa QRhi
+ */
+
+/*!
+ \enum QRhiWidget::TextureFormat
+ Specifies the format of the texture to which the QRhiWidget renders.
+
+ \value RGBA8 See QRhiTexture::RGBA8.
+ \value RGBA16F See QRhiTexture::RGBA16F.
+ \value RGBA32F See QRhiTexture::RGBA32F.
+ \value RGB10A2 See QRhiTexture::RGB10A2.
+
+ \sa QRhiTexture
+ */
+
+/*!
+ Constructs a widget which is a child of \a parent, with widget flags set to \a f.
+ */
+QRhiWidget::QRhiWidget(QWidget *parent, Qt::WindowFlags f)
+ : QWidget(*(new QRhiWidgetPrivate), parent, f)
+{
+ Q_D(QRhiWidget);
+ if (Q_UNLIKELY(!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::RhiBasedRendering)))
+ qWarning("QRhiWidget: QRhi is not supported on this platform.");
+ else
+ d->setRenderToTexture();
+
+ d->config.setEnabled(true);
+#if defined(Q_OS_DARWIN)
+ d->config.setApi(QPlatformBackingStoreRhiConfig::Metal);
+#elif defined(Q_OS_WIN)
+ d->config.setApi(QPlatformBackingStoreRhiConfig::D3D11);
+#else
+ d->config.setApi(QPlatformBackingStoreRhiConfig::OpenGL);
+#endif
+}
+
+/*!
+ Destructor.
+ */
+QRhiWidget::~QRhiWidget()
+{
+ Q_D(QRhiWidget);
+
+ if (d->rhi) {
+ d->rhi->removeCleanupCallback(this);
+ // rhi resources must be destroyed here, due to how QWidget teardown works;
+ // it should not be left to the private object's destruction.
+ d->resetRenderTargetObjects();
+ d->resetColorBufferObjects();
+ qDeleteAll(d->pendingDeletes);
+ }
+
+ d->offscreenRenderer.reset();
+}
+
+/*!
+ Handles resize events that are passed in the \a e event parameter. Calls
+ the virtual function initialize().
+
+ \note Avoid overriding this function in derived classes. If that is not
+ feasible, make sure that QRhiWidget's implementation is invoked too.
+ Otherwise the underlying texture object and related resources will not get
+ resized properly and will lead to incorrect rendering.
+ */
+void QRhiWidget::resizeEvent(QResizeEvent *e)
+{
+ Q_D(QRhiWidget);
+
+ if (e->size().isEmpty()) {
+ d->noSize = true;
+ return;
+ }
+ d->noSize = false;
+
+ d->sendPaintEvent(QRect(QPoint(0, 0), size()));
+}
+
+/*!
+ Handles paint events.
+
+ Calling QWidget::update() will lead to sending a paint event \a e, and thus
+ invoking this function. The sending of the event is asynchronous and will
+ happen at some point after returning from update(). This function will
+ then, after some preparation, call the virtual render() to update the
+ contents of the QRhiWidget's associated texture. The widget's top-level
+ window will then composite the texture with the rest of the window.
+ */
+void QRhiWidget::paintEvent(QPaintEvent *)
+{
+ Q_D(QRhiWidget);
+ if (!updatesEnabled() || d->noSize)
+ return;
+
+ d->ensureRhi();
+ if (!d->rhi) {
+ qWarning("QRhiWidget: No QRhi");
+ emit renderFailed();
+ return;
+ }
+
+ QRhiCommandBuffer *cb = nullptr;
+ if (d->rhi->beginOffscreenFrame(&cb) != QRhi::FrameOpSuccess)
+ return;
+
+ bool needsInit = false;
+ d->ensureTexture(&needsInit);
+ if (d->colorTexture || d->msaaColorBuffer) {
+ bool canRender = true;
+ if (needsInit)
+ canRender = d->invokeInitialize(cb);
+ if (canRender)
+ render(cb);
+ }
+
+ d->rhi->endOffscreenFrame();
+}
+
+/*!
+ \reimp
+*/
+bool QRhiWidget::event(QEvent *e)
+{
+ Q_D(QRhiWidget);
+ switch (e->type()) {
+ case QEvent::WindowChangeInternal:
+ // The QRhi will almost certainly change, prevent texture() from
+ // returning the existing QRhiTexture in the meantime.
+ d->textureInvalid = true;
+
+ if (d->rhi && d->rhi != d->offscreenRenderer.rhi()) {
+ // Drop the cleanup callback registered to the toplevel's rhi and
+ // do the early-release, there may not be another chance to do
+ // this, and the QRhi we have currently set may be destroyed by the
+ // time we get to ensureRhi() again.
+ d->rhi->removeCleanupCallback(this);
+ releaseResources(); // notify the user code about the early-release
+ d->releaseResources();
+ // must _not_ null out d->rhi here, for proper interaction with ensureRhi()
+ }
+
+ break;
+
+ case QEvent::Show:
+ if (isVisible())
+ d->sendPaintEvent(QRect(QPoint(0, 0), size()));
+ break;
+ default:
+ break;
+ }
+ return QWidget::event(e);
+}
+
+QWidgetPrivate::TextureData QRhiWidgetPrivate::texture() const
+{
+ // This is the only safe place to clear pendingDeletes, due to the
+ // possibility of the texture returned in the previous invocation of this
+ // function having been added to pendingDeletes, meaning the object then
+ // needs to be valid until the next (this) invocation of this function.
+ // (the exact object lifetime requirements depend on the
+ // QWidget/RepaintManager internal implementation; for now avoid relying on
+ // such details by clearing pendingDeletes only here, not in endCompose())
+ qDeleteAll(pendingDeletes);
+ pendingDeletes.clear();
+
+ TextureData td;
+ if (!textureInvalid)
+ td.textureLeft = resolveTexture ? resolveTexture : colorTexture;
+ return td;
+}
+
+QPlatformTextureList::Flags QRhiWidgetPrivate::textureListFlags()
+{
+ QPlatformTextureList::Flags flags = QWidgetPrivate::textureListFlags();
+ if (mirrorVertically)
+ flags |= QPlatformTextureList::MirrorVertically;
+ return flags;
+}
+
+QPlatformBackingStoreRhiConfig QRhiWidgetPrivate::rhiConfig() const
+{
+ return config;
+}
+
+void QRhiWidgetPrivate::endCompose()
+{
+ // This function is called by QWidgetRepaintManager right after the
+ // backingstore's QRhi-based flush returns. In practice that means after
+ // the begin-endFrame() on the top-level window's swapchain.
+
+ if (rhi) {
+ Q_Q(QRhiWidget);
+ emit q->frameSubmitted();
+ }
+}
+
+void QRhiWidgetPrivate::resetColorBufferObjects()
+{
+ if (colorTexture) {
+ pendingDeletes.append(colorTexture);
+ colorTexture = nullptr;
+ }
+ if (msaaColorBuffer) {
+ pendingDeletes.append(msaaColorBuffer);
+ msaaColorBuffer = nullptr;
+ }
+ if (resolveTexture) {
+ pendingDeletes.append(resolveTexture);
+ resolveTexture = nullptr;
+ }
+}
+
+void QRhiWidgetPrivate::resetRenderTargetObjects()
+{
+ if (renderTarget) {
+ renderTarget->deleteLater();
+ renderTarget = nullptr;
+ }
+ if (renderPassDescriptor) {
+ renderPassDescriptor->deleteLater();
+ renderPassDescriptor = nullptr;
+ }
+ if (depthStencilBuffer) {
+ depthStencilBuffer->deleteLater();
+ depthStencilBuffer = nullptr;
+ }
+}
+
+void QRhiWidgetPrivate::releaseResources()
+{
+ resetRenderTargetObjects();
+ resetColorBufferObjects();
+ qDeleteAll(pendingDeletes);
+ pendingDeletes.clear();
+}
+
+void QRhiWidgetPrivate::ensureRhi()
+{
+ Q_Q(QRhiWidget);
+ // the QRhi and infrastructure belongs to the top-level widget, not to this widget
+ QWidget *tlw = q->window();
+ QWidgetPrivate *wd = get(tlw);
+
+ QRhi *currentRhi = nullptr;
+ if (QWidgetRepaintManager *repaintManager = wd->maybeRepaintManager())
+ currentRhi = repaintManager->rhi();
+
+ if (currentRhi && currentRhi->backend() != QBackingStoreRhiSupport::apiToRhiBackend(config.api())) {
+ qWarning("The top-level window is already using another graphics API for composition, "
+ "'%s' is not compatible with this widget",
+ currentRhi->backendName());
+ return;
+ }
+
+ // NB the rhi member may be an invalid object, the pointer can be used, but no deref
+ if (currentRhi && rhi && rhi != currentRhi) {
+ // if previously we created our own but now get a QRhi from the
+ // top-level, then drop what we have and start using the top-level's
+ if (rhi == offscreenRenderer.rhi()) {
+ q->releaseResources(); // notify the user code about the early-release
+ releaseResources();
+ offscreenRenderer.reset();
+ } else {
+ // rhi resources created by us all belong to the old rhi, drop them;
+ // due to nulling out colorTexture this is also what ensures that
+ // initialize() is going to be called again eventually
+ resetRenderTargetObjects();
+ resetColorBufferObjects();
+ }
+
+ // Normally the widget gets destroyed before the QRhi (which is managed by
+ // the top-level's backingstore). When reparenting between top-levels is
+ // involved, that is not always the case. Therefore we use a per-widget rhi
+ // cleanup callback to get notified when the QRhi is about to be destroyed
+ // while the QRhiWidget is still around.
+ currentRhi->addCleanupCallback(q, [q, this](QRhi *regRhi) {
+ if (!QWidgetPrivate::get(q)->data.in_destructor && this->rhi == regRhi) {
+ q->releaseResources(); // notify the user code about the early-release
+ releaseResources();
+ // must null out our ref, the QRhi object is going to be invalid
+ this->rhi = nullptr;
+ }
+ });
+ }
+
+ rhi = currentRhi;
+}
+
+void QRhiWidgetPrivate::ensureTexture(bool *changed)
+{
+ Q_Q(QRhiWidget);
+
+ QSize newSize = explicitSize;
+ if (newSize.isEmpty())
+ newSize = q->size() * q->devicePixelRatio();
+
+ const int minTexSize = rhi->resourceLimit(QRhi::TextureSizeMin);
+ const int maxTexSize = rhi->resourceLimit(QRhi::TextureSizeMax);
+ newSize.setWidth(qMin(maxTexSize, qMax(minTexSize, newSize.width())));
+ newSize.setHeight(qMin(maxTexSize, qMax(minTexSize, newSize.height())));
+
+ if (colorTexture) {
+ if (colorTexture->format() != rhiTextureFormat || colorTexture->sampleCount() != samples) {
+ resetColorBufferObjects();
+ // sample count change needs new depth-stencil, possibly a new
+ // render target; format change needs new renderpassdescriptor;
+ // therefore must drop the rest too
+ resetRenderTargetObjects();
+ }
+ }
+
+ if (msaaColorBuffer) {
+ if (msaaColorBuffer->backingFormat() != rhiTextureFormat || msaaColorBuffer->sampleCount() != samples) {
+ resetColorBufferObjects();
+ // sample count change needs new depth-stencil, possibly a new
+ // render target; format change needs new renderpassdescriptor;
+ // therefore must drop the rest too
+ resetRenderTargetObjects();
+ }
+ }
+
+ if (!colorTexture && samples <= 1) {
+ if (changed)
+ *changed = true;
+ if (!rhi->isTextureFormatSupported(rhiTextureFormat)) {
+ qWarning("QRhiWidget: The requested texture format (%d) is not supported by the "
+ "underlying 3D graphics API implementation", int(rhiTextureFormat));
+ }
+ colorTexture = rhi->newTexture(rhiTextureFormat, newSize, samples, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource);
+ if (!colorTexture->create()) {
+ qWarning("Failed to create backing texture for QRhiWidget");
+ delete colorTexture;
+ colorTexture = nullptr;
+ return;
+ }
+ }
+
+ if (samples > 1) {
+ if (!msaaColorBuffer) {
+ if (changed)
+ *changed = true;
+ if (!rhi->isFeatureSupported(QRhi::MultisampleRenderBuffer)) {
+ qWarning("QRhiWidget: Multisample renderbuffers are reported as unsupported; "
+ "sample count %d will not work as expected", samples);
+ }
+ if (!rhi->isTextureFormatSupported(rhiTextureFormat)) {
+ qWarning("QRhiWidget: The requested texture format (%d) is not supported by the "
+ "underlying 3D graphics API implementation", int(rhiTextureFormat));
+ }
+ msaaColorBuffer = rhi->newRenderBuffer(QRhiRenderBuffer::Color, newSize, samples, {}, rhiTextureFormat);
+ if (!msaaColorBuffer->create()) {
+ qWarning("Failed to create multisample color buffer for QRhiWidget");
+ delete msaaColorBuffer;
+ msaaColorBuffer = nullptr;
+ return;
+ }
+ }
+ if (!resolveTexture) {
+ if (changed)
+ *changed = true;
+ resolveTexture = rhi->newTexture(rhiTextureFormat, newSize, 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource);
+ if (!resolveTexture->create()) {
+ qWarning("Failed to create resolve texture for QRhiWidget");
+ delete resolveTexture;
+ resolveTexture = nullptr;
+ return;
+ }
+ }
+ } else if (resolveTexture) {
+ resolveTexture->deleteLater();
+ resolveTexture = nullptr;
+ }
+
+ if (colorTexture && colorTexture->pixelSize() != newSize) {
+ if (changed)
+ *changed = true;
+ colorTexture->setPixelSize(newSize);
+ if (!colorTexture->create())
+ qWarning("Failed to rebuild texture for QRhiWidget after resizing");
+ }
+
+ if (msaaColorBuffer && msaaColorBuffer->pixelSize() != newSize) {
+ if (changed)
+ *changed = true;
+ msaaColorBuffer->setPixelSize(newSize);
+ if (!msaaColorBuffer->create())
+ qWarning("Failed to rebuild multisample color buffer for QRhiWidget after resizing");
+ }
+
+ if (resolveTexture && resolveTexture->pixelSize() != newSize) {
+ if (changed)
+ *changed = true;
+ resolveTexture->setPixelSize(newSize);
+ if (!resolveTexture->create())
+ qWarning("Failed to rebuild resolve texture for QRhiWidget after resizing");
+ }
+
+ textureInvalid = false;
+}
+
+bool QRhiWidgetPrivate::invokeInitialize(QRhiCommandBuffer *cb)
+{
+ Q_Q(QRhiWidget);
+ if (!colorTexture && !msaaColorBuffer)
+ return false;
+
+ if (autoRenderTarget) {
+ const QSize pixelSize = colorTexture ? colorTexture->pixelSize() : msaaColorBuffer->pixelSize();
+ if (!depthStencilBuffer) {
+ depthStencilBuffer = rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, pixelSize, samples);
+ if (!depthStencilBuffer->create()) {
+ qWarning("Failed to create depth-stencil buffer for QRhiWidget");
+ resetRenderTargetObjects();
+ return false;
+ }
+ } else if (depthStencilBuffer->pixelSize() != pixelSize) {
+ depthStencilBuffer->setPixelSize(pixelSize);
+ if (!depthStencilBuffer->create()) {
+ qWarning("Failed to rebuild depth-stencil buffer for QRhiWidget with new size");
+ return false;
+ }
+ }
+
+ if (!renderTarget) {
+ QRhiColorAttachment color0;
+ if (colorTexture)
+ color0.setTexture(colorTexture);
+ else
+ color0.setRenderBuffer(msaaColorBuffer);
+ if (samples > 1)
+ color0.setResolveTexture(resolveTexture);
+ QRhiTextureRenderTargetDescription rtDesc(color0, depthStencilBuffer);
+ renderTarget = rhi->newTextureRenderTarget(rtDesc);
+ renderPassDescriptor = renderTarget->newCompatibleRenderPassDescriptor();
+ renderTarget->setRenderPassDescriptor(renderPassDescriptor);
+ if (!renderTarget->create()) {
+ qWarning("Failed to create render target for QRhiWidget");
+ resetRenderTargetObjects();
+ return false;
+ }
+ }
+ } else {
+ resetRenderTargetObjects();
+ }
+
+ q->initialize(cb);
+
+ return true;
+}
+
+/*!
+ \return the currently set graphics API (QRhi backend).
+
+ \sa setApi()
+ */
+QRhiWidget::Api QRhiWidget::api() const
+{
+ Q_D(const QRhiWidget);
+ switch (d->config.api()) {
+ case QPlatformBackingStoreRhiConfig::OpenGL:
+ return Api::OpenGL;
+ case QPlatformBackingStoreRhiConfig::Metal:
+ return Api::Metal;
+ case QPlatformBackingStoreRhiConfig::Vulkan:
+ return Api::Vulkan;
+ case QPlatformBackingStoreRhiConfig::D3D11:
+ return Api::D3D11;
+ case QPlatformBackingStoreRhiConfig::D3D12:
+ return Api::D3D12;
+ default:
+ return Api::Null;
+ }
+}
+
+/*!
+ Sets the graphics API and QRhi backend to use to \a api.
+
+ \warning This function must be called early enough, before the widget is
+ added to a widget hierarchy and displayed on screen. For example, aim to
+ call the function for the subclass constructor. If called too late, the
+ function will have no effect.
+
+ The default value depends on the platform: Metal on macOS and iOS, Direct
+ 3D 11 on Windows, OpenGL otherwise.
+
+ The \a api can only be set once for the widget and its top-level window,
+ once it is done and takes effect, the window can only use that API and QRhi
+ backend to render. Attempting to set another value, or to add another
+ QRhiWidget with a different \a api will not function as expected.
+
+ \sa setTextureFormat(), setDebugLayer(), api()
+ */
+void QRhiWidget::setApi(Api api)
+{
+ Q_D(QRhiWidget);
+ switch (api) {
+ case Api::OpenGL:
+ d->config.setApi(QPlatformBackingStoreRhiConfig::OpenGL);
+ break;
+ case Api::Metal:
+ d->config.setApi(QPlatformBackingStoreRhiConfig::Metal);
+ break;
+ case Api::Vulkan:
+ d->config.setApi(QPlatformBackingStoreRhiConfig::Vulkan);
+ break;
+ case Api::D3D11:
+ d->config.setApi(QPlatformBackingStoreRhiConfig::D3D11);
+ break;
+ case Api::D3D12:
+ d->config.setApi(QPlatformBackingStoreRhiConfig::D3D12);
+ break;
+ case Api::Null:
+ d->config.setApi(QPlatformBackingStoreRhiConfig::Null);
+ break;
+ }
+}
+
+/*!
+ \return true if a debug or validation layer will be requested if applicable
+ to the graphics API in use.
+
+ \sa setDebugLayer()
+ */
+bool QRhiWidget::isDebugLayerEnabled() const
+{
+ Q_D(const QRhiWidget);
+ return d->config.isDebugLayerEnabled();
+}
+
+/*!
+ Requests the debug or validation layer of the underlying graphics API
+ when \a enable is true.
+
+ \warning This function must be called early enough, before the widget is added
+ to a widget hierarchy and displayed on screen. For example, aim to call the
+ function for the subclass constructor. If called too late, the function
+ will have no effect.
+
+ Applicable for Vulkan and Direct 3D.
+
+ By default this is disabled.
+
+ \sa setApi(), isDebugLayerEnabled()
+ */
+void QRhiWidget::setDebugLayer(bool enable)
+{
+ Q_D(QRhiWidget);
+ d->config.setDebugLayer(enable);
+}
+
+/*!
+ \property QRhiWidget::textureFormat
+
+ This property controls the texture format for the texture used as the color
+ buffer. The default value is TextureFormat::RGBA8. QRhiWidget supports
+ rendering to a subset of the formats supported by \l QRhiTexture. Only
+ formats that are reported as supported from
+ \l QRhi::isTextureFormatSupported() should be specified, rendering will not be
+ functional otherwise.
+
+ \note Setting a new format when the widget is already initialized and has
+ rendered implies that all QRhiGraphicsPipeline objects created by the
+ renderer may become unusable, if the associated QRhiRenderPassDescriptor is
+ now incompatible due to the different texture format. Similarly to changing
+ \l sampleCount dynamically, this means that initialize() or render()
+ implementations must then take care of releasing the existing pipelines and
+ creating new ones.
+ */
+
+QRhiWidget::TextureFormat QRhiWidget::textureFormat() const
+{
+ Q_D(const QRhiWidget);
+ return d->widgetTextureFormat;
+}
+
+void QRhiWidget::setTextureFormat(TextureFormat format)
+{
+ Q_D(QRhiWidget);
+ if (d->widgetTextureFormat != format) {
+ d->widgetTextureFormat = format;
+ switch (format) {
+ case TextureFormat::RGBA8:
+ d->rhiTextureFormat = QRhiTexture::RGBA8;
+ break;
+ case TextureFormat::RGBA16F:
+ d->rhiTextureFormat = QRhiTexture::RGBA16F;
+ break;
+ case TextureFormat::RGBA32F:
+ d->rhiTextureFormat = QRhiTexture::RGBA32F;
+ break;
+ case TextureFormat::RGB10A2:
+ d->rhiTextureFormat = QRhiTexture::RGB10A2;
+ break;
+ }
+ emit textureFormatChanged(format);
+ update();
+ }
+}
+
+/*!
+ \property QRhiWidget::sampleCount
+
+ This property controls for sample count for multisample antialiasing.
+ By default the value is \c 1 which means MSAA is disabled.
+
+ Valid values are 1, 4, 8, and sometimes 16 and 32.
+ \l QRhi::supportedSampleCounts() can be used to query the supported sample
+ counts at run time, but typically applications should request 1 (no MSAA),
+ 4x (normal MSAA) or 8x (high MSAA).
+
+ \note Setting a new value implies that all QRhiGraphicsPipeline objects
+ created by the renderer must use the same sample count from then on.
+ Existing QRhiGraphicsPipeline objects created with a different sample count
+ must not be used anymore. When the value changes, all color and
+ depth-stencil buffers are destroyed and recreated automatically, and
+ initialize() is invoked again. However, when
+ \l autoRenderTarget is \c false, it will be up to the application to
+ manage this with regards to the depth-stencil buffer or additional color
+ buffers.
+
+ Changing the sample count from the default 1 to a higher value implies that
+ colorTexture() becomes \nullptr and msaaColorBuffer() starts returning a
+ valid object. Switching back to 1 (or 0), implies the opposite: in the next
+ call to initialize() msaaColorBuffer() is going to return \nullptr, whereas
+ colorTexture() becomes once again valid. In addition, resolveTexture()
+ returns a valid (non-multisample) QRhiTexture whenever the sample count is
+ greater than 1 (i.e., MSAA is in use).
+
+ \sa msaaColorBuffer(), resolveTexture()
+ */
+
+int QRhiWidget::sampleCount() const
+{
+ Q_D(const QRhiWidget);
+ return d->samples;
+}
+
+void QRhiWidget::setSampleCount(int samples)
+{
+ Q_D(QRhiWidget);
+ if (d->samples != samples) {
+ d->samples = samples;
+ emit sampleCountChanged(samples);
+ update();
+ }
+}
+
+/*!
+ \property QRhiWidget::explicitSize
+
+ The fixed size, in pixels, of the QRhiWidget's associated texture. Relevant
+ when a fixed texture size is desired that does not depend on the widget's
+ size. This size has no effect on the geometry of the widget (its size and
+ placement within the top-level window), which means the texture's content
+ will appear stretched (scaled up) or scaled down onto the widget's area.
+
+ For example, setting a size that is exactly twice the widget's (pixel) size
+ effectively performs 2x supersampling (rendering at twice the resolution
+ and then implicitly scaling down when texturing the quad corresponding to
+ the widget in the window).
+
+ By default the value is a null QSize. A null or empty QSize means that the
+ texture's size follows the QRhiWidget's size. (\c{texture size} = \c{widget
+ size} * \c{device pixel ratio}).
+ */
+
+QSize QRhiWidget::explicitSize() const
+{
+ Q_D(const QRhiWidget);
+ return d->explicitSize;
+}
+
+void QRhiWidget::setExplicitSize(const QSize &pixelSize)
+{
+ Q_D(QRhiWidget);
+ if (d->explicitSize != pixelSize) {
+ d->explicitSize = pixelSize;
+ emit explicitSizeChanged(pixelSize);
+ update();
+ }
+}
+
+/*!
+ \property QRhiWidget::mirrorVertically
+
+ When enabled, flips the image around the X axis when compositing the
+ QRhiWidget's backing texture with the rest of the widget content in the
+ top-level window.
+
+ The default value is \c false.
+ */
+
+bool QRhiWidget::isMirrorVerticallyEnabled() const
+{
+ Q_D(const QRhiWidget);
+ return d->mirrorVertically;
+}
+
+void QRhiWidget::setMirrorVertically(bool enabled)
+{
+ Q_D(QRhiWidget);
+ if (d->mirrorVertically != enabled) {
+ d->mirrorVertically = enabled;
+ emit mirrorVerticallyChanged(enabled);
+ update();
+ }
+}
+
+/*!
+ \property QRhiWidget::autoRenderTarget
+
+ This property controls if a depth-stencil QRhiRenderBuffer and a
+ QRhiTextureRenderTarget is created and maintained automatically by the
+ widget. The default value is \c true.
+
+ In automatic mode, the size and sample count of the depth-stencil buffer
+ follows the color buffer texture's settings. In non-automatic mode,
+ renderTarget() and depthStencilBuffer() always return \nullptr and it is
+ then up to the application's implementation of initialize() to take care of
+ setting up and managing these objects.
+ */
+
+bool QRhiWidget::isAutoRenderTargetEnabled() const
+{
+ Q_D(const QRhiWidget);
+ return d->autoRenderTarget;
+}
+
+void QRhiWidget::setAutoRenderTarget(bool enabled)
+{
+ Q_D(QRhiWidget);
+ if (d->autoRenderTarget != enabled) {
+ d->autoRenderTarget = enabled;
+ emit autoRenderTargetChanged(enabled);
+ update();
+ }
+}
+
+/*!
+ Renders a new frame, reads the contents of the texture back, and returns it
+ as a QImage.
+
+ When an error occurs, a null QImage is returned.
+
+ The returned QImage will have a format of QImage::Format_RGBA8888,
+ QImage::Format_RGBA16FPx4, QImage::Format_RGBA32FPx4, or
+ QImage::Format_BGR30 depending on textureFormat().
+
+ QRhiWidget does not know the renderer's approach to blending and
+ composition, and therefore cannot know if the output has alpha
+ premultiplied in the RGB color values. Thus \c{_Premultiplied} QImage
+ formats are never used for the returned QImage, even when it would be
+ appropriate. It is up to the caller to reinterpret the resulting data as it
+ sees fit.
+
+ This function can also be called when the QRhiWidget is not added to a
+ widget hierarchy belonging to an on-screen top-level window. This allows
+ generating an image from a 3D rendering off-screen.
+
+ \sa setTextureFormat()
+ */
+QImage QRhiWidget::grab()
+{
+ Q_D(QRhiWidget);
+ if (d->noSize)
+ return QImage();
+
+ d->ensureRhi();
+ if (!d->rhi) {
+ // The widget (and its parent chain, if any) may not be shown at
+ // all, yet one may still want to use it for grabs. This is
+ // ridiculous of course because the rendering infrastructure is
+ // tied to the top-level widget that initializes upon expose, but
+ // it has to be supported.
+ d->offscreenRenderer.setConfig(d->config);
+ // no window passed in, so no swapchain, but we get a functional QRhi which we own
+ d->offscreenRenderer.create();
+ d->rhi = d->offscreenRenderer.rhi();
+ if (!d->rhi) {
+ qWarning("QRhiWidget: Failed to create dedicated QRhi for grabbing");
+ emit renderFailed();
+ return QImage();
+ }
+ }
+
+ QRhiCommandBuffer *cb = nullptr;
+ if (d->rhi->beginOffscreenFrame(&cb) != QRhi::FrameOpSuccess)
+ return QImage();
+
+ QRhiReadbackResult readResult;
+ bool readCompleted = false;
+ bool needsInit = false;
+ d->ensureTexture(&needsInit);
+
+ if (d->colorTexture || d->msaaColorBuffer) {
+ bool canRender = true;
+ if (needsInit)
+ canRender = d->invokeInitialize(cb);
+ if (canRender)
+ render(cb);
+
+ QRhiResourceUpdateBatch *readbackBatch = d->rhi->nextResourceUpdateBatch();
+ readResult.completed = [&readCompleted] { readCompleted = true; };
+ readbackBatch->readBackTexture(d->resolveTexture ? d->resolveTexture : d->colorTexture, &readResult);
+ cb->resourceUpdate(readbackBatch);
+ }
+
+ d->rhi->endOffscreenFrame();
+
+ if (readCompleted) {
+ QImage::Format imageFormat = QImage::Format_RGBA8888;
+ switch (d->widgetTextureFormat) {
+ case TextureFormat::RGBA8:
+ break;
+ case TextureFormat::RGBA16F:
+ imageFormat = QImage::Format_RGBA16FPx4;
+ break;
+ case TextureFormat::RGBA32F:
+ imageFormat = QImage::Format_RGBA32FPx4;
+ break;
+ case TextureFormat::RGB10A2:
+ imageFormat = QImage::Format_BGR30;
+ break;
+ }
+ QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ imageFormat);
+ QImage result;
+ if (d->rhi->isYUpInFramebuffer())
+ result = wrapperImage.mirrored();
+ else
+ result = wrapperImage.copy();
+ result.setDevicePixelRatio(devicePixelRatio());
+ return result;
+ } else {
+ Q_UNREACHABLE();
+ }
+
+ return QImage();
+}
+
+/*!
+ Called when the widget is initialized for the first time, when the
+ associated texture's size, format, or sample count changes, or when the
+ QRhi and texture change for any reason. The function is expected to
+ maintain (create if not yet created, adjust and rebuild if the size has
+ changed) the graphics resources used by the rendering code in render().
+
+ To query the QRhi, QRhiTexture, and other related objects, call rhi(),
+ colorTexture(), depthStencilBuffer(), and renderTarget().
+
+ When the widget size changes, the QRhi object, the color buffer texture,
+ and the depth stencil buffer objects are all the same instances (so the
+ getters return the same pointers) as before, but the color and
+ depth/stencil buffers will likely have been rebuilt, meaning the
+ \l{QRhiTexture::pixelSize()}{size} and the underlying native texture
+ resource may be different than in the last invocation.
+
+ Reimplementations should also be prepared that the QRhi object and the
+ color buffer texture may change between invocations of this function. One
+ special case where the objects will be different is when performing a
+ grab() with a widget that is not yet shown, and then making the
+ widget visible on-screen within a top-level widget. There the grab will
+ happen with a dedicated QRhi that is then replaced with the top-level
+ window's associated QRhi in subsequent initialize() and render()
+ invocations. Another, more common case is when the widget is reparented so
+ that it belongs to a new top-level window. In this case the QRhi and all
+ related resources managed by the QRhiWidget will be different instances
+ than before in the subsequent call to this function. Is is then important
+ that all existing QRhi resources previously created by the subclass are
+ destroyed because they belong to the previous QRhi that should not be used
+ by the widget anymore.
+
+ When \l autoRenderTarget is \c true, which is the default, a
+ depth-stencil QRhiRenderBuffer and a QRhiTextureRenderTarget associated
+ with colorTexture() (or msaaColorBuffer()) and the depth-stencil buffer are
+ created and managed automatically. Reimplementations of initialize() and
+ render() can query those objects via depthStencilBuffer() and
+ renderTarget(). When \l autoRenderTarget is set to \c false, these
+ objects are no longer created and managed automatically. Rather, it will be
+ up the the initialize() implementation to create buffers and set up the
+ render target as it sees fit. When manually managing additional color or
+ depth-stencil attachments for the render target, their size and sample
+ count must always follow the size and sample count of colorTexture() /
+ msaaColorBuffer(), otherwise rendering or 3D API validation errors may
+ occur.
+
+ The subclass-created graphics resources are expected to be released in the
+ destructor implementation of the subclass.
+
+ \a cb is the QRhiCommandBuffer for the current frame of the widget. The
+ function is called with a frame being recorded, but without an active
+ render pass. The command buffer is provided primarily to allow enqueuing
+ \l{QRhiCommandBuffer::resourceUpdate()}{resource updates} without deferring
+ to render().
+
+ \sa render()
+ */
+void QRhiWidget::initialize(QRhiCommandBuffer *cb)
+{
+ Q_UNUSED(cb);
+}
+
+/*!
+ Called when the widget contents (i.e. the contents of the texture) need
+ updating.
+
+ There is always at least one call to initialize() before this function is
+ called.
+
+ To request updates, call QWidget::update(). Calling update() from within
+ render() will lead to updating continuously, throttled by vsync.
+
+ \a cb is the QRhiCommandBuffer for the current frame of the widget. The
+ function is called with a frame being recorded, but without an active
+ render pass.
+
+ \sa initialize()
+ */
+void QRhiWidget::render(QRhiCommandBuffer *cb)
+{
+ Q_UNUSED(cb);
+}
+
+/*!
+ Called when the need to early-release the graphics resources arises.
+
+ This normally does not happen for a QRhiWidget that is added to a top-level
+ widget's child hierarchy and it then stays there for the rest of its and
+ the top-level's lifetime. Thus in many cases there is no need to
+ reimplement this function, e.g. because the application only ever has a
+ single top-level widget (native window). However, when reparenting of the
+ widget (or an ancestor of it) is involved, reimplementing this function
+ will become necessary in robust, well-written QRhiWidget subclasses.
+
+ When this function is called, the implementation is expected to destroy all
+ QRhi resources (QRhiBuffer, QRhiTexture, etc. objects), similarly to how it
+ is expected to do this in the destructor. Nulling out, using a smart
+ pointer, or setting a \c{resources-invalid} flag is going to be required as
+ well, because initialize() will eventually get called afterwards. Note
+ however that deferring the releasing of resources to the subsequent
+ initialize() is wrong. If this function is called, the resource must be
+ dropped before returning. Also note that implementing this function does
+ not replace the class destructor (or smart pointers): the graphics
+ resources must still be released in both.
+
+ See the \l{Cube RHI Widget Example} for an example of this in action. There
+ the button that toggles the QRhiWidget between being a child widget (due to
+ having a parent widget) and being a top-level widget (due to having no
+ parent widget), will trigger invoking this function since the associated
+ top-level widget, native window, and QRhi all change during the lifetime of
+ the QRhiWidget, with the previously used QRhi getting destroyed which
+ implies an early-release of the associated resources managed by the
+ still-alive QRhiWidget.
+
+ Another case when this function is called is when grab() is used
+ with a QRhiWidget that is not added to a visible window, i.e. the rendering
+ is performed offscreen. If later on this QRhiWidget is made visible, or
+ added to a visible widget hierarchy, the associated QRhi will change from
+ the temporary one used for offscreen rendering to the window's dedicated
+ one, thus triggering this function as well.
+
+ \sa initialize()
+ */
+void QRhiWidget::releaseResources()
+{
+}
+
+/*!
+ \return the current QRhi object.
+
+ Must only be called from initialize() and render().
+ */
+QRhi *QRhiWidget::rhi() const
+{
+ Q_D(const QRhiWidget);
+ return d->rhi;
+}
+
+/*!
+ \return the texture serving as the color buffer for the widget.
+
+ Must only be called from initialize() and render().
+
+ Unlike the depth-stencil buffer and the QRhiRenderTarget, this texture is
+ always available and is managed by the QRhiWidget, independent of the value
+ of \l autoRenderTarget.
+
+ \note When \l sampleCount is larger than 1, and so multisample antialiasing
+ is enabled, the return value is \nullptr. Instead, query the
+ \l QRhiRenderBuffer by calling msaaColorBuffer().
+
+ \note The backing texture size and sample count can also be queried via the
+ QRhiRenderTarget returned from renderTarget(). This can be more convenient
+ and compact than querying from the QRhiTexture or QRhiRenderBuffer, because
+ it works regardless of multisampling is in use or not.
+
+ \sa msaaColorBuffer(), depthStencilBuffer(), renderTarget(), resolveTexture()
+ */
+QRhiTexture *QRhiWidget::colorTexture() const
+{
+ Q_D(const QRhiWidget);
+ return d->colorTexture;
+}
+
+/*!
+ \return the renderbuffer serving as the multisample color buffer for the widget.
+
+ Must only be called from initialize() and render().
+
+ When \l sampleCount is larger than 1, and so multisample antialising is
+ enabled, the returned QRhiRenderBuffer has a matching sample count and
+ serves as the color buffer. Graphics pipelines used to render into this
+ buffer must be created with the same sample count, and the depth-stencil
+ buffer's sample count must match as well. The multisample content is
+ expected to be resolved into the texture returned from resolveTexture().
+ When \l autoRenderTarget is
+ \c true, renderTarget() is set up automatically to do this, by setting up
+ msaaColorBuffer() as the \l{QRhiColorAttachment::renderBuffer()}{renderbuffer} of
+ color attachment 0 and resolveTexture() as its
+ \l{QRhiColorAttachment::resolveTexture()}{resolveTexture}.
+
+ When MSAA is not in use, the return value is \nullptr. Use colorTexture()
+ instead then.
+
+ Depending on the underlying 3D graphics API, there may be no practical
+ difference between multisample textures and color renderbuffers with a
+ sample count larger than 1 (QRhi may just map both to the same native
+ resource type). Some older APIs however may differentiate between textures
+ and renderbuffers. In order to support OpenGL ES 3.0, where multisample
+ renderbuffers are available, but multisample textures are not, QRhiWidget
+ always performs MSAA by using a multisample QRhiRenderBuffer as the color
+ attachment (and never a multisample QRhiTexture).
+
+ \note The backing texture size and sample count can also be queried via the
+ QRhiRenderTarget returned from renderTarget(). This can be more convenient
+ and compact than querying from the QRhiTexture or QRhiRenderBuffer, because
+ it works regardless of multisampling is in use or not.
+
+ \sa colorTexture(), depthStencilBuffer(), renderTarget(), resolveTexture()
+ */
+QRhiRenderBuffer *QRhiWidget::msaaColorBuffer() const
+{
+ Q_D(const QRhiWidget);
+ return d->msaaColorBuffer;
+}
+
+/*!
+ \return the non-multisample texture to which the multisample content is resolved.
+
+ The result is \nullptr when multisample antialiasing is not enabled.
+
+ Must only be called from initialize() and render().
+
+ With MSAA enabled, this is the texture that gets composited with the rest
+ of the QWidget content on-screen. However, the QRhiWidget's rendering must
+ target the (multisample) QRhiRenderBuffer returned from
+ msaaColorBuffer(). When
+ \l autoRenderTarget is \c true, this is taken care of by the
+ QRhiRenderTarget returned from renderTarget(). Otherwise, it is up to the
+ subclass code to correctly configure a render target object with both the
+ color buffer and resolve textures.
+
+ \sa colorTexture()
+ */
+QRhiTexture *QRhiWidget::resolveTexture() const
+{
+ Q_D(const QRhiWidget);
+ return d->resolveTexture;
+}
+
+/*!
+ \return the depth-stencil buffer used by the widget's rendering.
+
+ Must only be called from initialize() and render().
+
+ Available only when \l autoRenderTarget is \c true. Otherwise the
+ returned value is \nullptr and it is up the reimplementation of
+ initialize() to create and manage a depth-stencil buffer and a
+ QRhiTextureRenderTarget.
+
+ \sa colorTexture(), renderTarget()
+ */
+QRhiRenderBuffer *QRhiWidget::depthStencilBuffer() const
+{
+ Q_D(const QRhiWidget);
+ return d->depthStencilBuffer;
+}
+
+/*!
+ \return the render target object that must be used with
+ \l QRhiCommandBuffer::beginPass() in reimplementations of render().
+
+ Must only be called from initialize() and render().
+
+ Available only when \l autoRenderTarget is \c true. Otherwise the
+ returned value is \nullptr and it is up the reimplementation of
+ initialize() to create and manage a depth-stencil buffer and a
+ QRhiTextureRenderTarget.
+
+ When creating \l{QRhiGraphicsPipeline}{graphics pipelines}, a
+ QRhiRenderPassDescriptor is needed. This can be queried from the returned
+ QRhiTextureRenderTarget by calling
+ \l{QRhiTextureRenderTarget::renderPassDescriptor()}{renderPassDescriptor()}.
+
+ \sa colorTexture(), depthStencilBuffer()
+ */
+QRhiTextureRenderTarget *QRhiWidget::renderTarget() const
+{
+ Q_D(const QRhiWidget);
+ return d->renderTarget;
+}
+
+/*!
+ \fn void QRhiWidget::framePresented()
+
+ This signal is emitted after the widget's top-level window has finished
+ composition and has \l{QRhi::endFrame()}{submitted a frame}.
+*/
+
+/*!
+ \fn void QRhiWidget::renderFailed()
+
+ This signal is emitted whenever the widget is supposed to render to its
+ backing texture (either due to a \l{QWidget::update()}{widget update} or
+ due to a call to grab()), but there is no \l QRhi for the widget to
+ use, likely due to issues related to graphics configuration.
+
+ This signal may be emitted multiple times when a problem arises. Do not
+ assume it is emitted only once. Connect with Qt::SingleShotConnection if
+ the error handling code is to be notified only once.
+*/
+
+QT_END_NAMESPACE
diff --git a/src/widgets/kernel/qrhiwidget.h b/src/widgets/kernel/qrhiwidget.h
new file mode 100644
index 0000000000..592c35e737
--- /dev/null
+++ b/src/widgets/kernel/qrhiwidget.h
@@ -0,0 +1,102 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef QRHIWIDGET_H
+#define QRHIWIDGET_H
+
+#include <QtWidgets/qwidget.h>
+
+QT_BEGIN_NAMESPACE
+
+class QRhiWidgetPrivate;
+class QRhi;
+class QRhiTexture;
+class QRhiRenderBuffer;
+class QRhiTextureRenderTarget;
+class QRhiCommandBuffer;
+
+class Q_WIDGETS_EXPORT QRhiWidget : public QWidget
+{
+ Q_OBJECT
+ Q_DECLARE_PRIVATE(QRhiWidget)
+ Q_PROPERTY(int sampleCount READ sampleCount WRITE setSampleCount NOTIFY sampleCountChanged)
+ Q_PROPERTY(TextureFormat textureFormat READ textureFormat WRITE setTextureFormat NOTIFY textureFormatChanged)
+ Q_PROPERTY(bool autoRenderTarget READ isAutoRenderTargetEnabled WRITE setAutoRenderTarget NOTIFY autoRenderTargetChanged)
+ Q_PROPERTY(QSize explicitSize READ explicitSize WRITE setExplicitSize NOTIFY explicitSizeChanged)
+ Q_PROPERTY(bool mirrorVertically READ isMirrorVerticallyEnabled WRITE setMirrorVertically NOTIFY mirrorVerticallyChanged)
+
+public:
+ QRhiWidget(QWidget *parent = nullptr, Qt::WindowFlags f = {});
+ ~QRhiWidget();
+
+ enum class Api {
+ OpenGL,
+ Metal,
+ Vulkan,
+ D3D11,
+ D3D12,
+ Null
+ };
+ Q_ENUM(Api)
+
+ enum class TextureFormat {
+ RGBA8,
+ RGBA16F,
+ RGBA32F,
+ RGB10A2
+ };
+ Q_ENUM(TextureFormat)
+
+ Api api() const;
+ void setApi(Api api);
+
+ bool isDebugLayerEnabled() const;
+ void setDebugLayer(bool enable);
+
+ int sampleCount() const;
+ void setSampleCount(int samples);
+
+ TextureFormat textureFormat() const;
+ void setTextureFormat(TextureFormat format);
+
+ QSize explicitSize() const;
+ void setExplicitSize(const QSize &pixelSize);
+ void setExplicitSize(int w, int h) { setExplicitSize(QSize(w, h)); }
+
+ bool isAutoRenderTargetEnabled() const;
+ void setAutoRenderTarget(bool enabled);
+
+ bool isMirrorVerticallyEnabled() const;
+ void setMirrorVertically(bool enabled);
+
+ QImage grab();
+
+ virtual void initialize(QRhiCommandBuffer *cb);
+ virtual void render(QRhiCommandBuffer *cb);
+ virtual void releaseResources();
+
+ QRhi *rhi() const;
+ QRhiTexture *colorTexture() const;
+ QRhiRenderBuffer *msaaColorBuffer() const;
+ QRhiTexture *resolveTexture() const;
+ QRhiRenderBuffer *depthStencilBuffer() const;
+ QRhiTextureRenderTarget *renderTarget() const;
+
+Q_SIGNALS:
+ void frameSubmitted();
+ void renderFailed();
+ void sampleCountChanged(int samples);
+ void textureFormatChanged(TextureFormat format);
+ void autoRenderTargetChanged(bool enabled);
+ void explicitSizeChanged(const QSize &pixelSize);
+ void mirrorVerticallyChanged(bool enabled);
+
+protected:
+ void resizeEvent(QResizeEvent *e) override;
+ void paintEvent(QPaintEvent *e) override;
+ bool event(QEvent *e) override;
+};
+
+QT_END_NAMESPACE
+
+#endif
diff --git a/src/widgets/kernel/qrhiwidget_p.h b/src/widgets/kernel/qrhiwidget_p.h
new file mode 100644
index 0000000000..cc3ab26861
--- /dev/null
+++ b/src/widgets/kernel/qrhiwidget_p.h
@@ -0,0 +1,63 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+#ifndef QRHIWIDGET_P_H
+#define QRHIWIDGET_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include "qrhiwidget.h"
+#include <rhi/qrhi.h>
+#include <private/qwidget_p.h>
+#include <private/qbackingstorerhisupport_p.h>
+
+QT_BEGIN_NAMESPACE
+
+class QRhiWidgetPrivate : public QWidgetPrivate
+{
+ Q_DECLARE_PUBLIC(QRhiWidget)
+public:
+ TextureData texture() const override;
+ QPlatformTextureList::Flags textureListFlags() override;
+ QPlatformBackingStoreRhiConfig rhiConfig() const override;
+ void endCompose() override;
+
+ void ensureRhi();
+ void ensureTexture(bool *changed);
+ bool invokeInitialize(QRhiCommandBuffer *cb);
+ void resetColorBufferObjects();
+ void resetRenderTargetObjects();
+ void releaseResources();
+
+ QRhi *rhi = nullptr;
+ bool noSize = false;
+ QPlatformBackingStoreRhiConfig config;
+ QRhiWidget::TextureFormat widgetTextureFormat = QRhiWidget::TextureFormat::RGBA8;
+ QRhiTexture::Format rhiTextureFormat = QRhiTexture::RGBA8;
+ int samples = 1;
+ QSize explicitSize;
+ bool autoRenderTarget = true;
+ bool mirrorVertically = false;
+ QBackingStoreRhiSupport offscreenRenderer;
+ bool textureInvalid = false;
+ QRhiTexture *colorTexture = nullptr;
+ QRhiRenderBuffer *msaaColorBuffer = nullptr;
+ QRhiTexture *resolveTexture = nullptr;
+ QRhiRenderBuffer *depthStencilBuffer = nullptr;
+ QRhiTextureRenderTarget *renderTarget = nullptr;
+ QRhiRenderPassDescriptor *renderPassDescriptor = nullptr;
+ mutable QVector<QRhiResource *> pendingDeletes;
+};
+
+QT_END_NAMESPACE
+
+#endif
diff --git a/src/widgets/kernel/qwidget.cpp b/src/widgets/kernel/qwidget.cpp
index f893dcda3d..da7fae2af8 100644
--- a/src/widgets/kernel/qwidget.cpp
+++ b/src/widgets/kernel/qwidget.cpp
@@ -10858,9 +10858,10 @@ void QWidget::setParent(QWidget *parent, Qt::WindowFlags f)
// do it on newtlw instead, the performance implications of that are
// problematic when it comes to large widget trees.
if (q_evaluateRhiConfig(this, nullptr, &surfaceType)) {
+ const bool wasUsingRhiFlush = newtlw->d_func()->usesRhiFlush;
newtlw->d_func()->usesRhiFlush = true;
if (QWindow *w = newtlw->windowHandle()) {
- if (w->surfaceType() != surfaceType) {
+ if (w->surfaceType() != surfaceType || !wasUsingRhiFlush) {
newtlw->destroy();
newtlw->create();
}
diff --git a/tests/auto/widgets/widgets/CMakeLists.txt b/tests/auto/widgets/widgets/CMakeLists.txt
index 8fe11d09ac..c6c940a40c 100644
--- a/tests/auto/widgets/widgets/CMakeLists.txt
+++ b/tests/auto/widgets/widgets/CMakeLists.txt
@@ -58,3 +58,4 @@ endif()
if(QT_FEATURE_opengl)
add_subdirectory(qopenglwidget)
endif()
+add_subdirectory(qrhiwidget)
diff --git a/tests/auto/widgets/widgets/qrhiwidget/CMakeLists.txt b/tests/auto/widgets/widgets/qrhiwidget/CMakeLists.txt
new file mode 100644
index 0000000000..f8d18bcf53
--- /dev/null
+++ b/tests/auto/widgets/widgets/qrhiwidget/CMakeLists.txt
@@ -0,0 +1,25 @@
+# Copyright (C) 2023 The Qt Company Ltd.
+# SPDX-License-Identifier: BSD-3-Clause
+
+if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
+ cmake_minimum_required(VERSION 3.16)
+ project(tst_qrhiwidget LANGUAGES CXX)
+ find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
+endif()
+
+file(GLOB_RECURSE qrhiwidget_resource_files
+ RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}"
+ data/*
+)
+
+qt_internal_add_test(tst_qrhiwidget
+ SOURCES
+ tst_qrhiwidget.cpp
+ LIBRARIES
+ Qt::CorePrivate
+ Qt::Gui
+ Qt::GuiPrivate
+ Qt::Widgets
+ TESTDATA ${qrhiwidget_resource_files}
+ BUILTIN_TESTDATA
+)
diff --git a/tests/auto/widgets/widgets/qrhiwidget/data/simple.frag b/tests/auto/widgets/widgets/qrhiwidget/data/simple.frag
new file mode 100644
index 0000000000..2aa500e09a
--- /dev/null
+++ b/tests/auto/widgets/widgets/qrhiwidget/data/simple.frag
@@ -0,0 +1,8 @@
+#version 440
+
+layout(location = 0) out vec4 fragColor;
+
+void main()
+{
+ fragColor = vec4(1.0, 0.0, 0.0, 1.0);
+}
diff --git a/tests/auto/widgets/widgets/qrhiwidget/data/simple.frag.qsb b/tests/auto/widgets/widgets/qrhiwidget/data/simple.frag.qsb
new file mode 100644
index 0000000000..40d0a296ac
--- /dev/null
+++ b/tests/auto/widgets/widgets/qrhiwidget/data/simple.frag.qsb
Binary files differ
diff --git a/tests/auto/widgets/widgets/qrhiwidget/data/simple.vert b/tests/auto/widgets/widgets/qrhiwidget/data/simple.vert
new file mode 100644
index 0000000000..6b954cdaec
--- /dev/null
+++ b/tests/auto/widgets/widgets/qrhiwidget/data/simple.vert
@@ -0,0 +1,8 @@
+#version 440
+
+layout(location = 0) in vec4 position;
+
+void main()
+{
+ gl_Position = position;
+}
diff --git a/tests/auto/widgets/widgets/qrhiwidget/data/simple.vert.qsb b/tests/auto/widgets/widgets/qrhiwidget/data/simple.vert.qsb
new file mode 100644
index 0000000000..5b7fd39668
--- /dev/null
+++ b/tests/auto/widgets/widgets/qrhiwidget/data/simple.vert.qsb
Binary files differ
diff --git a/tests/auto/widgets/widgets/qrhiwidget/tst_qrhiwidget.cpp b/tests/auto/widgets/widgets/qrhiwidget/tst_qrhiwidget.cpp
new file mode 100644
index 0000000000..9593b282d9
--- /dev/null
+++ b/tests/auto/widgets/widgets/qrhiwidget/tst_qrhiwidget.cpp
@@ -0,0 +1,792 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include <QtWidgets/QRhiWidget>
+#include <QtGui/QPainter>
+#include <QTest>
+#include <QSignalSpy>
+#include <private/qguiapplication_p.h>
+#include <qpa/qplatformintegration.h>
+#include <rhi/qrhi.h>
+
+#include <QApplication>
+#include <QFile>
+#include <QVBoxLayout>
+
+#if QT_CONFIG(vulkan)
+#include <private/qvulkandefaultinstance_p.h>
+#endif
+
+class tst_QRhiWidget : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void initTestCase();
+ void create_data();
+ void create();
+ void noCreate();
+ void simple_data();
+ void simple();
+ void msaa_data();
+ void msaa();
+ void explicitSize_data();
+ void explicitSize();
+ void autoRt_data();
+ void autoRt();
+ void reparent_data();
+ void reparent();
+ void grab_data();
+ void grab();
+ void mirror_data();
+ void mirror();
+
+private:
+ void testData();
+};
+
+void tst_QRhiWidget::initTestCase()
+{
+ if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::RhiBasedRendering))
+ QSKIP("RhiBasedRendering capability is reported as unsupported on this platform.");
+
+ qputenv("QT_RHI_LEAK_CHECK", "1");
+}
+
+void tst_QRhiWidget::testData()
+{
+ QTest::addColumn<QRhiWidget::Api>("api");
+
+#ifndef Q_OS_WEBOS
+ QTest::newRow("Null") << QRhiWidget::Api::Null;
+#endif
+
+#if QT_CONFIG(opengl)
+ if (QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::OpenGL))
+ QTest::newRow("OpenGL") << QRhiWidget::Api::OpenGL;
+#endif
+
+#if QT_CONFIG(vulkan)
+ // Have to probe to be sure Vulkan is actually working (the test cases
+ // themselves will assume QRhi init succeeds).
+ if (QVulkanDefaultInstance::instance()) {
+ QRhiVulkanInitParams vulkanInitParams;
+ vulkanInitParams.inst = QVulkanDefaultInstance::instance();
+ if (QRhi::probe(QRhi::Vulkan, &vulkanInitParams))
+ QTest::newRow("Vulkan") << QRhiWidget::Api::Vulkan;
+ }
+#endif
+
+#if defined(Q_OS_MACOS) || defined(Q_OS_IOS)
+ QRhiMetalInitParams metalInitParams;
+ if (QRhi::probe(QRhi::Metal, &metalInitParams))
+ QTest::newRow("Metal") << QRhiWidget::Api::Metal;
+#endif
+
+#ifdef Q_OS_WIN
+ QTest::newRow("D3D11") << QRhiWidget::Api::D3D11;
+ // D3D12 needs to be probed too due to being disabled if the SDK headers
+ // are too old (clang, mingw).
+ QRhiD3D12InitParams d3d12InitParams;
+ if (QRhi::probe(QRhi::D3D12, &d3d12InitParams))
+ QTest::newRow("D3D12") << QRhiWidget::Api::D3D12;
+#endif
+}
+
+void tst_QRhiWidget::create_data()
+{
+ testData();
+}
+
+void tst_QRhiWidget::create()
+{
+ QFETCH(QRhiWidget::Api, api);
+
+ {
+ QRhiWidget w;
+ w.setApi(api);
+ w.resize(320, 240);
+ w.show();
+ QVERIFY(QTest::qWaitForWindowExposed(&w));
+ }
+
+ {
+ QWidget topLevel;
+ topLevel.resize(320, 240);
+ QRhiWidget *w = new QRhiWidget(&topLevel);
+ w->setApi(api);
+ w->resize(100, 100);
+ topLevel.show();
+ QVERIFY(QTest::qWaitForWindowExposed(&topLevel));
+ }
+}
+
+void tst_QRhiWidget::noCreate()
+{
+ // Now try something that is guaranteed to fail.
+ // E.g. try using Metal on Windows.
+ // The error signal should be emitted. The frame signal should not.
+#ifdef Q_OS_WIN
+ qDebug("Warnings will be printed below, this is as expected");
+ QRhiWidget rhiWidget;
+ rhiWidget.setApi(QRhiWidget::Api::Metal);
+ QSignalSpy frameSpy(&rhiWidget, &QRhiWidget::frameSubmitted);
+ QSignalSpy errorSpy(&rhiWidget, &QRhiWidget::renderFailed);
+ rhiWidget.resize(320, 240);
+ rhiWidget.show();
+ QVERIFY(QTest::qWaitForWindowExposed(&rhiWidget));
+ QTRY_VERIFY(errorSpy.count() > 0);
+ QCOMPARE(frameSpy.count(), 0);
+#endif
+}
+
+static QShader getShader(const QString &name)
+{
+ QFile f(name);
+ return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader();
+}
+
+static bool submitResourceUpdates(QRhi *rhi, QRhiResourceUpdateBatch *batch)
+{
+ QRhiCommandBuffer *cb = nullptr;
+ QRhi::FrameOpResult result = rhi->beginOffscreenFrame(&cb);
+ if (result != QRhi::FrameOpSuccess) {
+ qWarning("beginOffscreenFrame returned %d", result);
+ return false;
+ }
+ if (!cb) {
+ qWarning("No command buffer from beginOffscreenFrame");
+ return false;
+ }
+ cb->resourceUpdate(batch);
+ rhi->endOffscreenFrame();
+ return true;
+}
+
+inline bool imageRGBAEquals(const QImage &a, const QImage &b, int maxFuzz = 1)
+{
+ if (a.size() != b.size())
+ return false;
+
+ const QImage image0 = a.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
+ const QImage image1 = b.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
+
+ const int width = image0.width();
+ const int height = image0.height();
+ for (int y = 0; y < height; ++y) {
+ const quint32 *p0 = reinterpret_cast<const quint32 *>(image0.constScanLine(y));
+ const quint32 *p1 = reinterpret_cast<const quint32 *>(image1.constScanLine(y));
+ int x = width - 1;
+ while (x-- >= 0) {
+ const QRgb c0(*p0++);
+ const QRgb c1(*p1++);
+ const int red = qAbs(qRed(c0) - qRed(c1));
+ const int green = qAbs(qGreen(c0) - qGreen(c1));
+ const int blue = qAbs(qBlue(c0) - qBlue(c1));
+ const int alpha = qAbs(qAlpha(c0) - qAlpha(c1));
+ if (red > maxFuzz || green > maxFuzz || blue > maxFuzz || alpha > maxFuzz)
+ return false;
+ }
+ }
+
+ return true;
+}
+
+class SimpleRhiWidget : public QRhiWidget
+{
+public:
+ SimpleRhiWidget(int sampleCount = 1, QWidget *parent = nullptr)
+ : QRhiWidget(parent),
+ m_sampleCount(sampleCount)
+ { }
+
+ ~SimpleRhiWidget()
+ {
+ delete m_rt;
+ delete m_rp;
+ }
+
+ void initialize(QRhiCommandBuffer *cb) override;
+ void render(QRhiCommandBuffer *cb) override;
+ void releaseResources() override;
+
+ int m_sampleCount;
+ QRhi *m_rhi = nullptr;
+ std::unique_ptr<QRhiBuffer> m_vbuf;
+ std::unique_ptr<QRhiBuffer> m_ubuf;
+ std::unique_ptr<QRhiShaderResourceBindings> m_srb;
+ std::unique_ptr<QRhiGraphicsPipeline> m_pipeline;
+ QRhiTextureRenderTarget *m_rt = nullptr; // used when autoRenderTarget is off
+ QRhiRenderPassDescriptor *m_rp = nullptr; // used when autoRenderTarget is off
+};
+
+void SimpleRhiWidget::initialize(QRhiCommandBuffer *cb)
+{
+ if (m_rhi != rhi()) {
+ m_pipeline.reset();
+ m_rhi = rhi();
+ }
+
+ if (!m_pipeline) {
+ if (!isAutoRenderTargetEnabled()) {
+ delete m_rt;
+ delete m_rp;
+ QRhiTextureRenderTargetDescription rtDesc;
+ if (colorTexture()) {
+ rtDesc.setColorAttachments({ colorTexture() });
+ } else if (msaaColorBuffer()) {
+ QRhiColorAttachment att;
+ att.setRenderBuffer(msaaColorBuffer());
+ rtDesc.setColorAttachments({ att });
+ }
+ m_rt = m_rhi->newTextureRenderTarget(rtDesc);
+ m_rp = m_rt->newCompatibleRenderPassDescriptor();
+ m_rt->setRenderPassDescriptor(m_rp);
+ m_rt->create();
+ }
+
+ static float vertexData[] = {
+ 0, 1,
+ -1, -1,
+ 1, -1
+ };
+
+ m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData)));
+ m_vbuf->create();
+
+ m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
+ m_ubuf->create();
+
+ m_srb.reset(m_rhi->newShaderResourceBindings());
+ m_srb->create();
+
+ m_pipeline.reset(m_rhi->newGraphicsPipeline());
+ m_pipeline->setShaderStages({
+ { QRhiShaderStage::Vertex, getShader(QLatin1String(":/data/simple.vert.qsb")) },
+ { QRhiShaderStage::Fragment, getShader(QLatin1String(":/data/simple.frag.qsb")) }
+ });
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 2 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float2, 0 }
+ });
+ m_pipeline->setSampleCount(m_sampleCount);
+ m_pipeline->setVertexInputLayout(inputLayout);
+ m_pipeline->setShaderResourceBindings(m_srb.get());
+ m_pipeline->setRenderPassDescriptor(renderTarget() ? renderTarget()->renderPassDescriptor() : m_rp);
+ m_pipeline->create();
+
+ QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch();
+ resourceUpdates->uploadStaticBuffer(m_vbuf.get(), vertexData);
+ cb->resourceUpdate(resourceUpdates);
+ }
+}
+
+void SimpleRhiWidget::render(QRhiCommandBuffer *cb)
+{
+ const QSize outputSize = colorTexture() ? colorTexture()->pixelSize() : msaaColorBuffer()->pixelSize();
+ if (renderTarget()) {
+ QCOMPARE(outputSize, renderTarget()->pixelSize());
+ if (rhi()->backend() != QRhi::Null && rhi()->supportedSampleCounts().contains(m_sampleCount))
+ QCOMPARE(m_sampleCount, renderTarget()->sampleCount());
+ }
+
+ const QColor clearColor = QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f);
+ cb->beginPass(renderTarget() ? renderTarget() : m_rt, clearColor, { 1.0f, 0 });
+ cb->setGraphicsPipeline(m_pipeline.get());
+ cb->setViewport(QRhiViewport(0, 0, outputSize.width(), outputSize.height()));
+ cb->setShaderResources();
+ const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0);
+ cb->setVertexInput(0, 1, &vbufBinding);
+ cb->draw(3);
+ cb->endPass();
+}
+
+void SimpleRhiWidget::releaseResources()
+{
+ m_pipeline.reset();
+ m_srb.reset();
+ m_ubuf.reset();
+ m_vbuf.reset();
+
+}
+
+void tst_QRhiWidget::simple_data()
+{
+ testData();
+}
+
+void tst_QRhiWidget::simple()
+{
+ QFETCH(QRhiWidget::Api, api);
+
+ SimpleRhiWidget *rhiWidget = new SimpleRhiWidget;
+ rhiWidget->setApi(api);
+ QSignalSpy frameSpy(rhiWidget, &QRhiWidget::frameSubmitted);
+ QSignalSpy errorSpy(rhiWidget, &QRhiWidget::renderFailed);
+
+ QVBoxLayout *layout = new QVBoxLayout;
+ layout->addWidget(rhiWidget);
+
+ QWidget w;
+ w.setLayout(layout);
+ w.resize(1280, 720);
+ w.show();
+ QVERIFY(QTest::qWaitForWindowExposed(&w));
+
+ QTRY_VERIFY(frameSpy.count() > 0);
+ QCOMPARE(errorSpy.count(), 0);
+
+ QCOMPARE(rhiWidget->sampleCount(), 1);
+ QCOMPARE(rhiWidget->textureFormat(), QRhiWidget::TextureFormat::RGBA8);
+ QVERIFY(rhiWidget->isAutoRenderTargetEnabled());
+
+ // Pull out the QRhiTexture (we know colorTexture() and rhi() and friends
+ // are all there even outside initialize() and render(), even though this
+ // is not quite documented), and read it back.
+ QRhiTexture *backingTexture = rhiWidget->colorTexture();
+ QVERIFY(backingTexture);
+ QCOMPARE(backingTexture->format(), QRhiTexture::RGBA8);
+ QVERIFY(rhiWidget->depthStencilBuffer());
+ QVERIFY(rhiWidget->renderTarget());
+ QVERIFY(!rhiWidget->resolveTexture());
+ QRhi *rhi = rhiWidget->rhi();
+ QVERIFY(rhi);
+
+ switch (api) {
+ case QRhiWidget::Api::OpenGL:
+ QCOMPARE(rhi->backend(), QRhi::OpenGLES2);
+ break;
+ case QRhiWidget::Api::Metal:
+ QCOMPARE(rhi->backend(), QRhi::Metal);
+ break;
+ case QRhiWidget::Api::Vulkan:
+ QCOMPARE(rhi->backend(), QRhi::Vulkan);
+ break;
+ case QRhiWidget::Api::D3D11:
+ QCOMPARE(rhi->backend(), QRhi::D3D11);
+ break;
+ case QRhiWidget::Api::D3D12:
+ QCOMPARE(rhi->backend(), QRhi::D3D12);
+ break;
+ case QRhiWidget::Api::Null:
+ QCOMPARE(rhi->backend(), QRhi::Null);
+ break;
+ default:
+ break;
+ }
+
+ const int maxFuzz = 1;
+ QImage resultOne;
+ if (rhi->backend() != QRhi::Null) {
+ QRhiReadbackResult readResult;
+ bool readCompleted = false;
+ readResult.completed = [&readCompleted] { readCompleted = true; };
+ QRhiResourceUpdateBatch *rub = rhi->nextResourceUpdateBatch();
+ rub->readBackTexture(backingTexture, &readResult);
+ QVERIFY(submitResourceUpdates(rhi, rub));
+ QVERIFY(readCompleted);
+
+ QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888);
+ if (rhi->isYUpInFramebuffer())
+ resultOne = wrapperImage.mirrored();
+ else
+ resultOne = wrapperImage.copy();
+
+ // result is now a red triangle upon greenish background, where the
+ // triangle's edges are (0, 1), (-1, -1), and (1, -1).
+ // It's upside down with Vulkan (Y is not corrected, clipSpaceCorrMatrix() is not used),
+ // but that won't matter for the test.
+
+ // Check that the center is a red pixel.
+ QRgb c = resultOne.pixel(resultOne.width() / 2, resultOne.height() / 2);
+ QVERIFY(qRed(c) >= 255 - maxFuzz);
+ QVERIFY(qGreen(c) <= maxFuzz);
+ QVERIFY(qBlue(c) <= maxFuzz);
+ }
+
+ // Now through grab().
+ QImage resultTwo;
+ if (rhi->backend() != QRhi::Null) {
+ resultTwo = rhiWidget->grab();
+ QCOMPARE(errorSpy.count(), 0);
+ QVERIFY(!resultTwo.isNull());
+ QRgb c = resultTwo.pixel(resultTwo.width() / 2, resultTwo.height() / 2);
+ QVERIFY(qRed(c) >= 255 - maxFuzz);
+ QVERIFY(qGreen(c) <= maxFuzz);
+ QVERIFY(qBlue(c) <= maxFuzz);
+ }
+
+ // Check we got the same result from our manual readback and when the
+ // texture was rendered to again and grab() was called.
+ QVERIFY(imageRGBAEquals(resultOne, resultTwo, maxFuzz));
+}
+
+void tst_QRhiWidget::msaa_data()
+{
+ testData();
+}
+
+void tst_QRhiWidget::msaa()
+{
+ QFETCH(QRhiWidget::Api, api);
+
+ const int SAMPLE_COUNT = 4;
+ SimpleRhiWidget *rhiWidget = new SimpleRhiWidget(SAMPLE_COUNT);
+ rhiWidget->setApi(api);
+ rhiWidget->setSampleCount(SAMPLE_COUNT);
+ QSignalSpy frameSpy(rhiWidget, &QRhiWidget::frameSubmitted);
+ QSignalSpy errorSpy(rhiWidget, &QRhiWidget::renderFailed);
+
+ QVBoxLayout *layout = new QVBoxLayout;
+ layout->addWidget(rhiWidget);
+
+ QWidget w;
+ w.setLayout(layout);
+ w.resize(1280, 720);
+ w.show();
+ QVERIFY(QTest::qWaitForWindowExposed(&w));
+
+ QTRY_VERIFY(frameSpy.count() > 0);
+ QCOMPARE(errorSpy.count(), 0);
+
+ QCOMPARE(rhiWidget->sampleCount(), 4);
+ QCOMPARE(rhiWidget->textureFormat(), QRhiWidget::TextureFormat::RGBA8);
+ QVERIFY(!rhiWidget->colorTexture());
+ QVERIFY(rhiWidget->msaaColorBuffer());
+ QVERIFY(rhiWidget->depthStencilBuffer());
+ QVERIFY(rhiWidget->renderTarget());
+ QVERIFY(rhiWidget->resolveTexture());
+ QCOMPARE(rhiWidget->resolveTexture()->format(), QRhiTexture::RGBA8);
+ QRhi *rhi = rhiWidget->rhi();
+ QVERIFY(rhi);
+
+ if (rhi->backend() != QRhi::Null) {
+ QRhiReadbackResult readResult;
+ QRhiResourceUpdateBatch *rub = rhi->nextResourceUpdateBatch();
+ rub->readBackTexture(rhiWidget->resolveTexture(), &readResult);
+ QVERIFY(submitResourceUpdates(rhi, rub));
+
+ QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888);
+ QImage result;
+ if (rhi->isYUpInFramebuffer())
+ result = wrapperImage.mirrored();
+ else
+ result = wrapperImage.copy();
+
+ // Check that the center is a red pixel.
+ const int maxFuzz = 1;
+ QRgb c = result.pixel(result.width() / 2, result.height() / 2);
+ QVERIFY(qRed(c) >= 255 - maxFuzz);
+ QVERIFY(qGreen(c) <= maxFuzz);
+ QVERIFY(qBlue(c) <= maxFuzz);
+ }
+
+ // See if switching back and forth works.
+ frameSpy.clear();
+ rhiWidget->m_pipeline.reset();
+ rhiWidget->m_sampleCount = 1;
+ rhiWidget->setSampleCount(1);
+ QTRY_VERIFY(frameSpy.count() > 0);
+ QCOMPARE(errorSpy.count(), 0);
+ QVERIFY(rhiWidget->colorTexture());
+ QVERIFY(!rhiWidget->msaaColorBuffer());
+
+ frameSpy.clear();
+ rhiWidget->m_pipeline.reset();
+ rhiWidget->m_sampleCount = SAMPLE_COUNT;
+ rhiWidget->setSampleCount(SAMPLE_COUNT);
+ QTRY_VERIFY(frameSpy.count() > 0);
+ QCOMPARE(errorSpy.count(), 0);
+ QVERIFY(!rhiWidget->colorTexture());
+ QVERIFY(rhiWidget->msaaColorBuffer());
+}
+
+void tst_QRhiWidget::explicitSize_data()
+{
+ testData();
+}
+
+void tst_QRhiWidget::explicitSize()
+{
+ QFETCH(QRhiWidget::Api, api);
+
+ SimpleRhiWidget *rhiWidget = new SimpleRhiWidget;
+ rhiWidget->setApi(api);
+ QSignalSpy frameSpy(rhiWidget, &QRhiWidget::frameSubmitted);
+ QSignalSpy errorSpy(rhiWidget, &QRhiWidget::renderFailed);
+
+ QVBoxLayout *layout = new QVBoxLayout;
+ layout->addWidget(rhiWidget);
+
+ rhiWidget->setExplicitSize(QSize(320, 200));
+
+ QWidget w;
+ w.setLayout(layout);
+ w.resize(1280, 720);
+ w.show();
+ QVERIFY(QTest::qWaitForWindowExposed(&w));
+
+ QTRY_VERIFY(frameSpy.count() > 0);
+ QCOMPARE(errorSpy.count(), 0);
+
+ QVERIFY(rhiWidget->rhi());
+ QVERIFY(rhiWidget->colorTexture());
+ QCOMPARE(rhiWidget->colorTexture()->pixelSize(), QSize(320, 200));
+ QVERIFY(rhiWidget->depthStencilBuffer());
+ QCOMPARE(rhiWidget->depthStencilBuffer()->pixelSize(), QSize(320, 200));
+ QVERIFY(rhiWidget->renderTarget());
+ QVERIFY(!rhiWidget->resolveTexture());
+
+ frameSpy.clear();
+ rhiWidget->setExplicitSize(640, 480); // should also trigger update()
+ QTRY_VERIFY(frameSpy.count() > 0);
+
+ QVERIFY(rhiWidget->colorTexture());
+ QCOMPARE(rhiWidget->colorTexture()->pixelSize(), QSize(640, 480));
+ QVERIFY(rhiWidget->depthStencilBuffer());
+ QCOMPARE(rhiWidget->depthStencilBuffer()->pixelSize(), QSize(640, 480));
+
+ frameSpy.clear();
+ rhiWidget->setExplicitSize(QSize());
+ QTRY_VERIFY(frameSpy.count() > 0);
+
+ QVERIFY(rhiWidget->colorTexture());
+ QVERIFY(rhiWidget->colorTexture()->pixelSize() != QSize(640, 480));
+ QVERIFY(rhiWidget->depthStencilBuffer());
+ QVERIFY(rhiWidget->depthStencilBuffer()->pixelSize() != QSize(640, 480));
+}
+
+void tst_QRhiWidget::autoRt_data()
+{
+ testData();
+}
+
+void tst_QRhiWidget::autoRt()
+{
+ QFETCH(QRhiWidget::Api, api);
+
+ SimpleRhiWidget *rhiWidget = new SimpleRhiWidget;
+ rhiWidget->setApi(api);
+ QVERIFY(rhiWidget->isAutoRenderTargetEnabled());
+ rhiWidget->setAutoRenderTarget(false);
+ QVERIFY(!rhiWidget->isAutoRenderTargetEnabled());
+ QSignalSpy frameSpy(rhiWidget, &QRhiWidget::frameSubmitted);
+ QSignalSpy errorSpy(rhiWidget, &QRhiWidget::renderFailed);
+
+ QVBoxLayout *layout = new QVBoxLayout;
+ layout->addWidget(rhiWidget);
+
+ QWidget w;
+ w.setLayout(layout);
+ w.resize(1280, 720);
+ w.show();
+ QVERIFY(QTest::qWaitForWindowExposed(&w));
+
+ QTRY_VERIFY(frameSpy.count() > 0);
+ QCOMPARE(errorSpy.count(), 0);
+
+ QVERIFY(rhiWidget->rhi());
+ QVERIFY(rhiWidget->colorTexture());
+ QVERIFY(!rhiWidget->depthStencilBuffer());
+ QVERIFY(!rhiWidget->renderTarget());
+ QVERIFY(!rhiWidget->resolveTexture());
+
+ QVERIFY(rhiWidget->m_rt);
+ QVERIFY(rhiWidget->m_rp);
+ QCOMPARE(rhiWidget->m_rt->description().cbeginColorAttachments()->texture(), rhiWidget->colorTexture());
+
+ frameSpy.clear();
+ // do something that triggers creating a new backing texture
+ rhiWidget->setExplicitSize(QSize(320, 200));
+ QTRY_VERIFY(frameSpy.count() > 0);
+
+ QVERIFY(rhiWidget->colorTexture());
+ QCOMPARE(rhiWidget->m_rt->description().cbeginColorAttachments()->texture(), rhiWidget->colorTexture());
+}
+
+void tst_QRhiWidget::reparent_data()
+{
+ testData();
+}
+
+void tst_QRhiWidget::reparent()
+{
+ if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::MultipleWindows))
+ QSKIP("MultipleWindows capability is reported as unsupported, skipping reparenting test.");
+
+ QFETCH(QRhiWidget::Api, api);
+
+ QWidget *windowOne = new QWidget;
+ windowOne->resize(1280, 720);
+
+ SimpleRhiWidget *rhiWidget = new SimpleRhiWidget(1, windowOne);
+ rhiWidget->setApi(api);
+ rhiWidget->resize(800, 600);
+ QSignalSpy frameSpy(rhiWidget, &QRhiWidget::frameSubmitted);
+ QSignalSpy errorSpy(rhiWidget, &QRhiWidget::renderFailed);
+
+ windowOne->show();
+ QVERIFY(QTest::qWaitForWindowExposed(windowOne));
+ QTRY_VERIFY(frameSpy.count() > 0);
+ QCOMPARE(errorSpy.count(), 0);
+
+ frameSpy.clear();
+ QWidget windowTwo;
+ windowTwo.resize(1280, 720);
+
+ rhiWidget->setParent(&windowTwo);
+
+ // There's nothing saying the old top-level parent is going to be around,
+ // which is interesting wrt to its QRhi and resources created with that;
+ // exercise this.
+ delete windowOne;
+
+ windowTwo.show();
+ QVERIFY(QTest::qWaitForWindowExposed(&windowTwo));
+ QTRY_VERIFY(frameSpy.count() > 0);
+ QCOMPARE(errorSpy.count(), 0);
+
+ // now reparent after show() has already been called
+ frameSpy.clear();
+ QWidget windowThree;
+ windowThree.resize(1280, 720);
+ windowThree.show();
+ QVERIFY(QTest::qWaitForWindowExposed(&windowThree));
+
+ rhiWidget->setParent(&windowThree);
+ // this case needs a show() on rhiWidget
+ rhiWidget->show();
+
+ QTRY_VERIFY(frameSpy.count() > 0);
+ QCOMPARE(errorSpy.count(), 0);
+}
+
+void tst_QRhiWidget::grab_data()
+{
+ testData();
+}
+
+void tst_QRhiWidget::grab()
+{
+ QFETCH(QRhiWidget::Api, api);
+
+ const int maxFuzz = 1;
+
+ SimpleRhiWidget w;
+ w.setApi(api);
+ w.resize(1280, 720);
+ QSignalSpy errorSpy(&w, &QRhiWidget::renderFailed);
+
+ QImage image = w.grab(); // creates its own QRhi just to render offscreen
+ QVERIFY(!image.isNull());
+ QVERIFY(w.rhi());
+ QVERIFY(w.colorTexture());
+ QCOMPARE(errorSpy.count(), 0);
+ if (api != QRhiWidget::Api::Null) {
+ QRgb c = image.pixel(image.width() / 2, image.height() / 2);
+ QVERIFY(qRed(c) >= 255 - maxFuzz);
+ QVERIFY(qGreen(c) <= maxFuzz);
+ QVERIFY(qBlue(c) <= maxFuzz);
+ }
+
+ // Make the window visible, this under the hood drops the QRhiWidget's
+ // own QRhi and attaches to the backingstore's.
+ QSignalSpy frameSpy(&w, &QRhiWidget::frameSubmitted);
+ w.show();
+ QVERIFY(QTest::qWaitForWindowExposed(&w));
+ QTRY_VERIFY(frameSpy.count() > 0);
+
+ QCOMPARE(errorSpy.count(), 0);
+
+ if (api != QRhiWidget::Api::Null) {
+ QRhiReadbackResult readResult;
+ QRhiResourceUpdateBatch *rub = w.rhi()->nextResourceUpdateBatch();
+ rub->readBackTexture(w.colorTexture(), &readResult);
+ QVERIFY(submitResourceUpdates(w.rhi(), rub));
+ QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888);
+ if (w.rhi()->isYUpInFramebuffer())
+ image = wrapperImage.mirrored();
+ else
+ image = wrapperImage.copy();
+ QRgb c = image.pixel(image.width() / 2, image.height() / 2);
+ QVERIFY(qRed(c) >= 255 - maxFuzz);
+ QVERIFY(qGreen(c) <= maxFuzz);
+ QVERIFY(qBlue(c) <= maxFuzz);
+ }
+}
+
+void tst_QRhiWidget::mirror_data()
+{
+ testData();
+}
+
+void tst_QRhiWidget::mirror()
+{
+ QFETCH(QRhiWidget::Api, api);
+
+ SimpleRhiWidget *rhiWidget = new SimpleRhiWidget;
+ rhiWidget->setApi(api);
+ QVERIFY(!rhiWidget->isMirrorVerticallyEnabled());
+
+ QSignalSpy frameSpy(rhiWidget, &QRhiWidget::frameSubmitted);
+ QSignalSpy errorSpy(rhiWidget, &QRhiWidget::renderFailed);
+
+ QVBoxLayout *layout = new QVBoxLayout;
+ layout->addWidget(rhiWidget);
+ QWidget w;
+ w.setLayout(layout);
+ w.resize(1280, 720);
+ w.show();
+ QVERIFY(QTest::qWaitForWindowExposed(&w));
+
+ QTRY_VERIFY(frameSpy.count() > 0);
+ QCOMPARE(errorSpy.count(), 0);
+
+ frameSpy.clear();
+ rhiWidget->setMirrorVertically(true);
+ QVERIFY(rhiWidget->isMirrorVerticallyEnabled());
+ QTRY_VERIFY(frameSpy.count() > 0);
+ QCOMPARE(errorSpy.count(), 0);
+
+ if (api != QRhiWidget::Api::Null) {
+ QRhi *rhi = rhiWidget->rhi();
+ QRhiReadbackResult readResult;
+ QRhiResourceUpdateBatch *rub = rhi->nextResourceUpdateBatch();
+ rub->readBackTexture(rhiWidget->colorTexture(), &readResult);
+ QVERIFY(submitResourceUpdates(rhi, rub));
+ QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888);
+ QImage image;
+ if (rhi->isYUpInFramebuffer())
+ image = wrapperImage.mirrored();
+ else
+ image = wrapperImage.copy();
+
+ const int maxFuzz = 1;
+ QRgb c = image.pixel(50, 5);
+ if (api != QRhiWidget::Api::Vulkan) {
+ // this should be the background (greenish), not the red triangle
+ QVERIFY(qGreen(c) > qRed(c));
+ } else {
+ // remember that Vulkan is upside down due to not correcting for Y down in NDC
+ // hence this is red
+ QVERIFY(qRed(c) >= 255 - maxFuzz);
+ QVERIFY(qGreen(c) <= maxFuzz);
+ }
+ QVERIFY(qBlue(c) <= maxFuzz);
+ }
+}
+
+QTEST_MAIN(tst_QRhiWidget)
+
+#include "tst_qrhiwidget.moc"
diff --git a/tests/manual/rhi/CMakeLists.txt b/tests/manual/rhi/CMakeLists.txt
index 9fbb924f77..b0637b208c 100644
--- a/tests/manual/rhi/CMakeLists.txt
+++ b/tests/manual/rhi/CMakeLists.txt
@@ -34,5 +34,5 @@ add_subdirectory(displacement)
add_subdirectory(imguirenderer)
add_subdirectory(multiview)
if(QT_FEATURE_widgets)
- add_subdirectory(rhiwidget)
+ add_subdirectory(rhiwidgetproto)
endif()
diff --git a/tests/manual/rhi/rhiwidget/CMakeLists.txt b/tests/manual/rhi/rhiwidgetproto/CMakeLists.txt
index 97bfea5590..5b62ef557d 100644
--- a/tests/manual/rhi/rhiwidget/CMakeLists.txt
+++ b/tests/manual/rhi/rhiwidgetproto/CMakeLists.txt
@@ -1,7 +1,7 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
-qt_internal_add_manual_test(rhiwidget
+qt_internal_add_manual_test(rhiwidgetproto
GUI
SOURCES
examplewidget.cpp examplewidget.h
@@ -20,14 +20,14 @@ set_source_files_properties("../shared/texture.vert.qsb"
set_source_files_properties("../shared/texture.frag.qsb"
PROPERTIES QT_RESOURCE_ALIAS "texture.frag.qsb"
)
-set(rhiwidget_resource_files
+set(rhiwidgetproto_resource_files
"../shared/texture.vert.qsb"
"../shared/texture.frag.qsb"
)
-qt_internal_add_resource(rhiwidget "rhiwidget"
+qt_internal_add_resource(rhiwidgetproto "rhiwidgetproto"
PREFIX
"/"
FILES
- ${rhiwidget_resource_files}
+ ${rhiwidgetproto_resource_files}
)
diff --git a/tests/manual/rhi/rhiwidget/examplewidget.cpp b/tests/manual/rhi/rhiwidgetproto/examplewidget.cpp
index 942766ac5c..942766ac5c 100644
--- a/tests/manual/rhi/rhiwidget/examplewidget.cpp
+++ b/tests/manual/rhi/rhiwidgetproto/examplewidget.cpp
diff --git a/tests/manual/rhi/rhiwidget/examplewidget.h b/tests/manual/rhi/rhiwidgetproto/examplewidget.h
index b85ae6f1f7..b85ae6f1f7 100644
--- a/tests/manual/rhi/rhiwidget/examplewidget.h
+++ b/tests/manual/rhi/rhiwidgetproto/examplewidget.h
diff --git a/tests/manual/rhi/rhiwidget/main.cpp b/tests/manual/rhi/rhiwidgetproto/main.cpp
index 8af531cd3b..8af531cd3b 100644
--- a/tests/manual/rhi/rhiwidget/main.cpp
+++ b/tests/manual/rhi/rhiwidgetproto/main.cpp
diff --git a/tests/manual/rhi/rhiwidget/rhiwidget.cpp b/tests/manual/rhi/rhiwidgetproto/rhiwidget.cpp
index 2ba8721102..2ba8721102 100644
--- a/tests/manual/rhi/rhiwidget/rhiwidget.cpp
+++ b/tests/manual/rhi/rhiwidgetproto/rhiwidget.cpp
diff --git a/tests/manual/rhi/rhiwidget/rhiwidget.h b/tests/manual/rhi/rhiwidgetproto/rhiwidget.h
index 94cc108e28..94cc108e28 100644
--- a/tests/manual/rhi/rhiwidget/rhiwidget.h
+++ b/tests/manual/rhi/rhiwidgetproto/rhiwidget.h
diff --git a/tests/manual/rhi/rhiwidget/rhiwidget_p.h b/tests/manual/rhi/rhiwidgetproto/rhiwidget_p.h
index b5cd058a94..b5cd058a94 100644
--- a/tests/manual/rhi/rhiwidget/rhiwidget_p.h
+++ b/tests/manual/rhi/rhiwidgetproto/rhiwidget_p.h