mirror of
https://github.com/LizardByte/Sunshine.git
synced 2025-02-19 12:41:00 +00:00
Implement HDR support for Linux KMS capture backend (#1994)
Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
This commit is contained in:
parent
3fb384f868
commit
056281b745
@ -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 <https://wiki.archlinux.org/title/HDR_monitor_support>`__ and
|
||||
|
@ -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;
|
||||
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>;
|
||||
|
||||
@ -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<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 == 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<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);
|
||||
@ -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<uint32_t> connector_id;
|
||||
std::optional<uint64_t> hdr_metadata_blob_id;
|
||||
|
||||
int cursor_plane_id;
|
||||
cursor_t captured_cursor {};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user