From 3fb384f868b39fcd0e6d2d316e2ab791a1808378 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jan 2024 10:06:53 +0000 Subject: [PATCH 1/3] build(deps): bump third-party/tray from `8bb9978` to `2bf1c61` Bumps [third-party/tray](https://github.com/LizardByte/tray) from `8bb9978` to `2bf1c61`. - [Commits](https://github.com/LizardByte/tray/compare/8bb9978991a1438fe0665513012628d85f0783ce...2bf1c610300b27f8d8ce87e2f13223fc83efeb42) --- updated-dependencies: - dependency-name: third-party/tray dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- third-party/tray | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/third-party/tray b/third-party/tray index 8bb9978991a..2bf1c610300 160000 --- a/third-party/tray +++ b/third-party/tray @@ -1 +1 @@ -Subproject commit 8bb9978991a1438fe0665513012628d85f0783ce +Subproject commit 2bf1c610300b27f8d8ce87e2f13223fc83efeb42 From 056281b745df3de3910d828cdc926436c7dda97a Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 11 Jan 2024 22:41:58 -0600 Subject: [PATCH 2/3] Implement HDR support for Linux KMS capture backend (#1994) Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> --- docs/source/about/setup.rst | 32 +++++--- src/platform/linux/graphics.cpp | 12 +-- src/platform/linux/graphics.h | 4 +- src/platform/linux/kmsgrab.cpp | 132 ++++++++++++++++++++++++++++++-- src/platform/linux/vaapi.cpp | 25 +++++- 5 files changed, 178 insertions(+), 27 deletions(-) diff --git a/docs/source/about/setup.rst b/docs/source/about/setup.rst index 01806887cd0..b2d2421ba56 100644 --- a/docs/source/about/setup.rst +++ b/docs/source/about/setup.rst @@ -586,15 +586,29 @@ Considerations HDR Support ----------- -Streaming HDR content is supported for Windows hosts with NVIDIA, AMD, or Intel GPUs that support encoding HEVC Main 10. -You must have an HDR-capable display or EDID emulator dongle connected to your host PC to activate HDR in Windows. - -- Ensure you enable the HDR option in your Moonlight client settings, otherwise the stream will be SDR. -- A good HDR experience relies on proper HDR display calibration both in Windows and in game. HDR calibration can differ significantly between client and host displays. -- We recommend calibrating the display by streaming the Windows HDR Calibration app to your client device and saving an HDR calibration profile to use while streaming. -- You may also need to tune the brightness slider or HDR calibration options in game to the different HDR brightness capabilities of your client's display. -- Older games that use NVIDIA-specific NVAPI HDR rather than native Windows 10 OS HDR support may not display in HDR. -- Some GPUs can produce lower image quality or encoding performance when streaming in HDR compared to SDR. +Streaming HDR content is officially supported on Windows hosts and experimentally supported for Linux hosts. + +- General HDR support information and requirements: + + - HDR must be activated in the host OS, which may require an HDR-capable display or EDID emulator dongle connected to your host PC. + - You must also enable the HDR option in your Moonlight client settings, otherwise the stream will be SDR (and probably overexposed if your host is HDR). + - A good HDR experience relies on proper HDR display calibration both in the OS and in game. HDR calibration can differ significantly between client and host displays. + - You may also need to tune the brightness slider or HDR calibration options in game to the different HDR brightness capabilities of your client's display. + - Some GPUs video encoders can produce lower image quality or encoding performance when streaming in HDR compared to SDR. + +- Additional information: + +.. tab:: Windows + + - HDR streaming is supported for Intel, AMD, and NVIDIA GPUs that support encoding HEVC Main 10 or AV1 10-bit profiles. + - We recommend calibrating the display by streaming the Windows HDR Calibration app to your client device and saving an HDR calibration profile to use while streaming. + - Older games that use NVIDIA-specific NVAPI HDR rather than native Windows HDR support may not display properly in HDR. + +.. tab:: Linux + + - HDR streaming is supported for Intel and AMD GPUs that support encoding HEVC Main 10 or AV1 10-bit profiles using VAAPI. + - The KMS capture backend is required for HDR capture. Other capture methods, like NvFBC or X11, do not support HDR. + - You will need a desktop environment with a compositor that supports HDR rendering, such as Gamescope or KDE Plasma 6. .. seealso:: `Arch wiki on HDR Support for Linux `__ and diff --git a/src/platform/linux/graphics.cpp b/src/platform/linux/graphics.cpp index 3673ef137a1..6fe51fc8c8c 100644 --- a/src/platform/linux/graphics.cpp +++ b/src/platform/linux/graphics.cpp @@ -662,19 +662,19 @@ namespace egl { } std::optional - sws_t::make(int in_width, int in_height, int out_width, int out_heigth, gl::tex_t &&tex) { + sws_t::make(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex) { sws_t sws; sws.serial = std::numeric_limits::max(); // Ensure aspect ratio is maintained - auto scalar = std::fminf(out_width / (float) in_width, out_heigth / (float) in_height); + auto scalar = std::fminf(out_width / (float) in_width, out_height / (float) in_height); auto out_width_f = in_width * scalar; auto out_height_f = in_height * scalar; // result is always positive auto offsetX_f = (out_width - out_width_f) / 2; - auto offsetY_f = (out_heigth - out_height_f) / 2; + auto offsetY_f = (out_height - out_height_f) / 2; sws.out_width = out_width_f; sws.out_height = out_height_f; @@ -806,12 +806,12 @@ namespace egl { } std::optional - sws_t::make(int in_width, int in_height, int out_width, int out_heigth) { + sws_t::make(int in_width, int in_height, int out_width, int out_height, GLint gl_tex_internal_fmt) { auto tex = gl::tex_t::make(2); gl::ctx.BindTexture(GL_TEXTURE_2D, tex[0]); - gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, in_width, in_height); + gl::ctx.TexStorage2D(GL_TEXTURE_2D, 1, gl_tex_internal_fmt, in_width, in_height); - return make(in_width, in_height, out_width, out_heigth, std::move(tex)); + return make(in_width, in_height, out_width, out_height, std::move(tex)); } void diff --git a/src/platform/linux/graphics.h b/src/platform/linux/graphics.h index d2874bde1d4..56995ca064f 100644 --- a/src/platform/linux/graphics.h +++ b/src/platform/linux/graphics.h @@ -314,9 +314,9 @@ namespace egl { class sws_t { public: static std::optional - make(int in_width, int in_height, int out_width, int out_heigth, gl::tex_t &&tex); + make(int in_width, int in_height, int out_width, int out_height, gl::tex_t &&tex); static std::optional - make(int in_width, int in_height, int out_width, int out_heigth); + make(int in_width, int in_height, int out_width, int out_height, GLint gl_tex_internal_fmt); // Convert the loaded image into the first two framebuffers int diff --git a/src/platform/linux/kmsgrab.cpp b/src/platform/linux/kmsgrab.cpp index bc8e567f3a4..492907c71df 100644 --- a/src/platform/linux/kmsgrab.cpp +++ b/src/platform/linux/kmsgrab.cpp @@ -105,6 +105,7 @@ namespace platf { using crtc_t = util::safe_ptr; using obj_prop_t = util::safe_ptr; using prop_t = util::safe_ptr; + using prop_blob_t = util::safe_ptr; using conn_type_count_t = std::map; @@ -135,6 +136,9 @@ namespace platf { // For example HDMI-A-{index} or HDMI-{index} std::uint32_t index; + // ID of the connector + std::uint32_t connector_id; + bool connected; }; @@ -336,14 +340,23 @@ namespace platf { return false; } - std::uint32_t - get_panel_orientation(std::uint32_t plane_id) { - auto props = plane_props(plane_id); + std::optional + prop_value_by_name(const std::vector> &props, std::string_view name) { for (auto &[prop, val] : props) { - if (prop->name == "rotation"sv) { + if (prop->name == name) { return val; } } + return std::nullopt; + } + + std::uint32_t + get_panel_orientation(std::uint32_t plane_id) { + auto props = plane_props(plane_id); + auto value = prop_value_by_name(props, "rotation"sv); + if (value) { + return *value; + } BOOST_LOG(error) << "Failed to determine panel orientation, defaulting to landscape."; return DRM_MODE_ROTATE_0; @@ -392,6 +405,7 @@ namespace platf { conn->connector_type, crtc_id, index, + conn->connector_id, conn->connection == DRM_MODE_CONNECTED, }); }); @@ -414,6 +428,9 @@ namespace platf { std::vector> props(std::uint32_t id, std::uint32_t type) { obj_prop_t obj_prop = drmModeObjectGetProperties(fd.el, id, type); + if (!obj_prop) { + return {}; + } std::vector> props; props.reserve(obj_prop->count_props); @@ -651,12 +668,24 @@ namespace platf { offset_y = crtc->y; } - this->card = std::move(card); - plane_id = plane->plane_id; crtc_id = plane->crtc_id; - crtc_index = this->card.get_crtc_index_by_id(plane->crtc_id); + crtc_index = card.get_crtc_index_by_id(plane->crtc_id); + + // Find the connector for this CRTC + kms::conn_type_count_t conn_type_count; + for (auto &connector : card.monitors(conn_type_count)) { + if (connector.crtc_id == crtc_id) { + BOOST_LOG(info) << "Found connector ID ["sv << connector.connector_id << ']'; + + connector_id = connector.connector_id; + + auto connector_props = card.connector_props(*connector_id); + hdr_metadata_blob_id = card.prop_value_by_name(connector_props, "HDR_OUTPUT_METADATA"sv); + } + } + this->card = std::move(card); goto break_loop; } } @@ -703,6 +732,83 @@ namespace platf { return 0; } + bool + is_hdr() { + if (!hdr_metadata_blob_id || *hdr_metadata_blob_id == 0) { + return false; + } + + prop_blob_t hdr_metadata_blob = drmModeGetPropertyBlob(card.fd.el, *hdr_metadata_blob_id); + if (hdr_metadata_blob == nullptr) { + BOOST_LOG(error) << "Unable to get HDR metadata blob: "sv << strerror(errno); + return false; + } + + if (hdr_metadata_blob->length < sizeof(uint32_t) + sizeof(hdr_metadata_infoframe)) { + BOOST_LOG(error) << "HDR metadata blob is too small: "sv << hdr_metadata_blob->length; + return false; + } + + auto raw_metadata = (hdr_output_metadata *) hdr_metadata_blob->data; + if (raw_metadata->metadata_type != 0) { // HDMI_STATIC_METADATA_TYPE1 + BOOST_LOG(error) << "Unknown HDMI_STATIC_METADATA_TYPE value: "sv << raw_metadata->metadata_type; + return false; + } + + if (raw_metadata->hdmi_metadata_type1.metadata_type != 0) { // Static Metadata Type 1 + BOOST_LOG(error) << "Unknown secondary metadata type value: "sv << raw_metadata->hdmi_metadata_type1.metadata_type; + return false; + } + + // We only support Traditional Gamma SDR or SMPTE 2084 PQ HDR EOTFs. + // Print a warning if we encounter any others. + switch (raw_metadata->hdmi_metadata_type1.eotf) { + case 0: // HDMI_EOTF_TRADITIONAL_GAMMA_SDR + return false; + case 1: // HDMI_EOTF_TRADITIONAL_GAMMA_HDR + BOOST_LOG(warning) << "Unsupported HDR EOTF: Traditional Gamma"sv; + return true; + case 2: // HDMI_EOTF_SMPTE_ST2084 + return true; + case 3: // HDMI_EOTF_BT_2100_HLG + BOOST_LOG(warning) << "Unsupported HDR EOTF: HLG"sv; + return true; + default: + BOOST_LOG(warning) << "Unsupported HDR EOTF: "sv << raw_metadata->hdmi_metadata_type1.eotf; + return true; + } + } + + bool + get_hdr_metadata(SS_HDR_METADATA &metadata) { + // This performs all the metadata validation + if (!is_hdr()) { + return false; + } + + prop_blob_t hdr_metadata_blob = drmModeGetPropertyBlob(card.fd.el, *hdr_metadata_blob_id); + if (hdr_metadata_blob == nullptr) { + BOOST_LOG(error) << "Unable to get HDR metadata blob: "sv << strerror(errno); + return false; + } + + auto raw_metadata = (hdr_output_metadata *) hdr_metadata_blob->data; + + for (int i = 0; i < 3; i++) { + metadata.displayPrimaries[i].x = raw_metadata->hdmi_metadata_type1.display_primaries[i].x; + metadata.displayPrimaries[i].y = raw_metadata->hdmi_metadata_type1.display_primaries[i].y; + } + + metadata.whitePoint.x = raw_metadata->hdmi_metadata_type1.white_point.x; + metadata.whitePoint.y = raw_metadata->hdmi_metadata_type1.white_point.y; + metadata.maxDisplayLuminance = raw_metadata->hdmi_metadata_type1.max_display_mastering_luminance; + metadata.minDisplayLuminance = raw_metadata->hdmi_metadata_type1.min_display_mastering_luminance; + metadata.maxContentLightLevel = raw_metadata->hdmi_metadata_type1.max_cll; + metadata.maxFrameAverageLightLevel = raw_metadata->hdmi_metadata_type1.max_fall; + + return true; + } + void update_cursor() { if (cursor_plane_id < 0) { @@ -881,6 +987,15 @@ namespace platf { inline capture_e refresh(file_t *file, egl::surface_descriptor_t *sd) { + // Check for a change in HDR metadata + if (connector_id) { + auto connector_props = card.connector_props(*connector_id); + if (hdr_metadata_blob_id != card.prop_value_by_name(connector_props, "HDR_OUTPUT_METADATA"sv)) { + BOOST_LOG(info) << "Reinitializing capture after HDR metadata change"sv; + return capture_e::reinit; + } + } + plane_t plane = drmModeGetPlane(card.fd.el, plane_id); auto fb = card.fb(plane.get()); @@ -944,6 +1059,9 @@ namespace platf { int crtc_id; int crtc_index; + std::optional connector_id; + std::optional hdr_metadata_blob_id; + int cursor_plane_id; cursor_t captured_cursor {}; diff --git a/src/platform/linux/vaapi.cpp b/src/platform/linux/vaapi.cpp index e2846b30f0c..118e1bd8da0 100644 --- a/src/platform/linux/vaapi.cpp +++ b/src/platform/linux/vaapi.cpp @@ -130,12 +130,12 @@ namespace va { } int - set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override { + set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx_buf) override { this->hwframe.reset(frame); this->frame = frame; if (!frame->buf[0]) { - if (av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) { + if (av_hwframe_get_buffer(hw_frames_ctx_buf, frame, 0)) { BOOST_LOG(error) << "Couldn't get hwframe for VAAPI"sv; return -1; } @@ -143,6 +143,7 @@ namespace va { va::DRMPRIMESurfaceDescriptor prime; va::VASurfaceID surface = (std::uintptr_t) frame->data[3]; + auto hw_frames_ctx = (AVHWFramesContext *) hw_frames_ctx_buf->data; auto status = vaExportSurfaceHandle( this->va_display, @@ -194,7 +195,25 @@ namespace va { return -1; } - auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height); + // Decide the bit depth format of the backing texture based the target frame format + GLint gl_format; + switch (hw_frames_ctx->sw_format) { + case AV_PIX_FMT_YUV420P: + case AV_PIX_FMT_NV12: + gl_format = GL_RGBA8; + break; + + case AV_PIX_FMT_YUV420P10: + case AV_PIX_FMT_P010: + gl_format = GL_RGB10_A2; + break; + + default: + BOOST_LOG(error) << "Unsupported pixel format for VA frame: "sv << hw_frames_ctx->sw_format; + return -1; + } + + auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height, gl_format); if (!sws_opt) { return -1; } From 545af98459417da145892cb0a80910b7b3496de0 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Mon, 8 Jan 2024 01:18:48 -0600 Subject: [PATCH 3/3] Add a fallback to retry codec init with more lenient config options This allows use of low_power=1 for VAAPI to allow more performant encoding on capable Intel hardware (like we do for QSV). This also provides a low_power=0 fallback for QSV to allow use on old/low-end Intel GPUs that don't support low power encoding. Finally, this also implements a fallback to deal with the AMD driver regression on pre-RDNA cards that causes H.264 encoding to fail with AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY. --- src/video.cpp | 395 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 238 insertions(+), 157 deletions(-) diff --git a/src/video.cpp b/src/video.cpp index 05e22209ca8..e9a1d6c38b4 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -364,6 +364,7 @@ namespace video { std::vector common_options; std::vector sdr_options; std::vector hdr_options; + std::vector fallback_options; std::optional qp; std::string name; @@ -578,6 +579,8 @@ namespace video { {}, // HDR-specific options {}, + // Fallback options + {}, std::nullopt, // QP "av1_nvenc"s, }, @@ -588,6 +591,8 @@ namespace video { {}, // HDR-specific options {}, + // Fallback options + {}, std::nullopt, // QP "hevc_nvenc"s, }, @@ -598,6 +603,8 @@ namespace video { {}, // HDR-specific options {}, + // Fallback options + {}, std::nullopt, // QP "h264_nvenc"s, }, @@ -636,6 +643,8 @@ namespace video { {}, // HDR-specific options {}, + // Fallback options + {}, std::nullopt, "av1_nvenc"s, }, @@ -658,6 +667,7 @@ namespace video { { { "profile"s, (int) nv::profile_hevc_e::main_10 }, }, + {}, // Fallback options std::nullopt, "hevc_nvenc"s, }, @@ -677,6 +687,7 @@ namespace video { { "profile"s, (int) nv::profile_h264_e::high }, }, {}, // HDR-specific options + {}, // Fallback options std::make_optional({ "qp"s, &config::video.qp }), "h264_nvenc"s, }, @@ -705,6 +716,8 @@ namespace video { {}, // HDR-specific options {}, + // Fallback options + {}, std::make_optional({ "qp"s, &config::video.qp }), "av1_qsv"s, }, @@ -727,6 +740,8 @@ namespace video { { { "profile"s, (int) qsv::profile_hevc_e::main_10 }, }, + // Fallback options + {}, std::make_optional({ "qp"s, &config::video.qp }), "hevc_qsv"s, }, @@ -748,7 +763,12 @@ namespace video { { { "profile"s, (int) qsv::profile_h264_e::high }, }, - {}, // HDR-specific options + // HDR-specific options + {}, + // Fallback options + { + { "low_power"s, 0 }, // Some old/low-end Intel GPUs don't support low power encoding + }, std::make_optional({ "qp"s, &config::video.qp }), "h264_qsv"s, }, @@ -774,6 +794,7 @@ namespace video { }, {}, // SDR-specific options {}, // HDR-specific options + {}, // Fallback options std::make_optional({ "qp_p"s, &config::video.qp }), "av1_amf"s, }, @@ -794,6 +815,7 @@ namespace video { }, {}, // SDR-specific options {}, // HDR-specific options + {}, // Fallback options std::make_optional({ "qp_p"s, &config::video.qp }), "hevc_amf"s, }, @@ -810,8 +832,14 @@ namespace video { { "usage"s, &config::video.amd.amd_usage_h264 }, { "vbaq"s, &config::video.amd.amd_vbaq }, }, - {}, // SDR-specific options - {}, // HDR-specific options + // SDR-specific options + {}, + // HDR-specific options + {}, + // Fallback options + { + { "usage"s, 2 /* AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY */ }, // Workaround for https://github.com/GPUOpen-LibrariesAndSDKs/AMF/issues/410 + }, std::make_optional({ "qp_p"s, &config::video.qp }), "h264_amf"s, }, @@ -837,6 +865,7 @@ namespace video { }, {}, // SDR-specific options {}, // HDR-specific options + {}, // Fallback options std::make_optional("qp"s, &config::video.qp), #ifdef ENABLE_BROKEN_AV1_ENCODER @@ -861,6 +890,7 @@ namespace video { }, {}, // SDR-specific options {}, // HDR-specific options + {}, // Fallback options std::make_optional("qp"s, &config::video.qp), "libx265"s, }, @@ -872,6 +902,7 @@ namespace video { }, {}, // SDR-specific options {}, // HDR-specific options + {}, // Fallback options std::make_optional("qp"s, &config::video.qp), "libx264"s, }, @@ -889,35 +920,56 @@ namespace video { { // Common options { + { "low_power"s, 1 }, { "async_depth"s, 1 }, { "idr_interval"s, std::numeric_limits::max() }, }, - {}, // SDR-specific options - {}, // HDR-specific options + // SDR-specific options + {}, + // HDR-specific options + {}, + // Fallback options + { + { "low_power"s, 0 }, // Not all VAAPI drivers expose LP entrypoints + }, std::make_optional("qp"s, &config::video.qp), "av1_vaapi"s, }, { // Common options { + { "low_power"s, 1 }, { "async_depth"s, 1 }, { "sei"s, 0 }, { "idr_interval"s, std::numeric_limits::max() }, }, - {}, // SDR-specific options - {}, // HDR-specific options + // SDR-specific options + {}, + // HDR-specific options + {}, + // Fallback options + { + { "low_power"s, 0 }, // Not all VAAPI drivers expose LP entrypoints + }, std::make_optional("qp"s, &config::video.qp), "hevc_vaapi"s, }, { // Common options { + { "low_power"s, 1 }, { "async_depth"s, 1 }, { "sei"s, 0 }, { "idr_interval"s, std::numeric_limits::max() }, }, - {}, // SDR-specific options - {}, // HDR-specific options + // SDR-specific options + {}, + // HDR-specific options + {}, + // Fallback options + { + { "low_power"s, 0 }, // Not all VAAPI drivers expose LP entrypoints + }, std::make_optional("qp"s, &config::video.qp), "h264_vaapi"s, }, @@ -943,6 +995,7 @@ namespace video { }, {}, // SDR-specific options {}, // HDR-specific options + {}, // Fallback options std::nullopt, "av1_videotoolbox"s, }, @@ -956,6 +1009,7 @@ namespace video { }, {}, // SDR-specific options {}, // HDR-specific options + {}, // Fallback options std::nullopt, "hevc_videotoolbox"s, }, @@ -969,6 +1023,7 @@ namespace video { }, {}, // SDR-specific options {}, // HDR-specific options + {}, // Fallback options std::nullopt, "h264_videotoolbox"s, }, @@ -1402,201 +1457,227 @@ namespace video { return nullptr; } - avcodec_ctx_t ctx { avcodec_alloc_context3(codec) }; - ctx->width = config.width; - ctx->height = config.height; - ctx->time_base = AVRational { 1, config.framerate }; - ctx->framerate = AVRational { config.framerate, 1 }; + auto colorspace = encode_device->colorspace; + auto sw_fmt = (colorspace.bit_depth == 10) ? platform_formats->avcodec_pix_fmt_10bit : platform_formats->avcodec_pix_fmt_8bit; - switch (config.videoFormat) { - case 0: - ctx->profile = FF_PROFILE_H264_HIGH; - break; + // Allow up to 1 retry to apply the set of fallback options. + // + // Note: If we later end up needing multiple sets of + // fallback options, we may need to allow more retries + // to try applying each set. + avcodec_ctx_t ctx; + for (int retries = 0; retries < 2; retries++) { + ctx.reset(avcodec_alloc_context3(codec)); + ctx->width = config.width; + ctx->height = config.height; + ctx->time_base = AVRational { 1, config.framerate }; + ctx->framerate = AVRational { config.framerate, 1 }; + + switch (config.videoFormat) { + case 0: + ctx->profile = FF_PROFILE_H264_HIGH; + break; - case 1: - ctx->profile = config.dynamicRange ? FF_PROFILE_HEVC_MAIN_10 : FF_PROFILE_HEVC_MAIN; - break; + case 1: + ctx->profile = config.dynamicRange ? FF_PROFILE_HEVC_MAIN_10 : FF_PROFILE_HEVC_MAIN; + break; - case 2: - // AV1 supports both 8 and 10 bit encoding with the same Main profile - ctx->profile = FF_PROFILE_AV1_MAIN; - break; - } + case 2: + // AV1 supports both 8 and 10 bit encoding with the same Main profile + ctx->profile = FF_PROFILE_AV1_MAIN; + break; + } - // B-frames delay decoder output, so never use them - ctx->max_b_frames = 0; + // B-frames delay decoder output, so never use them + ctx->max_b_frames = 0; - // Use an infinite GOP length since I-frames are generated on demand - ctx->gop_size = encoder.flags & LIMITED_GOP_SIZE ? - std::numeric_limits::max() : - std::numeric_limits::max(); + // Use an infinite GOP length since I-frames are generated on demand + ctx->gop_size = encoder.flags & LIMITED_GOP_SIZE ? + std::numeric_limits::max() : + std::numeric_limits::max(); - ctx->keyint_min = std::numeric_limits::max(); + ctx->keyint_min = std::numeric_limits::max(); - // Some client decoders have limits on the number of reference frames - if (config.numRefFrames) { - if (video_format[encoder_t::REF_FRAMES_RESTRICT]) { - ctx->refs = config.numRefFrames; - } - else { - BOOST_LOG(warning) << "Client requested reference frame limit, but encoder doesn't support it!"sv; + // Some client decoders have limits on the number of reference frames + if (config.numRefFrames) { + if (video_format[encoder_t::REF_FRAMES_RESTRICT]) { + ctx->refs = config.numRefFrames; + } + else { + BOOST_LOG(warning) << "Client requested reference frame limit, but encoder doesn't support it!"sv; + } } - } - ctx->flags |= (AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY); - ctx->flags2 |= AV_CODEC_FLAG2_FAST; + ctx->flags |= (AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY); + ctx->flags2 |= AV_CODEC_FLAG2_FAST; - auto colorspace = encode_device->colorspace; - auto avcodec_colorspace = avcodec_colorspace_from_sunshine_colorspace(colorspace); + auto avcodec_colorspace = avcodec_colorspace_from_sunshine_colorspace(colorspace); - ctx->color_range = avcodec_colorspace.range; - ctx->color_primaries = avcodec_colorspace.primaries; - ctx->color_trc = avcodec_colorspace.transfer_function; - ctx->colorspace = avcodec_colorspace.matrix; + ctx->color_range = avcodec_colorspace.range; + ctx->color_primaries = avcodec_colorspace.primaries; + ctx->color_trc = avcodec_colorspace.transfer_function; + ctx->colorspace = avcodec_colorspace.matrix; - auto sw_fmt = (colorspace.bit_depth == 10) ? platform_formats->avcodec_pix_fmt_10bit : platform_formats->avcodec_pix_fmt_8bit; + // Used by cbs::make_sps_hevc + ctx->sw_pix_fmt = sw_fmt; - // Used by cbs::make_sps_hevc - ctx->sw_pix_fmt = sw_fmt; + if (hardware) { + avcodec_buffer_t encoding_stream_context; - if (hardware) { - avcodec_buffer_t encoding_stream_context; + ctx->pix_fmt = platform_formats->avcodec_dev_pix_fmt; - ctx->pix_fmt = platform_formats->avcodec_dev_pix_fmt; + // Create the base hwdevice context + auto buf_or_error = platform_formats->init_avcodec_hardware_input_buffer(encode_device.get()); + if (buf_or_error.has_right()) { + return nullptr; + } + encoding_stream_context = std::move(buf_or_error.left()); - // Create the base hwdevice context - auto buf_or_error = platform_formats->init_avcodec_hardware_input_buffer(encode_device.get()); - if (buf_or_error.has_right()) { - return nullptr; - } - encoding_stream_context = std::move(buf_or_error.left()); + // If this encoder requires derivation from the base, derive the desired type + if (platform_formats->avcodec_derived_dev_type != AV_HWDEVICE_TYPE_NONE) { + avcodec_buffer_t derived_context; - // If this encoder requires derivation from the base, derive the desired type - if (platform_formats->avcodec_derived_dev_type != AV_HWDEVICE_TYPE_NONE) { - avcodec_buffer_t derived_context; + // Allow the hwdevice to prepare for this type of context to be derived + if (encode_device->prepare_to_derive_context(platform_formats->avcodec_derived_dev_type)) { + return nullptr; + } - // Allow the hwdevice to prepare for this type of context to be derived - if (encode_device->prepare_to_derive_context(platform_formats->avcodec_derived_dev_type)) { - return nullptr; - } + auto err = av_hwdevice_ctx_create_derived(&derived_context, platform_formats->avcodec_derived_dev_type, encoding_stream_context.get(), 0); + if (err) { + char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; + BOOST_LOG(error) << "Failed to derive device context: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); - auto err = av_hwdevice_ctx_create_derived(&derived_context, platform_formats->avcodec_derived_dev_type, encoding_stream_context.get(), 0); - if (err) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - BOOST_LOG(error) << "Failed to derive device context: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err); + return nullptr; + } - return nullptr; + encoding_stream_context = std::move(derived_context); } - encoding_stream_context = std::move(derived_context); - } + // Initialize avcodec hardware frames + { + avcodec_buffer_t frame_ref { av_hwframe_ctx_alloc(encoding_stream_context.get()) }; - // Initialize avcodec hardware frames - { - avcodec_buffer_t frame_ref { av_hwframe_ctx_alloc(encoding_stream_context.get()) }; + auto frame_ctx = (AVHWFramesContext *) frame_ref->data; + frame_ctx->format = ctx->pix_fmt; + frame_ctx->sw_format = sw_fmt; + frame_ctx->height = ctx->height; + frame_ctx->width = ctx->width; + frame_ctx->initial_pool_size = 0; - auto frame_ctx = (AVHWFramesContext *) frame_ref->data; - frame_ctx->format = ctx->pix_fmt; - frame_ctx->sw_format = sw_fmt; - frame_ctx->height = ctx->height; - frame_ctx->width = ctx->width; - frame_ctx->initial_pool_size = 0; + // Allow the hwdevice to modify hwframe context parameters + encode_device->init_hwframes(frame_ctx); - // Allow the hwdevice to modify hwframe context parameters - encode_device->init_hwframes(frame_ctx); + if (auto err = av_hwframe_ctx_init(frame_ref.get()); err < 0) { + return nullptr; + } - if (auto err = av_hwframe_ctx_init(frame_ref.get()); err < 0) { - return nullptr; + ctx->hw_frames_ctx = av_buffer_ref(frame_ref.get()); } - ctx->hw_frames_ctx = av_buffer_ref(frame_ref.get()); + ctx->slices = config.slicesPerFrame; } + else /* software */ { + ctx->pix_fmt = sw_fmt; - ctx->slices = config.slicesPerFrame; - } - else /* software */ { - ctx->pix_fmt = sw_fmt; + // Clients will request for the fewest slices per frame to get the + // most efficient encode, but we may want to provide more slices than + // requested to ensure we have enough parallelism for good performance. + ctx->slices = std::max(config.slicesPerFrame, config::video.min_threads); + } - // Clients will request for the fewest slices per frame to get the - // most efficient encode, but we may want to provide more slices than - // requested to ensure we have enough parallelism for good performance. - ctx->slices = std::max(config.slicesPerFrame, config::video.min_threads); - } + if (encoder.flags & SINGLE_SLICE_ONLY) { + ctx->slices = 1; + } - if (encoder.flags & SINGLE_SLICE_ONLY) { - ctx->slices = 1; - } + ctx->thread_type = FF_THREAD_SLICE; + ctx->thread_count = ctx->slices; - ctx->thread_type = FF_THREAD_SLICE; - ctx->thread_count = ctx->slices; + AVDictionary *options { nullptr }; + auto handle_option = [&options](const encoder_t::option_t &option) { + std::visit( + util::overloaded { + [&](int v) { av_dict_set_int(&options, option.name.c_str(), v, 0); }, + [&](int *v) { av_dict_set_int(&options, option.name.c_str(), *v, 0); }, + [&](std::optional *v) { if(*v) av_dict_set_int(&options, option.name.c_str(), **v, 0); }, + [&](std::function v) { av_dict_set_int(&options, option.name.c_str(), v(), 0); }, + [&](const std::string &v) { av_dict_set(&options, option.name.c_str(), v.c_str(), 0); }, + [&](std::string *v) { if(!v->empty()) av_dict_set(&options, option.name.c_str(), v->c_str(), 0); } }, + option.value); + }; - AVDictionary *options { nullptr }; - auto handle_option = [&options](const encoder_t::option_t &option) { - std::visit( - util::overloaded { - [&](int v) { av_dict_set_int(&options, option.name.c_str(), v, 0); }, - [&](int *v) { av_dict_set_int(&options, option.name.c_str(), *v, 0); }, - [&](std::optional *v) { if(*v) av_dict_set_int(&options, option.name.c_str(), **v, 0); }, - [&](std::function v) { av_dict_set_int(&options, option.name.c_str(), v(), 0); }, - [&](const std::string &v) { av_dict_set(&options, option.name.c_str(), v.c_str(), 0); }, - [&](std::string *v) { if(!v->empty()) av_dict_set(&options, option.name.c_str(), v->c_str(), 0); } }, - option.value); - }; + // Apply common options, then format-specific overrides + for (auto &option : video_format.common_options) { + handle_option(option); + } + for (auto &option : (config.dynamicRange ? video_format.hdr_options : video_format.sdr_options)) { + handle_option(option); + } + if (retries > 0) { + for (auto &option : video_format.fallback_options) { + handle_option(option); + } + } - // Apply common options, then format-specific overrides - for (auto &option : video_format.common_options) { - handle_option(option); - } - for (auto &option : (config.dynamicRange ? video_format.hdr_options : video_format.sdr_options)) { - handle_option(option); - } + if (video_format[encoder_t::CBR]) { + auto bitrate = config.bitrate * 1000; + ctx->rc_max_rate = bitrate; + ctx->bit_rate = bitrate; + + if (encoder.flags & CBR_WITH_VBR) { + // Ensure rc_max_bitrate != bit_rate to force VBR mode + ctx->bit_rate--; + } + else { + ctx->rc_min_rate = bitrate; + } - if (video_format[encoder_t::CBR]) { - auto bitrate = config.bitrate * 1000; - ctx->rc_max_rate = bitrate; - ctx->bit_rate = bitrate; + if (encoder.flags & RELAXED_COMPLIANCE) { + ctx->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL; + } - if (encoder.flags & CBR_WITH_VBR) { - // Ensure rc_max_bitrate != bit_rate to force VBR mode - ctx->bit_rate--; + if (!(encoder.flags & NO_RC_BUF_LIMIT)) { + if (!hardware && (ctx->slices > 1 || config.videoFormat == 1)) { + // Use a larger rc_buffer_size for software encoding when slices are enabled, + // because libx264 can severely degrade quality if the buffer is too small. + // libx265 encounters this issue more frequently, so always scale the + // buffer by 1.5x for software HEVC encoding. + ctx->rc_buffer_size = bitrate / ((config.framerate * 10) / 15); + } + else { + ctx->rc_buffer_size = bitrate / config.framerate; + } + } + } + else if (video_format.qp) { + handle_option(*video_format.qp); } else { - ctx->rc_min_rate = bitrate; + BOOST_LOG(error) << "Couldn't set video quality: encoder "sv << encoder.name << " doesn't support qp"sv; + return nullptr; } - if (encoder.flags & RELAXED_COMPLIANCE) { - ctx->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL; - } + if (auto status = avcodec_open2(ctx.get(), codec, &options)) { + char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - if (!(encoder.flags & NO_RC_BUF_LIMIT)) { - if (!hardware && (ctx->slices > 1 || config.videoFormat == 1)) { - // Use a larger rc_buffer_size for software encoding when slices are enabled, - // because libx264 can severely degrade quality if the buffer is too small. - // libx265 encounters this issue more frequently, so always scale the - // buffer by 1.5x for software HEVC encoding. - ctx->rc_buffer_size = bitrate / ((config.framerate * 10) / 15); + if (!video_format.fallback_options.empty() && retries == 0) { + BOOST_LOG(info) + << "Retrying with fallback configuration options for ["sv << video_format.name << "] after error: "sv + << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, status); + + continue; } else { - ctx->rc_buffer_size = bitrate / config.framerate; + BOOST_LOG(error) + << "Could not open codec ["sv + << video_format.name << "]: "sv + << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, status); + + return nullptr; } } - } - else if (video_format.qp) { - handle_option(*video_format.qp); - } - else { - BOOST_LOG(error) << "Couldn't set video quality: encoder "sv << encoder.name << " doesn't support qp"sv; - return nullptr; - } - if (auto status = avcodec_open2(ctx.get(), codec, &options)) { - char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 }; - BOOST_LOG(error) - << "Could not open codec ["sv - << video_format.name << "]: "sv - << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, status); - - return nullptr; + // Successfully opened the codec + break; } avcodec_frame_t frame { av_frame_alloc() };