diff options
| author | Anton <anton@picapica.im> | 2026-10-04 15:44:10 +0400 |
|---|---|---|
| committer | Anton <anton@picapica.im> | 2026-10-04 15:44:10 +0400 |
| commit | 85e2f8f019b50f16ad61e7c9f214b205aa528379 (patch) | |
| tree | f89c3154f8f0b3ca0b78bcd70c8059d392b3486f | |
| parent | e7a468d008de86ddfdf62c7d4cb2888678402370 (diff) | |
| download | pica-pica-85e2f8f019b50f16ad61e7c9f214b205aa528379.tar.gz pica-pica-85e2f8f019b50f16ad61e7c9f214b205aa528379.zip | |
pica-client: add d3d12va encoding acceleration on Windows
| -rw-r--r-- | configure.ac | 52 | ||||
| -rw-r--r-- | pica-client/dialogs/settingsdialog.cpp | 25 | ||||
| -rw-r--r-- | pica-client/videodevice.cpp | 224 | ||||
| -rw-r--r-- | pica-client/videodevice.h | 9 |
4 files changed, 241 insertions, 69 deletions
diff --git a/configure.ac b/configure.ac index eea64d1..f7b48f6 100644 --- a/configure.ac +++ b/configure.ac @@ -92,6 +92,16 @@ AC_ARG_ENABLE([vaapi], AS_HELP_STRING([--disable-vaapi], [Disable VAAPI accelerated video encoding, decoding and rendering])) +#checking if Direct3D 12 hardware encoding is wanted. The Windows counterpart +#of the VAAPI encoding path above - there is no libva there, but FFmpeg can +#reach the same GPU encoders through D3D12 Video. Autodetected the same way, +#and only ever looked for when building for Windows. +AC_ARG_ENABLE([d3d12va], + AS_HELP_STRING([--enable-d3d12va], + [Enable Direct3D 12 accelerated video encoding, Windows only (default: autodetect)]) + AS_HELP_STRING([--disable-d3d12va], + [Disable Direct3D 12 accelerated video encoding])) + #checking if PulseAudio device enumeration is wanted. Only affects what the #settings dialog can list; see the check further down. AC_ARG_ENABLE([pulse], @@ -313,6 +323,48 @@ AS_IF([test "x$PICA_VAAPI_ENABLED" = "xyes" && test "x$enable_vaapi_x11" != "xno [AC_MSG_WARN(["libva-x11, libX11 or libGL not found - building without X11 rendering, hardware rendering will need an EGL session"])]) ]) ]) + +#check whether FFmpeg knows how to encode through Direct3D 12, which is how a +#Windows build reaches the GPU: libva is not part of that platform, but the +#hwcontext machinery the VAAPI path uses carries a D3D12 device type as well, +#and libavcodec has h264_d3d12va and hevc_d3d12va encoders to go with it. +# +#Nothing is linked for this - libavutil loads d3d12.dll itself when a device is +#created - so all there is to check is that the headers are new enough to name +#the device type and the pixel format. Whether those encoders were compiled +#into the libavcodec being built against, and whether the machine the client +#ends up running on has a GPU that can encode, are both runtime questions, +#answered by falling back to the software encoder. +AS_IF([test "x$TARGET_OS" = "xwindows" && test "x$enable_d3d12va" != "xno"], [ + AC_MSG_CHECKING([whether FFmpeg supports Direct3D 12 video encoding]) + + pica_save_CPPFLAGS="$CPPFLAGS" + CPPFLAGS="$CPPFLAGS $FFMPEG_CFLAGS" + + AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM( + [[#include <libavutil/hwcontext.h> + #include <libavutil/pixfmt.h>]], + [[enum AVHWDeviceType device = AV_HWDEVICE_TYPE_D3D12VA; + enum AVPixelFormat format = AV_PIX_FMT_D3D12; + (void)device; (void)format;]])], + [ + PICA_D3D12VA_ENABLED=yes + AC_DEFINE([HAVE_D3D12VA], [1], [Define to 1 if FFmpeg can encode video through Direct3D 12]) + AC_MSG_RESULT([yes]) + ], + [ + AC_MSG_RESULT([no]) + AS_IF([test "x$enable_d3d12va" = "xyes"], + [AC_MSG_ERROR(["Direct3D 12 encoding was requested but this FFmpeg has no D3D12 hardware context"])], + [AC_MSG_WARN(["this FFmpeg has no D3D12 hardware context - building without Direct3D 12 encoding"])]) + ]) + + CPPFLAGS="$pica_save_CPPFLAGS" +]) + +AS_IF([test "x$TARGET_OS" != "xwindows" && test "x$enable_d3d12va" = "xyes"], + [AC_MSG_ERROR(["Direct3D 12 encoding was requested but it only exists on Windows"])]) ]) AM_CONDITIONAL([PICA_VAAPI], [test "x$PICA_VAAPI_ENABLED" = "xyes"]) diff --git a/pica-client/dialogs/settingsdialog.cpp b/pica-client/dialogs/settingsdialog.cpp index 0cfd965..a768b55 100644 --- a/pica-client/dialogs/settingsdialog.cpp +++ b/pica-client/dialogs/settingsdialog.cpp @@ -375,8 +375,19 @@ SettingsDialog::SettingsDialog(QWidget *parent) : cbPreferCompressed = new QCheckBox(tr("Prefer compressed formats if provided by the camera"), this); +#if defined(HAVE_VAAPI) || defined(HAVE_D3D12VA) + // Encoding is offered wherever there is an accelerator to encode on, + // named after the one this build uses; decoding and rendering below are + // VAAPI only, since the surfaces a D3D12 decoder produces have no way yet + // to reach the widget that would draw them. #ifdef HAVE_VAAPI cbVaapiEncoding = new QCheckBox(tr("enable VAAPI-accelerated hardware encoding"), this); +#else + cbVaapiEncoding = new QCheckBox(tr("enable Direct3D 12 accelerated hardware encoding"), this); +#endif +#endif + +#ifdef HAVE_VAAPI cbVaapiDecoding = new QCheckBox(tr("enable VAAPI-accelerated hardware decoding"), this); cbVaapiRendering = new QCheckBox(tr("enable VAAPI accelerated hardware rendering"), this); // Drawing a VA surface means having decoded into one first, so rendering @@ -418,8 +429,10 @@ SettingsDialog::SettingsDialog(QWidget *parent) : videodevLayout->addWidget(videoDevRefresh); videodevLayout->addLayout(videoFormatLayout); videodevLayout->addWidget(cbPreferCompressed); -#ifdef HAVE_VAAPI +#if defined(HAVE_VAAPI) || defined(HAVE_D3D12VA) videodevLayout->addWidget(cbVaapiEncoding); +#endif +#ifdef HAVE_VAAPI videodevLayout->addWidget(cbVaapiDecoding); videodevLayout->addWidget(cbVaapiRendering); #endif @@ -884,7 +897,7 @@ void SettingsDialog::toggleVideoTest() videoFpsTimer->start(); bool vaapiEncoding = false; -#ifdef HAVE_VAAPI +#if defined(HAVE_VAAPI) || defined(HAVE_D3D12VA) vaapiEncoding = cbVaapiEncoding->isChecked(); #endif @@ -1647,8 +1660,10 @@ void SettingsDialog::loadSettings() // actually changes - and the stored codec is often the one already showing. callAudioCodecChanged(); -#ifdef HAVE_VAAPI +#if defined(HAVE_VAAPI) || defined(HAVE_D3D12VA) cbVaapiEncoding->setChecked(st.loadValue("video.vaapi_encoding", 0).toBool()); +#endif +#ifdef HAVE_VAAPI cbVaapiDecoding->setChecked(st.loadValue("video.vaapi_decoding", 0).toBool()); cbVaapiRendering->setChecked(st.loadValue("video.vaapi_rendering", 0).toBool()); #endif @@ -1744,8 +1759,10 @@ void SettingsDialog::storeSettings() audioBitrateKbps = callAudioBitrate->value(); st.storeValue("call.audio_bitrate_kbps", QString::number(audioBitrateKbps)); -#ifdef HAVE_VAAPI +#if defined(HAVE_VAAPI) || defined(HAVE_D3D12VA) st.storeValue("video.vaapi_encoding", cbVaapiEncoding->isChecked() ? "1" : "0"); +#endif +#ifdef HAVE_VAAPI st.storeValue("video.vaapi_decoding", cbVaapiDecoding->isChecked() ? "1" : "0"); st.storeValue("video.vaapi_rendering", cbVaapiRendering->isChecked() ? "1" : "0"); #endif 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 diff --git a/pica-client/videodevice.h b/pica-client/videodevice.h index 02be737..5c4585c 100644 --- a/pica-client/videodevice.h +++ b/pica-client/videodevice.h @@ -173,10 +173,11 @@ public: // looking for one to forward, so choosing a codec the camera can produce // itself gets that one rather than merely a compatible one. // - // useVaapi asks for the frames to be encoded on the GPU. It is a request, - // not a guarantee: without VAAPI support built in, without usable - // hardware, or if the hardware encoder will not open, capture falls back - // to encoding in software. + // useVaapi asks for the frames to be encoded on the GPU, through whichever + // accelerator the build has - VAAPI where there is libva, Direct3D 12 on + // Windows. It is a request, not a guarantee: with no such support built + // in, without usable hardware, or if the hardware encoder will not open, + // capture falls back to encoding in software. Q_INVOKABLE void configureCapture(QString deviceName, int width, int height, int frameRate, bool preferCompressed, QString codec, int bitrate, bool useVaapi); |
