rhi: support partial region readbacks in QRhiReadbackDescription

Add a `rect` property to QRhiReadbackDescription, enabling users to
specify a sub-rectangle for texture or backbuffer readbacks. This is
particularly useful for scenarios where full texture readbacks are
unnecessary, like reading only one single pixel.

[ChangeLog][RHI] Added support for specifying a sub-rectangle for
readbacks in QRhiReadbackDescription. This allows partial texture or
backbuffer readbacks.

Change-Id: If7465e97bc90907442e2d2981826e1e26b0e4a50
Reviewed-by: Laszlo Agocs <laszlo.agocs@qt.io>
This commit is contained in:
Aurélien Brooke 2024-12-24 11:12:26 +01:00
parent 57d91d8029
commit dc033758a3
11 changed files with 171 additions and 59 deletions

View File

@ -3467,6 +3467,22 @@ QRhiReadbackDescription::QRhiReadbackDescription(QRhiTexture *texture)
Sets the mip \a level to read back.
*/
/*!
\fn const QRect &QRhiReadbackDescription::rect() const
\since 6.10
\return the rectangle to read back. Defaults to an invalid rectangle.
If invalid, the entire texture or swapchain backbuffer is read back.
*/
/*!
\fn void QRhiReadbackDescription::setRect(const QRect &rectangle)
\since 6.10
Sets the \a rectangle to read back.
*/
/*!
\class QRhiReadbackResult
\inmodule QtGuiPrivate

View File

@ -793,10 +793,14 @@ public:
int level() const { return m_level; }
void setLevel(int level) { m_level = level; }
QRect rect() const { return m_rect; }
void setRect(const QRect &rectangle) { m_rect = rectangle; }
private:
QRhiTexture *m_texture = nullptr;
int m_layer = 0;
int m_level = 0;
QRect m_rect;
};
Q_DECLARE_TYPEINFO(QRhiReadbackDescription, Q_RELOCATABLE_TYPE);

View File

@ -1990,7 +1990,7 @@ void QRhiD3D11::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate
ID3D11Resource *src;
DXGI_FORMAT dxgiFormat;
QSize pixelSize;
QRect rect;
QRhiTexture::Format format;
UINT subres = 0;
QD3D11Texture *texD = QRHI_RES(QD3D11Texture, u.rb.texture());
@ -2004,7 +2004,10 @@ void QRhiD3D11::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate
}
src = texD->textureResource();
dxgiFormat = texD->dxgiFormat;
pixelSize = q->sizeForMipLevel(u.rb.level(), texD->m_pixelSize);
if (u.rb.rect().isValid())
rect = u.rb.rect();
else
rect = QRect({0, 0}, q->sizeForMipLevel(u.rb.level(), texD->m_pixelSize));
format = texD->m_format;
is3D = texD->m_flags.testFlag(QRhiTexture::ThreeDimensional);
subres = D3D11CalcSubresource(UINT(u.rb.level()), UINT(is3D ? 0 : u.rb.layer()), texD->mipLevelCount);
@ -2024,18 +2027,21 @@ void QRhiD3D11::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate
}
src = swapChainD->backBufferTex;
dxgiFormat = swapChainD->colorFormat;
pixelSize = swapChainD->pixelSize;
if (u.rb.rect().isValid())
rect = u.rb.rect();
else
rect = QRect({0, 0}, swapChainD->pixelSize);
format = swapchainReadbackTextureFormat(dxgiFormat, nullptr);
if (format == QRhiTexture::UnknownFormat)
continue;
}
quint32 byteSize = 0;
quint32 bpl = 0;
textureFormatInfo(format, pixelSize, &bpl, &byteSize, nullptr);
textureFormatInfo(format, rect.size(), &bpl, &byteSize, nullptr);
D3D11_TEXTURE2D_DESC desc = {};
desc.Width = UINT(pixelSize.width());
desc.Height = UINT(pixelSize.height());
desc.Width = UINT(rect.width());
desc.Height = UINT(rect.height());
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = dxgiFormat;
@ -2059,22 +2065,22 @@ void QRhiD3D11::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate
cmd.args.copySubRes.dstZ = 0;
cmd.args.copySubRes.src = src;
cmd.args.copySubRes.srcSubRes = subres;
if (is3D) {
D3D11_BOX 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;
}
D3D11_BOX srcBox = {};
srcBox.left = UINT(rect.left());
srcBox.top = UINT(rect.top());
srcBox.front = is3D ? UINT(u.rb.layer()) : 0u;
// back, right, bottom are exclusive
srcBox.right = srcBox.left + desc.Width;
srcBox.bottom = srcBox.top + desc.Height;
srcBox.back = srcBox.front + 1;
cmd.args.copySubRes.hasSrcBox = true;
cmd.args.copySubRes.srcBox = srcBox;
readback.stagingTex = stagingTex;
readback.byteSize = byteSize;
readback.bpl = bpl;
readback.pixelSize = pixelSize;
readback.pixelSize = rect.size();
readback.format = format;
activeTextureReadbacks.append(readback);

View File

@ -3703,6 +3703,7 @@ void QRhiD3D12::enqueueResourceUpdates(QD3D12CommandBuffer *cbD, QRhiResourceUpd
readback.result = u.result;
QD3D12ObjectHandle srcHandle;
QRect rect;
bool is3D = false;
if (u.rb.texture()) {
QD3D12Texture *texD = QRHI_RES(QD3D12Texture, u.rb.texture());
@ -3711,17 +3712,24 @@ void QRhiD3D12::enqueueResourceUpdates(QD3D12CommandBuffer *cbD, QRhiResourceUpd
continue;
}
is3D = texD->m_flags.testFlag(QRhiTexture::ThreeDimensional);
readback.pixelSize = q->sizeForMipLevel(u.rb.level(), texD->m_pixelSize);
if (u.rb.rect().isValid())
rect = u.rb.rect();
else
rect = QRect({0, 0}, q->sizeForMipLevel(u.rb.level(), texD->m_pixelSize));
readback.format = texD->m_format;
srcHandle = texD->handle;
} else {
Q_ASSERT(currentSwapChain);
readback.pixelSize = currentSwapChain->pixelSize;
if (u.rb.rect().isValid())
rect = u.rb.rect();
else
rect = QRect({0, 0}, currentSwapChain->pixelSize);
readback.format = swapchainReadbackTextureFormat(currentSwapChain->colorFormat, nullptr);
if (readback.format == QRhiTexture::UnknownFormat)
continue;
srcHandle = currentSwapChain->colorBuffers[currentSwapChain->currentBackBufferIndex];
}
readback.pixelSize = rect.size();
textureFormatInfo(readback.format,
readback.pixelSize,
@ -3774,13 +3782,15 @@ void QRhiD3D12::enqueueResourceUpdates(QD3D12CommandBuffer *cbD, QRhiResourceUpd
src.SubresourceIndex = subresource;
D3D12_BOX srcBox = {};
if (is3D) {
srcBox.front = UINT(u.rb.layer());
srcBox.back = srcBox.front + 1;
srcBox.right = readback.pixelSize.width(); // exclusive
srcBox.bottom = readback.pixelSize.height();
}
cbD->cmdList->CopyTextureRegion(&dst, 0, 0, 0, &src, is3D ? &srcBox : nullptr);
srcBox.left = UINT(rect.left());
srcBox.top = UINT(rect.top());
srcBox.front = is3D ? UINT(u.rb.layer()) : 0u;
// back, right, bottom are exclusive
srcBox.right = srcBox.left + UINT(rect.width());
srcBox.bottom = srcBox.top + UINT(rect.height());
srcBox.back = srcBox.front + 1;
cbD->cmdList->CopyTextureRegion(&dst, 0, 0, 0, &src, &srcBox);
activeReadbacks.append(readback);
} else if (u.type == QRhiResourceUpdateBatchPrivate::TextureOp::GenMips) {
QD3D12Texture *texD = QRHI_RES(QD3D12Texture, u.dst);

View File

@ -2759,9 +2759,19 @@ void QRhiGles2::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate
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();
if (u.rb.rect().isValid()) {
cmd.args.readPixels.x = u.rb.rect().x();
cmd.args.readPixels.y = u.rb.rect().y();
cmd.args.readPixels.w = u.rb.rect().width();
cmd.args.readPixels.h = u.rb.rect().height();
}
else {
const QSize readImageSize = q->sizeForMipLevel(u.rb.level(), texD->m_pixelSize);
cmd.args.readPixels.x = 0;
cmd.args.readPixels.y = 0;
cmd.args.readPixels.w = readImageSize.width();
cmd.args.readPixels.h = readImageSize.height();
}
cmd.args.readPixels.format = texD->m_format;
if (texD->m_flags.testFlag(QRhiTexture::ThreeDimensional)
|| texD->m_flags.testFlag(QRhiTexture::TextureArray))
@ -2775,6 +2785,20 @@ void QRhiGles2::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate
}
cmd.args.readPixels.level = u.rb.level();
}
else { // swapchain
if (u.rb.rect().isValid()) {
cmd.args.readPixels.x = u.rb.rect().x();
cmd.args.readPixels.y = u.rb.rect().y();
cmd.args.readPixels.w = u.rb.rect().width();
cmd.args.readPixels.h = u.rb.rect().height();
}
else {
cmd.args.readPixels.x = 0;
cmd.args.readPixels.y = 0;
cmd.args.readPixels.w = currentSwapChain->pixelSize.width();
cmd.args.readPixels.h = currentSwapChain->pixelSize.height();
}
}
} else if (u.type == QRhiResourceUpdateBatchPrivate::TextureOp::GenMips) {
QGles2Texture *texD = QRHI_RES(QGles2Texture, u.dst);
trackedImageBarrier(cbD, texD, QGles2Texture::AccessFramebuffer);
@ -3600,8 +3624,8 @@ void QRhiGles2::executeCommandBuffer(QRhiCommandBuffer *cb)
GLuint tex = cmd.args.readPixels.texture;
GLuint fbo = 0;
int mipLevel = 0;
result->pixelSize = QSize(cmd.args.readPixels.w, cmd.args.readPixels.h);
if (tex) {
result->pixelSize = QSize(cmd.args.readPixels.w, cmd.args.readPixels.h);
result->format = cmd.args.readPixels.format;
mipLevel = cmd.args.readPixels.level;
if (mipLevel == 0 || caps.nonBaseLevelFramebufferTexture) {
@ -3619,12 +3643,13 @@ void QRhiGles2::executeCommandBuffer(QRhiCommandBuffer *cb)
}
}
} else {
result->pixelSize = currentSwapChain->pixelSize;
result->format = QRhiTexture::RGBA8;
// readPixels handles multisample resolving implicitly
}
const int w = result->pixelSize.width();
const int h = result->pixelSize.height();
const int x = cmd.args.readPixels.x;
const int y = cmd.args.readPixels.y;
const int w = cmd.args.readPixels.w;
const int h = cmd.args.readPixels.h;
if (mipLevel == 0 || caps.nonBaseLevelFramebufferTexture) {
// With GLES, GL_RGBA is the only mandated readback format, so stick with it.
// (and that's why we return false for the ReadBackAnyTextureFormat feature)
@ -3632,7 +3657,7 @@ void QRhiGles2::executeCommandBuffer(QRhiCommandBuffer *cb)
result->data.resize(w * h);
QByteArray tmpBuf;
tmpBuf.resize(w * h * 4);
f->glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, tmpBuf.data());
f->glReadPixels(x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, tmpBuf.data());
const quint8 *srcBase = reinterpret_cast<const quint8 *>(tmpBuf.constData());
quint8 *dstBase = reinterpret_cast<quint8 *>(result->data.data());
const int componentIndex = isFeatureSupported(QRhi::RedOrAlpha8IsRed) ? 0 : 3;
@ -3652,27 +3677,27 @@ void QRhiGles2::executeCommandBuffer(QRhiCommandBuffer *cb)
// not, there's nothing we can do.
case QRhiTexture::RGBA16F:
result->data.resize(w * h * 8);
f->glReadPixels(0, 0, w, h, GL_RGBA, GL_HALF_FLOAT, result->data.data());
f->glReadPixels(x, y, w, h, GL_RGBA, GL_HALF_FLOAT, result->data.data());
break;
case QRhiTexture::R16F:
result->data.resize(w * h * 2);
f->glReadPixels(0, 0, w, h, GL_RED, GL_HALF_FLOAT, result->data.data());
f->glReadPixels(x, y, w, h, GL_RED, GL_HALF_FLOAT, result->data.data());
break;
case QRhiTexture::R32F:
result->data.resize(w * h * 4);
f->glReadPixels(0, 0, w, h, GL_RED, GL_FLOAT, result->data.data());
f->glReadPixels(x, y, w, h, GL_RED, GL_FLOAT, result->data.data());
break;
case QRhiTexture::RGBA32F:
result->data.resize(w * h * 16);
f->glReadPixels(0, 0, w, h, GL_RGBA, GL_FLOAT, result->data.data());
f->glReadPixels(x, y, w, h, GL_RGBA, GL_FLOAT, result->data.data());
break;
case QRhiTexture::RGB10A2:
result->data.resize(w * h * 4);
f->glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV, result->data.data());
f->glReadPixels(x, y, w, h, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV, result->data.data());
break;
default:
result->data.resize(w * h * 4);
f->glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, result->data.data());
f->glReadPixels(x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, result->data.data());
break;
}
}

View File

@ -453,6 +453,8 @@ struct QGles2CommandBuffer : public QRhiCommandBuffer
struct {
QRhiReadbackResult *result;
GLuint texture;
int x;
int y;
int w;
int h;
QRhiTexture::Format format;

View File

@ -2914,7 +2914,7 @@ void QRhiMetal::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate
QMetalTexture *texD = QRHI_RES(QMetalTexture, u.rb.texture());
QMetalSwapChain *swapChainD = nullptr;
id<MTLTexture> src;
QSize srcSize;
QRect rect;
bool is3D = false;
if (texD) {
if (texD->samples > 1) {
@ -2922,22 +2922,27 @@ void QRhiMetal::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate
continue;
}
is3D = texD->m_flags.testFlag(QRhiTexture::ThreeDimensional);
readback.pixelSize = q->sizeForMipLevel(u.rb.level(), texD->m_pixelSize);
if (u.rb.rect().isValid())
rect = u.rb.rect();
else
rect = QRect({0, 0}, q->sizeForMipLevel(u.rb.level(), texD->m_pixelSize));
readback.format = texD->m_format;
src = texD->d->tex;
srcSize = readback.pixelSize;
texD->lastActiveFrameSlot = currentFrameSlot;
} else {
Q_ASSERT(currentSwapChain);
swapChainD = QRHI_RES(QMetalSwapChain, currentSwapChain);
readback.pixelSize = swapChainD->pixelSize;
if (u.rb.rect().isValid())
rect = u.rb.rect();
else
rect = QRect({0, 0}, swapChainD->pixelSize);
readback.format = swapChainD->d->rhiColorFormat;
// Multisample swapchains need nothing special since resolving
// happens when ending a renderpass.
const QMetalRenderTargetData::ColorAtt &colorAtt(swapChainD->rtWrapper.d->fb.colorAtt[0]);
src = colorAtt.resolveTex ? colorAtt.resolveTex : colorAtt.tex;
srcSize = swapChainD->rtWrapper.d->pixelSize;
}
readback.pixelSize = rect.size();
quint32 bpl = 0;
textureFormatInfo(readback.format, readback.pixelSize, &bpl, &readback.bufSize, nullptr);
@ -2947,8 +2952,8 @@ void QRhiMetal::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate
[blitEnc copyFromTexture: src
sourceSlice: NSUInteger(is3D ? 0 : u.rb.layer())
sourceLevel: NSUInteger(u.rb.level())
sourceOrigin: MTLOriginMake(0, 0, is3D ? u.rb.layer() : 0)
sourceSize: MTLSizeMake(NSUInteger(srcSize.width()), NSUInteger(srcSize.height()), 1)
sourceOrigin: MTLOriginMake(NSUInteger(rect.x()), NSUInteger(rect.y()), NSUInteger(is3D ? u.rb.layer() : 0))
sourceSize: MTLSizeMake(NSUInteger(rect.width()), NSUInteger(rect.height()), 1)
toBuffer: readback.buf
destinationOffset: 0
destinationBytesPerRow: bpl

View File

@ -515,11 +515,17 @@ void QRhiNull::resourceUpdate(QRhiCommandBuffer *cb, QRhiResourceUpdateBatch *re
QNullTexture *texD = QRHI_RES(QNullTexture, u.rb.texture());
if (texD) {
result->format = texD->format();
result->pixelSize = q->sizeForMipLevel(u.rb.level(), texD->pixelSize());
if (u.rb.rect().isValid())
result->pixelSize = u.rb.rect().size();
else
result->pixelSize = q->sizeForMipLevel(u.rb.level(), texD->pixelSize());
} else {
Q_ASSERT(currentSwapChain);
result->format = QRhiTexture::RGBA8;
result->pixelSize = currentSwapChain->currentPixelSize();
if (u.rb.rect().isValid())
result->pixelSize = u.rb.rect().size();
else
result->pixelSize = currentSwapChain->currentPixelSize();
}
quint32 bytesPerLine = 0;
quint32 byteSize = 0;

View File

@ -4321,7 +4321,10 @@ void QRhiVulkan::enqueueResourceUpdates(QVkCommandBuffer *cbD, QRhiResourceUpdat
continue;
}
is3D = texD->m_flags.testFlag(QRhiTexture::ThreeDimensional);
readback.pixelSize = q->sizeForMipLevel(u.rb.level(), texD->m_pixelSize);
if (u.rb.rect().isValid())
readback.rect = u.rb.rect();
else
readback.rect = QRect({0, 0}, q->sizeForMipLevel(u.rb.level(), texD->m_pixelSize));
readback.format = texD->m_format;
texD->lastActiveFrameSlot = currentFrameSlot;
} else {
@ -4331,7 +4334,10 @@ void QRhiVulkan::enqueueResourceUpdates(QVkCommandBuffer *cbD, QRhiResourceUpdat
qWarning("Swapchain does not support readback");
continue;
}
readback.pixelSize = swapChainD->pixelSize;
if (u.rb.rect().isValid())
readback.rect = u.rb.rect();
else
readback.rect = QRect({0, 0}, swapChainD->pixelSize);
readback.format = swapchainReadbackTextureFormat(swapChainD->colorFormat, nullptr);
if (readback.format == QRhiTexture::UnknownFormat)
continue;
@ -4339,7 +4345,7 @@ void QRhiVulkan::enqueueResourceUpdates(QVkCommandBuffer *cbD, QRhiResourceUpdat
// Multisample swapchains need nothing special since resolving
// happens when ending a renderpass.
}
textureFormatInfo(readback.format, readback.pixelSize, nullptr, &readback.byteSize, nullptr);
textureFormatInfo(readback.format, readback.rect.size(), nullptr, &readback.byteSize, nullptr);
// Create a host visible readback buffer.
VkBufferCreateInfo bufferInfo = {};
@ -4367,10 +4373,12 @@ void QRhiVulkan::enqueueResourceUpdates(QVkCommandBuffer *cbD, QRhiResourceUpdat
copyDesc.imageSubresource.mipLevel = uint32_t(u.rb.level());
copyDesc.imageSubresource.baseArrayLayer = is3D ? 0 : uint32_t(u.rb.layer());
copyDesc.imageSubresource.layerCount = 1;
copyDesc.imageOffset.x = readback.rect.x();
copyDesc.imageOffset.y = readback.rect.y();
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.width = uint32_t(readback.rect.width());
copyDesc.imageExtent.height = uint32_t(readback.rect.height());
copyDesc.imageExtent.depth = 1;
if (texD) {
@ -4634,7 +4642,7 @@ void QRhiVulkan::finishActiveReadbacks(bool forced)
const QRhiVulkan::TextureReadback &readback(activeTextureReadbacks[i]);
if (forced || currentFrameSlot == readback.activeFrameSlot || readback.activeFrameSlot < 0) {
readback.result->format = readback.format;
readback.result->pixelSize = readback.pixelSize;
readback.result->pixelSize = readback.rect.size();
VmaAllocation a = toVmaAllocation(readback.stagingAlloc);
void *p = nullptr;
VkResult err = vmaMapMemory(toVmaAllocator(allocator), a, &p);

View File

@ -999,7 +999,7 @@ public:
VkBuffer stagingBuf;
QVkAlloc stagingAlloc;
quint32 byteSize;
QSize pixelSize;
QRect rect;
QRhiTexture::Format format;
};
QVarLengthArray<TextureReadback, 2> activeTextureReadbacks;

View File

@ -4090,11 +4090,16 @@ void tst_QRhi::renderToWindowSimple()
const int asyncReadbackFrames = rhi->resourceLimit(QRhi::MaxAsyncReadbackFrames);
// one frame issues the readback, then we do MaxAsyncReadbackFrames more to ensure the readback completes
const int FRAME_COUNT = asyncReadbackFrames + 1;
bool readCompleted = false;
QRhiReadbackResult readResult;
QImage result;
int readbackWidth = 0;
bool readCompletedPartial = false;
QRhiReadbackResult readResultPartial;
QImage resultPartial;
for (int frameNo = 0; frameNo < FRAME_COUNT; ++frameNo) {
QVERIFY(rhi->beginFrame(swapChain.data()) == QRhi::FrameOpSuccess);
QRhiCommandBuffer *cb = swapChain->currentFrameCommandBuffer();
@ -4115,6 +4120,7 @@ void tst_QRhi::renderToWindowSimple()
cb->draw(3);
if (frameNo == 0) {
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readResult.completed = [&readCompleted, &readResult, &result, &rhi] {
readCompleted = true;
QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
@ -4127,9 +4133,26 @@ void tst_QRhi::renderToWindowSimple()
else
result = wrapperImage.copy();
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({}, &readResult); // read back the current backbuffer
QRhiReadbackDescription readbackDescription;
QVERIFY(!readbackDescription.rect().isValid());
readbackBatch->readBackTexture(readbackDescription, &readResult); // read back the current backbuffer
readbackWidth = outputSize.width();
readResultPartial.completed = [&readCompletedPartial, &readResultPartial, &resultPartial, &rhi] {
readCompletedPartial = true;
QImage wrapperImage(reinterpret_cast<const uchar *>(readResultPartial.data.constData()),
readResultPartial.pixelSize.width(), readResultPartial.pixelSize.height(),
QImage::Format_ARGB32_Premultiplied);
if (readResultPartial.format == QRhiTexture::RGBA8)
wrapperImage = wrapperImage.rgbSwapped();
if (rhi->isYUpInFramebuffer() == rhi->isYUpInNDC())
resultPartial = wrapperImage.flipped();
else
resultPartial = wrapperImage.copy();
};
QRhiReadbackDescription partialReadbackDescription;
partialReadbackDescription.setRect({100, 100, 1, 1});
QVERIFY(partialReadbackDescription.rect().isValid());
readbackBatch->readBackTexture(partialReadbackDescription, &readResultPartial); // read back one pixel at 100,100 of the current backbuffer
cb->endPass(readbackBatch);
} else {
cb->endPass();
@ -4166,6 +4189,13 @@ void tst_QRhi::renderToWindowSimple()
QCOMPARE(redCount + blueCount, readbackWidth);
QVERIFY(redCount < blueCount);
// Verify the backbuffer single-pixel readback
QVERIFY(readCompletedPartial);
QCOMPARE(readResultPartial.pixelSize, QSize(1, 1));
if (rhi->isYUpInFramebuffer() == rhi->isYUpInNDC())
result.flip();
QCOMPARE(resultPartial.pixelColor(0, 0), result.pixelColor(100, 100));
}
void tst_QRhi::continuousReadbackFromWindow_data()