| /**************************************************************************** |
| ** |
| ** 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 |
| |
| #ifdef Q_OS_DARWIN |
| # 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 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 renderToTextureTexturedQuad_data(); |
| void renderToTextureTexturedQuad(); |
| void renderToTextureTexturedQuadAndUniformBuffer_data(); |
| void renderToTextureTexturedQuadAndUniformBuffer(); |
| void renderToWindowSimple_data(); |
| void renderToWindowSimple(); |
| |
| 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<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 |
| }; |
| 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); |
| } |
| } |
| |
| // QRhiTexture::nativeHandles() |
| { |
| QScopedPointer<QRhiTexture> 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<const QRhiVulkanTextureNativeHandles *>(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<const QRhiGles2TextureNativeHandles *>(texHandles); |
| QVERIFY(glHandles->texture); |
| } |
| break; |
| #endif |
| #ifdef TST_D3D11 |
| case QRhi::D3D11: |
| { |
| const QRhiD3D11TextureNativeHandles *d3dHandles = static_cast<const QRhiD3D11TextureNativeHandles *>(texHandles); |
| QVERIFY(d3dHandles->texture); |
| } |
| break; |
| #endif |
| #ifdef TST_MTL |
| case QRhi::Metal: |
| { |
| const QRhiMetalTextureNativeHandles *mtlHandles = static_cast<const QRhiMetalTextureNativeHandles *>(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<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); |
| } |
| } |
| } |
| |
| 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::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::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::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); |
| 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; |
| } |
| |
| 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 framesInFlight = rhi->resourceLimit(QRhi::FramesInFlight); |
| QVERIFY(framesInFlight >= 1); |
| const int FRAME_COUNT = framesInFlight + 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::FramesInFlight 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); |
| } |
| |
| #include <tst_qrhi.moc> |
| QTEST_MAIN(tst_QRhi) |