qt 6.6.0 clean

This commit is contained in:
kleuter
2023-11-01 22:23:55 +01:00
parent 7b5ada15e7
commit 5d8194efa7
1449 changed files with 134276 additions and 31391 deletions

View File

@ -5,3 +5,4 @@ if(NOT TARGET Qt6::Gui)
return()
endif()
qt_internal_add_example(rasterwindow)
qt_internal_add_example(rhiwindow)

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -0,0 +1,440 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
/*!
\example rhiwindow
\title RHI Window Example
\examplecategory {Graphics & Multimedia}
\brief This example shows how to create a minimal QWindow-based
application using QRhi.
\image rhiwindow_example.jpg
Qt 6.6 starts offering its accelerated 3D API and shader abstraction layer
for application use as well. Applications can now use the same 3D graphics
classes Qt itself uses to implement the Qt Quick scenegraph or the Qt Quick
3D engine. In earlier Qt versions QRhi and the related classes were all
private APIs. From 6.6 on these classes are in a similar category as QPA
family of classes: neither fully public nor private, but something
in-between, with a more limited compatibility promise compared to public
APIs. On the other hand, QRhi and the related classes now come with full
documentation similarly to public APIs.
There are multiple ways to use QRhi, the example here shows the most
low-level approach: targeting a QWindow, while not using Qt Quick, Qt Quick
3D, or Widgets in any form, and setting up all the rendering and windowing
infrastructure in the application.
In contrast, when writing a QML application with Qt Quick or Qt Quick 3D,
and wanting to add QRhi-based rendering to it, such an application is going
to rely on the window and rendering infrastructure Qt Quick has already
initialized, and it is likely going to query an existing QRhi instance from
the QQuickWindow. There dealing with QRhi::create(), platform/API specifics
such as \l{QVulkanInstance}{Vulkan instances}, or correctly handling
\l{QExposeEvent}{expose} and resize events for the window are all managed
by Qt Quick. Whereas in this example, all that is managed and taken care
of by the application itself.
\note For QWidget-based applications in particular, it should be noted that
QWidget::createWindowContainer() allows embedding a QWindow (backed by a
native window) into the widget-based user interface. Therefore, the \c
HelloWindow class from this example is reusable in QWidget-based
applications, assuming the necessary initialization from \c main() is in
place as well.
\section1 3D API Support
The application supports all the current \l{QRhi::Implementation}{QRhi
backends}. When no command-line arguments are specified, platform-specific
defaults are used: Direct 3D 11 on Windows, OpenGL on Linux, Metal on
macOS/iOS.
Running with \c{--help} shows the available command-line options:
\list
\li -d or --d3d11 for Direct 3D 11
\li -D or --d3d12 for Direct 3D 12
\li -m or --metal for Metal
\li -v or --vulkan for Vulkan
\li -g or --opengl for OpenGL or OpenGL ES
\li -n or --null for the \l{QRhi::Null}{Null backend}
\endlist
\section1 Build System Notes
This application relies solely on the Qt GUI module. It does not use Qt
Widgets or Qt Quick.
In order to access the RHI APIs, which are available to all Qt applications
but come with a limited compatibility promise, the \c target_link_libraries
CMake command lists \c{Qt6::GuiPrivate}. This is what enables the
\c{#include <rhi/qrhi.h>} include statement to compile successfully.
\section1 Features
The application features:
\list
\li A resizable QWindow,
\li a swapchain and depth-stencil buffer that properly follows the size of
the window,
\li logic to initialize, render, and tear down at the appropriate time
based on events such as \l QExposeEvent and \l QPlatformSurfaceEvent,
\li rendering a fullscreen textured quad, using a texture the contents of
which is generated in a QImage via QPainter (using the raster paint engine,
i.e. the generating of the image's pixel data is all CPU-based, that data
is then uploaded into a GPU texture),
\li rendering a triangle with blending and depth testing enabled, using a
perspective projection, while applying a model transform that changes on
every frame,
\li an efficient, cross-platform render loop using
\l{QWindow::requestUpdate()}{requestUpdate()}.
\endlist
\section1 Shaders
The application uses two sets of vertex and fragment shader pairs:
\list
\li one for the fullscreen quad, which uses no vertex inputs and the
fragment shader samples a texture (\c quad.vert, \c quad.frag),
\li and another pair for the triangle, where vertex positions and colors
are provided in a vertex buffer and a modelview-projection matrix is
provided in a uniform buffer (\c color.vert, \c color.frag).
\endlist
The shaders are written as Vulkan-compatible GLSL source code.
Due to being a Qt GUI module example, this example cannot have a dependency
on the \l{Qt Shader Tools} module. This means that CMake helper functions
such as \c{qt_add_shaders} are not available for use. Therefore, the
example has the pre-processed \c{.qsb} files included in the
\c{shaders/prebuilt} folder, and they are simply included within the
executable via \c{qt_add_resources}. This approach is not generally
recommended for applications, consider rather using \l{Qt Shader Tools
Build System Integration}{qt_add_shaders}, which avoids the need to
manually generate and manage the \c{.qsb} files.
To generate the \c{.qsb} files for this example, the command \c{qsb --qt6
color.vert -o prebuilt/color.vert.qsb} etc. was used. This leads to
compiling to \l{https://www.khronos.org/spir/}{SPIR-V} and than transpiling
into GLSL (\c{100 es} and \c 120), HLSL (5.0), and MSL (1.2). All the
shader versions are then packed together into a QShader and serialized to
disk.
\section1 API-specific Initialization
For some of the 3D APIs the main() function has to perform the appropriate
API-specific initialiation, e.g. to create a QVulkanInstance when using
Vulkan. For OpenGL we have to ensure a depth buffer is available, this is
done via QSurfaceFormat. These steps are not in the scope of QRhi since
QRhi backends for OpenGL or Vulkan build on the existing Qt facilities such
as QOpenGLContext or QVulkanInstance.
\snippet rhiwindow/main.cpp api-setup
\note For Vulkan, note how
QRhiVulkanInitParams::preferredInstanceExtensions() is taken into account
to ensure the appropriate extensions are enabled.
\c HelloWindow is a subclass of \c RhiWindow, which in turn is a QWindow.
\c RhiWindow contains everything needed to manage a resizable window with
a\ swapchain (and depth-stencil buffer), and is potentially reusable in
other applications as well. \c HelloWindow contains the rendering logic
specific to this particular example application.
In the QWindow subclass constructor the surface type is set based on the
selected 3D API.
\snippet rhiwindow/rhiwindow.cpp rhiwindow-ctor
Creating and initializing a QRhi object is implemented in
RhiWindow::init(). Note that this is invoked only when the window is
\c renderable, which is indicated by an \l{QExposeEvent}{expose event}.
Depending on which 3D API we use, the appropriate InitParams struct needs
to be passed to QRhi::create(). With OpenGL for example, a
QOffscreenSurface (or some other QSurface) must be created by the
application and provided for use to the QRhi. With Vulkan, a successfully
initialized QVulkanInstance is required. Others, such as Direct 3D or Metal
need no additional information to be able to initialize.
\snippet rhiwindow/rhiwindow.cpp rhi-init
Apart from this, everything else, all the rendering code, is fully
cross-platform and has no branching or conditions specific to any of the 3D
API.
\section1 Expose Events
What \c renderable exactly means is platform-specific. For example, on
macOS a window that is fully obscured (fully behind some other window) is
not renderable, whereas on Windows obscuring has no significance.
Fortunately, the application needs no special knowledge about this: Qt's
platform plugins abstract the differences behind the expose event. However,
the \l{QWindow::exposeEvent()}{exposeEvent()} reimplementation also needs
to be aware that an empty output size (e.g. width and height of 0) is also
something that should be treated as a non-renderable situation. On Windows
for example, this is what is going to happen when minimizing the window.
Hence the check based on QRhiSwapChain::surfacePixelSize().
This implementation of expose event handling attempts to be robust, safe,
and portable. Qt Quick itself also implements a very similar logic in its
render loops.
\snippet rhiwindow/rhiwindow.cpp expose
In RhiWindow::render(), which is invoked in response to the
\l{QEvent::UpdateRequest}{UpdateRequest} event generated by
\l{QWindow::requestUpdate()}{requestUpdate()}, the following check is in
place, to prevent attempting to render when the swapchain initialization
failed, or when the window became non-renderable.
\snippet rhiwindow/rhiwindow.cpp render-precheck
\section1 Swapchain, Depth-Stencil buffer, and Resizing
To render to the QWindow, a QRhiSwapChain is needed. In addition, a
QRhiRenderBuffer acting as the depth-stencil buffer is created as well
since the application demonstrates how depth testing can be enabled in a
graphics pipeline. With some legacy 3D APIs managing the depth/stencil
buffer for a window is part of the corresponding windowing system interface
API (EGL, WGL, GLX, etc., meaning the depth/stencil buffer is implicitly
managed together with the \c{window surface}), whereas with modern APIs
managing the depth-stencil buffer for a window-based render target is no
different from offscreen render targets. QRhi abstracts this, but for best
performance it still needs to be indicated that the QRhiRenderBuffer is
\l{QRhiRenderBuffer::UsedWithSwapChainOnly}{used with together with a
QRhiSwapChain}.
The QRhiSwapChain is associated with the QWindow and the depth/stencil
buffer.
\snippet rhiwindow/rhiwindow.h swapchain-data
\codeline
\snippet rhiwindow/rhiwindow.cpp swapchain-init
When the window size changes, the swapchain needs to be resized as well.
This is implemented in resizeSwapChain().
\snippet rhiwindow/rhiwindow.cpp swapchain-resize
Unlike other QRhiResource subclasses, QRhiSwapChain features slightly
different semantics when it comes to its create-function. As the name,
\l{QRhiSwapChain::createOrResize()}{createOrResize()}, suggests, this needs
to be called whenever it is known that the output window size may be out of
sync with what the swapchain was last initialized. The associated
QRhiRenderBuffer for depth-stencil gets its
\l{QRhiRenderBuffer::pixelSize()}{size} set automatically, and
\l{QRhiRenderBuffer::create()}{create()} is called on it implicitly from the
swapchain's createOrResize().
This is also a convenient place to (re)calculate the projection and view
matrices since the perspective projection we set up depends on the output
aspect ratio.
\note To eliminate coordinate system differences, the
\l{QRhi::clipSpaceCorrMatrix()}{a backend/API-specific "correction" matrix}
is queried from QRhi and baked in to the projection matrix. This is what
allows the application to work with OpenGL-style vertex data, assuming a
coordinate system with the origin at the bottom-left.
The resizeSwapChain() function is called from RhiWindow::render() when it
is discovered that the currently reported size is not the same anymore as
what the swapchain was last initialized with.
See QRhiSwapChain::currentPixelSize() and QRhiSwapChain::surfacePixelSize()
for further details.
High DPI support is built-in: the sizes, as the naming indicates, are
always in pixels, taking the window-specific
\l{QWindow::devicePixelRatio()}{scale factor} into account. On the QRhi
(and 3D API) level there is no concept of high DPI scaling, everything is
always in pixels. This means that a QWindow with a size() of 1280x720 and
a devicePixelRatio() of 2 is a render target (swapchain) with a (pixel) size
of 2560x1440.
\snippet rhiwindow/rhiwindow.cpp render-resize
\section1 Render Loop
The application renders continuously, throttled by the presentation rate
(vsync). This is ensured by calling
\l{QWindow::requestUpdate()}{requestUpdate()} from RhiWindow::render() when
the currently recorded frame has been submitted.
\snippet rhiwindow/rhiwindow.cpp request-update
This eventually leads to getting a \l{QEvent::UpdateRequest}{UpdateRequest}
event. This is handled in the reimplementation of event().
\snippet rhiwindow/rhiwindow.cpp event
\section1 Resource and Pipeline Setup
The application records a single render pass that issues two draw calls,
with two different graphics pipelines. One is the "background", with the
texture containing the QPainter-generated image, then a single triangle is
rendered on top with blending enabled.
The vertex and uniform buffer used with the triangle is created like this.
The size of the uniform buffer is 68 bytes since the shader specific a \c
mat4 and a \c float member in the uniform block. Watch out for the
\l{https://registry.khronos.org/OpenGL/specs/gl/glspec45.core.pdf#page=159}{std140
layout rules}. This presents no surprises in this example since the \c
float member that follows the \c mat4 has the correct alignment without any
additional padding, but it may become relevant in other applications,
especially when working with types such as \c vec2 or \c vec3. When in
doubt, consider checking the QShaderDescription for the
\l{QShader::description()}{QShader}, or, what is often more convenient, run
the \c qsb tool on the \c{.qsb} file with the \c{-d} argument to inspect
the metadata in human-readable form. The printed information includes,
among other things, the uniform block member offsets, sizes, and the total
size in bytes of each uniform block.
\snippet rhiwindow/rhiwindow.cpp render-init-1
The vertex and fragment shaders both need a uniform buffer at binding point
0. This is ensured by the QRhiShaderResourceBindings object. The graphics
pipeline is then setup with the shaders and a number of additional
information. The example also relies on a number of convenient defaults,
e.g. the primitive topology is
\l{QRhiGraphicsPipeline::Triangles}{Triangles}, but that is the default,
and therefore it is not explicitly set. See QRhiGraphicsPipeline for
further details.
In addition to specifying the topology and various state, the pipeline must
also be associated with:
\list
\li The vertex input layout in form of a QRhiVertexInputLayout. This
specifies the type and component count for each vertex input location, the
total stride in bytes per vertex, and other related data.
QRhiVertexInputLayout only holds data, not actual native resources, and is
copiable.
\li A valid and successfully initialized QRhiShaderResourceBindings object.
This describes the layout of the resource bindings (uniform buffers,
textures, samplers) the shaders expect. This must either by the
QRhiShaderResourceBindings used when recording the draw calls, or another
that is
\l{QRhiShaderResourceBindings::isLayoutCompatible()}{layout-compatible with it}.
This simple application takes the former approach.
\li A valid QRhiRenderPassDescriptor object. This must be retrieved from,
or \l{QRhiRenderPassDescriptor::isCompatible()}{be compatible with} the
render target. The example uses the former, by creating a
QRhiRenderPassDescriptor object via
QRhiSwapChain::newCompatibleRenderPassDescriptor().
\endlist
\snippet rhiwindow/rhiwindow.cpp render-init-2
getShader() is a helper function that loads a \c{.qsb} file and
deserializes a QShader from it.
\snippet rhiwindow/rhiwindow.cpp getshader
The \c{color.vert} shader specifies the following as the vertex inputs:
\badcode
layout(location = 0) in vec4 position;
layout(location = 1) in vec3 color;
\endcode
The C++ code however provides vertex data as 2 floats for position, with 3
floats for the color interleaved. (\c x, \c y, \c r, \c g, \c b for each
vertex) This is why the stride is \c{5 * sizeof(float)} and the inputs for
locations 0 and 1 are specified as \c Float2 and \c Float3, respectively.
This is valid, and the \c z and \c w of the \c vec4 position will get set
automatically.
\section1 Rendering
Recording a frame is started by calling \l{QRhi::beginFrame()} and finished
by calling \l{QRhi::endFrame()}.
\snippet rhiwindow/rhiwindow.cpp beginframe
Some of the resources (buffers, textures) have static data in the
application, meaning the content never changes. The vertex buffer's content
is provided in the initialization step for example, and is not changed
afterwards. These data update operations are recorded in \c
m_initialUpdates. When not yet done, the commands on this resource update
batch are merged into the per-frame batch.
\snippet rhiwindow/rhiwindow.cpp render-1
Having a per-frame resource update batch is necessary since the uniform
buffer contents with the modelview-projection matrix and the opacity
changes on every frame.
\snippet rhiwindow/rhiwindow.cpp render-rotation
\snippet rhiwindow/rhiwindow.cpp render-opacity
To begin recording the render pass, a QRhiCommandBuffer is queried, and the
output size is determined, which will be useful for setting up the viewport
and resizing our fullscreen texture, if needed.
\snippet rhiwindow/rhiwindow.cpp render-cb
Starting a render pass implies clearing the render target's color and
depth-stencil buffers (unless the render target flags indicate otherwise,
but that is only an option for texture-based render targets). Here we
specify black for color, 1.0f for depth, and 0 for stencil (unused). The
last argument, \c resourceUpdates, is what ensures that the data update
commands recorded on the batch get committed. Alternatively, we could have
used QRhiCommandBuffer::resourceUpdate() instead. The render pass targets a
swapchain, hence calling
\l{QRhiSwapChain::currentFrameRenderTarget()}{currentFrameRenderTarget()}
to get a valid QRhiRenderTarget.
\snippet rhiwindow/rhiwindow.cpp render-pass
Recording the draw call for the triangle is straightforward: set the
pipeline, set the shader resources, set the vertex/index buffer(s), and
record the draw call. Here we use a non-indexed draw with just 3 vertices.
\snippet rhiwindow/rhiwindow.cpp render-pass-record
The \l{QRhiCommandBuffer::setShaderResources()}{setShaderResources()} call
has no arguments given, which implies using \c m_colorTriSrb since that was
associated with the active QRhiGraphicsPipeline (\c m_colorPipeline).
We will not dive into the details of the rendering of the fullscreen
background image. See the example source code for that. It is however worth
noting a common pattern for "resizing" a texture or buffer resource. There
is no such thing as changing the size of an existing native resource, so
changing a texture or buffer size must be followed by a call to create(),
to release and recreate the underlying native resources. To ensure that the
QRhiTexture always has the required size, the application implements the
following logic. Note that \c m_texture stays valid for the entire lifetime
of the window, which means object references to it, e.g. in a
QRhiShaderResourceBindings, continue to be valid all the time. It is only
the underlying native resources that come and go over time.
\snippet rhiwindow/rhiwindow.cpp ensure-texture
Once a QImage is generated and the QPainter-based drawing into it has
finished, we use
\l{QRhiResourceUpdateBatch::uploadTexture()}{uploadTexture()} to record a
texture upload on the resource update batch:
\snippet rhiwindow/rhiwindow.cpp ensure-texture-2
\sa QRhi, QRhiSwapChain, QWindow, QRhiCommandBuffer, QRhiResourceUpdateBatch, QRhiBuffer, QRhiTexture
*/

