From 72a453c6a8b02c08c5c09842f468459d6a51c387 Mon Sep 17 00:00:00 2001 From: Laszlo Agocs Date: Tue, 13 Jun 2023 13:10:40 +0200 Subject: [PATCH] Add QRhiWidget Task-number: QTBUG-113331 Change-Id: I8baa697b4997b05f52acdee0e08d3c368fde5bc2 Reviewed-by: Andy Nichols --- examples/widgets/doc/src/cuberhiwidget.qdoc | 169 +++ examples/widgets/doc/src/simplerhiwidget.qdoc | 203 +++ examples/widgets/rhi/CMakeLists.txt | 8 + .../widgets/rhi/cuberhiwidget/CMakeLists.txt | 47 + examples/widgets/rhi/cuberhiwidget/cube.h | 139 ++ .../rhi/cuberhiwidget/cuberhiwidget.pro | 12 + .../rhi/cuberhiwidget/cuberhiwidget.qrc | 6 + .../rhi/cuberhiwidget/examplewidget.cpp | 172 +++ .../widgets/rhi/cuberhiwidget/examplewidget.h | 72 + examples/widgets/rhi/cuberhiwidget/main.cpp | 166 +++ .../shader_assets/texture.frag.qsb | Bin 0 -> 1042 bytes .../shader_assets/texture.vert.qsb | Bin 0 -> 1161 bytes .../rhi/cuberhiwidget/shaders/texture.frag | 12 + .../rhi/cuberhiwidget/shaders/texture.vert | 15 + examples/widgets/rhi/rhi.pro | 4 + .../rhi/simplerhiwidget/CMakeLists.txt | 47 + .../rhi/simplerhiwidget/examplewidget.cpp | 102 ++ .../rhi/simplerhiwidget/examplewidget.h | 30 + examples/widgets/rhi/simplerhiwidget/main.cpp | 26 + .../shader_assets/color.frag.qsb | Bin 0 -> 738 bytes .../shader_assets/color.vert.qsb | Bin 0 -> 1091 bytes .../rhi/simplerhiwidget/shaders/color.frag | 10 + .../rhi/simplerhiwidget/shaders/color.vert | 16 + .../rhi/simplerhiwidget/simplerhiwidget.pro | 12 + .../rhi/simplerhiwidget/simplerhiwidget.qrc | 6 + examples/widgets/widgets.pro | 1 + src/gui/doc/src/qtgui-overview.qdoc | 12 + .../qbackingstoredefaultcompositor.cpp | 5 +- src/gui/painting/qplatformbackingstore.h | 3 +- src/widgets/CMakeLists.txt | 1 + .../doc/images/cuberhiwidget-example.jpg | Bin 0 -> 70232 bytes src/widgets/doc/images/qrhiwidget-intro.jpg | Bin 0 -> 9508 bytes .../doc/images/simplerhiwidget-example.jpg | Bin 0 -> 12489 bytes .../snippets/qrhiwidget/rhiwidgetintro.cpp | 109 ++ .../snippets/qrhiwidget/rhiwidgetintro.frag | 10 + .../snippets/qrhiwidget/rhiwidgetintro.vert | 15 + src/widgets/kernel/qrhiwidget.cpp | 1283 +++++++++++++++++ src/widgets/kernel/qrhiwidget.h | 102 ++ src/widgets/kernel/qrhiwidget_p.h | 63 + src/widgets/kernel/qwidget.cpp | 3 +- tests/auto/widgets/widgets/CMakeLists.txt | 1 + .../widgets/widgets/qrhiwidget/CMakeLists.txt | 25 + .../widgets/qrhiwidget/data/simple.frag | 8 + .../widgets/qrhiwidget/data/simple.frag.qsb | Bin 0 -> 724 bytes .../widgets/qrhiwidget/data/simple.vert | 8 + .../widgets/qrhiwidget/data/simple.vert.qsb | Bin 0 -> 783 bytes .../widgets/qrhiwidget/tst_qrhiwidget.cpp | 792 ++++++++++ tests/manual/rhi/CMakeLists.txt | 2 +- .../CMakeLists.txt | 8 +- .../examplewidget.cpp | 0 .../examplewidget.h | 0 .../{rhiwidget => rhiwidgetproto}/main.cpp | 0 .../rhiwidget.cpp | 0 .../{rhiwidget => rhiwidgetproto}/rhiwidget.h | 0 .../rhiwidget_p.h | 0 55 files changed, 3717 insertions(+), 8 deletions(-) create mode 100644 examples/widgets/doc/src/cuberhiwidget.qdoc create mode 100644 examples/widgets/doc/src/simplerhiwidget.qdoc create mode 100644 examples/widgets/rhi/CMakeLists.txt create mode 100644 examples/widgets/rhi/cuberhiwidget/CMakeLists.txt create mode 100644 examples/widgets/rhi/cuberhiwidget/cube.h create mode 100644 examples/widgets/rhi/cuberhiwidget/cuberhiwidget.pro create mode 100644 examples/widgets/rhi/cuberhiwidget/cuberhiwidget.qrc create mode 100644 examples/widgets/rhi/cuberhiwidget/examplewidget.cpp create mode 100644 examples/widgets/rhi/cuberhiwidget/examplewidget.h create mode 100644 examples/widgets/rhi/cuberhiwidget/main.cpp create mode 100644 examples/widgets/rhi/cuberhiwidget/shader_assets/texture.frag.qsb create mode 100644 examples/widgets/rhi/cuberhiwidget/shader_assets/texture.vert.qsb create mode 100644 examples/widgets/rhi/cuberhiwidget/shaders/texture.frag create mode 100644 examples/widgets/rhi/cuberhiwidget/shaders/texture.vert create mode 100644 examples/widgets/rhi/rhi.pro create mode 100644 examples/widgets/rhi/simplerhiwidget/CMakeLists.txt create mode 100644 examples/widgets/rhi/simplerhiwidget/examplewidget.cpp create mode 100644 examples/widgets/rhi/simplerhiwidget/examplewidget.h create mode 100644 examples/widgets/rhi/simplerhiwidget/main.cpp create mode 100644 examples/widgets/rhi/simplerhiwidget/shader_assets/color.frag.qsb create mode 100644 examples/widgets/rhi/simplerhiwidget/shader_assets/color.vert.qsb create mode 100644 examples/widgets/rhi/simplerhiwidget/shaders/color.frag create mode 100644 examples/widgets/rhi/simplerhiwidget/shaders/color.vert create mode 100644 examples/widgets/rhi/simplerhiwidget/simplerhiwidget.pro create mode 100644 examples/widgets/rhi/simplerhiwidget/simplerhiwidget.qrc create mode 100644 src/widgets/doc/images/cuberhiwidget-example.jpg create mode 100644 src/widgets/doc/images/qrhiwidget-intro.jpg create mode 100644 src/widgets/doc/images/simplerhiwidget-example.jpg create mode 100644 src/widgets/doc/snippets/qrhiwidget/rhiwidgetintro.cpp create mode 100644 src/widgets/doc/snippets/qrhiwidget/rhiwidgetintro.frag create mode 100644 src/widgets/doc/snippets/qrhiwidget/rhiwidgetintro.vert create mode 100644 src/widgets/kernel/qrhiwidget.cpp create mode 100644 src/widgets/kernel/qrhiwidget.h create mode 100644 src/widgets/kernel/qrhiwidget_p.h create mode 100644 tests/auto/widgets/widgets/qrhiwidget/CMakeLists.txt create mode 100644 tests/auto/widgets/widgets/qrhiwidget/data/simple.frag create mode 100644 tests/auto/widgets/widgets/qrhiwidget/data/simple.frag.qsb create mode 100644 tests/auto/widgets/widgets/qrhiwidget/data/simple.vert create mode 100644 tests/auto/widgets/widgets/qrhiwidget/data/simple.vert.qsb create mode 100644 tests/auto/widgets/widgets/qrhiwidget/tst_qrhiwidget.cpp rename tests/manual/rhi/{rhiwidget => rhiwidgetproto}/CMakeLists.txt (78%) rename tests/manual/rhi/{rhiwidget => rhiwidgetproto}/examplewidget.cpp (100%) rename tests/manual/rhi/{rhiwidget => rhiwidgetproto}/examplewidget.h (100%) rename tests/manual/rhi/{rhiwidget => rhiwidgetproto}/main.cpp (100%) rename tests/manual/rhi/{rhiwidget => rhiwidgetproto}/rhiwidget.cpp (100%) rename tests/manual/rhi/{rhiwidget => rhiwidgetproto}/rhiwidget.h (100%) rename tests/manual/rhi/{rhiwidget => rhiwidgetproto}/rhiwidget_p.h (100%) 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 } 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 +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 +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 @@ + + + shader_assets/texture.vert.qsb + shader_assets/texture.frag.qsb + + 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 +#include + +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 +#include + +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 vbuf; + std::unique_ptr ubuf; + std::unique_ptr srb; + std::unique_ptr ps; + std::unique_ptr sampler; + std::unique_ptr 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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!

" + "The cube is textured with QPainter-generated content.

