diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 61daafa2..c8fbf901 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1124,6 +1124,13 @@ impl ConfigClient { }); } + pub fn connector_set_use_native_gamut(&self, connector: Connector, use_native_gamut: bool) { + self.send(&ClientMessage::ConnectorSetUseNativeGamut { + connector, + use_native_gamut, + }); + } + 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 87661413..f9e1889c 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -816,6 +816,10 @@ pub enum ClientMessage<'a> { position: BarPosition, }, GetBarPosition, + ConnectorSetUseNativeGamut { + connector: Connector, + use_native_gamut: bool, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/video.rs b/jay-config/src/video.rs index 1ad31411..dd5b6829 100644 --- a/jay-config/src/video.rs +++ b/jay-config/src/video.rs @@ -338,6 +338,26 @@ impl Connector { pub fn connector_in_direction(self, direction: Direction) -> Connector { get!(Connector(0)).get_connector_in_direction(self, direction) } + + /// Configures whether the display primaries are used. + /// + /// By default, Jay pretends that the display uses sRGB primaries. This is also how + /// most other systems behave. In reality, most displays use a much larger gamut. For + /// example, they advertise that they support 95% of the DCI-P3 gamut. If the display + /// is interpreting colors in their native gamut, then colors will appear more + /// saturated than their specification. + /// + /// If this is set to `true`, Jay assumes that the display uses the primaries + /// advertised in its EDID. This might produce more accurate colors while also + /// allowing color-managed applications to use the full gamut of the display. + /// + /// This setting has no effect when the display is explicitly operating in a wide + /// color space. + /// + /// The default is `false`. + pub fn set_use_native_gamut(self, use_native_gamut: bool) { + get!().connector_set_use_native_gamut(self, use_native_gamut); + } } /// Returns all available DRM devices. diff --git a/src/cli/randr.rs b/src/cli/randr.rs index b5b976ce..35ee8da3 100644 --- a/src/cli/randr.rs +++ b/src/cli/randr.rs @@ -2,11 +2,14 @@ use { crate::{ backend::{BackendColorSpace, BackendEotfs}, cli::GlobalArgs, + cmm::cmm_primaries::Primaries, format::{Format, XRGB8888}, ifs::wl_output::BlendSpace, scale::Scale, tools::tool_client::{Handle, ToolClient, with_tool_client}, - utils::{errorfmt::ErrorFmt, transform_ext::TransformExt}, + utils::{ + debug_fn::debug_fn, errorfmt::ErrorFmt, ordered_float::F64, transform_ext::TransformExt, + }, wire::{JayRandrId, jay_compositor, jay_randr}, }, clap::{ @@ -167,6 +170,30 @@ pub enum OutputCommand { Brightness(BrightnessArgs), /// Change the blend space. BlendSpace(BlendSpaceArgs), + /// Change whether the display primaries are used. + UseNativeGamut(UseNativeGamutArgs), +} + +#[derive(Args, Debug, Clone)] +pub struct UseNativeGamutArgs { + /// Configures whether the display primaries are used. + /// + /// By default, Jay pretends that the display uses sRGB primaries. This is also how + /// most other systems behave. In reality, most displays use a much larger gamut. For + /// example, they advertise that they support 95% of the DCI-P3 gamut. If the display + /// is interpreting colors in their native gamut, then colors will appear more + /// saturated than their specification. + /// + /// If this is set to `true`, Jay assumes that the display uses the primaries + /// advertised in its EDID. This might produce more accurate colors while also + /// allowing color-managed applications to use the full gamut of the display. + /// + /// This setting has no effect when the display is explicitly operating in a wide + /// color space. + /// + /// The default is `false`. + #[arg(action = clap::ArgAction::Set)] + pub use_native_gamut: bool, } #[derive(ValueEnum, Debug, Clone)] @@ -499,6 +526,8 @@ struct Output { pub brightness_range: Option<(f64, f64)>, pub brightness: Option, pub blend_space: Option, + pub native_gamut: Option, + pub use_native_gamut: bool, } #[derive(Copy, Clone, Debug)] @@ -789,6 +818,19 @@ impl Randr { blend_space: &a.blend_space, }); } + OutputCommand::UseNativeGamut(a) => { + self.handle_error(randr, move |msg| { + eprintln!( + "Could not change whether the compositor uses the native gamut: {}", + msg, + ); + }); + tc.send(jay_randr::SetUseNativeGamut { + self_id: randr, + output: &args.output, + use_native_gamut: a.use_native_gamut as _, + }); + } } tc.round_trip().await; } @@ -1024,6 +1066,25 @@ impl Randr { if let Some(bs) = &o.blend_space { println!(" blend space: {bs}"); } + if let Some(p) = &o.native_gamut { + println!( + " native gamut:{}", + debug_fn(|f| { + if o.use_native_gamut { + f.write_str(" (used for default color space)")?; + } + Ok(()) + }), + ); + println!( + " red: {:.6} {:.6} green: {:.6} {:.6}", + p.r.0.0, p.r.1.0, p.g.0.0, p.g.1.0 + ); + println!( + " blue: {:.6} {:.6} white: {:.6} {:.6}", + p.b.0.0, p.b.1.0, p.wp.0.0, p.wp.1.0 + ); + } if o.modes.is_not_empty() && modes { println!(" modes:"); for mode in &o.modes { @@ -1204,6 +1265,24 @@ impl Randr { let output = c.output.as_mut().unwrap(); output.blend_space = Some(msg.blend_space.to_string()); }); + jay_randr::NativeGamut::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(); + let primaries = Primaries { + r: (F64(msg.r_x), F64(msg.r_y)), + g: (F64(msg.g_x), F64(msg.g_y)), + b: (F64(msg.b_x), F64(msg.b_y)), + wp: (F64(msg.w_x), F64(msg.w_y)), + }; + output.native_gamut = Some(primaries); + }); + jay_randr::UseNativeGamut::handle(tc, randr, data.clone(), |data, _| { + let mut data = data.borrow_mut(); + let c = data.connectors.last_mut().unwrap(); + let output = c.output.as_mut().unwrap(); + output.use_native_gamut = true; + }); tc.round_trip().await; data.borrow_mut().clone() } diff --git a/src/compositor.rs b/src/compositor.rs index 5dd23473..f5b61b1c 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -33,7 +33,7 @@ use { HeadManagers, HeadState, jay_head_manager_session_v1::handle_jay_head_manager_done, }, jay_screencast::{perform_screencast_realloc, perform_toplevel_screencasts}, - wl_output::{BlendSpace, OutputId, PersistentOutputState, WlOutputGlobal}, + wl_output::{OutputId, PersistentOutputState, WlOutputGlobal}, wl_seat::handle_position_hint_requests, wl_surface::{ NoneSurfaceExt, xdg_surface::handle_xdg_surface_configure_events, @@ -629,16 +629,7 @@ fn create_dummy_output(state: &Rc) { model: "jay-dummy-output".to_string(), serial_number: "".to_string(), }); - let persistent_state = Rc::new(PersistentOutputState { - transform: Default::default(), - scale: Default::default(), - pos: Default::default(), - vrr_mode: Cell::new(VrrMode::NEVER), - vrr_cursor_hz: Default::default(), - tearing_mode: Cell::new(&TearingMode::Never), - brightness: Cell::new(None), - blend_space: Cell::new(BlendSpace::Srgb), - }); + let persistent_state = Rc::new(PersistentOutputState::default()); let mode = backend::Mode { width: 0, height: 0, diff --git a/src/config/handler.rs b/src/config/handler.rs index 492eb358..639b8cfc 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1353,6 +1353,16 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_connector_set_use_native_gamut( + &self, + connector: Connector, + use_native_gamut: bool, + ) -> Result<(), CphError> { + let connector = self.get_output_node(connector)?; + connector.set_use_native_gamut(use_native_gamut); + Ok(()) + } + fn handle_set_float_above_fullscreen(&self, above: bool) { self.state.float_above_fullscreen.set(above); for seat in self.state.globals.seats.lock().values() { @@ -3316,6 +3326,12 @@ impl ConfigProxyHandler { ClientMessage::SeatEnableUnicodeInput { seat } => self .handle_seat_enable_unicode_input(seat) .wrn("seat_enable_unicode_input")?, + ClientMessage::ConnectorSetUseNativeGamut { + connector, + use_native_gamut, + } => self + .handle_connector_set_use_native_gamut(connector, use_native_gamut) + .wrn("connector_set_use_native_gamut")?, } Ok(()) } diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 7e1dde99..46aa4393 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -79,7 +79,7 @@ impl Global for JayCompositorGlobal { } fn version(&self) -> u32 { - 22 + 23 } fn required_caps(&self) -> ClientCaps { diff --git a/src/ifs/jay_randr.rs b/src/ifs/jay_randr.rs index 89b7fe99..8ba940ac 100644 --- a/src/ifs/jay_randr.rs +++ b/src/ifs/jay_randr.rs @@ -36,6 +36,7 @@ const FLIP_MARGIN_SINCE: Version = Version(10); const COLORIMETRY_SINCE: Version = Version(15); const BRIGHTNESS_SINCE: Version = Version(16); const BLEND_SPACE_SINCE: Version = Version(21); +const NATIVE_GAMUT_SINCE: Version = Version(23); impl JayRandr { pub fn new(id: JayRandrId, client: &Rc, version: Version) -> Self { @@ -215,6 +216,23 @@ impl JayRandr { blend_space: node.global.persistent.blend_space.get().name(), }); } + if self.version >= NATIVE_GAMUT_SINCE { + let p = &node.global.primaries; + self.client.event(NativeGamut { + self_id: self.id, + r_x: p.r.0.0, + r_y: p.r.1.0, + g_x: p.g.0.0, + g_y: p.g.1.0, + b_x: p.b.0.0, + b_y: p.b.1.0, + w_x: p.wp.0.0, + w_y: p.wp.1.0, + }); + if node.global.persistent.use_native_gamut.get() { + self.client.event(UseNativeGamut { self_id: self.id }); + } + } } fn send_error(&self, msg: &str) { @@ -551,6 +569,18 @@ impl JayRandrRequestHandler for JayRandr { c.set_blend_space(space); Ok(()) } + + fn set_use_native_gamut( + &self, + req: SetUseNativeGamut<'_>, + _slf: &Rc, + ) -> Result<(), Self::Error> { + let Some(c) = self.get_output_node(req.output) else { + return Ok(()); + }; + c.set_use_native_gamut(req.use_native_gamut != 0); + Ok(()) + } } object_base! { diff --git a/src/ifs/wl_output.rs b/src/ifs/wl_output.rs index 40c8e753..dd055b61 100644 --- a/src/ifs/wl_output.rs +++ b/src/ifs/wl_output.rs @@ -140,6 +140,7 @@ pub struct PersistentOutputState { pub tearing_mode: Cell<&'static TearingMode>, pub brightness: Cell>, pub blend_space: Cell, + pub use_native_gamut: Cell, } impl Default for PersistentOutputState { @@ -153,6 +154,7 @@ impl Default for PersistentOutputState { tearing_mode: Cell::new(&TearingMode::Never), brightness: Default::default(), blend_space: Cell::new(BlendSpace::Srgb), + use_native_gamut: Cell::new(false), } } } @@ -384,18 +386,25 @@ impl WlOutputGlobal { let target_primaries; match self.bcs.get() { BackendColorSpace::Default => { - named_primaries = NamedPrimaries::Srgb; - primaries = named_primaries.primaries(); + if self.persistent.use_native_gamut.get() + && self.primaries != NamedPrimaries::Srgb.primaries() + { + named_primaries = None; + primaries = self.primaries; + } else { + named_primaries = Some(NamedPrimaries::Srgb); + primaries = NamedPrimaries::Srgb.primaries(); + } target_primaries = primaries; } BackendColorSpace::Bt2020 => { - named_primaries = NamedPrimaries::Bt2020; - primaries = named_primaries.primaries(); + named_primaries = Some(NamedPrimaries::Bt2020); + primaries = NamedPrimaries::Bt2020.primaries(); target_primaries = self.primaries; } } let cd = self.state.color_manager.get_description( - Some(named_primaries), + named_primaries, primaries, luminance, tf, diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index 54deab43..4b24cba5 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -184,6 +184,7 @@ impl ConnectorHandler { tearing_mode: Cell::new(self.state.default_tearing_mode.get()), brightness: Cell::new(None), blend_space: Cell::new(BlendSpace::Srgb), + use_native_gamut: Cell::new(false), }); self.state .persistent_output_states diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index 7a66b935..20b0eb48 100644 --- a/src/tools/tool_client.rs +++ b/src/tools/tool_client.rs @@ -335,7 +335,7 @@ impl ToolClient { self_id: s.registry, name: s.jay_compositor.0, interface: JayCompositor.name(), - version: s.jay_compositor.1.min(22), + version: s.jay_compositor.1.min(23), id: id.into(), }); self.jay_compositor.set(Some(id)); diff --git a/src/tree/output.rs b/src/tree/output.rs index 74640db3..ff87fa99 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -1005,6 +1005,17 @@ impl OutputNode { } } + pub fn set_use_native_gamut(&self, use_native_gamut: bool) { + let old = self + .global + .persistent + .use_native_gamut + .replace(use_native_gamut); + if old != use_native_gamut { + self.update_color_description(); + } + } + pub fn set_blend_space(&self, blend_space: BlendSpace) { let old = self.global.persistent.blend_space.replace(blend_space); if old != blend_space { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index fa49dd4e..182ebaf4 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -366,6 +366,7 @@ pub struct Output { pub eotf: Option, pub brightness: Option>, pub blend_space: Option, + pub use_native_gamut: Option, } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/output.rs b/toml-config/src/config/parsers/output.rs index b39cc4f8..120bfe25 100644 --- a/toml-config/src/config/parsers/output.rs +++ b/toml-config/src/config/parsers/output.rs @@ -3,7 +3,7 @@ use { config::{ Output, context::Context, - extractor::{Extractor, ExtractorError, fltorint, opt, recover, s32, str, val}, + extractor::{Extractor, ExtractorError, bol, fltorint, opt, recover, s32, str, val}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parsers::{ format::FormatParser, @@ -51,7 +51,7 @@ impl Parser for OutputParser<'_> { let mut ext = Extractor::new(self.cx, span, table); let ( (name, match_val, x, y, scale, transform, mode, vrr_val, tearing_val, format_val), - (color_space, eotf, brightness_val, blend_space), + (color_space, eotf, brightness_val, blend_space, use_native_gamut), ) = ext.extract(( ( opt(str("name")), @@ -70,6 +70,7 @@ impl Parser for OutputParser<'_> { recover(opt(str("transfer-function"))), opt(val("brightness")), recover(opt(str("blend-space"))), + recover(opt(bol("use-native-gamut"))), ), ))?; let transform = match transform { @@ -208,6 +209,7 @@ impl Parser for OutputParser<'_> { eotf, brightness, blend_space, + use_native_gamut: use_native_gamut.despan(), }) } } diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 79949da5..5138c8c8 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -862,6 +862,9 @@ impl Output { if let Some(bs) = self.blend_space { c.set_blend_space(bs); } + if let Some(use_native_gamut) = self.use_native_gamut { + c.set_use_native_gamut(use_native_gamut); + } } } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index d2f350cf..60af4936 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -1744,6 +1744,10 @@ "blend-space": { "description": "The blend space of the output.\n\nThe default is `srgb`.\n", "$ref": "#/$defs/BlendSpace" + }, + "use-native-gamut": { + "type": "boolean", + "description": "Configures whether the display primaries are used.\n\nBy default, Jay pretends that the display uses sRGB primaries. This is also how\nmost other systems behave. In reality, most displays use a much larger gamut. For\nexample, they advertise that they support 95% of the DCI-P3 gamut. If the display\nis interpreting colors in their native gamut, then colors will appear more\nsaturated than their specification.\n\nIf this is set to `true`, Jay assumes that the display uses the primaries\nadvertised in its EDID. This might produce more accurate colors while also\nallowing color-managed applications to use the full gamut of the display.\n\nThis setting has no effect when the display is explicitly operating in a wide\ncolor space.\n\nThe default is `false`.\n" } }, "required": [ diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 70c30733..48c01716 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -3836,6 +3836,27 @@ The table has the following fields: The value of this field should be a [BlendSpace](#types-BlendSpace). +- `use-native-gamut` (optional): + + Configures whether the display primaries are used. + + By default, Jay pretends that the display uses sRGB primaries. This is also how + most other systems behave. In reality, most displays use a much larger gamut. For + example, they advertise that they support 95% of the DCI-P3 gamut. If the display + is interpreting colors in their native gamut, then colors will appear more + saturated than their specification. + + If this is set to `true`, Jay assumes that the display uses the primaries + advertised in its EDID. This might produce more accurate colors while also + allowing color-managed applications to use the full gamut of the display. + + This setting has no effect when the display is explicitly operating in a wide + color space. + + The default is `false`. + + The value of this field should be a boolean. + ### `OutputMatch` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index fa6c122c..20dbacb3 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -2069,6 +2069,26 @@ Output: The blend space of the output. The default is `srgb`. + use-native-gamut: + kind: boolean + required: false + description: | + Configures whether the display primaries are used. + + By default, Jay pretends that the display uses sRGB primaries. This is also how + most other systems behave. In reality, most displays use a much larger gamut. For + example, they advertise that they support 95% of the DCI-P3 gamut. If the display + is interpreting colors in their native gamut, then colors will appear more + saturated than their specification. + + If this is set to `true`, Jay assumes that the display uses the primaries + advertised in its EDID. This might produce more accurate colors while also + allowing color-managed applications to use the full gamut of the display. + + This setting has no effect when the display is explicitly operating in a wide + color space. + + The default is `false`. Transform: diff --git a/wire/jay_randr.txt b/wire/jay_randr.txt index 5450dfaf..0305d734 100644 --- a/wire/jay_randr.txt +++ b/wire/jay_randr.txt @@ -100,6 +100,11 @@ request set_blend_space (since = 21) { blend_space: str, } +request set_use_native_gamut (since = 23) { + output: str, + use_native_gamut: u32, +} + # events event global { @@ -210,3 +215,17 @@ event brightness (since = 16) { event blend_space (since = 21) { blend_space: str, } + +event native_gamut (since = 23) { + r_x: pod(f64), + r_y: pod(f64), + g_x: pod(f64), + g_y: pod(f64), + b_x: pod(f64), + b_y: pod(f64), + w_x: pod(f64), + w_y: pod(f64), +} + +event use_native_gamut (since = 23) { +}