/**************************************************************************** ** ** 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 #include #include #include #include #include #include #if QT_CONFIG(opengl) # include # include # define TST_GL #endif #if QT_CONFIG(vulkan) # include # include # define TST_VK #endif #ifdef Q_OS_WIN #include # define TST_D3D11 #endif #ifdef Q_OS_DARWIN # include # 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 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(); private: 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("impl"); QTest::addColumn("initParams"); QTest::newRow("Null") << QRhi::Null << static_cast(&initParams.null); #ifdef TST_GL QTest::newRow("OpenGL") << QRhi::OpenGLES2 << static_cast(&initParams.gl); #endif #ifdef TST_VK if (vulkanInstance.isValid()) QTest::newRow("Vulkan") << QRhi::Vulkan << static_cast(&initParams.vk); #endif #ifdef TST_D3D11 QTest::newRow("Direct3D 11") << QRhi::D3D11 << static_cast(&initParams.d3d); #endif #ifdef TST_MTL QTest::newRow("Metal") << QRhi::Metal << static_cast(&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 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 }; for (size_t i = 0; i 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 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(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(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(rhiHandles); QVERIFY(d3dHandles->dev); QVERIFY(d3dHandles->context); } break; #endif #ifdef TST_MTL case QRhi::Metal: { const QRhiMetalNativeHandles *mtlHandles = static_cast(rhiHandles); QVERIFY(mtlHandles->dev); QVERIFY(mtlHandles->cmdQueue); } break; #endif default: Q_ASSERT(false); } } // QRhiTexture::nativeHandles() { QScopedPointer tex(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 256))); QVERIFY(tex->build()); const QRhiNativeHandles *texHandles = tex->nativeHandles(); QVERIFY(texHandles); switch (impl) { case QRhi::Null: break; #ifdef TST_VK case QRhi::Vulkan: { const QRhiVulkanTextureNativeHandles *vkHandles = static_cast(texHandles); QVERIFY(vkHandles->image); QVERIFY(vkHandles->layout >= 1); // VK_IMAGE_LAYOUT_GENERAL QVERIFY(vkHandles->layout <= 8); // VK_IMAGE_LAYOUT_PREINITIALIZED } break; #endif #ifdef TST_GL case QRhi::OpenGLES2: { const QRhiGles2TextureNativeHandles *glHandles = static_cast(texHandles); QVERIFY(glHandles->texture); } break; #endif #ifdef TST_D3D11 case QRhi::D3D11: { const QRhiD3D11TextureNativeHandles *d3dHandles = static_cast(texHandles); QVERIFY(d3dHandles->texture); } break; #endif #ifdef TST_MTL case QRhi::Metal: { const QRhiMetalTextureNativeHandles *mtlHandles = static_cast(texHandles); QVERIFY(mtlHandles->texture); } 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(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(cbHandles); QVERIFY(mtlHandles); QVERIFY(mtlHandles->commandBuffer); QVERIFY(!mtlHandles->encoder); QScopedPointer tex(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 1, QRhiTexture::RenderTarget)); QVERIFY(tex->build()); QScopedPointer rt(rhi->newTextureRenderTarget({ tex.data() })); QScopedPointer rpDesc(rt->newCompatibleRenderPassDescriptor()); QVERIFY(rpDesc); rt->setRenderPassDescriptor(rpDesc.data()); QVERIFY(rt->build()); cb->beginPass(rt.data(), Qt::red, { 1.0f, 0 }); QVERIFY(static_cast(cb->nativeHandles())->encoder); cb->endPass(); } break; #endif default: Q_ASSERT(false); } rhi->endOffscreenFrame(); } // QRhiRenderPassDescriptor::nativeHandles() { QScopedPointer tex(rhi->newTexture(QRhiTexture::RGBA8, QSize(512, 512), 1, QRhiTexture::RenderTarget)); QVERIFY(tex->build()); QScopedPointer rt(rhi->newTextureRenderTarget({ tex.data() })); QScopedPointer 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(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); } } } 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 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 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 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(image0.constScanLine(y)); const quint32 *p1 = reinterpret_cast(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 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 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(readResult.data.constData()), readResult.pixelSize.width(), readResult.pixelSize.height(), image.format()); QVERIFY(imageRGBAEquals(image, wrapperImage)); } // the same with raw data { QScopedPointer 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(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 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(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 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(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(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)); } } void tst_QRhi::resourceUpdateBatchRGBATextureCopy_data() { rhiTestData(); } void tst_QRhi::resourceUpdateBatchRGBATextureCopy() { QFETCH(QRhi::Implementation, impl); QFETCH(QRhiInitParams *, initParams); QScopedPointer 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 redTexture(rhi->newTexture(QRhiTexture::RGBA8, red.size(), 1, QRhiTexture::UsedAsTransferSource)); QVERIFY(redTexture->build()); batch->uploadTexture(redTexture.data(), red); QScopedPointer 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 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(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(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(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 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 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(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 rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr)); if (!rhi) QSKIP("QRhi could not be created, skipping testing empty shader"); QScopedPointer texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(256, 256), 1, QRhiTexture::RenderTarget)); QVERIFY(texture->build()); QScopedPointer rt(rhi->newTextureRenderTarget({ texture.data() })); QScopedPointer rpDesc(rt->newCompatibleRenderPassDescriptor()); rt->setRenderPassDescriptor(rpDesc.data()); QVERIFY(rt->build()); QRhiCommandBuffer *cb = nullptr; QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess); QVERIFY(cb); QScopedPointer srb(rhi->newShaderResourceBindings()); QVERIFY(srb->build()); QRhiVertexInputLayout inputLayout; inputLayout.setBindings({ { 2 * sizeof(float) } }); inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } }); // no stages QScopedPointer 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 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 texture(rhi->newTexture(QRhiTexture::RGBA8, outputSize, 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource)); QVERIFY(texture->build()); QScopedPointer rt(rhi->newTextureRenderTarget({ texture.data() })); QScopedPointer 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 vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertices))); QVERIFY(vbuf->build()); updates->uploadStaticBuffer(vbuf.data(), vertices); QScopedPointer srb(rhi->newShaderResourceBindings()); QVERIFY(srb->build()); QScopedPointer 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(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(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); } #include QTEST_MAIN(tst_QRhi)