Add QRhiWidget

Task-number: QTBUG-113331
Change-Id: I8baa697b4997b05f52acdee0e08d3c368fde5bc2
Reviewed-by: Andy Nichols <andy.nichols@qt.io>
This commit is contained in:
Laszlo Agocs 2023-06-13 13:10:40 +02:00
parent 85e2f79e9e
commit 72a453c6a8
55 changed files with 3717 additions and 8 deletions

View File

@ -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}
*/

View File

@ -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}
*/

View File

@ -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)

View File

@ -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}"
)

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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
}

View File

@ -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

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -0,0 +1,4 @@
requires(qtHaveModule(widgets))
TEMPLATE = subdirs
SUBDIRS = simplerhiwidget \
cuberhiwidget

View File

@ -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}"
)

View File

@ -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]

View File

@ -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

View File

@ -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]

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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

View File

@ -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>

View File

@ -15,6 +15,7 @@ SUBDIRS = \
layouts \ layouts \
mainwindows \ mainwindows \
painting \ painting \
rhi \
richtext \ richtext \
tools \ tools \
touch \ touch \

View File

@ -90,6 +90,18 @@
portable, cross-platform application that performs accelerated 3D rendering portable, cross-platform application that performs accelerated 3D rendering
onto a QWindow using QRhi. 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 \note The RHI family of APIs are currently offered with a limited
compatibility guarantee, as opposed to regular Qt public APIs. See \l QRhi compatibility guarantee, as opposed to regular Qt public APIs. See \l QRhi
for details. for details.

View File

@ -551,10 +551,13 @@ QPlatformBackingStore::FlushResult QBackingStoreDefaultCompositor::flush(QPlatfo
} }
for (int i = 0; i < textureWidgetCount; ++i) { for (int i = 0; i < textureWidgetCount; ++i) {
const bool invertSourceForTextureWidget = textures->flags(i).testFlag(QPlatformTextureList::MirrorVertically)
? !invertSource : invertSource;
QMatrix4x4 target; QMatrix4x4 target;
QMatrix3x3 source; QMatrix3x3 source;
if (!prepareDrawForRenderToTextureWidget(textures, i, window, deviceWindowRect, if (!prepareDrawForRenderToTextureWidget(textures, i, window, deviceWindowRect,
offset, invertTargetY, invertSource, &target, &source)) offset, invertTargetY, invertSourceForTextureWidget,
&target, &source))
{ {
m_textureQuadData[i].reset(); m_textureQuadData[i].reset();
continue; continue;

View File

@ -94,7 +94,8 @@ public:
enum Flag { enum Flag {
StacksOnTop = 0x01, StacksOnTop = 0x01,
TextureIsSrgb = 0x02, TextureIsSrgb = 0x02,
NeedsPremultipliedAlphaBlending = 0x04 NeedsPremultipliedAlphaBlending = 0x04,
MirrorVertically = 0x08
}; };
Q_DECLARE_FLAGS(Flags, Flag) Q_DECLARE_FLAGS(Flags, Flag)

View File

@ -20,6 +20,7 @@ qt_internal_add_module(Widgets
kernel/qlayout.cpp kernel/qlayout.h kernel/qlayout_p.h kernel/qlayout.cpp kernel/qlayout.h kernel/qlayout_p.h
kernel/qlayoutengine.cpp kernel/qlayoutengine_p.h kernel/qlayoutengine.cpp kernel/qlayoutengine_p.h
kernel/qlayoutitem.cpp kernel/qlayoutitem.h kernel/qlayoutitem.cpp kernel/qlayoutitem.h
kernel/qrhiwidget.cpp kernel/qrhiwidget.h kernel/qrhiwidget_p.h
kernel/qsizepolicy.cpp kernel/qsizepolicy.h kernel/qsizepolicy.cpp kernel/qsizepolicy.h
kernel/qstackedlayout.cpp kernel/qstackedlayout.h kernel/qstackedlayout.cpp kernel/qstackedlayout.h
kernel/qstandardgestures.cpp kernel/qstandardgestures_p.h kernel/qstandardgestures.cpp kernel/qstandardgestures_p.h

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -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]

View File

@ -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]

View File

@ -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]

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

View File

@ -10858,9 +10858,10 @@ void QWidget::setParent(QWidget *parent, Qt::WindowFlags f)
// do it on newtlw instead, the performance implications of that are // do it on newtlw instead, the performance implications of that are
// problematic when it comes to large widget trees. // problematic when it comes to large widget trees.
if (q_evaluateRhiConfig(this, nullptr, &surfaceType)) { if (q_evaluateRhiConfig(this, nullptr, &surfaceType)) {
const bool wasUsingRhiFlush = newtlw->d_func()->usesRhiFlush;
newtlw->d_func()->usesRhiFlush = true; newtlw->d_func()->usesRhiFlush = true;
if (QWindow *w = newtlw->windowHandle()) { if (QWindow *w = newtlw->windowHandle()) {
if (w->surfaceType() != surfaceType) { if (w->surfaceType() != surfaceType || !wasUsingRhiFlush) {
newtlw->destroy(); newtlw->destroy();
newtlw->create(); newtlw->create();
} }

View File

@ -58,3 +58,4 @@ endif()
if(QT_FEATURE_opengl) if(QT_FEATURE_opengl)
add_subdirectory(qopenglwidget) add_subdirectory(qopenglwidget)
endif() endif()
add_subdirectory(qrhiwidget)

View File

@ -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
)

View File

@ -0,0 +1,8 @@
#version 440
layout(location = 0) out vec4 fragColor;
void main()
{
fragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

View File

@ -0,0 +1,8 @@
#version 440
layout(location = 0) in vec4 position;
void main()
{
gl_Position = position;
}

View File

@ -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"

View File

@ -34,5 +34,5 @@ add_subdirectory(displacement)
add_subdirectory(imguirenderer) add_subdirectory(imguirenderer)
add_subdirectory(multiview) add_subdirectory(multiview)
if(QT_FEATURE_widgets) if(QT_FEATURE_widgets)
add_subdirectory(rhiwidget) add_subdirectory(rhiwidgetproto)
endif() endif()

View File

@ -1,7 +1,7 @@
# Copyright (C) 2022 The Qt Company Ltd. # Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause # SPDX-License-Identifier: BSD-3-Clause
qt_internal_add_manual_test(rhiwidget qt_internal_add_manual_test(rhiwidgetproto
GUI GUI
SOURCES SOURCES
examplewidget.cpp examplewidget.h examplewidget.cpp examplewidget.h
@ -20,14 +20,14 @@ set_source_files_properties("../shared/texture.vert.qsb"
set_source_files_properties("../shared/texture.frag.qsb" set_source_files_properties("../shared/texture.frag.qsb"
PROPERTIES QT_RESOURCE_ALIAS "texture.frag.qsb" PROPERTIES QT_RESOURCE_ALIAS "texture.frag.qsb"
) )
set(rhiwidget_resource_files set(rhiwidgetproto_resource_files
"../shared/texture.vert.qsb" "../shared/texture.vert.qsb"
"../shared/texture.frag.qsb" "../shared/texture.frag.qsb"
) )
qt_internal_add_resource(rhiwidget "rhiwidget" qt_internal_add_resource(rhiwidgetproto "rhiwidgetproto"
PREFIX PREFIX
"/" "/"
FILES FILES
${rhiwidget_resource_files} ${rhiwidgetproto_resource_files}
) )