rhi: Add support for custom bytes-per-line for uncompressed raw data

Fixes: QTBUG-90770
Change-Id: Icba328c417bcce256e7b44f1d540af7f8e83376b
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Christian Strømme <christian.stromme@qt.io>
This commit is contained in:
Laszlo Agocs 2021-02-03 18:07:47 +01:00
parent 5ce367a552
commit 7ccd2d0246
10 changed files with 144 additions and 20 deletions

View File

@ -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);

View File

@ -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 {

View File

@ -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);

View File

@ -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));

View File

@ -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);

View File

@ -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 {

View File

@ -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<char *>(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)),

View File

@ -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()]);

View File

@ -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;

View File

@ -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 <sizeof(features) / sizeof(QRhi::Feature); ++i)
rhi->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<QRhi> 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<QRhiTexture> 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<const uchar *>(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<const uchar *>(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));