View File

@ -4,4 +4,5 @@ TEMPLATE = subdirs
QT_FOR_CONFIG += gui
CONFIG += no_docs_target
SUBDIRS += rasterwindow
SUBDIRS += rasterwindow \
rhiwindow

View File

@ -0,0 +1,59 @@
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
cmake_minimum_required(VERSION 3.16)
project(rhiwindow LANGUAGES CXX)
if(NOT DEFINED INSTALL_EXAMPLESDIR)
set(INSTALL_EXAMPLESDIR "examples")
endif()
set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/gui/rhiwindow")
find_package(Qt6 REQUIRED COMPONENTS Core Gui)
qt_standard_project_setup()
qt_add_executable(rhiwindow
main.cpp
rhiwindow.cpp rhiwindow.h
)
set_target_properties(rhiwindow PROPERTIES
WIN32_EXECUTABLE TRUE
MACOSX_BUNDLE TRUE
)
target_link_libraries(rhiwindow PRIVATE
Qt6::Core
Qt6::Gui
Qt6::GuiPrivate
)
set_source_files_properties("shaders/prebuilt/color.vert.qsb"
PROPERTIES QT_RESOURCE_ALIAS "color.vert.qsb"
)
set_source_files_properties("shaders/prebuilt/color.frag.qsb"
PROPERTIES QT_RESOURCE_ALIAS "color.frag.qsb"
)
set_source_files_properties("shaders/prebuilt/quad.vert.qsb"
PROPERTIES QT_RESOURCE_ALIAS "quad.vert.qsb"
)
set_source_files_properties("shaders/prebuilt/quad.frag.qsb"
PROPERTIES QT_RESOURCE_ALIAS "quad.frag.qsb"
)
qt_add_resources(rhiwindow "rhiwindow"
PREFIX
"/"
FILES
"shaders/prebuilt/color.vert.qsb"
"shaders/prebuilt/color.frag.qsb"
"shaders/prebuilt/quad.vert.qsb"
"shaders/prebuilt/quad.frag.qsb"
)
install(TARGETS rhiwindow
RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
)

