Skip to content

Commit

Permalink
Implement HDR support for Linux KMS capture backend (#1994)
Browse files Browse the repository at this point in the history
Co-authored-by: ReenigneArcher <[email protected]>
  • Loading branch information
cgutman and ReenigneArcher authored Jan 12, 2024
1 parent 3fb384f commit 056281b
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 27 deletions.
32 changes: 23 additions & 9 deletions docs/source/about/setup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://wiki.archlinux.org/title/HDR_monitor_support>`__ and
Expand Down
12 changes: 6 additions & 6 deletions src/platform/linux/graphics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -662,19 +662,19 @@ namespace egl {
}

std::optional<sws_t>
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<std::uint64_t>::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;
Expand Down Expand Up @@ -806,12 +806,12 @@ namespace egl {
}

std::optional<sws_t>
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
Expand Down
4 changes: 2 additions & 2 deletions src/platform/linux/graphics.h
Original file line number Diff line number Diff line change
Expand Up @@ -314,9 +314,9 @@ namespace egl {
class sws_t {
public:
static std::optional<sws_t>
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<sws_t>
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
Expand Down
132 changes: 125 additions & 7 deletions src/platform/linux/kmsgrab.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ namespace platf {
using crtc_t = util::safe_ptr<drmModeCrtc, drmModeFreeCrtc>;
using obj_prop_t = util::safe_ptr<drmModeObjectProperties, drmModeFreeObjectProperties>;
using prop_t = util::safe_ptr<drmModePropertyRes, drmModeFreeProperty>;
using prop_blob_t = util::safe_ptr<drmModePropertyBlobRes, drmModeFreePropertyBlob>;

using conn_type_count_t = std::map<std::uint32_t, std::uint32_t>;

Expand Down Expand Up @@ -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;
};

Expand Down Expand Up @@ -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<std::uint64_t>
prop_value_by_name(const std::vector<std::pair<prop_t, std::uint64_t>> &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;
Expand Down Expand Up @@ -392,6 +405,7 @@ namespace platf {
conn->connector_type,
crtc_id,
index,
conn->connector_id,
conn->connection == DRM_MODE_CONNECTED,
});
});
Expand All @@ -414,6 +428,9 @@ namespace platf {
std::vector<std::pair<prop_t, std::uint64_t>>
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<std::pair<prop_t, std::uint64_t>> props;
props.reserve(obj_prop->count_props);
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -944,6 +1059,9 @@ namespace platf {
int crtc_id;
int crtc_index;

std::optional<uint32_t> connector_id;
std::optional<uint64_t> hdr_metadata_blob_id;

int cursor_plane_id;
cursor_t captured_cursor {};

Expand Down
25 changes: 22 additions & 3 deletions src/platform/linux/vaapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,19 +130,20 @@ 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;
}
}

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,
Expand Down Expand Up @@ -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;
}
Expand Down

0 comments on commit 056281b

Please sign in to comment.