From 056281b745df3de3910d828cdc926436c7dda97a Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 11 Jan 2024 22:41:58 -0600 Subject: [PATCH] Implement HDR support for Linux KMS capture backend (#1994) Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> --- docs/source/about/setup.rst | 30 ++++++-- 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, 177 insertions(+), 26 deletions(-) diff --git a/docs/source/about/setup.rst b/docs/source/about/setup.rst index 01806887..b2d2421b 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. +Streaming HDR content is officially supported on Windows hosts and experimentally supported for Linux hosts. -- 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. +- 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 3673ef13..6fe51fc8 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 d2874bde..56995ca0 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 bc8e567f..492907c7 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,13 +340,22 @@ namespace platf { return false; } + std::optional + prop_value_by_name(const std::vector> &props, std::string_view name) { + for (auto &[prop, val] : props) { + 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); - for (auto &[prop, val] : props) { - if (prop->name == "rotation"sv) { - return val; - } + auto value = prop_value_by_name(props, "rotation"sv); + if (value) { + return *value; } BOOST_LOG(error) << "Failed to determine panel orientation, defaulting to landscape."; @@ -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 e2846b30..118e1bd8 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; }