From 5ad5c5cbcfe2b47f1353221190d92f467979b456 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 25 Feb 2025 18:44:30 +0100 Subject: [PATCH 1/3] edid: parse cta colorimetry and static hdr metadata --- src/edid.rs | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/edid.rs b/src/edid.rs index 999b09fe..cccc2ee3 100644 --- a/src/edid.rs +++ b/src/edid.rs @@ -1035,9 +1035,59 @@ impl<'a> EdidParser<'a> { } } + fn parse_cta_colorimetry_data_block(&mut self) -> Result { + let [lo, hi] = *self.read_n::<2>()?; + Ok(CtaDataBlock::Colorimetry(CtaColorimetryDataBlock { + bt2020_rgb: lo.contains(0x80), + bt2020_ycc: lo.contains(0x40), + bt2020_cycc: lo.contains(0x20), + op_rgb: lo.contains(0x10), + op_ycc_601601: lo.contains(0x08), + s_ycc_601: lo.contains(0x04), + xv_ycc_709: lo.contains(0x02), + xv_ycc_601: lo.contains(0x01), + dci_p3: hi.contains(0x80), + })) + } + + fn parse_cta_hdr_static_metadata_data_block(&mut self) -> Result { + let et = self.read_u8()?; + let _ = self.read_u8()?; + let mut read_luminance = |min: bool| { + let v = self.read_u8().unwrap_or_default(); + if v == 0 { + None + } else if min { + Some((v as f64 / 255.0).powi(2) / 100.0) + } else { + Some(50.0 * 2.0f64.powf(v as f64 / 32.0)) + } + }; + Ok(CtaDataBlock::StaticHdrMetadata( + CtaStaticHdrMetadataDataBlock { + traditional_gamma_sdr_luminance: et.contains(0x01), + traditional_gamma_hdr_luminance: et.contains(0x02), + smpte_st_2084: et.contains(0x04), + hlg: et.contains(0x08), + max_luminance: read_luminance(false), + max_frame_average_luminance: read_luminance(false), + min_luminance: read_luminance(true), + }, + )) + } + + fn parse_cta_extended_data_block(&mut self) -> Result { + match self.read_u8()? { + 0x5 => self.parse_cta_colorimetry_data_block(), + 0x6 => self.parse_cta_hdr_static_metadata_data_block(), + _ => Ok(CtaDataBlock::Unknown), + } + } + fn parse_cta_data_block(&mut self, tag: u8) -> Result { match tag { 0x3 => self.parse_cta_vendor_data_block(), + 0x7 => self.parse_cta_extended_data_block(), _ => Ok(CtaDataBlock::Unknown), } } @@ -1173,6 +1223,8 @@ pub struct CtaExtensionV3 { pub enum CtaDataBlock { Unknown, VendorAmd(CtaAmdVendorDataBlock), + Colorimetry(#[expect(dead_code)] CtaColorimetryDataBlock), + StaticHdrMetadata(#[expect(dead_code)] CtaStaticHdrMetadataDataBlock), } #[derive(Debug)] @@ -1182,6 +1234,32 @@ pub struct CtaAmdVendorDataBlock { pub maximum_refresh_hz: u8, } +#[derive(Copy, Clone, Debug)] +#[expect(dead_code)] +pub struct CtaColorimetryDataBlock { + pub bt2020_rgb: bool, + pub bt2020_ycc: bool, + pub bt2020_cycc: bool, + pub op_rgb: bool, + pub op_ycc_601601: bool, + pub s_ycc_601: bool, + pub xv_ycc_709: bool, + pub xv_ycc_601: bool, + pub dci_p3: bool, +} + +#[derive(Copy, Clone, Debug)] +#[expect(dead_code)] +pub struct CtaStaticHdrMetadataDataBlock { + pub traditional_gamma_sdr_luminance: bool, + pub traditional_gamma_hdr_luminance: bool, + pub smpte_st_2084: bool, + pub hlg: bool, + pub max_luminance: Option, + pub max_frame_average_luminance: Option, + pub min_luminance: Option, +} + #[derive(Debug)] pub struct EdidFile { pub base_block: EdidBaseBlock, From 04f280aabe79168368b2d1f6856d09e1cdd3a9d4 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 11 Mar 2025 11:41:42 +0100 Subject: [PATCH 2/3] color-management-v1: implement target color volume --- src/backends/metal/present.rs | 4 +- src/cmm/cmm_description.rs | 29 ++++- src/cmm/cmm_luminance.rs | 15 +++ src/cmm/cmm_manager.rs | 40 +++++- src/cmm/cmm_tests.rs | 13 +- src/gfx_apis/vulkan/renderer.rs | 2 +- .../color_management/wp_color_manager_v1.rs | 7 + .../wp_image_description_creator_params_v1.rs | 79 ++++++++++-- .../wp_image_description_info_v1.rs | 122 ++++++++++++------ .../wp_image_description_v1.rs | 2 +- 10 files changed, 249 insertions(+), 64 deletions(-) diff --git a/src/backends/metal/present.rs b/src/backends/metal/present.rs index 3af4f794..56fc7e3f 100644 --- a/src/backends/metal/present.rs +++ b/src/backends/metal/present.rs @@ -560,8 +560,8 @@ impl MetalConnector { } return None; }; - if ct.cd.id != self.state.color_manager.srgb_srgb().id { - // Direct scanout requires identical color descriptions. + if !ct.cd.embeds_into(self.state.color_manager.srgb_srgb()) { + // Direct scanout requires embeddable color descriptions. return None; } if ct.alpha.is_some() { diff --git a/src/cmm/cmm_description.rs b/src/cmm/cmm_description.rs index eef9a923..642d271c 100644 --- a/src/cmm/cmm_description.rs +++ b/src/cmm/cmm_description.rs @@ -1,13 +1,13 @@ use { crate::{ cmm::{ - cmm_luminance::{Luminance, white_balance}, + cmm_luminance::{Luminance, TargetLuminance, white_balance}, cmm_manager::Shared, cmm_primaries::{NamedPrimaries, Primaries}, cmm_transfer_function::TransferFunction, cmm_transform::{ColorMatrix, Local, Xyz, bradford_adjustment}, }, - utils::free_list::FreeList, + utils::{free_list::FreeList, ordered_float::F64}, }, std::rc::Rc, }; @@ -38,6 +38,10 @@ pub struct LinearColorDescription { pub xyz_from_local: ColorMatrix, pub local_from_xyz: ColorMatrix, pub luminance: Luminance, + pub target_primaries: Primaries, + pub target_luminance: TargetLuminance, + pub max_cll: Option, + pub max_fall: Option, pub(super) shared: Rc, } @@ -45,7 +49,6 @@ pub struct LinearColorDescription { pub struct ColorDescription { pub id: ColorDescriptionId, pub linear: Rc, - #[expect(dead_code)] pub named_primaries: Option, pub transfer_function: TransferFunction, pub(super) shared: Rc, @@ -62,6 +65,26 @@ impl LinearColorDescription { } mat * self.xyz_from_local } + + pub fn embeds_into(&self, target: &Self) -> bool { + if self.id == target.id { + return true; + } + if self.primaries != target.primaries { + return false; + } + if self.luminance != target.luminance { + return false; + } + true + } +} + +impl ColorDescription { + pub fn embeds_into(&self, target: &Self) -> bool { + self.transfer_function == target.transfer_function + && self.linear.embeds_into(&target.linear) + } } impl Drop for LinearColorDescription { diff --git a/src/cmm/cmm_luminance.rs b/src/cmm/cmm_luminance.rs index 760f50f5..3cbb31ee 100644 --- a/src/cmm/cmm_luminance.rs +++ b/src/cmm/cmm_luminance.rs @@ -10,6 +10,12 @@ pub struct Luminance { pub white: F64, } +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct TargetLuminance { + pub min: F64, + pub max: F64, +} + impl Luminance { pub const SRGB: Self = Self { min: F64(0.2), @@ -46,6 +52,15 @@ impl Luminance { }; } +impl Luminance { + pub fn to_target(&self) -> TargetLuminance { + TargetLuminance { + min: self.min, + max: self.max, + } + } +} + impl Default for Luminance { fn default() -> Self { Self::SRGB diff --git a/src/cmm/cmm_manager.rs b/src/cmm/cmm_manager.rs index 6837fadc..02658e40 100644 --- a/src/cmm/cmm_manager.rs +++ b/src/cmm/cmm_manager.rs @@ -5,11 +5,11 @@ use { ColorDescription, ColorDescriptionIds, LinearColorDescription, LinearColorDescriptionId, LinearColorDescriptionIds, }, - cmm_luminance::Luminance, + cmm_luminance::{Luminance, TargetLuminance}, cmm_primaries::{NamedPrimaries, Primaries}, cmm_transfer_function::TransferFunction, }, - utils::{copyhashmap::CopyHashMap, numcell::NumCell}, + utils::{copyhashmap::CopyHashMap, numcell::NumCell, ordered_float::F64}, }, std::rc::{Rc, Weak}, }; @@ -35,6 +35,10 @@ pub(super) struct Shared { struct LinearDescriptionKey { primaries: Primaries, luminance: Luminance, + target_primaries: Primaries, + target_luminance: TargetLuminance, + max_cll: Option, + max_fall: Option, } #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] @@ -60,6 +64,10 @@ impl ColorManager { Primaries::SRGB, Luminance::SRGB, TransferFunction::Srgb, + Primaries::SRGB, + Luminance::SRGB.to_target(), + None, + None, ); let srgb_linear = get_description( &shared, @@ -70,6 +78,10 @@ impl ColorManager { Primaries::SRGB, Luminance::SRGB, TransferFunction::Linear, + Primaries::SRGB, + Luminance::SRGB.to_target(), + None, + None, ); let windows_scrgb = get_description( &shared, @@ -80,6 +92,10 @@ impl ColorManager { Primaries::SRGB, Luminance::WINDOWS_SCRGB, TransferFunction::Linear, + Primaries::BT2020, + Luminance::ST2084_PQ.to_target(), + None, + None, ); Rc::new(Self { linear_ids, @@ -110,6 +126,10 @@ impl ColorManager { primaries: Primaries, luminance: Luminance, transfer_function: TransferFunction, + target_primaries: Primaries, + target_luminance: TargetLuminance, + max_cll: Option, + max_fall: Option, ) -> Rc { get_description( &self.shared, @@ -120,6 +140,10 @@ impl ColorManager { primaries, luminance, transfer_function, + target_primaries, + target_luminance, + max_cll, + max_fall, ) } } @@ -133,6 +157,10 @@ fn get_description( primaries: Primaries, luminance: Luminance, transfer_function: TransferFunction, + target_primaries: Primaries, + target_luminance: TargetLuminance, + max_cll: Option, + max_fall: Option, ) -> Rc { macro_rules! gc { ($d:ident, $i:expr) => { @@ -147,6 +175,10 @@ fn get_description( let key = LinearDescriptionKey { primaries, luminance, + target_primaries, + target_luminance, + max_cll, + max_fall, }; if let Some(d) = linear_descriptions.get(&key) { if let Some(d) = d.upgrade() { @@ -180,6 +212,10 @@ fn get_description( xyz_from_local, local_from_xyz, luminance, + target_primaries, + target_luminance, + max_cll, + max_fall, shared: shared.clone(), }); linear_descriptions.set(key, Rc::downgrade(&d)); diff --git a/src/cmm/cmm_tests.rs b/src/cmm/cmm_tests.rs index 52b98d72..5bc11101 100644 --- a/src/cmm/cmm_tests.rs +++ b/src/cmm/cmm_tests.rs @@ -141,7 +141,18 @@ mod transforms { fn check(p1: Primaries, p2: Primaries, expected: [[f64; 4]; 3]) { let manager = ColorManager::new(); - let d = |p| manager.get_description(None, p, Luminance::SRGB, TransferFunction::Linear); + let d = |p| { + manager.get_description( + None, + p, + Luminance::SRGB, + TransferFunction::Linear, + p, + Luminance::SRGB.to_target(), + None, + None, + ) + }; let d1 = d(p1); let d2 = d(p2); let m = d1.linear.color_transform(&d2.linear); diff --git a/src/gfx_apis/vulkan/renderer.rs b/src/gfx_apis/vulkan/renderer.rs index efb31306..f3da7e78 100644 --- a/src/gfx_apis/vulkan/renderer.rs +++ b/src/gfx_apis/vulkan/renderer.rs @@ -2138,7 +2138,7 @@ impl ColorTransforms { src: &LinearColorDescription, dst: &ColorDescription, ) -> Option<&mut ColorTransform> { - if src.id == dst.linear.id { + if src.embeds_into(&dst.linear) { return None; } let ct = match self.map.entry([src.id, dst.linear.id]) { diff --git a/src/ifs/color_management/wp_color_manager_v1.rs b/src/ifs/color_management/wp_color_manager_v1.rs index 05546d97..5dcc4ebf 100644 --- a/src/ifs/color_management/wp_color_manager_v1.rs +++ b/src/ifs/color_management/wp_color_manager_v1.rs @@ -4,6 +4,7 @@ use { globals::{Global, GlobalName}, ifs::{ color_management::{ + FEATURE_EXTENDED_TARGET_VOLUME, FEATURE_SET_MASTERING_DISPLAY_PRIMARIES, consts::{ FEATURE_PARAMETRIC, FEATURE_SET_LUMINANCES, FEATURE_SET_PRIMARIES, FEATURE_WINDOWS_SCRGB, PRIMARIES_ADOBE_RGB, PRIMARIES_BT2020, @@ -77,6 +78,8 @@ impl WpColorManagerV1 { self.send_supported_feature(FEATURE_PARAMETRIC); self.send_supported_feature(FEATURE_SET_PRIMARIES); self.send_supported_feature(FEATURE_SET_LUMINANCES); + self.send_supported_feature(FEATURE_SET_MASTERING_DISPLAY_PRIMARIES); + self.send_supported_feature(FEATURE_EXTENDED_TARGET_VOLUME); self.send_supported_feature(FEATURE_WINDOWS_SCRGB); self.send_supported_tf_named(TRANSFER_FUNCTION_BT1886); self.send_supported_tf_named(TRANSFER_FUNCTION_GAMMA22); @@ -209,6 +212,10 @@ impl WpColorManagerV1RequestHandler for WpColorManagerV1 { tf: Default::default(), primaries: Default::default(), luminance: Default::default(), + mastering_primaries: Default::default(), + mastering_luminance: Default::default(), + max_cll: Default::default(), + max_fall: Default::default(), }); track!(self.client, obj); self.client.add_client_obj(&obj)?; diff --git a/src/ifs/color_management/wp_image_description_creator_params_v1.rs b/src/ifs/color_management/wp_image_description_creator_params_v1.rs index a4d7dc2f..15de1e84 100644 --- a/src/ifs/color_management/wp_image_description_creator_params_v1.rs +++ b/src/ifs/color_management/wp_image_description_creator_params_v1.rs @@ -2,7 +2,7 @@ use { crate::{ client::{Client, ClientError}, cmm::{ - cmm_luminance::Luminance, + cmm_luminance::{Luminance, TargetLuminance}, cmm_primaries::{NamedPrimaries, Primaries}, cmm_transfer_function::TransferFunction, }, @@ -43,6 +43,10 @@ pub struct WpImageDescriptionCreatorParamsV1 { pub tf: Cell>, pub primaries: Cell, Primaries)>>, pub luminance: Cell>, + pub mastering_primaries: Cell>, + pub mastering_luminance: Cell>, + pub max_cll: Cell>, + pub max_fall: Cell>, } impl WpImageDescriptionCreatorParamsV1RequestHandler for WpImageDescriptionCreatorParamsV1 { @@ -67,11 +71,20 @@ impl WpImageDescriptionCreatorParamsV1RequestHandler for WpImageDescriptionCreat if luminance.max.0 <= luminance.min.0 || luminance.white.0 <= luminance.min.0 { return Err(WpImageDescriptionCreatorParamsV1Error::MinLuminanceTooLow); } + let target_primaries = self.mastering_primaries.get().unwrap_or(primaries); + let target_luminance = self + .mastering_luminance + .get() + .unwrap_or(luminance.to_target()); let description = self.client.state.color_manager.get_description( named_primaries, primaries, luminance, transfer_function, + target_primaries, + target_luminance, + self.max_cll.get(), + self.max_fall.get(), ); let obj = Rc::new(WpImageDescriptionV1 { id: req.image_description, @@ -174,25 +187,59 @@ impl WpImageDescriptionCreatorParamsV1RequestHandler for WpImageDescriptionCreat fn set_mastering_display_primaries( &self, - _req: SetMasteringDisplayPrimaries, + req: SetMasteringDisplayPrimaries, _slf: &Rc, ) -> Result<(), Self::Error> { - Err(WpImageDescriptionCreatorParamsV1Error::SetMasteringDisplayPrimariesNotSupported) + let map = |n: i32| F64(n as f64 * PRIMARIES_MUL_INV); + let primaries = Primaries { + r: (map(req.r_x), map(req.r_y)), + g: (map(req.g_x), map(req.g_y)), + b: (map(req.b_x), map(req.b_y)), + wp: (map(req.w_x), map(req.w_y)), + }; + if self.mastering_primaries.replace(Some(primaries)).is_some() { + return Err(WpImageDescriptionCreatorParamsV1Error::MasteringPrimariesAlreadySet); + } + Ok(()) } fn set_mastering_luminance( &self, - _req: SetMasteringLuminance, + req: SetMasteringLuminance, _slf: &Rc, ) -> Result<(), Self::Error> { - Err(WpImageDescriptionCreatorParamsV1Error::SetMasteringLuminanceNotSupported) - } - - fn set_max_cll(&self, _req: SetMaxCll, _slf: &Rc) -> Result<(), Self::Error> { + let luminance = TargetLuminance { + min: F64(req.min_lum as f64 * MIN_LUM_MUL_INV), + max: F64(req.max_lum as f64), + }; + if luminance.max.0 <= luminance.min.0 { + return Err(WpImageDescriptionCreatorParamsV1Error::MinMasteringLuminanceTooLow); + } + if self.mastering_luminance.replace(Some(luminance)).is_some() { + return Err(WpImageDescriptionCreatorParamsV1Error::MasteringLuminancesAlreadySet); + } Ok(()) } - fn set_max_fall(&self, _req: SetMaxFall, _slf: &Rc) -> Result<(), Self::Error> { + fn set_max_cll(&self, req: SetMaxCll, _slf: &Rc) -> Result<(), Self::Error> { + if self + .max_cll + .replace(Some(F64(req.max_cll as f64))) + .is_some() + { + return Err(WpImageDescriptionCreatorParamsV1Error::MaxCllAlreadySet); + } + Ok(()) + } + + fn set_max_fall(&self, req: SetMaxFall, _slf: &Rc) -> Result<(), Self::Error> { + if self + .max_fall + .replace(Some(F64(req.max_fall as f64))) + .is_some() + { + return Err(WpImageDescriptionCreatorParamsV1Error::MaxFallAlreadySet); + } Ok(()) } } @@ -210,10 +257,6 @@ simple_add_obj!(WpImageDescriptionCreatorParamsV1); pub enum WpImageDescriptionCreatorParamsV1Error { #[error(transparent)] ClientError(Box), - #[error("set_mastering_luminance is not supported")] - SetMasteringLuminanceNotSupported, - #[error("set_mastering_display_primaries is not supported")] - SetMasteringDisplayPrimariesNotSupported, #[error("{} is not a supported named primary", .0)] UnsupportedPrimaries(u32), #[error("set_tf_power is not supported")] @@ -232,5 +275,15 @@ pub enum WpImageDescriptionCreatorParamsV1Error { TfNotSet, #[error("The primaries were not set")] PrimariesNotSet, + #[error("The mastering display primaries have already been set")] + MasteringPrimariesAlreadySet, + #[error("The mastering display luminances have already been set")] + MasteringLuminancesAlreadySet, + #[error("The minimum mastering luminance is too low")] + MinMasteringLuminanceTooLow, + #[error("The max CLL has already been set")] + MaxCllAlreadySet, + #[error("The max FALL has already been set")] + MaxFallAlreadySet, } efrom!(WpImageDescriptionCreatorParamsV1Error, ClientError); diff --git a/src/ifs/color_management/wp_image_description_info_v1.rs b/src/ifs/color_management/wp_image_description_info_v1.rs index bb23069f..352f7a90 100644 --- a/src/ifs/color_management/wp_image_description_info_v1.rs +++ b/src/ifs/color_management/wp_image_description_info_v1.rs @@ -1,9 +1,22 @@ use { crate::{ client::Client, - ifs::color_management::consts::{PRIMARIES_SRGB, TRANSFER_FUNCTION_SRGB}, + cmm::{ + cmm_description::ColorDescription, cmm_primaries::NamedPrimaries, + cmm_transfer_function::TransferFunction, + }, + ifs::color_management::{ + MIN_LUM_MUL, PRIMARIES_ADOBE_RGB, PRIMARIES_BT2020, PRIMARIES_CIE1931_XYZ, + PRIMARIES_DCI_P3, PRIMARIES_DISPLAY_P3, PRIMARIES_GENERIC_FILM, PRIMARIES_MUL, + PRIMARIES_NTSC, PRIMARIES_PAL, PRIMARIES_PAL_M, TRANSFER_FUNCTION_BT1886, + TRANSFER_FUNCTION_EXT_LINEAR, TRANSFER_FUNCTION_EXT_SRGB, TRANSFER_FUNCTION_GAMMA22, + TRANSFER_FUNCTION_GAMMA28, TRANSFER_FUNCTION_LOG_100, TRANSFER_FUNCTION_LOG_316, + TRANSFER_FUNCTION_ST240, TRANSFER_FUNCTION_ST428, TRANSFER_FUNCTION_ST2084_PQ, + consts::{PRIMARIES_SRGB, TRANSFER_FUNCTION_SRGB}, + }, leaks::Tracker, object::{Object, Version}, + utils::ordered_float::F64, wire::{WpImageDescriptionInfoV1Id, wp_image_description_info_v1::*}, }, std::{convert::Infallible, rc::Rc}, @@ -18,17 +31,46 @@ pub struct WpImageDescriptionInfoV1 { } impl WpImageDescriptionInfoV1 { - pub fn send_srgb(&self) { - let red = [0.64, 0.33]; - let green = [0.3, 0.6]; - let blue = [0.15, 0.06]; - let white = [0.3127, 0.3290]; - self.send_primaries(red, green, blue, white); - self.send_primaries_named(PRIMARIES_SRGB); - self.send_tf_named(TRANSFER_FUNCTION_SRGB); - self.send_luminances(0.2, 80.0, 80.0); - self.send_target_primaries(red, green, blue, white); - self.send_target_luminances(0.2, 80.0); + pub fn send_description(&self, d: &ColorDescription) { + let tf = match d.transfer_function { + TransferFunction::Srgb => TRANSFER_FUNCTION_SRGB, + TransferFunction::Linear => TRANSFER_FUNCTION_EXT_LINEAR, + TransferFunction::St2084Pq => TRANSFER_FUNCTION_ST2084_PQ, + TransferFunction::Bt1886 => TRANSFER_FUNCTION_BT1886, + TransferFunction::Gamma22 => TRANSFER_FUNCTION_GAMMA22, + TransferFunction::Gamma28 => TRANSFER_FUNCTION_GAMMA28, + TransferFunction::St240 => TRANSFER_FUNCTION_ST240, + TransferFunction::ExtSrgb => TRANSFER_FUNCTION_EXT_SRGB, + TransferFunction::Log100 => TRANSFER_FUNCTION_LOG_100, + TransferFunction::Log316 => TRANSFER_FUNCTION_LOG_316, + TransferFunction::St428 => TRANSFER_FUNCTION_ST428, + }; + self.send_primaries(&d.linear.primaries); + if let Some(n) = d.named_primaries { + let n = match n { + NamedPrimaries::Srgb => PRIMARIES_SRGB, + NamedPrimaries::PalM => PRIMARIES_PAL_M, + NamedPrimaries::Pal => PRIMARIES_PAL, + NamedPrimaries::Ntsc => PRIMARIES_NTSC, + NamedPrimaries::GenericFilm => PRIMARIES_GENERIC_FILM, + NamedPrimaries::Bt2020 => PRIMARIES_BT2020, + NamedPrimaries::Cie1931Xyz => PRIMARIES_CIE1931_XYZ, + NamedPrimaries::DciP3 => PRIMARIES_DCI_P3, + NamedPrimaries::DisplayP3 => PRIMARIES_DISPLAY_P3, + NamedPrimaries::AdobeRgb => PRIMARIES_ADOBE_RGB, + }; + self.send_primaries_named(n); + } + self.send_tf_named(tf); + self.send_luminances(&d.linear.luminance); + self.send_target_primaries(&d.linear.target_primaries); + self.send_target_luminances(&d.linear.target_luminance); + if let Some(max_cll) = d.linear.max_cll { + self.send_target_max_cll(max_cll.0); + } + if let Some(max_fall) = d.linear.max_fall { + self.send_target_max_fall(max_fall.0); + } self.send_done(); } @@ -45,18 +87,18 @@ impl WpImageDescriptionInfoV1 { }); } - pub fn send_primaries(&self, r: [f64; 2], g: [f64; 2], b: [f64; 2], w: [f64; 2]) { - let map = |c: f64| (c * 1_000_000.0) as i32; + pub fn send_primaries(&self, p: &crate::cmm::cmm_primaries::Primaries) { + let map = |c: F64| (c.0 * PRIMARIES_MUL) as i32; self.client.event(Primaries { self_id: self.id, - r_x: map(r[0]), - r_y: map(r[1]), - g_x: map(g[0]), - g_y: map(g[1]), - b_x: map(b[0]), - b_y: map(b[1]), - w_x: map(w[0]), - w_y: map(w[1]), + r_x: map(p.r.0), + r_y: map(p.r.1), + g_x: map(p.g.0), + g_y: map(p.g.1), + b_x: map(p.b.0), + b_y: map(p.b.1), + w_x: map(p.wp.0), + w_y: map(p.wp.1), }); } @@ -82,39 +124,38 @@ impl WpImageDescriptionInfoV1 { }); } - pub fn send_luminances(&self, min_lum: f64, max_lum: f64, reference_lum: f64) { + pub fn send_luminances(&self, l: &crate::cmm::cmm_luminance::Luminance) { self.client.event(Luminances { self_id: self.id, - min_lum: (min_lum * 10_000.0) as u32, - max_lum: max_lum as _, - reference_lum: reference_lum as _, + min_lum: (l.min.0 * MIN_LUM_MUL) as u32, + max_lum: l.max.0 as _, + reference_lum: l.white.0 as _, }); } - pub fn send_target_primaries(&self, r: [f64; 2], g: [f64; 2], b: [f64; 2], w: [f64; 2]) { - let map = |c: f64| (c * 1_000_000.0) as i32; + pub fn send_target_primaries(&self, p: &crate::cmm::cmm_primaries::Primaries) { + let map = |c: F64| (c.0 * PRIMARIES_MUL) as i32; self.client.event(TargetPrimaries { self_id: self.id, - r_x: map(r[0]), - r_y: map(r[1]), - g_x: map(g[0]), - g_y: map(g[1]), - b_x: map(b[0]), - b_y: map(b[1]), - w_x: map(w[0]), - w_y: map(w[1]), + r_x: map(p.r.0), + r_y: map(p.r.1), + g_x: map(p.g.0), + g_y: map(p.g.1), + b_x: map(p.b.0), + b_y: map(p.b.1), + w_x: map(p.wp.0), + w_y: map(p.wp.1), }); } - pub fn send_target_luminances(&self, min_lum: f64, max_lum: f64) { + pub fn send_target_luminances(&self, l: &crate::cmm::cmm_luminance::TargetLuminance) { self.client.event(TargetLuminance { self_id: self.id, - min_lum: (min_lum * 10_000.0) as u32, - max_lum: max_lum as _, + min_lum: (l.min.0 * MIN_LUM_MUL) as u32, + max_lum: l.max.0 as _, }); } - #[expect(dead_code)] pub fn send_target_max_cll(&self, max_cll: f64) { self.client.event(TargetMaxCll { self_id: self.id, @@ -122,7 +163,6 @@ impl WpImageDescriptionInfoV1 { }); } - #[expect(dead_code)] pub fn send_target_max_fall(&self, max_fall: f64) { self.client.event(TargetMaxFall { self_id: self.id, diff --git a/src/ifs/color_management/wp_image_description_v1.rs b/src/ifs/color_management/wp_image_description_v1.rs index 00ec4d02..f72a84a7 100644 --- a/src/ifs/color_management/wp_image_description_v1.rs +++ b/src/ifs/color_management/wp_image_description_v1.rs @@ -54,7 +54,7 @@ impl WpImageDescriptionV1RequestHandler for WpImageDescriptionV1 { }); self.client.add_client_obj(&obj)?; track!(self.client, obj); - obj.send_srgb(); + obj.send_description(self.client.state.color_manager.srgb_srgb()); self.client.remove_obj(&*obj)?; Ok(()) } From bb56efb96868afd49487d292f0f9aa71eedbbab1 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 11 Mar 2025 14:54:35 +0100 Subject: [PATCH 3/3] metal: allow configuring color space and transfer function --- jay-config/src/_private/client.rs | 16 +- jay-config/src/_private/ipc.rs | 9 +- jay-config/src/video.rs | 35 ++ release-notes.md | 1 + src/backend.rs | 72 +++- src/backends/metal/present.rs | 54 ++- src/backends/metal/video.rs | 392 +++++++++++++++--- src/backends/x.rs | 15 +- src/cli/randr.rs | 138 +++++- src/cmm/cmm_description.rs | 6 + src/cmm/cmm_manager.rs | 77 ++-- src/compositor.rs | 10 +- src/config/handler.rs | 39 +- src/cursor_user.rs | 3 +- src/edid.rs | 5 +- .../wp_color_management_output_v1.rs | 30 +- ...wp_color_management_surface_feedback_v1.rs | 16 +- .../color_management/wp_color_manager_v1.rs | 14 +- .../wp_image_description_creator_params_v1.rs | 2 +- .../wp_image_description_v1.rs | 12 +- src/ifs/jay_compositor.rs | 2 +- src/ifs/jay_randr.rs | 58 ++- src/ifs/wl_output.rs | 86 +++- src/ifs/wl_surface.rs | 39 +- .../wp_color_management_surface_v1.rs | 8 +- src/it/test_backend.rs | 15 +- src/it/tests/t0034_workspace_restoration.rs | 12 +- src/tasks/connector.rs | 9 + src/tools/tool_client.rs | 2 +- src/tree/output.rs | 24 +- src/video/drm.rs | 131 +++++- toml-config/src/config.rs | 4 +- toml-config/src/config/parsers/output.rs | 48 ++- toml-config/src/lib.rs | 13 +- toml-spec/spec/spec.generated.json | 24 ++ toml-spec/spec/spec.generated.md | 50 +++ toml-spec/spec/spec.yaml | 32 ++ wire/jay_randr.txt | 22 + 38 files changed, 1365 insertions(+), 160 deletions(-) diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index d60f2864..668af129 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -25,7 +25,8 @@ use { theme::{Color, colors::Colorable, sized::Resizable}, timer::Timer, video::{ - Connector, DrmDevice, Format, GfxApi, Mode, TearingMode, Transform, VrrMode, + ColorSpace, Connector, DrmDevice, Format, GfxApi, Mode, TearingMode, TransferFunction, + Transform, VrrMode, connector_type::{CON_UNKNOWN, ConnectorType}, }, xwayland::XScalingMode, @@ -781,6 +782,19 @@ impl Client { self.send(&ClientMessage::ConnectorSetFormat { connector, format }); } + pub fn connector_set_colors( + &self, + connector: Connector, + color_space: ColorSpace, + transfer_function: TransferFunction, + ) { + self.send(&ClientMessage::ConnectorSetColors { + connector, + color_space, + transfer_function, + }); + } + pub fn connector_get_scale(&self, connector: Connector) -> f64 { let res = self.send_with_response(&ClientMessage::ConnectorGetScale { connector }); get_response!(res, 1.0, ConnectorGetScale { scale }); diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 6b3e5116..cc94ebd1 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -11,8 +11,8 @@ use { theme::{Color, colors::Colorable, sized::Resizable}, timer::Timer, video::{ - Connector, DrmDevice, Format, GfxApi, TearingMode, Transform, VrrMode, - connector_type::ConnectorType, + ColorSpace, Connector, DrmDevice, Format, GfxApi, TearingMode, TransferFunction, + Transform, VrrMode, connector_type::ConnectorType, }, xwayland::XScalingMode, }, @@ -533,6 +533,11 @@ pub enum ClientMessage<'a> { SetColorManagementEnabled { enabled: bool, }, + ConnectorSetColors { + connector: Connector, + color_space: ColorSpace, + transfer_function: TransferFunction, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/video.rs b/jay-config/src/video.rs index 3aba3d1e..1f9df481 100644 --- a/jay-config/src/video.rs +++ b/jay-config/src/video.rs @@ -272,6 +272,19 @@ impl Connector { pub fn set_format(self, format: Format) { get!().connector_set_format(self, format); } + + /// Sets the color space and transfer function of the connector. + /// + /// By default, the default values are used which usually means sRGB color space with + /// sRGB transfer function. + /// + /// If the output supports it, HDR10 can be enabled by setting the color space to + /// BT.2020 and the transfer function to PQ. + /// + /// Note that some displays might ignore incompatible settings. + pub fn set_colors(self, color_space: ColorSpace, transfer_function: TransferFunction) { + get!().connector_set_colors(self, color_space, transfer_function); + } } /// Returns all available DRM devices. @@ -662,3 +675,25 @@ impl Format { pub const ABGR16161616F: Self = Self(26); pub const XBGR16161616F: Self = Self(27); } + +/// A color space. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct ColorSpace(pub u32); + +impl ColorSpace { + /// The default color space (usually sRGB). + pub const DEFAULT: Self = Self(0); + /// The BT.2020 color space. + pub const BT2020: Self = Self(1); +} + +/// A transfer function. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct TransferFunction(pub u32); + +impl TransferFunction { + /// The default transfer function (usually sRGB). + pub const DEFAULT: Self = Self(0); + /// The PQ transfer function. + pub const PQ: Self = Self(1); +} diff --git a/release-notes.md b/release-notes.md index 36786759..a7a470ed 100644 --- a/release-notes.md +++ b/release-notes.md @@ -11,6 +11,7 @@ observable. This should have no impact on performance in the common case. - Implement color-management-v1. - Implement cursor-shape-v1 version 2. +- Outputs can now optionally use the BT.2020/PQ color space. # 1.9.1 (2025-02-13) diff --git a/src/backend.rs b/src/backend.rs index ea1240d1..227114ee 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -1,6 +1,7 @@ use { crate::{ async_engine::SpawnedFuture, + cmm::cmm_primaries::Primaries, drm_feedback::DrmFeedback, fixed::Fixed, format::Format, @@ -17,9 +18,14 @@ use { }, }, libinput::consts::DeviceCapability, - video::drm::{ConnectorType, DrmConnector, DrmError, DrmVersion}, + video::drm::{ + ConnectorType, DRM_MODE_COLORIMETRY_BT2020_RGB, DRM_MODE_COLORIMETRY_DEFAULT, + DrmConnector, DrmError, DrmVersion, HDMI_EOTF_SMPTE_ST2084, + HDMI_EOTF_TRADITIONAL_GAMMA_SDR, + }, }, jay_config::{input::SwitchEvent, video::GfxApi}, + linearize::Linearize, std::{ any::Any, error::Error, @@ -83,6 +89,12 @@ pub struct MonitorInfo { pub height_mm: i32, pub non_desktop: bool, pub vrr_capable: bool, + pub transfer_functions: Vec, + pub transfer_function: BackendTransferFunction, + pub color_spaces: Vec, + pub color_space: BackendColorSpace, + pub primaries: Primaries, + pub luminance: Option, } #[derive(Copy, Clone, Debug)] @@ -129,6 +141,10 @@ pub trait Connector { fn set_fb_format(&self, format: &'static Format) { let _ = format; } + fn set_colors(&self, bcs: BackendColorSpace, btf: BackendTransferFunction) { + let _ = bcs; + let _ = btf; + } } #[derive(Debug)] @@ -142,6 +158,7 @@ pub enum ConnectorEvent { Available, VrrChanged(bool), FormatsChanged(Rc>, &'static Format), + ColorsChanged(BackendColorSpace, BackendTransferFunction), } pub trait HardwareCursorUpdate { @@ -477,3 +494,56 @@ pub trait BackendDrmLease { pub trait BackendDrmLessee { fn created(&self, lease: Rc); } + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Linearize)] +pub enum BackendTransferFunction { + #[default] + Default, + Pq, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Linearize)] +pub enum BackendColorSpace { + #[default] + Default, + Bt2020, +} + +#[derive(Copy, Clone, Debug)] +pub struct BackendLuminance { + pub min: f64, + pub max: f64, + pub max_fall: f64, +} + +impl BackendTransferFunction { + pub fn to_drm(self) -> u8 { + match self { + BackendTransferFunction::Default => HDMI_EOTF_TRADITIONAL_GAMMA_SDR, + BackendTransferFunction::Pq => HDMI_EOTF_SMPTE_ST2084, + } + } + + pub const fn name(self) -> &'static str { + match self { + BackendTransferFunction::Default => "default", + BackendTransferFunction::Pq => "pq", + } + } +} + +impl BackendColorSpace { + pub fn to_drm(self) -> u64 { + match self { + BackendColorSpace::Default => DRM_MODE_COLORIMETRY_DEFAULT, + BackendColorSpace::Bt2020 => DRM_MODE_COLORIMETRY_BT2020_RGB, + } + } + + pub const fn name(self) -> &'static str { + match self { + BackendColorSpace::Default => "default", + BackendColorSpace::Bt2020 => "bt2020", + } + } +} diff --git a/src/backends/metal/present.rs b/src/backends/metal/present.rs index 56fc7e3f..3cfaeba5 100644 --- a/src/backends/metal/present.rs +++ b/src/backends/metal/present.rs @@ -7,6 +7,7 @@ use { MetalConnector, MetalCrtc, MetalHardwareCursorChange, MetalPlane, RenderBuffer, }, }, + cmm::cmm_description::ColorDescription, gfx_api::{ AcquireSync, BufferResv, GfxApiOpt, GfxRenderPass, GfxTexture, ReleaseSync, SyncFile, create_render_pass, @@ -176,10 +177,13 @@ impl MetalConnector { }; let buffer = &buffers[self.next_buffer.get() % buffers.len()]; + let cd = node.global.color_description.get(); + let linear_cd = node.global.linear_color_description.get(); + if self.has_damage.get() > 0 || self.cursor_damage.get() { node.schedule.commit_cursor(); } - self.latch_cursor(&node)?; + self.latch_cursor(&node, &cd)?; let cursor_programming = self.compute_cursor_programming(); let latched = self.latch(&node, buffer); node.latched(self.try_async_flip()); @@ -191,11 +195,11 @@ impl MetalConnector { let mut present_fb = None; let mut direct_scanout_id = None; if let Some(latched) = &latched { - let fb = self.prepare_present_fb(buffer, &plane, latched, true)?; + let fb = self.prepare_present_fb(&cd, &linear_cd, buffer, &plane, latched, true)?; direct_scanout_id = fb.direct_scanout_data.as_ref().map(|d| d.dma_buf_id); present_fb = Some(fb); } - self.perform_screencopies(&present_fb, &node); + self.perform_screencopies(&present_fb, &node, &cd); if let Some(sync_file) = self.cursor_sync_file.take() { if let Err(e) = self.state.ring.readable(&sync_file).await { log::error!( @@ -214,8 +218,14 @@ impl MetalConnector { ); if res.is_err() { if let Some(dsd_id) = direct_scanout_id { - let fb = - self.prepare_present_fb(buffer, &plane, latched.as_ref().unwrap(), false)?; + let fb = self.prepare_present_fb( + &cd, + &linear_cd, + buffer, + &plane, + latched.as_ref().unwrap(), + false, + )?; present_fb = Some(fb); self.await_present_fb(present_fb.as_mut()).await; res = self.program_connector( @@ -432,7 +442,11 @@ impl MetalConnector { res.map_err(MetalError::Commit) } - fn latch_cursor(&self, node: &Rc) -> Result<(), MetalError> { + fn latch_cursor( + &self, + node: &Rc, + cd: &Rc, + ) -> Result<(), MetalError> { if !self.cursor_damage.take() { return Ok(()); } @@ -451,9 +465,7 @@ impl MetalConnector { }; self.state.present_hardware_cursor(node, &mut c); if c.cursor_swap_buffer { - c.sync_file = c - .cursor_buffer - .copy_to_dev(&self.state.color_manager, c.sync_file)?; + c.sync_file = c.cursor_buffer.copy_to_dev(cd, c.sync_file)?; } self.cursor_swap_buffer.set(c.cursor_swap_buffer); if c.sync_file.is_some() { @@ -544,6 +556,7 @@ impl MetalConnector { &self, pass: &GfxRenderPass, plane: &Rc, + cd: &Rc, ) -> Option { let ct = 'ct: { let mut ops = pass.ops.iter().rev(); @@ -560,7 +573,7 @@ impl MetalConnector { } return None; }; - if !ct.cd.embeds_into(self.state.color_manager.srgb_srgb()) { + if !ct.cd.embeds_into(cd) { // Direct scanout requires embeddable color descriptions. return None; } @@ -717,6 +730,8 @@ impl MetalConnector { fn prepare_present_fb( &self, + cd: &Rc, + linear_cd: &Rc, buffer: &RenderBuffer, plane: &Rc, latched: &Latched, @@ -733,7 +748,7 @@ impl MetalConnector { && self.dev.is_render_device(); let mut direct_scanout_data = None; if try_direct_scanout { - direct_scanout_data = self.prepare_direct_scanout(&latched.pass, plane); + direct_scanout_data = self.prepare_direct_scanout(&latched.pass, plane, cd); } let direct_scanout_active = direct_scanout_data.is_some(); if self.direct_scanout_active.replace(direct_scanout_active) != direct_scanout_active { @@ -753,14 +768,14 @@ impl MetalConnector { .perform_render_pass( AcquireSync::Unnecessary, ReleaseSync::Explicit, - self.state.color_manager.srgb_srgb(), + cd, &latched.pass, &latched.damage, buffer.blend_buffer.as_ref(), - self.state.color_manager.srgb_linear(), + linear_cd, ) .map_err(MetalError::RenderFrame)?; - sync_file = buffer.copy_to_dev(&self.state.color_manager, sf)?; + sync_file = buffer.copy_to_dev(cd, sf)?; fb = buffer.drm.clone(); tex = buffer.render_tex.clone(); } @@ -783,7 +798,12 @@ impl MetalConnector { }) } - fn perform_screencopies(&self, new_fb: &Option, output: &OutputNode) { + fn perform_screencopies( + &self, + new_fb: &Option, + output: &OutputNode, + cd: &Rc, + ) { let active_fb; let fb = match &new_fb { Some(f) => f, @@ -800,7 +820,7 @@ impl MetalConnector { None => { output.perform_screencopies( &fb.tex, - self.state.color_manager.srgb_srgb(), + cd, None, &AcquireSync::Unnecessary, ReleaseSync::None, @@ -813,7 +833,7 @@ impl MetalConnector { Some(dsd) => { output.perform_screencopies( &dsd.tex, - self.state.color_manager.srgb_srgb(), + cd, dsd.resv.as_ref(), &dsd.acquire_sync, dsd.release_sync, diff --git a/src/backends/metal/video.rs b/src/backends/metal/video.rs index 0897f8cb..6952061d 100644 --- a/src/backends/metal/video.rs +++ b/src/backends/metal/video.rs @@ -3,9 +3,10 @@ use { allocator::BufferObject, async_engine::{Phase, SpawnedFuture}, backend::{ - BackendDrmDevice, BackendDrmLease, BackendDrmLessee, BackendEvent, Connector, - ConnectorEvent, ConnectorId, ConnectorKernelId, DrmDeviceId, HardwareCursor, - HardwareCursorUpdate, Mode, MonitorInfo, + BackendColorSpace, BackendDrmDevice, BackendDrmLease, BackendDrmLessee, BackendEvent, + BackendLuminance, BackendTransferFunction, Connector, ConnectorEvent, ConnectorId, + ConnectorKernelId, DrmDeviceId, HardwareCursor, HardwareCursorUpdate, Mode, + MonitorInfo, }, backends::metal::{ MetalBackend, MetalError, @@ -14,7 +15,7 @@ use { POST_COMMIT_MARGIN_DELTA, PresentFb, }, }, - cmm::cmm_manager::ColorManager, + cmm::{cmm_description::ColorDescription, cmm_primaries::Primaries}, drm_feedback::DrmFeedback, edid::{CtaDataBlock, Descriptor, EdidExtension}, format::{ARGB8888, Format, XRGB8888}, @@ -33,7 +34,8 @@ use { utils::{ asyncevent::AsyncEvent, bitflags::BitflagsExt, cell_ext::CellExt, clonecell::CloneCell, copyhashmap::CopyHashMap, errorfmt::ErrorFmt, geometric_decay::GeometricDecay, - numcell::NumCell, on_change::OnChange, opaque_cell::OpaqueCell, oserror::OsError, + numcell::NumCell, on_change::OnChange, opaque_cell::OpaqueCell, ordered_float::F64, + oserror::OsError, }, video::{ INVALID_MODIFIER, Modifier, @@ -43,7 +45,7 @@ use { DRM_MODE_ATOMIC_ALLOW_MODESET, DrmBlob, DrmConnector, DrmCrtc, DrmEncoder, DrmError, DrmEvent, DrmFramebuffer, DrmLease, DrmMaster, DrmModeInfo, DrmObject, DrmPlane, DrmProperty, DrmPropertyDefinition, DrmPropertyType, DrmVersion, - PropBlob, drm_mode_modeinfo, + PropBlob, drm_mode_modeinfo, hdr_output_metadata, }, gbm::{GBM_BO_USE_LINEAR, GBM_BO_USE_RENDERING, GBM_BO_USE_SCANOUT, GbmBo, GbmDevice}, }, @@ -64,6 +66,7 @@ use { ops::DerefMut, rc::Rc, }, + thiserror::Error, uapi::{ OwnedFd, c::{self, dev_t}, @@ -322,6 +325,8 @@ pub struct PersistentDisplayData { pub mode: RefCell>, pub vrr_requested: Cell, pub format: Cell<&'static Format>, + pub eotf: Cell, + pub color_space: Cell, } #[derive(Debug)] @@ -346,6 +351,15 @@ pub struct ConnectorDisplayData { pub mm_width: u32, pub mm_height: u32, pub _subpixel: u32, + + pub supports_bt2020: bool, + pub supports_pq: bool, + pub primaries: Primaries, + pub luminance: Option, + + pub colorspace: Option>, + pub hdr_metadata: Option>, + pub hdr_metadata_blob: Option, } impl ConnectorDisplayData { @@ -653,6 +667,21 @@ impl MetalConnector { pub fn send_event(&self, event: ConnectorEvent) { let state = self.frontend_state.get(); + macro_rules! desktop_event { + ($name:expr) => { + match state { + FrontState::Connected { non_desktop: false } => { + self.on_change.send_event(event); + } + FrontState::Connected { non_desktop: true } + | FrontState::Removed + | FrontState::Disconnected + | FrontState::Unavailable => { + log::error!("Tried to send {} event in invalid state: {state:?}", $name); + } + } + }; + } match &event { ConnectorEvent::Connected(ty) => match state { FrontState::Disconnected => { @@ -665,21 +694,12 @@ impl MetalConnector { log::error!("Tried to send connected event in invalid state: {state:?}"); } }, - ConnectorEvent::HardwareCursor(_) | ConnectorEvent::ModeChanged(_) => match state { - FrontState::Connected { non_desktop: false } => { - self.on_change.send_event(event); - } - FrontState::Connected { non_desktop: true } - | FrontState::Removed - | FrontState::Disconnected - | FrontState::Unavailable => { - let name = match &event { - ConnectorEvent::HardwareCursor(_) => "hardware cursor", - _ => "mode change", - }; - log::error!("Tried to send {name} event in invalid state: {state:?}"); - } - }, + ConnectorEvent::HardwareCursor(_) => { + desktop_event!("hardware cursor"); + } + ConnectorEvent::ModeChanged(_) => { + desktop_event!("mode change"); + } ConnectorEvent::Disconnected => match state { FrontState::Connected { .. } | FrontState::Unavailable => { self.on_change.send_event(event); @@ -720,28 +740,15 @@ impl MetalConnector { log::error!("Tried to send available event in invalid state: {state:?}"); } }, - ConnectorEvent::VrrChanged(_) => match state { - FrontState::Connected { non_desktop: false } => { - self.on_change.send_event(event); - } - FrontState::Connected { non_desktop: true } - | FrontState::Removed - | FrontState::Disconnected - | FrontState::Unavailable => { - log::error!("Tried to send vrr-changed event in invalid state: {state:?}"); - } - }, - ConnectorEvent::FormatsChanged(_, _) => match state { - FrontState::Connected { non_desktop: false } => { - self.on_change.send_event(event); - } - FrontState::Connected { non_desktop: true } - | FrontState::Removed - | FrontState::Disconnected - | FrontState::Unavailable => { - log::error!("Tried to send format-changed event in invalid state: {state:?}"); - } - }, + ConnectorEvent::VrrChanged(_) => { + desktop_event!("vrr-changed"); + } + ConnectorEvent::FormatsChanged(_, _) => { + desktop_event!("formats-changed"); + } + ConnectorEvent::ColorsChanged(_, _) => { + desktop_event!("colors-changed"); + } } } @@ -765,6 +772,55 @@ impl MetalConnector { } } } + + fn change_property( + &self, + name: &str, + needs_change: impl FnOnce(&ConnectorDisplayData) -> bool, + supports_change: impl FnOnce(&ConnectorDisplayData) -> bool, + change: impl FnOnce(&ConnectorDisplayData), + changed: impl FnOnce(), + reset: impl FnOnce(&ConnectorDisplayData), + ) { + match self.frontend_state.get() { + FrontState::Connected { non_desktop: false } => {} + FrontState::Connected { non_desktop: true } + | FrontState::Removed + | FrontState::Disconnected + | FrontState::Unavailable => return, + } + let dd = self.display.borrow(); + if !needs_change(&dd) { + return; + } + if !supports_change(&dd) { + return; + } + if dd.connection != ConnectorStatus::Connected { + log::warn!("Cannot change {name} of connector that is not connected"); + return; + } + let Some(dev) = self.backend.device_holder.drm_devices.get(&self.dev.devnum) else { + log::warn!("Cannot change {name} because underlying device does not exist?"); + return; + }; + change(&dd); + drop(dd); + let Err(e) = self.backend.handle_drm_change_(&dev, true) else { + changed(); + return; + }; + log::warn!("Could not change {name}: {}", ErrorFmt(&e)); + reset(&self.display.borrow()); + if let MetalError::Modeset(DrmError::Atomic(OsError(c::EACCES))) = e { + log::warn!("Failed due to access denied. Resetting in memory only."); + return; + } + log::warn!("Trying to re-initialize the drm device"); + if let Err(e) = self.backend.handle_drm_change_(&dev, true) { + log::warn!("Could not restore the previous {name}: {}", ErrorFmt(e)); + }; + } } impl Connector for MetalConnector { @@ -942,6 +998,47 @@ impl Connector for MetalConnector { } } } + + fn set_colors(&self, bcs: BackendColorSpace, btf: BackendTransferFunction) { + let prev_bcs = Cell::new(bcs); + let prev_btf = Cell::new(btf); + self.change_property( + "colors", + |dd| { + prev_bcs.set(dd.persistent.color_space.get()); + prev_btf.set(dd.persistent.eotf.get()); + prev_bcs.get() != bcs || prev_btf.get() != btf + }, + |dd| { + let cs = match bcs { + BackendColorSpace::Default => true, + BackendColorSpace::Bt2020 => dd.supports_bt2020, + }; + if !cs { + log::warn!("Display does not support color space {:?}", bcs); + } + let tf = match btf { + BackendTransferFunction::Default => true, + BackendTransferFunction::Pq => dd.supports_pq, + }; + if !tf { + log::warn!("Display does not support transfer function {:?}", btf); + } + cs && tf + }, + |dd| { + dd.persistent.color_space.set(bcs); + dd.persistent.eotf.set(btf); + }, + || { + self.send_event(ConnectorEvent::ColorsChanged(bcs, btf)); + }, + |dd| { + dd.persistent.color_space.set(prev_bcs.get()); + dd.persistent.eotf.set(prev_btf.get()); + }, + ); + } } pub struct MetalCrtc { @@ -1203,6 +1300,10 @@ fn create_connector_display_data( ty: ConnectorType::from_drm(info.connector_type), idx: info.connector_type_id, }; + let mut supports_bt2020 = false; + let mut supports_pq = false; + let mut luminance = None; + let mut primaries = Primaries::SRGB; 'fetch_edid: { if connection != ConnectorStatus::Connected { break 'fetch_edid; @@ -1286,6 +1387,40 @@ fn create_connector_display_data( if min_vrr_hz > 0 { vrr_refresh_max_nsec = 1_000_000_000 / min_vrr_hz; } + let cc = &edid.base_block.chromaticity_coordinates; + let map = |c: u16| F64(c as f64 / 1024.0); + primaries = Primaries { + r: (map(cc.red_x), map(cc.red_y)), + g: (map(cc.green_x), map(cc.green_y)), + b: (map(cc.blue_x), map(cc.blue_y)), + wp: (map(cc.white_x), map(cc.white_y)), + }; + for ext in &edid.extension_blocks { + if let EdidExtension::CtaV3(cta) = ext { + for data_block in &cta.data_blocks { + match data_block { + CtaDataBlock::Colorimetry(c) => { + if c.bt2020_rgb { + supports_bt2020 = true; + } + } + CtaDataBlock::StaticHdrMetadata(h) => { + if h.smpte_st_2084 { + supports_pq = true; + } + if let Some(max) = h.max_luminance { + luminance = Some(BackendLuminance { + min: h.min_luminance.unwrap_or(0.0), + max, + max_fall: h.max_luminance.unwrap_or(max), + }); + } + } + _ => {} + } + } + } + } } let output_id = Rc::new(OutputId::new( connector_id.to_string(), @@ -1295,7 +1430,9 @@ fn create_connector_display_data( )); let desired_state = match dev.backend.persistent_display_data.get(&output_id) { Some(ds) => { - log::info!("Reusing desired state for {:?}", output_id); + if connection != ConnectorStatus::Disconnected { + log::info!("Reusing desired state for {:?}", output_id); + } ds } None => { @@ -1303,6 +1440,8 @@ fn create_connector_display_data( mode: RefCell::new(info.modes.first().cloned()), vrr_requested: Default::default(), format: Cell::new(XRGB8888), + eotf: Default::default(), + color_space: Default::default(), }); dev.backend .persistent_display_data @@ -1331,12 +1470,30 @@ fn create_connector_display_data( }; let mode = mode_opt.clone(); drop(mode_opt); + { + let viable = match desired_state.eotf.get() { + BackendTransferFunction::Default => true, + BackendTransferFunction::Pq => supports_pq, + }; + if !viable { + log::warn!("Discarding previously desired transfer function"); + desired_state.eotf.set(BackendTransferFunction::Default); + } + } + { + let viable = match desired_state.color_space.get() { + BackendColorSpace::Default => true, + BackendColorSpace::Bt2020 => supports_bt2020, + }; + if !viable { + log::warn!("Discarding previously desired color space"); + desired_state.color_space.set(BackendColorSpace::Default); + } + } let default_properties = create_default_properties( &props, &[ ("Broadcast RGB", DefaultValue::Enum("Automatic")), - ("Colorspace", DefaultValue::Enum("Default")), - ("HDR_OUTPUT_METADATA", DefaultValue::Fixed(0)), ("HDR_SOURCE_METADATA", DefaultValue::Fixed(0)), ("Output format", DefaultValue::Enum("Default")), ("WRITEBACK_FB_ID", DefaultValue::Fixed(0)), @@ -1363,8 +1520,18 @@ fn create_connector_display_data( mm_width: info.mm_width, mm_height: info.mm_height, _subpixel: info.subpixel, + supports_bt2020, + supports_pq, + primaries, + luminance, connector_id, output_id, + colorspace: props.get("Colorspace").ok(), + hdr_metadata: props + .get("HDR_OUTPUT_METADATA") + .ok() + .map(|v| v.map(|v| DrmBlob(v as _))), + hdr_metadata_blob: None, }) } @@ -1802,6 +1969,14 @@ impl MetalBackend { modes.push(mode); } } + let mut transfer_functions = vec![]; + if dd.supports_pq { + transfer_functions.push(BackendTransferFunction::Pq); + } + let mut color_spaces = vec![]; + if dd.supports_bt2020 { + color_spaces.push(BackendColorSpace::Bt2020); + } connector.send_event(ConnectorEvent::Connected(MonitorInfo { modes, output_id: dd.output_id.clone(), @@ -1810,6 +1985,12 @@ impl MetalBackend { height_mm: dd.mm_height as _, non_desktop: dd.non_desktop_effective, vrr_capable: dd.vrr_capable, + transfer_functions, + transfer_function: dd.persistent.eotf.get(), + color_spaces, + color_space: dd.persistent.color_space.get(), + primaries: dd.primaries, + luminance: dd.luminance, })); connector.send_hardware_cursor(); connector.send_vrr_enabled(); @@ -1971,9 +2152,16 @@ impl MetalBackend { for c in dev.connectors.lock().values() { let dd = &mut *c.display.borrow_mut(); collect_untyped_properties(master, c.id, &mut dd.untyped_properties)?; + let props = &dd.untyped_properties; dd.crtc_id .value - .set(DrmCrtc(get(&dd.untyped_properties, dd.crtc_id.id)? as _)); + .set(DrmCrtc(get(props, dd.crtc_id.id)? as _)); + if let Some(meta) = &dd.hdr_metadata { + meta.value.set(DrmBlob(get(props, meta.id)? as _)); + } + if let Some(cs) = &dd.colorspace { + cs.value.set(get(props, cs.id)?); + } } for c in dev.dev.crtcs.values() { let props = &mut *c.untyped_properties.borrow_mut(); @@ -2229,9 +2417,21 @@ impl MetalBackend { connector.version.fetch_add(1); let dd = connector.display.borrow_mut(); dd.crtc_id.value.set(DrmCrtc::NONE); + if let Some(cs) = &dd.colorspace { + cs.value.set(0); + } + if let Some(hdr) = &dd.hdr_metadata { + hdr.value.set(DrmBlob(0)); + } changes.change_object(connector.id, |c| { c.change(dd.crtc_id.id, 0); - }) + if let Some(cs) = &dd.colorspace { + c.change(cs.id, 0); + } + if let Some(hdr) = &dd.hdr_metadata { + c.change(hdr.id, 0); + } + }); } for crtc in dev.dev.crtcs.values() { if preserve.crtcs.contains(&crtc.id) { @@ -2310,6 +2510,16 @@ impl MetalBackend { fail!(c.id); } } + if let Some(m) = &dd.colorspace { + if m.value.get() != dd.persistent.color_space.get().to_drm() { + log::debug!("Connector has wrong colorspace"); + fail!(c.id); + } + } + if let Some(diff) = self.compare_hdr_metadata(&dev.dev, &dd) { + log::debug!("{}", diff); + fail!(c.id); + } } } for c in remove_connectors { @@ -2509,6 +2719,39 @@ impl MetalBackend { Ok(()) } + fn compare_hdr_metadata( + &self, + dev: &MetalDrmDevice, + dd: &ConnectorDisplayData, + ) -> Option { + let Some(m) = &dd.hdr_metadata else { + return None; + }; + match dd.persistent.eotf.get() { + BackendTransferFunction::Default => { + if m.value.get() != DrmBlob::NONE { + return Some(HdrMetadataDiff::Undesired); + } + } + eotf => { + if m.value.get() == DrmBlob::NONE { + return Some(HdrMetadataDiff::No); + } + let current_metadata = + match dev.master.getblob::(m.value.get()) { + Ok(m) => m, + _ => { + return Some(HdrMetadataDiff::CouldNotRetrieve); + } + }; + if current_metadata != hdr_output_metadata::from_eotf(eotf.to_drm()) { + return Some(HdrMetadataDiff::Incompatible); + } + } + } + None + } + fn can_use_current_drm_mode(&self, dev: &Rc) -> bool { let mut used_crtcs = AHashSet::new(); let mut vrr_crtcs = AHashSet::new(); @@ -2532,6 +2775,16 @@ impl MetalBackend { if dd.should_enable_vrr() { vrr_crtcs.insert(crtc_id); } + if let Some(m) = &dd.colorspace { + if m.value.get() != dd.persistent.color_space.get().to_drm() { + log::debug!("Connector has wrong colorspace"); + return false; + } + } + if let Some(diff) = self.compare_hdr_metadata(&dev.dev, &dd) { + log::debug!("{}", diff); + return false; + } let crtc = dev.dev.crtcs.get(&crtc_id).unwrap(); connector.crtc.set(Some(crtc.clone())); connector.version.fetch_add(1); @@ -2804,8 +3057,8 @@ impl MetalBackend { connector: &Rc, changes: &mut Change, ) -> Result<(), MetalError> { - let dd = connector.display.borrow_mut(); - if should_ignore(connector, &dd) { + let dd = &mut *connector.display.borrow_mut(); + if should_ignore(connector, dd) { return Ok(()); } let crtc = 'crtc: { @@ -2820,9 +3073,23 @@ impl MetalBackend { Some(m) => m, _ => return Err(MetalError::NoModeForConnector), }; + let hdr_blob = match dd.persistent.eotf.get() { + BackendTransferFunction::Default => None, + eotf => { + let m = hdr_output_metadata::from_eotf(eotf.to_drm()); + Some(connector.master.create_blob(&m)?) + } + }; + let hdr_blob_id = hdr_blob.as_ref().map(|b| b.id()).unwrap_or_default(); let mode_blob = mode.create_blob(&connector.master)?; changes.change_object(connector.id, |c| { c.change(dd.crtc_id.id, crtc.id.0 as _); + if let Some(meta) = &dd.hdr_metadata { + c.change(meta.id, hdr_blob_id.0 as _); + } + if let Some(cs) = &dd.colorspace { + c.change(cs.id, dd.persistent.color_space.get().to_drm()); + } }); changes.change_object(crtc.id, |c| { c.change(crtc.active.id, 1); @@ -2832,6 +3099,13 @@ impl MetalBackend { connector.crtc.set(Some(crtc.clone())); connector.version.fetch_add(1); dd.crtc_id.value.set(crtc.id); + dd.hdr_metadata_blob = hdr_blob; + if let Some(meta) = &dd.hdr_metadata { + meta.value.set(hdr_blob_id); + } + if let Some(cs) = &dd.colorspace { + cs.value.set(dd.persistent.color_space.get().to_drm()); + } crtc.connector.set(Some(connector.clone())); crtc.active.value.set(true); crtc.mode_id.value.set(mode_blob.id()); @@ -3041,7 +3315,7 @@ impl RenderBuffer { pub fn copy_to_dev( &self, - cm: &ColorManager, + cd: &Rc, sync_file: Option, ) -> Result, MetalError> { let Some(tex) = &self.dev_tex else { @@ -3051,9 +3325,9 @@ impl RenderBuffer { .copy_texture( AcquireSync::Unnecessary, ReleaseSync::Explicit, - cm.srgb_srgb(), + cd, tex, - cm.srgb_srgb(), + cd, None, AcquireSync::from_sync_file(sync_file), ReleaseSync::None, @@ -3091,3 +3365,15 @@ fn should_ignore(connector: &MetalConnector, dd: &ConnectorDisplayData) -> bool || dd.connection != ConnectorStatus::Connected || dd.non_desktop_effective } + +#[derive(Error, Debug)] +enum HdrMetadataDiff { + #[error("Connector has undesired HDR metadata")] + Undesired, + #[error("Connector has no HDR metadata")] + No, + #[error("Could not retrieve current HDR metadata of connector")] + CouldNotRetrieve, + #[error("Connector has incompatible HDR metadata")] + Incompatible, +} diff --git a/src/backends/x.rs b/src/backends/x.rs index 8cf4442d..7ce71e85 100644 --- a/src/backends/x.rs +++ b/src/backends/x.rs @@ -3,11 +3,12 @@ use { allocator::BufferObject, async_engine::{Phase, SpawnedFuture}, backend::{ - AXIS_120, AxisSource, Backend, BackendDrmDevice, BackendEvent, Connector, - ConnectorEvent, ConnectorId, ConnectorKernelId, DrmDeviceId, DrmEvent, InputDevice, - InputDeviceAccelProfile, InputDeviceCapability, InputDeviceId, InputEvent, KeyState, - Mode, MonitorInfo, ScrollAxis, TransformMatrix, + AXIS_120, AxisSource, Backend, BackendColorSpace, BackendDrmDevice, BackendEvent, + BackendTransferFunction, Connector, ConnectorEvent, ConnectorId, ConnectorKernelId, + DrmDeviceId, DrmEvent, InputDevice, InputDeviceAccelProfile, InputDeviceCapability, + InputDeviceId, InputEvent, KeyState, Mode, MonitorInfo, ScrollAxis, TransformMatrix, }, + cmm::cmm_primaries::Primaries, fixed::Fixed, format::XRGB8888, gfx_api::{AcquireSync, GfxContext, GfxError, GfxFramebuffer, GfxTexture, ReleaseSync}, @@ -581,6 +582,12 @@ impl XBackend { height_mm: output.height.get(), non_desktop: false, vrr_capable: false, + transfer_functions: vec![], + transfer_function: BackendTransferFunction::Default, + color_spaces: vec![], + color_space: BackendColorSpace::Default, + primaries: Primaries::SRGB, + luminance: None, })); output.changed(); self.present(output).await; diff --git a/src/cli/randr.rs b/src/cli/randr.rs index 9ed0e17d..15a7df3f 100644 --- a/src/cli/randr.rs +++ b/src/cli/randr.rs @@ -1,5 +1,6 @@ use { crate::{ + backend::{BackendColorSpace, BackendTransferFunction}, cli::GlobalArgs, format::{Format, XRGB8888}, scale::Scale, @@ -7,9 +8,13 @@ use { utils::{errorfmt::ErrorFmt, transform_ext::TransformExt}, wire::{JayRandrId, jay_compositor, jay_randr}, }, - clap::{Args, Subcommand, ValueEnum}, + clap::{ + Args, Subcommand, ValueEnum, + builder::{PossibleValue, PossibleValuesParser}, + }, isnt::std_1::vec::IsntVecExt, jay_config::video::{TearingMode, Transform, VrrMode}, + linearize::LinearizeExt, std::{ cell::RefCell, fmt::{Display, Formatter}, @@ -154,6 +159,8 @@ pub enum OutputCommand { Tearing(TearingArgs), /// Change format settings. Format(FormatSettings), + /// Change color settings. + Colors(ColorsSettings), } #[derive(ValueEnum, Debug, Clone)] @@ -315,6 +322,51 @@ pub enum TransformCmd { FlipRotate270, } +#[derive(Args, Debug, Clone)] +pub struct ColorsSettings { + #[clap(subcommand)] + pub command: ColorsCommand, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum ColorsCommand { + /// Sets the color space and transfer function of the output. + Set { + /// The name of the color space. + #[clap(value_parser = PossibleValuesParser::new(color_space_possible_values()))] + color_space: String, + /// The name of the transfer function. + #[clap(value_parser = PossibleValuesParser::new(transfer_function_possible_values()))] + transfer_function: String, + }, +} + +fn color_space_possible_values() -> Vec { + let mut res = vec![]; + for cs in BackendColorSpace::variants() { + use BackendColorSpace::*; + let help = match cs { + Default => "The default color space (usually sRGB)", + Bt2020 => "The BT.2020 color space", + }; + res.push(PossibleValue::new(cs.name()).help(help)); + } + res +} + +fn transfer_function_possible_values() -> Vec { + let mut res = vec![]; + for cs in BackendTransferFunction::variants() { + use BackendTransferFunction::*; + let help = match cs { + Default => "The default transfer function (usually sRGB)", + Pq => "The PQ transfer function", + }; + res.push(PossibleValue::new(cs.name()).help(help)); + } + res +} + pub fn main(global: GlobalArgs, args: RandrArgs) { with_tool_client(global.log_level.into(), |tc| async move { let idle = Rc::new(Randr { tc: tc.clone() }); @@ -368,6 +420,10 @@ struct Output { pub formats: Vec, pub format: Option, pub flip_margin_ns: Option, + pub supported_color_spaces: Vec, + pub current_color_space: Option, + pub supported_transfer_functions: Vec, + pub current_transfer_function: Option, } #[derive(Copy, Clone, Debug)] @@ -610,6 +666,24 @@ impl Randr { } } } + OutputCommand::Colors(a) => { + self.handle_error(randr, move |msg| { + eprintln!("Could not change the colors: {}", msg); + }); + match a.command { + ColorsCommand::Set { + color_space, + transfer_function, + } => { + tc.send(jay_randr::SetColors { + self_id: randr, + output: &args.output, + color_space: &color_space, + transfer_function: &transfer_function, + }); + } + } + } } tc.round_trip().await; } @@ -806,6 +880,32 @@ impl Randr { ); } } + if o.supported_color_spaces.is_not_empty() { + println!(" color spaces:"); + let handle_cs = |cs: &str| { + let current = match Some(cs) == o.current_color_space.as_deref() { + false => "", + true => " (current)", + }; + println!(" {cs}{current}"); + }; + handle_cs("default"); + o.supported_color_spaces.iter().for_each(|cs| handle_cs(cs)); + } + if o.supported_transfer_functions.is_not_empty() { + println!(" transfer functions:"); + let handle_tf = |tf: &str| { + let current = match Some(tf) == o.current_transfer_function.as_deref() { + false => "", + true => " (current)", + }; + println!(" {tf}{current}"); + }; + handle_tf("default"); + o.supported_transfer_functions + .iter() + .for_each(|tf| handle_tf(tf)); + } if o.modes.is_not_empty() && modes { println!(" modes:"); for mode in &o.modes { @@ -886,6 +986,10 @@ impl Randr { formats: vec![], format: None, flip_margin_ns: None, + supported_color_spaces: vec![], + current_color_space: None, + supported_transfer_functions: vec![], + current_transfer_function: None, }); }); jay_randr::NonDesktopOutput::handle(tc, randr, data.clone(), |data, msg| { @@ -914,6 +1018,10 @@ impl Randr { formats: vec![], format: None, flip_margin_ns: None, + supported_color_spaces: vec![], + current_color_space: None, + supported_transfer_functions: vec![], + current_transfer_function: None, }); }); jay_randr::VrrState::handle(tc, randr, data.clone(), |data, msg| { @@ -966,6 +1074,34 @@ impl Randr { } o.modes.push(mode); }); + jay_randr::SupportedColorSpace::handle(tc, randr, data.clone(), |data, msg| { + let mut data = data.borrow_mut(); + let c = data.connectors.last_mut().unwrap(); + let output = c.output.as_mut().unwrap(); + output + .supported_color_spaces + .push(msg.color_space.to_string()); + }); + jay_randr::CurrentColorSpace::handle(tc, randr, data.clone(), |data, msg| { + let mut data = data.borrow_mut(); + let c = data.connectors.last_mut().unwrap(); + let output = c.output.as_mut().unwrap(); + output.current_color_space = Some(msg.color_space.to_string()); + }); + jay_randr::SupportedTransferFunction::handle(tc, randr, data.clone(), |data, msg| { + let mut data = data.borrow_mut(); + let c = data.connectors.last_mut().unwrap(); + let output = c.output.as_mut().unwrap(); + output + .supported_transfer_functions + .push(msg.transfer_function.to_string()); + }); + jay_randr::CurrentTransferFunction::handle(tc, randr, data.clone(), |data, msg| { + let mut data = data.borrow_mut(); + let c = data.connectors.last_mut().unwrap(); + let output = c.output.as_mut().unwrap(); + output.current_transfer_function = Some(msg.transfer_function.to_string()); + }); tc.round_trip().await; let x = data.borrow_mut().clone(); x diff --git a/src/cmm/cmm_description.rs b/src/cmm/cmm_description.rs index 642d271c..bb6bb2eb 100644 --- a/src/cmm/cmm_description.rs +++ b/src/cmm/cmm_description.rs @@ -19,6 +19,12 @@ pub type ColorDescriptionIds = FreeList; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct ColorDescriptionId(u32); +impl ColorDescriptionId { + pub fn raw(self) -> u32 { + self.0 + } +} + impl From for ColorDescriptionId { fn from(value: u32) -> Self { Self(value) diff --git a/src/cmm/cmm_manager.rs b/src/cmm/cmm_manager.rs index 02658e40..4e81f8ca 100644 --- a/src/cmm/cmm_manager.rs +++ b/src/cmm/cmm_manager.rs @@ -69,19 +69,12 @@ impl ColorManager { None, None, ); - let srgb_linear = get_description( + let srgb_linear = get_description2( &shared, - &linear_descriptions, + &srgb_srgb.linear, &complete_descriptions, - &linear_ids, Some(NamedPrimaries::Srgb), - Primaries::SRGB, - Luminance::SRGB, TransferFunction::Linear, - Primaries::SRGB, - Luminance::SRGB.to_target(), - None, - None, ); let windows_scrgb = get_description( &shared, @@ -146,6 +139,20 @@ impl ColorManager { max_fall, ) } + + pub fn get_with_tf( + self: &Rc, + cd: &Rc, + transfer_function: TransferFunction, + ) -> Rc { + get_description2( + &self.shared, + &cd.linear, + &self.complete_descriptions, + cd.named_primaries, + transfer_function, + ) + } } fn get_description( @@ -182,26 +189,13 @@ fn get_description( }; if let Some(d) = linear_descriptions.get(&key) { if let Some(d) = d.upgrade() { - let key = CompleteDescriptionKey { - linear: d.id, + return get_description2( + shared, + &d, + complete_descriptions, named_primaries, transfer_function, - }; - if let Some(d) = complete_descriptions.get(&key) { - if let Some(d) = d.upgrade() { - return d; - } - shared.dead_complete.fetch_sub(1); - } - let d = Rc::new(ColorDescription { - id: shared.complete_ids.acquire(), - linear: d, - named_primaries, - transfer_function, - shared: shared.clone(), - }); - complete_descriptions.set(key, Rc::downgrade(&d)); - return d; + ); } shared.dead_linear.fetch_sub(1); } @@ -234,3 +228,32 @@ fn get_description( complete_descriptions.set(key, Rc::downgrade(&d)); d } + +fn get_description2( + shared: &Rc, + ld: &Rc, + complete_descriptions: &CopyHashMap>, + named_primaries: Option, + transfer_function: TransferFunction, +) -> Rc { + let key = CompleteDescriptionKey { + linear: ld.id, + named_primaries, + transfer_function, + }; + if let Some(d) = complete_descriptions.get(&key) { + if let Some(d) = d.upgrade() { + return d; + } + shared.dead_complete.fetch_sub(1); + } + let d = Rc::new(ColorDescription { + id: shared.complete_ids.acquire(), + linear: ld.clone(), + named_primaries, + transfer_function, + shared: shared.clone(), + }); + complete_descriptions.set(key, Rc::downgrade(&d)); + d +} diff --git a/src/compositor.rs b/src/compositor.rs index 3b902ead..da6f22e9 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -4,7 +4,7 @@ use { crate::{ acceptor::{Acceptor, AcceptorError}, async_engine::{AsyncEngine, Phase, SpawnedFuture}, - backend::{self, Backend, Connector}, + backend::{self, Backend, BackendColorSpace, BackendTransferFunction, Connector}, backends::{ dummy::{DummyBackend, DummyOutput}, metal, x, @@ -12,7 +12,7 @@ use { cli::{CliBackend, GlobalArgs, RunArgs}, client::{ClientId, Clients}, clientmem::{self, ClientMemError}, - cmm::cmm_manager::ColorManager, + cmm::{cmm_manager::ColorManager, cmm_primaries::Primaries}, config::ConfigProxy, cpu_worker::{CpuWorker, CpuWorkerError}, damage::{DamageVisualizer, visualize_damage}, @@ -574,6 +574,12 @@ fn create_dummy_output(state: &Rc) { 0, &output_id, &persistent_state, + Vec::new(), + BackendTransferFunction::Default, + Vec::new(), + BackendColorSpace::Default, + Primaries::SRGB, + None, )), jay_outputs: Default::default(), workspaces: Default::default(), diff --git a/src/config/handler.rs b/src/config/handler.rs index aa416023..ba85bbab 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -2,8 +2,8 @@ use { crate::{ async_engine::SpawnedFuture, backend::{ - self, ConnectorId, DrmDeviceId, InputDeviceAccelProfile, InputDeviceCapability, - InputDeviceId, + self, BackendColorSpace, BackendTransferFunction, ConnectorId, DrmDeviceId, + InputDeviceAccelProfile, InputDeviceCapability, InputDeviceId, }, cmm::cmm_transfer_function::TransferFunction, compositor::MAX_EXTENTS, @@ -51,7 +51,8 @@ use { theme::{colors::Colorable, sized::Resizable}, timer::Timer as JayTimer, video::{ - Connector, DrmDevice, Format as ConfigFormat, GfxApi, TearingMode as ConfigTearingMode, + ColorSpace, Connector, DrmDevice, Format as ConfigFormat, GfxApi, + TearingMode as ConfigTearingMode, TransferFunction as ConfigTransferFunction, Transform, VrrMode as ConfigVrrMode, }, xwayland::XScalingMode, @@ -1104,6 +1105,27 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_connector_set_colors( + &self, + connector: Connector, + color_space: ColorSpace, + transfer_function: ConfigTransferFunction, + ) -> Result<(), CphError> { + let bcs = match color_space { + ColorSpace::DEFAULT => BackendColorSpace::Default, + ColorSpace::BT2020 => BackendColorSpace::Bt2020, + _ => return Err(CphError::UnknownColorSpace(color_space)), + }; + let btf = match transfer_function { + ConfigTransferFunction::DEFAULT => BackendTransferFunction::Default, + ConfigTransferFunction::PQ => BackendTransferFunction::Pq, + _ => return Err(CphError::UnknownTransferFunction(transfer_function)), + }; + let connector = self.get_connector(connector)?; + connector.connector.set_colors(bcs, btf); + Ok(()) + } + fn handle_set_vrr_mode( &self, connector: Option, @@ -1994,6 +2016,13 @@ impl ConfigProxyHandler { ClientMessage::SetColorManagementEnabled { enabled } => { self.handle_set_color_management_enabled(enabled) } + ClientMessage::ConnectorSetColors { + connector, + color_space, + transfer_function, + } => self + .handle_connector_set_colors(connector, color_space, transfer_function) + .wrn("connector_set_colors")?, } Ok(()) } @@ -2065,6 +2094,10 @@ enum CphError { UnknownFormat(ConfigFormat), #[error("Unknown x scaling mode {0:?}")] UnknownXScalingMode(XScalingMode), + #[error("Unknown color space {0:?}")] + UnknownColorSpace(ColorSpace), + #[error("Unknown transfer function {0:?}")] + UnknownTransferFunction(ConfigTransferFunction), } trait WithRequestName { diff --git a/src/cursor_user.rs b/src/cursor_user.rs index e1970e2d..bb8e96c9 100644 --- a/src/cursor_user.rs +++ b/src/cursor_user.rs @@ -468,6 +468,7 @@ impl CursorUser { let transform = output.global.persistent.transform.get(); let render = output.hardware_cursor_needs_render.take(); let scale = output.global.persistent.scale.get(); + let cd = output.global.color_description.get(); if render { cursor.tick(); } @@ -507,7 +508,7 @@ impl CursorUser { &self.group.state, scale, transform, - self.group.state.color_manager.srgb_srgb(), + &cd, ); match res { Ok(sync_file) => { diff --git a/src/edid.rs b/src/edid.rs index cccc2ee3..685ac0a1 100644 --- a/src/edid.rs +++ b/src/edid.rs @@ -75,7 +75,6 @@ pub struct ScreenDimensions { } #[derive(Copy, Clone, Debug)] -#[expect(dead_code)] pub struct ChromaticityCoordinates { pub red_x: u16, pub red_y: u16, @@ -1223,8 +1222,8 @@ pub struct CtaExtensionV3 { pub enum CtaDataBlock { Unknown, VendorAmd(CtaAmdVendorDataBlock), - Colorimetry(#[expect(dead_code)] CtaColorimetryDataBlock), - StaticHdrMetadata(#[expect(dead_code)] CtaStaticHdrMetadataDataBlock), + Colorimetry(CtaColorimetryDataBlock), + StaticHdrMetadata(CtaStaticHdrMetadataDataBlock), } #[derive(Debug)] diff --git a/src/ifs/color_management/wp_color_management_output_v1.rs b/src/ifs/color_management/wp_color_management_output_v1.rs index bbcb58a8..65e6f057 100644 --- a/src/ifs/color_management/wp_color_management_output_v1.rs +++ b/src/ifs/color_management/wp_color_management_output_v1.rs @@ -1,7 +1,10 @@ use { crate::{ client::{Client, ClientError}, - ifs::color_management::wp_image_description_v1::WpImageDescriptionV1, + ifs::{ + color_management::{CAUSE_NO_OUTPUT, wp_image_description_v1::WpImageDescriptionV1}, + wl_output::OutputGlobalOpt, + }, leaks::Tracker, object::{Object, Version}, wire::{WpColorManagementOutputV1Id, wp_color_management_output_v1::*}, @@ -15,20 +18,29 @@ pub struct WpColorManagementOutputV1 { pub client: Rc, pub version: Version, pub tracker: Tracker, + pub output: Rc, } impl WpColorManagementOutputV1 { - #[expect(dead_code)] pub fn send_image_description_changed(&self) { self.client .event(ImageDescriptionChanged { self_id: self.id }); } + + fn detach(&self) { + if let Some(output) = self.output.get() { + output + .color_description_listeners + .remove(&(self.client.id, self.id)); + } + } } impl WpColorManagementOutputV1RequestHandler for WpColorManagementOutputV1 { type Error = WpColorManagementOutputV1Error; fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.detach(); self.client.remove_obj(self)?; Ok(()) } @@ -43,11 +55,15 @@ impl WpColorManagementOutputV1RequestHandler for WpColorManagementOutputV1 { client: self.client.clone(), version: self.version, tracker: Default::default(), - description: self.client.state.color_manager.srgb_srgb().clone(), + description: self.output.get().map(|o| o.color_description.get()), }); track!(self.client, obj); self.client.add_client_obj(&obj)?; - obj.send_ready(); + if obj.description.is_some() { + obj.send_ready(); + } else { + obj.send_failed(CAUSE_NO_OUTPUT, "the output no longer exists"); + } Ok(()) } } @@ -57,7 +73,11 @@ object_base! { version = self.version; } -impl Object for WpColorManagementOutputV1 {} +impl Object for WpColorManagementOutputV1 { + fn break_loops(&self) { + self.detach(); + } +} simple_add_obj!(WpColorManagementOutputV1); diff --git a/src/ifs/color_management/wp_color_management_surface_feedback_v1.rs b/src/ifs/color_management/wp_color_management_surface_feedback_v1.rs index 0b166fe6..a6779b5b 100644 --- a/src/ifs/color_management/wp_color_management_surface_feedback_v1.rs +++ b/src/ifs/color_management/wp_color_management_surface_feedback_v1.rs @@ -1,7 +1,10 @@ use { crate::{ client::{Client, ClientError}, - ifs::color_management::wp_image_description_v1::WpImageDescriptionV1, + cmm::cmm_description::ColorDescription, + ifs::{ + color_management::wp_image_description_v1::WpImageDescriptionV1, wl_surface::WlSurface, + }, leaks::Tracker, object::{Object, Version}, wire::{ @@ -18,6 +21,7 @@ pub struct WpColorManagementSurfaceFeedbackV1 { pub client: Rc, pub version: Version, pub tracker: Tracker, + pub surface: Rc, } impl WpColorManagementSurfaceFeedbackV1 { @@ -30,13 +34,20 @@ impl WpColorManagementSurfaceFeedbackV1 { client: self.client.clone(), version: self.version, tracker: Default::default(), - description: self.client.state.color_manager.srgb_srgb().clone(), + description: Some(self.surface.get_output().global.color_description.get()), }); track!(self.client, obj); self.client.add_client_obj(&obj)?; obj.send_ready(); Ok(()) } + + pub fn send_preferred_changed(&self, cd: &ColorDescription) { + self.client.event(PreferredChanged { + self_id: self.id, + identity: cd.id.raw(), + }); + } } impl WpColorManagementSurfaceFeedbackV1RequestHandler for WpColorManagementSurfaceFeedbackV1 { @@ -44,6 +55,7 @@ impl WpColorManagementSurfaceFeedbackV1RequestHandler for WpColorManagementSurfa fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { self.client.remove_obj(self)?; + self.surface.remove_color_management_feedback(self); Ok(()) } diff --git a/src/ifs/color_management/wp_color_manager_v1.rs b/src/ifs/color_management/wp_color_manager_v1.rs index 5dcc4ebf..422c2dc0 100644 --- a/src/ifs/color_management/wp_color_manager_v1.rs +++ b/src/ifs/color_management/wp_color_manager_v1.rs @@ -147,15 +147,21 @@ impl WpColorManagerV1RequestHandler for WpColorManagerV1 { } fn get_output(&self, req: GetOutput, _slf: &Rc) -> Result<(), Self::Error> { - let _ = self.client.lookup(req.output)?; + let output = self.client.lookup(req.output)?; let obj = Rc::new(WpColorManagementOutputV1 { id: req.id, client: self.client.clone(), version: self.version, tracker: Default::default(), + output: output.global.clone(), }); track!(self.client, obj); self.client.add_client_obj(&obj)?; + if let Some(global) = output.global.get() { + global + .color_description_listeners + .set((self.client.id, req.id), obj); + } Ok(()) } @@ -179,15 +185,17 @@ impl WpColorManagerV1RequestHandler for WpColorManagerV1 { req: GetSurfaceFeedback, _slf: &Rc, ) -> Result<(), Self::Error> { - let _ = self.client.lookup(req.surface)?; + let surface = self.client.lookup(req.surface)?; let obj = Rc::new(WpColorManagementSurfaceFeedbackV1 { id: req.id, client: self.client.clone(), version: self.version, tracker: Default::default(), + surface: surface.clone(), }); track!(self.client, obj); self.client.add_client_obj(&obj)?; + surface.add_color_management_feedback(&obj); Ok(()) } @@ -232,7 +240,7 @@ impl WpColorManagerV1RequestHandler for WpColorManagerV1 { client: self.client.clone(), version: self.version, tracker: Default::default(), - description: self.client.state.color_manager.windows_scrgb().clone(), + description: Some(self.client.state.color_manager.windows_scrgb().clone()), }); track!(self.client, obj); self.client.add_client_obj(&obj)?; diff --git a/src/ifs/color_management/wp_image_description_creator_params_v1.rs b/src/ifs/color_management/wp_image_description_creator_params_v1.rs index 15de1e84..22356594 100644 --- a/src/ifs/color_management/wp_image_description_creator_params_v1.rs +++ b/src/ifs/color_management/wp_image_description_creator_params_v1.rs @@ -91,7 +91,7 @@ impl WpImageDescriptionCreatorParamsV1RequestHandler for WpImageDescriptionCreat client: self.client.clone(), version: self.version, tracker: Default::default(), - description, + description: Some(description), }); track!(self.client, obj); self.client.add_client_obj(&obj)?; diff --git a/src/ifs/color_management/wp_image_description_v1.rs b/src/ifs/color_management/wp_image_description_v1.rs index f72a84a7..93b53cbb 100644 --- a/src/ifs/color_management/wp_image_description_v1.rs +++ b/src/ifs/color_management/wp_image_description_v1.rs @@ -16,11 +16,10 @@ pub struct WpImageDescriptionV1 { pub client: Rc, pub version: Version, pub tracker: Tracker, - pub description: Rc, + pub description: Option>, } impl WpImageDescriptionV1 { - #[expect(dead_code)] pub fn send_failed(&self, cause: u32, msg: &str) { self.client.event(Failed { self_id: self.id, @@ -32,7 +31,7 @@ impl WpImageDescriptionV1 { pub fn send_ready(&self) { self.client.event(Ready { self_id: self.id, - identity: self.description.id.into(), + identity: self.description.as_ref().unwrap().id.raw(), }); } } @@ -46,6 +45,9 @@ impl WpImageDescriptionV1RequestHandler for WpImageDescriptionV1 { } fn get_information(&self, req: GetInformation, _slf: &Rc) -> Result<(), Self::Error> { + let Some(desc) = &self.description else { + return Err(WpImageDescriptionV1Error::NotReady); + }; let obj = Rc::new(WpImageDescriptionInfoV1 { id: req.information, client: self.client.clone(), @@ -54,7 +56,7 @@ impl WpImageDescriptionV1RequestHandler for WpImageDescriptionV1 { }); self.client.add_client_obj(&obj)?; track!(self.client, obj); - obj.send_description(self.client.state.color_manager.srgb_srgb()); + obj.send_description(desc); self.client.remove_obj(&*obj)?; Ok(()) } @@ -77,5 +79,7 @@ dedicated_add_obj!( pub enum WpImageDescriptionV1Error { #[error(transparent)] ClientError(Box), + #[error("The description is not ready")] + NotReady, } efrom!(WpImageDescriptionV1Error, ClientError); diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 2da874f1..4551b606 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -73,7 +73,7 @@ impl Global for JayCompositorGlobal { } fn version(&self) -> u32 { - 14 + 15 } fn required_caps(&self) -> ClientCaps { diff --git a/src/ifs/jay_randr.rs b/src/ifs/jay_randr.rs index 2db096dd..bcb5fdd0 100644 --- a/src/ifs/jay_randr.rs +++ b/src/ifs/jay_randr.rs @@ -1,6 +1,6 @@ use { crate::{ - backend, + backend::{self, BackendColorSpace, BackendTransferFunction}, client::{Client, ClientError}, compositor::MAX_EXTENTS, format::named_formats, @@ -15,6 +15,7 @@ use { jay_config::video::{ GfxApi, TearingMode as ConfigTearingMode, Transform, VrrMode as ConfigVrrMode, }, + linearize::LinearizeExt, std::rc::Rc, thiserror::Error, }; @@ -30,6 +31,7 @@ const VRR_CAPABLE_SINCE: Version = Version(2); const TEARING_SINCE: Version = Version(3); const FORMAT_SINCE: Version = Version(8); const FLIP_MARGIN_SINCE: Version = Version(10); +const COLORIMETRY_SINCE: Version = Version(15); impl JayRandr { pub fn new(id: JayRandrId, client: &Rc, version: Version) -> Self { @@ -163,6 +165,28 @@ impl JayRandr { current: (mode == ¤t_mode) as _, }); } + if self.version >= COLORIMETRY_SINCE { + for tf in &node.global.transfer_functions { + self.client.event(SupportedTransferFunction { + self_id: self.id, + transfer_function: tf.name(), + }); + } + self.client.event(CurrentTransferFunction { + self_id: self.id, + transfer_function: node.global.btf.get().name(), + }); + for cs in &node.global.color_spaces { + self.client.event(SupportedColorSpace { + self_id: self.id, + color_space: cs.name(), + }); + } + self.client.event(CurrentColorSpace { + self_id: self.id, + color_space: node.global.bcs.get().name(), + }); + } } fn send_error(&self, msg: &str) { @@ -412,6 +436,34 @@ impl JayRandrRequestHandler for JayRandr { dev.dev.set_flip_margin(req.margin_ns); Ok(()) } + + fn set_colors(&self, req: SetColors<'_>, _slf: &Rc) -> Result<(), Self::Error> { + let cs = 'cs: { + for cs in BackendColorSpace::variants() { + if cs.name() == req.color_space { + break 'cs cs; + } + } + return Err(JayRandrError::UnknownColorSpace( + req.color_space.to_string(), + )); + }; + let tf = 'tf: { + for tf in BackendTransferFunction::variants() { + if tf.name() == req.transfer_function { + break 'tf tf; + } + } + return Err(JayRandrError::UnknownTransferFunction( + req.transfer_function.to_string(), + )); + }; + let Some(c) = self.get_connector(req.output) else { + return Ok(()); + }; + c.connector.set_colors(cs, tf); + Ok(()) + } } object_base! { @@ -433,5 +485,9 @@ pub enum JayRandrError { UnknownTearingMode(u32), #[error("Unknown format {0}")] UnknownFormat(String), + #[error("Unknown color space {0}")] + UnknownColorSpace(String), + #[error("Unknown transfer function {0}")] + UnknownTransferFunction(String), } efrom!(JayRandrError, ClientError); diff --git a/src/ifs/wl_output.rs b/src/ifs/wl_output.rs index c1cc82f5..243b6828 100644 --- a/src/ifs/wl_output.rs +++ b/src/ifs/wl_output.rs @@ -2,22 +2,31 @@ mod removed_output; use { crate::{ - backend, + backend::{self, BackendColorSpace, BackendLuminance, BackendTransferFunction}, client::{Client, ClientError, ClientId}, + cmm::{ + cmm_description::ColorDescription, + cmm_luminance::Luminance, + cmm_primaries::{NamedPrimaries, Primaries}, + cmm_transfer_function::TransferFunction, + }, damage::DamageMatrix, format::{Format, XRGB8888}, globals::{Global, GlobalName}, - ifs::{wl_surface::WlSurface, zxdg_output_v1::ZxdgOutputV1}, + ifs::{ + color_management::wp_color_management_output_v1::WpColorManagementOutputV1, + wl_surface::WlSurface, zxdg_output_v1::ZxdgOutputV1, + }, leaks::Tracker, object::{Object, Version}, rect::Rect, state::{ConnectorData, State}, tree::{OutputNode, TearingMode, VrrMode, calculate_logical_size}, utils::{ - cell_ext::CellExt, clonecell::CloneCell, copyhashmap::CopyHashMap, rc_eq::rc_eq, - transform_ext::TransformExt, + cell_ext::CellExt, clonecell::CloneCell, copyhashmap::CopyHashMap, ordered_float::F64, + rc_eq::rc_eq, transform_ext::TransformExt, }, - wire::{WlOutputId, ZxdgOutputV1Id, wl_output::*}, + wire::{WlOutputId, WpColorManagementOutputV1Id, ZxdgOutputV1Id, wl_output::*}, }, ahash::AHashMap, jay_config::video::Transform, @@ -67,12 +76,22 @@ pub struct WlOutputGlobal { pub format: Cell<&'static Format>, pub width_mm: i32, pub height_mm: i32, + pub transfer_functions: Vec, + pub color_spaces: Vec, + pub primaries: Primaries, + pub luminance: Option, pub bindings: RefCell>>>, pub destroyed: Cell, pub legacy_scale: Cell, pub persistent: Rc, pub opt: Rc, pub damage_matrix: Cell, + pub btf: Cell, + pub bcs: Cell, + pub color_description: CloneCell>, + pub linear_color_description: CloneCell>, + pub color_description_listeners: + CopyHashMap<(ClientId, WpColorManagementOutputV1Id), Rc>, } #[derive(Default)] @@ -133,6 +152,7 @@ impl WlOutputGlobal { pub fn clear(&self) { self.opt.clear(); self.bindings.borrow_mut().clear(); + self.color_description_listeners.clear(); } pub fn new( @@ -145,6 +165,12 @@ impl WlOutputGlobal { height_mm: i32, output_id: &Rc, persistent_state: &Rc, + transfer_functions: Vec, + btf: BackendTransferFunction, + color_spaces: Vec, + bcs: BackendColorSpace, + primaries: Primaries, + luminance: Option, ) -> Self { let (x, y) = persistent_state.pos.get(); let scale = persistent_state.scale.get(); @@ -166,14 +192,24 @@ impl WlOutputGlobal { format: Cell::new(XRGB8888), width_mm, height_mm, + transfer_functions, + color_spaces, + primaries, + luminance, bindings: Default::default(), destroyed: Cell::new(false), legacy_scale: Cell::new(scale.round_up()), persistent: persistent_state.clone(), opt: Default::default(), damage_matrix: Default::default(), + btf: Cell::new(btf), + bcs: Cell::new(bcs), + color_description: CloneCell::new(state.color_manager.srgb_srgb().clone()), + linear_color_description: CloneCell::new(state.color_manager.srgb_linear().clone()), + color_description_listeners: Default::default(), }; global.update_damage_matrix(); + global.update_color_description(); global } @@ -292,6 +328,46 @@ impl WlOutputGlobal { pub fn add_visualizer_damage(&self) { self.state.damage_visualizer.copy_damage(self); } + + pub fn update_color_description(&self) -> bool { + let mut luminance = Luminance::SRGB; + let tf = match self.btf.get() { + BackendTransferFunction::Default => TransferFunction::Srgb, + BackendTransferFunction::Pq => { + luminance = Luminance::ST2084_PQ; + TransferFunction::St2084Pq + } + }; + let mut target_luminance = luminance.to_target(); + let mut max_cll = None; + let mut max_fall = None; + if let Some(l) = self.luminance { + target_luminance.min = F64(l.min); + target_luminance.max = F64(l.max); + max_cll = Some(F64(l.max)); + max_fall = Some(F64(l.max_fall)); + } + let primaries = match self.bcs.get() { + BackendColorSpace::Default => NamedPrimaries::Srgb, + BackendColorSpace::Bt2020 => NamedPrimaries::Bt2020, + }; + let cd = self.state.color_manager.get_description( + Some(primaries), + primaries.primaries(), + luminance, + tf, + self.primaries, + target_luminance, + max_cll, + max_fall, + ); + let cd_linear = self + .state + .color_manager + .get_with_tf(&cd, TransferFunction::Linear); + self.linear_color_description.set(cd_linear.clone()); + self.color_description.set(cd.clone()).id != cd.id + } } global_base!(WlOutputGlobal, WlOutput, WlOutputError); diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index cc0f97f0..ec7bdf1a 100644 --- a/src/ifs/wl_surface.rs +++ b/src/ifs/wl_surface.rs @@ -33,6 +33,7 @@ use { ReleaseSync, SampleRect, SyncFile, }, ifs::{ + color_management::wp_color_management_surface_feedback_v1::WpColorManagementSurfaceFeedbackV1, wl_buffer::WlBuffer, wl_callback::WlCallback, wl_seat::{ @@ -89,8 +90,8 @@ use { drm::sync_obj::{SyncObj, SyncObjPoint}, }, wire::{ - WlOutputId, WlSurfaceId, ZwpIdleInhibitorV1Id, ZwpLinuxDmabufFeedbackV1Id, - wl_surface::*, + WlOutputId, WlSurfaceId, WpColorManagementSurfaceFeedbackV1Id, ZwpIdleInhibitorV1Id, + ZwpLinuxDmabufFeedbackV1Id, wl_surface::*, }, xwayland::XWaylandEvent, }, @@ -201,6 +202,14 @@ impl NodeVisitorBase for SurfaceSendPreferredTransformVisitor { } } +pub struct SurfaceSendPreferredColorDescription; +impl NodeVisitorBase for SurfaceSendPreferredColorDescription { + fn visit_surface(&mut self, node: &Rc) { + node.send_preferred_color_description(); + node.node_visit_children(self); + } +} + struct SurfaceBufferExplicitRelease { sync_obj: Rc, point: SyncObjPoint, @@ -336,6 +345,8 @@ pub struct WlSurface { before_latch_listener: EventListener, is_opaque: Cell, color_management_surface: CloneCell>>, + color_management_feedback: + CopyHashMap>, color_description: CloneCell>>, } @@ -678,6 +689,7 @@ impl WlSurface { before_latch_listener: EventListener::new(slf.clone()), is_opaque: Cell::new(false), color_management_surface: Default::default(), + color_management_feedback: Default::default(), color_description: Default::default(), } } @@ -699,7 +711,6 @@ impl WlSurface { Ok(ext.into_xsurface().unwrap()) } - #[cfg_attr(not(feature = "it"), expect(dead_code))] pub fn get_output(&self) -> Rc { self.output.get() } @@ -720,6 +731,9 @@ impl WlSurface { if old.global.persistent.transform.get() != output.global.persistent.transform.get() { self.send_preferred_buffer_transform(); } + if old.global.color_description.get().id != output.global.color_description.get().id { + self.send_preferred_color_description(); + } let children = self.children.borrow_mut(); if let Some(children) = &*children { for ss in children.subsurfaces.values() { @@ -1682,6 +1696,24 @@ impl WlSurface { None => self.client.state.color_manager.srgb_srgb().clone(), } } + + pub fn add_color_management_feedback(&self, fb: &Rc) { + self.color_management_feedback.set(fb.id, fb.clone()); + } + + pub fn remove_color_management_feedback(&self, fb: &WpColorManagementSurfaceFeedbackV1) { + self.color_management_feedback.remove(&fb.id); + } + + pub fn send_preferred_color_description(&self) { + if self.color_management_feedback.is_empty() { + return; + } + let cd = self.output.get().global.color_description.get(); + for fb in self.color_management_feedback.lock().values() { + fb.send_preferred_changed(&cd); + } + } } object_base! { @@ -1714,6 +1746,7 @@ impl Object for WlSurface { self.fifo.take(); self.commit_timer.take(); self.color_management_surface.take(); + self.color_management_feedback.clear(); } } diff --git a/src/ifs/wl_surface/wp_color_management_surface_v1.rs b/src/ifs/wl_surface/wp_color_management_surface_v1.rs index 4666c53d..33ad5df5 100644 --- a/src/ifs/wl_surface/wp_color_management_surface_v1.rs +++ b/src/ifs/wl_surface/wp_color_management_surface_v1.rs @@ -41,6 +41,7 @@ impl WpColorManagementSurfaceV1RequestHandler for WpColorManagementSurfaceV1 { fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { self.surface.color_management_surface.take(); + self.surface.pending.borrow_mut().color_description = Some(None); self.client.remove_obj(self)?; Ok(()) } @@ -56,7 +57,10 @@ impl WpColorManagementSurfaceV1RequestHandler for WpColorManagementSurfaceV1 { )); } let desc = self.client.lookup(req.image_description)?; - self.surface.pending.borrow_mut().color_description = Some(Some(desc.description.clone())); + if desc.description.is_none() { + return Err(WpColorManagementSurfaceV1Error::NotReady); + } + self.surface.pending.borrow_mut().color_description = Some(desc.description.clone()); Ok(()) } @@ -87,5 +91,7 @@ pub enum WpColorManagementSurfaceV1Error { UnsupportedRenderIntent(u32), #[error("wl_surface already has a color-management extension")] HasSurface, + #[error("The color description is not ready")] + NotReady, } efrom!(WpColorManagementSurfaceV1Error, ClientError); diff --git a/src/it/test_backend.rs b/src/it/test_backend.rs index 1afd78ad..31275aad 100644 --- a/src/it/test_backend.rs +++ b/src/it/test_backend.rs @@ -3,11 +3,12 @@ use { allocator::{Allocator, AllocatorError}, async_engine::SpawnedFuture, backend::{ - AxisSource, Backend, BackendEvent, Connector, ConnectorEvent, ConnectorId, - ConnectorKernelId, DrmDeviceId, InputDevice, InputDeviceAccelProfile, - InputDeviceCapability, InputDeviceId, InputEvent, KeyState, Mode, MonitorInfo, - ScrollAxis, TransformMatrix, + AxisSource, Backend, BackendColorSpace, BackendEvent, BackendTransferFunction, + Connector, ConnectorEvent, ConnectorId, ConnectorKernelId, DrmDeviceId, InputDevice, + InputDeviceAccelProfile, InputDeviceCapability, InputDeviceId, InputEvent, KeyState, + Mode, MonitorInfo, ScrollAxis, TransformMatrix, }, + cmm::cmm_primaries::Primaries, compositor::TestFuture, drm_feedback::DrmFeedback, fixed::Fixed, @@ -129,6 +130,12 @@ impl TestBackend { height_mm: 60, non_desktop: false, vrr_capable: false, + transfer_functions: vec![], + transfer_function: BackendTransferFunction::Default, + color_spaces: vec![], + color_space: BackendColorSpace::Default, + primaries: Primaries::SRGB, + luminance: None, }; Self { state: state.clone(), diff --git a/src/it/tests/t0034_workspace_restoration.rs b/src/it/tests/t0034_workspace_restoration.rs index 10098aab..2065c206 100644 --- a/src/it/tests/t0034_workspace_restoration.rs +++ b/src/it/tests/t0034_workspace_restoration.rs @@ -1,6 +1,10 @@ use { crate::{ - backend::{BackendEvent, ConnectorEvent, ConnectorKernelId, Mode, MonitorInfo}, + backend::{ + BackendColorSpace, BackendEvent, BackendTransferFunction, ConnectorEvent, + ConnectorKernelId, Mode, MonitorInfo, + }, + cmm::cmm_primaries::Primaries, ifs::wl_output::OutputId, it::{test_backend::TestConnector, test_error::TestResult, testrun::TestRun}, video::drm::ConnectorType, @@ -48,6 +52,12 @@ async fn test(run: Rc) -> TestResult { height_mm: 0, non_desktop: false, vrr_capable: false, + transfer_functions: vec![], + transfer_function: BackendTransferFunction::Default, + color_spaces: vec![], + color_space: BackendColorSpace::Default, + primaries: Primaries::SRGB, + luminance: None, }; run.backend .state diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index b763533b..9ebc55d8 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -136,6 +136,12 @@ impl ConnectorHandler { info.height_mm, &output_id, &desired_state, + info.transfer_functions.clone(), + info.transfer_function, + info.color_spaces.clone(), + info.color_space, + info.primaries, + info.luminance, )); let schedule = Rc::new(OutputSchedule::new( &self.state.ring, @@ -270,6 +276,9 @@ impl ConnectorHandler { on.global.formats.set(formats); on.global.format.set(format); } + ConnectorEvent::ColorsChanged(bcs, btf) => { + on.update_btf_and_bcs(btf, bcs); + } ev => unreachable!("received unexpected event {:?}", ev), } } diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index 04ab2684..5ae36506 100644 --- a/src/tools/tool_client.rs +++ b/src/tools/tool_client.rs @@ -332,7 +332,7 @@ impl ToolClient { self_id: s.registry, name: s.jay_compositor.0, interface: JayCompositor.name(), - version: s.jay_compositor.1.min(14), + version: s.jay_compositor.1.min(15), id: id.into(), }); self.jay_compositor.set(Some(id)); diff --git a/src/tree/output.rs b/src/tree/output.rs index 2d291819..8a67c9ad 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -1,6 +1,6 @@ use { crate::{ - backend::{HardwareCursor, KeyState, Mode}, + backend::{BackendColorSpace, BackendTransferFunction, HardwareCursor, KeyState, Mode}, client::ClientId, cmm::cmm_description::ColorDescription, cursor::KnownCursor, @@ -18,7 +18,8 @@ use { wl_pointer::PendingScroll, }, wl_surface::{ - SurfaceSendPreferredScaleVisitor, SurfaceSendPreferredTransformVisitor, + SurfaceSendPreferredColorDescription, SurfaceSendPreferredScaleVisitor, + SurfaceSendPreferredTransformVisitor, ext_session_lock_surface_v1::ExtSessionLockSurfaceV1, tray::DynTrayItem, zwlr_layer_surface_v1::{ExclusiveSize, ZwlrLayerSurfaceV1}, @@ -837,6 +838,25 @@ impl OutputNode { self.state.tree_changed(); } + pub fn update_btf_and_bcs(&self, btf: BackendTransferFunction, bcs: BackendColorSpace) { + let old_btf = self.global.btf.replace(btf); + let old_bcs = self.global.bcs.replace(bcs); + if (old_btf, old_bcs) == (btf, bcs) { + return; + } + if self.global.update_color_description() { + self.state.damage(self.global.position()); + if let Some(hc) = self.hardware_cursor.get() { + self.hardware_cursor_needs_render.set(true); + hc.damage(); + } + for fb in self.global.color_description_listeners.lock().values() { + fb.send_image_description_changed(); + } + self.node_visit_children(&mut SurfaceSendPreferredColorDescription); + } + } + fn find_stacked_at( &self, stack: &LinkedList>, diff --git a/src/video/drm.rs b/src/video/drm.rs index b5f97698..7d9c8ed1 100644 --- a/src/video/drm.rs +++ b/src/video/drm.rs @@ -1,6 +1,7 @@ pub mod sync_obj; mod sys; pub mod wait_for_sync_obj; +pub use consts::*; use { crate::{ @@ -668,7 +669,7 @@ pub struct DrmPropertyDefinition { pub ty: DrmPropertyType, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum DrmPropertyType { Range { _min: u64, @@ -688,7 +689,7 @@ pub enum DrmPropertyType { }, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct DrmPropertyEnumValue { pub value: u64, pub name: BString, @@ -803,6 +804,132 @@ pub struct DrmVersion { pub desc: BString, } +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct HdrMetadata { + pub eotf: u8, + pub metadata_type: u8, + pub red: (u16, u16), + pub green: (u16, u16), + pub blue: (u16, u16), + pub white: (u16, u16), + pub max_display_mastering_luminance: u16, + pub min_display_mastering_luminance: u16, + pub max_cll: u16, + pub max_fall: u16, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] +pub struct hdr_metadata_primary { + pub x: u16, + pub y: u16, +} + +#[repr(C)] +#[derive(Copy, Clone)] +union hdr_output_metadata_type { + hdmi_metadata_type1: hdr_metadata_infoframe, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct hdr_output_metadata { + metadata_type: u32, + ty: hdr_output_metadata_type, +} + +impl hdr_output_metadata { + pub fn new(infoframe: hdr_metadata_infoframe) -> Self { + Self { + metadata_type: 0, + ty: hdr_output_metadata_type { + hdmi_metadata_type1: infoframe, + }, + } + } + + pub fn from_eotf(eotf: u8) -> Self { + Self::new(hdr_metadata_infoframe { + eotf, + metadata_type: 0, + ..hdr_metadata_infoframe::default() + }) + } +} + +unsafe impl Pod for hdr_output_metadata {} + +impl Debug for hdr_output_metadata { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut f = f.debug_struct("hdr_output_metadata"); + f.field("metadata_type", &self.metadata_type); + match self.metadata_type { + 0 => unsafe { + f.field("hdmi_metadata_type1", &self.ty.hdmi_metadata_type1) + .finish() + }, + _ => f.finish_non_exhaustive(), + } + } +} + +impl Eq for hdr_output_metadata {} + +impl PartialEq for hdr_output_metadata { + fn eq(&self, other: &Self) -> bool { + if self.metadata_type != other.metadata_type { + return false; + } + match self.metadata_type { + 0 => unsafe { + self.ty + .hdmi_metadata_type1 + .eq(&other.ty.hdmi_metadata_type1) + }, + _ => return false, + } + } +} + +#[expect(dead_code)] +mod consts { + pub const HDMI_EOTF_TRADITIONAL_GAMMA_SDR: u8 = 0; + pub const HDMI_EOTF_TRADITIONAL_GAMMA_HDR: u8 = 1; + pub const HDMI_EOTF_SMPTE_ST2084: u8 = 2; + pub const HDMI_EOTF_BT_2100_HLG: u8 = 3; + + pub const DRM_MODE_COLORIMETRY_DEFAULT: u64 = 0; + pub const DRM_MODE_COLORIMETRY_NO_DATA: u64 = 0; + pub const DRM_MODE_COLORIMETRY_SMPTE_170M_YCC: u64 = 1; + pub const DRM_MODE_COLORIMETRY_BT709_YCC: u64 = 2; + pub const DRM_MODE_COLORIMETRY_XVYCC_601: u64 = 3; + pub const DRM_MODE_COLORIMETRY_XVYCC_709: u64 = 4; + pub const DRM_MODE_COLORIMETRY_SYCC_601: u64 = 5; + pub const DRM_MODE_COLORIMETRY_OPYCC_601: u64 = 6; + pub const DRM_MODE_COLORIMETRY_OPRGB: u64 = 7; + pub const DRM_MODE_COLORIMETRY_BT2020_CYCC: u64 = 8; + pub const DRM_MODE_COLORIMETRY_BT2020_RGB: u64 = 9; + pub const DRM_MODE_COLORIMETRY_BT2020_YCC: u64 = 10; + pub const DRM_MODE_COLORIMETRY_DCI_P3_RGB_D65: u64 = 11; + pub const DRM_MODE_COLORIMETRY_DCI_P3_RGB_THEATER: u64 = 12; + pub const DRM_MODE_COLORIMETRY_RGB_WIDE_FIXED: u64 = 13; + pub const DRM_MODE_COLORIMETRY_RGB_WIDE_FLOAT: u64 = 14; + pub const DRM_MODE_COLORIMETRY_BT601_YCC: u64 = 15; +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] +pub struct hdr_metadata_infoframe { + pub eotf: u8, + pub metadata_type: u8, + pub display_primaries: [hdr_metadata_primary; 3], + pub white_point: hdr_metadata_primary, + pub max_display_mastering_luminance: u16, + pub min_display_mastering_luminance: u16, + pub max_cll: u16, + pub max_fall: u16, +} + impl DrmModeInfo { pub fn create_blob(&self, master: &Rc) -> Result { let raw = self.to_raw(); diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index d13a3522..709fecf3 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -26,7 +26,7 @@ use { logging::LogLevel, status::MessageFormat, theme::Color, - video::{Format, GfxApi, TearingMode, Transform, VrrMode}, + video::{ColorSpace, Format, GfxApi, TearingMode, TransferFunction, Transform, VrrMode}, xwayland::XScalingMode, }, std::{ @@ -220,6 +220,8 @@ pub struct Output { pub vrr: Option, pub tearing: Option, pub format: Option, + pub color_space: Option, + pub transfer_function: Option, } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/output.rs b/toml-config/src/config/parsers/output.rs index 2f2940ba..809ff172 100644 --- a/toml-config/src/config/parsers/output.rs +++ b/toml-config/src/config/parsers/output.rs @@ -19,7 +19,7 @@ use { }, }, indexmap::IndexMap, - jay_config::video::Transform, + jay_config::video::{ColorSpace, TransferFunction, Transform}, thiserror::Error, }; @@ -49,8 +49,11 @@ impl Parser for OutputParser<'_> { table: &IndexMap, Spanned>, ) -> ParseResult { let mut ext = Extractor::new(self.cx, span, table); - let (name, match_val, x, y, scale, transform, mode, vrr_val, tearing_val, format_val) = ext - .extract(( + let ( + (name, match_val, x, y, scale, transform, mode, vrr_val, tearing_val, format_val), + (color_space, transfer_function), + ) = ext.extract(( + ( opt(str("name")), val("match"), recover(opt(s32("x"))), @@ -61,7 +64,12 @@ impl Parser for OutputParser<'_> { opt(val("vrr")), opt(val("tearing")), opt(val("format")), - ))?; + ), + ( + recover(opt(str("color-space"))), + recover(opt(str("transfer-function"))), + ), + ))?; let transform = match transform { None => None, Some(t) => match t.value { @@ -79,6 +87,36 @@ impl Parser for OutputParser<'_> { } }, }; + let color_space = match color_space { + None => None, + Some(cs) => match cs.value { + "default" => Some(ColorSpace::DEFAULT), + "bt2020" => Some(ColorSpace::BT2020), + _ => { + log::warn!( + "Unknown color space {}: {}", + cs.value, + self.cx.error3(cs.span) + ); + None + } + }, + }; + let transfer_function = match transfer_function { + None => None, + Some(tf) => match tf.value { + "default" => Some(TransferFunction::DEFAULT), + "pq" => Some(TransferFunction::PQ), + _ => { + log::warn!( + "Unknown transfer function {}: {}", + tf.value, + self.cx.error3(tf.span) + ); + None + } + }, + }; let mode = match mode { Some(mode) => match mode.parse(&mut ModeParser(self.cx)) { Ok(m) => Some(m), @@ -144,6 +182,8 @@ impl Parser for OutputParser<'_> { vrr, tearing, format, + color_space, + transfer_function, }) } } diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 4883548c..3412d222 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -30,10 +30,10 @@ use { switch_to_vt, theme::{reset_colors, reset_font, reset_sizes, set_font}, video::{ - Connector, DrmDevice, connectors, drm_devices, on_connector_connected, - on_connector_disconnected, on_graphics_initialized, on_new_connector, - on_new_drm_device, set_direct_scanout_enabled, set_gfx_api, set_tearing_mode, - set_vrr_cursor_hz, set_vrr_mode, + ColorSpace, Connector, DrmDevice, TransferFunction, connectors, drm_devices, + on_connector_connected, on_connector_disconnected, on_graphics_initialized, + on_new_connector, on_new_drm_device, set_direct_scanout_enabled, set_gfx_api, + set_tearing_mode, set_vrr_cursor_hz, set_vrr_mode, }, xwayland::set_x_scaling_mode, }, @@ -588,6 +588,11 @@ impl Output { if let Some(format) = self.format { c.set_format(format); } + if self.color_space.is_some() || self.transfer_function.is_some() { + let cs = self.color_space.unwrap_or(ColorSpace::DEFAULT); + let tf = self.transfer_function.unwrap_or(TransferFunction::DEFAULT); + c.set_colors(cs, tf); + } } } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 4b3dfc8b..cfc6d0c4 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -438,6 +438,14 @@ }, "required": [] }, + "ColorSpace": { + "type": "string", + "description": "The color space of an output.\n", + "enum": [ + "default", + "bt2020" + ] + }, "ComplexShortcut": { "description": "Describes a complex shortcut.\n\n- Example:\n\n ```toml\n [complex-shortcuts.XF86AudioRaiseVolume]\n mod-mask = \"alt\"\n action = { type = \"exec\", exec = [\"pactl\", \"set-sink-volume\", \"0\", \"+10%\"] }\n ```\n", "type": "object", @@ -1152,6 +1160,14 @@ "format": { "description": "Configures the framebuffer format of this output.\n\nBy default, the format is `xrgb8888`.\n\n- Example:\n\n ```toml\n [[outputs]]\n match.serial-number = \"33K03894SL0\"\n format = \"rgb565\"\n ```\n", "$ref": "#/$defs/Format" + }, + "color-space": { + "description": "The color space of the output.\n", + "$ref": "#/$defs/ColorSpace" + }, + "transfer-function": { + "description": "The transfer function of the output.\n", + "$ref": "#/$defs/TransferFunction" } }, "required": [ @@ -1372,6 +1388,14 @@ }, "required": [] }, + "TransferFunction": { + "type": "string", + "description": "The transfer function of an output.\n", + "enum": [ + "default", + "pq" + ] + }, "Transform": { "type": "string", "description": "An output transformation.", diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 3b9c8352..51c50f46 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -617,6 +617,25 @@ The table has the following fields: The value of this field should be a boolean. + +### `ColorSpace` + +The color space of an output. + +Values of this type should be strings. + +The string should have one of the following values: + +- `default`: + + The default color space (usually sRGB). + +- `bt2020`: + + The BT.2020 color space. + + + ### `ComplexShortcut` @@ -2530,6 +2549,18 @@ The table has the following fields: The value of this field should be a [Format](#types-Format). +- `color-space` (optional): + + The color space of the output. + + The value of this field should be a [ColorSpace](#types-ColorSpace). + +- `transfer-function` (optional): + + The transfer function of the output. + + The value of this field should be a [TransferFunction](#types-TransferFunction). + ### `OutputMatch` @@ -3050,6 +3081,25 @@ The table has the following fields: The value of this field should be a string. + +### `TransferFunction` + +The transfer function of an output. + +Values of this type should be strings. + +The string should have one of the following values: + +- `default`: + + The default transfer function (usually sRGB). + +- `pq`: + + The PQ transfer function. + + + ### `Transform` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 89752fbb..8ca87a55 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -1631,6 +1631,16 @@ Output: match.serial-number = "33K03894SL0" format = "rgb565" ``` + color-space: + ref: ColorSpace + required: false + description: | + The color space of the output. + transfer-function: + ref: TransferFunction + required: false + description: | + The transfer function of the output. Transform: @@ -2762,3 +2772,25 @@ ColorManagement: The default is `false`. kind: boolean required: false + + +ColorSpace: + description: | + The color space of an output. + kind: string + values: + - value: default + description: The default color space (usually sRGB). + - value: bt2020 + description: The BT.2020 color space. + + +TransferFunction: + description: | + The transfer function of an output. + kind: string + values: + - value: default + description: The default transfer function (usually sRGB). + - value: pq + description: The PQ transfer function. diff --git a/wire/jay_randr.txt b/wire/jay_randr.txt index 4799bd62..bea7297e 100644 --- a/wire/jay_randr.txt +++ b/wire/jay_randr.txt @@ -80,6 +80,12 @@ request set_flip_margin (since = 10) { margin_ns: pod(u64), } +request set_colors (since = 15) { + output: str, + color_space: str, + transfer_function: str, +} + # events event global { @@ -160,3 +166,19 @@ event fb_format (since = 8) { event flip_margin (since = 10) { margin_ns: pod(u64), } + +event supported_color_space (since = 15) { + color_space: str, +} + +event current_color_space (since = 15) { + color_space: str, +} + +event supported_transfer_function (since = 15) { + transfer_function: str, +} + +event current_transfer_function (since = 15) { + transfer_function: str, +}