View File

@ -0,0 +1,110 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include <QGuiApplication>
#include <QCommandLineParser>
#include "rhiwindow.h"
int main(int argc, char **argv)
{
QGuiApplication app(argc, argv);
QRhi::Implementation graphicsApi;
// Use platform-specific defaults when no command-line arguments given.
#if defined(Q_OS_WIN)
graphicsApi = QRhi::D3D11;
#elif defined(Q_OS_MACOS) || defined(Q_OS_IOS)
graphicsApi = QRhi::Metal;
#elif QT_CONFIG(vulkan)
graphicsApi = QRhi::Vulkan;
#else
graphicsApi = QRhi::OpenGLES2;
#endif
QCommandLineParser cmdLineParser;
cmdLineParser.addHelpOption();
QCommandLineOption nullOption({ "n", "null" }, QLatin1String("Null"));
cmdLineParser.addOption(nullOption);
QCommandLineOption glOption({ "g", "opengl" }, QLatin1String("OpenGL"));
cmdLineParser.addOption(glOption);
QCommandLineOption vkOption({ "v", "vulkan" }, QLatin1String("Vulkan"));
cmdLineParser.addOption(vkOption);
QCommandLineOption d3d11Option({ "d", "d3d11" }, QLatin1String("Direct3D 11"));
cmdLineParser.addOption(d3d11Option);
QCommandLineOption d3d12Option({ "D", "d3d12" }, QLatin1String("Direct3D 12"));
cmdLineParser.addOption(d3d12Option);
QCommandLineOption mtlOption({ "m", "metal" }, QLatin1String("Metal"));
cmdLineParser.addOption(mtlOption);
cmdLineParser.process(app);
if (cmdLineParser.isSet(nullOption))
graphicsApi = QRhi::Null;
if (cmdLineParser.isSet(glOption))
graphicsApi = QRhi::OpenGLES2;
if (cmdLineParser.isSet(vkOption))
graphicsApi = QRhi::Vulkan;
if (cmdLineParser.isSet(d3d11Option))
graphicsApi = QRhi::D3D11;
if (cmdLineParser.isSet(d3d12Option))
graphicsApi = QRhi::D3D12;
if (cmdLineParser.isSet(mtlOption))
graphicsApi = QRhi::Metal;
//! [api-setup]
// For OpenGL, to ensure there is a depth/stencil buffer for the window.
// With other APIs this is under the application's control (QRhiRenderBuffer etc.)
// and so no special setup is needed for those.
QSurfaceFormat fmt;
fmt.setDepthBufferSize(24);
fmt.setStencilBufferSize(8);
// Special case macOS to allow using OpenGL there.
// (the default Metal is the recommended approach, though)
// gl_VertexID is a GLSL 130 feature, and so the default OpenGL 2.1 context
// we get on macOS is not sufficient.
#ifdef Q_OS_MACOS
fmt.setVersion(4, 1);
fmt.setProfile(QSurfaceFormat::CoreProfile);
#endif
QSurfaceFormat::setDefaultFormat(fmt);
// For Vulkan.
#if QT_CONFIG(vulkan)
QVulkanInstance inst;
if (graphicsApi == QRhi::Vulkan) {
// Request validation, if available. This is completely optional
// and has a performance impact, and should be avoided in production use.
inst.setLayers({ "VK_LAYER_KHRONOS_validation" });
// Play nice with QRhi.
inst.setExtensions(QRhiVulkanInitParams::preferredInstanceExtensions());
if (!inst.create()) {
qWarning("Failed to create Vulkan instance, switching to OpenGL");
graphicsApi = QRhi::OpenGLES2;
}
}
#endif
//! [api-setup]
HelloWindow window(graphicsApi);
#if QT_CONFIG(vulkan)
if (graphicsApi == QRhi::Vulkan)
window.setVulkanInstance(&inst);
#endif
window.resize(1280, 720);
window.setTitle(QCoreApplication::applicationName() + QLatin1String(" - ") + window.graphicsApiName());
window.show();
int ret = app.exec();
// RhiWindow::event() will not get invoked when the
// PlatformSurfaceAboutToBeDestroyed event is sent during the QWindow
// destruction. That happens only when exiting via app::quit() instead of
// the more common QWindow::close(). Take care of it: if the QPlatformWindow
// is still around (there was no close() yet), get rid of the swapchain
// while it's not too late.
if (window.handle())
window.releaseSwapChain();
return ret;
}

