From 64089b900fe245cc64c10ff47033dda60d4a5f86 Mon Sep 17 00:00:00 2001 From: Laszlo Agocs Date: Fri, 28 May 2021 19:23:49 +0200 Subject: [PATCH] rhi: Enable reading back slices of 3D textures Change-Id: I0c687677b7e86b7284130c775718b29aca2cca40 Reviewed-by: Qt CI Bot Reviewed-by: Andy Nichols --- src/gui/rhi/qrhi.cpp | 9 ++-- src/gui/rhi/qrhid3d11.cpp | 23 ++++++--- src/gui/rhi/qrhigles2.cpp | 21 ++++++-- src/gui/rhi/qrhigles2_p_p.h | 1 + src/gui/rhi/qrhimetal.mm | 10 ++-- src/gui/rhi/qrhivulkan.cpp | 10 ++-- tests/auto/gui/rhi/qrhi/BLACKLIST | 3 ++ tests/auto/gui/rhi/qrhi/tst_qrhi.cpp | 73 ++++++++++++++++++++++++++-- 8 files changed, 118 insertions(+), 32 deletions(-) diff --git a/src/gui/rhi/qrhi.cpp b/src/gui/rhi/qrhi.cpp index 58962b8e35..78c615451e 100644 --- a/src/gui/rhi/qrhi.cpp +++ b/src/gui/rhi/qrhi.cpp @@ -2426,8 +2426,7 @@ bool QRhiRenderBuffer::createFrom(NativeRenderBuffer src) \value ThreeDimensional The texture is a 3D texture. Such textures should be created with the QRhi::newTexture() overload taking a depth in addition to width and height. A 3D texture can have mipmaps but cannot be - multisample. Reading back the contents of a 3D texture is not currently - supported. When rendering into a 3D texture, the layer specified in the + multisample. When rendering into a 3D texture, the layer specified in the render target's color attachment refers to a slice in range [0..depth-1]. The underlying graphics API may not support 3D textures at run time. Support is indicated by the QRhi::ThreeDimensionalTextures feature. @@ -5322,8 +5321,6 @@ void QRhiResourceUpdateBatch::copyTexture(QRhiTexture *dst, QRhiTexture *src, co \note Multisample textures cannot be read back. - \note 3D textures cannot be read back. - \note The readback returns raw byte data, in order to allow the applications to interpret it in any way they see fit. Be aware of the blending settings of rendering code: if the blending is set up to rely on premultiplied alpha, @@ -5342,6 +5339,10 @@ void QRhiResourceUpdateBatch::copyTexture(QRhiTexture *dst, QRhiTexture *src, co N is the \l{QRhi::resourceLimit()}{resource limit value} returned for QRhi::MaxAsyncReadbackFrames. + A single readback operation copies one mip level of one layer (cubemap face + or 3D slice) at a time. The level and layer are specified by the respective + fields in \a rb. + \sa readBackBuffer(), QRhi::resourceLimit() */ void QRhiResourceUpdateBatch::readBackTexture(const QRhiReadbackDescription &rb, QRhiReadbackResult *result) diff --git a/src/gui/rhi/qrhid3d11.cpp b/src/gui/rhi/qrhid3d11.cpp index 35118a4d81..6b4be76fef 100644 --- a/src/gui/rhi/qrhid3d11.cpp +++ b/src/gui/rhi/qrhid3d11.cpp @@ -1564,23 +1564,19 @@ void QRhiD3D11::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate UINT subres = 0; QD3D11Texture *texD = QRHI_RES(QD3D11Texture, u.rb.texture()); QD3D11SwapChain *swapChainD = nullptr; + bool is3D = false; if (texD) { if (texD->sampleDesc.Count > 1) { qWarning("Multisample texture cannot be read back"); continue; } - // No support for reading back 3D, not because it is not - // possible technically, but we need to draw the line somewhere. - if (texD->m_flags.testFlag(QRhiTexture::ThreeDimensional)) { - qWarning("3D texture cannot be read back"); - continue; - } src = texD->textureResource(); dxgiFormat = texD->dxgiFormat; pixelSize = q->sizeForMipLevel(u.rb.level(), texD->m_pixelSize); format = texD->m_format; - subres = D3D11CalcSubresource(UINT(u.rb.level()), UINT(u.rb.layer()), texD->mipLevelCount); + is3D = texD->m_flags.testFlag(QRhiTexture::ThreeDimensional); + subres = D3D11CalcSubresource(UINT(u.rb.level()), UINT(is3D ? 0 : u.rb.layer()), texD->mipLevelCount); } else { Q_ASSERT(contextState.currentSwapChain); swapChainD = QRHI_RES(QD3D11SwapChain, contextState.currentSwapChain); @@ -1635,7 +1631,18 @@ void QRhiD3D11::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate cmd.args.copySubRes.dstZ = 0; cmd.args.copySubRes.src = src; cmd.args.copySubRes.srcSubRes = subres; - cmd.args.copySubRes.hasSrcBox = false; + if (is3D) { + D3D11_BOX srcBox; + memset(&srcBox, 0, sizeof(srcBox)); + srcBox.front = UINT(u.rb.layer()); + srcBox.right = desc.Width; // exclusive + srcBox.bottom = desc.Height; + srcBox.back = srcBox.front + 1; + cmd.args.copySubRes.hasSrcBox = true; + cmd.args.copySubRes.srcBox = srcBox; + } else { + cmd.args.copySubRes.hasSrcBox = false; + } readback.stagingTex = stagingTex; readback.byteSize = byteSize; diff --git a/src/gui/rhi/qrhigles2.cpp b/src/gui/rhi/qrhigles2.cpp index 5f8ef08e31..a703c4bc9a 100644 --- a/src/gui/rhi/qrhigles2.cpp +++ b/src/gui/rhi/qrhigles2.cpp @@ -2045,14 +2045,20 @@ void QRhiGles2::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate if (texD) trackedImageBarrier(cbD, texD, QGles2Texture::AccessRead); cmd.args.readPixels.texture = texD ? texD->texture : 0; + cmd.args.readPixels.slice3D = -1; if (texD) { const QSize readImageSize = q->sizeForMipLevel(u.rb.level(), texD->m_pixelSize); cmd.args.readPixels.w = readImageSize.width(); cmd.args.readPixels.h = readImageSize.height(); cmd.args.readPixels.format = texD->m_format; - const GLenum faceTargetBase = texD->m_flags.testFlag(QRhiTexture::CubeMap) - ? GL_TEXTURE_CUBE_MAP_POSITIVE_X : texD->target; - cmd.args.readPixels.readTarget = faceTargetBase + uint(u.rb.layer()); + if (texD->m_flags.testFlag(QRhiTexture::ThreeDimensional)) { + cmd.args.readPixels.readTarget = texD->target; + cmd.args.readPixels.slice3D = u.rb.layer(); + } else { + const GLenum faceTargetBase = texD->m_flags.testFlag(QRhiTexture::CubeMap) + ? GL_TEXTURE_CUBE_MAP_POSITIVE_X : texD->target; + cmd.args.readPixels.readTarget = faceTargetBase + uint(u.rb.layer()); + } cmd.args.readPixels.level = u.rb.level(); } } else if (u.type == QRhiResourceUpdateBatchPrivate::TextureOp::GenMips) { @@ -2802,8 +2808,13 @@ void QRhiGles2::executeCommandBuffer(QRhiCommandBuffer *cb) if (mipLevel == 0 || caps.nonBaseLevelFramebufferTexture) { f->glGenFramebuffers(1, &fbo); f->glBindFramebuffer(GL_FRAMEBUFFER, fbo); - f->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, - cmd.args.readPixels.readTarget, cmd.args.readPixels.texture, mipLevel); + if (cmd.args.readPixels.slice3D >= 0) { + f->glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + tex, mipLevel, cmd.args.readPixels.slice3D); + } else { + f->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + cmd.args.readPixels.readTarget, tex, mipLevel); + } } } else { result->pixelSize = currentSwapChain->pixelSize; diff --git a/src/gui/rhi/qrhigles2_p_p.h b/src/gui/rhi/qrhigles2_p_p.h index b4179c0ce8..d5087b92ed 100644 --- a/src/gui/rhi/qrhigles2_p_p.h +++ b/src/gui/rhi/qrhigles2_p_p.h @@ -467,6 +467,7 @@ struct QGles2CommandBuffer : public QRhiCommandBuffer QRhiTexture::Format format; GLenum readTarget; int level; + int slice3D; } readPixels; struct { GLenum target; diff --git a/src/gui/rhi/qrhimetal.mm b/src/gui/rhi/qrhimetal.mm index b31d7cccaa..cf11f12ead 100644 --- a/src/gui/rhi/qrhimetal.mm +++ b/src/gui/rhi/qrhimetal.mm @@ -1854,15 +1854,13 @@ void QRhiMetal::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate QMetalSwapChain *swapChainD = nullptr; id src; QSize srcSize; + bool is3D = false; if (texD) { if (texD->samples > 1) { qWarning("Multisample texture cannot be read back"); continue; } - if (texD->m_flags.testFlag(QRhiTexture::ThreeDimensional)) { - qWarning("3D texture readback is not implemented"); - continue; - } + is3D = texD->m_flags.testFlag(QRhiTexture::ThreeDimensional); readback.pixelSize = q->sizeForMipLevel(u.rb.level(), texD->m_pixelSize); readback.format = texD->m_format; src = texD->d->tex; @@ -1890,9 +1888,9 @@ void QRhiMetal::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate ensureBlit(); [blitEnc copyFromTexture: src - sourceSlice: NSUInteger(u.rb.layer()) + sourceSlice: NSUInteger(is3D ? 0 : u.rb.layer()) sourceLevel: NSUInteger(u.rb.level()) - sourceOrigin: MTLOriginMake(0, 0, 0) + sourceOrigin: MTLOriginMake(0, 0, is3D ? u.rb.layer() : 0) sourceSize: MTLSizeMake(NSUInteger(srcSize.width()), NSUInteger(srcSize.height()), 1) toBuffer: readback.buf destinationOffset: 0 diff --git a/src/gui/rhi/qrhivulkan.cpp b/src/gui/rhi/qrhivulkan.cpp index d5b6f5760d..64b979b8c7 100644 --- a/src/gui/rhi/qrhivulkan.cpp +++ b/src/gui/rhi/qrhivulkan.cpp @@ -3347,15 +3347,13 @@ void QRhiVulkan::enqueueResourceUpdates(QVkCommandBuffer *cbD, QRhiResourceUpdat QVkTexture *texD = QRHI_RES(QVkTexture, u.rb.texture()); QVkSwapChain *swapChainD = nullptr; + bool is3D = false; if (texD) { if (texD->samples > VK_SAMPLE_COUNT_1_BIT) { qWarning("Multisample texture cannot be read back"); continue; } - if (texD->m_flags.testFlag(QRhiTexture::ThreeDimensional)) { - qWarning("3D texture cannot be read back"); - continue; - } + is3D = texD->m_flags.testFlag(QRhiTexture::ThreeDimensional); readback.pixelSize = q->sizeForMipLevel(u.rb.level(), texD->m_pixelSize); readback.format = texD->m_format; texD->lastActiveFrameSlot = currentFrameSlot; @@ -3405,8 +3403,10 @@ void QRhiVulkan::enqueueResourceUpdates(QVkCommandBuffer *cbD, QRhiResourceUpdat copyDesc.bufferOffset = 0; copyDesc.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; copyDesc.imageSubresource.mipLevel = uint32_t(u.rb.level()); - copyDesc.imageSubresource.baseArrayLayer = uint32_t(u.rb.layer()); + copyDesc.imageSubresource.baseArrayLayer = is3D ? 0 : uint32_t(u.rb.layer()); copyDesc.imageSubresource.layerCount = 1; + if (is3D) + copyDesc.imageOffset.z = u.rb.layer(); copyDesc.imageExtent.width = uint32_t(readback.pixelSize.width()); copyDesc.imageExtent.height = uint32_t(readback.pixelSize.height()); copyDesc.imageExtent.depth = 1; diff --git a/tests/auto/gui/rhi/qrhi/BLACKLIST b/tests/auto/gui/rhi/qrhi/BLACKLIST index 0e117c0483..68621ac66b 100644 --- a/tests/auto/gui/rhi/qrhi/BLACKLIST +++ b/tests/auto/gui/rhi/qrhi/BLACKLIST @@ -4,3 +4,6 @@ android # QTBUG-92211 [renderPassDescriptorCompatibility] android +# Skip 3D textures with Android emulator, the sw-based GL there is no good +[threeDimTexture] +android diff --git a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp index 11e82215d0..8b6f55b3b6 100644 --- a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp +++ b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp @@ -910,10 +910,8 @@ void tst_QRhi::resourceUpdateBatchBuffer() } } -inline bool imageRGBAEquals(const QImage &a, const QImage &b) +inline bool imageRGBAEquals(const QImage &a, const QImage &b, int maxFuzz = 1) { - const int maxFuzz = 1; - if (a.size() != b.size()) return false; @@ -3882,6 +3880,33 @@ void tst_QRhi::threeDimTexture() batch->generateMips(texture.data()); QVERIFY(submitResourceUpdates(rhi.data(), batch)); + + // read back slice 63 of level 1 (256x128, almost red) + batch = rhi->nextResourceUpdateBatch(); + QRhiReadbackResult readResult; + QImage result; + readResult.completed = [&readResult, &result] { + result = QImage(reinterpret_cast(readResult.data.constData()), + readResult.pixelSize.width(), readResult.pixelSize.height(), + QImage::Format_RGBA8888); + }; + QRhiReadbackDescription readbackDescription(texture.data()); + readbackDescription.setLevel(1); + readbackDescription.setLayer(63); + batch->readBackTexture(readbackDescription, &readResult); + QVERIFY(submitResourceUpdates(rhi.data(), batch)); + QVERIFY(!result.isNull()); + QImage referenceImage(WIDTH / 2, HEIGHT / 2, result.format()); + referenceImage.fill(QColor::fromRgb(253, 0, 0)); + + // Now restrict the test a bit. The Null QRhi backend has broken support for + // mipmap generation of 3D textures (it ignores the depth, effectively behaving as + // if the 3D texture was a 2D array which is incorrect wrt mipmapping) + // Some software-based OpenGL implementations, such as Mesa llvmpipe builds that are + // used both in Qt CI and are shipped with the official Qt binaries also seem to have + // problems with this. + if (impl != QRhi::Null && impl != QRhi::OpenGLES2) + QVERIFY(imageRGBAEquals(result, referenceImage, 2)); } // render target (one slice) @@ -3889,7 +3914,7 @@ void tst_QRhi::threeDimTexture() { const int SLICE = 23; QScopedPointer texture(rhi->newTexture(QRhiTexture::RGBA8, WIDTH, HEIGHT, DEPTH, - 1, QRhiTexture::RenderTarget)); + 1, QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource)); QVERIFY(texture->create()); QRhiColorAttachment att(texture.data()); @@ -3917,6 +3942,46 @@ void tst_QRhi::threeDimTexture() // slice 23 is now blue cb->endPass(); rhi->endOffscreenFrame(); + + // read back slice 23 (blue) + batch = rhi->nextResourceUpdateBatch(); + QRhiReadbackResult readResult; + QImage result; + readResult.completed = [&readResult, &result] { + result = QImage(reinterpret_cast(readResult.data.constData()), + readResult.pixelSize.width(), readResult.pixelSize.height(), + QImage::Format_RGBA8888); + }; + QRhiReadbackDescription readbackDescription(texture.data()); + readbackDescription.setLayer(23); + batch->readBackTexture(readbackDescription, &readResult); + QVERIFY(submitResourceUpdates(rhi.data(), batch)); + QVERIFY(!result.isNull()); + QImage referenceImage(WIDTH, HEIGHT, result.format()); + referenceImage.fill(QColor::fromRgbF(0.0f, 0.0f, 1.0f)); + // the Null backend does not render so skip the verification for that + if (impl != QRhi::Null) + QVERIFY(imageRGBAEquals(result, referenceImage)); + + // read back slice 0 (black) + batch = rhi->nextResourceUpdateBatch(); + result = QImage(); + readbackDescription.setLayer(0); + batch->readBackTexture(readbackDescription, &readResult); + QVERIFY(submitResourceUpdates(rhi.data(), batch)); + QVERIFY(!result.isNull()); + referenceImage.fill(QColor::fromRgbF(0.0f, 0.0f, 0.0f)); + QVERIFY(imageRGBAEquals(result, referenceImage)); + + // read back slice 127 (almost red) + batch = rhi->nextResourceUpdateBatch(); + result = QImage(); + readbackDescription.setLayer(127); + batch->readBackTexture(readbackDescription, &readResult); + QVERIFY(submitResourceUpdates(rhi.data(), batch)); + QVERIFY(!result.isNull()); + referenceImage.fill(QColor::fromRgb(254, 0, 0)); + QVERIFY(imageRGBAEquals(result, referenceImage)); } }