qt5base-lts/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp
Laszlo Agocs 8cdc9ac5b2 rhi: vulkan: Fix calling finish() twice with some copy commands in-between
The native command buffer handle was not updated, so the subsequent
finish() call attempted to record an invalid VkCommandBuffer. The
problem was not present with offscreen frames, only when finish() is
called with a swapchain-based frame active.

Task-number: QTBUG-84066
Change-Id: I9c4cb701c3dbbc28f237d6ae1cbf65aafd1fa95f
Reviewed-by: Christian Strømme <christian.stromme@qt.io>
2020-05-07 21:15:52 +02:00

2680 lines
104 KiB
C++

/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include <QtTest/QtTest>
#include <QThread>
#include <QFile>
#include <QOffscreenSurface>
#include <QPainter>
#include <QtGui/private/qrhi_p.h>
#include <QtGui/private/qrhinull_p.h>
#if QT_CONFIG(opengl)
# include <QOpenGLContext>
# include <QtGui/private/qrhigles2_p.h>
# define TST_GL
#endif
#if QT_CONFIG(vulkan)
# include <QVulkanInstance>
# include <QtGui/private/qrhivulkan_p.h>
# define TST_VK
#endif
#ifdef Q_OS_WIN
#include <QtGui/private/qrhid3d11_p.h>
# define TST_D3D11
#endif
#if defined(Q_OS_MACOS) || defined(Q_OS_IOS)
# include <QtGui/private/qrhimetal_p.h>
# define TST_MTL
#endif
Q_DECLARE_METATYPE(QRhi::Implementation)
Q_DECLARE_METATYPE(QRhiInitParams *)
class tst_QRhi : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void cleanupTestCase();
void rhiTestData();
void create_data();
void create();
void nativeHandles_data();
void nativeHandles();
void nativeTexture_data();
void nativeTexture();
void nativeBuffer_data();
void nativeBuffer();
void resourceUpdateBatchBuffer_data();
void resourceUpdateBatchBuffer();
void resourceUpdateBatchRGBATextureUpload_data();
void resourceUpdateBatchRGBATextureUpload();
void resourceUpdateBatchRGBATextureCopy_data();
void resourceUpdateBatchRGBATextureCopy();
void resourceUpdateBatchRGBATextureMip_data();
void resourceUpdateBatchRGBATextureMip();
void invalidPipeline_data();
void invalidPipeline();
void renderToTextureSimple_data();
void renderToTextureSimple();
void renderToTextureMip_data();
void renderToTextureMip();
void renderToTextureCubemapFace_data();
void renderToTextureCubemapFace();
void renderToTextureTexturedQuad_data();
void renderToTextureTexturedQuad();
void renderToTextureArrayOfTexturedQuad_data();
void renderToTextureArrayOfTexturedQuad();
void renderToTextureTexturedQuadAndUniformBuffer_data();
void renderToTextureTexturedQuadAndUniformBuffer();
void renderToWindowSimple_data();
void renderToWindowSimple();
void finishWithinSwapchainFrame_data();
void finishWithinSwapchainFrame();
void srbLayoutCompatibility_data();
void srbLayoutCompatibility();
void renderPassDescriptorCompatibility_data();
void renderPassDescriptorCompatibility();
private:
void setWindowType(QWindow *window, QRhi::Implementation impl);
struct {
QRhiNullInitParams null;
#ifdef TST_GL
QRhiGles2InitParams gl;
#endif
#ifdef TST_VK
QRhiVulkanInitParams vk;
#endif
#ifdef TST_D3D11
QRhiD3D11InitParams d3d;
#endif
#ifdef TST_MTL
QRhiMetalInitParams mtl;
#endif
} initParams;
#ifdef TST_VK
QVulkanInstance vulkanInstance;
#endif
QOffscreenSurface *fallbackSurface = nullptr;
};
void tst_QRhi::initTestCase()
{
#ifdef TST_GL
fallbackSurface = QRhiGles2InitParams::newFallbackSurface();
initParams.gl.fallbackSurface = fallbackSurface;
#endif
#ifdef TST_VK
#ifndef Q_OS_ANDROID
vulkanInstance.setLayers({ QByteArrayLiteral("VK_LAYER_LUNARG_standard_validation") });
#else
vulkanInstance.setLayers({ QByteArrayLiteral("VK_LAYER_GOOGLE_threading"),
QByteArrayLiteral("VK_LAYER_LUNARG_parameter_validation"),
QByteArrayLiteral("VK_LAYER_LUNARG_object_tracker"),
QByteArrayLiteral("VK_LAYER_LUNARG_core_validation"),
QByteArrayLiteral("VK_LAYER_LUNARG_image"),
QByteArrayLiteral("VK_LAYER_LUNARG_swapchain"),
QByteArrayLiteral("VK_LAYER_GOOGLE_unique_objects") });
#endif
vulkanInstance.setExtensions(QByteArrayList()
<< "VK_KHR_get_physical_device_properties2");
vulkanInstance.create();
initParams.vk.inst = &vulkanInstance;
#endif
#ifdef TST_D3D11
initParams.d3d.enableDebugLayer = true;
#endif
}
void tst_QRhi::cleanupTestCase()
{
#ifdef TST_VK
vulkanInstance.destroy();
#endif
delete fallbackSurface;
}
void tst_QRhi::rhiTestData()
{
QTest::addColumn<QRhi::Implementation>("impl");
QTest::addColumn<QRhiInitParams *>("initParams");
QTest::newRow("Null") << QRhi::Null << static_cast<QRhiInitParams *>(&initParams.null);
#ifdef TST_GL
QTest::newRow("OpenGL") << QRhi::OpenGLES2 << static_cast<QRhiInitParams *>(&initParams.gl);
#endif
#ifdef TST_VK
if (vulkanInstance.isValid())
QTest::newRow("Vulkan") << QRhi::Vulkan << static_cast<QRhiInitParams *>(&initParams.vk);
#endif
#ifdef TST_D3D11
QTest::newRow("Direct3D 11") << QRhi::D3D11 << static_cast<QRhiInitParams *>(&initParams.d3d);
#endif
#ifdef TST_MTL
QTest::newRow("Metal") << QRhi::Metal << static_cast<QRhiInitParams *>(&initParams.mtl);
#endif
}
void tst_QRhi::create_data()
{
rhiTestData();
}
static int aligned(int v, int a)
{
return (v + a - 1) & ~(a - 1);
}
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) {
QCOMPARE(rhi->backend(), impl);
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->runCleanup();
QCOMPARE(cleanupOk, 1);
cleanupOk = 0;
rhi->addCleanupCallback(cleanupFunc);
QRhiResourceUpdateBatch *resUpd = rhi->nextResourceUpdateBatch();
QVERIFY(resUpd);
resUpd->release();
QVERIFY(!rhi->supportedSampleCounts().isEmpty());
QVERIFY(rhi->supportedSampleCounts().contains(1));
QVERIFY(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);
QVERIFY(texMin >= 1);
QVERIFY(texMax >= texMin);
QVERIFY(maxAtt >= 1);
QVERIFY(framesInFlight >= 1);
QVERIFY(rhi->nativeHandles());
QVERIFY(rhi->profiler());
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
};
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, 1);
}
}
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->physDev);
QVERIFY(vkHandles->dev);
QVERIFY(vkHandles->gfxQueueFamilyIdx >= 0);
QVERIFY(vkHandles->gfxQueue);
QVERIFY(vkHandles->cmdPool);
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);
}
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);
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_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->build());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ tex.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
QVERIFY(rpDesc);
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->build());
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->build());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ tex.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
QVERIFY(rpDesc);
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->build());
const QRhiNativeHandles *rpHandles = rpDesc->nativeHandles();
switch (impl) {
case QRhi::Null:
break;
#ifdef TST_VK
case QRhi::Vulkan:
{
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_MTL
case QRhi::Metal:
break;
#endif
default:
Q_ASSERT(false);
}
}
}
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->build());
const QRhiTexture::NativeTexture nativeTex = tex->nativeTexture();
switch (impl) {
case QRhi::Null:
break;
#ifdef TST_VK
case QRhi::Vulkan:
{
auto *image = static_cast<const VkImage *>(nativeTex.object);
QVERIFY(image);
QVERIFY(*image);
QVERIFY(nativeTex.layout >= 1); // VK_IMAGE_LAYOUT_GENERAL
QVERIFY(nativeTex.layout <= 8); // VK_IMAGE_LAYOUT_PREINITIALIZED
}
break;
#endif
#ifdef TST_GL
case QRhi::OpenGLES2:
{
auto *textureId = static_cast<const uint *>(nativeTex.object);
QVERIFY(textureId);
QVERIFY(*textureId);
}
break;
#endif
#ifdef TST_D3D11
case QRhi::D3D11:
{
auto *texture = static_cast<void * const *>(nativeTex.object);
QVERIFY(texture);
QVERIFY(*texture);
}
break;
#endif
#ifdef TST_MTL
case QRhi::Metal:
{
void * const * texture = (void * const *)nativeTex.object;
QVERIFY(texture);
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->build());
const QRhiBuffer::NativeBuffer nativeBuf = buf->nativeBuffer();
QVERIFY(nativeBuf.slotCount <= rhi->resourceLimit(QRhi::FramesInFlight));
switch (impl) {
case QRhi::Null:
break;
#ifdef TST_VK
case QRhi::Vulkan:
{
QVERIFY(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:
{
QVERIFY(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:
{
QVERIFY(nativeBuf.slotCount >= 1); // always backed by native buffers
for (int i = 0; i < nativeBuf.slotCount; ++i) {
auto *buffer = static_cast<void * const *>(nativeBuf.objects[i]);
QVERIFY(buffer);
QVERIFY(*buffer);
}
}
break;
#endif
#ifdef TST_MTL
case QRhi::Metal:
{
QVERIFY(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);
}
}
}
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->build());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QVERIFY(batch);
batch->updateDynamicBuffer(dynamicBuffer.data(), 10, bufferSize - 10, a.constData());
batch->updateDynamicBuffer(dynamicBuffer.data(), 0, 12, b.constData());
QRhiBufferReadbackResult 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);
QVERIFY(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->build());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QVERIFY(batch);
batch->uploadStaticBuffer(dynamicBuffer.data(), 10, bufferSize - 10, a.constData());
batch->uploadStaticBuffer(dynamicBuffer.data(), 0, 12, b.constData());
QRhiBufferReadbackResult readResult;
bool readCompleted = false;
readResult.completed = [&readCompleted] { readCompleted = true; };
if (rhi->isFeatureSupported(QRhi::ReadBackNonUniformBuffer))
batch->readBackBuffer(dynamicBuffer.data(), 5, 10, &readResult);
QVERIFY(submitResourceUpdates(rhi.data(), batch));
if (rhi->isFeatureSupported(QRhi::ReadBackNonUniformBuffer)) {
QVERIFY(readCompleted);
QVERIFY(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");
}
}
}
inline bool imageRGBAEquals(const QImage &a, const QImage &b)
{
const 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->build());
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->build());
QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch();
QRhiTextureUploadEntry upload(0, 0, { image.constBits(), int(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->build());
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->build());
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()),
int(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->build());
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));
}
}
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->build());
batch->uploadTexture(redTexture.data(), red);
QScopedPointer<QRhiTexture> greenTexture(rhi->newTexture(QRhiTexture::RGBA8, green.size(),
1, QRhiTexture::UsedAsTransferSource));
QVERIFY(greenTexture->build());
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->build());
// 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->build());
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));
}
}
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->build());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->build());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->build());
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->build());
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->build());
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->build());
// no vertex inputs
pipeline.reset(rhi->newGraphicsPipeline());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
pipeline->setRenderPassDescriptor(rpDesc.data());
pipeline->setShaderResourceBindings(srb.data());
QVERIFY(!pipeline->build());
// no renderpass descriptor
pipeline.reset(rhi->newGraphicsPipeline());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.data());
QVERIFY(!pipeline->build());
// 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->build());
// 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->build());
}
void tst_QRhi::renderToTextureSimple_data()
{
rhiTestData();
}
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->build());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->build());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
static const float vertices[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
0.0f, 1.0f
};
QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
QVERIFY(vbuf->build());
updates->uploadStaticBuffer(vbuf.data(), vertices);
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->build());
QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
QShader vs = loadShader(":/data/simple.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(":/data/simple.frag.qsb");
QVERIFY(fs.isValid());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({ { 2 * sizeof(float) } });
inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.data());
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline->build());
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())
QVERIFY(redCount < blueCount);
else
QVERIFY(redCount > blueCount);
}
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->build());
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->build());
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();
static const float vertices[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
0.0f, 1.0f
};
QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
QVERIFY(vbuf->build());
updates->uploadStaticBuffer(vbuf.data(), vertices);
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->build());
QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
QShader vs = loadShader(":/data/simple.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(":/data/simple.frag.qsb");
QVERIFY(fs.isValid());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({ { 2 * sizeof(float) } });
inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.data());
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline->build());
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())
QVERIFY(redCount > blueCount); // 100, 28
else
QVERIFY(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->build());
const int LAYER = 1; // render into the layer for face -X
const int BAD_LAYER = 2; // +Y
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->build());
QCOMPARE(rt->pixelSize(), texture->pixelSize());
QCOMPARE(rt->pixelSize(), outputSize);
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
static const float vertices[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
0.0f, 1.0f
};
QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
QVERIFY(vbuf->build());
updates->uploadStaticBuffer(vbuf.data(), vertices);
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->build());
QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
QShader vs = loadShader(":/data/simple.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(":/data/simple.frag.qsb");
QVERIFY(fs.isValid());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({ { 2 * sizeof(float) } });
inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.data());
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline->build());
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
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);
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, outputSize.width());
if (rhi->isYUpInFramebuffer() == rhi->isYUpInNDC())
QVERIFY(redCount < blueCount); // 100, 412
else
QVERIFY(redCount > blueCount); // 412, 100
}
void tst_QRhi::renderToTextureTexturedQuad_data()
{
rhiTestData();
}
void tst_QRhi::renderToTextureTexturedQuad()
{
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->build());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->build());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
static const float verticesUvs[] = {
-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
};
QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(verticesUvs)));
QVERIFY(vbuf->build());
updates->uploadStaticBuffer(vbuf.data(), verticesUvs);
QScopedPointer<QRhiTexture> inputTexture(rhi->newTexture(QRhiTexture::RGBA8, inputImage.size()));
QVERIFY(inputTexture->build());
updates->uploadTexture(inputTexture.data(), inputImage);
QScopedPointer<QRhiSampler> sampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
QVERIFY(sampler->build());
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
srb->setBindings({
QRhiShaderResourceBinding::sampledTexture(0, QRhiShaderResourceBinding::FragmentStage, inputTexture.data(), sampler.data())
});
QVERIFY(srb->build());
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->build());
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 = std::move(result).mirrored();
// 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);
QVERIFY(qGreen(result.pixel(32, 52)) > 2 * qRed(result.pixel(32, 52)));
QVERIFY(qGreen(result.pixel(32, 52)) > 2 * qBlue(result.pixel(32, 52)));
QVERIFY(qGreen(result.pixel(214, 191)) > 2 * qRed(result.pixel(214, 191)));
QVERIFY(qGreen(result.pixel(214, 191)) > 2 * qBlue(result.pixel(214, 191)));
}
void tst_QRhi::renderToTextureArrayOfTexturedQuad_data()
{
rhiTestData();
}
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");
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->build());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->build());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
static const float verticesUvs[] = {
-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
};
QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(verticesUvs)));
QVERIFY(vbuf->build());
updates->uploadStaticBuffer(vbuf.data(), verticesUvs);
// 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->build());
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->build());
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->build());
updates->uploadTexture(greenTexture.data(), greenImage);
QScopedPointer<QRhiSampler> sampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
QVERIFY(sampler->build());
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->build());
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->build());
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 = std::move(result).mirrored();
// 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::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->build());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->build());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
static const float verticesUvs[] = {
-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
};
QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(verticesUvs)));
QVERIFY(vbuf->build());
updates->uploadStaticBuffer(vbuf.data(), verticesUvs);
// 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->build());
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->build());
updates->uploadTexture(inputTexture.data(), inputImage);
QScopedPointer<QRhiSampler> sampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
QVERIFY(sampler->build());
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->build());
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->build());
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->build());
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 = std::move(result0).mirrored();
result1 = std::move(result1).mirrored();
}
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::setWindowType(QWindow *window, QRhi::Implementation impl)
{
switch (impl) {
case QRhi::OpenGLES2:
#if QT_CONFIG(opengl)
window->setFormat(QRhiGles2InitParams::adjustedFormat());
#endif
Q_FALLTHROUGH();
case QRhi::D3D11:
window->setSurfaceType(QSurface::OpenGLSurface);
break;
case QRhi::Metal:
window->setSurfaceType(QSurface::MetalSurface);
break;
case QRhi::Vulkan:
window->setSurfaceType(QSurface::VulkanSurface);
#if QT_CONFIG(vulkan)
window->setVulkanInstance(&vulkanInstance);
#endif
break;
default:
break;
}
}
void tst_QRhi::renderToWindowSimple_data()
{
rhiTestData();
}
void tst_QRhi::renderToWindowSimple()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
#ifdef Q_OS_WINRT
if (impl == QRhi::D3D11)
QSKIP("Skipping window-based QRhi rendering on WinRT as the platform and the D3D11 backend are not prepared for this yet");
#endif
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->buildOrResize());
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
static const float vertices[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
0.0f, 1.0f
};
QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
QVERIFY(vbuf->build());
updates->uploadStaticBuffer(vbuf.data(), vertices);
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->build());
QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
QShader vs = loadShader(":/data/simple.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(":/data/simple.frag.qsb");
QVERIFY(fs.isValid());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({ { 2 * sizeof(float) } });
inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.data());
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline->build());
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;
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();
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) {
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.mirrored();
else
result = wrapperImage.copy();
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({}, &readResult); // read back the current backbuffer
readbackWidth = outputSize.width();
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);
QVERIFY(redCount < blueCount);
}
void tst_QRhi::finishWithinSwapchainFrame_data()
{
rhiTestData();
}
void tst_QRhi::finishWithinSwapchainFrame()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
#ifdef Q_OS_WINRT
if (impl == QRhi::D3D11)
QSKIP("Skipping window-based QRhi rendering on WinRT as the platform and the D3D11 backend are not prepared for this yet");
#endif
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->buildOrResize());
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QVERIFY(srb->build());
QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
QShader vs = loadShader(":/data/simple.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(":/data/simple.frag.qsb");
QVERIFY(fs.isValid());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({ { 2 * sizeof(float) } });
inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.data());
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline->build());
static const float vertices[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
0.0f, 1.0f
};
QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices)));
QVERIFY(vbuf->build());
// exercise begin/endExternal() just a little bit, hence ExternalContentsInPass
QVERIFY(rhi->beginFrame(swapChain.data(), QRhi::ExternalContentsInPass) == 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(), vertices);
cb->beginPass(rt, Qt::blue, { 1.0f, 0 }, updates);
// 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::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->build());
QScopedPointer<QRhiSampler> sampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
QVERIFY(sampler->build());
QScopedPointer<QRhiSampler> otherSampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
QVERIFY(otherSampler->build());
QScopedPointer<QRhiBuffer> buf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 1024));
QVERIFY(buf->build());
QScopedPointer<QRhiBuffer> otherBuf(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 256));
QVERIFY(otherBuf->build());
// empty (compatible)
{
QScopedPointer<QRhiShaderResourceBindings> srb1(rhi->newShaderResourceBindings());
QVERIFY(srb1->build());
QScopedPointer<QRhiShaderResourceBindings> srb2(rhi->newShaderResourceBindings());
QVERIFY(srb2->build());
QVERIFY(srb1->isLayoutCompatible(srb2.data()));
QVERIFY(srb2->isLayoutCompatible(srb1.data()));
}
// different count (not compatible)
{
QScopedPointer<QRhiShaderResourceBindings> srb1(rhi->newShaderResourceBindings());
QVERIFY(srb1->build());
QScopedPointer<QRhiShaderResourceBindings> srb2(rhi->newShaderResourceBindings());
srb2->setBindings({
QRhiShaderResourceBinding::sampledTexture(0, QRhiShaderResourceBinding::FragmentStage, texture.data(), sampler.data())
});
QVERIFY(srb2->build());
QVERIFY(!srb1->isLayoutCompatible(srb2.data()));
QVERIFY(!srb2->isLayoutCompatible(srb1.data()));
}
// 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->build());
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->build());
QVERIFY(srb1->isLayoutCompatible(srb2.data()));
QVERIFY(srb2->isLayoutCompatible(srb1.data()));
}
// different visibility (not compatible)
{
QScopedPointer<QRhiShaderResourceBindings> srb1(rhi->newShaderResourceBindings());
srb1->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, buf.data()),
});
QVERIFY(srb1->build());
QScopedPointer<QRhiShaderResourceBindings> srb2(rhi->newShaderResourceBindings());
srb2->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, buf.data()),
});
QVERIFY(srb2->build());
QVERIFY(!srb1->isLayoutCompatible(srb2.data()));
QVERIFY(!srb2->isLayoutCompatible(srb1.data()));
}
// different binding points (not compatible)
{
QScopedPointer<QRhiShaderResourceBindings> srb1(rhi->newShaderResourceBindings());
srb1->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage, buf.data()),
});
QVERIFY(srb1->build());
QScopedPointer<QRhiShaderResourceBindings> srb2(rhi->newShaderResourceBindings());
srb2->setBindings({
QRhiShaderResourceBinding::uniformBuffer(1, QRhiShaderResourceBinding::VertexStage, buf.data()),
});
QVERIFY(srb2->build());
QVERIFY(!srb1->isLayoutCompatible(srb2.data()));
QVERIFY(!srb2->isLayoutCompatible(srb1.data()));
}
// 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->build());
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->build());
QVERIFY(srb1->isLayoutCompatible(srb2.data()));
QVERIFY(srb2->isLayoutCompatible(srb1.data()));
}
// 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->build());
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->build());
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 texture resource updates");
// 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->build());
QScopedPointer<QRhiTexture> tex2(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 1, QRhiTexture::RenderTarget));
QVERIFY(tex2->build());
QScopedPointer<QRhiRenderBuffer> ds(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, QSize(512, 512)));
QVERIFY(ds->build());
// 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->build());
QScopedPointer<QRhiTextureRenderTarget> rt2(rhi->newTextureRenderTarget({ tex2.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc2(rt2->newCompatibleRenderPassDescriptor());
rt2->setRenderPassDescriptor(rpDesc2.data());
QVERIFY(rt2->build());
QVERIFY(rpDesc->isCompatible(rpDesc2.data()));
QVERIFY(rpDesc2->isCompatible(rpDesc.data()));
}
// 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->build());
QScopedPointer<QRhiTextureRenderTarget> rt2(rhi->newTextureRenderTarget(desc));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc2(rt2->newCompatibleRenderPassDescriptor());
rt2->setRenderPassDescriptor(rpDesc2.data());
QVERIFY(rt2->build());
QVERIFY(rpDesc->isCompatible(rpDesc2.data()));
QVERIFY(rpDesc2->isCompatible(rpDesc.data()));
}
// 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->build());
QScopedPointer<QRhiTextureRenderTarget> rt2(rhi->newTextureRenderTarget({ tex.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc2(rt2->newCompatibleRenderPassDescriptor());
rt2->setRenderPassDescriptor(rpDesc2.data());
QVERIFY(rt2->build());
if (impl == QRhi::Vulkan || impl == QRhi::Metal) {
QVERIFY(!rpDesc->isCompatible(rpDesc2.data()));
QVERIFY(!rpDesc2->isCompatible(rpDesc.data()));
}
}
if (rhi->isFeatureSupported(QRhi::MultisampleRenderBuffer)) {
// resolve attachments (compatible)
{
QScopedPointer<QRhiRenderBuffer> msaaRenderBuffer(rhi->newRenderBuffer(QRhiRenderBuffer::Color, QSize(512, 512), 4));
QVERIFY(msaaRenderBuffer->build());
QScopedPointer<QRhiRenderBuffer> msaaRenderBuffer2(rhi->newRenderBuffer(QRhiRenderBuffer::Color, QSize(512, 512), 4));
QVERIFY(msaaRenderBuffer2->build());
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->build());
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->build());
QVERIFY(rpDesc->isCompatible(rpDesc2.data()));
QVERIFY(rpDesc2->isCompatible(rpDesc.data()));
}
// missing resolve for one of them (not compatible)
{
QScopedPointer<QRhiRenderBuffer> msaaRenderBuffer(rhi->newRenderBuffer(QRhiRenderBuffer::Color, QSize(512, 512), 4));
QVERIFY(msaaRenderBuffer->build());
QScopedPointer<QRhiRenderBuffer> msaaRenderBuffer2(rhi->newRenderBuffer(QRhiRenderBuffer::Color, QSize(512, 512), 4));
QVERIFY(msaaRenderBuffer2->build());
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->build());
QRhiColorAttachment colorAtt2(msaaRenderBuffer2.data()); // color0, multisample
QScopedPointer<QRhiTextureRenderTarget> rt2(rhi->newTextureRenderTarget({ colorAtt2 }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc2(rt2->newCompatibleRenderPassDescriptor());
rt2->setRenderPassDescriptor(rpDesc2.data());
QVERIFY(rt2->build());
if (impl == QRhi::Vulkan) { // no Metal here
QVERIFY(!rpDesc->isCompatible(rpDesc2.data()));
QVERIFY(!rpDesc2->isCompatible(rpDesc.data()));
}
}
} 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->build());
// different texture formats (not compatible)
{
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ tex.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->build());
QScopedPointer<QRhiTextureRenderTarget> rt2(rhi->newTextureRenderTarget({ tex3.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc2(rt2->newCompatibleRenderPassDescriptor());
rt2->setRenderPassDescriptor(rpDesc2.data());
QVERIFY(rt2->build());
if (impl == QRhi::Vulkan || impl == QRhi::Metal) {
QVERIFY(!rpDesc->isCompatible(rpDesc2.data()));
QVERIFY(!rpDesc2->isCompatible(rpDesc.data()));
}
}
} else {
qDebug("Skipping texture format dependent tests");
}
}
#include <tst_qrhi.moc>
QTEST_MAIN(tst_QRhi)