Revision control
Copy as Markdown
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#ifndef HWFRAMECONTEXT_P_H
#define HWFRAMECONTEXT_P_H
#include <QtFFmpegMediaPluginImpl/private/qffmpeg_p.h>
#include <QtFFmpegMediaPluginImpl/private/qffmpeghwaccel_p.h>
#include <QtFFmpegMediaPluginImpl/private/qffmpegvideobuffer_p.h>
#include <QtMultimedia/qvideoframe.h>
#include <QtMultimedia/private/qvideoframe_p.h>
#include <QtMultimedia/private/qvideotexturehelper_p.h>
#include <QtMultimediaTestLib/private/qvideoframetestutils_p.h>
#include <QtCore/private/qexpected_p.h>
#include <QtCore/qbytearray.h>
#include <QtCore/qlist.h>
#include <QtCore/qsize.h>
#include <QtCore/qstring.h>
#include <memory>
extern "C" {
#include <libavutil/hwcontext.h>
#include <libavutil/pixfmt.h>
}
using namespace QFFmpeg;
using namespace Qt::Literals;
// A hw-frames context plus one hw AVFrame filled with a known per-plane byte pattern, suitable
// for exercising a texture converter or QVideoFrame::map() without a real decoder.
struct HwFrameContext
{
HWAccelUPtr hwAccel;
AVBufferUPtr framesContext;
AVFrameUPtr hwFrame;
QSize size;
QVideoFrameFormat::PixelFormat pixelFormat;
QList<QByteArray> planeTexels; // the fill pattern each plane was uniformly populated with
};
// Creates an HwFrameContext for a hw device of type "deviceType", with hw pixel format
// "hwFormat" and decoded (sw) pixel format "swFormat". Each entry in "planeTexels" is the byte
// pattern one plane is uniformly filled with (e.g. {luma}, {chromaU, chromaV} for NV12).
// "configureFramesContext" is invoked on the allocated AVHWFramesContext right before
// av_hwframe_ctx_init(), so that callers can set backend-specific fields (e.g. VAAPI surface
// attributes).
template <typename ConfigureFramesContext>
q23::expected<HwFrameContext, QString>
createHwFrameContext(AVHWDeviceType deviceType, AVPixelFormat hwFormat, AVPixelFormat swFormat,
QSize size, QList<QByteArray> planeTexels,
ConfigureFramesContext configureFramesContext)
{
const QVideoFrameFormat::PixelFormat pixelFormat =
QFFmpegVideoBuffer::toQtPixelFormat(swFormat);
// QFFmpegVideoBuffer::map() round-trips a hw frame's sw_format through toQtPixelFormat() and
// back via convertSWFrame(); a format whose round trip isn't the identity gets silently
// sws_scale'd, which would make a pixel-exact comparison against planeTexels meaningless.
Q_ASSERT(QFFmpegVideoBuffer::toAVPixelFormat(pixelFormat) == swFormat);
HWAccelUPtr hwAccel = HWAccel::create(deviceType);
if (!hwAccel)
return q23::unexpected(u"No usable hw device of the requested type on this system"_s);
AVBufferUPtr framesContext(av_hwframe_ctx_alloc(hwAccel->hwDeviceContextAsBuffer()));
if (!framesContext)
return q23::unexpected(u"av_hwframe_ctx_alloc failed"_s);
auto *ctx = reinterpret_cast<AVHWFramesContext *>(framesContext->data);
ctx->format = hwFormat;
ctx->sw_format = swFormat;
ctx->width = size.width();
ctx->height = size.height();
configureFramesContext(*ctx);
if (av_hwframe_ctx_init(framesContext.get()) < 0)
return q23::unexpected(
u"av_hwframe_ctx_init failed (format unsupported by this driver?)"_s);
AVFrameUPtr swFrame = makeAVFrame();
swFrame->format = swFormat;
swFrame->width = size.width();
swFrame->height = size.height();
if (av_frame_get_buffer(swFrame.get(), 32) < 0)
return q23::unexpected(u"av_frame_get_buffer failed"_s);
const auto *desc = QVideoTextureHelper::textureDescription(pixelFormat);
Q_ASSERT(desc->nplanes == planeTexels.size());
for (int plane = 0; plane < desc->nplanes; ++plane) {
const int width = desc->widthForPlane(size.width(), plane);
const int height = desc->heightForPlane(size.height(), plane);
QtMultimediaTest::fillPlaneRows(swFrame->data[plane], swFrame->linesize[plane], width,
height, planeTexels[plane]);
}
AVFrameUPtr hwFrame = makeAVFrame();
if (av_hwframe_get_buffer(framesContext.get(), hwFrame.get(), 0) < 0)
return q23::unexpected(u"av_hwframe_get_buffer failed"_s);
if (av_hwframe_transfer_data(hwFrame.get(), swFrame.get(), 0) < 0)
return q23::unexpected(u"av_hwframe_transfer_data failed"_s);
return HwFrameContext{ std::move(hwAccel), std::move(framesContext), std::move(hwFrame), size,
pixelFormat, std::move(planeTexels) };
}
// Convenience wrapper for a two-plane 4:2:0 8-bit format (NV12): a full-size luma plane and a
// half-size interleaved-chroma plane.
template <typename ConfigureFramesContext>
q23::expected<HwFrameContext, QString>
createNv12HwFrameContext(AVHWDeviceType deviceType, AVPixelFormat hwFormat, QSize size,
quint8 lumaValue, quint8 chromaUValue, quint8 chromaVValue,
ConfigureFramesContext configureFramesContext)
{
return createHwFrameContext(deviceType, hwFormat, AV_PIX_FMT_NV12, size,
{ QByteArray(1, char(lumaValue)),
QtMultimediaTest::makeBytes({ chromaUValue, chromaVValue }) },
configureFramesContext);
}
// Convenience wrapper for a two-plane 4:2:0 10-bit format (P010): each plane's samples are
// 16-bit, with the 10-bit value left-shifted into the high bits (FFmpeg's P010 convention).
template <typename ConfigureFramesContext>
q23::expected<HwFrameContext, QString>
createP010HwFrameContext(AVHWDeviceType deviceType, AVPixelFormat hwFormat, QSize size,
quint16 lumaValue, quint16 chromaUValue, quint16 chromaVValue,
ConfigureFramesContext configureFramesContext)
{
const auto toBytes = [](quint16 value) {
const quint16 shifted = quint16(value << 6);
return QByteArray(reinterpret_cast<const char *>(&shifted), sizeof(shifted));
};
return createHwFrameContext(
deviceType, hwFormat, AV_PIX_FMT_P010, size,
{ toBytes(lumaValue), toBytes(chromaUValue) + toBytes(chromaVValue) },
configureFramesContext);
}
// Convenience wrapper for a single-plane packed 8-bit format (BGRA8888).
template <typename ConfigureFramesContext>
q23::expected<HwFrameContext, QString>
createSinglePlaneHwFrameContext(AVHWDeviceType deviceType, AVPixelFormat hwFormat, QSize size,
QByteArray texel, ConfigureFramesContext configureFramesContext)
{
return createHwFrameContext(deviceType, hwFormat, AV_PIX_FMT_BGRA, size, { std::move(texel) },
configureFramesContext);
}
// Constructs a QFFmpegVideoBuffer/QVideoFrame around "hwFrame" (as QFFmpeg::VideoRenderer does)
// and maps it, verifying every plane's content against "planeTexels" -- exercising exactly the
// CPU (QVideoFrame::map()) path, independent of any GPU texture importer. Returns an error
// describing the mismatch, if any.
inline q23::expected<void, QString> verifyMappedHwFrame(AVFrameUPtr hwFrame,
const QList<QByteArray> &planeTexels)
{
auto buffer = std::make_unique<QFFmpegVideoBuffer>(std::move(hwFrame));
const QVideoFrameFormat::PixelFormat pixelFormat = buffer->pixelFormat();
QVideoFrameFormat format(buffer->size(), pixelFormat);
QVideoFrame frame = QVideoFramePrivate::createFrame(std::move(buffer), format);
if (!frame.map(QVideoFrame::ReadOnly))
return q23::unexpected(u"QVideoFrame::map() failed"_s);
const auto *desc = QVideoTextureHelper::textureDescription(pixelFormat);
if (desc->nplanes != planeTexels.size())
return q23::unexpected(u"Unexpected plane count: %1"_s.arg(desc->nplanes));
for (int plane = 0; plane < desc->nplanes; ++plane) {
const int width = desc->widthForPlane(frame.size().width(), plane);
const int height = desc->heightForPlane(frame.size().height(), plane);
const q23::expected<void, QString> result = QtMultimediaTest::comparePlaneRows(
frame.bits(plane), frame.bytesPerLine(plane), width, height, planeTexels[plane]);
if (!result)
return q23::unexpected(u"plane %1: %2"_s.arg(plane).arg(result.error()));
}
frame.unmap();
return {};
}
#endif // HWFRAMECONTEXT_P_H