Revision control
Copy as Markdown
// 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 <QSpan>
#include <QRegularExpression>
#include <QOperatingSystemVersion>
#include <qrgbafloat.h>
#include <qrgba64.h>
#include <qendian.h>
#include <private/qrhi_p.h>
#if QT_CONFIG(opengl)
# include <QOpenGLContext>
# include <QOpenGLFunctions>
# include <QtGui/private/qguiapplication_p.h>
# include <qpa/qplatformintegration.h>
# define TST_GL
#endif
#if QT_CONFIG(vulkan)
# include <QVulkanInstance>
# include <QVulkanFunctions>
# define TST_VK
#endif
#ifdef Q_OS_WIN
# define TST_D3D11
# define TST_D3D12
#endif
#if QT_CONFIG(metal)
# define TST_MTL
// Implemented in tst_qrhi_metal.mm
quint64 tst_qrhi_createMetalRasterizationRateMap(void *device, const QSize &screenSize);
void tst_qrhi_releaseMetalObject(quint64 object);
#endif
Q_DECLARE_METATYPE(QRhi::Implementation)
Q_DECLARE_METATYPE(QRhiInitParams *)
class tst_QRhi : public QObject
{
Q_OBJECT
private:
template<typename T, std::size_t E, typename ParamStringifier>
void rhiTestDataWithParam(const char *paramName, QSpan<T, E> paramValues, ParamStringifier &¶mStringifier);
private slots:
void initTestCase();
void cleanupTestCase();
void rhiTestData();
void create_data();
void create();
void adapters_data();
void adapters();
void stats_data();
void stats();
void nativeHandles_data();
void nativeHandles();
void shadingRateMapLogicalSize_data();
void shadingRateMapLogicalSize();
void nativeHandlesImportVulkan();
void nativeHandlesImportD3D11();
void nativeHandlesImportOpenGL();
void nativeTexture_data();
void nativeTexture();
void nativeBuffer_data();
void nativeBuffer();
void resourceUpdateBatchBuffer_data();
void resourceUpdateBatchBuffer();
void resourceUpdateBatchBufferCopy_data();
void resourceUpdateBatchBufferCopy();
void resourceUpdateBatchBufferCopyInvalid_data();
void resourceUpdateBatchBufferCopyInvalid();
void resourceUpdateBatchBufferCopyAndUploadOrder_data();
void resourceUpdateBatchBufferCopyAndUploadOrder();
void resourceUpdateBatchBufferClear_data();
void resourceUpdateBatchBufferClear();
void resourceUpdateBatchBufferClearInvalid_data();
void resourceUpdateBatchBufferClearInvalid();
void computeAfterBufferClear_data();
void computeAfterBufferClear();
void resourceUpdateBatchStaticBufferUploadEdgeCases_data();
void resourceUpdateBatchStaticBufferUploadEdgeCases();
void resourceUpdateBatchRGBATextureUpload_data();
void resourceUpdateBatchRGBATextureUpload();
void resourceUpdateBatchR8TextureUpload_data();
void resourceUpdateBatchR8TextureUpload();
void resourceUpdateBatchRGBATextureCopy_data();
void resourceUpdateBatchRGBATextureCopy();
void resourceUpdateBatchRGBATextureMip_data();
void resourceUpdateBatchRGBATextureMip();
void resourceUpdateBatchR8TextureMip_data();
void resourceUpdateBatchR8TextureMip();
void resourceUpdateBatchTextureRawDataStride_data();
void resourceUpdateBatchTextureRawDataStride();
void resourceUpdateBatchTextureInvalidSizeAndStride_data();
void resourceUpdateBatchTextureInvalidSizeAndStride();
void resourceUpdateBatchLotsOfResources_data();
void resourceUpdateBatchLotsOfResources();
void resourceUpdateBatchBetweenFrames_data();
void resourceUpdateBatchBetweenFrames();
void invalidPipeline_data();
void invalidPipeline();
void srbLayoutCompatibility_data();
void srbLayoutCompatibility();
void srbWithNoResource_data();
void srbWithNoResource();
void renderPassDescriptorCompatibility_data();
void renderPassDescriptorCompatibility();
void renderPassDescriptorClone_data();
void renderPassDescriptorClone();
void textureWithSampleCount_data();
void textureWithSampleCount();
void textureFormats_data();
void textureFormats();
void depthTextureReadback_data();
void depthTextureReadback();
void renderToTextureSimple_data();
void renderToTextureSimple();
void frameEndBindsDefaultFramebuffer_data();
void frameEndBindsDefaultFramebuffer();
void renderToTextureMip_data();
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 renderToTextureArrayOfSampledTextures_data();
void renderToTextureArrayOfSampledTextures();
void renderToTextureTexturedQuadAndUniformBuffer_data();
void renderToTextureTexturedQuadAndUniformBuffer();
void renderToTextureTexturedQuadAllDynamicBuffers_data();
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 renderToTextureSameSrbDifferentShaders_data();
void renderToTextureSameSrbDifferentShaders();
void renderToTextureScissorChange_data();
void renderToTextureScissorChange();
void renderToTextureAllUniformTypes_data();
void renderToTextureAllUniformTypes();
void renderToWindowSimple_data();
void renderToWindowSimple();
void continuousReadbackFromWindow_data();
void continuousReadbackFromWindow();
void finishWithinSwapchainFrame_data();
void finishWithinSwapchainFrame();
void resourceUpdateBatchBufferTextureWithSwapchainFrames_data();
void resourceUpdateBatchBufferTextureWithSwapchainFrames();
void textureRenderTargetAutoRebuild_data();
void textureRenderTargetAutoRebuild();
void renderToMRTPerRenderTargetBlending();
void renderToMRTPerRenderTargetBlending_data();
void indexedIndirectMultiDrawBaseline_data();
void indexedIndirectMultiDrawBaseline();
void indirectMultiDrawWithTexture_data();
void indirectMultiDrawWithTexture();
void indexedIndirectMultiDrawCustomStride_data();
void indexedIndirectMultiDrawCustomStride();
void indexedIndirectMultiDrawFromCompute_data();
void indexedIndirectMultiDrawFromCompute();
void indexedIndirectMultiDrawHighDrawCount_data();
void indexedIndirectMultiDrawHighDrawCount();
void indirectMultiDrawHighDrawCount_data();
void indirectMultiDrawHighDrawCount();
void indexedIndirectMultiDrawHighDrawCountViewport_data();
void indexedIndirectMultiDrawHighDrawCountViewport();
void indexedIndirectMultiDrawHighDrawCountMultisample_data();
void indexedIndirectMultiDrawHighDrawCountMultisample();
void indexedIndirectMultiDrawHighDrawCountMultisampleDepthResolve_data();
void indexedIndirectMultiDrawHighDrawCountMultisampleDepthResolve();
void indexedIndirectMultiDrawHighDrawCountRenderBufferDepth_data();
void indexedIndirectMultiDrawHighDrawCountRenderBufferDepth();
void dispatchIndirectBaseline_data();
void dispatchIndirectBaseline();
void dispatchIndirectFromCompute_data();
void dispatchIndirectFromCompute();
void dispatchIndirectFromComputeSamePass_data();
void dispatchIndirectFromComputeSamePass();
void dispatchIndirectConsumerReadsArgs_data();
void dispatchIndirectConsumerReadsArgs();
void bufferReadbackIsSnapshot_data();
void bufferReadbackIsSnapshot();
void indexedIndirectMultiDrawShaderDrawParams_data();
void indexedIndirectMultiDrawShaderDrawParams();
void indexedIndirectDrawCountFromCompute_data();
void indexedIndirectDrawCountFromCompute();
void indexedIndirectDrawCountCustomStride_data();
void indexedIndirectDrawCountCustomStride();
void indirectDrawCountCustomStride_data();
void indirectDrawCountCustomStride();
void indexedIndirectDrawCountMultisample_data();
void indexedIndirectDrawCountMultisample();
void baseInstanceDraw_data();
void baseInstanceDraw();
void indirectCommandBufferBaseline_data();
void indirectCommandBufferBaseline();
void indirectCommandBufferNonIndexed_data();
void indirectCommandBufferNonIndexed();
void indirectCommandBufferSubrange_data();
void indirectCommandBufferSubrange();
void indirectCommandBufferFromCompute_data();
void indirectCommandBufferFromCompute();
void indirectCommandBufferFromComputeSubrange_data();
void indirectCommandBufferFromComputeSubrange();
void indirectCommandBufferFromComputeCount_data();
void indirectCommandBufferFromComputeCount();
void indirectCommandBufferRerecord_data();
void indirectCommandBufferRerecord();
void indirectCommandBufferCpuThenBuild_data();
void indirectCommandBufferCpuThenBuild();
void pipelineCache_data();
void pipelineCache();
void pipelineCacheBlobReader();
void textureImportOpenGL();
void renderbufferImportOpenGL();
void threeDimTexture_data();
void threeDimTexture();
void oneDimTexture_data();
void oneDimTexture();
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();
void graphicsPipelineMixedGlslVersions_data();
void graphicsPipelineMixedGlslVersions();
void textureViewFormat_data();
void textureViewFormat();
void textureArrayRange_data();
void textureArrayRange();
void pushConstants_data();
void pushConstants();
void pushConstantsCompute_data();
void pushConstantsCompute();
void pushConstantsStd430_data();
void pushConstantsStd430();
// Make this the last, in case the leaked Vk object test confuses the Vulkan
// validation or some third-party implicitly loaded layer.
void leakedResourceDestroy_data();
void leakedResourceDestroy();
private:
void setWindowType(QWindow *window, QRhi::Implementation impl);
bool isAndroidSwiftShader(const QRhi *rhi);
QRhi *sharedRhi(QRhi::Implementation impl, QRhiInitParams *initParams);
struct {
QRhiNullInitParams null;
#ifdef TST_GL
QRhiGles2InitParams gl;
#endif
#ifdef TST_VK
QRhiVulkanInitParams vk;
#endif
#ifdef TST_D3D11
QRhiD3D11InitParams d3d11;
#endif
#ifdef TST_D3D12
QRhiD3D12InitParams d3d12;
#endif
#ifdef TST_MTL
QRhiMetalInitParams mtl;
#endif
} initParams;
#ifdef TST_VK
QVulkanInstance vulkanInstance;
#endif
QOffscreenSurface *fallbackSurface = nullptr;
// One QRhi per backend, created on demand, kept alive until the end of the
// test run. A null value means the creation was attempted and failed.
QHash<QRhi::Implementation, QRhi *> m_sharedRhis;
};
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
#ifdef TST_VK
const QVersionNumber supportedVersion = vulkanInstance.supportedApiVersion();
if (supportedVersion >= QVersionNumber(1, 2))
vulkanInstance.setApiVersion(QVersionNumber(1, 2));
else if (supportedVersion >= QVersionNumber(1, 1))
vulkanInstance.setApiVersion(QVersionNumber(1, 1));
vulkanInstance.setLayers({ "VK_LAYER_KHRONOS_validation" });
vulkanInstance.setExtensions(QRhiVulkanInitParams::preferredInstanceExtensions());
vulkanInstance.create();
initParams.vk.inst = &vulkanInstance;
#endif
#ifdef TST_D3D11
initParams.d3d11.enableDebugLayer = true;
#endif
#ifdef TST_D3D12
initParams.d3d12.enableDebugLayer = true;
#endif
}
void tst_QRhi::cleanupTestCase()
{
qDeleteAll(m_sharedRhis);
m_sharedRhis.clear();
#ifdef TST_VK
vulkanInstance.destroy();
#endif
delete fallbackSurface;
}
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
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.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
}
template<typename T, std::size_t E, typename ParamStringifier>
void tst_QRhi::rhiTestDataWithParam(const char *paramName, QSpan<T, E> paramValues, ParamStringifier &¶mStringifier)
{
QTest::addColumn<QRhi::Implementation>("impl");
QTest::addColumn<QRhiInitParams *>("initParams");
QTest::addColumn<T>(paramName);
#ifndef Q_OS_WEBOS
for (auto ¶mValue : paramValues) {
QTest::newRow(qPrintable(QStringLiteral("Null-") + paramStringifier(paramValue)))
<< QRhi::Null << static_cast<QRhiInitParams *>(&initParams.null) << paramValue;
}
#endif
#ifdef TST_GL
if (QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::OpenGL)) {
for (auto ¶mValue : paramValues) {
QTest::newRow(qPrintable(QStringLiteral("OpenGL-") + paramStringifier(paramValue)))
<< QRhi::OpenGLES2 << static_cast<QRhiInitParams *>(&initParams.gl) << paramValue;
}
}
#endif
#ifdef TST_VK
if (vulkanInstance.isValid()) {
for (auto ¶mValue : paramValues) {
QTest::newRow(qPrintable(QStringLiteral("Vulkan-") + paramStringifier(paramValue)))
<< QRhi::Vulkan << static_cast<QRhiInitParams *>(&initParams.vk) << paramValue;
}
}
#endif
#ifdef TST_D3D11
for (auto ¶mValue : paramValues) {
QTest::newRow(qPrintable(QStringLiteral("Direct3D11-") + paramStringifier(paramValue)))
<< QRhi::D3D11 << static_cast<QRhiInitParams *>(&initParams.d3d11) << paramValue;
}
#endif
#ifdef TST_D3D12
for (auto ¶mValue : paramValues) {
QTest::newRow(qPrintable(QStringLiteral("Direct3D12-") + paramStringifier(paramValue)))
<< QRhi::D3D12 << static_cast<QRhiInitParams *>(&initParams.d3d12) << paramValue;
}
#endif
#ifdef TST_MTL
for (auto ¶mValue : paramValues) {
QTest::newRow(qPrintable(QStringLiteral("Metal-") + paramStringifier(paramValue)))
<< QRhi::Metal << static_cast<QRhiInitParams *>(&initParams.mtl) << paramValue;
}
#endif
}
// True when running on the Android emulator's SwiftShader software renderer.
bool tst_QRhi::isAndroidSwiftShader(const QRhi *rhi)
{
#ifdef Q_OS_ANDROID
qWarning() << rhi->driverInfo();
return rhi->driverInfo().deviceName.contains("SwiftShader");
#else
Q_UNUSED(rhi);
return false;
#endif
}
void tst_QRhi::create_data()
{
rhiTestData();
}
static int aligned(int v, int a)
{
return (v + a - 1) & ~(a - 1);
}
// MoltenVK cannot be told apart from other Vulkan implementations based on
// QRhi::driverInfo(), it reports the Metal device name as the device name, so
// just assume Vulkan+Apple=MoltenVK for now, even though this will not
// necessarily be true in the future (KosmicKrisp).
static bool isMoltenVk(QRhi *rhi)
{
#ifdef Q_OS_DARWIN
return rhi->backend() == QRhi::Vulkan;
#else
Q_UNUSED(rhi);
return false;
#endif
}
void tst_QRhi::create()
{
// Merely attempting to create a QRhi should survive, with an error when
// not supported. (of course, there is always a chance we encounter a crash
// due to some random graphics stack...)
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
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());
// do a basic smoke test for the apis that do not directly render anything
int cleanupOk = 0;
QRhi *rhiPtr = rhi.data();
auto cleanupFunc = [rhiPtr, &cleanupOk](QRhi *dyingRhi) {
if (rhiPtr == dyingRhi)
cleanupOk += 1;
};
rhi->addCleanupCallback(cleanupFunc);
rhi->addCleanupCallback(cleanupFunc);
rhi->addCleanupCallback(reinterpret_cast<const void *>(quintptr(1234)), cleanupFunc);
rhi->addCleanupCallback(reinterpret_cast<const void *>(quintptr(12345)), cleanupFunc);
rhi->removeCleanupCallback(reinterpret_cast<const void *>(quintptr(1234)));
QRhiResourceUpdateBatch *resUpd = rhi->nextResourceUpdateBatch();
QVERIFY(resUpd);
resUpd->release();
QRhiResourceUpdateBatch *resUpdArray[64];
for (int i = 0; i < 64; ++i) {
resUpdArray[i] = rhi->nextResourceUpdateBatch();
QVERIFY(resUpdArray[i]);
}
resUpd = rhi->nextResourceUpdateBatch();
QVERIFY(!resUpd);
for (int i = 0; i < 64; ++i)
resUpdArray[i]->release();
resUpd = rhi->nextResourceUpdateBatch();
QVERIFY(resUpd);
resUpd->release();
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.
QCOMPARE_GT(supportedSampleCounts[i], supportedSampleCounts[i - 1]);
}
QCOMPARE_GT(rhi->ubufAlignment(), 0);
QCOMPARE(rhi->ubufAligned(123), aligned(123, rhi->ubufAlignment()));
QCOMPARE(rhi->mipLevelsForSize(QSize(512, 300)), 10);
QCOMPARE(rhi->sizeForMipLevel(0, QSize(512, 300)), QSize(512, 300));
QCOMPARE(rhi->sizeForMipLevel(1, QSize(512, 300)), QSize(256, 150));
QCOMPARE(rhi->sizeForMipLevel(2, QSize(512, 300)), QSize(128, 75));
QCOMPARE(rhi->sizeForMipLevel(9, QSize(512, 300)), QSize(1, 1));
const bool fbUp = rhi->isYUpInFramebuffer();
const bool ndcUp = rhi->isYUpInNDC();
const bool d0to1 = rhi->isClipDepthZeroToOne();
const QMatrix4x4 corrMat = rhi->clipSpaceCorrMatrix();
if (impl == QRhi::OpenGLES2) {
QVERIFY(fbUp);
QVERIFY(ndcUp);
QVERIFY(!d0to1);
QVERIFY(corrMat.isIdentity());
} else if (impl == QRhi::Vulkan) {
QVERIFY(!fbUp);
QVERIFY(!ndcUp);
QVERIFY(d0to1);
QVERIFY(!corrMat.isIdentity());
} else if (impl == QRhi::D3D11) {
QVERIFY(!fbUp);
QVERIFY(ndcUp);
QVERIFY(d0to1);
QVERIFY(!corrMat.isIdentity());
} else if (impl == QRhi::Metal) {
QVERIFY(!fbUp);
QVERIFY(ndcUp);
QVERIFY(d0to1);
QVERIFY(!corrMat.isIdentity());
}
const int texMin = rhi->resourceLimit(QRhi::TextureSizeMin);
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);
const int maxVertexStorageBuffers = rhi->resourceLimit(QRhi::MaxVertexStorageBuffers);
const int maxFragmentStorageBuffers = rhi->resourceLimit(QRhi::MaxFragmentStorageBuffers);
QCOMPARE_GE(texMin, 1);
QCOMPARE_GE(texMax, texMin);
QCOMPARE_GE(maxAtt, 1);
QCOMPARE_GE(framesInFlight, 1);
if (rhi->isFeatureSupported(QRhi::TextureArrays))
QCOMPARE_GT(texArrayMax, 1);
QCOMPARE_GE(uniBufRangeMax, 224 * 4 * 4);
QCOMPARE_GE(maxVertexInputs, 8);
QCOMPARE_GE(maxVertexOutputs, 8);
// 0 is legitimate (optional per stage in OpenGL ES, no vertex-stage
// support in the D3D11 backend), negative is not.
QCOMPARE_GE(maxVertexStorageBuffers, 0);
QCOMPARE_GE(maxFragmentStorageBuffers, 0);
QVERIFY(rhi->nativeHandles());
const QRhi::Feature features[] = {
QRhi::MultisampleTexture,
QRhi::MultisampleRenderBuffer,
QRhi::DebugMarkers,
QRhi::Timestamps,
QRhi::Instancing,
QRhi::CustomInstanceStepRate,
QRhi::PrimitiveRestart,
QRhi::NonDynamicUniformBuffers,
QRhi::NonFourAlignedEffectiveIndexBufferOffset,
QRhi::NPOTTextureRepeat,
QRhi::RedOrAlpha8IsRed,
QRhi::ElementIndexUint,
QRhi::Compute,
QRhi::WideLines,
QRhi::VertexShaderPointSize,
QRhi::BaseVertex,
QRhi::BaseInstance,
QRhi::TriangleFanTopology,
QRhi::ReadBackNonUniformBuffer,
QRhi::ReadBackNonBaseMipLevel,
QRhi::TexelFetch,
QRhi::RenderToNonBaseMipLevel,
QRhi::IntAttributes,
QRhi::ScreenSpaceDerivatives,
QRhi::ReadBackAnyTextureFormat,
QRhi::PipelineCacheDataLoadSave,
QRhi::ImageDataStride,
QRhi::RenderBufferImport,
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]);
QVERIFY(rhi->isTextureFormatSupported(QRhiTexture::RGBA8));
rhi->releaseCachedResources();
QVERIFY(!rhi->isDeviceLost());
rhi.reset();
QCOMPARE(cleanupOk, 3);
}
}
void tst_QRhi::adapters_data()
{
rhiTestData();
}
void tst_QRhi::adapters()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi::AdapterList adapters = QRhi::enumerateAdapters(impl, initParams);
for (QRhiAdapter *adapter : adapters)
qDebug() << adapter->info();
if (!adapters.isEmpty()) {
QRhi *rhi = QRhi::create(impl, initParams, QRhi::Flags(), nullptr, adapters[0]);
if (rhi) {
QCOMPARE(rhi->driverInfo().deviceName, adapters[0]->info().deviceName);
QCOMPARE(rhi->driverInfo().deviceId, adapters[0]->info().deviceId);
QCOMPARE(rhi->driverInfo().vendorId, adapters[0]->info().vendorId);
QCOMPARE(rhi->driverInfo().deviceType, adapters[0]->info().deviceType);
}
delete rhi;
// test filtering based on luid/physdev
if (impl == QRhi::D3D11) {
#ifdef TST_D3D11
QRhi *rhi = QRhi::create(impl, initParams);
if (rhi) {
QRhiD3D11NativeHandles h = *static_cast<const QRhiD3D11NativeHandles *>(rhi->nativeHandles());
delete rhi;
if (h.adapterLuidLow || h.adapterLuidHigh) {
QRhi::AdapterList filteredList = QRhi::enumerateAdapters(impl, initParams, &h);
QCOMPARE(filteredList.count(), 1);
qDeleteAll(filteredList);
}
}
#endif
} else if (impl == QRhi::D3D12) {
#ifdef TST_D3D12
QRhi *rhi = QRhi::create(impl, initParams);
if (rhi) {
QRhiD3D12NativeHandles h = *static_cast<const QRhiD3D12NativeHandles *>(rhi->nativeHandles());
delete rhi;
if (h.adapterLuidLow || h.adapterLuidHigh) {
QRhi::AdapterList filteredList = QRhi::enumerateAdapters(impl, initParams, &h);
QCOMPARE(filteredList.count(), 1);
qDeleteAll(filteredList);
}
}
#endif
} else if (impl == QRhi::Vulkan) {
#ifdef TST_VK
QRhi *rhi = QRhi::create(impl, initParams);
if (rhi) {
QRhiVulkanNativeHandles h = *static_cast<const QRhiVulkanNativeHandles *>(rhi->nativeHandles());
delete rhi;
QVERIFY(h.physDev);
QRhi::AdapterList filteredList = QRhi::enumerateAdapters(impl, initParams, &h);
QCOMPARE(filteredList.count(), 1);
qDeleteAll(filteredList);
}
#endif
}
}
qDeleteAll(adapters);
adapters.clear();
}
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;
QCOMPARE_GT(stats.allocCount, 0);
QCOMPARE_GT(stats.blockCount, 0);
QCOMPARE_GT(stats.usedBytes, 0);
}
}
void tst_QRhi::shadingRateMapLogicalSize_data()
{
rhiTestData();
}
void tst_QRhi::shadingRateMapLogicalSize()
{
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 shading rate maps");
if (!rhi->isFeatureSupported(QRhi::VariableRateShadingMap))
QSKIP("Variable rate shading is not supported, skipping testing shading rate maps");
QScopedPointer<QRhiShadingRateMap> srm(rhi->newShadingRateMap());
if (!srm)
QSKIP("Shading rate maps are not supported, skipping testing shading rate maps");
// Nothing has been set up yet, so there is no logical size.
QVERIFY(!srm->logicalSize().isValid());
#ifdef TST_MTL
if (impl == QRhi::Metal) {
// A Metal rasterization rate map maps a logical (screen space) size
// onto fewer physical pixels. logicalSize() must report the screen
// size, since that is the coordinate space viewports and scissors are
// interpreted in when the map is attached to a render target.
const QSize screenSize(1024, 512);
const QRhiMetalNativeHandles *handles = static_cast<const QRhiMetalNativeHandles *>(rhi->nativeHandles());
QVERIFY(handles && handles->dev);
const quint64 rateMap = tst_qrhi_createMetalRasterizationRateMap(handles->dev, screenSize);
QVERIFY(rateMap);
// The QRhiShadingRateMap retains the map.
const bool created = srm->createFrom(QRhiShadingRateMap::NativeShadingRateMap { rateMap });
tst_qrhi_releaseMetalObject(rateMap);
QVERIFY(created);
QCOMPARE(srm->logicalSize(), screenSize);
}
#endif
}
void tst_QRhi::nativeHandles_data()
{
rhiTestData();
}
void tst_QRhi::nativeHandles()
{
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 native handles");
// QRhi::nativeHandles()
{
const QRhiNativeHandles *rhiHandles = rhi->nativeHandles();
Q_ASSERT(rhiHandles);
switch (impl) {
case QRhi::Null:
break;
#ifdef TST_VK
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->gfxQueue);
QVERIFY(vkHandles->vmemAllocator);
}
break;
#endif
#ifdef TST_GL
case QRhi::OpenGLES2:
{
const QRhiGles2NativeHandles *glHandles = static_cast<const QRhiGles2NativeHandles *>(rhiHandles);
QVERIFY(glHandles->context);
QVERIFY(glHandles->context->isValid());
glHandles->context->doneCurrent();
QVERIFY(!QOpenGLContext::currentContext());
rhi->makeThreadLocalNativeContextCurrent();
QVERIFY(QOpenGLContext::currentContext() == glHandles->context);
}
break;
#endif
#ifdef TST_D3D11
case QRhi::D3D11:
{
const QRhiD3D11NativeHandles *d3dHandles = static_cast<const QRhiD3D11NativeHandles *>(rhiHandles);
QVERIFY(d3dHandles->dev);
QVERIFY(d3dHandles->context);
QVERIFY(d3dHandles->featureLevel > 0);
QVERIFY(d3dHandles->adapterLuidLow || d3dHandles->adapterLuidHigh);
}
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:
{
const QRhiMetalNativeHandles *mtlHandles = static_cast<const QRhiMetalNativeHandles *>(rhiHandles);
QVERIFY(mtlHandles->dev);
QVERIFY(mtlHandles->cmdQueue);
}
break;
#endif
default:
Q_ASSERT(false);
}
}
// QRhiCommandBuffer::nativeHandles()
{
QRhiCommandBuffer *cb = nullptr;
QRhi::FrameOpResult result = rhi->beginOffscreenFrame(&cb);
QVERIFY(result == QRhi::FrameOpSuccess);
QVERIFY(cb);
Q_DECL_UNUSED const QRhiNativeHandles *cbHandles = cb->nativeHandles();
// no null check here, backends where not applicable will return null
switch (impl) {
case QRhi::Null:
break;
#ifdef TST_VK
case QRhi::Vulkan:
{
const QRhiVulkanCommandBufferNativeHandles *vkHandles = static_cast<const QRhiVulkanCommandBufferNativeHandles *>(cbHandles);
QVERIFY(vkHandles);
QVERIFY(vkHandles->commandBuffer);
}
break;
#endif
#ifdef TST_GL
case QRhi::OpenGLES2:
break;
#endif
#ifdef TST_D3D11
case QRhi::D3D11:
break;
#endif
#ifdef TST_D3D12
case QRhi::D3D12:
break;
#endif
#ifdef TST_MTL
case QRhi::Metal:
{
const QRhiMetalCommandBufferNativeHandles *mtlHandles = static_cast<const QRhiMetalCommandBufferNativeHandles *>(cbHandles);
QVERIFY(mtlHandles);
QVERIFY(mtlHandles->commandBuffer);
QVERIFY(!mtlHandles->encoder);
QScopedPointer<QRhiTexture> tex(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 1, QRhiTexture::RenderTarget));
QVERIFY(tex->create());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ tex.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
QVERIFY(rpDesc);
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
cb->beginPass(rt.data(), Qt::red, { 1.0f, 0 });
QVERIFY(static_cast<const QRhiMetalCommandBufferNativeHandles *>(cb->nativeHandles())->encoder);
cb->endPass();
}
break;
#endif
default:
Q_ASSERT(false);
}
rhi->endOffscreenFrame();
}
// QRhiRenderPassDescriptor::nativeHandles()
{
QScopedPointer<QRhiTexture> tex(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 1, QRhiTexture::RenderTarget));
QVERIFY(tex->create());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ tex.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
QVERIFY(rpDesc);
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
switch (impl) {
case QRhi::Null:
break;
#ifdef TST_VK
case QRhi::Vulkan:
{
const QRhiNativeHandles *rpHandles = rpDesc->nativeHandles();
const QRhiVulkanRenderPassNativeHandles *vkHandles = static_cast<const QRhiVulkanRenderPassNativeHandles *>(rpHandles);
QVERIFY(vkHandles);
QVERIFY(vkHandles->renderPass);
}
break;
#endif
#ifdef TST_GL
case QRhi::OpenGLES2:
break;
#endif
#ifdef TST_D3D11
case QRhi::D3D11:
break;
#endif
#ifdef TST_D3D12
case QRhi::D3D12:
break;
#endif
#ifdef TST_MTL
case QRhi::Metal:
break;
#endif
default:
Q_ASSERT(false);
}
}
}
void tst_QRhi::nativeHandlesImportVulkan()
{
#ifdef TST_VK
// VkDevice and everything else. For simplicity we'll get QRhi to create one, and then use that with another QRhi.
{
QScopedPointer<QRhi> rhi(QRhi::create(QRhi::Vulkan, &initParams.vk, QRhi::Flags(), nullptr));
if (!rhi)
QSKIP("Skipping native Vulkan test");
const QRhiVulkanNativeHandles *nativeHandles = static_cast<const QRhiVulkanNativeHandles *>(rhi->nativeHandles());
QRhiVulkanNativeHandles h = *nativeHandles;
// do not pass the rarely used fields, this is useful to test if it creates its own as expected
h.vmemAllocator = nullptr;
QScopedPointer<QRhi> adoptingRhi(QRhi::create(QRhi::Vulkan, &initParams.vk, QRhi::Flags(), &h));
QVERIFY(adoptingRhi);
const QRhiVulkanNativeHandles *newNativeHandles = static_cast<const QRhiVulkanNativeHandles *>(adoptingRhi->nativeHandles());
QCOMPARE(newNativeHandles->physDev, nativeHandles->physDev);
QCOMPARE(newNativeHandles->dev, nativeHandles->dev);
QCOMPARE(newNativeHandles->gfxQueueFamilyIdx, nativeHandles->gfxQueueFamilyIdx);
QCOMPARE(newNativeHandles->gfxQueueIdx, nativeHandles->gfxQueueIdx);
QCOMPARE(newNativeHandles->gfxQueueFlags, nativeHandles->gfxQueueFlags);
QVERIFY(newNativeHandles->vmemAllocator != nativeHandles->vmemAllocator);
}
// Physical device only
{
uint32_t physDevCount = 0;
QVulkanFunctions *f = vulkanInstance.functions();
f->vkEnumeratePhysicalDevices(vulkanInstance.vkInstance(), &physDevCount, nullptr);
if (physDevCount < 1)
QSKIP("No Vulkan physical devices, skip");
QVarLengthArray<VkPhysicalDevice, 4> physDevs(physDevCount);
f->vkEnumeratePhysicalDevices(vulkanInstance.vkInstance(), &physDevCount, physDevs.data());
for (uint32_t i = 0; i < physDevCount; ++i) {
QRhiVulkanNativeHandles h;
h.physDev = physDevs[i];
QScopedPointer<QRhi> rhi(QRhi::create(QRhi::Vulkan, &initParams.vk, QRhi::Flags(), &h));
// ok if fails, what we want to know is that if it succeeds, it must use that given phys.dev.
if (!rhi) {
qWarning("Skipping native Vulkan handle test for physical device %u", i);
continue;
}
const QRhiVulkanNativeHandles *actualNativeHandles = static_cast<const QRhiVulkanNativeHandles *>(rhi->nativeHandles());
QCOMPARE(actualNativeHandles->physDev, physDevs[i]);
}
}
#else
QSKIP("Skipping Vulkan-specific test");
#endif
}
void tst_QRhi::nativeHandlesImportD3D11()
{
#ifdef TST_D3D11
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");
const QRhiD3D11NativeHandles *nativeHandles = static_cast<const QRhiD3D11NativeHandles *>(rhi->nativeHandles());
// Case 1: device and context
{
QRhiD3D11NativeHandles h = *nativeHandles;
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.d3d11, QRhi::Flags(), &h));
QVERIFY(adoptingRhi);
const QRhiD3D11NativeHandles *newNativeHandles = static_cast<const QRhiD3D11NativeHandles *>(adoptingRhi->nativeHandles());
QCOMPARE(newNativeHandles->dev, nativeHandles->dev);
QCOMPARE(newNativeHandles->context, nativeHandles->context);
QCOMPARE(newNativeHandles->featureLevel, nativeHandles->featureLevel);
QCOMPARE(newNativeHandles->adapterLuidLow, nativeHandles->adapterLuidLow);
QCOMPARE(newNativeHandles->adapterLuidHigh, nativeHandles->adapterLuidHigh);
}
// Case 2: adapter and feature level only (hello OpenXR)
{
QRhiD3D11NativeHandles h = *nativeHandles;
h.dev = nullptr;
h.context = nullptr;
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);
QVERIFY(newNativeHandles->context != nativeHandles->context);
QCOMPARE(newNativeHandles->featureLevel, nativeHandles->featureLevel);
QCOMPARE(newNativeHandles->adapterLuidLow, nativeHandles->adapterLuidLow);
QCOMPARE(newNativeHandles->adapterLuidHigh, nativeHandles->adapterLuidHigh);
}
#else
QSKIP("Skipping D3D11-specific test");
#endif
}
void tst_QRhi::nativeHandlesImportOpenGL()
{
#ifdef TST_GL
QRhiGles2NativeHandles h;
QScopedPointer<QOpenGLContext> ctx(new QOpenGLContext);
if (!ctx->create())
QSKIP("No OpenGL context, skipping OpenGL-specific test");
h.context = ctx.data();
QScopedPointer<QRhi> rhi(QRhi::create(QRhi::OpenGLES2, &initParams.gl, QRhi::Flags(), &h));
if (!rhi)
QSKIP("QRhi could not be created, skipping testing OpenGL native handle import");
const QRhiGles2NativeHandles *actualNativeHandles = static_cast<const QRhiGles2NativeHandles *>(rhi->nativeHandles());
QCOMPARE(actualNativeHandles->context, ctx.data());
rhi->makeThreadLocalNativeContextCurrent();
QCOMPARE(QOpenGLContext::currentContext(), ctx.data());
#else
QSKIP("Skipping OpenGL-specific test");
#endif
}
void tst_QRhi::nativeTexture_data()
{
rhiTestData();
}
void tst_QRhi::nativeTexture()
{
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 native texture");
QScopedPointer<QRhiTexture> tex(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 256)));
QVERIFY(tex->create());
const QRhiTexture::NativeTexture nativeTex = tex->nativeTexture();
switch (impl) {
case QRhi::Null:
break;
#ifdef TST_VK
case QRhi::Vulkan:
{
auto image = VkImage(nativeTex.object);
QVERIFY(image);
// A texture that has not been used in any way yet is still in the
// initial, UNDEFINED layout.
QCOMPARE(nativeTex.layout, int(VK_IMAGE_LAYOUT_UNDEFINED));
}
break;
#endif
#ifdef TST_GL
case QRhi::OpenGLES2:
{
auto textureId = uint(nativeTex.object);
QVERIFY(textureId);
}
break;
#endif
#ifdef TST_D3D11
case QRhi::D3D11:
{
auto *texture = reinterpret_cast<void *>(nativeTex.object);
QVERIFY(texture);
}
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:
{
auto texture = (void *)nativeTex.object;
QVERIFY(texture);
}
break;
#endif
default:
Q_ASSERT(false);
}
}
void tst_QRhi::nativeBuffer_data()
{
rhiTestData();
}
void tst_QRhi::nativeBuffer()
{
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 native buffer query");
const QRhiBuffer::Type types[3] = { QRhiBuffer::Immutable, QRhiBuffer::Static, QRhiBuffer::Dynamic };
const QRhiBuffer::UsageFlags usages[3] = { QRhiBuffer::VertexBuffer, QRhiBuffer::IndexBuffer, QRhiBuffer::UniformBuffer };
for (int typeUsageIdx = 0; typeUsageIdx < 3; ++typeUsageIdx) {
QScopedPointer<QRhiBuffer> buf(rhi->newBuffer(types[typeUsageIdx], usages[typeUsageIdx], 256));
QVERIFY(buf->create());
const QRhiBuffer::NativeBuffer nativeBuf = buf->nativeBuffer();
QCOMPARE_LE(nativeBuf.slotCount, rhi->resourceLimit(QRhi::FramesInFlight));
switch (impl) {
case QRhi::Null:
break;
#ifdef TST_VK
case QRhi::Vulkan:
{
QCOMPARE_GE(nativeBuf.slotCount, 1); // always backed by native buffers
for (int i = 0; i < nativeBuf.slotCount; ++i) {
auto *buffer = static_cast<const VkBuffer *>(nativeBuf.objects[i]);
QVERIFY(buffer);
QVERIFY(*buffer);
}
}
break;
#endif
#ifdef TST_GL
case QRhi::OpenGLES2:
{
QCOMPARE_GE(nativeBuf.slotCount, 0); // UniformBuffers are not backed by native buffers, so 0 is perfectly valid
for (int i = 0; i < nativeBuf.slotCount; ++i) {
auto *bufferId = static_cast<const uint *>(nativeBuf.objects[i]);
QVERIFY(bufferId);
QVERIFY(*bufferId);
}
}
break;
#endif
#ifdef TST_D3D11
case QRhi::D3D11:
{
QCOMPARE_GE(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_D3D12
case QRhi::D3D12:
{
QCOMPARE_GE(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:
{
QCOMPARE_GE(nativeBuf.slotCount, 1); // always backed by native buffers
for (int i = 0; i < nativeBuf.slotCount; ++i) {
void * const * buffer = (void * const *) nativeBuf.objects[i];
QVERIFY(buffer);
QVERIFY(*buffer);
}
}
break;
#endif
default:
Q_ASSERT(false);
}
}
}
// When using sharedRhi(), prefer using a local OffscreenFrameGuard to get an
// automatic endOffscreenFrame() if the test function returns early due to a
// failure.
class OffscreenFrameGuard
{
public:
OffscreenFrameGuard(QRhi *rhi) : m_rhi(rhi) { }
~OffscreenFrameGuard() { endFrame(); }
void endFrame()
{
if (m_rhi) {
m_rhi->endOffscreenFrame();
m_rhi = nullptr;
}
}
private:
QRhi *m_rhi;
};
static bool submitResourceUpdates(QRhi *rhi, QRhiResourceUpdateBatch *batch)
{
QRhiCommandBuffer *cb = nullptr;
QRhi::FrameOpResult result = rhi->beginOffscreenFrame(&cb);
if (result != QRhi::FrameOpSuccess) {
qWarning("beginOffscreenFrame returned %d", result);
return false;
}
if (!cb) {
qWarning("No command buffer from beginOffscreenFrame");
return false;
}
cb->resourceUpdate(batch);
rhi->endOffscreenFrame();
return true;
}
void tst_QRhi::resourceUpdateBatchBuffer_data()
{
rhiTestData();
}
void tst_QRhi::resourceUpdateBatchBuffer()
{
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");
const int bufferSize = 23;
const QByteArray a(bufferSize, 'A');
const QByteArray b(bufferSize, 'B');
// dynamic buffer, updates, readback
{
QScopedPointer<QRhiBuffer> dynamicBuffer(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, bufferSize));
QVERIFY(dynamicBuffer->create());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QVERIFY(batch);
batch->updateDynamicBuffer(dynamicBuffer.data(), 10, bufferSize - 10, a.constData());
batch->updateDynamicBuffer(dynamicBuffer.data(), 0, 12, b.constData());
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
batch->readBackBuffer(dynamicBuffer.data(), 5, 10, &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
// Offscreen frames are synchronous, so the readback must have
// completed at this point. With swapchain frames this would not be the
// case.
QVERIFY(readCompleted);
QCOMPARE(readResult.data.size(), 10);
QCOMPARE(readResult.data.left(7), QByteArrayLiteral("BBBBBBB"));
QCOMPARE(readResult.data.mid(7), QByteArrayLiteral("AAA"));
}
// static buffer, updates, readback
{
QScopedPointer<QRhiBuffer> dynamicBuffer(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::VertexBuffer, bufferSize));
QVERIFY(dynamicBuffer->create());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QVERIFY(batch);
batch->uploadStaticBuffer(dynamicBuffer.data(), 10, bufferSize - 10, a.constData());
batch->uploadStaticBuffer(dynamicBuffer.data(), 0, 12, b.constData());
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));
if (rhi->isFeatureSupported(QRhi::ReadBackNonUniformBuffer)) {
QVERIFY(readCompleted);
QCOMPARE(readResult.data.size(), 10);
QCOMPARE(readResult.data.left(7), QByteArrayLiteral("BBBBBBB"));
QCOMPARE(readResult.data.mid(7), QByteArrayLiteral("AAA"));
} else {
qDebug("Skipping verifying buffer contents because readback is not supported");
}
}
}
void tst_QRhi::resourceUpdateBatchBufferCopy_data()
{
rhiTestData();
}
void tst_QRhi::resourceUpdateBatchBufferCopy()
{
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-to-buffer copy");
if (!rhi->isFeatureSupported(QRhi::BufferToBufferCopy))
QSKIP("Buffer-to-buffer copy is not supported on this backend");
const int bufferSize = 32;
const QByteArray a(bufferSize, 'A');
const QByteArray b(bufferSize, 'B');
const bool canReadBack = rhi->isFeatureSupported(QRhi::ReadBackNonUniformBuffer);
if (!canReadBack)
qDebug("Skipping verification of buffer data as ReadBackNonUniformBuffer is not supported");
// full copy
{
QScopedPointer<QRhiBuffer> src(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::VertexBuffer, bufferSize));
QVERIFY(src->create());
QScopedPointer<QRhiBuffer> dst(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::VertexBuffer, bufferSize));
QVERIFY(dst->create());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QVERIFY(batch);
batch->uploadStaticBuffer(src.data(), 0, bufferSize, a.constData());
batch->uploadStaticBuffer(dst.data(), 0, bufferSize, b.constData());
QVERIFY(submitResourceUpdates(rhi.data(), batch));
batch = rhi->nextResourceUpdateBatch();
QVERIFY(batch);
batch->copyBuffer(dst.data(), src.data());
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
if (canReadBack)
batch->readBackBuffer(dst.data(), 0, bufferSize, &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
if (canReadBack && impl != QRhi::Null) {
QVERIFY(readCompleted);
QCOMPARE(readResult.data.size(), bufferSize);
QCOMPARE(readResult.data, a);
}
}
// partial copy: 8 bytes from the middle of src to the middle of dst
{
QScopedPointer<QRhiBuffer> src(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::VertexBuffer, bufferSize));
QVERIFY(src->create());
QScopedPointer<QRhiBuffer> dst(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::VertexBuffer, bufferSize));
QVERIFY(dst->create());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QVERIFY(batch);
batch->uploadStaticBuffer(src.data(), 0, bufferSize, a.constData());
batch->uploadStaticBuffer(dst.data(), 0, bufferSize, b.constData());
QVERIFY(submitResourceUpdates(rhi.data(), batch));
batch = rhi->nextResourceUpdateBatch();
QVERIFY(batch);
QRhiBufferCopyDescription desc;
desc.setSize(8);
desc.setSourceOffset(4);
desc.setDestinationOffset(12);
batch->copyBuffer(dst.data(), src.data(), desc);
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
if (canReadBack)
batch->readBackBuffer(dst.data(), 0, bufferSize, &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
if (canReadBack && impl != QRhi::Null) {
QVERIFY(readCompleted);
QCOMPARE(readResult.data.size(), bufferSize);
QByteArray expected = b;
expected.replace(12, 8, QByteArray(8, 'A'));
QCOMPARE(readResult.data, expected);
}
}
}
void tst_QRhi::resourceUpdateBatchBufferCopyInvalid_data()
{
rhiTestData();
}
void tst_QRhi::resourceUpdateBatchBufferCopyInvalid()
{
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 invalid buffer-to-buffer copies");
// The rejections all happen in the front-end, before the backend ever sees
// the operation, so this does not depend on BufferToBufferCopy support.
const int bufferSize = 32;
QScopedPointer<QRhiBuffer> buf(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::VertexBuffer, bufferSize));
QVERIFY(buf->create());
QScopedPointer<QRhiBuffer> buf2(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::VertexBuffer, bufferSize));
QVERIFY(buf2->create());
QScopedPointer<QRhiBuffer> dynamicBuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::VertexBuffer, bufferSize));
QVERIFY(dynamicBuf->create());
// Dynamic, because a non-Dynamic uniform buffer cannot be created at all
// with some backends (NonDynamicUniformBuffers). copyBuffer() checks the
// UniformBuffer usage before the buffer type, so this still exercises the
// UniformBuffer rejection and not the Dynamic one.
QScopedPointer<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, bufferSize));
QVERIFY(ubuf->create());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QVERIFY(batch);
QTest::ignoreMessage(QtWarningMsg, "Buffer copy with a null source or destination is not supported");
batch->copyBuffer(buf.data(), nullptr);
QTest::ignoreMessage(QtWarningMsg, "Buffer copy with a null source or destination is not supported");
batch->copyBuffer(nullptr, buf.data());
QTest::ignoreMessage(QtWarningMsg, "Buffer copy with matching source and destination is not supported");
batch->copyBuffer(buf.data(), buf.data());
QTest::ignoreMessage(QtWarningMsg, "Buffer copy is not supported for Dynamic buffers");
batch->copyBuffer(buf.data(), dynamicBuf.data());
QTest::ignoreMessage(QtWarningMsg, "Buffer copy is not supported for Dynamic buffers");
batch->copyBuffer(dynamicBuf.data(), buf.data());
QTest::ignoreMessage(QtWarningMsg, "Buffer copy is not supported for buffers with UniformBuffer usage");
batch->copyBuffer(buf.data(), ubuf.data());
QTest::ignoreMessage(QtWarningMsg, "Buffer copy is not supported for buffers with UniformBuffer usage");
batch->copyBuffer(ubuf.data(), buf.data());
// Does not fit the source.
QRhiBufferCopyDescription desc;
desc.setSize(bufferSize);
desc.setSourceOffset(4);
QTest::ignoreMessage(QtWarningMsg, "Buffer copy of 32 bytes (source offset 4, destination offset 0) "
"does not fit the source (32 bytes) or destination (32 bytes) buffer");
batch->copyBuffer(buf2.data(), buf.data(), desc);
// Does not fit the destination.
desc.setSourceOffset(0);
desc.setDestinationOffset(4);
QTest::ignoreMessage(QtWarningMsg, "Buffer copy of 32 bytes (source offset 0, destination offset 4) "
"does not fit the source (32 bytes) or destination (32 bytes) buffer");
batch->copyBuffer(buf2.data(), buf.data(), desc);
// A zero sized copy is a no-op, not a warning. Cannot be expressed with a
// size of 0 in the description, because that means the whole source buffer,
// so use a zero sized source.
QScopedPointer<QRhiBuffer> emptyBuf(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::VertexBuffer, 0));
QVERIFY(emptyBuf->create());
QCOMPARE(emptyBuf->size(), 0u);
batch->copyBuffer(buf.data(), emptyBuf.data());
// Nothing should have been recorded.
QVERIFY(submitResourceUpdates(rhi.data(), batch));
}
void tst_QRhi::resourceUpdateBatchBufferCopyAndUploadOrder_data()
{
rhiTestData();
}
void tst_QRhi::resourceUpdateBatchBufferCopyAndUploadOrder()
{
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 copy and upload ordering");
if (!rhi->isFeatureSupported(QRhi::BufferToBufferCopy))
QSKIP("Buffer-to-buffer copy is not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::ReadBackNonUniformBuffer))
QSKIP("Reading buffers with non-uniform usage is not supported on this backend");
if (impl == QRhi::Null)
QSKIP("The Null backend does not have real buffer contents");
// Where buffer uploads are host writes instead of commands in the stream
// their ordering against GPU-side copies is undefined, as documented for
// copyBuffer(). In practice this is Metal on macOS on devices that do not
// report an Apple GPU.
if (!rhi->isFeatureSupported(QRhi::StaticBuffersOnGpuTimeline))
QSKIP("Buffer upload and GPU-side copy ordering is undefined on this backend");
// A GPU-side copy and a host-side upload targeting the same buffer within
// one batch must take effect in the order they were recorded in. This
// requires the backend to put the upload into the command stream instead of
// performing it as a host write on an unrelated timeline.
const int bufferSize = 32;
const QByteArray a(bufferSize, 'A');
const QByteArray b(bufferSize, 'B');
// The readback is deliberately part of the same batch. Backends that keep
// the upload on the CPU timeline apply it lazily, when the buffer is next
// used - which the readback in a later batch would happen to get right by
// accident. Within one batch there is nowhere to hide.
auto runInOneBatch = [&rhi, &a](bool copyFirst, quint32 uploadOffset,
const QByteArray &uploadData) {
QScopedPointer<QRhiBuffer> src(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::VertexBuffer, bufferSize));
if (!src->create())
return QByteArray();
QScopedPointer<QRhiBuffer> dst(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::VertexBuffer, bufferSize));
if (!dst->create())
return QByteArray();
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
batch->uploadStaticBuffer(src.data(), 0, bufferSize, a.constData());
if (!submitResourceUpdates(rhi.data(), batch))
return QByteArray();
batch = rhi->nextResourceUpdateBatch();
if (copyFirst) {
batch->copyBuffer(dst.data(), src.data());
batch->uploadStaticBuffer(dst.data(), uploadOffset, uploadData.size(), uploadData.constData());
} else {
batch->uploadStaticBuffer(dst.data(), uploadOffset, uploadData.size(), uploadData.constData());
batch->copyBuffer(dst.data(), src.data());
}
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
batch->readBackBuffer(dst.data(), 0, bufferSize, &readResult);
// Offscreen frames are synchronous, so this completes here and now.
if (!submitResourceUpdates(rhi.data(), batch) || !readCompleted)
return QByteArray();
return readResult.data;
};
// copy, then upload: the upload has to win
QCOMPARE(runInOneBatch(true, 0, b), b);
// upload, then copy: the copy has to win
QCOMPARE(runInOneBatch(false, 0, b), a);
// partial upload after a full copy
{
QByteArray expected = a;
expected.replace(8, 8, QByteArray(8, 'B'));
QCOMPARE(runInOneBatch(true, 8, QByteArray(8, 'B')), expected);
}
}
void tst_QRhi::resourceUpdateBatchBufferClear_data()
{
rhiTestData();
}
void tst_QRhi::resourceUpdateBatchBufferClear()
{
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 clears");
if (!rhi->isFeatureSupported(QRhi::Compute))
QSKIP("Compute is not supported, and so there are no storage buffers to clear");
if (!rhi->isFeatureSupported(QRhi::ReadBackNonUniformBuffer))
QSKIP("Reading back non-uniform buffers is not supported, skipping testing buffer clears");
if (impl == QRhi::Vulkan && isAndroidSwiftShader(rhi.get()))
QSKIP("SwiftShader renders and reads back unreliably (QTBUG-146930)");
const auto makePattern = [](int size) {
QByteArray pattern(size, 0);
for (int i = 0; i < size; ++i)
pattern[i] = char('A' + i % 26);
return pattern;
};
// Uploads the pattern, then does the clears and the readback in a second batch.
const auto clearAndReadBack = [&rhi](const QByteArray &pattern,
const std::function<void(QRhiResourceUpdateBatch *, QRhiBuffer *)> &clear) {
QScopedPointer<QRhiBuffer> buf(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, quint32(pattern.size())));
if (!buf->create())
return QByteArray();
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
batch->uploadStaticBuffer(buf.data(), pattern.constData());
if (!submitResourceUpdates(rhi.data(), batch))
return QByteArray();
batch = rhi->nextResourceUpdateBatch();
clear(batch, buf.data());
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
batch->readBackBuffer(buf.data(), 0, quint32(pattern.size()), &readResult);
if (!submitResourceUpdates(rhi.data(), batch) || !readCompleted)
return QByteArray();
return readResult.data;
};
const int bufferSize = 64;
const QByteArray pattern = makePattern(bufferSize);
// whole buffer
QCOMPARE(clearAndReadBack(pattern, [](QRhiResourceUpdateBatch *batch, QRhiBuffer *buf) {
batch->clearStorageBuffer(buf, 0);
}), QByteArray(bufferSize, 0));
// range starting at a multiple of 16
{
QByteArray expected = pattern;
expected.replace(16, 8, QByteArray(8, char(0xAB)));
QCOMPARE(clearAndReadBack(pattern, [](QRhiResourceUpdateBatch *batch, QRhiBuffer *buf) {
batch->clearStorageBuffer(buf, 16, 8, 0xAB);
}), expected);
}
// range starting at a multiple of 4 that is not a multiple of 16
{
QByteArray expected = pattern;
expected.replace(4, 20, QByteArray(20, char(0x5A)));
QCOMPARE(clearAndReadBack(pattern, [](QRhiResourceUpdateBatch *batch, QRhiBuffer *buf) {
batch->clearStorageBuffer(buf, 4, 20, 0x5A);
}), expected);
}
// Sizes that are not a multiple of 4 get rounded down, leaving the last
// bytes untouched. Also checks that clears follow the recording order.
{
const int oddBufferSize = 30;
const QByteArray oddPattern = makePattern(oddBufferSize);
QByteArray expected = oddPattern;
expected.replace(0, 28, QByteArray(28, char(0x11)));
expected.replace(4, 8, QByteArray(8, char(0x22)));
QTest::ignoreMessage(QtWarningMsg, "Buffer clear size 30 is not a multiple of 4, rounding down");
QTest::ignoreMessage(QtWarningMsg, "Buffer clear size 10 is not a multiple of 4, rounding down");
QCOMPARE(clearAndReadBack(oddPattern, [](QRhiResourceUpdateBatch *batch, QRhiBuffer *buf) {
batch->clearStorageBuffer(buf, 0x11);
batch->clearStorageBuffer(buf, 4, 10, 0x22);
}), expected);
}
}
void tst_QRhi::resourceUpdateBatchBufferClearInvalid_data()
{
rhiTestData();
}
void tst_QRhi::resourceUpdateBatchBufferClearInvalid()
{
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 invalid buffer clears");
if (!rhi->isFeatureSupported(QRhi::Compute))
QSKIP("Compute is not supported, and so there are no storage buffers to clear");
const int bufferSize = 32;
QScopedPointer<QRhiBuffer> buf(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer, bufferSize));
QVERIFY(buf->create());
QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::VertexBuffer, bufferSize));
QVERIFY(vbuf->create());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QVERIFY(batch);
QTest::ignoreMessage(QtWarningMsg, "Buffer clear with a null buffer is not supported");
batch->clearStorageBuffer(nullptr, 0);
QTest::ignoreMessage(QtWarningMsg, "Buffer clear with a null buffer is not supported");
batch->clearStorageBuffer(nullptr, 0, 4, 0);
QTest::ignoreMessage(QtWarningMsg, "Buffer clear is only supported for buffers with StorageBuffer usage");
batch->clearStorageBuffer(vbuf.data(), 0);
// Most backends refuse to create such a buffer in the first place.
QScopedPointer<QRhiBuffer> dynamicBuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::StorageBuffer, bufferSize));
if (dynamicBuf->create()) {
QTest::ignoreMessage(QtWarningMsg, "Buffer clear is not supported for Dynamic buffers");
batch->clearStorageBuffer(dynamicBuf.data(), 0);
}
QTest::ignoreMessage(QtWarningMsg, "Buffer clear offset 2 is not a multiple of 4");
batch->clearStorageBuffer(buf.data(), 2, 8, 0);
QTest::ignoreMessage(QtWarningMsg, "Buffer clear of 32 bytes at offset 16 does not fit the buffer (32 bytes)");
batch->clearStorageBuffer(buf.data(), 16, 32, 0);
// A zero sized clear is a no-op, not a warning, but rounding a size down
// to zero warns.
batch->clearStorageBuffer(buf.data(), 0, 0, 0);
QTest::ignoreMessage(QtWarningMsg, "Buffer clear size 3 is not a multiple of 4, rounding down");
batch->clearStorageBuffer(buf.data(), 0, 3, 0);
// Nothing should have been recorded.
QVERIFY(submitResourceUpdates(rhi.data(), batch));
}
void tst_QRhi::resourceUpdateBatchStaticBufferUploadEdgeCases_data()
{
rhiTestData();
}
void tst_QRhi::resourceUpdateBatchStaticBufferUploadEdgeCases()
{
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 static buffer upload edge cases");
const bool canReadBack = rhi->isFeatureSupported(QRhi::ReadBackNonUniformBuffer) && impl != QRhi::Null;
// Uploading and then destroying as soon as the frame is submitted, without
// ever binding the buffer. Backends that record the upload as a copy
// command must keep the native buffer alive until the frame has been
// executed. (destroying any earlier, while the frame is still being
// recorded, is not allowed by QRhi)
{
QRhiCommandBuffer *cb = nullptr;
QCOMPARE(rhi->beginOffscreenFrame(&cb), QRhi::FrameOpSuccess);
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QScopedPointer<QRhiBuffer> buf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, 256));
QVERIFY(buf->create());
const QByteArray data(256, 'X');
batch->uploadStaticBuffer(buf.data(), 0, 256, data.constData());
cb->resourceUpdate(batch);
rhi->endOffscreenFrame();
buf.reset();
}
// Several partial uploads to disjoint ranges of one buffer in one batch.
{
const int bufferSize = 64;
QScopedPointer<QRhiBuffer> buf(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::VertexBuffer, bufferSize));
QVERIFY(buf->create());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
const QByteArray zero(bufferSize, '0');
batch->uploadStaticBuffer(buf.data(), 0, bufferSize, zero.constData());
const QByteArray a(8, 'A');
const QByteArray b(8, 'B');
const QByteArray c(8, 'C');
batch->uploadStaticBuffer(buf.data(), 0, 8, a.constData());
batch->uploadStaticBuffer(buf.data(), 24, 8, b.constData());
batch->uploadStaticBuffer(buf.data(), 56, 8, c.constData());
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
if (canReadBack)
batch->readBackBuffer(buf.data(), 0, bufferSize, &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
if (canReadBack) {
QVERIFY(readCompleted);
QByteArray expected = zero;
expected.replace(0, 8, a);
expected.replace(24, 8, b);
expected.replace(56, 8, c);
QCOMPARE(readResult.data, expected);
}
}
// A large upload, exercising backends that sub-allocate staging memory from
// a per-frame area and need a separate path above some size.
{
const int bufferSize = 4 * 1024 * 1024;
QScopedPointer<QRhiBuffer> buf(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::VertexBuffer, bufferSize));
QVERIFY(buf->create());
QByteArray data(bufferSize, 'L');
data.replace(bufferSize - 4, 4, QByteArrayLiteral("End!"));
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
batch->uploadStaticBuffer(buf.data(), 0, bufferSize, data.constData());
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
if (canReadBack)
batch->readBackBuffer(buf.data(), bufferSize - 16, 16, &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
if (canReadBack) {
QVERIFY(readCompleted);
QCOMPARE(readResult.data, data.right(16));
}
}
}
inline bool imageRGBAEquals(const QImage &a, const QImage &b, int maxFuzz = 1)
{
if (a.size() != b.size())
return false;
const QImage image0 = a.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
const QImage image1 = b.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
const int width = image0.width();
const int height = image0.height();
for (int y = 0; y < height; ++y) {
const quint32 *p0 = reinterpret_cast<const quint32 *>(image0.constScanLine(y));
const quint32 *p1 = reinterpret_cast<const quint32 *>(image1.constScanLine(y));
int x = width - 1;
while (x-- >= 0) {
const QRgb c0(*p0++);
const QRgb c1(*p1++);
const int red = qAbs(qRed(c0) - qRed(c1));
const int green = qAbs(qGreen(c0) - qGreen(c1));
const int blue = qAbs(qBlue(c0) - qBlue(c1));
const int alpha = qAbs(qAlpha(c0) - qAlpha(c1));
if (red > maxFuzz || green > maxFuzz || blue > maxFuzz || alpha > maxFuzz)
return false;
}
}
return true;
}
void tst_QRhi::resourceUpdateBatchRGBATextureUpload_data()
{
rhiTestData();
}
void tst_QRhi::resourceUpdateBatchRGBATextureUpload()
{
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 texture resource updates");
QImage image(234, 123, QImage::Format_RGBA8888_Premultiplied);
image.fill(Qt::red);
QPainter painter;
const QPoint greenRectPos(35, 50);
const QSize greenRectSize(100, 50);
painter.begin(&image);
painter.fillRect(QRect(greenRectPos, greenRectSize), Qt::green);
painter.end();
// simple image upload; uploading and reading back RGBA8 is supported by the Null backend even
{
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, image.size(),
1, QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
batch->uploadTexture(texture.data(), image);
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
batch->readBackTexture(texture.data(), &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
// like with buffers, the readback is now complete due to endOffscreenFrame()
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(),
image.format());
QVERIFY(imageRGBAEquals(image, wrapperImage));
}
// the same with raw data
{
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, image.size(),
1, QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QRhiTextureUploadEntry upload(0, 0, { image.constBits(), quint32(image.sizeInBytes()) });
QRhiTextureUploadDescription uploadDesc(upload);
batch->uploadTexture(texture.data(), uploadDesc);
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
batch->readBackTexture(texture.data(), &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
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(),
image.format());
QVERIFY(imageRGBAEquals(image, wrapperImage));
}
// partial image upload at a non-zero destination position
{
const QSize copySize(30, 40);
const int gap = 10;
const QSize fullSize(copySize.width() + gap, copySize.height() + gap);
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, fullSize,
1, QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QImage clearImage(fullSize, image.format());
clearImage.fill(Qt::black);
batch->uploadTexture(texture.data(), clearImage);
// copy green pixels of copySize to (gap, gap), leaving a black bar of
// gap pixels on the left and top
QRhiTextureSubresourceUploadDescription desc;
desc.setImage(image);
desc.setSourceSize(copySize);
desc.setDestinationTopLeft(QPoint(gap, gap));
desc.setSourceTopLeft(greenRectPos);
batch->uploadTexture(texture.data(), QRhiTextureUploadDescription({ 0, 0, desc }));
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
batch->readBackTexture(texture.data(), &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
QVERIFY(readCompleted);
QCOMPARE(readResult.format, QRhiTexture::RGBA8);
QCOMPARE(readResult.pixelSize, clearImage.size());
QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
readResult.pixelSize.width(), readResult.pixelSize.height(),
image.format());
QVERIFY(!imageRGBAEquals(clearImage, wrapperImage));
QImage expectedImage = clearImage;
QPainter painter(&expectedImage);
painter.fillRect(QRect(QPoint(gap, gap), QSize(copySize)), Qt::green);
painter.end();
QVERIFY(imageRGBAEquals(expectedImage, wrapperImage));
}
// the same (partial upload) with raw data as source
{
const QSize copySize(30, 40);
const int gap = 10;
const QSize fullSize(copySize.width() + gap, copySize.height() + gap);
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, fullSize,
1, QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QImage clearImage(fullSize, image.format());
clearImage.fill(Qt::black);
batch->uploadTexture(texture.data(), clearImage);
// 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()), im.sizeInBytes()));
desc.setSourceSize(copySize);
desc.setDestinationTopLeft(QPoint(gap, gap));
batch->uploadTexture(texture.data(), QRhiTextureUploadDescription({ 0, 0, desc }));
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
batch->readBackTexture(texture.data(), &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
QVERIFY(readCompleted);
QCOMPARE(readResult.format, QRhiTexture::RGBA8);
QCOMPARE(readResult.pixelSize, clearImage.size());
QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
readResult.pixelSize.width(), readResult.pixelSize.height(),
image.format());
QVERIFY(!imageRGBAEquals(clearImage, wrapperImage));
QImage expectedImage = clearImage;
QPainter painter(&expectedImage);
painter.fillRect(QRect(QPoint(gap, gap), QSize(copySize)), Qt::green);
painter.end();
QVERIFY(imageRGBAEquals(expectedImage, wrapperImage));
}
// now a QImage from an actual file
{
QImage inputImage;
inputImage.load(QLatin1String(":/data/qt256.png"));
QVERIFY(!inputImage.isNull());
inputImage = std::move(inputImage).convertToFormat(image.format());
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, inputImage.size(),
1, QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
batch->uploadTexture(texture.data(), inputImage);
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
batch->readBackTexture(texture.data(), &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
QVERIFY(readCompleted);
QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
readResult.pixelSize.width(), readResult.pixelSize.height(),
inputImage.format());
QVERIFY(imageRGBAEquals(inputImage, wrapperImage));
// Now try uploading to position (0, 1) which is out of bounds by one
// row. This is expected to lead to printing a warning, and clamping the
// upload size so that the underlying 3D APIs will not crash.
if (impl != QRhi::Null) {
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(QStringLiteral("Invalid texture upload issued.+")));
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QRhiTextureSubresourceUploadDescription subresDesc(inputImage);
subresDesc.setDestinationTopLeft(QPoint(0, 1));
QRhiTextureUploadDescription uploadDesc(QRhiTextureUploadEntry(0, 0, subresDesc));
batch->uploadTexture(texture.data(), uploadDesc);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
}
}
}
void tst_QRhi::resourceUpdateBatchR8TextureUpload_data()
{
rhiTestData();
}
void tst_QRhi::resourceUpdateBatchR8TextureUpload()
{
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 texture resource updates");
if (!rhi->isTextureFormatSupported(QRhiTexture::R8))
QSKIP("R8 texture format not supported on this backend");
const int WIDTH = 4;
const int HEIGHT = 4;
// A value distinct from both 0x00 and 0xFF, so a readback that picks the wrong channel (e.g.
// alpha, which is defined to read back as 1.0 == 0xFF for a texture format with no alpha
// channel) is unambiguously wrong, not accidentally right.
const char RED_VALUE = 0x40;
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::R8, QSize(WIDTH, HEIGHT), 1,
QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
const QByteArray data(WIDTH * HEIGHT, RED_VALUE);
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QRhiTextureSubresourceUploadDescription subresDesc(data.constData(), data.size());
batch->uploadTexture(texture.data(),
QRhiTextureUploadDescription(QRhiTextureUploadEntry(0, 0, subresDesc)));
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
batch->readBackTexture(texture.data(), &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
QVERIFY(readCompleted);
QCOMPARE(readResult.format, QRhiTexture::R8);
QCOMPARE(readResult.pixelSize, QSize(WIDTH, HEIGHT));
// the Null backend does not render so skip the verification for that
if (impl == QRhi::Null)
return;
QCOMPARE(readResult.data, data);
}
void tst_QRhi::resourceUpdateBatchRGBATextureCopy_data()
{
rhiTestData();
}
void tst_QRhi::resourceUpdateBatchRGBATextureCopy()
{
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 texture resource updates");
QImage red(256, 256, QImage::Format_RGBA8888_Premultiplied);
red.fill(Qt::red);
QImage green(35, 73, red.format());
green.fill(Qt::green);
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QScopedPointer<QRhiTexture> redTexture(rhi->newTexture(QRhiTexture::RGBA8, red.size(),
1, QRhiTexture::UsedAsTransferSource));
QVERIFY(redTexture->create());
batch->uploadTexture(redTexture.data(), red);
QScopedPointer<QRhiTexture> greenTexture(rhi->newTexture(QRhiTexture::RGBA8, green.size(),
1, QRhiTexture::UsedAsTransferSource));
QVERIFY(greenTexture->create());
batch->uploadTexture(greenTexture.data(), green);
// 1. simple copy red -> texture; 2. subimage copy green -> texture; 3. partial subimage copy green -> texture
{
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, red.size(),
1, QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
// 1.
batch->copyTexture(texture.data(), redTexture.data());
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
batch->readBackTexture(texture.data(), &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
QVERIFY(readCompleted);
QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
readResult.pixelSize.width(), readResult.pixelSize.height(),
red.format());
QVERIFY(imageRGBAEquals(red, wrapperImage));
batch = rhi->nextResourceUpdateBatch();
readCompleted = false;
// 2.
QRhiTextureCopyDescription copyDesc;
copyDesc.setDestinationTopLeft(QPoint(15, 23));
batch->copyTexture(texture.data(), greenTexture.data(), copyDesc);
batch->readBackTexture(texture.data(), &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
QVERIFY(readCompleted);
wrapperImage = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
readResult.pixelSize.width(), readResult.pixelSize.height(),
red.format());
QImage expectedImage = red;
QPainter painter(&expectedImage);
painter.drawImage(copyDesc.destinationTopLeft(), green);
painter.end();
QVERIFY(imageRGBAEquals(expectedImage, wrapperImage));
batch = rhi->nextResourceUpdateBatch();
readCompleted = false;
// 3.
copyDesc.setDestinationTopLeft(QPoint(125, 89));
copyDesc.setSourceTopLeft(QPoint(5, 5));
copyDesc.setPixelSize(QSize(26, 45));
batch->copyTexture(texture.data(), greenTexture.data(), copyDesc);
batch->readBackTexture(texture.data(), &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
QVERIFY(readCompleted);
wrapperImage = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
readResult.pixelSize.width(), readResult.pixelSize.height(),
red.format());
painter.begin(&expectedImage);
painter.drawImage(copyDesc.destinationTopLeft(), green,
QRect(copyDesc.sourceTopLeft(), copyDesc.pixelSize()));
painter.end();
QVERIFY(imageRGBAEquals(expectedImage, wrapperImage));
}
}
void tst_QRhi::resourceUpdateBatchRGBATextureMip_data()
{
rhiTestData();
}
void tst_QRhi::resourceUpdateBatchRGBATextureMip()
{
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 texture resource updates");
QImage red(512, 512, QImage::Format_RGBA8888_Premultiplied);
red.fill(Qt::red);
const QRhiTexture::Flags textureFlags =
QRhiTexture::UsedAsTransferSource
| QRhiTexture::MipMapped
| QRhiTexture::UsedWithGenerateMips;
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, red.size(), 1, textureFlags));
QVERIFY(texture->create());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
batch->uploadTexture(texture.data(), red);
batch->generateMips(texture.data());
QVERIFY(submitResourceUpdates(rhi.data(), batch));
const int levelCount = rhi->mipLevelsForSize(red.size());
QCOMPARE(levelCount, 10);
for (int level = 0; level < levelCount; ++level) {
batch = rhi->nextResourceUpdateBatch();
QRhiReadbackDescription readDesc(texture.data());
readDesc.setLevel(level);
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
batch->readBackTexture(readDesc, &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
QVERIFY(readCompleted);
const QSize expectedSize = rhi->sizeForMipLevel(level, texture->pixelSize());
QCOMPARE(readResult.pixelSize, expectedSize);
QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
readResult.pixelSize.width(), readResult.pixelSize.height(),
red.format());
QImage expectedImage;
if (level == 0 || rhi->isFeatureSupported(QRhi::ReadBackNonBaseMipLevel)) {
// Compare to a scaled version; we can do this safely only because we
// only have plain red pixels in the source image.
expectedImage = red.scaled(expectedSize);
} else {
qDebug("Expecting all-zero image for level %d because reading back a level other than 0 is not supported", level);
expectedImage = QImage(readResult.pixelSize, red.format());
expectedImage.fill(0);
}
QVERIFY(imageRGBAEquals(expectedImage, wrapperImage));
}
}
void tst_QRhi::resourceUpdateBatchR8TextureMip_data()
{
rhiTestData();
}
void tst_QRhi::resourceUpdateBatchR8TextureMip()
{
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 texture resource updates");
if (!rhi->isTextureFormatSupported(QRhiTexture::R8))
QSKIP("R8 texture format not supported on this backend");
const QSize baseSize(64, 64);
QScopedPointer<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::R8, baseSize, 1,
QRhiTexture::UsedAsTransferSource | QRhiTexture::MipMapped));
QVERIFY(texture->create());
const QByteArray data(baseSize.width() * baseSize.height(), 0x40);
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QRhiTextureSubresourceUploadDescription subresDesc(data.constData(), data.size());
batch->uploadTexture(texture.data(),
QRhiTextureUploadDescription(QRhiTextureUploadEntry(0, 0, subresDesc)));
QVERIFY(submitResourceUpdates(rhi.data(), batch));
const bool canReadNonBaseLevel = rhi->isFeatureSupported(QRhi::ReadBackNonBaseMipLevel);
const int levelCount = rhi->mipLevelsForSize(baseSize);
for (int level = 0; level < levelCount; ++level) {
QRhiReadbackDescription readDesc(texture.data());
readDesc.setLevel(level);
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
batch = rhi->nextResourceUpdateBatch();
batch->readBackTexture(readDesc, &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
QVERIFY(readCompleted);
const QSize expectedSize = rhi->sizeForMipLevel(level, baseSize);
QCOMPARE(readResult.format, QRhiTexture::R8);
QCOMPARE(readResult.pixelSize, expectedSize);
QCOMPARE(readResult.data.size(), expectedSize.width() * expectedSize.height());
if (level > 0 && !canReadNonBaseLevel)
QCOMPARE(readResult.data.count('\0'), readResult.data.size());
}
// Level 0 always reads back for real; the Null backend only simulates content for RGBA8.
if (impl != QRhi::Null) {
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
batch = rhi->nextResourceUpdateBatch();
batch->readBackTexture(texture.data(), &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
QVERIFY(readCompleted);
QCOMPARE(readResult.data, data);
}
}
void tst_QRhi::resourceUpdateBatchTextureRawDataStride_data()
{
rhiTestData();
}
void tst_QRhi::resourceUpdateBatchTextureRawDataStride()
{
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 texture resource updates");
const int WIDTH = 150;
const int DATA_WIDTH = 180;
const int HEIGHT = 50;
QByteArray image;
image.resize(DATA_WIDTH * HEIGHT * 4);
for (int y = 0; y < HEIGHT; ++y) {
char *p = image.data() + y * DATA_WIDTH * 4;
memset(p, y, DATA_WIDTH * 4);
}
{
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(WIDTH, HEIGHT),
1, QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QRhiTextureSubresourceUploadDescription subresDesc(image.constData(), image.size());
subresDesc.setDataStride(DATA_WIDTH * 4);
QRhiTextureUploadEntry upload(0, 0, subresDesc);
QRhiTextureUploadDescription uploadDesc(upload);
batch->uploadTexture(texture.data(), uploadDesc);
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
batch->readBackTexture(texture.data(), &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
QVERIFY(readCompleted);
QCOMPARE(readResult.format, QRhiTexture::RGBA8);
QCOMPARE(readResult.pixelSize, QSize(WIDTH, HEIGHT));
QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
readResult.pixelSize.width(), readResult.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
// wrap the original data, note the bytesPerLine argument
QImage originalWrapperImage(reinterpret_cast<const uchar *>(image.constData()),
WIDTH, HEIGHT, DATA_WIDTH * 4,
QImage::Format_RGBA8888_Premultiplied);
QVERIFY(imageRGBAEquals(wrapperImage, originalWrapperImage));
}
}
void tst_QRhi::resourceUpdateBatchTextureInvalidSizeAndStride_data()
{
rhiTestData();
}
void tst_QRhi::resourceUpdateBatchTextureInvalidSizeAndStride()
{
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 texture resource updates");
// Raw data uploads where sourceSize() or dataStride() describe more data than
// was actually provided, or where the destination region runs outside the
// subresource. All of these are invalid usage and are expected to produce a
// warning and a clamped (or skipped) upload, never a crash or an out-of-bounds
// access. Verify that by starting from a known texture content and checking
// that only the rows the data can back are touched.
const int WIDTH = 64;
const int HEIGHT = 32;
const int GOOD_ROWS = 4;
const char BASE = 0x10;
const char NEW = 0x20;
// Uniform values across all four channels, so that the test does not depend
// on the channel order of the read back data.
const QByteArray baseData(WIDTH * HEIGHT * 4, BASE);
const QByteArray fullData(WIDTH * HEIGHT * 4, NEW);
const QByteArray partialData(WIDTH * GOOD_ROWS * 4, NEW);
// Uploads badDesc onto a texture pre-filled with BASE, then reads back.
auto upload = [&](const QRhiTextureSubresourceUploadDescription &badDesc, QImage *result) -> bool {
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(WIDTH, HEIGHT),
1, QRhiTexture::UsedAsTransferSource));
if (!texture->create())
return false;
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QRhiTextureSubresourceUploadDescription baseDesc(baseData.constData(), baseData.size());
batch->uploadTexture(texture.data(),
QRhiTextureUploadDescription(QRhiTextureUploadEntry(0, 0, baseDesc)));
if (!submitResourceUpdates(rhi.data(), batch))
return false;
batch = rhi->nextResourceUpdateBatch();
batch->uploadTexture(texture.data(),
QRhiTextureUploadDescription(QRhiTextureUploadEntry(0, 0, badDesc)));
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
batch->readBackTexture(texture.data(), &readResult);
if (!submitResourceUpdates(rhi.data(), batch) || !readCompleted)
return false;
*result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
readResult.pixelSize.width(), readResult.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied).copy();
return !result->isNull();
};
// Rows [0, changedRows) are expected to hold NEW, the rest must still hold BASE.
auto verifyRows = [&](const QImage &image, int firstChangedRow, int lastChangedRow) -> bool {
for (int y = 0; y < image.height(); ++y) {
const bool changed = y >= firstChangedRow && y < lastChangedRow;
const uchar expected = uchar(changed ? NEW : BASE);
const uchar *p = image.constScanLine(y);
for (int i = 0, count = image.width() * 4; i < count; ++i) {
if (p[i] != expected) {
qWarning("Row %d byte %d is 0x%02x, expected 0x%02x", y, i, p[i], expected);
return false;
}
}
}
return true;
};
// sourceSize() claims the full texture, but only GOOD_ROWS rows were provided.
{
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(QStringLiteral("Invalid texture upload issued.+")));
QRhiTextureSubresourceUploadDescription desc(partialData.constData(), partialData.size());
desc.setSourceSize(QSize(WIDTH, HEIGHT));
QImage result;
QVERIFY(upload(desc, &result));
QVERIFY(verifyRows(result, 0, GOOD_ROWS));
}
// destinationTopLeft() puts the bottom of the upload outside the subresource.
{
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(QStringLiteral("Invalid texture upload issued.+")));
QRhiTextureSubresourceUploadDescription desc(fullData.constData(), fullData.size());
desc.setSourceSize(QSize(WIDTH, HEIGHT));
desc.setDestinationTopLeft(QPoint(0, 2));
QImage result;
QVERIFY(upload(desc, &result));
QVERIFY(verifyRows(result, 2, HEIGHT));
}
if (rhi->isFeatureSupported(QRhi::ImageDataStride)) {
// A padded dataStride(), with data for GOOD_ROWS rows only but a
// sourceSize() claiming the full height.
{
const int STRIDE = WIDTH * 4 + 64;
const QByteArray stridedData(STRIDE * GOOD_ROWS, NEW);
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(QStringLiteral("Invalid texture upload issued.+")));
QRhiTextureSubresourceUploadDescription desc(stridedData.constData(), stridedData.size());
desc.setDataStride(STRIDE);
desc.setSourceSize(QSize(WIDTH, HEIGHT));
QImage result;
QVERIFY(upload(desc, &result));
QVERIFY(verifyRows(result, 0, GOOD_ROWS));
}
// A dataStride() smaller than a single row needs. Nothing can be
// satisfied, so the upload is expected to be skipped entirely.
{
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(QStringLiteral("Invalid texture upload issued.+")));
QRhiTextureSubresourceUploadDescription desc(fullData.constData(), fullData.size());
desc.setDataStride(WIDTH * 2);
desc.setSourceSize(QSize(WIDTH, HEIGHT));
QImage result;
QVERIFY(upload(desc, &result));
QVERIFY(verifyRows(result, 0, 0));
}
}
}
void tst_QRhi::resourceUpdateBatchLotsOfResources_data()
{
rhiTestData();
}
void tst_QRhi::resourceUpdateBatchLotsOfResources()
{
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] = {};
QRhiResourceUpdateBatch *b = rhi->nextResourceUpdateBatch();
std::vector<std::unique_ptr<QRhiTexture>> textures;
std::vector<std::unique_ptr<QRhiBuffer>> buffers;
// QTBUG-96619
static const int TEXTURE_COUNT = 3 * QRhiResourceUpdateBatchPrivate::TEXTURE_OPS_STATIC_ALLOC;
static const int BUFFER_COUNT = 3 * QRhiResourceUpdateBatchPrivate::BUFFER_OPS_STATIC_ALLOC;
for (int i = 0; i < TEXTURE_COUNT; ++i) {
std::unique_ptr<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, image.size()));
QVERIFY(texture->create());
b->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());
b->uploadStaticBuffer(buffer.get(), bufferData);
buffers.push_back(std::move(buffer));
}
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));
if (f.open(QIODevice::ReadOnly)) {
const QByteArray contents = f.readAll();
return QShader::fromSerialized(contents);
}
return QShader();
}
void tst_QRhi::invalidPipeline_data()
{
rhiTestData();
}
void tst_QRhi::invalidPipeline()
{
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 empty shader");
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(256, 256), 1, QRhiTexture::RenderTarget));
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);
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->create());
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({ { 2 * sizeof(float) } });
inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
// no stages
QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.data());
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(!pipeline->create());
QShader vs;
QShader fs;
// no shaders in the stages
pipeline.reset(rhi->newGraphicsPipeline());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.data());
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(!pipeline->create());
vs = loadShader(":/data/simple.vert.qsb");
QVERIFY(vs.isValid());
fs = loadShader(":/data/simple.frag.qsb");
QVERIFY(fs.isValid());
// no vertex stage
pipeline.reset(rhi->newGraphicsPipeline());
pipeline->setShaderStages({ { QRhiShaderStage::Fragment, fs } });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.data());
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(!pipeline->create());
// no renderpass descriptor
pipeline.reset(rhi->newGraphicsPipeline());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.data());
QVERIFY(!pipeline->create());
// no shader resource bindings
pipeline.reset(rhi->newGraphicsPipeline());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(!pipeline->create());
// correct
pipeline.reset(rhi->newGraphicsPipeline());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setRenderPassDescriptor(rpDesc.data());
pipeline->setShaderResourceBindings(srb.data());
QVERIFY(pipeline->create());
}
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);
QFETCH(QRhiInitParams *, initParams);
QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
if (!rhi)
QSKIP("QRhi could not be created, skipping testing rendering");
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();
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_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());
// 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())
QCOMPARE_LT(redCount, blueCount);
else
QCOMPARE_GT(redCount, blueCount);
}
void tst_QRhi::frameEndBindsDefaultFramebuffer_data()
{
rhiTestData();
}
void tst_QRhi::frameEndBindsDefaultFramebuffer()
{
// A frame must not leave a non-default framebuffer bound: on OpenGL the
// leftover binding leaks into the present and into the next frame, which
// crashes the GL-on-Metal renderer on macOS when an offscreen frame's
// target lingers into a swapchain present (QTBUG-149985).
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");
const QSize outputSize(256, 256);
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget));
QVERIFY(texture->create());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
// A pass that ends without a readback or resolve leaves its own framebuffer
// bound as the last framebuffer operation; only the end-of-frame handling
// restores the default one.
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 });
cb->endPass();
rhi->endOffscreenFrame();
#ifdef TST_GL
if (impl == QRhi::OpenGLES2) {
QVERIFY(rhi->makeThreadLocalNativeContextCurrent());
QOpenGLContext *ctx = QOpenGLContext::currentContext();
QVERIFY(ctx);
GLint boundFbo = -1;
ctx->functions()->glGetIntegerv(GL_FRAMEBUFFER_BINDING, &boundFbo);
QCOMPARE(GLuint(boundFbo), ctx->defaultFramebufferObject());
}
#endif
}
void tst_QRhi::renderToTextureMip_data()
{
rhiTestData();
}
void tst_QRhi::renderToTextureMip()
{
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::RenderToNonBaseMipLevel))
QSKIP("Rendering to non-base mip levels is not supported on this platform, skipping test");
const QSize baseLevelSize(1024, 1024);
const int LEVEL = 3; // render into mip #3 (128x128)
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, baseLevelSize, 1,
QRhiTexture::RenderTarget
| QRhiTexture::UsedAsTransferSource
| QRhiTexture::MipMapped));
QVERIFY(texture->create());
QRhiColorAttachment colorAtt(texture.data());
colorAtt.setLevel(LEVEL);
QRhiTextureRenderTargetDescription rtDesc(colorAtt);
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(rtDesc));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
QCOMPARE(rt->pixelSize(), rhi->sizeForMipLevel(LEVEL, baseLevelSize));
const QSize mipSize(baseLevelSize.width() >> LEVEL, baseLevelSize.height() >> LEVEL);
QCOMPARE(rt->pixelSize(), mipSize);
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_Premultiplied);
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
QRhiReadbackDescription readbackDescription(texture.data());
readbackDescription.setLevel(LEVEL);
readbackBatch->readBackTexture(readbackDescription, &readResult);
cb->endPass(readbackBatch);
rhi->endOffscreenFrame();
if (!rhi->isFeatureSupported(QRhi::ReadBackNonBaseMipLevel))
QSKIP("Reading back non-base mip levels is not supported on this platform, skipping readback");
QCOMPARE(result.size(), mipSize);
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");
}
QCOMPARE(redCount + blueCount, mipSize.width());
if (rhi->isYUpInFramebuffer() == rhi->isYUpInNDC())
QCOMPARE_GT(redCount, blueCount); // 100, 28
else
QCOMPARE_LT(redCount, blueCount); // 28, 100
}
void tst_QRhi::renderToTextureCubemapFace_data()
{
rhiTestData();
}
void tst_QRhi::renderToTextureCubemapFace()
{
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");
const QSize outputSize(512, 512); // width must be same as height
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget
| QRhiTexture::UsedAsTransferSource
| QRhiTexture::CubeMap)); // will be a cubemap, so 6 layers
QVERIFY(texture->create());
const int LAYER = 1; // render into the layer for face -X
const int BAD_LAYER = 2; // +Y
const QColor badLayerColor = Qt::green;
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);
// The face we do not render into gets read back later on. Give it a known
// content: reading back a texture subresource that was never written to
// gives undefined results, and with D3D12 it makes the debug layer complain
// as well, because such a subresource is not initialized.
QImage badLayerImage(outputSize, QImage::Format_RGBA8888);
badLayerImage.fill(badLayerColor);
updates->uploadTexture(texture.data(),
QRhiTextureUploadDescription({ BAD_LAYER, 0,
QRhiTextureSubresourceUploadDescription(badLayerImage) }));
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_Premultiplied);
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
QRhiReadbackDescription readbackDescription(texture.data());
readbackDescription.setLayer(LAYER);
readbackBatch->readBackTexture(readbackDescription, &readResult);
// also read back a layer we did not render into, only uploaded to
QRhiReadbackResult readResult2;
QImage result2;
readResult2.completed = [&readResult2, &result2] {
result2 = QImage(reinterpret_cast<const uchar *>(readResult2.data.constData()),
readResult2.pixelSize.width(), readResult2.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiReadbackDescription readbackDescription2(texture.data());
readbackDescription2.setLayer(BAD_LAYER);
readbackBatch->readBackTexture(readbackDescription2, &readResult2);
cb->endPass(readbackBatch);
rhi->endOffscreenFrame();
QCOMPARE(result.size(), outputSize);
QCOMPARE(result2.size(), outputSize);
if (impl == QRhi::Null)
return;
// just want to ensure that we did not read the same thing back twice, i.e.
// that the 'layer' parameter was not ignored
QVERIFY(result != result2);
// the face we only uploaded to must have the uploaded content
QCOMPARE(result2.pixelColor(result2.width() / 2, result2.height() / 2), badLayerColor);
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_GT(redCount, 0);
QCOMPARE_GT(blueCount, 0);
QCOMPARE(redCount + blueCount, outputSize.width());
if (rhi->isYUpInFramebuffer() == rhi->isYUpInNDC())
QCOMPARE_LT(redCount, blueCount); // 100, 412
else
QCOMPARE_GT(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 (impl == QRhi::OpenGLES2 && isAndroidSwiftShader(rhi.get()))
QSKIP("SwiftShader does not support this OpenGLES feature (QTBUG-132934)");
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");
}
QCOMPARE_GT(redCount, 0);
QCOMPARE_GT(blueCount, 0);
QCOMPARE(redCount + blueCount, outputSize.width());
if (rhi->isYUpInFramebuffer() == rhi->isYUpInNDC())
QCOMPARE_LT(redCount, blueCount); // 100, 412
else
QCOMPARE_GT(redCount, blueCount); // 412, 100
}
static const float quadVerticesUvs[] = {
-1.0f, -1.0f, 0.0f, 0.0f,
1.0f, -1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f
};
void tst_QRhi::renderToTextureTexturedQuad_data()
{
bool srgbModes[] = { false, true };
rhiTestDataWithParam("srgb", QSpan(srgbModes), [](bool srgb) {
return srgb ? QStringLiteral("sRGB") : QStringLiteral("linear");
});
}
void tst_QRhi::renderToTextureTexturedQuad()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QFETCH(bool, srgb);
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());
inputImage = inputImage.convertToFormat(QImage::Format_RGBA8888);
const QPoint srgbProbePoint(64, 96);
inputImage.setPixel(srgbProbePoint, qRgba(225, 225, 225, 255));
QRhiTexture::Flags inputTextureFlags;
if (srgb)
inputTextureFlags |= QRhiTexture::sRGB;
if (!rhi->isTextureFormatSupported(QRhiTexture::RGBA8, inputTextureFlags))
QSKIP("Texture format not supported on this backend");
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(), 1,
inputTextureFlags));
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::sampledTexture(0, QRhiShaderResourceBinding::FragmentStage, inputTexture.data(), 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.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;
// Flip with D3D and Metal because these have Y down in images. Vulkan does
// not need this because there Y is down both in images and in NDC, which
// just happens to give correct results with our OpenGL-targeted vertex and
// UV data.
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
// check a few points that are expected to match regardless of the implementation
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);
QCOMPARE_GT(qGreen(result.pixel(32, 52)), 2 * qRed(result.pixel(32, 52)));
QCOMPARE_GT(qGreen(result.pixel(32, 52)), 2 * qBlue(result.pixel(32, 52)));
QCOMPARE_GT(qGreen(result.pixel(214, 191)), 2 * qRed(result.pixel(214, 191)));
QCOMPARE_GT(qGreen(result.pixel(214, 191)), 2 * qBlue(result.pixel(214, 191)));
const QRgb srgbProbe = result.pixel(srgbProbePoint);
const int expectedProbeValue = srgb ? 192 : 225;
QCOMPARE_EQ(qRed(srgbProbe), expectedProbeValue);
QCOMPARE_EQ(qGreen(srgbProbe), expectedProbeValue);
QCOMPARE_EQ(qBlue(srgbProbe), expectedProbeValue);
QCOMPARE_EQ(qAlpha(srgbProbe), 255);
}
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.flip();
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);
QCOMPARE_GT(qGreen(result.pixel(32, 52)), 2 * qRed(result.pixel(32, 52)));
QCOMPARE_GT(qGreen(result.pixel(32, 52)), 2 * qBlue(result.pixel(32, 52)));
QCOMPARE_GT(qGreen(result.pixel(214, 191)), 2 * qRed(result.pixel(214, 191)));
QCOMPARE_GT(qGreen(result.pixel(214, 191)), 2 * qBlue(result.pixel(214, 191)));
}
void tst_QRhi::renderToTextureArrayOfTexturedQuad_data()
{
rhiTestData();
}
void tst_QRhi::renderToTextureArrayOfTexturedQuad()
{
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 (impl == QRhi::OpenGLES2 && isAndroidSwiftShader(rhi.get()))
QSKIP("SwiftShader does not support this OpenGLES feature (QTBUG-132934)");
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);
// In this test we pass 3 textures (and samplers) to the fragment shader in
// form of an array of combined image samplers.
QScopedPointer<QRhiTexture> inputTexture(rhi->newTexture(QRhiTexture::RGBA8, inputImage.size()));
QVERIFY(inputTexture->create());
updates->uploadTexture(inputTexture.data(), inputImage);
QImage redImage(inputImage.size(), QImage::Format_RGBA8888);
redImage.fill(Qt::red);
QScopedPointer<QRhiTexture> redTexture(rhi->newTexture(QRhiTexture::RGBA8, inputImage.size()));
QVERIFY(redTexture->create());
updates->uploadTexture(redTexture.data(), redImage);
QImage greenImage(inputImage.size(), QImage::Format_RGBA8888);
greenImage.fill(Qt::green);
QScopedPointer<QRhiTexture> greenTexture(rhi->newTexture(QRhiTexture::RGBA8, inputImage.size()));
QVERIFY(greenTexture->create());
updates->uploadTexture(greenTexture.data(), greenImage);
QScopedPointer<QRhiSampler> sampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
QVERIFY(sampler->create());
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QRhiShaderResourceBinding::TextureAndSampler texSamplers[3] = {
{ inputTexture.data(), sampler.data() },
{ redTexture.data(), sampler.data() },
{ greenTexture.data(), sampler.data() }
};
srb->setBindings({
QRhiShaderResourceBinding::sampledTextures(0, QRhiShaderResourceBinding::FragmentStage, 3, texSamplers)
});
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_array.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;
// Flip with D3D and Metal because these have Y down in images. Vulkan does
// not need this because there Y is down both in images and in NDC, which
// just happens to give correct results with our OpenGL-targeted vertex and
// UV data.
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
// we added the input image + red + green together, so red and green must be all 1
for (int y = 0; y < result.height(); ++y) {
for (int x = 0; x < result.width(); ++x) {
const QRgb pixel = result.pixel(x, y);
QCOMPARE(qRed(pixel), 255);
QCOMPARE(qGreen(pixel), 255);
}
}
}
void tst_QRhi::renderToTextureArrayOfSampledTextures_data()
{
rhiTestData();
}
void tst_QRhi::renderToTextureArrayOfSampledTextures()
{
// Like renderToTextureArrayOfTexturedQuad, but each element of the array of
// combined image samplers is sampled into a column of its own, so the
// result also tells which texture ended up where in the array. Unlike most
// other shaders here, these have SM 6.0 bytecode as well: with D3D12 that
// is what makes an array of textures/samplers appear as a single
// shader-declared descriptor range, which a root signature built from a
// series of one-descriptor ranges does not satisfy.
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 (impl == QRhi::OpenGLES2 && isAndroidSwiftShader(rhi.get()))
QSKIP("SwiftShader does not support this OpenGLES feature (QTBUG-132934)");
#ifdef Q_OS_WIN
// The in-box software rasterizer ('Microsoft Basic Render Driver', vendor
// 0x1414) in Windows 10 samples the wrong element of the array, even though
// pipeline creation succeeds. This is specific to the DXIL (SM 6.0) shader
// the D3D12 backend picks here: the SM 5.0 based
// renderToTextureArrayOfTexturedQuad passes on the same machine. Does not
// happen with the much newer WARP in Windows 11, nor with real GPUs, so
// keep testing everywhere else.
if (impl == QRhi::D3D12 && rhi->driverInfo().vendorId == 0x1414
&& QOperatingSystemVersion::current() < QOperatingSystemVersion::Windows11)
{
QSKIP("Windows 10 software rasterizer is used which cannot sample an array of "
"textures correctly with SM 6.0 shaders");
}
#endif
constexpr int ARRAY_SIZE = 8; // must match the shader
static const QRgb colors[ARRAY_SIZE] = {
qRgb(255, 0, 0), qRgb(0, 255, 0), qRgb(0, 0, 255), qRgb(255, 255, 0),
qRgb(255, 0, 255), qRgb(0, 255, 255), qRgb(255, 255, 255), qRgb(128, 128, 128)
};
const QSize outputSize(8 * ARRAY_SIZE, 32);
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();
QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(quadVerticesUvs)));
QVERIFY(vbuf->create());
updates->uploadStaticBuffer(vbuf.data(), quadVerticesUvs);
std::unique_ptr<QRhiTexture> inputTextures[ARRAY_SIZE];
for (int i = 0; i < ARRAY_SIZE; ++i) {
QImage image(QSize(16, 16), QImage::Format_RGBA8888);
image.fill(QColor::fromRgb(colors[i]));
inputTextures[i].reset(rhi->newTexture(QRhiTexture::RGBA8, image.size()));
QVERIFY(inputTextures[i]->create());
updates->uploadTexture(inputTextures[i].get(), image);
}
// Two different samplers, so that the array is not just the same sampler
// repeated. (with some backends an array of samplers needs a set of
// consecutive native sampler descriptors of its own) The input textures are
// single color fills, so the filtering does not affect the result.
QScopedPointer<QRhiSampler> nearestSampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
QVERIFY(nearestSampler->create());
QScopedPointer<QRhiSampler> linearSampler(rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
QVERIFY(linearSampler->create());
QRhiShaderResourceBinding::TextureAndSampler texSamplers[ARRAY_SIZE];
for (int i = 0; i < ARRAY_SIZE; ++i) {
texSamplers[i] = { inputTextures[i].get(),
i % 2 ? linearSampler.data() : nearestSampler.data() };
}
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({
QRhiShaderResourceBinding::sampledTextures(0, QRhiShaderResourceBinding::FragmentStage, ARRAY_SIZE, texSamplers)
});
QVERIFY(srb->create());
QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
QShader vs = loadShader(":/data/arrayofsampledtextures.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(":/data/arrayofsampledtextures.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(outputSize.width()), float(outputSize.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;
// No flipping needed here, unlike in the other tests: the columns run along
// X, so the Y direction difference between the backends does not change
// which column a pixel belongs to.
const int columnWidth = outputSize.width() / ARRAY_SIZE;
for (int i = 0; i < ARRAY_SIZE; ++i) {
const QRgb pixel = result.pixel(i * columnWidth + columnWidth / 2, outputSize.height() / 2);
QCOMPARE(qRed(pixel), qRed(colors[i]));
QCOMPARE(qGreen(pixel), qGreen(colors[i]));
QCOMPARE(qBlue(pixel), qBlue(colors[i]));
}
}
void tst_QRhi::renderToTextureTexturedQuadAndUniformBuffer_data()
{
rhiTestData();
}
void tst_QRhi::renderToTextureTexturedQuadAndUniformBuffer()
{
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);
// There will be two renderpasses. One renders with no transformation and
// an opacity of 0.5, the second has a rotation. Bake the uniform data for
// both into a single buffer.
const int UNIFORM_BLOCK_SIZE = 64 + 4; // matrix + opacity
const int secondUbufOffset = rhi->ubufAligned(UNIFORM_BLOCK_SIZE);
const int UBUF_SIZE = secondUbufOffset + UNIFORM_BLOCK_SIZE;
QScopedPointer<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, UBUF_SIZE));
QVERIFY(ubuf->create());
QMatrix4x4 matrix;
updates->updateDynamicBuffer(ubuf.data(), 0, 64, matrix.constData());
float opacity = 0.5f;
updates->updateDynamicBuffer(ubuf.data(), 64, 4, &opacity);
// rotation by 45 degrees around the Z axis
matrix.rotate(45, 0, 0, 1);
updates->updateDynamicBuffer(ubuf.data(), secondUbufOffset, 64, matrix.constData());
updates->updateDynamicBuffer(ubuf.data(), secondUbufOffset + 64, 4, &opacity);
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());
const QRhiShaderResourceBinding::StageFlags commonVisibility = QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage;
QScopedPointer<QRhiShaderResourceBindings> srb0(rhi->newShaderResourceBindings());
srb0->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, commonVisibility, ubuf.data(), 0, UNIFORM_BLOCK_SIZE),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, inputTexture.data(), sampler.data())
});
QVERIFY(srb0->create());
QScopedPointer<QRhiShaderResourceBindings> srb1(rhi->newShaderResourceBindings());
srb1->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, commonVisibility, ubuf.data(), secondUbufOffset, UNIFORM_BLOCK_SIZE),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, inputTexture.data(), sampler.data())
});
QVERIFY(srb1->create());
QVERIFY(srb1->isLayoutCompatible(srb0.data())); // hence no need for a second pipeline
QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
QShader vs = loadShader(":/data/textured.vert.qsb");
QVERIFY(vs.isValid());
QShaderDescription shaderDesc = vs.description();
QVERIFY(!shaderDesc.uniformBlocks().isEmpty());
QCOMPARE(shaderDesc.uniformBlocks().first().size, UNIFORM_BLOCK_SIZE);
QShader fs = loadShader(":/data/textured.frag.qsb");
QVERIFY(fs.isValid());
shaderDesc = fs.description();
QVERIFY(!shaderDesc.uniformBlocks().isEmpty());
QCOMPARE(shaderDesc.uniformBlocks().first().size, UNIFORM_BLOCK_SIZE);
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(srb0.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 readResult0;
QImage result0;
readResult0.completed = [&readResult0, &result0] {
result0 = QImage(reinterpret_cast<const uchar *>(readResult0.data.constData()),
readResult0.pixelSize.width(), readResult0.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({ texture.data() }, &readResult0);
cb->endPass(readbackBatch);
// second pass (rotated)
cb->beginPass(rt.data(), Qt::black, { 1.0f, 0 });
cb->setGraphicsPipeline(pipeline.data());
cb->setShaderResources(srb1.data()); // sources data from a different offset in ubuf
cb->setViewport({ 0, 0, float(texture->pixelSize().width()), float(texture->pixelSize().height()) });
cb->setVertexInput(0, 1, &vbindings);
cb->draw(4);
QRhiReadbackResult readResult1;
QImage result1;
readResult1.completed = [&readResult1, &result1] {
result1 = QImage(reinterpret_cast<const uchar *>(readResult1.data.constData()),
readResult1.pixelSize.width(), readResult1.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({ texture.data() }, &readResult1);
cb->endPass(readbackBatch);
rhi->endOffscreenFrame();
QVERIFY(!result0.isNull());
QVERIFY(!result1.isNull());
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC()) {
result0.flip();
result1.flip();
}
if (impl == QRhi::Null)
return;
// 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(result0.pixel(79, 77)));
QVERIFY(checkSemiWhite(result0.pixel(124, 81)));
QVERIFY(checkSemiWhite(result0.pixel(128, 149)));
QVERIFY(checkSemiWhite(result0.pixel(120, 189)));
QVERIFY(checkSemiWhite(result0.pixel(116, 185)));
QVERIFY(checkSemiWhite(result0.pixel(191, 172)));
QRgb empty = qRgba(0, 0, 0, 0);
QCOMPARE(result0.pixel(11, 45), empty);
QCOMPARE(result0.pixel(246, 202), empty);
QCOMPARE(result0.pixel(130, 18), empty);
QCOMPARE(result0.pixel(4, 227), empty);
// also rotated 45 degrees around Z
QRgb black = qRgba(0, 0, 0, 255);
QCOMPARE(result1.pixel(20, 23), black);
QCOMPARE(result1.pixel(47, 5), black);
QCOMPARE(result1.pixel(238, 22), black);
QCOMPARE(result1.pixel(250, 203), black);
QCOMPARE(result1.pixel(224, 237), black);
QCOMPARE(result1.pixel(12, 221), black);
QVERIFY(checkSemiWhite(result1.pixel(142, 67)));
QVERIFY(checkSemiWhite(result1.pixel(81, 79)));
QVERIFY(checkSemiWhite(result1.pixel(79, 168)));
QVERIFY(checkSemiWhite(result1.pixel(146, 204)));
QVERIFY(checkSemiWhite(result1.pixel(186, 156)));
QCOMPARE(result1.pixel(204, 45), empty);
QCOMPARE(result1.pixel(28, 178), empty);
}
void tst_QRhi::renderToTextureTexturedQuadAllDynamicBuffers_data()
{
rhiTestData();
}
void tst_QRhi::renderToTextureTexturedQuadAllDynamicBuffers()
{
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);
// Do like renderToTextureTexturedQuadAndUniformBuffer but only use Dynamic
// buffers, and do updates with the direct beginFullDynamicBufferUpdate
// function. (for some backend this is different for UniformBuffer and
// others, hence useful exercising it also on a VertexBuffer)
QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::VertexBuffer, sizeof(quadVerticesUvs)));
QVERIFY(vbuf->create());
char *p = vbuf->beginFullDynamicBufferUpdateForCurrentFrame();
QVERIFY(p);
memcpy(p, quadVerticesUvs, sizeof(quadVerticesUvs));
vbuf->endFullDynamicBufferUpdateForCurrentFrame();
const int UNIFORM_BLOCK_SIZE = 64 + 4; // matrix + opacity
const int secondUbufOffset = rhi->ubufAligned(UNIFORM_BLOCK_SIZE);
const int UBUF_SIZE = secondUbufOffset + UNIFORM_BLOCK_SIZE;
QScopedPointer<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, UBUF_SIZE));
QVERIFY(ubuf->create());
p = ubuf->beginFullDynamicBufferUpdateForCurrentFrame();
QVERIFY(p);
QMatrix4x4 matrix;
memcpy(p, matrix.constData(), 64);
float opacity = 0.5f;
memcpy(p + 64, &opacity, 4);
// rotation by 45 degrees around the Z axis
matrix.rotate(45, 0, 0, 1);
memcpy(p + secondUbufOffset, matrix.constData(), 64);
memcpy(p + secondUbufOffset + 64, &opacity, 4);
ubuf->endFullDynamicBufferUpdateForCurrentFrame();
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
QScopedPointer<QRhiTexture> inputTexture(rhi->newTexture(QRhiTexture::RGBA8, inputImage.size()));
QVERIFY(inputTexture->create());
updates->uploadTexture(inputTexture.data(), inputImage);
cb->resourceUpdate(updates);
QScopedPointer<QRhiSampler> sampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
QVERIFY(sampler->create());
const QRhiShaderResourceBinding::StageFlags commonVisibility = QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage;
QScopedPointer<QRhiShaderResourceBindings> srb0(rhi->newShaderResourceBindings());
srb0->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, commonVisibility, ubuf.data(), 0, UNIFORM_BLOCK_SIZE),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, inputTexture.data(), sampler.data())
});
QVERIFY(srb0->create());
QScopedPointer<QRhiShaderResourceBindings> srb1(rhi->newShaderResourceBindings());
srb1->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, commonVisibility, ubuf.data(), secondUbufOffset, UNIFORM_BLOCK_SIZE),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, inputTexture.data(), sampler.data())
});
QVERIFY(srb1->create());
QVERIFY(srb1->isLayoutCompatible(srb0.data())); // hence no need for a second pipeline
QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
QShader vs = loadShader(":/data/textured.vert.qsb");
QVERIFY(vs.isValid());
QShaderDescription shaderDesc = vs.description();
QVERIFY(!shaderDesc.uniformBlocks().isEmpty());
QCOMPARE(shaderDesc.uniformBlocks().first().size, UNIFORM_BLOCK_SIZE);
QShader fs = loadShader(":/data/textured.frag.qsb");
QVERIFY(fs.isValid());
shaderDesc = fs.description();
QVERIFY(!shaderDesc.uniformBlocks().isEmpty());
QCOMPARE(shaderDesc.uniformBlocks().first().size, UNIFORM_BLOCK_SIZE);
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(srb0.data());
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline->create());
cb->beginPass(rt.data(), Qt::black, { 1.0f, 0 });
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 readResult0;
QImage result0;
readResult0.completed = [&readResult0, &result0] {
result0 = QImage(reinterpret_cast<const uchar *>(readResult0.data.constData()),
readResult0.pixelSize.width(), readResult0.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({ texture.data() }, &readResult0);
cb->endPass(readbackBatch);
// second pass (rotated)
cb->beginPass(rt.data(), Qt::black, { 1.0f, 0 });
cb->setGraphicsPipeline(pipeline.data());
cb->setShaderResources(srb1.data()); // sources data from a different offset in ubuf
cb->setViewport({ 0, 0, float(texture->pixelSize().width()), float(texture->pixelSize().height()) });
cb->setVertexInput(0, 1, &vbindings);
cb->draw(4);
QRhiReadbackResult readResult1;
QImage result1;
readResult1.completed = [&readResult1, &result1] {
result1 = QImage(reinterpret_cast<const uchar *>(readResult1.data.constData()),
readResult1.pixelSize.width(), readResult1.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({ texture.data() }, &readResult1);
cb->endPass(readbackBatch);
rhi->endOffscreenFrame();
QVERIFY(!result0.isNull());
QVERIFY(!result1.isNull());
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC()) {
result0.flip();
result1.flip();
}
if (impl == QRhi::Null)
return;
// 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(result0.pixel(79, 77)));
QVERIFY(checkSemiWhite(result0.pixel(124, 81)));
QVERIFY(checkSemiWhite(result0.pixel(128, 149)));
QVERIFY(checkSemiWhite(result0.pixel(120, 189)));
QVERIFY(checkSemiWhite(result0.pixel(116, 185)));
QVERIFY(checkSemiWhite(result0.pixel(191, 172)));
QRgb empty = qRgba(0, 0, 0, 0);
QCOMPARE(result0.pixel(11, 45), empty);
QCOMPARE(result0.pixel(246, 202), empty);
QCOMPARE(result0.pixel(130, 18), empty);
QCOMPARE(result0.pixel(4, 227), empty);
// also rotated 45 degrees around Z
QRgb black = qRgba(0, 0, 0, 255);
QCOMPARE(result1.pixel(20, 23), black);
QCOMPARE(result1.pixel(47, 5), black);
QCOMPARE(result1.pixel(238, 22), black);
QCOMPARE(result1.pixel(250, 203), black);
QCOMPARE(result1.pixel(224, 237), black);
QCOMPARE(result1.pixel(12, 221), black);
QVERIFY(checkSemiWhite(result1.pixel(142, 67)));
QVERIFY(checkSemiWhite(result1.pixel(81, 79)));
QVERIFY(checkSemiWhite(result1.pixel(79, 168)));
QVERIFY(checkSemiWhite(result1.pixel(146, 204)));
QVERIFY(checkSemiWhite(result1.pixel(186, 156)));
QCOMPARE(result1.pixel(204, 45), empty);
QCOMPARE(result1.pixel(28, 178), empty);
}
void tst_QRhi::renderToTextureDeferredSrb_data()
{
rhiTestData();
}
void tst_QRhi::renderToTextureDeferredSrb()
{
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<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);
// this is the specific thing to test here: an srb with null resources
const QRhiShaderResourceBinding::StageFlags commonVisibility = QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage;
QScopedPointer<QRhiShaderResourceBindings> layoutOnlySrb(rhi->newShaderResourceBindings());
layoutOnlySrb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, commonVisibility, nullptr),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, nullptr, nullptr)
});
QVERIFY(layoutOnlySrb->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(layoutOnlySrb.data()); // no resources needed yet
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline->create());
// another, layout compatible, srb with the actual resources
QScopedPointer<QRhiShaderResourceBindings> layoutCompatibleSrbWithResources(rhi->newShaderResourceBindings());
layoutCompatibleSrbWithResources->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, commonVisibility, ubuf.data()),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, inputTexture.data(), sampler.data())
});
QVERIFY(layoutCompatibleSrbWithResources->create());
cb->beginPass(rt.data(), Qt::black, { 1.0f, 0 }, updates);
cb->setGraphicsPipeline(pipeline.data());
cb->setShaderResources(layoutCompatibleSrbWithResources.data()); // here we must use the srb referencing the resources
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.flip();
// 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::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.flip();
// 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();
}
void tst_QRhi::renderToTextureMultipleUniformBuffersAndDynamicOffset()
{
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());
const int MATRIX_COUNT = 4; // put 4 mat4s into the buffer, will only use one
const int ubufElemSize = rhi->ubufAligned(64);
QCOMPARE_GE(ubufElemSize, 64);
QScopedPointer<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, MATRIX_COUNT * ubufElemSize));
QVERIFY(ubuf->create());
float zeroes[16];
memset(zeroes, 0, sizeof(zeroes));
updates->updateDynamicBuffer(ubuf.data(), 0, 64, zeroes);
updates->updateDynamicBuffer(ubuf.data(), ubufElemSize, 64, zeroes);
// the only correct matrix is the third one
QMatrix4x4 matrix;
updates->updateDynamicBuffer(ubuf.data(), ubufElemSize * 2, 64, matrix.constData());
updates->updateDynamicBuffer(ubuf.data(), ubufElemSize * 3, 64, zeroes);
const int OPACITY_COUNT = 6; // put 6 floats into the buffer, will only use one
const int ubuf2ElemSize = rhi->ubufAligned(4);
QCOMPARE_GE(ubuf2ElemSize, 4);
QScopedPointer<QRhiBuffer> ubuf2(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, OPACITY_COUNT * ubuf2ElemSize));
QVERIFY(ubuf2->create());
updates->updateDynamicBuffer(ubuf2.data(), 0, 4, &zeroes[0]);
updates->updateDynamicBuffer(ubuf2.data(), ubuf2ElemSize, 4, &zeroes[0]);
updates->updateDynamicBuffer(ubuf2.data(), ubuf2ElemSize * 2, 4, &zeroes[0]);
// the only correct opacity value is the fourth one
float opacity = 0.5f;
updates->updateDynamicBuffer(ubuf2.data(), ubuf2ElemSize * 3, 4, &opacity);
updates->updateDynamicBuffer(ubuf2.data(), ubuf2ElemSize * 4, 4, &zeroes[0]);
updates->updateDynamicBuffer(ubuf2.data(), ubuf2ElemSize * 5, 4, &zeroes[0]);
const QRhiShaderResourceBinding::StageFlags commonVisibility = QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage;
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({
QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, commonVisibility, ubuf.data(), 64),
QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(1, commonVisibility, ubuf2.data(), 4),
QRhiShaderResourceBinding::sampledTexture(2, QRhiShaderResourceBinding::FragmentStage, inputTexture.data(), sampler.data())
});
QVERIFY(srb->create());
QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
QShader vs = loadShader(":/data/textured_multiubuf.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(":/data/textured_multiubuf.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());
// Now the magic, expose the 3rd matrix and 4th opacity value to the shader.
// If the handling of dynamic offsets were broken, the shaders would likely
// "see" an all zero matrix and zero opacity, thus leading to different
// rendering output. This way we can verify if using dynamic offsets, and
// more than one at the same time, is functional.
QVarLengthArray<std::pair<int, quint32>, 2> dynamicOffset = {
{ 0, quint32(ubufElemSize * 2) },
{ 1, quint32(ubuf2ElemSize * 3) },
};
cb->setShaderResources(srb.data(), 2, dynamicOffset.constData());
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.flip();
// 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::renderToTextureSrbReuse_data()
{
rhiTestData();
}
void tst_QRhi::renderToTextureSrbReuse()
{
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");
// Draw a textured quad with opacity 0.5. The difference to the simple tests
// of the same kind is that there are two (configuration-wise identical)
// pipeline objects that are bound after each other, with the same one srb,
// on the command buffer. This exercises, in particular for the OpenGL
// backend, that the uniforms are set for the pipelines' underlying shader
// program correctly. (with OpenGL we may not use real uniform buffers,
// which presents extra pipeline-srb tracking work for the backend)
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<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(), sampler.data())
});
QVERIFY(srb->create());
QScopedPointer<QRhiGraphicsPipeline> pipeline1(rhi->newGraphicsPipeline());
pipeline1->setTopology(QRhiGraphicsPipeline::TriangleStrip);
QShader vs = loadShader(":/data/textured.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(":/data/textured.frag.qsb");
QVERIFY(fs.isValid());
pipeline1->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) }
});
pipeline1->setVertexInputLayout(inputLayout);
pipeline1->setShaderResourceBindings(srb.data());
pipeline1->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline1->create());
QScopedPointer<QRhiGraphicsPipeline> pipeline2(rhi->newGraphicsPipeline());
pipeline2->setTopology(QRhiGraphicsPipeline::TriangleStrip);
pipeline2->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
pipeline2->setVertexInputLayout(inputLayout);
pipeline2->setShaderResourceBindings(srb.data());
pipeline2->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline2->create());
cb->beginPass(rt.data(), Qt::black, { 1.0f, 0 }, updates);
// The key step in this test: set the 1st pipeline, then the 2nd, the
// srb is the same. This should lead to identical results to just
// binding one of them.
cb->setGraphicsPipeline(pipeline1.data());
cb->setShaderResources(srb.data());
cb->setGraphicsPipeline(pipeline2.data());
cb->setShaderResources(srb.data());
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.flip();
// 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::setWindowType(QWindow *window, QRhi::Implementation impl)
{
switch (impl) {
#ifdef TST_GL
case QRhi::OpenGLES2:
window->setSurfaceType(QSurface::OpenGLSurface);
break;
#endif
case QRhi::D3D11:
window->setSurfaceType(QSurface::Direct3DSurface);
break;
case QRhi::Metal:
window->setSurfaceType(QSurface::MetalSurface);
break;
#ifdef TST_VK
case QRhi::Vulkan:
window->setSurfaceType(QSurface::VulkanSurface);
window->setVulkanInstance(&vulkanInstance);
break;
#endif
default:
break;
}
}
void tst_QRhi::renderToTextureIndexedDraw_data()
{
rhiTestData();
}
void tst_QRhi::renderToTextureIndexedDraw()
{
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");
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();
static const quint16 indices[] = {
0, 1, 2
};
QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices)));
QVERIFY(vbuf->create());
updates->uploadStaticBuffer(vbuf.data(), triangleVertices);
QScopedPointer<QRhiBuffer> ibuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(indices)));
QVERIFY(ibuf->create());
updates->uploadStaticBuffer(ibuf.data(), indices);
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->create());
QScopedPointer<QRhiGraphicsPipeline> pipeline(createSimplePipeline(rhi.data(), srb.data(), rpDesc.data()));
QVERIFY(pipeline);
QRhiCommandBuffer::VertexInput vbindings(vbuf.data(), 0);
// Do three render passes, even though all render the same thing. This is done to
// verify that QTBUG-89765 is fixed. One of them specifies ExternalContent which
// triggers special behavior with some backends (uses a secondary command buffer with
// Vulkan for example). This way we can see that optimizations, such as keeping track
// of what index buffer is active, are handled correctly across pass boundaries in the
// QRhi backends. Without the fix for QTBUG-89765 this test would show validation
// warnings and even crash when run with Vulkan.
cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 }, updates);
cb->setGraphicsPipeline(pipeline.data());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
cb->setVertexInput(0, 1, &vbindings, ibuf.data(), 0, QRhiCommandBuffer::IndexUInt16);
cb->drawIndexed(3);
cb->endPass();
cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 }, nullptr, QRhiCommandBuffer::ExternalContent);
cb->setGraphicsPipeline(pipeline.data());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
cb->setVertexInput(0, 1, &vbindings, ibuf.data(), 0, QRhiCommandBuffer::IndexUInt16);
cb->drawIndexed(3);
cb->endPass();
cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 }, nullptr);
cb->setGraphicsPipeline(pipeline.data());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
cb->setVertexInput(0, 1, &vbindings, ibuf.data(), 0, QRhiCommandBuffer::IndexUInt16);
cb->drawIndexed(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);
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({ texture.data() }, &readResult);
cb->endPass(readbackBatch);
rhi->endOffscreenFrame();
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());
if (rhi->isYUpInFramebuffer() == rhi->isYUpInNDC())
QCOMPARE_LT(redCount, blueCount);
else
QCOMPARE_GT(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 (qgetenv("XDG_CURRENT_DESKTOP").toLower().contains("ubuntu") && QSysInfo::productVersion().startsWith("24"))
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");
if (QSysInfo::productType() == "opensuse-leap" && QSysInfo::productVersion() == QLatin1String("16.0") && rhi->backend() == QRhi::OpenGLES2)
QSKIP("QTBUG-141769: opensuse-leap 16.0 fails with OpenGL, 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());
// Some Vulkan drivers lack D24S8, so fallback to D32FS8.
const QRhiTexture::Format dsFormat = rhi->isTextureFormatSupported(QRhiTexture::D24S8)
? QRhiTexture::D24S8
: QRhiTexture::D32FS8;
QScopedPointer<QRhiTexture> ds(rhi->newTextureArray(dsFormat, 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.flip();
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.flip();
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;
}
QCOMPARE_GE(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;
}
QCOMPARE_GE(n, 10);
}
}
void tst_QRhi::renderToTextureScissorChange_data()
{
rhiTestData();
}
void tst_QRhi::renderToTextureScissorChange()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
std::unique_ptr<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
if (!rhi)
QSKIP("QRhi could not be created, skipping testing rendering");
#ifdef TST_GL
if (impl == QRhi::OpenGLES2) {
rhi->makeThreadLocalNativeContextCurrent();
QOpenGLContext *ctx = QOpenGLContext::currentContext();
// because of gl_VertexId in the shader, which is added in GLSL 130 and GLSL ES 300
if (!ctx || ctx->format().majorVersion() < 3)
QSKIP("This test needs at least OpenGL 3.0 or OpenGL ES 3.0, skipping on OpenGL");
}
#endif
const QSize outputSize(1920, 1080);
std::unique_ptr<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 16));
QVERIFY(ubuf->create());
QVector4D color = { 1.0f, 0.0f, 0.0f, 1.0f };
updates->updateDynamicBuffer(ubuf.get(), 0, 16, &color);
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::FragmentStage, ubuf.get()),
});
QVERIFY(srb->create());
auto setupPipeline = [&srb, &rpDesc](QRhiGraphicsPipeline *pipeline) {
QShader vs = loadShader(":/data/fullscreenquad.vert.qsb");
QShader fs = loadShader(":/data/fullscreenquad_color.frag.qsb");
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
pipeline->setShaderResourceBindings(srb.get());
pipeline->setRenderPassDescriptor(rpDesc.get());
};
std::unique_ptr<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
setupPipeline(pipeline.get());
QVERIFY(pipeline->create());
std::unique_ptr<QRhiGraphicsPipeline> scissoringPipeline(rhi->newGraphicsPipeline());
setupPipeline(scissoringPipeline.get());
scissoringPipeline->setFlags(QRhiGraphicsPipeline::UsesScissor);
QVERIFY(scissoringPipeline->create());
cb->beginPass(rt.get(), Qt::white, { 1.0f, 0 }, updates);
// fullscreen quad, scissored to (bottom-left) 100x100 - 300x300
cb->setGraphicsPipeline(scissoringPipeline.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
cb->setShaderResources();
cb->setScissor({ 100, 100, 200, 200 });
cb->draw(3);
cb->endPass();
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.get() }, &readResult);
cb->resourceUpdate(readbackBatch);
rhi->finish();
if (rhi->isYUpInFramebuffer())
result = result.flipped();
if (rhi->backend() != QRhi::Null) {
// 200x200 red rect at (bottom left) 100,100
int y = outputSize.height() - 1 - 150;
int redCount = 0;
for (int x = 100; x < 100 + 300; ++x) {
QRgb c = result.pixel(x, y);
if (qRed(c) >= 254 && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
}
QCOMPARE(redCount, 200);
// should be white outside the scissor rect
y = outputSize.height() - 50;
redCount = 0;
for (int x = 100; x < 100 + 300; ++x) {
QRgb c = result.pixel(x, y);
if (qRed(c) >= 254 && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
}
QCOMPARE(redCount, 0);
}
cb->beginPass(rt.get(), Qt::white, { 1.0f, 0 });
// fullscreen quad, scissored to (bottom-left) 100x100 - 300x300
cb->setGraphicsPipeline(scissoringPipeline.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
cb->setShaderResources();
cb->setScissor({ 100, 100, 200, 200 });
cb->draw(3);
// switch to the non-UsesScissor pipeline, the end result should be the whole textured filled with red,
// the scissoring is expected to be "disabled" (or made to match the viewport) automatically
cb->setGraphicsPipeline(pipeline.get());
cb->setShaderResources();
cb->draw(3);
cb->endPass();
readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({ texture.get() }, &readResult);
cb->resourceUpdate(readbackBatch);
rhi->finish();
if (rhi->backend() != QRhi::Null) {
const int y = outputSize.height() - 1 - 150;
// no scissoring, everything is supposed to be red
int nonRedCount = 0;
for (int x = 100; x < 100 + 300; ++x) {
QRgb c = result.pixel(x, y);
if (!(qRed(c) >= 254 && qGreen(c) == 0 && qBlue(c) == 0))
++nonRedCount;
}
QCOMPARE(nonRedCount, 0);
}
rhi->endOffscreenFrame();
}
void tst_QRhi::renderToTextureAllUniformTypes_data()
{
rhiTestData();
}
// covers the whole viewport when used with simple.vert
static const float fullscreenTriangleVertices[] = {
-1.0f, -1.0f,
3.0f, -1.0f,
-1.0f, 3.0f
};
void tst_QRhi::renderToTextureAllUniformTypes()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing rendering");
// Sets *result to 0 when all uniforms had the expected value, or to the
// 1-based index of the first failing check in the shader. Left untouched
// when something goes wrong before that.
// (there is no coverage for bool, bvec2, bvec3 and bvec4 because SPIR-V
// does not allow booleans in uniform blocks; such members come out of the
// shader conditioning pipeline as uint and uvecN)
auto runCase = [rhi, impl](const char *fragShaderName, const QByteArray &ubufData, int *result)
{
const QSize outputSize(64, 64);
std::unique_ptr<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer,
sizeof(fullscreenTriangleVertices)));
QVERIFY(vbuf->create());
updates->uploadStaticBuffer(vbuf.get(), fullscreenTriangleVertices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer,
ubufData.size()));
QVERIFY(ubuf->create());
updates->updateDynamicBuffer(ubuf.get(), 0, ubufData.size(), ubufData.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::FragmentStage, ubuf.get())
});
QVERIFY(srb->create());
QShader vs = loadShader(":/data/simple.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(fragShaderName);
QVERIFY(fs.isValid());
std::unique_ptr<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
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.get());
pipeline->setRenderPassDescriptor(rpDesc.get());
QVERIFY(pipeline->create());
cb->beginPass(rt.get(), Qt::white, { 1.0f, 0 }, updates);
cb->setGraphicsPipeline(pipeline.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
cb->setShaderResources();
QRhiCommandBuffer::VertexInput vbindings(vbuf.get(), 0);
cb->setVertexInput(0, 1, &vbindings);
cb->draw(3);
QRhiReadbackResult readResult;
QImage image;
readResult.completed = [&readResult, &image] {
image = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
readResult.pixelSize.width(), readResult.pixelSize.height(),
QImage::Format_RGBA8888);
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({ texture.get() }, &readResult);
cb->endPass(readbackBatch);
frameGuard.endFrame();
if (impl == QRhi::Null) {
*result = 0;
return;
}
QCOMPARE(image.size(), outputSize);
const QRgb c = image.pixel(outputSize.width() / 2, outputSize.height() / 2);
QCOMPARE(qGreen(c), 0);
QCOMPARE(qBlue(c), 0);
*result = qRed(c);
};
QByteArray ubufData;
float nextFloat = 1.5f;
qint32 nextInt = -1;
quint32 nextUint = 1;
auto putFloats = [&ubufData, &nextFloat](int offset, int count) {
for (int i = 0; i < count; ++i, nextFloat += 1.0f)
memcpy(ubufData.data() + offset + i * 4, &nextFloat, 4);
};
auto putInts = [&ubufData, &nextInt](int offset, int count) {
for (int i = 0; i < count; ++i, --nextInt)
memcpy(ubufData.data() + offset + i * 4, &nextInt, 4);
};
auto putUints = [&ubufData, &nextUint](int offset, int count) {
for (int i = 0; i < count; ++i, ++nextUint)
memcpy(ubufData.data() + offset + i * 4, &nextUint, 4);
};
// matrices are column-major with every column padded to 16 bytes
auto putMatrix = [&putFloats](int offset, int cols, int rows) {
for (int c = 0; c < cols; ++c)
putFloats(offset + c * 16, rows);
};
// elements of an array of scalars or vectors are padded to 16 bytes
auto putArray = [](auto put, int offset, int comps, int elemCount) {
for (int e = 0; e < elemCount; ++e)
put(offset + e * 16, comps);
};
auto putMatrixArray = [&putMatrix](int offset, int cols, int rows, int elemCount) {
for (int e = 0; e < elemCount; ++e)
putMatrix(offset + e * cols * 16, cols, rows);
};
ubufData = QByteArray(784, 0);
putFloats(0, 1); // float f
putFloats(8, 2); // vec2 v2
putFloats(16, 3); // vec3 v3
putFloats(32, 4); // vec4 v4
putMatrix(96, 2, 2); // mat2 m2
putMatrix(128, 3, 3); // mat3 m3
putMatrix(176, 4, 4); // mat4 m4
putArray(putFloats, 240, 1, 2); // float fArr[2]
putArray(putFloats, 272, 2, 2); // vec2 v2Arr[2]
putArray(putFloats, 304, 3, 2); // vec3 v3Arr[2]
putArray(putFloats, 336, 4, 2); // vec4 v4Arr[2]
putMatrixArray(496, 2, 2, 2); // mat2 m2Arr[2]
putMatrixArray(560, 3, 3, 2); // mat3 m3Arr[2]
putMatrixArray(656, 4, 4, 2); // mat4 m4Arr[2]
putInts(48, 1); // int i1
putInts(56, 2); // ivec2 i2
putInts(64, 3); // ivec3 i3
putInts(80, 4); // ivec4 i4
putArray(putInts, 368, 1, 2); // int i1Arr[2]
putArray(putInts, 400, 2, 2); // ivec2 i2Arr[2]
putArray(putInts, 432, 3, 2); // ivec3 i3Arr[2]
putArray(putInts, 464, 4, 2); // ivec4 i4Arr[2]
int failedCheck = -1;
runCase(":/data/uniformtypes.frag.qsb", ubufData, &failedCheck);
if (QTest::currentTestFailed())
return;
if (failedCheck != 0)
qWarning("Check %d in uniformtypes.frag failed", failedCheck);
QCOMPARE(failedCheck, 0);
#ifdef TST_GL
if (impl == QRhi::OpenGLES2) {
rhi->makeThreadLocalNativeContextCurrent();
QOpenGLContext *ctx = QOpenGLContext::currentContext();
// uint and uvecN need GLSL 1.30 / 3.00 es and glUniform*ui()
if (!ctx || ctx->format().majorVersion() < 3)
QSKIP("Unsigned integer uniforms need at least OpenGL 3.0 or OpenGL ES 3.0, skipping the rest");
}
#endif
ubufData = QByteArray(176, 0);
putUints(0, 1); // uint u1
putUints(8, 2); // uvec2 u2
putUints(16, 3); // uvec3 u3
putUints(32, 4); // uvec4 u4
putArray(putUints, 48, 1, 2); // uint u1Arr[2]
putArray(putUints, 80, 2, 2); // uvec2 u2Arr[2]
putArray(putUints, 112, 3, 2); // uvec3 u3Arr[2]
putArray(putUints, 144, 4, 2); // uvec4 u4Arr[2]
failedCheck = -1;
runCase(":/data/uniformtypes_uint.frag.qsb", ubufData, &failedCheck);
if (QTest::currentTestFailed())
return;
if (failedCheck != 0)
qWarning("Check %d in uniformtypes_uint.frag failed", failedCheck);
QCOMPARE(failedCheck, 0);
}
void tst_QRhi::renderToWindowSimple_data()
{
rhiTestData();
}
void tst_QRhi::renderToWindowSimple()
{
if (QGuiApplication::platformName().startsWith(QLatin1String("offscreen"), Qt::CaseInsensitive))
QSKIP("Offscreen: This fails.");
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");
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());
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);
const int asyncReadbackFrames = rhi->resourceLimit(QRhi::MaxAsyncReadbackFrames);
// one frame issues the readback, then we do MaxAsyncReadbackFrames more to ensure the readback completes
const int FRAME_COUNT = asyncReadbackFrames + 1;
bool readCompleted = false;
QRhiReadbackResult readResult;
QImage result;
int readbackWidth = 0;
bool readCompletedPartial = false;
QRhiReadbackResult readResultPartial;
QImage resultPartial;
for (int frameNo = 0; frameNo < FRAME_COUNT; ++frameNo) {
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()));
cb->beginPass(rt, Qt::blue, { 1.0f, 0 }, updates);
updates = nullptr;
cb->setGraphicsPipeline(pipeline.data());
cb->setViewport(viewport);
QRhiCommandBuffer::VertexInput vbindings(vbuf.data(), 0);
cb->setVertexInput(0, 1, &vbindings);
cb->draw(3);
if (frameNo == 0) {
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readResult.completed = [&readCompleted, &readResult, &result, &rhi] {
readCompleted = true;
QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
readResult.pixelSize.width(), readResult.pixelSize.height(),
QImage::Format_ARGB32_Premultiplied);
if (readResult.format == QRhiTexture::RGBA8)
wrapperImage = wrapperImage.rgbSwapped();
if (rhi->isYUpInFramebuffer() == rhi->isYUpInNDC())
result = wrapperImage.flipped();
else
result = wrapperImage.copy();
};
QRhiReadbackDescription readbackDescription;
QVERIFY(!readbackDescription.rect().isValid());
readbackBatch->readBackTexture(readbackDescription, &readResult); // read back the current backbuffer
readbackWidth = outputSize.width();
readResultPartial.completed = [&readCompletedPartial, &readResultPartial, &resultPartial, &rhi] {
readCompletedPartial = true;
QImage wrapperImage(reinterpret_cast<const uchar *>(readResultPartial.data.constData()),
readResultPartial.pixelSize.width(), readResultPartial.pixelSize.height(),
QImage::Format_ARGB32_Premultiplied);
if (readResultPartial.format == QRhiTexture::RGBA8)
wrapperImage = wrapperImage.rgbSwapped();
if (rhi->isYUpInFramebuffer() == rhi->isYUpInNDC())
resultPartial = wrapperImage.flipped();
else
resultPartial = wrapperImage.copy();
};
QRhiReadbackDescription partialReadbackDescription;
partialReadbackDescription.setRect({100, 100, 1, 1});
QVERIFY(partialReadbackDescription.rect().isValid());
readbackBatch->readBackTexture(partialReadbackDescription, &readResultPartial); // read back one pixel at 100,100 of the current backbuffer
cb->endPass(readbackBatch);
} else {
cb->endPass();
}
rhi->endFrame(swapChain.data());
}
// The readback is asynchronous here. However it is guaranteed that it
// finished at latest after rendering QRhi::MaxAsyncReadbackFrames frames
// after the one that enqueues the readback.
QVERIFY(readCompleted);
QVERIFY(readbackWidth > 0);
if (impl == QRhi::Null)
return;
// Now we have a red rectangle on blue background.
const int y = 50;
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, readbackWidth);
QCOMPARE_LT(redCount, blueCount);
// Verify the backbuffer single-pixel readback
QVERIFY(readCompletedPartial);
QCOMPARE(readResultPartial.pixelSize, QSize(1, 1));
if (rhi->isYUpInFramebuffer() == rhi->isYUpInNDC())
result.flip();
QCOMPARE(resultPartial.pixelColor(0, 0), result.pixelColor(100, 100));
}
void tst_QRhi::renderToTextureSameSrbDifferentShaders_data()
{
rhiTestData();
}
void tst_QRhi::renderToTextureSameSrbDifferentShaders()
{
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");
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());
QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices)));
QVERIFY(vbuf->create());
QScopedPointer<QRhiBuffer> ubuf1(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 4));
QVERIFY(ubuf1->create());
QScopedPointer<QRhiBuffer> ubuf2(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 4));
QVERIFY(ubuf2->create());
// one srb, suitable for both vertex shaders (the samesrb_1.vert does not use the buffer at binding 2, that's fine)
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(1, QRhiShaderResourceBinding::VertexStage, ubuf1.data()),
QRhiShaderResourceBinding::uniformBuffer(2, QRhiShaderResourceBinding::VertexStage, ubuf2.data()),
});
QVERIFY(srb->create());
QScopedPointer<QRhiGraphicsPipeline> pipeline1(rhi->newGraphicsPipeline());
QShader vs = loadShader(":/data/samesrb_1.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(":/data/samesrb.frag.qsb");
QVERIFY(fs.isValid());
pipeline1->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({ { 2 * sizeof(float) } });
inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
pipeline1->setVertexInputLayout(inputLayout);
pipeline1->setShaderResourceBindings(srb.data());
pipeline1->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline1->create());
QScopedPointer<QRhiGraphicsPipeline> pipeline2(rhi->newGraphicsPipeline());
vs = loadShader(":/data/samesrb_2.vert.qsb");
QVERIFY(vs.isValid());
pipeline2->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
pipeline2->setVertexInputLayout(inputLayout);
pipeline2->setShaderResourceBindings(srb.data());
pipeline2->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline2->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
u->uploadStaticBuffer(vbuf.data(), triangleVertices);
float color1 = 1.0;
float color2 = 0.5;
u->updateDynamicBuffer(ubuf1.data(), 0, 4, &color1);
u->updateDynamicBuffer(ubuf2.data(), 0, 4, &color2);
cb->resourceUpdate(u);
cb->beginPass(rt.data(), Qt::black, { 1.0f, 0 });
for (int i = 0; i < 4; ++i) {
cb->setGraphicsPipeline(pipeline1.data());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
cb->setShaderResources(srb.data());
QRhiCommandBuffer::VertexInput vbindings(vbuf.data(), 0);
cb->setVertexInput(0, 1, &vbindings);
// triangle, color (1, 1, 1)
cb->draw(3);
// another pipeline, different vertex shader, same srb
cb->setGraphicsPipeline(pipeline2.data());
cb->setShaderResources(srb.data());
// triangle, color (0.5, 0.5, 0.5) (overwriting the previous one)
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);
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({ texture.data() }, &readResult);
cb->endPass(readbackBatch);
rhi->endOffscreenFrame();
QCOMPARE(result.size(), texture->pixelSize());
if (impl == QRhi::Null)
return;
QImage image = result;
if (rhi->isYUpInFramebuffer())
image = result.flipped();
// before the fix for QTBUG-140795 D3D11 (and 12 in fact) does not render
// correctly, and instead of getting a triangle with color (0.5, 0.5, 0.5),
// it's just not there; should be with the patch in place, with all backends
const int maxFuzz = 4;
QRgb c = image.pixel(result.width() / 2, result.height() / 2);
QCOMPARE_LE(qAbs(qRed(c) - 127), maxFuzz);
QCOMPARE_LE(qAbs(qGreen(c) - 127), maxFuzz);
QCOMPARE_LE(qAbs(qBlue(c) - 127), maxFuzz);
}
void tst_QRhi::continuousReadbackFromWindow_data()
{
rhiTestData();
}
void tst_QRhi::continuousReadbackFromWindow()
{
if (QGuiApplication::platformName().startsWith(QLatin1String("offscreen"), Qt::CaseInsensitive))
QSKIP("Offscreen: This fails.");
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");
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());
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);
const int asyncReadbackFrames = rhi->resourceLimit(QRhi::MaxAsyncReadbackFrames);
const int FRAME_COUNT = asyncReadbackFrames * 10;
QVector<QRhiReadbackResult> readResults(FRAME_COUNT);
int readbackCompletedCount = 0;
for (int frameNo = 0; frameNo < FRAME_COUNT; ++frameNo) {
QVERIFY(rhi->beginFrame(swapChain.data()) == QRhi::FrameOpSuccess);
QRhiCommandBuffer *cb = swapChain->currentFrameCommandBuffer();
QRhiRenderTarget *rt = swapChain->currentFrameRenderTarget();
const QSize outputSize = swapChain->currentPixelSize();
QRhiViewport viewport(0, 0, float(outputSize.width()), float(outputSize.height()));
cb->beginPass(rt, Qt::blue, { 1.0f, 0 }, updates);
updates = nullptr;
cb->setGraphicsPipeline(pipeline.data());
cb->setViewport(viewport);
QRhiCommandBuffer::VertexInput vbindings(vbuf.data(), 0);
cb->setVertexInput(0, 1, &vbindings);
cb->draw(3);
readResults[frameNo].completed = [&readbackCompletedCount] {
readbackCompletedCount += 1;
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({}, &readResults[frameNo]); // read back the current backbuffer
cb->endPass(readbackBatch);
rhi->endFrame(swapChain.data());
}
QVERIFY(readbackCompletedCount >= FRAME_COUNT - asyncReadbackFrames);
rhi->finish();
QCOMPARE(readbackCompletedCount, FRAME_COUNT);
}
void tst_QRhi::finishWithinSwapchainFrame_data()
{
rhiTestData();
}
void tst_QRhi::finishWithinSwapchainFrame()
{
if (QGuiApplication::platformName().startsWith(QLatin1String("offscreen"), Qt::CaseInsensitive))
QSKIP("Offscreen: This fails.");
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");
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());
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->create());
QScopedPointer<QRhiGraphicsPipeline> pipeline(createSimplePipeline(rhi.data(), srb.data(), rpDesc.data()));
QVERIFY(pipeline);
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()
QVERIFY(rhi->beginFrame(swapChain.data()) == QRhi::FrameOpSuccess);
QRhiCommandBuffer *cb = swapChain->currentFrameCommandBuffer();
QRhiRenderTarget *rt = swapChain->currentFrameRenderTarget();
const QSize outputSize = swapChain->currentPixelSize();
// repeat a sequence of upload, renderpass, readback, finish a number of
// times within the same frame
for (int i = 0; i < 5; ++i) {
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
updates->uploadStaticBuffer(vbuf.data(), triangleVertices);
cb->beginPass(rt, Qt::blue, { 1.0f, 0 }, updates, QRhiCommandBuffer::ExternalContent);
// just have some commands, do not bother with draw calls
cb->setGraphicsPipeline(pipeline.data());
QRhiViewport viewport(0, 0, float(outputSize.width()), float(outputSize.height()));
cb->setViewport(viewport);
// do a dummy begin/endExternal round: interesting for Vulkan because
// there this may start end then submit a secondary command buffer
cb->beginExternal();
cb->endExternal();
cb->endPass();
QRhiReadbackResult readResult;
bool ok = false;
readResult.completed = [&readResult, &ok, impl] {
QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
readResult.pixelSize.width(), readResult.pixelSize.height(),
QImage::Format_ARGB32_Premultiplied);
if (readResult.format == QRhiTexture::RGBA8)
wrapperImage = wrapperImage.rgbSwapped();
if (impl != QRhi::Null)
ok = qBlue(wrapperImage.pixel(43, 89)) > 250;
else
ok = true; // the Null backend does not actually render
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({}, &readResult); // read back the current backbuffer
cb->resourceUpdate(readbackBatch);
// force submit what we have so far, wait for the queue, and then start
// a new primary command buffer
rhi->finish();
QVERIFY(ok);
}
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::renderToMRTPerRenderTargetBlending_data()
{
rhiTestData();
}
static QRhiGraphicsPipeline *createMRTBLPipeline(QRhi *rhi, QRhiShaderResourceBindings *srb, QRhiRenderPassDescriptor *rpDesc, QRhiGraphicsPipeline::TargetBlend blend[2])
{
std::unique_ptr<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
QShader vs = loadShader(":/data/mrtbl.vert.qsb");
if (!vs.isValid())
return nullptr;
QShader fs = loadShader(":/data/mrtbl.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);
pipeline->setTargetBlends({blend[0], blend[1]});
return pipeline->create() ? pipeline.release() : nullptr;
}
void tst_QRhi::renderToMRTPerRenderTargetBlending()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing rendering");
if (!rhi->isFeatureSupported(QRhi::PerRenderTargetBlending))
QSKIP("QRhi::PerRenderTargetBlending not supported, skipping testing rendering");
const QSize outputSize(1920, 1080);
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
QScopedPointer<QRhiTexture> texture2(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture2->create());
QRhiTextureRenderTargetDescription desc;
desc.setColorAttachments({{texture.data()}, {texture2.data()}});
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(desc));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
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());
QRhiGraphicsPipeline::TargetBlend blend[2];
blend[0].enable = true; // Use src-over for first rt
blend[0].srcColor = QRhiGraphicsPipeline::One;
blend[0].dstColor = QRhiGraphicsPipeline::Zero;
blend[0].srcAlpha = QRhiGraphicsPipeline::One;
blend[0].dstAlpha = QRhiGraphicsPipeline::Zero;
blend[1].enable = true; // Use custom blending second rt
blend[1].srcColor = QRhiGraphicsPipeline::Zero;
blend[1].dstColor = QRhiGraphicsPipeline::OneMinusSrcColor;
blend[1].srcAlpha = QRhiGraphicsPipeline::One;
blend[1].dstAlpha = QRhiGraphicsPipeline::Zero;
QScopedPointer<QRhiGraphicsPipeline> pipeline(createMRTBLPipeline(rhi, srb.data(), rpDesc.data(), blend));
QVERIFY(pipeline);
cb->beginPass(rt.data(), Qt::white, { 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 readResult1;
QImage result1;
readResult1.completed = [&readResult1, &result1] {
result1 = QImage(reinterpret_cast<const uchar *>(readResult1.data.constData()),
readResult1.pixelSize.width(), readResult1.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied); // non-owning, no copy needed because readResult outlives result
};
QRhiReadbackResult readResult2;
QImage result2;
readResult2.completed = [&readResult2, &result2] {
result2 = QImage(reinterpret_cast<const uchar *>(readResult2.data.constData()),
readResult2.pixelSize.width(), readResult2.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied); // non-owning, no copy needed because readResult outlives result
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({ texture.data() }, &readResult1);
readbackBatch->readBackTexture({ texture2.data() }, &readResult2);
cb->endPass(readbackBatch);
frameGuard.endFrame();
QCOMPARE(result1.size(), texture->pixelSize());
QCOMPARE(result2.size(), texture2->pixelSize());
if (impl == QRhi::Null)
return;
// Now we have a red triangle in result1 and an 'electric blue' triangle in the result2
// which are complementary colors. If we sum them up they should return all white.
int y = result1.size().height() / 2;
const quint32 *p1 = reinterpret_cast<const quint32 *>(result1.constScanLine(y));
const quint32 *p2 = reinterpret_cast<const quint32 *>(result2.constScanLine(y));
const int width = result1.size().width();
for (int i = 0; i < width; i++) {
QRgb c1(*p1++);
QRgb c2(*p2++);
const bool c1white = (qAbs(qRed(c1) - 255) <= 1 && qAbs(qGreen(c1) - 255) <= 1 && qAbs(qBlue(c1) - 255) <= 1);
const bool c2white = (qAbs(qRed(c2) - 255) <= 1 && qAbs(qGreen(c2) - 255) <= 1 && qAbs(qBlue(c2) - 255) <= 1);
// skip if both pixels are white
if (c1white && c2white)
continue;
// remove the chance that either color accounts
// for all the value in the color sum
if (c1white || c2white)
QFAIL("If both colors are not white then neither can be white.");
if (qAbs((qRed(c1) + qRed(c2) - 255)) > 1
|| qAbs((qGreen(c1) + qGreen(c2) - 255)) > 1
|| qAbs((qBlue(c1) + qBlue(c2) - 255)) > 1) {
QFAIL("Colors in resulting images are not complementary");
}
}
}
struct IndirectTestVertex {
float x, y, z;
float r, g, b, a;
};
static constexpr IndirectTestVertex IndirectTestVertices[] = {
// Left Quad (red) : x in [-1..0]
{ -1.f, -1.f, 0.f, 1.f, 0.f, 0.f, 1.f },
{ 0.f, -1.f, 0.f, 1.f, 0.f, 0.f, 1.f },
{ 0.f, 1.f, 0.f, 1.f, 0.f, 0.f, 1.f },
{ -1.f, 1.f, 0.f, 1.f, 0.f, 0.f, 1.f },
// Right Quad (green) : x in [0..1]
{ 0.f, -1.f, 0.f, 0.f, 1.f, 0.f, 1.f },
{ 1.f, -1.f, 0.f, 0.f, 1.f, 0.f, 1.f },
{ 1.f, 1.f, 0.f, 0.f, 1.f, 0.f, 1.f },
{ 0.f, 1.f, 0.f, 0.f, 1.f, 0.f, 1.f },
// Full width quad (white) : x in [-1..1]. For draw commands that must not
// be issued: covers everything, so it cannot go unnoticed.
{ -1.f, -1.f, 0.f, 1.f, 1.f, 1.f, 1.f },
{ 1.f, -1.f, 0.f, 1.f, 1.f, 1.f, 1.f },
{ 1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f },
{ -1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f },
};
static constexpr quint16 IndirectTestIndices[] = {
0, 1, 2, 2, 3, 0,
};
// IndirectTestVertices with the index pattern above baked in, for the
// non-indexed draw calls.
static constexpr IndirectTestVertex IndirectTestVerticesNonIndexed[] = {
// Left Quad (red) : x in [-1..0]
{ -1.f, -1.f, 0.f, 1.f, 0.f, 0.f, 1.f },
{ 0.f, -1.f, 0.f, 1.f, 0.f, 0.f, 1.f },
{ 0.f, 1.f, 0.f, 1.f, 0.f, 0.f, 1.f },
{ 0.f, 1.f, 0.f, 1.f, 0.f, 0.f, 1.f },
{ -1.f, 1.f, 0.f, 1.f, 0.f, 0.f, 1.f },
{ -1.f, -1.f, 0.f, 1.f, 0.f, 0.f, 1.f },
// Right Quad (green) : x in [0..1]
{ 0.f, -1.f, 0.f, 0.f, 1.f, 0.f, 1.f },
{ 1.f, -1.f, 0.f, 0.f, 1.f, 0.f, 1.f },
{ 1.f, 1.f, 0.f, 0.f, 1.f, 0.f, 1.f },
{ 1.f, 1.f, 0.f, 0.f, 1.f, 0.f, 1.f },
{ 0.f, 1.f, 0.f, 0.f, 1.f, 0.f, 1.f },
{ 0.f, -1.f, 0.f, 0.f, 1.f, 0.f, 1.f },
// Full width quad (white) : x in [-1..1]
{ -1.f, -1.f, 0.f, 1.f, 1.f, 1.f, 1.f },
{ 1.f, -1.f, 0.f, 1.f, 1.f, 1.f, 1.f },
{ 1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f },
{ 1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f },
{ -1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f },
{ -1.f, -1.f, 0.f, 1.f, 1.f, 1.f, 1.f },
};
void tst_QRhi::indexedIndirectMultiDrawBaseline_data()
{
rhiTestData();
}
void tst_QRhi::indexedIndirectMultiDrawBaseline()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing indexedIndirectMultiDrawBaseline");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
static constexpr QRhiIndexedIndirectDrawCommand IndirectDrawCommands[] = {
{ 6, 1, 0, 0, 0 }, // vertexOffset = 0 -> left quad
{ 6, 1, 0, 4, 0 }, // vertexOffset = 4 -> right quad
};
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
std::unique_ptr<QRhiBuffer> db(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndirectBuffer, sizeof(IndirectDrawCommands)));
QVERIFY(db->create());
u->uploadStaticBuffer(db.get(), IndirectDrawCommands);
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
// Core check: drawCount=2 (we do NOT require DrawIndirectMulti)
cb->drawIndexedIndirect(db.get(), 0, std::size(IndirectDrawCommands));
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
// cannot check rendering results with Null, because there is no rendering there
if (impl == QRhi::Null)
return;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
// Convert to a byte-order independent format, otherwise the blue and red are swapped on little endian
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), texture->pixelSize());
const int y = result.height() / 2; // Check a horizontal line in the middle
const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
int redCount = 0, greenCount = 0, blueCount = 0;
const int maxFuzz = 1;
for (int x = 0; x < result.width(); ++x) {
const QRgb c(p[x]);
if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
else if (qRed(c) == 0 && qGreen(c) >= (255 - maxFuzz) && qBlue(c) == 0)
++greenCount;
else if (qRed(c) == 0 && qGreen(c) == 0 && qBlue(c) >= (255 - maxFuzz))
++blueCount;
else
QFAIL(qPrintable(QStringLiteral("Unexpected color at x=%1 y=%2: %3,%4,%5")
.arg(x).arg(y).arg(qRed(c)).arg(qGreen(c)).arg(qBlue(c))));
}
QCOMPARE(redCount, 64); // We should see the left red quad
QCOMPARE(greenCount, 64); // We should see the right green quad
QCOMPARE(blueCount, 0); // We should not see the background
}
void tst_QRhi::indirectMultiDrawWithTexture_data()
{
rhiTestData();
}
// Same as indexedIndirectMultiDrawBaseline, but with a pipeline that samples a
// texture and is flagged UsesIndirectDraws. On Metal such a pipeline cannot
// have its textures bound directly to its functions, so this covers the
// argument buffer shader variant, once with the fragment stage sampling and
// once with both stages. Uses the count variant where available, because that
// is what forces the indirect command buffer path on Metal.
void tst_QRhi::indirectMultiDrawWithTexture()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing indirectMultiDrawWithTexture");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
static constexpr QRhiIndexedIndirectDrawCommand IndirectDrawCommands[] = {
{ 6, 1, 0, 4, 0 }, // vertexOffset = 4 -> right quad
};
struct {
const char *vsFile;
QRhiShaderResourceBinding::StageFlags texStages;
} testCases[] = {
// Only the fragment stage samples the texture.
{ ":/data/colored.vert.qsb", QRhiShaderResourceBinding::FragmentStage },
// Both stages do. With Metal this means an argument buffer for the
// vertex stage as well, which also shifts the vertex input buffer
// indices, so exercise that too.
{ ":/data/colored_texture.vert.qsb",
QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage }
};
for (const auto &testCase : testCases) {
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
// White, so that the sampled result leaves the vertex colors unchanged
QImage whiteImage(1, 1, QImage::Format_RGBA8888);
whiteImage.fill(Qt::white);
std::unique_ptr<QRhiTexture> sampledTex(rhi->newTexture(QRhiTexture::RGBA8, QSize(1, 1)));
QVERIFY(sampledTex->create());
u->uploadTexture(sampledTex.get(), whiteImage);
std::unique_ptr<QRhiSampler> sampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
QVERIFY(sampler->create());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()),
QRhiShaderResourceBinding::sampledTexture(1, testCase.texStages,
sampledTex.get(), sampler.get())
});
QVERIFY(srb->create());
const QShader vs = loadShader(testCase.vsFile);
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored_texture.frag.qsb");
QVERIFY(fs.isValid());
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
// The same shaders back two pipelines, one indirect and one not, which on
// Metal need different builds of them. The direct one is created first, so
// that it is the indirect pipeline that would pick up the wrong, already
// cached build if the two could not be told apart. It would then be unable
// to use an indirect command buffer, leaving its half of the image blue.
std::unique_ptr<QRhiGraphicsPipeline> psDirect(rhi->newGraphicsPipeline());
psDirect->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
psDirect->setVertexInputLayout(vlayout);
psDirect->setShaderResourceBindings(srb.get());
psDirect->setRenderPassDescriptor(rpDesc.get());
QVERIFY(psDirect->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
ps->setVertexInputLayout(vlayout);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
std::unique_ptr<QRhiBuffer> db(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndirectBuffer, sizeof(IndirectDrawCommands)));
QVERIFY(db->create());
u->uploadStaticBuffer(db.get(), IndirectDrawCommands);
const bool useCount = rhi->isFeatureSupported(QRhi::DrawIndirectCount);
static const quint32 DrawCount = quint32(std::size(IndirectDrawCommands));
std::unique_ptr<QRhiBuffer> countBuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndirectBuffer,
sizeof(quint32)));
if (useCount) {
QVERIFY(countBuf->create());
u->uploadStaticBuffer(countBuf.get(), &DrawCount);
}
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
// Left, red quad: textures bound directly.
cb->setGraphicsPipeline(psDirect.get());
cb->setShaderResources(srb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
cb->drawIndexed(6, 1, 0, 0);
// Right, green quad: indirect. A half of the image each, so neither
// pipeline can quietly draw nothing.
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
if (useCount)
cb->drawIndexedIndirectCount(db.get(), 0, countBuf.get(), 0, std::size(IndirectDrawCommands));
else
cb->drawIndexedIndirect(db.get(), 0, std::size(IndirectDrawCommands));
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
continue;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), texture->pixelSize());
const int y = result.height() / 2;
const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
int redCount = 0, greenCount = 0, blueCount = 0;
const int maxFuzz = 1;
for (int x = 0; x < result.width(); ++x) {
const QRgb c(p[x]);
if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
else if (qRed(c) == 0 && qGreen(c) >= (255 - maxFuzz) && qBlue(c) == 0)
++greenCount;
else if (qRed(c) == 0 && qGreen(c) == 0 && qBlue(c) >= (255 - maxFuzz))
++blueCount;
else
QFAIL(qPrintable(QStringLiteral("%1: unexpected color at x=%2 y=%3: %4,%5,%6")
.arg(QLatin1StringView(testCase.vsFile)).arg(x).arg(y)
.arg(qRed(c)).arg(qGreen(c)).arg(qBlue(c))));
}
QVERIFY2(redCount == 64 && greenCount == 64 && blueCount == 0,
qPrintable(QStringLiteral("%1: red=%2 green=%3 blue=%4, expected 64, 64, 0")
.arg(QLatin1StringView(testCase.vsFile))
.arg(redCount).arg(greenCount).arg(blueCount)));
}
}
void tst_QRhi::indexedIndirectMultiDrawCustomStride_data()
{
rhiTestData();
}
// Same as indexedIndirectMultiDrawBaseline but with a custom stride between draw commands
void tst_QRhi::indexedIndirectMultiDrawCustomStride()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing indexedIndirectMultiDrawCustomStride");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
struct PaddedDrawCommand {
QRhiIndexedIndirectDrawCommand cmd;
quint32 pad[3]; // 12 bytes padding -> stride != sizeof(QRhiIndexedIndirectDrawCommand)
};
static constexpr PaddedDrawCommand IndirectDrawCommands[] = {
{ { 6, 1, 0, 0, 0 }, { 0xBEEFCAFE, 0xDEADCAFE, 0xF00DCAFE }, }, // vertexOffset = 0 -> left quad
{ { 6, 1, 0, 4, 0 }, { 0xBEEFCAFE, 0xDEADCAFE, 0xF00DCAFE }, }, // vertexOffset = 4 -> right quad
};
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
std::unique_ptr<QRhiBuffer> db(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndirectBuffer, sizeof(IndirectDrawCommands)));
QVERIFY(db->create());
u->uploadStaticBuffer(db.get(), IndirectDrawCommands);
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
// Core check: drawCount=2 (we do NOT require DrawIndirectMulti)
// The custom stride will trigger CPU emulation on some backends
cb->drawIndexedIndirect(db.get(), 0, std::size(IndirectDrawCommands), sizeof(PaddedDrawCommand));
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
// cannot check rendering results with Null, because there is no rendering there
if (impl == QRhi::Null)
return;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
// Convert to a byte-order independent format, otherwise the blue and red are swapped on little endian
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), texture->pixelSize());
const int y = result.height() / 2; // Check a horizontal line in the middle
const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
int redCount = 0, greenCount = 0, blueCount = 0;
const int maxFuzz = 1;
for (int x = 0; x < result.width(); ++x) {
const QRgb c(p[x]);
if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
else if (qRed(c) == 0 && qGreen(c) >= (255 - maxFuzz) && qBlue(c) == 0)
++greenCount;
else if (qRed(c) == 0 && qGreen(c) == 0 && qBlue(c) >= (255 - maxFuzz))
++blueCount;
else
QFAIL(qPrintable(QStringLiteral("Unexpected color at x=%1 y=%2: %3,%4,%5")
.arg(x).arg(y).arg(qRed(c)).arg(qGreen(c)).arg(qBlue(c))));
}
QCOMPARE(redCount, 64); // We should see the left red quad
QCOMPARE(greenCount, 64); // We should see the right green quad
QCOMPARE(blueCount, 0); // We should not see the background
}
void tst_QRhi::indexedIndirectMultiDrawFromCompute_data()
{
rhiTestData();
}
void tst_QRhi::indexedIndirectMultiDrawFromCompute()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing indexedIndirectMultiDrawFromCompute");
if (!rhi->isFeatureSupported(QRhi::Compute))
QSKIP("Compute not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> graphicsSrb(rhi->newShaderResourceBindings());
graphicsSrb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(graphicsSrb->create());
std::unique_ptr<QRhiGraphicsPipeline> graphicsPipeline(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
graphicsPipeline->setShaderStages({
{ QRhiShaderStage::Vertex, vs },
{ QRhiShaderStage::Fragment, fs }
});
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
graphicsPipeline->setVertexInputLayout(vlayout);
graphicsPipeline->setShaderResourceBindings(graphicsSrb.get());
graphicsPipeline->setRenderPassDescriptor(rpDesc.get());
QVERIFY(graphicsPipeline->create());
static constexpr QRhiIndexedIndirectDrawCommand EmptyCommands[2] = {};
std::unique_ptr<QRhiBuffer> db(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer | QRhiBuffer::IndirectBuffer, sizeof(EmptyCommands)));
QVERIFY(db->create());
u->uploadStaticBuffer(db.get(), EmptyCommands);
std::unique_ptr<QRhiShaderResourceBindings> computeSrb(rhi->newShaderResourceBindings());
computeSrb->setBindings({ QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, db.get()) });
QVERIFY(computeSrb->create());
std::unique_ptr<QRhiComputePipeline> computePipeline(rhi->newComputePipeline());
const QShader cs = loadShader(":/data/indirect_draw_args.comp.qsb");
QVERIFY(cs.isValid());
computePipeline->setShaderStage({ QRhiShaderStage::Compute, cs });
computePipeline->setShaderResourceBindings(computeSrb.get());
QVERIFY(computePipeline->create());
cb->beginComputePass(u);
cb->setComputePipeline(computePipeline.get());
cb->setShaderResources(computeSrb.get());
cb->dispatch(2, 1, 1);
cb->endComputePass();
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, nullptr);
cb->setGraphicsPipeline(graphicsPipeline.get());
cb->setShaderResources(graphicsSrb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
cb->drawIndexedIndirect(db.get(), 0, 2);
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
// cannot check rendering results with Null, because there is no rendering there
if (impl == QRhi::Null)
return;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
// Convert to a byte-order independent format, otherwise the blue and red are swapped on little endian
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), texture->pixelSize());
const int y = result.height() / 2; // Check a horizontal line in the middle
const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
int redCount = 0, greenCount = 0, blueCount = 0;
const int maxFuzz = 1;
for (int x = 0; x < result.width(); ++x) {
const QRgb c(p[x]);
if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
else if (qRed(c) == 0 && qGreen(c) >= (255 - maxFuzz) && qBlue(c) == 0)
++greenCount;
else if (qRed(c) == 0 && qGreen(c) == 0 && qBlue(c) >= (255 - maxFuzz))
++blueCount;
else
QFAIL(qPrintable(QStringLiteral("Unexpected color at x=%1 y=%2: %3,%4,%5")
.arg(x).arg(y).arg(qRed(c)).arg(qGreen(c)).arg(qBlue(c))));
}
QCOMPARE(redCount, 64); // We should see the left red quad
QCOMPARE(greenCount, 64); // We should see the right green quad
QCOMPARE(blueCount, 0); // We should not see the background
}
// Shared tail for the QRhiIndirectCommandBuffer tests: checks a horizontal
// line in the middle of the readback against the expected number of red, green
// and blue pixels. White (the "must not be drawn" quad) fails the color check.
static void checkIndirectCommandBufferResult(QRhi *rhi, const QImage &image, const QSize &expectedSize,
int expectedRed, int expectedGreen, int expectedBlue)
{
QImage result = image;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), expectedSize);
const int y = result.height() / 2;
const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
int redCount = 0, greenCount = 0, blueCount = 0;
const int maxFuzz = 1;
for (int x = 0; x < result.width(); ++x) {
const QRgb c(p[x]);
if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
else if (qRed(c) == 0 && qGreen(c) >= (255 - maxFuzz) && qBlue(c) == 0)
++greenCount;
else if (qRed(c) == 0 && qGreen(c) == 0 && qBlue(c) >= (255 - maxFuzz))
++blueCount;
else
QFAIL(qPrintable(QStringLiteral("Unexpected color at x=%1 y=%2: %3,%4,%5")
.arg(x).arg(y).arg(qRed(c)).arg(qGreen(c)).arg(qBlue(c))));
}
QCOMPARE(redCount, expectedRed);
QCOMPARE(greenCount, expectedGreen);
QCOMPARE(blueCount, expectedBlue);
}
void tst_QRhi::indirectCommandBufferBaseline_data()
{
rhiTestData();
}
// The QRhiIndirectCommandBuffer counterpart of
// indexedIndirectMultiDrawBaseline: the same two quads, but the draw commands
// are recorded on the CPU into an indirect command buffer and replayed with a
// single executeIndirect().
void tst_QRhi::indirectCommandBufferBaseline()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing indirectCommandBufferBaseline");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
std::unique_ptr<QRhiIndirectCommandBuffer> icb(rhi->newIndirectCommandBuffer(QRhiIndirectCommandBuffer::IndexedDraws, 8));
QVERIFY(icb->create());
icb->clear();
icb->drawIndexed(6, 1, 0, 0, 0); // left quad
icb->drawIndexed(6, 1, 0, 4, 0); // right quad
QCOMPARE(icb->commandCount(), 2u);
u->commitIndirectCommandBuffer(icb.get());
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
cb->executeIndirect(icb.get());
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
checkIndirectCommandBufferResult(rhi, result, texture->pixelSize(), 64, 64, 0);
}
void tst_QRhi::indirectCommandBufferNonIndexed_data()
{
rhiTestData();
}
// Same, with a Draws (non-indexed) indirect command buffer.
void tst_QRhi::indirectCommandBufferNonIndexed()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing indirectCommandBufferNonIndexed");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVerticesNonIndexed)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVerticesNonIndexed);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
std::unique_ptr<QRhiIndirectCommandBuffer> icb(rhi->newIndirectCommandBuffer(QRhiIndirectCommandBuffer::Draws, 8));
QVERIFY(icb->create());
icb->clear();
icb->draw(6, 1, 0, 0); // left quad
icb->draw(6, 1, 6, 0); // right quad
QCOMPARE(icb->commandCount(), 2u);
// Recording the wrong kind must be rejected, not silently corrupt the batch.
QTest::ignoreMessage(QtWarningMsg, "QRhiIndirectCommandBuffer: drawIndexed() on a Draws indirect command buffer; ignored");
icb->drawIndexed(6, 1, 0, 0, 0);
QCOMPARE(icb->commandCount(), 2u);
u->commitIndirectCommandBuffer(icb.get());
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput);
cb->executeIndirect(icb.get());
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
checkIndirectCommandBufferResult(rhi, result, texture->pixelSize(), 64, 64, 0);
}
void tst_QRhi::indirectCommandBufferSubrange_data()
{
rhiTestData();
}
// Records three commands - left red quad, right green quad, and a full width
// white quad that must not be drawn - then executes only the middle one. Covers
// both the firstCommand offset and the clamping of commandCount.
void tst_QRhi::indirectCommandBufferSubrange()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing indirectCommandBufferSubrange");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
std::unique_ptr<QRhiIndirectCommandBuffer> icb(rhi->newIndirectCommandBuffer(QRhiIndirectCommandBuffer::IndexedDraws, 4));
QVERIFY(icb->create());
icb->clear();
icb->drawIndexed(6, 1, 0, 0, 0); // left red quad
icb->drawIndexed(6, 1, 0, 4, 0); // right green quad
icb->drawIndexed(6, 1, 0, 8, 0); // full width white quad, must not be drawn
QCOMPARE(icb->commandCount(), 3u);
u->commitIndirectCommandBuffer(icb.get());
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
cb->executeIndirect(icb.get(), 1, 1);
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
// Only the green quad, the left half stays at the blue clear color.
checkIndirectCommandBufferResult(rhi, result, texture->pixelSize(), 0, 64, 64);
}
void tst_QRhi::indirectCommandBufferFromCompute_data()
{
rhiTestData();
}
// The GPU-driven path: a compute shader writes the draw arguments, and
// buildIndirect() - called outside of any pass - populates the indirect command
// buffer from that buffer.
void tst_QRhi::indirectCommandBufferFromCompute()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing indirectCommandBufferFromCompute");
if (!rhi->isFeatureSupported(QRhi::Compute))
QSKIP("Compute not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> graphicsSrb(rhi->newShaderResourceBindings());
graphicsSrb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(graphicsSrb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setShaderResourceBindings(graphicsSrb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
static constexpr QRhiIndexedIndirectDrawCommand EmptyCommands[2] = {};
std::unique_ptr<QRhiBuffer> db(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer | QRhiBuffer::IndirectBuffer, sizeof(EmptyCommands)));
QVERIFY(db->create());
u->uploadStaticBuffer(db.get(), EmptyCommands);
std::unique_ptr<QRhiShaderResourceBindings> computeSrb(rhi->newShaderResourceBindings());
computeSrb->setBindings({ QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, db.get()) });
QVERIFY(computeSrb->create());
std::unique_ptr<QRhiComputePipeline> computePipeline(rhi->newComputePipeline());
const QShader cs = loadShader(":/data/indirect_draw_args.comp.qsb");
QVERIFY(cs.isValid());
computePipeline->setShaderStage({ QRhiShaderStage::Compute, cs });
computePipeline->setShaderResourceBindings(computeSrb.get());
QVERIFY(computePipeline->create());
std::unique_ptr<QRhiIndirectCommandBuffer> icb(rhi->newIndirectCommandBuffer(QRhiIndirectCommandBuffer::IndexedDraws, 2));
QVERIFY(icb->create());
cb->beginComputePass(u);
cb->setComputePipeline(computePipeline.get());
cb->setShaderResources(computeSrb.get());
cb->dispatch(2, 1, 1);
cb->endComputePass();
QRhiIndirectCommandBufferBuildInfo buildInfo;
buildInfo.topology = ps->topology();
buildInfo.sourceBuffer = db.get();
buildInfo.commandCount = 2;
buildInfo.indexBuffer = ib.get();
buildInfo.indexFormat = QRhiCommandBuffer::IndexUInt16;
cb->buildIndirect(icb.get(), buildInfo);
QVERIFY(icb->isGpuBuilt());
QCOMPARE(icb->commandCount(), 2u);
QCOMPARE(icb->recordedCommandCount(), 0u);
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, nullptr);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(graphicsSrb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
cb->executeIndirect(icb.get());
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
checkIndirectCommandBufferResult(rhi, result, texture->pixelSize(), 64, 64, 0);
}
void tst_QRhi::indirectCommandBufferFromComputeSubrange_data()
{
rhiTestData();
}
// Same as indirectCommandBufferFromCompute, but executing a subrange of the
// GPU-built commands: firstCommand and commandCount have to be honoured there
// just like with a CPU-recorded indirect command buffer.
void tst_QRhi::indirectCommandBufferFromComputeSubrange()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing indirectCommandBufferFromComputeSubrange");
if (!rhi->isFeatureSupported(QRhi::Compute))
QSKIP("Compute not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> graphicsSrb(rhi->newShaderResourceBindings());
graphicsSrb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(graphicsSrb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setShaderResourceBindings(graphicsSrb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
static constexpr QRhiIndexedIndirectDrawCommand EmptyCommands[2] = {};
std::unique_ptr<QRhiBuffer> db(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer | QRhiBuffer::IndirectBuffer, sizeof(EmptyCommands)));
QVERIFY(db->create());
u->uploadStaticBuffer(db.get(), EmptyCommands);
std::unique_ptr<QRhiShaderResourceBindings> computeSrb(rhi->newShaderResourceBindings());
computeSrb->setBindings({ QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, db.get()) });
QVERIFY(computeSrb->create());
std::unique_ptr<QRhiComputePipeline> computePipeline(rhi->newComputePipeline());
const QShader cs = loadShader(":/data/indirect_draw_args.comp.qsb");
QVERIFY(cs.isValid());
computePipeline->setShaderStage({ QRhiShaderStage::Compute, cs });
computePipeline->setShaderResourceBindings(computeSrb.get());
QVERIFY(computePipeline->create());
std::unique_ptr<QRhiIndirectCommandBuffer> icb(rhi->newIndirectCommandBuffer(QRhiIndirectCommandBuffer::IndexedDraws, 2));
QVERIFY(icb->create());
cb->beginComputePass(u);
cb->setComputePipeline(computePipeline.get());
cb->setShaderResources(computeSrb.get());
cb->dispatch(2, 1, 1);
cb->endComputePass();
QRhiIndirectCommandBufferBuildInfo buildInfo;
buildInfo.topology = ps->topology();
buildInfo.sourceBuffer = db.get();
buildInfo.commandCount = 2;
buildInfo.indexBuffer = ib.get();
buildInfo.indexFormat = QRhiCommandBuffer::IndexUInt16;
cb->buildIndirect(icb.get(), buildInfo);
QVERIFY(icb->isGpuBuilt());
QCOMPARE(icb->commandCount(), 2u);
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, nullptr);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(graphicsSrb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
// Skip the first command, so only the right (green) quad is drawn
cb->executeIndirect(icb.get(), 1, 1);
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
// the left half stays the blue clear color
checkIndirectCommandBufferResult(rhi, result, texture->pixelSize(), 0, 64, 64);
}
void tst_QRhi::indirectCommandBufferFromComputeCount_data()
{
rhiTestData();
}
// The GPU-built path with a device-side count: the count buffer holds 1, so
// only the first of the two commands must be executed.
void tst_QRhi::indirectCommandBufferFromComputeCount()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing indirectCommandBufferFromComputeCount");
if (!rhi->isFeatureSupported(QRhi::Compute))
QSKIP("Compute not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::DrawIndirectCount))
QSKIP("Indirect draw with a device-side count not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> graphicsSrb(rhi->newShaderResourceBindings());
graphicsSrb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(graphicsSrb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setShaderResourceBindings(graphicsSrb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
static constexpr QRhiIndexedIndirectDrawCommand EmptyCommands[2] = {};
std::unique_ptr<QRhiBuffer> db(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer | QRhiBuffer::IndirectBuffer, sizeof(EmptyCommands)));
QVERIFY(db->create());
u->uploadStaticBuffer(db.get(), EmptyCommands);
static const quint32 DrawCount = 1;
std::unique_ptr<QRhiBuffer> countBuf(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::IndirectBuffer,
sizeof(quint32)));
QVERIFY(countBuf->create());
u->uploadStaticBuffer(countBuf.get(), &DrawCount);
std::unique_ptr<QRhiShaderResourceBindings> computeSrb(rhi->newShaderResourceBindings());
computeSrb->setBindings({ QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, db.get()) });
QVERIFY(computeSrb->create());
std::unique_ptr<QRhiComputePipeline> computePipeline(rhi->newComputePipeline());
const QShader cs = loadShader(":/data/indirect_draw_args.comp.qsb");
QVERIFY(cs.isValid());
computePipeline->setShaderStage({ QRhiShaderStage::Compute, cs });
computePipeline->setShaderResourceBindings(computeSrb.get());
QVERIFY(computePipeline->create());
std::unique_ptr<QRhiIndirectCommandBuffer> icb(rhi->newIndirectCommandBuffer(QRhiIndirectCommandBuffer::IndexedDraws, 2));
QVERIFY(icb->create());
cb->beginComputePass(u);
cb->setComputePipeline(computePipeline.get());
cb->setShaderResources(computeSrb.get());
cb->dispatch(2, 1, 1);
cb->endComputePass();
QRhiIndirectCommandBufferBuildInfo buildInfo;
buildInfo.topology = ps->topology();
buildInfo.sourceBuffer = db.get();
buildInfo.commandCount = 2; // an upper bound now, the count buffer decides
buildInfo.countBuffer = countBuf.get();
buildInfo.indexBuffer = ib.get();
buildInfo.indexFormat = QRhiCommandBuffer::IndexUInt16;
cb->buildIndirect(icb.get(), buildInfo);
QVERIFY(icb->isGpuBuilt());
QCOMPARE(icb->commandCount(), 2u);
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, nullptr);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(graphicsSrb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
cb->executeIndirect(icb.get());
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
// only the first command ran, so the right half stays the blue clear color
checkIndirectCommandBufferResult(rhi, result, texture->pixelSize(), 64, 0, 64);
}
void tst_QRhi::indirectCommandBufferRerecord_data()
{
rhiTestData();
}
// Records one set of commands, executes it, then clears and records a different
// set and executes that in a later frame. Covers re-encoding an indirect
// command buffer whose contents changed, which on Metal is keyed on the
// contents generation and on the other backends means re-uploading the buffer.
void tst_QRhi::indirectCommandBufferRerecord()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing indirectCommandBufferRerecord");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
std::unique_ptr<QRhiIndirectCommandBuffer> icb(rhi->newIndirectCommandBuffer(QRhiIndirectCommandBuffer::IndexedDraws, 4));
QVERIFY(icb->create());
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
bool uploaded = false;
// Frame 0 draws both quads, frame 1 only the right one. Each frame gets its
// own recording, so the second one has to actually take effect.
for (int frame = 0; frame < 2; ++frame) {
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
if (!uploaded) {
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
uploaded = true;
}
icb->clear();
if (frame == 0)
icb->drawIndexed(6, 1, 0, 0, 0); // left red quad
icb->drawIndexed(6, 1, 0, 4, 0); // right green quad
QCOMPARE(icb->commandCount(), frame == 0 ? 2u : 1u);
u->commitIndirectCommandBuffer(icb.get());
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
cb->executeIndirect(icb.get());
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
continue;
if (frame == 0)
checkIndirectCommandBufferResult(rhi, result, texture->pixelSize(), 64, 64, 0);
else // the left half is back to the blue clear color
checkIndirectCommandBufferResult(rhi, result, texture->pixelSize(), 0, 64, 64);
}
}
void tst_QRhi::indirectCommandBufferCpuThenBuild_data()
{
rhiTestData();
}
// Uses the same indirect command buffer first with CPU-recorded commands and
// then with buildIndirect(). Backends that keep different native objects for
// the two - Metal allocates its MTLIndirectCommandBuffers with a different
// storage mode depending on who fills them - have to switch over cleanly.
void tst_QRhi::indirectCommandBufferCpuThenBuild()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing indirectCommandBufferCpuThenBuild");
if (!rhi->isFeatureSupported(QRhi::Compute))
QSKIP("Compute not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
std::unique_ptr<QRhiShaderResourceBindings> graphicsSrb(rhi->newShaderResourceBindings());
graphicsSrb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(graphicsSrb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setShaderResourceBindings(graphicsSrb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
static constexpr QRhiIndexedIndirectDrawCommand EmptyCommands[2] = {};
std::unique_ptr<QRhiBuffer> db(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer | QRhiBuffer::IndirectBuffer, sizeof(EmptyCommands)));
QVERIFY(db->create());
std::unique_ptr<QRhiShaderResourceBindings> computeSrb(rhi->newShaderResourceBindings());
computeSrb->setBindings({ QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, db.get()) });
QVERIFY(computeSrb->create());
std::unique_ptr<QRhiComputePipeline> computePipeline(rhi->newComputePipeline());
const QShader cs = loadShader(":/data/indirect_draw_args.comp.qsb");
QVERIFY(cs.isValid());
computePipeline->setShaderStage({ QRhiShaderStage::Compute, cs });
computePipeline->setShaderResourceBindings(computeSrb.get());
QVERIFY(computePipeline->create());
std::unique_ptr<QRhiIndirectCommandBuffer> icb(rhi->newIndirectCommandBuffer(QRhiIndirectCommandBuffer::IndexedDraws, 2));
QVERIFY(icb->create());
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
bool uploaded = false;
// Frame 0: CPU-recorded, only the right quad. Frame 1: the same object
// rebuilt from the compute-written buffer, which draws both.
for (int frame = 0; frame < 2; ++frame) {
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
if (!uploaded) {
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
u->uploadStaticBuffer(db.get(), EmptyCommands);
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
uploaded = true;
}
if (frame == 0) {
icb->clear();
icb->drawIndexed(6, 1, 0, 4, 0); // right green quad
u->commitIndirectCommandBuffer(icb.get());
} else {
cb->beginComputePass(u);
cb->setComputePipeline(computePipeline.get());
cb->setShaderResources(computeSrb.get());
cb->dispatch(2, 1, 1);
cb->endComputePass();
u = nullptr;
QRhiIndirectCommandBufferBuildInfo buildInfo;
buildInfo.topology = ps->topology();
buildInfo.sourceBuffer = db.get();
// commandCount left at 0, which means maxCommandCount(), so 2
buildInfo.indexBuffer = ib.get();
buildInfo.indexFormat = QRhiCommandBuffer::IndexUInt16;
cb->buildIndirect(icb.get(), buildInfo);
QVERIFY(icb->isGpuBuilt());
QCOMPARE(icb->commandCount(), 2u);
// the CPU-side recording from frame 0 is still there, but ignored
QCOMPARE(icb->recordedCommandCount(), 1u);
}
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(graphicsSrb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
cb->executeIndirect(icb.get());
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
continue;
if (frame == 0) // only the right quad, left half stays blue
checkIndirectCommandBufferResult(rhi, result, texture->pixelSize(), 0, 64, 64);
else
checkIndirectCommandBufferResult(rhi, result, texture->pixelSize(), 64, 64, 0);
}
}
void tst_QRhi::indexedIndirectMultiDrawHighDrawCount_data()
{
rhiTestData();
}
// Verifies that indirect drawing works correctly with a high draw count
// (above the ICB_DRAW_COUNT_THRESHOLD of 128). On Metal, when
// UsesIndirectDraws is set, draw counts above 128 trigger the GPU-driven
// Indirect Command Buffer (ICB) path. This test ensures that path produces
// the same rendering result as the CPU-side loop fallback.
void tst_QRhi::indexedIndirectMultiDrawHighDrawCount()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing "
"indexedIndirectMultiDrawHighDrawCount");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
// 130 draws: above the Metal ICB threshold of 128. The first command
// draws the left (red) quad, the second draws the right (green) quad,
// and the remaining 128 re-draw the left quad on top (red over red).
// The net visible result is identical to the 2-draw baseline.
static constexpr int DrawCount = 130;
QRhiIndexedIndirectDrawCommand cmds[DrawCount];
cmds[0] = { 6, 1, 0, 0, 0 }; // left quad (red)
cmds[1] = { 6, 1, 0, 4, 0 }; // right quad (green)
for (int i = 2; i < DrawCount; ++i)
cmds[i] = { 6, 1, 0, 0, 0 }; // left quad again (red over red)
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget
| QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(
rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(
rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(
rhi->newBuffer(QRhiBuffer::Immutable,
QRhiBuffer::VertexBuffer,
sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
std::unique_ptr<QRhiBuffer> ib(
rhi->newBuffer(QRhiBuffer::Immutable,
QRhiBuffer::IndexBuffer,
sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(
rhi->newBuffer(QRhiBuffer::Dynamic,
QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(
rhi->newShaderResourceBindings());
srb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(
0, QRhiShaderResourceBinding::VertexStage, ubuf.get())
});
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(
rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({
{ QRhiShaderStage::Vertex, vs },
{ QRhiShaderStage::Fragment, fs }
});
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3,
offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4,
offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
std::unique_ptr<QRhiBuffer> db(
rhi->newBuffer(QRhiBuffer::Immutable,
QRhiBuffer::IndirectBuffer,
sizeof(cmds)));
QVERIFY(db->create());
u->uploadStaticBuffer(db.get(), cmds);
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
cb->setViewport({
0, 0,
float(outputSize.width()),
float(outputSize.height())
});
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput,
ib.get(), 0,
QRhiCommandBuffer::IndexUInt16);
// drawCount=130 exceeds the Metal ICB threshold (128)
cb->drawIndexedIndirect(db.get(), 0, DrawCount);
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(
reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
// Cannot check rendering results with Null, because there is no rendering there
if (impl == QRhi::Null)
return;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
// Convert to a byte-order independent format, otherwise the blue and red are swapped on little endian
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), texture->pixelSize());
const int y = result.height() / 2; // Check a horizontal line in the middle
const quint32 *p =
reinterpret_cast<const quint32 *>(result.constScanLine(y));
int redCount = 0, greenCount = 0, blueCount = 0;
const int maxFuzz = 1;
for (int x = 0; x < result.width(); ++x) {
const QRgb c(p[x]);
if (qRed(c) >= (255 - maxFuzz)
&& qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
else if (qRed(c) == 0
&& qGreen(c) >= (255 - maxFuzz) && qBlue(c) == 0)
++greenCount;
else if (qRed(c) == 0
&& qGreen(c) == 0 && qBlue(c) >= (255 - maxFuzz))
++blueCount;
else
QFAIL(qPrintable(
QStringLiteral("Unexpected color at x=%1 y=%2: "
"%3,%4,%5")
.arg(x).arg(y)
.arg(qRed(c)).arg(qGreen(c)).arg(qBlue(c))));
}
QCOMPARE(redCount, 64); // We should see the left red quad
QCOMPARE(greenCount, 64); // We should see the right green quad
QCOMPARE(blueCount, 0); // We should not see the background
}
void tst_QRhi::indirectMultiDrawHighDrawCount_data()
{
rhiTestData();
}
// Non-indexed counterpart of indexedIndirectMultiDrawHighDrawCount and of
// indexedIndirectMultiDrawHighDrawCountRenderBufferDepth. On Metal a draw count
// above the ICB_DRAW_COUNT_THRESHOLD of 128 routes drawIndirect() through the
// GPU-driven Indirect Command Buffer path as well, using the non-indexed encode
// kernel. That path interrupts and restarts the render pass, so the test also
// depth tests and depth writes against a QRhiRenderBuffer depth-stencil buffer,
// created with NoTransientBacking so that its contents survive the interruption.
//
// A near quad is drawn on the left before the indirect draw, and has to keep
// winning the depth test against the far quad that the indirect draw covers
// everything with: that only holds if the depth contents survived. The second
// indirect command draws a near quad on the right, so the colour of the far quad
// must end up nowhere, and the result also proves that more than one of the
// encoded commands ran.
void tst_QRhi::indirectMultiDrawHighDrawCount()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing "
"indirectMultiDrawHighDrawCount");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
// Three non-indexed quads: a near red one on the left, a near green one on
// the right, and a far white one covering everything.
static constexpr IndirectTestVertex Vertices[] = {
// firstVertex = 0: near left, red
{ -1.f, -1.f, -0.5f, 1.f, 0.f, 0.f, 1.f },
{ 0.f, -1.f, -0.5f, 1.f, 0.f, 0.f, 1.f },
{ 0.f, 1.f, -0.5f, 1.f, 0.f, 0.f, 1.f },
{ 0.f, 1.f, -0.5f, 1.f, 0.f, 0.f, 1.f },
{ -1.f, 1.f, -0.5f, 1.f, 0.f, 0.f, 1.f },
{ -1.f, -1.f, -0.5f, 1.f, 0.f, 0.f, 1.f },
// firstVertex = 6: near right, green
{ 0.f, -1.f, -0.5f, 0.f, 1.f, 0.f, 1.f },
{ 1.f, -1.f, -0.5f, 0.f, 1.f, 0.f, 1.f },
{ 1.f, 1.f, -0.5f, 0.f, 1.f, 0.f, 1.f },
{ 1.f, 1.f, -0.5f, 0.f, 1.f, 0.f, 1.f },
{ 0.f, 1.f, -0.5f, 0.f, 1.f, 0.f, 1.f },
{ 0.f, -1.f, -0.5f, 0.f, 1.f, 0.f, 1.f },
// firstVertex = 12: far, full width, white
{ -1.f, -1.f, 0.5f, 1.f, 1.f, 1.f, 1.f },
{ 1.f, -1.f, 0.5f, 1.f, 1.f, 1.f, 1.f },
{ 1.f, 1.f, 0.5f, 1.f, 1.f, 1.f, 1.f },
{ 1.f, 1.f, 0.5f, 1.f, 1.f, 1.f, 1.f },
{ -1.f, 1.f, 0.5f, 1.f, 1.f, 1.f, 1.f },
{ -1.f, -1.f, 0.5f, 1.f, 1.f, 1.f, 1.f },
};
// 130 draws: above the Metal ICB threshold of 128.
static constexpr int DrawCount = 130;
QRhiIndirectDrawCommand cmds[DrawCount];
for (int i = 0; i < DrawCount; ++i)
cmds[i] = { 6, 1, 12, 0 }; // the far quad
cmds[1] = { 6, 1, 6, 0 }; // except the second one, the near right quad
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiRenderBuffer> dsBuf(
rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, outputSize, 1,
QRhiRenderBuffer::NoTransientBacking));
QVERIFY(dsBuf->create());
QRhiColorAttachment colorAtt(texture.get());
QRhiTextureRenderTargetDescription rtDesc(colorAtt);
rtDesc.setDepthStencilBuffer(dsBuf.get());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(rtDesc));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(Vertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), Vertices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({
{ QRhiShaderStage::Vertex, vs },
{ QRhiShaderStage::Fragment, fs }
});
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
// Needed by the Metal backend to take the ICB path. No effect elsewhere.
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
ps->setDepthTest(true);
ps->setDepthWrite(true);
ps->setDepthOp(QRhiGraphicsPipeline::Less);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
std::unique_ptr<QRhiBuffer> db(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndirectBuffer, sizeof(cmds)));
QVERIFY(db->create());
u->uploadStaticBuffer(db.get(), cmds);
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput);
// The near left quad, and the depth values it writes, have to survive the
// pass interruption that the indirect draw may involve.
cb->draw(6);
// drawCount=130 exceeds the Metal ICB threshold (128)
cb->drawIndirect(db.get(), 0, DrawCount);
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
// Cannot check rendering results with Null, because there is no rendering there
if (impl == QRhi::Null)
return;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
// Convert to a byte-order independent format, otherwise the blue and red are swapped on little endian
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), texture->pixelSize());
const int y = result.height() / 2; // Check a horizontal line in the middle
const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
int redCount = 0, greenCount = 0, whiteCount = 0, otherCount = 0;
const int maxFuzz = 1;
for (int x = 0; x < result.width(); ++x) {
const QRgb c(p[x]);
if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
else if (qRed(c) == 0 && qGreen(c) >= (255 - maxFuzz) && qBlue(c) == 0)
++greenCount;
else if (qRed(c) >= (255 - maxFuzz) && qGreen(c) >= (255 - maxFuzz)
&& qBlue(c) >= (255 - maxFuzz))
++whiteCount;
else
++otherCount;
}
QCOMPARE(redCount, 64); // The near left quad wins the depth test
QCOMPARE(greenCount, 64); // So does the near right quad, from command 1
QCOMPARE(whiteCount, 0); // The far quad never wins
QCOMPARE(otherCount, 0); // And the background is covered
}
void tst_QRhi::indexedIndirectMultiDrawHighDrawCountViewport_data()
{
rhiTestData();
}
// Like indexedIndirectMultiDrawHighDrawCount, but with a viewport covering
// only the left half of the render target. On Metal the ICB path interrupts
// and restarts the render pass, which must not lose the viewport.
void tst_QRhi::indexedIndirectMultiDrawHighDrawCountViewport()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing "
"indexedIndirectMultiDrawHighDrawCountViewport");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
static constexpr int DrawCount = 130; // above the Metal ICB threshold of 128
QRhiIndexedIndirectDrawCommand cmds[DrawCount];
cmds[0] = { 6, 1, 0, 0, 0 }; // left quad (red)
cmds[1] = { 6, 1, 0, 4, 0 }; // right quad (green)
for (int i = 2; i < DrawCount; ++i)
cmds[i] = { 6, 1, 0, 0, 0 }; // left quad again (red over red)
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({
{ QRhiShaderStage::Vertex, vs },
{ QRhiShaderStage::Fragment, fs }
});
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
std::unique_ptr<QRhiBuffer> db(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndirectBuffer, sizeof(cmds)));
QVERIFY(db->create());
u->uploadStaticBuffer(db.get(), cmds);
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
// Left half only.
cb->setViewport({ 0, 0, float(outputSize.width() / 2), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
cb->drawIndexedIndirect(db.get(), 0, DrawCount);
// Redraws the left quad, so it does not change the result, but recording a
// draw call after the pass was interrupted has to work.
cb->drawIndexed(6);
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), texture->pixelSize());
const int y = result.height() / 2;
const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
int redCount = 0, greenCount = 0, blueCount = 0;
const int maxFuzz = 1;
for (int x = 0; x < result.width(); ++x) {
const QRgb c(p[x]);
if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
else if (qRed(c) == 0 && qGreen(c) >= (255 - maxFuzz) && qBlue(c) == 0)
++greenCount;
else if (qRed(c) == 0 && qGreen(c) == 0 && qBlue(c) >= (255 - maxFuzz))
++blueCount;
else
QFAIL(qPrintable(QStringLiteral("Unexpected color at x=%1 y=%2: %3,%4,%5")
.arg(x).arg(y).arg(qRed(c)).arg(qGreen(c)).arg(qBlue(c))));
}
// Both quads squeezed into the left half, the right half left at the clear color.
QCOMPARE(redCount, 32);
QCOMPARE(greenCount, 32);
QCOMPARE(blueCount, 64);
}
void tst_QRhi::indexedIndirectMultiDrawHighDrawCountMultisample_data()
{
rhiTestData();
}
// Draws into a multisample render target, first with a normal draw call and then
// with a high draw count indirect draw. On Metal the latter takes the ICB path,
// which interrupts and restarts the render pass; that must not discard what the
// first draw put into the multisample attachment.
void tst_QRhi::indexedIndirectMultiDrawHighDrawCountMultisample()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing "
"indexedIndirectMultiDrawHighDrawCountMultisample");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::MultisampleRenderBuffer))
QSKIP("Multisample renderbuffer not supported on this backend");
const int sampleCount = 4;
static constexpr int DrawCount = 130; // above the Metal ICB threshold of 128
QRhiIndexedIndirectDrawCommand cmds[DrawCount];
for (int i = 0; i < DrawCount; ++i)
cmds[i] = { 6, 1, 0, 4, 0 }; // right quad (green)
const QSize outputSize(128, 64);
std::unique_ptr<QRhiRenderBuffer> msaaBuf(rhi->newRenderBuffer(QRhiRenderBuffer::Color, outputSize, sampleCount));
QVERIFY(msaaBuf->create());
std::unique_ptr<QRhiTexture> resolveTex(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(resolveTex->create());
QRhiColorAttachment colorAtt(msaaBuf.get());
colorAtt.setResolveTexture(resolveTex.get());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ colorAtt }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
ps->setSampleCount(sampleCount);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
std::unique_ptr<QRhiBuffer> db(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndirectBuffer, sizeof(cmds)));
QVERIFY(db->create());
u->uploadStaticBuffer(db.get(), cmds);
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
// Left (red) quad with a normal draw call, before any pass interruption.
cb->drawIndexed(6);
// Right (green) quad, with enough draws to trigger the ICB path on Metal.
cb->drawIndexedIndirect(db.get(), 0, DrawCount);
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ resolveTex.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), resolveTex->pixelSize());
const int y = result.height() / 2;
const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
int redCount = 0, greenCount = 0, otherCount = 0;
const int maxFuzz = 1;
for (int x = 0; x < result.width(); ++x) {
const QRgb c(p[x]);
if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
else if (qRed(c) == 0 && qGreen(c) >= (255 - maxFuzz) && qBlue(c) == 0)
++greenCount;
else
++otherCount;
}
// The red quad was drawn before the interruption, the green one after it.
QCOMPARE(redCount, 64);
QCOMPARE(greenCount, 64);
QCOMPARE(otherCount, 0);
}
void tst_QRhi::indexedIndirectMultiDrawHighDrawCountMultisampleDepthResolve_data()
{
rhiTestData();
}
// Same idea as indexedIndirectMultiDrawHighDrawCountMultisample, but the
// interruption has to preserve the multisample depth buffer, not the color one.
// A near quad is drawn normally, then a farther, full width quad with a high
// draw count indirect draw. The near quad has to win the depth test, which it
// can only do if the depth contents survived the interruption. The depth
// attachment has a resolve texture, so its store action only resolves.
void tst_QRhi::indexedIndirectMultiDrawHighDrawCountMultisampleDepthResolve()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing "
"indexedIndirectMultiDrawHighDrawCountMultisampleDepthResolve");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::MultisampleTexture))
QSKIP("Multisample texture not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::ResolveDepthStencil))
QSKIP("Depth-stencil resolve not supported on this backend");
const int sampleCount = 4;
// Near quad on the left, then a farther quad covering everything.
static constexpr IndirectTestVertex Vertices[] = {
{ -1.f, -1.f, -0.5f, 1.f, 0.f, 0.f, 1.f },
{ 0.f, -1.f, -0.5f, 1.f, 0.f, 0.f, 1.f },
{ 0.f, 1.f, -0.5f, 1.f, 0.f, 0.f, 1.f },
{ -1.f, 1.f, -0.5f, 1.f, 0.f, 0.f, 1.f },
{ -1.f, -1.f, 0.5f, 0.f, 1.f, 0.f, 1.f },
{ 1.f, -1.f, 0.5f, 0.f, 1.f, 0.f, 1.f },
{ 1.f, 1.f, 0.5f, 0.f, 1.f, 0.f, 1.f },
{ -1.f, 1.f, 0.5f, 0.f, 1.f, 0.f, 1.f },
};
static constexpr int DrawCount = 130; // above the Metal ICB threshold of 128
QRhiIndexedIndirectDrawCommand cmds[DrawCount];
for (int i = 0; i < DrawCount; ++i)
cmds[i] = { 6, 1, 0, 4, 0 }; // the far quad
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> msaaTex(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, sampleCount,
QRhiTexture::RenderTarget));
QVERIFY(msaaTex->create());
std::unique_ptr<QRhiTexture> resolveTex(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(resolveTex->create());
std::unique_ptr<QRhiTexture> msaaDepthTex(
rhi->newTexture(QRhiTexture::D32F, outputSize, sampleCount,
QRhiTexture::RenderTarget));
QVERIFY(msaaDepthTex->create());
std::unique_ptr<QRhiTexture> depthResolveTex(
rhi->newTexture(QRhiTexture::D32F, outputSize, 1, QRhiTexture::RenderTarget));
QVERIFY(depthResolveTex->create());
QRhiColorAttachment colorAtt(msaaTex.get());
colorAtt.setResolveTexture(resolveTex.get());
QRhiTextureRenderTargetDescription rtDesc(colorAtt);
rtDesc.setDepthTexture(msaaDepthTex.get());
rtDesc.setDepthResolveTexture(depthResolveTex.get());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(rtDesc));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(Vertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), Vertices);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
ps->setSampleCount(sampleCount);
ps->setDepthTest(true);
ps->setDepthWrite(true);
ps->setDepthOp(QRhiGraphicsPipeline::Less);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
std::unique_ptr<QRhiBuffer> db(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndirectBuffer, sizeof(cmds)));
QVERIFY(db->create());
u->uploadStaticBuffer(db.get(), cmds);
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
// Near quad on the left, writing depth, before the interruption.
cb->drawIndexed(6);
// Farther, full width quad. Must be rejected where the near quad already is.
cb->drawIndexedIndirect(db.get(), 0, DrawCount);
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ resolveTex.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), resolveTex->pixelSize());
const int y = result.height() / 2;
const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
int redCount = 0, greenCount = 0, otherCount = 0;
const int maxFuzz = 1;
for (int x = 0; x < result.width(); ++x) {
const QRgb c(p[x]);
if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
else if (qRed(c) == 0 && qGreen(c) >= (255 - maxFuzz) && qBlue(c) == 0)
++greenCount;
else
++otherCount;
}
// Left half kept by the near quad, right half taken by the farther one.
QCOMPARE(redCount, 64);
QCOMPARE(greenCount, 64);
QCOMPARE(otherCount, 0);
}
void tst_QRhi::indexedIndirectMultiDrawHighDrawCountRenderBufferDepth_data()
{
rhiTestData();
}
// Depth testing across a pass interruption with a QRhiRenderBuffer as the
// depth-stencil buffer, which is what QRhiSwapChain requires. Such a
// renderbuffer may be backed by transient memory, in which case its contents
// cannot survive the interruption; QRhiRenderBuffer::NoTransientBacking opts out
// of that. A near quad is drawn normally, then a farther, full width quad with a
// high draw count indirect draw, and the near one has to win the depth test.
void tst_QRhi::indexedIndirectMultiDrawHighDrawCountRenderBufferDepth()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing "
"indexedIndirectMultiDrawHighDrawCountRenderBufferDepth");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
// Near quad on the left, then a farther quad covering everything.
static constexpr IndirectTestVertex Vertices[] = {
{ -1.f, -1.f, -0.5f, 1.f, 0.f, 0.f, 1.f },
{ 0.f, -1.f, -0.5f, 1.f, 0.f, 0.f, 1.f },
{ 0.f, 1.f, -0.5f, 1.f, 0.f, 0.f, 1.f },
{ -1.f, 1.f, -0.5f, 1.f, 0.f, 0.f, 1.f },
{ -1.f, -1.f, 0.5f, 0.f, 1.f, 0.f, 1.f },
{ 1.f, -1.f, 0.5f, 0.f, 1.f, 0.f, 1.f },
{ 1.f, 1.f, 0.5f, 0.f, 1.f, 0.f, 1.f },
{ -1.f, 1.f, 0.5f, 0.f, 1.f, 0.f, 1.f },
};
static constexpr int DrawCount = 130; // above the Metal ICB threshold of 128
QRhiIndexedIndirectDrawCommand cmds[DrawCount];
for (int i = 0; i < DrawCount; ++i)
cmds[i] = { 6, 1, 0, 4, 0 }; // the far quad
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiRenderBuffer> dsBuf(
rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, outputSize, 1,
QRhiRenderBuffer::NoTransientBacking));
QVERIFY(dsBuf->create());
QRhiColorAttachment colorAtt(texture.get());
QRhiTextureRenderTargetDescription rtDesc(colorAtt);
rtDesc.setDepthStencilBuffer(dsBuf.get());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(rtDesc));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(Vertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), Vertices);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
ps->setDepthTest(true);
ps->setDepthWrite(true);
ps->setDepthOp(QRhiGraphicsPipeline::Less);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
std::unique_ptr<QRhiBuffer> db(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndirectBuffer, sizeof(cmds)));
QVERIFY(db->create());
u->uploadStaticBuffer(db.get(), cmds);
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
cb->drawIndexed(6);
cb->drawIndexedIndirect(db.get(), 0, DrawCount);
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), texture->pixelSize());
const int y = result.height() / 2;
const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
int redCount = 0, greenCount = 0, otherCount = 0;
const int maxFuzz = 1;
for (int x = 0; x < result.width(); ++x) {
const QRgb c(p[x]);
if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
else if (qRed(c) == 0 && qGreen(c) >= (255 - maxFuzz) && qBlue(c) == 0)
++greenCount;
else
++otherCount;
}
QCOMPARE(redCount, 64);
QCOMPARE(greenCount, 64);
QCOMPARE(otherCount, 0);
}
void tst_QRhi::indexedIndirectMultiDrawShaderDrawParams_data()
{
rhiTestData();
}
void tst_QRhi::indexedIndirectMultiDrawShaderDrawParams()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing rendering");
if (!rhi->isFeatureSupported(QRhi::DrawIndirect))
QSKIP("Indirect draw is not supported, skipping test");
if (!rhi->isFeatureSupported(QRhi::DrawIndirectMulti))
QSKIP("Indirect multi-draw is not supported, skipping test");
if (!rhi->isFeatureSupported(QRhi::ShaderDrawParameters))
QSKIP("gl_DrawID is not supported, skipping test");
// the Vulkan implementation advertises VK_KHR_shader_draw_parameters, but
// the SPIR-V DrawIndex builtin cannot be translated to MSL
if (isMoltenVk(rhi))
QSKIP("gl_DrawID is not supported by MoltenVK, skipping test");
static constexpr float quadVertices[] {
-1.0f, -1.0f, 0.0f, // Bottom-left
0.0f, -1.0f, 0.0f, // Bottom-right
0.0f, 1.0f, 0.0f, // Top-right
-1.0f, 1.0f, 0.0f, // Top-left
};
static constexpr uint16_t quadIndices[] {
0, 1, 2,
2, 3, 0,
};
static constexpr QRhiIndexedIndirectDrawCommand quadDrawCommands[] {
{ /* indexCount */ 6, /* instanceCount */ 1, /* firstIndex */ 0, /* vertexOffset */ 0, /* firstInstance */ 0 },
{ /* indexCount */ 6, /* instanceCount */ 1, /* firstIndex */ 0, /* vertexOffset */ 0, /* firstInstance */ 0 },
};
static constexpr float quadColors[] {
1.0f, 0.0f, 0.0f, 1.0f, // Red
0.0f, 1.0f, 0.0f, 1.0f, // Green
};
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *beginPassUpdates = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vertexBuffer(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(quadVertices)));
QVERIFY(vertexBuffer->create());
beginPassUpdates->uploadStaticBuffer(vertexBuffer.get(), quadVertices);
std::unique_ptr<QRhiBuffer> indexBuffer(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(quadIndices)));
QVERIFY(indexBuffer->create());
beginPassUpdates->uploadStaticBuffer(indexBuffer.get(), quadIndices);
// In this test case, we construct the draw commands on the CPU
std::unique_ptr<QRhiBuffer> indirectBuffer(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::IndirectBuffer, sizeof(quadDrawCommands)));
QVERIFY(indirectBuffer->create());
beginPassUpdates->updateDynamicBuffer(indirectBuffer.get(), 0, sizeof(quadDrawCommands), quadDrawCommands);
std::unique_ptr<QRhiBuffer> uniformBuffer(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 160));
QVERIFY(uniformBuffer->create());
beginPassUpdates->updateDynamicBuffer(uniformBuffer.get(), 0, 32, quadColors);
QMatrix4x4 quadMvp1 = rhi->clipSpaceCorrMatrix(); quadMvp1.translate(0.0, 0.0f, 0.0f);
beginPassUpdates->updateDynamicBuffer(uniformBuffer.get(), 32, 64, quadMvp1.constData());
QMatrix4x4 quadMvp2 = rhi->clipSpaceCorrMatrix(); quadMvp2.translate(1.0, 0.0f, 0.0f);
beginPassUpdates->updateDynamicBuffer(uniformBuffer.get(), 32+64, 64, quadMvp2.constData());
std::unique_ptr<QRhiShaderResourceBindings> shaderBindings(rhi->newShaderResourceBindings());
shaderBindings->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, uniformBuffer.get()),
});
QVERIFY(shaderBindings->create());
std::unique_ptr<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
QShader vs = loadShader(":/data/indirect_multi_draw_sdp.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(":/data/indirect_multi_draw_sdp.frag.qsb");
QVERIFY(fs.isValid());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout vertexLayout;
vertexLayout.setBindings({ { 3 * sizeof(float) } });
vertexLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float3, 0 } });
pipeline->setVertexInputLayout(vertexLayout);
pipeline->setShaderResourceBindings(shaderBindings.get());
pipeline->setRenderPassDescriptor(rpDesc.get());
QVERIFY(pipeline->create());
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, beginPassUpdates);
cb->setGraphicsPipeline(pipeline.get());
cb->setShaderResources(shaderBindings.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vertexBindings(vertexBuffer.get(), 0);
cb->setVertexInput(0, 1, &vertexBindings, indexBuffer.get());
cb->drawIndexedIndirect(indirectBuffer.get(), 0, 2);
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 *inPassUpdates = rhi->nextResourceUpdateBatch();
inPassUpdates->readBackTexture({ texture.get() }, &readResult);
cb->endPass(inPassUpdates);
frameGuard.endFrame();
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
// Convert to a byte-order independent format, otherwise the blue and red are swapped on little endian
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), texture->pixelSize());
if (impl == QRhi::Null)
return;
const int y = result.height() / 2; // Check a horizontal line in the middle
const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
int x = result.width();
int redCount = 0;
int greenCount = 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) >= (255 - maxFuzz) && qBlue(c) == 0)
++greenCount;
else if (qRed(c) == 0 && qGreen(c) == 0 && qBlue(c) >= (255 - maxFuzz))
++blueCount;
else
QFAIL(qPrintable(QStringLiteral("Unexpected color at x=%1, y=%2: %3,%4,%5")
.arg(x).arg(y).arg(qRed(c)).arg(qGreen(c)).arg(qBlue(c))));
}
QCOMPARE(redCount, 64); // We should see the left red quad (drawID == 0)
QCOMPARE(greenCount, 64); // We should see the right green quad (drawID == 1)
QCOMPARE(blueCount, 0); // We should not see the background
}
void tst_QRhi::dispatchIndirectBaseline_data()
{
rhiTestData();
}
void tst_QRhi::dispatchIndirectBaseline()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing dispatchIndirectBaseline");
if (!rhi->isFeatureSupported(QRhi::Compute))
QSKIP("Compute not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::DispatchIndirect))
QSKIP("Indirect compute dispatch not supported on this backend");
// Indirect dispatch arguments uploaded directly from the CPU. Groups
// must fit the 16-wide fixed stride encoded in the consumer shader.
static constexpr quint32 GroupsX = 4u;
static constexpr quint32 GroupsY = 3u;
static constexpr quint32 GroupsZ = 2u;
const QRhiDispatchIndirectCommand cmdArgs = { GroupsX, GroupsY, GroupsZ };
const quint32 expectedGroupCount = GroupsX * GroupsY * GroupsZ;
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> indirectBuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndirectBuffer,
sizeof(QRhiDispatchIndirectCommand)));
QVERIFY(indirectBuf->create());
u->uploadStaticBuffer(indirectBuf.get(), &cmdArgs);
// The consumer shader writes at slot (x + y*16 + z*16*16); size the
// output buffer to cover every slot in the 16^3 lattice, pre-filled
// with a sentinel so we can detect over- or under-dispatch.
static constexpr quint32 SlotStride = 16u;
static constexpr quint32 SlotCount = SlotStride * SlotStride * SlotStride;
QByteArray sentinel(int(SlotCount * sizeof(quint32)), 0);
quint32 *sentinelPtr = reinterpret_cast<quint32 *>(sentinel.data());
for (quint32 i = 0; i < SlotCount; ++i)
sentinelPtr[i] = 0xFFFFFFFFu;
std::unique_ptr<QRhiBuffer> outBuf(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer,
SlotCount * sizeof(quint32)));
QVERIFY(outBuf->create());
u->uploadStaticBuffer(outBuf.get(), sentinel.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, outBuf.get())
});
QVERIFY(srb->create());
std::unique_ptr<QRhiComputePipeline> ps(rhi->newComputePipeline());
const QShader cs = loadShader(":/data/dispatch_indirect_consume.comp.qsb");
QVERIFY(cs.isValid());
ps->setShaderStage({ QRhiShaderStage::Compute, cs });
ps->setShaderResourceBindings(srb.get());
QVERIFY(ps->create());
cb->beginComputePass(u);
cb->setComputePipeline(ps.get());
cb->setShaderResources(srb.get());
cb->dispatchIndirect(indirectBuf.get(), 0);
cb->endComputePass();
QRhiReadbackResult rb;
QByteArray result;
rb.completed = [&] { result = rb.data; };
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackBuffer(outBuf.get(), 0, SlotCount * sizeof(quint32), &rb);
cb->resourceUpdate(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
QCOMPARE(quint32(result.size()), SlotCount * sizeof(quint32));
const quint32 *p = reinterpret_cast<const quint32 *>(result.constData());
quint32 written = 0;
for (quint32 i = 0; i < SlotCount; ++i) {
if (p[i] != 0xFFFFFFFFu)
++written;
}
QCOMPARE(written, expectedGroupCount);
// Verify that every (x, y, z) work group inside the dispatch grid wrote
// the expected encoding at its own slot and that no other slot was
// touched.
for (quint32 z = 0; z < SlotStride; ++z) {
for (quint32 y = 0; y < SlotStride; ++y) {
for (quint32 x = 0; x < SlotStride; ++x) {
const quint32 idx = x + y * SlotStride + z * SlotStride * SlotStride;
const bool inGrid = x < GroupsX && y < GroupsY && z < GroupsZ;
if (inGrid) {
const quint32 expected = (z << 20) | (y << 10) | x;
QCOMPARE(p[idx], expected);
} else {
QCOMPARE(p[idx], 0xFFFFFFFFu);
}
}
}
}
}
void tst_QRhi::computeAfterBufferClear_data()
{
rhiTestData();
}
void tst_QRhi::computeAfterBufferClear()
{
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 compute with buffer clears");
if (!rhi->isFeatureSupported(QRhi::Compute))
QSKIP("Compute is not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::ReadBackNonUniformBuffer))
QSKIP("Reading back non-uniform buffers is not supported, skipping testing compute with buffer clears");
if (impl == QRhi::Vulkan && isAndroidSwiftShader(rhi.get()))
QSKIP("SwiftShader renders and reads back unreliably (QTBUG-146930)");
// Work group x writes x to element x.
const QShader cs = loadShader(":/data/dispatch_indirect_consume.comp.qsb");
QVERIFY(cs.isValid());
const quint32 elementCount = 16;
QScopedPointer<QRhiBuffer> buf(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer,
elementCount * sizeof(quint32)));
QVERIFY(buf->create());
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, buf.data())
});
QVERIFY(srb->create());
QScopedPointer<QRhiComputePipeline> ps(rhi->newComputePipeline());
ps->setShaderStage({ QRhiShaderStage::Compute, cs });
ps->setShaderResourceBindings(srb.data());
QVERIFY(ps->create());
QRhiCommandBuffer *cb = nullptr;
QCOMPARE(rhi->beginOffscreenFrame(&cb), QRhi::FrameOpSuccess);
QVERIFY(cb);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
u->uploadStaticBuffer(buf.data(), QByteArray(int(elementCount * sizeof(quint32)), 0).constData());
// A clear between two dispatches has to be ordered after the writes of the
// first one, and before the writes of the second one.
cb->beginComputePass(u);
cb->setComputePipeline(ps.data());
cb->setShaderResources(srb.data());
cb->dispatch(4, 1, 1);
u = rhi->nextResourceUpdateBatch();
u->clearStorageBuffer(buf.data(), 0xFF);
cb->endComputePass(u);
cb->beginComputePass();
cb->setComputePipeline(ps.data());
cb->setShaderResources(srb.data());
cb->dispatch(2, 1, 1);
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
u = rhi->nextResourceUpdateBatch();
u->readBackBuffer(buf.data(), 0, elementCount * sizeof(quint32), &readResult);
cb->endComputePass(u);
rhi->endOffscreenFrame();
if (impl == QRhi::Null)
return;
QVERIFY(readCompleted);
QCOMPARE(quint32(readResult.data.size()), elementCount * sizeof(quint32));
const quint32 *p = reinterpret_cast<const quint32 *>(readResult.data.constData());
QCOMPARE(p[0], 0u);
QCOMPARE(p[1], 1u);
for (quint32 i = 2; i < elementCount; ++i)
QCOMPARE(p[i], 0xFFFFFFFFu);
}
void tst_QRhi::dispatchIndirectFromCompute_data()
{
rhiTestData();
}
void tst_QRhi::dispatchIndirectFromCompute()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing dispatchIndirectFromCompute");
if (!rhi->isFeatureSupported(QRhi::Compute))
QSKIP("Compute not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::DispatchIndirect))
QSKIP("Indirect compute dispatch not supported on this backend");
// First compute pass: a single-thread shader writes the dispatch
// arguments into the indirect buffer based on a uniform. Second compute
// pass: dispatchIndirect() consumes those arguments. Validates GPU-side
// handoff between the two passes (in particular the write -> indirect
// read barrier).
// Groups must fit the 16-wide stride encoded in the consumer shader.
static constexpr quint32 GroupsX = 5u;
static constexpr quint32 GroupsY = 2u;
static constexpr quint32 GroupsZ = 3u;
const quint32 expectedGroupCount = GroupsX * GroupsY * GroupsZ;
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
// The same buffer is used as a storage buffer (write-target for the
// first compute pass) and as the indirect dispatch buffer (read by the
// device for the second compute pass).
static constexpr QRhiDispatchIndirectCommand zeroCmd = { 0u, 0u, 0u };
std::unique_ptr<QRhiBuffer> indirectBuf(rhi->newBuffer(QRhiBuffer::Static,
QRhiBuffer::StorageBuffer | QRhiBuffer::IndirectBuffer,
sizeof(QRhiDispatchIndirectCommand)));
QVERIFY(indirectBuf->create());
u->uploadStaticBuffer(indirectBuf.get(), &zeroCmd);
struct ArgsUbuf {
quint32 groupsX;
quint32 groupsY;
quint32 groupsZ;
quint32 _pad;
};
const ArgsUbuf argsUbufData = { GroupsX, GroupsY, GroupsZ, 0u };
std::unique_ptr<QRhiBuffer> argsUbuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer,
sizeof(ArgsUbuf)));
QVERIFY(argsUbuf->create());
u->updateDynamicBuffer(argsUbuf.get(), 0, sizeof(ArgsUbuf), &argsUbufData);
static constexpr quint32 SlotStride = 16u;
static constexpr quint32 SlotCount = SlotStride * SlotStride * SlotStride;
QByteArray sentinel(int(SlotCount * sizeof(quint32)), 0);
quint32 *sentinelPtr = reinterpret_cast<quint32 *>(sentinel.data());
for (quint32 i = 0; i < SlotCount; ++i)
sentinelPtr[i] = 0xFFFFFFFFu;
std::unique_ptr<QRhiBuffer> outBuf(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer,
SlotCount * sizeof(quint32)));
QVERIFY(outBuf->create());
u->uploadStaticBuffer(outBuf.get(), sentinel.constData());
// Pipeline #1: writes the indirect command into indirectBuf.
std::unique_ptr<QRhiShaderResourceBindings> writeSrb(rhi->newShaderResourceBindings());
writeSrb->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, indirectBuf.get()),
QRhiShaderResourceBinding::uniformBuffer(1, QRhiShaderResourceBinding::ComputeStage, argsUbuf.get())
});
QVERIFY(writeSrb->create());
std::unique_ptr<QRhiComputePipeline> writePs(rhi->newComputePipeline());
const QShader writeCs = loadShader(":/data/dispatch_indirect_args.comp.qsb");
QVERIFY(writeCs.isValid());
writePs->setShaderStage({ QRhiShaderStage::Compute, writeCs });
writePs->setShaderResourceBindings(writeSrb.get());
QVERIFY(writePs->create());
// Pipeline #2: indirect-dispatched consumer that fills outBuf.
std::unique_ptr<QRhiShaderResourceBindings> consumeSrb(rhi->newShaderResourceBindings());
consumeSrb->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, outBuf.get())
});
QVERIFY(consumeSrb->create());
std::unique_ptr<QRhiComputePipeline> consumePs(rhi->newComputePipeline());
const QShader consumeCs = loadShader(":/data/dispatch_indirect_consume.comp.qsb");
QVERIFY(consumeCs.isValid());
consumePs->setShaderStage({ QRhiShaderStage::Compute, consumeCs });
consumePs->setShaderResourceBindings(consumeSrb.get());
QVERIFY(consumePs->create());
cb->beginComputePass(u);
cb->setComputePipeline(writePs.get());
cb->setShaderResources(writeSrb.get());
cb->dispatch(1, 1, 1);
cb->endComputePass();
cb->beginComputePass();
cb->setComputePipeline(consumePs.get());
cb->setShaderResources(consumeSrb.get());
cb->dispatchIndirect(indirectBuf.get(), 0);
cb->endComputePass();
QRhiReadbackResult rb;
QByteArray result;
rb.completed = [&] { result = rb.data; };
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackBuffer(outBuf.get(), 0, SlotCount * sizeof(quint32), &rb);
cb->resourceUpdate(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
QCOMPARE(quint32(result.size()), SlotCount * sizeof(quint32));
const quint32 *p = reinterpret_cast<const quint32 *>(result.constData());
quint32 written = 0;
for (quint32 i = 0; i < SlotCount; ++i) {
if (p[i] != 0xFFFFFFFFu)
++written;
}
QCOMPARE(written, expectedGroupCount);
for (quint32 z = 0; z < SlotStride; ++z) {
for (quint32 y = 0; y < SlotStride; ++y) {
for (quint32 x = 0; x < SlotStride; ++x) {
const quint32 idx = x + y * SlotStride + z * SlotStride * SlotStride;
const bool inGrid = x < GroupsX && y < GroupsY && z < GroupsZ;
if (inGrid) {
const quint32 expected = (z << 20) | (y << 10) | x;
QCOMPARE(p[idx], expected);
} else {
QCOMPARE(p[idx], 0xFFFFFFFFu);
}
}
}
}
}
void tst_QRhi::bufferReadbackIsSnapshot_data()
{
rhiTestData();
}
void tst_QRhi::bufferReadbackIsSnapshot()
{
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 bufferReadbackIsSnapshot");
if (!rhi->isFeatureSupported(QRhi::Compute))
QSKIP("Compute not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::ReadBackNonUniformBuffer))
QSKIP("Reading buffers with non-uniform usage is not supported on this backend");
// readBackBuffer() must capture the contents the buffer has at the point
// the batch is executed, not whatever happens to be in it when the
// asynchronous readback completes. Two compute passes write different
// values into the same buffer, with the readback enqueued in between; the
// result has to be the first value.
//
// A StorageBuffer is deliberately used: backends are free to keep one
// native buffer per frame slot for other usages, and it is the single,
// shared native buffer that makes a deferred read observe later writes.
struct ArgsUbuf {
quint32 x;
quint32 y;
quint32 z;
quint32 _pad;
};
static constexpr ArgsUbuf firstData = { 11u, 22u, 33u, 0u };
static constexpr ArgsUbuf secondData = { 44u, 55u, 66u, 0u };
static constexpr quint32 OutSize = 3 * sizeof(quint32);
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi.data());
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> outBuf(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer,
OutSize));
QVERIFY(outBuf->create());
const QByteArray zeros(int(OutSize), 0);
u->uploadStaticBuffer(outBuf.get(), 0, OutSize, zeros.constData());
std::unique_ptr<QRhiBuffer> firstUbuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer,
sizeof(ArgsUbuf)));
QVERIFY(firstUbuf->create());
u->updateDynamicBuffer(firstUbuf.get(), 0, sizeof(ArgsUbuf), &firstData);
std::unique_ptr<QRhiBuffer> secondUbuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer,
sizeof(ArgsUbuf)));
QVERIFY(secondUbuf->create());
u->updateDynamicBuffer(secondUbuf.get(), 0, sizeof(ArgsUbuf), &secondData);
// The shader writes three uints taken from a uniform buffer, so the same
// pipeline can produce either value depending on the bindings used.
std::unique_ptr<QRhiShaderResourceBindings> firstSrb(rhi->newShaderResourceBindings());
firstSrb->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, outBuf.get()),
QRhiShaderResourceBinding::uniformBuffer(1, QRhiShaderResourceBinding::ComputeStage, firstUbuf.get())
});
QVERIFY(firstSrb->create());
std::unique_ptr<QRhiShaderResourceBindings> secondSrb(rhi->newShaderResourceBindings());
secondSrb->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, outBuf.get()),
QRhiShaderResourceBinding::uniformBuffer(1, QRhiShaderResourceBinding::ComputeStage, secondUbuf.get())
});
QVERIFY(secondSrb->create());
std::unique_ptr<QRhiComputePipeline> ps(rhi->newComputePipeline());
const QShader cs = loadShader(":/data/dispatch_indirect_args.comp.qsb");
QVERIFY(cs.isValid());
ps->setShaderStage({ QRhiShaderStage::Compute, cs });
ps->setShaderResourceBindings(firstSrb.get());
QVERIFY(ps->create());
cb->beginComputePass(u);
cb->setComputePipeline(ps.get());
cb->setShaderResources(firstSrb.get());
cb->dispatch(1, 1, 1);
cb->endComputePass();
QRhiReadbackResult rb;
QByteArray result;
rb.completed = [&] { result = rb.data; };
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackBuffer(outBuf.get(), 0, OutSize, &rb);
cb->resourceUpdate(u2);
// Overwrites the buffer after the readback was enqueued. A backend that
// defers the copy to completion time reports these values instead.
cb->beginComputePass();
cb->setComputePipeline(ps.get());
cb->setShaderResources(secondSrb.get());
cb->dispatch(1, 1, 1);
cb->endComputePass();
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
QCOMPARE(quint32(result.size()), OutSize);
const quint32 *p = reinterpret_cast<const quint32 *>(result.constData());
QCOMPARE(p[0], firstData.x);
QCOMPARE(p[1], firstData.y);
QCOMPARE(p[2], firstData.z);
}
void tst_QRhi::dispatchIndirectFromComputeSamePass_data()
{
rhiTestData();
}
void tst_QRhi::dispatchIndirectFromComputeSamePass()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing dispatchIndirectFromComputeSamePass");
if (!rhi->isFeatureSupported(QRhi::Compute))
QSKIP("Compute not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::DispatchIndirect))
QSKIP("Indirect compute dispatch not supported on this backend");
// Like dispatchIndirectFromCompute, but with the producing dispatch and the
// consuming dispatchIndirect() in the same compute pass, so that the
// indirect buffer is used both as a storage buffer and as the source of the
// work group counts within one pass.
static constexpr quint32 GroupsX = 5u;
static constexpr quint32 GroupsY = 2u;
static constexpr quint32 GroupsZ = 3u;
const quint32 expectedGroupCount = GroupsX * GroupsY * GroupsZ;
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
static constexpr QRhiDispatchIndirectCommand zeroCmd = { 0u, 0u, 0u };
std::unique_ptr<QRhiBuffer> indirectBuf(rhi->newBuffer(QRhiBuffer::Static,
QRhiBuffer::StorageBuffer | QRhiBuffer::IndirectBuffer,
sizeof(QRhiDispatchIndirectCommand)));
QVERIFY(indirectBuf->create());
u->uploadStaticBuffer(indirectBuf.get(), &zeroCmd);
struct ArgsUbuf {
quint32 groupsX;
quint32 groupsY;
quint32 groupsZ;
quint32 _pad;
};
const ArgsUbuf argsUbufData = { GroupsX, GroupsY, GroupsZ, 0u };
std::unique_ptr<QRhiBuffer> argsUbuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer,
sizeof(ArgsUbuf)));
QVERIFY(argsUbuf->create());
u->updateDynamicBuffer(argsUbuf.get(), 0, sizeof(ArgsUbuf), &argsUbufData);
static constexpr quint32 SlotStride = 16u;
static constexpr quint32 SlotCount = SlotStride * SlotStride * SlotStride;
QByteArray sentinel(int(SlotCount * sizeof(quint32)), 0);
quint32 *sentinelPtr = reinterpret_cast<quint32 *>(sentinel.data());
for (quint32 i = 0; i < SlotCount; ++i)
sentinelPtr[i] = 0xFFFFFFFFu;
std::unique_ptr<QRhiBuffer> outBuf(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer,
SlotCount * sizeof(quint32)));
QVERIFY(outBuf->create());
u->uploadStaticBuffer(outBuf.get(), sentinel.constData());
std::unique_ptr<QRhiShaderResourceBindings> writeSrb(rhi->newShaderResourceBindings());
writeSrb->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, indirectBuf.get()),
QRhiShaderResourceBinding::uniformBuffer(1, QRhiShaderResourceBinding::ComputeStage, argsUbuf.get())
});
QVERIFY(writeSrb->create());
std::unique_ptr<QRhiComputePipeline> writePs(rhi->newComputePipeline());
const QShader writeCs = loadShader(":/data/dispatch_indirect_args.comp.qsb");
QVERIFY(writeCs.isValid());
writePs->setShaderStage({ QRhiShaderStage::Compute, writeCs });
writePs->setShaderResourceBindings(writeSrb.get());
QVERIFY(writePs->create());
std::unique_ptr<QRhiShaderResourceBindings> consumeSrb(rhi->newShaderResourceBindings());
consumeSrb->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, outBuf.get())
});
QVERIFY(consumeSrb->create());
std::unique_ptr<QRhiComputePipeline> consumePs(rhi->newComputePipeline());
const QShader consumeCs = loadShader(":/data/dispatch_indirect_consume.comp.qsb");
QVERIFY(consumeCs.isValid());
consumePs->setShaderStage({ QRhiShaderStage::Compute, consumeCs });
consumePs->setShaderResourceBindings(consumeSrb.get());
QVERIFY(consumePs->create());
cb->beginComputePass(u);
cb->setComputePipeline(writePs.get());
cb->setShaderResources(writeSrb.get());
cb->dispatch(1, 1, 1);
cb->setComputePipeline(consumePs.get());
cb->setShaderResources(consumeSrb.get());
cb->dispatchIndirect(indirectBuf.get(), 0);
cb->endComputePass();
QRhiReadbackResult rb;
QByteArray result;
rb.completed = [&] { result = rb.data; };
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackBuffer(outBuf.get(), 0, SlotCount * sizeof(quint32), &rb);
cb->resourceUpdate(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
QCOMPARE(quint32(result.size()), SlotCount * sizeof(quint32));
const quint32 *p = reinterpret_cast<const quint32 *>(result.constData());
quint32 written = 0;
for (quint32 i = 0; i < SlotCount; ++i) {
if (p[i] != 0xFFFFFFFFu)
++written;
}
QCOMPARE(written, expectedGroupCount);
for (quint32 z = 0; z < SlotStride; ++z) {
for (quint32 y = 0; y < SlotStride; ++y) {
for (quint32 x = 0; x < SlotStride; ++x) {
const quint32 idx = x + y * SlotStride + z * SlotStride * SlotStride;
const bool inGrid = x < GroupsX && y < GroupsY && z < GroupsZ;
if (inGrid) {
const quint32 expected = (z << 20) | (y << 10) | x;
QCOMPARE(p[idx], expected);
} else {
QCOMPARE(p[idx], 0xFFFFFFFFu);
}
}
}
}
}
void tst_QRhi::dispatchIndirectConsumerReadsArgs_data()
{
rhiTestData();
}
void tst_QRhi::dispatchIndirectConsumerReadsArgs()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing dispatchIndirectConsumerReadsArgs");
if (!rhi->isFeatureSupported(QRhi::Compute))
QSKIP("Compute not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::DispatchIndirect))
QSKIP("Indirect compute dispatch not supported on this backend");
// Like dispatchIndirectFromCompute, but the consuming pipeline also binds the
// indirect buffer as a read-only storage buffer, so that it is used with two
// different accesses in that pass: a shader read via the SRB and an indirect
// command read via dispatchIndirect().
static constexpr quint32 GroupsX = 4u;
static constexpr quint32 GroupsY = 3u;
static constexpr quint32 GroupsZ = 2u;
const quint32 expectedGroupCount = GroupsX * GroupsY * GroupsZ;
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
static constexpr QRhiDispatchIndirectCommand zeroCmd = { 0u, 0u, 0u };
std::unique_ptr<QRhiBuffer> indirectBuf(rhi->newBuffer(QRhiBuffer::Static,
QRhiBuffer::StorageBuffer | QRhiBuffer::IndirectBuffer,
sizeof(QRhiDispatchIndirectCommand)));
QVERIFY(indirectBuf->create());
u->uploadStaticBuffer(indirectBuf.get(), &zeroCmd);
struct ArgsUbuf {
quint32 groupsX;
quint32 groupsY;
quint32 groupsZ;
quint32 _pad;
};
const ArgsUbuf argsUbufData = { GroupsX, GroupsY, GroupsZ, 0u };
std::unique_ptr<QRhiBuffer> argsUbuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer,
sizeof(ArgsUbuf)));
QVERIFY(argsUbuf->create());
u->updateDynamicBuffer(argsUbuf.get(), 0, sizeof(ArgsUbuf), &argsUbufData);
static constexpr quint32 SlotStride = 16u;
static constexpr quint32 SlotCount = SlotStride * SlotStride * SlotStride;
QByteArray sentinel(int(SlotCount * sizeof(quint32)), 0);
quint32 *sentinelPtr = reinterpret_cast<quint32 *>(sentinel.data());
for (quint32 i = 0; i < SlotCount; ++i)
sentinelPtr[i] = 0xFFFFFFFFu;
std::unique_ptr<QRhiBuffer> outBuf(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer,
SlotCount * sizeof(quint32)));
QVERIFY(outBuf->create());
u->uploadStaticBuffer(outBuf.get(), sentinel.constData());
std::unique_ptr<QRhiShaderResourceBindings> writeSrb(rhi->newShaderResourceBindings());
writeSrb->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, indirectBuf.get()),
QRhiShaderResourceBinding::uniformBuffer(1, QRhiShaderResourceBinding::ComputeStage, argsUbuf.get())
});
QVERIFY(writeSrb->create());
std::unique_ptr<QRhiComputePipeline> writePs(rhi->newComputePipeline());
const QShader writeCs = loadShader(":/data/dispatch_indirect_args.comp.qsb");
QVERIFY(writeCs.isValid());
writePs->setShaderStage({ QRhiShaderStage::Compute, writeCs });
writePs->setShaderResourceBindings(writeSrb.get());
QVERIFY(writePs->create());
std::unique_ptr<QRhiShaderResourceBindings> consumeSrb(rhi->newShaderResourceBindings());
consumeSrb->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, outBuf.get()),
QRhiShaderResourceBinding::bufferLoad(1, QRhiShaderResourceBinding::ComputeStage, indirectBuf.get())
});
QVERIFY(consumeSrb->create());
std::unique_ptr<QRhiComputePipeline> consumePs(rhi->newComputePipeline());
const QShader consumeCs = loadShader(":/data/dispatch_indirect_consume_readargs.comp.qsb");
QVERIFY(consumeCs.isValid());
consumePs->setShaderStage({ QRhiShaderStage::Compute, consumeCs });
consumePs->setShaderResourceBindings(consumeSrb.get());
QVERIFY(consumePs->create());
cb->beginComputePass(u);
cb->setComputePipeline(writePs.get());
cb->setShaderResources(writeSrb.get());
cb->dispatch(1, 1, 1);
cb->endComputePass();
cb->beginComputePass();
cb->setComputePipeline(consumePs.get());
cb->setShaderResources(consumeSrb.get());
cb->dispatchIndirect(indirectBuf.get(), 0);
cb->endComputePass();
QRhiReadbackResult rb;
QByteArray result;
rb.completed = [&] { result = rb.data; };
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackBuffer(outBuf.get(), 0, SlotCount * sizeof(quint32), &rb);
cb->resourceUpdate(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
QCOMPARE(quint32(result.size()), SlotCount * sizeof(quint32));
const quint32 *p = reinterpret_cast<const quint32 *>(result.constData());
quint32 written = 0;
for (quint32 i = 0; i < SlotCount; ++i) {
if (p[i] != 0xFFFFFFFFu)
++written;
}
QCOMPARE(written, expectedGroupCount);
for (quint32 z = 0; z < SlotStride; ++z) {
for (quint32 y = 0; y < SlotStride; ++y) {
for (quint32 x = 0; x < SlotStride; ++x) {
const quint32 idx = x + y * SlotStride + z * SlotStride * SlotStride;
const bool inGrid = x < GroupsX && y < GroupsY && z < GroupsZ;
if (inGrid) {
// Top bit set means the shader saw stale, all-zero arguments.
const quint32 expected = (z << 20) | (y << 10) | x;
QCOMPARE(p[idx], expected);
} else {
QCOMPARE(p[idx], 0xFFFFFFFFu);
}
}
}
}
}
void tst_QRhi::indexedIndirectDrawCountFromCompute_data()
{
rhiTestData();
}
// Same shape as indexedIndirectMultiDrawFromCompute (compute pass produces the
// indirect draw arguments that the next render pass consumes), but the actual
// number of draws is also chosen by the GPU and read by the device from a
// separate count buffer using drawIndexedIndirectCount(). Verifies that the
// count buffer is read fresh from the GPU and not from a stale upload.
void tst_QRhi::indexedIndirectDrawCountFromCompute()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing indexedIndirectDrawCountFromCompute");
if (!rhi->isFeatureSupported(QRhi::Compute))
QSKIP("Compute not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::DrawIndirectCount))
QSKIP("DrawIndirectCount not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> graphicsSrb(rhi->newShaderResourceBindings());
graphicsSrb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(graphicsSrb->create());
std::unique_ptr<QRhiGraphicsPipeline> graphicsPipeline(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
graphicsPipeline->setShaderStages({
{ QRhiShaderStage::Vertex, vs },
{ QRhiShaderStage::Fragment, fs }
});
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
graphicsPipeline->setVertexInputLayout(vlayout);
graphicsPipeline->setShaderResourceBindings(graphicsSrb.get());
graphicsPipeline->setRenderPassDescriptor(rpDesc.get());
// Required by the Metal backend, which implements the count variants
// via indirect command buffers. No effect on other backends.
graphicsPipeline->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
QVERIFY(graphicsPipeline->create());
// Indirect-arg buffer with room for 2 draws; both will be filled by the
// first compute pass. Then the count buffer (filled by a separate compute
// pass) tells the GPU how many of those to actually issue.
static constexpr QRhiIndexedIndirectDrawCommand EmptyCommands[2] = {};
std::unique_ptr<QRhiBuffer> argsBuf(rhi->newBuffer(QRhiBuffer::Static,
QRhiBuffer::StorageBuffer | QRhiBuffer::IndirectBuffer,
sizeof(EmptyCommands)));
QVERIFY(argsBuf->create());
u->uploadStaticBuffer(argsBuf.get(), EmptyCommands);
static constexpr quint32 zeroCount = 0u;
std::unique_ptr<QRhiBuffer> countBuf(rhi->newBuffer(QRhiBuffer::Static,
QRhiBuffer::StorageBuffer | QRhiBuffer::IndirectBuffer,
sizeof(quint32)));
QVERIFY(countBuf->create());
u->uploadStaticBuffer(countBuf.get(), &zeroCount);
// The compute shader that fills the indirect args buffer is the same one
// used by the existing indexedIndirectMultiDrawFromCompute test.
std::unique_ptr<QRhiShaderResourceBindings> argsSrb(rhi->newShaderResourceBindings());
argsSrb->setBindings({ QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, argsBuf.get()) });
QVERIFY(argsSrb->create());
std::unique_ptr<QRhiComputePipeline> argsPipeline(rhi->newComputePipeline());
const QShader argsCs = loadShader(":/data/indirect_draw_args.comp.qsb");
QVERIFY(argsCs.isValid());
argsPipeline->setShaderStage({ QRhiShaderStage::Compute, argsCs });
argsPipeline->setShaderResourceBindings(argsSrb.get());
QVERIFY(argsPipeline->create());
// Count writer: pass a uniform that picks how many draws to actually
// issue (1 in this run -> only the first quad should appear).
struct CountUbuf { quint32 value; quint32 _pad[3]; };
const CountUbuf countUbufData = { 1u, {0, 0, 0} };
std::unique_ptr<QRhiBuffer> countUbuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer,
sizeof(CountUbuf)));
QVERIFY(countUbuf->create());
u->updateDynamicBuffer(countUbuf.get(), 0, sizeof(CountUbuf), &countUbufData);
std::unique_ptr<QRhiShaderResourceBindings> countSrb(rhi->newShaderResourceBindings());
countSrb->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, countBuf.get()),
QRhiShaderResourceBinding::uniformBuffer(1, QRhiShaderResourceBinding::ComputeStage, countUbuf.get())
});
QVERIFY(countSrb->create());
std::unique_ptr<QRhiComputePipeline> countPipeline(rhi->newComputePipeline());
const QShader countCs = loadShader(":/data/indirect_draw_count.comp.qsb");
QVERIFY(countCs.isValid());
countPipeline->setShaderStage({ QRhiShaderStage::Compute, countCs });
countPipeline->setShaderResourceBindings(countSrb.get());
QVERIFY(countPipeline->create());
cb->beginComputePass(u);
cb->setComputePipeline(argsPipeline.get());
cb->setShaderResources(argsSrb.get());
cb->dispatch(2, 1, 1);
cb->endComputePass();
cb->beginComputePass();
cb->setComputePipeline(countPipeline.get());
cb->setShaderResources(countSrb.get());
cb->dispatch(1, 1, 1);
cb->endComputePass();
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, nullptr);
cb->setGraphicsPipeline(graphicsPipeline.get());
cb->setShaderResources(graphicsSrb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
// maxDrawCount is 2 (the upper bound), but the count buffer holds 1 -> only
// the first (left, red) quad is drawn. The right (green) quad must NOT
// appear; that's the unique observable signal that proves the count was
// actually read from the GPU buffer rather than treated as maxDrawCount.
cb->drawIndexedIndirectCount(argsBuf.get(), 0, countBuf.get(), 0, 2u);
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), texture->pixelSize());
const int y = result.height() / 2;
const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
int redCount = 0, greenCount = 0, blueCount = 0;
const int maxFuzz = 1;
for (int x = 0; x < result.width(); ++x) {
const QRgb c(p[x]);
if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
else if (qRed(c) == 0 && qGreen(c) >= (255 - maxFuzz) && qBlue(c) == 0)
++greenCount;
else if (qRed(c) == 0 && qGreen(c) == 0 && qBlue(c) >= (255 - maxFuzz))
++blueCount;
else
QFAIL(qPrintable(QStringLiteral("Unexpected color at x=%1 y=%2: %3,%4,%5")
.arg(x).arg(y).arg(qRed(c)).arg(qGreen(c)).arg(qBlue(c))));
}
// The left quad (red, drawn) covers x = 0..63; the right half should
// remain at the clear colour because only 1 of the 2 indirect commands
// was issued.
QCOMPARE(redCount, 64);
QCOMPARE(greenCount, 0);
QCOMPARE(blueCount, 64);
}
void tst_QRhi::indexedIndirectDrawCountCustomStride_data()
{
rhiTestData();
}
// Same as indexedIndirectDrawCountFromCompute, but the indirect commands are
// spaced wider than sizeof(QRhiIndexedIndirectDrawCommand) and the count comes
// from a CPU-filled buffer. Backends that need a stride-specific object to
// describe the indirect layout (D3D12 command signatures) take a separate path
// here.
//
// Two of the three commands are issued. The second quad only shows up if the
// custom stride is honored when fetching the second command, while the third
// command, which would cover everything, must not be issued at all because the
// count buffer says 2.
void tst_QRhi::indexedIndirectDrawCountCustomStride()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing indexedIndirectDrawCountCustomStride");
if (!rhi->isFeatureSupported(QRhi::DrawIndirectCount))
QSKIP("DrawIndirectCount not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
struct PaddedDrawCommand {
QRhiIndexedIndirectDrawCommand cmd;
quint32 pad[3]; // stride != sizeof(QRhiIndexedIndirectDrawCommand)
};
static constexpr PaddedDrawCommand IndirectDrawCommands[] = {
{ { 6, 1, 0, 0, 0 }, { 0xBEEFCAFE, 0xDEADCAFE, 0xF00DCAFE }, }, // vertexOffset = 0 -> left quad (red)
{ { 6, 1, 0, 4, 0 }, { 0xBEEFCAFE, 0xDEADCAFE, 0xF00DCAFE }, }, // vertexOffset = 4 -> right quad (green)
{ { 6, 1, 0, 8, 0 }, { 0xBEEFCAFE, 0xDEADCAFE, 0xF00DCAFE }, }, // vertexOffset = 8 -> full width quad (white)
};
static constexpr quint32 DrawCount = 2; // the white quad must not be drawn
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
// Required by the Metal backend, which implements the count variants
// via indirect command buffers. No effect on other backends.
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
QVERIFY(ps->create());
std::unique_ptr<QRhiBuffer> argsBuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndirectBuffer,
sizeof(IndirectDrawCommands)));
QVERIFY(argsBuf->create());
u->uploadStaticBuffer(argsBuf.get(), IndirectDrawCommands);
std::unique_ptr<QRhiBuffer> countBuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndirectBuffer,
sizeof(quint32)));
QVERIFY(countBuf->create());
u->uploadStaticBuffer(countBuf.get(), &DrawCount);
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
cb->drawIndexedIndirectCount(argsBuf.get(), 0, countBuf.get(), 0,
quint32(std::size(IndirectDrawCommands)),
sizeof(PaddedDrawCommand));
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), texture->pixelSize());
const int y = result.height() / 2;
const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
int redCount = 0, greenCount = 0, blueCount = 0;
const int maxFuzz = 1;
for (int x = 0; x < result.width(); ++x) {
const QRgb c(p[x]);
if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
else if (qRed(c) == 0 && qGreen(c) >= (255 - maxFuzz) && qBlue(c) == 0)
++greenCount;
else if (qRed(c) == 0 && qGreen(c) == 0 && qBlue(c) >= (255 - maxFuzz))
++blueCount;
else
QFAIL(qPrintable(QStringLiteral("Unexpected color at x=%1 y=%2: %3,%4,%5")
.arg(x).arg(y).arg(qRed(c)).arg(qGreen(c)).arg(qBlue(c))));
}
QCOMPARE(redCount, 64); // Left red quad, from the first command
QCOMPARE(greenCount, 64); // Right green quad, only reachable via the stride
QCOMPARE(blueCount, 0); // No background left visible
}
void tst_QRhi::indirectDrawCountCustomStride_data()
{
rhiTestData();
}
// Non-indexed counterpart of indexedIndirectDrawCountCustomStride, exercising
// drawIndirectCount() and, on D3D12, the non-indexed per-stride command
// signature. Two of the three commands are issued, so the result proves both
// that the custom stride is used when fetching the second command and that the
// count buffer, not maxDrawCount, decides how many commands run.
void tst_QRhi::indirectDrawCountCustomStride()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing indirectDrawCountCustomStride");
if (!rhi->isFeatureSupported(QRhi::DrawIndirectCount))
QSKIP("DrawIndirectCount not supported on this backend");
struct PaddedDrawCommand {
QRhiIndirectDrawCommand cmd;
quint32 pad[3]; // stride != sizeof(QRhiIndirectDrawCommand)
};
static constexpr PaddedDrawCommand IndirectDrawCommands[] = {
{ { 6, 1, 0, 0 }, { 0xBEEFCAFE, 0xDEADCAFE, 0xF00DCAFE }, }, // firstVertex = 0 -> left quad (red)
{ { 6, 1, 6, 0 }, { 0xBEEFCAFE, 0xDEADCAFE, 0xF00DCAFE }, }, // firstVertex = 6 -> right quad (green)
{ { 6, 1, 12, 0 }, { 0xBEEFCAFE, 0xDEADCAFE, 0xF00DCAFE }, }, // firstVertex = 12 -> full width quad (white)
};
static constexpr quint32 DrawCount = 2; // the white quad must not be drawn
const QSize outputSize(128, 64);
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer,
sizeof(IndirectTestVerticesNonIndexed)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVerticesNonIndexed);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
// Required by the Metal backend, which implements the count variants
// via indirect command buffers. No effect on other backends.
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
QVERIFY(ps->create());
std::unique_ptr<QRhiBuffer> argsBuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndirectBuffer,
sizeof(IndirectDrawCommands)));
QVERIFY(argsBuf->create());
u->uploadStaticBuffer(argsBuf.get(), IndirectDrawCommands);
std::unique_ptr<QRhiBuffer> countBuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndirectBuffer,
sizeof(quint32)));
QVERIFY(countBuf->create());
u->uploadStaticBuffer(countBuf.get(), &DrawCount);
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput);
cb->drawIndirectCount(argsBuf.get(), 0, countBuf.get(), 0,
quint32(std::size(IndirectDrawCommands)),
sizeof(PaddedDrawCommand));
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), texture->pixelSize());
const int y = result.height() / 2;
const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
int redCount = 0, greenCount = 0, blueCount = 0;
const int maxFuzz = 1;
for (int x = 0; x < result.width(); ++x) {
const QRgb c(p[x]);
if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
else if (qRed(c) == 0 && qGreen(c) >= (255 - maxFuzz) && qBlue(c) == 0)
++greenCount;
else if (qRed(c) == 0 && qGreen(c) == 0 && qBlue(c) >= (255 - maxFuzz))
++blueCount;
else
QFAIL(qPrintable(QStringLiteral("Unexpected color at x=%1 y=%2: %3,%4,%5")
.arg(x).arg(y).arg(qRed(c)).arg(qGreen(c)).arg(qBlue(c))));
}
QCOMPARE(redCount, 64); // Left red quad, from the first command
QCOMPARE(greenCount, 64); // Right green quad, only reachable via the stride
QCOMPARE(blueCount, 0); // No background left visible
}
void tst_QRhi::indexedIndirectDrawCountMultisample_data()
{
rhiTestData();
}
// drawIndexedIndirectCount() into a multisample render target. On Metal this
// always interrupts the render pass, so the normal draw preceding it must
// survive, just like in indexedIndirectMultiDrawHighDrawCountMultisample.
void tst_QRhi::indexedIndirectDrawCountMultisample()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing "
"indexedIndirectDrawCountMultisample");
if (!rhi->isFeatureSupported(QRhi::DrawIndirectCount))
QSKIP("DrawIndirectCount not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::BaseVertex))
QSKIP("Base vertex not supported on this backend");
if (!rhi->isFeatureSupported(QRhi::MultisampleRenderBuffer))
QSKIP("Multisample renderbuffer not supported on this backend");
const int sampleCount = 4;
// Two commands, but only the first one may be issued.
static constexpr QRhiIndexedIndirectDrawCommand IndirectDrawCommands[] = {
{ 6, 1, 0, 4, 0 }, // right quad (green)
{ 6, 1, 0, 0, 0 }, // left quad (red), must not overdraw
};
static constexpr quint32 DrawCount = 1;
const QSize outputSize(128, 64);
std::unique_ptr<QRhiRenderBuffer> msaaBuf(rhi->newRenderBuffer(QRhiRenderBuffer::Color, outputSize, sampleCount));
QVERIFY(msaaBuf->create());
std::unique_ptr<QRhiTexture> resolveTex(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(resolveTex->create());
QRhiColorAttachment colorAtt(msaaBuf.get());
colorAtt.setResolveTexture(resolveTex.get());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ colorAtt }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(IndirectTestVertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), IndirectTestVertices);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer, sizeof(IndirectTestIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), IndirectTestIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout vlayout;
vlayout.setBindings({ { sizeof(IndirectTestVertex) } });
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, offsetof(IndirectTestVertex, x) },
{ 0, 1, QRhiVertexInputAttribute::Float4, offsetof(IndirectTestVertex, r) },
});
ps->setVertexInputLayout(vlayout);
ps->setFlags(QRhiGraphicsPipeline::UsesIndirectDraws);
ps->setSampleCount(sampleCount);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
std::unique_ptr<QRhiBuffer> argsBuf(
rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndirectBuffer,
sizeof(IndirectDrawCommands)));
QVERIFY(argsBuf->create());
u->uploadStaticBuffer(argsBuf.get(), IndirectDrawCommands);
std::unique_ptr<QRhiBuffer> countBuf(
rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndirectBuffer, sizeof(quint32)));
QVERIFY(countBuf->create());
u->uploadStaticBuffer(countBuf.get(), &DrawCount);
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
const QRhiCommandBuffer::VertexInput vinput(vb.get(), 0);
cb->setVertexInput(0, 1, &vinput, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
// Left (red) quad with a normal draw call, before the pass interruption.
cb->drawIndexed(6);
// Right (green) quad from the first indirect command only.
cb->drawIndexedIndirectCount(argsBuf.get(), 0, countBuf.get(), 0,
quint32(std::size(IndirectDrawCommands)));
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ resolveTex.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), resolveTex->pixelSize());
const int y = result.height() / 2;
const quint32 *p = reinterpret_cast<const quint32 *>(result.constScanLine(y));
int redCount = 0, greenCount = 0, otherCount = 0;
const int maxFuzz = 1;
for (int x = 0; x < result.width(); ++x) {
const QRgb c(p[x]);
if (qRed(c) >= (255 - maxFuzz) && qGreen(c) == 0 && qBlue(c) == 0)
++redCount;
else if (qRed(c) == 0 && qGreen(c) >= (255 - maxFuzz) && qBlue(c) == 0)
++greenCount;
else
++otherCount;
}
QCOMPARE(redCount, 64);
QCOMPARE(greenCount, 64);
QCOMPARE(otherCount, 0);
}
void tst_QRhi::baseInstanceDraw_data()
{
rhiTestData();
}
// Draws with instanceCount 1 and a non-zero firstInstance, with the color fed
// from a per-instance vertex buffer. The base instance must be taken into
// account when fetching the per-instance attribute, so the color at the base
// instance, not the first one, is expected in the result.
//
// Three draws into three separate vertical bands, one per code path: indexed,
// non-indexed, and indexed with a non-zero base vertex. On OpenGL these map to
// glDrawElementsInstancedBaseInstance, glDrawArraysInstancedBaseInstance and
// glDrawElementsInstancedBaseVertexBaseInstance, respectively.
void tst_QRhi::baseInstanceDraw()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing baseInstanceDraw");
if (!rhi->isFeatureSupported(QRhi::BaseInstance))
QSKIP("Base instance not supported on this backend");
const bool baseVertexSupported = rhi->isFeatureSupported(QRhi::BaseVertex);
// Two triangle lists: vertices 0-5 are a fullscreen quad, vertices 6-11
// cover the left half only. A base vertex of 6 therefore has a visible
// effect, and is not just a different way of drawing the same thing.
static constexpr float QuadVertices[] = {
-1.f, -1.f, 0.f,
1.f, -1.f, 0.f,
1.f, 1.f, 0.f,
-1.f, -1.f, 0.f,
1.f, 1.f, 0.f,
-1.f, 1.f, 0.f,
-1.f, -1.f, 0.f,
0.f, -1.f, 0.f,
0.f, 1.f, 0.f,
-1.f, -1.f, 0.f,
0.f, 1.f, 0.f,
-1.f, 1.f, 0.f,
};
// Identity, so that the base vertex alone selects the vertex block.
static constexpr quint16 QuadIndices[] = { 0, 1, 2, 3, 4, 5 };
// Instance 0 is green and is never expected to show up: every draw below
// passes a firstInstance of 1 or 2.
static constexpr float InstanceColors[] = {
0.f, 1.f, 0.f, 1.f,
1.f, 0.f, 0.f, 1.f,
1.f, 1.f, 0.f, 1.f,
};
const QSize outputSize(96, 64);
const int bandWidth = outputSize.width() / 3;
std::unique_ptr<QRhiTexture> texture(
rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
std::unique_ptr<QRhiBuffer> vb(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer,
sizeof(QuadVertices)));
QVERIFY(vb->create());
u->uploadStaticBuffer(vb.get(), QuadVertices);
std::unique_ptr<QRhiBuffer> instBuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer,
sizeof(InstanceColors)));
QVERIFY(instBuf->create());
u->uploadStaticBuffer(instBuf.get(), InstanceColors);
std::unique_ptr<QRhiBuffer> ib(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::IndexBuffer,
sizeof(QuadIndices)));
QVERIFY(ib->create());
u->uploadStaticBuffer(ib.get(), QuadIndices);
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64));
QVERIFY(ubuf->create());
const QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
u->updateDynamicBuffer(ubuf.get(), 0, 64, mvp.constData());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, ubuf.get()) });
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> ps(rhi->newGraphicsPipeline());
const QShader vs = loadShader(":/data/colored.vert.qsb");
QVERIFY(vs.isValid());
const QShader fs = loadShader(":/data/colored.frag.qsb");
QVERIFY(fs.isValid());
ps->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout vlayout;
vlayout.setBindings({
{ 3 * sizeof(float) },
{ 4 * sizeof(float), QRhiVertexInputBinding::PerInstance }
});
vlayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float3, 0 },
{ 1, 1, QRhiVertexInputAttribute::Float4, 0 },
});
ps->setVertexInputLayout(vlayout);
ps->setShaderResourceBindings(srb.get());
ps->setRenderPassDescriptor(rpDesc.get());
QVERIFY(ps->create());
cb->beginPass(rt.get(), Qt::blue, { 1.0f, 0 }, u);
cb->setGraphicsPipeline(ps.get());
cb->setShaderResources(srb.get());
const QRhiCommandBuffer::VertexInput vinputs[] = { { vb.get(), 0 }, { instBuf.get(), 0 } };
cb->setVertexInput(0, 2, vinputs, ib.get(), 0, QRhiCommandBuffer::IndexUInt16);
// Band 0: indexed, base instance 1 -> red, filling the band.
cb->setViewport({ 0, 0, float(bandWidth), float(outputSize.height()) });
cb->drawIndexed(6, 1, 0, 0, 1);
// Band 1: non-indexed, base instance 2 -> yellow, filling the band.
cb->setViewport({ float(bandWidth), 0, float(bandWidth), float(outputSize.height()) });
cb->draw(6, 1, 0, 2);
// Band 2: indexed with base vertex 6, base instance 1 -> red, but only in
// the left half of the band, that being the extent of the second block.
if (baseVertexSupported) {
cb->setViewport({ float(2 * bandWidth), 0, float(bandWidth), float(outputSize.height()) });
cb->drawIndexed(6, 1, 0, 6, 1);
}
QRhiReadbackResult rb;
QImage result;
rb.completed = [&] {
result = QImage(reinterpret_cast<const uchar *>(rb.data.constData()),
rb.pixelSize.width(), rb.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *u2 = rhi->nextResourceUpdateBatch();
u2->readBackTexture({ texture.get() }, &rb);
cb->endPass(u2);
frameGuard.endFrame();
if (impl == QRhi::Null)
return;
result.convertTo(QImage::Format_ARGB32);
QCOMPARE(result.size(), texture->pixelSize());
const int y = outputSize.height() / 2;
const QRgb indexedColor = result.pixel(bandWidth / 2, y);
QCOMPARE(qRed(indexedColor), 255);
QCOMPARE(qGreen(indexedColor), 0);
QCOMPARE(qBlue(indexedColor), 0);
const QRgb nonIndexedColor = result.pixel(bandWidth + bandWidth / 2, y);
QCOMPARE(qRed(nonIndexedColor), 255);
QCOMPARE(qGreen(nonIndexedColor), 255);
QCOMPARE(qBlue(nonIndexedColor), 0);
if (baseVertexSupported) {
const QRgb baseVertexColor = result.pixel(2 * bandWidth + bandWidth / 4, y);
QCOMPARE(qRed(baseVertexColor), 255);
QCOMPARE(qGreen(baseVertexColor), 0);
QCOMPARE(qBlue(baseVertexColor), 0);
// The right half of the band is not covered by the second vertex block.
const QRgb untouched = result.pixel(2 * bandWidth + 3 * bandWidth / 4, y);
QCOMPARE(qRed(untouched), 0);
QCOMPARE(qGreen(untouched), 0);
QCOMPARE(qBlue(untouched), 255);
}
}
void tst_QRhi::srbLayoutCompatibility_data()
{
rhiTestData();
}
void tst_QRhi::srbLayoutCompatibility()
{
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 texture resource updates");
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512)));
QVERIFY(texture->create());
QScopedPointer<QRhiSampler> sampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
QVERIFY(sampler->create());
QScopedPointer<QRhiSampler> otherSampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
QVERIFY(otherSampler->create());
QScopedPointer<QRhiBuffer> buf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 1024));
QVERIFY(buf->create());
QScopedPointer<QRhiBuffer> otherBuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 256));
QVERIFY(otherBuf->create());
// empty (compatible)
{
QScopedPointer<QRhiShaderResourceBindings> srb1(rhi->newShaderResourceBindings());
QVERIFY(srb1->create());
QScopedPointer<QRhiShaderResourceBindings> srb2(rhi->newShaderResourceBindings());
QVERIFY(srb2->create());
QVERIFY(srb1->isLayoutCompatible(srb2.data()));
QVERIFY(srb2->isLayoutCompatible(srb1.data()));
QCOMPARE(srb1->serializedLayoutDescription(), srb2->serializedLayoutDescription());
QVERIFY(srb1->serializedLayoutDescription().size() == 0);
}
// different count (not compatible)
{
QScopedPointer<QRhiShaderResourceBindings> srb1(rhi->newShaderResourceBindings());
QVERIFY(srb1->create());
QScopedPointer<QRhiShaderResourceBindings> srb2(rhi->newShaderResourceBindings());
srb2->setBindings({
QRhiShaderResourceBinding::sampledTexture(0, QRhiShaderResourceBinding::FragmentStage, texture.data(), sampler.data())
});
QVERIFY(srb2->create());
QVERIFY(!srb1->isLayoutCompatible(srb2.data()));
QVERIFY(!srb2->isLayoutCompatible(srb1.data()));
QVERIFY(srb1->serializedLayoutDescription() != srb2->serializedLayoutDescription());
QVERIFY(srb1->serializedLayoutDescription().size() == 0);
QVERIFY(srb2->serializedLayoutDescription().size() == 1 * QRhiShaderResourceBinding::LAYOUT_DESC_ENTRIES_PER_BINDING);
}
// full match (compatible)
{
QScopedPointer<QRhiShaderResourceBindings> srb1(rhi->newShaderResourceBindings());
srb1->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, buf.data()),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, texture.data(), sampler.data())
});
QVERIFY(srb1->create());
QScopedPointer<QRhiShaderResourceBindings> srb2(rhi->newShaderResourceBindings());
srb2->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, buf.data()),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, texture.data(), sampler.data())
});
QVERIFY(srb2->create());
QVERIFY(srb1->isLayoutCompatible(srb2.data()));
QVERIFY(srb2->isLayoutCompatible(srb1.data()));
QVERIFY(!srb1->serializedLayoutDescription().isEmpty());
QVERIFY(!srb2->serializedLayoutDescription().isEmpty());
QCOMPARE(srb1->serializedLayoutDescription(), srb2->serializedLayoutDescription());
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)
QVector<quint32> layoutDesc1;
QRhiShaderResourceBinding::serializeLayoutDescription(srb1->cbeginBindings(), srb1->cendBindings(), std::back_inserter(layoutDesc1));
QCOMPARE(layoutDesc1, srb1->serializedLayoutDescription());
QVector<quint32> layoutDesc2;
QRhiShaderResourceBinding::serializeLayoutDescription(srb2->cbeginBindings(), srb2->cendBindings(), std::back_inserter(layoutDesc2));
QCOMPARE(layoutDesc2, srb2->serializedLayoutDescription());
// exercise with an "output iterator" different from back_inserter
quint32 layoutDesc3[2 * QRhiShaderResourceBinding::LAYOUT_DESC_ENTRIES_PER_BINDING];
QRhiShaderResourceBinding::serializeLayoutDescription(srb1->cbeginBindings(), srb1->cendBindings(), layoutDesc3);
QVERIFY(!memcmp(layoutDesc3, layoutDesc1.constData(), sizeof(quint32) * 2 * QRhiShaderResourceBinding::LAYOUT_DESC_ENTRIES_PER_BINDING));
}
// different visibility (not compatible)
{
QScopedPointer<QRhiShaderResourceBindings> srb1(rhi->newShaderResourceBindings());
srb1->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, buf.data()),
});
QVERIFY(srb1->create());
QScopedPointer<QRhiShaderResourceBindings> srb2(rhi->newShaderResourceBindings());
srb2->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, buf.data()),
});
QVERIFY(srb2->create());
QVERIFY(!srb1->isLayoutCompatible(srb2.data()));
QVERIFY(!srb2->isLayoutCompatible(srb1.data()));
QVERIFY(srb1->serializedLayoutDescription() != srb2->serializedLayoutDescription());
}
// different binding points (not compatible)
{
QScopedPointer<QRhiShaderResourceBindings> srb1(rhi->newShaderResourceBindings());
srb1->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, buf.data()),
});
QVERIFY(srb1->create());
QScopedPointer<QRhiShaderResourceBindings> srb2(rhi->newShaderResourceBindings());
srb2->setBindings({
QRhiShaderResourceBinding::uniformBuffer(1, QRhiShaderResourceBinding::VertexStage, buf.data()),
});
QVERIFY(srb2->create());
QVERIFY(!srb1->isLayoutCompatible(srb2.data()));
QVERIFY(!srb2->isLayoutCompatible(srb1.data()));
QVERIFY(srb1->serializedLayoutDescription() != srb2->serializedLayoutDescription());
}
// different buffer region offset and size (compatible)
{
QScopedPointer<QRhiShaderResourceBindings> srb1(rhi->newShaderResourceBindings());
srb1->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, buf.data(), rhi->ubufAligned(1), 128),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, texture.data(), sampler.data())
});
QVERIFY(srb1->create());
QScopedPointer<QRhiShaderResourceBindings> srb2(rhi->newShaderResourceBindings());
srb2->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, buf.data()),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, texture.data(), sampler.data())
});
QVERIFY(srb2->create());
QVERIFY(srb1->isLayoutCompatible(srb2.data()));
QVERIFY(srb2->isLayoutCompatible(srb1.data()));
QCOMPARE(srb1->serializedLayoutDescription(), srb2->serializedLayoutDescription());
}
// different resources (compatible)
{
QScopedPointer<QRhiShaderResourceBindings> srb1(rhi->newShaderResourceBindings());
srb1->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, otherBuf.data()),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, texture.data(), otherSampler.data())
});
QVERIFY(srb1->create());
QScopedPointer<QRhiShaderResourceBindings> srb2(rhi->newShaderResourceBindings());
srb2->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, buf.data()),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, texture.data(), sampler.data())
});
QVERIFY(srb2->create());
QVERIFY(srb1->isLayoutCompatible(srb2.data()));
QVERIFY(srb2->isLayoutCompatible(srb1.data()));
QCOMPARE(srb1->serializedLayoutDescription(), srb2->serializedLayoutDescription());
}
}
void tst_QRhi::srbWithNoResource_data()
{
rhiTestData();
}
void tst_QRhi::srbWithNoResource()
{
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 srb");
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512)));
QVERIFY(texture->create());
QScopedPointer<QRhiSampler> sampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
QVERIFY(sampler->create());
QScopedPointer<QRhiBuffer> buf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 1024));
QVERIFY(buf->create());
{
QScopedPointer<QRhiShaderResourceBindings> srb1(rhi->newShaderResourceBindings());
srb1->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, nullptr),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, nullptr, nullptr)
});
QVERIFY(srb1->create());
QScopedPointer<QRhiShaderResourceBindings> srb2(rhi->newShaderResourceBindings());
srb2->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, buf.data()),
QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, texture.data(), sampler.data())
});
QVERIFY(srb2->create());
QVERIFY(srb1->isLayoutCompatible(srb2.data()));
QVERIFY(srb2->isLayoutCompatible(srb1.data()));
}
}
void tst_QRhi::renderPassDescriptorCompatibility_data()
{
rhiTestData();
}
void tst_QRhi::renderPassDescriptorCompatibility()
{
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 renderpass descriptors");
// Note that checking compatibility is only relevant with backends where
// there is a concept of renderpass descriptions (Vulkan, and partially
// Metal). It is perfectly fine for isCompatible() to always return true
// when that is not the case (D3D11, OpenGL). Hence the 'if (Vulkan or
// Metal)' for all the negative tests. Also note "partial" for Metal:
// resolve textures for examples have no effect on compatibility with Metal.
// tex and tex2 have the same format
QScopedPointer<QRhiTexture> tex(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 1, QRhiTexture::RenderTarget));
QVERIFY(tex->create());
QScopedPointer<QRhiTexture> tex2(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 1, QRhiTexture::RenderTarget));
QVERIFY(tex2->create());
QScopedPointer<QRhiRenderBuffer> ds(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, QSize(512, 512)));
QVERIFY(ds->create());
// two texture rendertargets with tex and tex2 as color0 (compatible)
{
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ tex.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
QScopedPointer<QRhiTextureRenderTarget> rt2(rhi->newTextureRenderTarget({ tex2.data() }));
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());
}
// two texture rendertargets with tex and tex2 as color0, and a depth-stencil attachment as well (compatible)
{
QRhiTextureRenderTargetDescription desc({ tex.data() }, ds.data());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(desc));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
QScopedPointer<QRhiTextureRenderTarget> rt2(rhi->newTextureRenderTarget(desc));
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());
}
// now one of them does not have the ds attachment (not compatible)
{
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ { tex.data() }, ds.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
QScopedPointer<QRhiTextureRenderTarget> rt2(rhi->newTextureRenderTarget({ tex.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc2(rt2->newCompatibleRenderPassDescriptor());
rt2->setRenderPassDescriptor(rpDesc2.data());
QVERIFY(rt2->create());
// these backends have a real concept of rp compatibility, with those we
// know that incompatibility must be reported; verify this
if (impl == QRhi::Vulkan || impl == QRhi::Metal) {
QVERIFY(!rpDesc->isCompatible(rpDesc2.data()));
QVERIFY(!rpDesc2->isCompatible(rpDesc.data()));
QVERIFY(!rpDesc->serializedFormat().isEmpty());
QVERIFY(rpDesc->serializedFormat() != rpDesc2->serializedFormat());
}
}
if (rhi->isFeatureSupported(QRhi::MultisampleRenderBuffer)) {
// resolve attachments (compatible)
{
QScopedPointer<QRhiRenderBuffer> msaaRenderBuffer(rhi->newRenderBuffer(QRhiRenderBuffer::Color, QSize(512, 512), 4));
QVERIFY(msaaRenderBuffer->create());
QScopedPointer<QRhiRenderBuffer> msaaRenderBuffer2(rhi->newRenderBuffer(QRhiRenderBuffer::Color, QSize(512, 512), 4));
QVERIFY(msaaRenderBuffer2->create());
QRhiColorAttachment colorAtt(msaaRenderBuffer.data()); // color0, multisample
colorAtt.setResolveTexture(tex.data()); // resolved into a non-msaa texture
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ colorAtt }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
QRhiColorAttachment colorAtt2(msaaRenderBuffer2.data()); // color0, multisample
colorAtt2.setResolveTexture(tex2.data()); // resolved into a non-msaa texture
QScopedPointer<QRhiTextureRenderTarget> rt2(rhi->newTextureRenderTarget({ colorAtt2 }));
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());
}
// missing resolve for one of them (not compatible)
{
QScopedPointer<QRhiRenderBuffer> msaaRenderBuffer(rhi->newRenderBuffer(QRhiRenderBuffer::Color, QSize(512, 512), 4));
QVERIFY(msaaRenderBuffer->create());
QScopedPointer<QRhiRenderBuffer> msaaRenderBuffer2(rhi->newRenderBuffer(QRhiRenderBuffer::Color, QSize(512, 512), 4));
QVERIFY(msaaRenderBuffer2->create());
QRhiColorAttachment colorAtt(msaaRenderBuffer.data()); // color0, multisample
colorAtt.setResolveTexture(tex.data()); // resolved into a non-msaa texture
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ colorAtt }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
QRhiColorAttachment colorAtt2(msaaRenderBuffer2.data()); // color0, multisample
QScopedPointer<QRhiTextureRenderTarget> rt2(rhi->newTextureRenderTarget({ colorAtt2 }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc2(rt2->newCompatibleRenderPassDescriptor());
rt2->setRenderPassDescriptor(rpDesc2.data());
QVERIFY(rt2->create());
if (impl == QRhi::Vulkan) { // no Metal here
QVERIFY(!rpDesc->isCompatible(rpDesc2.data()));
QVERIFY(!rpDesc2->isCompatible(rpDesc.data()));
QVERIFY(!rpDesc->serializedFormat().isEmpty());
QVERIFY(rpDesc->serializedFormat() != rpDesc2->serializedFormat());
}
}
} else {
qDebug("Skipping multisample renderbuffer dependent tests");
}
if (rhi->isTextureFormatSupported(QRhiTexture::RGBA32F)) {
QScopedPointer<QRhiTexture> tex3(rhi->newTexture(QRhiTexture::RGBA32F, QSize(512, 512), 1, QRhiTexture::RenderTarget));
QVERIFY(tex3->create());
// different texture formats (not compatible)
{
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ tex.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
QScopedPointer<QRhiTextureRenderTarget> rt2(rhi->newTextureRenderTarget({ tex3.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc2(rt2->newCompatibleRenderPassDescriptor());
rt2->setRenderPassDescriptor(rpDesc2.data());
QVERIFY(rt2->create());
if (impl == QRhi::Vulkan || impl == QRhi::Metal) {
QVERIFY(!rpDesc->isCompatible(rpDesc2.data()));
QVERIFY(!rpDesc2->isCompatible(rpDesc.data()));
QVERIFY(!rpDesc->serializedFormat().isEmpty());
QVERIFY(rpDesc->serializedFormat() != rpDesc2->serializedFormat());
}
}
} 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()
{
rhiTestData();
}
void tst_QRhi::renderPassDescriptorClone()
{
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 renderpass descriptors");
// tex and tex2 have the same format
QScopedPointer<QRhiTexture> tex(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 1, QRhiTexture::RenderTarget));
QVERIFY(tex->create());
QScopedPointer<QRhiTexture> tex2(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 1, QRhiTexture::RenderTarget));
QVERIFY(tex2->create());
QScopedPointer<QRhiRenderBuffer> ds(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, QSize(512, 512)));
QVERIFY(ds->create());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ tex.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
QScopedPointer<QRhiRenderPassDescriptor> rpDescClone(rpDesc->newCompatibleRenderPassDescriptor());
QVERIFY(rpDescClone);
QVERIFY(rpDesc->isCompatible(rpDescClone.data()));
// rt and rt2 have the same set of attachments
QScopedPointer<QRhiTextureRenderTarget> rt2(rhi->newTextureRenderTarget({ tex2.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc2(rt2->newCompatibleRenderPassDescriptor());
rt2->setRenderPassDescriptor(rpDesc2.data());
QVERIFY(rt2->create());
QVERIFY(rpDesc2->isCompatible(rpDescClone.data()));
}
void tst_QRhi::pipelineCache_data()
{
rhiTestData();
}
void tst_QRhi::pipelineCache()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QByteArray pcd;
QShader vs = loadShader(":/data/simple.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(":/data/simple.frag.qsb");
QVERIFY(fs.isValid());
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({ { 2 * sizeof(float) } });
inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
{
QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::EnablePipelineCacheDataSave));
if (!rhi)
QSKIP("QRhi could not be created, skipping testing (set)pipelineCacheData()");
if (!rhi->isFeatureSupported(QRhi::PipelineCacheDataLoadSave))
QSKIP("PipelineCacheDataLoadSave is not supported with this backend, skipping test");
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(256, 256), 1, QRhiTexture::RenderTarget));
QVERIFY(texture->create());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->create());
QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.data());
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline->create());
// This cannot be more than a basic smoketest: ensure that passing
// in the data we retrieve still gives us successful pipeline
// creation. What happens internally we cannot check.
pcd = rhi->pipelineCacheData();
rhi->setPipelineCacheData(pcd);
QVERIFY(pipeline->create());
}
{
// Now from scratch, with seeding the cache right from the start,
// presumably leading to a cache hit when creating the pipeline.
QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::EnablePipelineCacheDataSave));
QVERIFY(rhi);
rhi->setPipelineCacheData(pcd);
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(256, 256), 1, QRhiTexture::RenderTarget));
QVERIFY(texture->create());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->create());
QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.data());
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline->create());
}
{
// Corrupted and truncated blobs must be rejected by QRhi's own header
// and size validation, without any part of the payload being handed to
// the 3D API.
QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::EnablePipelineCacheDataSave));
QVERIFY(rhi);
// rhiId and arch are the first two quint32s of the header with every
// backend, and both are validated up front. (the blob can legitimately
// be empty, e.g. with the Null backend, hence the clamping)
for (qsizetype offset = 0; offset < qMin<qsizetype>(8, pcd.size()); ++offset) {
for (uchar value : { uchar(0x00), uchar(0xff) }) {
QByteArray corrupt = pcd;
corrupt[offset] = char(value);
rhi->setPipelineCacheData(corrupt);
}
}
// Truncated: lengths that do not even cover the header, and lengths
// that cover the header but not all of the payload it advertises. Note
// that we won't test the case of corrupting the actual blob that, with
// Vulkan for example, is passed on to the driver.
const qsizetype truncationWindow = qMin<qsizetype>(320, pcd.size());
for (qsizetype size = 0; size < truncationWindow; ++size) {
rhi->setPipelineCacheData(pcd.left(size));
rhi->setPipelineCacheData(pcd.left(pcd.size() - 1 - size));
}
// Now reload the valid data and verify the QRhi still works.
rhi->setPipelineCacheData(pcd);
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(256, 256), 1, QRhiTexture::RenderTarget));
QVERIFY(texture->create());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->create());
QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.data());
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline->create());
}
}
void tst_QRhi::pipelineCacheBlobReader()
{
// The OpenGL and D3D11 backends serialize their own list of entries into the
// pipeline cache blob and have to walk it in setPipelineCacheData(). Verify
// that the reader they share for that never goes past the end of the data,
// so that a truncated or crafted blob gets rejected instead of resulting in
// an out-of-bounds read. This is a device-independent check on the parsing
// itself, hence no QRhi here.
const auto appendUInt32 = [](QByteArray *blob, quint32 value) {
blob->append(reinterpret_cast<const char *>(&value), 4);
};
const auto makeByteArrayEntry = [&appendUInt32](const QByteArray &payload) {
QByteArray blob;
appendUInt32(&blob, quint32(payload.size()));
blob.append(payload);
return blob;
};
// Well-formed: a length-prefixed byte array followed by a quint32, with
// nothing left over afterwards.
{
QByteArray blob = makeByteArrayEntry(QByteArrayLiteral("test"));
appendUInt32(&blob, 42);
QRhiPipelineCacheDataReader reader(blob.constData(), quint32(blob.size()));
QByteArray value;
QVERIFY(reader.readByteArray(&value));
QCOMPARE(value, QByteArrayLiteral("test"));
quint32 number = 0;
QVERIFY(reader.readUInt32(&number));
QCOMPARE(number, 42u);
QVERIFY(!reader.readUInt32(&number));
}
// An empty payload is legitimate, and must not be confused with a failure.
{
const QByteArray blob = makeByteArrayEntry(QByteArray());
QRhiPipelineCacheDataReader reader(blob.constData(), quint32(blob.size()));
QByteArray value = QByteArrayLiteral("not empty");
QVERIFY(reader.readByteArray(&value));
QVERIFY(value.isEmpty());
}
// Truncated at every possible length: must fail instead of reading beyond
// the end of the buffer.
{
const QByteArray blob = makeByteArrayEntry(QByteArrayLiteral("test"));
for (qsizetype size = 0; size < blob.size(); ++size) {
QRhiPipelineCacheDataReader reader(blob.constData(), quint32(size));
QByteArray value;
QVERIFY(!reader.readByteArray(&value));
}
for (qsizetype size = 0; size < 4; ++size) {
QRhiPipelineCacheDataReader reader(blob.constData(), quint32(size));
quint32 number = 0;
QVERIFY(!reader.readUInt32(&number));
}
}
// A length prefix that claims more than the blob can possibly hold.
{
for (quint32 len : { 6u, 0x10000u, 0x7fffffffu, 0xffffffffu }) {
QByteArray blob;
appendUInt32(&blob, len);
blob.append(QByteArrayLiteral("test"));
QRhiPipelineCacheDataReader reader(blob.constData(), quint32(blob.size()));
QByteArray value;
QVERIFY(!reader.readByteArray(&value));
}
}
// Zero-sized input: every read must fail immediately.
{
QRhiPipelineCacheDataReader reader(nullptr, 0);
QByteArray value;
quint32 number = 0;
QVERIFY(!reader.readByteArray(&value));
QVERIFY(!reader.readUInt32(&number));
}
}
void tst_QRhi::textureWithSampleCount_data()
{
rhiTestData();
}
void tst_QRhi::textureWithSampleCount()
{
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 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());
}
}
// Returns a QRhi for the given backend, created on the first call and then kept
// alive, so that subsequent calls - with this or any other impl - get the same
// object back. Data-driven tests with many rows that do not need a pristine
// QRhi should use this instead of creating one per row: creating and destroying
// a graphics device e.g. 50 times in a row is wasteful, and some drivers start
// failing device creation after a while. It also makes the textureFormats()
// test complete a lot faster since device creation can be fairly expensive.
//
// The returned QRhi is owned by the test object and lives until
// cleanupTestCase(). Do not destroy it, and do not leave resources created from
// it alive when returning from the test function.
QRhi *tst_QRhi::sharedRhi(QRhi::Implementation impl, QRhiInitParams *initParams)
{
// Note that a null value is stored when the creation fails, so that it is
// not attempted again for every subsequent data row.
if (!m_sharedRhis.contains(impl))
m_sharedRhis.insert(impl, QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
return m_sharedRhis.value(impl);
}
void tst_QRhi::textureFormats_data()
{
static constexpr QRhiTexture::Format textureFormats[] = {
QRhiTexture::RGBA8,
QRhiTexture::BGRA8,
QRhiTexture::R8,
QRhiTexture::RG8,
QRhiTexture::R16,
QRhiTexture::RG16,
QRhiTexture::RED_OR_ALPHA8,
QRhiTexture::RGBA16F,
QRhiTexture::RGBA32F,
QRhiTexture::R16F,
QRhiTexture::R32F,
QRhiTexture::RGB10A2,
QRhiTexture::R8SI,
QRhiTexture::R32SI,
QRhiTexture::RG32SI,
QRhiTexture::RGBA32SI,
QRhiTexture::R8UI,
QRhiTexture::R32UI,
QRhiTexture::RG32UI,
QRhiTexture::RGBA32UI,
QRhiTexture::D16,
QRhiTexture::D24,
QRhiTexture::D24S8,
QRhiTexture::D32F,
QRhiTexture::D32FS8,
QRhiTexture::BC1,
QRhiTexture::BC2,
QRhiTexture::BC3,
QRhiTexture::BC4,
QRhiTexture::BC5,
QRhiTexture::BC6H,
QRhiTexture::BC7,
QRhiTexture::ETC2_RGB8,
QRhiTexture::ETC2_RGB8A1,
QRhiTexture::ETC2_RGBA8,
QRhiTexture::ASTC_4x4,
QRhiTexture::ASTC_5x4,
QRhiTexture::ASTC_5x5,
QRhiTexture::ASTC_6x5,
QRhiTexture::ASTC_6x6,
QRhiTexture::ASTC_8x5,
QRhiTexture::ASTC_8x6,
QRhiTexture::ASTC_8x8,
QRhiTexture::ASTC_10x5,
QRhiTexture::ASTC_10x6,
QRhiTexture::ASTC_10x8,
QRhiTexture::ASTC_10x10,
QRhiTexture::ASTC_12x10,
QRhiTexture::ASTC_12x12,
};
rhiTestDataWithParam("textureFormat", QSpan(textureFormats), [](const QRhiTexture::Format format) {
return QString::number(format);
});
}
void tst_QRhi::textureFormats()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QFETCH(QRhiTexture::Format, textureFormat);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing texture formats");
if (!rhi->isTextureFormatSupported(textureFormat))
QSKIP("Texture format not supported on this backend");
std::unique_ptr<QRhiTexture> tex(rhi->newTexture(textureFormat, QSize(512, 512), 1));
QVERIFY(tex->create());
}
void tst_QRhi::depthTextureReadback_data()
{
static constexpr QRhiTexture::Format depthFormats[] = {
QRhiTexture::D16,
QRhiTexture::D24,
QRhiTexture::D24S8,
QRhiTexture::D32F,
QRhiTexture::D32FS8,
};
rhiTestDataWithParam("depthFormat", QSpan(depthFormats), [](const QRhiTexture::Format format) {
return QString::number(format);
});
}
// Verifies that reading back a QRhiTexture with a depth (or depth-stencil)
// format returns the depth (and stencil) values the texture was cleared to, in
// the layout documented for QRhiResourceUpdateBatch::readBackTexture().
void tst_QRhi::depthTextureReadback()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QFETCH(QRhiTexture::Format, depthFormat);
std::unique_ptr<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
if (!rhi)
QSKIP("QRhi could not be created, skipping testing depth texture readback");
if (!rhi->isTextureFormatSupported(depthFormat))
QSKIP("Depth format not supported on this backend");
#ifdef TST_GL
// glReadPixels() with GL_DEPTH_COMPONENT is not available in OpenGL ES.
if (impl == QRhi::OpenGLES2) {
const QRhiGles2NativeHandles *glHandles = static_cast<const QRhiGles2NativeHandles *>(rhi->nativeHandles());
if (glHandles->context->isOpenGLES())
QSKIP("Reading back depth textures is not supported with OpenGL ES");
}
#endif
const QSize outputSize(15, 15);
std::unique_ptr<QRhiTexture> depthTex(rhi->newTexture(depthFormat, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(depthTex->create());
// A render target with a depth texture but no color attachment is valid
// (and is exactly what shadow-map-style rendering uses in practice).
QRhiTextureRenderTargetDescription rtDesc;
rtDesc.setDepthTexture(depthTex.get());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(rtDesc));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
// Arbitrary values, deliberately not 0 or 1, so that the all-zero data a
// badly broken readback tends to produce cannot be mistaken for a correct
// result.
const float clearDepth = 0.25f;
const quint32 clearStencil = 0x5A;
cb->beginPass(rt.get(), Qt::black, { clearDepth, clearStencil });
// No draws: the clear value alone is what gets verified below.
// Read back the whole texture, and also a sub-rectangle of it.
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
QRhiReadbackResult subResult;
bool subReadCompleted = false;
subResult.completed = [&subReadCompleted] { subReadCompleted = true; };
const QRect subRect(3, 2, 7, 5);
QRhiReadbackDescription subReadback(depthTex.get());
subReadback.setRect(subRect);
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture(depthTex.get(), &readResult);
readbackBatch->readBackTexture(subReadback, &subResult);
cb->endPass(readbackBatch);
rhi->endOffscreenFrame();
// Offscreen frames are synchronous, so the readback is guaranteed to
// have completed by now.
QVERIFY(readCompleted);
QVERIFY(subReadCompleted);
QCOMPARE(readResult.pixelSize, outputSize);
QCOMPARE(subResult.pixelSize, subRect.size());
if (impl == QRhi::Null)
return;
quint32 bytesPerPixel = 0;
switch (depthFormat) {
case QRhiTexture::D16:
bytesPerPixel = 2;
break;
case QRhiTexture::D24:
case QRhiTexture::D24S8:
case QRhiTexture::D32F:
bytesPerPixel = 4;
break;
case QRhiTexture::D32FS8:
bytesPerPixel = 8;
break;
default:
Q_UNREACHABLE();
}
// Check that the values we cleared to come back. Every texel is checked,
// not just a few of them, since all texels have the same value and a
// readback with the wrong row stride or offset would otherwise go unnoticed.
const auto verifyTexels = [&](const QRhiReadbackResult &result) {
const int texelCount = result.pixelSize.width() * result.pixelSize.height();
QCOMPARE(quint32(result.data.size()), bytesPerPixel * quint32(texelCount));
for (int i = 0; i < texelCount; ++i) {
const char *texel = result.data.constData() + i * int(bytesPerPixel);
switch (depthFormat) {
case QRhiTexture::D32F:
case QRhiTexture::D32FS8:
{
const float depth = qFromUnaligned<float>(texel);
QCOMPARE(depth, clearDepth);
if (depthFormat == QRhiTexture::D32FS8)
QCOMPARE(qFromUnaligned<quint32>(texel + 4), clearStencil);
}
break;
case QRhiTexture::D16:
{
// 16-bit normalized depth cannot represent clearDepth exactly,
// and the conversion may round to either of the two nearest
// values, so allow an error of one.
const quint16 rawDepth = qFromUnaligned<quint16>(texel);
const int expectedRawDepth = qRound(clearDepth * 0xFFFF);
QCOMPARE_LE(qAbs(int(rawDepth) - expectedRawDepth), 1);
}
break;
case QRhiTexture::D24:
case QRhiTexture::D24S8:
{
// The 24-bit depth is in the upper 24 bits, the stencil, if any,
// in the lower 8 bits. Same as above, allow an error of one.
const quint32 rawTexel = qFromUnaligned<quint32>(texel);
const int expectedRawDepth = qRound(clearDepth * 0xFFFFFF);
QCOMPARE_LE(qAbs(int(rawTexel >> 8) - expectedRawDepth), 1);
QCOMPARE(rawTexel & 0xFF, depthFormat == QRhiTexture::D24S8 ? clearStencil : 0u);
}
break;
default:
Q_UNREACHABLE();
}
}
};
verifyTexels(readResult);
if (QTest::currentTestFailed())
return;
verifyTexels(subResult);
}
void tst_QRhi::textureImportOpenGL()
{
#ifdef TST_GL
if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::OpenGL))
QSKIP("Skipping OpenGL-dependent test");
QRhi *rhi = sharedRhi(QRhi::OpenGLES2, &initParams.gl);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing native texture");
QVERIFY(rhi->makeThreadLocalNativeContextCurrent());
QOpenGLContext *ctx = QOpenGLContext::currentContext();
QVERIFY(ctx);
QOpenGLFunctions *f = ctx->functions();
QImage image(320, 200, QImage::Format_RGBA8888_Premultiplied);
image.fill(Qt::red);
GLuint t = 0;
f->glGenTextures(1, &t);
f->glBindTexture(GL_TEXTURE_2D, t);
f->glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width(), image.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, image.constBits());
QScopedPointer<QRhiTexture> tex(rhi->newTexture(QRhiTexture::RGBA8, image.size()));
QRhiTexture::NativeTexture nativeTex = { t, 0 };
QVERIFY(tex->createFrom(nativeTex));
QCOMPARE(tex->nativeTexture().object, nativeTex.object);
QRhiReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
batch->readBackTexture(tex.data(), &readResult);
QVERIFY(submitResourceUpdates(rhi, batch));
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(),
image.format());
QVERIFY(imageRGBAEquals(image, wrapperImage));
f->glDeleteTextures(1, &t);
#endif
}
void tst_QRhi::renderbufferImportOpenGL()
{
#ifdef TST_GL
if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::OpenGL))
QSKIP("Skipping OpenGL-dependent test");
QRhi *rhi = sharedRhi(QRhi::OpenGLES2, &initParams.gl);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing native texture");
QVERIFY(rhi->makeThreadLocalNativeContextCurrent());
QOpenGLContext *ctx = QOpenGLContext::currentContext();
QVERIFY(ctx);
QOpenGLFunctions *f = ctx->functions();
const QSize size(320, 200);
GLuint b = 0;
f->glGenRenderbuffers(1, &b);
f->glBindRenderbuffer(GL_RENDERBUFFER, b);
// in a real world use case this would be some extension, e.g. glEGLImageTargetRenderbufferStorageOES instead
f->glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA4, size.width(), size.height());
f->glBindRenderbuffer(GL_RENDERBUFFER, 0);
QScopedPointer<QRhiRenderBuffer> rb(rhi->newRenderBuffer(QRhiRenderBuffer::Color, size));
QVERIFY(rb->createFrom({ b }));
QScopedPointer<QRhiRenderBuffer> depthStencil(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, size));
QVERIFY(depthStencil->create());
QRhiColorAttachment att(rb.data());
QRhiTextureRenderTargetDescription rtDesc(att);
rtDesc.setDepthStencilBuffer(depthStencil.data());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(rtDesc));
QScopedPointer<QRhiRenderPassDescriptor> rp(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rp.data());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
cb->beginPass(rt.data(), Qt::red, { 1.0f, 0 }, nullptr, QRhiCommandBuffer::ExternalContent);
cb->beginExternal();
QByteArray tmpBuf;
tmpBuf.resize(size.width() * size.height() * 4);
f->glReadPixels(0, 0, size.width(), size.height(), GL_RGBA, GL_UNSIGNED_BYTE, tmpBuf.data());
cb->endExternal();
cb->endPass();
frameGuard.endFrame();
f->glDeleteRenderbuffers(1, &b);
QImage wrapperImage(reinterpret_cast<const uchar *>(tmpBuf.constData()),
size.width(), size.height(), QImage::Format_RGBA8888_Premultiplied);
QImage image(320, 200, QImage::Format_RGBA8888_Premultiplied);
image.fill(Qt::red);
QVERIFY(imageRGBAEquals(image, wrapperImage));
#endif
}
void tst_QRhi::threeDimTexture_data()
{
rhiTestData();
}
void tst_QRhi::threeDimTexture()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
#ifdef Q_OS_ANDROID
if (QNativeInterface::QAndroidApplication::sdkVersion() == 36)
QSKIP("Fails and crashes on Android 16 (QTBUG-140189)");
#endif
if (!rhi)
QSKIP("QRhi could not be created, skipping testing 3D textures");
if (!rhi->isFeatureSupported(QRhi::ThreeDimensionalTextures))
QSKIP("Skipping testing 3D textures because they are reported as unsupported");
// SwiftShader reports 3D textures as supported but samples them incorrectly,
// on Vulkan as well as on OpenGLES.
if (impl == QRhi::OpenGLES2 && isAndroidSwiftShader(rhi))
QSKIP("SwiftShader does not implement 3D textures correctly (QTBUG-132934)");
const int WIDTH = 512;
const int HEIGHT = 256;
const int DEPTH = 128;
{
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, WIDTH, HEIGHT, DEPTH));
QVERIFY(texture->create());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QVERIFY(batch);
for (int i = 0; i < DEPTH; ++i) {
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, batch));
}
// mipmaps
if (rhi->isFeatureSupported(QRhi::ThreeDimensionalTextureMipmaps)) {
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, WIDTH, HEIGHT, DEPTH,
1, QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips));
QVERIFY(texture->create());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QVERIFY(batch);
for (int i = 0; i < DEPTH; ++i) {
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);
}
batch->generateMips(texture.data());
QVERIFY(submitResourceUpdates(rhi, batch));
// read back slice 63 of level 1 (256x128, almost red)
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, batch));
QVERIFY(!result.isNull());
QImage referenceImage(WIDTH / 2, HEIGHT / 2, result.format());
referenceImage.fill(QColor::fromRgb(253, 0, 0));
// Now restrict the test a bit. The Null QRhi backend has broken support for
// mipmap generation of 3D textures (it ignores the depth, effectively behaving as
// if the 3D texture was a 2D array which is incorrect wrt mipmapping)
// Some software-based OpenGL implementations, such as Mesa llvmpipe builds that are
// used both in Qt CI and are shipped with the official Qt binaries also seem to have
// 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)
// NB with Vulkan we require Vulkan 1.1 for this to work.
{
const int SLICE = 23;
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, WIDTH, HEIGHT, DEPTH,
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());
// render to slice 23
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 });
// slice 23 is now blue
cb->endPass();
frameGuard.endFrame();
// 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) {
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, 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, 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, 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, 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);
QRhi *rhi = sharedRhi(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, 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, batch));
}
// On Vulkan cannot copy between 1D and 2D/3D, unless VK_KHR_maintenance5 is
// present and enabled, according to the validation layer. Yet it seems to
// work, and enabling that device extension seem to make no difference when
// it comes to the validation layer warnings, which is odd. In any case,
// skip these on Vulkan for now.
if (impl != QRhi::Vulkan) {
// 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, 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, 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, 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, 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, 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, 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, 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, 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);
OffscreenFrameGuard frameGuard(rhi);
cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 }, batch);
// texture is now blue
cb->endPass();
frameGuard.endFrame();
// 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, 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);
}
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 }, batch);
// slice 23 is now blue
cb->endPass();
frameGuard.endFrame();
// read back slice 23 (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());
readbackDescription.setLayer(23);
batch->readBackTexture(readbackDescription, &readResult);
QVERIFY(submitResourceUpdates(rhi, 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));
// read back slice 0 (black)
batch = rhi->nextResourceUpdateBatch();
result = QImage();
readbackDescription.setLayer(0);
batch->readBackTexture(readbackDescription, &readResult);
QVERIFY(submitResourceUpdates(rhi, 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, batch));
QVERIFY(!result.isNull());
referenceImage.fill(QColor::fromRgb(254, 0, 0));
QVERIFY(imageRGBAEquals(result, referenceImage));
}
}
void tst_QRhi::graphicsPipelineMixedGlslVersions_data()
{
rhiTestData();
}
void tst_QRhi::graphicsPipelineMixedGlslVersions()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing graphics pipelines");
// Only the vertex shader has a 300 es variant, so 100 es is the only GLSL ES
// version both stages provide and the backend has to settle on it. Only
// OpenGL ES contexts discriminate: the desktop targets are the same for both.
QShader vs = loadShader(":/data/glslversionmix.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(":/data/glslversionmix.frag.qsb");
QVERIFY(fs.isValid());
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(256, 256), 1,
QRhiTexture::RenderTarget));
QVERIFY(texture->create());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->create());
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->create());
QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({ { 4 * sizeof(float) } });
inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float4, 0 } });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.data());
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline->create());
}
void tst_QRhi::leakedResourceDestroy_data()
{
rhiTestData();
}
void tst_QRhi::leakedResourceDestroy()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
#ifdef Q_OS_ANDROID
if ((QNativeInterface::QAndroidApplication::sdkVersion() == 36) && (impl == QRhi::Vulkan))
QSKIP("Fails and crashes on Android 16 (QTBUG-140189)");
#endif
QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams));
if (!rhi)
QSKIP("QRhi could not be created, skipping");
// Incorrectly destroy the QRhi before the resources created from it. Attempting to
// destroy the resources afterwards is pointless, the native resources are leaked.
// Nonetheless, it should not crash, which is what we are testing here.
//
// We do not however have control over other, native and 3rd party components: a
// validation or debug layer, or a memory allocator may warn, assert, or abort when
// not releasing all native resources correctly.
#ifndef QT_NO_DEBUG
// don't want asserts from vkmemalloc, skip the test in debug builds
if (impl == QRhi::Vulkan)
QSKIP("Skipping leaked resource destroy test due to Vulkan and debug build");
#endif
QScopedPointer<QRhiBuffer> buffer(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, 256));
QVERIFY(buffer->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> rpDesc(rt->newCompatibleRenderPassDescriptor());
QVERIFY(rpDesc);
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();
// 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);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing rendering");
if (impl == QRhi::OpenGLES2 && isAndroidSwiftShader(rhi))
QSKIP("SwiftShader does not support this OpenGLES feature (QTBUG-132934)");
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);
OffscreenFrameGuard frameGuard(rhi);
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, 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);
frameGuard.endFrame();
QCOMPARE(result.size(), texture->pixelSize());
if (impl == QRhi::Null)
return;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
// 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());
QCOMPARE_GT(redCount, blueCount); // 1742 > 178
}
void tst_QRhi::renderToRgb10Texture_data()
{
rhiTestData();
}
void tst_QRhi::renderToRgb10Texture()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing rendering");
if (impl == QRhi::OpenGLES2 && isAndroidSwiftShader(rhi))
QSKIP("SwiftShader does not support this OpenGLES feature (QTBUG-132934)");
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);
OffscreenFrameGuard frameGuard(rhi);
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, 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);
frameGuard.endFrame();
QCOMPARE(result.size(), texture->pixelSize());
if (impl == QRhi::Null)
return;
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result.flip();
// 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());
QCOMPARE_GT(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)");
#elif defined(Q_OS_QNX)
QSKIP("V3D 4.2 Vulkan on QNX does not support tessellation");
#endif
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QRhi *rhi = sharedRhi(impl, initParams);
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);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
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);
frameGuard.endFrame();
if (rhi->isYUpInFramebuffer()) // we used clipSpaceCorrMatrix so this is different from many other tests
result.flip();
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)
#if defined(Q_OS_QNX)
QSKIP("V3D 4.2 Vulkan on QNX does not support tessellation");
#endif
QRhi *rhi = sharedRhi(impl, initParams);
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, 2048));
QVERIFY(buffer->create());
// 2048 is a large buffer for RUB
u->uploadStaticBuffer(buffer.data(), QByteArray(2048, 0));
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);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
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);
frameGuard.endFrame();
if (rhi->isYUpInFramebuffer()) // we used clipSpaceCorrMatrix so this is different from many
// other tests
result.flip();
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;
QRhi *rhi = sharedRhi(impl, initParams);
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);
OffscreenFrameGuard frameGuard(rhi);
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(), std::move(toGpuData));
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);
frameGuard.endFrame();
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);
#ifdef Q_OS_ANDROID
if (QNativeInterface::QAndroidApplication::sdkVersion() == 36)
QSKIP("pipeline->create()) Fails on Android 16 (QTBUG-140189)");
#endif
// we can't test with Null as there is no compute
if (impl == QRhi::Null)
return;
QRhi *rhi = sharedRhi(impl, initParams);
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);
OffscreenFrameGuard frameGuard(rhi);
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());
if (!pipeline->create())
QSKIP("Compute pipeline could not be created (shader version not supported on this backend)");
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);
frameGuard.endFrame();
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)");
#elif defined(Q_OS_QNX)
QSKIP("V3D 4.2 Vulkan on QNX does not support tessellation");
#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);
QRhi *rhi = sharedRhi(impl, initParams);
if (!rhi)
QSKIP("QRhi could not be created, skipping testing rendering");
if (!rhi->isFeatureSupported(QRhi::Compute)) // also indicates storage buffers and image load/store
QSKIP("Storage buffers are not supported with this graphics API, skipping test");
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);
QVERIFY(cb);
OffscreenFrameGuard frameGuard(rhi);
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);
frameGuard.endFrame();
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);
QRhi *rhi = sharedRhi(impl, initParams);
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);
OffscreenFrameGuard frameGuard(rhi);
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);
frameGuard.endFrame();
// 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())
QCOMPARE_LT(redCount, blueCount);
else
QCOMPARE_GT(redCount, blueCount);
}
void tst_QRhi::textureViewFormat_data()
{
rhiTestData();
}
void tst_QRhi::textureViewFormat()
{
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 texture view formats");
if (!rhi->isFeatureSupported(QRhi::TextureViewFormat))
QSKIP("Setting a texture view format is not supported, skipping test");
const QSize outputSize(64, 64);
auto renderAndReadBack = [&rhi, outputSize](QRhiTexture *texture, const char *fragShader,
auto setupSrb, auto setupUpdates)
{
QImage result;
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
if (!rt->create())
return result;
QRhiCommandBuffer *cb = nullptr;
if (rhi->beginOffscreenFrame(&cb) != QRhi::FrameOpSuccess)
return result;
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
setupUpdates(updates);
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
setupSrb(srb.get());
if (!srb->create()) {
rhi->endOffscreenFrame();
return result;
}
std::unique_ptr<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, loadShader(":/data/fullscreenquad.vert.qsb") },
{ QRhiShaderStage::Fragment, loadShader(fragShader) } });
pipeline->setShaderResourceBindings(srb.get());
pipeline->setRenderPassDescriptor(rpDesc.get());
if (!pipeline->create()) {
rhi->endOffscreenFrame();
return result;
}
cb->beginPass(rt.get(), Qt::black, { 1.0f, 0 }, updates);
cb->setGraphicsPipeline(pipeline.get());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
cb->setShaderResources();
cb->draw(3);
cb->endPass();
QRhiReadbackResult readResult;
readResult.completed = [&readResult, &result] {
result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
readResult.pixelSize.width(), readResult.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied).copy();
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({ texture }, &readResult);
cb->resourceUpdate(readbackBatch);
rhi->endOffscreenFrame();
return result;
};
// Part 1, write view format. Render the same known, linear color into two
// sRGB textures. The second one has a non-sRGB write view format, which
// must switch off the implicit linear->sRGB conversion the hardware
// performs on shader writes. Reading back gives the stored bytes in both
// cases, so the results must differ, and the second one must match what
// the fragment shader wrote.
const QRhiTexture::Flags texFlags = QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource | QRhiTexture::sRGB;
std::unique_ptr<QRhiTexture> srgbTex(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1, texFlags));
QVERIFY(srgbTex->create());
std::unique_ptr<QRhiTexture> castTex(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1, texFlags));
castTex->setWriteViewFormat({ QRhiTexture::RGBA8, false });
QVERIFY(castTex->create());
std::unique_ptr<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 16));
QVERIFY(ubuf->create());
const QVector4D color = { 0.5f, 0.5f, 0.5f, 1.0f };
auto setupColorSrb = [&ubuf](QRhiShaderResourceBindings *srb) {
srb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::FragmentStage, ubuf.get())
});
};
auto uploadColor = [&ubuf, &color](QRhiResourceUpdateBatch *u) {
u->updateDynamicBuffer(ubuf.get(), 0, 16, &color);
};
const QImage srgbResult = renderAndReadBack(srgbTex.get(), ":/data/fullscreenquad_color.frag.qsb",
setupColorSrb, uploadColor);
QCOMPARE(srgbResult.size(), outputSize);
const QImage castResult = renderAndReadBack(castTex.get(), ":/data/fullscreenquad_color.frag.qsb",
setupColorSrb, uploadColor);
QCOMPARE(castResult.size(), outputSize);
// Part 2, read view format. Sample a plain, non-sRGB texture holding a
// known byte value, once as-is and once through an sRGB read view. The
// latter makes the hardware decode sRGB->linear when sampling, so the
// value that ends up in the (non-sRGB) render target is lower.
const int srcValue = 188;
QImage srcImage(outputSize, QImage::Format_RGBA8888);
srcImage.fill(QColor(srcValue, srcValue, srcValue));
std::unique_ptr<QRhiTexture> plainSrc(rhi->newTexture(QRhiTexture::RGBA8, outputSize));
QVERIFY(plainSrc->create());
std::unique_ptr<QRhiTexture> srgbViewSrc(rhi->newTexture(QRhiTexture::RGBA8, outputSize));
srgbViewSrc->setReadViewFormat({ QRhiTexture::RGBA8, true });
QVERIFY(srgbViewSrc->create());
std::unique_ptr<QRhiSampler> sampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
QVERIFY(sampler->create());
std::unique_ptr<QRhiTexture> outTex(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(outTex->create());
auto sampleFrom = [&](QRhiTexture *src) {
return renderAndReadBack(outTex.get(), ":/data/fullscreenquad_texture.frag.qsb",
[&](QRhiShaderResourceBindings *srb) {
srb->setBindings({
QRhiShaderResourceBinding::sampledTexture(0, QRhiShaderResourceBinding::FragmentStage,
src, sampler.get())
});
},
[&](QRhiResourceUpdateBatch *u) { u->uploadTexture(src, srcImage); });
};
const QImage plainSampled = sampleFrom(plainSrc.get());
QCOMPARE(plainSampled.size(), outputSize);
const QImage srgbSampled = sampleFrom(srgbViewSrc.get());
QCOMPARE(srgbSampled.size(), outputSize);
if (impl == QRhi::Null)
return;
const int maxFuzz = 2;
// Part 1. Casting to non-sRGB must have suppressed the conversion, so the
// stored value is the one the shader wrote. Without the cast hardware encoded
// to sRGB, so the stored value is visibly higher.
QVERIFY(qAbs(qRed(castResult.pixel(32, 32)) - 128) <= maxFuzz);
QVERIFY(qRed(srgbResult.pixel(32, 32)) > qRed(castResult.pixel(32, 32)) + 32);
// Part 2. Without a read view format the sampled value passes through
// unchanged. With the sRGB read view, 188/255 is decoded as sRGB, giving ~0.5 linear
QVERIFY(qAbs(qRed(plainSampled.pixel(32, 32)) - srcValue) <= maxFuzz);
QVERIFY(qAbs(qRed(srgbSampled.pixel(32, 32)) - 128) <= maxFuzz);
}
void tst_QRhi::textureArrayRange_data()
{
rhiTestData();
}
void tst_QRhi::textureArrayRange()
{
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 texture array ranges");
if (!rhi->isFeatureSupported(QRhi::TextureArrays))
QSKIP("Texture arrays are not supported, skipping test");
if (!rhi->isFeatureSupported(QRhi::TextureArrayRange))
QSKIP("Selecting a texture array range is not supported, skipping test");
const QSize layerSize(64, 64);
const QColor layerColors[3] = { Qt::red, Qt::green, Qt::blue };
std::unique_ptr<QRhiTexture> texArray(rhi->newTextureArray(QRhiTexture::RGBA8, 3, layerSize));
texArray->setArrayRange(1, 1);
QVERIFY(texArray->create());
std::unique_ptr<QRhiTexture> outTex(rhi->newTexture(QRhiTexture::RGBA8, layerSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(outTex->create());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ outTex.get() }));
std::unique_ptr<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.get());
QVERIFY(rt->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
for (int layer = 0; layer < 3; ++layer) {
QImage img(layerSize, QImage::Format_RGBA8888);
img.fill(layerColors[layer]);
QRhiTextureSubresourceUploadDescription subresDesc(img);
QRhiTextureUploadEntry entry(layer, 0, subresDesc);
updates->uploadTexture(texArray.get(), QRhiTextureUploadDescription({ entry }));
}
std::unique_ptr<QRhiSampler> sampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
QVERIFY(sampler->create());
std::unique_ptr<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({QRhiShaderResourceBinding::sampledTexture(0, QRhiShaderResourceBinding::FragmentStage,
texArray.get(), sampler.get())
});
QVERIFY(srb->create());
std::unique_ptr<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
QShader vs = loadShader(":/data/fullscreenquad.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(":/data/sampletexturearraylayer0.frag.qsb");
QVERIFY(fs.isValid());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
pipeline->setShaderResourceBindings(srb.get());
pipeline->setRenderPassDescriptor(rpDesc.get());
QVERIFY(pipeline->create());
cb->beginPass(rt.get(), Qt::black, { 1.0f, 0 }, updates);
cb->setGraphicsPipeline(pipeline.get());
cb->setViewport({ 0, 0, float(layerSize.width()), float(layerSize.height()) });
cb->setShaderResources();
cb->draw(3);
cb->endPass();
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).copy();
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({ outTex.get() }, &readResult);
cb->resourceUpdate(readbackBatch);
rhi->endOffscreenFrame();
QCOMPARE(result.size(), layerSize);
if (impl == QRhi::Null)
return;
// Layer 0 of the range is layer 1 of the array, which is green.
const QRgb c = result.pixel(32, 32);
const int maxFuzz = 1;
QVERIFY(qRed(c) <= maxFuzz);
QVERIFY(qGreen(c) >= 255 - maxFuzz);
QVERIFY(qBlue(c) <= maxFuzz);
}
void tst_QRhi::pushConstants_data()
{
rhiTestData();
}
void tst_QRhi::pushConstants()
{
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 push constants");
if (!rhi->isFeatureSupported(QRhi::PushConstants)) {
QCOMPARE(rhi->resourceLimit(QRhi::MaxPushConstantsSize), 0);
QSKIP("Push constants are not supported with this backend, skipping test");
}
QVERIFY(rhi->resourceLimit(QRhi::MaxPushConstantsSize) >= 128);
const QSize outputSize(512, 512);
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();
QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(triangleVertices)));
QVERIFY(vbuf->create());
updates->uploadStaticBuffer(vbuf.data(), triangleVertices);
// A uniform buffer alongside the push constant block: the fragment shader
// has both, which with HLSL means the uniform block cannot stay on the
// register the push constants take.
static const float tint[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
QScopedPointer<QRhiBuffer> ubuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(tint)));
QVERIFY(ubuf->create());
updates->updateDynamicBuffer(ubuf.data(), 0, sizeof(tint), tint);
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::FragmentStage, ubuf.data())
});
QVERIFY(srb->create());
QShader vs = loadShader(":/data/pushconstants.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(":/data/pushconstants.frag.qsb");
QVERIFY(fs.isValid());
QCOMPARE(vs.description().pushConstantBlocks().count(), 1);
QCOMPARE(vs.description().pushConstantBlocks().first().size, 112);
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({ { 2 * sizeof(float) } });
inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
// Two pipelines from the same shaders, to exercise whatever the backend
// caches per shader for the push constant block.
QScopedPointer<QRhiGraphicsPipeline> pipeline1(rhi->newGraphicsPipeline());
QScopedPointer<QRhiGraphicsPipeline> pipeline2(rhi->newGraphicsPipeline());
for (QRhiGraphicsPipeline *p : { pipeline1.data(), pipeline2.data() }) {
p->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
p->setVertexInputLayout(inputLayout);
p->setShaderResourceBindings(srb.data());
p->setRenderPassDescriptor(rpDesc.data());
QVERIFY(p->create());
}
// Two draws differing only in the push constant data: a red triangle in
// the left half and a green one in the right half. The block is set in
// three parts, so partial updates at a non-zero offset are covered too.
static const float leftRed[8] = { -0.5f, 0.0f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f };
static const float rightGreen[8] = { 0.5f, 0.0f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f };
// mat3 colorMat (identity, 16 byte column stride) followed by
// float weights[2] (0.5 each, 16 byte array stride), std140 both
static const float tail[20] = {
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.0f, 0.0f, 0.0f,
0.5f, 0.0f, 0.0f, 0.0f
};
cb->beginPass(rt.data(), Qt::blue, { 1.0f, 0 }, updates);
QRhiCommandBuffer::VertexInput vbindings(vbuf.data(), 0);
cb->setGraphicsPipeline(pipeline1.data());
cb->setViewport({ 0, 0, float(outputSize.width()), float(outputSize.height()) });
cb->setShaderResources();
cb->setVertexInput(0, 1, &vbindings);
cb->setPushConstants(0, 16, leftRed);
cb->setPushConstants(16, 16, leftRed + 4);
cb->setPushConstants(32, 80, tail);
cb->draw(3);
// The data does not survive the pipeline change, so set it again.
cb->setGraphicsPipeline(pipeline2.data());
cb->setShaderResources();
cb->setVertexInput(0, 1, &vbindings);
cb->setPushConstants(0, 16, rightGreen);
cb->setPushConstants(16, 16, rightGreen + 4);
cb->setPushConstants(32, 80, tail);
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);
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({ texture.data() }, &readResult);
cb->endPass(readbackBatch);
rhi->endOffscreenFrame();
QCOMPARE(result.size(), texture->pixelSize());
if (impl == QRhi::Null)
return;
// The readback is RGBA8888; convert so that qRed() and friends work.
const QImage img = result.convertToFormat(QImage::Format_ARGB32);
int redCount = 0;
int greenCount = 0;
int blueCount = 0;
int redOnRight = 0;
int greenOnLeft = 0;
for (int y = 0; y < img.height(); ++y) {
const QRgb *p = reinterpret_cast<const QRgb *>(img.constScanLine(y));
for (int x = 0; x < img.width(); ++x) {
const QRgb c(*p++);
if (qRed(c) >= 254 && qGreen(c) == 0 && qBlue(c) == 0) {
++redCount;
if (x >= img.width() / 2)
++redOnRight;
} else if (qRed(c) == 0 && qGreen(c) >= 254 && qBlue(c) == 0) {
++greenCount;
if (x < img.width() / 2)
++greenOnLeft;
} else if (qRed(c) == 0 && qGreen(c) == 0 && qBlue(c) >= 254) {
++blueCount;
} else {
QFAIL("Encountered a pixel that is neither red, green nor blue");
}
}
}
// Both triangles must be present and about the same size, the red one in
// the left half and the green one in the right half. Any of this failing
// means the per-draw push constant data did not reach the shaders. The
// colors being exactly saturated also means the uniform buffer was read
// from the right place.
QVERIFY(redCount > 1000);
QVERIFY(greenCount > 1000);
QVERIFY(blueCount > 1000);
QVERIFY(qAbs(redCount - greenCount) < redCount / 10);
QCOMPARE(redOnRight, 0);
QCOMPARE(greenOnLeft, 0);
}
void tst_QRhi::pushConstantsCompute_data()
{
rhiTestData();
}
void tst_QRhi::pushConstantsCompute()
{
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 push constants with compute");
if (!rhi->isFeatureSupported(QRhi::PushConstants))
QSKIP("Push constants are not supported with this backend, skipping test");
if (!rhi->isFeatureSupported(QRhi::Compute))
QSKIP("Compute is not supported with this backend, skipping test");
static const int ELEM_COUNT = 256;
QScopedPointer<QRhiBuffer> outBuf(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer,
ELEM_COUNT * sizeof(quint32)));
QVERIFY(outBuf->create());
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, outBuf.data())
});
QVERIFY(srb->create());
QShader cs = loadShader(":/data/pushconstants.comp.qsb");
QVERIFY(cs.isValid());
// Two pipelines from the same shader: the second one takes whatever the
// backend cached for the first, which has to include the push constant
// block or its root signature ends up without one.
QScopedPointer<QRhiComputePipeline> pipeline1(rhi->newComputePipeline());
QScopedPointer<QRhiComputePipeline> pipeline2(rhi->newComputePipeline());
for (QRhiComputePipeline *p : { pipeline1.data(), pipeline2.data() }) {
p->setShaderStage({ QRhiShaderStage::Compute, cs });
p->setShaderResourceBindings(srb.data());
QVERIFY(p->create());
}
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
QByteArray zeroes(ELEM_COUNT * sizeof(quint32), 0);
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
updates->uploadStaticBuffer(outBuf.data(), zeroes.constData());
// base and stride are set separately, covering a non-zero offset.
const quint32 base = 1000;
const quint32 stride = 7;
cb->beginComputePass(updates);
cb->setComputePipeline(pipeline1.data());
cb->setShaderResources();
cb->setPushConstants(0, sizeof(quint32), &base);
cb->setPushConstants(sizeof(quint32), sizeof(quint32), &stride);
cb->dispatch(ELEM_COUNT / 16, 1, 1);
cb->endComputePass();
const quint32 base2 = 5;
const quint32 stride2 = 0;
cb->beginComputePass();
cb->setComputePipeline(pipeline2.data());
cb->setShaderResources();
cb->setPushConstants(0, sizeof(quint32), &base2);
cb->setPushConstants(sizeof(quint32), sizeof(quint32), &stride2);
cb->dispatch(1, 1, 1);
cb->endComputePass();
QRhiReadbackResult readResult;
QByteArray bufferData;
readResult.completed = [&readResult, &bufferData] { bufferData = readResult.data; };
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackBuffer(outBuf.data(), 0, ELEM_COUNT * sizeof(quint32), &readResult);
cb->resourceUpdate(readbackBatch);
rhi->endOffscreenFrame();
if (impl == QRhi::Null)
return;
QCOMPARE(bufferData.size(), qsizetype(ELEM_COUNT * sizeof(quint32)));
const quint32 *p = reinterpret_cast<const quint32 *>(bufferData.constData());
// The second dispatch covers the first 16 elements only.
for (int i = 0; i < 16; ++i)
QCOMPARE(p[i], base2);
for (int i = 16; i < ELEM_COUNT; ++i)
QCOMPARE(p[i], base + quint32(i) * stride);
}
void tst_QRhi::pushConstantsStd430_data()
{
rhiTestData();
}
void tst_QRhi::pushConstantsStd430()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
// A push constant block defaults to std430, where an array of scalars or
// of vec2 is tightly packed. The backends that hand the block to the GPU
// as-is do not care, but OpenGL, which sets the members one by one, does.
// There is no D3D here because an HLSL cbuffer cannot express this.
if (impl == QRhi::D3D11 || impl == QRhi::D3D12)
QSKIP("Tightly packed arrays in a push constant block are not expressible in HLSL");
QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
if (!rhi)
QSKIP("QRhi could not be created, skipping testing push constants");
if (!rhi->isFeatureSupported(QRhi::PushConstants))
QSKIP("Push constants are not supported with this backend, skipping test");
if (!rhi->isFeatureSupported(QRhi::Compute))
QSKIP("Compute is not supported with this backend, skipping test");
static const int ELEM_COUNT = 17; // 4 floats, 2 vec2s, one mat3
QScopedPointer<QRhiBuffer> outBuf(rhi->newBuffer(QRhiBuffer::Static, QRhiBuffer::StorageBuffer,
ELEM_COUNT * sizeof(float)));
QVERIFY(outBuf->create());
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({
QRhiShaderResourceBinding::bufferLoadStore(0, QRhiShaderResourceBinding::ComputeStage, outBuf.data())
});
QVERIFY(srb->create());
QShader cs = loadShader(":/data/pushconstants_std430.comp.qsb");
QVERIFY(cs.isValid());
QCOMPARE(cs.description().pushConstantBlocks().count(), 1);
QCOMPARE(cs.description().pushConstantBlocks().first().size, 80);
QScopedPointer<QRhiComputePipeline> pipeline(rhi->newComputePipeline());
pipeline->setShaderStage({ QRhiShaderStage::Compute, cs });
pipeline->setShaderResourceBindings(srb.data());
QVERIFY(pipeline->create());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
QByteArray zeroes(ELEM_COUNT * sizeof(float), 0);
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
updates->uploadStaticBuffer(outBuf.data(), zeroes.constData());
// float weights[4] and vec2 v2s[2] are tightly packed, the mat3 columns
// are 16 bytes apart even here.
static const float pc[20] = {
1.0f, 2.0f, 3.0f, 4.0f,
5.0f, 6.0f, 7.0f, 8.0f,
9.0f, 10.0f, 11.0f, 0.0f,
12.0f, 13.0f, 14.0f, 0.0f,
15.0f, 16.0f, 17.0f, 0.0f
};
cb->beginComputePass(updates);
cb->setComputePipeline(pipeline.data());
cb->setShaderResources();
cb->setPushConstants(0, sizeof(pc), pc);
cb->dispatch(1, 1, 1);
cb->endComputePass();
QRhiReadbackResult readResult;
QByteArray bufferData;
readResult.completed = [&readResult, &bufferData] { bufferData = readResult.data; };
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackBuffer(outBuf.data(), 0, ELEM_COUNT * sizeof(float), &readResult);
cb->resourceUpdate(readbackBatch);
rhi->endOffscreenFrame();
if (impl == QRhi::Null)
return;
QCOMPARE(bufferData.size(), qsizetype(ELEM_COUNT * sizeof(float)));
const float *p = reinterpret_cast<const float *>(bufferData.constData());
// 1 to 17, in order: anything else means the block was taken apart with
// the wrong strides.
for (int i = 0; i < ELEM_COUNT; ++i)
QCOMPARE(p[i], float(i + 1));
}
#include <tst_qrhi.moc>
QTEST_MAIN(tst_QRhi)