View File

@ -0,0 +1,435 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "rhiwindow.h"
#include <QPlatformSurfaceEvent>
#include <QPainter>
#include <QFile>
#include <rhi/qshader.h>
//! [rhiwindow-ctor]
RhiWindow::RhiWindow(QRhi::Implementation graphicsApi)
: m_graphicsApi(graphicsApi)
{
switch (graphicsApi) {
case QRhi::OpenGLES2:
setSurfaceType(OpenGLSurface);
break;
case QRhi::Vulkan:
setSurfaceType(VulkanSurface);
break;
case QRhi::D3D11:
case QRhi::D3D12:
setSurfaceType(Direct3DSurface);
break;
case QRhi::Metal:
setSurfaceType(MetalSurface);
break;
case QRhi::Null:
break; // RasterSurface
}
}
//! [rhiwindow-ctor]
QString RhiWindow::graphicsApiName() const
{
switch (m_graphicsApi) {
case QRhi::Null:
return QLatin1String("Null (no output)");
case QRhi::OpenGLES2:
return QLatin1String("OpenGL");
case QRhi::Vulkan:
return QLatin1String("Vulkan");
case QRhi::D3D11:
return QLatin1String("Direct3D 11");
case QRhi::D3D12:
return QLatin1String("Direct3D 12");
case QRhi::Metal:
return QLatin1String("Metal");
}
return QString();
}
//! [expose]
void RhiWindow::exposeEvent(QExposeEvent *)
{
// initialize and start rendering when the window becomes usable for graphics purposes
if (isExposed() && !m_initialized) {
init();
resizeSwapChain();
m_initialized = true;
}
const QSize surfaceSize = m_hasSwapChain ? m_sc->surfacePixelSize() : QSize();
// stop pushing frames when not exposed (or size is 0)
if ((!isExposed() || (m_hasSwapChain && surfaceSize.isEmpty())) && m_initialized && !m_notExposed)
m_notExposed = true;
// Continue when exposed again and the surface has a valid size. Note that
// surfaceSize can be (0, 0) even though size() reports a valid one, hence
// trusting surfacePixelSize() and not QWindow.
if (isExposed() && m_initialized && m_notExposed && !surfaceSize.isEmpty()) {
m_notExposed = false;
m_newlyExposed = true;
}
// always render a frame on exposeEvent() (when exposed) in order to update
// immediately on window resize.
if (isExposed() && !surfaceSize.isEmpty())
render();
}
//! [expose]
//! [event]
bool RhiWindow::event(QEvent *e)
{
switch (e->type()) {
case QEvent::UpdateRequest:
render();
break;
case QEvent::PlatformSurface:
// this is the proper time to tear down the swapchain (while the native window and surface are still around)
if (static_cast<QPlatformSurfaceEvent *>(e)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed)
releaseSwapChain();
break;
default:
break;
}
return QWindow::event(e);
}
//! [event]
//! [rhi-init]
void RhiWindow::init()
{
if (m_graphicsApi == QRhi::Null) {
QRhiNullInitParams params;
m_rhi.reset(QRhi::create(QRhi::Null, &params));
}
#if QT_CONFIG(opengl)
if (m_graphicsApi == QRhi::OpenGLES2) {
m_fallbackSurface.reset(QRhiGles2InitParams::newFallbackSurface());
QRhiGles2InitParams params;
params.fallbackSurface = m_fallbackSurface.get();
params.window = this;
m_rhi.reset(QRhi::create(QRhi::OpenGLES2, &params));
}
#endif
#if QT_CONFIG(vulkan)
if (m_graphicsApi == QRhi::Vulkan) {
QRhiVulkanInitParams params;
params.inst = vulkanInstance();
params.window = this;
m_rhi.reset(QRhi::create(QRhi::Vulkan, &params));
}
#endif
#ifdef Q_OS_WIN
if (m_graphicsApi == QRhi::D3D11) {
QRhiD3D11InitParams params;
// Enable the debug layer, if available. This is optional
// and should be avoided in production builds.
params.enableDebugLayer = true;
m_rhi.reset(QRhi::create(QRhi::D3D11, &params));
} else if (m_graphicsApi == QRhi::D3D12) {
QRhiD3D12InitParams params;
// Enable the debug layer, if available. This is optional
// and should be avoided in production builds.
params.enableDebugLayer = true;
m_rhi.reset(QRhi::create(QRhi::D3D12, &params));
}
#endif
#if defined(Q_OS_MACOS) || defined(Q_OS_IOS)
if (m_graphicsApi == QRhi::Metal) {
QRhiMetalInitParams params;
m_rhi.reset(QRhi::create(QRhi::Metal, &params));
}
#endif
if (!m_rhi)
qFatal("Failed to create RHI backend");
//! [rhi-init]
//! [swapchain-init]
m_sc.reset(m_rhi->newSwapChain());
m_ds.reset(m_rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil,
QSize(), // no need to set the size here, due to UsedWithSwapChainOnly
1,
QRhiRenderBuffer::UsedWithSwapChainOnly));
m_sc->setWindow(this);
m_sc->setDepthStencil(m_ds.get());
m_rp.reset(m_sc->newCompatibleRenderPassDescriptor());
m_sc->setRenderPassDescriptor(m_rp.get());
//! [swapchain-init]
customInit();
}
//! [swapchain-resize]
void RhiWindow::resizeSwapChain()
{
m_hasSwapChain = m_sc->createOrResize(); // also handles m_ds
const QSize outputSize = m_sc->currentPixelSize();
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);
}
//! [swapchain-resize]
void RhiWindow::releaseSwapChain()
{
if (m_hasSwapChain) {
m_hasSwapChain = false;
m_sc->destroy();
}
}
//! [render-precheck]
void RhiWindow::render()
{
if (!m_hasSwapChain || m_notExposed)
return;
//! [render-precheck]
//! [render-resize]
// If the window got resized or newly exposed, resize the swapchain. (the
// newly-exposed case is not actually required by some platforms, but is
// here for robustness and portability)
//
// This (exposeEvent + the logic here) is the only safe way to perform
// resize handling. Note the usage of the RHI's surfacePixelSize(), and
// never QWindow::size(). (the two may or may not be the same under the hood,
// depending on the backend and platform)
//
if (m_sc->currentPixelSize() != m_sc->surfacePixelSize() || m_newlyExposed) {
resizeSwapChain();
if (!m_hasSwapChain)
return;
m_newlyExposed = false;
}
//! [render-resize]
//! [beginframe]
QRhi::FrameOpResult result = m_rhi->beginFrame(m_sc.get());
if (result == QRhi::FrameOpSwapChainOutOfDate) {
resizeSwapChain();
if (!m_hasSwapChain)
return;
result = m_rhi->beginFrame(m_sc.get());
}
if (result != QRhi::FrameOpSuccess) {
qWarning("beginFrame failed with %d, will retry", result);
requestUpdate();
return;
}
customRender();
//! [beginframe]
//! [request-update]
m_rhi->endFrame(m_sc.get());
// Always request the next frame via requestUpdate(). On some platforms this is backed
// by a platform-specific solution, e.g. CVDisplayLink on macOS, which is potentially
// more efficient than a timer, queued metacalls, etc.
requestUpdate();
}
//! [request-update]
static float vertexData[] = {
// Y up (note clipSpaceCorrMatrix in m_viewProjection), CCW
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,
};
//! [getshader]
static QShader getShader(const QString &name)
{
QFile f(name);
if (f.open(QIODevice::ReadOnly))
return QShader::fromSerialized(f.readAll());
return QShader();
}
//! [getshader]
HelloWindow::HelloWindow(QRhi::Implementation graphicsApi)
: RhiWindow(graphicsApi)
{
}
//! [ensure-texture]
void HelloWindow::ensureFullscreenTexture(const QSize &pixelSize, QRhiResourceUpdateBatch *u)
{
if (m_texture && m_texture->pixelSize() == pixelSize)
return;
if (!m_texture)
m_texture.reset(m_rhi->newTexture(QRhiTexture::RGBA8, pixelSize));
else
m_texture->setPixelSize(pixelSize);
m_texture->create();
QImage image(pixelSize, QImage::Format_RGBA8888_Premultiplied);
//! [ensure-texture]
QPainter painter(&image);
painter.fillRect(QRectF(QPointF(0, 0), pixelSize), QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f));
painter.setPen(Qt::transparent);
painter.setBrush({ QGradient(QGradient::DeepBlue) });
painter.drawRoundedRect(QRectF(QPointF(20, 20), pixelSize - QSize(40, 40)), 16, 16);
painter.setPen(Qt::black);
QFont font;
font.setPixelSize(0.05 * qMin(pixelSize.width(), pixelSize.height()));
painter.setFont(font);
painter.drawText(QRectF(QPointF(60, 60), pixelSize - QSize(120, 120)), 0,
QLatin1String("Rendering with QRhi to a resizable QWindow.\nThe 3D API is %1.\nUse the command-line options to choose a different API.")
.arg(graphicsApiName()));
painter.end();
if (m_rhi->isYUpInNDC())
image = image.mirrored();
//! [ensure-texture-2]
u->uploadTexture(m_texture.get(), image);
//! [ensure-texture-2]
}
//! [render-init-1]
void HelloWindow::customInit()
{
m_initialUpdates = m_rhi->nextResourceUpdateBatch();
m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData)));
m_vbuf->create();
m_initialUpdates->uploadStaticBuffer(m_vbuf.get(), vertexData);
static const quint32 UBUF_SIZE = 68;
m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, UBUF_SIZE));
m_ubuf->create();
//! [render-init-1]
ensureFullscreenTexture(m_sc->surfacePixelSize(), m_initialUpdates);
m_sampler.reset(m_rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
m_sampler->create();
//! [render-init-2]
m_colorTriSrb.reset(m_rhi->newShaderResourceBindings());
static const QRhiShaderResourceBinding::StageFlags visibility =
QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage;
m_colorTriSrb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, visibility, m_ubuf.get())
});
m_colorTriSrb->create();
m_colorPipeline.reset(m_rhi->newGraphicsPipeline());
// Enable depth testing; not quite needed for a simple triangle, but we
// have a depth-stencil buffer so why not.
m_colorPipeline->setDepthTest(true);
m_colorPipeline->setDepthWrite(true);
// Blend factors default to One, OneOneMinusSrcAlpha, which is convenient.
QRhiGraphicsPipeline::TargetBlend premulAlphaBlend;
premulAlphaBlend.enable = true;
m_colorPipeline->setTargetBlends({ premulAlphaBlend });
m_colorPipeline->setShaderStages({
{ QRhiShaderStage::Vertex, getShader(QLatin1String(":/color.vert.qsb")) },
{ QRhiShaderStage::Fragment, getShader(QLatin1String(":/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_colorPipeline->setVertexInputLayout(inputLayout);
m_colorPipeline->setShaderResourceBindings(m_colorTriSrb.get());
m_colorPipeline->setRenderPassDescriptor(m_rp.get());
m_colorPipeline->create();
//! [render-init-2]
m_fullscreenQuadSrb.reset(m_rhi->newShaderResourceBindings());
m_fullscreenQuadSrb->setBindings({
QRhiShaderResourceBinding::sampledTexture(0, QRhiShaderResourceBinding::FragmentStage,
m_texture.get(), m_sampler.get())
});
m_fullscreenQuadSrb->create();
m_fullscreenQuadPipeline.reset(m_rhi->newGraphicsPipeline());
m_fullscreenQuadPipeline->setShaderStages({
{ QRhiShaderStage::Vertex, getShader(QLatin1String(":/quad.vert.qsb")) },
{ QRhiShaderStage::Fragment, getShader(QLatin1String(":/quad.frag.qsb")) }
});
m_fullscreenQuadPipeline->setVertexInputLayout({});
m_fullscreenQuadPipeline->setShaderResourceBindings(m_fullscreenQuadSrb.get());
m_fullscreenQuadPipeline->setRenderPassDescriptor(m_rp.get());
m_fullscreenQuadPipeline->create();
}
//! [render-1]
void HelloWindow::customRender()
{
QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch();
if (m_initialUpdates) {
resourceUpdates->merge(m_initialUpdates);
m_initialUpdates->release();
m_initialUpdates = nullptr;
}
//! [render-1]
//! [render-rotation]
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-rotation]
//! [render-opacity]
m_opacity += m_opacityDir * 0.005f;
if (m_opacity < 0.0f || m_opacity > 1.0f) {
m_opacityDir *= -1;
m_opacity = qBound(0.0f, m_opacity, 1.0f);
}
resourceUpdates->updateDynamicBuffer(m_ubuf.get(), 64, 4, &m_opacity);
//! [render-opacity]
//! [render-cb]
QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
const QSize outputSizeInPixels = m_sc->currentPixelSize();
//! [render-cb]
// (re)create the texture with a size matching the output surface size, when necessary.
ensureFullscreenTexture(outputSizeInPixels, resourceUpdates);
//! [render-pass]
cb->beginPass(m_sc->currentFrameRenderTarget(), Qt::black, { 1.0f, 0 }, resourceUpdates);
//! [render-pass]
cb->setGraphicsPipeline(m_fullscreenQuadPipeline.get());
cb->setViewport({ 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) });
cb->setShaderResources();
cb->draw(3);
//! [render-pass-record]
cb->setGraphicsPipeline(m_colorPipeline.get());
cb->setShaderResources();
const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0);
cb->setVertexInput(0, 1, &vbufBinding);
cb->draw(3);
cb->endPass();
//! [render-pass-record]
}

View File

@ -0,0 +1,78 @@
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef WINDOW_H
#define WINDOW_H
#include <QWindow>
#include <QOffscreenSurface>
#include <rhi/qrhi.h>
class RhiWindow : public QWindow
{
public:
RhiWindow(QRhi::Implementation graphicsApi);
QString graphicsApiName() const;
void releaseSwapChain();
protected:
virtual void customInit() = 0;
virtual void customRender() = 0;
// destruction order matters to a certain degree: the fallbackSurface must
// outlive the rhi, the rhi must outlive all other resources. The resources
// need no special order when destroying.
#if QT_CONFIG(opengl)
std::unique_ptr<QOffscreenSurface> m_fallbackSurface;
#endif
std::unique_ptr<QRhi> m_rhi;
//! [swapchain-data]
std::unique_ptr<QRhiSwapChain> m_sc;
std::unique_ptr<QRhiRenderBuffer> m_ds;
std::unique_ptr<QRhiRenderPassDescriptor> m_rp;
//! [swapchain-data]
bool m_hasSwapChain = false;
QMatrix4x4 m_viewProjection;
private:
void init();
void resizeSwapChain();
void render();
void exposeEvent(QExposeEvent *) override;
bool event(QEvent *) override;
QRhi::Implementation m_graphicsApi;
bool m_initialized = false;
bool m_notExposed = false;
bool m_newlyExposed = false;
};
class HelloWindow : public RhiWindow
{
public:
HelloWindow(QRhi::Implementation graphicsApi);
void customInit() override;
void customRender() override;
private:
void ensureFullscreenTexture(const QSize &pixelSize, QRhiResourceUpdateBatch *u);
std::unique_ptr<QRhiBuffer> m_vbuf;
std::unique_ptr<QRhiBuffer> m_ubuf;
std::unique_ptr<QRhiTexture> m_texture;
std::unique_ptr<QRhiSampler> m_sampler;
std::unique_ptr<QRhiShaderResourceBindings> m_colorTriSrb;
std::unique_ptr<QRhiGraphicsPipeline> m_colorPipeline;
std::unique_ptr<QRhiShaderResourceBindings> m_fullscreenQuadSrb;
std::unique_ptr<QRhiGraphicsPipeline> m_fullscreenQuadPipeline;
QRhiResourceUpdateBatch *m_initialUpdates = nullptr;
float m_rotation = 0;
float m_opacity = 1;
int m_opacityDir = -1;
};
#endif

View File

@ -0,0 +1,4 @@
INCLUDEPATH += $$PWD
SOURCES += $$PWD/rhiwindow.cpp
HEADERS += $$PWD/rhiwindow.h
RESOURCES += $$PWD/rhiwindow.qrc

View File

@ -0,0 +1,9 @@
include(rhiwindow.pri)
QT += gui-private
SOURCES += \
main.cpp
target.path = $$[QT_INSTALL_EXAMPLES]/gui/rhiwindow
INSTALLS += target

View File

@ -0,0 +1,8 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file alias="color.vert.qsb">shaders/prebuilt/color.vert.qsb</file>
<file alias="color.frag.qsb">shaders/prebuilt/color.frag.qsb</file>
<file alias="quad.vert.qsb">shaders/prebuilt/quad.vert.qsb</file>
<file alias="quad.frag.qsb">shaders/prebuilt/quad.frag.qsb</file>
</qresource>
</RCC>

View File

@ -0,0 +1,15 @@
#version 440
layout(location = 0) in vec3 v_color;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 mvp;
float opacity;
};
void main()
{
fragColor = vec4(v_color * opacity, opacity);
}

View File

@ -0,0 +1,17 @@
#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;
float opacity;
};
void main()
{
v_color = color;
gl_Position = mvp * position;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,11 @@
#version 440
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 fragColor;
layout(binding = 0) uniform sampler2D tex;
void main()
{
vec4 c = texture(tex, v_uv);
fragColor = vec4(c.rgb * c.a, c.a);
}

View File

@ -0,0 +1,10 @@
#version 440
layout (location = 0) out vec2 v_uv;
void main()
{
// https://www.saschawillems.de/blog/2016/08/13/vulkan-tutorial-on-rendering-a-fullscreen-quad-without-buffers/
v_uv = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
gl_Position = vec4(v_uv * 2.0 - 1.0, 0.0, 1.0);
}