Revision control
Copy as Markdown
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtTest/QtTest>
#include <QtMultimediaTestLib/private/qwindowsd3d11testdevicecontext_p.h>
#include <QtMultimedia/qvideoframeformat.h>
#include <QtMultimedia/private/qwindowsd3d11texturereadback_p.h>
#include <QtGui/qcolor.h>
#include <QtCore/private/qsystemerror_p.h>
using namespace QtMultimediaPrivate;
// Helper macro to verify q23::expected<T, HRESULT>
#define QVERIFYCOMRESULT(comresult) \
QVERIFY2(comresult, \
comresult ? "no error" \
: qPrintable(QSystemError::windowsComString((comresult).error())))
class tst_QWindowsD3D11TextureReadback : public QObject
{
Q_OBJECT
private slots:
void init();
void map_returnsPixelData_forRgbaTexture();
void map_returnsPixelData_forPlanarTexture_data();
void map_returnsPixelData_forPlanarTexture();
void map_returnsEmpty_whenAlreadyMapped();
void map_succeedsAgain_afterUnmap();
void unmap_isSafeToCall_whenNotMapped();
private:
ComResult<QWindowsD3D11TestDeviceContext> m_device = q23::unexpected{ E_FAIL };
};
void tst_QWindowsD3D11TextureReadback::init()
{
m_device = createD3D11TestDeviceContext();
QVERIFYCOMRESULT(m_device);
}
void tst_QWindowsD3D11TextureReadback::map_returnsPixelData_forRgbaTexture()
{
constexpr QSize frameSize{ 64, 32 };
const ComResult<ComPtr<ID3D11Texture2D>> texture =
m_device->createTextureArray(frameSize, { Qt::red });
QVERIFYCOMRESULT(texture);
QWindowsD3D11TextureReadback readback;
const QAbstractVideoBuffer::MapData data = readback.map(m_device->device, *texture, frameSize);
QCOMPARE(data.planeCount, 1);
QVERIFY(data.data[0]);
QVERIFY(data.bytesPerLine[0] >= frameSize.width() * 4);
QVERIFY(data.dataSize[0] >= data.bytesPerLine[0] * frameSize.height());
// Verify the mapped bytes match the texture's actual (red) contents.
const unsigned char *pixel = data.data[0];
QCOMPARE(pixel[0], 0xff); // R
QCOMPARE(pixel[1], 0x00); // G
QCOMPARE(pixel[2], 0x00); // B
QCOMPARE(pixel[3], 0xff); // A
readback.unmap();
}
void tst_QWindowsD3D11TextureReadback::map_returnsPixelData_forPlanarTexture_data()
{
QTest::addColumn<QVideoFrameFormat::PixelFormat>("pixelFormat");
QTest::newRow("NV12") << QVideoFrameFormat::Format_NV12;
QTest::newRow("P010") << QVideoFrameFormat::Format_P010;
}
void tst_QWindowsD3D11TextureReadback::map_returnsPixelData_forPlanarTexture()
{
// QWindowsD3D11TextureReadback doesn't special-case pixel format: verify the single-plane
// raw-byte readback also works for semi-planar 8-bit (NV12) and 10-bit (P010) formats, not
// just packed RGBA.
QFETCH(QVideoFrameFormat::PixelFormat, pixelFormat);
constexpr QSize frameSize{ 64, 32 };
int bytesPerSample = 0;
ComResult<ComPtr<ID3D11Texture2D>> texture = q23::unexpected{ E_FAIL };
switch (pixelFormat) {
case QVideoFrameFormat::Format_NV12: {
bytesPerSample = 1;
texture = m_device->createNV12Texture(frameSize);
QVERIFYCOMRESULT(texture);
auto result = m_device->fillNV12Texture(*texture, frameSize, 0x40, 0x60, 0x80);
if (FAILED(result))
QSKIP("Cannot fill NV12 Texture");
break;
}
case QVideoFrameFormat::Format_P010: {
bytesPerSample = 2;
if (!m_device->isTextureFormatSupported(DXGI_FORMAT_P010))
QSKIP("P010 Texture2D not supported by D3D11 device");
texture = m_device->createP010Texture(frameSize);
QVERIFYCOMRESULT(texture);
auto result = m_device->fillP010Texture(*texture, frameSize, 0x100, 0x180, 0x200);
if (FAILED(result))
QSKIP("Cannot fill P010 Texture");
break;
}
default:
QFAIL("unexpected pixel format");
}
QWindowsD3D11TextureReadback readback;
const QAbstractVideoBuffer::MapData data = readback.map(m_device->device, *texture, frameSize);
QCOMPARE(data.planeCount, 1);
QVERIFY(data.data[0]);
QVERIFY(data.bytesPerLine[0] >= frameSize.width() * bytesPerSample);
// First sample of the mapped region is the top-left luma sample.
switch (pixelFormat) {
case QVideoFrameFormat::Format_NV12:
QCOMPARE(data.data[0][0], quint8(0x40));
break;
case QVideoFrameFormat::Format_P010:
QCOMPARE(*reinterpret_cast<const quint16 *>(data.data[0]), quint16(0x100 << 6));
break;
default:
QFAIL("unexpected pixel format");
}
readback.unmap();
}
void tst_QWindowsD3D11TextureReadback::map_returnsEmpty_whenAlreadyMapped()
{
constexpr QSize frameSize{ 64, 32 };
const ComResult<ComPtr<ID3D11Texture2D>> texture =
m_device->createTextureArray(frameSize, { Qt::blue });
QVERIFYCOMRESULT(texture);
QWindowsD3D11TextureReadback readback;
const QAbstractVideoBuffer::MapData firstMap =
readback.map(m_device->device, *texture, frameSize);
QCOMPARE(firstMap.planeCount, 1);
// Only one mapping may be active at a time.
const QAbstractVideoBuffer::MapData secondMap =
readback.map(m_device->device, *texture, frameSize);
QCOMPARE(secondMap.planeCount, 0);
readback.unmap();
}
void tst_QWindowsD3D11TextureReadback::map_succeedsAgain_afterUnmap()
{
constexpr QSize frameSize{ 64, 32 };
QWindowsD3D11TextureReadback readback;
const std::vector<QColor> colors{ Qt::green, Qt::yellow };
for (const QColor &color : colors) {
const ComResult<ComPtr<ID3D11Texture2D>> texture =
m_device->createTextureArray(frameSize, { color });
QVERIFYCOMRESULT(texture);
const QAbstractVideoBuffer::MapData data =
readback.map(m_device->device, *texture, frameSize);
QCOMPARE(data.planeCount, 1);
QVERIFY(data.data[0]);
readback.unmap();
}
}
void tst_QWindowsD3D11TextureReadback::unmap_isSafeToCall_whenNotMapped()
{
QWindowsD3D11TextureReadback readback;
readback.unmap(); // must not crash
}
QTEST_MAIN(tst_QWindowsD3D11TextureReadback)
#include "tst_qwindowsd3d11texturereadback.moc"