// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include #include #include #include #include #include #include #include #include #include #include #if QT_CONFIG(vulkan) #include #endif class tst_QRhiWidget : public QObject { Q_OBJECT private slots: void initTestCase(); void create_data(); void create(); void noCreate(); void simple_data(); void simple(); void msaa_data(); void msaa(); void fixedSize_data(); void fixedSize(); void autoRt_data(); void autoRt(); void reparent_data(); void reparent(); void grabFramebufferWhileStillInvisible_data(); void grabFramebufferWhileStillInvisible(); void grabViaQWidgetGrab_data(); void grabViaQWidgetGrab(); void mirror_data(); void mirror(); private: void testData(); }; void tst_QRhiWidget::initTestCase() { if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::RhiBasedRendering)) QSKIP("RhiBasedRendering capability is reported as unsupported on this platform."); qputenv("QT_RHI_LEAK_CHECK", "1"); } void tst_QRhiWidget::testData() { QTest::addColumn("api"); #ifndef Q_OS_WEBOS QTest::newRow("Null") << QRhiWidget::Api::Null; #endif #if QT_CONFIG(opengl) if (QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::OpenGL)) QTest::newRow("OpenGL") << QRhiWidget::Api::OpenGL; #endif #if QT_CONFIG(vulkan) #if defined(Q_OS_ANDROID) qWarning() << "Skipping Vulkan for Android (QTQAINFRA-5971)"; #else // Have to probe to be sure Vulkan is actually working (the test cases // themselves will assume QRhi init succeeds). if (QVulkanDefaultInstance::instance()) { QRhiVulkanInitParams vulkanInitParams; vulkanInitParams.inst = QVulkanDefaultInstance::instance(); if (QRhi::probe(QRhi::Vulkan, &vulkanInitParams)) QTest::newRow("Vulkan") << QRhiWidget::Api::Vulkan; } #endif #endif #if defined(Q_OS_MACOS) || defined(Q_OS_IOS) QRhiMetalInitParams metalInitParams; if (QRhi::probe(QRhi::Metal, &metalInitParams)) QTest::newRow("Metal") << QRhiWidget::Api::Metal; #endif #ifdef Q_OS_WIN QTest::newRow("D3D11") << QRhiWidget::Api::Direct3D11; // D3D12 needs to be probed too due to being disabled if the SDK headers // are too old (clang, mingw). QRhiD3D12InitParams d3d12InitParams; if (QRhi::probe(QRhi::D3D12, &d3d12InitParams)) QTest::newRow("D3D12") << QRhiWidget::Api::Direct3D12; #endif } void tst_QRhiWidget::create_data() { testData(); } void tst_QRhiWidget::create() { QFETCH(QRhiWidget::Api, api); { QRhiWidget w; w.setApi(api); w.resize(320, 240); w.show(); QVERIFY(QTest::qWaitForWindowExposed(&w)); } { QWidget topLevel; topLevel.resize(320, 240); QRhiWidget *w = new QRhiWidget(&topLevel); w->setApi(api); w->resize(100, 100); topLevel.show(); QVERIFY(QTest::qWaitForWindowExposed(&topLevel)); } } void tst_QRhiWidget::noCreate() { // Now try something that is guaranteed to fail. // E.g. try using Metal on Windows. // The error signal should be emitted. The frame signal should not. #ifdef Q_OS_WIN qDebug("Warnings will be printed below, this is as expected"); QRhiWidget rhiWidget; rhiWidget.setApi(QRhiWidget::Api::Metal); QSignalSpy frameSpy(&rhiWidget, &QRhiWidget::frameSubmitted); QSignalSpy errorSpy(&rhiWidget, &QRhiWidget::renderFailed); rhiWidget.resize(320, 240); rhiWidget.show(); QVERIFY(QTest::qWaitForWindowExposed(&rhiWidget)); QTRY_VERIFY(errorSpy.count() > 0); QCOMPARE(frameSpy.count(), 0); #endif } static QShader getShader(const QString &name) { QFile f(name); return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader(); } 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; } inline bool imageRGBAEquals(const QImage &a, const QImage &b, int maxFuzz = 1) { if (a.size() != b.size()) return false; const QImage image0 = a.convertToFormat(QImage::Format_RGBA8888_Premultiplied); const QImage image1 = b.convertToFormat(QImage::Format_RGBA8888_Premultiplied); const int width = image0.width(); const int height = image0.height(); for (int y = 0; y < height; ++y) { const quint32 *p0 = reinterpret_cast(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; } class SimpleRhiWidget : public QRhiWidget { public: SimpleRhiWidget(int sampleCount = 1, QWidget *parent = nullptr) : QRhiWidget(parent), m_sampleCount(sampleCount) { } ~SimpleRhiWidget() { delete m_rt; delete m_rp; } void initialize(QRhiCommandBuffer *cb) override; void render(QRhiCommandBuffer *cb) override; void releaseResources() override; int m_sampleCount; QRhi *m_rhi = nullptr; std::unique_ptr m_vbuf; std::unique_ptr m_ubuf; std::unique_ptr m_srb; std::unique_ptr m_pipeline; QRhiTextureRenderTarget *m_rt = nullptr; // used when autoRenderTarget is off QRhiRenderPassDescriptor *m_rp = nullptr; // used when autoRenderTarget is off friend class tst_QRhiWidget; }; void SimpleRhiWidget::initialize(QRhiCommandBuffer *cb) { if (m_rhi != rhi()) { m_pipeline.reset(); m_rhi = rhi(); } if (!m_pipeline) { if (!isAutoRenderTargetEnabled()) { delete m_rt; delete m_rp; QRhiTextureRenderTargetDescription rtDesc; if (colorTexture()) { rtDesc.setColorAttachments({ colorTexture() }); } else if (msaaColorBuffer()) { QRhiColorAttachment att; att.setRenderBuffer(msaaColorBuffer()); rtDesc.setColorAttachments({ att }); } m_rt = m_rhi->newTextureRenderTarget(rtDesc); m_rp = m_rt->newCompatibleRenderPassDescriptor(); m_rt->setRenderPassDescriptor(m_rp); m_rt->create(); } static float vertexData[] = { 0, 1, -1, -1, 1, -1 }; m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData))); m_vbuf->create(); m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 64)); m_ubuf->create(); m_srb.reset(m_rhi->newShaderResourceBindings()); m_srb->create(); m_pipeline.reset(m_rhi->newGraphicsPipeline()); m_pipeline->setShaderStages({ { QRhiShaderStage::Vertex, getShader(QLatin1String(":/data/simple.vert.qsb")) }, { QRhiShaderStage::Fragment, getShader(QLatin1String(":/data/simple.frag.qsb")) } }); QRhiVertexInputLayout inputLayout; inputLayout.setBindings({ { 2 * sizeof(float) } }); inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } }); m_pipeline->setSampleCount(m_sampleCount); m_pipeline->setVertexInputLayout(inputLayout); m_pipeline->setShaderResourceBindings(m_srb.get()); m_pipeline->setRenderPassDescriptor(renderTarget() ? renderTarget()->renderPassDescriptor() : m_rp); m_pipeline->create(); QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch(); resourceUpdates->uploadStaticBuffer(m_vbuf.get(), vertexData); cb->resourceUpdate(resourceUpdates); } } void SimpleRhiWidget::render(QRhiCommandBuffer *cb) { const QSize outputSize = colorTexture() ? colorTexture()->pixelSize() : msaaColorBuffer()->pixelSize(); if (renderTarget()) { QCOMPARE(outputSize, renderTarget()->pixelSize()); if (rhi()->backend() != QRhi::Null && rhi()->supportedSampleCounts().contains(m_sampleCount)) QCOMPARE(m_sampleCount, renderTarget()->sampleCount()); } const QColor clearColor = QColor::fromRgbF(0.4f, 0.7f, 0.0f, 1.0f); cb->beginPass(renderTarget() ? renderTarget() : m_rt, clearColor, { 1.0f, 0 }); cb->setGraphicsPipeline(m_pipeline.get()); cb->setViewport(QRhiViewport(0, 0, outputSize.width(), outputSize.height())); cb->setShaderResources(); const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0); cb->setVertexInput(0, 1, &vbufBinding); cb->draw(3); cb->endPass(); } void SimpleRhiWidget::releaseResources() { m_pipeline.reset(); m_srb.reset(); m_ubuf.reset(); m_vbuf.reset(); } void tst_QRhiWidget::simple_data() { testData(); } void tst_QRhiWidget::simple() { QFETCH(QRhiWidget::Api, api); SimpleRhiWidget *rhiWidget = new SimpleRhiWidget; rhiWidget->setApi(api); QSignalSpy frameSpy(rhiWidget, &QRhiWidget::frameSubmitted); QSignalSpy errorSpy(rhiWidget, &QRhiWidget::renderFailed); QVBoxLayout *layout = new QVBoxLayout; layout->addWidget(rhiWidget); QWidget w; w.setLayout(layout); w.resize(1280, 720); w.show(); QVERIFY(QTest::qWaitForWindowExposed(&w)); QTRY_VERIFY(frameSpy.count() > 0); QCOMPARE(errorSpy.count(), 0); QCOMPARE(rhiWidget->sampleCount(), 1); QCOMPARE(rhiWidget->colorBufferFormat(), QRhiWidget::TextureFormat::RGBA8); QVERIFY(rhiWidget->isAutoRenderTargetEnabled()); // Pull out the QRhiTexture (we know colorTexture() and rhi() and friends // are all there even outside initialize() and render(), even though this // is not quite documented), and read it back. QRhiTexture *backingTexture = rhiWidget->colorTexture(); QVERIFY(backingTexture); QCOMPARE(backingTexture->format(), QRhiTexture::RGBA8); QVERIFY(rhiWidget->depthStencilBuffer()); QVERIFY(rhiWidget->renderTarget()); QVERIFY(!rhiWidget->resolveTexture()); QRhi *rhi = rhiWidget->rhi(); QVERIFY(rhi); switch (api) { case QRhiWidget::Api::OpenGL: QCOMPARE(rhi->backend(), QRhi::OpenGLES2); break; case QRhiWidget::Api::Metal: QCOMPARE(rhi->backend(), QRhi::Metal); break; case QRhiWidget::Api::Vulkan: QCOMPARE(rhi->backend(), QRhi::Vulkan); break; case QRhiWidget::Api::Direct3D11: QCOMPARE(rhi->backend(), QRhi::D3D11); break; case QRhiWidget::Api::Direct3D12: QCOMPARE(rhi->backend(), QRhi::D3D12); break; case QRhiWidget::Api::Null: QCOMPARE(rhi->backend(), QRhi::Null); break; default: break; } const int maxFuzz = 1; QImage resultOne; if (rhi->backend() != QRhi::Null) { QRhiReadbackResult readResult; bool readCompleted = false; readResult.completed = [&readCompleted] { readCompleted = true; }; QRhiResourceUpdateBatch *rub = rhi->nextResourceUpdateBatch(); rub->readBackTexture(backingTexture, &readResult); QVERIFY(submitResourceUpdates(rhi, rub)); QVERIFY(readCompleted); QImage wrapperImage(reinterpret_cast(readResult.data.constData()), readResult.pixelSize.width(), readResult.pixelSize.height(), QImage::Format_RGBA8888); if (rhi->isYUpInFramebuffer()) resultOne = wrapperImage.mirrored(); else resultOne = wrapperImage.copy(); // result is now a red triangle upon greenish background, where the // triangle's edges are (0, 1), (-1, -1), and (1, -1). // It's upside down with Vulkan (Y is not corrected, clipSpaceCorrMatrix() is not used), // but that won't matter for the test. // Check that the center is a red pixel. QRgb c = resultOne.pixel(resultOne.width() / 2, resultOne.height() / 2); QVERIFY(qRed(c) >= 255 - maxFuzz); QVERIFY(qGreen(c) <= maxFuzz); QVERIFY(qBlue(c) <= maxFuzz); } // Now through grabFramebuffer(). QImage resultTwo; if (rhi->backend() != QRhi::Null) { resultTwo = rhiWidget->grabFramebuffer(); QCOMPARE(errorSpy.count(), 0); QVERIFY(!resultTwo.isNull()); QRgb c = resultTwo.pixel(resultTwo.width() / 2, resultTwo.height() / 2); QVERIFY(qRed(c) >= 255 - maxFuzz); QVERIFY(qGreen(c) <= maxFuzz); QVERIFY(qBlue(c) <= maxFuzz); } // Check we got the same result from our manual readback and when the // texture was rendered to again and grabFramebuffer() was called. QVERIFY(imageRGBAEquals(resultOne, resultTwo, maxFuzz)); } void tst_QRhiWidget::msaa_data() { testData(); } void tst_QRhiWidget::msaa() { QFETCH(QRhiWidget::Api, api); const int SAMPLE_COUNT = 4; SimpleRhiWidget *rhiWidget = new SimpleRhiWidget(SAMPLE_COUNT); rhiWidget->setApi(api); rhiWidget->setSampleCount(SAMPLE_COUNT); QSignalSpy frameSpy(rhiWidget, &QRhiWidget::frameSubmitted); QSignalSpy errorSpy(rhiWidget, &QRhiWidget::renderFailed); QVBoxLayout *layout = new QVBoxLayout; layout->addWidget(rhiWidget); QWidget w; w.setLayout(layout); w.resize(1280, 720); w.show(); QVERIFY(QTest::qWaitForWindowExposed(&w)); QTRY_VERIFY(frameSpy.count() > 0); QCOMPARE(errorSpy.count(), 0); QCOMPARE(rhiWidget->sampleCount(), 4); QCOMPARE(rhiWidget->colorBufferFormat(), QRhiWidget::TextureFormat::RGBA8); QVERIFY(!rhiWidget->colorTexture()); QVERIFY(rhiWidget->msaaColorBuffer()); QVERIFY(rhiWidget->depthStencilBuffer()); QVERIFY(rhiWidget->renderTarget()); QVERIFY(rhiWidget->resolveTexture()); QCOMPARE(rhiWidget->resolveTexture()->format(), QRhiTexture::RGBA8); QRhi *rhi = rhiWidget->rhi(); QVERIFY(rhi); if (rhi->backend() != QRhi::Null) { QRhiReadbackResult readResult; QRhiResourceUpdateBatch *rub = rhi->nextResourceUpdateBatch(); rub->readBackTexture(rhiWidget->resolveTexture(), &readResult); QVERIFY(submitResourceUpdates(rhi, rub)); QImage wrapperImage(reinterpret_cast(readResult.data.constData()), readResult.pixelSize.width(), readResult.pixelSize.height(), QImage::Format_RGBA8888); QImage result; if (rhi->isYUpInFramebuffer()) result = wrapperImage.mirrored(); else result = wrapperImage.copy(); // Check that the center is a red pixel. const int maxFuzz = 1; QRgb c = result.pixel(result.width() / 2, result.height() / 2); QVERIFY(qRed(c) >= 255 - maxFuzz); QVERIFY(qGreen(c) <= maxFuzz); QVERIFY(qBlue(c) <= maxFuzz); } // See if switching back and forth works. frameSpy.clear(); rhiWidget->m_pipeline.reset(); rhiWidget->m_sampleCount = 1; rhiWidget->setSampleCount(1); QTRY_VERIFY(frameSpy.count() > 0); QCOMPARE(errorSpy.count(), 0); QVERIFY(rhiWidget->colorTexture()); QVERIFY(!rhiWidget->msaaColorBuffer()); frameSpy.clear(); rhiWidget->m_pipeline.reset(); rhiWidget->m_sampleCount = SAMPLE_COUNT; rhiWidget->setSampleCount(SAMPLE_COUNT); QTRY_VERIFY(frameSpy.count() > 0); QCOMPARE(errorSpy.count(), 0); QVERIFY(!rhiWidget->colorTexture()); QVERIFY(rhiWidget->msaaColorBuffer()); } void tst_QRhiWidget::fixedSize_data() { testData(); } void tst_QRhiWidget::fixedSize() { QFETCH(QRhiWidget::Api, api); SimpleRhiWidget *rhiWidget = new SimpleRhiWidget; rhiWidget->setApi(api); QSignalSpy frameSpy(rhiWidget, &QRhiWidget::frameSubmitted); QSignalSpy errorSpy(rhiWidget, &QRhiWidget::renderFailed); QVBoxLayout *layout = new QVBoxLayout; layout->addWidget(rhiWidget); rhiWidget->setFixedColorBufferSize(QSize(320, 200)); QWidget w; w.setLayout(layout); w.resize(1280, 720); w.show(); QVERIFY(QTest::qWaitForWindowExposed(&w)); QTRY_VERIFY(frameSpy.count() > 0); QCOMPARE(errorSpy.count(), 0); QVERIFY(rhiWidget->rhi()); QVERIFY(rhiWidget->colorTexture()); QCOMPARE(rhiWidget->colorTexture()->pixelSize(), QSize(320, 200)); QVERIFY(rhiWidget->depthStencilBuffer()); QCOMPARE(rhiWidget->depthStencilBuffer()->pixelSize(), QSize(320, 200)); QVERIFY(rhiWidget->renderTarget()); QVERIFY(!rhiWidget->resolveTexture()); frameSpy.clear(); rhiWidget->setFixedColorBufferSize(640, 480); // should also trigger update() QTRY_VERIFY(frameSpy.count() > 0); QVERIFY(rhiWidget->colorTexture()); QCOMPARE(rhiWidget->colorTexture()->pixelSize(), QSize(640, 480)); QVERIFY(rhiWidget->depthStencilBuffer()); QCOMPARE(rhiWidget->depthStencilBuffer()->pixelSize(), QSize(640, 480)); frameSpy.clear(); rhiWidget->setFixedColorBufferSize(QSize()); QTRY_VERIFY(frameSpy.count() > 0); QVERIFY(rhiWidget->colorTexture()); QVERIFY(rhiWidget->colorTexture()->pixelSize() != QSize(640, 480)); QVERIFY(rhiWidget->depthStencilBuffer()); QVERIFY(rhiWidget->depthStencilBuffer()->pixelSize() != QSize(640, 480)); } void tst_QRhiWidget::autoRt_data() { testData(); } void tst_QRhiWidget::autoRt() { QFETCH(QRhiWidget::Api, api); SimpleRhiWidget *rhiWidget = new SimpleRhiWidget; rhiWidget->setApi(api); QVERIFY(rhiWidget->isAutoRenderTargetEnabled()); rhiWidget->setAutoRenderTarget(false); QVERIFY(!rhiWidget->isAutoRenderTargetEnabled()); QSignalSpy frameSpy(rhiWidget, &QRhiWidget::frameSubmitted); QSignalSpy errorSpy(rhiWidget, &QRhiWidget::renderFailed); QVBoxLayout *layout = new QVBoxLayout; layout->addWidget(rhiWidget); QWidget w; w.setLayout(layout); w.resize(1280, 720); w.show(); QVERIFY(QTest::qWaitForWindowExposed(&w)); QTRY_VERIFY(frameSpy.count() > 0); QCOMPARE(errorSpy.count(), 0); QVERIFY(rhiWidget->rhi()); QVERIFY(rhiWidget->colorTexture()); QVERIFY(!rhiWidget->depthStencilBuffer()); QVERIFY(!rhiWidget->renderTarget()); QVERIFY(!rhiWidget->resolveTexture()); QVERIFY(rhiWidget->m_rt); QVERIFY(rhiWidget->m_rp); QCOMPARE(rhiWidget->m_rt->description().cbeginColorAttachments()->texture(), rhiWidget->colorTexture()); frameSpy.clear(); // do something that triggers creating a new backing texture rhiWidget->setFixedColorBufferSize(QSize(320, 200)); QTRY_VERIFY(frameSpy.count() > 0); QVERIFY(rhiWidget->colorTexture()); QCOMPARE(rhiWidget->m_rt->description().cbeginColorAttachments()->texture(), rhiWidget->colorTexture()); } void tst_QRhiWidget::reparent_data() { testData(); } void tst_QRhiWidget::reparent() { if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::MultipleWindows)) QSKIP("MultipleWindows capability is reported as unsupported, skipping reparenting test."); QFETCH(QRhiWidget::Api, api); QWidget *windowOne = new QWidget; windowOne->resize(1280, 720); SimpleRhiWidget *rhiWidget = new SimpleRhiWidget(1, windowOne); rhiWidget->setApi(api); rhiWidget->resize(800, 600); QSignalSpy frameSpy(rhiWidget, &QRhiWidget::frameSubmitted); QSignalSpy errorSpy(rhiWidget, &QRhiWidget::renderFailed); windowOne->show(); QVERIFY(QTest::qWaitForWindowExposed(windowOne)); QTRY_VERIFY(frameSpy.count() > 0); QCOMPARE(errorSpy.count(), 0); frameSpy.clear(); QWidget windowTwo; windowTwo.resize(1280, 720); rhiWidget->setParent(&windowTwo); // There's nothing saying the old top-level parent is going to be around, // which is interesting wrt to its QRhi and resources created with that; // exercise this. delete windowOne; windowTwo.show(); QVERIFY(QTest::qWaitForWindowExposed(&windowTwo)); QTRY_VERIFY(frameSpy.count() > 0); QCOMPARE(errorSpy.count(), 0); // now reparent after show() has already been called frameSpy.clear(); QWidget windowThree; windowThree.resize(1280, 720); windowThree.show(); QVERIFY(QTest::qWaitForWindowExposed(&windowThree)); rhiWidget->setParent(&windowThree); // this case needs a show() on rhiWidget rhiWidget->show(); QTRY_VERIFY(frameSpy.count() > 0); QCOMPARE(errorSpy.count(), 0); } void tst_QRhiWidget::grabFramebufferWhileStillInvisible_data() { testData(); } void tst_QRhiWidget::grabFramebufferWhileStillInvisible() { QFETCH(QRhiWidget::Api, api); const int maxFuzz = 1; SimpleRhiWidget w; w.setApi(api); w.resize(1280, 720); QSignalSpy errorSpy(&w, &QRhiWidget::renderFailed); QImage image = w.grabFramebuffer(); // creates its own QRhi just to render offscreen QVERIFY(!image.isNull()); QVERIFY(w.rhi()); QVERIFY(w.colorTexture()); QCOMPARE(errorSpy.count(), 0); if (api != QRhiWidget::Api::Null) { QRgb c = image.pixel(image.width() / 2, image.height() / 2); QVERIFY(qRed(c) >= 255 - maxFuzz); QVERIFY(qGreen(c) <= maxFuzz); QVERIFY(qBlue(c) <= maxFuzz); } // Make the window visible, this under the hood drops the QRhiWidget's // own QRhi and attaches to the backingstore's. QSignalSpy frameSpy(&w, &QRhiWidget::frameSubmitted); w.show(); QVERIFY(QTest::qWaitForWindowExposed(&w)); QTRY_VERIFY(frameSpy.count() > 0); QCOMPARE(errorSpy.count(), 0); if (api != QRhiWidget::Api::Null) { QRhiReadbackResult readResult; QRhiResourceUpdateBatch *rub = w.rhi()->nextResourceUpdateBatch(); rub->readBackTexture(w.colorTexture(), &readResult); QVERIFY(submitResourceUpdates(w.rhi(), rub)); QImage wrapperImage(reinterpret_cast(readResult.data.constData()), readResult.pixelSize.width(), readResult.pixelSize.height(), QImage::Format_RGBA8888); if (w.rhi()->isYUpInFramebuffer()) image = wrapperImage.mirrored(); else image = wrapperImage.copy(); QRgb c = image.pixel(image.width() / 2, image.height() / 2); QVERIFY(qRed(c) >= 255 - maxFuzz); QVERIFY(qGreen(c) <= maxFuzz); QVERIFY(qBlue(c) <= maxFuzz); } } void tst_QRhiWidget::grabViaQWidgetGrab_data() { testData(); } void tst_QRhiWidget::grabViaQWidgetGrab() { QFETCH(QRhiWidget::Api, api); SimpleRhiWidget w; w.setApi(api); w.resize(1280, 720); QSignalSpy frameSpy(&w, &QRhiWidget::frameSubmitted); w.show(); QVERIFY(QTest::qWaitForWindowExposed(&w)); QTRY_VERIFY(frameSpy.count() > 0); QImage image = w.grab().toImage(); if (w.rhi()->backend() != QRhi::Null) { // It's upside down with Vulkan (Y is not corrected, clipSpaceCorrMatrix() is not used), // but that won't matter for the test. QRgb c = image.pixel(image.width() / 2, image.height() / 2); const int maxFuzz = 1; QVERIFY(qRed(c) >= 255 - maxFuzz); QVERIFY(qGreen(c) <= maxFuzz); QVERIFY(qBlue(c) <= maxFuzz); } } void tst_QRhiWidget::mirror_data() { testData(); } void tst_QRhiWidget::mirror() { QFETCH(QRhiWidget::Api, api); SimpleRhiWidget *rhiWidget = new SimpleRhiWidget; rhiWidget->setApi(api); QVERIFY(!rhiWidget->isMirrorVerticallyEnabled()); QSignalSpy frameSpy(rhiWidget, &QRhiWidget::frameSubmitted); QSignalSpy errorSpy(rhiWidget, &QRhiWidget::renderFailed); QVBoxLayout *layout = new QVBoxLayout; layout->addWidget(rhiWidget); QWidget w; w.setLayout(layout); w.resize(1280, 720); w.show(); QVERIFY(QTest::qWaitForWindowExposed(&w)); QTRY_VERIFY(frameSpy.count() > 0); QCOMPARE(errorSpy.count(), 0); frameSpy.clear(); rhiWidget->setMirrorVertically(true); QVERIFY(rhiWidget->isMirrorVerticallyEnabled()); QTRY_VERIFY(frameSpy.count() > 0); QCOMPARE(errorSpy.count(), 0); if (api != QRhiWidget::Api::Null) { QRhi *rhi = rhiWidget->rhi(); QRhiReadbackResult readResult; QRhiResourceUpdateBatch *rub = rhi->nextResourceUpdateBatch(); rub->readBackTexture(rhiWidget->colorTexture(), &readResult); QVERIFY(submitResourceUpdates(rhi, rub)); QImage wrapperImage(reinterpret_cast(readResult.data.constData()), readResult.pixelSize.width(), readResult.pixelSize.height(), QImage::Format_RGBA8888); QImage image; if (rhi->isYUpInFramebuffer()) image = wrapperImage.mirrored(); else image = wrapperImage.copy(); const int maxFuzz = 1; QRgb c = image.pixel(50, 5); if (api != QRhiWidget::Api::Vulkan) { // this should be the background (greenish), not the red triangle QVERIFY(qGreen(c) > qRed(c)); } else { // remember that Vulkan is upside down due to not correcting for Y down in NDC // hence this is red QVERIFY(qRed(c) >= 255 - maxFuzz); QVERIFY(qGreen(c) <= maxFuzz); } QVERIFY(qBlue(c) <= maxFuzz); } } QTEST_MAIN(tst_QRhiWidget) #include "tst_qrhiwidget.moc"