" + "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 0000000000000000000000000000000000000000..dc440d8067577b0b9249e1d3b7156adf8154042b GIT binary patch literal 1042 zcmV+t1nv6(01I?@oXu9-Zrer>U0od2bkaC!Y^O;t8#@K)R$v&xDT*pGP}pq|Ab^v| zNP-9g!IEp407VKUr8o)v3w`V_^s%4OZ|I-&rD$h%hvJHo5A9B0Mb&2kN0o9td>P`ds4fYZHiqLDI79*~K3*LWz+BzCj}E603tE6&u&5&}D5Qco4#8;De9dcIM@cM@Kx9A-CZW9;!e9Km39`(Glu>J;Qg<@eX zR>{5t`!(4|rKdw$u2a4*z^fr^u51wB2gX&xI#lZ|;yeO&%>5=|TUuQ)ryqb1bN!6$ zZxILP{W;;kg3TuN`zGPJ-70|^ z4LbV=7`{_eQl}Zmjn9hF`D4=|%v75E$;2;9f7)QZL zoD2y|-Qg${N$Zej;tWnFZt^CG27DsC7N0l(dU2ficoGLb$8^kH;AP(c?LW-8?nUY*s3QlmVP*Qz$oR{pO zoVUGw49UOei5LWFCK5BVfuyUXbu%{;9J6pptZtd2rICV7;Hl}I)vlk+79h=%v6u1g zlSj{NZBV$F;b@$dD72u!(0%S4yU9RgL?OF? z2F^??$dj63sY;l74L;BqMX;nb^9id#m@5T`>J5Yw;PZ4!ih^|Xb4#y!o(`!}z=E*K zKb9M1q$AdhqM~(Zmk5}ks6=qCPaXb$xm-+L<B#j2mtIBNF^r9&mvy_>^YCukyZ!#mz$nikS~_XH-Z+KX z4r4DWoRN4aA}z3bGIKh3OM_?Ll77EAeKq;&d#1i-yP@21im%$?_0 M*-l>n0URV*m>ReaF8}}l literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..84aed7fee2ec758f227d550080bb78d987b5649e GIT binary patch literal 1161 zcmV;41a|uX01hR1oaI+rZyQAv9$%7@ZD|tPCV}QM)M>FHGIp9GRO1L#5Uo@xQG+TZ zD_X7B>#*S6wf16MRsIpY@XL7TH}J*_f^%lh?9Q%jP#jC8( z5;hbwlRac@n0-En@gT$)X4grpBQ_Sfev?t6ca`3&^d4l8$}g32?HWQ2_7-c9b`wf0 ziL6$|`|IZy&zn&^Xr4T3q5USSqyg4xgD{9^xk#?iU`6l3bBOD~in@LBkMebr(Z^G^ z!nVn#m{7VB>2zF~bQR&NlIos<-#uEtLU<+DG`~s*g$tI^#~%5>+z#M=2;-chF#e zd;y=x)jIKu9CiV@N`0}eVcsFkriQsi`g6eGUfrg3s6T~BYotRToXH+KAYQt}$vx6{ zXcstBic=@tAHcw;^4M4DETS-A*Cyss0CuZzS?H~vT z9m9&_(CsI&BTY*TCwv@4ZVd6!SKtmf=2>r0^voge3O{uvH>XFb&*rN}N5HQ}<~g|o zJS5nJ%=w2}jG18+$e6NhXk;A!FUj~>BjeYMj7R^5j39~UiE#ZQ15)$fmX7zqaaMU{ z--a@VPAIB*%tPnOjbbOP_ggBVF!<5*tnY)64@UtIF+IUBCWd*;$Cmv=FdRlsT(?^d z$q(>n#jZ_oJ;P4Ul&33iS**`dN4s=!{_Mp|^GO&)k@?IYCo!d6SOB?x^VPRc&dy$Z z+3I9~ERqIj%Q_YWI7{L}1igqQF>(|D=HjJB$V@c>bYN^Q?*tj_O?tsgMs-#Fk=n9` z=!f}|WDm}9$$|YV$8>S7($bRBtj%o45H!y6&_UVui2}2drD8!|a&;gC3DPyqe95_1 zo5SW$mYGjDbmAoR3*lz^$-!^Y8`;SzZ`WMk9wh^Zf95%{H8SB>;>hiv8ks(Fy}`%$ z(fsZd?u`g9OyBaH2#$@z#j4JN>n1&C>6Jqe%%p|cE$zTwFNxsvjsn|nH7+lgc44ol zwgN$OAzXjSDeCpoYrI|vpdBS0)o!Q=xZTb)D?-ldDk68$z3P_B%Oh?FK8U`c<^gsv z*yU{{VmH#;Svka~%U{oARzY#k=?uAkQ$I$!s7_Ka*IL=l3ix~*Q|kn>W3XjbL`rnq b7H-`1=Ln;Jp$dI9XIHu+PK>_*LXrSI<)%u> literal 0 HcmV?d00001 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 +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 + +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 +#include + +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 m_vbuf; + std::unique_ptr m_ubuf; + std::unique_ptr m_srb; + std::unique_ptr 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 +#include +#include +#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 0000000000000000000000000000000000000000..32bd2d595321b683a078addcdce0dd3b6f4dc216 GIT binary patch literal 738 zcmV<80v-JT00+c)oV8S4Yt%p#o$aPwo7UP7&^Hm((@o**(^f%5e@|5h0EiEy7%eIK&%R16GJXBo5p!YWp?d zoPp;f$ieEkz7H!D&0#)(_y%3VkeC6AmEnlDXg^OF#+wvhr}zcZY*PFJ=|t=??C0d& zaC~|FoXtuN2I<2ZKLMGAXPau@rTq7R&Pgv6#x!i1u$qRg5Qg=SKt1YTrTk?&M;*Js zI!|Z6pu!7Tg&E{?je1!l%CGBVKS@O#@lD${M~UwVxjPrZ`G^m~*v(qkU=aamj@{(5 zhyp(Ly=Q#vcySmf2*;w&k*&F5UYHz45IS!YH`sGCw`=!!8#A_>l<|abTJ}cEyfn>< z%unSj$DC5;mi_;!ziHH>q}~DQIL~AeK7hI%MJ%G*m0X4Yl?d$eR;i)(BhB1Qc&Z^2 z-1>p8AkC88%lOIB{_&a9N#Znh_M=gr6>=ux`AdHK=0oT3@OaM#Dbv#r^Q@Au34sbH zXU?gc1b#*(qz4$_+-QWVGGWC)d2yXs*?H|g91IA2N8q}kC~no(53MV23KgVF0aFN7 zax6D0NJp%RiCKmUt?L z{ipb2o$s1vp5g}(xkEo4xt`BuR@peDv61?wes#Ojji$ZP>rJ#=Wr}F}i_-1psh>2% z*o(?ctUB}~ja99hMJ@QpfXt#9y7XWt=oYs3sApAX=$Xss>d^ zR~W|A<* zR+t)fm=<$c%mSDRNDY{^H#FFSvB4(bnlhgWm@$h$*F?iVZK}asV^e0&VZC+RG8?jt zjpfQ_d+Y#KU(NZ15PSlw%cM0uHWr0`mr84`T-9PA|eN9myZR>O>2ogFz(keAr(I(70xF4)5v}PnZEJPSdCow2;y$j{w4C)a?6OTbx^ou8Dnfy49x8&eD4~$qP%0scZ^y# zVOL$+8!{hUZ@~)5<}kOx{}Pekkh&%?t=6dTle}WsEvi@BErSD$w@UGl^Xu?)jj(Wh zO7c4S-vW7q{MTjPibEY^gD|fW{uRQ!O6S)gjBA8ZBaCZOuf`3KwU!cMBKP2woC;pDWx{%rqZjm12>`=dKQXIs&OMb5*>o3$*Y|{NeVZ@62 z`Fm^XC8-}re7D!LtW+eKEBG)QTdyn*BXq<*52us9HS1ee7Wv~i3DM5&y#vrpo#baf zI^$E%z0aq%8wYXHw;Ul7f0zkR=@)7D`6Nz#0mv97_D38uYKIVX?Vx>ipuJL zxRB=9OJueWc;cP;sqm8au%}^@_@x~>KLYSLh@sH)eT1~}rmFbmqcwtM<0I`W+7b5|9&bMGI?cz z804>Ny8@tZhUv1~(c$rkz4Ib{wQ6fP?qh7-A*ZMJAXo%qBuV)g`xWf!&5^*J{0-Fs zaUSwlGnGnCOwLkhcW-H&gj2`O3TdIwXW(X J$KNI4w4#ItHi!TK literal 0 HcmV?d00001 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 +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 @@ + + + shader_assets/color.vert.qsb + shader_assets/color.frag.qsb + + 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 0000000000000000000000000000000000000000..70baab8bebeec7cd815c04145c9be114e92664a5 GIT binary patch literal 70232 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 s5%b5B8!d z>VmZLg6CEr23LW|kjR7@rvlaML1UnSs$byenYlpJb*&52#APrbjN%+4mg?aKBvNWD z!A08EL3^>E@oOX4#816xIA5mr}>dS@YV z?X9LRKn~XRKXNdCn(iR~8yVxhk0WijG94{+$c<%&rsQdp&1u&fnxj&0(W};J^{I!F z8=*ogpUvyG9aXQOa?r-NYsc9iOXdOW1HVQ?ro9!{i_flYUVkQG$&s9tOf}apm`_PP zv|J$)u98QA8!8gB9dnwJzc0^(Ea+^Fk>^BOeK|ZN?=*!w1xe^v0u##ImcsD`O+GNQ z@vE%*tknfw4edAH8bC6eO{X~j9Qw0OouiX#cC#8iQFH+z41pZYUplV27yZ`NAy{%E z$id_(yA>ayBB~El&9rb}o>3UHEz?gj^=uuXoJX@7rDS|B@HAhXl~t0ZCE6N=eC$;c z7n957wCtZZgy%=lEk8klx&`KkZxH2a_z~6xer~$W^)<0I_M3Kb+9mSi%0Hj-)wxO% z5HNVcP=l(FF3`g!P9QQ+_`TbkF@~YaU{!-fCJH3|K>adEi1V!Nsg~UBzQj$#ccTE# zR!^{1Lz&vM=7g2{31(`AQta3=2O8vySmLYk7u7wjm^Jznuf?vpM@FxDS8>e)cl1*uG(jxNgP0p9^FP`jVC&@cl z-4reSiIFamW8;P?~zAOhAILG_1026;ujzG_dOGCGlK;&uh7vqbWZ)qoA&E)C}()!%i9E} zf)#t7#}F6tiXZoMIZVZP_0) zbUmS^c9#9oL!?Q0jfB1XA_l*i^Hy&-L)RXra%@&v3%aB#S(i7M@Nz0yJTwQYB)ooF9=cwGbUn}oNNbq#W5F2yHmgr2xZT}nwC?J&qNy@m-$TtUC0PFwsr zH^KqlkZs4WBG`Q^f5t1=*BCw6lPZL~jE3=1xQ@95umB2UJNMq*s6Q2`h;M?>xvL`{ zptV$fPZNYDPQ4`_)vOyd8TAZ*3y`%cE(-mD(g(F8Pf=o$Jm$Lf^fVU#~Os0 zKg=_~P=9PJtRjWt$MAJ=((Tbp5E`IWmYH^E9&TrKu`tJyj;i~c-+ zI)YK5$|wy3fIyH#pbGEDOgAkq0CSB5{#;0!VU<5zVU_X!mHu(vzI$a?#>ge~ohd1g z0D|##$_6?YB%IbYOyxkX6V^<5+?_QSv-GZ@2Sth+Y{RCL4%4a{E`>`(Jy;QBuCO8^ zgxWFHaS#i07Q6qH^nA8c%o0R}E~Y0M;t-&4ByuWLZNV88PB5&frYZE2F|IT+kT!yoD9|H4*`c3+Q*Udv{93!;rAkKgT>DNrpPeL^-% zOb-;zWz~`9HhbD-Mc6`tQJ>)pe2btDYn`r;1w9@G%KqWKgvI$kLR(EbYC|_w3+6Z< zKJcX6CLOo%?F2t+twTYhz4sZlw3||odWLVh^_CsyU1HWZz-!1p8-L@{cygKpflA}A z_)XWsdR-1%h6H>Cs?*+YDx}nb!5nU{yrINr5OR$0ct{nWJg?t`2~i>aU;u>NuXyho z5WDugb6~g%F=%It?~2#3_e<-R&MP&_6q*#M;4d#*i{CSuU#&T*Dsn!yzO38RTH_gw zm7EA`0@tk~Yg!2N@@jXM=GH)Dz?6}dZz0sjEllwC*2wvu%7@Lo617ym+FBwj3qhqX z-C6p1?zlV_VmqOYFce3qyJ_DNv;)4kqjar3N~q%)HWX7rvAsNFLg)1`tk98MwqgDL z)N1Da&g6K}NaKx6jfrPl!aVW97qFTIt$vZX)eqkx+)=PHxU$pjcA0~`0P6kci?5>? zog$Oy3iP#^%r5IGFjh3!_11H`3VLi*K$CmCI5Im{z{$m95HjKAi*8JW{ z?0hoB>Y}eLtjhTdhaz=!J8r#23YGFrUKp+3ooq!S%6NVGr99PwO1xDmLfaRfDi%BY zA#dH6L;Ojh-;X3V%HzQM$WtXGetK=g$EyS)`!ds={AoWH2ie3Y?kD$Y0OH$;8`M%X zd?B^BOV0#B1_CN3Es0ydR&03EPSsx=(_7M~e{npnT1PR=U?6h@lKzfX7Kt0!J*e7c zcCf#hCs4Z0TPtA)&?VjxC$UY_lWkzrdT~l&W^eO`!a6ZesO%Cv* z9`Kx={HMNTJpZ%j;=cn^;KKQ(O;8a$!d%l*0{Htj6hyQlaHj)xn5G*m$7{sDE{PQW ziKZK3;feqCTYKS0`C}15g58h^D{4`zZ#q9RyK5Rp_-V5$8yZ8E>fouAsZ!bY8Og7> zO;mNC+ouzMqWO!Hv!dL>W-=4u#ig&4neua^JdY_C3VFA`If0Z|uoz=j zO|a5U&jdSS)9`}-cV4US z$jC{k-w3W8cNQx6=iA38e%Wg?szR3jT=0}JQX$J1-&>d0|FCZAg>$mfQWn&JFGn{D zqdWo)(1B30JhZasT8X5rB2ya4wSxr3z0?4I;x<(p&dX0U>`QR=7aOaOw1`j@J@Sw1 zqqU}a1Jw%_O>e=f(|&H&Hujfzi}XH=Hr7%-0W&|-3=7U__u*HWy+k2>M@*0t*#%%S z43-h|5;@cw-Nd}kD1Xtj_Q$+*Y>D3*;#ets*OOLzuuc%qbC~DFdRGf$&5iOr3_xiI znSXk2S+HEGe)1L98W2?IU%&S;>WXXfYfb>P6qYrD(L(3sVx<1{sqA&tsnhCp_dZt` z(3WMi#oM=TXT{$CUbt86KHnhTTBXVwcFby&M7yr^9p9T#g|V$~B;P((Wx$nZ5G9ys zl2qz9JfYzfP!bR;A3$5Wk+F@Ph@~kfNct`G05Pm|*!EC*8!Q6&6BL97T9?~j&Wkta z4m7_c>?m|GVA~@7Vi%V-7OrNHS-hc|Z4kUB3$? z5!sl;>PT|;Xlgdkx?g!YTbUcEpz zb7u-c0?`K=$92P8a^9sn4(9W{NUCnND3H}Ccoo#fc_L-(t{1Bmb$zAM!P`Eb3%r@>{Ns&A^I(-*qVkzIo)q?B5hH>Erija+3u^0{)ZqlU&;iwO_g zp8^ZpP3(;wrrXU=JhE0m5o?o;N(I0?d%|nL7T#4vHz=>D^yQNE^O1ZwUlE&afLtPB zd-Aa}b)JMOKo9L1G4m+o7SfDSh9rVVU`X;aKUww)q~#|c1d+O?=8Ur?WB&-0)+g0dh6h`uw ztq!u!@I^(^Zh-j0hm^W!CP2{#Ldtm8w372;YPruYQtB45ot+e zqud~qI<~++LbB!d@gs@o&PTRH&BV9^St2xn198q~*!QWK?aQ5#gN&>a=xi4>eb9*? zL$AafqajYAbGIhOB_2>8{~l zfcwGVJZl7VR<;7E!bV6hFm>Kd^BHh|*MfWqO7lpAB(x|-khP+#ph`YIF#v4l2OX9} z1{A;v>s0xFujBMU{dOdWcJrvHl&|UQw9MM7H1i<2o#MQQ)6G^8LKbw}${B}{CQhKfo&#{!38SZ#ki!wOYlQE*&-;<-;%(S@V?W_5FBJXiF=3ENpS>(s#aa`9LS& zI$hr9z*cWERLb2aEKfs62V`KDOw}%tuqcT!UnX|$BFyRYNIvj~p+7E|D0@#tT1NE@ zybO8=y_b03zs}<8O{V)%11m%R2naofrMjYS+*L)u2$`P4_Lgd*YM17H`X6AX3F*(R zA7c&6d-&xM?rV5P{1XNJPYhk&vwNz2mc57GIaEX*euhc2=3BeD8^{;sZQFSg){Pw4 z9gunJnSZ!vN@`7C!1;*2cjr6*udO8yc@CseIt@Uw(s#9VE!_F)0c&^rxplZcxAWEi z(Lth=d83)8Ah7^s;^;=wIEQ78bS$hSx z1RNBbRo6nbYreM3M5YQ-CRyTi<}f@ve#)sRZ5k3A6=TQ;g1uOCHuw3cdS2=2>i}D} z@asLNwcOXelEC0ymq>$AwASos!<9rvfpI>Iz#lEmVMDC6^XB?BT|&V(^z8iVN&wed zeRD&p4+|i-ga%;ACUQ_+J)L)2rh$I%o#C80nNliw$SMQw=v)sySc{kR zmb{`8P-}1=>}+?^X13*2PAr`tJlQT3@E}Jp1)Rd*9INi3DJ#KeBn>M}xNVjNFY-Of zAHnO-8!UCCQAe zb?>8gt~ACW6$i?On5n7ik!}v(kk13XiY%%$QBMIez-%Td0d~ZXpbYu8L4=s& zll05vs)%v9AUl4Qu2G>IxL!CNF}UnrVJ_?Z`f7uI;tIUrh9_l)`lq7}xjx4z^3J7u z7zfmJH~7A^E{cG`P-WuO7fUm~Itn~RbV7a=-2W;?(`=j}NboTm2>inXJg^T(?JzRS z5I91Bc-F?NXGUa!$Tw3?i7&ysccmp>FRJKf!#ZX<0tf zAgE2Vh}}EifWGxm>2%o@KTy+ts%4cE+^7H;on+}dE0fa3Hsw87-`)qTk+&QnOtM_? zt+!&=$v)yJXpff>Xb#KStfJgkWmq*)%Az;Q_c`Q&krH=fjPK+Ag(~ zFaoHW@J6*2htPBew5ia{lEDr!NAOAhS^%Bh(Cg3m7m9ohF;2ra3oj5yfHuCKaZEeU zaIepq&jED^K!X;iwtUhG!bb~?h?TLj(&tz5PUpwRE~}+BMb`)Md!5%nD74PZF(`_O z8pE_wdrEr4#;f^I3VC!fsLP$Sivaf=?fs#{V(!3vB7*yKtZt~ z{P!r?Kdi36Tj5|R1Y|ct|AgNS*v9^S0PnYhS9BO-@GXzYMye7cyYZ@Od^%xzIcE&V zR>!(58a5`)I{X`FvgO`S>gs(eQG7^{-|iI*61{8Y0V}kK+qjy=oXFPdv&Iw3vDADh z(o}o(8%L9svtJyfn^u2sfJK2^nbl7FR)v?A&iOs#xgK|0p}q$pjFPN&{X6o{ZaF?< z*tM$4&3n*qAQ0*TzTb56L}n#MMeJhlFv}{5mL^_1>}xBd_UPxW{+-sZt?6{DP>kR9 zZ=BP9wT-Hc4QPr$>=Ks#%ma*7(82-(F{M;ZFLzjs!p_l-Aoh%lEAEUQTHYm}dX z&-wZEo4fW7X37R#0PS{eOCF{!5Z8`{h?#?9YrYQl~r8e&~K$ijlOn4=`#wxgQ@xHq>+5t}$x-*% z?l_q^j+H$4k=fjAb>!zgl^1+!*t<^aZQQq3vk=ki7PLE__Zz3Qx`etvrEY8eD8gxC zK7Q{vjxwDtRZYyV8d7&2#SGV9!H$=-$QC%qSy)rusE;vnb0w;vcov7Z?Wip}zF_h`M{4 zuJ}@^1JT_pOFv7um{gjf{SzWv1N1N1uf&t$J9zrpHkx~@(F=eJE3dz)H@+lkjOAsMEyCMCkkDlyx z&`>6F7t)}FDR+Z~k~PH9>BFwbmTz_=siK`3^Fh*I4o5$7#>I;|)38-;Ii7<_q%~1= z-D9?k;z+yFM27-{{b=?Qwj*=PL2 zhbj+vhrY-DfO&CjRz-jqta0BWqw=!$=Iwdcw)(>HZ&m!4rhwJ{&-Di~li~UsCr7O6oDopf^*Mh6G_m1FmN~J% zaeDg`3$~Y#x&jyE(o}#eLAm9cior+t?NY?jk)xn&9AFp=aOh+@63NKKe0>8>V0`?~ zVP~3|35t*OSKM^uiHh@X-Se7?vr&y#93mMM89z3N;(w)-Wv@o807HALjAB?bt?P{! zt6-#3nnBpR>lx}~f;B1qU)LS*;H9Se?XtKp1QWzRaRM&FGcWW4=#xXG+6`;$!}071 z0X`U;o&3h)6v8_&+HkN|S@MjA()OJ*U1#p%3IB~M_1`!<&|4NuyBq{7!UP{%WF%^K z&9a*uoKNQ&ASLfsbYwKHF5}mb?mZ1^cWe?3Kq1nuP6#|J2fM~+0GKf+|7U&R7~K?h zu%=${tvs1rvXsfML^9qhqOf}$!a0W65ik*!(L=l)&|V&#LP>kRUl(o+O?Q#ooL8f) zV~X|24orD+;(nB1hdCFk$9lN4KNOew8kgOlbg9`0i{5G>XlB_46E+K2pDjK@ZB!M^ z`#hgyuhE^ziLp8`br3^zpDp26@wHP{e)!tEZ(l4H_r3>XH{EExl30a=MaW9pQeIBT z3N_SC>e$*kR5EY{4BjY@=sKR`zTn@({mOT6gV~?^cta!c%v*OBYsWYY;9YtTGJYK` z-S;?0h)3-=D&>JXPqBkhV|r}Zu9_NG_M2Z0tk{@CqM5zD^CuEIr1NLn<1^l%S5edL zmyp2ylR2eE-F#r*c5MTnf{!uP*a!*<}VU^CQEM zq;0QWFKySrHgL!|*kbn4Px9jcQNs+CC4D*V`871oYCMw6ZYn~jfyd3xhTrW&Ktoh0_ z6d?fUBJTOH=5-W$6tz9cO|x@?vLnvA6~|PC+ur+kxq~Y*_Ku5uo}(kP4A4O3V9+Vc zFp(7(ep(a5xz_o5p!&6GKE{@ICPY3w&fw@&!bz`Ru)Y3k%&vo$XHWRaoH1Qjayrx` z;wOBE`~^XUS)lGi=MxGyQozJYeu$Cv(Kbh(8zidSzsPWas(kIJnRXL|dTpKk43y|s zogNv}&LVJfc57-rDh)**_oTCpL#i;K<$cx5>3x=^eadP97SEncGa`G*?Wyn60CWY4 zyR-Tl1r9!RnX7GYnLR9lbK&ecw$j3ERW;U{v&T9$&vpdk;n&1V^cafkA3oYwGHCx* zsx}n@!=y$jBHD;2Hrq!7PNfR;TSst%PdUcvY)SYu+`qo#Ht+yDkJgMYOO&n zb2O8!IYrVilYlWwR-pm#o5>V=xOaTTfRMUDqEaE!QYK|0EO-gZR;JX~n>08iLS8zF zq0*$Q0{CqXt_Uch4Xuy+3$dfO`i-^%G#AP@l22HlI91*sJ>BD|*3z05K0az$(;Pi9 z26u|1whc%UojhH+I^E7jDjIxoXi9hvgEH=kq}hyjz+1kYDjN=L-!w2Z((5gM)p?>W zueM^~t=O~Ror;dIpo~;iQsUBY@L~YS1kaG+fE=q;;MTU=s{B-nR-GQ)&Sw7$e1KWO z)c(`z`8SQ{QI0JJXwP>jSSOfvX3nD@A!9PFH%d6@U}-XX9Vv8p6 z|8Vs(?2|~!N4EpX4`!>P*7dqMDxwcuUWa1$UtaU*9XS5i)%fOrU5&el9=-(ndT-x) zz@R?fS-^A*w5p=1fM4~#`76sM{~mNC3=6t^_pa`UsDiS2P^k-pNXA6bk}g^GWwv}C z`u_js!%$r|Zp4!@^hyXA_sJs#I(I>6r4J<=WsP5-o7F}d?v=-gz0QWl`M0ss3!cEL z4JXxz2O-~?_#!M7#{)~@$t6o9?fuom`Yc+^Ap{{Byr#G{tBXvQj3$2J%N84;EiXQu8LUO30M+3=U?|yaNv$1I}2$Z@3Oh z+WhL_+%e_U!;l+jN!G@rOweN!#u^=0^tg37-r`o2BAa`0%2FVh$dwLXqyfnBOXi(A z)>AO&70Bt-E&(o%E{WK6CzfKXsSHfFx7{4PbHoz6qSi5Ld7VZY$Y&CuH#Zwlv%vW9 zizY8u{6mDeg}IDYgDG8o(h?_C(XVMfQ$14gyT=?)k!nj8Uv7N0IXfV98AJq@SC7*#dZBw;idoem&5 zwuM%g8Qq7W2H4iJ_6xna7_U8!=gkK)-Q2HAq2HY1 z-mPU`7`W-oO7aH|KkztQX@d4J1ub%+bgHr|lvk2g>{O-vMIO(8O`%EaGZ8Ynf>^1(KB2o~R)I<$bTY&IC8w+{j{E$|HkeP?YTa%F8zYXCK}( z8tl{Of4DJ!z9VuCHfls)XQ6kM(BnBApY2_uDbk^g#XpQ2vyLuU*Kgf#a9X<&^+d;S zI3W%0;FlspqN|V)3AYp3&ne!kXQD=DZmK~M=|4O@iuek5#~Oclk}{<#U9Ra)kJ(;M zo_7s$AcljRCAKL6%Aws!&Xs)Pbt#$X8=5;}HOuz%3T$RB`(Y=7(TTkD@h9HyTV9d@ zzB6z6ibP75l!XWPu^a|g85NdLKq4RX_TiJ)iEkyMPpYai29o#|rf&-dffH-H@hAqd zv&|2KyJV@+cBS(+PK!3=9(oL~mKAA!eTzs#%Y0PYoXQO3fBoET2cTBb%h0?arxCSu zcV9P?T4t|7F?E8<4?8n%_=uqs9ZV6SyerTSx67fXM>$Pjc^A!k`;{Md2r}c$Yu%LJ zi}F9SjZwRg?Q0>xNCC}0D?=Ia_Nm+s-3*N>B3-KVm(A++(NJ;1hmmVUAKCON>INMV zTt}vCCD&G7f?B47+;ORyxhdrf5+cpp;L(b)MkWN9#G_)hr&Gmn<*dq*ap`WzoQ}pV z_7!=e+3iYtvy%(&X?#?X)`XADmB<{L6=4bG#XLhum00dmT1c(9vfYcix?BZ1rp-Sk z9y}L{8>_CTUEUg!4CGcNg>$GZ&OM^5oc(;o=;8lzoCBp3BK^!Vd-pOo>>ctf$E{6A zve_Otqh1N_dv>L(gnH5GHT>D{mh~b93Lx)?xPoZ}LUeXj>MAFR0EQjzdBp6z)aPN; zJC{|P=-ASe-Ihen3c)O{O%(}o4$jK`%rIzU^YLa2n&$<27=hOVCD?ib3D2FV99>pu zMwZ4nfJRMFa@l3WgbDLn$|5(d22348EopAdNmF$&1zZYrBs5O&x)N(_C?hi^PL+Rf zmDSu%;xlb+&!nNNV@p?R&BBw1x9pbfn4M%P@Z`qX#V}Ke&u_y^wjo4;X>}Q^%yS zDb`JBwzk-Q(TB20tlel2ovD_#=Ll~s~Cx}{lm#|-_XrPM|cg?isXc-+A)hx@gGe&#utWqesMm*M_2GpB0fJQ&q zN98*ysc!?8Oh#bO&jF5UsX+RD_+WSKW#(&R(*eL&Poy#SOuJgfAhkhy-VC>7ye2#9 z&PUW$nIj+4Bt^!D{Vod6iOi`#;K~pyqq%F!9-y(F8FGrKAx~CV zu|Dc?X4J0J)JH;te0($K(ZbzcHjCC?UkZ)kl@XawOQ~;a`G0xZJ9M{K@>bZc%Yv}l zXyukjBzwPfvxTRXQFgU%{MByRMLRST-w-y;kE$X9& zpP{AxNCtot7M0+F7DOzaUv$Bc<4b1_@-mjKPp7_RNQFJxz!9i#hSavDZGt$B|O<(%o1@H>0UZ}vK+E7;$~^prqKs9zh?Yg12# zk7i>PcE)VpXc))xSksrZ7hSWBA zGDKq%?&usqO#=a4oV(iQ1wGTso9W({*9I@lawkhvJzeiyRRM{T!L7u~tDE6wW6iUV z(o-y{l6-yXZGHv#S3TDII3vCMIVxVX(3{p??Z@US3ar`js`N}sjmYm;4aW*8l-Ct@ zV^BQYrR3EV1B*ypWnsCH%l+!{c*#u(#`rbtL$nm?MLU+K8Yy45oU-L=I7w`2QWi@4 z&bzez*_Ic1gw6w}`_4YY;|B_{fxoyWiX{5q+K$JAj;Rn#Z4O~z!gn?GsKYM@m0R|z z%*iUn5laj60iS$B2OR({+%1vAuye1P5NX5WzC2)WnE9iZm zg%n!Uy>ugXAh=jIG?nhCs{kWLVuE%2ROUJ7a_Fqqym|0JIqAWSfA=0!@6?2#y(=Go zfu4&XcFZk(pT}!Xb7FiM)WJk&3n3Fxb_l-A2y*qslZ!fkF=j8y%mrQ`o2yR5L4FPv=(pA|aaBe=%?UHjh$GK%(z~`VHYvUAIk44? z-JSgLAZ3@SuBhx%t&KGJEtNx%&bi->iAK3%bgW7;aLbL^oia0`&nzC-nd8z7FVf5! zwWT0`rb_R^}wzL!gpco`eRPAZ0^WNbv?q%8IRarl$%vq$`Xt-!a*#^s%k2rOKV4 z3RpLc$T~53v1OUC!O)QU*0g3HoA(=1Ke~-`Ts9r8-JW=7pf4*t+q$V|9+WTa(f24_s~JVyI_fiX(Hy7uzI_heZuI4UCF7v9U1<92`xa z98h49AVseOH54w!t!xZt_A>}W=&kZ<;j4;yKIDf~Qym^#cGzz1%|a!K0fN1P0S%-v z44s~Kc`nrwh{erleamn~ndf(I5I8Q!pZ9T^_+f%sb!>}Ar z{x$6o5GVNG9Mr4LCyG0Qz2`%S{EgEMzKu@lSfpwmKUuoBw4}GVRI(;SedrUcNx%2T zKPV#e-h;sTJXT@F$s&~QN?s@sl0q=+^Lpa_h>Q2Ot0Gu^iXFw~e8H&o=EsW-Lpb5& zy;(ywdaF>OKKrdavj~Mt*A;$8w13Ah62VsokJS_Xth`IH4H*UI9?)X*%FU@M2+KiD zE_U3&A`4;LLJTAi$cLYmm~inq@VP>(IBW0Uxw6&^^`6PnlxCNFK%+6$_E?-EYB(6n ziJsH1jK)50&LLi(99!5PU9#3NybwF4UOA!^3zF0}tyS`~)^$&0Q(adsS2*|B)`_Ah zC=rp(`|EK5IA&89eJs&6D|b)%NIOjxlCccx@D5MW)KG@{qT+PZ%%8${$}MW?2v)wQ zIUl7Zzc`DSu=BAL4{`d$9DaT%nOc2SCY{DTO+*7#sAWislW=<>lJDWAK><=AU!f11{dF zA0)FUhRBBKE}u6 zp!f{zOkN{(`~fz{3aHaq_exv^WuN|#Hj9hLj+5wygK0|(J<;iEZW`!JEXKWQx_3J- zIOEJPOfC6*+7}qtvyRK&!ElaaNzmdwjQmK2PGY)lc-v_5;;<@mAJN}OM}Azp08nn{ zh8HB*k_%Q2;f62Ka!96?g{rtq`8-!U2+J;9xfkkcM|m#wWz$7vOR?CT2C*YvQGZqJ zs1;sad+Zm%g;9Nve*vze?fm zKtF41?a>zfWd$a~Nap(uJ|Rg&2r$eJ78#;1=|cGqP9NEZtQ?n{aSztA<4bOb<)~(t z@EAI@>19}IWXIJR)CepmTV(mXL#ifSxq)|4=o&S^Krt9IvtaXPQGTk;U_t)%r~Ov* zw#~2+wMY=L$S~YwU}YZ`<}$uq>+{fsz1eW>ihoLFJ$%!_h~IfC?&ooQ8ReJ86odf*V_O-e!#8%-d}V8js`Hc|Tz9x3cRU8#7{$=~ zh~XeOxXV|(!z&YAj<|5(he_uj^1+|WPir2N=BlYaww}lmeQ@FSre&!BVEt&MS;;E! za4xt{Yq=UgyM}`dNV~wn$?=CkTh7)NL^5FMUfA>aYlrY{Lbgw2>erq$8|6NlIp_@e z>lCbj$}%+U5*%J7H9(oDvf#IGdn?M&{sPC5E;U|#$;Gn7>6LJV+Y)B@xt`Yqvl?8( z@0>5=2%cry;NV$e}_*TS*5VD6w*kC02oxA5E4Eg-z>Dn@s$&d*Nz`X-1t^WsrK^=L z1*JthwEyfrcXN}+Pclg}Ml#3fO^Nf>@HF1{*9TQh=7oOnA*!wq5DU+=KW^k=`!|C|uj|Gr{tqbfqTkoI! zckb1?4)q;bQef;i`oFtSu2O@qdp3sK(T7+CD8Msfg*2!uEjDc}IUhGZJ3idkMBNsZ zF<`oBU2zaLd|uPwMbjYAjJLBmxh9I$LLYVtA_N`?p8nwJPL1b0tE9-lsyIt1BrwbQ zJyHMXV`1RGzQTWGQVe56{_ZLHZZN%QB2LDqDK8m(W$(*}BBg z`A}*dt23aA8b5Y3d;*JsIKS?NdA39Wq1g?dmN_ylaFZ+Q)C$A0AUU@Su{t{Xb>ToZ zSidSRKcrCjRov$1Kk#i^se-@)R|{}0qezbB#zZNPU5WV@Esj~=jXlo}iVSDo#;}z! zra%jdr33=NL?=a1snXB8jEzVdBY8ZXp=#0<=5roZvIZJw5c00&NNsYy&@73Izz1pO@g#J^Ka z{Ohip$JkzAZbnJc!XX3m{1i+m4yU|mz=56qV)R$%zBGw|BAOH>uyTDEh_NK9ul%UD zb?KXsXPMby>-w>L7IFW0pz<3>jh3$V2QH#EciHvfol}Zr;kZQ2QuL`Mimq*%(vK7E zTnJXUQ-I(TS$G=D#6PyOp-8JF;c#LZrMCF+7G5Ovy9&{df*1qzu-y+^vTf%@zSI4U zyxbZ1qF#2a{?6l9=WjAgM>i@A8ly{wq>l~XRhA9rT1(`95Y*B(ujJka2Ott-$}U0zr+CVoSYID_180uJj4 z=i>G8T}k8T)0peRdtDFNxBSx2$rf13w<+{(9xC*R8y6?{Gi7|=LQ7Dglb&y6L zja{AwIAE!2GsNe`YSVN0a7W_E7M0frcu+9Rx;}uY{F(hN_cuSIo{tHsJ|?(BRNmbh z|E_05mH{I--puXs5k$T1WuN2V;2O$o_iD*hwHOzl>b5@@Ua1$-(b<^VV6>>d!u_hA zKAv;je*+p#529?Qnnw>g1UmxLWkox;o44xK&c6757o?f1kmR72p#LQAt&>w)l?7I{ z`%q=8pRF5hAd#!{uD8qR9b;jK^$9Cp@{YE(azVtZl->8YyY;eZ`TE=9@a%i@OBH+( zWW5x5=Eg%0m>QoSJ>8_ASWVp3Jy=|b^7Ik6^7Fl|lC?l*d+LK+;&ImGERf6m=^QDo z#Wt;Ff+jz!A8!Pq#`sv3(C=M~tH#mGFqY`;0#O?%FnlwD4kcKE3@V%d(2OI1-K z@$GeeixmWJx{?a#XiKAOlf6CK@xn6?-eH3@L`Rm)ms~grGm}J3OS7 zQX-O=fdT!x{AHm_Zy}WC*#`$%T#BWQ*F+Ct`xCmC+Po&OOB+aXau?Cbh*i}IK=C_a zOiskIW7V>qvRE?bA4BK1toYtopu6F8#E)^&1L(kbnV=TcSGDdIjG;^z zs)j10AfijPNx@U4PJClw_S+ky<-biH1;2*0aY5Er|0x1V>feMw;>7u1EqWZ`Zk|by zB~vhnq*=g5+>ajRmzB?+Mda9L{h>at#kBs5B)-R5^}_l<{Kt}7hQ12P$AD~M7{@ux zQ!SL*OWWLPS|j(`pb_$*WXIbG6mljRre@8b{o(yf=}By%5U{JF zQ&G@E<%XNzZnYH~KN+t6;7OVq!*fBF^iueTan6cP;5c#PgOV0%ut$c$CAx z(>$^Y{`AvwfI=(JOZ{N1y7Jdqr7%&$a2fBF z>#nq>#{1;F?q8G|x5W!PO}LvZBlp{MOw0*el~~=E(9cjctsvSH>r?RUUAIJ1tKOQa zv-1%nk=xTB*MGRn-?Ahsme)=!{K@bDbw4gne^BD8=E>QXJU7VZKypKLhvvgRhq-47 ztA+lput91G$H#QLQP)-x+C!f7yFScl15w*1M<37_<z9RAz>rK?|n97cY-3X|R_6Us%aLV& z<1=-aE>gE0O6wtxaP0}<6}@I(>ZqYS_Jqr(<@MpGTfQ4$Wsc5LP)pgA({V&Q&S`~3 z(s1QbRYjP;Q4)EFQAgV$R2T=kbkgaOBIzvSjoRXK@1y#6sQFLQln1n^?wXn37K*Ml zf49{?FX50F3tiR?KUq-dOwJ#XhEmuhizbFtX57>7{#6ZM6D%|~IG`b%REexkh>zFh-5ctiQz7mE{MSNG(o^O)U zOdmgv5E8~^?t?29$ZM^8G)Nj^@u6LGqTb*Wp{3Q9?nsBuda6K|@IgG~^kc0Fj7+gH z*!oC?xU|ITWwi7=#Yi@Hv8y!Ahq*ZOJz*!Xz@_`HU<%`%#d|2fL}F+NM#_o)Nvc>m zCGJTdBB-A7f4QiHxlg9LK%VB)-hly1=w@7PH9Q zST@ObC~Es!%)iLS){`;#L6mt+_x$IYelp3;m^Lj-g72O#+e*6bTMI1$XpVXH#L6t1 zbLEpK0-^P)0WAclpu2?rfbWCW);hr_xP6Z!iVI=2je9yb$28E*Gu{r%pyK@Rrn`w#ntg}o{uD@Ewy?u z&Mj{S?I*A2>bcaeezDLX?9BRL83XwwAh}VWdo?*KFP57aJs@)jWMI8*eE+ZT-AuoD zr}@={pHKF~ z?6zH~SPd!28ExsX2@dh)kO;qG>TnP6uS97^kYRVubB;y@k}meaAxCf?%%t$&&QE~# z_divtf1{GmgZ%qM@_+0KZghK;fbcx>G?8kd#Te}@rD{aKYqVsmgZ7_D5HEez_nKUT zoTk<-Ua6~(b}2KqtY2m2Xb2nT`Yx@IQdrB1A}2G#DwP@sYRR9at;oQUWh3)xWZeMx)<;&!YUHQ#j4MA6MP&(e^Bkz<9 z()MyCqOv|VN$AJk-odZgccil9Z>@c)qu$8h1`&NncAjf^D0jNUeZ(2T@$bsok%sr# z&yL!ljye~gHM+C}XUOtqI+C|G7jVth4Kx37)c7mM zHqyD9tIBKX5-@_W;tSGasv`4!WRUz7*+y1gc>yA`#fR*|Mut_%b}=ueo(rD;!Qptr z?HFGgk;C;Nsl!L_I!#)@8>zb43L*62GS6F>vAJ*ig#KPxsG<57?Id@JcOAdDO)ZC> z*Rg&t4PF^;m!WI*nkrbmHRAW$kAz9O9M7<{y%@twG61H{2Q+h;me7DORm zp7An1F}-C&QYSXg;I5x!n@9GbZjN+z zjKCOFy%KkmyDL3ylz_L(x+j#Jdf_8oDeH-bp}e~mSz4;5f;%>9Mo+vqrvW&~3(pOe z0)@Q(B5%5vRCmp*Vpf)KeCsWXd7jQQ(+KdH6p+SeFz>vtyM;E>R8w2X$f++*bi-sd zyxN=k><=@VxTtZ6oHR5F-t>b7YIl+vDbzna#i7UfBP?#=&pgZjJs`gJ!0Df9=KzF+ zz(1r4tTE^=6ai@vmY{a3gR4IFTt~3%YV3a7B?({^fPt9@MQX@JJCB6>SSj7@idaxiD0JF9Z5YVv^ zXjVX)VDF*!oGgC*S-sjAKQO}SGXS%m36MB3&QZw8ObbciwsnbOPkk}&AjfFBlibPF za=>X)Ld5ZNS{jj}O`dKTU3QUQ`1tEzhcv{_LtItzLbRI^8l}%i-ph2?T@-LW;fAyWixp=gm>-ie}v)<_t~Dy({JQ z6L0XBc^}=mY*Fxz^ZRv@UsXu|6qqbvl=@R^7Pch;&7qnO!l3GrEZC&hGm8~lIR~~_pyTs1_ zO~r1a)W8E9T=|3<)iPR+{-5{OIt;FhU_np!@a0<9Fz<;}J#N=7hq3#)VX)xLtLfC~ z3vEQrJxK8)vjz3qA(h6PaO|GUq(e;I#XJy@+#=2PFF{YC=tlzo?V}U*8*+CEY2{)0 zTTe=wE3)TB#KkIjfA%+Ne_o!Rz%ru_GEhA2^`zk+Xq;t=!b2u2)?X1EYND|LUel+R zqZzVit`$VdAF*tv8azY%V>NxS+H0Qn%c>T|L2guEiGMtmrU?XDs|r+1 zoT2@4$~1yqy!RXL!{>VJ2);&t*S;OviaT*+eHtd44sfOZ5u&SNAOBz68#EjLK};|8 z8_Bo_O#E<=#2vH4)tA?iP2Y4b5+5Ew&;;kFEU+-)!&A$S2tQMA3C#=vbn-eBG2HHP zsWSwaG#kj*HZ0uc0mY3n<4HVLZ6dW$Iq7uz5F|yw^Y~w?3;svMQGedCJf&z5>Ne2Z z$@h7bzhGA};ZFTRygwG@50ETy9oE5_Xx&uhN2|pQZu?aHN?ytf*P61^9q`4TuzZjf z*6K{`YNBTsm_5tsc0$w?BQG^=qV#WUznKb_KBNAP;}uiLAsw<+_&AzoR}Q!XHmjm$`R&E9TH{n^?ftXy z`9FW ziUI7);oHA)W;70gFRKSkG+na_Oi_u{azNM2bSOMbB*#_@+XLP8;E2C%9Q;|)>B$Nt zD|4YmZ(x(#BY#LhyYN(D($OOInGy`td&#;Y5M_co_vbsc_)3@G z6YJH~>EALE!PH%^#B=?4#b-=%nsmjf3_{PcR8obphrD@Huq(b(}DK zgRiT?pUG$C83-HGVuqsigbv=G=qCFG``P&b%umzno&3}>VAYQBGZ^Eb|HG%MHdZQ? z>PjR)=L0Cg12~lG$9dW3d<*sZZW$_Gu<|x69ra!&qYj#i(Ei&L84zofuk`HA#%uNY|h?Na+a z@EC!jk$}44CKbjy16e3-BY_?M>(KV{h8Y?_Sy7sUe~C6zcnnit-GZR+wXD7d5j0vn znE@`#XF{xKW*@4D7u~>xeGf$zb$braumT1RU_?!83y_u*mi{_9IR@ZNuSR(~xoKlM zA;=bBJb&XPO8j-M zN^DOLb+b?(Bq`D%i19H8`ya0Z|M|Ky6h!T3&mUh2a59me>yLexN@;h3kptcC2?V-o zP(e)IHSlgqnF1EYc3l4a=#cKl`XpH)Ve5>{WpoEuiGQG+%or;OA7eXxCHvQjK|ot& z6Eq7HZGT=*&NcL+e+I=m_!qE8C6Knw_{XJ-1it%J|MyJTTLS2*B987Cfd-0I+$IfsREldT14mkOE>AkuZ8FS#|Q8bN$!FY!nwwS_8M;wyoR?v@!- z78RVM9`!1;`P7blI6p|KucyjmbKeg25SX#P@eqDpJLg$MzFGboXF$s7yyY;fXM`3r z-7Whtkr?gp%_NKGmAWGoojaksJiE4`+n%l^Xd8U{O|k!{O;3p|L%(mYZ=P2yw*^ROT#{1HHM@gm3P{?*wR60 zQl8P_v27&qC+UN@)7YijMvP+pR>2TX@DqJL#9F9ZcO@#szdP_>lkYsX5kOxB$`qe; zAHMsMl|p)t6KRI;c^DSBl@Tm^^i~_$EhvrG){?87P+=fuVS}Ig0XD9{g(Za<6$&`Lif#pVUKciccKpdp8A~0<&O`ZJo(a^NaVhNb_x@ny9Ea z!E56KJ&gkp-x`ZqNR5!}!|$~#2HHJeHlppGo%2d?NsTzM`prYpNY_|hQESIUcmMjy zx6~0Q`sa9||GYVT4<3?^9{%TIO=BE6_KfF3_uw(Auy!EDh-e27X7K0)KoS^BXjnWK z9CgxPO7}%y*-Tf3GSbGd&UH#vPE>h^Q3^NVT&WiC;`QyN+&pa{u6Q>H73IXr30iRh zVx9AYex}Rj3vBROTy&V`_=Wru-tVHWI;IG!qh?(8H8 zOmFipGG$nun@e8A)t%xdYfM#SFj=#3gnss3je_n2TGR&nZ$8mUPowU6eg+ zqRk!bIN2;cPCi}xUVV9g7;7Pyq1G2C<~+`RipB>Jdrm1OMG!oc^FI@auc>m5&OI~?Y;USeM7rf7Pr;N<~fEaa4+Nc`uLAI1~ zS{BoTc$ia33TLW#688)~TbGWSUz@XFF8MHYLUC%}j~I6a*M6hXT_)quh(%RGR0zGD z2ZsT68)KMq^7O-!BdJ?ytb2HMA; zEK*_DsMefTmJ6p7vr)c8w_h_Ue&~U{oHXXxOqf-SHn#A zVd##FA<<69htWBqZNA2xrp1Y!JIZ^_ybBvGu&$X>C})J>-JQh)UH5MIwmTjQ^AC_{ zeB)No80Nb!OC$0po7O3AHYl}3poQzls1#z*P~tQHDuGwQ@nJ3ZWCuKzqd7h&%hut7 zneTDSl1*k9o$?a5D?rN>+M>2UIm>xI5FJb~_?C~4`^YgRFNO_41eOc*=#p4=&+g(C2!XtTmbkNCm1}Y#93db1^KZ@3&9!X& zw*kKR(ARGzp+YO(ugpC`mTNWQx-?O?9n1GtAg{?Vvy$H|VTQ?2AvQ+v5zzsw`caO0 zg2DS&YAcY`Pk}vuNT7W?-prAiy*?5Sla9n6VNg_T-W7PjJ0`TG1AaaomjtS2P#Jpm zl7@IEC`GD+#zQ0_g4DMMPDF70C9bTRMxd0T`z+cJ~UTOOLA{meO2-Faf`R zyam@^oJhPt;~WC}Wv!N~Kmz@_u7ePyYc3O86!{!`!D2++&e*=rTlI28t^s&UX&!ZTKJ5wa}<|N0^XU8PJrB;I0kd36V;aE zR*CXs$41S8l)nV9if3rEGR5As6j8%!5v)FU2?%aQYZxHH#w<#{G{CfpL5IU%MX*{c zFY8ty;Kb*7cxtr-Rv-IMiaYQ{Z@>R;U-b6nzJ11hd$#?P*rjbtjK?Sj zjk}W^&JSuNA@{keb+Mrd&*RHDi<)v?xZ(pvH}T1kunw>#O; zN?P)p5p9?JD#Y%Zyt-v7GQ{`nd%yxET8pZep#5k*S+6Pc-nP)nliJu<1dBpryjWHb zN+FIYF%X;$4jd}Wj(Xw##n%$jE*nVcHq%*eh(>&?-FHnp7w5&pc&W%znMHYH8R42# zp*BKrFb2H>`Jp8jK-^Jcd^DzC5g@jn+Csj<*_J0_4_Zxz(0pVF+K;o@*0leO>$)C< zPpVMom((W8c2>^d`)5A@*`lgT{WkSHD1tqNoop(fwpyzlN0!v9N`31dlY(65si zB}>OXcS{4)L(P;*0yrk;*s^~0-35sow~@RRcIWuVwN?;rCtQ~gO&GO<+Ehms7L`=V zXADM+AB#bnHQu)mh=6o5;DBPG7b${(^bR6LilRXcZ2*xfTtEcH5S2b4 zJ#<7sdK5xcDbf=JlDAJl$9vyD@7_Ce*IVno$XUyEvd>BO`MzKKLK&cp!uFom)Y62} z(9ppA!9N&f7xJgPA6prjCwA5Jt;EL&rfwX@Cj9U^EQi z+d^M>=S4$HN6)|rz7i`NSWvtde0@4PTJU8V=;^`gVDLVSo`Zq&h?E*5m!36~pcnV4 z+p(Fo34XFoRf$G(POyXa^@@#r{UNEki~jo9o&3b2Z1-TMFIf6Fr{ z;8{bFA}BCc3XCvI{80?K!G@w!y6MC#HPpl-SWSi!nS{|?! zzMcMyh?1$+%$3~^W|5!XCy1jvl%zX5Pg&UOB=xrzGJ6|mE2z5^WetU*yH@d{S?K1a zFtq63X#W#OmC;sa)sfJ;;PJuAWWUefZn$AeO7s>&0vLIp;Xg1p8XgNY`F%5RkpkO1 zoKBn39B-eQXFoLTVjV9u{oB>B(^z#ysf(-Fhx3ex6>Y{gNs*|OrI;@im`)`HrlET0 zBY&O_N0a=g8H_j&1=d<l|MI6k2}O`6q3%SqBQW~y3Jy7rp~$?;_}hFz|vp6H)y zfN@W-dDDVitTpdDIC1^Do<@jgC_Rc>?-z!@{4hDV*wkdTaF=w*^gy2#>kQ^rCX zX$;(AZGVW{TuF)1LWmP13zN@vAwJ#ogy~YjA_)fFGP5nBNNpWTv@5(tU~YtIl2`Rm z>&Zb0OU(JZ+$N7pJ>F_B1rOT?;{p?BV;3dK95MT~GOt`HX)~k149Z`=!qY6RUS(WN z80Bmif=~7<8yQtRs+(6BjXURnYKU#&hx-p42xFeud$YgCW+^Zrn$ce|Rm)>=CG?mC zlHe-B<2i-6_o{lg;~;V}rH_*WE4>!KJp7FK2HvqLL_+a2P++hD=Vn{3=JYG?onr9q zm5SrSw=caw7{qRFXz_v>hYK;Ko>HLX;p@)v#!z}9^tJed)mk2*vH>RKj-Ow6F)8&g(R>q zT&psed-JA!ul38ejLLC(j-u;A*>G%XfktpY(1s@W zwsAN}n*zIQ-q`IZr$6_qj{3ZuPsV})!5faB!P1w53+xZeg%8DrSG+@N^C`lmh)!pM82z#o@<6@NncfI>=#35K-O|0;QPT5@CKz%f1;$-XfG@VEFyMz>Wjj}uM%du3 zTX?%${Ea5JOp|^^w74^h7y0=(5xwx{ZNgkNW9KHK(3P(2XJ`TWsljLME0GuE+r%e( zOHy73r!R(z*!x$y)t6a{HcKfSk|gBwi)_9&tFv00jSIQyi#+34U^_b9aKB1?Q@LmL zuGE7G9f^72rg;a;gI?wL`PLs&39}%*8s#-0KA?Q*;;|!Up6(|Ca_vK7_zFxPw0RYK zZ(a*<){l=&bl|b)*3f;X^5H3lkX@;#~$ z$Wq>w!_~*CI%zJhI{pN{=;Aeh*0cF%_XmERwOTYGb08145H`w723*RP07~N;j!TPPST4B60^-?8z?4#-7bS6%XISH1bpP3+ZFgA+q*V;<;knv#5S zXGCpzI7JfuIBkj2SoShJP7Qe~%%}KadhzDSI0eR}eEL}8UhDCBBcjNp%$t6&Ar13B zRfh#)cWM}XS6w1EL1^B%NZkBMHxaZn^rp(-`z{Um=EqRszedrFnHpjMq1P!9%9Tro zeS9|ZUV56SnCB_8k7pvxx^lz|4aX}~cI8W8l5E9B^MVYVB$rn%m`nte;&iycbr;4= zl}kUkxFluPHoFE-0o2Va-YRawqrr36oUpGpZC&WfWv){a+UOC?JaeK}zs9-CilbDCyb*&HR-kymje*Tpj!@VTdX-@GIZ4` z9dp1b!^==Y!_gwrh#!S5tT7mXiaUc|Lk=X4b#4k;swCEz#=G^bQedn60GHh{iF{WX zS6dg@q?u9vt2HU82pY%_|n{63WG5EsYlnwE+X4SUz6OaSQjp z)+W16KP~NAh0H7@g$-Jjt9(zB!*_hAKr`xkZ$Ro|9*hYt!F?C)t3S zF*sTnvBg?Fy(%^vvUPU910J32WyS-l2#O_^o&$;n!L`E?&)QiA6$@aKidNbp2}WmB z^UkRr)u}wZF$$+!{-dBkD&4`&(V3ozIUt`9rUyfMSxUM}0-1 zKc8)jd`Z`C@vP=}yC8gxA;eMR=IP{{@Q$MHVZ3?SBQaa=82)F?c0#|Y!NL$Ldq(f! zm_tqm$@KV^wX2UAE@1^PhKv#XW4+4JVIg+()3#zNF6(rk)oOHDB6D!Omp2kHtmy_9 zJ|u3vfqPsgIxHc(5qEo!yQvD+OWwd_8f-e<#F9UvOJ4-NeT^2yP86=E{h4(W&3diu zfC;tzgnji&W~3s9DfUa?^zD)N=d{EkI7D~H=p10Q-l^{oNoSR`Q#6m{bw162J1PN(h zR9!FIAL!iHQ9#KFKlpjnq?lZn0Wt-ForIDM|LQQ9>woW%Cb90!jqJ(B%c9FXqMlU5 zV(P~7&jhF@2k#WL`8Dpiw%(Yp!mcOY%<_lPBj_@v-3^2LM1*l}#sIO`iX*G%28B1* z}%yc`W*Oi*R@c6kCV`S~l1*CRf!P!v+F(8F1n`Gcw ziKl{8Cg=Fplo4v`9`F-uR{2}1%Ru_rZkx#+6bJBn_Yycy4am0uABtgv?9A)}cc*c* zOr{THMMuHXZ7T{D99SjRcYu{AOa@2As7wpMi`z!{j$eHU8Cnb#`N8R+QGkj%;90Zf zJ0Pg~SUKE!8d&7@G`RV>9N=t(>Zr|*`s#RW-BFbSn!&foX|>z3vrv0+A3*Qj@Hq&* zdQ%d*KyCL%_pbSdTsaf0ni#yB3g2N+Z5e8{jg1e4u`>(hgrGN>NEoKZP}Gop1>b#( z)^wv;y>TKBTSG)Cm2P>bZI7t9kS=__O(?9j{j?a)j;z2s`?_^MF)0@z%Oe8oD`Tc8 zFn)hu1Kb&q*0RL_k_zXXFGQrhN$KL^2iGeipPYR|A!LtIe)+?UYK zVne~nquho@AEI4ATw9CI^7P@h67;EEMaC@QtbzY5H-iZ8^%rq54FPjh!*fsF=;lsf zN_n<%$GQPo9-C;Hw`D+KjZt;IR1NHb=uvSj58sDs?Pa@-3x3L&-t>%G8pyy-I!D#@ z4~{y`QfmWP7Q>eCTn@k*${^j}Rksz~t0VX+Z8C@@$vqJ9suP@}{-Ee>e{7cRS-h6S zdh-wv;>TW=pEqbW?LmBz9FwWyt;(=Z9a9d@xJfutp|>=CoI4=mp*%zKU7=!sx4K?O zkKAr-=I_NR<}RrL`!3r*XjRtE6>qJFXj#!FH_D%%4OgjzCTv>9Vi@DerWk94> zCG%&G{K9fl?Rq`oB`G6?ACU2dNJGa0rBMM~lB#SFqI-1!pxALU|C6oIKBCx9eUPc* z@(Q~^fho2u254JbA)4yriuulSUCH5bAYr~`G$vJ^u#gQP%plI6cv|^RPXj)gR;$6S zOY`p>7X9}`iVY2;gU{V6hQZi9i zebg9L18M>%^0GSE9dH8ikPYMzQ1KteH6;^hDxECV0f}uF5m+Q3p^AXDf5IHkK^G{j z^aJVrzDkY`;7}U)rQk=5$zLFlLmwDb^AF3m-E%VzV47NfB2Wus$W_aJuQkwda1}Mr z!ofKQ0Q{TPn_OSVO*W4pIqetTtZc}1a*oe#Ow^t6|CzzhI&XMai;h-jXjO-Q>2q=` zaC>5EogyM8to;OTXWe}GmfArns~lt@1%_)@l?xKiz}=D5WB@h`pu5|MKldZ5YC;(ONr-DeSYrA~oYkj51tHH2-J8nC z5JthhSpnw*&kU)2Clr8-+!a7P{!vo!1pu2sT>_eIXOSjELeL`UPLON&q}~ZE?fWj^ zp%hq{Z;&V8dY&=YkKEN|gE^!O0sCd)IyDII)2vlClkD7u@^^1wf1g=8HKJSAElH>- zN_R2OHwB%5+UW!Z`n}9TPZ2A`H9r#0W3iubD1MB&9pNhza%ofQH-t5L6V(5u>AU!a%R!%2?j5@mA=RW`Yn>bMVru|Vy|g|X{@PV1FxMNQNpMEh z_2-76I;Si&eGGW+rytAj0|UJNr;WfKo5*H*7i;=rU)S^~g89)5Kop~k=A`rXU}76( z(Z!melE6d*$O4(pKg;U?wF5M)09O{(^z48UGHc&2rJmb011<%k4+%B&2?MK6wjlUs z`#{Fsoq;O)`4{#(Kn+lw?Opzy2XgMzF(V%^^WQ%dqN2b1P{_JdyXEb+(QxNU0X_VK zc0d~fJ%VfgSO$_@X6R@d@*t8xPs)_LyUG@oVuF@h^8l>C^IY|!yJ!AX0hJYg`D~?v zFA#rImFboJW$%X}9a&88iwGI%Ge@+J_$nA1gC7HE=)IY-N)6cEFO~i3E`NkKeLnE( z{zyO!A9H2NEIE=gDT9yY?;S8=(M|7M{uENJXMvoZUVJn%-c)x`q(O>zhZThO^=ti5 zekv=>BgihKh{H|SObdjLwi8;c_9z&nS!t=26%#Q6M$guR&wJ;}vABnU$i{u^pSA87 zwws}a!;xE46TQI_J#!|?F$oGtf|;CPZh&Y;7^xa3&J})1W9=aYw$=+K=E#lfouC}l z5mhrbyT$;yKRZg)0$@-9!*&}e0@ZeHt1~F@b`I93_Q6JM3gpBfIryHr=0Q7wVuIxL zBR)eu9xBpy?uNuahb`8W2+syHF{*>xxtF1YVjloHn6v#f6l0{0I3d@tPVf9>~Ftjlu zG$wQtaE`2{il0&nppUjf+&^a*@_%Q~Slb}PZj_Pji7NurA0Ix+ zD(I9{>{wX0q%E*U-$2QiBfBI|uUNXi>v65ZT|1@fx1KsYyt&Z>MkqiNJVk49Egv#! zOcVF^x4$ZuBHjD?KHg)xE8u9>lBO?+Q?kH!tB;xtUUf=@2!j>i8;W5N?7!m~WOyh* zWK)S9MDfe@r@>eS-38jh%gSko2>NNEQ`DHTP5&p~mQho^hF%%p_Vh(`eAf&JbD*0+ z9^7^sk4|=gRS4*m1v?ZWskc{z90@D+Ba7FP_Cp$E{g^tO17jc%6a05#0@Q>Ueh@im zt9od3YuTS11Yb^B3oJ4A4oH{_C!ak5Pc9FvnbS~d~&%z50vDi2?NT!T|{0bMi{ z$@9pk!^w}@Ma}x>uACEZt(iD^c<Z2N84f3jM zW%&Z0^tG?-J7Rd}4$=-x< z-Dpw|?Dw-;>TJL(BzclO9yAv^yQd57*1!HmV2i>4hKec4BuO>9i>Z*?S4o&;LfFb>=aNdy z&aEWIEkb3NYa~X<-He!gt@6Px*dY7&wXGfisF90^HjDQW2$k(nzuS4Kuw zMp{}{K~`2y9&OTd=V27&=gnNtOfoZkW)%9Bmywp4>GAs-QU(-d#H_^aB*c~iaYZo+ zMKPobU;z-5M75n!_)`-TmzX0dg{mYghju8KkE$;rA&x35IcE;qI}p7O<|s-o_;$?} zDJ2It>7`!EYcE9IlF{1wpj>6=E0(t5LCVFsvI|ugsV&x7rmMGn#X6()#wHs!Zu^^s zrPX%p9lLfrIuZBmb#_1GaoF?7(PO92ob~qcJ?9sEDdci!SU5F0=6Y;g{0|8?Qq$5i zZfD-PdoMRHzo78pqoT(Zl~qrxYo68Cy>4u3ZfR|M)6VR9-`m&!VPJ5GJ@IjJio@mc z1v9wBfW$vw{S(<|xD-)b;^-dCk)FXNChmiZgyI~@Z`VjI*y14V=B2cB?FAX-tx>lg zl+V>N+{scoNO>i@Pd}op)S!mULBHm{jC|6Y!jpX#8e?HaN5W0)rL1+ETC3SK^={u|S2ywo z>~Rpga~i5lo7_niRv{qtvd|3yw-^Xm?nDwErty4FR$~R~ z2$+cVKtMJ5BLa9dp&0_q$x!!d1oVZ*!uf7c9EpMe!^yGVGRF}Fgdw1^>tp{zSONp@ z!46xh@;$h?k9fNQ`g6i(|A_Yfl}`Vrz=hfWpI!f1`>q=`n}4-Dtzp;HB?*0@HIG*HotTQtZZhtimFHN+t#y{s`sP#UqCQFpX6yHKzOq- z57k=(0Rmexi9mdgXIUeFZ+zSnuHJxv$r~-D$@FnZ0pA;64qXWPUbIOM0hG*v-?|e0 z(7zUf&CBB1PnkT{OJ&4j!e#YR&;Jz!zlG5qgNCyw+8aa>rx7q{lpSbBLL=F7T?F)Y z7Nkrs`hbAfWM_dNqeQq40nsE$SdyPNIg%!-OZk!53yta^Kv(H~*Yu((jA(f@!$p4r z&xuArOgMX76yX;NU#}%4h`z=5?!SZp4aQ_lO%I*9t*K`ql?v}R*Vap=g1>@@q!4hC zfPmU!GM=-h0RibP1(Q<&l?a%Vf`ajGfkxcX3+rt1;1JAZHNw^bsyx;HhQ?ePcSXqW6`D4Ig{N z7r$w%z=&44>R4lzcB^8rUtu6CbL(ms~mbT}ZgP>-i@3zEV4V_qa5RPXuM%E`2vdum7I6vE^8*p}&-Y!vgaNWW; zaj;MZe?r6r3SS~2e&`pDsc4*;DU9afShyD{nI71&-6HWXoIU~b*K{Rp7@UiMr6an$ zK9u{C@hXt4W~QozGI8P-`&$cTd$B(%drNWbm97}pi~fw&Z{wokgzm27tWmr5{jF(n*9ht92|eb^OK&|hNgKGH%*u5Bz{nq4x5Tww@Jg7KwLIFF zbmVws`r5?#TgoNqDx&39mzVb!9h%@~uH}-B;zESUFwsgtui3Cr`id`6snYTIqL&MA zkEZLYZ=^WAv9#W=?`ju}WX7?f>0XBZmGKpBY>^4S4b@BDy_j)BwsUwXaZTFzQ zvaI*dL=IoI$4~1qqu><=e2jA=vLBz2Yk1kIaymF7TFU=$=`ps!^BW-}txD_pp59R% z;KV_SowUj@NnN;wlhD&>FT57SA^B)sX#B~H#`Y&h4-L04?7IUB>pQH;xMwhfg%gv% zX%J_g$@W?pbd4m-@2r1e(&Zl2q7>s&n3F`#X>Q$+vybRnAlg#VW=0!)TO97)GThBi8C>Vq7JZScY%v{1b8a*BdWqXARk;AI)=l^emhr&84~V5*=^!mTC>Tp4Pc1YJ+_c z;5#Y2APUUNk`6tawZijOoLgF0Rdph)S(?J~HvLegWY$@g`^?8$OVvH%P_iw>@9_jV zySXvOr4%&^fpo}EB-sZdY@F9(s>7As4NKfs+fv#ePvsO7?G(_9v-H7v3(H3W`$<*B z$0!>srszjS7vPnUyKnAvIoEdjD(P$=ZOJM{$oI*rpd(EhdB8MLERL?;+?ah}+x`8% zo-X-K_tPg%uH(P%e`kB^Ns(gk^71(v^G&o=1l9gb(L86`}JFP37JjfA2aB?=?6G6Z}u)JWjcNorq`AdsOI8Plnzggc+zFa!wSt zq-pcliaTwe*gDoeR9_616lp9T&=)2=rwHvNREZK1!6D8&uj{Bj@mSat^U-!)12e)> zGeb7_)?R0a<3?|?7f)AE^>Vc4|H1I}YYjPN#&RUHi=RQ4JT(i-$d_Jdn<67 z(AwKM->x#oB{C=BMIcFmdG6lnyqA~#WlciuFvsWF?D@qsSTkmnjoJu>EHDiD$sg+$ z&tBG^7?<qnCQ-v7! zv0PZYW7k_ocM@X}{(&h8>MnkW0IL=s*R}wLH&cmwrQ$<>_?G(B618P&e$(?9`UVBX z3^{U(U(%&g3r^Chc&CX`r-@!m`Rt1_Cf{hxw+Q+nK#Lc3lR9#(Y zJ)u44epK(hxVBGbqDZP>*fQpN#2n@Y8I@Hutx4|NcYHBBl9tQKZ+^Y6F3H;X9QKCq zm5Y}ikPXucY;|DmhkW`|=)((hO33l(-k!aI5(x=)If8h(101|`nU~>-F)N+>x~K_U zdWo73Y9NBgQ=cfNW&bY}gASSTQZ1-|D+=7dv2_wc$C?l0diEEf?Bv&Ka3AM9TyW-@ zQT?&u#Ma5sU+%7&xod#-{+YqDC0xa~xf(W^D)=H`K=5u@gzuYvsFzY=r<{_ktq^x0 ztM`C>Nq{Je?p&GIN;lQ%Q8yqG;zzgwk18MU*CPsj_3BcVmE!XjNnDhfqL;8GDqL`3 z8fsUWE{>_gm0SvR@P6RXu5pibMbRzx{gJOdn+ZmlS1alj9L9O(5hAbDqLg4rY1G^> z0QV|@aIn;6W5J`S)Ozev`yDo#JMJEj*3TIM9TbzLkB=cBY2|ypYL7hvzN@=!BYi6+ zb0^04DH&J{@`Kla3S7WiDs znapjq_0KKMIMJ%sZpdqym&WWU`dGLU0cAD3R$GfeCy6U;Up%t{(~O6$v4&RViOKPm zt`A{J4D58SC9T_FXTWGdgQ$SOzQ4qVX;2nlwfInyK$3G z#D9Y#&SAsxZiEn599sp>zQfx$y7=k3^jCNvpf4!EfcSRkjUvLU&UZm;|ot96N zpiwVxj98IzZx=PTWK~jRd;W4qrz~ZWV;zY#jtlOZ)Pwq*xCo#*AYk4E53M$A4TT*~ zLcA$uVho(Y;)(!zryMOn^nQoWV=+Ds;d!!XoPoYPbuIv{Lfk^Dmn$AJgnl%hPki~f zKp8)gg%&Vt?xfPDaG{I=aT+Weie^KxZ+ImB6jmq+U4ts0lwzQt(F$kmzAo6zO1P=M zn=6vV@*PXi5TVJzN)TV!H7RC=07-Q6D9nfuClt_NamF(|^BqZ=F@P#lmpzUE%`|vP zAIl#O!3giYL&KvXy)=;xb6fz@&;k@X{j*R`kSZLAP9#R^#r`L_4*!mJ+caJdeHjg% z>O6e{^BK5bp{JtN%$+=c_&oaCeFvKOR+JKgfE0A;%7}dF&k%mL02a#7-2imqH!DM| z-FW_dxS;|84R_Fq^o7BnA^dy=QmOc+Qgj=Bl*RK_V1;3jClmV61hG`$EuZh_=L=wg z9U`IIAc+;KVBo*a?(P2>?(E*qp4-`TJIl$locwGOi&l}Po*1(>RpuQvS8asy)=giD zD~`tH+pHhl>nL~F(w*yIX&AZjY!3S7MOUTU5$0XX%bK$DBf~AU<5Qir?#++Ts+T+j HA#eT;z|L5{ literal 0 HcmV?d00001 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 +#include +#include + +//![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 m_vbuf; + std::unique_ptr m_ubuf; + std::unique_ptr m_srb; + std::unique_ptr 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 +#include +#include + +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 }. + + 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(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 + +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 +#include +#include + +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 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 0000000000000000000000000000000000000000..40d0a296ac8ddfbfbe50c6c1a4f87b1c552fdef2 GIT binary patch literal 724 zcmV;_0xSIh00yXdoV8R>YZFlrpG^~+ZmO*c>Peo&LxLgQl(q-5*g9Xj(qWWA_N4k0%x%I7K zSF_qbukG81AJXUi0zDUJdn7!m!j(g%p2VYN7nJ#HIGQ~&b+MUw7SM__f4 zbe|ip<2fy#Whz;=f$xM>J9^V}S;^`4xP?kV%NO8ob$u%-V@N(gJ137YDRYN3Cgl`z z^-*J6ciW~9hlkaT)f)BkP=0%o1jR)6FKk-)FJP)rkKLm6U}c!~V5NxVs@u1>UqkTX z2B$Z22BY@h=(PTSpzwa_F-}JCZ)t8DR=;VorLO7gRs(-V7T+zZL)_!t<_s%e}cl(Af22g9@fQB;-!=WdYg1S_^$C$0f?FFI{ovR}jw`&ClYEum_`^>v GwD;^MsA1~> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5b7fd3966830f8e7fb5cb749090425ef61ae0cdf GIT binary patch literal 783 zcmV+q1MvI+00*yloV8R>PZLoPe_L8xR{m50^bDlkx1uQ@@HwFMa|qJ#jK7iZi?Ox?3V9Br?hDd+#^DnR$O^mN7QS zJkdbotjTQVFu^3K%`CveXgnifS0EaVVH&T|SPf9R1EHxXQgs$%6NKJiA=mjjxZs%I zC~hPjSxsis z_J+0vS`QP3IYualMma{@`|$+*<7A7Hf1K_~Bl{S|Li;@IAryE##D4%1fpLk>ca!{= z0Y%?&+7IT;g2y;TH;DP1?DY>EB0H2INP$?&G_ z2nogoT|cr#m1B^^jDDhXkXpUGO?JK%puW}ha|w@`FM))!SC;T&AmOH;a1Ig*>%V2f zry)$7Y~i9+2Sj9qbcezNvD!& z_Pm(id`MRm>vTF>7x-vFAJF2f-j%W9`s(gez_x*fwf;KB zv@+07<8hZ?gF8Is@m`#HIIUW*O?qP%OvRn$oe#|Gb0xvK^1qB+mj4e?b%3op +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#if QT_CONFIG(vulkan) +#include +#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("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(image0.constScanLine(y)); + const quint32 *p1 = reinterpret_cast(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 m_vbuf; + std::unique_ptr m_ubuf; + std::unique_ptr m_srb; + std::unique_ptr 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(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(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(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(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 similarity index 78% rename from tests/manual/rhi/rhiwidget/CMakeLists.txt rename to 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 similarity index 100% rename from tests/manual/rhi/rhiwidget/examplewidget.cpp rename to tests/manual/rhi/rhiwidgetproto/examplewidget.cpp diff --git a/tests/manual/rhi/rhiwidget/examplewidget.h b/tests/manual/rhi/rhiwidgetproto/examplewidget.h similarity index 100% rename from tests/manual/rhi/rhiwidget/examplewidget.h rename to tests/manual/rhi/rhiwidgetproto/examplewidget.h diff --git a/tests/manual/rhi/rhiwidget/main.cpp b/tests/manual/rhi/rhiwidgetproto/main.cpp similarity index 100% rename from tests/manual/rhi/rhiwidget/main.cpp rename to tests/manual/rhi/rhiwidgetproto/main.cpp diff --git a/tests/manual/rhi/rhiwidget/rhiwidget.cpp b/tests/manual/rhi/rhiwidgetproto/rhiwidget.cpp similarity index 100% rename from tests/manual/rhi/rhiwidget/rhiwidget.cpp rename to tests/manual/rhi/rhiwidgetproto/rhiwidget.cpp diff --git a/tests/manual/rhi/rhiwidget/rhiwidget.h b/tests/manual/rhi/rhiwidgetproto/rhiwidget.h similarity index 100% rename from tests/manual/rhi/rhiwidget/rhiwidget.h rename to tests/manual/rhi/rhiwidgetproto/rhiwidget.h diff --git a/tests/manual/rhi/rhiwidget/rhiwidget_p.h b/tests/manual/rhi/rhiwidgetproto/rhiwidget_p.h similarity index 100% rename from tests/manual/rhi/rhiwidget/rhiwidget_p.h rename to tests/manual/rhi/rhiwidgetproto/rhiwidget_p.h