summaryrefslogtreecommitdiffstats
path: root/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tests/auto/gui/rhi/qrhi/tst_qrhi.cpp')
-rw-r--r--tests/auto/gui/rhi/qrhi/tst_qrhi.cpp2954
1 files changed, 2711 insertions, 243 deletions
diff --git a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp
index 5142d5566f..8929b69cec 100644
--- a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp
+++ b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp
@@ -1,62 +1,36 @@
-/****************************************************************************
-**
-** Copyright (C) 2019 The Qt Company Ltd.
-** Contact: https://www.qt.io/licensing/
-**
-** This file is part of the test suite of the Qt Toolkit.
-**
-** $QT_BEGIN_LICENSE:GPL-EXCEPT$
-** 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.
-**
-** GNU General Public License Usage
-** Alternatively, this file may be used under the terms of the GNU
-** General Public License version 3 as published by the Free Software
-** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
-** included in the packaging of this file. Please review the following
-** information to ensure the GNU General Public License requirements will
-** be met: https://www.gnu.org/licenses/gpl-3.0.html.
-**
-** $QT_END_LICENSE$
-**
-****************************************************************************/
+// Copyright (C) 2019 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QTest>
#include <QThread>
#include <QFile>
#include <QOffscreenSurface>
#include <QPainter>
+#include <qrgbafloat.h>
+#include <qrgba64.h>
-#include <QtGui/private/qrhi_p.h>
-#include <QtGui/private/qrhi_p_p.h>
-#include <QtGui/private/qrhinull_p.h>
+#include <private/qrhi_p.h>
#if QT_CONFIG(opengl)
# include <QOpenGLContext>
# include <QOpenGLFunctions>
-# include <QtGui/private/qrhigles2_p.h>
+# include <QtGui/private/qguiapplication_p.h>
+# include <qpa/qplatformintegration.h>
# define TST_GL
#endif
#if QT_CONFIG(vulkan)
# include <QVulkanInstance>
# include <QVulkanFunctions>
-# include <QtGui/private/qrhivulkan_p.h>
# define TST_VK
#endif
#ifdef Q_OS_WIN
-#include <QtGui/private/qrhid3d11_p.h>
# define TST_D3D11
+# define TST_D3D12
#endif
-#if defined(Q_OS_MACOS) || defined(Q_OS_IOS)
-# include <QtGui/private/qrhimetal_p.h>
+#if QT_CONFIG(metal)
# define TST_MTL
#endif
@@ -72,9 +46,10 @@ private slots:
void cleanupTestCase();
void rhiTestData();
- void rhiTestDataOpenGL();
void create_data();
void create();
+ void stats_data();
+ void stats();
void nativeHandles_data();
void nativeHandles();
void nativeHandlesImportVulkan();
@@ -96,6 +71,8 @@ private slots:
void resourceUpdateBatchTextureRawDataStride();
void resourceUpdateBatchLotsOfResources_data();
void resourceUpdateBatchLotsOfResources();
+ void resourceUpdateBatchBetweenFrames_data();
+ void resourceUpdateBatchBetweenFrames();
void invalidPipeline_data();
void invalidPipeline();
void srbLayoutCompatibility_data();
@@ -106,6 +83,8 @@ private slots:
void renderPassDescriptorCompatibility();
void renderPassDescriptorClone_data();
void renderPassDescriptorClone();
+ void textureWithSampleCount_data();
+ void textureWithSampleCount();
void renderToTextureSimple_data();
void renderToTextureSimple();
@@ -117,6 +96,8 @@ private slots:
void renderToTextureTextureArray();
void renderToTextureTexturedQuad_data();
void renderToTextureTexturedQuad();
+ void renderToTextureSampleWithSeparateTextureAndSampler_data();
+ void renderToTextureSampleWithSeparateTextureAndSampler();
void renderToTextureArrayOfTexturedQuad_data();
void renderToTextureArrayOfTexturedQuad();
void renderToTextureTexturedQuadAndUniformBuffer_data();
@@ -125,30 +106,57 @@ private slots:
void renderToTextureTexturedQuadAllDynamicBuffers();
void renderToTextureDeferredSrb_data();
void renderToTextureDeferredSrb();
+ void renderToTextureDeferredUpdateSamplerInSrb_data();
+ void renderToTextureDeferredUpdateSamplerInSrb();
void renderToTextureMultipleUniformBuffersAndDynamicOffset_data();
void renderToTextureMultipleUniformBuffersAndDynamicOffset();
void renderToTextureSrbReuse_data();
void renderToTextureSrbReuse();
void renderToTextureIndexedDraw_data();
void renderToTextureIndexedDraw();
+ void renderToTextureArrayMultiView_data();
+ void renderToTextureArrayMultiView();
void renderToWindowSimple_data();
void renderToWindowSimple();
void finishWithinSwapchainFrame_data();
void finishWithinSwapchainFrame();
void resourceUpdateBatchBufferTextureWithSwapchainFrames_data();
void resourceUpdateBatchBufferTextureWithSwapchainFrames();
+ void textureRenderTargetAutoRebuild_data();
+ void textureRenderTargetAutoRebuild();
void pipelineCache_data();
void pipelineCache();
- void textureImportOpenGL_data();
void textureImportOpenGL();
- void renderbufferImportOpenGL_data();
void renderbufferImportOpenGL();
void threeDimTexture_data();
void threeDimTexture();
+ void oneDimTexture_data();
+ void oneDimTexture();
void leakedResourceDestroy_data();
void leakedResourceDestroy();
+ void renderToFloatTexture_data();
+ void renderToFloatTexture();
+ void renderToRgb10Texture_data();
+ void renderToRgb10Texture();
+
+ void tessellation_data();
+ void tessellation();
+
+ void tessellationInterfaceBlocks_data();
+ void tessellationInterfaceBlocks();
+
+ void storageBuffer_data();
+ void storageBuffer();
+ void storageBufferRuntimeSizeCompute_data();
+ void storageBufferRuntimeSizeCompute();
+ void storageBufferRuntimeSizeGraphics_data();
+ void storageBufferRuntimeSizeGraphics();
+
+ void halfPrecisionAttributes_data();
+ void halfPrecisionAttributes();
+
private:
void setWindowType(QWindow *window, QRhi::Implementation impl);
@@ -161,7 +169,10 @@ private:
QRhiVulkanInitParams vk;
#endif
#ifdef TST_D3D11
- QRhiD3D11InitParams d3d;
+ QRhiD3D11InitParams d3d11;
+#endif
+#ifdef TST_D3D12
+ QRhiD3D12InitParams d3d12;
#endif
#ifdef TST_MTL
QRhiMetalInitParams mtl;
@@ -177,6 +188,12 @@ private:
void tst_QRhi::initTestCase()
{
#ifdef TST_GL
+ QSurfaceFormat fmt;
+ fmt.setDepthBufferSize(24);
+ fmt.setStencilBufferSize(8);
+ QSurfaceFormat::setDefaultFormat(fmt);
+
+ initParams.gl.format = QSurfaceFormat::defaultFormat();
fallbackSurface = QRhiGles2InitParams::newFallbackSurface();
initParams.gl.fallbackSurface = fallbackSurface;
#endif
@@ -186,7 +203,7 @@ void tst_QRhi::initTestCase()
if (supportedVersion >= QVersionNumber(1, 2))
vulkanInstance.setApiVersion(QVersionNumber(1, 2));
else if (supportedVersion >= QVersionNumber(1, 1))
- vulkanInstance.setApiVersion(QVersionNumber(1, 2));
+ vulkanInstance.setApiVersion(QVersionNumber(1, 1));
vulkanInstance.setLayers({ "VK_LAYER_KHRONOS_validation" });
vulkanInstance.setExtensions(QRhiVulkanInitParams::preferredInstanceExtensions());
vulkanInstance.create();
@@ -194,7 +211,10 @@ void tst_QRhi::initTestCase()
#endif
#ifdef TST_D3D11
- initParams.d3d.enableDebugLayer = true;
+ initParams.d3d11.enableDebugLayer = true;
+#endif
+#ifdef TST_D3D12
+ initParams.d3d12.enableDebugLayer = true;
#endif
}
@@ -212,32 +232,29 @@ void tst_QRhi::rhiTestData()
QTest::addColumn<QRhi::Implementation>("impl");
QTest::addColumn<QRhiInitParams *>("initParams");
+// webOS does not support raster (software) pipeline
+#ifndef Q_OS_WEBOS
QTest::newRow("Null") << QRhi::Null << static_cast<QRhiInitParams *>(&initParams.null);
+#endif
#ifdef TST_GL
- QTest::newRow("OpenGL") << QRhi::OpenGLES2 << static_cast<QRhiInitParams *>(&initParams.gl);
+ if (QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::OpenGL))
+ QTest::newRow("OpenGL") << QRhi::OpenGLES2 << static_cast<QRhiInitParams *>(&initParams.gl);
#endif
#ifdef TST_VK
if (vulkanInstance.isValid())
QTest::newRow("Vulkan") << QRhi::Vulkan << static_cast<QRhiInitParams *>(&initParams.vk);
#endif
#ifdef TST_D3D11
- QTest::newRow("Direct3D 11") << QRhi::D3D11 << static_cast<QRhiInitParams *>(&initParams.d3d);
+ QTest::newRow("Direct3D 11") << QRhi::D3D11 << static_cast<QRhiInitParams *>(&initParams.d3d11);
+#endif
+#ifdef TST_D3D12
+ QTest::newRow("Direct3D 12") << QRhi::D3D12 << static_cast<QRhiInitParams *>(&initParams.d3d12);
#endif
#ifdef TST_MTL
QTest::newRow("Metal") << QRhi::Metal << static_cast<QRhiInitParams *>(&initParams.mtl);
#endif
}
-void tst_QRhi::rhiTestDataOpenGL()
-{
- QTest::addColumn<QRhi::Implementation>("impl");
- QTest::addColumn<QRhiInitParams *>("initParams");
-
-#ifdef TST_GL
- QTest::newRow("OpenGL") << QRhi::OpenGLES2 << static_cast<QRhiInitParams *>(&initParams.gl);
-#endif
-}
-
void tst_QRhi::create_data()
{
rhiTestData();
@@ -260,10 +277,13 @@ void tst_QRhi::create()
QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
if (rhi) {
+ QVERIFY(QRhi::probe(impl, initParams));
+
qDebug() << rhi->driverInfo();
QCOMPARE(rhi->backend(), impl);
QVERIFY(strcmp(rhi->backendName(), ""));
+ QVERIFY(!strcmp(rhi->backendName(), QRhi::backendName(rhi->backend())));
QVERIFY(!rhi->driverInfo().deviceName.isEmpty());
QCOMPARE(rhi->thread(), QThread::currentThread());
@@ -298,8 +318,13 @@ void tst_QRhi::create()
QVERIFY(resUpd);
resUpd->release();
- QVERIFY(!rhi->supportedSampleCounts().isEmpty());
- QVERIFY(rhi->supportedSampleCounts().contains(1));
+ const QVector<int> supportedSampleCounts = rhi->supportedSampleCounts();
+ QVERIFY(!supportedSampleCounts.isEmpty());
+ QVERIFY(supportedSampleCounts.contains(1));
+ for (int i = 1; i < supportedSampleCounts.count(); ++i) {
+ // Verify the list is sorted. Internally the backends rely on this.
+ QVERIFY(supportedSampleCounts[i] > supportedSampleCounts[i - 1]);
+ }
QVERIFY(rhi->ubufAlignment() > 0);
QCOMPARE(rhi->ubufAligned(123), aligned(123, rhi->ubufAlignment()));
@@ -342,6 +367,9 @@ void tst_QRhi::create()
const int framesInFlight = rhi->resourceLimit(QRhi::FramesInFlight);
const int texArrayMax = rhi->resourceLimit(QRhi::TextureArraySizeMax);
const int uniBufRangeMax = rhi->resourceLimit(QRhi::MaxUniformBufferRange);
+ const int maxVertexInputs = rhi->resourceLimit(QRhi::MaxVertexInputs);
+ const int maxVertexOutputs = rhi->resourceLimit(QRhi::MaxVertexOutputs);
+
QVERIFY(texMin >= 1);
QVERIFY(texMax >= texMin);
QVERIFY(maxAtt >= 1);
@@ -349,9 +377,10 @@ void tst_QRhi::create()
if (rhi->isFeatureSupported(QRhi::TextureArrays))
QVERIFY(texArrayMax > 1);
QVERIFY(uniBufRangeMax >= 224 * 4 * 4);
+ QVERIFY(maxVertexInputs >= 8);
+ QVERIFY(maxVertexOutputs >= 8);
QVERIFY(rhi->nativeHandles());
- QVERIFY(rhi->profiler());
const QRhi::Feature features[] = {
QRhi::MultisampleTexture,
@@ -382,7 +411,21 @@ void tst_QRhi::create()
QRhi::PipelineCacheDataLoadSave,
QRhi::ImageDataStride,
QRhi::RenderBufferImport,
- QRhi::ThreeDimensionalTextures
+ QRhi::ThreeDimensionalTextures,
+ QRhi::RenderTo3DTextureSlice,
+ QRhi::TextureArrays,
+ QRhi::Tessellation,
+ QRhi::GeometryShader,
+ QRhi::TextureArrayRange,
+ QRhi::NonFillPolygonMode,
+ QRhi::OneDimensionalTextures,
+ QRhi::OneDimensionalTextureMipmaps,
+ QRhi::HalfAttributes,
+ QRhi::RenderToOneDimensionalTexture,
+ QRhi::ThreeDimensionalTextureMipmaps,
+ QRhi::MultiView,
+ QRhi::TextureViewFormat,
+ QRhi::ResolveDepthStencil
};
for (size_t i = 0; i <sizeof(features) / sizeof(QRhi::Feature); ++i)
rhi->isFeatureSupported(features[i]);
@@ -398,6 +441,38 @@ void tst_QRhi::create()
}
}
+void tst_QRhi::stats_data()
+{
+ rhiTestData();
+}
+
+void tst_QRhi::stats()
+{
+ QFETCH(QRhi::Implementation, impl);
+ QFETCH(QRhiInitParams *, initParams);
+
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
+ if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing statistics getter");
+
+ QRhiStats stats = rhi->statistics();
+ qDebug() << stats;
+ QCOMPARE(stats.totalPipelineCreationTime, 0);
+
+ if (impl == QRhi::Vulkan) {
+ QScopedPointer<QRhiBuffer> buf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, 32768));
+ QVERIFY(buf->create());
+ QScopedPointer<QRhiTexture> tex(rhi->newTexture(QRhiTexture::RGBA8, QSize(1024, 1024)));
+ QVERIFY(tex->create());
+
+ stats = rhi->statistics();
+ qDebug() << stats;
+ QVERIFY(stats.allocCount > 0);
+ QVERIFY(stats.blockCount > 0);
+ QVERIFY(stats.usedBytes > 0);
+ }
+}
+
void tst_QRhi::nativeHandles_data()
{
rhiTestData();
@@ -424,10 +499,10 @@ void tst_QRhi::nativeHandles()
case QRhi::Vulkan:
{
const QRhiVulkanNativeHandles *vkHandles = static_cast<const QRhiVulkanNativeHandles *>(rhiHandles);
+ QVERIFY(vkHandles->inst);
+ QCOMPARE(vkHandles->inst, &vulkanInstance);
QVERIFY(vkHandles->physDev);
QVERIFY(vkHandles->dev);
- QVERIFY(vkHandles->gfxQueueFamilyIdx >= 0);
- QVERIFY(vkHandles->gfxQueueIdx >= 0);
QVERIFY(vkHandles->gfxQueue);
QVERIFY(vkHandles->vmemAllocator);
}
@@ -457,6 +532,17 @@ void tst_QRhi::nativeHandles()
}
break;
#endif
+#ifdef TST_D3D12
+ case QRhi::D3D12:
+ {
+ const QRhiD3D12NativeHandles *d3dHandles = static_cast<const QRhiD3D12NativeHandles *>(rhiHandles);
+ QVERIFY(d3dHandles->dev);
+ QVERIFY(d3dHandles->minimumFeatureLevel > 0);
+ QVERIFY(d3dHandles->adapterLuidLow || d3dHandles->adapterLuidHigh);
+ QVERIFY(d3dHandles->commandQueue);
+ }
+ break;
+#endif
#ifdef TST_MTL
case QRhi::Metal:
{
@@ -501,6 +587,10 @@ void tst_QRhi::nativeHandles()
case QRhi::D3D11:
break;
#endif
+#ifdef TST_D3D12
+ case QRhi::D3D12:
+ break;
+#endif
#ifdef TST_MTL
case QRhi::Metal:
{
@@ -560,6 +650,10 @@ void tst_QRhi::nativeHandles()
case QRhi::D3D11:
break;
#endif
+#ifdef TST_D3D12
+ case QRhi::D3D12:
+ break;
+#endif
#ifdef TST_MTL
case QRhi::Metal:
break;
@@ -627,7 +721,7 @@ void tst_QRhi::nativeHandlesImportVulkan()
void tst_QRhi::nativeHandlesImportD3D11()
{
#ifdef TST_D3D11
- QScopedPointer<QRhi> rhi(QRhi::create(QRhi::D3D11, &initParams.d3d, QRhi::Flags(), nullptr));
+ QScopedPointer<QRhi> rhi(QRhi::create(QRhi::D3D11, &initParams.d3d11, QRhi::Flags(), nullptr));
if (!rhi)
QSKIP("QRhi could not be created, skipping testing D3D11 native handle import");
@@ -639,7 +733,7 @@ void tst_QRhi::nativeHandlesImportD3D11()
h.featureLevel = 0; // see if these are queried as expected, even when not provided
h.adapterLuidLow = 0;
h.adapterLuidHigh = 0;
- QScopedPointer<QRhi> adoptingRhi(QRhi::create(QRhi::D3D11, &initParams.d3d, QRhi::Flags(), &h));
+ QScopedPointer<QRhi> adoptingRhi(QRhi::create(QRhi::D3D11, &initParams.d3d11, QRhi::Flags(), &h));
QVERIFY(adoptingRhi);
const QRhiD3D11NativeHandles *newNativeHandles = static_cast<const QRhiD3D11NativeHandles *>(adoptingRhi->nativeHandles());
QCOMPARE(newNativeHandles->dev, nativeHandles->dev);
@@ -654,7 +748,7 @@ void tst_QRhi::nativeHandlesImportD3D11()
QRhiD3D11NativeHandles h = *nativeHandles;
h.dev = nullptr;
h.context = nullptr;
- QScopedPointer<QRhi> adoptingRhi(QRhi::create(QRhi::D3D11, &initParams.d3d, QRhi::Flags(), &h));
+ QScopedPointer<QRhi> adoptingRhi(QRhi::create(QRhi::D3D11, &initParams.d3d11, QRhi::Flags(), &h));
QVERIFY(adoptingRhi);
const QRhiD3D11NativeHandles *newNativeHandles = static_cast<const QRhiD3D11NativeHandles *>(adoptingRhi->nativeHandles());
QVERIFY(newNativeHandles->dev != nativeHandles->dev);
@@ -674,7 +768,6 @@ void tst_QRhi::nativeHandlesImportOpenGL()
#ifdef TST_GL
QRhiGles2NativeHandles h;
QScopedPointer<QOpenGLContext> ctx(new QOpenGLContext);
- ctx->setFormat(QRhiGles2InitParams::adjustedFormat());
if (!ctx->create())
QSKIP("No OpenGL context, skipping OpenGL-specific test");
h.context = ctx.data();
@@ -740,6 +833,14 @@ void tst_QRhi::nativeTexture()
}
break;
#endif
+#ifdef TST_D3D12
+ case QRhi::D3D12:
+ {
+ auto *texture = reinterpret_cast<void *>(nativeTex.object);
+ QVERIFY(texture);
+ }
+ break;
+#endif
#ifdef TST_MTL
case QRhi::Metal:
{
@@ -815,6 +916,18 @@ void tst_QRhi::nativeBuffer()
}
break;
#endif
+ #ifdef TST_D3D12
+ case QRhi::D3D12:
+ {
+ QVERIFY(nativeBuf.slotCount >= 1); // always backed by native buffers
+ for (int i = 0; i < nativeBuf.slotCount; ++i) {
+ auto *buffer = static_cast<void * const *>(nativeBuf.objects[i]);
+ QVERIFY(buffer);
+ QVERIFY(*buffer);
+ }
+ }
+ break;
+ #endif
#ifdef TST_MTL
case QRhi::Metal:
{
@@ -879,7 +992,7 @@ void tst_QRhi::resourceUpdateBatchBuffer()
batch->updateDynamicBuffer(dynamicBuffer.data(), 10, bufferSize - 10, a.constData());
batch->updateDynamicBuffer(dynamicBuffer.data(), 0, 12, b.constData());
- QRhiBufferReadbackResult readResult;
+ QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
batch->readBackBuffer(dynamicBuffer.data(), 5, 10, &readResult);
@@ -906,7 +1019,7 @@ void tst_QRhi::resourceUpdateBatchBuffer()
batch->uploadStaticBuffer(dynamicBuffer.data(), 10, bufferSize - 10, a.constData());
batch->uploadStaticBuffer(dynamicBuffer.data(), 0, 12, b.constData());
- QRhiBufferReadbackResult readResult;
+ QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
@@ -1015,7 +1128,7 @@ void tst_QRhi::resourceUpdateBatchRGBATextureUpload()
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
- QRhiTextureUploadEntry upload(0, 0, { image.constBits(), int(image.sizeInBytes()) });
+ QRhiTextureUploadEntry upload(0, 0, { image.constBits(), quint32(image.sizeInBytes()) });
QRhiTextureUploadDescription uploadDesc(upload);
batch->uploadTexture(texture.data(), uploadDesc);
@@ -1103,8 +1216,8 @@ void tst_QRhi::resourceUpdateBatchRGBATextureUpload()
// SourceTopLeft is not supported for non-QImage-based uploads.
const QImage im = image.copy(QRect(greenRectPos, copySize));
QRhiTextureSubresourceUploadDescription desc;
- desc.setData(QByteArray::fromRawData(reinterpret_cast<const char *>(im.constBits()),
- int(im.sizeInBytes())));
+ desc.setData(QByteArray::fromRawData(reinterpret_cast<const char *>(im.constBits()), im.sizeInBytes()));
+
desc.setSourceSize(copySize);
desc.setDestinationTopLeft(QPoint(gap, gap));
@@ -1428,6 +1541,86 @@ void tst_QRhi::resourceUpdateBatchLotsOfResources()
submitResourceUpdates(rhi.data(), b);
}
+void tst_QRhi::resourceUpdateBatchBetweenFrames_data()
+{
+ rhiTestData();
+}
+
+void tst_QRhi::resourceUpdateBatchBetweenFrames()
+{
+ QFETCH(QRhi::Implementation, impl);
+ QFETCH(QRhiInitParams *, initParams);
+
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
+ if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing resource updates");
+
+ QImage image(128, 128, QImage::Format_RGBA8888_Premultiplied);
+ image.fill(Qt::red);
+ static const float bufferData[64] = {};
+
+ QRhiCommandBuffer *cb = nullptr;
+ QRhi::FrameOpResult result = rhi->beginOffscreenFrame(&cb);
+ QVERIFY(result == QRhi::FrameOpSuccess);
+ QVERIFY(cb);
+
+ static const int TEXTURE_COUNT = 123;
+ static const int BUFFER_COUNT = 456;
+
+ QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
+ std::vector<std::unique_ptr<QRhiTexture>> textures;
+ std::vector<std::unique_ptr<QRhiBuffer>> buffers;
+
+ for (int i = 0; i < TEXTURE_COUNT; ++i) {
+ std::unique_ptr<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8,
+ image.size(),
+ 1,
+ QRhiTexture::UsedAsTransferSource));
+ QVERIFY(texture->create());
+ u->uploadTexture(texture.get(), image);
+ textures.push_back(std::move(texture));
+ }
+
+ for (int i = 0; i < BUFFER_COUNT; ++i) {
+ std::unique_ptr<QRhiBuffer> buffer(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, 256));
+ QVERIFY(buffer->create());
+ u->uploadStaticBuffer(buffer.get(), bufferData);
+ buffers.push_back(std::move(buffer));
+ }
+
+ rhi->endOffscreenFrame();
+ cb = nullptr;
+
+ // 'u' stays valid, commit it in another frame
+
+ result = rhi->beginOffscreenFrame(&cb);
+ QVERIFY(result == QRhi::FrameOpSuccess);
+ QVERIFY(cb);
+
+ cb->resourceUpdate(u); // this should work
+
+ rhi->endOffscreenFrame();
+
+ u = rhi->nextResourceUpdateBatch();
+ QRhiReadbackResult readResult;
+ bool readCompleted = false;
+ readResult.completed = [&readCompleted] { readCompleted = true; };
+ u->readBackTexture(textures[5].get(), &readResult);
+
+ QVERIFY(submitResourceUpdates(rhi.data(), u));
+ QVERIFY(readCompleted);
+ QCOMPARE(readResult.format, QRhiTexture::RGBA8);
+ QCOMPARE(readResult.pixelSize, image.size());
+
+ QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888_Premultiplied);
+ for (int y = 0; y < image.height(); ++y) {
+ for (int x = 0; x < image.width(); ++x)
+ QCOMPARE(wrapperImage.pixel(x, y), qRgba(255, 0, 0, 255));
+ }
+}
+
static QShader loadShader(const char *name)
{
QFile f(QString::fromUtf8(name));
@@ -1529,6 +1722,31 @@ void tst_QRhi::renderToTextureSimple_data()
rhiTestData();
}
+static QRhiGraphicsPipeline *createSimplePipeline(QRhi *rhi, QRhiShaderResourceBindings *srb, QRhiRenderPassDescriptor *rpDesc)
+{
+ std::unique_ptr<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
+ QShader vs = loadShader(":/data/simple.vert.qsb");
+ if (!vs.isValid())
+ return nullptr;
+ QShader fs = loadShader(":/data/simple.frag.qsb");
+ if (!fs.isValid())
+ return nullptr;
+ pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({ { 2 * sizeof(float) } });
+ inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
+ pipeline->setVertexInputLayout(inputLayout);
+ pipeline->setShaderResourceBindings(srb);
+ pipeline->setRenderPassDescriptor(rpDesc);
+ return pipeline->create() ? pipeline.release() : nullptr;
+}
+
+static const float triangleVertices[] = {
+ -1.0f, -1.0f,
+ 1.0f, -1.0f,
+ 0.0f, 1.0f
+};
+
void tst_QRhi::renderToTextureSimple()
{
QFETCH(QRhi::Implementation, impl);
@@ -1554,32 +1772,15 @@ void tst_QRhi::renderToTextureSimple()
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
- static const float vertices[] = {
- -1.0f, -1.0f,
- 1.0f, -1.0f,
- 0.0f, 1.0f
- };
- QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
+ QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices)));
QVERIFY(vbuf->create());
- updates->uploadStaticBuffer(vbuf.data(), vertices);
+ updates->uploadStaticBuffer(vbuf.data(), triangleVertices);
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->create());
- QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
- QShader vs = loadShader(":/data/simple.vert.qsb");
- QVERIFY(vs.isValid());
- QShader fs = loadShader(":/data/simple.frag.qsb");
- QVERIFY(fs.isValid());
- pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
- QRhiVertexInputLayout inputLayout;
- inputLayout.setBindings({ { 2 * sizeof(float) } });
- inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
- pipeline->setVertexInputLayout(inputLayout);
- pipeline->setShaderResourceBindings(srb.data());
- pipeline->setRenderPassDescriptor(rpDesc.data());
-
- QVERIFY(pipeline->create());
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(createSimplePipeline(rhi.data(), srb.data(), rpDesc.data()));
+ QVERIFY(pipeline);
cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 }, updates);
cb->setGraphicsPipeline(pipeline.data());
@@ -1681,32 +1882,15 @@ void tst_QRhi::renderToTextureMip()
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
- static const float vertices[] = {
- -1.0f, -1.0f,
- 1.0f, -1.0f,
- 0.0f, 1.0f
- };
- QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
+ QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices)));
QVERIFY(vbuf->create());
- updates->uploadStaticBuffer(vbuf.data(), vertices);
+ updates->uploadStaticBuffer(vbuf.data(), triangleVertices);
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->create());
- QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
- QShader vs = loadShader(":/data/simple.vert.qsb");
- QVERIFY(vs.isValid());
- QShader fs = loadShader(":/data/simple.frag.qsb");
- QVERIFY(fs.isValid());
- pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
- QRhiVertexInputLayout inputLayout;
- inputLayout.setBindings({ { 2 * sizeof(float) } });
- inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
- pipeline->setVertexInputLayout(inputLayout);
- pipeline->setShaderResourceBindings(srb.data());
- pipeline->setRenderPassDescriptor(rpDesc.data());
-
- QVERIFY(pipeline->create());
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(createSimplePipeline(rhi.data(), srb.data(), rpDesc.data()));
+ QVERIFY(pipeline);
cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 }, updates);
cb->setGraphicsPipeline(pipeline.data());
@@ -1803,32 +1987,15 @@ void tst_QRhi::renderToTextureCubemapFace()
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
- static const float vertices[] = {
- -1.0f, -1.0f,
- 1.0f, -1.0f,
- 0.0f, 1.0f
- };
- QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
+ QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices)));
QVERIFY(vbuf->create());
- updates->uploadStaticBuffer(vbuf.data(), vertices);
+ updates->uploadStaticBuffer(vbuf.data(), triangleVertices);
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->create());
- QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
- QShader vs = loadShader(":/data/simple.vert.qsb");
- QVERIFY(vs.isValid());
- QShader fs = loadShader(":/data/simple.frag.qsb");
- QVERIFY(fs.isValid());
- pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
- QRhiVertexInputLayout inputLayout;
- inputLayout.setBindings({ { 2 * sizeof(float) } });
- inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
- pipeline->setVertexInputLayout(inputLayout);
- pipeline->setShaderResourceBindings(srb.data());
- pipeline->setRenderPassDescriptor(rpDesc.data());
-
- QVERIFY(pipeline->create());
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(createSimplePipeline(rhi.data(), srb.data(), rpDesc.data()));
+ QVERIFY(pipeline);
cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 }, updates);
cb->setGraphicsPipeline(pipeline.data());
@@ -1946,32 +2113,15 @@ void tst_QRhi::renderToTextureTextureArray()
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
- static const float vertices[] = {
- -1.0f, -1.0f,
- 1.0f, -1.0f,
- 0.0f, 1.0f
- };
- QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
+ QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices)));
QVERIFY(vbuf->create());
- updates->uploadStaticBuffer(vbuf.data(), vertices);
+ updates->uploadStaticBuffer(vbuf.data(), triangleVertices);
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->create());
- QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
- QShader vs = loadShader(":/data/simple.vert.qsb");
- QVERIFY(vs.isValid());
- QShader fs = loadShader(":/data/simple.frag.qsb");
- QVERIFY(fs.isValid());
- pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
- QRhiVertexInputLayout inputLayout;
- inputLayout.setBindings({ { 2 * sizeof(float) } });
- inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
- pipeline->setVertexInputLayout(inputLayout);
- pipeline->setShaderResourceBindings(srb.data());
- pipeline->setRenderPassDescriptor(rpDesc.data());
-
- QVERIFY(pipeline->create());
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(createSimplePipeline(rhi.data(), srb.data(), rpDesc.data()));
+ QVERIFY(pipeline);
cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 }, updates);
cb->setGraphicsPipeline(pipeline.data());
@@ -2156,6 +2306,131 @@ void tst_QRhi::renderToTextureTexturedQuad()
QVERIFY(qGreen(result.pixel(214, 191)) > 2 * qBlue(result.pixel(214, 191)));
}
+void tst_QRhi::renderToTextureSampleWithSeparateTextureAndSampler_data()
+{
+ rhiTestData();
+}
+
+void tst_QRhi::renderToTextureSampleWithSeparateTextureAndSampler()
+{
+ // Same as renderToTextureTexturedQuad but the fragment shader uses a
+ // separate image and sampler. For Vulkan/Metal/D3D11 these are natively
+ // supported. For OpenGL this exercises the auto-generated combined sampler
+ // in the GLSL code and the mapping table that gets applied at run time by
+ // the backend.
+
+ QFETCH(QRhi::Implementation, impl);
+ QFETCH(QRhiInitParams *, initParams);
+
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
+ if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing rendering");
+
+ QImage inputImage;
+ inputImage.load(QLatin1String(":/data/qt256.png"));
+ QVERIFY(!inputImage.isNull());
+
+ QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, inputImage.size(), 1,
+ QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
+ QVERIFY(texture->create());
+
+ QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
+ QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
+ rt->setRenderPassDescriptor(rpDesc.data());
+ QVERIFY(rt->create());
+
+ QRhiCommandBuffer *cb = nullptr;
+ QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
+ QVERIFY(cb);
+
+ QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
+
+ QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(quadVerticesUvs)));
+ QVERIFY(vbuf->create());
+ updates->uploadStaticBuffer(vbuf.data(), quadVerticesUvs);
+
+ QScopedPointer<QRhiTexture> inputTexture(rhi->newTexture(QRhiTexture::RGBA8, inputImage.size()));
+ QVERIFY(inputTexture->create());
+ updates->uploadTexture(inputTexture.data(), inputImage);
+
+ QScopedPointer<QRhiSampler> sampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
+ QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
+ QVERIFY(sampler->create());
+
+ QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
+ srb->setBindings({
+ QRhiShaderResourceBinding::texture(3, QRhiShaderResourceBinding::FragmentStage, inputTexture.data()),
+ QRhiShaderResourceBinding::sampler(5, QRhiShaderResourceBinding::FragmentStage, sampler.data())
+ });
+ QVERIFY(srb->create());
+
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
+ pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
+ QShader vs = loadShader(":/data/simpletextured.vert.qsb");
+ QVERIFY(vs.isValid());
+ QShader fs = loadShader(":/data/simpletextured_separate.frag.qsb");
+ QVERIFY(fs.isValid());
+ pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({ { 4 * sizeof(float) } });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) }
+ });
+ pipeline->setVertexInputLayout(inputLayout);
+ pipeline->setShaderResourceBindings(srb.data());
+ pipeline->setRenderPassDescriptor(rpDesc.data());
+
+ QVERIFY(pipeline->create());
+
+ cb->beginPass(rt.data(), Qt::black, { 1.0f, 0 }, updates);
+ cb->setGraphicsPipeline(pipeline.data());
+ cb->setShaderResources();
+ cb->setViewport({ 0, 0, float(texture->pixelSize().width()), float(texture->pixelSize().height()) });
+ QRhiCommandBuffer::VertexInput vbindings(vbuf.data(), 0);
+ cb->setVertexInput(0, 1, &vbindings);
+ cb->draw(4);
+
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readResult, &result] {
+ result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888_Premultiplied);
+ };
+ QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
+ readbackBatch->readBackTexture({ texture.data() }, &readResult);
+ cb->endPass(readbackBatch);
+
+ rhi->endOffscreenFrame();
+
+ QVERIFY(!result.isNull());
+
+ if (impl == QRhi::Null)
+ return;
+
+ if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
+ result = std::move(result).mirrored();
+
+ QRgb white = qRgba(255, 255, 255, 255);
+ QCOMPARE(result.pixel(79, 77), white);
+ QCOMPARE(result.pixel(124, 81), white);
+ QCOMPARE(result.pixel(128, 149), white);
+ QCOMPARE(result.pixel(120, 189), white);
+ QCOMPARE(result.pixel(116, 185), white);
+
+ QRgb empty = qRgba(0, 0, 0, 0);
+ QCOMPARE(result.pixel(11, 45), empty);
+ QCOMPARE(result.pixel(246, 202), empty);
+ QCOMPARE(result.pixel(130, 18), empty);
+ QCOMPARE(result.pixel(4, 227), empty);
+
+ QVERIFY(qGreen(result.pixel(32, 52)) > 2 * qRed(result.pixel(32, 52)));
+ QVERIFY(qGreen(result.pixel(32, 52)) > 2 * qBlue(result.pixel(32, 52)));
+ QVERIFY(qGreen(result.pixel(214, 191)) > 2 * qRed(result.pixel(214, 191)));
+ QVERIFY(qGreen(result.pixel(214, 191)) > 2 * qBlue(result.pixel(214, 191)));
+}
+
void tst_QRhi::renderToTextureArrayOfTexturedQuad_data()
{
rhiTestData();
@@ -2834,6 +3109,147 @@ void tst_QRhi::renderToTextureDeferredSrb()
QCOMPARE(result.pixel(4, 227), empty);
}
+void tst_QRhi::renderToTextureDeferredUpdateSamplerInSrb_data()
+{
+ rhiTestData();
+}
+
+void tst_QRhi::renderToTextureDeferredUpdateSamplerInSrb()
+{
+ QFETCH(QRhi::Implementation, impl);
+ QFETCH(QRhiInitParams *, initParams);
+
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
+ if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing rendering");
+
+ QImage inputImage;
+ inputImage.load(QLatin1String(":/data/qt256.png"));
+ QVERIFY(!inputImage.isNull());
+
+ QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, inputImage.size(), 1,
+ QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
+ QVERIFY(texture->create());
+
+ QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
+ QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
+ rt->setRenderPassDescriptor(rpDesc.data());
+ QVERIFY(rt->create());
+
+ QRhiCommandBuffer *cb = nullptr;
+ QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
+ QVERIFY(cb);
+
+ QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
+
+ QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(quadVerticesUvs)));
+ QVERIFY(vbuf->create());
+ updates->uploadStaticBuffer(vbuf.data(), quadVerticesUvs);
+
+ QScopedPointer<QRhiTexture> inputTexture(rhi->newTexture(QRhiTexture::RGBA8, inputImage.size()));
+ QVERIFY(inputTexture->create());
+ updates->uploadTexture(inputTexture.data(), inputImage);
+
+ QScopedPointer<QRhiSampler> sampler1(rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::Linear,
+ QRhiSampler::Repeat, QRhiSampler::Repeat));
+ QVERIFY(sampler1->create());
+ QScopedPointer<QRhiSampler> sampler2(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
+ QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
+ QVERIFY(sampler2->create());
+
+ QScopedPointer<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64 + 4));
+ QVERIFY(ubuf->create());
+
+ QMatrix4x4 matrix;
+ updates->updateDynamicBuffer(ubuf.data(), 0, 64, matrix.constData());
+ float opacity = 0.5f;
+ updates->updateDynamicBuffer(ubuf.data(), 64, 4, &opacity);
+
+ const QRhiShaderResourceBinding::StageFlags commonVisibility = QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage;
+ QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
+ srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, commonVisibility, ubuf.data()),
+ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, inputTexture.data(), sampler1.data())
+ });
+ QVERIFY(srb->create());
+
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
+ pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
+ QShader vs = loadShader(":/data/textured.vert.qsb");
+ QVERIFY(vs.isValid());
+ QShader fs = loadShader(":/data/textured.frag.qsb");
+ QVERIFY(fs.isValid());
+ pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({ { 4 * sizeof(float) } });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) }
+ });
+ pipeline->setVertexInputLayout(inputLayout);
+ pipeline->setShaderResourceBindings(srb.data());
+ pipeline->setRenderPassDescriptor(rpDesc.data());
+
+ QVERIFY(pipeline->create());
+
+ // Now update the sampler to a different one, so if the pipeline->create()
+ // baked in static samplers somewhere (with 3D APIs where that's a thing),
+ // based on sampler1, that's now all invalid.
+ srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, commonVisibility, ubuf.data()),
+ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, inputTexture.data(), sampler2.data())
+ });
+ srb->updateResources(); // now it references sampler2, not sampler1
+
+ cb->beginPass(rt.data(), Qt::black, { 1.0f, 0 }, updates);
+ cb->setGraphicsPipeline(pipeline.data());
+ cb->setShaderResources();
+ cb->setViewport({ 0, 0, float(texture->pixelSize().width()), float(texture->pixelSize().height()) });
+ QRhiCommandBuffer::VertexInput vbindings(vbuf.data(), 0);
+ cb->setVertexInput(0, 1, &vbindings);
+ cb->draw(4);
+
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readResult, &result] {
+ result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888_Premultiplied);
+ };
+ QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
+ readbackBatch->readBackTexture({ texture.data() }, &readResult);
+ cb->endPass(readbackBatch);
+
+ rhi->endOffscreenFrame();
+
+ QVERIFY(!result.isNull());
+
+ if (impl == QRhi::Null)
+ return;
+
+ if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
+ result = std::move(result).mirrored();
+
+ // opacity 0.5 (premultiplied)
+ static const auto checkSemiWhite = [](const QRgb &c) {
+ QRgb semiWhite127 = qPremultiply(qRgba(255, 255, 255, 127));
+ QRgb semiWhite128 = qPremultiply(qRgba(255, 255, 255, 128));
+ return c == semiWhite127 || c == semiWhite128;
+ };
+ QVERIFY(checkSemiWhite(result.pixel(79, 77)));
+ QVERIFY(checkSemiWhite(result.pixel(124, 81)));
+ QVERIFY(checkSemiWhite(result.pixel(128, 149)));
+ QVERIFY(checkSemiWhite(result.pixel(120, 189)));
+ QVERIFY(checkSemiWhite(result.pixel(116, 185)));
+ QVERIFY(checkSemiWhite(result.pixel(191, 172)));
+
+ QRgb empty = qRgba(0, 0, 0, 0);
+ QCOMPARE(result.pixel(11, 45), empty);
+ QCOMPARE(result.pixel(246, 202), empty);
+ QCOMPARE(result.pixel(130, 18), empty);
+ QCOMPARE(result.pixel(4, 227), empty);
+}
+
void tst_QRhi::renderToTextureMultipleUniformBuffersAndDynamicOffset_data()
{
rhiTestData();
@@ -3151,7 +3567,6 @@ void tst_QRhi::setWindowType(QWindow *window, QRhi::Implementation impl)
switch (impl) {
#ifdef TST_GL
case QRhi::OpenGLES2:
- window->setFormat(QRhiGles2InitParams::adjustedFormat());
window->setSurfaceType(QSurface::OpenGLSurface);
break;
#endif
@@ -3202,18 +3617,13 @@ void tst_QRhi::renderToTextureIndexedDraw()
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
- static const float vertices[] = {
- -1.0f, -1.0f,
- 1.0f, -1.0f,
- 0.0f, 1.0f
- };
static const quint16 indices[] = {
0, 1, 2
};
- QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
+ QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices)));
QVERIFY(vbuf->create());
- updates->uploadStaticBuffer(vbuf.data(), vertices);
+ updates->uploadStaticBuffer(vbuf.data(), triangleVertices);
QScopedPointer<QRhiBuffer> ibuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(indices)));
QVERIFY(ibuf->create());
@@ -3222,20 +3632,8 @@ void tst_QRhi::renderToTextureIndexedDraw()
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->create());
- QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
- QShader vs = loadShader(":/data/simple.vert.qsb");
- QVERIFY(vs.isValid());
- QShader fs = loadShader(":/data/simple.frag.qsb");
- QVERIFY(fs.isValid());
- pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
- QRhiVertexInputLayout inputLayout;
- inputLayout.setBindings({ { 2 * sizeof(float) } });
- inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
- pipeline->setVertexInputLayout(inputLayout);
- pipeline->setShaderResourceBindings(srb.data());
- pipeline->setRenderPassDescriptor(rpDesc.data());
-
- QVERIFY(pipeline->create());
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(createSimplePipeline(rhi.data(), srb.data(), rpDesc.data()));
+ QVERIFY(pipeline);
QRhiCommandBuffer::VertexInput vbindings(vbuf.data(), 0);
@@ -3309,6 +3707,182 @@ void tst_QRhi::renderToTextureIndexedDraw()
QVERIFY(redCount > blueCount);
}
+void tst_QRhi::renderToTextureArrayMultiView_data()
+{
+ rhiTestData();
+}
+
+void tst_QRhi::renderToTextureArrayMultiView()
+{
+ QFETCH(QRhi::Implementation, impl);
+ QFETCH(QRhiInitParams *, initParams);
+
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
+ if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing rendering");
+
+ if (!rhi->isFeatureSupported(QRhi::MultiView))
+ QSKIP("Multiview not supported, skipping testing on this backend");
+
+ if (rhi->backend() == QRhi::Vulkan && rhi->driverInfo().deviceType == QRhiDriverInfo::CpuDevice)
+ QSKIP("lavapipe does not like multiview, skip for now");
+
+ for (int sampleCount : rhi->supportedSampleCounts()) {
+ const QSize outputSize(1920, 1080);
+ QRhiTexture::Flags textureFlags = QRhiTexture::RenderTarget;
+ if (sampleCount <= 1)
+ textureFlags |= QRhiTexture::UsedAsTransferSource;
+ QScopedPointer<QRhiTexture> texture(rhi->newTextureArray(QRhiTexture::RGBA8, 2, outputSize, sampleCount, textureFlags));
+ QVERIFY(texture->create());
+
+ // exercise a depth-stencil buffer as well, not that the triangle needs it; note that this also needs to be a two-layer texture array
+ QScopedPointer<QRhiTexture> ds(rhi->newTextureArray(QRhiTexture::D24S8, 2, outputSize, sampleCount, QRhiTexture::RenderTarget));
+ QVERIFY(ds->create());
+
+ QScopedPointer<QRhiTexture> resolveTexture;
+ if (sampleCount > 1) {
+ resolveTexture.reset(rhi->newTextureArray(QRhiTexture::RGBA8, 2, outputSize, 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
+ QVERIFY(resolveTexture->create());
+ }
+
+ QRhiColorAttachment multiViewAtt(texture.get());
+ multiViewAtt.setMultiViewCount(2);
+ if (sampleCount > 1)
+ multiViewAtt.setResolveTexture(resolveTexture.get());
+
+ QRhiTextureRenderTargetDescription rtDesc(multiViewAtt);
+ rtDesc.setDepthTexture(ds.get());
+
+ QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(rtDesc));
+ QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
+ rt->setRenderPassDescriptor(rpDesc.data());
+ QVERIFY(rt->create());
+
+ QRhiCommandBuffer *cb = nullptr;
+ QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
+ QVERIFY(cb);
+
+ QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
+
+ static float triangleData[] = {
+ 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
+ };
+
+ QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleData)));
+ QVERIFY(vbuf->create());
+ updates->uploadStaticBuffer(vbuf.data(), triangleData);
+
+ QScopedPointer<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 128)); // mat4 mvp[2]
+ QVERIFY(ubuf->create());
+
+ QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
+ srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, ubuf.get())
+ });
+ QVERIFY(srb->create());
+
+ QScopedPointer<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
+ ps->setShaderStages({
+ { QRhiShaderStage::Vertex, loadShader(":/data/multiview.vert.qsb") },
+ { QRhiShaderStage::Fragment, loadShader(":/data/multiview.frag.qsb") }
+ });
+ ps->setMultiViewCount(2); // the view count must be set both on the render target and the pipeline
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 5 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float3, quint32(2 * sizeof(float)) }
+ });
+ ps->setDepthTest(true);
+ ps->setDepthWrite(true);
+ ps->setSampleCount(sampleCount);
+ ps->setVertexInputLayout(inputLayout);
+ ps->setShaderResourceBindings(srb.get());
+ ps->setRenderPassDescriptor(rpDesc.get());
+ QVERIFY(ps->create());
+
+ QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
+ mvp.perspective(45.0f, outputSize.width() / float(outputSize.height()), 0.01f, 1000.0f);
+ mvp.translate(0, 0, -2);
+ mvp.rotate(90, 0, 0, 1); // point left
+ updates->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
+ mvp.rotate(-180, 0, 0, 1); // point right
+ updates->updateDynamicBuffer(ubuf.get(), 64, 64, mvp.constData());
+
+ cb->beginPass(rt.data(), Qt::black, { 1.0f, 0 }, updates);
+ cb->setGraphicsPipeline(ps.data());
+ cb->setShaderResources();
+ cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
+ QRhiCommandBuffer::VertexInput vbindings(vbuf.data(), 0);
+ cb->setVertexInput(0, 1, &vbindings);
+ cb->draw(3);
+
+ QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
+ QRhiReadbackResult readResult[2];
+ QRhiReadbackDescription readbackDesc;
+ if (sampleCount > 1)
+ readbackDesc.setTexture(resolveTexture.get());
+ else
+ readbackDesc.setTexture(texture.get());
+ readbackDesc.setLayer(0);
+ readbackBatch->readBackTexture(readbackDesc, &readResult[0]);
+ readbackDesc.setLayer(1);
+ readbackBatch->readBackTexture(readbackDesc, &readResult[1]);
+
+ cb->endPass(readbackBatch);
+
+ rhi->endOffscreenFrame();
+
+ if (rhi->backend() == QRhi::Null)
+ QSKIP("No real content with Null backend, skipping multiview content check");
+
+ // both readbacks should be finished now due to using offscreen frames
+
+ QImage image0 = QImage(reinterpret_cast<const uchar *>(readResult[0].data.constData()),
+ readResult[0].pixelSize.width(), readResult[0].pixelSize.height(),
+ QImage::Format_RGBA8888);
+ if (rhi->isYUpInFramebuffer()) // note that we used clipSpaceCorrMatrix
+ image0 = image0.mirrored();
+
+ QImage image1 = QImage(reinterpret_cast<const uchar *>(readResult[1].data.constData()),
+ readResult[1].pixelSize.width(), readResult[1].pixelSize.height(),
+ QImage::Format_RGBA8888);
+ if (rhi->isYUpInFramebuffer())
+ image1 = image1.mirrored();
+
+ QVERIFY(!image0.isNull());
+ QVERIFY(!image1.isNull());
+
+ // image0 should have a triangle rotated so that it points left with the red
+ // tip. image1 should have a triangle rotated so that it points right with
+ // the red tip. Both are centered, so we will check in range 0..width/2 for
+ // image0 and width/2..width-1 for image1 to see if the red-enough pixels
+ // are present.
+
+ int y = image0.height() / 2;
+ int n = 0;
+ for (int x = 0; x < image0.width() / 2; ++x) {
+ QRgb c = image0.pixel(x, y);
+ if (qRed(c) > 250 && qGreen(c) < 10 && qBlue(c) < 10)
+ ++n;
+ }
+ QVERIFY(n >= 10);
+
+ y = image1.height() / 2;
+ n = 0;
+ for (int x = image1.width() / 2; x < image1.width(); ++x) {
+ QRgb c = image1.pixel(x, y);
+ if (qRed(c) > 250 && qGreen(c) < 10 && qBlue(c) < 10)
+ ++n;
+ }
+ QVERIFY(n >= 10);
+ }
+}
+
void tst_QRhi::renderToWindowSimple_data()
{
rhiTestData();
@@ -3342,32 +3916,15 @@ void tst_QRhi::renderToWindowSimple()
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
- static const float vertices[] = {
- -1.0f, -1.0f,
- 1.0f, -1.0f,
- 0.0f, 1.0f
- };
- QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
+ QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices)));
QVERIFY(vbuf->create());
- updates->uploadStaticBuffer(vbuf.data(), vertices);
+ updates->uploadStaticBuffer(vbuf.data(), triangleVertices);
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->create());
- QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
- QShader vs = loadShader(":/data/simple.vert.qsb");
- QVERIFY(vs.isValid());
- QShader fs = loadShader(":/data/simple.frag.qsb");
- QVERIFY(fs.isValid());
- pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
- QRhiVertexInputLayout inputLayout;
- inputLayout.setBindings({ { 2 * sizeof(float) } });
- inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
- pipeline->setVertexInputLayout(inputLayout);
- pipeline->setShaderResourceBindings(srb.data());
- pipeline->setRenderPassDescriptor(rpDesc.data());
-
- QVERIFY(pipeline->create());
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(createSimplePipeline(rhi.data(), srb.data(), rpDesc.data()));
+ QVERIFY(pipeline);
const int asyncReadbackFrames = rhi->resourceLimit(QRhi::MaxAsyncReadbackFrames);
// one frame issues the readback, then we do MaxAsyncReadbackFrames more to ensure the readback completes
@@ -3381,6 +3938,9 @@ void tst_QRhi::renderToWindowSimple()
QVERIFY(rhi->beginFrame(swapChain.data()) == QRhi::FrameOpSuccess);
QRhiCommandBuffer *cb = swapChain->currentFrameCommandBuffer();
QRhiRenderTarget *rt = swapChain->currentFrameRenderTarget();
+ QCOMPARE(rt->resourceType(), QRhiResource::SwapChainRenderTarget);
+ QVERIFY(rt->renderPassDescriptor());
+ QCOMPARE(static_cast<QRhiSwapChainRenderTarget *>(rt)->swapChain(), swapChain.data());
const QSize outputSize = swapChain->currentPixelSize();
QCOMPARE(rt->pixelSize(), outputSize);
QRhiViewport viewport(0, 0, float(outputSize.width()), float(outputSize.height()));
@@ -3481,26 +4041,10 @@ void tst_QRhi::finishWithinSwapchainFrame()
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->create());
- QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
- QShader vs = loadShader(":/data/simple.vert.qsb");
- QVERIFY(vs.isValid());
- QShader fs = loadShader(":/data/simple.frag.qsb");
- QVERIFY(fs.isValid());
- pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
- QRhiVertexInputLayout inputLayout;
- inputLayout.setBindings({ { 2 * sizeof(float) } });
- inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
- pipeline->setVertexInputLayout(inputLayout);
- pipeline->setShaderResourceBindings(srb.data());
- pipeline->setRenderPassDescriptor(rpDesc.data());
- QVERIFY(pipeline->create());
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(createSimplePipeline(rhi.data(), srb.data(), rpDesc.data()));
+ QVERIFY(pipeline);
- static const float vertices[] = {
- -1.0f, -1.0f,
- 1.0f, -1.0f,
- 0.0f, 1.0f
- };
- QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
+ QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices)));
QVERIFY(vbuf->create());
// exercise begin/endExternal() just a little bit, note ExternalContent for beginPass()
@@ -3513,7 +4057,7 @@ void tst_QRhi::finishWithinSwapchainFrame()
// times within the same frame
for (int i = 0; i < 5; ++i) {
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
- updates->uploadStaticBuffer(vbuf.data(), vertices);
+ updates->uploadStaticBuffer(vbuf.data(), triangleVertices);
cb->beginPass(rt, Qt::blue, { 1.0f, 0 }, updates, QRhiCommandBuffer::ExternalContent);
@@ -3593,7 +4137,7 @@ void tst_QRhi::resourceUpdateBatchBufferTextureWithSwapchainFrames()
const char *b = "abcdefghi";
bool readCompleted = false;
- QRhiBufferReadbackResult readResult;
+ QRhiReadbackResult readResult;
readResult.completed = [&readCompleted] { readCompleted = true; };
QRhiReadbackResult texReadResult;
texReadResult.completed = [&readCompleted] { readCompleted = true; };
@@ -3767,6 +4311,67 @@ void tst_QRhi::resourceUpdateBatchBufferTextureWithSwapchainFrames()
}
}
+void tst_QRhi::textureRenderTargetAutoRebuild_data()
+{
+ rhiTestData();
+}
+
+void tst_QRhi::textureRenderTargetAutoRebuild()
+{
+ QFETCH(QRhi::Implementation, impl);
+ QFETCH(QRhiInitParams *, initParams);
+
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
+ if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing rendering");
+
+ // case 1: beginPass's implicit create()
+ {
+ QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 1, QRhiTexture::RenderTarget));
+ QVERIFY(texture->create());
+ QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ { texture.data() } }));
+ QScopedPointer<QRhiRenderPassDescriptor> rp(rt->newCompatibleRenderPassDescriptor());
+ rt->setRenderPassDescriptor(rp.data());
+ QVERIFY(rt->create());
+
+ QRhiCommandBuffer *cb = nullptr;
+ QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
+ QVERIFY(cb);
+ cb->beginPass(rt.data(), Qt::red, { 1.0f, 0 });
+ cb->endPass();
+ rhi->endOffscreenFrame();
+
+ texture->setPixelSize(QSize(256, 256));
+ QVERIFY(texture->create());
+ QCOMPARE(texture->pixelSize(), QSize(256, 256));
+
+ QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
+ QVERIFY(cb);
+ // no rt->create() but beginPass() does it implicitly for us
+ cb->beginPass(rt.data(), Qt::red, { 1.0f, 0 });
+ QCOMPARE(rt->pixelSize(), QSize(256, 256));
+ cb->endPass();
+ rhi->endOffscreenFrame();
+ }
+
+ // case 2: pixelSize's implicit create()
+ {
+ QSize sz(512, 512);
+ QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, sz, 1, QRhiTexture::RenderTarget));
+ QVERIFY(texture->create());
+ QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ { texture.data() } }));
+ QScopedPointer<QRhiRenderPassDescriptor> rp(rt->newCompatibleRenderPassDescriptor());
+ rt->setRenderPassDescriptor(rp.data());
+ QVERIFY(rt->create());
+ QCOMPARE(rt->pixelSize(), sz);
+
+ sz = QSize(256, 256);
+ texture->setPixelSize(sz);
+ QVERIFY(texture->create());
+ QCOMPARE(rt->pixelSize(), sz);
+ }
+}
+
void tst_QRhi::srbLayoutCompatibility_data()
{
rhiTestData();
@@ -3806,7 +4411,7 @@ void tst_QRhi::srbLayoutCompatibility()
QVERIFY(srb2->isLayoutCompatible(srb1.data()));
QCOMPARE(srb1->serializedLayoutDescription(), srb2->serializedLayoutDescription());
- QVERIFY(srb1->serializedLayoutDescription().count() == 0);
+ QVERIFY(srb1->serializedLayoutDescription().size() == 0);
}
// different count (not compatible)
@@ -3824,8 +4429,8 @@ void tst_QRhi::srbLayoutCompatibility()
QVERIFY(!srb2->isLayoutCompatible(srb1.data()));
QVERIFY(srb1->serializedLayoutDescription() != srb2->serializedLayoutDescription());
- QVERIFY(srb1->serializedLayoutDescription().count() == 0);
- QVERIFY(srb2->serializedLayoutDescription().count() == 1 * QRhiShaderResourceBinding::LAYOUT_DESC_ENTRIES_PER_BINDING);
+ QVERIFY(srb1->serializedLayoutDescription().size() == 0);
+ QVERIFY(srb2->serializedLayoutDescription().size() == 1 * QRhiShaderResourceBinding::LAYOUT_DESC_ENTRIES_PER_BINDING);
}
// full match (compatible)
@@ -3850,7 +4455,7 @@ void tst_QRhi::srbLayoutCompatibility()
QVERIFY(!srb1->serializedLayoutDescription().isEmpty());
QVERIFY(!srb2->serializedLayoutDescription().isEmpty());
QCOMPARE(srb1->serializedLayoutDescription(), srb2->serializedLayoutDescription());
- QVERIFY(srb1->serializedLayoutDescription().count() == 2 * QRhiShaderResourceBinding::LAYOUT_DESC_ENTRIES_PER_BINDING);
+ QVERIFY(srb1->serializedLayoutDescription().size() == 2 * QRhiShaderResourceBinding::LAYOUT_DESC_ENTRIES_PER_BINDING);
// see what we would get if a binding list got serialized "manually", without pulling it out from the srb after building
// (the results should be identical)
@@ -4165,6 +4770,59 @@ void tst_QRhi::renderPassDescriptorCompatibility()
} else {
qDebug("Skipping texture format dependent tests");
}
+
+ if (rhi->isFeatureSupported(QRhi::MultiView)) {
+ {
+ QScopedPointer<QRhiTexture> texArr(rhi->newTextureArray(QRhiTexture::RGBA8, 2, QSize(512, 512), 1, QRhiTexture::RenderTarget));
+ QVERIFY(texArr->create());
+ QRhiColorAttachment multiViewAtt(texArr.data());
+ multiViewAtt.setMultiViewCount(2);
+ QRhiTextureRenderTargetDescription rtDesc(multiViewAtt);
+ QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(rtDesc));
+ QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
+ rt->setRenderPassDescriptor(rpDesc.data());
+ QVERIFY(rt->create());
+
+ QScopedPointer<QRhiTextureRenderTarget> rt2(rhi->newTextureRenderTarget(rtDesc));
+ QScopedPointer<QRhiRenderPassDescriptor> rpDesc2(rt2->newCompatibleRenderPassDescriptor());
+ rt2->setRenderPassDescriptor(rpDesc2.data());
+ QVERIFY(rt2->create());
+
+ QVERIFY(rpDesc->isCompatible(rpDesc2.data()));
+ QVERIFY(rpDesc2->isCompatible(rpDesc.data()));
+ QCOMPARE(rpDesc->serializedFormat(), rpDesc2->serializedFormat());
+
+ QScopedPointer<QRhiRenderPassDescriptor> rpDescClone(rpDesc->newCompatibleRenderPassDescriptor());
+ QVERIFY(rpDesc->isCompatible(rpDescClone.data()));
+ QVERIFY(rpDesc2->isCompatible(rpDescClone.data()));
+ QCOMPARE(rpDesc->serializedFormat(), rpDescClone->serializedFormat());
+
+ // With Vulkan the multiViewCount really matters since it is baked
+ // in to underlying native object (VkRenderPass). Verify that the
+ // compatibility check fails when the view count differs. Other
+ // backends cannot do this test since they will likely report the
+ // rps being compatible regardless.
+ if (impl == QRhi::Vulkan) {
+ QRhiColorAttachment nonMultiViewAtt(texArr.data());
+ QRhiTextureRenderTargetDescription rtDesc3(nonMultiViewAtt);
+ QScopedPointer<QRhiTextureRenderTarget> rt3(rhi->newTextureRenderTarget(rtDesc3));
+ QScopedPointer<QRhiRenderPassDescriptor> rpDesc3(rt3->newCompatibleRenderPassDescriptor());
+ rt3->setRenderPassDescriptor(rpDesc3.data());
+ QVERIFY(rt3->create());
+
+ QVERIFY(!rpDesc->isCompatible(rpDesc3.data()));
+ QVERIFY(!rpDesc2->isCompatible(rpDesc3.data()));
+ QVERIFY(rpDesc->serializedFormat() != rpDesc3->serializedFormat());
+
+ QScopedPointer<QRhiRenderPassDescriptor> rpDesc3Clone(rpDesc3->newCompatibleRenderPassDescriptor());
+ QVERIFY(!rpDesc->isCompatible(rpDesc3Clone.data()));
+ QVERIFY(!rpDesc2->isCompatible(rpDesc3Clone.data()));
+ QVERIFY(rpDesc->serializedFormat() != rpDesc3Clone->serializedFormat());
+ }
+ }
+ } else {
+ qDebug("Skipping multiview dependent tests");
+ }
}
void tst_QRhi::renderPassDescriptorClone_data()
@@ -4282,22 +4940,67 @@ void tst_QRhi::pipelineCache()
}
}
-void tst_QRhi::textureImportOpenGL_data()
+void tst_QRhi::textureWithSampleCount_data()
{
- rhiTestDataOpenGL();
+ rhiTestData();
}
-void tst_QRhi::textureImportOpenGL()
+void tst_QRhi::textureWithSampleCount()
{
QFETCH(QRhi::Implementation, impl);
- if (impl != QRhi::OpenGLES2)
- QSKIP("Skipping OpenGL-dependent test");
-
-#ifdef TST_GL
QFETCH(QRhiInitParams *, initParams);
QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing renderpass descriptors");
+
+ if (!rhi->isFeatureSupported(QRhi::MultisampleTexture))
+ QSKIP("No multisample texture support with this backend, skipping");
+
+ {
+ QScopedPointer<QRhiTexture> tex(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 1));
+ QVERIFY(tex->create());
+ }
+
+ // Ensure 0 is accepted the same way as 1.
+ {
+ QScopedPointer<QRhiTexture> tex(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 0));
+ QVERIFY(tex->create());
+ }
+
+ // Note that we intentionally do not pass in RenderTarget in flags. Where
+ // matters for create(), the backend is expected to act as if it was
+ // specified whenever samples > 1. (in practice it does not make sense to not
+ // have the flag for an msaa texture, but we only care about create() here)
+
+ // Pick the commonly supported sample count of 4.
+ {
+ QScopedPointer<QRhiTexture> tex(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 4));
+ QVERIFY(tex->create());
+ }
+
+ // Now a bogus value that is typically in-between the supported values.
+ {
+ QScopedPointer<QRhiTexture> tex(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 3));
+ QVERIFY(tex->create());
+ }
+
+ // Now a bogus value that is out of range.
+ {
+ QScopedPointer<QRhiTexture> tex(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 123));
+ QVERIFY(tex->create());
+ }
+}
+
+
+void tst_QRhi::textureImportOpenGL()
+{
+#ifdef TST_GL
+ if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::OpenGL))
+ QSKIP("Skipping OpenGL-dependent test");
+
+ QScopedPointer<QRhi> rhi(QRhi::create(QRhi::OpenGLES2, &initParams.gl, QRhi::Flags(), nullptr));
+ if (!rhi)
QSKIP("QRhi could not be created, skipping testing native texture");
QVERIFY(rhi->makeThreadLocalNativeContextCurrent());
@@ -4336,21 +5039,13 @@ void tst_QRhi::textureImportOpenGL()
#endif
}
-void tst_QRhi::renderbufferImportOpenGL_data()
-{
- rhiTestDataOpenGL();
-}
-
void tst_QRhi::renderbufferImportOpenGL()
{
- QFETCH(QRhi::Implementation, impl);
- if (impl != QRhi::OpenGLES2)
- QSKIP("Skipping OpenGL-dependent test");
-
#ifdef TST_GL
- QFETCH(QRhiInitParams *, initParams);
+ if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::OpenGL))
+ QSKIP("Skipping OpenGL-dependent test");
- QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
+ QScopedPointer<QRhi> rhi(QRhi::create(QRhi::OpenGLES2, &initParams.gl, QRhi::Flags(), nullptr));
if (!rhi)
QSKIP("QRhi could not be created, skipping testing native texture");
@@ -4442,7 +5137,7 @@ void tst_QRhi::threeDimTexture()
}
// mipmaps
- {
+ if (rhi->isFeatureSupported(QRhi::ThreeDimensionalTextureMipmaps)) {
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, WIDTH, HEIGHT, DEPTH,
1, QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips));
QVERIFY(texture->create());
@@ -4487,6 +5182,8 @@ void tst_QRhi::threeDimTexture()
// problems with this.
if (impl != QRhi::Null && impl != QRhi::OpenGLES2)
QVERIFY(imageRGBAEquals(result, referenceImage, 2));
+ } else {
+ qDebug("Skipping 3D texture mipmap generation test because it is reported as unsupported");
}
// render target (one slice)
@@ -4505,11 +5202,531 @@ void tst_QRhi::threeDimTexture()
rt->setRenderPassDescriptor(rp.data());
QVERIFY(rt->create());
+ // render to slice 23
+ QRhiCommandBuffer *cb = nullptr;
+ QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
+ QVERIFY(cb);
+ cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 });
+ // slice 23 is now blue
+ cb->endPass();
+ rhi->endOffscreenFrame();
+
+ // Fill all other slices with some color. We should be free to do this
+ // step *before* the "render to slice 23" block above as well. However,
+ // as QTBUG-111772 shows, some Vulkan implementations have problems
+ // then. (or it could be QRhi is doing something wrong, but there is no
+ // evidence of that yet) For now, keep the order of first rendering to
+ // a slice and then uploading data for the rest.
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QVERIFY(batch);
-
for (int i = 0; i < DEPTH; ++i) {
- QImage img(WIDTH, HEIGHT, QImage::Format_RGBA8888);
+ if (i != SLICE) {
+ QImage img(WIDTH, HEIGHT, QImage::Format_RGBA8888);
+ img.fill(QColor::fromRgb(i * 2, 0, 0));
+ QRhiTextureUploadEntry sliceUpload(i, 0, QRhiTextureSubresourceUploadDescription(img));
+ batch->uploadTexture(texture.data(), sliceUpload);
+ }
+ }
+ QVERIFY(submitResourceUpdates(rhi.data(), batch));
+
+ // read back slice 23 (blue)
+ batch = rhi->nextResourceUpdateBatch();
+ QVERIFY(batch);
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readResult, &result] {
+ result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888);
+ };
+ QRhiReadbackDescription readbackDescription(texture.data());
+ readbackDescription.setLayer(23);
+ batch->readBackTexture(readbackDescription, &readResult);
+ QVERIFY(submitResourceUpdates(rhi.data(), batch));
+ QVERIFY(!result.isNull());
+ QImage referenceImage(WIDTH, HEIGHT, result.format());
+ referenceImage.fill(QColor::fromRgbF(0.0f, 0.0f, 1.0f));
+ // the Null backend does not render so skip the verification for that
+ if (impl != QRhi::Null)
+ QVERIFY(imageRGBAEquals(result, referenceImage));
+
+ // read back slice 0 (black)
+ batch = rhi->nextResourceUpdateBatch();
+ result = QImage();
+ readbackDescription.setLayer(0);
+ batch->readBackTexture(readbackDescription, &readResult);
+ QVERIFY(submitResourceUpdates(rhi.data(), batch));
+ QVERIFY(!result.isNull());
+ referenceImage.fill(QColor::fromRgbF(0.0f, 0.0f, 0.0f));
+ QVERIFY(imageRGBAEquals(result, referenceImage));
+
+ // read back slice 127 (almost red)
+ batch = rhi->nextResourceUpdateBatch();
+ result = QImage();
+ readbackDescription.setLayer(127);
+ batch->readBackTexture(readbackDescription, &readResult);
+ QVERIFY(submitResourceUpdates(rhi.data(), batch));
+ QVERIFY(!result.isNull());
+ referenceImage.fill(QColor::fromRgb(254, 0, 0));
+ QVERIFY(imageRGBAEquals(result, referenceImage));
+ }
+}
+void tst_QRhi::oneDimTexture_data()
+{
+ rhiTestData();
+}
+
+void tst_QRhi::oneDimTexture()
+{
+ QFETCH(QRhi::Implementation, impl);
+ QFETCH(QRhiInitParams *, initParams);
+
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams));
+ if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing 1D textures");
+
+ if (!rhi->isFeatureSupported(QRhi::OneDimensionalTextures))
+ QSKIP("Skipping testing 1D textures because they are reported as unsupported");
+
+ const int WIDTH = 512;
+ const int LAYERS = 128;
+
+ {
+ QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, WIDTH, 0, 0));
+ QVERIFY(texture->create());
+
+ QVERIFY(texture->flags().testFlag(QRhiTexture::Flag::OneDimensional));
+
+ QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
+ QVERIFY(batch);
+
+ QImage img(WIDTH, 1, QImage::Format_RGBA8888);
+ img.fill(QColor::fromRgb(255, 0, 0));
+
+ QRhiTextureUploadEntry upload(0, 0, QRhiTextureSubresourceUploadDescription(img));
+ batch->uploadTexture(texture.data(), upload);
+
+ QVERIFY(submitResourceUpdates(rhi.data(), batch));
+ }
+
+ {
+ QScopedPointer<QRhiTexture> texture(
+ rhi->newTextureArray(QRhiTexture::RGBA8, LAYERS, QSize(WIDTH, 0)));
+ QVERIFY(texture->create());
+
+ QVERIFY(texture->flags().testFlag(QRhiTexture::Flag::OneDimensional));
+ QVERIFY(texture->flags().testFlag(QRhiTexture::Flag::TextureArray));
+
+ QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
+ QVERIFY(batch);
+
+ for (int i = 0; i < LAYERS; ++i) {
+ QImage img(WIDTH, 1, QImage::Format_RGBA8888);
+ img.fill(QColor::fromRgb(i * 2, 0, 0));
+ QRhiTextureUploadEntry layerUpload(i, 0, QRhiTextureSubresourceUploadDescription(img));
+ batch->uploadTexture(texture.data(), layerUpload);
+ }
+
+ QVERIFY(submitResourceUpdates(rhi.data(), batch));
+ }
+
+ // Copy from 2D texture to 1D texture
+ {
+ const int WIDTH = 256;
+ const int HEIGHT = 256;
+
+ QScopedPointer<QRhiTexture> srcTexture(rhi->newTexture(
+ QRhiTexture::RGBA8, WIDTH, HEIGHT, 0, 1, QRhiTexture::Flag::UsedAsTransferSource));
+ QVERIFY(srcTexture->create());
+
+ QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
+ QVERIFY(batch);
+
+ QImage img(WIDTH, HEIGHT, QImage::Format_RGBA8888);
+ for (int x = 0; x < WIDTH; ++x) {
+ for (int y = 0; y < HEIGHT; ++y) {
+ img.setPixelColor(x, y, QColor::fromRgb(x, y, 0));
+ }
+ }
+ QRhiTextureUploadEntry upload(0, 0, QRhiTextureSubresourceUploadDescription(img));
+ batch->uploadTexture(srcTexture.data(), upload);
+
+ QScopedPointer<QRhiTexture> dstTexture(rhi->newTexture(
+ QRhiTexture::RGBA8, WIDTH, 0, 0, 1, QRhiTexture::Flag::UsedAsTransferSource));
+ QVERIFY(dstTexture->create());
+
+ QRhiTextureCopyDescription copy;
+ copy.setPixelSize(QSize(WIDTH / 2, 1));
+ copy.setDestinationTopLeft(QPoint(WIDTH / 2, 0));
+ copy.setSourceTopLeft(QPoint(33, 67));
+ batch->copyTexture(dstTexture.data(), srcTexture.data(), copy);
+
+ copy.setDestinationTopLeft(QPoint(0, 0));
+ copy.setSourceTopLeft(QPoint(99, 12));
+ batch->copyTexture(dstTexture.data(), srcTexture.data(), copy);
+
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readResult, &result] {
+ result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888);
+ };
+
+ QRhiReadbackDescription readbackDescription(dstTexture.data());
+ batch->readBackTexture(readbackDescription, &readResult);
+ QVERIFY(submitResourceUpdates(rhi.data(), batch));
+ QVERIFY(!result.isNull());
+ QImage referenceImage(WIDTH, 1, result.format());
+ for (int i = 0; i < WIDTH / 2; ++i) {
+ referenceImage.setPixelColor(i, 0, img.pixelColor(99 + i, 12));
+ referenceImage.setPixelColor(WIDTH / 2 + i, 0, img.pixelColor(33 + i, 67));
+ }
+
+ QVERIFY(imageRGBAEquals(result, referenceImage));
+ }
+
+ // Copy from 2D texture to 1D texture array
+ {
+ const int WIDTH = 256;
+ const int HEIGHT = 256;
+ const int LAYERS = 64;
+
+ QScopedPointer<QRhiTexture> srcTexture(rhi->newTexture(
+ QRhiTexture::RGBA8, WIDTH, HEIGHT, 0, 1, QRhiTexture::Flag::UsedAsTransferSource));
+ QVERIFY(srcTexture->create());
+
+ QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
+ QVERIFY(batch);
+
+ QImage img(WIDTH, HEIGHT, QImage::Format_RGBA8888);
+ for (int x = 0; x < WIDTH; ++x) {
+ for (int y = 0; y < HEIGHT; ++y) {
+ img.setPixelColor(x, y, QColor::fromRgb(x, y, 0));
+ }
+ }
+ QRhiTextureUploadEntry upload(0, 0, QRhiTextureSubresourceUploadDescription(img));
+ batch->uploadTexture(srcTexture.data(), upload);
+
+ QScopedPointer<QRhiTexture> dstTexture(
+ rhi->newTextureArray(QRhiTexture::RGBA8, LAYERS, QSize(WIDTH, 0), 1,
+ QRhiTexture::Flag::UsedAsTransferSource));
+ QVERIFY(dstTexture->create());
+
+ QRhiTextureCopyDescription copy;
+ copy.setPixelSize(QSize(WIDTH / 2, 1));
+ copy.setDestinationTopLeft(QPoint(WIDTH / 2, 0));
+ copy.setSourceTopLeft(QPoint(33, 67));
+ copy.setDestinationLayer(12);
+ batch->copyTexture(dstTexture.data(), srcTexture.data(), copy);
+
+ copy.setDestinationTopLeft(QPoint(0, 0));
+ copy.setSourceTopLeft(QPoint(99, 12));
+ batch->copyTexture(dstTexture.data(), srcTexture.data(), copy);
+
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readResult, &result] {
+ result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888);
+ };
+
+ QRhiReadbackDescription readbackDescription(dstTexture.data());
+ readbackDescription.setLayer(12);
+ batch->readBackTexture(readbackDescription, &readResult);
+ QVERIFY(submitResourceUpdates(rhi.data(), batch));
+ QVERIFY(!result.isNull());
+ QImage referenceImage(WIDTH, 1, result.format());
+ for (int i = 0; i < WIDTH / 2; ++i) {
+ referenceImage.setPixelColor(i, 0, img.pixelColor(99 + i, 12));
+ referenceImage.setPixelColor(WIDTH / 2 + i, 0, img.pixelColor(33 + i, 67));
+ }
+
+ QVERIFY(imageRGBAEquals(result, referenceImage));
+ }
+
+ // Copy from 1D texture array to 1D texture
+ {
+ const int WIDTH = 256;
+ const int LAYERS = 256;
+
+ QScopedPointer<QRhiTexture> srcTexture(
+ rhi->newTextureArray(QRhiTexture::RGBA8, LAYERS, QSize(WIDTH, 0), 1,
+ QRhiTexture::Flag::UsedAsTransferSource));
+ QVERIFY(srcTexture->create());
+
+ QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
+ QVERIFY(batch);
+
+ for (int y = 0; y < LAYERS; ++y) {
+ QImage img(WIDTH, 1, QImage::Format_RGBA8888);
+ for (int x = 0; x < WIDTH; ++x) {
+ img.setPixelColor(x, 0, QColor::fromRgb(x, y, 0));
+ }
+ QRhiTextureUploadEntry upload(y, 0, QRhiTextureSubresourceUploadDescription(img));
+ batch->uploadTexture(srcTexture.data(), upload);
+ }
+
+ QScopedPointer<QRhiTexture> dstTexture(rhi->newTexture(
+ QRhiTexture::RGBA8, WIDTH, 0, 0, 1, QRhiTexture::Flag::UsedAsTransferSource));
+ QVERIFY(dstTexture->create());
+
+ QRhiTextureCopyDescription copy;
+ copy.setPixelSize(QSize(WIDTH / 2, 1));
+ copy.setDestinationTopLeft(QPoint(WIDTH / 2, 0));
+ copy.setSourceLayer(67);
+ copy.setSourceTopLeft(QPoint(33, 0));
+ batch->copyTexture(dstTexture.data(), srcTexture.data(), copy);
+
+ copy.setDestinationTopLeft(QPoint(0, 0));
+ copy.setSourceLayer(12);
+ copy.setSourceTopLeft(QPoint(99, 0));
+ batch->copyTexture(dstTexture.data(), srcTexture.data(), copy);
+
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readResult, &result] {
+ result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888);
+ };
+
+ QRhiReadbackDescription readbackDescription(dstTexture.data());
+ batch->readBackTexture(readbackDescription, &readResult);
+ QVERIFY(submitResourceUpdates(rhi.data(), batch));
+ QVERIFY(!result.isNull());
+ QImage referenceImage(WIDTH, 1, result.format());
+ for (int i = 0; i < WIDTH / 2; ++i) {
+ referenceImage.setPixelColor(i, 0, QColor::fromRgb(99 + i, 12, 0));
+ referenceImage.setPixelColor(WIDTH / 2 + i, 0, QColor::fromRgb(33 + i, 67, 0));
+ }
+
+ QVERIFY(imageRGBAEquals(result, referenceImage));
+ }
+
+ // Copy from 1D texture to 1D texture array
+ {
+ const int WIDTH = 256;
+ const int LAYERS = 256;
+
+ QScopedPointer<QRhiTexture> srcTexture(rhi->newTexture(
+ QRhiTexture::RGBA8, WIDTH, 0, 0, 1, QRhiTexture::Flag::UsedAsTransferSource));
+ QVERIFY(srcTexture->create());
+
+ QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
+ QVERIFY(batch);
+
+ QImage img(WIDTH, 1, QImage::Format_RGBA8888);
+ for (int x = 0; x < WIDTH; ++x) {
+ img.setPixelColor(x, 0, QColor::fromRgb(x, 0, 0));
+ }
+ QRhiTextureUploadEntry upload(0, 0, QRhiTextureSubresourceUploadDescription(img));
+ batch->uploadTexture(srcTexture.data(), upload);
+
+ QScopedPointer<QRhiTexture> dstTexture(
+ rhi->newTextureArray(QRhiTexture::RGBA8, LAYERS, QSize(WIDTH, 0), 1,
+ QRhiTexture::Flag::UsedAsTransferSource));
+ QVERIFY(dstTexture->create());
+
+ QRhiTextureCopyDescription copy;
+ copy.setPixelSize(QSize(WIDTH / 2, 1));
+ copy.setDestinationTopLeft(QPoint(WIDTH / 2, 0));
+ copy.setDestinationLayer(67);
+ copy.setSourceTopLeft(QPoint(33, 0));
+ batch->copyTexture(dstTexture.data(), srcTexture.data(), copy);
+
+ copy.setDestinationTopLeft(QPoint(0, 0));
+ copy.setSourceTopLeft(QPoint(99, 0));
+ batch->copyTexture(dstTexture.data(), srcTexture.data(), copy);
+
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readResult, &result] {
+ result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888);
+ };
+
+ QRhiReadbackDescription readbackDescription(dstTexture.data());
+ readbackDescription.setLayer(67);
+ batch->readBackTexture(readbackDescription, &readResult);
+ QVERIFY(submitResourceUpdates(rhi.data(), batch));
+ QVERIFY(!result.isNull());
+ QImage referenceImage(WIDTH, 1, result.format());
+ for (int i = 0; i < WIDTH / 2; ++i) {
+ referenceImage.setPixelColor(i, 0, QColor::fromRgb(99 + i, 0, 0));
+ referenceImage.setPixelColor(WIDTH / 2 + i, 0, QColor::fromRgb(33 + i, 0, 0));
+ }
+
+ QVERIFY(imageRGBAEquals(result, referenceImage));
+ }
+
+ // mipmaps and 1D render target
+ if (!rhi->isFeatureSupported(QRhi::OneDimensionalTextureMipmaps)
+ || !rhi->isFeatureSupported(QRhi::RenderToOneDimensionalTexture))
+ {
+ QSKIP("Skipping testing 1D texture mipmaps and 1D render target because they are reported as unsupported");
+ }
+
+ {
+ QScopedPointer<QRhiTexture> texture(
+ rhi->newTexture(QRhiTexture::RGBA8, WIDTH, 0, 0, 1,
+ QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips));
+ QVERIFY(texture->create());
+
+ QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
+ QVERIFY(batch);
+
+ QImage img(WIDTH, 1, QImage::Format_RGBA8888);
+ img.fill(QColor::fromRgb(128, 0, 0));
+ QRhiTextureUploadEntry upload(0, 0, QRhiTextureSubresourceUploadDescription(img));
+ batch->uploadTexture(texture.data(), upload);
+
+ batch->generateMips(texture.data());
+
+ QVERIFY(submitResourceUpdates(rhi.data(), batch));
+
+ // read back level 1 (256x1, #800000ff)
+ batch = rhi->nextResourceUpdateBatch();
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readResult, &result] {
+ result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888);
+ };
+ QRhiReadbackDescription readbackDescription(texture.data());
+ readbackDescription.setLevel(1);
+ readbackDescription.setLayer(0);
+ batch->readBackTexture(readbackDescription, &readResult);
+ QVERIFY(submitResourceUpdates(rhi.data(), batch));
+ QVERIFY(!result.isNull());
+ QImage referenceImage(WIDTH / 2, 1, result.format());
+ referenceImage.fill(QColor::fromRgb(128, 0, 0));
+
+ QVERIFY(imageRGBAEquals(result, referenceImage, 2));
+ }
+
+ {
+ QScopedPointer<QRhiTexture> texture(
+ rhi->newTextureArray(QRhiTexture::RGBA8, LAYERS, QSize(WIDTH, 0), 1,
+ QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips));
+ QVERIFY(texture->create());
+
+ QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
+ QVERIFY(batch);
+
+ for (int i = 0; i < LAYERS; ++i) {
+ QImage img(WIDTH, 1, QImage::Format_RGBA8888);
+ img.fill(QColor::fromRgb(i * 2, 0, 0));
+ QRhiTextureUploadEntry sliceUpload(i, 0, QRhiTextureSubresourceUploadDescription(img));
+ batch->uploadTexture(texture.data(), sliceUpload);
+ }
+
+ batch->generateMips(texture.data());
+
+ QVERIFY(submitResourceUpdates(rhi.data(), batch));
+
+ // read back slice 63 of level 1 (256x1, #7E0000FF)
+ batch = rhi->nextResourceUpdateBatch();
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readResult, &result] {
+ result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888);
+ };
+ QRhiReadbackDescription readbackDescription(texture.data());
+ readbackDescription.setLevel(1);
+ readbackDescription.setLayer(63);
+ batch->readBackTexture(readbackDescription, &readResult);
+ QVERIFY(submitResourceUpdates(rhi.data(), batch));
+ QVERIFY(!result.isNull());
+ QImage referenceImage(WIDTH / 2, 1, result.format());
+ referenceImage.fill(QColor::fromRgb(126, 0, 0));
+
+ // Now restrict the test a bit. The Null QRhi backend has broken support for
+ // mipmap generation of 1D texture arrays.
+ if (impl != QRhi::Null)
+ QVERIFY(imageRGBAEquals(result, referenceImage, 2));
+ }
+
+ // 1D texture render target
+ // NB with Vulkan we require Vulkan 1.1 for this to work.
+ // Metal does not allow 1D texture render targets
+ {
+ QScopedPointer<QRhiTexture> texture(
+ rhi->newTexture(QRhiTexture::RGBA8, WIDTH, 0, 0, 1,
+ QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
+ QVERIFY(texture->create());
+
+ QRhiColorAttachment att(texture.data());
+ QRhiTextureRenderTargetDescription rtDesc(att);
+ QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(rtDesc));
+ QScopedPointer<QRhiRenderPassDescriptor> rp(rt->newCompatibleRenderPassDescriptor());
+ rt->setRenderPassDescriptor(rp.data());
+ QVERIFY(rt->create());
+
+ QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
+ QVERIFY(batch);
+
+ QImage img(WIDTH, 1, QImage::Format_RGBA8888);
+ img.fill(QColor::fromRgb(128, 0, 0));
+ QRhiTextureUploadEntry upload(0, 0, QRhiTextureSubresourceUploadDescription(img));
+ batch->uploadTexture(texture.data(), upload);
+
+ QRhiCommandBuffer *cb = nullptr;
+ QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
+ QVERIFY(cb);
+ cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 }, batch);
+ // texture is now blue
+ cb->endPass();
+ rhi->endOffscreenFrame();
+
+ // read back texture (blue)
+ batch = rhi->nextResourceUpdateBatch();
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readResult, &result] {
+ result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888);
+ };
+ QRhiReadbackDescription readbackDescription(texture.data());
+ batch->readBackTexture(readbackDescription, &readResult);
+ QVERIFY(submitResourceUpdates(rhi.data(), batch));
+ QVERIFY(!result.isNull());
+ QImage referenceImage(WIDTH, 1, result.format());
+ referenceImage.fill(QColor::fromRgbF(0.0f, 0.0f, 1.0f));
+ // the Null backend does not render so skip the verification for that
+ if (impl != QRhi::Null)
+ QVERIFY(imageRGBAEquals(result, referenceImage));
+ }
+
+ // 1D array texture render target (one slice)
+ // NB with Vulkan we require Vulkan 1.1 for this to work.
+ // Metal does not allow 1D texture render targets
+ {
+ const int SLICE = 23;
+ QScopedPointer<QRhiTexture> texture(rhi->newTextureArray(
+ QRhiTexture::RGBA8, LAYERS, QSize(WIDTH, 0), 1,
+ QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
+ QVERIFY(texture->create());
+
+ QRhiColorAttachment att(texture.data());
+ att.setLayer(SLICE);
+ QRhiTextureRenderTargetDescription rtDesc(att);
+ QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(rtDesc));
+ QScopedPointer<QRhiRenderPassDescriptor> rp(rt->newCompatibleRenderPassDescriptor());
+ rt->setRenderPassDescriptor(rp.data());
+ QVERIFY(rt->create());
+
+ QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
+ QVERIFY(batch);
+
+ for (int i = 0; i < LAYERS; ++i) {
+ QImage img(WIDTH, 1, QImage::Format_RGBA8888);
img.fill(QColor::fromRgb(i * 2, 0, 0));
QRhiTextureUploadEntry sliceUpload(i, 0, QRhiTextureSubresourceUploadDescription(img));
batch->uploadTexture(texture.data(), sliceUpload);
@@ -4537,7 +5754,7 @@ void tst_QRhi::threeDimTexture()
batch->readBackTexture(readbackDescription, &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
QVERIFY(!result.isNull());
- QImage referenceImage(WIDTH, HEIGHT, result.format());
+ QImage referenceImage(WIDTH, 1, result.format());
referenceImage.fill(QColor::fromRgbF(0.0f, 0.0f, 1.0f));
// the Null backend does not render so skip the verification for that
if (impl != QRhi::Null)
@@ -4604,12 +5821,1263 @@ void tst_QRhi::leakedResourceDestroy()
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
+ QRhiRenderBuffer *rb = rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, QSize(512, 512));
+ QVERIFY(rb->create());
+
+ QRhiShaderResourceBindings *srb = rhi->newShaderResourceBindings();
+ QVERIFY(srb->create());
+
if (impl == QRhi::Vulkan)
qDebug("Vulkan validation layer warnings may be printed below - this is expected");
+ if (impl == QRhi::D3D12)
+ qDebug("QD3D12CpuDescriptorPool warnings may be printed below - this is expected");
+
+ qDebug("QRhi resource leak check warnings may be printed below - this is expected");
+
+ // make the QRhi go away early
rhi.reset();
- // let the scoped ptr do its job with the resources
+ // see if the internal rhi backpointer got nulled out
+ QVERIFY(buffer->rhi() == nullptr);
+ QVERIFY(texture->rhi() == nullptr);
+ QVERIFY(rt->rhi() == nullptr);
+ QVERIFY(rpDesc->rhi() == nullptr);
+ QVERIFY(rb->rhi() == nullptr);
+ QVERIFY(srb->rhi() == nullptr);
+
+ // test out deleteLater on some of the resources
+ rb->deleteLater();
+ srb->deleteLater();
+
+ // let the scoped ptr do its job with the rest
+}
+
+void tst_QRhi::renderToFloatTexture_data()
+{
+ rhiTestData();
+}
+
+void tst_QRhi::renderToFloatTexture()
+{
+ QFETCH(QRhi::Implementation, impl);
+ QFETCH(QRhiInitParams *, initParams);
+
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
+ if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing rendering");
+
+ if (!rhi->isTextureFormatSupported(QRhiTexture::RGBA16F))
+ QSKIP("RGBA16F is not supported, skipping test");
+
+ const QSize outputSize(1920, 1080);
+ QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA16F, outputSize, 1,
+ QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
+ QVERIFY(texture->create());
+
+ QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
+ QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
+ rt->setRenderPassDescriptor(rpDesc.data());
+ QVERIFY(rt->create());
+
+ QRhiCommandBuffer *cb = nullptr;
+ QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
+ QVERIFY(cb);
+
+ QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
+
+ QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices)));
+ QVERIFY(vbuf->create());
+ updates->uploadStaticBuffer(vbuf.data(), triangleVertices);
+
+ QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
+ QVERIFY(srb->create());
+
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(createSimplePipeline(rhi.data(), srb.data(), rpDesc.data()));
+ QVERIFY(pipeline);
+
+ cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 }, updates);
+ cb->setGraphicsPipeline(pipeline.data());
+ cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
+ QRhiCommandBuffer::VertexInput vbindings(vbuf.data(), 0);
+ cb->setVertexInput(0, 1, &vbindings);
+ cb->draw(3);
+
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readResult, &result] {
+ result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA16FPx4);
+ };
+ QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
+ readbackBatch->readBackTexture({ texture.data() }, &readResult);
+ cb->endPass(readbackBatch);
+
+ rhi->endOffscreenFrame();
+ QCOMPARE(result.size(), texture->pixelSize());
+
+ if (impl == QRhi::Null)
+ return;
+
+ if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
+ result = std::move(result).mirrored();
+
+ // Now we have a red rectangle on blue background.
+ const int y = 100;
+ const QRgbaFloat16 *p = reinterpret_cast<const QRgbaFloat16 *>(result.constScanLine(y));
+ int redCount = 0;
+ int blueCount = 0;
+ int x = result.width() - 1;
+ while (x-- >= 0) {
+ QRgbaFloat16 c = *p++;
+ if (c.red() >= 0.95f && qFuzzyIsNull(c.green()) && qFuzzyIsNull(c.blue()))
+ ++redCount;
+ else if (qFuzzyIsNull(c.red()) && qFuzzyIsNull(c.green()) && c.blue() >= 0.95f)
+ ++blueCount;
+ else
+ QFAIL("Encountered a pixel that is neither red or blue");
+ }
+ QCOMPARE(redCount + blueCount, texture->pixelSize().width());
+ QVERIFY(redCount > blueCount); // 1742 > 178
+}
+
+void tst_QRhi::renderToRgb10Texture_data()
+{
+ rhiTestData();
+}
+
+void tst_QRhi::renderToRgb10Texture()
+{
+ QFETCH(QRhi::Implementation, impl);
+ QFETCH(QRhiInitParams *, initParams);
+
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
+ if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing rendering");
+
+ if (!rhi->isTextureFormatSupported(QRhiTexture::RGB10A2))
+ QSKIP("RGB10A2 is not supported, skipping test");
+
+ const QSize outputSize(1920, 1080);
+ QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGB10A2, outputSize, 1,
+ QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
+ QVERIFY(texture->create());
+
+ QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
+ QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
+ rt->setRenderPassDescriptor(rpDesc.data());
+ QVERIFY(rt->create());
+
+ QRhiCommandBuffer *cb = nullptr;
+ QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
+ QVERIFY(cb);
+
+ QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
+
+ QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices)));
+ QVERIFY(vbuf->create());
+ updates->uploadStaticBuffer(vbuf.data(), triangleVertices);
+
+ QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
+ QVERIFY(srb->create());
+
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(createSimplePipeline(rhi.data(), srb.data(), rpDesc.data()));
+ QVERIFY(pipeline);
+
+ cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 }, updates);
+ cb->setGraphicsPipeline(pipeline.data());
+ cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
+ QRhiCommandBuffer::VertexInput vbindings(vbuf.data(), 0);
+ cb->setVertexInput(0, 1, &vbindings);
+ cb->draw(3);
+
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readResult, &result] {
+ result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_A2BGR30_Premultiplied);
+ };
+ QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
+ readbackBatch->readBackTexture({ texture.data() }, &readResult);
+ cb->endPass(readbackBatch);
+
+ rhi->endOffscreenFrame();
+ QCOMPARE(result.size(), texture->pixelSize());
+
+ if (impl == QRhi::Null)
+ return;
+
+ if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
+ result = std::move(result).mirrored();
+
+ // Now we have a red rectangle on blue background.
+ const int y = 100;
+ int redCount = 0;
+ int blueCount = 0;
+ const int maxFuzz = 1;
+ for (int x = 0; x < result.width(); ++x) {
+ QRgb c = result.pixel(x, y);
+ if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
+ ++redCount;
+ else if (qRed(c) == 0 && qGreen(c) == 0 && qBlue(c) >= (255 - maxFuzz))
+ ++blueCount;
+ else
+ QFAIL("Encountered a pixel that is neither red or blue");
+ }
+ QCOMPARE(redCount + blueCount, texture->pixelSize().width());
+ QVERIFY(redCount > blueCount); // 1742 > 178
+}
+
+void tst_QRhi::tessellation_data()
+{
+ rhiTestData();
+}
+
+void tst_QRhi::tessellation()
+{
+#ifdef Q_OS_ANDROID
+ if (QNativeInterface::QAndroidApplication::sdkVersion() >= 31)
+ QSKIP("Fails on Android 12 (QTBUG-108844)");
+#endif
+ QFETCH(QRhi::Implementation, impl);
+ QFETCH(QRhiInitParams *, initParams);
+
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
+ if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing rendering");
+
+ if (!rhi->isFeatureSupported(QRhi::Tessellation)) {
+ // From a Vulkan or Metal implementation we expect tessellation to work,
+ // even though it is optional (as per spec) for Vulkan.
+ QVERIFY(rhi->backend() != QRhi::Vulkan);
+ QVERIFY(rhi->backend() != QRhi::Metal);
+ QSKIP("Tessellation is not supported with this graphics API, skipping test");
+ }
+
+ if (rhi->backend() == QRhi::D3D11 || rhi->backend() == QRhi::D3D12)
+ QSKIP("Skipping tessellation test on D3D for now, test assets not prepared for HLSL yet");
+
+ QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(1280, 720), 1,
+ QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
+ QVERIFY(texture->create());
+
+ QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
+ QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
+ rt->setRenderPassDescriptor(rpDesc.data());
+ QVERIFY(rt->create());
+
+ static const float triangleVertices[] = {
+ 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f,
+ -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f,
+ 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f,
+ };
+
+ QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
+ QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices)));
+ QVERIFY(vbuf->create());
+ u->uploadStaticBuffer(vbuf.data(), triangleVertices);
+
+ QScopedPointer<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
+ QVERIFY(ubuf->create());
+
+ // Use the 3D API specific correction matrix that flips Y, so we can use
+ // the OpenGL-targeted vertex data and the tessellation winding order of
+ // counter-clockwise to get uniform results.
+ QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
+ u->updateDynamicBuffer(ubuf.data(), 0, 64, mvp.constData());
+
+ QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
+ srb->setBindings({
+ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::TessellationEvaluationStage, ubuf.data()),
+ });
+ QVERIFY(srb->create());
+
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
+
+ pipeline->setTopology(QRhiGraphicsPipeline::Patches);
+ pipeline->setPatchControlPointCount(3);
+
+ pipeline->setShaderStages({
+ { QRhiShaderStage::Vertex, loadShader(":/data/simpletess.vert.qsb") },
+ { QRhiShaderStage::TessellationControl, loadShader(":/data/simpletess.tesc.qsb") },
+ { QRhiShaderStage::TessellationEvaluation, loadShader(":/data/simpletess.tese.qsb") },
+ { QRhiShaderStage::Fragment, loadShader(":/data/simpletess.frag.qsb") }
+ });
+
+ pipeline->setCullMode(QRhiGraphicsPipeline::Back); // to ensure the winding order is correct
+
+ // won't get the wireframe with OpenGL ES
+ if (rhi->isFeatureSupported(QRhi::NonFillPolygonMode))
+ pipeline->setPolygonMode(QRhiGraphicsPipeline::Line);
+
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 6 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float3, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float3, 3 * sizeof(float) }
+ });
+
+ pipeline->setVertexInputLayout(inputLayout);
+ pipeline->setShaderResourceBindings(srb.data());
+ pipeline->setRenderPassDescriptor(rpDesc.data());
+
+ QVERIFY(pipeline->create());
+
+ QRhiCommandBuffer *cb = nullptr;
+ QCOMPARE(rhi->beginOffscreenFrame(&cb), QRhi::FrameOpSuccess);
+
+ cb->beginPass(rt.data(), Qt::black, { 1.0f, 0 }, u);
+ cb->setGraphicsPipeline(pipeline.data());
+ cb->setViewport({ 0, 0, float(rt->pixelSize().width()), float(rt->pixelSize().height()) });
+ cb->setShaderResources();
+ QRhiCommandBuffer::VertexInput vbufBinding(vbuf.data(), 0);
+ cb->setVertexInput(0, 1, &vbufBinding);
+ cb->draw(3);
+
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readResult, &result] {
+ result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888);
+ };
+ QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
+ readbackBatch->readBackTexture({ texture.data() }, &readResult);
+ cb->endPass(readbackBatch);
+
+ rhi->endOffscreenFrame();
+
+ if (rhi->isYUpInFramebuffer()) // we used clipSpaceCorrMatrix so this is different from many other tests
+ result = std::move(result).mirrored();
+
+ QCOMPARE(result.size(), rt->pixelSize());
+
+ // cannot check rendering results with Null, because there is no rendering there
+ if (impl == QRhi::Null)
+ return;
+
+ int redCount = 0, greenCount = 0, blueCount = 0;
+ for (int y = 0; y < result.height(); ++y) {
+ const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
+ int x = result.width() - 1;
+ while (x-- >= 0) {
+ const QRgb c(*p++);
+ const int red = qRed(c);
+ const int green = qGreen(c);
+ const int blue = qBlue(c);
+ // just count the color components that are above a certain threshold
+ if (red > 240)
+ ++redCount;
+ if (green > 240)
+ ++greenCount;
+ if (blue > 240)
+ ++blueCount;
+ }
+ }
+
+ // Line drawing can be different between the 3D APIs. What we will check if
+ // the number of strong-enough r/g/b components above a certain threshold.
+ // That is good enough to ensure that something got rendered, i.e. that
+ // tessellation is not completely broken.
+ //
+ // For the record the actual values are something like:
+ // OpenGL (NVIDIA, Windows) 59 82 82
+ // Metal (Intel, macOS 12.5) 59 79 79
+ // Vulkan (NVIDIA, Windows) 71 85 85
+
+ QVERIFY(redCount > 50);
+ QVERIFY(blueCount > 50);
+ QVERIFY(greenCount > 50);
+}
+
+void tst_QRhi::tessellationInterfaceBlocks_data()
+{
+ rhiTestData();
+}
+
+void tst_QRhi::tessellationInterfaceBlocks()
+{
+#ifdef Q_OS_ANDROID
+ if (QNativeInterface::QAndroidApplication::sdkVersion() >= 31)
+ QSKIP("Fails on Android 12 (QTBUG-108844)");
+#endif
+ QFETCH(QRhi::Implementation, impl);
+ QFETCH(QRhiInitParams *, initParams);
+
+ // This test is intended for Metal, but will run on other tessellation render pipelines
+ //
+ // Metal tessellation uses a combination of compute pipelines for the vert and tesc, and a
+ // render pipeline for the tese and frag. This test uses input output interface blocks between
+ // the tesc and tese, and all tese stage builtin inputs to check that the Metal tese-frag
+ // pipeline vertex inputs are correctly configured. The tese writes the values to a storage
+ // buffer whose values are checked by the unit test. MSL 2.1 is required for this test.
+ // (requires support for writing to a storage buffer in the vertex shader within a render
+ // pipeline)
+
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
+ if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing rendering");
+
+ if (!rhi->isFeatureSupported(QRhi::Tessellation)) {
+ // From a Vulkan or Metal implementation we expect tessellation to work,
+ // even though it is optional (as per spec) for Vulkan.
+ QVERIFY(rhi->backend() != QRhi::Vulkan);
+ QVERIFY(rhi->backend() != QRhi::Metal);
+ QSKIP("Tessellation is not supported with this graphics API, skipping test");
+ }
+
+ if (rhi->backend() == QRhi::D3D11 || rhi->backend() == QRhi::D3D12)
+ QSKIP("Skipping tessellation test on D3D for now, test assets not prepared for HLSL yet");
+
+ if (rhi->backend() == QRhi::OpenGLES2)
+ QSKIP("Skipping test on OpenGL as gl_ClipDistance[] support inconsistent");
+
+ QScopedPointer<QRhiTexture> texture(
+ rhi->newTexture(QRhiTexture::RGBA8, QSize(1280, 720), 1,
+ QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
+ QVERIFY(texture->create());
+
+ QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
+ QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
+ rt->setRenderPassDescriptor(rpDesc.data());
+ QVERIFY(rt->create());
+
+ static const float triangleVertices[] = {
+ 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, -0.5f, -0.5f, 0.0f,
+ 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f,
+ };
+
+ QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
+ QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer,
+ sizeof(triangleVertices)));
+ QVERIFY(vbuf->create());
+ u->uploadStaticBuffer(vbuf.data(), triangleVertices);
+
+ QScopedPointer<QRhiBuffer> ubuf(
+ rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
+ QVERIFY(ubuf->create());
+
+ // Use the 3D API specific correction matrix that flips Y, so we can use
+ // the OpenGL-targeted vertex data and the tessellation winding order of
+ // counter-clockwise to get uniform results.
+ QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
+ u->updateDynamicBuffer(ubuf.data(), 0, 64, mvp.constData());
+
+ QScopedPointer<QRhiBuffer> buffer(
+ rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::UsageFlag::StorageBuffer, 1024));
+ QVERIFY(buffer->create());
+
+ u->uploadStaticBuffer(buffer.data(), 0, 1024, QByteArray(1024, 0).constData());
+
+ QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
+ srb->setBindings(
+ { QRhiShaderResourceBinding::uniformBuffer(
+ 0, QRhiShaderResourceBinding::TessellationEvaluationStage, ubuf.data()),
+ QRhiShaderResourceBinding::bufferLoadStore(
+ 1, QRhiShaderResourceBinding::TessellationEvaluationStage, buffer.data()) });
+ QVERIFY(srb->create());
+
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
+
+ pipeline->setTopology(QRhiGraphicsPipeline::Patches);
+ pipeline->setPatchControlPointCount(3);
+
+ pipeline->setShaderStages(
+ { { QRhiShaderStage::Vertex, loadShader(":/data/tessinterfaceblocks.vert.qsb") },
+ { QRhiShaderStage::TessellationControl,
+ loadShader(":/data/tessinterfaceblocks.tesc.qsb") },
+ { QRhiShaderStage::TessellationEvaluation,
+ loadShader(":/data/tessinterfaceblocks.tese.qsb") },
+ { QRhiShaderStage::Fragment, loadShader(":/data/tessinterfaceblocks.frag.qsb") } });
+
+ pipeline->setCullMode(QRhiGraphicsPipeline::Back); // to ensure the winding order is correct
+
+ // won't get the wireframe with OpenGL ES
+ if (rhi->isFeatureSupported(QRhi::NonFillPolygonMode))
+ pipeline->setPolygonMode(QRhiGraphicsPipeline::Line);
+
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({ { 6 * sizeof(float) } });
+ inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float3, 0 },
+ { 0, 1, QRhiVertexInputAttribute::Float3, 3 * sizeof(float) } });
+
+ pipeline->setVertexInputLayout(inputLayout);
+ pipeline->setShaderResourceBindings(srb.data());
+ pipeline->setRenderPassDescriptor(rpDesc.data());
+
+ QVERIFY(pipeline->create());
+
+ QRhiCommandBuffer *cb = nullptr;
+ QCOMPARE(rhi->beginOffscreenFrame(&cb), QRhi::FrameOpSuccess);
+
+ cb->beginPass(rt.data(), Qt::black, { 1.0f, 0 }, u);
+ cb->setGraphicsPipeline(pipeline.data());
+ cb->setViewport({ 0, 0, float(rt->pixelSize().width()), float(rt->pixelSize().height()) });
+ cb->setShaderResources();
+ QRhiCommandBuffer::VertexInput vbufBinding(vbuf.data(), 0);
+ cb->setVertexInput(0, 1, &vbufBinding);
+ cb->draw(3);
+
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readResult, &result] {
+ result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888);
+ };
+ QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
+ readbackBatch->readBackTexture({ texture.data() }, &readResult);
+
+ QRhiReadbackResult bufferReadResult;
+ bufferReadResult.completed = []() {};
+ readbackBatch->readBackBuffer(buffer.data(), 0, 1024, &bufferReadResult);
+
+ cb->endPass(readbackBatch);
+
+ rhi->endOffscreenFrame();
+
+ if (rhi->isYUpInFramebuffer()) // we used clipSpaceCorrMatrix so this is different from many
+ // other tests
+ result = std::move(result).mirrored();
+
+ QCOMPARE(result.size(), rt->pixelSize());
+
+ // cannot check rendering results with Null, because there is no rendering there
+ if (impl == QRhi::Null)
+ return;
+
+ int redCount = 0, greenCount = 0, blueCount = 0;
+ for (int y = 0; y < result.height(); ++y) {
+ const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
+ int x = result.width() - 1;
+ while (x-- >= 0) {
+ const QRgb c(*p++);
+ const int red = qRed(c);
+ const int green = qGreen(c);
+ const int blue = qBlue(c);
+ // just count the color components that are above a certain threshold
+ if (red > 240)
+ ++redCount;
+ if (green > 240)
+ ++greenCount;
+ if (blue > 240)
+ ++blueCount;
+ }
+ }
+
+ // make sure we drew something
+ QVERIFY(redCount > 50);
+ QVERIFY(blueCount > 50);
+ QVERIFY(greenCount > 50);
+
+ // StorageBlock("result" "" knownSize=16 binding=1 set=0 runtimeArrayStride=336 QList(
+ // BlockVariable("int" "count" offset=0 size=4),
+ // BlockVariable("struct" "elements" offset=16 size=0 array=QList(0) structMembers=QList(
+ // BlockVariable("struct" "a" offset=0 size=48 array=QList(3) structMembers=QList(
+ // BlockVariable("vec3" "color" offset=0 size=12),
+ // BlockVariable("int" "id" offset=12 size=4))),
+ // BlockVariable("struct" "b" offset=48 size=144 array=QList(3) structMembers=QList(
+ // BlockVariable("vec2" "some" offset=0 size=8),
+ // BlockVariable("int" "other" offset=8 size=12 array=QList(3)),
+ // BlockVariable("vec3" "variables" offset=32 size=12))),
+ // BlockVariable("struct" "c" offset=192 size=16 structMembers=QList(
+ // BlockVariable("vec3" "stuff" offset=0 size=12),
+ // BlockVariable("float" "more_stuff" offset=12 size=4))),
+ // BlockVariable("vec4" "tesslevelOuter" offset=208 size=16),
+ // BlockVariable("vec2" "tessLevelInner" offset=224 size=8),
+ // BlockVariable("float" "pointSize" offset=232 size=12 array=QList(3)),
+ // BlockVariable("float" "clipDistance" offset=244 size=60 array=QList(5, 3)),
+ // BlockVariable("vec3" "tessCoord" offset=304 size=12),
+ // BlockVariable("int" "patchVerticesIn" offset=316 size=4),
+ // BlockVariable("int" "primitiveID" offset=320 size=4)))))
+
+ // int count
+ QCOMPARE(reinterpret_cast<const int *>(&bufferReadResult.data.constData()[0])[0], 1);
+
+ // a[0].color
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 0 + 0])[0], 0.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 0 + 0])[1], 0.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 0 + 0])[2], 1.0f);
+
+ // a[0].id
+ QCOMPARE(reinterpret_cast<const int *>(&bufferReadResult.data.constData()[16 + 0 + 12])[0], 91);
+
+ // a[1].color
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 16 + 0])[0], 1.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 16 + 0])[1], 0.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 16 + 0])[2], 0.0f);
+
+ // a[1].id
+ QCOMPARE(reinterpret_cast<const int *>(&bufferReadResult.data.constData()[16 + 16 + 12])[0], 92);
+
+ // a[2].color
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 32 + 0])[0], 0.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 32 + 0])[1], 1.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 32 + 0])[2], 0.0f);
+
+ // a[2].id
+ QCOMPARE(reinterpret_cast<const int *>(&bufferReadResult.data.constData()[16 + 32 + 12])[0], 93);
+
+ // b[0].some
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 48 + 0])[0], 0.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 48 + 0])[1], 0.0f);
+
+ // b[0].other[0]
+ QCOMPARE(reinterpret_cast<const int *>(&bufferReadResult.data.constData()[16 + 48 + 8])[0], 10.0f);
+ QCOMPARE(reinterpret_cast<const int *>(&bufferReadResult.data.constData()[16 + 48 + 8])[1], 20.0f);
+ QCOMPARE(reinterpret_cast<const int *>(&bufferReadResult.data.constData()[16 + 48 + 8])[2], 30.0f);
+
+ // b[0].variables
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 48 + 32])[0], 3.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 48 + 32])[1], 13.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 48 + 32])[2], 17.0f);
+
+ // b[1].some
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 96 + 0])[0], 1.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 96 + 0])[1], 1.0f);
+
+ // b[1].other[0]
+ QCOMPARE(reinterpret_cast<const int *>(&bufferReadResult.data.constData()[16 + 96 + 8])[0], 10.0f);
+ QCOMPARE(reinterpret_cast<const int *>(&bufferReadResult.data.constData()[16 + 96 + 8])[1], 20.0f);
+ QCOMPARE(reinterpret_cast<const int *>(&bufferReadResult.data.constData()[16 + 96 + 8])[2], 30.0f);
+
+ // b[1].variables
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 96 + 32])[0], 3.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 96 + 32])[1], 14.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 96 + 32])[2], 17.0f);
+
+ // b[2].some
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 144 + 0])[0], 2.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 144 + 0])[1], 2.0f);
+
+ // b[2].other[0]
+ QCOMPARE(reinterpret_cast<const int *>(&bufferReadResult.data.constData()[16 + 144 + 8])[0], 10.0f);
+ QCOMPARE(reinterpret_cast<const int *>(&bufferReadResult.data.constData()[16 + 144 + 8])[1], 20.0f);
+ QCOMPARE(reinterpret_cast<const int *>(&bufferReadResult.data.constData()[16 + 144 + 8])[2], 30.0f);
+
+ // b[2].variables
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 144 + 32])[0], 3.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 144 + 32])[1], 15.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 144 + 32])[2], 17.0f);
+
+ // c.stuff
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 192 + 0])[0], 1.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 192 + 0])[1], 2.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 192 + 0])[2], 3.0f);
+
+ // c.more_stuff
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 192 + 12])[0], 4.0f);
+
+ // tessLevelOuter
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 208 + 0])[0], 1.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 208 + 0])[1], 2.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 208 + 0])[2], 3.0f);
+
+ // tessLevelInner
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 224 + 0])[0], 5.0f);
+
+ // pointSize[0]
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 232 + 0])[0], 10.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 232 + 0])[1], 11.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 232 + 0])[2], 12.0f);
+
+ // clipDistance[0][0]
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 244 + 0])[0], 20.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 244 + 0])[1], 40.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 244 + 0])[2], 60.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 244 + 0])[3], 80.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 244 + 0])[4], 100.0f);
+
+ // clipDistance[1][0]
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 244 + 20])[0], 21.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 244 + 20])[1], 41.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 244 + 20])[2], 61.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 244 + 20])[3], 81.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 244 + 20])[4], 101.0f);
+
+ // clipDistance[2][0]
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 244 + 40])[0], 22.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 244 + 40])[1], 42.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 244 + 40])[2], 62.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 244 + 40])[3], 82.0f);
+ QCOMPARE(reinterpret_cast<const float *>(&bufferReadResult.data.constData()[16 + 244 + 40])[4], 102.0f);
+
+ // patchVerticesIn
+ QCOMPARE(reinterpret_cast<const int *>(&bufferReadResult.data.constData()[16 + 316 + 0])[0], 3);
+
+ // primitiveID
+ QCOMPARE(reinterpret_cast<const int *>(&bufferReadResult.data.constData()[16 + 320 + 0])[0], 0);
+}
+
+void tst_QRhi::storageBuffer_data()
+{
+ rhiTestData();
+}
+
+void tst_QRhi::storageBuffer()
+{
+ // Use a compute shader to copy from one storage buffer of float types to
+ // another of int types. We fill the "toGpu" buffer with known float type
+ // data generated and uploaded from the CPU, then dispatch a compute shader
+ // to copy from the "toGpu" buffer to the "fromGpu" buffer. We then
+ // readback the "fromGpu" buffer and verify that the results are as
+ // expected.
+
+ QFETCH(QRhi::Implementation, impl);
+ QFETCH(QRhiInitParams *, initParams);
+
+ // we can't test with Null as there is no compute
+ if (impl == QRhi::Null)
+ return;
+
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
+ if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing");
+
+ if (!rhi->isFeatureSupported(QRhi::Feature::Compute))
+ QSKIP("Compute is not supported with this graphics API, skipping test");
+
+ QShader s = loadShader(":/data/storagebuffer.comp.qsb");
+ QVERIFY(s.isValid());
+ QCOMPARE(s.description().storageBlocks().size(), 2);
+
+ QMap<QByteArray, QShaderDescription::StorageBlock> blocks;
+ for (const QShaderDescription::StorageBlock &block : s.description().storageBlocks())
+ blocks[block.blockName] = block;
+
+ QMap<QByteArray, QShaderDescription::BlockVariable> toGpuMembers;
+ for (const QShaderDescription::BlockVariable &member: blocks["toGpu"].members)
+ toGpuMembers[member.name] = member;
+
+ QMap<QByteArray, QShaderDescription::BlockVariable> fromGpuMembers;
+ for (const QShaderDescription::BlockVariable &member: blocks["fromGpu"].members)
+ fromGpuMembers[member.name] = member;
+
+ for (QRhiBuffer::Type type : {QRhiBuffer::Type::Immutable, QRhiBuffer::Type::Static}) {
+
+ QRhiCommandBuffer *cb = nullptr;
+ rhi->beginOffscreenFrame(&cb);
+ QVERIFY(cb);
+
+ QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
+ QVERIFY(u);
+
+ QScopedPointer<QRhiBuffer> toGpuBuffer(rhi->newBuffer(type, QRhiBuffer::UsageFlag::StorageBuffer, blocks["toGpu"].knownSize));
+ QVERIFY(toGpuBuffer->create());
+
+ QScopedPointer<QRhiBuffer> fromGpuBuffer(rhi->newBuffer(type, QRhiBuffer::UsageFlag::StorageBuffer, blocks["fromGpu"].knownSize));
+ QVERIFY(fromGpuBuffer->create());
+
+ QByteArray toGpuData(blocks["toGpu"].knownSize, 0);
+ reinterpret_cast<float *>(&toGpuData.data()[toGpuMembers["_float"].offset])[0] = 1.0f;
+ reinterpret_cast<float *>(&toGpuData.data()[toGpuMembers["_vec2"].offset])[0] = 2.0f;
+ reinterpret_cast<float *>(&toGpuData.data()[toGpuMembers["_vec2"].offset])[1] = 3.0f;
+ reinterpret_cast<float *>(&toGpuData.data()[toGpuMembers["_vec3"].offset])[0] = 4.0f;
+ reinterpret_cast<float *>(&toGpuData.data()[toGpuMembers["_vec3"].offset])[1] = 5.0f;
+ reinterpret_cast<float *>(&toGpuData.data()[toGpuMembers["_vec3"].offset])[2] = 6.0f;
+ reinterpret_cast<float *>(&toGpuData.data()[toGpuMembers["_vec4"].offset])[0] = 7.0f;
+ reinterpret_cast<float *>(&toGpuData.data()[toGpuMembers["_vec4"].offset])[1] = 8.0f;
+ reinterpret_cast<float *>(&toGpuData.data()[toGpuMembers["_vec4"].offset])[2] = 9.0f;
+ reinterpret_cast<float *>(&toGpuData.data()[toGpuMembers["_vec4"].offset])[3] = 10.0f;
+
+ u->uploadStaticBuffer(toGpuBuffer.data(), 0, toGpuData.size(), toGpuData.constData());
+ u->uploadStaticBuffer(fromGpuBuffer.data(), 0, blocks["fromGpu"].knownSize, QByteArray(blocks["fromGpu"].knownSize, 0).constData());
+
+ QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
+ srb->setBindings({QRhiShaderResourceBinding::bufferLoad(blocks["toGpu"].binding, QRhiShaderResourceBinding::ComputeStage, toGpuBuffer.data()),
+ QRhiShaderResourceBinding::bufferLoadStore(blocks["fromGpu"].binding, QRhiShaderResourceBinding::ComputeStage, fromGpuBuffer.data())});
+
+ QVERIFY(srb->create());
+
+ QScopedPointer<QRhiComputePipeline> pipeline(rhi->newComputePipeline());
+ pipeline->setShaderStage({QRhiShaderStage::Compute, s});
+ pipeline->setShaderResourceBindings(srb.data());
+ QVERIFY(pipeline->create());
+
+ cb->beginComputePass(u);
+
+ cb->setComputePipeline(pipeline.data());
+ cb->setShaderResources();
+ cb->dispatch(1, 1, 1);
+
+ u = rhi->nextResourceUpdateBatch();
+ QVERIFY(u);
+
+ int readCompletedNotifications = 0;
+ QRhiReadbackResult result;
+ result.completed = [&readCompletedNotifications]() { readCompletedNotifications++; };
+ u->readBackBuffer(fromGpuBuffer.data(), 0, blocks["fromGpu"].knownSize, &result);
+
+ cb->endComputePass(u);
+
+ rhi->endOffscreenFrame();
+
+ QCOMPARE(readCompletedNotifications, 1);
+
+ QCOMPARE(result.data.size(), blocks["fromGpu"].knownSize);
+ QCOMPARE(reinterpret_cast<const int *>(&result.data.constData()[fromGpuMembers["_int"].offset])[0], 1);
+ QCOMPARE(reinterpret_cast<const int *>(&result.data.constData()[fromGpuMembers["_ivec2"].offset])[0], 2);
+ QCOMPARE(reinterpret_cast<const int *>(&result.data.constData()[fromGpuMembers["_ivec2"].offset])[1], 3);
+ QCOMPARE(reinterpret_cast<const int *>(&result.data.constData()[fromGpuMembers["_ivec3"].offset])[0], 4);
+ QCOMPARE(reinterpret_cast<const int *>(&result.data.constData()[fromGpuMembers["_ivec3"].offset])[1], 5);
+ QCOMPARE(reinterpret_cast<const int *>(&result.data.constData()[fromGpuMembers["_ivec3"].offset])[2], 6);
+ QCOMPARE(reinterpret_cast<const int *>(&result.data.constData()[fromGpuMembers["_ivec4"].offset])[0], 7);
+ QCOMPARE(reinterpret_cast<const int *>(&result.data.constData()[fromGpuMembers["_ivec4"].offset])[1], 8);
+ QCOMPARE(reinterpret_cast<const int *>(&result.data.constData()[fromGpuMembers["_ivec4"].offset])[2], 9);
+ QCOMPARE(reinterpret_cast<const int *>(&result.data.constData()[fromGpuMembers["_ivec4"].offset])[3], 10);
+
+ }
+}
+
+ void tst_QRhi::storageBufferRuntimeSizeCompute_data()
+{
+ rhiTestData();
+}
+
+ void tst_QRhi::storageBufferRuntimeSizeCompute()
+{
+ // Use a compute shader to copy from one storage buffer with std430 runtime
+ // float array to another with std140 runtime int array. We fill the
+ // "toGpu" buffer with known float data generated and uploaded from the
+ // CPU, then dispatch a compute shader to copy from the "toGpu" buffer to
+ // the "fromGpu" buffer. We then readback the "fromGpu" buffer and verify
+ // that the results are as expected. This is primarily to test Metal
+ // SPIRV-Cross buffer size buffers.
+
+ QFETCH(QRhi::Implementation, impl);
+ QFETCH(QRhiInitParams *, initParams);
+
+ // we can't test with Null as there is no compute
+ if (impl == QRhi::Null)
+ return;
+
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
+ if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing");
+
+ if (!rhi->isFeatureSupported(QRhi::Feature::Compute))
+ QSKIP("Compute is not supported with this graphics API, skipping test");
+
+ QShader s = loadShader(":/data/storagebuffer_runtime.comp.qsb");
+ QVERIFY(s.isValid());
+ QCOMPARE(s.description().storageBlocks().size(), 2);
+
+ QMap<QByteArray, QShaderDescription::StorageBlock> blocks;
+ for (const QShaderDescription::StorageBlock &block : s.description().storageBlocks())
+ blocks[block.blockName] = block;
+
+ QMap<QByteArray, QShaderDescription::BlockVariable> toGpuMembers;
+ for (const QShaderDescription::BlockVariable &member : blocks["toGpu"].members)
+ toGpuMembers[member.name] = member;
+
+ QMap<QByteArray, QShaderDescription::BlockVariable> fromGpuMembers;
+ for (const QShaderDescription::BlockVariable &member : blocks["fromGpu"].members)
+ fromGpuMembers[member.name] = member;
+
+ for (QRhiBuffer::Type type : { QRhiBuffer::Type::Immutable, QRhiBuffer::Type::Static }) {
+ QRhiCommandBuffer *cb = nullptr;
+
+ rhi->beginOffscreenFrame(&cb);
+ QVERIFY(cb);
+
+ QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
+ QVERIFY(u);
+
+ const int stride430 = sizeof(float);
+ const int stride140 = 4 * sizeof(float);
+ const int length = 32;
+
+ QScopedPointer<QRhiBuffer> toGpuBuffer(
+ rhi->newBuffer(type, QRhiBuffer::UsageFlag::StorageBuffer,
+ blocks["toGpu"].knownSize + length * stride430));
+ QVERIFY(toGpuBuffer->create());
+
+ QScopedPointer<QRhiBuffer> fromGpuBuffer(
+ rhi->newBuffer(type, QRhiBuffer::UsageFlag::StorageBuffer,
+ blocks["fromGpu"].knownSize + length * stride140));
+ QVERIFY(fromGpuBuffer->create());
+
+ QByteArray toGpuData(toGpuBuffer->size(), 0);
+ for (int i = 0; i < length; ++i)
+ reinterpret_cast<float &>(toGpuData.data()[toGpuMembers["_float"].offset + i * stride430]) = float(i);
+
+ u->uploadStaticBuffer(toGpuBuffer.data(), 0, toGpuData.size(), toGpuData.constData());
+ u->uploadStaticBuffer(fromGpuBuffer.data(), 0, blocks["fromGpu"].knownSize,
+ QByteArray(fromGpuBuffer->size(), 0).constData());
+
+ QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
+ srb->setBindings(
+ { QRhiShaderResourceBinding::bufferLoadStore(
+ blocks["toGpu"].binding, QRhiShaderResourceBinding::ComputeStage,
+ toGpuBuffer.data()),
+ QRhiShaderResourceBinding::bufferLoadStore(
+ blocks["fromGpu"].binding, QRhiShaderResourceBinding::ComputeStage,
+ fromGpuBuffer.data()) });
+ QVERIFY(srb->create());
+
+ QScopedPointer<QRhiComputePipeline> pipeline(rhi->newComputePipeline());
+ pipeline->setShaderStage({ QRhiShaderStage::Compute, s });
+ pipeline->setShaderResourceBindings(srb.data());
+ QVERIFY(pipeline->create());
+
+ cb->beginComputePass(u);
+
+ cb->setComputePipeline(pipeline.data());
+ cb->setShaderResources();
+ cb->dispatch(1, 1, 1);
+
+ u = rhi->nextResourceUpdateBatch();
+ QVERIFY(u);
+ int readbackCompleted = 0;
+ QRhiReadbackResult result;
+ result.completed = [&readbackCompleted]() { readbackCompleted++; };
+ u->readBackBuffer(fromGpuBuffer.data(), 0, fromGpuBuffer->size(), &result);
+
+ cb->endComputePass(u);
+
+ rhi->endOffscreenFrame();
+
+ QVERIFY(readbackCompleted > 0);
+ QCOMPARE(result.data.size(), fromGpuBuffer->size());
+
+ for (int i = 0; i < length; ++i)
+ QCOMPARE(reinterpret_cast<const int &>(result.data.constData()[fromGpuMembers["_int"].offset + i * stride140]), i);
+
+ QCOMPARE(readbackCompleted, 1);
+
+ }
+
+}
+
+void tst_QRhi::storageBufferRuntimeSizeGraphics_data()
+{
+ rhiTestData();
+}
+
+void tst_QRhi::storageBufferRuntimeSizeGraphics()
+{
+#ifdef Q_OS_ANDROID
+ if (QNativeInterface::QAndroidApplication::sdkVersion() >= 31)
+ QSKIP("Fails on Android 12 (QTBUG-108844)");
+#endif
+ // Draws a tessellated triangle with color determined by the length of
+ // buffers bound to shader stages. This is primarily to test Metal
+ // SPIRV-Cross buffer size buffers.
+
+ QFETCH(QRhi::Implementation, impl);
+ QFETCH(QRhiInitParams *, initParams);
+
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
+ if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing rendering");
+
+ if (!rhi->isFeatureSupported(QRhi::Tessellation)) {
+ // From a Vulkan or Metal implementation we expect tessellation to work,
+ // even though it is optional (as per spec) for Vulkan.
+ QVERIFY(rhi->backend() != QRhi::Vulkan);
+ QVERIFY(rhi->backend() != QRhi::Metal);
+ QSKIP("Tessellation is not supported with this graphics API, skipping test");
+ }
+
+ if (rhi->backend() == QRhi::D3D11 || rhi->backend() == QRhi::D3D12)
+ QSKIP("Skipping tessellation test on D3D for now, test assets not prepared for HLSL yet");
+
+ QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(64, 64), 1,
+ QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
+ QVERIFY(texture->create());
+
+ QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
+ QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
+ rt->setRenderPassDescriptor(rpDesc.data());
+ QVERIFY(rt->create());
+
+ static const float triangleVertices[] = {
+ 0.0f, 0.5f, 0.0f,
+ -0.5f, -0.5f, 0.0f,
+ 0.5f, -0.5f, 0.0f,
+ };
+
+ QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
+ QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices)));
+ QVERIFY(vbuf->create());
+ u->uploadStaticBuffer(vbuf.data(), triangleVertices);
+
+ QScopedPointer<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
+ QVERIFY(ubuf->create());
+
+ QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
+ u->updateDynamicBuffer(ubuf.data(), 0, 64, mvp.constData());
+
+ QScopedPointer<QRhiBuffer> ssbo5(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, 256));
+ QVERIFY(ssbo5->create());
+
+ QScopedPointer<QRhiBuffer> ssbo3(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, 16));
+ QVERIFY(ssbo3->create());
+
+ u->uploadStaticBuffer(ssbo3.data(), QVector<float>({ 1.0f, 1.0f, 1.0f, 1.0f }).constData());
+
+ QScopedPointer<QRhiBuffer> ssbo4(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, 128));
+ QVERIFY(ssbo4->create());
+
+ const int red = 79;
+ const int green = 43;
+ const int blue = 251;
+
+ QScopedPointer<QRhiBuffer> ssboR(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, red * sizeof(float)));
+ QVERIFY(ssboR->create());
+
+ QScopedPointer<QRhiBuffer> ssboG(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, green * sizeof(float)));
+ QVERIFY(ssboG->create());
+
+ QScopedPointer<QRhiBuffer> ssboB(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, blue * sizeof(float)));
+ QVERIFY(ssboB->create());
+
+ const int tessOuter0 = 1;
+ const int tessOuter1 = 2;
+ const int tessOuter2 = 3;
+ const int tessInner0 = 4;
+
+ QScopedPointer<QRhiBuffer> ssboTessOuter0(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, tessOuter0 * sizeof(float)));
+ QVERIFY(ssboTessOuter0->create());
+
+ QScopedPointer<QRhiBuffer> ssboTessOuter1(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, tessOuter1 * sizeof(float)));
+ QVERIFY(ssboTessOuter1->create());
+
+ QScopedPointer<QRhiBuffer> ssboTessOuter2(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, tessOuter2 * sizeof(float)));
+ QVERIFY(ssboTessOuter2->create());
+
+ QScopedPointer<QRhiBuffer> ssboTessInner0(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, tessInner0 * sizeof(float)));
+ QVERIFY(ssboTessInner0->create());
+
+
+ QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
+ srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::TessellationEvaluationStage, ubuf.data()),
+ QRhiShaderResourceBinding::bufferLoad(5, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::TessellationEvaluationStage, ssbo5.data()),
+ QRhiShaderResourceBinding::bufferLoad(3, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::TessellationEvaluationStage | QRhiShaderResourceBinding::FragmentStage, ssbo3.data()),
+ QRhiShaderResourceBinding::bufferLoad(4, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::TessellationEvaluationStage, ssbo4.data()),
+ QRhiShaderResourceBinding::bufferLoad(7, QRhiShaderResourceBinding::TessellationControlStage, ssboTessOuter0.data()),
+ QRhiShaderResourceBinding::bufferLoad(8, QRhiShaderResourceBinding::TessellationControlStage | QRhiShaderResourceBinding::TessellationEvaluationStage, ssboTessOuter1.data()),
+ QRhiShaderResourceBinding::bufferLoad(9, QRhiShaderResourceBinding::TessellationControlStage, ssboTessOuter2.data()),
+ QRhiShaderResourceBinding::bufferLoad(10, QRhiShaderResourceBinding::TessellationControlStage, ssboTessInner0.data()),
+ QRhiShaderResourceBinding::bufferLoad(1, QRhiShaderResourceBinding::FragmentStage, ssboG.data()),
+ QRhiShaderResourceBinding::bufferLoad(2, QRhiShaderResourceBinding::FragmentStage, ssboB.data()),
+ QRhiShaderResourceBinding::bufferLoad(6, QRhiShaderResourceBinding::FragmentStage, ssboR.data()) });
+
+ QVERIFY(srb->create());
+
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
+
+ pipeline->setTopology(QRhiGraphicsPipeline::Patches);
+ pipeline->setPatchControlPointCount(3);
+
+ pipeline->setShaderStages({
+ { QRhiShaderStage::Vertex, loadShader(":/data/storagebuffer_runtime.vert.qsb") },
+ { QRhiShaderStage::TessellationControl, loadShader(":/data/storagebuffer_runtime.tesc.qsb") },
+ { QRhiShaderStage::TessellationEvaluation, loadShader(":/data/storagebuffer_runtime.tese.qsb") },
+ { QRhiShaderStage::Fragment, loadShader(":/data/storagebuffer_runtime.frag.qsb") }
+ });
+
+ pipeline->setCullMode(QRhiGraphicsPipeline::None);
+
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({
+ { 3 * sizeof(float) }
+ });
+ inputLayout.setAttributes({
+ { 0, 0, QRhiVertexInputAttribute::Float3, 0 },
+ });
+
+ pipeline->setVertexInputLayout(inputLayout);
+ pipeline->setShaderResourceBindings(srb.data());
+ pipeline->setRenderPassDescriptor(rpDesc.data());
+
+ QVERIFY(pipeline->create());
+
+ QRhiCommandBuffer *cb = nullptr;
+ QCOMPARE(rhi->beginOffscreenFrame(&cb), QRhi::FrameOpSuccess);
+
+ cb->beginPass(rt.data(), Qt::black, { 1.0f, 0 }, u);
+ cb->setGraphicsPipeline(pipeline.data());
+ cb->setViewport({ 0, 0, float(rt->pixelSize().width()), float(rt->pixelSize().height()) });
+ cb->setShaderResources();
+ QRhiCommandBuffer::VertexInput vbufBinding(vbuf.data(), 0);
+ cb->setVertexInput(0, 1, &vbufBinding);
+ cb->draw(3);
+
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readResult, &result] {
+ result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888);
+ };
+ QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
+ readbackBatch->readBackTexture({ texture.data() }, &readResult);
+ cb->endPass(readbackBatch);
+
+ rhi->endOffscreenFrame();
+
+ QCOMPARE(result.size(), rt->pixelSize());
+
+ // cannot check rendering results with Null, because there is no rendering there
+ if (impl == QRhi::Null)
+ return;
+
+ QCOMPARE(result.pixel(32, 32), qRgb(red, green, blue));
+}
+
+void tst_QRhi::halfPrecisionAttributes_data()
+{
+ rhiTestData();
+}
+
+void tst_QRhi::halfPrecisionAttributes()
+{
+#ifdef Q_OS_ANDROID
+ if (QNativeInterface::QAndroidApplication::sdkVersion() >= 31)
+ QSKIP("Fails on Android 12 (QTBUG-108844)");
+#endif
+ QFETCH(QRhi::Implementation, impl);
+ QFETCH(QRhiInitParams *, initParams);
+
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
+ if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing rendering");
+
+ if (!rhi->isFeatureSupported(QRhi::HalfAttributes)) {
+ QVERIFY(rhi->backend() != QRhi::Vulkan);
+ QVERIFY(rhi->backend() != QRhi::Metal);
+ QVERIFY(rhi->backend() != QRhi::D3D11);
+ QVERIFY(rhi->backend() != QRhi::D3D12);
+ QSKIP("Half precision vertex attributes are not supported with this graphics API, skipping test");
+ }
+
+ const QSize outputSize(1920, 1080);
+ QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
+ QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
+ QVERIFY(texture->create());
+
+ QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
+ QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
+ rt->setRenderPassDescriptor(rpDesc.data());
+ QVERIFY(rt->create());
+
+ QRhiCommandBuffer *cb = nullptr;
+ QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
+ QVERIFY(cb);
+
+ QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
+
+ //
+ // This test uses half3 vertices
+ //
+ // Note: D3D does not support half3 - rhi passes it through as half4. Because of this, D3D will
+ // report the following warning and error if we don't take precautions:
+ //
+ // D3D11 WARNING: ID3D11DeviceContext::Draw: Input vertex slot 0 has stride 6 which is less than
+ // the minimum stride logically expected from the current Input Layout (8 bytes). This is OK, as
+ // hardware is perfectly capable of reading overlapping data. However the developer probably did
+ // not intend to make use of this behavior. [ EXECUTION WARNING #355:
+ // DEVICE_DRAW_VERTEX_BUFFER_STRIDE_TOO_SMALL]
+ //
+ // D3D11 ERROR: ID3D11DeviceContext::Draw: Vertex Buffer Stride (6) at the input vertex slot 0
+ // is not aligned properly. The current Input Layout imposes an alignment of (4) because of the
+ // Formats used with this slot. [ EXECUTION ERROR #367: DEVICE_DRAW_VERTEX_STRIDE_UNALIGNED]
+ //
+ // The same warning and error are produced for D3D12. The rendered output is correct despite
+ // the warning and error.
+ //
+ // To avoid these errors, we pad the vertices to 8 byte stride.
+ //
+ static const qfloat16 vertices[] = {
+ qfloat16(-1.0), qfloat16(-1.0), qfloat16(0.0), qfloat16(0.0),
+ qfloat16(1.0), qfloat16(-1.0), qfloat16(0.0), qfloat16(0.0),
+ qfloat16(0.0), qfloat16(1.0), qfloat16(0.0), qfloat16(0.0),
+ };
+
+ QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
+ QVERIFY(vbuf->create());
+ updates->uploadStaticBuffer(vbuf.data(), vertices);
+
+ QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
+ QVERIFY(srb->create());
+
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
+ QShader vs = loadShader(":/data/half.vert.qsb");
+ QVERIFY(vs.isValid());
+ QShader fs = loadShader(":/data/simple.frag.qsb");
+ QVERIFY(fs.isValid());
+ pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({ { 4 * sizeof(qfloat16) } }); // 8 byte vertex stride for D3D
+ inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Half3, 0 } });
+ pipeline->setVertexInputLayout(inputLayout);
+ pipeline->setShaderResourceBindings(srb.data());
+ pipeline->setRenderPassDescriptor(rpDesc.data());
+ QVERIFY(pipeline->create());
+
+ cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 }, updates);
+ cb->setGraphicsPipeline(pipeline.data());
+ cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
+ QRhiCommandBuffer::VertexInput vbindings(vbuf.data(), 0);
+ cb->setVertexInput(0, 1, &vbindings);
+ cb->draw(3);
+
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readResult, &result] {
+ result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888_Premultiplied); // non-owning, no copy needed because readResult outlives result
+ };
+ QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
+ readbackBatch->readBackTexture({ texture.data() }, &readResult);
+ cb->endPass(readbackBatch);
+
+ rhi->endOffscreenFrame();
+ // Offscreen frames are synchronous, so the readback is guaranteed to
+ // complete at this point. This would not be the case with swapchain-based
+ // frames.
+ QCOMPARE(result.size(), texture->pixelSize());
+
+ if (impl == QRhi::Null)
+ return;
+
+ // Now we have a red rectangle on blue background.
+ const int y = 100;
+ const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
+ int x = result.width() - 1;
+ int redCount = 0;
+ int blueCount = 0;
+ const int maxFuzz = 1;
+ while (x-- >= 0) {
+ const QRgb c(*p++);
+ if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
+ ++redCount;
+ else if (qRed(c) == 0 && qGreen(c) == 0 && qBlue(c) >= (255 - maxFuzz))
+ ++blueCount;
+ else
+ QFAIL("Encountered a pixel that is neither red or blue");
+ }
+
+ QCOMPARE(redCount + blueCount, texture->pixelSize().width());
+ QVERIFY(redCount != 0);
+ QVERIFY(blueCount != 0);
+
+ // The triangle is "pointing up" in the resulting image with OpenGL
+ // (because Y is up both in normalized device coordinates and in images)
+ // and Vulkan (because Y is down in both and the vertex data was specified
+ // with Y up in mind), but "pointing down" with D3D (because Y is up in NDC
+ // but down in images).
+ if (rhi->isYUpInFramebuffer() == rhi->isYUpInNDC())
+ QVERIFY(redCount < blueCount);
+ else
+ QVERIFY(redCount > blueCount);
+
}
#include <tst_qrhi.moc>