diff options
Diffstat (limited to 'tests/auto/gui/rhi/qrhi/tst_qrhi.cpp')
-rw-r--r-- | tests/auto/gui/rhi/qrhi/tst_qrhi.cpp | 3258 |
1 files changed, 3037 insertions, 221 deletions
diff --git a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp index 24d6ad2d7c..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(); @@ -113,8 +92,12 @@ private slots: void renderToTextureMip(); void renderToTextureCubemapFace_data(); void renderToTextureCubemapFace(); + void renderToTextureTextureArray_data(); + void renderToTextureTextureArray(); void renderToTextureTexturedQuad_data(); void renderToTextureTexturedQuad(); + void renderToTextureSampleWithSeparateTextureAndSampler_data(); + void renderToTextureSampleWithSeparateTextureAndSampler(); void renderToTextureArrayOfTexturedQuad_data(); void renderToTextureArrayOfTexturedQuad(); void renderToTextureTexturedQuadAndUniformBuffer_data(); @@ -123,28 +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); @@ -157,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; @@ -173,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 @@ -182,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(); @@ -190,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 } @@ -208,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(); @@ -256,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()); @@ -294,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())); @@ -336,13 +365,22 @@ void tst_QRhi::create() const int texMax = rhi->resourceLimit(QRhi::TextureSizeMax); const int maxAtt = rhi->resourceLimit(QRhi::MaxColorAttachments); 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); QVERIFY(framesInFlight >= 1); + 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, @@ -373,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]); @@ -389,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(); @@ -415,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); } @@ -448,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: { @@ -492,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: { @@ -551,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; @@ -618,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"); @@ -630,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); @@ -645,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); @@ -665,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(); @@ -731,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: { @@ -806,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: { @@ -870,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); @@ -897,12 +1019,14 @@ 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; }; if (rhi->isFeatureSupported(QRhi::ReadBackNonUniformBuffer)) batch->readBackBuffer(dynamicBuffer.data(), 5, 10, &readResult); + else + qDebug("Skipping verification of buffer data as ReadBackNonUniformBuffer is not supported"); QVERIFY(submitResourceUpdates(rhi.data(), batch)); @@ -1004,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); @@ -1092,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)); @@ -1417,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)); @@ -1518,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); @@ -1543,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()); @@ -1670,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()); @@ -1792,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()); @@ -1880,6 +2058,116 @@ void tst_QRhi::renderToTextureCubemapFace() QFAIL("Encountered a pixel that is neither red or blue"); } + QVERIFY(redCount > 0 && blueCount > 0); + QCOMPARE(redCount + blueCount, outputSize.width()); + + if (rhi->isYUpInFramebuffer() == rhi->isYUpInNDC()) + QVERIFY(redCount < blueCount); // 100, 412 + else + QVERIFY(redCount > blueCount); // 412, 100 +} + +void tst_QRhi::renderToTextureTextureArray_data() +{ + rhiTestData(); +} + +void tst_QRhi::renderToTextureTextureArray() +{ + 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::TextureArrays)) + QSKIP("TextureArrays is not supported with this backend, skipping test"); + + const QSize outputSize(512, 256); + const int ARRAY_SIZE = 8; + QScopedPointer<QRhiTexture> texture(rhi->newTextureArray(QRhiTexture::RGBA8, + ARRAY_SIZE, + outputSize, + 1, + QRhiTexture::RenderTarget + | QRhiTexture::UsedAsTransferSource)); + QVERIFY(texture->create()); + + const int LAYER = 5; // render into element #5 + + QRhiColorAttachment colorAtt(texture.data()); + colorAtt.setLayer(LAYER); + QRhiTextureRenderTargetDescription rtDesc(colorAtt); + QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(rtDesc)); + QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor()); + rt->setRenderPassDescriptor(rpDesc.data()); + QVERIFY(rt->create()); + + QCOMPARE(rt->pixelSize(), texture->pixelSize()); + QCOMPARE(rt->pixelSize(), outputSize); + + 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(rt->pixelSize().width()), float(rt->pixelSize().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); + }; + QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch(); + QRhiReadbackDescription readbackDescription(texture.data()); + readbackDescription.setLayer(LAYER); + readbackBatch->readBackTexture(readbackDescription, &readResult); + + cb->endPass(readbackBatch); + + rhi->endOffscreenFrame(); + + QCOMPARE(result.size(), outputSize); + + if (impl == QRhi::Null) + return; + + 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"); + } + + QVERIFY(redCount > 0 && blueCount > 0); QCOMPARE(redCount + blueCount, outputSize.width()); if (rhi->isYUpInFramebuffer() == rhi->isYUpInNDC()) @@ -2018,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(); @@ -2696,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(); @@ -3013,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 @@ -3064,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()); @@ -3084,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); @@ -3171,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(); @@ -3204,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 @@ -3243,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())); @@ -3343,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() @@ -3375,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); @@ -3419,6 +4101,277 @@ void tst_QRhi::finishWithinSwapchainFrame() rhi->endFrame(swapChain.data()); } +void tst_QRhi::resourceUpdateBatchBufferTextureWithSwapchainFrames_data() +{ + rhiTestData(); +} + +void tst_QRhi::resourceUpdateBatchBufferTextureWithSwapchainFrames() +{ + if (QGuiApplication::platformName().startsWith(QLatin1String("offscreen"), Qt::CaseInsensitive)) + QSKIP("Offscreen: Skipping onscreen test"); + + 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 buffer resource updates"); + + QScopedPointer<QWindow> window(new QWindow); + setWindowType(window.data(), impl); + + window->setGeometry(0, 0, 640, 480); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QScopedPointer<QRhiSwapChain> swapChain(rhi->newSwapChain()); + swapChain->setWindow(window.data()); + swapChain->setFlags(QRhiSwapChain::UsedAsTransferSource); + QScopedPointer<QRhiRenderPassDescriptor> rpDesc(swapChain->newCompatibleRenderPassDescriptor()); + swapChain->setRenderPassDescriptor(rpDesc.data()); + QVERIFY(swapChain->createOrResize()); + + const int bufferSize = 18; + const char *a = "123456789"; + const char *b = "abcdefghi"; + + bool readCompleted = false; + QRhiReadbackResult readResult; + readResult.completed = [&readCompleted] { readCompleted = true; }; + QRhiReadbackResult texReadResult; + texReadResult.completed = [&readCompleted] { readCompleted = true; }; + + { + QScopedPointer<QRhiBuffer> dynamicBuffer(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, bufferSize)); + QVERIFY(dynamicBuffer->create()); + + for (int i = 0; i < bufferSize; ++i) { + QVERIFY(rhi->beginFrame(swapChain.data()) == QRhi::FrameOpSuccess); + + QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch(); + + // One byte every 16.66 ms should be enough for everyone: fill up + // the buffer with "123456789abcdefghi", one byte in each frame. + if (i >= bufferSize / 2) + batch->updateDynamicBuffer(dynamicBuffer.data(), i, 1, b + (i - bufferSize / 2)); + else + batch->updateDynamicBuffer(dynamicBuffer.data(), i, 1, a + i); + + QRhiCommandBuffer *cb = swapChain->currentFrameCommandBuffer(); + // just clear to black, but submit the resource update + cb->beginPass(swapChain->currentFrameRenderTarget(), Qt::black, { 1.0f, 0 }, batch); + cb->endPass(); + + rhi->endFrame(swapChain.data()); + } + + { + QVERIFY(rhi->beginFrame(swapChain.data()) == QRhi::FrameOpSuccess); + + QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch(); + readCompleted = false; + batch->readBackBuffer(dynamicBuffer.data(), 0, bufferSize, &readResult); + + QRhiCommandBuffer *cb = swapChain->currentFrameCommandBuffer(); + cb->beginPass(swapChain->currentFrameRenderTarget(), Qt::black, { 1.0f, 0 }, batch); + cb->endPass(); + + rhi->endFrame(swapChain.data()); + + // This is a proper, typically at least double buffered renderer (as + // a real swapchain is involved). readCompleted may only become true + // in a future frame. + while (!readCompleted) { + QVERIFY(rhi->beginFrame(swapChain.data()) == QRhi::FrameOpSuccess); + rhi->endFrame(swapChain.data()); + } + + QVERIFY(readResult.data.size() == bufferSize); + QCOMPARE(readResult.data.left(bufferSize / 2), QByteArray(a)); + QCOMPARE(readResult.data.mid(bufferSize / 2), QByteArray(b)); + } + } + + // Repeat for types Immutable and Static, declare Vertex usage. + // This may not be readable on GLES 2.0 so skip the verification then. + for (QRhiBuffer::Type type : { QRhiBuffer::Immutable, QRhiBuffer::Static }) { + QScopedPointer<QRhiBuffer> buffer(rhi->newBuffer(type, QRhiBuffer::VertexBuffer, bufferSize)); + QVERIFY(buffer->create()); + + for (int i = 0; i < bufferSize; ++i) { + QVERIFY(rhi->beginFrame(swapChain.data()) == QRhi::FrameOpSuccess); + + QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch(); + if (i >= bufferSize / 2) + batch->uploadStaticBuffer(buffer.data(), i, 1, b + (i - bufferSize / 2)); + else + batch->uploadStaticBuffer(buffer.data(), i, 1, a + i); + + QRhiCommandBuffer *cb = swapChain->currentFrameCommandBuffer(); + cb->beginPass(swapChain->currentFrameRenderTarget(), Qt::black, { 1.0f, 0 }, batch); + cb->endPass(); + + rhi->endFrame(swapChain.data()); + } + + if (rhi->isFeatureSupported(QRhi::ReadBackNonUniformBuffer)) { + QVERIFY(rhi->beginFrame(swapChain.data()) == QRhi::FrameOpSuccess); + + QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch(); + readCompleted = false; + batch->readBackBuffer(buffer.data(), 0, bufferSize, &readResult); + + QRhiCommandBuffer *cb = swapChain->currentFrameCommandBuffer(); + cb->beginPass(swapChain->currentFrameRenderTarget(), Qt::black, { 1.0f, 0 }, batch); + cb->endPass(); + + rhi->endFrame(swapChain.data()); + + while (!readCompleted) { + QVERIFY(rhi->beginFrame(swapChain.data()) == QRhi::FrameOpSuccess); + rhi->endFrame(swapChain.data()); + } + + QVERIFY(readResult.data.size() == bufferSize); + QCOMPARE(readResult.data.left(bufferSize / 2), QByteArray(a)); + QCOMPARE(readResult.data.mid(bufferSize / 2), QByteArray(b)); + } else { + qDebug("Skipping verification of buffer data as ReadBackNonUniformBuffer is not supported"); + } + } + + // Now exercise a texture. Internally this is expected (with low level APIs + // at least) to be similar to what happens with a staic buffer: copy to host + // visible staging buffer, enqueue buffer-to-buffer (or here + // buffer-to-image) copy. + { + const int w = 234; + const int h = 8; // use a small height because vsync throttling is active + const QColor colors[] = { Qt::red, Qt::green, Qt::blue, Qt::gray, Qt::yellow, Qt::black, Qt::white, Qt::magenta }; + QImage image(w, h, QImage::Format_RGBA8888); + for (int i = 0; i < h; ++i) { + QRgb c = colors[i].rgb(); + uchar *p = image.scanLine(i); + int x = w; + while (x--) { + *p++ = qRed(c); + *p++ = qGreen(c); + *p++ = qBlue(c); + *p++ = qAlpha(c); + } + } + + QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(w, h), 1, QRhiTexture::UsedAsTransferSource)); + QVERIFY(texture->create()); + + // fill a texture from the image, two lines at a time + for (int i = 0; i < h / 2; ++i) { + QVERIFY(rhi->beginFrame(swapChain.data()) == QRhi::FrameOpSuccess); + QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch(); + + QRhiTextureSubresourceUploadDescription subresDesc(image); + subresDesc.setSourceSize(QSize(w, 2)); + subresDesc.setSourceTopLeft(QPoint(0, i * 2)); + subresDesc.setDestinationTopLeft(QPoint(0, i * 2)); + QRhiTextureUploadDescription uploadDesc(QRhiTextureUploadEntry(0, 0, subresDesc)); + batch->uploadTexture(texture.data(), uploadDesc); + + QRhiCommandBuffer *cb = swapChain->currentFrameCommandBuffer(); + cb->beginPass(swapChain->currentFrameRenderTarget(), Qt::black, { 1.0f, 0 }, batch); + cb->endPass(); + + rhi->endFrame(swapChain.data()); + } + + { + QVERIFY(rhi->beginFrame(swapChain.data()) == QRhi::FrameOpSuccess); + + QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch(); + readCompleted = false; + batch->readBackTexture(texture.data(), &texReadResult); + + QRhiCommandBuffer *cb = swapChain->currentFrameCommandBuffer(); + cb->beginPass(swapChain->currentFrameRenderTarget(), Qt::black, { 1.0f, 0 }, batch); + cb->endPass(); + + rhi->endFrame(swapChain.data()); + + while (!readCompleted) { + QVERIFY(rhi->beginFrame(swapChain.data()) == QRhi::FrameOpSuccess); + rhi->endFrame(swapChain.data()); + } + + QCOMPARE(texReadResult.pixelSize, image.size()); + QImage wrapperImage(reinterpret_cast<const uchar *>(texReadResult.data.constData()), + texReadResult.pixelSize.width(), texReadResult.pixelSize.height(), + image.format()); + QVERIFY(imageRGBAEquals(image, wrapperImage)); + } + } +} + +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(); @@ -3458,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) @@ -3476,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) @@ -3502,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) @@ -3817,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() @@ -3934,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()); @@ -3988,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"); @@ -4094,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()); @@ -4139,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) @@ -4157,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); @@ -4189,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) @@ -4256,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> |