summaryrefslogtreecommitdiffstats
path: root/pica-client/videodevice.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'pica-client/videodevice.cpp')
-rw-r--r--pica-client/videodevice.cpp224
1 files changed, 163 insertions, 61 deletions
diff --git a/pica-client/videodevice.cpp b/pica-client/videodevice.cpp
index 7e4976a..419d1df 100644
--- a/pica-client/videodevice.cpp
+++ b/pica-client/videodevice.cpp
@@ -57,7 +57,7 @@ extern "C" {
#include <libavutil/mem.h>
#include <libavutil/error.h>
#include <libswscale/swscale.h>
-#ifdef HAVE_VAAPI
+#if defined(HAVE_VAAPI) || defined(HAVE_D3D12VA)
#include <libavutil/hwcontext.h>
#endif
}
@@ -110,21 +110,28 @@ static const struct
#endif
// Which FFmpeg encoder produces each of the codecs the "Video Codec" setting
-// offers, on the GPU and in software. Two names per codec because FFmpeg names
-// its external library encoders after the library rather than after the codec,
-// and the id as well so that a build without the library named here can still
-// fall back to whatever other encoder it has for the same codec.
+// offers, on the GPU and in software. Several names per codec because FFmpeg
+// names its external library encoders after the library rather than after the
+// codec, and names a hardware one after the API that drives it - VAAPI where
+// there is libva, Direct3D 12 on Windows. The id as well, so that a build
+// without the library named here can still fall back to whatever other encoder
+// it has for the same codec.
+//
+// A null hardware name means that API has no encoder for the codec: D3D12
+// Video defines H.264, HEVC and AV1 encoding and no VP9, so a call set to VP9
+// encodes in software on Windows however the acceleration setting stands.
static const struct
{
const char *codec;
AVCodecID id;
const char *vaapiEncoder;
+ const char *d3d12vaEncoder;
const char *swEncoder;
} kVideoEncoders[] =
{
- { "h264", AV_CODEC_ID_H264, "h264_vaapi", "libx264" },
- { "hevc", AV_CODEC_ID_HEVC, "hevc_vaapi", "libx265" },
- { "vp9", AV_CODEC_ID_VP9, "vp9_vaapi", "libvpx-vp9" }
+ { "h264", AV_CODEC_ID_H264, "h264_vaapi", "h264_d3d12va", "libx264" },
+ { "hevc", AV_CODEC_ID_HEVC, "hevc_vaapi", "hevc_d3d12va", "libx265" },
+ { "vp9", AV_CODEC_ID_VP9, "vp9_vaapi", nullptr, "libvpx-vp9" }
};
static int videoEncoderIndex(const QString &codec)
@@ -1281,6 +1288,83 @@ static AVPixelFormat swscaleSourceFormat(AVPixelFormat format, bool *fullRange)
return format;
}
+#if defined(HAVE_VAAPI) || defined(HAVE_D3D12VA)
+// Opens one of FFmpeg's hardware encoders on a device that has already been
+// created, and makes the pool of GPU surfaces it takes its input from.
+//
+// Shared by the VAAPI and the Direct3D 12 paths, which differ only in the
+// device handed in here and in the pixel format naming the memory the surfaces
+// live in - from the pool onwards it is the same hwcontext machinery, and the
+// encoder is configured exactly as the software one is below.
+//
+// Returns the opened context with the pool it draws from in *hw_frames_ref, or
+// null with nothing left allocated and the reason logged, which leaves the
+// caller to encode in software.
+static AVCodecContext *openHwEncoder(const AVCodec *enc, AVBufferRef *hw_device_ref,
+ AVPixelFormat hw_pix_fmt, int width, int height,
+ AVRational frameRate, int64_t bitrate, int gopSize,
+ AVBufferRef **hw_frames_ref)
+{
+ AVCodecContext *enc_ctx = avcodec_alloc_context3(enc);
+
+ *hw_frames_ref = enc_ctx ? av_hwframe_ctx_alloc(hw_device_ref) : nullptr;
+
+ if (!*hw_frames_ref)
+ {
+ qWarning() << QString("Could not create a surface pool for the %1 encoder, encoding in software")
+ .arg(QLatin1String(enc->name));
+ if (enc_ctx) avcodec_free_context(&enc_ctx);
+ return nullptr;
+ }
+
+ // The pool the encoder draws its input surfaces from. NV12 is what these
+ // encoders take; the frames themselves live in GPU memory, which is what
+ // hw_pix_fmt denotes.
+ AVHWFramesContext *frames_ctx = (AVHWFramesContext *)(*hw_frames_ref)->data;
+ frames_ctx->format = hw_pix_fmt;
+ frames_ctx->sw_format = AV_PIX_FMT_NV12;
+ frames_ctx->width = width;
+ frames_ctx->height = height;
+ frames_ctx->initial_pool_size = 20;
+
+ int ret = av_hwframe_ctx_init(*hw_frames_ref);
+
+ if (ret < 0)
+ {
+ qWarning() << QString("Could not initialise the surface pool for the %1 encoder (%2), encoding in software")
+ .arg(QLatin1String(enc->name), ff_errstr(ret));
+ av_buffer_unref(hw_frames_ref);
+ avcodec_free_context(&enc_ctx);
+ return nullptr;
+ }
+
+ enc_ctx->width = width;
+ enc_ctx->height = height;
+ enc_ctx->pix_fmt = hw_pix_fmt;
+ enc_ctx->time_base = av_inv_q(frameRate);
+ enc_ctx->framerate = frameRate;
+ enc_ctx->bit_rate = bitrate;
+ enc_ctx->gop_size = gopSize;
+ enc_ctx->max_b_frames = 0;
+ enc_ctx->hw_frames_ctx = av_buffer_ref(*hw_frames_ref);
+
+ // No preset/tune here: those belong to the software encoders and mean
+ // nothing to a hardware one.
+ ret = avcodec_open2(enc_ctx, enc, nullptr);
+
+ if (ret < 0)
+ {
+ qWarning() << QString("The %1 encoder would not open (%2), encoding in software")
+ .arg(QLatin1String(enc->name), ff_errstr(ret));
+ av_buffer_unref(hw_frames_ref);
+ avcodec_free_context(&enc_ctx);
+ return nullptr;
+ }
+
+ return enc_ctx;
+}
+#endif
+
void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
{
const AVRational frameRate = cameraFrameRate(in_st, m_frameRate);
@@ -1315,9 +1399,9 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
}
// The GPU encoder is tried first when asked for, and anything that goes
- // wrong on the way - no such encoder built into FFmpeg, no usable VAAPI
- // device, no surface pool, the encoder refusing to open - just leaves
- // hardware false and takes the software path below.
+ // wrong on the way - no such encoder built into FFmpeg, no usable device,
+ // no surface pool, the encoder refusing to open - just leaves hardware
+ // false and takes the software path below.
bool hardware = false;
const AVCodec *enc = nullptr;
AVCodecContext *enc_ctx = nullptr;
@@ -1331,46 +1415,10 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
if (hw_device_ref)
{
- enc_ctx = avcodec_alloc_context3(enc);
- hw_frames_ref = enc_ctx ? av_hwframe_ctx_alloc(hw_device_ref) : nullptr;
-
- if (hw_frames_ref)
- {
- // The pool the encoder draws its input surfaces from. NV12 is
- // what VAAPI encoders take; the frames themselves live in GPU
- // memory, which is what AV_PIX_FMT_VAAPI denotes.
- AVHWFramesContext *frames_ctx = (AVHWFramesContext *)hw_frames_ref->data;
- frames_ctx->format = AV_PIX_FMT_VAAPI;
- frames_ctx->sw_format = AV_PIX_FMT_NV12;
- frames_ctx->width = m_width;
- frames_ctx->height = m_height;
- frames_ctx->initial_pool_size = 20;
-
- if (av_hwframe_ctx_init(hw_frames_ref) >= 0)
- {
- enc_ctx->width = m_width;
- enc_ctx->height = m_height;
- enc_ctx->pix_fmt = AV_PIX_FMT_VAAPI;
- enc_ctx->time_base = av_inv_q(frameRate);
- enc_ctx->framerate = frameRate;
- enc_ctx->bit_rate = m_bitrate;
- enc_ctx->gop_size = gopSize;
- enc_ctx->max_b_frames = 0;
- enc_ctx->hw_frames_ctx = av_buffer_ref(hw_frames_ref);
-
- // No preset/tune here: those belong to the software
- // encoders and mean nothing to a VAAPI one.
- if ((ret = avcodec_open2(enc_ctx, enc, nullptr)) >= 0)
- hardware = true;
- else
- qWarning() << QString("The VAAPI %1 encoder would not open (%2), encoding in software")
- .arg(QLatin1String(kVideoEncoders[encIdx].codec), ff_errstr(ret));
- }
- else
- {
- qWarning() << "Could not create a VAAPI surface pool, encoding in software";
- }
- }
+ enc_ctx = openHwEncoder(enc, hw_device_ref, AV_PIX_FMT_VAAPI,
+ m_width, m_height, frameRate, m_bitrate, gopSize,
+ &hw_frames_ref);
+ hardware = enc_ctx != nullptr;
av_buffer_unref(&hw_device_ref);
}
@@ -1385,11 +1433,62 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
}
if (!hardware)
- {
- if (enc_ctx) avcodec_free_context(&enc_ctx);
- if (hw_frames_ref) av_buffer_unref(&hw_frames_ref);
enc = nullptr;
+ }
+#endif
+
+#ifdef HAVE_D3D12VA
+ // What the same request means on Windows, where the GPU is reached
+ // through D3D12 Video rather than through libva. Written as a fallback
+ // after the VAAPI attempt rather than as its alternative so that a build
+ // that somehow has both - libva has a Windows port of its own - still
+ // gets a hardware encoder if only one of the two comes up.
+ //
+ // The device is created per capture session and dropped with it, unlike
+ // the VAAPI one: nothing else in the client holds a D3D12 device, since
+ // decoding and rendering do not go through it.
+ if (!hardware && m_useVaapi)
+ {
+ const char *encoderName = kVideoEncoders[encIdx].d3d12vaEncoder;
+
+ enc = encoderName ? avcodec_find_encoder_by_name(encoderName) : nullptr;
+
+ AVBufferRef *hw_device_ref = nullptr;
+ ret = enc ? av_hwdevice_ctx_create(&hw_device_ref, AV_HWDEVICE_TYPE_D3D12VA,
+ nullptr, nullptr, 0)
+ : 0;
+
+ if (hw_device_ref)
+ {
+ enc_ctx = openHwEncoder(enc, hw_device_ref, AV_PIX_FMT_D3D12,
+ m_width, m_height, frameRate, m_bitrate, gopSize,
+ &hw_frames_ref);
+ hardware = enc_ctx != nullptr;
+
+ av_buffer_unref(&hw_device_ref);
+ }
+ else if (enc)
+ {
+ // Where the GPU or the version of Windows cannot encode at all,
+ // which is a plain answer rather than a fault: D3D12 video
+ // encoding needs Windows 11, or Windows 10 with the Agility SDK
+ // alongside the client.
+ qWarning() << QString("No usable Direct3D 12 encoding device (%1), encoding in software")
+ .arg(ff_errstr(ret));
}
+ else if (encoderName)
+ {
+ qWarning() << QString("FFmpeg has no %1 encoder, encoding in software")
+ .arg(QLatin1String(encoderName));
+ }
+ else
+ {
+ qWarning() << QString("Direct3D 12 cannot encode %1, encoding in software")
+ .arg(QLatin1String(kVideoEncoders[encIdx].codec));
+ }
+
+ if (!hardware)
+ enc = nullptr;
}
#endif
@@ -1451,9 +1550,12 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
? QStringLiteral("uncompressed")
: QString::fromLatin1(avcodec_get_name(in_st->codecpar->codec_id));
+ // Named from the encoder that was actually opened rather than written out
+ // per API: "h264_vaapi" and "hevc_d3d12va" say both which codec and which
+ // accelerator settled it, and cannot drift from what is running.
emit accelerationInUse(hardware
- ? QString("GPU encoding (VAAPI %1) from %2")
- .arg(QLatin1String(kVideoEncoders[encIdx].codec), source)
+ ? QString("GPU encoding (%1) from %2")
+ .arg(QLatin1String(enc->name), source)
: QString("CPU encoding (%1) from %2").arg(swEncoderName, source));
// Everything the camera sends is scaled to the encoder's size and re-encoded,
@@ -1470,9 +1572,9 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
enc_ctx->width, enc_ctx->height, av_q2d(frameRate));
// What the camera's frames are scaled into. With a hardware encoder this
- // is only a staging buffer in system memory - NV12, which is what VAAPI
- // takes - that gets uploaded to a GPU surface below; the encoder itself
- // consumes AV_PIX_FMT_VAAPI frames and never sees this one.
+ // is only a staging buffer in system memory - NV12, which is what the
+ // GPU encoders take - that gets uploaded to a GPU surface below; the
+ // encoder itself consumes surfaces and never sees this frame.
AVPixelFormat sw_pix_fmt = hardware ? AV_PIX_FMT_NV12 : enc_ctx->pix_fmt;
AVFrame *enc_frame = av_frame_alloc();
@@ -1576,7 +1678,7 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
// What actually goes to the encoder: the staging frame itself in
// software, or a GPU surface holding a copy of it in hardware.
AVFrame *frame_to_encode = enc_frame;
-#ifdef HAVE_VAAPI
+#if defined(HAVE_VAAPI) || defined(HAVE_D3D12VA)
AVFrame *hw_frame = nullptr;
if (hardware)
@@ -1602,7 +1704,7 @@ void VideoDevice::runTranscodeLoop(AVFormatContext *ifmt_ctx, AVStream *in_st)
#endif
int send_ret = avcodec_send_frame(enc_ctx, frame_to_encode);
-#ifdef HAVE_VAAPI
+#if defined(HAVE_VAAPI) || defined(HAVE_D3D12VA)
if (hw_frame)
av_frame_free(&hw_frame);
#endif