summaryrefslogtreecommitdiffstats
path: root/tests/manual/rhi
diff options
context:
space:
mode:
authorLaszlo Agocs <laszlo.agocs@qt.io>2019-03-22 09:55:03 +0100
committerLaszlo Agocs <laszlo.agocs@qt.io>2019-06-13 10:13:45 +0200
commit53599592e09edd215bfa1eaa7e6f3a9f3fc50ae6 (patch)
tree1083ec3a18030bb6f09de58b2fe0ac1cb8aedc0b /tests/manual/rhi
parentc143161608ded130919006f151bf92c44a0991d0 (diff)
Introduce the Qt graphics abstraction as private QtGui helpers
Comes with backends for Vulkan, Metal, Direct3D 11.1, and OpenGL (ES). All APIs are private for now. Shader conditioning (i.e. generating a QRhiShader in memory or on disk from some shader source code) is done via the tools and APIs provided by qt-labs/qtshadertools. The OpenGL support follows the cross-platform tradition of requiring ES 2.0 only, while optionally using some (ES) 3.x features. It can operate in core profile contexts as well. Task-number: QTBUG-70287 Change-Id: I246f2e36d562e404012c05db2aa72487108aa7cc Reviewed-by: Lars Knoll <lars.knoll@qt.io>
Diffstat (limited to 'tests/manual/rhi')
-rw-r--r--tests/manual/rhi/compressedtexture_bc1/compressedtexture_bc1.cpp198
-rw-r--r--tests/manual/rhi/compressedtexture_bc1/compressedtexture_bc1.pro8
-rw-r--r--tests/manual/rhi/compressedtexture_bc1/compressedtexture_bc1.qrc7
-rw-r--r--tests/manual/rhi/compressedtexture_bc1_subupload/compressedtexture_bc1_subupload.cpp213
-rw-r--r--tests/manual/rhi/compressedtexture_bc1_subupload/compressedtexture_bc1_subupload.pro8
-rw-r--r--tests/manual/rhi/compressedtexture_bc1_subupload/compressedtexture_bc1_subupload.qrc8
-rw-r--r--tests/manual/rhi/cubemap/buildshaders.bat2
-rw-r--r--tests/manual/rhi/cubemap/c.pngbin0 -> 27923 bytes
-rw-r--r--tests/manual/rhi/cubemap/cubemap.cpp179
-rw-r--r--tests/manual/rhi/cubemap/cubemap.frag10
-rw-r--r--tests/manual/rhi/cubemap/cubemap.frag.qsbbin0 -> 1311 bytes
-rw-r--r--tests/manual/rhi/cubemap/cubemap.pro8
-rw-r--r--tests/manual/rhi/cubemap/cubemap.qrc7
-rw-r--r--tests/manual/rhi/cubemap/cubemap.vert15
-rw-r--r--tests/manual/rhi/cubemap/cubemap.vert.qsbbin0 -> 1499 bytes
-rw-r--r--tests/manual/rhi/floattexture/floattexture.cpp328
-rw-r--r--tests/manual/rhi/floattexture/floattexture.pro8
-rw-r--r--tests/manual/rhi/floattexture/floattexture.qrc7
-rw-r--r--tests/manual/rhi/hellominimalcrossgfxtriangle/hellominimalcrossgfxtriangle.cpp554
-rw-r--r--tests/manual/rhi/hellominimalcrossgfxtriangle/hellominimalcrossgfxtriangle.pro8
-rw-r--r--tests/manual/rhi/hellominimalcrossgfxtriangle/hellominimalcrossgfxtriangle.qrc6
-rw-r--r--tests/manual/rhi/mrt/buildshaders.bat2
-rw-r--r--tests/manual/rhi/mrt/mrt.cpp296
-rw-r--r--tests/manual/rhi/mrt/mrt.frag22
-rw-r--r--tests/manual/rhi/mrt/mrt.frag.qsbbin0 -> 1934 bytes
-rw-r--r--tests/manual/rhi/mrt/mrt.pro8
-rw-r--r--tests/manual/rhi/mrt/mrt.qrc8
-rw-r--r--tests/manual/rhi/mrt/mrt.vert19
-rw-r--r--tests/manual/rhi/mrt/mrt.vert.qsbbin0 -> 1633 bytes
-rw-r--r--tests/manual/rhi/msaarenderbuffer/msaarenderbuffer.cpp259
-rw-r--r--tests/manual/rhi/msaarenderbuffer/msaarenderbuffer.pro8
-rw-r--r--tests/manual/rhi/msaarenderbuffer/msaarenderbuffer.qrc8
-rw-r--r--tests/manual/rhi/msaatexture/msaatexture.cpp329
-rw-r--r--tests/manual/rhi/msaatexture/msaatexture.pro8
-rw-r--r--tests/manual/rhi/msaatexture/msaatexture.qrc9
-rw-r--r--tests/manual/rhi/multiwindow/multiwindow.cpp644
-rw-r--r--tests/manual/rhi/multiwindow/multiwindow.pro8
-rw-r--r--tests/manual/rhi/multiwindow/multiwindow.qrc6
-rw-r--r--tests/manual/rhi/multiwindow_threaded/multiwindow_threaded.cpp829
-rw-r--r--tests/manual/rhi/multiwindow_threaded/multiwindow_threaded.pro12
-rw-r--r--tests/manual/rhi/multiwindow_threaded/multiwindow_threaded.qrc7
-rw-r--r--tests/manual/rhi/multiwindow_threaded/window.cpp142
-rw-r--r--tests/manual/rhi/multiwindow_threaded/window.h86
-rw-r--r--tests/manual/rhi/offscreen/offscreen.cpp366
-rw-r--r--tests/manual/rhi/offscreen/offscreen.pro9
-rw-r--r--tests/manual/rhi/offscreen/offscreen.qrc6
-rw-r--r--tests/manual/rhi/qrhiprof/qrhiprof.cpp671
-rw-r--r--tests/manual/rhi/qrhiprof/qrhiprof.pro6
-rw-r--r--tests/manual/rhi/rhi.pro22
-rw-r--r--tests/manual/rhi/shadowmap/buildshaders.bat4
-rwxr-xr-xtests/manual/rhi/shadowmap/buildshaders.sh4
-rw-r--r--tests/manual/rhi/shadowmap/main.frag30
-rw-r--r--tests/manual/rhi/shadowmap/main.frag.qsbbin0 -> 2489 bytes
-rw-r--r--tests/manual/rhi/shadowmap/main.vert20
-rw-r--r--tests/manual/rhi/shadowmap/main.vert.qsbbin0 -> 1595 bytes
-rw-r--r--tests/manual/rhi/shadowmap/shadowmap.cpp304
-rw-r--r--tests/manual/rhi/shadowmap/shadowmap.frag5
-rw-r--r--tests/manual/rhi/shadowmap/shadowmap.frag.qsbbin0 -> 408 bytes
-rw-r--r--tests/manual/rhi/shadowmap/shadowmap.pro8
-rw-r--r--tests/manual/rhi/shadowmap/shadowmap.qrc8
-rw-r--r--tests/manual/rhi/shadowmap/shadowmap.vert14
-rw-r--r--tests/manual/rhi/shadowmap/shadowmap.vert.qsbbin0 -> 1215 bytes
-rw-r--r--tests/manual/rhi/shared/OpenfootageNET_fieldairport-512.hdrbin0 -> 385303 bytes
-rw-r--r--tests/manual/rhi/shared/buildshaders.bat5
-rw-r--r--tests/manual/rhi/shared/bwqt224_64.pngbin0 -> 6339 bytes
-rw-r--r--tests/manual/rhi/shared/bwqt224_64_nomips.ddsbin0 -> 9712 bytes
-rw-r--r--tests/manual/rhi/shared/color.frag15
-rw-r--r--tests/manual/rhi/shared/color.frag.qsbbin0 -> 1483 bytes
-rw-r--r--tests/manual/rhi/shared/color.vert19
-rw-r--r--tests/manual/rhi/shared/color.vert.qsbbin0 -> 1648 bytes
-rw-r--r--tests/manual/rhi/shared/cube.h119
-rw-r--r--tests/manual/rhi/shared/dds_bc1.h137
-rw-r--r--tests/manual/rhi/shared/examplefw.h548
-rw-r--r--tests/manual/rhi/shared/qt256.pngbin0 -> 6208 bytes
-rw-r--r--tests/manual/rhi/shared/qt256_bc1_9mips.ddsbin0 -> 43832 bytes
-rw-r--r--tests/manual/rhi/shared/texture.frag18
-rw-r--r--tests/manual/rhi/shared/texture.frag.qsbbin0 -> 1710 bytes
-rw-r--r--tests/manual/rhi/shared/texture.vert21
-rw-r--r--tests/manual/rhi/shared/texture.vert.qsbbin0 -> 1897 bytes
-rw-r--r--tests/manual/rhi/shared/texture_ms4.frag20
-rw-r--r--tests/manual/rhi/shared/texture_ms4.frag.qsbbin0 -> 1847 bytes
-rw-r--r--tests/manual/rhi/texuploads/texuploads.cpp314
-rw-r--r--tests/manual/rhi/texuploads/texuploads.pro8
-rw-r--r--tests/manual/rhi/texuploads/texuploads.qrc7
-rw-r--r--tests/manual/rhi/triquadcube/quadrenderer.cpp148
-rw-r--r--tests/manual/rhi/triquadcube/quadrenderer.h87
-rw-r--r--tests/manual/rhi/triquadcube/texturedcuberenderer.cpp222
-rw-r--r--tests/manual/rhi/triquadcube/texturedcuberenderer.h86
-rw-r--r--tests/manual/rhi/triquadcube/triangleoncuberenderer.cpp292
-rw-r--r--tests/manual/rhi/triquadcube/triangleoncuberenderer.h95
-rw-r--r--tests/manual/rhi/triquadcube/trianglerenderer.cpp202
-rw-r--r--tests/manual/rhi/triquadcube/trianglerenderer.h93
-rw-r--r--tests/manual/rhi/triquadcube/triquadcube.cpp271
-rw-r--r--tests/manual/rhi/triquadcube/triquadcube.pro18
-rw-r--r--tests/manual/rhi/triquadcube/triquadcube.qrc9
95 files changed, 8515 insertions, 0 deletions
diff --git a/tests/manual/rhi/compressedtexture_bc1/compressedtexture_bc1.cpp b/tests/manual/rhi/compressedtexture_bc1/compressedtexture_bc1.cpp
new file mode 100644
index 0000000000..d50e183096
--- /dev/null
+++ b/tests/manual/rhi/compressedtexture_bc1/compressedtexture_bc1.cpp
@@ -0,0 +1,198 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "../shared/examplefw.h"
+#include "../shared/cube.h"
+#include "../shared/dds_bc1.h"
+
+struct {
+ QRhiBuffer *vbuf = nullptr;
+ bool vbufReady = false;
+ QRhiBuffer *ubuf = nullptr;
+ QRhiTexture *tex = nullptr;
+ QRhiSampler *sampler = nullptr;
+ QRhiShaderResourceBindings *srb = nullptr;
+ QRhiGraphicsPipeline *ps = nullptr;
+
+ float rotation = 0;
+
+ QByteArrayList compressedData;
+} d;
+
+void Window::customInit()
+{
+ if (!m_r->isTextureFormatSupported(QRhiTexture::BC1))
+ qFatal("This backend does not support BC1");
+
+ d.vbuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube));
+ d.vbuf->build();
+ d.vbufReady = false;
+
+ d.ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68);
+ d.ubuf->build();
+
+ QSize imageSize;
+ d.compressedData = loadBC1(QLatin1String(":/qt256_bc1_9mips.dds"), &imageSize);
+ qDebug() << d.compressedData.count() << imageSize << m_r->mipLevelsForSize(imageSize);
+
+ d.tex = m_r->newTexture(QRhiTexture::BC1, imageSize, 1, QRhiTexture::MipMapped);
+ d.tex->build();
+
+ d.sampler = m_r->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::Linear,
+ QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
+ d.sampler->build();
+
+ d.srb = m_r->newShaderResourceBindings();
+ d.srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, d.ubuf),
+ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, d.tex, d.sampler)
+ });
+ d.srb->build();
+
+ d.ps = m_r->newGraphicsPipeline();
+
+ d.ps->setDepthTest(true);
+ d.ps->setDepthWrite(true);
+ d.ps->setDepthOp(QRhiGraphicsPipeline::Less);
+
+ d.ps->setCullMode(QRhiGraphicsPipeline::Back);
+ d.ps->setFrontFace(QRhiGraphicsPipeline::CCW);
+
+ const QShader vs = getShader(QLatin1String(":/texture.vert.qsb"));
+ if (!vs.isValid())
+ qFatal("Failed to load shader pack (vertex)");
+ const QShader fs = getShader(QLatin1String(":/texture.frag.qsb"));
+ if (!fs.isValid())
+ qFatal("Failed to load shader pack (fragment)");
+
+ d.ps->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, vs },
+ { QRhiGraphicsShaderStage::Fragment, fs }
+ });
+
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 3 * sizeof(float) },
+ { 2 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float3, 0 },
+ { 1, 1, QRhiVertexInputAttribute::Float2, 0 }
+ });
+
+ d.ps->setVertexInputLayout(inputLayout);
+ d.ps->setShaderResourceBindings(d.srb);
+ d.ps->setRenderPassDescriptor(m_rp);
+
+ d.ps->build();
+}
+
+void Window::customRelease()
+{
+ delete d.ps;
+ d.ps = nullptr;
+
+ delete d.srb;
+ d.srb = nullptr;
+
+ delete d.ubuf;
+ d.ubuf = nullptr;
+
+ delete d.vbuf;
+ d.vbuf = nullptr;
+
+ delete d.sampler;
+ d.sampler = nullptr;
+
+ delete d.tex;
+ d.tex = nullptr;
+}
+
+void Window::customRender()
+{
+ QRhiResourceUpdateBatch *u = m_r->nextResourceUpdateBatch();
+ if (!d.vbufReady) {
+ d.vbufReady = true;
+ u->uploadStaticBuffer(d.vbuf, cube);
+ qint32 flip = 0;
+ u->updateDynamicBuffer(d.ubuf, 64, 4, &flip);
+ }
+ if (!d.compressedData.isEmpty()) {
+ QRhiTextureUploadDescription desc;
+ for (int i = 0; i < d.compressedData.count(); ++i) {
+ QRhiTextureSubresourceUploadDescription image(d.compressedData[i].constData(), d.compressedData[i].size());
+ desc.append({ 0, i, image });
+ }
+ u->uploadTexture(d.tex, desc);
+ d.compressedData.clear();
+ }
+ d.rotation += 1.0f;
+ QMatrix4x4 mvp = m_proj;
+ mvp.scale(0.5f);
+ mvp.rotate(d.rotation, 0, 1, 0);
+ u->updateDynamicBuffer(d.ubuf, 0, 64, mvp.constData());
+
+ QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
+ const QSize outputSizeInPixels = m_sc->currentPixelSize();
+
+ cb->beginPass(m_sc->currentFrameRenderTarget(), QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f), { 1.0f, 0 }, u);
+
+ cb->setGraphicsPipeline(d.ps);
+ cb->setViewport({ 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) });
+ cb->setShaderResources();
+ const QRhiCommandBuffer::VertexInput vbufBindings[] = {
+ { d.vbuf, 0 },
+ { d.vbuf, 36 * 3 * sizeof(float) }
+ };
+ cb->setVertexInput(0, 2, vbufBindings);
+ cb->draw(36);
+
+ cb->endPass();
+}
diff --git a/tests/manual/rhi/compressedtexture_bc1/compressedtexture_bc1.pro b/tests/manual/rhi/compressedtexture_bc1/compressedtexture_bc1.pro
new file mode 100644
index 0000000000..d185aa6b9a
--- /dev/null
+++ b/tests/manual/rhi/compressedtexture_bc1/compressedtexture_bc1.pro
@@ -0,0 +1,8 @@
+TEMPLATE = app
+
+QT += gui-private
+
+SOURCES = \
+ compressedtexture_bc1.cpp
+
+RESOURCES = compressedtexture_bc1.qrc
diff --git a/tests/manual/rhi/compressedtexture_bc1/compressedtexture_bc1.qrc b/tests/manual/rhi/compressedtexture_bc1/compressedtexture_bc1.qrc
new file mode 100644
index 0000000000..a83259a7d3
--- /dev/null
+++ b/tests/manual/rhi/compressedtexture_bc1/compressedtexture_bc1.qrc
@@ -0,0 +1,7 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+ <file alias="qt256_bc1_9mips.dds">../shared/qt256_bc1_9mips.dds</file>
+ <file alias="texture.vert.qsb">../shared/texture.vert.qsb</file>
+ <file alias="texture.frag.qsb">../shared/texture.frag.qsb</file>
+</qresource>
+</RCC>
diff --git a/tests/manual/rhi/compressedtexture_bc1_subupload/compressedtexture_bc1_subupload.cpp b/tests/manual/rhi/compressedtexture_bc1_subupload/compressedtexture_bc1_subupload.cpp
new file mode 100644
index 0000000000..7ae5dd0d73
--- /dev/null
+++ b/tests/manual/rhi/compressedtexture_bc1_subupload/compressedtexture_bc1_subupload.cpp
@@ -0,0 +1,213 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "../shared/examplefw.h"
+#include "../shared/cube.h"
+#include "../shared/dds_bc1.h"
+
+struct {
+ QRhiBuffer *vbuf = nullptr;
+ bool vbufReady = false;
+ QRhiBuffer *ubuf = nullptr;
+ QRhiTexture *tex = nullptr;
+ QRhiSampler *sampler = nullptr;
+ QRhiShaderResourceBindings *srb = nullptr;
+ QRhiGraphicsPipeline *ps = nullptr;
+
+ float rotation = 0;
+
+ QByteArrayList compressedData;
+ QByteArrayList compressedData2;
+} d;
+
+void Window::customInit()
+{
+ if (!m_r->isTextureFormatSupported(QRhiTexture::BC1))
+ qFatal("This backend does not support BC1");
+
+ d.vbuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube));
+ d.vbuf->build();
+ d.vbufReady = false;
+
+ d.ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68);
+ d.ubuf->build();
+
+ QSize imageSize;
+ d.compressedData = loadBC1(QLatin1String(":/qt256_bc1_9mips.dds"), &imageSize);
+ Q_ASSERT(imageSize == QSize(256, 256));
+
+ d.tex = m_r->newTexture(QRhiTexture::BC1, imageSize);
+ d.tex->build();
+
+ d.compressedData2 = loadBC1(QLatin1String(":/bwqt224_64_nomips.dds"), &imageSize);
+ Q_ASSERT(imageSize == QSize(224, 64));
+
+ d.sampler = m_r->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None, // no mipmapping here
+ QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
+ d.sampler->build();
+
+ d.srb = m_r->newShaderResourceBindings();
+ d.srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, d.ubuf),
+ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, d.tex, d.sampler)
+ });
+ d.srb->build();
+
+ d.ps = m_r->newGraphicsPipeline();
+
+ d.ps->setDepthTest(true);
+ d.ps->setDepthWrite(true);
+ d.ps->setDepthOp(QRhiGraphicsPipeline::Less);
+
+ d.ps->setCullMode(QRhiGraphicsPipeline::Back);
+ d.ps->setFrontFace(QRhiGraphicsPipeline::CCW);
+
+ const QShader vs = getShader(QLatin1String(":/texture.vert.qsb"));
+ if (!vs.isValid())
+ qFatal("Failed to load shader pack (vertex)");
+ const QShader fs = getShader(QLatin1String(":/texture.frag.qsb"));
+ if (!fs.isValid())
+ qFatal("Failed to load shader pack (fragment)");
+
+ d.ps->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, vs },
+ { QRhiGraphicsShaderStage::Fragment, fs }
+ });
+
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 3 * sizeof(float) },
+ { 2 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float3, 0 },
+ { 1, 1, QRhiVertexInputAttribute::Float2, 0 }
+ });
+
+ d.ps->setVertexInputLayout(inputLayout);
+ d.ps->setShaderResourceBindings(d.srb);
+ d.ps->setRenderPassDescriptor(m_rp);
+
+ d.ps->build();
+}
+
+void Window::customRelease()
+{
+ delete d.ps;
+ d.ps = nullptr;
+
+ delete d.srb;
+ d.srb = nullptr;
+
+ delete d.ubuf;
+ d.ubuf = nullptr;
+
+ delete d.vbuf;
+ d.vbuf = nullptr;
+
+ delete d.sampler;
+ d.sampler = nullptr;
+
+ delete d.tex;
+ d.tex = nullptr;
+}
+
+void Window::customRender()
+{
+ QRhiResourceUpdateBatch *u = m_r->nextResourceUpdateBatch();
+ if (!d.vbufReady) {
+ d.vbufReady = true;
+ u->uploadStaticBuffer(d.vbuf, cube);
+ qint32 flip = 0;
+ u->updateDynamicBuffer(d.ubuf, 64, 4, &flip);
+ }
+ if (!d.compressedData.isEmpty()) {
+ {
+ QRhiTextureUploadDescription desc({ 0, 0, { d.compressedData[0].constData(), d.compressedData[0].size() } });
+ u->uploadTexture(d.tex, desc);
+ d.compressedData.clear();
+ }
+
+ // now exercise uploading a smaller compressed image into the same texture
+ {
+ QRhiTextureSubresourceUploadDescription image(d.compressedData2[0].constData(), d.compressedData2[0].size());
+ // positions and sizes must be multiples of 4 here (for BC1)
+ image.setDestinationTopLeft(QPoint(16, 32));
+ // the image is smaller than the subresource size (224x64 vs
+ // 256x256) so the size must be specified manually
+ image.setSourceSize(QSize(224, 64));
+ QRhiTextureUploadDescription desc({ 0, 0, image });
+ u->uploadTexture(d.tex, desc);
+ d.compressedData2.clear();
+ }
+ }
+ d.rotation += 1.0f;
+ QMatrix4x4 mvp = m_proj;
+ mvp.scale(0.5f);
+ mvp.rotate(d.rotation, 0, 1, 0);
+ u->updateDynamicBuffer(d.ubuf, 0, 64, mvp.constData());
+
+ QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
+ const QSize outputSizeInPixels = m_sc->currentPixelSize();
+
+ cb->beginPass(m_sc->currentFrameRenderTarget(), QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f), { 1.0f, 0 }, u);
+
+ cb->setGraphicsPipeline(d.ps);
+ cb->setViewport({ 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) });
+ cb->setShaderResources();
+ const QRhiCommandBuffer::VertexInput vbufBindings[] = {
+ { d.vbuf, 0 },
+ { d.vbuf, 36 * 3 * sizeof(float) }
+ };
+ cb->setVertexInput(0, 2, vbufBindings);
+ cb->draw(36);
+
+ cb->endPass();
+}
diff --git a/tests/manual/rhi/compressedtexture_bc1_subupload/compressedtexture_bc1_subupload.pro b/tests/manual/rhi/compressedtexture_bc1_subupload/compressedtexture_bc1_subupload.pro
new file mode 100644
index 0000000000..a1184fc754
--- /dev/null
+++ b/tests/manual/rhi/compressedtexture_bc1_subupload/compressedtexture_bc1_subupload.pro
@@ -0,0 +1,8 @@
+TEMPLATE = app
+
+QT += gui-private
+
+SOURCES = \
+ compressedtexture_bc1_subupload.cpp
+
+RESOURCES = compressedtexture_bc1_subupload.qrc
diff --git a/tests/manual/rhi/compressedtexture_bc1_subupload/compressedtexture_bc1_subupload.qrc b/tests/manual/rhi/compressedtexture_bc1_subupload/compressedtexture_bc1_subupload.qrc
new file mode 100644
index 0000000000..859a0a79f6
--- /dev/null
+++ b/tests/manual/rhi/compressedtexture_bc1_subupload/compressedtexture_bc1_subupload.qrc
@@ -0,0 +1,8 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+ <file alias="qt256_bc1_9mips.dds">../shared/qt256_bc1_9mips.dds</file>
+ <file alias="bwqt224_64_nomips.dds">../shared/bwqt224_64_nomips.dds</file>
+ <file alias="texture.vert.qsb">../shared/texture.vert.qsb</file>
+ <file alias="texture.frag.qsb">../shared/texture.frag.qsb</file>
+</qresource>
+</RCC>
diff --git a/tests/manual/rhi/cubemap/buildshaders.bat b/tests/manual/rhi/cubemap/buildshaders.bat
new file mode 100644
index 0000000000..ebf673875d
--- /dev/null
+++ b/tests/manual/rhi/cubemap/buildshaders.bat
@@ -0,0 +1,2 @@
+qsb --glsl "100 es,120" --hlsl 50 --msl 12 -c cubemap.vert -o cubemap.vert.qsb
+qsb --glsl "100 es,120" --hlsl 50 --msl 12 -c cubemap.frag -o cubemap.frag.qsb
diff --git a/tests/manual/rhi/cubemap/c.png b/tests/manual/rhi/cubemap/c.png
new file mode 100644
index 0000000000..7b00455e53
--- /dev/null
+++ b/tests/manual/rhi/cubemap/c.png
Binary files differ
diff --git a/tests/manual/rhi/cubemap/cubemap.cpp b/tests/manual/rhi/cubemap/cubemap.cpp
new file mode 100644
index 0000000000..0420a38cbd
--- /dev/null
+++ b/tests/manual/rhi/cubemap/cubemap.cpp
@@ -0,0 +1,179 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "../shared/examplefw.h"
+#include "../shared/cube.h"
+
+struct {
+ QVector<QRhiResource *> releasePool;
+ QRhiBuffer *vbuf = nullptr;
+ QRhiBuffer *ubuf = nullptr;
+ QRhiTexture *tex = nullptr;
+ QRhiSampler *sampler = nullptr;
+ QRhiShaderResourceBindings *srb = nullptr;
+ QRhiGraphicsPipeline *ps = nullptr;
+ QRhiResourceUpdateBatch *initialUpdates = nullptr;
+} d;
+
+void Window::customInit()
+{
+ d.vbuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube));
+ d.vbuf->build();
+ d.releasePool << d.vbuf;
+
+ d.ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64);
+ d.ubuf->build();
+ d.releasePool << d.ubuf;
+
+ const QSize cubeMapSize(512, 512);
+ d.tex = m_r->newTexture(QRhiTexture::RGBA8, cubeMapSize, 1, QRhiTexture::CubeMap);
+ d.releasePool << d.tex;
+ d.tex->build();
+
+ d.initialUpdates = m_r->nextResourceUpdateBatch();
+ d.initialUpdates->uploadStaticBuffer(d.vbuf, cube);
+
+ QImage img = QImage(":/c.png").mirrored().convertToFormat(QImage::Format_RGBA8888);
+ // just use the same image for all faces for now
+ QRhiTextureSubresourceUploadDescription subresDesc(img);
+ QRhiTextureUploadDescription desc({
+ { 0, 0, subresDesc }, // +X
+ { 1, 0, subresDesc }, // -X
+ { 2, 0, subresDesc }, // +Y
+ { 3, 0, subresDesc }, // -Y
+ { 4, 0, subresDesc }, // +Z
+ { 5, 0, subresDesc } // -Z
+ });
+ d.initialUpdates->uploadTexture(d.tex, desc);
+
+ d.sampler = m_r->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
+ QRhiSampler::Repeat, QRhiSampler::Repeat);
+ d.releasePool << d.sampler;
+ d.sampler->build();
+
+ d.srb = m_r->newShaderResourceBindings();
+ d.releasePool << d.srb;
+ d.srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, d.ubuf),
+ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, d.tex, d.sampler)
+ });
+ d.srb->build();
+
+ d.ps = m_r->newGraphicsPipeline();
+ d.releasePool << d.ps;
+
+ d.ps->setDepthTest(true);
+ d.ps->setDepthWrite(true);
+ d.ps->setDepthOp(QRhiGraphicsPipeline::LessOrEqual);
+
+ d.ps->setCullMode(QRhiGraphicsPipeline::Front); // we are inside the cube so cull front, not back
+ d.ps->setFrontFace(QRhiGraphicsPipeline::CCW); // front is ccw in the cube data
+
+ QShader vs = getShader(QLatin1String(":/cubemap.vert.qsb"));
+ Q_ASSERT(vs.isValid());
+ QShader fs = getShader(QLatin1String(":/cubemap.frag.qsb"));
+ Q_ASSERT(fs.isValid());
+ d.ps->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, vs },
+ { QRhiGraphicsShaderStage::Fragment, fs }
+ });
+
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 3 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float3, 0 }
+ });
+
+ d.ps->setVertexInputLayout(inputLayout);
+ d.ps->setShaderResourceBindings(d.srb);
+ d.ps->setRenderPassDescriptor(m_rp);
+
+ d.ps->build();
+}
+
+void Window::customRelease()
+{
+ qDeleteAll(d.releasePool);
+ d.releasePool.clear();
+}
+
+void Window::customRender()
+{
+ const QSize outputSizeInPixels = m_sc->currentPixelSize();
+ QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
+ QRhiResourceUpdateBatch *u = m_r->nextResourceUpdateBatch();
+
+ if (d.initialUpdates) {
+ u->merge(d.initialUpdates);
+ d.initialUpdates->release();
+ d.initialUpdates = nullptr;
+ }
+
+ QMatrix4x4 mvp = m_r->clipSpaceCorrMatrix();
+ mvp.perspective(90.0f, outputSizeInPixels.width() / (float) outputSizeInPixels.height(), 0.01f, 1000.0f);
+ // cube vertices go from -1..1
+ mvp.scale(10);
+ static float rx = 0;
+ mvp.rotate(rx, 1, 0, 0);
+ rx += 0.5f;
+ // no translation
+ u->updateDynamicBuffer(d.ubuf, 0, 64, mvp.constData());
+
+ cb->beginPass(m_sc->currentFrameRenderTarget(), QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f), { 1.0f, 0 }, u);
+ cb->setGraphicsPipeline(d.ps);
+ cb->setViewport(QRhiViewport(0, 0, outputSizeInPixels.width(), outputSizeInPixels.height()));
+ cb->setShaderResources();
+ const QRhiCommandBuffer::VertexInput vbufBinding(d.vbuf, 0);
+ cb->setVertexInput(0, 1, &vbufBinding);
+ cb->draw(36);
+ cb->endPass();
+}
diff --git a/tests/manual/rhi/cubemap/cubemap.frag b/tests/manual/rhi/cubemap/cubemap.frag
new file mode 100644
index 0000000000..13a365ed0c
--- /dev/null
+++ b/tests/manual/rhi/cubemap/cubemap.frag
@@ -0,0 +1,10 @@
+#version 440
+
+layout(location = 0) in vec3 v_coord;
+layout(location = 0) out vec4 fragColor;
+layout(binding = 1) uniform samplerCube tex;
+
+void main()
+{
+ fragColor = vec4(texture(tex, v_coord).rgb, 1.0);
+}
diff --git a/tests/manual/rhi/cubemap/cubemap.frag.qsb b/tests/manual/rhi/cubemap/cubemap.frag.qsb
new file mode 100644
index 0000000000..d7cc025554
--- /dev/null
+++ b/tests/manual/rhi/cubemap/cubemap.frag.qsb
Binary files differ
diff --git a/tests/manual/rhi/cubemap/cubemap.pro b/tests/manual/rhi/cubemap/cubemap.pro
new file mode 100644
index 0000000000..61910ffe20
--- /dev/null
+++ b/tests/manual/rhi/cubemap/cubemap.pro
@@ -0,0 +1,8 @@
+TEMPLATE = app
+
+QT += gui-private
+
+SOURCES = \
+ cubemap.cpp
+
+RESOURCES = cubemap.qrc
diff --git a/tests/manual/rhi/cubemap/cubemap.qrc b/tests/manual/rhi/cubemap/cubemap.qrc
new file mode 100644
index 0000000000..bacd15446c
--- /dev/null
+++ b/tests/manual/rhi/cubemap/cubemap.qrc
@@ -0,0 +1,7 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+ <file>cubemap.vert.qsb</file>
+ <file>cubemap.frag.qsb</file>
+ <file>c.png</file>
+</qresource>
+</RCC>
diff --git a/tests/manual/rhi/cubemap/cubemap.vert b/tests/manual/rhi/cubemap/cubemap.vert
new file mode 100644
index 0000000000..65216dd6b0
--- /dev/null
+++ b/tests/manual/rhi/cubemap/cubemap.vert
@@ -0,0 +1,15 @@
+#version 440
+
+layout(location = 0) in vec4 position;
+layout(location = 0) out vec3 v_coord;
+layout(std140, binding = 0) uniform buf {
+ mat4 mvp;
+} ubuf;
+
+out gl_PerVertex { vec4 gl_Position; };
+
+void main()
+{
+ v_coord = position.xyz;
+ gl_Position = ubuf.mvp * position;
+}
diff --git a/tests/manual/rhi/cubemap/cubemap.vert.qsb b/tests/manual/rhi/cubemap/cubemap.vert.qsb
new file mode 100644
index 0000000000..fae136337c
--- /dev/null
+++ b/tests/manual/rhi/cubemap/cubemap.vert.qsb
Binary files differ
diff --git a/tests/manual/rhi/floattexture/floattexture.cpp b/tests/manual/rhi/floattexture/floattexture.cpp
new file mode 100644
index 0000000000..b62ef290f7
--- /dev/null
+++ b/tests/manual/rhi/floattexture/floattexture.cpp
@@ -0,0 +1,328 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "../shared/examplefw.h"
+#include <qmath.h>
+
+QByteArray loadHdr(const QString &fn, QSize *size)
+{
+ QFile f(fn);
+ if (!f.open(QIODevice::ReadOnly)) {
+ qWarning("Failed to open %s", qPrintable(fn));
+ return QByteArray();
+ }
+
+ char sig[256];
+ f.read(sig, 11);
+ if (strncmp(sig, "#?RADIANCE\n", 11))
+ return QByteArray();
+
+ QByteArray buf = f.readAll();
+ const char *p = buf.constData();
+ const char *pEnd = p + buf.size();
+
+ // Process lines until the empty one.
+ QByteArray line;
+ while (p < pEnd) {
+ char c = *p++;
+ if (c == '\n') {
+ if (line.isEmpty())
+ break;
+ if (line.startsWith(QByteArrayLiteral("FORMAT="))) {
+ const QByteArray format = line.mid(7).trimmed();
+ if (format != QByteArrayLiteral("32-bit_rle_rgbe")) {
+ qWarning("HDR format '%s' is not supported", format.constData());
+ return QByteArray();
+ }
+ }
+ line.clear();
+ } else {
+ line.append(c);
+ }
+ }
+ if (p == pEnd) {
+ qWarning("Malformed HDR image data at property strings");
+ return QByteArray();
+ }
+
+ // Get the resolution string.
+ while (p < pEnd) {
+ char c = *p++;
+ if (c == '\n')
+ break;
+ line.append(c);
+ }
+ if (p == pEnd) {
+ qWarning("Malformed HDR image data at resolution string");
+ return QByteArray();
+ }
+
+ int w = 0, h = 0;
+ // We only care about the standard orientation.
+ if (!sscanf(line.constData(), "-Y %d +X %d", &h, &w)) {
+ qWarning("Unsupported HDR resolution string '%s'", line.constData());
+ return QByteArray();
+ }
+ if (w <= 0 || h <= 0) {
+ qWarning("Invalid HDR resolution");
+ return QByteArray();
+ }
+
+ // output is RGBA32F
+ const int blockSize = 4 * sizeof(float);
+ QByteArray data;
+ data.resize(w * h * blockSize);
+
+ typedef unsigned char RGBE[4];
+ RGBE *scanline = new RGBE[w];
+
+ for (int y = 0; y < h; ++y) {
+ if (pEnd - p < 4) {
+ qWarning("Unexpected end of HDR data");
+ delete[] scanline;
+ return QByteArray();
+ }
+
+ scanline[0][0] = *p++;
+ scanline[0][1] = *p++;
+ scanline[0][2] = *p++;
+ scanline[0][3] = *p++;
+
+ if (scanline[0][0] == 2 && scanline[0][1] == 2 && scanline[0][2] < 128) {
+ // new rle, the first pixel was a dummy
+ for (int channel = 0; channel < 4; ++channel) {
+ for (int x = 0; x < w && p < pEnd; ) {
+ unsigned char c = *p++;
+ if (c > 128) { // run
+ if (p < pEnd) {
+ int repCount = c & 127;
+ c = *p++;
+ while (repCount--)
+ scanline[x++][channel] = c;
+ }
+ } else { // not a run
+ while (c-- && p < pEnd)
+ scanline[x++][channel] = *p++;
+ }
+ }
+ }
+ } else {
+ // old rle
+ scanline[0][0] = 2;
+ int bitshift = 0;
+ int x = 1;
+ while (x < w && pEnd - p >= 4) {
+ scanline[x][0] = *p++;
+ scanline[x][1] = *p++;
+ scanline[x][2] = *p++;
+ scanline[x][3] = *p++;
+
+ if (scanline[x][0] == 1 && scanline[x][1] == 1 && scanline[x][2] == 1) { // run
+ int repCount = scanline[x][3] << bitshift;
+ while (repCount--) {
+ memcpy(scanline[x], scanline[x - 1], 4);
+ ++x;
+ }
+ bitshift += 8;
+ } else { // not a run
+ ++x;
+ bitshift = 0;
+ }
+ }
+ }
+
+ // adjust for -Y orientation
+ float *fp = reinterpret_cast<float *>(data.data() + (h - 1 - y) * blockSize * w);
+ for (int x = 0; x < w; ++x) {
+ float d = qPow(2.0f, float(scanline[x][3]) - 128.0f);
+ // r, g, b, a
+ *fp++ = scanline[x][0] / 256.0f * d;
+ *fp++ = scanline[x][1] / 256.0f * d;
+ *fp++ = scanline[x][2] / 256.0f * d;
+ *fp++ = 1.0f;
+ }
+ }
+
+ delete[] scanline;
+
+ if (size)
+ *size = QSize(w, h);
+
+ return data;
+}
+
+static float vertexData[] =
+{ // Y up, CCW
+ -0.5f, 0.5f, 0.0f, 0.0f,
+ -0.5f, -0.5f, 0.0f, 1.0f,
+ 0.5f, -0.5f, 1.0f, 1.0f,
+ 0.5f, 0.5f, 1.0f, 0.0f
+};
+
+static quint16 indexData[] =
+{
+ 0, 1, 2, 0, 2, 3
+};
+
+struct {
+ QVector<QRhiResource *> releasePool;
+ QRhiBuffer *vbuf = nullptr;
+ QRhiBuffer *ibuf = nullptr;
+ QRhiBuffer *ubuf = nullptr;
+ QRhiTexture *tex = nullptr;
+ QRhiSampler *sampler = nullptr;
+ QRhiShaderResourceBindings *srb = nullptr;
+ QRhiGraphicsPipeline *ps = nullptr;
+ QRhiResourceUpdateBatch *initialUpdates = nullptr;
+ QMatrix4x4 winProj;
+} d;
+
+void Window::customInit()
+{
+ if (!m_r->isTextureFormatSupported(QRhiTexture::RGBA32F))
+ qWarning("RGBA32F texture format is not supported");
+
+ QSize size;
+ QByteArray floatData = loadHdr(QLatin1String(":/OpenfootageNET_fieldairport-512.hdr"), &size);
+ Q_ASSERT(!floatData.isEmpty());
+ qDebug() << size;
+
+ d.vbuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData));
+ d.vbuf->build();
+ d.releasePool << d.vbuf;
+
+ d.ibuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(indexData));
+ d.ibuf->build();
+ d.releasePool << d.ibuf;
+
+ d.ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68);
+ d.ubuf->build();
+ d.releasePool << d.ubuf;
+
+ d.tex = m_r->newTexture(QRhiTexture::RGBA32F, size);
+ d.releasePool << d.tex;
+ d.tex->build();
+
+ d.sampler = m_r->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
+ QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
+ d.releasePool << d.sampler;
+ d.sampler->build();
+
+ d.srb = m_r->newShaderResourceBindings();
+ d.releasePool << d.srb;
+ d.srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, d.ubuf),
+ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, d.tex, d.sampler)
+ });
+ d.srb->build();
+
+ d.ps = m_r->newGraphicsPipeline();
+ d.releasePool << d.ps;
+ d.ps->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, getShader(QLatin1String(":/texture.vert.qsb")) },
+ { QRhiGraphicsShaderStage::Fragment, getShader(QLatin1String(":/texture.frag.qsb")) }
+ });
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 4 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) }
+ });
+ d.ps->setVertexInputLayout(inputLayout);
+ d.ps->setShaderResourceBindings(d.srb);
+ d.ps->setRenderPassDescriptor(m_rp);
+ d.ps->build();
+
+ d.initialUpdates = m_r->nextResourceUpdateBatch();
+ d.initialUpdates->uploadStaticBuffer(d.vbuf, vertexData);
+ d.initialUpdates->uploadStaticBuffer(d.ibuf, indexData);
+
+ qint32 flip = 1;
+ d.initialUpdates->updateDynamicBuffer(d.ubuf, 64, 4, &flip);
+
+ QRhiTextureUploadDescription desc({ 0, 0, { floatData.constData(), floatData.size() } });
+ d.initialUpdates->uploadTexture(d.tex, desc);
+}
+
+void Window::customRelease()
+{
+ qDeleteAll(d.releasePool);
+ d.releasePool.clear();
+}
+
+void Window::customRender()
+{
+ QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
+ QRhiResourceUpdateBatch *u = m_r->nextResourceUpdateBatch();
+ if (d.initialUpdates) {
+ u->merge(d.initialUpdates);
+ d.initialUpdates->release();
+ d.initialUpdates = nullptr;
+ }
+
+ if (d.winProj != m_proj) {
+ d.winProj = m_proj;
+ QMatrix4x4 mvp = m_proj;
+ mvp.scale(2.5f);
+ u->updateDynamicBuffer(d.ubuf, 0, 64, mvp.constData());
+ }
+
+ const QSize outputSizeInPixels = m_sc->currentPixelSize();
+ cb->beginPass(m_sc->currentFrameRenderTarget(), QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f), { 1.0f, 0 }, u);
+ cb->setGraphicsPipeline(d.ps);
+ cb->setViewport({ 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) });
+ cb->setShaderResources();
+ const QRhiCommandBuffer::VertexInput vbufBinding(d.vbuf, 0);
+ cb->setVertexInput(0, 1, &vbufBinding, d.ibuf, 0, QRhiCommandBuffer::IndexUInt16);
+ cb->drawIndexed(6);
+ cb->endPass();
+}
diff --git a/tests/manual/rhi/floattexture/floattexture.pro b/tests/manual/rhi/floattexture/floattexture.pro
new file mode 100644
index 0000000000..e566da2b94
--- /dev/null
+++ b/tests/manual/rhi/floattexture/floattexture.pro
@@ -0,0 +1,8 @@
+TEMPLATE = app
+
+QT += gui-private
+
+SOURCES = \
+ floattexture.cpp
+
+RESOURCES = floattexture.qrc
diff --git a/tests/manual/rhi/floattexture/floattexture.qrc b/tests/manual/rhi/floattexture/floattexture.qrc
new file mode 100644
index 0000000000..fefb56b910
--- /dev/null
+++ b/tests/manual/rhi/floattexture/floattexture.qrc
@@ -0,0 +1,7 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+ <file alias="texture.vert.qsb">../shared/texture.vert.qsb</file>
+ <file alias="texture.frag.qsb">../shared/texture.frag.qsb</file>
+ <file alias="OpenfootageNET_fieldairport-512.hdr">../shared/OpenfootageNET_fieldairport-512.hdr</file>
+</qresource>
+</RCC>
diff --git a/tests/manual/rhi/hellominimalcrossgfxtriangle/hellominimalcrossgfxtriangle.cpp b/tests/manual/rhi/hellominimalcrossgfxtriangle/hellominimalcrossgfxtriangle.cpp
new file mode 100644
index 0000000000..a24b980173
--- /dev/null
+++ b/tests/manual/rhi/hellominimalcrossgfxtriangle/hellominimalcrossgfxtriangle.cpp
@@ -0,0 +1,554 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+// This is a compact, minimal, single-file demo of deciding the backend at
+// runtime while using the exact same shaders and rendering code without any
+// branching whatsoever once the QWindow is up and the RHI is initialized.
+
+#include <QGuiApplication>
+#include <QCommandLineParser>
+#include <QWindow>
+#include <QPlatformSurfaceEvent>
+#include <QElapsedTimer>
+
+#include <QtGui/private/qshader_p.h>
+#include <QFile>
+
+#include <QtGui/private/qrhinull_p.h>
+
+#ifndef QT_NO_OPENGL
+#include <QtGui/private/qrhigles2_p.h>
+#include <QOffscreenSurface>
+#endif
+
+#if QT_CONFIG(vulkan)
+#include <QLoggingCategory>
+#include <QtGui/private/qrhivulkan_p.h>
+#endif
+
+#ifdef Q_OS_WIN
+#include <QtGui/private/qrhid3d11_p.h>
+#endif
+
+#ifdef Q_OS_DARWIN
+#include <QtGui/private/qrhimetal_p.h>
+#endif
+
+static float vertexData[] = {
+ // Y up (note clipSpaceCorrMatrix in m_proj), 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,
+};
+
+static QShader getShader(const QString &name)
+{
+ QFile f(name);
+ if (f.open(QIODevice::ReadOnly))
+ return QShader::fromSerialized(f.readAll());
+
+ return QShader();
+}
+
+enum GraphicsApi
+{
+ OpenGL,
+ Vulkan,
+ D3D11,
+ Metal,
+ Null
+};
+
+static GraphicsApi graphicsApi;
+
+static QString graphicsApiName()
+{
+ switch (graphicsApi) {
+ case OpenGL:
+ return QLatin1String("OpenGL 2.x");
+ case Vulkan:
+ return QLatin1String("Vulkan");
+ case D3D11:
+ return QLatin1String("Direct3D 11");
+ case Metal:
+ return QLatin1String("Metal");
+ case Null:
+ return QLatin1String("Null");
+ default:
+ break;
+ }
+ return QString();
+}
+
+class Window : public QWindow
+{
+public:
+ Window();
+ ~Window();
+
+ void init();
+ void releaseResources();
+ void resizeSwapChain();
+ void releaseSwapChain();
+ void render();
+
+ void exposeEvent(QExposeEvent *) override;
+ bool event(QEvent *) override;
+
+private:
+ bool m_running = false;
+ bool m_notExposed = false;
+ bool m_newlyExposed = false;
+
+ QRhi *m_r = nullptr;
+ bool m_hasSwapChain = false;
+ QRhiSwapChain *m_sc = nullptr;
+ QRhiRenderBuffer *m_ds = nullptr;
+ QRhiRenderPassDescriptor *m_rp = nullptr;
+ QRhiBuffer *m_vbuf = nullptr;
+ bool m_vbufReady = false;
+ QRhiBuffer *m_ubuf = nullptr;
+ QRhiShaderResourceBindings *m_srb = nullptr;
+ QRhiGraphicsPipeline *m_ps = nullptr;
+ QVector<QRhiResource *> releasePool;
+
+ QMatrix4x4 m_proj;
+ float m_rotation = 0;
+ float m_opacity = 1;
+ int m_opacityDir = -1;
+
+ QElapsedTimer m_timer;
+ qint64 m_elapsedMs;
+ int m_elapsedCount;
+
+#ifndef QT_NO_OPENGL
+ QOffscreenSurface *m_fallbackSurface = nullptr;
+#endif
+};
+
+Window::Window()
+{
+ // Tell the platform plugin what we want.
+ switch (graphicsApi) {
+ case OpenGL:
+#if QT_CONFIG(opengl)
+ setSurfaceType(OpenGLSurface);
+ setFormat(QRhiGles2InitParams::adjustedFormat());
+#endif
+ break;
+ case Vulkan:
+ setSurfaceType(VulkanSurface);
+ break;
+ case D3D11:
+ setSurfaceType(OpenGLSurface); // not a typo
+ break;
+ case Metal:
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
+ setSurfaceType(MetalSurface);
+#endif
+ break;
+ default:
+ break;
+ }
+}
+
+Window::~Window()
+{
+ releaseResources();
+}
+
+void Window::exposeEvent(QExposeEvent *)
+{
+ // initialize and start rendering when the window becomes usable for graphics purposes
+ if (isExposed() && !m_running) {
+ m_running = true;
+ init();
+ resizeSwapChain();
+ }
+
+ // stop pushing frames when not exposed (or size is 0)
+ if ((!isExposed() || (m_hasSwapChain && m_sc->surfacePixelSize().isEmpty())) && m_running)
+ m_notExposed = true;
+
+ // continue when exposed again and the surface has a valid size.
+ // note that the surface size can be (0, 0) even though size() reports a valid one...
+ if (isExposed() && m_running && m_notExposed && !m_sc->surfacePixelSize().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() && !m_sc->surfacePixelSize().isEmpty())
+ render();
+}
+
+bool Window::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);
+}
+
+void Window::init()
+{
+ if (graphicsApi == Null) {
+ QRhiNullInitParams params;
+ m_r = QRhi::create(QRhi::Null, &params);
+ }
+
+#ifndef QT_NO_OPENGL
+ if (graphicsApi == OpenGL) {
+ m_fallbackSurface = QRhiGles2InitParams::newFallbackSurface();
+ QRhiGles2InitParams params;
+ params.fallbackSurface = m_fallbackSurface;
+ params.window = this;
+ m_r = QRhi::create(QRhi::OpenGLES2, &params);
+ }
+#endif
+
+#if QT_CONFIG(vulkan)
+ if (graphicsApi == Vulkan) {
+ QRhiVulkanInitParams params;
+ params.inst = vulkanInstance();
+ params.window = this;
+ m_r = QRhi::create(QRhi::Vulkan, &params);
+ }
+#endif
+
+#ifdef Q_OS_WIN
+ if (graphicsApi == D3D11) {
+ QRhiD3D11InitParams params;
+ m_r = QRhi::create(QRhi::D3D11, &params);
+ }
+#endif
+
+#ifdef Q_OS_DARWIN
+ if (graphicsApi == Metal) {
+ QRhiMetalInitParams params;
+ m_r = QRhi::create(QRhi::Metal, &params);
+ }
+#endif
+
+ if (!m_r)
+ qFatal("Failed to create RHI backend");
+
+ // now onto the backend-independent init
+
+ m_sc = m_r->newSwapChain();
+ // allow depth-stencil, although we do not actually enable depth test/write for the triangle
+ m_ds = m_r->newRenderBuffer(QRhiRenderBuffer::DepthStencil,
+ QSize(), // no need to set the size yet
+ 1,
+ QRhiRenderBuffer::UsedWithSwapChainOnly);
+ releasePool << m_ds;
+ m_sc->setWindow(this);
+ m_sc->setDepthStencil(m_ds);
+ m_rp = m_sc->newCompatibleRenderPassDescriptor();
+ releasePool << m_rp;
+ m_sc->setRenderPassDescriptor(m_rp);
+
+ m_vbuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData));
+ releasePool << m_vbuf;
+ m_vbuf->build();
+ m_vbufReady = false;
+
+ m_ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68);
+ releasePool << m_ubuf;
+ m_ubuf->build();
+
+ m_srb = m_r->newShaderResourceBindings();
+ releasePool << m_srb;
+ m_srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, m_ubuf)
+ });
+ m_srb->build();
+
+ m_ps = m_r->newGraphicsPipeline();
+ releasePool << m_ps;
+
+ QRhiGraphicsPipeline::TargetBlend premulAlphaBlend;
+ premulAlphaBlend.enable = true;
+ m_ps->setTargetBlends({ premulAlphaBlend });
+
+ const QShader vs = getShader(QLatin1String(":/color.vert.qsb"));
+ if (!vs.isValid())
+ qFatal("Failed to load shader pack (vertex)");
+ const QShader fs = getShader(QLatin1String(":/color.frag.qsb"));
+ if (!fs.isValid())
+ qFatal("Failed to load shader pack (fragment)");
+
+ m_ps->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, vs },
+ { QRhiGraphicsShaderStage::Fragment, fs }
+ });
+
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 5 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) }
+ });
+
+ m_ps->setVertexInputLayout(inputLayout);
+ m_ps->setShaderResourceBindings(m_srb);
+ m_ps->setRenderPassDescriptor(m_rp);
+
+ m_ps->build();
+}
+
+void Window::releaseResources()
+{
+ qDeleteAll(releasePool);
+ releasePool.clear();
+
+ delete m_sc; // native swapchain is likely released already
+ m_sc = nullptr;
+
+ delete m_r;
+
+#ifndef QT_NO_OPENGL
+ delete m_fallbackSurface;
+#endif
+}
+
+void Window::resizeSwapChain()
+{
+ const QSize outputSize = m_sc->surfacePixelSize();
+
+ m_ds->setPixelSize(outputSize);
+ m_ds->build(); // == m_ds->release(); m_ds->build();
+
+ m_hasSwapChain = m_sc->buildOrResize();
+
+ m_elapsedMs = 0;
+ m_elapsedCount = 0;
+
+ m_proj = m_r->clipSpaceCorrMatrix();
+ m_proj.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 100.0f);
+ m_proj.translate(0, 0, -4);
+}
+
+void Window::releaseSwapChain()
+{
+ if (m_hasSwapChain) {
+ m_hasSwapChain = false;
+ m_sc->release();
+ }
+}
+
+void Window::render()
+{
+ if (!m_hasSwapChain || m_notExposed)
+ return;
+
+ // If the window got resized or got newly exposed, resize the swapchain.
+ // (the newly-exposed case is not actually required by some
+ // platforms/backends, but f.ex. Vulkan on Windows seems to need it)
+ if (m_sc->currentPixelSize() != m_sc->surfacePixelSize() || m_newlyExposed) {
+ resizeSwapChain();
+ if (!m_hasSwapChain)
+ return;
+ m_newlyExposed = false;
+ }
+
+ // Start a new frame. This is where we block when too far ahead of
+ // GPU/present, and that's what throttles the thread to the refresh rate.
+ // (except for OpenGL where it happens either in endFrame or somewhere else
+ // depending on the GL implementation)
+ QRhi::FrameOpResult r = m_r->beginFrame(m_sc);
+ if (r == QRhi::FrameOpSwapChainOutOfDate) {
+ resizeSwapChain();
+ if (!m_hasSwapChain)
+ return;
+ r = m_r->beginFrame(m_sc);
+ }
+ if (r != QRhi::FrameOpSuccess) {
+ requestUpdate();
+ return;
+ }
+
+ if (m_elapsedCount)
+ m_elapsedMs += m_timer.elapsed();
+ m_timer.restart();
+ m_elapsedCount += 1;
+ if (m_elapsedMs >= 4000) {
+ qDebug("%f", m_elapsedCount / 4.0f);
+ m_elapsedMs = 0;
+ m_elapsedCount = 0;
+ }
+
+ // Set up buffer updates.
+ QRhiResourceUpdateBatch *u = m_r->nextResourceUpdateBatch();
+ if (!m_vbufReady) {
+ m_vbufReady = true;
+ u->uploadStaticBuffer(m_vbuf, vertexData);
+ }
+ m_rotation += 1.0f;
+ QMatrix4x4 mvp = m_proj;
+ mvp.rotate(m_rotation, 0, 1, 0);
+ u->updateDynamicBuffer(m_ubuf, 0, 64, mvp.constData());
+ 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);
+ }
+ u->updateDynamicBuffer(m_ubuf, 64, 4, &m_opacity);
+
+ QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
+ const QSize outputSizeInPixels = m_sc->currentPixelSize();
+
+ // Apply buffer updates, clear, start the renderpass (where applicable).
+ cb->beginPass(m_sc->currentFrameRenderTarget(), QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f), { 1.0f, 0 }, u);
+
+ cb->setGraphicsPipeline(m_ps);
+ cb->setViewport({ 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) });
+ cb->setShaderResources();
+
+ const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf, 0);
+ cb->setVertexInput(0, 1, &vbufBinding);
+ cb->draw(3);
+
+ cb->endPass();
+
+ // Submit.
+ m_r->endFrame(m_sc);
+
+ requestUpdate(); // render continuously, throttled by the presentation rate (due to beginFrame above)
+}
+
+int main(int argc, char **argv)
+{
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+ QGuiApplication app(argc, argv);
+
+ // Defaults.
+#if defined(Q_OS_WIN)
+ graphicsApi = D3D11;
+#elif defined(Q_OS_DARWIN)
+ graphicsApi = Metal;
+#elif QT_CONFIG(vulkan)
+ graphicsApi = Vulkan;
+#else
+ graphicsApi = OpenGL;
+#endif
+
+ // Allow overriding via the command line.
+ QCommandLineParser cmdLineParser;
+ cmdLineParser.addHelpOption();
+ QCommandLineOption glOption({ "g", "opengl" }, QLatin1String("OpenGL (2.x)"));
+ cmdLineParser.addOption(glOption);
+ QCommandLineOption vkOption({ "v", "vulkan" }, QLatin1String("Vulkan"));
+ cmdLineParser.addOption(vkOption);
+ QCommandLineOption d3dOption({ "d", "d3d11" }, QLatin1String("Direct3D 11"));
+ cmdLineParser.addOption(d3dOption);
+ QCommandLineOption mtlOption({ "m", "metal" }, QLatin1String("Metal"));
+ cmdLineParser.addOption(mtlOption);
+ QCommandLineOption nullOption({ "n", "null" }, QLatin1String("Null"));
+ cmdLineParser.addOption(nullOption);
+ cmdLineParser.process(app);
+ if (cmdLineParser.isSet(glOption))
+ graphicsApi = OpenGL;
+ if (cmdLineParser.isSet(vkOption))
+ graphicsApi = Vulkan;
+ if (cmdLineParser.isSet(d3dOption))
+ graphicsApi = D3D11;
+ if (cmdLineParser.isSet(mtlOption))
+ graphicsApi = Metal;
+ if (cmdLineParser.isSet(nullOption))
+ graphicsApi = Null;
+
+ // Vulkan setup.
+#if QT_CONFIG(vulkan)
+ QVulkanInstance inst;
+ if (graphicsApi == Vulkan) {
+ if (!inst.create()) {
+ qWarning("Failed to create Vulkan instance, switching to OpenGL");
+ graphicsApi = OpenGL;
+ }
+ }
+#endif
+
+ // Create and show the window.
+ Window w;
+#if QT_CONFIG(vulkan)
+ if (graphicsApi == Vulkan)
+ w.setVulkanInstance(&inst);
+#endif
+ w.resize(1280, 720);
+ w.setTitle(graphicsApiName());
+ w.show();
+
+ // Window::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 (w.handle())
+ w.releaseSwapChain();
+
+ return app.exec();
+}
diff --git a/tests/manual/rhi/hellominimalcrossgfxtriangle/hellominimalcrossgfxtriangle.pro b/tests/manual/rhi/hellominimalcrossgfxtriangle/hellominimalcrossgfxtriangle.pro
new file mode 100644
index 0000000000..eeef33ff6a
--- /dev/null
+++ b/tests/manual/rhi/hellominimalcrossgfxtriangle/hellominimalcrossgfxtriangle.pro
@@ -0,0 +1,8 @@
+TEMPLATE = app
+
+QT += gui-private
+
+SOURCES = \
+ hellominimalcrossgfxtriangle.cpp
+
+RESOURCES = hellominimalcrossgfxtriangle.qrc
diff --git a/tests/manual/rhi/hellominimalcrossgfxtriangle/hellominimalcrossgfxtriangle.qrc b/tests/manual/rhi/hellominimalcrossgfxtriangle/hellominimalcrossgfxtriangle.qrc
new file mode 100644
index 0000000000..8cec4fa9ec
--- /dev/null
+++ b/tests/manual/rhi/hellominimalcrossgfxtriangle/hellominimalcrossgfxtriangle.qrc
@@ -0,0 +1,6 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+ <file alias="color.vert.qsb">../shared/color.vert.qsb</file>
+ <file alias="color.frag.qsb">../shared/color.frag.qsb</file>
+</qresource>
+</RCC>
diff --git a/tests/manual/rhi/mrt/buildshaders.bat b/tests/manual/rhi/mrt/buildshaders.bat
new file mode 100644
index 0000000000..75741448b1
--- /dev/null
+++ b/tests/manual/rhi/mrt/buildshaders.bat
@@ -0,0 +1,2 @@
+qsb --glsl "100 es,120" --hlsl 50 --msl 12 -c mrt.vert -o mrt.vert.qsb
+qsb --glsl "100 es,120" --hlsl 50 --msl 12 -c mrt.frag -o mrt.frag.qsb
diff --git a/tests/manual/rhi/mrt/mrt.cpp b/tests/manual/rhi/mrt/mrt.cpp
new file mode 100644
index 0000000000..2bbd879dc4
--- /dev/null
+++ b/tests/manual/rhi/mrt/mrt.cpp
@@ -0,0 +1,296 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "../shared/examplefw.h"
+
+// Exercises MRT, by attaching 4 textures to a single QRhiTextureRenderTarget.
+// The fragment shader outputs to all four targets.
+// The textures are then used to render 4 quads, so their contents can be confirmed.
+
+const int ATTCOUNT = 4;
+
+static float quadVertexData[] =
+{ // Y up, CCW
+ -0.5f, 0.5f, 0.0f, 0.0f,
+ -0.5f, -0.5f, 0.0f, 1.0f,
+ 0.5f, -0.5f, 1.0f, 1.0f,
+ 0.5f, 0.5f, 1.0f, 0.0f
+};
+
+static quint16 quadIndexData[] =
+{
+ 0, 1, 2, 0, 2, 3
+};
+
+static float triangleData[] =
+{ // Y up, 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,
+};
+
+struct {
+ QVector<QRhiResource *> releasePool;
+ QRhiBuffer *vbuf = nullptr;
+ QRhiBuffer *ibuf = nullptr;
+ QRhiBuffer *ubuf = nullptr;
+ QRhiTextureRenderTarget *rt = nullptr;
+ QRhiRenderPassDescriptor *rtRp = nullptr;
+ QRhiSampler *sampler = nullptr;
+ QRhiGraphicsPipeline *ps = nullptr;
+ QRhiResourceUpdateBatch *initialUpdates = nullptr;
+ QMatrix4x4 winProj;
+ struct PerColorBuffer {
+ QRhiTexture *tex = nullptr;
+ QRhiShaderResourceBindings *srb = nullptr;
+ };
+ PerColorBuffer colData[ATTCOUNT];
+
+ QRhiBuffer *triUbuf = nullptr;
+ QRhiShaderResourceBindings *triSrb = nullptr;
+ QRhiGraphicsPipeline *triPs = nullptr;
+ float triRot = 0;
+ QMatrix4x4 triBaseMvp;
+} d;
+
+void Window::customInit()
+{
+ qDebug("Max color attachments: %d", m_r->resourceLimit(QRhi::MaxColorAttachments));
+ if (m_r->resourceLimit(QRhi::MaxColorAttachments) < 4)
+ qWarning("MRT is not supported");
+
+ d.vbuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(quadVertexData) + sizeof(triangleData));
+ d.vbuf->build();
+ d.releasePool << d.vbuf;
+
+ d.ibuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(quadIndexData));
+ d.ibuf->build();
+ d.releasePool << d.ibuf;
+
+ const int oneRoundedUniformBlockSize = m_r->ubufAligned(68);
+ d.ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, oneRoundedUniformBlockSize * ATTCOUNT);
+ d.ubuf->build();
+ d.releasePool << d.ubuf;
+
+ d.sampler = m_r->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
+ QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
+ d.releasePool << d.sampler;
+ d.sampler->build();
+
+ for (int i = 0; i < ATTCOUNT; ++i) {
+ QRhiTexture *tex = m_r->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 1, QRhiTexture::RenderTarget);
+ d.releasePool << tex;
+ tex->build();
+
+ QRhiShaderResourceBindings *srb = m_r->newShaderResourceBindings();
+ d.releasePool << srb;
+ srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage,
+ d.ubuf, i * oneRoundedUniformBlockSize, 68),
+ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, tex, d.sampler)
+ });
+ srb->build();
+
+ d.colData[i].tex = tex;
+ d.colData[i].srb = srb;
+ }
+
+ QRhiTextureRenderTargetDescription rtDesc;
+ QVector<QRhiColorAttachment> att;
+ for (int i = 0; i < ATTCOUNT; ++i)
+ att.append(QRhiColorAttachment(d.colData[i].tex));
+ rtDesc.setColorAttachments(att);
+ d.rt = m_r->newTextureRenderTarget(rtDesc);
+ d.releasePool << d.rt;
+ d.rtRp = d.rt->newCompatibleRenderPassDescriptor();
+ d.releasePool << d.rtRp;
+ d.rt->setRenderPassDescriptor(d.rtRp);
+ d.rt->build();
+
+ d.ps = m_r->newGraphicsPipeline();
+ d.releasePool << d.ps;
+ d.ps->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, getShader(QLatin1String(":/texture.vert.qsb")) },
+ { QRhiGraphicsShaderStage::Fragment, getShader(QLatin1String(":/texture.frag.qsb")) }
+ });
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 4 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) }
+ });
+ d.ps->setVertexInputLayout(inputLayout);
+ d.ps->setShaderResourceBindings(d.colData[0].srb); // all of them are layout-compatible
+ d.ps->setRenderPassDescriptor(m_rp);
+ d.ps->build();
+
+ d.initialUpdates = m_r->nextResourceUpdateBatch();
+ d.initialUpdates->uploadStaticBuffer(d.vbuf, 0, sizeof(quadVertexData), quadVertexData);
+ d.initialUpdates->uploadStaticBuffer(d.vbuf, sizeof(quadVertexData), sizeof(triangleData), triangleData);
+ d.initialUpdates->uploadStaticBuffer(d.ibuf, quadIndexData);
+
+ qint32 flip = m_r->isYUpInFramebuffer() ? 1 : 0;
+ for (int i = 0; i < ATTCOUNT; ++i)
+ d.initialUpdates->updateDynamicBuffer(d.ubuf, i * oneRoundedUniformBlockSize + 64, 4, &flip);
+
+ // triangle
+ d.triUbuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68);
+ d.releasePool << d.triUbuf;
+ d.triUbuf->build();
+
+ d.triSrb = m_r->newShaderResourceBindings();
+ d.releasePool << d.triSrb;
+ d.triSrb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, d.triUbuf)
+ });
+ d.triSrb->build();
+
+ d.triPs = m_r->newGraphicsPipeline();
+ d.releasePool << d.triPs;
+ d.triPs->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, getShader(QLatin1String(":/mrt.vert.qsb")) },
+ { QRhiGraphicsShaderStage::Fragment, getShader(QLatin1String(":/mrt.frag.qsb")) }
+ });
+ QVector<QRhiGraphicsPipeline::TargetBlend> blends;
+ for (int i = 0; i < ATTCOUNT; ++i) {
+ QRhiGraphicsPipeline::TargetBlend blend;
+ blends.append(blend);
+ }
+ d.triPs->setTargetBlends(blends);
+ inputLayout.setBindings({
+ { 5 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) }
+ });
+ d.triPs->setVertexInputLayout(inputLayout);
+ d.triPs->setShaderResourceBindings(d.triSrb);
+ d.triPs->setRenderPassDescriptor(d.rtRp);
+ d.triPs->build();
+
+ d.triBaseMvp = m_r->clipSpaceCorrMatrix();
+ d.triBaseMvp.perspective(45.0f, d.rt->pixelSize().width() / float(d.rt->pixelSize().height()), 0.01f, 1000.0f);
+ d.triBaseMvp.translate(0, 0, -2);
+ float opacity = 1.0f;
+ d.initialUpdates->updateDynamicBuffer(d.triUbuf, 64, 4, &opacity);
+}
+
+void Window::customRelease()
+{
+ qDeleteAll(d.releasePool);
+ d.releasePool.clear();
+}
+
+void Window::customRender()
+{
+ QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
+ QRhiResourceUpdateBatch *u = m_r->nextResourceUpdateBatch();
+ if (d.initialUpdates) {
+ u->merge(d.initialUpdates);
+ d.initialUpdates->release();
+ d.initialUpdates = nullptr;
+ }
+
+ QMatrix4x4 triMvp = d.triBaseMvp;
+ triMvp.rotate(d.triRot, 0, 1, 0);
+ d.triRot += 1;
+ u->updateDynamicBuffer(d.triUbuf, 0, 64, triMvp.constData());
+
+ cb->beginPass(d.rt, QColor::fromRgbF(0.5f, 0.2f, 0.0f, 1.0f), { 1.0f, 0 }, u);
+ cb->setGraphicsPipeline(d.triPs);
+ cb->setViewport({ 0, 0, float(d.rt->pixelSize().width()), float(d.rt->pixelSize().height()) });
+ cb->setShaderResources();
+ QRhiCommandBuffer::VertexInput vbufBinding(d.vbuf, sizeof(quadVertexData));
+ cb->setVertexInput(0, 1, &vbufBinding);
+ cb->draw(3);
+ cb->endPass();
+
+ u = m_r->nextResourceUpdateBatch();
+ if (d.winProj != m_proj) {
+ d.winProj = m_proj;
+ const int oneRoundedUniformBlockSize = m_r->ubufAligned(68);
+ for (int i = 0; i < ATTCOUNT; ++i) {
+ QMatrix4x4 mvp = m_proj;
+ switch (i) {
+ case 0:
+ mvp.translate(-2.0f, 0, 0);
+ break;
+ case 1:
+ mvp.translate(-0.8f, 0, 0);
+ break;
+ case 2:
+ mvp.translate(0.4f, 0, 0);
+ break;
+ case 3:
+ mvp.translate(1.6f, 0, 0);
+ break;
+ default:
+ Q_UNREACHABLE();
+ break;
+ }
+ u->updateDynamicBuffer(d.ubuf, i * oneRoundedUniformBlockSize, 64, mvp.constData());
+ }
+ }
+
+ const QSize outputSizeInPixels = m_sc->currentPixelSize();
+ cb->beginPass(m_sc->currentFrameRenderTarget(), QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f), { 1.0f, 0 }, u);
+ cb->setGraphicsPipeline(d.ps);
+ cb->setViewport({ 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) });
+ vbufBinding.second = 0;
+ cb->setVertexInput(0, 1, &vbufBinding, d.ibuf, 0, QRhiCommandBuffer::IndexUInt16);
+ for (int i = 0; i < ATTCOUNT; ++i) {
+ cb->setShaderResources(d.colData[i].srb);
+ cb->drawIndexed(6);
+ }
+ cb->endPass();
+}
diff --git a/tests/manual/rhi/mrt/mrt.frag b/tests/manual/rhi/mrt/mrt.frag
new file mode 100644
index 0000000000..9166b552b9
--- /dev/null
+++ b/tests/manual/rhi/mrt/mrt.frag
@@ -0,0 +1,22 @@
+#version 440
+
+layout(location = 0) in vec3 v_color;
+
+layout(location = 0) out vec4 c0;
+layout(location = 1) out vec4 c1;
+layout(location = 2) out vec4 c2;
+layout(location = 3) out vec4 c3;
+
+layout(std140, binding = 0) uniform buf {
+ mat4 mvp;
+ float opacity;
+} ubuf;
+
+void main()
+{
+ vec4 c = vec4(v_color * ubuf.opacity, ubuf.opacity);
+ c0 = vec4(c.r, 0, 0, c.a);
+ c1 = vec4(0, c.g, 0, c.a);
+ c2 = vec4(0, 0, c.b, c.a);
+ c3 = c;
+}
diff --git a/tests/manual/rhi/mrt/mrt.frag.qsb b/tests/manual/rhi/mrt/mrt.frag.qsb
new file mode 100644
index 0000000000..389b6affa3
--- /dev/null
+++ b/tests/manual/rhi/mrt/mrt.frag.qsb
Binary files differ
diff --git a/tests/manual/rhi/mrt/mrt.pro b/tests/manual/rhi/mrt/mrt.pro
new file mode 100644
index 0000000000..7cde425488
--- /dev/null
+++ b/tests/manual/rhi/mrt/mrt.pro
@@ -0,0 +1,8 @@
+TEMPLATE = app
+
+QT += gui-private
+
+SOURCES = \
+ mrt.cpp
+
+RESOURCES = mrt.qrc
diff --git a/tests/manual/rhi/mrt/mrt.qrc b/tests/manual/rhi/mrt/mrt.qrc
new file mode 100644
index 0000000000..275f476013
--- /dev/null
+++ b/tests/manual/rhi/mrt/mrt.qrc
@@ -0,0 +1,8 @@
+<!DOCTYPE RCC><RCC version="1.0">
+ <qresource>
+ <file>mrt.vert.qsb</file>
+ <file>mrt.frag.qsb</file>
+ <file alias="texture.vert.qsb">../shared/texture.vert.qsb</file>
+ <file alias="texture.frag.qsb">../shared/texture.frag.qsb</file>
+</qresource>
+</RCC>
diff --git a/tests/manual/rhi/mrt/mrt.vert b/tests/manual/rhi/mrt/mrt.vert
new file mode 100644
index 0000000000..43af543f44
--- /dev/null
+++ b/tests/manual/rhi/mrt/mrt.vert
@@ -0,0 +1,19 @@
+#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;
+} ubuf;
+
+out gl_PerVertex { vec4 gl_Position; };
+
+void main()
+{
+ v_color = color;
+ gl_Position = ubuf.mvp * position;
+}
diff --git a/tests/manual/rhi/mrt/mrt.vert.qsb b/tests/manual/rhi/mrt/mrt.vert.qsb
new file mode 100644
index 0000000000..ff8c0e50bb
--- /dev/null
+++ b/tests/manual/rhi/mrt/mrt.vert.qsb
Binary files differ
diff --git a/tests/manual/rhi/msaarenderbuffer/msaarenderbuffer.cpp b/tests/manual/rhi/msaarenderbuffer/msaarenderbuffer.cpp
new file mode 100644
index 0000000000..b61fb91c6d
--- /dev/null
+++ b/tests/manual/rhi/msaarenderbuffer/msaarenderbuffer.cpp
@@ -0,0 +1,259 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "../shared/examplefw.h"
+
+// Uses a multisample renderbuffer (whatever that may be on a given backend) to
+// render to and then resolves the samples into a non-multisample texture.
+
+static float vertexData[] =
+{ // Y up, CCW
+ -0.5f, 0.5f, 0.0f, 0.0f,
+ -0.5f, -0.5f, 0.0f, 1.0f,
+ 0.5f, -0.5f, 1.0f, 1.0f,
+ 0.5f, 0.5f, 1.0f, 0.0f
+};
+
+static quint16 indexData[] =
+{
+ 0, 1, 2, 0, 2, 3
+};
+
+static float triangleData[] =
+{ // Y up, 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,
+};
+
+struct {
+ QVector<QRhiResource *> releasePool;
+ QRhiBuffer *vbuf = nullptr;
+ QRhiBuffer *ibuf = nullptr;
+ QRhiBuffer *ubuf = nullptr;
+ QRhiRenderBuffer *rb = nullptr;
+ QRhiTextureRenderTarget *rt = nullptr;
+ QRhiRenderPassDescriptor *rtRp = nullptr;
+ QRhiTexture *tex = nullptr;
+ QRhiSampler *sampler = nullptr;
+ QRhiBuffer *triUbuf = nullptr;
+ QRhiShaderResourceBindings *triSrb = nullptr;
+ QRhiGraphicsPipeline *triPs = nullptr;
+ QRhiShaderResourceBindings *srb = nullptr;
+ QRhiGraphicsPipeline *ps = nullptr;
+ QRhiResourceUpdateBatch *initialUpdates = nullptr;
+ QMatrix4x4 triBaseMvp;
+ float triRot = 0;
+ QMatrix4x4 winProj;
+} d;
+
+void Window::customInit()
+{
+ d.vbuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData) + sizeof(triangleData));
+ d.vbuf->build();
+ d.releasePool << d.vbuf;
+
+ d.ibuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(indexData));
+ d.ibuf->build();
+ d.releasePool << d.ibuf;
+
+ d.ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68);
+ d.ubuf->build();
+ d.releasePool << d.ubuf;
+
+ d.rb = m_r->newRenderBuffer(QRhiRenderBuffer::Color, QSize(512, 512), 4); // 4x MSAA
+ d.rb->build();
+ d.releasePool << d.rb;
+
+ // the non-msaa texture that will be the destination in the resolve
+ d.tex = m_r->newTexture(QRhiTexture::RGBA8, d.rb->pixelSize(), 1, QRhiTexture::RenderTarget);
+ d.releasePool << d.tex;
+ d.tex->build();
+
+ // rb is multisample, instead of writing out the msaa data into it,
+ // resolve into d.tex at the end of each render pass
+ QRhiTextureRenderTargetDescription rtDesc;
+ QRhiColorAttachment rtAtt(d.rb);
+ rtAtt.setResolveTexture(d.tex);
+ rtDesc.setColorAttachments({ rtAtt });
+
+ d.rt = m_r->newTextureRenderTarget(rtDesc);
+ d.releasePool << d.rt;
+ d.rtRp = d.rt->newCompatibleRenderPassDescriptor();
+ d.releasePool << d.rtRp;
+ d.rt->setRenderPassDescriptor(d.rtRp);
+ d.rt->build();
+
+ d.triUbuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68);
+ d.releasePool << d.triUbuf;
+ d.triUbuf->build();
+
+ d.triSrb = m_r->newShaderResourceBindings();
+ d.releasePool << d.triSrb;
+ d.triSrb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, d.triUbuf)
+ });
+ d.triSrb->build();
+
+ d.triPs = m_r->newGraphicsPipeline();
+ d.releasePool << d.triPs;
+ d.triPs->setSampleCount(4); // must match the render target
+ d.triPs->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, getShader(QLatin1String(":/color.vert.qsb")) },
+ { QRhiGraphicsShaderStage::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) }
+ });
+ d.triPs->setVertexInputLayout(inputLayout);
+ d.triPs->setShaderResourceBindings(d.triSrb);
+ d.triPs->setRenderPassDescriptor(d.rtRp);
+ d.triPs->build();
+
+ d.sampler = m_r->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
+ QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
+ d.releasePool << d.sampler;
+ d.sampler->build();
+
+ d.srb = m_r->newShaderResourceBindings();
+ d.releasePool << d.srb;
+ d.srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, d.ubuf),
+ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, d.tex, d.sampler)
+ });
+ d.srb->build();
+
+ d.ps = m_r->newGraphicsPipeline();
+ d.releasePool << d.ps;
+ d.ps->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, getShader(QLatin1String(":/texture.vert.qsb")) },
+ { QRhiGraphicsShaderStage::Fragment, getShader(QLatin1String(":/texture.frag.qsb")) }
+ });
+ inputLayout.setBindings({
+ { 4 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) }
+ });
+ d.ps->setVertexInputLayout(inputLayout);
+ d.ps->setShaderResourceBindings(d.srb);
+ d.ps->setRenderPassDescriptor(m_rp);
+ d.ps->build();
+
+ d.initialUpdates = m_r->nextResourceUpdateBatch();
+ d.initialUpdates->uploadStaticBuffer(d.vbuf, 0, sizeof(vertexData), vertexData);
+ d.initialUpdates->uploadStaticBuffer(d.vbuf, sizeof(vertexData), sizeof(triangleData), triangleData);
+ d.initialUpdates->uploadStaticBuffer(d.ibuf, indexData);
+
+ d.triBaseMvp = m_r->clipSpaceCorrMatrix();
+ d.triBaseMvp.perspective(45.0f, d.rb->pixelSize().width() / float(d.rb->pixelSize().height()), 0.01f, 1000.0f);
+ d.triBaseMvp.translate(0, 0, -2);
+ float opacity = 1.0f;
+ d.initialUpdates->updateDynamicBuffer(d.triUbuf, 64, 4, &opacity);
+
+ qint32 flip = m_r->isYUpInFramebuffer() ? 1 : 0;
+ d.initialUpdates->updateDynamicBuffer(d.ubuf, 64, 4, &flip);
+}
+
+void Window::customRelease()
+{
+ qDeleteAll(d.releasePool);
+ d.releasePool.clear();
+}
+
+void Window::customRender()
+{
+ QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
+ QRhiResourceUpdateBatch *u = m_r->nextResourceUpdateBatch();
+ if (d.initialUpdates) {
+ u->merge(d.initialUpdates);
+ d.initialUpdates->release();
+ d.initialUpdates = nullptr;
+ }
+
+ QMatrix4x4 triMvp = d.triBaseMvp;
+ triMvp.rotate(d.triRot, 0, 1, 0);
+ d.triRot += 1;
+ u->updateDynamicBuffer(d.triUbuf, 0, 64, triMvp.constData());
+
+ if (d.winProj != m_proj) {
+ d.winProj = m_proj;
+ QMatrix4x4 mvp = m_proj;
+ mvp.scale(2.5f);
+ u->updateDynamicBuffer(d.ubuf, 0, 64, mvp.constData());
+ }
+
+ // offscreen (triangle, msaa)
+ cb->beginPass(d.rt, QColor::fromRgbF(0.5f, 0.2f, 0.0f, 1.0f), { 1.0f, 0 }, u);
+ cb->setGraphicsPipeline(d.triPs);
+ cb->setViewport({ 0, 0, float(d.rb->pixelSize().width()), float(d.rb->pixelSize().height()) });
+ cb->setShaderResources();
+ QRhiCommandBuffer::VertexInput vbufBinding(d.vbuf, sizeof(vertexData));
+ cb->setVertexInput(0, 1, &vbufBinding);
+ cb->draw(3);
+ cb->endPass();
+
+ // onscreen (quad)
+ const QSize outputSizeInPixels = m_sc->currentPixelSize();
+ cb->beginPass(m_sc->currentFrameRenderTarget(), QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f), { 1.0f, 0 });
+ cb->setGraphicsPipeline(d.ps);
+ cb->setViewport({ 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) });
+ cb->setShaderResources();
+ vbufBinding.second = 0;
+ cb->setVertexInput(0, 1, &vbufBinding, d.ibuf, 0, QRhiCommandBuffer::IndexUInt16);
+ cb->drawIndexed(6);
+ cb->endPass();
+}
diff --git a/tests/manual/rhi/msaarenderbuffer/msaarenderbuffer.pro b/tests/manual/rhi/msaarenderbuffer/msaarenderbuffer.pro
new file mode 100644
index 0000000000..4b31e39e7d
--- /dev/null
+++ b/tests/manual/rhi/msaarenderbuffer/msaarenderbuffer.pro
@@ -0,0 +1,8 @@
+TEMPLATE = app
+
+QT += gui-private
+
+SOURCES = \
+ msaarenderbuffer.cpp
+
+RESOURCES = msaarenderbuffer.qrc
diff --git a/tests/manual/rhi/msaarenderbuffer/msaarenderbuffer.qrc b/tests/manual/rhi/msaarenderbuffer/msaarenderbuffer.qrc
new file mode 100644
index 0000000000..c7a2380a42
--- /dev/null
+++ b/tests/manual/rhi/msaarenderbuffer/msaarenderbuffer.qrc
@@ -0,0 +1,8 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+ <file alias="color.vert.qsb">../shared/color.vert.qsb</file>
+ <file alias="color.frag.qsb">../shared/color.frag.qsb</file>
+ <file alias="texture.vert.qsb">../shared/texture.vert.qsb</file>
+ <file alias="texture.frag.qsb">../shared/texture.frag.qsb</file>
+</qresource>
+</RCC>
diff --git a/tests/manual/rhi/msaatexture/msaatexture.cpp b/tests/manual/rhi/msaatexture/msaatexture.cpp
new file mode 100644
index 0000000000..1b652cce92
--- /dev/null
+++ b/tests/manual/rhi/msaatexture/msaatexture.cpp
@@ -0,0 +1,329 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "../shared/examplefw.h"
+
+// Renders into a non-multisample and a multisample (4x) texture, and then uses
+// those textures to draw two quads. Note that this uses an MSAA sampler in the
+// shader, not resolves. Not supported on the GL(ES) backend atm.
+
+static float vertexData[] =
+{ // Y up, CCW
+ -0.5f, 0.5f, 0.0f, 0.0f,
+ -0.5f, -0.5f, 0.0f, 1.0f,
+ 0.5f, -0.5f, 1.0f, 1.0f,
+ 0.5f, 0.5f, 1.0f, 0.0f
+};
+
+static quint16 indexData[] =
+{
+ 0, 1, 2, 0, 2, 3
+};
+
+static float triangleData[] =
+{ // Y up, 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,
+};
+
+const int UBUFSZ = 68;
+
+struct {
+ QVector<QRhiResource *> releasePool;
+ QRhiBuffer *vbuf = nullptr;
+ QRhiBuffer *ibuf = nullptr;
+ QRhiBuffer *ubuf = nullptr;
+ QRhiTexture *tex = nullptr;
+ QRhiTexture *msaaTex = nullptr;
+ QRhiSampler *sampler = nullptr;
+ QRhiShaderResourceBindings *srbLeft = nullptr;
+ QRhiShaderResourceBindings *srbRight = nullptr;
+ QRhiGraphicsPipeline *psLeft = nullptr;
+ QRhiGraphicsPipeline *psRight = nullptr;
+ QRhiResourceUpdateBatch *initialUpdates = nullptr;
+ int rightOfs;
+ QMatrix4x4 winProj;
+ QMatrix4x4 triBaseMvp;
+ float triRot = 0;
+
+ QRhiShaderResourceBindings *triSrb = nullptr;
+ QRhiGraphicsPipeline *msaaTriPs = nullptr;
+ QRhiGraphicsPipeline *triPs = nullptr;
+ QRhiBuffer *triUbuf = nullptr;
+ QRhiTextureRenderTarget *msaaRt = nullptr;
+ QRhiRenderPassDescriptor *msaaRtRp = nullptr;
+ QRhiTextureRenderTarget *rt = nullptr;
+ QRhiRenderPassDescriptor *rtRp = nullptr;
+} d;
+
+//#define NO_MSAA
+
+void Window::customInit()
+{
+ if (!m_r->isFeatureSupported(QRhi::MultisampleTexture))
+ qFatal("Multisample textures not supported by this backend");
+
+ d.vbuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData) + sizeof(triangleData));
+ d.releasePool << d.vbuf;
+ d.vbuf->build();
+
+ d.ibuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(indexData));
+ d.releasePool << d.ibuf;
+ d.ibuf->build();
+
+ d.rightOfs = m_r->ubufAligned(UBUFSZ);
+ d.ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, d.rightOfs + UBUFSZ);
+ d.releasePool << d.ubuf;
+ d.ubuf->build();
+
+ d.tex = m_r->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 1, QRhiTexture::RenderTarget);
+ d.releasePool << d.tex;
+ d.tex->build();
+#ifndef NO_MSAA
+ d.msaaTex = m_r->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 4, QRhiTexture::RenderTarget);
+#else
+ d.msaaTex = m_r->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 1, QRhiTexture::RenderTarget);
+#endif
+ d.releasePool << d.msaaTex;
+ d.msaaTex->build();
+
+ d.initialUpdates = m_r->nextResourceUpdateBatch();
+ d.initialUpdates->uploadStaticBuffer(d.vbuf, 0, sizeof(vertexData), vertexData);
+ d.initialUpdates->uploadStaticBuffer(d.vbuf, sizeof(vertexData), sizeof(triangleData), triangleData);
+ d.initialUpdates->uploadStaticBuffer(d.ibuf, indexData);
+
+ d.sampler = m_r->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
+ QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
+ d.releasePool << d.sampler;
+ d.sampler->build();
+
+ d.srbLeft = m_r->newShaderResourceBindings();
+ d.releasePool << d.srbLeft;
+ d.srbLeft->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, d.ubuf, 0, UBUFSZ),
+ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, d.tex, d.sampler)
+ });
+ d.srbLeft->build();
+
+ d.srbRight = m_r->newShaderResourceBindings();
+ d.releasePool << d.srbRight;
+ d.srbRight->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, d.ubuf, d.rightOfs, UBUFSZ),
+ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, d.msaaTex, d.sampler)
+ });
+ d.srbRight->build();
+
+ d.psLeft = m_r->newGraphicsPipeline();
+ d.releasePool << d.psLeft;
+ d.psLeft->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, getShader(QLatin1String(":/texture.vert.qsb")) },
+ { QRhiGraphicsShaderStage::Fragment, getShader(QLatin1String(":/texture.frag.qsb")) }
+ });
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({ { 4 * sizeof(float) } });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) }
+ });
+ d.psLeft->setVertexInputLayout(inputLayout);
+ d.psLeft->setShaderResourceBindings(d.srbLeft);
+ d.psLeft->setRenderPassDescriptor(m_rp);
+ d.psLeft->build();
+
+ d.psRight = m_r->newGraphicsPipeline();
+ d.releasePool << d.psRight;
+ d.psRight->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, getShader(QLatin1String(":/texture.vert.qsb")) },
+#ifndef NO_MSAA
+ { QRhiGraphicsShaderStage::Fragment, getShader(QLatin1String(":/texture_ms4.frag.qsb")) }
+#else
+ { QRhiGraphicsShaderStage::Fragment, getShader(QLatin1String(":/texture.frag.qsb")) }
+#endif
+ });
+ d.psRight->setVertexInputLayout(d.psLeft->vertexInputLayout());
+ d.psRight->setShaderResourceBindings(d.srbRight);
+ d.psRight->setRenderPassDescriptor(m_rp);
+ d.psRight->build();
+
+ // set up the offscreen triangle that goes into tex and msaaTex
+ d.triUbuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68);
+ d.releasePool << d.triUbuf;
+ d.triUbuf->build();
+ d.triSrb = m_r->newShaderResourceBindings();
+ d.releasePool << d.triSrb;
+ d.triSrb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, d.triUbuf)
+ });
+ d.triSrb->build();
+ d.rt = m_r->newTextureRenderTarget({ d.tex });
+ d.releasePool << d.rt;
+ d.rtRp = d.rt->newCompatibleRenderPassDescriptor();
+ d.releasePool << d.rtRp;
+ d.rt->setRenderPassDescriptor(d.rtRp);
+ d.rt->build();
+ d.msaaRt = m_r->newTextureRenderTarget({ d.msaaTex });
+ d.releasePool << d.msaaRt;
+ d.msaaRtRp = d.msaaRt->newCompatibleRenderPassDescriptor();
+ d.releasePool << d.msaaRtRp;
+ d.msaaRt->setRenderPassDescriptor(d.msaaRtRp);
+ d.msaaRt->build();
+ d.triPs = m_r->newGraphicsPipeline();
+ d.releasePool << d.triPs;
+ d.triPs->setSampleCount(1);
+ d.triPs->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, getShader(QLatin1String(":/color.vert.qsb")) },
+ { QRhiGraphicsShaderStage::Fragment, getShader(QLatin1String(":/color.frag.qsb")) }
+ });
+ inputLayout.setBindings({
+ { 5 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) }
+ });
+ d.triPs->setVertexInputLayout(inputLayout);
+ d.triPs->setShaderResourceBindings(d.triSrb);
+ d.triPs->setRenderPassDescriptor(d.rtRp);
+ d.triPs->build();
+ d.msaaTriPs = m_r->newGraphicsPipeline();
+ d.releasePool << d.msaaTriPs;
+#ifndef NO_MSAA
+ d.msaaTriPs->setSampleCount(4);
+#else
+ d.msaaTriPs->setSampleCount(1);
+#endif
+ d.msaaTriPs->setShaderStages(d.triPs->shaderStages());
+ d.msaaTriPs->setVertexInputLayout(d.triPs->vertexInputLayout());
+ d.msaaTriPs->setShaderResourceBindings(d.triSrb);
+ d.msaaTriPs->setRenderPassDescriptor(d.msaaRtRp);
+ d.msaaTriPs->build();
+}
+
+void Window::customRelease()
+{
+ qDeleteAll(d.releasePool);
+ d.releasePool.clear();
+}
+
+void Window::customRender()
+{
+ QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
+ QRhiResourceUpdateBatch *u = m_r->nextResourceUpdateBatch();
+ if (d.initialUpdates) {
+ u->merge(d.initialUpdates);
+ d.initialUpdates->release();
+ d.initialUpdates = nullptr;
+
+ // onscreen ubuf
+ qint32 flip = m_r->isYUpInFramebuffer() ? 1 : 0;
+ u->updateDynamicBuffer(d.ubuf, 64, 4, &flip);
+ u->updateDynamicBuffer(d.ubuf, d.rightOfs + 64, 4, &flip);
+
+ // offscreen ubuf
+ d.triBaseMvp = m_r->clipSpaceCorrMatrix();
+ d.triBaseMvp.perspective(45.0f, d.msaaTex->pixelSize().width() / float(d.msaaTex->pixelSize().height()), 0.01f, 1000.0f);
+ d.triBaseMvp.translate(0, 0, -2);
+ float opacity = 1.0f;
+ u->updateDynamicBuffer(d.triUbuf, 64, 4, &opacity);
+ }
+
+ if (d.winProj != m_proj) {
+ // onscreen buf, window size dependent
+ d.winProj = m_proj;
+ QMatrix4x4 mvp = m_proj;
+ mvp.scale(2);
+ mvp.translate(-0.8f, 0, 0);
+ u->updateDynamicBuffer(d.ubuf, 0, 64, mvp.constData());
+ mvp.translate(1.6f, 0, 0);
+ u->updateDynamicBuffer(d.ubuf, d.rightOfs, 64, mvp.constData());
+ }
+
+ // offscreen buf, apply the rotation on every frame
+ QMatrix4x4 triMvp = d.triBaseMvp;
+ triMvp.rotate(d.triRot, 0, 1, 0);
+ d.triRot += 1;
+ u->updateDynamicBuffer(d.triUbuf, 0, 64, triMvp.constData());
+
+ cb->resourceUpdate(u); // could have passed u to beginPass but exercise this one too
+
+ // offscreen
+ cb->beginPass(d.rt, QColor::fromRgbF(0.5f, 0.2f, 0.0f, 1.0f), { 1.0f, 0 });
+ cb->setGraphicsPipeline(d.triPs);
+ cb->setViewport({ 0, 0, float(d.msaaTex->pixelSize().width()), float(d.msaaTex->pixelSize().height()) });
+ cb->setShaderResources();
+ QRhiCommandBuffer::VertexInput vbufBinding(d.vbuf, sizeof(vertexData));
+ cb->setVertexInput(0, 1, &vbufBinding);
+ cb->draw(3);
+ cb->endPass();
+
+ // offscreen msaa
+ cb->beginPass(d.msaaRt, QColor::fromRgbF(0.5f, 0.2f, 0.0f, 1.0f), { 1.0f, 0 });
+ cb->setGraphicsPipeline(d.msaaTriPs);
+ cb->setViewport({ 0, 0, float(d.msaaTex->pixelSize().width()), float(d.msaaTex->pixelSize().height()) });
+ cb->setShaderResources();
+ cb->setVertexInput(0, 1, &vbufBinding);
+ cb->draw(3);
+ cb->endPass();
+
+ // onscreen
+ const QSize outputSizeInPixels = m_sc->currentPixelSize();
+ cb->beginPass(m_sc->currentFrameRenderTarget(), QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f), { 1.0f, 0 });
+ cb->setGraphicsPipeline(d.psLeft); // showing the non-msaa version
+ cb->setViewport({ 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) });
+ cb->setShaderResources();
+ vbufBinding.second = 0;
+ cb->setVertexInput(0, 1, &vbufBinding, d.ibuf, 0, QRhiCommandBuffer::IndexUInt16);
+ cb->drawIndexed(6);
+ cb->setGraphicsPipeline(d.psRight); // showing the msaa version, resolved in the shader
+ cb->setShaderResources();
+ cb->drawIndexed(6);
+ cb->endPass();
+}
diff --git a/tests/manual/rhi/msaatexture/msaatexture.pro b/tests/manual/rhi/msaatexture/msaatexture.pro
new file mode 100644
index 0000000000..0467e4dc9d
--- /dev/null
+++ b/tests/manual/rhi/msaatexture/msaatexture.pro
@@ -0,0 +1,8 @@
+TEMPLATE = app
+
+QT += gui-private
+
+SOURCES = \
+ msaatexture.cpp
+
+RESOURCES = msaatexture.qrc
diff --git a/tests/manual/rhi/msaatexture/msaatexture.qrc b/tests/manual/rhi/msaatexture/msaatexture.qrc
new file mode 100644
index 0000000000..ecd968481c
--- /dev/null
+++ b/tests/manual/rhi/msaatexture/msaatexture.qrc
@@ -0,0 +1,9 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+ <file alias="color.vert.qsb">../shared/color.vert.qsb</file>
+ <file alias="color.frag.qsb">../shared/color.frag.qsb</file>
+ <file alias="texture.vert.qsb">../shared/texture.vert.qsb</file>
+ <file alias="texture.frag.qsb">../shared/texture.frag.qsb</file>
+ <file alias="texture_ms4.frag.qsb">../shared/texture_ms4.frag.qsb</file>
+</qresource>
+</RCC>
diff --git a/tests/manual/rhi/multiwindow/multiwindow.cpp b/tests/manual/rhi/multiwindow/multiwindow.cpp
new file mode 100644
index 0000000000..a5eed78b1d
--- /dev/null
+++ b/tests/manual/rhi/multiwindow/multiwindow.cpp
@@ -0,0 +1,644 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QApplication>
+#include <QWidget>
+#include <QLabel>
+#include <QPlainTextEdit>
+#include <QPushButton>
+#include <QCheckBox>
+#include <QVBoxLayout>
+
+#include <QCommandLineParser>
+#include <QWindow>
+#include <QPlatformSurfaceEvent>
+#include <QElapsedTimer>
+
+#include <QtGui/private/qshader_p.h>
+#include <QFile>
+
+#ifndef QT_NO_OPENGL
+#include <QtGui/private/qrhigles2_p.h>
+#include <QOffscreenSurface>
+#endif
+
+#if QT_CONFIG(vulkan)
+#include <QLoggingCategory>
+#include <QtGui/private/qrhivulkan_p.h>
+#endif
+
+#ifdef Q_OS_WIN
+#include <QtGui/private/qrhid3d11_p.h>
+#endif
+
+#ifdef Q_OS_DARWIN
+#include <QtGui/private/qrhimetal_p.h>
+#endif
+
+enum GraphicsApi
+{
+ OpenGL,
+ Vulkan,
+ D3D11,
+ Metal
+};
+
+static GraphicsApi graphicsApi;
+
+static QString graphicsApiName()
+{
+ switch (graphicsApi) {
+ case OpenGL:
+ return QLatin1String("OpenGL 2.x");
+ case Vulkan:
+ return QLatin1String("Vulkan");
+ case D3D11:
+ return QLatin1String("Direct3D 11");
+ case Metal:
+ return QLatin1String("Metal");
+ default:
+ break;
+ }
+ return QString();
+}
+
+static struct {
+#if QT_CONFIG(vulkan)
+ QVulkanInstance *instance = nullptr;
+#endif
+ QRhi *r = nullptr;
+#ifndef QT_NO_OPENGL
+ QOffscreenSurface *fallbackSurface = nullptr;
+#endif
+} r;
+
+void createRhi()
+{
+#ifndef QT_NO_OPENGL
+ if (graphicsApi == OpenGL) {
+ r.fallbackSurface = QRhiGles2InitParams::newFallbackSurface();
+ QRhiGles2InitParams params;
+ params.fallbackSurface = r.fallbackSurface;
+ //params.window = this;
+ r.r = QRhi::create(QRhi::OpenGLES2, &params);
+ }
+#endif
+
+#if QT_CONFIG(vulkan)
+ if (graphicsApi == Vulkan) {
+ QRhiVulkanInitParams params;
+ params.inst = r.instance;
+ //params.window = this;
+ r.r = QRhi::create(QRhi::Vulkan, &params);
+ }
+#endif
+
+#ifdef Q_OS_WIN
+ if (graphicsApi == D3D11) {
+ QRhiD3D11InitParams params;
+ params.enableDebugLayer = true;
+ r.r = QRhi::create(QRhi::D3D11, &params);
+ }
+#endif
+
+#ifdef Q_OS_DARWIN
+ if (graphicsApi == Metal) {
+ QRhiMetalInitParams params;
+ r.r = QRhi::create(QRhi::Metal, &params);
+ }
+#endif
+
+ if (!r.r)
+ qFatal("Failed to create RHI backend");
+}
+
+void destroyRhi()
+{
+ delete r.r;
+
+#ifndef QT_NO_OPENGL
+ delete r.fallbackSurface;
+#endif
+}
+
+struct {
+ QVector<QWindow *> windows;
+
+ QRhiBuffer *vbuf = nullptr;
+ QRhiBuffer *ubuf = nullptr;
+ QRhiShaderResourceBindings *srb = nullptr;
+ QRhiGraphicsPipeline *ps = nullptr;
+ QRhiResourceUpdateBatch *initialUpdates = nullptr;
+} d;
+
+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,
+};
+
+static QShader getShader(const QString &name)
+{
+ QFile f(name);
+ if (f.open(QIODevice::ReadOnly))
+ return QShader::fromSerialized(f.readAll());
+
+ return QShader();
+}
+
+// can use just one rpd from whichever window comes first since they are
+// actually compatible due to all windows using the same config (have
+// depth-stencil, sample count 1, same format). this means the same pso can be
+// reused too.
+void ensureSharedResources(QRhiRenderPassDescriptor *rp)
+{
+ if (!d.vbuf) {
+ d.vbuf = r.r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData));
+ d.vbuf->build();
+ d.initialUpdates = r.r->nextResourceUpdateBatch();
+ d.initialUpdates->uploadStaticBuffer(d.vbuf, vertexData);
+ }
+
+ if (!d.ubuf) {
+ d.ubuf = r.r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68);
+ d.ubuf->build();
+ }
+
+ if (!d.srb) {
+ d.srb = r.r->newShaderResourceBindings();
+ d.srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, d.ubuf)
+ });
+ d.srb->build();
+ }
+
+ if (!d.ps) {
+ d.ps = r.r->newGraphicsPipeline();
+
+ QRhiGraphicsPipeline::TargetBlend premulAlphaBlend;
+ premulAlphaBlend.enable = true;
+ d.ps->setTargetBlends({ premulAlphaBlend });
+
+ const QShader vs = getShader(QLatin1String(":/color.vert.qsb"));
+ if (!vs.isValid())
+ qFatal("Failed to load shader pack (vertex)");
+ const QShader fs = getShader(QLatin1String(":/color.frag.qsb"));
+ if (!fs.isValid())
+ qFatal("Failed to load shader pack (fragment)");
+
+ d.ps->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, vs },
+ { QRhiGraphicsShaderStage::Fragment, fs }
+ });
+
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 5 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) }
+ });
+
+ d.ps->setVertexInputLayout(inputLayout);
+ d.ps->setShaderResourceBindings(d.srb);
+ d.ps->setRenderPassDescriptor(rp);
+
+ d.ps->build();
+ }
+}
+
+void destroySharedResources()
+{
+ delete d.ps;
+ d.ps = nullptr;
+
+ delete d.srb;
+ d.srb = nullptr;
+
+ delete d.vbuf;
+ d.vbuf = nullptr;
+
+ delete d.ubuf;
+ d.ubuf = nullptr;
+}
+
+class Window : public QWindow
+{
+public:
+ Window(const QString &title, const QColor &bgColor, int axis, bool noVSync);
+ ~Window();
+
+protected:
+ void init();
+ void releaseResources();
+ void resizeSwapChain();
+ void releaseSwapChain();
+ void render();
+
+ void exposeEvent(QExposeEvent *) override;
+ bool event(QEvent *) override;
+
+ QColor m_bgColor;
+ int m_rotationAxis;
+ bool m_noVSync;
+
+ bool m_running = false;
+ bool m_notExposed = false;
+ bool m_newlyExposed = false;
+
+ QMatrix4x4 m_proj;
+ QVector<QRhiResource *> m_releasePool;
+
+ bool m_hasSwapChain = false;
+ QRhiSwapChain *m_sc = nullptr;
+ QRhiRenderBuffer *m_ds = nullptr;
+ QRhiRenderPassDescriptor *m_rp = nullptr;
+
+ float m_rotation = 0;
+ float m_opacity = 1;
+ int m_opacityDir = -1;
+};
+
+Window::Window(const QString &title, const QColor &bgColor, int axis, bool noVSync)
+ : m_bgColor(bgColor),
+ m_rotationAxis(axis),
+ m_noVSync(noVSync)
+{
+ switch (graphicsApi) {
+ case OpenGL:
+ {
+#if QT_CONFIG(opengl)
+ setSurfaceType(OpenGLSurface);
+ QSurfaceFormat fmt = QRhiGles2InitParams::adjustedFormat(); // default + depth/stencil
+ fmt.setSwapInterval(noVSync ? 0 : 1); // + swap interval
+ setFormat(fmt);
+#endif
+ }
+ break;
+ case Vulkan:
+#if QT_CONFIG(vulkan)
+ setSurfaceType(VulkanSurface);
+ setVulkanInstance(r.instance);
+#endif
+ break;
+ case D3D11:
+ setSurfaceType(OpenGLSurface); // not a typo
+ break;
+ case Metal:
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
+ setSurfaceType(MetalSurface);
+#endif
+ break;
+ default:
+ break;
+ }
+
+ resize(800, 600);
+ setTitle(title);
+}
+
+Window::~Window()
+{
+ releaseResources();
+}
+
+void Window::exposeEvent(QExposeEvent *)
+{
+ // initialize and start rendering when the window becomes usable for graphics purposes
+ if (isExposed() && !m_running) {
+ m_running = true;
+ init();
+ resizeSwapChain();
+ }
+
+ // stop pushing frames when not exposed (or size is 0)
+ if ((!isExposed() || (m_hasSwapChain && m_sc->surfacePixelSize().isEmpty())) && m_running)
+ m_notExposed = true;
+
+ // continue when exposed again and the surface has a valid size.
+ // note that the surface size can be (0, 0) even though size() reports a valid one...
+ if (isExposed() && m_running && m_notExposed && !m_sc->surfacePixelSize().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() && !m_sc->surfacePixelSize().isEmpty())
+ render();
+}
+
+bool Window::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);
+}
+
+void Window::init()
+{
+ m_sc = r.r->newSwapChain();
+ m_ds = r.r->newRenderBuffer(QRhiRenderBuffer::DepthStencil,
+ QSize(), // no need to set the size yet
+ 1,
+ QRhiRenderBuffer::UsedWithSwapChainOnly);
+ m_releasePool << m_ds;
+ m_sc->setWindow(this);
+ m_sc->setDepthStencil(m_ds);
+ if (m_noVSync)
+ m_sc->setFlags(QRhiSwapChain::NoVSync);
+
+ m_rp = m_sc->newCompatibleRenderPassDescriptor();
+ m_releasePool << m_rp;
+ m_sc->setRenderPassDescriptor(m_rp);
+
+ ensureSharedResources(m_rp);
+}
+
+void Window::releaseResources()
+{
+ qDeleteAll(m_releasePool);
+ m_releasePool.clear();
+
+ delete m_sc;
+ m_sc = nullptr;
+}
+
+void Window::resizeSwapChain()
+{
+ const QSize outputSize = m_sc->surfacePixelSize();
+
+ m_ds->setPixelSize(outputSize);
+ m_ds->build();
+
+ m_hasSwapChain = m_sc->buildOrResize();
+
+ m_proj = r.r->clipSpaceCorrMatrix();
+ m_proj.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 1000.0f);
+ m_proj.translate(0, 0, -4);
+}
+
+void Window::releaseSwapChain()
+{
+ if (m_hasSwapChain) {
+ m_hasSwapChain = false;
+ m_sc->release();
+ }
+}
+
+void Window::render()
+{
+ if (!m_hasSwapChain || m_notExposed)
+ return;
+
+ // If the window got resized or got newly exposed, resize the swapchain.
+ // (the newly-exposed case is not actually required by some
+ // platforms/backends, but f.ex. Vulkan on Windows seems to need it)
+ if (m_sc->currentPixelSize() != m_sc->surfacePixelSize() || m_newlyExposed) {
+ resizeSwapChain();
+ if (!m_hasSwapChain)
+ return;
+ m_newlyExposed = false;
+ }
+
+ QRhi::FrameOpResult result = r.r->beginFrame(m_sc);
+ if (result == QRhi::FrameOpSwapChainOutOfDate) {
+ resizeSwapChain();
+ if (!m_hasSwapChain)
+ return;
+ result = r.r->beginFrame(m_sc);
+ }
+ if (result != QRhi::FrameOpSuccess) {
+ requestUpdate();
+ return;
+ }
+
+ QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
+ const QSize outputSizeInPixels = m_sc->currentPixelSize();
+
+ QRhiResourceUpdateBatch *u = r.r->nextResourceUpdateBatch();
+ if (d.initialUpdates) {
+ u->merge(d.initialUpdates);
+ d.initialUpdates->release();
+ d.initialUpdates = nullptr;
+ }
+
+ m_rotation += 1.0f;
+ QMatrix4x4 mvp = m_proj;
+ mvp.rotate(m_rotation, m_rotationAxis == 0 ? 1 : 0, m_rotationAxis == 1 ? 1 : 0, m_rotationAxis == 2 ? 1 : 0);
+ u->updateDynamicBuffer(d.ubuf, 0, 64, mvp.constData());
+ 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);
+ }
+ u->updateDynamicBuffer(d.ubuf, 64, 4, &m_opacity);
+
+ cb->beginPass(m_sc->currentFrameRenderTarget(),
+ QColor::fromRgbF(float(m_bgColor.redF()), float(m_bgColor.greenF()), float(m_bgColor.blueF()), 1.0f),
+ { 1.0f, 0 },
+ u);
+
+ cb->setGraphicsPipeline(d.ps);
+ cb->setViewport({ 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) });
+ cb->setShaderResources();
+ const QRhiCommandBuffer::VertexInput vbufBinding(d.vbuf, 0);
+ cb->setVertexInput(0, 1, &vbufBinding);
+ cb->draw(3);
+
+ cb->endPass();
+
+ r.r->endFrame(m_sc);
+
+ requestUpdate();
+}
+
+void createWindow(bool noVSync)
+{
+ static QColor colors[] = { Qt::red, Qt::green, Qt::blue, Qt::yellow, Qt::cyan, Qt::gray };
+ const int n = d.windows.count();
+ d.windows.append(new Window(QString::asprintf("Window #%d%s", n, noVSync ? " (no vsync)" : ""), colors[n % 6], n % 3, noVSync));
+ d.windows.last()->show();
+}
+
+void closeWindow()
+{
+ delete d.windows.takeLast();
+}
+
+int main(int argc, char **argv)
+{
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+ QApplication app(argc, argv);
+
+#if defined(Q_OS_WIN)
+ graphicsApi = D3D11;
+#elif defined(Q_OS_DARWIN)
+ graphicsApi = Metal;
+#elif QT_CONFIG(vulkan)
+ graphicsApi = Vulkan;
+#else
+ graphicsApi = OpenGL;
+#endif
+
+ QCommandLineParser cmdLineParser;
+ cmdLineParser.addHelpOption();
+ QCommandLineOption glOption({ "g", "opengl" }, QLatin1String("OpenGL (2.x)"));
+ cmdLineParser.addOption(glOption);
+ QCommandLineOption vkOption({ "v", "vulkan" }, QLatin1String("Vulkan"));
+ cmdLineParser.addOption(vkOption);
+ QCommandLineOption d3dOption({ "d", "d3d11" }, QLatin1String("Direct3D 11"));
+ cmdLineParser.addOption(d3dOption);
+ QCommandLineOption mtlOption({ "m", "metal" }, QLatin1String("Metal"));
+ cmdLineParser.addOption(mtlOption);
+ cmdLineParser.process(app);
+ if (cmdLineParser.isSet(glOption))
+ graphicsApi = OpenGL;
+ if (cmdLineParser.isSet(vkOption))
+ graphicsApi = Vulkan;
+ if (cmdLineParser.isSet(d3dOption))
+ graphicsApi = D3D11;
+ if (cmdLineParser.isSet(mtlOption))
+ graphicsApi = Metal;
+
+ qDebug("Selected graphics API is %s", qPrintable(graphicsApiName()));
+ qDebug("This is a multi-api example, use command line arguments to override:\n%s", qPrintable(cmdLineParser.helpText()));
+
+#if QT_CONFIG(vulkan)
+ r.instance = new QVulkanInstance;
+ if (graphicsApi == Vulkan) {
+#ifndef Q_OS_ANDROID
+ r.instance->setLayers({ "VK_LAYER_LUNARG_standard_validation" });
+#else
+ r.instance->setLayers(QByteArrayList()
+ << "VK_LAYER_GOOGLE_threading"
+ << "VK_LAYER_LUNARG_parameter_validation"
+ << "VK_LAYER_LUNARG_object_tracker"
+ << "VK_LAYER_LUNARG_core_validation"
+ << "VK_LAYER_LUNARG_image"
+ << "VK_LAYER_LUNARG_swapchain"
+ << "VK_LAYER_GOOGLE_unique_objects");
+#endif
+ if (!r.instance->create()) {
+ qWarning("Failed to create Vulkan instance, switching to OpenGL");
+ graphicsApi = OpenGL;
+ }
+ }
+#endif
+
+ createRhi();
+
+ int winCount = 0;
+ QWidget w;
+ w.resize(800, 600);
+ w.setWindowTitle(QCoreApplication::applicationName() + QLatin1String(" - ") + graphicsApiName());
+ QVBoxLayout *layout = new QVBoxLayout(&w);
+
+ QPlainTextEdit *info = new QPlainTextEdit(
+ QLatin1String("This application tests rendering with the same QRhi instance (and so the same Vulkan/Metal/D3D device or OpenGL context) "
+ "to multiple windows via multiple QRhiSwapChain objects, from the same one thread. Some resources are shared across all windows.\n"
+ "\nNote that the behavior may differ depending on the underlying graphics API implementation and the number of windows. "
+ "One challenge here is the vsync throttling: with the default vsync/fifo presentation mode the behavior may differ between "
+ "platforms, drivers, and APIs as we present different swapchains' images in a row on the same thread. As a potential solution, "
+ "setting NoVSync on the second, third, and later window swapchains is offered as an option.\n"
+ "\n\nUsing API: ") + graphicsApiName());
+ info->setReadOnly(true);
+ layout->addWidget(info);
+ QLabel *label = new QLabel(QLatin1String("Window count: 0"));
+ layout->addWidget(label);
+ QCheckBox *vsCb = new QCheckBox(QLatin1String("Set NoVSync on all swapchains except the first"));
+ vsCb->setChecked(false);
+ layout->addWidget(vsCb);
+ QPushButton *btn = new QPushButton(QLatin1String("New window"));
+ QObject::connect(btn, &QPushButton::clicked, btn, [label, vsCb, &winCount] {
+ winCount += 1;
+ label->setText(QString::asprintf("Window count: %d", winCount));
+ const bool noVSync = vsCb->isChecked() && winCount > 1;
+ createWindow(noVSync);
+ });
+ layout->addWidget(btn);
+ btn = new QPushButton(QLatin1String("Close window"));
+ QObject::connect(btn, &QPushButton::clicked, btn, [label, &winCount] {
+ if (winCount > 0) {
+ winCount -= 1;
+ label->setText(QString::asprintf("Window count: %d", winCount));
+ closeWindow();
+ }
+ });
+ layout->addWidget(btn);
+ w.show();
+
+ int result = app.exec();
+
+ qDeleteAll(d.windows);
+
+ destroySharedResources();
+ destroyRhi();
+
+#if QT_CONFIG(vulkan)
+ delete r.instance;
+#endif
+
+ return result;
+}
diff --git a/tests/manual/rhi/multiwindow/multiwindow.pro b/tests/manual/rhi/multiwindow/multiwindow.pro
new file mode 100644
index 0000000000..b9c3867bf8
--- /dev/null
+++ b/tests/manual/rhi/multiwindow/multiwindow.pro
@@ -0,0 +1,8 @@
+TEMPLATE = app
+
+QT += gui-private widgets
+
+SOURCES = \
+ multiwindow.cpp
+
+RESOURCES = multiwindow.qrc
diff --git a/tests/manual/rhi/multiwindow/multiwindow.qrc b/tests/manual/rhi/multiwindow/multiwindow.qrc
new file mode 100644
index 0000000000..8cec4fa9ec
--- /dev/null
+++ b/tests/manual/rhi/multiwindow/multiwindow.qrc
@@ -0,0 +1,6 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+ <file alias="color.vert.qsb">../shared/color.vert.qsb</file>
+ <file alias="color.frag.qsb">../shared/color.frag.qsb</file>
+</qresource>
+</RCC>
diff --git a/tests/manual/rhi/multiwindow_threaded/multiwindow_threaded.cpp b/tests/manual/rhi/multiwindow_threaded/multiwindow_threaded.cpp
new file mode 100644
index 0000000000..45818e91e9
--- /dev/null
+++ b/tests/manual/rhi/multiwindow_threaded/multiwindow_threaded.cpp
@@ -0,0 +1,829 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QApplication>
+#include <QWidget>
+#include <QLabel>
+#include <QPlainTextEdit>
+#include <QPushButton>
+#include <QCheckBox>
+#include <QVBoxLayout>
+
+#include <QThread>
+#include <QMutex>
+#include <QWaitCondition>
+#include <QQueue>
+#include <QEvent>
+#include <QCommandLineParser>
+#include <QElapsedTimer>
+
+#include <QtGui/private/qshader_p.h>
+#include <QFile>
+#include <QtGui/private/qrhiprofiler_p.h>
+
+#ifndef QT_NO_OPENGL
+#include <QtGui/private/qrhigles2_p.h>
+#include <QOffscreenSurface>
+#endif
+
+#if QT_CONFIG(vulkan)
+#include <QLoggingCategory>
+#include <QtGui/private/qrhivulkan_p.h>
+#endif
+
+#ifdef Q_OS_WIN
+#include <QtGui/private/qrhid3d11_p.h>
+#endif
+
+#ifdef Q_OS_DARWIN
+#include <QtGui/private/qrhimetal_p.h>
+#endif
+
+#include "window.h"
+
+#include "../shared/cube.h"
+
+static QShader getShader(const QString &name)
+{
+ QFile f(name);
+ if (f.open(QIODevice::ReadOnly))
+ return QShader::fromSerialized(f.readAll());
+
+ return QShader();
+}
+
+static GraphicsApi graphicsApi;
+
+static QString graphicsApiName()
+{
+ switch (graphicsApi) {
+ case OpenGL:
+ return QLatin1String("OpenGL 2.x");
+ case Vulkan:
+ return QLatin1String("Vulkan");
+ case D3D11:
+ return QLatin1String("Direct3D 11");
+ case Metal:
+ return QLatin1String("Metal");
+ default:
+ break;
+ }
+ return QString();
+}
+
+#if QT_CONFIG(vulkan)
+QVulkanInstance *instance = nullptr;
+#endif
+
+// Window (main thread) emit signals -> Renderer::send* (main thread) -> event queue (add on main, process on render thread) -> Renderer::renderEvent (render thread)
+
+// event queue is taken from the Qt Quick scenegraph as-is
+// all this below is conceptually the same as the QSG threaded render loop
+class RenderThreadEventQueue : public QQueue<QEvent *>
+{
+public:
+ RenderThreadEventQueue()
+ : waiting(false)
+ {
+ }
+
+ void addEvent(QEvent *e) {
+ mutex.lock();
+ enqueue(e);
+ if (waiting)
+ condition.wakeOne();
+ mutex.unlock();
+ }
+
+ QEvent *takeEvent(bool wait) {
+ mutex.lock();
+ if (isEmpty() && wait) {
+ waiting = true;
+ condition.wait(&mutex);
+ waiting = false;
+ }
+ QEvent *e = dequeue();
+ mutex.unlock();
+ return e;
+ }
+
+ bool hasMoreEvents() {
+ mutex.lock();
+ bool has = !isEmpty();
+ mutex.unlock();
+ return has;
+ }
+
+private:
+ QMutex mutex;
+ QWaitCondition condition;
+ bool waiting;
+};
+
+struct Renderer;
+
+struct Thread : public QThread
+{
+ Thread(Renderer *renderer_)
+ : renderer(renderer_)
+ {
+ active = true;
+ start();
+ }
+ void run() override;
+
+ Renderer *renderer;
+ bool active;
+ RenderThreadEventQueue eventQueue;
+ bool sleeping = false;
+ bool stopEventProcessing = false;
+ bool pendingRender = false;
+ bool pendingRenderIsNewExpose = false;
+ // mutex and cond used to allow the main thread waiting until something completes on the render thread
+ QMutex mutex;
+ QWaitCondition cond;
+};
+
+class RenderThreadEvent : public QEvent
+{
+public:
+ RenderThreadEvent(QEvent::Type type) : QEvent(type) { }
+};
+
+class InitEvent : public RenderThreadEvent
+{
+public:
+ static const QEvent::Type TYPE = QEvent::Type(QEvent::User + 1);
+ InitEvent() : RenderThreadEvent(TYPE)
+ { }
+};
+
+class RequestRenderEvent : public RenderThreadEvent
+{
+public:
+ static const QEvent::Type TYPE = QEvent::Type(QEvent::User + 2);
+ RequestRenderEvent(bool newlyExposed_) : RenderThreadEvent(TYPE), newlyExposed(newlyExposed_)
+ { }
+ bool newlyExposed;
+};
+
+class SurfaceCleanupEvent : public RenderThreadEvent
+{
+public:
+ static const QEvent::Type TYPE = QEvent::Type(QEvent::User + 3);
+ SurfaceCleanupEvent() : RenderThreadEvent(TYPE)
+ { }
+};
+
+class CloseEvent : public RenderThreadEvent
+{
+public:
+ static const QEvent::Type TYPE = QEvent::Type(QEvent::User + 4);
+ CloseEvent() : RenderThreadEvent(TYPE)
+ { }
+};
+
+class SyncSurfaceSizeEvent : public RenderThreadEvent
+{
+public:
+ static const QEvent::Type TYPE = QEvent::Type(QEvent::User + 5);
+ SyncSurfaceSizeEvent() : RenderThreadEvent(TYPE)
+ { }
+};
+
+struct Renderer
+{
+ // ctor and dtor and send* are called main thread, rest on the render thread
+
+ Renderer(QWindow *w, const QColor &bgColor, int rotationAxis);
+ ~Renderer();
+
+ void sendInit();
+ void sendRender(bool newlyExposed);
+ void sendSurfaceGoingAway();
+ void sendSyncSurfaceSize();
+
+ QWindow *window;
+ Thread *thread;
+ QRhi *r = nullptr;
+#ifndef QT_NO_OPENGL
+ QOffscreenSurface *fallbackSurface = nullptr;
+#endif
+
+ void createRhi();
+ void destroyRhi();
+ void renderEvent(QEvent *e);
+ void init();
+ void releaseSwapChain();
+ void releaseResources();
+ void render(bool newlyExposed, bool wakeBeforePresent);
+
+ QColor m_bgColor;
+ int m_rotationAxis;
+
+ QVector<QRhiResource *> m_releasePool;
+ bool m_hasSwapChain = false;
+ QRhiSwapChain *m_sc = nullptr;
+ QRhiRenderBuffer *m_ds = nullptr;
+ QRhiRenderPassDescriptor *m_rp = nullptr;
+
+ QRhiBuffer *m_vbuf = nullptr;
+ QRhiBuffer *m_ubuf = nullptr;
+ QRhiTexture *m_tex = nullptr;
+ QRhiSampler *m_sampler = nullptr;
+ QRhiShaderResourceBindings *m_srb = nullptr;
+ QRhiGraphicsPipeline *m_ps = nullptr;
+ QRhiResourceUpdateBatch *m_initialUpdates = nullptr;
+
+ QMatrix4x4 m_proj;
+ float m_rotation = 0;
+ int m_frameCount = 0;
+};
+
+void Thread::run()
+{
+ while (active) {
+ if (pendingRender) {
+ pendingRender = false;
+ renderer->render(pendingRenderIsNewExpose, false);
+ }
+
+ while (eventQueue.hasMoreEvents()) {
+ QEvent *e = eventQueue.takeEvent(false);
+ renderer->renderEvent(e);
+ delete e;
+ }
+
+ if (active && !pendingRender) {
+ sleeping = true;
+ stopEventProcessing = false;
+ while (!stopEventProcessing) {
+ QEvent *e = eventQueue.takeEvent(true);
+ renderer->renderEvent(e);
+ delete e;
+ }
+ sleeping = false;
+ }
+ }
+}
+
+Renderer::Renderer(QWindow *w, const QColor &bgColor, int rotationAxis)
+ : window(w),
+ m_bgColor(bgColor),
+ m_rotationAxis(rotationAxis)
+{ // main thread
+ thread = new Thread(this);
+
+#ifndef QT_NO_OPENGL
+ if (graphicsApi == OpenGL)
+ fallbackSurface = QRhiGles2InitParams::newFallbackSurface();
+#endif
+}
+
+Renderer::~Renderer()
+{ // main thread
+ thread->eventQueue.addEvent(new CloseEvent);
+ thread->wait();
+ delete thread;
+
+#ifndef QT_NO_OPENGL
+ delete fallbackSurface;
+#endif
+}
+
+void Renderer::createRhi()
+{
+ if (r)
+ return;
+
+ qDebug() << "renderer" << this << "creating rhi";
+ QRhi::Flags rhiFlags = QRhi::EnableProfiling;
+
+#ifndef QT_NO_OPENGL
+ if (graphicsApi == OpenGL) {
+ QRhiGles2InitParams params;
+ params.fallbackSurface = fallbackSurface;
+ params.window = window;
+ r = QRhi::create(QRhi::OpenGLES2, &params, rhiFlags);
+ }
+#endif
+
+#if QT_CONFIG(vulkan)
+ if (graphicsApi == Vulkan) {
+ QRhiVulkanInitParams params;
+ params.inst = instance;
+ params.window = window;
+ r = QRhi::create(QRhi::Vulkan, &params, rhiFlags);
+ }
+#endif
+
+#ifdef Q_OS_WIN
+ if (graphicsApi == D3D11) {
+ QRhiD3D11InitParams params;
+ params.enableDebugLayer = true;
+ r = QRhi::create(QRhi::D3D11, &params, rhiFlags);
+ }
+#endif
+
+#ifdef Q_OS_DARWIN
+ if (graphicsApi == Metal) {
+ QRhiMetalInitParams params;
+ r = QRhi::create(QRhi::Metal, &params, rhiFlags);
+ }
+#endif
+
+ if (!r)
+ qFatal("Failed to create RHI backend");
+}
+
+void Renderer::destroyRhi()
+{
+ qDebug() << "renderer" << this << "destroying rhi";
+ delete r;
+ r = nullptr;
+}
+
+void Renderer::renderEvent(QEvent *e)
+{
+ Q_ASSERT(QThread::currentThread() == thread);
+
+ if (thread->sleeping)
+ thread->stopEventProcessing = true;
+
+ switch (int(e->type())) {
+ case InitEvent::TYPE:
+ qDebug() << "renderer" << this << "for window" << window << "is initializing";
+ createRhi();
+ init();
+ break;
+ case RequestRenderEvent::TYPE:
+ thread->pendingRender = true;
+ thread->pendingRenderIsNewExpose = static_cast<RequestRenderEvent *>(e)->newlyExposed;
+ break;
+ case SurfaceCleanupEvent::TYPE: // when the QWindow is closed, before QPlatformWindow goes away
+ thread->mutex.lock();
+ qDebug() << "renderer" << this << "for window" << window << "is destroying swapchain";
+ thread->pendingRender = false;
+ releaseSwapChain();
+ thread->cond.wakeOne();
+ thread->mutex.unlock();
+ break;
+ case CloseEvent::TYPE: // when destroying the window+renderer (NB not the same as hitting X on the window, that's just QWindow close)
+ qDebug() << "renderer" << this << "for window" << window << "is shutting down";
+ thread->pendingRender = false;
+ thread->active = false;
+ thread->stopEventProcessing = true;
+ releaseResources();
+ destroyRhi();
+ break;
+ case SyncSurfaceSizeEvent::TYPE:
+ thread->mutex.lock();
+ thread->pendingRender = false;
+ render(false, true);
+ break;
+ default:
+ break;
+ }
+}
+
+void Renderer::init()
+{
+ m_sc = r->newSwapChain();
+ m_ds = r->newRenderBuffer(QRhiRenderBuffer::DepthStencil,
+ QSize(), // no need to set the size yet
+ 1,
+ QRhiRenderBuffer::UsedWithSwapChainOnly);
+ m_releasePool << m_ds;
+ m_sc->setWindow(window);
+ m_sc->setDepthStencil(m_ds);
+ m_rp = m_sc->newCompatibleRenderPassDescriptor();
+ m_releasePool << m_rp;
+ m_sc->setRenderPassDescriptor(m_rp);
+
+ m_vbuf = r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube));
+ m_releasePool << m_vbuf;
+ m_vbuf->build();
+
+ m_ubuf = r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64 + 4);
+ m_releasePool << m_ubuf;
+ m_ubuf->build();
+
+ QImage image = QImage(QLatin1String(":/qt256.png")).convertToFormat(QImage::Format_RGBA8888);
+ m_tex = r->newTexture(QRhiTexture::RGBA8, image.size());
+ m_releasePool << m_tex;
+ m_tex->build();
+
+ m_sampler = r->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
+ QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
+ m_releasePool << m_sampler;
+ m_sampler->build();
+
+ m_srb = r->newShaderResourceBindings();
+ m_releasePool << m_srb;
+ m_srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, m_ubuf),
+ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, m_tex, m_sampler)
+ });
+ m_srb->build();
+
+ m_ps = r->newGraphicsPipeline();
+ m_releasePool << m_ps;
+
+ m_ps->setDepthTest(true);
+ m_ps->setDepthWrite(true);
+ m_ps->setDepthOp(QRhiGraphicsPipeline::Less);
+
+ m_ps->setCullMode(QRhiGraphicsPipeline::Back);
+ m_ps->setFrontFace(QRhiGraphicsPipeline::CCW);
+
+ m_ps->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, getShader(QLatin1String(":/texture.vert.qsb")) },
+ { QRhiGraphicsShaderStage::Fragment, getShader(QLatin1String(":/texture.frag.qsb")) }
+ });
+
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 3 * sizeof(float) },
+ { 2 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float3, 0 },
+ { 1, 1, QRhiVertexInputAttribute::Float2, 0 }
+ });
+
+ m_ps->setVertexInputLayout(inputLayout);
+ m_ps->setShaderResourceBindings(m_srb);
+ m_ps->setRenderPassDescriptor(m_rp);
+
+ m_ps->build();
+
+ m_initialUpdates = r->nextResourceUpdateBatch();
+
+ m_initialUpdates->uploadStaticBuffer(m_vbuf, cube);
+
+ qint32 flip = 0;
+ m_initialUpdates->updateDynamicBuffer(m_ubuf, 64, 4, &flip);
+
+ m_initialUpdates->uploadTexture(m_tex, image);
+}
+
+void Renderer::releaseSwapChain()
+{
+ if (m_hasSwapChain) {
+ m_hasSwapChain = false;
+ m_sc->release();
+ }
+}
+
+void Renderer::releaseResources()
+{
+ qDeleteAll(m_releasePool);
+ m_releasePool.clear();
+
+ delete m_sc;
+ m_sc = nullptr;
+}
+
+void Renderer::render(bool newlyExposed, bool wakeBeforePresent)
+{
+ // This function handles both resizing and rendering. Resizes have some
+ // complications due to the threaded model (check exposeEvent and
+ // sendSyncSurfaceSize) but don't have to worry about that in here.
+
+ auto buildOrResizeSwapChain = [this] {
+ qDebug() << "renderer" << this << "build or resize swapchain for window" << window;
+ const QSize outputSize = m_sc->surfacePixelSize();
+ qDebug() << " size is" << outputSize;
+ m_ds->setPixelSize(outputSize);
+ m_ds->build();
+ m_hasSwapChain = m_sc->buildOrResize();
+ m_proj = r->clipSpaceCorrMatrix();
+ m_proj.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 100.0f);
+ m_proj.translate(0, 0, -4);
+ };
+
+ auto wakeUpIfNeeded = [wakeBeforePresent, this] {
+ // make sure the main/gui thread is not blocked when issuing the Present (or equivalent)
+ if (wakeBeforePresent) {
+ thread->cond.wakeOne();
+ thread->mutex.unlock();
+ }
+ };
+
+ const QSize surfaceSize = m_sc->surfacePixelSize();
+ if (surfaceSize.isEmpty()) {
+ wakeUpIfNeeded();
+ return;
+ }
+
+ if (newlyExposed || m_sc->currentPixelSize() != surfaceSize)
+ buildOrResizeSwapChain();
+
+ if (!m_hasSwapChain) {
+ wakeUpIfNeeded();
+ return;
+ }
+
+ QRhi::FrameOpResult result = r->beginFrame(m_sc);
+ if (result == QRhi::FrameOpSwapChainOutOfDate) {
+ buildOrResizeSwapChain();
+ if (!m_hasSwapChain) {
+ wakeUpIfNeeded();
+ return;
+ }
+ result = r->beginFrame(m_sc);
+ }
+ if (result != QRhi::FrameOpSuccess) {
+ wakeUpIfNeeded();
+ return;
+ }
+
+ QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
+ const QSize outputSize = m_sc->currentPixelSize();
+
+ QRhiResourceUpdateBatch *u = r->nextResourceUpdateBatch();
+ if (m_initialUpdates) {
+ u->merge(m_initialUpdates);
+ m_initialUpdates->release();
+ m_initialUpdates = nullptr;
+ }
+
+ m_rotation += 1.0f;
+ QMatrix4x4 mvp = m_proj;
+ mvp.scale(0.5f);
+ mvp.rotate(m_rotation, m_rotationAxis == 0 ? 1 : 0, m_rotationAxis == 1 ? 1 : 0, m_rotationAxis == 2 ? 1 : 0);
+ u->updateDynamicBuffer(m_ubuf, 0, 64, mvp.constData());
+
+ cb->beginPass(m_sc->currentFrameRenderTarget(),
+ QColor::fromRgbF(float(m_bgColor.redF()), float(m_bgColor.greenF()), float(m_bgColor.blueF()), 1.0f),
+ { 1.0f, 0 },
+ u);
+
+ cb->setGraphicsPipeline(m_ps);
+ cb->setViewport(QRhiViewport(0, 0, outputSize.width(), outputSize.height()));
+ cb->setShaderResources();
+ const QRhiCommandBuffer::VertexInput vbufBindings[] = {
+ { m_vbuf, 0 },
+ { m_vbuf, 36 * 3 * sizeof(float) }
+ };
+ cb->setVertexInput(0, 2, vbufBindings);
+ cb->draw(36);
+
+ cb->endPass();
+
+ wakeUpIfNeeded();
+
+ r->endFrame(m_sc);
+
+ m_frameCount += 1;
+ if ((m_frameCount % 300) == 0) {
+ const QRhiProfiler::CpuTime ff = r->profiler()->frameToFrameTimes(m_sc);
+ const QRhiProfiler::CpuTime be = r->profiler()->frameBuildTimes(m_sc);
+ const QRhiProfiler::GpuTime gp = r->profiler()->gpuFrameTimes(m_sc);
+ if (r->isFeatureSupported(QRhi::Timestamps)) {
+ qDebug("[renderer %p] frame-to-frame: min %lld max %lld avg %f. "
+ "frame build: min %lld max %lld avg %f. "
+ "gpu frame time: min %f max %f avg %f",
+ this,
+ ff.minTime, ff.maxTime, ff.avgTime,
+ be.minTime, be.maxTime, be.avgTime,
+ gp.minTime, gp.maxTime, gp.avgTime);
+ } else {
+ qDebug("[renderer %p] frame-to-frame: min %lld max %lld avg %f. "
+ "frame build: min %lld max %lld avg %f. ",
+ this,
+ ff.minTime, ff.maxTime, ff.avgTime,
+ be.minTime, be.maxTime, be.avgTime);
+ }
+ }
+}
+
+void Renderer::sendInit()
+{ // main thread
+ InitEvent *e = new InitEvent;
+ thread->eventQueue.addEvent(e);
+}
+
+void Renderer::sendRender(bool newlyExposed)
+{ // main thread
+ RequestRenderEvent *e = new RequestRenderEvent(newlyExposed);
+ thread->eventQueue.addEvent(e);
+}
+
+void Renderer::sendSurfaceGoingAway()
+{ // main thread
+ SurfaceCleanupEvent *e = new SurfaceCleanupEvent;
+
+ // cannot let this thread to proceed with tearing down the native window
+ // before the render thread completes the swapchain release
+ thread->mutex.lock();
+
+ thread->eventQueue.addEvent(e);
+
+ thread->cond.wait(&thread->mutex);
+ thread->mutex.unlock();
+}
+
+void Renderer::sendSyncSurfaceSize()
+{ // main thread
+ SyncSurfaceSizeEvent *e = new SyncSurfaceSizeEvent;
+ // must block to prevent surface size mess. the render thread will do a
+ // full rendering round before it unlocks which is good since it can thus
+ // pick up and the surface (window) size atomically.
+ thread->mutex.lock();
+ thread->eventQueue.addEvent(e);
+ thread->cond.wait(&thread->mutex);
+ thread->mutex.unlock();
+}
+
+struct WindowAndRenderer
+{
+ QWindow *window;
+ Renderer *renderer;
+};
+
+QVector<WindowAndRenderer> windows;
+
+void createWindow()
+{
+ static QColor colors[] = { Qt::red, Qt::green, Qt::blue, Qt::yellow, Qt::cyan, Qt::gray };
+ const int n = windows.count();
+ Window *w = new Window(QString::asprintf("Window+Thread #%d (%s)", n, qPrintable(graphicsApiName())), graphicsApi);
+ Renderer *renderer = new Renderer(w, colors[n % 6], n % 3);;
+ QObject::connect(w, &Window::initRequested, w, [renderer] {
+ renderer->sendInit();
+ });
+ QObject::connect(w, &Window::renderRequested, w, [w, renderer](bool newlyExposed) {
+ renderer->sendRender(newlyExposed);
+ w->requestUpdate();
+ });
+ QObject::connect(w, &Window::surfaceGoingAway, w, [renderer] {
+ renderer->sendSurfaceGoingAway();
+ });
+ QObject::connect(w, &Window::syncSurfaceSizeRequested, w, [renderer] {
+ renderer->sendSyncSurfaceSize();
+ });
+ windows.append({ w, renderer });
+ w->show();
+}
+
+void closeWindow()
+{
+ WindowAndRenderer wr = windows.takeLast();
+ delete wr.renderer;
+ delete wr.window;
+}
+
+int main(int argc, char **argv)
+{
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+ QApplication app(argc, argv);
+
+#if defined(Q_OS_WIN)
+ graphicsApi = D3D11;
+#elif defined(Q_OS_DARWIN)
+ graphicsApi = Metal;
+#elif QT_CONFIG(vulkan)
+ graphicsApi = Vulkan;
+#else
+ graphicsApi = OpenGL;
+#endif
+
+ QCommandLineParser cmdLineParser;
+ cmdLineParser.addHelpOption();
+ QCommandLineOption glOption({ "g", "opengl" }, QLatin1String("OpenGL (2.x)"));
+ cmdLineParser.addOption(glOption);
+ QCommandLineOption vkOption({ "v", "vulkan" }, QLatin1String("Vulkan"));
+ cmdLineParser.addOption(vkOption);
+ QCommandLineOption d3dOption({ "d", "d3d11" }, QLatin1String("Direct3D 11"));
+ cmdLineParser.addOption(d3dOption);
+ QCommandLineOption mtlOption({ "m", "metal" }, QLatin1String("Metal"));
+ cmdLineParser.addOption(mtlOption);
+ cmdLineParser.process(app);
+ if (cmdLineParser.isSet(glOption))
+ graphicsApi = OpenGL;
+ if (cmdLineParser.isSet(vkOption))
+ graphicsApi = Vulkan;
+ if (cmdLineParser.isSet(d3dOption))
+ graphicsApi = D3D11;
+ if (cmdLineParser.isSet(mtlOption))
+ graphicsApi = Metal;
+
+ qDebug("Selected graphics API is %s", qPrintable(graphicsApiName()));
+ qDebug("This is a multi-api example, use command line arguments to override:\n%s", qPrintable(cmdLineParser.helpText()));
+
+#if QT_CONFIG(vulkan)
+ instance = new QVulkanInstance;
+ if (graphicsApi == Vulkan) {
+#ifndef Q_OS_ANDROID
+ instance->setLayers({ "VK_LAYER_LUNARG_standard_validation" });
+#else
+ instance->setLayers(QByteArrayList()
+ << "VK_LAYER_GOOGLE_threading"
+ << "VK_LAYER_LUNARG_parameter_validation"
+ << "VK_LAYER_LUNARG_object_tracker"
+ << "VK_LAYER_LUNARG_core_validation"
+ << "VK_LAYER_LUNARG_image"
+ << "VK_LAYER_LUNARG_swapchain"
+ << "VK_LAYER_GOOGLE_unique_objects");
+#endif
+ if (!instance->create()) {
+ qWarning("Failed to create Vulkan instance, switching to OpenGL");
+ graphicsApi = OpenGL;
+ }
+ }
+#endif
+
+ int winCount = 0;
+ QWidget w;
+ w.resize(800, 600);
+ w.setWindowTitle(QCoreApplication::applicationName() + QLatin1String(" - ") + graphicsApiName());
+ QVBoxLayout *layout = new QVBoxLayout(&w);
+
+ QPlainTextEdit *info = new QPlainTextEdit(
+ QLatin1String("This application tests rendering on a separate thread per window, with dedicated QRhi instances and resources. "
+ "\n\nThis is the same concept as the Qt Quick Scenegraph's threaded render loop. This should allow rendering to the different windows "
+ "without unintentionally throttling each other's threads."
+ "\n\nUsing API: ") + graphicsApiName());
+ info->setReadOnly(true);
+ layout->addWidget(info);
+ QLabel *label = new QLabel(QLatin1String("Window and thread count: 0"));
+ layout->addWidget(label);
+ QPushButton *btn = new QPushButton(QLatin1String("New window"));
+ QObject::connect(btn, &QPushButton::clicked, btn, [label, &winCount] {
+ winCount += 1;
+ label->setText(QString::asprintf("Window count: %d", winCount));
+ createWindow();
+ });
+ layout->addWidget(btn);
+ btn = new QPushButton(QLatin1String("Close window"));
+ QObject::connect(btn, &QPushButton::clicked, btn, [label, &winCount] {
+ if (winCount > 0) {
+ winCount -= 1;
+ label->setText(QString::asprintf("Window count: %d", winCount));
+ closeWindow();
+ }
+ });
+ layout->addWidget(btn);
+ w.show();
+
+ int result = app.exec();
+
+ for (const WindowAndRenderer &wr : windows) {
+ delete wr.renderer;
+ delete wr.window;
+ }
+
+#if QT_CONFIG(vulkan)
+ delete instance;
+#endif
+
+ return result;
+}
diff --git a/tests/manual/rhi/multiwindow_threaded/multiwindow_threaded.pro b/tests/manual/rhi/multiwindow_threaded/multiwindow_threaded.pro
new file mode 100644
index 0000000000..0b78518b9b
--- /dev/null
+++ b/tests/manual/rhi/multiwindow_threaded/multiwindow_threaded.pro
@@ -0,0 +1,12 @@
+TEMPLATE = app
+
+QT += gui-private widgets
+
+SOURCES = \
+ multiwindow_threaded.cpp \
+ window.cpp
+
+HEADERS = \
+ window.h
+
+RESOURCES = multiwindow_threaded.qrc
diff --git a/tests/manual/rhi/multiwindow_threaded/multiwindow_threaded.qrc b/tests/manual/rhi/multiwindow_threaded/multiwindow_threaded.qrc
new file mode 100644
index 0000000000..742b2b77af
--- /dev/null
+++ b/tests/manual/rhi/multiwindow_threaded/multiwindow_threaded.qrc
@@ -0,0 +1,7 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+ <file alias="qt256.png">../shared/qt256.png</file>
+ <file alias="texture.vert.qsb">../shared/texture.vert.qsb</file>
+ <file alias="texture.frag.qsb">../shared/texture.frag.qsb</file>
+</qresource>
+</RCC>
diff --git a/tests/manual/rhi/multiwindow_threaded/window.cpp b/tests/manual/rhi/multiwindow_threaded/window.cpp
new file mode 100644
index 0000000000..aa7b479047
--- /dev/null
+++ b/tests/manual/rhi/multiwindow_threaded/window.cpp
@@ -0,0 +1,142 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "window.h"
+#include <QPlatformSurfaceEvent>
+
+#ifndef QT_NO_OPENGL
+#include <QtGui/private/qrhigles2_p.h>
+#endif
+
+#if QT_CONFIG(vulkan)
+extern QVulkanInstance *instance;
+#endif
+
+Window::Window(const QString &title, GraphicsApi api)
+{
+ switch (api) {
+ case OpenGL:
+#if QT_CONFIG(opengl)
+ setSurfaceType(OpenGLSurface);
+ setFormat(QRhiGles2InitParams::adjustedFormat());
+#endif
+ break;
+ case Vulkan:
+#if QT_CONFIG(vulkan)
+ setSurfaceType(VulkanSurface);
+ setVulkanInstance(instance);
+#endif
+ break;
+ case D3D11:
+ setSurfaceType(OpenGLSurface); // not a typo
+ break;
+ case Metal:
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
+ setSurfaceType(MetalSurface);
+#endif
+ break;
+ default:
+ break;
+ }
+
+ resize(800, 600);
+ setTitle(title);
+}
+
+Window::~Window()
+{
+}
+
+void Window::exposeEvent(QExposeEvent *)
+{
+ if (isExposed()) {
+ if (!m_running) {
+ // initialize and start rendering when the window becomes usable for graphics purposes
+ m_running = true;
+ m_notExposed = false;
+ emit initRequested();
+ emit renderRequested(true);
+ } else {
+ // continue when exposed again
+ if (m_notExposed) {
+ m_notExposed = false;
+ emit renderRequested(true);
+ } else {
+ // resize generates exposes - this is very important here (unlike in a single-threaded renderer)
+ emit syncSurfaceSizeRequested();
+ }
+ }
+ } else {
+ // stop pushing frames when not exposed (on some platforms this is essential, optional on others)
+ if (m_running)
+ m_notExposed = true;
+ }
+}
+
+bool Window::event(QEvent *e)
+{
+ switch (e->type()) {
+ case QEvent::UpdateRequest:
+ if (!m_notExposed)
+ emit renderRequested(false);
+ 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)
+ emit surfaceGoingAway();
+ break;
+
+ default:
+ break;
+ }
+
+ return QWindow::event(e);
+}
diff --git a/tests/manual/rhi/multiwindow_threaded/window.h b/tests/manual/rhi/multiwindow_threaded/window.h
new file mode 100644
index 0000000000..7810858d0a
--- /dev/null
+++ b/tests/manual/rhi/multiwindow_threaded/window.h
@@ -0,0 +1,86 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef WINDOW_H
+#define WINDOW_H
+
+#include <QWindow>
+
+enum GraphicsApi
+{
+ OpenGL,
+ Vulkan,
+ D3D11,
+ Metal
+};
+
+class Window : public QWindow
+{
+ Q_OBJECT
+
+public:
+ Window(const QString &title, GraphicsApi api);
+ ~Window();
+
+ void exposeEvent(QExposeEvent *) override;
+ bool event(QEvent *) override;
+
+signals:
+ void initRequested();
+ void renderRequested(bool newlyExposed);
+ void surfaceGoingAway();
+ void syncSurfaceSizeRequested();
+
+protected:
+ bool m_running = false;
+ bool m_notExposed = true;
+};
+
+#endif
diff --git a/tests/manual/rhi/offscreen/offscreen.cpp b/tests/manual/rhi/offscreen/offscreen.cpp
new file mode 100644
index 0000000000..838dc9d2c8
--- /dev/null
+++ b/tests/manual/rhi/offscreen/offscreen.cpp
@@ -0,0 +1,366 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QGuiApplication>
+#include <QImage>
+#include <QFileInfo>
+#include <QFile>
+#include <QLoggingCategory>
+#include <QCommandLineParser>
+#include <QtGui/private/qshader_p.h>
+
+#include <QtGui/private/qrhinull_p.h>
+
+#ifndef QT_NO_OPENGL
+#include <QtGui/private/qrhigles2_p.h>
+#include <QOffscreenSurface>
+#endif
+
+#if QT_CONFIG(vulkan)
+#include <QLoggingCategory>
+#include <QtGui/private/qrhivulkan_p.h>
+#endif
+
+#ifdef Q_OS_WIN
+#include <QtGui/private/qrhid3d11_p.h>
+#endif
+
+#ifdef Q_OS_DARWIN
+#include <QtGui/private/qrhimetal_p.h>
+#endif
+
+//#define TEST_FINISH
+
+static float vertexData[] = { // Y up (note m_proj), 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,
+};
+
+static QShader getShader(const QString &name)
+{
+ QFile f(name);
+ if (f.open(QIODevice::ReadOnly))
+ return QShader::fromSerialized(f.readAll());
+
+ return QShader();
+}
+
+enum GraphicsApi
+{
+ OpenGL,
+ Vulkan,
+ D3D11,
+ Metal,
+ Null
+};
+
+GraphicsApi graphicsApi;
+
+QString graphicsApiName()
+{
+ switch (graphicsApi) {
+ case OpenGL:
+ return QLatin1String("OpenGL 2.x");
+ case Vulkan:
+ return QLatin1String("Vulkan");
+ case D3D11:
+ return QLatin1String("Direct3D 11");
+ case Metal:
+ return QLatin1String("Metal");
+ case Null:
+ return QLatin1String("Null");
+ default:
+ break;
+ }
+ return QString();
+}
+
+int main(int argc, char **argv)
+{
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+ QGuiApplication app(argc, argv);
+
+#if defined(Q_OS_WIN)
+ graphicsApi = D3D11;
+#elif defined(Q_OS_DARWIN)
+ graphicsApi = Metal;
+#elif QT_CONFIG(vulkan)
+ graphicsApi = Vulkan;
+#else
+ graphicsApi = OpenGL;
+#endif
+
+ QCommandLineParser cmdLineParser;
+ cmdLineParser.addHelpOption();
+ QCommandLineOption glOption({ "g", "opengl" }, QLatin1String("OpenGL (2.x)"));
+ cmdLineParser.addOption(glOption);
+ QCommandLineOption vkOption({ "v", "vulkan" }, QLatin1String("Vulkan"));
+ cmdLineParser.addOption(vkOption);
+ QCommandLineOption d3dOption({ "d", "d3d11" }, QLatin1String("Direct3D 11"));
+ cmdLineParser.addOption(d3dOption);
+ QCommandLineOption mtlOption({ "m", "metal" }, QLatin1String("Metal"));
+ cmdLineParser.addOption(mtlOption);
+ QCommandLineOption nullOption({ "n", "null" }, QLatin1String("Null"));
+ cmdLineParser.addOption(nullOption);
+ cmdLineParser.process(app);
+ if (cmdLineParser.isSet(glOption))
+ graphicsApi = OpenGL;
+ if (cmdLineParser.isSet(vkOption))
+ graphicsApi = Vulkan;
+ if (cmdLineParser.isSet(d3dOption))
+ graphicsApi = D3D11;
+ if (cmdLineParser.isSet(mtlOption))
+ graphicsApi = Metal;
+ if (cmdLineParser.isSet(nullOption))
+ graphicsApi = Null;
+
+ qDebug("Selected graphics API is %s", qPrintable(graphicsApiName()));
+ qDebug("This is a multi-api example, use command line arguments to override:\n%s", qPrintable(cmdLineParser.helpText()));
+
+ QRhi *r = nullptr;
+
+ if (graphicsApi == Null) {
+ QRhiNullInitParams params;
+ r = QRhi::create(QRhi::Null, &params);
+ }
+
+#if QT_CONFIG(vulkan)
+ QVulkanInstance inst;
+ if (graphicsApi == Vulkan) {
+ QLoggingCategory::setFilterRules(QStringLiteral("qt.vulkan=true"));
+#ifndef Q_OS_ANDROID
+ inst.setLayers(QByteArrayList() << "VK_LAYER_LUNARG_standard_validation");
+#else
+ inst.setLayers(QByteArrayList()
+ << "VK_LAYER_GOOGLE_threading"
+ << "VK_LAYER_LUNARG_parameter_validation"
+ << "VK_LAYER_LUNARG_object_tracker"
+ << "VK_LAYER_LUNARG_core_validation"
+ << "VK_LAYER_LUNARG_image"
+ << "VK_LAYER_LUNARG_swapchain"
+ << "VK_LAYER_GOOGLE_unique_objects");
+#endif
+ if (inst.create()) {
+ QRhiVulkanInitParams params;
+ params.inst = &inst;
+ r = QRhi::create(QRhi::Vulkan, &params);
+ } else {
+ qWarning("Failed to create Vulkan instance, switching to OpenGL");
+ graphicsApi = OpenGL;
+ }
+ }
+#endif
+
+#ifndef QT_NO_OPENGL
+ QScopedPointer<QOffscreenSurface> offscreenSurface;
+ if (graphicsApi == OpenGL) {
+ offscreenSurface.reset(QRhiGles2InitParams::newFallbackSurface());
+ QRhiGles2InitParams params;
+ params.fallbackSurface = offscreenSurface.data();
+ r = QRhi::create(QRhi::OpenGLES2, &params);
+ }
+#endif
+
+#ifdef Q_OS_WIN
+ if (graphicsApi == D3D11) {
+ QRhiD3D11InitParams params;
+ params.enableDebugLayer = true;
+ r = QRhi::create(QRhi::D3D11, &params);
+ }
+#endif
+
+#ifdef Q_OS_DARWIN
+ if (graphicsApi == Metal) {
+ QRhiMetalInitParams params;
+ r = QRhi::create(QRhi::Metal, &params);
+ }
+#endif
+
+ if (!r)
+ qFatal("Failed to initialize RHI");
+
+ QRhiTexture *tex = r->newTexture(QRhiTexture::RGBA8, QSize(1280, 720), 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource);
+ tex->build();
+ QRhiTextureRenderTarget *rt = r->newTextureRenderTarget({ tex });
+ QRhiRenderPassDescriptor *rp = rt->newCompatibleRenderPassDescriptor();
+ rt->setRenderPassDescriptor(rp);
+ rt->build();
+
+ QMatrix4x4 proj = r->clipSpaceCorrMatrix();
+ proj.perspective(45.0f, 1280 / 720.f, 0.01f, 1000.0f);
+ proj.translate(0, 0, -4);
+
+ QRhiBuffer *vbuf = r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData));
+ vbuf->build();
+
+ QRhiBuffer *ubuf = r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68);
+ ubuf->build();
+
+ QRhiShaderResourceBindings *srb = r->newShaderResourceBindings();
+ srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, ubuf)
+ });
+ srb->build();
+
+ QRhiGraphicsPipeline *ps = r->newGraphicsPipeline();
+
+ QRhiGraphicsPipeline::TargetBlend premulAlphaBlend;
+ premulAlphaBlend.enable = true;
+ ps->setTargetBlends({ premulAlphaBlend });
+
+ const QShader vs = getShader(QLatin1String(":/color.vert.qsb"));
+ if (!vs.isValid())
+ qFatal("Failed to load shader pack (vertex)");
+ const QShader fs = getShader(QLatin1String(":/color.frag.qsb"));
+ if (!fs.isValid())
+ qFatal("Failed to load shader pack (fragment)");
+
+ ps->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, vs },
+ { QRhiGraphicsShaderStage::Fragment, fs }
+ });
+
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 5 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) }
+ });
+
+ ps->setVertexInputLayout(inputLayout);
+ ps->setShaderResourceBindings(srb);
+ ps->setRenderPassDescriptor(rp);
+ ps->build();
+
+ int frame = 0;
+ for (; frame < 20; ++frame) {
+ QRhiCommandBuffer *cb;
+ if (r->beginOffscreenFrame(&cb) != QRhi::FrameOpSuccess)
+ break;
+
+ qDebug("Generating offscreen frame %d", frame);
+ QRhiResourceUpdateBatch *u = r->nextResourceUpdateBatch();
+ if (frame == 0)
+ u->uploadStaticBuffer(vbuf, vertexData);
+
+ static float rotation = 0.0f;
+ QMatrix4x4 mvp = proj;
+ mvp.rotate(rotation, 0, 1, 0);
+ u->updateDynamicBuffer(ubuf, 0, 64, mvp.constData());
+ rotation += 5.0f;
+ static float opacity = 1.0f;
+ static int opacityDir= 1;
+ u->updateDynamicBuffer(ubuf, 64, 4, &opacity);
+ opacity += opacityDir * 0.005f;
+ if (opacity < 0.0f || opacity > 1.0f) {
+ opacityDir *= -1;
+ opacity = qBound(0.0f, opacity, 1.0f);
+ }
+
+ cb->beginPass(rt, { 0, 1, 0, 1 }, { 1, 0 }, u);
+ cb->setGraphicsPipeline(ps);
+ cb->setViewport({ 0, 0, 1280, 720 });
+ cb->setShaderResources();
+ const QRhiCommandBuffer::VertexInput vbufBinding(vbuf, 0);
+ cb->setVertexInput(0, 1, &vbufBinding);
+ cb->draw(3);
+
+ u = r->nextResourceUpdateBatch();
+ QRhiReadbackDescription rb(tex);
+ QRhiReadbackResult rbResult;
+ rbResult.completed = [frame] { qDebug(" - readback %d completed", frame); };
+ u->readBackTexture(rb, &rbResult);
+
+ cb->endPass(u);
+
+ qDebug("Submit and wait");
+#ifdef TEST_FINISH
+ r->finish();
+#else
+ r->endOffscreenFrame();
+#endif
+ // The data should be ready either because endOffscreenFrame() waits
+ // for completion or because finish() did.
+ if (!rbResult.data.isEmpty()) {
+ const uchar *p = reinterpret_cast<const uchar *>(rbResult.data.constData());
+ QImage image(p, rbResult.pixelSize.width(), rbResult.pixelSize.height(), QImage::Format_RGBA8888);
+ QString fn = QString::asprintf("frame%d.png", frame);
+ fn = QFileInfo(fn).absoluteFilePath();
+ qDebug("Saving into %s", qPrintable(fn));
+ if (r->isYUpInFramebuffer())
+ image.mirrored().save(fn);
+ else
+ image.save(fn);
+ } else {
+ qWarning("Readback failed!");
+ }
+#ifdef TEST_FINISH
+ r->endOffscreenFrame();
+#endif
+ }
+
+ delete ps;
+ delete srb;
+ delete ubuf;
+ delete vbuf;
+
+ delete rt;
+ delete rp;
+ delete tex;
+
+ delete r;
+
+ qDebug("\nRendered and read back %d frames using %s", frame, qPrintable(graphicsApiName()));
+
+ return 0;
+}
diff --git a/tests/manual/rhi/offscreen/offscreen.pro b/tests/manual/rhi/offscreen/offscreen.pro
new file mode 100644
index 0000000000..ae040a4b6c
--- /dev/null
+++ b/tests/manual/rhi/offscreen/offscreen.pro
@@ -0,0 +1,9 @@
+TEMPLATE = app
+CONFIG += console
+
+QT += gui-private
+
+SOURCES = \
+ offscreen.cpp
+
+RESOURCES = offscreen.qrc
diff --git a/tests/manual/rhi/offscreen/offscreen.qrc b/tests/manual/rhi/offscreen/offscreen.qrc
new file mode 100644
index 0000000000..8cec4fa9ec
--- /dev/null
+++ b/tests/manual/rhi/offscreen/offscreen.qrc
@@ -0,0 +1,6 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+ <file alias="color.vert.qsb">../shared/color.vert.qsb</file>
+ <file alias="color.frag.qsb">../shared/color.frag.qsb</file>
+</qresource>
+</RCC>
diff --git a/tests/manual/rhi/qrhiprof/qrhiprof.cpp b/tests/manual/rhi/qrhiprof/qrhiprof.cpp
new file mode 100644
index 0000000000..7e3027a951
--- /dev/null
+++ b/tests/manual/rhi/qrhiprof/qrhiprof.cpp
@@ -0,0 +1,671 @@
+/****************************************************************************
+**
+** Copyright (C) 2019 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+// Simple QRhiProfiler receiver app. Start it and then in a QRhi-based
+// application connect with a QTcpSocket to 127.0.0.1:30667 and set that as the
+// QRhiProfiler's device.
+
+#include <QTcpServer>
+#include <QTcpSocket>
+#include <QApplication>
+#include <QWidget>
+#include <QVBoxLayout>
+#include <QGroupBox>
+#include <QTextEdit>
+#include <QLabel>
+#include <QTime>
+#include <QtGui/private/qrhiprofiler_p.h>
+
+const int MIN_KNOWN_OP = 1;
+const int MAX_KNOWN_OP = 18;
+
+class Parser : public QObject
+{
+ Q_OBJECT
+
+public:
+ void feed(const QByteArray &line);
+
+ struct Event {
+ QRhiProfiler::StreamOp op;
+ qint64 timestamp;
+ quint64 resource;
+ QByteArray resourceName;
+
+ struct Param {
+ enum ValueType {
+ Int64,
+ Float
+ };
+ QByteArray key;
+ ValueType valueType;
+ union {
+ qint64 intValue;
+ float floatValue;
+ };
+ };
+
+ QVector<Param> params;
+
+ const Param *param(const char *key) const {
+ auto it = std::find_if(params.cbegin(), params.cend(), [key](const Param &p) {
+ return !strcmp(p.key.constData(), key);
+ });
+ return it == params.cend() ? nullptr : &*it;
+ }
+ };
+
+signals:
+ void eventReceived(const Event &e);
+};
+
+void Parser::feed(const QByteArray &line)
+{
+ const QList<QByteArray> elems = line.split(',');
+ if (elems.count() < 4) {
+ qWarning("Malformed line '%s'", line.constData());
+ return;
+ }
+ bool ok = false;
+ const int op = elems[0].toInt(&ok);
+ if (!ok) {
+ qWarning("Invalid op %s", elems[0].constData());
+ return;
+ }
+ if (op < MIN_KNOWN_OP || op > MAX_KNOWN_OP) {
+ qWarning("Unknown op %d", op);
+ return;
+ }
+
+ Event e;
+ e.op = QRhiProfiler::StreamOp(op);
+ e.timestamp = elems[1].toLongLong();
+ e.resource = elems[2].toULongLong();
+ e.resourceName = elems[3];
+
+ const int elemCount = elems.count();
+ for (int i = 4; i < elemCount; i += 2) {
+ if (i + 1 < elemCount && !elems[i].isEmpty() && !elems[i + 1].isEmpty()) {
+ QByteArray key = elems[i];
+ if (key.startsWith('F')) {
+ key = key.mid(1);
+ bool ok = false;
+ const float value = elems[i + 1].toFloat(&ok);
+ if (!ok) {
+ qWarning("Failed to parse float %s in line '%s'", elems[i + 1].constData(), line.constData());
+ continue;
+ }
+ Event::Param param;
+ param.key = key;
+ param.valueType = Event::Param::Float;
+ param.floatValue = value;
+ e.params.append(param);
+ } else {
+ const qint64 value = elems[i + 1].toLongLong();
+ Event::Param param;
+ param.key = key;
+ param.valueType = Event::Param::Int64;
+ param.intValue = value;
+ e.params.append(param);
+ }
+ }
+ }
+
+ emit eventReceived(e);
+}
+
+class Tracker : public QObject
+{
+ Q_OBJECT
+
+public slots:
+ void handleEvent(const Parser::Event &e);
+
+signals:
+ void buffersTouched();
+ void texturesTouched();
+ void swapchainsTouched();
+ void frameTimeTouched();
+ void gpuFrameTimeTouched();
+ void gpuMemAllocStatsTouched();
+
+public:
+ Tracker() {
+ reset();
+ }
+
+ static const int MAX_STAGING_SLOTS = 3;
+
+ struct Buffer {
+ Buffer()
+ {
+ memset(stagingExtraSize, 0, sizeof(stagingExtraSize));
+ }
+ quint64 lastTimestamp;
+ QByteArray resourceName;
+ qint64 effectiveSize = 0;
+ int backingGpuBufCount = 1;
+ qint64 stagingExtraSize[MAX_STAGING_SLOTS];
+ };
+ QHash<qint64, Buffer> m_buffers;
+ qint64 m_totalBufferApproxByteSize;
+ qint64 m_peakBufferApproxByteSize;
+ qint64 m_totalStagingBufferApproxByteSize;
+ qint64 m_peakStagingBufferApproxByteSize;
+
+ struct Texture {
+ Texture()
+ {
+ memset(stagingExtraSize, 0, sizeof(stagingExtraSize));
+ }
+ quint64 lastTimestamp;
+ QByteArray resourceName;
+ qint64 approxByteSize = 0;
+ bool ownsNativeResource = true;
+ qint64 stagingExtraSize[MAX_STAGING_SLOTS];
+ };
+ QHash<qint64, Texture> m_textures;
+ qint64 m_totalTextureApproxByteSize;
+ qint64 m_peakTextureApproxByteSize;
+ qint64 m_totalTextureStagingBufferApproxByteSize;
+ qint64 m_peakTextureStagingBufferApproxByteSize;
+
+ struct SwapChain {
+ quint64 lastTimestamp;
+ QByteArray resourceName;
+ qint64 approxByteSize = 0;
+ };
+ QHash<qint64, SwapChain> m_swapchains;
+ qint64 m_totalSwapChainApproxByteSize;
+ qint64 m_peakSwapChainApproxByteSize;
+
+ struct FrameTime {
+ qint64 framesSinceResize = 0;
+ int minDelta = 0;
+ int maxDelta = 0;
+ float avgDelta = 0;
+ };
+ FrameTime m_lastFrameTime;
+
+ struct GpuFrameTime {
+ float minTime = 0;
+ float maxTime = 0;
+ float avgTime = 0;
+ };
+ GpuFrameTime m_lastGpuFrameTime;
+
+ struct GpuMemAllocStats {
+ qint64 realAllocCount;
+ qint64 subAllocCount;
+ qint64 totalSize;
+ qint64 unusedSize;
+ };
+ GpuMemAllocStats m_lastGpuMemAllocStats;
+
+ void reset() {
+ m_buffers.clear();
+ m_textures.clear();
+ m_totalBufferApproxByteSize = 0;
+ m_peakBufferApproxByteSize = 0;
+ m_totalStagingBufferApproxByteSize = 0;
+ m_peakStagingBufferApproxByteSize = 0;
+ m_totalTextureApproxByteSize = 0;
+ m_peakTextureApproxByteSize = 0;
+ m_totalTextureStagingBufferApproxByteSize = 0;
+ m_peakTextureStagingBufferApproxByteSize = 0;
+ m_totalSwapChainApproxByteSize = 0;
+ m_peakSwapChainApproxByteSize = 0;
+ m_lastFrameTime = FrameTime();
+ m_lastGpuFrameTime = GpuFrameTime();
+ m_lastGpuMemAllocStats = GpuMemAllocStats();
+ emit buffersTouched();
+ emit texturesTouched();
+ emit swapchainsTouched();
+ emit frameTimeTouched();
+ emit gpuFrameTimeTouched();
+ emit gpuMemAllocStatsTouched();
+ }
+};
+
+Q_DECLARE_TYPEINFO(Tracker::Buffer, Q_MOVABLE_TYPE);
+Q_DECLARE_TYPEINFO(Tracker::Texture, Q_MOVABLE_TYPE);
+Q_DECLARE_TYPEINFO(Tracker::FrameTime, Q_MOVABLE_TYPE);
+Q_DECLARE_TYPEINFO(Tracker::GpuFrameTime, Q_MOVABLE_TYPE);
+
+void Tracker::handleEvent(const Parser::Event &e)
+{
+ switch (e.op) {
+ case QRhiProfiler::NewBuffer:
+ {
+ Buffer b;
+ b.lastTimestamp = e.timestamp;
+ b.resourceName = e.resourceName;
+ // type,0,usage,1,logical_size,84,effective_size,84,backing_gpu_buf_count,1,backing_cpu_buf_count,0
+ for (const Parser::Event::Param &p : e.params) {
+ if (p.key == QByteArrayLiteral("effective_size"))
+ b.effectiveSize = p.intValue;
+ else if (p.key == QByteArrayLiteral("backing_gpu_buf_count"))
+ b.backingGpuBufCount = p.intValue;
+ }
+ m_totalBufferApproxByteSize += b.effectiveSize * b.backingGpuBufCount;
+ m_peakBufferApproxByteSize = qMax(m_peakBufferApproxByteSize, m_totalBufferApproxByteSize);
+ m_buffers.insert(e.resource, b);
+ emit buffersTouched();
+ }
+ break;
+ case QRhiProfiler::ReleaseBuffer:
+ {
+ auto it = m_buffers.find(e.resource);
+ if (it != m_buffers.end()) {
+ m_totalBufferApproxByteSize -= it->effectiveSize * it->backingGpuBufCount;
+ m_buffers.erase(it);
+ emit buffersTouched();
+ }
+ }
+ break;
+
+ case QRhiProfiler::NewBufferStagingArea:
+ {
+ qint64 slot = -1;
+ qint64 size = 0;
+ for (const Parser::Event::Param &p : e.params) {
+ if (p.key == QByteArrayLiteral("slot"))
+ slot = p.intValue;
+ else if (p.key == QByteArrayLiteral("size"))
+ size = p.intValue;
+ }
+ if (slot >= 0 && slot < MAX_STAGING_SLOTS) {
+ auto it = m_buffers.find(e.resource);
+ if (it != m_buffers.end()) {
+ it->stagingExtraSize[slot] = size;
+ m_totalStagingBufferApproxByteSize += size;
+ m_peakStagingBufferApproxByteSize = qMax(m_peakStagingBufferApproxByteSize, m_totalStagingBufferApproxByteSize);
+ emit buffersTouched();
+ }
+ }
+ }
+ break;
+ case QRhiProfiler::ReleaseBufferStagingArea:
+ {
+ qint64 slot = -1;
+ for (const Parser::Event::Param &p : e.params) {
+ if (p.key == QByteArrayLiteral("slot"))
+ slot = p.intValue;
+ }
+ if (slot >= 0 && slot < MAX_STAGING_SLOTS) {
+ auto it = m_buffers.find(e.resource);
+ if (it != m_buffers.end()) {
+ m_totalStagingBufferApproxByteSize -= it->stagingExtraSize[slot];
+ it->stagingExtraSize[slot] = 0;
+ emit buffersTouched();
+ }
+ }
+ }
+ break;
+
+ case QRhiProfiler::NewTexture:
+ {
+ Texture t;
+ t.lastTimestamp = e.timestamp;
+ t.resourceName = e.resourceName;
+ // width,256,height,256,format,1,owns_native_resource,1,mip_count,9,layer_count,1,effective_sample_count,1,approx_byte_size,349524
+ for (const Parser::Event::Param &p : e.params) {
+ if (p.key == QByteArrayLiteral("approx_byte_size"))
+ t.approxByteSize = p.intValue;
+ else if (p.key == QByteArrayLiteral("owns_native_resource"))
+ t.ownsNativeResource = p.intValue;
+ }
+ if (t.ownsNativeResource) {
+ m_totalTextureApproxByteSize += t.approxByteSize;
+ m_peakTextureApproxByteSize = qMax(m_peakTextureApproxByteSize, m_totalTextureApproxByteSize);
+ }
+ m_textures.insert(e.resource, t);
+ emit texturesTouched();
+ }
+ break;
+ case QRhiProfiler::ReleaseTexture:
+ {
+ auto it = m_textures.find(e.resource);
+ if (it != m_textures.end()) {
+ if (it->ownsNativeResource)
+ m_totalTextureApproxByteSize -= it->approxByteSize;
+ m_textures.erase(it);
+ emit texturesTouched();
+ }
+ }
+ break;
+
+ case QRhiProfiler::NewTextureStagingArea:
+ {
+ qint64 slot = -1;
+ qint64 size = 0;
+ for (const Parser::Event::Param &p : e.params) {
+ if (p.key == QByteArrayLiteral("slot"))
+ slot = p.intValue;
+ else if (p.key == QByteArrayLiteral("size"))
+ size = p.intValue;
+ }
+ if (slot >= 0 && slot < MAX_STAGING_SLOTS) {
+ auto it = m_textures.find(e.resource);
+ if (it != m_textures.end()) {
+ it->stagingExtraSize[slot] = size;
+ m_totalTextureStagingBufferApproxByteSize += size;
+ m_peakTextureStagingBufferApproxByteSize = qMax(m_peakTextureStagingBufferApproxByteSize, m_totalTextureStagingBufferApproxByteSize);
+ emit texturesTouched();
+ }
+ }
+ }
+ break;
+ case QRhiProfiler::ReleaseTextureStagingArea:
+ {
+ qint64 slot = -1;
+ for (const Parser::Event::Param &p : e.params) {
+ if (p.key == QByteArrayLiteral("slot"))
+ slot = p.intValue;
+ }
+ if (slot >= 0 && slot < MAX_STAGING_SLOTS) {
+ auto it = m_textures.find(e.resource);
+ if (it != m_textures.end()) {
+ m_totalTextureStagingBufferApproxByteSize -= it->stagingExtraSize[slot];
+ it->stagingExtraSize[slot] = 0;
+ emit texturesTouched();
+ }
+ }
+ }
+ break;
+
+ case QRhiProfiler::ResizeSwapChain:
+ {
+ auto it = m_swapchains.find(e.resource);
+ if (it != m_swapchains.end())
+ m_totalSwapChainApproxByteSize -= it->approxByteSize;
+
+ SwapChain s;
+ s.lastTimestamp = e.timestamp;
+ s.resourceName = e.resourceName;
+ // width,1280,height,720,buffer_count,2,msaa_buffer_count,0,effective_sample_count,1,approx_total_byte_size,7372800
+ for (const Parser::Event::Param &p : e.params) {
+ if (p.key == QByteArrayLiteral("approx_total_byte_size"))
+ s.approxByteSize = p.intValue;
+ }
+ m_totalSwapChainApproxByteSize += s.approxByteSize;
+ m_peakSwapChainApproxByteSize = qMax(m_peakSwapChainApproxByteSize, m_totalSwapChainApproxByteSize);
+ m_swapchains.insert(e.resource, s);
+ emit swapchainsTouched();
+ }
+ break;
+ case QRhiProfiler::ReleaseSwapChain:
+ {
+ auto it = m_swapchains.find(e.resource);
+ if (it != m_swapchains.end()) {
+ m_totalSwapChainApproxByteSize -= it->approxByteSize;
+ m_swapchains.erase(it);
+ emit swapchainsTouched();
+ }
+ }
+ break;
+
+ case QRhiProfiler::GpuFrameTime:
+ {
+ // Fmin_ms_gpu_frame_time,0.15488,Fmax_ms_gpu_frame_time,0.494592,Favg_ms_gpu_frame_time,0.33462
+ for (const Parser::Event::Param &p : e.params) {
+ if (p.key == QByteArrayLiteral("min_ms_gpu_frame_time"))
+ m_lastGpuFrameTime.minTime = p.floatValue;
+ else if (p.key == QByteArrayLiteral("max_ms_gpu_frame_time"))
+ m_lastGpuFrameTime.maxTime = p.floatValue;
+ else if (p.key == QByteArrayLiteral("avg_ms_gpu_frame_time"))
+ m_lastGpuFrameTime.avgTime = p.floatValue;
+ }
+ emit gpuFrameTimeTouched();
+ }
+ break;
+ case QRhiProfiler::FrameToFrameTime:
+ {
+ // frames_since_resize,121,min_ms_frame_delta,9,max_ms_frame_delta,33,Favg_ms_frame_delta,16.1167
+ for (const Parser::Event::Param &p : e.params) {
+ if (p.key == QByteArrayLiteral("frames_since_resize"))
+ m_lastFrameTime.framesSinceResize = p.intValue;
+ else if (p.key == QByteArrayLiteral("min_ms_frame_delta"))
+ m_lastFrameTime.minDelta = p.intValue;
+ else if (p.key == QByteArrayLiteral("max_ms_frame_delta"))
+ m_lastFrameTime.maxDelta = p.intValue;
+ else if (p.key == QByteArrayLiteral("avg_ms_frame_delta"))
+ m_lastFrameTime.avgDelta = p.floatValue;
+ }
+ emit frameTimeTouched();
+ }
+ break;
+
+ case QRhiProfiler::GpuMemAllocStats:
+ {
+ // real_alloc_count,2,sub_alloc_count,154,total_size,10752,unused_size,50320896
+ for (const Parser::Event::Param &p : e.params) {
+ if (p.key == QByteArrayLiteral("real_alloc_count"))
+ m_lastGpuMemAllocStats.realAllocCount = p.intValue;
+ else if (p.key == QByteArrayLiteral("sub_alloc_count"))
+ m_lastGpuMemAllocStats.subAllocCount = p.intValue;
+ else if (p.key == QByteArrayLiteral("total_size"))
+ m_lastGpuMemAllocStats.totalSize = p.intValue;
+ else if (p.key == QByteArrayLiteral("unused_size"))
+ m_lastGpuMemAllocStats.unusedSize = p.intValue;
+ }
+ emit gpuMemAllocStatsTouched();
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+
+class Server : public QTcpServer
+{
+ Q_OBJECT
+
+protected:
+ void incomingConnection(qintptr socketDescriptor) override;
+
+signals:
+ void clientConnected();
+ void clientDisconnected();
+ void receiveStarted();
+ void lineReceived(const QByteArray &line);
+
+private:
+ bool m_valid = false;
+ QTcpSocket m_socket;
+ QByteArray m_buf;
+};
+
+void Server::incomingConnection(qintptr socketDescriptor)
+{
+ if (m_valid)
+ return;
+
+ m_socket.setSocketDescriptor(socketDescriptor);
+ m_valid = true;
+ emit clientConnected();
+ connect(&m_socket, &QAbstractSocket::readyRead, this, [this] {
+ bool receiveStartedSent = false;
+ m_buf += m_socket.readAll();
+ while (m_buf.contains('\n')) {
+ const int lfpos = m_buf.indexOf('\n');
+ const QByteArray line = m_buf.left(lfpos).trimmed();
+ m_buf = m_buf.mid(lfpos + 1);
+ if (!receiveStartedSent) {
+ receiveStartedSent = true;
+ emit receiveStarted();
+ }
+ emit lineReceived(line);
+ }
+ });
+ connect(&m_socket, &QAbstractSocket::disconnected, this, [this] {
+ if (m_valid) {
+ m_valid = false;
+ emit clientDisconnected();
+ }
+ });
+}
+
+int main(int argc, char **argv)
+{
+ QApplication app(argc, argv);
+
+ Tracker tracker;
+ Parser parser;
+ QObject::connect(&parser, &Parser::eventReceived, &tracker, &Tracker::handleEvent);
+
+ Server server;
+ if (!server.listen(QHostAddress::Any, 30667))
+ qFatal("Failed to start server: %s", qPrintable(server.errorString()));
+
+ QVBoxLayout *layout = new QVBoxLayout;
+
+ QLabel *infoLabel = new QLabel(QLatin1String("<i>Launch a Qt Quick application with QSG_RHI_PROFILE=1 and QSG_RHI_PROFILE_HOST set to the IP address.<br>"
+ "(resource memory usage reporting works best with the Vulkan backend)</i>"));
+ layout->addWidget(infoLabel);
+
+ QGroupBox *groupBox = new QGroupBox(QLatin1String("RHI statistics"));
+ QVBoxLayout *groupLayout = new QVBoxLayout;
+
+ QLabel *buffersLabel = new QLabel;
+ QObject::connect(&tracker, &Tracker::buffersTouched, buffersLabel, [buffersLabel, &tracker] {
+ const QString msg = QString::asprintf("%d buffers with ca. %lld bytes of current memory (sub)allocations (peak %lld) + %lld bytes of known staging buffers (peak %lld)",
+ tracker.m_buffers.count(),
+ tracker.m_totalBufferApproxByteSize, tracker.m_peakBufferApproxByteSize,
+ tracker.m_totalStagingBufferApproxByteSize, tracker.m_peakStagingBufferApproxByteSize);
+ buffersLabel->setText(msg);
+ });
+ groupLayout->addWidget(buffersLabel);
+
+ QLabel *texturesLabel = new QLabel;
+ QObject::connect(&tracker, &Tracker::texturesTouched, texturesLabel, [texturesLabel, &tracker] {
+ const QString msg = QString::asprintf("%d textures with ca. %lld bytes of current memory (sub)allocations (peak %lld) + %lld bytes of known staging buffers (peak %lld)",
+ tracker.m_textures.count(),
+ tracker.m_totalTextureApproxByteSize, tracker.m_peakTextureApproxByteSize,
+ tracker.m_totalTextureStagingBufferApproxByteSize, tracker.m_peakTextureStagingBufferApproxByteSize);
+ texturesLabel->setText(msg);
+ });
+ groupLayout->addWidget(texturesLabel);
+
+ QLabel *swapchainsLabel = new QLabel;
+ QObject::connect(&tracker, &Tracker::swapchainsTouched, swapchainsLabel, [swapchainsLabel, &tracker] {
+ const QString msg = QString::asprintf("Estimated total swapchain color buffer size is %lld bytes (peak %lld)",
+ tracker.m_totalSwapChainApproxByteSize, tracker.m_peakSwapChainApproxByteSize);
+ swapchainsLabel->setText(msg);
+ });
+ groupLayout->addWidget(swapchainsLabel);
+
+ QLabel *frameTimeLabel = new QLabel;
+ QObject::connect(&tracker, &Tracker::frameTimeTouched, frameTimeLabel, [frameTimeLabel, &tracker] {
+ const QString msg = QString::asprintf("Frames since resize %lld Frame delta min %d ms max %d ms avg %f ms",
+ tracker.m_lastFrameTime.framesSinceResize,
+ tracker.m_lastFrameTime.minDelta,
+ tracker.m_lastFrameTime.maxDelta,
+ tracker.m_lastFrameTime.avgDelta);
+ frameTimeLabel->setText(msg);
+ });
+ groupLayout->addWidget(frameTimeLabel);
+
+ QLabel *gpuFrameTimeLabel = new QLabel;
+ QObject::connect(&tracker, &Tracker::gpuFrameTimeTouched, gpuFrameTimeLabel, [gpuFrameTimeLabel, &tracker] {
+ const QString msg = QString::asprintf("GPU frame time min %f ms max %f ms avg %f ms",
+ tracker.m_lastGpuFrameTime.minTime,
+ tracker.m_lastGpuFrameTime.maxTime,
+ tracker.m_lastGpuFrameTime.avgTime);
+ gpuFrameTimeLabel->setText(msg);
+ });
+ groupLayout->addWidget(gpuFrameTimeLabel);
+
+ QLabel *gpuMemAllocStatsLabel = new QLabel;
+ QObject::connect(&tracker, &Tracker::gpuMemAllocStatsTouched, gpuMemAllocStatsLabel, [gpuMemAllocStatsLabel, &tracker] {
+ const QString msg = QString::asprintf("GPU memory allocator status: %lld real allocations %lld sub-allocations %lld total bytes %lld unused bytes",
+ tracker.m_lastGpuMemAllocStats.realAllocCount,
+ tracker.m_lastGpuMemAllocStats.subAllocCount,
+ tracker.m_lastGpuMemAllocStats.totalSize,
+ tracker.m_lastGpuMemAllocStats.unusedSize);
+ gpuMemAllocStatsLabel->setText(msg);
+ });
+ groupLayout->addWidget(gpuMemAllocStatsLabel);
+
+ groupBox->setLayout(groupLayout);
+ layout->addWidget(groupBox);
+
+ QTextEdit *rawLog = new QTextEdit;
+ rawLog->setReadOnly(true);
+ layout->addWidget(rawLog);
+
+ QObject::connect(&server, &Server::clientConnected, rawLog, [rawLog] {
+ rawLog->append(QLatin1String("\nCONNECTED\n"));
+ });
+ QObject::connect(&server, &Server::clientDisconnected, rawLog, [rawLog, &tracker] {
+ rawLog->append(QLatin1String("\nDISCONNECTED\n"));
+ tracker.reset();
+ });
+ QObject::connect(&server, &Server::receiveStarted, rawLog, [rawLog] {
+ rawLog->setFontItalic(true);
+ rawLog->append(QLatin1String("[") + QTime::currentTime().toString() + QLatin1String("]"));
+ rawLog->setFontItalic(false);
+ });
+
+ QObject::connect(&server, &Server::lineReceived, rawLog, [rawLog, &parser](const QByteArray &line) {
+ rawLog->append(QString::fromUtf8(line));
+ parser.feed(line);
+ });
+
+ QWidget w;
+ w.resize(800, 600);
+ w.setLayout(layout);
+ w.show();
+
+ return app.exec();
+}
+
+#include "qrhiprof.moc"
diff --git a/tests/manual/rhi/qrhiprof/qrhiprof.pro b/tests/manual/rhi/qrhiprof/qrhiprof.pro
new file mode 100644
index 0000000000..2939dd7ce2
--- /dev/null
+++ b/tests/manual/rhi/qrhiprof/qrhiprof.pro
@@ -0,0 +1,6 @@
+TEMPLATE = app
+
+QT += network widgets gui-private
+
+SOURCES = \
+ qrhiprof.cpp
diff --git a/tests/manual/rhi/rhi.pro b/tests/manual/rhi/rhi.pro
new file mode 100644
index 0000000000..1a65f84a89
--- /dev/null
+++ b/tests/manual/rhi/rhi.pro
@@ -0,0 +1,22 @@
+TEMPLATE = subdirs
+
+SUBDIRS += \
+ hellominimalcrossgfxtriangle \
+ compressedtexture_bc1 \
+ compressedtexture_bc1_subupload \
+ texuploads \
+ msaatexture \
+ msaarenderbuffer \
+ cubemap \
+ multiwindow \
+ multiwindow_threaded \
+ triquadcube \
+ offscreen \
+ floattexture \
+ mrt \
+ shadowmap
+
+qtConfig(widgets) {
+ SUBDIRS += \
+ qrhiprof
+}
diff --git a/tests/manual/rhi/shadowmap/buildshaders.bat b/tests/manual/rhi/shadowmap/buildshaders.bat
new file mode 100644
index 0000000000..7b84cc0952
--- /dev/null
+++ b/tests/manual/rhi/shadowmap/buildshaders.bat
@@ -0,0 +1,4 @@
+qsb --glsl "120,300 es" --hlsl 50 --msl 12 -c shadowmap.vert -o shadowmap.vert.qsb
+qsb --glsl "120,300 es" --hlsl 50 --msl 12 -c shadowmap.frag -o shadowmap.frag.qsb
+qsb --glsl "120,300 es" --hlsl 50 --msl 12 -c main.vert -o main.vert.qsb
+qsb --glsl "120,300 es" --hlsl 50 --msl 12 -c main.frag -o main.frag.qsb
diff --git a/tests/manual/rhi/shadowmap/buildshaders.sh b/tests/manual/rhi/shadowmap/buildshaders.sh
new file mode 100755
index 0000000000..c4d17841e6
--- /dev/null
+++ b/tests/manual/rhi/shadowmap/buildshaders.sh
@@ -0,0 +1,4 @@
+qsb --glsl "120,300 es" --hlsl 50 --msl 12 shadowmap.vert -o shadowmap.vert.qsb
+qsb --glsl "120,300 es" --hlsl 50 --msl 12 shadowmap.frag -o shadowmap.frag.qsb
+qsb --glsl "120,300 es" --hlsl 50 --msl 12 main.vert -o main.vert.qsb
+qsb --glsl "120,300 es" --hlsl 50 --msl 12 main.frag -o main.frag.qsb
diff --git a/tests/manual/rhi/shadowmap/main.frag b/tests/manual/rhi/shadowmap/main.frag
new file mode 100644
index 0000000000..8fefe2bb42
--- /dev/null
+++ b/tests/manual/rhi/shadowmap/main.frag
@@ -0,0 +1,30 @@
+#version 440
+
+layout(location = 0) in vec4 vLCVertPos;
+
+layout(location = 0) out vec4 fragColor;
+
+layout(binding = 1) uniform sampler2DShadow shadowMap;
+
+layout(std140, binding = 0) uniform buf {
+ mat4 mvp;
+ mat4 lightMvp;
+ mat4 shadowBias;
+ int useShadow;
+} ubuf;
+
+void main()
+{
+ vec4 adjustedLcVertPos = vLCVertPos;
+ adjustedLcVertPos.z -= 0.0001; // bias to avoid acne
+
+ // no textureProj, that seems to end up not doing the perspective divide for z (?)
+ vec3 v = adjustedLcVertPos.xyz / adjustedLcVertPos.w;
+ float sc = texture(shadowMap, v); // sampler is comparison enabled so compares to z
+
+ float shadowFactor = 0.2;
+ if (sc > 0 || ubuf.useShadow == 0)
+ shadowFactor = 1.0;
+
+ fragColor = vec4(0.5, 0.3 + ubuf.useShadow * 0.2, 0.7, 1.0) * shadowFactor;
+}
diff --git a/tests/manual/rhi/shadowmap/main.frag.qsb b/tests/manual/rhi/shadowmap/main.frag.qsb
new file mode 100644
index 0000000000..3001908b85
--- /dev/null
+++ b/tests/manual/rhi/shadowmap/main.frag.qsb
Binary files differ
diff --git a/tests/manual/rhi/shadowmap/main.vert b/tests/manual/rhi/shadowmap/main.vert
new file mode 100644
index 0000000000..9d6cade634
--- /dev/null
+++ b/tests/manual/rhi/shadowmap/main.vert
@@ -0,0 +1,20 @@
+#version 440
+
+layout(location = 0) in vec4 position;
+
+layout(std140, binding = 0) uniform buf {
+ mat4 mvp;
+ mat4 lightMvp;
+ mat4 shadowBias;
+ int useShadow;
+} ubuf;
+
+out gl_PerVertex { vec4 gl_Position; };
+
+layout(location = 0) out vec4 vLCVertPos;
+
+void main()
+{
+ vLCVertPos = ubuf.shadowBias * ubuf.lightMvp * position;
+ gl_Position = ubuf.mvp * position;
+}
diff --git a/tests/manual/rhi/shadowmap/main.vert.qsb b/tests/manual/rhi/shadowmap/main.vert.qsb
new file mode 100644
index 0000000000..8b81a93c00
--- /dev/null
+++ b/tests/manual/rhi/shadowmap/main.vert.qsb
Binary files differ
diff --git a/tests/manual/rhi/shadowmap/shadowmap.cpp b/tests/manual/rhi/shadowmap/shadowmap.cpp
new file mode 100644
index 0000000000..c31674a520
--- /dev/null
+++ b/tests/manual/rhi/shadowmap/shadowmap.cpp
@@ -0,0 +1,304 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "../shared/examplefw.h"
+#include "../shared/cube.h"
+
+// Depth texture / shadow sampler / shadow map example.
+// Not available on GLES 2.0.
+
+static float quadVertexData[] =
+{ // Y up, CCW, x-y-z
+ -0.5f, 0.5f, 0.0f,
+ -0.5f, -0.5f, 0.0f,
+ 0.5f, -0.5f, 0.0f,
+ 0.5f, 0.5f, 0.0f,
+};
+
+static quint16 quadIndexData[] =
+{
+ 0, 1, 2, 0, 2, 3
+};
+
+struct {
+ QVector<QRhiResource *> releasePool;
+ QRhiBuffer *vbuf = nullptr;
+ QRhiBuffer *ibuf = nullptr;
+ QRhiBuffer *ubuf = nullptr;
+ QRhiSampler *shadowSampler = nullptr;
+ QRhiShaderResourceBindings *srb = nullptr;
+ QRhiGraphicsPipeline *ps = nullptr;
+ QRhiResourceUpdateBatch *initialUpdates = nullptr;
+ QMatrix4x4 winProj;
+ float cubeRot = 0;
+
+ QRhiTextureRenderTarget *rt = nullptr;
+ QRhiRenderPassDescriptor *rtRp = nullptr;
+ QRhiTexture *shadowMap = nullptr;
+ QRhiShaderResourceBindings *shadowSrb = nullptr;
+ QRhiGraphicsPipeline *shadowPs = nullptr;
+} d;
+
+const int UBLOCK_SIZE = 64 * 3 + 4;
+const int SHADOW_UBLOCK_SIZE = 64 * 1;
+const int UBUF_SLOTS = 4; // 2 objects * 2 passes with different cameras
+
+void Window::customInit()
+{
+ if (!m_r->isTextureFormatSupported(QRhiTexture::D32F))
+ qFatal("Depth texture is not supported");
+
+ d.vbuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(quadVertexData) + sizeof(cube));
+ d.vbuf->build();
+ d.releasePool << d.vbuf;
+
+ d.ibuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(quadIndexData));
+ d.ibuf->build();
+ d.releasePool << d.ibuf;
+
+ const int oneRoundedUniformBlockSize = m_r->ubufAligned(UBLOCK_SIZE);
+ d.ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, UBUF_SLOTS * oneRoundedUniformBlockSize);
+ d.ubuf->build();
+ d.releasePool << d.ubuf;
+
+ d.shadowMap = m_r->newTexture(QRhiTexture::D32F, QSize(1024, 1024), 1, QRhiTexture::RenderTarget);
+ d.releasePool << d.shadowMap;
+ d.shadowMap->build();
+
+ d.shadowSampler = m_r->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
+ QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
+ d.releasePool << d.shadowSampler;
+ d.shadowSampler->setTextureCompareOp(QRhiSampler::Less);
+ d.shadowSampler->build();
+
+ d.srb = m_r->newShaderResourceBindings();
+ d.releasePool << d.srb;
+ const QRhiShaderResourceBinding::StageFlags stages = QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage;
+ d.srb->setBindings({ QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, stages, d.ubuf, UBLOCK_SIZE),
+ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, d.shadowMap, d.shadowSampler) });
+ d.srb->build();
+
+ d.ps = m_r->newGraphicsPipeline();
+ d.releasePool << d.ps;
+ d.ps->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, getShader(QLatin1String(":/main.vert.qsb")) },
+ { QRhiGraphicsShaderStage::Fragment, getShader(QLatin1String(":/main.frag.qsb")) }
+ });
+ d.ps->setDepthTest(true);
+ d.ps->setDepthWrite(true);
+ // fits both the quad and cube vertex data
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 3 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float3, 0 },
+ });
+ d.ps->setVertexInputLayout(inputLayout);
+ d.ps->setShaderResourceBindings(d.srb);
+ d.ps->setRenderPassDescriptor(m_rp);
+ d.ps->build();
+
+ d.initialUpdates = m_r->nextResourceUpdateBatch();
+ d.initialUpdates->uploadStaticBuffer(d.vbuf, 0, sizeof(quadVertexData), quadVertexData);
+ d.initialUpdates->uploadStaticBuffer(d.vbuf, sizeof(quadVertexData), sizeof(cube), cube);
+ d.initialUpdates->uploadStaticBuffer(d.ibuf, quadIndexData);
+
+ QRhiTextureRenderTargetDescription rtDesc;
+ rtDesc.setDepthTexture(d.shadowMap);
+ d.rt = m_r->newTextureRenderTarget(rtDesc);
+ d.releasePool << d.rt;
+ d.rtRp = d.rt->newCompatibleRenderPassDescriptor();
+ d.releasePool << d.rtRp;
+ d.rt->setRenderPassDescriptor(d.rtRp);
+ d.rt->build();
+
+ d.shadowSrb = m_r->newShaderResourceBindings();
+ d.releasePool << d.shadowSrb;
+ d.shadowSrb->setBindings({ QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, stages, d.ubuf, SHADOW_UBLOCK_SIZE) });
+ d.shadowSrb->build();
+
+ d.shadowPs = m_r->newGraphicsPipeline();
+ d.releasePool << d.shadowPs;
+ d.shadowPs->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, getShader(QLatin1String(":/shadowmap.vert.qsb")) },
+ { QRhiGraphicsShaderStage::Fragment, getShader(QLatin1String(":/shadowmap.frag.qsb")) }
+ });
+ d.shadowPs->setDepthTest(true);
+ d.shadowPs->setDepthWrite(true);
+ inputLayout.setBindings({
+ { 3 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float3, 0 }
+ });
+ d.shadowPs->setVertexInputLayout(inputLayout);
+ d.shadowPs->setShaderResourceBindings(d.shadowSrb);
+ d.shadowPs->setRenderPassDescriptor(d.rtRp);
+ d.shadowPs->build();
+}
+
+void Window::customRelease()
+{
+ qDeleteAll(d.releasePool);
+ d.releasePool.clear();
+}
+
+static void enqueueScene(QRhiCommandBuffer *cb, QRhiShaderResourceBindings *srb, int oneRoundedUniformBlockSize, int firstUbufSlot)
+{
+ QRhiCommandBuffer::DynamicOffset ubufOffset(0, quint32(firstUbufSlot * oneRoundedUniformBlockSize));
+ // draw the ground (the quad)
+ cb->setShaderResources(srb, 1, &ubufOffset);
+ QRhiCommandBuffer::VertexInput vbufBinding(d.vbuf, 0);
+ cb->setVertexInput(0, 1, &vbufBinding, d.ibuf, 0, QRhiCommandBuffer::IndexUInt16);
+ cb->drawIndexed(6);
+
+ // Draw the object (the cube). Both vertex and uniform data are in the same
+ // buffer, right after the quad's.
+ ubufOffset.second += oneRoundedUniformBlockSize;
+ cb->setShaderResources(srb, 1, &ubufOffset);
+ vbufBinding.second = sizeof(quadVertexData);
+ cb->setVertexInput(0, 1, &vbufBinding);
+ cb->draw(36);
+}
+
+void Window::customRender()
+{
+ const QSize outputSizeInPixels = m_sc->currentPixelSize();
+ QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
+ QRhiResourceUpdateBatch *u = m_r->nextResourceUpdateBatch();
+ if (d.initialUpdates) {
+ u->merge(d.initialUpdates);
+ d.initialUpdates->release();
+ d.initialUpdates = nullptr;
+ }
+
+ const int oneRoundedUniformBlockSize = m_r->ubufAligned(UBLOCK_SIZE);
+
+ QMatrix4x4 shadowBias;
+ // fill it in column-major order to keep our sanity (ctor would take row-major)
+ float *sbp = shadowBias.data();
+ if (m_r->isClipDepthZeroToOne()) {
+ // convert x, y [-1, 1] -> [0, 1]
+ *sbp++ = 0.5f; *sbp++ = 0.0f; *sbp++ = 0.0f; *sbp++ = 0.0f;
+ *sbp++ = 0.0f; *sbp++ = m_r->isYUpInNDC() ? -0.5f : 0.5f; *sbp++ = 0.0f; *sbp++ = 0.0f;
+ *sbp++ = 0.0f; *sbp++ = 0.0f; *sbp++ = 1.0f; *sbp++ = 0.0f;
+ *sbp++ = 0.5f; *sbp++ = 0.5f; *sbp++ = 0.0f; *sbp++ = 1.0f;
+ } else {
+ // convert x, y, z [-1, 1] -> [0, 1]
+ *sbp++ = 0.5f; *sbp++ = 0.0f; *sbp++ = 0.0f; *sbp++ = 0.0f;
+ *sbp++ = 0.0f; *sbp++ = 0.5f; *sbp++ = 0.0f; *sbp++ = 0.0f;
+ *sbp++ = 0.0f; *sbp++ = 0.0f; *sbp++ = 0.5f; *sbp++ = 0.0f;
+ *sbp++ = 0.5f; *sbp++ = 0.5f; *sbp++ = 0.5f; *sbp++ = 1.0f;
+ }
+
+ const QVector3D lightPos(5, 10, 10);
+ QMatrix4x4 lightViewProj = m_r->clipSpaceCorrMatrix();
+ lightViewProj.perspective(45.0f, 1, 0.1f, 100.0f);
+ lightViewProj.lookAt(lightPos, QVector3D(0, 0, 0), QVector3D(0, 1, 0));
+
+ // uniform data for the ground
+ if (d.winProj != m_proj) {
+ d.winProj = m_proj;
+
+ QMatrix4x4 m;
+ m.scale(4.0f);
+ m.rotate(-60, 1, 0, 0);
+
+ // for the main pass
+ const QMatrix4x4 mvp = m_proj * m; // m_proj is in fact projection * view
+ u->updateDynamicBuffer(d.ubuf, 0, 64, mvp.constData());
+ const QMatrix4x4 shadowMvp = lightViewProj * m;
+ u->updateDynamicBuffer(d.ubuf, 64, 64, shadowMvp.constData());
+ u->updateDynamicBuffer(d.ubuf, 128, 64, shadowBias.constData());
+ qint32 useShadows = 1;
+ u->updateDynamicBuffer(d.ubuf, 192, 4, &useShadows);
+
+ // for the shadow pass
+ u->updateDynamicBuffer(d.ubuf, 2 * oneRoundedUniformBlockSize, 64, shadowMvp.constData());
+ }
+
+ // uniform data for the rotating cube
+ QMatrix4x4 m;
+ m.translate(0, 0.5f, 2);
+ m.scale(0.2f);
+ m.rotate(d.cubeRot, 0, 1, 0);
+ m.rotate(45, 1, 0, 0);
+ d.cubeRot += 1;
+
+ // for the main pass
+ const QMatrix4x4 mvp = m_proj * m;
+ u->updateDynamicBuffer(d.ubuf, oneRoundedUniformBlockSize, 64, mvp.constData());
+ const QMatrix4x4 shadowMvp = lightViewProj * m;
+ u->updateDynamicBuffer(d.ubuf, oneRoundedUniformBlockSize + 64, 64, shadowMvp.constData());
+ u->updateDynamicBuffer(d.ubuf, oneRoundedUniformBlockSize + 128, 64, shadowBias.constData());
+ qint32 useShadows = 0;
+ u->updateDynamicBuffer(d.ubuf, oneRoundedUniformBlockSize + 192, 4, &useShadows);
+
+ // for the shadow pass
+ u->updateDynamicBuffer(d.ubuf, 3 * oneRoundedUniformBlockSize, 64, shadowMvp.constData());
+
+ cb->resourceUpdate(u);
+
+ // shadow pass
+ const QSize shadowMapSize = d.shadowMap->pixelSize();
+ cb->beginPass(d.rt, QColor(), { 1.0f, 0 });
+ cb->setGraphicsPipeline(d.shadowPs);
+ cb->setViewport({ 0, 0, float(shadowMapSize.width()), float(shadowMapSize.height()) });
+ enqueueScene(cb, d.shadowSrb, oneRoundedUniformBlockSize, 2);
+ cb->endPass();
+
+ // main pass
+ cb->beginPass(m_sc->currentFrameRenderTarget(), QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f), { 1.0f, 0 });
+ cb->setGraphicsPipeline(d.ps);
+ cb->setViewport({ 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) });
+ enqueueScene(cb, d.srb, oneRoundedUniformBlockSize, 0);
+ cb->endPass();
+}
diff --git a/tests/manual/rhi/shadowmap/shadowmap.frag b/tests/manual/rhi/shadowmap/shadowmap.frag
new file mode 100644
index 0000000000..fa868f31a2
--- /dev/null
+++ b/tests/manual/rhi/shadowmap/shadowmap.frag
@@ -0,0 +1,5 @@
+#version 440
+
+void main()
+{
+}
diff --git a/tests/manual/rhi/shadowmap/shadowmap.frag.qsb b/tests/manual/rhi/shadowmap/shadowmap.frag.qsb
new file mode 100644
index 0000000000..3cad114cf4
--- /dev/null
+++ b/tests/manual/rhi/shadowmap/shadowmap.frag.qsb
Binary files differ
diff --git a/tests/manual/rhi/shadowmap/shadowmap.pro b/tests/manual/rhi/shadowmap/shadowmap.pro
new file mode 100644
index 0000000000..baf740927c
--- /dev/null
+++ b/tests/manual/rhi/shadowmap/shadowmap.pro
@@ -0,0 +1,8 @@
+TEMPLATE = app
+
+QT += gui-private
+
+SOURCES = \
+ shadowmap.cpp
+
+RESOURCES = shadowmap.qrc
diff --git a/tests/manual/rhi/shadowmap/shadowmap.qrc b/tests/manual/rhi/shadowmap/shadowmap.qrc
new file mode 100644
index 0000000000..8256d5ca25
--- /dev/null
+++ b/tests/manual/rhi/shadowmap/shadowmap.qrc
@@ -0,0 +1,8 @@
+<!DOCTYPE RCC><RCC version="1.0">
+ <qresource>
+ <file>shadowmap.vert.qsb</file>
+ <file>shadowmap.frag.qsb</file>
+ <file>main.vert.qsb</file>
+ <file>main.frag.qsb</file>
+</qresource>
+</RCC>
diff --git a/tests/manual/rhi/shadowmap/shadowmap.vert b/tests/manual/rhi/shadowmap/shadowmap.vert
new file mode 100644
index 0000000000..72b5370aec
--- /dev/null
+++ b/tests/manual/rhi/shadowmap/shadowmap.vert
@@ -0,0 +1,14 @@
+#version 440
+
+layout(location = 0) in vec4 position;
+
+layout(std140, binding = 0) uniform buf {
+ mat4 mvp;
+} ubuf;
+
+out gl_PerVertex { vec4 gl_Position; };
+
+void main()
+{
+ gl_Position = ubuf.mvp * position;
+}
diff --git a/tests/manual/rhi/shadowmap/shadowmap.vert.qsb b/tests/manual/rhi/shadowmap/shadowmap.vert.qsb
new file mode 100644
index 0000000000..37a5e6ecbf
--- /dev/null
+++ b/tests/manual/rhi/shadowmap/shadowmap.vert.qsb
Binary files differ
diff --git a/tests/manual/rhi/shared/OpenfootageNET_fieldairport-512.hdr b/tests/manual/rhi/shared/OpenfootageNET_fieldairport-512.hdr
new file mode 100644
index 0000000000..abdebdb74b
--- /dev/null
+++ b/tests/manual/rhi/shared/OpenfootageNET_fieldairport-512.hdr
Binary files differ
diff --git a/tests/manual/rhi/shared/buildshaders.bat b/tests/manual/rhi/shared/buildshaders.bat
new file mode 100644
index 0000000000..dde5f7b3d2
--- /dev/null
+++ b/tests/manual/rhi/shared/buildshaders.bat
@@ -0,0 +1,5 @@
+qsb --glsl "100 es,120" --hlsl 50 --msl 12 -c color.vert -o color.vert.qsb
+qsb --glsl "100 es,120" --hlsl 50 --msl 12 -c color.frag -o color.frag.qsb
+qsb --glsl "100 es,120" --hlsl 50 --msl 12 -c texture.vert -o texture.vert.qsb
+qsb --glsl "100 es,120" --hlsl 50 --msl 12 -c texture.frag -o texture.frag.qsb
+qsb --hlsl 50 --msl 12 -c texture_ms4.frag -o texture_ms4.frag.qsb
diff --git a/tests/manual/rhi/shared/bwqt224_64.png b/tests/manual/rhi/shared/bwqt224_64.png
new file mode 100644
index 0000000000..8020cbb2a8
--- /dev/null
+++ b/tests/manual/rhi/shared/bwqt224_64.png
Binary files differ
diff --git a/tests/manual/rhi/shared/bwqt224_64_nomips.dds b/tests/manual/rhi/shared/bwqt224_64_nomips.dds
new file mode 100644
index 0000000000..393cfec9db
--- /dev/null
+++ b/tests/manual/rhi/shared/bwqt224_64_nomips.dds
Binary files differ
diff --git a/tests/manual/rhi/shared/color.frag b/tests/manual/rhi/shared/color.frag
new file mode 100644
index 0000000000..bb94a5a605
--- /dev/null
+++ b/tests/manual/rhi/shared/color.frag
@@ -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;
+} ubuf;
+
+void main()
+{
+ fragColor = vec4(v_color * ubuf.opacity, ubuf.opacity);
+}
diff --git a/tests/manual/rhi/shared/color.frag.qsb b/tests/manual/rhi/shared/color.frag.qsb
new file mode 100644
index 0000000000..3a965682eb
--- /dev/null
+++ b/tests/manual/rhi/shared/color.frag.qsb
Binary files differ
diff --git a/tests/manual/rhi/shared/color.vert b/tests/manual/rhi/shared/color.vert
new file mode 100644
index 0000000000..43af543f44
--- /dev/null
+++ b/tests/manual/rhi/shared/color.vert
@@ -0,0 +1,19 @@
+#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;
+} ubuf;
+
+out gl_PerVertex { vec4 gl_Position; };
+
+void main()
+{
+ v_color = color;
+ gl_Position = ubuf.mvp * position;
+}
diff --git a/tests/manual/rhi/shared/color.vert.qsb b/tests/manual/rhi/shared/color.vert.qsb
new file mode 100644
index 0000000000..e34eae79a0
--- /dev/null
+++ b/tests/manual/rhi/shared/color.vert.qsb
Binary files differ
diff --git a/tests/manual/rhi/shared/cube.h b/tests/manual/rhi/shared/cube.h
new file mode 100644
index 0000000000..252e80c56c
--- /dev/null
+++ b/tests/manual/rhi/shared/cube.h
@@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2015-2016 The Khronos Group Inc.
+ * Copyright (c) 2015-2016 Valve Corporation
+ * Copyright (c) 2015-2016 LunarG, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * Author: Chia-I Wu <olv@lunarg.com>
+ * Author: Courtney Goeltzenleuchter <courtney@LunarG.com>
+ * Author: Ian Elliott <ian@LunarG.com>
+ * Author: Ian Elliott <ianelliott@google.com>
+ * Author: Jon Ashburn <jon@lunarg.com>
+ * Author: Gwan-gyeong Mun <elongbug@gmail.com>
+ * Author: Tony Barbour <tony@LunarG.com>
+ * Author: Bill Hollings <bill.hollings@brenwill.com>
+ */
+
+#ifndef CUBE_H
+#define CUBE_H
+
+// clang-format off
+static const float cube[] = {
+ -1.0f,-1.0f,-1.0f, // -X side
+ -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 side
+ 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 side
+ 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 side
+ -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 side
+ 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 side
+ -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,
+
+ 0.0f, 1.0f, // -X side
+ 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 side
+ 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 side
+ 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 side
+ 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 side
+ 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 side
+ 0.0f, 1.0f,
+ 1.0f, 0.0f,
+ 0.0f, 1.0f,
+ 1.0f, 1.0f,
+ 1.0f, 0.0f,
+};
+// clang-format on
+
+#endif
diff --git a/tests/manual/rhi/shared/dds_bc1.h b/tests/manual/rhi/shared/dds_bc1.h
new file mode 100644
index 0000000000..3c98a94cfc
--- /dev/null
+++ b/tests/manual/rhi/shared/dds_bc1.h
@@ -0,0 +1,137 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+static const quint32 DDS_MAGIC = 0x20534444; // 'DDS '
+static const quint32 DDS_FOURCC = 4;
+
+#define FOURCC(c0, c1, c2, c3) ((c0) | ((c1) << 8) | ((c2) << 16) | ((c3 << 24)))
+
+struct DDS_PIXELFORMAT {
+ quint32 size;
+ quint32 flags;
+ quint32 fourCC;
+ quint32 rgbBitCount;
+ quint32 rBitMask;
+ quint32 gBitMask;
+ quint32 bBitMask;
+ quint32 aBitMask;
+};
+
+struct DDS_HEADER {
+ quint32 size;
+ quint32 flags;
+ quint32 height;
+ quint32 width;
+ quint32 pitch;
+ quint32 depth;
+ quint32 mipMapCount;
+ quint32 reserved1[11];
+ DDS_PIXELFORMAT pixelFormat;
+ quint32 caps;
+ quint32 caps2;
+ quint32 caps3;
+ quint32 caps4;
+ quint32 reserved2;
+};
+
+static quint32 bc1size(const QSize &size)
+{
+ static const quint32 blockSize = 8; // 8 bytes for BC1
+ const quint32 bytesPerLine = qMax<quint32>(1, (size.width() + 3) / 4) * blockSize;
+ const quint32 ySize = qMax<quint32>(1, (size.height() + 3) / 4);
+ return bytesPerLine * ySize;
+}
+
+static QByteArrayList loadBC1(const QString &filename, QSize *size)
+{
+ QFile f(filename);
+ if (!f.open(QIODevice::ReadOnly)) {
+ qWarning("Failed to open %s", qPrintable(filename));
+ return QByteArrayList();
+ }
+
+ quint32 magic = 0;
+ f.read(reinterpret_cast<char *>(&magic), sizeof(magic));
+ if (magic != DDS_MAGIC) {
+ qWarning("%s is not a DDS file", qPrintable(filename));
+ return QByteArrayList();
+ }
+ DDS_HEADER header;
+ f.read(reinterpret_cast<char *>(&header), sizeof(header));
+ if (header.size != sizeof(DDS_HEADER)) {
+ qWarning("Invalid DDS header size");
+ return QByteArrayList();
+ }
+ if (header.pixelFormat.size != sizeof(DDS_PIXELFORMAT)) {
+ qWarning("Invalid DDS pixel format size");
+ return QByteArrayList();
+ }
+ if (!(header.pixelFormat.flags & DDS_FOURCC)) {
+ qWarning("Invalid DDS pixel format");
+ return QByteArrayList();
+ }
+ if (header.pixelFormat.fourCC != FOURCC('D', 'X', 'T', '1')) {
+ qWarning("Only DXT1 (BC1) is supported");
+ return QByteArrayList();
+ }
+
+ QByteArrayList data;
+ QSize sz(header.width, header.height);
+ for (quint32 level = 0; level < header.mipMapCount; ++level) {
+ data.append(f.read(bc1size(sz)));
+ sz.setWidth(qMax(1, sz.width() / 2));
+ sz.setHeight(qMax(1, sz.height() / 2));
+ }
+
+ if (size)
+ *size = QSize(header.width, header.height);
+
+ return data;
+}
diff --git a/tests/manual/rhi/shared/examplefw.h b/tests/manual/rhi/shared/examplefw.h
new file mode 100644
index 0000000000..450aa172c2
--- /dev/null
+++ b/tests/manual/rhi/shared/examplefw.h
@@ -0,0 +1,548 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+// Adapted from hellominimalcrossgfxtriangle with the frame rendering stripped out.
+// Include this file and implement Window::customInit, release and render.
+// Debug/validation layer is enabled for D3D and Vulkan.
+
+#include <QGuiApplication>
+#include <QCommandLineParser>
+#include <QWindow>
+#include <QPlatformSurfaceEvent>
+#include <QElapsedTimer>
+#include <QTimer>
+
+#include <QtGui/private/qshader_p.h>
+#include <QFile>
+#include <QtGui/private/qrhiprofiler_p.h>
+#include <QtGui/private/qrhinull_p.h>
+
+#ifndef QT_NO_OPENGL
+#include <QtGui/private/qrhigles2_p.h>
+#include <QOffscreenSurface>
+#endif
+
+#if QT_CONFIG(vulkan)
+#include <QLoggingCategory>
+#include <QtGui/private/qrhivulkan_p.h>
+#endif
+
+#ifdef Q_OS_WIN
+#include <QtGui/private/qrhid3d11_p.h>
+#endif
+
+#ifdef Q_OS_DARWIN
+#include <QtGui/private/qrhimetal_p.h>
+#endif
+
+QShader getShader(const QString &name)
+{
+ QFile f(name);
+ if (f.open(QIODevice::ReadOnly))
+ return QShader::fromSerialized(f.readAll());
+
+ return QShader();
+}
+
+enum GraphicsApi
+{
+ Null,
+ OpenGL,
+ Vulkan,
+ D3D11,
+ Metal
+};
+
+GraphicsApi graphicsApi;
+
+QString graphicsApiName()
+{
+ switch (graphicsApi) {
+ case Null:
+ return QLatin1String("Null (no output)");
+ case OpenGL:
+ return QLatin1String("OpenGL 2.x");
+ case Vulkan:
+ return QLatin1String("Vulkan");
+ case D3D11:
+ return QLatin1String("Direct3D 11");
+ case Metal:
+ return QLatin1String("Metal");
+ default:
+ break;
+ }
+ return QString();
+}
+
+QRhi::Flags rhiFlags = QRhi::EnableDebugMarkers;
+int sampleCount = 1;
+QRhiSwapChain::Flags scFlags = 0;
+QRhi::EndFrameFlags endFrameFlags = 0;
+
+class Window : public QWindow
+{
+public:
+ Window();
+ ~Window();
+
+protected:
+ void init();
+ void releaseResources();
+ void resizeSwapChain();
+ void releaseSwapChain();
+ void render();
+
+ void customInit();
+ void customRelease();
+ void customRender();
+
+ void exposeEvent(QExposeEvent *) override;
+ bool event(QEvent *) override;
+
+ bool m_running = false;
+ bool m_notExposed = false;
+ bool m_newlyExposed = false;
+
+ QRhi *m_r = nullptr;
+ bool m_hasSwapChain = false;
+ QRhiSwapChain *m_sc = nullptr;
+ QRhiRenderBuffer *m_ds = nullptr;
+ QRhiRenderPassDescriptor *m_rp = nullptr;
+
+ QMatrix4x4 m_proj;
+
+ QElapsedTimer m_timer;
+ int m_frameCount;
+
+#ifndef QT_NO_OPENGL
+ QOffscreenSurface *m_fallbackSurface = nullptr;
+#endif
+
+ friend int main(int, char**);
+};
+
+Window::Window()
+{
+ // Tell the platform plugin what we want.
+ switch (graphicsApi) {
+ case OpenGL:
+#if QT_CONFIG(opengl)
+ setSurfaceType(OpenGLSurface);
+ setFormat(QRhiGles2InitParams::adjustedFormat());
+#endif
+ break;
+ case Vulkan:
+ setSurfaceType(VulkanSurface);
+ break;
+ case D3D11:
+ setSurfaceType(OpenGLSurface); // not a typo
+ break;
+ case Metal:
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
+ setSurfaceType(MetalSurface);
+#endif
+ break;
+ default:
+ break;
+ }
+}
+
+Window::~Window()
+{
+ releaseResources();
+}
+
+void Window::exposeEvent(QExposeEvent *)
+{
+ // initialize and start rendering when the window becomes usable for graphics purposes
+ if (isExposed() && !m_running) {
+ m_running = true;
+ init();
+ resizeSwapChain();
+ }
+
+ 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_running)
+ m_notExposed = true;
+
+ // continue when exposed again and the surface has a valid size.
+ // note that the surface size can be (0, 0) even though size() reports a valid one...
+ if (isExposed() && m_running && 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();
+}
+
+bool Window::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);
+}
+
+void Window::init()
+{
+ if (graphicsApi == Null) {
+ QRhiNullInitParams params;
+ m_r = QRhi::create(QRhi::Null, &params, rhiFlags);
+ }
+
+#ifndef QT_NO_OPENGL
+ if (graphicsApi == OpenGL) {
+ m_fallbackSurface = QRhiGles2InitParams::newFallbackSurface();
+ QRhiGles2InitParams params;
+ params.fallbackSurface = m_fallbackSurface;
+ params.window = this;
+ m_r = QRhi::create(QRhi::OpenGLES2, &params, rhiFlags);
+ }
+#endif
+
+#if QT_CONFIG(vulkan)
+ if (graphicsApi == Vulkan) {
+ QRhiVulkanInitParams params;
+ params.inst = vulkanInstance();
+ params.window = this;
+ m_r = QRhi::create(QRhi::Vulkan, &params, rhiFlags);
+ }
+#endif
+
+#ifdef Q_OS_WIN
+ if (graphicsApi == D3D11) {
+ QRhiD3D11InitParams params;
+ params.enableDebugLayer = true;
+ m_r = QRhi::create(QRhi::D3D11, &params, rhiFlags);
+ }
+#endif
+
+#ifdef Q_OS_DARWIN
+ if (graphicsApi == Metal) {
+ QRhiMetalInitParams params;
+ m_r = QRhi::create(QRhi::Metal, &params, rhiFlags);
+ }
+#endif
+
+ if (!m_r)
+ qFatal("Failed to create RHI backend");
+
+ // now onto the backend-independent init
+
+ m_sc = m_r->newSwapChain();
+ // allow depth-stencil, although we do not actually enable depth test/write for the triangle
+ m_ds = m_r->newRenderBuffer(QRhiRenderBuffer::DepthStencil,
+ QSize(), // no need to set the size yet
+ sampleCount,
+ QRhiRenderBuffer::UsedWithSwapChainOnly);
+ m_sc->setWindow(this);
+ m_sc->setDepthStencil(m_ds);
+ m_sc->setSampleCount(sampleCount);
+ m_sc->setFlags(scFlags);
+ m_rp = m_sc->newCompatibleRenderPassDescriptor();
+ m_sc->setRenderPassDescriptor(m_rp);
+
+ customInit();
+}
+
+void Window::releaseResources()
+{
+ customRelease();
+
+ delete m_rp;
+ m_rp = nullptr;
+
+ delete m_ds;
+ m_ds = nullptr;
+
+ delete m_sc;
+ m_sc = nullptr;
+
+ delete m_r;
+ m_r = nullptr;
+
+#ifndef QT_NO_OPENGL
+ delete m_fallbackSurface;
+ m_fallbackSurface = nullptr;
+#endif
+}
+
+void Window::resizeSwapChain()
+{
+ const QSize outputSize = m_sc->surfacePixelSize();
+
+ m_ds->setPixelSize(outputSize);
+ m_ds->build(); // == m_ds->release(); m_ds->build();
+
+ m_hasSwapChain = m_sc->buildOrResize();
+
+ m_frameCount = 0;
+ m_timer.restart();
+
+ m_proj = m_r->clipSpaceCorrMatrix();
+ m_proj.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 1000.0f);
+ m_proj.translate(0, 0, -4);
+}
+
+void Window::releaseSwapChain()
+{
+ if (m_hasSwapChain) {
+ m_hasSwapChain = false;
+ m_sc->release();
+ }
+}
+
+void Window::render()
+{
+ if (!m_hasSwapChain || m_notExposed)
+ return;
+
+ // If the window got resized or got newly exposed, resize the swapchain.
+ // (the newly-exposed case is not actually required by some
+ // platforms/backends, but f.ex. Vulkan on Windows seems to need it)
+ if (m_sc->currentPixelSize() != m_sc->surfacePixelSize() || m_newlyExposed) {
+ resizeSwapChain();
+ if (!m_hasSwapChain)
+ return;
+ m_newlyExposed = false;
+ }
+
+ // Start a new frame. This is where we block when too far ahead of
+ // GPU/present, and that's what throttles the thread to the refresh rate.
+ // (except for OpenGL where it happens either in endFrame or somewhere else
+ // depending on the GL implementation)
+ QRhi::FrameOpResult r = m_r->beginFrame(m_sc);
+ if (r == QRhi::FrameOpSwapChainOutOfDate) {
+ resizeSwapChain();
+ if (!m_hasSwapChain)
+ return;
+ r = m_r->beginFrame(m_sc);
+ }
+ if (r != QRhi::FrameOpSuccess) {
+ requestUpdate();
+ return;
+ }
+
+ m_frameCount += 1;
+ if (m_timer.elapsed() > 1000) {
+ if (rhiFlags.testFlag(QRhi::EnableProfiling)) {
+ const QRhiProfiler::CpuTime ff = m_r->profiler()->frameToFrameTimes(m_sc);
+ const QRhiProfiler::CpuTime be = m_r->profiler()->frameBuildTimes(m_sc);
+ const QRhiProfiler::GpuTime gp = m_r->profiler()->gpuFrameTimes(m_sc);
+ if (m_r->isFeatureSupported(QRhi::Timestamps)) {
+ qDebug("ca. %d fps. "
+ "frame-to-frame: min %lld max %lld avg %f. "
+ "frame build: min %lld max %lld avg %f. "
+ "gpu frame time: min %f max %f avg %f",
+ m_frameCount,
+ ff.minTime, ff.maxTime, ff.avgTime,
+ be.minTime, be.maxTime, be.avgTime,
+ gp.minTime, gp.maxTime, gp.avgTime);
+ } else {
+ qDebug("ca. %d fps. "
+ "frame-to-frame: min %lld max %lld avg %f. "
+ "frame build: min %lld max %lld avg %f. ",
+ m_frameCount,
+ ff.minTime, ff.maxTime, ff.avgTime,
+ be.minTime, be.maxTime, be.avgTime);
+ }
+ } else {
+ qDebug("ca. %d fps", m_frameCount);
+ }
+
+ m_timer.restart();
+ m_frameCount = 0;
+ }
+
+ customRender();
+
+ m_r->endFrame(m_sc, endFrameFlags);
+
+ if (!scFlags.testFlag(QRhiSwapChain::NoVSync))
+ requestUpdate();
+ else // try prevent all delays when NoVSync
+ QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest));
+}
+
+int main(int argc, char **argv)
+{
+ QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+ QGuiApplication app(argc, argv);
+
+ // Defaults.
+#if defined(Q_OS_WIN)
+ graphicsApi = D3D11;
+#elif defined(Q_OS_DARWIN)
+ graphicsApi = Metal;
+#elif QT_CONFIG(vulkan)
+ graphicsApi = Vulkan;
+#else
+ graphicsApi = OpenGL;
+#endif
+
+ // Allow overriding via the command line.
+ QCommandLineParser cmdLineParser;
+ cmdLineParser.addHelpOption();
+ QCommandLineOption nullOption({ "n", "null" }, QLatin1String("Null"));
+ cmdLineParser.addOption(nullOption);
+ QCommandLineOption glOption({ "g", "opengl" }, QLatin1String("OpenGL (2.x)"));
+ cmdLineParser.addOption(glOption);
+ QCommandLineOption vkOption({ "v", "vulkan" }, QLatin1String("Vulkan"));
+ cmdLineParser.addOption(vkOption);
+ QCommandLineOption d3dOption({ "d", "d3d11" }, QLatin1String("Direct3D 11"));
+ cmdLineParser.addOption(d3dOption);
+ QCommandLineOption mtlOption({ "m", "metal" }, QLatin1String("Metal"));
+ cmdLineParser.addOption(mtlOption);
+ // Testing cleanup both with QWindow::close() (hitting X or Alt-F4) and
+ // QCoreApplication::quit() (e.g. what a menu widget would do) is important.
+ // Use this parameter for the latter.
+ QCommandLineOption sdOption({ "s", "self-destruct" }, QLatin1String("Self destruct after 5 seconds"));
+ cmdLineParser.addOption(sdOption);
+
+ cmdLineParser.process(app);
+ if (cmdLineParser.isSet(nullOption))
+ graphicsApi = Null;
+ if (cmdLineParser.isSet(glOption))
+ graphicsApi = OpenGL;
+ if (cmdLineParser.isSet(vkOption))
+ graphicsApi = Vulkan;
+ if (cmdLineParser.isSet(d3dOption))
+ graphicsApi = D3D11;
+ if (cmdLineParser.isSet(mtlOption))
+ graphicsApi = Metal;
+
+ qDebug("Selected graphics API is %s", qPrintable(graphicsApiName()));
+ qDebug("This is a multi-api example, use command line arguments to override:\n%s", qPrintable(cmdLineParser.helpText()));
+
+#ifdef EXAMPLEFW_PREINIT
+ void preInit();
+ preInit();
+#endif
+
+ // OpenGL specifics.
+ QSurfaceFormat fmt;
+ fmt.setDepthBufferSize(24);
+ fmt.setStencilBufferSize(8);
+ if (sampleCount > 1)
+ fmt.setSamples(sampleCount);
+ if (scFlags.testFlag(QRhiSwapChain::NoVSync))
+ fmt.setSwapInterval(0);
+ if (scFlags.testFlag(QRhiSwapChain::sRGB))
+ fmt.setColorSpace(QSurfaceFormat::sRGBColorSpace);
+ QSurfaceFormat::setDefaultFormat(fmt);
+
+ // Vulkan setup.
+#if QT_CONFIG(vulkan)
+ QVulkanInstance inst;
+ if (graphicsApi == Vulkan) {
+#ifndef Q_OS_ANDROID
+ inst.setLayers(QByteArrayList() << "VK_LAYER_LUNARG_standard_validation");
+#else
+ inst.setLayers(QByteArrayList()
+ << "VK_LAYER_GOOGLE_threading"
+ << "VK_LAYER_LUNARG_parameter_validation"
+ << "VK_LAYER_LUNARG_object_tracker"
+ << "VK_LAYER_LUNARG_core_validation"
+ << "VK_LAYER_LUNARG_image"
+ << "VK_LAYER_LUNARG_swapchain"
+ << "VK_LAYER_GOOGLE_unique_objects");
+#endif
+ inst.setExtensions(QByteArrayList()
+ << "VK_KHR_get_physical_device_properties2");
+ if (!inst.create()) {
+ qWarning("Failed to create Vulkan instance, switching to OpenGL");
+ graphicsApi = OpenGL;
+ }
+ }
+#endif
+
+ // Create and show the window.
+ Window w;
+#if QT_CONFIG(vulkan)
+ if (graphicsApi == Vulkan)
+ w.setVulkanInstance(&inst);
+#endif
+ w.resize(1280, 720);
+ w.setTitle(QCoreApplication::applicationName() + QLatin1String(" - ") + graphicsApiName());
+ w.show();
+
+ if (cmdLineParser.isSet(sdOption))
+ QTimer::singleShot(5000, qGuiApp, SLOT(quit()));
+
+ int ret = app.exec();
+
+ // Window::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 (w.handle())
+ w.releaseSwapChain();
+
+ return ret;
+}
diff --git a/tests/manual/rhi/shared/qt256.png b/tests/manual/rhi/shared/qt256.png
new file mode 100644
index 0000000000..30c621c9c6
--- /dev/null
+++ b/tests/manual/rhi/shared/qt256.png
Binary files differ
diff --git a/tests/manual/rhi/shared/qt256_bc1_9mips.dds b/tests/manual/rhi/shared/qt256_bc1_9mips.dds
new file mode 100644
index 0000000000..734a5d184f
--- /dev/null
+++ b/tests/manual/rhi/shared/qt256_bc1_9mips.dds
Binary files differ
diff --git a/tests/manual/rhi/shared/texture.frag b/tests/manual/rhi/shared/texture.frag
new file mode 100644
index 0000000000..5405444c68
--- /dev/null
+++ b/tests/manual/rhi/shared/texture.frag
@@ -0,0 +1,18 @@
+#version 440
+
+layout(location = 0) in vec2 v_texcoord;
+
+layout(location = 0) out vec4 fragColor;
+
+layout(std140, binding = 0) uniform buf {
+ mat4 mvp;
+ int flip;
+} ubuf;
+
+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/tests/manual/rhi/shared/texture.frag.qsb b/tests/manual/rhi/shared/texture.frag.qsb
new file mode 100644
index 0000000000..31bcd7105e
--- /dev/null
+++ b/tests/manual/rhi/shared/texture.frag.qsb
Binary files differ
diff --git a/tests/manual/rhi/shared/texture.vert b/tests/manual/rhi/shared/texture.vert
new file mode 100644
index 0000000000..ad7e783dfb
--- /dev/null
+++ b/tests/manual/rhi/shared/texture.vert
@@ -0,0 +1,21 @@
+#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;
+ int flip;
+} ubuf;
+
+out gl_PerVertex { vec4 gl_Position; };
+
+void main()
+{
+ v_texcoord = vec2(texcoord.x, texcoord.y);
+ if (ubuf.flip != 0)
+ v_texcoord.y = 1.0 - v_texcoord.y;
+ gl_Position = ubuf.mvp * position;
+}
diff --git a/tests/manual/rhi/shared/texture.vert.qsb b/tests/manual/rhi/shared/texture.vert.qsb
new file mode 100644
index 0000000000..1b9e52890d
--- /dev/null
+++ b/tests/manual/rhi/shared/texture.vert.qsb
Binary files differ
diff --git a/tests/manual/rhi/shared/texture_ms4.frag b/tests/manual/rhi/shared/texture_ms4.frag
new file mode 100644
index 0000000000..d4f2bd3654
--- /dev/null
+++ b/tests/manual/rhi/shared/texture_ms4.frag
@@ -0,0 +1,20 @@
+#version 440
+
+layout(location = 0) in vec2 v_texcoord;
+
+layout(location = 0) out vec4 fragColor;
+
+layout(std140, binding = 0) uniform buf {
+ mat4 mvp;
+ int flip;
+} ubuf;
+
+layout(binding = 1) uniform sampler2DMS tex;
+
+void main()
+{
+ ivec2 tc = ivec2(floor(vec2(textureSize(tex)) * v_texcoord));
+ vec4 c = texelFetch(tex, tc, 0) + texelFetch(tex, tc, 1) + texelFetch(tex, tc, 2) + texelFetch(tex, tc, 3);
+ c /= 4.0;
+ fragColor = vec4(c.rgb * c.a, c.a);
+}
diff --git a/tests/manual/rhi/shared/texture_ms4.frag.qsb b/tests/manual/rhi/shared/texture_ms4.frag.qsb
new file mode 100644
index 0000000000..7f187ce7f0
--- /dev/null
+++ b/tests/manual/rhi/shared/texture_ms4.frag.qsb
Binary files differ
diff --git a/tests/manual/rhi/texuploads/texuploads.cpp b/tests/manual/rhi/texuploads/texuploads.cpp
new file mode 100644
index 0000000000..6576b763d5
--- /dev/null
+++ b/tests/manual/rhi/texuploads/texuploads.cpp
@@ -0,0 +1,314 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "../shared/examplefw.h"
+#include "../shared/cube.h"
+#include <QPainter>
+
+struct {
+ QRhiBuffer *vbuf = nullptr;
+ QRhiBuffer *ubuf = nullptr;
+ QRhiTexture *tex = nullptr;
+ QRhiSampler *sampler = nullptr;
+ QRhiShaderResourceBindings *srb = nullptr;
+ QRhiGraphicsPipeline *ps = nullptr;
+ QVector<QRhiResource *> releasePool;
+
+ float rotation = 0;
+ QRhiResourceUpdateBatch *initialUpdates = nullptr;
+ int frameCount = 0;
+ QImage customImage;
+ QRhiTexture *newTex = nullptr;
+ QRhiTexture *importedTex = nullptr;
+ int testStage = 0;
+} d;
+
+void Window::customInit()
+{
+ d.vbuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube));
+ d.releasePool << d.vbuf;
+ d.vbuf->build();
+
+ d.ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68);
+ d.releasePool << d.ubuf;
+ d.ubuf->build();
+
+ QImage baseImage(QLatin1String(":/qt256.png"));
+ d.tex = m_r->newTexture(QRhiTexture::RGBA8, baseImage.size(), 1, QRhiTexture::UsedAsTransferSource);
+ d.releasePool << d.tex;
+ d.tex->build();
+
+ // As an alternative to what some of the other examples do, prepare an
+ // update batch right here instead of relying on vbufReady and similar flags.
+ d.initialUpdates = m_r->nextResourceUpdateBatch();
+ d.initialUpdates->uploadStaticBuffer(d.vbuf, cube);
+ qint32 flip = 0;
+ d.initialUpdates->updateDynamicBuffer(d.ubuf, 64, 4, &flip);
+ d.initialUpdates->uploadTexture(d.tex, baseImage);
+
+ d.sampler = m_r->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
+ QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
+ d.releasePool << d.sampler;
+ d.sampler->build();
+
+ d.srb = m_r->newShaderResourceBindings();
+ d.releasePool << d.srb;
+ d.srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, d.ubuf),
+ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, d.tex, d.sampler)
+ });
+ d.srb->build();
+
+ d.ps = m_r->newGraphicsPipeline();
+ d.releasePool << d.ps;
+
+ d.ps->setDepthTest(true);
+ d.ps->setDepthWrite(true);
+ d.ps->setDepthOp(QRhiGraphicsPipeline::Less);
+
+ d.ps->setCullMode(QRhiGraphicsPipeline::Back);
+ d.ps->setFrontFace(QRhiGraphicsPipeline::CCW);
+
+ const QShader vs = getShader(QLatin1String(":/texture.vert.qsb"));
+ if (!vs.isValid())
+ qFatal("Failed to load shader pack (vertex)");
+ const QShader fs = getShader(QLatin1String(":/texture.frag.qsb"));
+ if (!fs.isValid())
+ qFatal("Failed to load shader pack (fragment)");
+
+ d.ps->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, vs },
+ { QRhiGraphicsShaderStage::Fragment, fs }
+ });
+
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 3 * sizeof(float) },
+ { 2 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float3, 0 },
+ { 1, 1, QRhiVertexInputAttribute::Float2, 0 }
+ });
+
+ d.ps->setVertexInputLayout(inputLayout);
+ d.ps->setShaderResourceBindings(d.srb);
+ d.ps->setRenderPassDescriptor(m_rp);
+
+ d.ps->build();
+
+ d.customImage = QImage(128, 64, QImage::Format_RGBA8888);
+ d.customImage.fill(Qt::red);
+ QPainter painter(&d.customImage);
+ // the text may look different on different platforms, so no guarantee the
+ // output on the screen will be identical everywhere
+ painter.drawText(5, 25, "Hello world");
+ painter.end();
+}
+
+void Window::customRelease()
+{
+ qDeleteAll(d.releasePool);
+ d.releasePool.clear();
+}
+
+void Window::customRender()
+{
+ QRhiResourceUpdateBatch *u = m_r->nextResourceUpdateBatch();
+
+ // take the initial set of updates, if this is the first frame
+ if (d.initialUpdates) {
+ u->merge(d.initialUpdates);
+ d.initialUpdates->release();
+ d.initialUpdates = nullptr;
+ }
+
+ d.rotation += 1.0f;
+ QMatrix4x4 mvp = m_proj;
+ mvp.scale(0.5f);
+ mvp.rotate(d.rotation, 0, 1, 0);
+ u->updateDynamicBuffer(d.ubuf, 0, 64, mvp.constData());
+
+ if (d.frameCount > 0 && (d.frameCount % 100) == 0) {
+ d.testStage += 1;
+ qDebug("testStage = %d", d.testStage);
+
+ // Partially change the texture.
+ if (d.testStage == 1) {
+ QRhiTextureSubresourceUploadDescription mipDesc(d.customImage);
+ // The image here is smaller than the original. Use a non-zero position
+ // to make it more interesting.
+ mipDesc.setDestinationTopLeft(QPoint(100, 20));
+ QRhiTextureUploadDescription desc({ 0, 0, mipDesc });
+ u->uploadTexture(d.tex, desc);
+ }
+
+ // Exercise image copying.
+ if (d.testStage == 2) {
+ const QSize sz = d.tex->pixelSize();
+ d.newTex = m_r->newTexture(QRhiTexture::RGBA8, sz);
+ d.releasePool << d.newTex;
+ d.newTex->build();
+
+ QImage empty(sz.width(), sz.height(), QImage::Format_RGBA8888);
+ empty.fill(Qt::blue);
+ u->uploadTexture(d.newTex, empty);
+
+ // Copy the left-half of tex to the right-half of newTex, while
+ // leaving the left-half of newTex blue. Keep a 20 pixel gap at
+ // the top.
+ QRhiTextureCopyDescription desc;
+ desc.setSourceTopLeft(QPoint(0, 20));
+ desc.setPixelSize(QSize(sz.width() / 2, sz.height() - 20));
+ desc.setDestinationTopLeft(QPoint(sz.width() / 2, 20));
+ u->copyTexture(d.newTex, d.tex, desc);
+
+ // Now replace d.tex with d.newTex as the shader resource.
+ auto bindings = d.srb->bindings();
+ bindings[1] = QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, d.newTex, d.sampler);
+ d.srb->setBindings(bindings);
+ // "rebuild", whatever that means for a given backend. This srb is
+ // already live as the ps in the setGraphicsPipeline references it,
+ // but that's fine. Changes will be picked up automatically.
+ d.srb->build();
+ }
+
+ // Exercise simple, full texture copy.
+ if (d.testStage == 4)
+ u->copyTexture(d.newTex, d.tex);
+
+ // Now again upload customImage but this time only a part of it.
+ if (d.testStage == 5) {
+ QRhiTextureSubresourceUploadDescription mipDesc(d.customImage);
+ mipDesc.setDestinationTopLeft(QPoint(10, 120));
+ mipDesc.setSourceSize(QSize(50, 40));
+ mipDesc.setSourceTopLeft(QPoint(20, 10));
+ QRhiTextureUploadDescription desc({ 0, 0, mipDesc });
+ u->uploadTexture(d.newTex, desc);
+ }
+
+ // Exercise texture object export/import.
+ if (d.testStage == 6) {
+ const QRhiNativeHandles *h = d.tex->nativeHandles();
+ if (h) {
+#ifdef Q_OS_DARWIN
+ if (graphicsApi == Metal) {
+ qDebug() << "Metal texture: " << static_cast<const QRhiMetalTextureNativeHandles *>(h)->texture;
+ // Now could cast to id<MTLTexture> and do something with
+ // it, keeping in mind that copy operations are only done
+ // in beginPass, while rendering into a texture may only
+ // have proper results in current_frame + 2, or after a
+ // finish(). The QRhiTexture still owns the native object.
+ }
+#endif
+ // omit for other backends, the idea is the same
+
+ d.importedTex = m_r->newTexture(QRhiTexture::RGBA8, d.tex->pixelSize());
+ d.releasePool << d.importedTex;
+ if (!d.importedTex->buildFrom(h))
+ qWarning("Texture import failed");
+
+ // now d.tex and d.importedTex use the same MTLTexture
+ // underneath (owned by d.tex)
+
+ // switch to showing d.importedTex
+ auto bindings = d.srb->bindings();
+ bindings[1] = QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, d.importedTex, d.sampler);
+ d.srb->setBindings(bindings);
+ d.srb->build();
+ } else {
+ qWarning("Accessing native texture object is not supported");
+ }
+ }
+
+ // Exercise uploading uncompressed data without a QImage.
+ if (d.testStage == 7) {
+ auto bindings = d.srb->bindings();
+ bindings[1] = QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, d.newTex, d.sampler);
+ d.srb->setBindings(bindings);
+ d.srb->build();
+
+ const QSize sz(221, 139);
+ QByteArray data;
+ data.resize(sz.width() * sz.height() * 4);
+ for (int y = 0; y < sz.height(); ++y) {
+ uchar *p = reinterpret_cast<uchar *>(data.data()) + y * sz.width() * 4;
+ for (int x = 0; x < sz.width(); ++x) {
+ *p++ = 0; *p++ = 0; *p++ = y * (255 / sz.height());
+ *p++ = 255;
+ }
+ }
+ QRhiTextureSubresourceUploadDescription mipDesc(data.constData(), data.size());
+ mipDesc.setSourceSize(sz);
+ mipDesc.setDestinationTopLeft(QPoint(5, 25));
+ QRhiTextureUploadDescription desc({ 0, 0, mipDesc });
+ u->uploadTexture(d.newTex, desc);
+ }
+ }
+
+ QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
+ const QSize outputSizeInPixels = m_sc->currentPixelSize();
+
+ cb->beginPass(m_sc->currentFrameRenderTarget(), QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f), { 1.0f, 0 }, u);
+
+ cb->setGraphicsPipeline(d.ps);
+ cb->setViewport({ 0, 0, float(outputSizeInPixels.width()), float(outputSizeInPixels.height()) });
+ cb->setShaderResources();
+ const QRhiCommandBuffer::VertexInput vbufBindings[] = {
+ { d.vbuf, 0 },
+ { d.vbuf, 36 * 3 * sizeof(float) }
+ };
+ cb->setVertexInput(0, 2, vbufBindings);
+ cb->draw(36);
+
+ cb->endPass();
+
+ d.frameCount += 1;
+}
diff --git a/tests/manual/rhi/texuploads/texuploads.pro b/tests/manual/rhi/texuploads/texuploads.pro
new file mode 100644
index 0000000000..64f204fb32
--- /dev/null
+++ b/tests/manual/rhi/texuploads/texuploads.pro
@@ -0,0 +1,8 @@
+TEMPLATE = app
+
+QT += gui-private
+
+SOURCES = \
+ texuploads.cpp
+
+RESOURCES = texuploads.qrc
diff --git a/tests/manual/rhi/texuploads/texuploads.qrc b/tests/manual/rhi/texuploads/texuploads.qrc
new file mode 100644
index 0000000000..742b2b77af
--- /dev/null
+++ b/tests/manual/rhi/texuploads/texuploads.qrc
@@ -0,0 +1,7 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+ <file alias="qt256.png">../shared/qt256.png</file>
+ <file alias="texture.vert.qsb">../shared/texture.vert.qsb</file>
+ <file alias="texture.frag.qsb">../shared/texture.frag.qsb</file>
+</qresource>
+</RCC>
diff --git a/tests/manual/rhi/triquadcube/quadrenderer.cpp b/tests/manual/rhi/triquadcube/quadrenderer.cpp
new file mode 100644
index 0000000000..6137fb1b9a
--- /dev/null
+++ b/tests/manual/rhi/triquadcube/quadrenderer.cpp
@@ -0,0 +1,148 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "quadrenderer.h"
+#include <QFile>
+#include <QtGui/private/qshader_p.h>
+
+// Renders a quad using indexed drawing. No QRhiGraphicsPipeline is created, it
+// expects to reuse the one created by TriangleRenderer. A separate
+// QRhiShaderResourceBindings is still needed, this will override the one the
+// QRhiGraphicsPipeline references.
+
+static float vertexData[] =
+{ // Y up (note m_proj), CCW
+ -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
+ -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
+ 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
+ 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f
+};
+
+static quint16 indexData[] =
+{
+ 0, 1, 2, 0, 2, 3
+};
+
+void QuadRenderer::initResources(QRhiRenderPassDescriptor *)
+{
+ m_vbuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData));
+ m_vbuf->build();
+ m_vbufReady = false;
+
+ m_ibuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(indexData));
+ m_ibuf->build();
+
+ m_ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68);
+ m_ubuf->build();
+
+ m_srb = m_r->newShaderResourceBindings();
+ m_srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, m_ubuf)
+ });
+ m_srb->build();
+}
+
+void QuadRenderer::setPipeline(QRhiGraphicsPipeline *ps)
+{
+ m_ps = ps;
+}
+
+void QuadRenderer::resize(const QSize &pixelSize)
+{
+ m_proj = m_r->clipSpaceCorrMatrix();
+ m_proj.perspective(45.0f, pixelSize.width() / (float) pixelSize.height(), 0.01f, 100.0f);
+ m_proj.translate(0, 0, -4);
+}
+
+void QuadRenderer::releaseResources()
+{
+ delete m_srb;
+ m_srb = nullptr;
+
+ delete m_ubuf;
+ m_ubuf = nullptr;
+
+ delete m_ibuf;
+ m_ibuf = nullptr;
+
+ delete m_vbuf;
+ m_vbuf = nullptr;
+}
+
+void QuadRenderer::queueResourceUpdates(QRhiResourceUpdateBatch *resourceUpdates)
+{
+ if (!m_vbufReady) {
+ m_vbufReady = true;
+ resourceUpdates->uploadStaticBuffer(m_vbuf, vertexData);
+ resourceUpdates->uploadStaticBuffer(m_ibuf, indexData);
+ }
+
+ m_rotation += 1.0f;
+ QMatrix4x4 mvp = m_proj;
+ mvp.translate(m_translation);
+ mvp.rotate(m_rotation, 0, 1, 0);
+ resourceUpdates->updateDynamicBuffer(m_ubuf, 0, 64, mvp.constData());
+
+ if (!m_opacityReady) {
+ m_opacityReady = true;
+ const float opacity = 1.0f;
+ resourceUpdates->updateDynamicBuffer(m_ubuf, 64, 4, &opacity);
+ }
+}
+
+void QuadRenderer::queueDraw(QRhiCommandBuffer *cb, const QSize &/*outputSizeInPixels*/)
+{
+ cb->setGraphicsPipeline(m_ps);
+ //cb->setViewport(QRhiViewport(0, 0, outputSizeInPixels.width(), outputSizeInPixels.height()));
+ cb->setShaderResources(m_srb);
+ const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf, 0);
+ cb->setVertexInput(0, 1, &vbufBinding, m_ibuf, 0, QRhiCommandBuffer::IndexUInt16);
+ cb->drawIndexed(6);
+}
diff --git a/tests/manual/rhi/triquadcube/quadrenderer.h b/tests/manual/rhi/triquadcube/quadrenderer.h
new file mode 100644
index 0000000000..79d5b8209d
--- /dev/null
+++ b/tests/manual/rhi/triquadcube/quadrenderer.h
@@ -0,0 +1,87 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QUADRENDERER_H
+#define QUADRENDERER_H
+
+#include <QtGui/private/qrhi_p.h>
+
+class QuadRenderer
+{
+public:
+ void setRhi(QRhi *r) { m_r = r; }
+ void setSampleCount(int samples) { m_sampleCount = samples; }
+ int sampleCount() const { return m_sampleCount; }
+ void setTranslation(const QVector3D &v) { m_translation = v; }
+ void initResources(QRhiRenderPassDescriptor *rp);
+ void releaseResources();
+ void setPipeline(QRhiGraphicsPipeline *ps);
+ void resize(const QSize &pixelSize);
+ void queueResourceUpdates(QRhiResourceUpdateBatch *resourceUpdates);
+ void queueDraw(QRhiCommandBuffer *cb, const QSize &outputSizeInPixels);
+
+private:
+ QRhi *m_r;
+
+ QRhiBuffer *m_vbuf = nullptr;
+ bool m_vbufReady = false;
+ QRhiBuffer *m_ibuf = nullptr;
+ bool m_opacityReady = false;
+ QRhiBuffer *m_ubuf = nullptr;
+ QRhiGraphicsPipeline *m_ps = nullptr;
+ QRhiShaderResourceBindings *m_srb = nullptr;
+
+ QVector3D m_translation;
+ QMatrix4x4 m_proj;
+ float m_rotation = 0;
+ int m_sampleCount = 1; // no MSAA by default
+};
+
+#endif
diff --git a/tests/manual/rhi/triquadcube/texturedcuberenderer.cpp b/tests/manual/rhi/triquadcube/texturedcuberenderer.cpp
new file mode 100644
index 0000000000..d2e1f99923
--- /dev/null
+++ b/tests/manual/rhi/triquadcube/texturedcuberenderer.cpp
@@ -0,0 +1,222 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "texturedcuberenderer.h"
+#include <QFile>
+#include <QtGui/private/qshader_p.h>
+
+#include "../shared/cube.h"
+
+const bool MIPMAP = true;
+const bool AUTOGENMIPMAP = true;
+
+static QShader getShader(const QString &name)
+{
+ QFile f(name);
+ if (f.open(QIODevice::ReadOnly))
+ return QShader::fromSerialized(f.readAll());
+
+ return QShader();
+}
+
+void TexturedCubeRenderer::initResources(QRhiRenderPassDescriptor *rp)
+{
+ m_vbuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube));
+ m_vbuf->setName(QByteArrayLiteral("Cube vbuf (textured)"));
+ m_vbuf->build();
+ m_vbufReady = false;
+
+ m_ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64 + 4);
+ m_ubuf->setName(QByteArrayLiteral("Cube ubuf (textured)"));
+ m_ubuf->build();
+
+ m_image = QImage(QLatin1String(":/qt256.png")).convertToFormat(QImage::Format_RGBA8888);
+ QRhiTexture::Flags texFlags = 0;
+ if (MIPMAP)
+ texFlags |= QRhiTexture::MipMapped;
+ if (AUTOGENMIPMAP)
+ texFlags |= QRhiTexture::UsedWithGenerateMips;
+ m_tex = m_r->newTexture(QRhiTexture::RGBA8, QSize(m_image.width(), m_image.height()), 1, texFlags);
+ m_tex->setName(QByteArrayLiteral("Qt texture"));
+ m_tex->build();
+
+ m_sampler = m_r->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, MIPMAP ? QRhiSampler::Linear : QRhiSampler::None,
+ QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
+ m_sampler->build();
+
+ m_srb = m_r->newShaderResourceBindings();
+ m_srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, m_ubuf),
+ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, m_tex, m_sampler)
+ });
+ m_srb->build();
+
+ m_ps = m_r->newGraphicsPipeline();
+
+ // No blending but the texture has alpha which we do not want to write out.
+ // Be nice. (would not matter for an onscreen window but makes a difference
+ // when reading back and saving into image files f.ex.)
+ QRhiGraphicsPipeline::TargetBlend blend;
+ blend.colorWrite = QRhiGraphicsPipeline::R | QRhiGraphicsPipeline::G | QRhiGraphicsPipeline::B;
+ m_ps->setTargetBlends({ blend });
+
+ m_ps->setDepthTest(true);
+ m_ps->setDepthWrite(true);
+ m_ps->setDepthOp(QRhiGraphicsPipeline::Less);
+
+ m_ps->setCullMode(QRhiGraphicsPipeline::Back);
+ m_ps->setFrontFace(QRhiGraphicsPipeline::CCW);
+
+ m_ps->setSampleCount(m_sampleCount);
+
+ QShader vs = getShader(QLatin1String(":/texture.vert.qsb"));
+ Q_ASSERT(vs.isValid());
+ QShader fs = getShader(QLatin1String(":/texture.frag.qsb"));
+ Q_ASSERT(fs.isValid());
+ m_ps->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, vs },
+ { QRhiGraphicsShaderStage::Fragment, fs }
+ });
+
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 3 * sizeof(float) },
+ { 2 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float3, 0 },
+ { 1, 1, QRhiVertexInputAttribute::Float2, 0 }
+ });
+
+ m_ps->setVertexInputLayout(inputLayout);
+ m_ps->setShaderResourceBindings(m_srb);
+ m_ps->setRenderPassDescriptor(rp);
+
+ m_ps->build();
+}
+
+void TexturedCubeRenderer::resize(const QSize &pixelSize)
+{
+ m_proj = m_r->clipSpaceCorrMatrix();
+ m_proj.perspective(45.0f, pixelSize.width() / (float) pixelSize.height(), 0.01f, 100.0f);
+ m_proj.translate(0, 0, -4);
+}
+
+void TexturedCubeRenderer::releaseResources()
+{
+ delete m_ps;
+ m_ps = nullptr;
+
+ delete m_srb;
+ m_srb = nullptr;
+
+ delete m_sampler;
+ m_sampler = nullptr;
+
+ delete m_tex;
+ m_tex = nullptr;
+
+ delete m_ubuf;
+ m_ubuf = nullptr;
+
+ delete m_vbuf;
+ m_vbuf = nullptr;
+}
+
+void TexturedCubeRenderer::queueResourceUpdates(QRhiResourceUpdateBatch *resourceUpdates)
+{
+ if (!m_vbufReady) {
+ m_vbufReady = true;
+ resourceUpdates->uploadStaticBuffer(m_vbuf, cube);
+ qint32 flip = 0;
+ resourceUpdates->updateDynamicBuffer(m_ubuf, 64, 4, &flip);
+ }
+
+ if (!m_image.isNull()) {
+ if (MIPMAP) {
+ QRhiTextureUploadDescription desc;
+ if (!AUTOGENMIPMAP) {
+ // the ghetto mipmap generator...
+ for (int i = 0, ie = m_r->mipLevelsForSize(m_image.size()); i != ie; ++i) {
+ QImage image = m_image.scaled(m_r->sizeForMipLevel(i, m_image.size()));
+ desc.append({ 0, i, image });
+ }
+ } else {
+ desc.append({ 0, 0, m_image });
+ }
+ resourceUpdates->uploadTexture(m_tex, desc);
+ if (AUTOGENMIPMAP)
+ resourceUpdates->generateMips(m_tex);
+ } else {
+ resourceUpdates->uploadTexture(m_tex, m_image);
+ }
+ m_image = QImage();
+ }
+
+ m_rotation += 1.0f;
+ QMatrix4x4 mvp = m_proj;
+ mvp.translate(m_translation);
+ mvp.scale(0.5f);
+ mvp.rotate(m_rotation, 0, 1, 0);
+ resourceUpdates->updateDynamicBuffer(m_ubuf, 0, 64, mvp.constData());
+}
+
+void TexturedCubeRenderer::queueDraw(QRhiCommandBuffer *cb, const QSize &outputSizeInPixels)
+{
+ cb->setGraphicsPipeline(m_ps);
+ cb->setViewport(QRhiViewport(0, 0, outputSizeInPixels.width(), outputSizeInPixels.height()));
+ cb->setShaderResources();
+ const QRhiCommandBuffer::VertexInput vbufBindings[] = {
+ { m_vbuf, 0 },
+ { m_vbuf, 36 * 3 * sizeof(float) }
+ };
+ cb->setVertexInput(0, 2, vbufBindings);
+ cb->draw(36);
+}
diff --git a/tests/manual/rhi/triquadcube/texturedcuberenderer.h b/tests/manual/rhi/triquadcube/texturedcuberenderer.h
new file mode 100644
index 0000000000..879f875209
--- /dev/null
+++ b/tests/manual/rhi/triquadcube/texturedcuberenderer.h
@@ -0,0 +1,86 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef TEXTUREDCUBERENDERER_H
+#define TEXTUREDCUBERENDERER_H
+
+#include <QtGui/private/qrhi_p.h>
+
+class TexturedCubeRenderer
+{
+public:
+ void setRhi(QRhi *r) { m_r = r; }
+ void setSampleCount(int samples) { m_sampleCount = samples; }
+ void setTranslation(const QVector3D &v) { m_translation = v; }
+ void initResources(QRhiRenderPassDescriptor *rp);
+ void releaseResources();
+ void resize(const QSize &pixelSize);
+ void queueResourceUpdates(QRhiResourceUpdateBatch *resourceUpdates);
+ void queueDraw(QRhiCommandBuffer *cb, const QSize &outputSizeInPixels);
+
+private:
+ QRhi *m_r;
+
+ QRhiBuffer *m_vbuf = nullptr;
+ bool m_vbufReady = false;
+ QRhiBuffer *m_ubuf = nullptr;
+ QImage m_image;
+ QRhiTexture *m_tex = nullptr;
+ QRhiSampler *m_sampler = nullptr;
+ QRhiShaderResourceBindings *m_srb = nullptr;
+ QRhiGraphicsPipeline *m_ps = nullptr;
+
+ QVector3D m_translation;
+ QMatrix4x4 m_proj;
+ float m_rotation = 0;
+ int m_sampleCount = 1; // no MSAA by default
+};
+
+#endif
diff --git a/tests/manual/rhi/triquadcube/triangleoncuberenderer.cpp b/tests/manual/rhi/triquadcube/triangleoncuberenderer.cpp
new file mode 100644
index 0000000000..40d9615aa1
--- /dev/null
+++ b/tests/manual/rhi/triquadcube/triangleoncuberenderer.cpp
@@ -0,0 +1,292 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "triangleoncuberenderer.h"
+#include <QFile>
+#include <QtGui/private/qshader_p.h>
+
+// toggle to test the preserved content (no clear) path
+const bool IMAGE_UNDER_OFFSCREEN_RENDERING = false;
+const bool UPLOAD_UNDERLAY_ON_EVERY_FRAME = false;
+
+const bool DS_ATT = false; // have a depth-stencil attachment for the offscreen pass
+
+const bool DEPTH_TEXTURE = false; // offscreen pass uses a depth texture (verify with renderdoc etc., ignore valid.layer about ps slot 0)
+const bool MRT = false; // two textures, the second is just cleared as the shader does not write anything (valid.layer may warn but for testing that's ok)
+
+#include "../shared/cube.h"
+
+static QShader getShader(const QString &name)
+{
+ QFile f(name);
+ if (f.open(QIODevice::ReadOnly))
+ return QShader::fromSerialized(f.readAll());
+
+ return QShader();
+}
+
+static const QSize OFFSCREEN_SIZE(512, 512);
+
+void TriangleOnCubeRenderer::initResources(QRhiRenderPassDescriptor *rp)
+{
+ m_vbuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube));
+ m_vbuf->setName(QByteArrayLiteral("Cube vbuf (textured with offscreen)"));
+ m_vbuf->build();
+ m_vbufReady = false;
+
+ m_ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64 + 4);
+ m_ubuf->setName(QByteArrayLiteral("Cube ubuf (textured with offscreen)"));
+ m_ubuf->build();
+
+ if (IMAGE_UNDER_OFFSCREEN_RENDERING) {
+ m_image = QImage(QLatin1String(":/qt256.png")).scaled(OFFSCREEN_SIZE).convertToFormat(QImage::Format_RGBA8888);
+ if (m_r->isYUpInFramebuffer())
+ m_image = m_image.mirrored(); // just cause we'll flip texcoord Y when y up so accommodate our static background image as well
+ }
+
+ m_tex = m_r->newTexture(QRhiTexture::RGBA8, OFFSCREEN_SIZE, 1, QRhiTexture::RenderTarget);
+ m_tex->setName(QByteArrayLiteral("Texture for offscreen content"));
+ m_tex->build();
+
+ if (MRT) {
+ m_tex2 = m_r->newTexture(QRhiTexture::RGBA8, OFFSCREEN_SIZE, 1, QRhiTexture::RenderTarget);
+ m_tex2->build();
+ }
+
+ if (DS_ATT) {
+ m_offscreenTriangle.setDepthWrite(true);
+ m_ds = m_r->newRenderBuffer(QRhiRenderBuffer::DepthStencil, m_tex->pixelSize());
+ m_ds->build();
+ }
+
+ if (DEPTH_TEXTURE) {
+ m_offscreenTriangle.setDepthWrite(true);
+ m_depthTex = m_r->newTexture(QRhiTexture::D32F, OFFSCREEN_SIZE, 1, QRhiTexture::RenderTarget);
+ m_depthTex->build();
+ }
+
+ m_sampler = m_r->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None, QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
+ m_sampler->build();
+
+ m_srb = m_r->newShaderResourceBindings();
+ m_srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, m_ubuf),
+ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, m_tex, m_sampler)
+ });
+ m_srb->build();
+
+ m_ps = m_r->newGraphicsPipeline();
+
+ m_ps->setDepthTest(true);
+ m_ps->setDepthWrite(true);
+ m_ps->setDepthOp(QRhiGraphicsPipeline::Less);
+
+ m_ps->setCullMode(QRhiGraphicsPipeline::Back);
+ m_ps->setFrontFace(QRhiGraphicsPipeline::CCW);
+
+ m_ps->setSampleCount(m_sampleCount);
+
+ QShader vs = getShader(QLatin1String(":/texture.vert.qsb"));
+ Q_ASSERT(vs.isValid());
+ QShader fs = getShader(QLatin1String(":/texture.frag.qsb"));
+ Q_ASSERT(fs.isValid());
+ m_ps->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, vs },
+ { QRhiGraphicsShaderStage::Fragment, fs }
+ });
+
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 3 * sizeof(float) },
+ { 2 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float3, 0 },
+ { 1, 1, QRhiVertexInputAttribute::Float2, 0 }
+ });
+
+ m_ps->setVertexInputLayout(inputLayout);
+ m_ps->setShaderResourceBindings(m_srb);
+ m_ps->setRenderPassDescriptor(rp);
+
+ m_ps->build();
+
+ QRhiTextureRenderTarget::Flags rtFlags = 0;
+ if (IMAGE_UNDER_OFFSCREEN_RENDERING)
+ rtFlags |= QRhiTextureRenderTarget::PreserveColorContents;
+
+ if (DEPTH_TEXTURE) {
+ QRhiTextureRenderTargetDescription desc;
+ desc.setDepthTexture(m_depthTex);
+ m_rt = m_r->newTextureRenderTarget(desc, rtFlags);
+ } else {
+ QRhiTextureRenderTargetDescription desc;
+ QRhiColorAttachment color0 { m_tex };
+ if (DS_ATT)
+ desc.setDepthStencilBuffer(m_ds);
+ if (MRT) {
+ m_offscreenTriangle.setColorAttCount(2);
+ QRhiColorAttachment color1 { m_tex2 };
+ desc.setColorAttachments({ color0, color1 });
+ } else {
+ desc.setColorAttachments({ color0 });
+ }
+ m_rt = m_r->newTextureRenderTarget(desc, rtFlags);
+ }
+
+ m_rp = m_rt->newCompatibleRenderPassDescriptor();
+ m_rt->setRenderPassDescriptor(m_rp);
+
+ m_rt->build();
+
+ m_offscreenTriangle.setRhi(m_r);
+ m_offscreenTriangle.initResources(m_rp);
+ m_offscreenTriangle.setScale(2);
+ // m_tex and the offscreen triangle are never multisample
+}
+
+void TriangleOnCubeRenderer::resize(const QSize &pixelSize)
+{
+ m_proj = m_r->clipSpaceCorrMatrix();
+ m_proj.perspective(45.0f, pixelSize.width() / (float) pixelSize.height(), 0.01f, 100.0f);
+ m_proj.translate(0, 0, -4);
+
+ m_offscreenTriangle.resize(pixelSize);
+}
+
+void TriangleOnCubeRenderer::releaseResources()
+{
+ m_offscreenTriangle.releaseResources();
+
+ delete m_ps;
+ m_ps = nullptr;
+
+ delete m_srb;
+ m_srb = nullptr;
+
+ delete m_rt;
+ m_rt = nullptr;
+
+ delete m_rp;
+ m_rp = nullptr;
+
+ delete m_sampler;
+ m_sampler = nullptr;
+
+ delete m_depthTex;
+ m_depthTex = nullptr;
+
+ delete m_tex2;
+ m_tex2 = nullptr;
+
+ delete m_tex;
+ m_tex = nullptr;
+
+ delete m_ds;
+ m_ds = nullptr;
+
+ delete m_ubuf;
+ m_ubuf = nullptr;
+
+ delete m_vbuf;
+ m_vbuf = nullptr;
+}
+
+void TriangleOnCubeRenderer::queueResourceUpdates(QRhiResourceUpdateBatch *resourceUpdates)
+{
+ if (!m_vbufReady) {
+ m_vbufReady = true;
+ resourceUpdates->uploadStaticBuffer(m_vbuf, cube);
+ qint32 flip = m_r->isYUpInFramebuffer() ? 1 : 0;
+ resourceUpdates->updateDynamicBuffer(m_ubuf, 64, 4, &flip);
+ }
+
+ m_rotation += 1.0f;
+ QMatrix4x4 mvp = m_proj;
+ mvp.translate(m_translation);
+ mvp.scale(0.5f);
+ mvp.rotate(m_rotation, 1, 0, 0);
+ resourceUpdates->updateDynamicBuffer(m_ubuf, 0, 64, mvp.constData());
+
+ // ###
+// if (DEPTH_TEXTURE) {
+// // m_tex is basically undefined here, be nice and transition the layout properly at least
+// resourceUpdates->prepareTextureForUse(m_tex, QRhiResourceUpdateBatch::TextureRead);
+// }
+}
+
+void TriangleOnCubeRenderer::queueOffscreenPass(QRhiCommandBuffer *cb)
+{
+ QRhiResourceUpdateBatch *u = m_r->nextResourceUpdateBatch();
+ m_offscreenTriangle.queueResourceUpdates(u);
+
+ if (IMAGE_UNDER_OFFSCREEN_RENDERING && !m_image.isNull()) {
+ u->uploadTexture(m_tex, m_image);
+ if (!UPLOAD_UNDERLAY_ON_EVERY_FRAME)
+ m_image = QImage();
+ }
+
+ cb->beginPass(m_rt, QColor::fromRgbF(0.0f, 0.4f, 0.7f, 1.0f), { 1.0f, 0 }, u);
+ m_offscreenTriangle.queueDraw(cb, OFFSCREEN_SIZE);
+ cb->endPass();
+}
+
+void TriangleOnCubeRenderer::queueDraw(QRhiCommandBuffer *cb, const QSize &outputSizeInPixels)
+{
+ cb->setGraphicsPipeline(m_ps);
+ cb->setViewport(QRhiViewport(0, 0, outputSizeInPixels.width(), outputSizeInPixels.height()));
+ cb->setShaderResources();
+ const QRhiCommandBuffer::VertexInput vbufBindings[] = {
+ { m_vbuf, 0 },
+ { m_vbuf, 36 * 3 * sizeof(float) }
+ };
+ cb->setVertexInput(0, 2, vbufBindings);
+ cb->draw(36);
+}
diff --git a/tests/manual/rhi/triquadcube/triangleoncuberenderer.h b/tests/manual/rhi/triquadcube/triangleoncuberenderer.h
new file mode 100644
index 0000000000..5a56bf6bd1
--- /dev/null
+++ b/tests/manual/rhi/triquadcube/triangleoncuberenderer.h
@@ -0,0 +1,95 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef TRIANGLEONCUBERENDERER_H
+#define TRIANGLEONCUBERENDERER_H
+
+#include "trianglerenderer.h"
+
+class TriangleOnCubeRenderer
+{
+public:
+ void setRhi(QRhi *r) { m_r = r; }
+ void setSampleCount(int samples) { m_sampleCount = samples; }
+ void setTranslation(const QVector3D &v) { m_translation = v; }
+ void initResources(QRhiRenderPassDescriptor *rp);
+ void releaseResources();
+ void resize(const QSize &pixelSize);
+ void queueResourceUpdates(QRhiResourceUpdateBatch *resourceUpdates);
+ void queueOffscreenPass(QRhiCommandBuffer *cb);
+ void queueDraw(QRhiCommandBuffer *cb, const QSize &outputSizeInPixels);
+
+private:
+ QRhi *m_r;
+
+ QRhiBuffer *m_vbuf = nullptr;
+ bool m_vbufReady = false;
+ QRhiBuffer *m_ubuf = nullptr;
+ QRhiTexture *m_tex = nullptr;
+ QRhiRenderBuffer *m_ds = nullptr;
+ QRhiTexture *m_tex2 = nullptr;
+ QRhiTexture *m_depthTex = nullptr;
+ QRhiSampler *m_sampler = nullptr;
+ QRhiTextureRenderTarget *m_rt = nullptr;
+ QRhiRenderPassDescriptor *m_rp = nullptr;
+ QRhiShaderResourceBindings *m_srb = nullptr;
+ QRhiGraphicsPipeline *m_ps = nullptr;
+
+ QVector3D m_translation;
+ QMatrix4x4 m_proj;
+ float m_rotation = 0;
+ int m_sampleCount = 1; // no MSAA by default
+
+ TriangleRenderer m_offscreenTriangle;
+
+ QImage m_image;
+};
+
+#endif
diff --git a/tests/manual/rhi/triquadcube/trianglerenderer.cpp b/tests/manual/rhi/triquadcube/trianglerenderer.cpp
new file mode 100644
index 0000000000..aac76d4110
--- /dev/null
+++ b/tests/manual/rhi/triquadcube/trianglerenderer.cpp
@@ -0,0 +1,202 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "trianglerenderer.h"
+#include <QFile>
+#include <QtGui/private/qshader_p.h>
+
+//#define VBUF_IS_DYNAMIC
+
+static float vertexData[] = { // Y up (note m_proj), CCW
+ 0.0f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
+ -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
+ 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f
+};
+
+static QShader getShader(const QString &name)
+{
+ QFile f(name);
+ if (f.open(QIODevice::ReadOnly))
+ return QShader::fromSerialized(f.readAll());
+
+ return QShader();
+}
+
+void TriangleRenderer::initResources(QRhiRenderPassDescriptor *rp)
+{
+#ifdef VBUF_IS_DYNAMIC
+ m_vbuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::VertexBuffer, sizeof(vertexData));
+#else
+ m_vbuf = m_r->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData));
+#endif
+ m_vbuf->setName(QByteArrayLiteral("Triangle vbuf"));
+ m_vbuf->build();
+ m_vbufReady = false;
+
+ m_ubuf = m_r->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68);
+ m_ubuf->setName(QByteArrayLiteral("Triangle ubuf"));
+ m_ubuf->build();
+
+ m_srb = m_r->newShaderResourceBindings();
+ m_srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, m_ubuf)
+ });
+ m_srb->build();
+
+ m_ps = m_r->newGraphicsPipeline();
+
+ QRhiGraphicsPipeline::TargetBlend premulAlphaBlend; // convenient defaults...
+ premulAlphaBlend.enable = true;
+ QVector<QRhiGraphicsPipeline::TargetBlend> rtblends;
+ for (int i = 0; i < m_colorAttCount; ++i)
+ rtblends << premulAlphaBlend;
+
+ m_ps->setTargetBlends(rtblends);
+ m_ps->setSampleCount(m_sampleCount);
+
+ if (m_depthWrite) { // TriangleOnCube may want to exercise this
+ m_ps->setDepthTest(true);
+ m_ps->setDepthOp(QRhiGraphicsPipeline::Always);
+ m_ps->setDepthWrite(true);
+ }
+
+ QShader vs = getShader(QLatin1String(":/color.vert.qsb"));
+ Q_ASSERT(vs.isValid());
+ QShader fs = getShader(QLatin1String(":/color.frag.qsb"));
+ Q_ASSERT(fs.isValid());
+ m_ps->setShaderStages({
+ { QRhiGraphicsShaderStage::Vertex, vs },
+ { QRhiGraphicsShaderStage::Fragment, fs }
+ });
+
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 7 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) }
+ });
+
+ m_ps->setVertexInputLayout(inputLayout);
+ m_ps->setShaderResourceBindings(m_srb);
+ m_ps->setRenderPassDescriptor(rp);
+
+ m_ps->build();
+}
+
+void TriangleRenderer::resize(const QSize &pixelSize)
+{
+ m_proj = m_r->clipSpaceCorrMatrix();
+ m_proj.perspective(45.0f, pixelSize.width() / (float) pixelSize.height(), 0.01f, 100.0f);
+ m_proj.translate(0, 0, -4);
+}
+
+void TriangleRenderer::releaseResources()
+{
+ delete m_ps;
+ m_ps = nullptr;
+
+ delete m_srb;
+ m_srb = nullptr;
+
+ delete m_ubuf;
+ m_ubuf = nullptr;
+
+ delete m_vbuf;
+ m_vbuf = nullptr;
+}
+
+void TriangleRenderer::queueResourceUpdates(QRhiResourceUpdateBatch *resourceUpdates)
+{
+#if 0
+ static int messWithBufferTrigger = 0;
+ // recreate the underlying VkBuffer every second frame
+ // to exercise setShaderResources' built-in smartness
+ if (!(messWithBufferTrigger & 1)) {
+ m_ubuf->release();
+ m_ubuf->build();
+ }
+ ++messWithBufferTrigger;
+#endif
+
+ if (!m_vbufReady) {
+ m_vbufReady = true;
+#ifdef VBUF_IS_DYNAMIC
+ resourceUpdates->updateDynamicBuffer(m_vbuf, 0, m_vbuf->size(), vertexData);
+#else
+ resourceUpdates->uploadStaticBuffer(m_vbuf, vertexData);
+#endif
+ }
+
+ m_rotation += 1.0f;
+ QMatrix4x4 mvp = m_proj;
+ mvp.translate(m_translation);
+ mvp.scale(m_scale);
+ mvp.rotate(m_rotation, 0, 1, 0);
+ resourceUpdates->updateDynamicBuffer(m_ubuf, 0, 64, mvp.constData());
+
+ 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, 64, 4, &m_opacity);
+}
+
+void TriangleRenderer::queueDraw(QRhiCommandBuffer *cb, const QSize &outputSizeInPixels)
+{
+ cb->setGraphicsPipeline(m_ps);
+ cb->setViewport(QRhiViewport(0, 0, outputSizeInPixels.width(), outputSizeInPixels.height()));
+ cb->setShaderResources();
+ const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf, 0);
+ cb->setVertexInput(0, 1, &vbufBinding);
+ cb->draw(3);
+}
diff --git a/tests/manual/rhi/triquadcube/trianglerenderer.h b/tests/manual/rhi/triquadcube/trianglerenderer.h
new file mode 100644
index 0000000000..53d6926a62
--- /dev/null
+++ b/tests/manual/rhi/triquadcube/trianglerenderer.h
@@ -0,0 +1,93 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef TRIANGLERENDERER_H
+#define TRIANGLERENDERER_H
+
+#include <QtGui/private/qrhi_p.h>
+
+class TriangleRenderer
+{
+public:
+ void setRhi(QRhi *r) { m_r = r; }
+ void setSampleCount(int samples) { m_sampleCount = samples; }
+ int sampleCount() const { return m_sampleCount; }
+ void setTranslation(const QVector3D &v) { m_translation = v; }
+ void setScale(float f) { m_scale = f; }
+ void setDepthWrite(bool enable) { m_depthWrite = enable; }
+ void setColorAttCount(int count) { m_colorAttCount = count; }
+ QRhiGraphicsPipeline *pipeline() const { return m_ps; }
+ void initResources(QRhiRenderPassDescriptor *rp);
+ void releaseResources();
+ void resize(const QSize &pixelSize);
+ void queueResourceUpdates(QRhiResourceUpdateBatch *resourceUpdates);
+ void queueDraw(QRhiCommandBuffer *cb, const QSize &outputSizeInPixels);
+
+private:
+ QRhi *m_r;
+
+ QRhiBuffer *m_vbuf = nullptr;
+ bool m_vbufReady = false;
+ QRhiBuffer *m_ubuf = nullptr;
+ QRhiShaderResourceBindings *m_srb = nullptr;
+ QRhiGraphicsPipeline *m_ps = nullptr;
+
+ QVector3D m_translation;
+ float m_scale = 1;
+ bool m_depthWrite = false;
+ int m_colorAttCount = 1;
+ QMatrix4x4 m_proj;
+ float m_rotation = 0;
+ float m_opacity = 1;
+ int m_opacityDir = -1;
+ int m_sampleCount = 1; // no MSAA by default
+};
+
+#endif
diff --git a/tests/manual/rhi/triquadcube/triquadcube.cpp b/tests/manual/rhi/triquadcube/triquadcube.cpp
new file mode 100644
index 0000000000..741e64be0a
--- /dev/null
+++ b/tests/manual/rhi/triquadcube/triquadcube.cpp
@@ -0,0 +1,271 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+** * Redistributions of source code must retain the above copyright
+** notice, this list of conditions and the following disclaimer.
+** * Redistributions in binary form must reproduce the above copyright
+** notice, this list of conditions and the following disclaimer in
+** the documentation and/or other materials provided with the
+** distribution.
+** * Neither the name of The Qt Company Ltd nor the names of its
+** contributors may be used to endorse or promote products derived
+** from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+// An example exercising more than a single feature. Enables profiling
+// (resource logging to a file) and inserts debug markers and sets some
+// object names. Can also be used to test MSAA swapchains, swapchain image
+// readback, requesting an sRGB swapchain, and some texture features.
+
+#define EXAMPLEFW_PREINIT
+#include "../shared/examplefw.h"
+#include "trianglerenderer.h"
+#include "quadrenderer.h"
+#include "texturedcuberenderer.h"
+#include "triangleoncuberenderer.h"
+
+#include <QFileInfo>
+#include <QFile>
+#include <QtGui/private/qrhiprofiler_p.h>
+
+#define PROFILE_TO_FILE
+//#define SKIP_PRESENT
+//#define USE_MSAA
+//#define USE_SRGB_SWAPCHAIN
+//#define READBACK_SWAPCHAIN
+//#define NO_VSYNC
+//#define USE_MIN_SWAPCHAIN_BUFFERS
+
+struct {
+ TriangleRenderer triRenderer;
+ QuadRenderer quadRenderer;
+ TexturedCubeRenderer cubeRenderer;
+ TriangleOnCubeRenderer liveTexCubeRenderer;
+ bool onScreenOnly = false;
+ bool triangleOnly = false;
+ QSize lastOutputSize;
+ int frameCount = 0;
+ QFile profOut;
+} d;
+
+void preInit()
+{
+#ifdef PROFILE_TO_FILE
+ rhiFlags |= QRhi::EnableProfiling;
+ const QString profFn = QFileInfo(QLatin1String("rhiprof.txt")).absoluteFilePath();
+ qDebug("Writing profiling output to %s", qPrintable(profFn));
+ d.profOut.setFileName(profFn);
+ d.profOut.open(QIODevice::WriteOnly);
+#endif
+
+#ifdef SKIP_PRESENT
+ endFrameFlags |= QRhi::SkipPresent;
+#endif
+
+#ifdef USE_MSAA
+ sampleCount = 4; // enable 4x MSAA (except for the render-to-texture pass)
+#endif
+
+#ifdef READBACK_SWAPCHAIN
+ scFlags |= QRhiSwapChain::UsedAsTransferSource;
+#endif
+
+#ifdef USE_SRGB_SWAPCHAIN
+ scFlags |= QRhiSwapChain::sRGB;
+#endif
+
+#ifdef NO_VSYNC
+ scFlags |= QRhiSwapChain::NoVSync;
+#endif
+
+#ifdef USE_MIN_SWAPCHAIN_BUFFERS
+ scFlags |= QRhiSwapChain::MinimalBufferCount;
+#endif
+
+ // For OpenGL some of these are incorporated into the QSurfaceFormat by
+ // examplefw.h after returning from here as that is out of the RHI's control.
+}
+
+void Window::customInit()
+{
+#ifdef PROFILE_TO_FILE
+ m_r->profiler()->setDevice(&d.profOut);
+#endif
+
+ d.triRenderer.setRhi(m_r);
+ d.triRenderer.setSampleCount(sampleCount);
+ d.triRenderer.initResources(m_rp);
+
+ if (!d.triangleOnly) {
+ d.triRenderer.setTranslation(QVector3D(0, 0.5f, 0));
+
+ d.quadRenderer.setRhi(m_r);
+ d.quadRenderer.setSampleCount(sampleCount);
+ d.quadRenderer.setPipeline(d.triRenderer.pipeline());
+ d.quadRenderer.initResources(m_rp);
+ d.quadRenderer.setTranslation(QVector3D(1.5f, -0.5f, 0));
+
+ d.cubeRenderer.setRhi(m_r);
+ d.cubeRenderer.setSampleCount(sampleCount);
+ d.cubeRenderer.initResources(m_rp);
+ d.cubeRenderer.setTranslation(QVector3D(0, -0.5f, 0));
+ }
+
+ if (!d.onScreenOnly) {
+ d.liveTexCubeRenderer.setRhi(m_r);
+ d.liveTexCubeRenderer.setSampleCount(sampleCount);
+ d.liveTexCubeRenderer.initResources(m_rp);
+ d.liveTexCubeRenderer.setTranslation(QVector3D(-2.0f, 0, 0));
+ }
+
+ // Put the gpu mem allocator statistics to the profiling stream after doing
+ // all the init. (where applicable)
+ m_r->profiler()->addVMemAllocatorStats();
+
+ // Check some features/limits.
+ qDebug("isFeatureSupported(MultisampleTexture): %d", m_r->isFeatureSupported(QRhi::MultisampleTexture));
+ qDebug("isFeatureSupported(MultisampleRenderBuffer): %d", m_r->isFeatureSupported(QRhi::MultisampleRenderBuffer));
+ qDebug("isFeatureSupported(DebugMarkers): %d", m_r->isFeatureSupported(QRhi::DebugMarkers));
+ qDebug("isFeatureSupported(Timestamps): %d", m_r->isFeatureSupported(QRhi::Timestamps));
+ qDebug("isFeatureSupported(Instancing): %d", m_r->isFeatureSupported(QRhi::Instancing));
+ qDebug("isFeatureSupported(CustomInstanceStepRate): %d", m_r->isFeatureSupported(QRhi::CustomInstanceStepRate));
+ qDebug("isFeatureSupported(PrimitiveRestart): %d", m_r->isFeatureSupported(QRhi::PrimitiveRestart));
+ qDebug("isFeatureSupported(NonDynamicUniformBuffers): %d", m_r->isFeatureSupported(QRhi::NonDynamicUniformBuffers));
+ qDebug("isFeatureSupported(NonFourAlignedEffectiveIndexBufferOffset): %d", m_r->isFeatureSupported(QRhi::NonFourAlignedEffectiveIndexBufferOffset));
+ qDebug("isFeatureSupported(NPOTTextureRepeat): %d", m_r->isFeatureSupported(QRhi::NPOTTextureRepeat));
+ qDebug("isFeatureSupported(RedOrAlpha8IsRed): %d", m_r->isFeatureSupported(QRhi::RedOrAlpha8IsRed));
+ qDebug("isFeatureSupported(ElementIndexUint): %d", m_r->isFeatureSupported(QRhi::ElementIndexUint));
+ qDebug("Min 2D texture width/height: %d", m_r->resourceLimit(QRhi::TextureSizeMin));
+ qDebug("Max 2D texture width/height: %d", m_r->resourceLimit(QRhi::TextureSizeMax));
+ qDebug("Max color attachment count: %d", m_r->resourceLimit(QRhi::MaxColorAttachments));
+}
+
+void Window::customRelease()
+{
+ d.triRenderer.releaseResources();
+
+ if (!d.triangleOnly) {
+ d.quadRenderer.releaseResources();
+ d.cubeRenderer.releaseResources();
+ }
+
+ if (!d.onScreenOnly)
+ d.liveTexCubeRenderer.releaseResources();
+}
+
+void Window::customRender()
+{
+ const QSize outputSize = m_sc->currentPixelSize();
+ QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
+
+ if (outputSize != d.lastOutputSize) {
+ d.triRenderer.resize(outputSize);
+ if (!d.triangleOnly) {
+ d.quadRenderer.resize(outputSize);
+ d.cubeRenderer.resize(outputSize);
+ }
+ if (!d.onScreenOnly)
+ d.liveTexCubeRenderer.resize(outputSize);
+ d.lastOutputSize = outputSize;
+ }
+
+ if (!d.onScreenOnly) {
+ cb->debugMarkBegin("Offscreen triangle pass");
+ d.liveTexCubeRenderer.queueOffscreenPass(cb);
+ cb->debugMarkEnd();
+ }
+
+ QRhiResourceUpdateBatch *u = m_r->nextResourceUpdateBatch();
+ d.triRenderer.queueResourceUpdates(u);
+ if (!d.triangleOnly) {
+ d.quadRenderer.queueResourceUpdates(u);
+ d.cubeRenderer.queueResourceUpdates(u);
+ }
+ if (!d.onScreenOnly)
+ d.liveTexCubeRenderer.queueResourceUpdates(u);
+
+ cb->beginPass(m_sc->currentFrameRenderTarget(), QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f), { 1.0f, 0 }, u);
+ cb->debugMarkBegin(QByteArrayLiteral("Triangle"));
+ d.triRenderer.queueDraw(cb, outputSize);
+ cb->debugMarkEnd();
+ if (!d.triangleOnly) {
+ cb->debugMarkMsg(QByteArrayLiteral("More stuff"));
+ cb->debugMarkBegin(QByteArrayLiteral("Quad"));
+ d.quadRenderer.queueDraw(cb, outputSize);
+ cb->debugMarkEnd();
+ cb->debugMarkBegin(QByteArrayLiteral("Cube"));
+ d.cubeRenderer.queueDraw(cb, outputSize);
+ cb->debugMarkEnd();
+ }
+ if (!d.onScreenOnly) {
+ cb->debugMarkMsg(QByteArrayLiteral("Even more stuff"));
+ cb->debugMarkBegin(QByteArrayLiteral("Cube with offscreen triangle"));
+ d.liveTexCubeRenderer.queueDraw(cb, outputSize);
+ cb->debugMarkEnd();
+ }
+
+ QRhiResourceUpdateBatch *passEndUpdates = nullptr;
+#ifdef READBACK_SWAPCHAIN
+ passEndUpdates = m_r->nextResourceUpdateBatch();
+ QRhiReadbackDescription rb; // no texture given -> backbuffer
+ QRhiReadbackResult *rbResult = new QRhiReadbackResult;
+ int frameNo = d.frameCount;
+ rbResult->completed = [this, rbResult, frameNo] {
+ {
+ QImage::Format fmt = rbResult->format == QRhiTexture::BGRA8 ? QImage::Format_ARGB32_Premultiplied
+ : QImage::Format_RGBA8888_Premultiplied;
+ const uchar *p = reinterpret_cast<const uchar *>(rbResult->data.constData());
+ QImage image(p, rbResult->pixelSize.width(), rbResult->pixelSize.height(), fmt);
+ QString fn = QString::asprintf("frame%d.png", frameNo);
+ fn = QFileInfo(fn).absoluteFilePath();
+ qDebug("Saving into %s", qPrintable(fn));
+ if (m_r->isYUpInFramebuffer())
+ image.mirrored().save(fn);
+ else
+ image.save(fn);
+ }
+ delete rbResult;
+ };
+ passEndUpdates->readBackTexture(rb, rbResult);
+#endif
+
+ cb->endPass(passEndUpdates);
+
+ d.frameCount += 1;
+}
diff --git a/tests/manual/rhi/triquadcube/triquadcube.pro b/tests/manual/rhi/triquadcube/triquadcube.pro
new file mode 100644
index 0000000000..0002b3cad1
--- /dev/null
+++ b/tests/manual/rhi/triquadcube/triquadcube.pro
@@ -0,0 +1,18 @@
+TEMPLATE = app
+
+QT += gui-private
+
+SOURCES = \
+ triquadcube.cpp \
+ trianglerenderer.cpp \
+ quadrenderer.cpp \
+ texturedcuberenderer.cpp \
+ triangleoncuberenderer.cpp
+
+HEADERS = \
+ trianglerenderer.h \
+ quadrenderer.h \
+ texturedcuberenderer.h \
+ triangleoncuberenderer.h
+
+RESOURCES = triquadcube.qrc
diff --git a/tests/manual/rhi/triquadcube/triquadcube.qrc b/tests/manual/rhi/triquadcube/triquadcube.qrc
new file mode 100644
index 0000000000..41b72edc41
--- /dev/null
+++ b/tests/manual/rhi/triquadcube/triquadcube.qrc
@@ -0,0 +1,9 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+ <file alias="color.vert.qsb">../shared/color.vert.qsb</file>
+ <file alias="color.frag.qsb">../shared/color.frag.qsb</file>
+ <file alias="texture.vert.qsb">../shared/texture.vert.qsb</file>
+ <file alias="texture.frag.qsb">../shared/texture.frag.qsb</file>
+ <file alias="qt256.png">../shared/qt256.png</file>
+</qresource>
+</RCC>