From 7ccd2d02463f8076631bc45e5bf1386c61037dc7 Mon Sep 17 00:00:00 2001 From: Laszlo Agocs Date: Wed, 3 Feb 2021 18:07:47 +0100 Subject: [PATCH] rhi: Add support for custom bytes-per-line for uncompressed raw data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: QTBUG-90770 Change-Id: Icba328c417bcce256e7b44f1d540af7f8e83376b Reviewed-by: Qt CI Bot Reviewed-by: Christian Strømme --- src/gui/rhi/qrhi.cpp | 33 +++++++++++---- src/gui/rhi/qrhi_p.h | 7 +++- src/gui/rhi/qrhi_p_p.h | 2 +- src/gui/rhi/qrhid3d11.cpp | 9 +++- src/gui/rhi/qrhigles2.cpp | 22 ++++++++-- src/gui/rhi/qrhigles2_p_p.h | 1 + src/gui/rhi/qrhimetal.mm | 10 ++++- src/gui/rhi/qrhinull.cpp | 7 +++- src/gui/rhi/qrhivulkan.cpp | 10 ++++- tests/auto/gui/rhi/qrhi/tst_qrhi.cpp | 63 +++++++++++++++++++++++++++- 10 files changed, 144 insertions(+), 20 deletions(-) diff --git a/src/gui/rhi/qrhi.cpp b/src/gui/rhi/qrhi.cpp index db85730040..fea688e9cd 100644 --- a/src/gui/rhi/qrhi.cpp +++ b/src/gui/rhi/qrhi.cpp @@ -636,6 +636,12 @@ Q_LOGGING_CATEGORY(QRHI_LOG_INFO, "qt.rhi.general") functions will not perform any action, the retrieved blob is always empty, and thus no benefits can be expected from retrieving and, during a subsequent run of the application, reloading the pipeline cache content. + + \value ImageDataStride Indicates that specifying a custom stride (row + length) for raw image data in texture uploads is supported. When not + supported (which can happen when the underlying API is OpenGL ES 2.0 without + support for GL_UNPACK_ROW_LENGTH), + QRhiTextureSubresourceUploadDescription::setDataStride() must not be used. */ /*! @@ -1585,16 +1591,28 @@ QRhiTextureRenderTargetDescription::QRhiTextureRenderTargetDescription(const QRh \note Setting sourceSize() or sourceTopLeft() may trigger a QImage copy internally, depending on the format and the backend. - When providing raw data, the stride (row pitch, row length in bytes) of the + When providing raw data, and the stride is not specified via + setDataStride(), the stride (row pitch, row length in bytes) of the provided data must be equal to \c{width * pixelSize} where \c pixelSize is the number of bytes used for one pixel, and there must be no additional padding between rows. There is no row start alignment requirement. + When there is unused data at the end of each row in the input raw data, + call setDataStride() with the total number of bytes per row. The stride + must always be a multiple of the number of bytes for one pixel. The row + stride is only applicable to image data for textures with an uncompressed + format. + \note The format of the source data must be compatible with the texture format. With many graphics APIs the data is copied as-is into a staging buffer, there is no intermediate format conversion provided by QRhi. This applies to floating point formats as well, with, for example, RGBA16F requiring half floats in the source data. + + \note Setting the stride via setDataStride() is only functional when + QRhi::ImageDataStride is reported as + \l{QRhi::isFeatureSupported()}{supported}. In practice this can be expected + to be supported everywhere except for OpenGL ES 2.0. */ /*! @@ -1637,11 +1655,10 @@ QRhiTextureSubresourceUploadDescription::QRhiTextureSubresourceUploadDescription } /*! - Constructs a mip level description with the image data specified by \a data. This is suitable - for floating point and compressed formats as well. + Constructs a mip level description with the image data specified by \a + data. This is suitable for floating point and compressed formats as well. */ -QRhiTextureSubresourceUploadDescription::QRhiTextureSubresourceUploadDescription( - const QByteArray &data) +QRhiTextureSubresourceUploadDescription::QRhiTextureSubresourceUploadDescription(const QByteArray &data) : m_data(data) { } @@ -4492,7 +4509,7 @@ void QRhiImplementation::compressedFormatInfo(QRhiTexture::Format format, const } void QRhiImplementation::textureFormatInfo(QRhiTexture::Format format, const QSize &size, - quint32 *bpl, quint32 *byteSize) const + quint32 *bpl, quint32 *byteSize, quint32 *bytesPerPixel) const { if (isCompressedFormat(format)) { compressedFormatInfo(format, size, bpl, byteSize, nullptr); @@ -4551,6 +4568,8 @@ void QRhiImplementation::textureFormatInfo(QRhiTexture::Format format, const QSi *bpl = uint(size.width()) * bpc; if (byteSize) *byteSize = uint(size.width() * size.height()) * bpc; + if (bytesPerPixel) + *bytesPerPixel = bpc; } // Approximate because it excludes subresource alignment or multisampling. @@ -4562,7 +4581,7 @@ quint32 QRhiImplementation::approxByteSizeForTexture(QRhiTexture::Format format, quint32 byteSize = 0; const QSize size(qFloor(qreal(qMax(1, baseSize.width() >> level))), qFloor(qreal(qMax(1, baseSize.height() >> level)))); - textureFormatInfo(format, size, nullptr, &byteSize); + textureFormatInfo(format, size, nullptr, &byteSize, nullptr); approxSize += byteSize; } approxSize *= uint(layerCount); diff --git a/src/gui/rhi/qrhi_p.h b/src/gui/rhi/qrhi_p.h index e0afabc73a..a4d65a661a 100644 --- a/src/gui/rhi/qrhi_p.h +++ b/src/gui/rhi/qrhi_p.h @@ -518,6 +518,9 @@ public: QByteArray data() const { return m_data; } void setData(const QByteArray &data) { m_data = data; } + quint32 dataStride() const { return m_dataStride; } + void setDataStride(quint32 stride) { m_dataStride = stride; } + QPoint destinationTopLeft() const { return m_destinationTopLeft; } void setDestinationTopLeft(const QPoint &p) { m_destinationTopLeft = p; } @@ -530,6 +533,7 @@ public: private: QImage m_image; QByteArray m_data; + quint32 m_dataStride = 0; QPoint m_destinationTopLeft; QSize m_sourceSize; QPoint m_sourceTopLeft; @@ -1528,7 +1532,8 @@ public: IntAttributes, ScreenSpaceDerivatives, ReadBackAnyTextureFormat, - PipelineCacheDataLoadSave + PipelineCacheDataLoadSave, + ImageDataStride }; enum BeginFrameFlag { diff --git a/src/gui/rhi/qrhi_p_p.h b/src/gui/rhi/qrhi_p_p.h index fc9c99af7e..f33626ae1d 100644 --- a/src/gui/rhi/qrhi_p_p.h +++ b/src/gui/rhi/qrhi_p_p.h @@ -179,7 +179,7 @@ public: quint32 *bpl, quint32 *byteSize, QSize *blockDim) const; void textureFormatInfo(QRhiTexture::Format format, const QSize &size, - quint32 *bpl, quint32 *byteSize) const; + quint32 *bpl, quint32 *byteSize, quint32 *bytesPerPixel) const; quint32 approxByteSizeForTexture(QRhiTexture::Format format, const QSize &baseSize, int mipCount, int layerCount); diff --git a/src/gui/rhi/qrhid3d11.cpp b/src/gui/rhi/qrhid3d11.cpp index 3e0ba0820b..addb058d4d 100644 --- a/src/gui/rhi/qrhid3d11.cpp +++ b/src/gui/rhi/qrhid3d11.cpp @@ -542,6 +542,8 @@ bool QRhiD3D11::isFeatureSupported(QRhi::Feature feature) const return true; case QRhi::PipelineCacheDataLoadSave: return false; + case QRhi::ImageDataStride: + return true; default: Q_UNREACHABLE(); return false; @@ -1401,7 +1403,10 @@ void QRhiD3D11::enqueueSubresUpload(QD3D11Texture *texD, QD3D11CommandBuffer *cb const QSize size = subresDesc.sourceSize().isEmpty() ? q->sizeForMipLevel(level, texD->m_pixelSize) : subresDesc.sourceSize(); quint32 bpl = 0; - textureFormatInfo(texD->m_format, size, &bpl, nullptr); + if (subresDesc.dataStride()) + bpl = subresDesc.dataStride(); + else + textureFormatInfo(texD->m_format, size, &bpl, nullptr, nullptr); box.left = UINT(dp.x()); box.top = UINT(dp.y()); box.right = UINT(dp.x() + size.width()); @@ -1577,7 +1582,7 @@ void QRhiD3D11::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate } quint32 byteSize = 0; quint32 bpl = 0; - textureFormatInfo(format, pixelSize, &bpl, &byteSize); + textureFormatInfo(format, pixelSize, &bpl, &byteSize, nullptr); D3D11_TEXTURE2D_DESC desc; memset(&desc, 0, sizeof(desc)); diff --git a/src/gui/rhi/qrhigles2.cpp b/src/gui/rhi/qrhigles2.cpp index e3895897e9..c2035869c3 100644 --- a/src/gui/rhi/qrhigles2.cpp +++ b/src/gui/rhi/qrhigles2.cpp @@ -340,6 +340,10 @@ QT_BEGIN_NAMESPACE #define GL_NUM_PROGRAM_BINARY_FORMATS 0x87FE #endif +#ifndef GL_UNPACK_ROW_LENGTH +#define GL_UNPACK_ROW_LENGTH 0x0CF2 +#endif + /*! Constructs a new QRhiGles2InitParams. @@ -992,6 +996,8 @@ bool QRhiGles2::isFeatureSupported(QRhi::Feature feature) const return false; case QRhi::PipelineCacheDataLoadSave: return caps.programBinary; + case QRhi::ImageDataStride: + return !caps.gles || caps.ctxMajor >= 3; default: Q_UNREACHABLE(); return false; @@ -1796,6 +1802,7 @@ void QRhiGles2::enqueueSubresUpload(QGles2Texture *texD, QGles2CommandBuffer *cb cmd.args.subImage.glformat = texD->glformat; cmd.args.subImage.gltype = texD->gltype; cmd.args.subImage.rowStartAlign = 4; + cmd.args.subImage.rowLength = 0; cmd.args.subImage.data = cbD->retainImage(img); } else if (!rawData.isEmpty() && isCompressed) { if (!texD->compressedAtlasBuilt && (texD->flags() & QRhiTexture::UsedAsCompressedAtlas)) { @@ -1849,8 +1856,9 @@ void QRhiGles2::enqueueSubresUpload(QGles2Texture *texD, QGles2CommandBuffer *cb } else if (!rawData.isEmpty()) { const QSize size = subresDesc.sourceSize().isEmpty() ? q->sizeForMipLevel(level, texD->m_pixelSize) : subresDesc.sourceSize(); - quint32 bpl = 0; - textureFormatInfo(texD->m_format, size, &bpl, nullptr); + quint32 bytesPerLine = 0; + quint32 bytesPerPixel = 0; + textureFormatInfo(texD->m_format, size, &bytesPerLine, nullptr, &bytesPerPixel); QGles2CommandBuffer::Command &cmd(cbD->commands.get()); cmd.cmd = QGles2CommandBuffer::Command::SubImage; cmd.args.subImage.target = texD->target; @@ -1866,7 +1874,11 @@ void QRhiGles2::enqueueSubresUpload(QGles2Texture *texD, QGles2CommandBuffer *cb // Default unpack alignment (row start aligment // requirement) is 4. QImage guarantees 4 byte aligned // row starts, but our raw data here does not. - cmd.args.subImage.rowStartAlign = (bpl & 3) ? 1 : 4; + cmd.args.subImage.rowStartAlign = (bytesPerLine & 3) ? 1 : 4; + if (subresDesc.dataStride() && bytesPerPixel) + cmd.args.subImage.rowLength = subresDesc.dataStride() / bytesPerPixel; + else + cmd.args.subImage.rowLength = 0; cmd.args.subImage.data = cbD->retainData(rawData); } else { qWarning("Invalid texture upload for %p layer=%d mip=%d", texD, layer, level); @@ -2796,6 +2808,8 @@ void QRhiGles2::executeCommandBuffer(QRhiCommandBuffer *cb) f->glBindTexture(cmd.args.subImage.target, cmd.args.subImage.texture); if (cmd.args.subImage.rowStartAlign != 4) f->glPixelStorei(GL_UNPACK_ALIGNMENT, cmd.args.subImage.rowStartAlign); + if (cmd.args.subImage.rowLength != 0) + f->glPixelStorei(GL_UNPACK_ROW_LENGTH, cmd.args.subImage.rowLength); f->glTexSubImage2D(cmd.args.subImage.faceTarget, cmd.args.subImage.level, cmd.args.subImage.dx, cmd.args.subImage.dy, cmd.args.subImage.w, cmd.args.subImage.h, @@ -2803,6 +2817,8 @@ void QRhiGles2::executeCommandBuffer(QRhiCommandBuffer *cb) cmd.args.subImage.data); if (cmd.args.subImage.rowStartAlign != 4) f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + if (cmd.args.subImage.rowLength != 0) + f->glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); break; case QGles2CommandBuffer::Command::CompressedImage: f->glBindTexture(cmd.args.compressedImage.target, cmd.args.compressedImage.texture); diff --git a/src/gui/rhi/qrhigles2_p_p.h b/src/gui/rhi/qrhigles2_p_p.h index d30fd89545..67a21f3053 100644 --- a/src/gui/rhi/qrhigles2_p_p.h +++ b/src/gui/rhi/qrhigles2_p_p.h @@ -473,6 +473,7 @@ struct QGles2CommandBuffer : public QRhiCommandBuffer GLenum glformat; GLenum gltype; int rowStartAlign; + int rowLength; const void *data; // must come from retainImage() } subImage; struct { diff --git a/src/gui/rhi/qrhimetal.mm b/src/gui/rhi/qrhimetal.mm index ddbd0ea740..ab255f74df 100644 --- a/src/gui/rhi/qrhimetal.mm +++ b/src/gui/rhi/qrhimetal.mm @@ -593,6 +593,8 @@ bool QRhiMetal::isFeatureSupported(QRhi::Feature feature) const return true; case QRhi::PipelineCacheDataLoadSave: return false; + case QRhi::ImageDataStride: + return true; default: Q_UNREACHABLE(); return false; @@ -1703,7 +1705,11 @@ void QRhiMetal::enqueueSubresUpload(QMetalTexture *texD, void *mp, void *blitEnc } quint32 bpl = 0; - textureFormatInfo(texD->m_format, QSize(w, h), &bpl, nullptr); + if (subresDesc.dataStride()) + bpl = subresDesc.dataStride(); + else + textureFormatInfo(texD->m_format, QSize(w, h), &bpl, nullptr, nullptr); + memcpy(reinterpret_cast(mp) + *curOfs, rawData.constData(), size_t(rawData.size())); [blitEnc copyFromBuffer: texD->d->stagingBuf[currentFrameSlot] @@ -1860,7 +1866,7 @@ void QRhiMetal::enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdate } quint32 bpl = 0; - textureFormatInfo(readback.format, readback.pixelSize, &bpl, &readback.bufSize); + textureFormatInfo(readback.format, readback.pixelSize, &bpl, &readback.bufSize, nullptr); readback.buf = [d->dev newBufferWithLength: readback.bufSize options: MTLResourceStorageModeShared]; QRHI_PROF_F(newReadbackBuffer(qint64(qintptr(readback.buf)), diff --git a/src/gui/rhi/qrhinull.cpp b/src/gui/rhi/qrhinull.cpp index 29f3bab8b0..0a25b58b91 100644 --- a/src/gui/rhi/qrhinull.cpp +++ b/src/gui/rhi/qrhinull.cpp @@ -437,12 +437,15 @@ void QRhiNull::simulateTextureUpload(const QRhiResourceUpdateBatchPrivate::Textu // sourceTopLeft is not supported on this path as per QRhi docs const char *src = subresDesc.data().constData(); const int srcBpl = w * 4; + int srcStride = srcBpl; + if (subresDesc.dataStride()) + srcStride = subresDesc.dataStride(); const QPoint dstOffset = subresDesc.destinationTopLeft(); uchar *dst = texD->image[layer][level].bits(); const int dstBpl = texD->image[layer][level].bytesPerLine(); for (int y = 0; y < h; ++y) { memcpy(dst + dstOffset.x() * 4 + (y + dstOffset.y()) * dstBpl, - src + y * srcBpl, + src + y * srcStride, size_t(srcBpl)); } } @@ -516,7 +519,7 @@ void QRhiNull::resourceUpdate(QRhiCommandBuffer *cb, QRhiResourceUpdateBatch *re } quint32 bytesPerLine = 0; quint32 byteSize = 0; - textureFormatInfo(result->format, result->pixelSize, &bytesPerLine, &byteSize); + textureFormatInfo(result->format, result->pixelSize, &bytesPerLine, &byteSize, nullptr); if (texD && texD->format() == QRhiTexture::RGBA8) { result->data.resize(int(byteSize)); const QImage &src(texD->image[u.rb.layer()][u.rb.level()]); diff --git a/src/gui/rhi/qrhivulkan.cpp b/src/gui/rhi/qrhivulkan.cpp index f6b1654acd..26d6de498b 100644 --- a/src/gui/rhi/qrhivulkan.cpp +++ b/src/gui/rhi/qrhivulkan.cpp @@ -3012,6 +3012,12 @@ void QRhiVulkan::prepareUploadSubres(QVkTexture *texD, int layer, int level, copySizeBytes = imageSizeBytes = rawData.size(); src = rawData.constData(); QSize size = q->sizeForMipLevel(level, texD->m_pixelSize); + if (subresDesc.dataStride()) { + quint32 bytesPerPixel = 0; + textureFormatInfo(texD->m_format, size, nullptr, nullptr, &bytesPerPixel); + if (bytesPerPixel) + copyInfo.bufferRowLength = subresDesc.dataStride() / bytesPerPixel; + } if (!subresDesc.sourceSize().isEmpty()) size = subresDesc.sourceSize(); copyInfo.imageOffset.x = dp.x(); @@ -3350,7 +3356,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); + textureFormatInfo(readback.format, readback.pixelSize, nullptr, &readback.byteSize, nullptr); // Create a host visible readback buffer. VkBufferCreateInfo bufferInfo; @@ -4231,6 +4237,8 @@ bool QRhiVulkan::isFeatureSupported(QRhi::Feature feature) const return true; case QRhi::PipelineCacheDataLoadSave: return true; + case QRhi::ImageDataStride: + return true; default: Q_UNREACHABLE(); return false; diff --git a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp index c4d1dca1bb..6d0ec2a488 100644 --- a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp +++ b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp @@ -89,6 +89,8 @@ private slots: void resourceUpdateBatchRGBATextureCopy(); void resourceUpdateBatchRGBATextureMip_data(); void resourceUpdateBatchRGBATextureMip(); + void resourceUpdateBatchTextureRawDataStride_data(); + void resourceUpdateBatchTextureRawDataStride(); void invalidPipeline_data(); void invalidPipeline(); void srbLayoutCompatibility_data(); @@ -348,7 +350,8 @@ void tst_QRhi::create() QRhi::IntAttributes, QRhi::ScreenSpaceDerivatives, QRhi::ReadBackAnyTextureFormat, - QRhi::PipelineCacheDataLoadSave + QRhi::PipelineCacheDataLoadSave, + QRhi::ImageDataStride }; for (size_t i = 0; i isFeatureSupported(features[i]); @@ -1293,6 +1296,64 @@ void tst_QRhi::resourceUpdateBatchRGBATextureMip() } } +void tst_QRhi::resourceUpdateBatchTextureRawDataStride_data() +{ + rhiTestData(); +} + +void tst_QRhi::resourceUpdateBatchTextureRawDataStride() +{ + 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"); + + const int WIDTH = 150; + const int DATA_WIDTH = 180; + const int HEIGHT = 50; + QByteArray image; + image.resize(DATA_WIDTH * HEIGHT * 4); + for (int y = 0; y < HEIGHT; ++y) { + char *p = image.data() + y * DATA_WIDTH * 4; + memset(p, y, DATA_WIDTH * 4); + } + + { + QScopedPointer texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(WIDTH, HEIGHT), + 1, QRhiTexture::UsedAsTransferSource)); + QVERIFY(texture->create()); + + QRhiResourceUpdateBatch *batch = rhi->nextResourceUpdateBatch(); + + QRhiTextureSubresourceUploadDescription subresDesc(image.constData(), image.size()); + subresDesc.setDataStride(DATA_WIDTH * 4); + QRhiTextureUploadEntry upload(0, 0, subresDesc); + 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, QSize(WIDTH, HEIGHT)); + + QImage wrapperImage(reinterpret_cast(readResult.data.constData()), + readResult.pixelSize.width(), readResult.pixelSize.height(), + QImage::Format_RGBA8888_Premultiplied); + // wrap the original data, note the bytesPerLine argument + QImage originalWrapperImage(reinterpret_cast(image.constData()), + WIDTH, HEIGHT, DATA_WIDTH * 4, + QImage::Format_RGBA8888_Premultiplied); + QVERIFY(imageRGBAEquals(wrapperImage, originalWrapperImage)); + } +} + static QShader loadShader(const char *name) { QFile f(QString::fromUtf8(name));