From 9c466946d0c5b4e319f1c953caeea63f0d453138 Mon Sep 17 00:00:00 2001 From: Laszlo Agocs Date: Fri, 4 Oct 2019 15:46:49 +0200 Subject: rhi: Autotest basic texture operations ...and make the Null backend able to deal with these, for RGBA8 textures at least. Naturally it is all QImage and QPainter under the hood. Also fix a bug in the OpenGL backend, as discovered by the autotest: the size from the readback did not reflect the mip level. Task-number: QTBUG-78971 Change-Id: Ie424b268bf5feb09021099b67068f4418a9b583e Reviewed-by: Paul Olav Tvete --- tests/auto/gui/rhi/qrhi/tst_qrhi.cpp | 408 +++++++++++++++++++++++++++++++++-- 1 file changed, 395 insertions(+), 13 deletions(-) (limited to 'tests') diff --git a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp index 409152db0d..a17495f317 100644 --- a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp +++ b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp @@ -30,6 +30,8 @@ #include #include #include +#include + #include #include @@ -73,6 +75,12 @@ private slots: void nativeHandles(); void resourceUpdateBatchBuffer_data(); void resourceUpdateBatchBuffer(); + void resourceUpdateBatchRGBATextureUpload_data(); + void resourceUpdateBatchRGBATextureUpload(); + void resourceUpdateBatchRGBATextureCopy_data(); + void resourceUpdateBatchRGBATextureCopy(); + void resourceUpdateBatchRGBATextureMip_data(); + void resourceUpdateBatchRGBATextureMip(); private: struct { @@ -505,6 +513,23 @@ void tst_QRhi::nativeHandles() } } +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(); @@ -517,7 +542,7 @@ void tst_QRhi::resourceUpdateBatchBuffer() QScopedPointer rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr)); if (!rhi) - QSKIP("QRhi could not be created, skipping testing resource updates"); + QSKIP("QRhi could not be created, skipping testing buffer resource updates"); const int bufferSize = 23; const QByteArray a(bufferSize, 'A'); @@ -539,12 +564,7 @@ void tst_QRhi::resourceUpdateBatchBuffer() readResult.completed = [&readCompleted] { readCompleted = true; }; batch->readBackBuffer(dynamicBuffer.data(), 5, 10, &readResult); - QRhiCommandBuffer *cb = nullptr; - QRhi::FrameOpResult result = rhi->beginOffscreenFrame(&cb); - QVERIFY(result == QRhi::FrameOpSuccess); - QVERIFY(cb); - cb->resourceUpdate(batch); - rhi->endOffscreenFrame(); + 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 @@ -573,12 +593,7 @@ void tst_QRhi::resourceUpdateBatchBuffer() if (rhi->isFeatureSupported(QRhi::ReadBackNonUniformBuffer)) batch->readBackBuffer(dynamicBuffer.data(), 5, 10, &readResult); - QRhiCommandBuffer *cb = nullptr; - QRhi::FrameOpResult result = rhi->beginOffscreenFrame(&cb); - QVERIFY(result == QRhi::FrameOpSuccess); - QVERIFY(cb); - cb->resourceUpdate(batch); - rhi->endOffscreenFrame(); + QVERIFY(submitResourceUpdates(rhi.data(), batch)); if (rhi->isFeatureSupported(QRhi::ReadBackNonUniformBuffer)) { QVERIFY(readCompleted); @@ -591,5 +606,372 @@ void tst_QRhi::resourceUpdateBatchBuffer() } } +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()); + + // Compare to a scaled version; we can do this safely only because we + // only have plain red pixels in the source image. + QImage expectedImage = red.scaled(expectedSize); + QVERIFY(imageRGBAEquals(expectedImage, wrapperImage)); + } +} + #include QTEST_MAIN(tst_QRhi) -- cgit v1.2.3