diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 668af129..00e0358e 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -795,6 +795,13 @@ impl Client { }); } + pub fn connector_set_brightness(&self, connector: Connector, brightness: Option) { + self.send(&ClientMessage::ConnectorSetBrightness { + connector, + brightness, + }); + } + 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 cc94ebd1..b1bc0de3 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -538,6 +538,10 @@ pub enum ClientMessage<'a> { color_space: ColorSpace, transfer_function: TransferFunction, }, + ConnectorSetBrightness { + connector: Connector, + brightness: Option, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/video.rs b/jay-config/src/video.rs index 1f9df481..070f1b80 100644 --- a/jay-config/src/video.rs +++ b/jay-config/src/video.rs @@ -285,6 +285,22 @@ impl Connector { pub fn set_colors(self, color_space: ColorSpace, transfer_function: TransferFunction) { get!().connector_set_colors(self, color_space, transfer_function); } + + /// Sets the brightness of the output. + /// + /// By default or when `brightness` is `None`, the brightness depends on the + /// transfer function: + /// + /// - [`TransferFunction::DEFAULT`]: The maximum brightness of the output. + /// - [`TransferFunction::PQ`]: 203 cd/m^2. + /// + /// This should only be used with the PQ transfer function. If the default transfer + /// function is used, you should instead calibrate the hardware directly. + /// + /// This has no effect unless the vulkan renderer is used. + pub fn set_brightness(self, brightness: Option) { + get!().connector_set_brightness(self, brightness); + } } /// Returns all available DRM devices. diff --git a/release-notes.md b/release-notes.md index 0daeb177..48961fea 100644 --- a/release-notes.md +++ b/release-notes.md @@ -13,6 +13,7 @@ - Implement cursor-shape-v1 version 2. - Outputs can now optionally use the BT.2020/PQ color space. - Implement ext-shell version 7. +- The reference brightness of outputs can now be configured. # 1.9.1 (2025-02-13) diff --git a/src/cli/randr.rs b/src/cli/randr.rs index 15a7df3f..c71d374a 100644 --- a/src/cli/randr.rs +++ b/src/cli/randr.rs @@ -22,6 +22,7 @@ use { str::FromStr, time::Duration, }, + thiserror::Error, }; #[derive(Args, Debug)] @@ -161,6 +162,8 @@ pub enum OutputCommand { Format(FormatSettings), /// Change color settings. Colors(ColorsSettings), + /// Change the output brightness. + Brightness(BrightnessArgs), } #[derive(ValueEnum, Debug, Clone)] @@ -367,6 +370,43 @@ fn transfer_function_possible_values() -> Vec { res } +#[derive(Args, Debug, Clone)] +pub struct BrightnessArgs { + /// The brightness of standard white in cd/m^2 or `default` to use the default + /// brightness. + /// + /// The default brightness depends on the transfer function: + /// + /// - default: the maximum display brightness + /// - PQ: 203 cd/m^2. + /// + /// When using the default transfer function, you likely want to set this to `default` + /// and adjust the display hardware brightness setting instead. + /// + /// This has no effect unless the vulkan renderer is used. + #[clap(verbatim_doc_comment, value_parser = parse_brightness)] + brightness: Brightness, +} + +#[derive(Debug, Clone)] +pub enum Brightness { + Default, + Lux(f64), +} + +#[derive(Debug, Error)] +#[error("Value is neither `default` nor a floating point value")] +struct ParseBrightnessError; + +fn parse_brightness(s: &str) -> Result { + if s == "default" { + return Ok(Brightness::Default); + } + f64::from_str(s) + .map(Brightness::Lux) + .map_err(|_| ParseBrightnessError) +} + 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() }); @@ -396,7 +436,7 @@ struct Connector { pub output: Option, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] struct Output { pub scale: f64, pub width: i32, @@ -424,6 +464,8 @@ struct Output { pub current_color_space: Option, pub supported_transfer_functions: Vec, pub current_transfer_function: Option, + pub brightness_range: Option<(f64, f64)>, + pub brightness: Option, } #[derive(Copy, Clone, Debug)] @@ -684,6 +726,26 @@ impl Randr { } } } + OutputCommand::Brightness(a) => { + self.handle_error(randr, move |msg| { + eprintln!("Could not change the brightness: {}", msg); + }); + match a.brightness { + Brightness::Default => { + tc.send(jay_randr::UnsetBrightness { + self_id: randr, + output: &args.output, + }); + } + Brightness::Lux(lux) => { + tc.send(jay_randr::SetBrightness { + self_id: randr, + output: &args.output, + lux, + }); + } + } + } } tc.round_trip().await; } @@ -906,6 +968,15 @@ impl Randr { .iter() .for_each(|tf| handle_tf(tf)); } + if let Some((min, max)) = o.brightness_range { + println!(" min brightness: {:>10.4} cd/m^2", min); + println!(" max brightness: {:>10.4} cd/m^2", max); + } else { + println!(" max brightness: {:>10.4} cd/m^2 (implied)", 80.0); + } + if let Some(lux) = o.brightness { + println!(" brightness: {:>10.4} cd/m^2", lux); + } if o.modes.is_not_empty() && modes { println!(" modes:"); for mode in &o.modes { @@ -975,21 +1046,7 @@ impl Randr { serial_number: msg.serial_number.to_string(), width_mm: msg.width_mm, height_mm: msg.height_mm, - modes: Default::default(), - current_mode: None, - non_desktop: false, - vrr_capable: false, - vrr_enabled: false, - vrr_mode: VrrMode::NEVER, - vrr_cursor_hz: None, - tearing_mode: TearingMode::NEVER, - formats: vec![], - format: None, - flip_margin_ns: None, - supported_color_spaces: vec![], - current_color_space: None, - supported_transfer_functions: vec![], - current_transfer_function: None, + ..Default::default() }); }); jay_randr::NonDesktopOutput::handle(tc, randr, data.clone(), |data, msg| { @@ -997,31 +1054,13 @@ impl Randr { let c = data.connectors.last_mut().unwrap(); c.output = Some(Output { scale: 1.0, - width: 0, - height: 0, - x: 0, - y: 0, - transform: Transform::None, manufacturer: msg.manufacturer.to_string(), product: msg.product.to_string(), serial_number: msg.serial_number.to_string(), width_mm: msg.width_mm, height_mm: msg.height_mm, - modes: Default::default(), - current_mode: None, non_desktop: true, - vrr_capable: false, - vrr_enabled: false, - vrr_mode: VrrMode::NEVER, - vrr_cursor_hz: None, - tearing_mode: TearingMode::NEVER, - formats: vec![], - format: None, - flip_margin_ns: None, - supported_color_spaces: vec![], - current_color_space: None, - supported_transfer_functions: vec![], - current_transfer_function: None, + ..Default::default() }); }); jay_randr::VrrState::handle(tc, randr, data.clone(), |data, msg| { @@ -1102,6 +1141,18 @@ impl Randr { let output = c.output.as_mut().unwrap(); output.current_transfer_function = Some(msg.transfer_function.to_string()); }); + jay_randr::BrightnessRange::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.brightness_range = Some((msg.min, msg.max)); + }); + jay_randr::Brightness::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.brightness = Some(msg.lux); + }); tc.round_trip().await; let x = data.borrow_mut().clone(); x diff --git a/src/compositor.rs b/src/compositor.rs index da6f22e9..12cfd166 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -536,6 +536,7 @@ fn create_dummy_output(state: &Rc) { vrr_mode: Cell::new(VrrMode::NEVER), vrr_cursor_hz: Default::default(), tearing_mode: Cell::new(&TearingMode::Never), + brightness: Cell::new(None), }); let connector = Rc::new(DummyOutput { id: state.connector_ids.next(), diff --git a/src/config/handler.rs b/src/config/handler.rs index ba85bbab..03123f02 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1126,6 +1126,16 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_connector_set_brightness( + &self, + connector: Connector, + brightness: Option, + ) -> Result<(), CphError> { + let connector = self.get_output_node(connector)?; + connector.global.set_brightness(brightness); + Ok(()) + } + fn handle_set_vrr_mode( &self, connector: Option, @@ -2023,6 +2033,12 @@ impl ConfigProxyHandler { } => self .handle_connector_set_colors(connector, color_space, transfer_function) .wrn("connector_set_colors")?, + ClientMessage::ConnectorSetBrightness { + connector, + brightness, + } => self + .handle_connector_set_brightness(connector, brightness) + .wrn("connector_set_brightness")?, } Ok(()) } diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 4551b606..41452698 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 { - 15 + 16 } fn required_caps(&self) -> ClientCaps { diff --git a/src/ifs/jay_randr.rs b/src/ifs/jay_randr.rs index bcb5fdd0..45dc0531 100644 --- a/src/ifs/jay_randr.rs +++ b/src/ifs/jay_randr.rs @@ -32,6 +32,7 @@ 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); +const BRIGHTNESS_SINCE: Version = Version(16); impl JayRandr { pub fn new(id: JayRandrId, client: &Rc, version: Version) -> Self { @@ -187,6 +188,22 @@ impl JayRandr { color_space: node.global.bcs.get().name(), }); } + if self.version >= BRIGHTNESS_SINCE { + if let Some(lum) = node.global.luminance { + self.client.event(BrightnessRange { + self_id: self.id, + min: lum.min, + max: lum.max, + max_fall: lum.max_fall, + }); + } + if let Some(lux) = node.global.persistent.brightness.get() { + self.client.event(Brightness { + self_id: self.id, + lux, + }); + } + } } fn send_error(&self, msg: &str) { @@ -464,6 +481,26 @@ impl JayRandrRequestHandler for JayRandr { c.connector.set_colors(cs, tf); Ok(()) } + + fn set_brightness(&self, req: SetBrightness<'_>, _slf: &Rc) -> Result<(), Self::Error> { + let Some(c) = self.get_output_node(req.output) else { + return Ok(()); + }; + c.global.set_brightness(Some(req.lux)); + Ok(()) + } + + fn unset_brightness( + &self, + req: UnsetBrightness<'_>, + _slf: &Rc, + ) -> Result<(), Self::Error> { + let Some(c) = self.get_output_node(req.output) else { + return Ok(()); + }; + c.global.set_brightness(None); + Ok(()) + } } object_base! { diff --git a/src/ifs/wl_output.rs b/src/ifs/wl_output.rs index 243b6828..09b8190d 100644 --- a/src/ifs/wl_output.rs +++ b/src/ifs/wl_output.rs @@ -122,6 +122,7 @@ pub struct PersistentOutputState { pub vrr_mode: Cell<&'static VrrMode>, pub vrr_cursor_hz: Cell>, pub tearing_mode: Cell<&'static TearingMode>, + pub brightness: Cell>, } #[derive(Eq, PartialEq, Hash, Debug)] @@ -332,9 +333,21 @@ impl WlOutputGlobal { pub fn update_color_description(&self) -> bool { let mut luminance = Luminance::SRGB; let tf = match self.btf.get() { - BackendTransferFunction::Default => TransferFunction::Srgb, + BackendTransferFunction::Default => { + if let Some(brightness) = self.persistent.brightness.get() { + let output_max = match self.luminance { + None => 80.0, + Some(v) => v.max, + }; + luminance.white.0 = luminance.max.0 * brightness / output_max; + } + TransferFunction::Srgb + } BackendTransferFunction::Pq => { luminance = Luminance::ST2084_PQ; + if let Some(brightness) = self.persistent.brightness.get() { + luminance.white.0 = brightness; + } TransferFunction::St2084Pq } }; @@ -368,6 +381,12 @@ impl WlOutputGlobal { self.linear_color_description.set(cd_linear.clone()); self.color_description.set(cd.clone()).id != cd.id } + + pub fn set_brightness(&self, brightness: Option) { + self.persistent.brightness.set(brightness); + self.update_color_description(); + self.state.damage(self.pos.get()); + } } global_base!(WlOutputGlobal, WlOutput, WlOutputError); diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index 9ebc55d8..32857e54 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -119,6 +119,7 @@ impl ConnectorHandler { vrr_mode: Cell::new(self.state.default_vrr_mode.get()), vrr_cursor_hz: Cell::new(self.state.default_vrr_cursor_hz.get()), tearing_mode: Cell::new(self.state.default_tearing_mode.get()), + brightness: Cell::new(None), }); self.state .persistent_output_states diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index 5ae36506..b33b0de3 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(15), + version: s.jay_compositor.1.min(16), id: id.into(), }); self.jay_compositor.set(Some(id)); diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 709fecf3..b3924c49 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -222,6 +222,7 @@ pub struct Output { pub format: Option, pub color_space: Option, pub transfer_function: Option, + pub brightness: Option>, } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/output.rs b/toml-config/src/config/parsers/output.rs index 809ff172..d0baef16 100644 --- a/toml-config/src/config/parsers/output.rs +++ b/toml-config/src/config/parsers/output.rs @@ -14,7 +14,7 @@ use { }, }, toml::{ - toml_span::{DespanExt, Span, Spanned}, + toml_span::{DespanExt, Span, Spanned, SpannedExt}, toml_value::Value, }, }, @@ -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, transfer_function), + (color_space, transfer_function, brightness_val), ) = ext.extract(( ( opt(str("name")), @@ -68,6 +68,7 @@ impl Parser for OutputParser<'_> { ( recover(opt(str("color-space"))), recover(opt(str("transfer-function"))), + opt(val("brightness")), ), ))?; let transform = match transform { @@ -171,6 +172,15 @@ impl Parser for OutputParser<'_> { } } } + let mut brightness = None; + if let Some(value) = brightness_val { + match value.parse(&mut BrightnessParser) { + Ok(v) => brightness = Some(v), + Err(e) => { + log::warn!("Could not parse brightness setting: {}", self.cx.error(e)); + } + } + } Ok(Output { name: name.despan().map(|v| v.to_string()), match_: match_val.parse_map(&mut OutputMatchParser(self.cx))?, @@ -184,6 +194,7 @@ impl Parser for OutputParser<'_> { format, color_space, transfer_function, + brightness, }) } } @@ -228,3 +239,34 @@ impl Parser for OutputsParser<'_> { .map(|v| vec![v]) } } + +struct BrightnessParser; + +#[derive(Debug, Error)] +pub enum BrightnessParserError { + #[error(transparent)] + Expected(#[from] UnexpectedDataType), + #[error("Expected `default`")] + UnexpectedString(String), +} + +impl Parser for BrightnessParser { + type Value = Option; + type Error = BrightnessParserError; + const EXPECTED: &'static [DataType] = &[DataType::Float, DataType::Integer, DataType::String]; + + fn parse_string(&mut self, span: Span, string: &str) -> ParseResult { + if string == "default" { + return Ok(None); + } + Err(BrightnessParserError::UnexpectedString(string.to_string()).spanned(span)) + } + + fn parse_integer(&mut self, _span: Span, integer: i64) -> ParseResult { + Ok(Some(integer as _)) + } + + fn parse_float(&mut self, _span: Span, float: f64) -> ParseResult { + Ok(Some(float)) + } +} diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 3412d222..b4bf7998 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -593,6 +593,9 @@ impl Output { let tf = self.transfer_function.unwrap_or(TransferFunction::DEFAULT); c.set_colors(cs, tf); } + if let Some(brightness) = self.brightness { + c.set_brightness(brightness); + } } } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index cfc6d0c4..b9fb6ac2 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -423,6 +423,22 @@ } ] }, + "Brightness": { + "description": "The brightness setting of an output.\n", + "anyOf": [ + { + "type": "string", + "description": "The default brightness setting.\n", + "enum": [ + "default" + ] + }, + { + "type": "number", + "description": "The brightness in cd/m^2.\n" + } + ] + }, "Color": { "type": "string", "description": "A color.\n\nThe format should be one of the following:\n\n- `#rgb`\n- `#rrggbb`\n- `#rgba`\n- `#rrggbba`\n" @@ -1168,6 +1184,10 @@ "transfer-function": { "description": "The transfer function of the output.\n", "$ref": "#/$defs/TransferFunction" + }, + "brightness": { + "description": "The brightness of the output.\n\nThis setting has no effect unless the vulkan renderer is used.\n", + "$ref": "#/$defs/Brightness" } }, "required": [ diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index 51c50f46..94c4bd67 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -575,6 +575,34 @@ This table is a tagged union. The variant is determined by the `type` field. It The value of this field should be a [DrmDeviceMatch](#types-DrmDeviceMatch). + +### `Brightness` + +The brightness setting of an output. + +Values of this type should have one of the following forms: + +#### A string + +The default brightness setting. + +The string should have one of the following values: + +- `default`: + + The default brightness setting. + + The behavior depends on the transfer function: + + - `default`: The maximum brightness of the output. + - `PQ`: 203 cd/m^2 + + +#### A number + +The brightness in cd/m^2. + + ### `Color` @@ -2561,6 +2589,14 @@ The table has the following fields: The value of this field should be a [TransferFunction](#types-TransferFunction). +- `brightness` (optional): + + The brightness of the output. + + This setting has no effect unless the vulkan renderer is used. + + The value of this field should be a [Brightness](#types-Brightness). + ### `OutputMatch` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index 8ca87a55..99d52cf2 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -1641,6 +1641,13 @@ Output: required: false description: | The transfer function of the output. + brightness: + ref: Brightness + required: false + description: | + The brightness of the output. + + This setting has no effect unless the vulkan renderer is used. Transform: @@ -2794,3 +2801,25 @@ TransferFunction: description: The default transfer function (usually sRGB). - value: pq description: The PQ transfer function. + + +Brightness: + kind: variable + description: | + The brightness setting of an output. + variants: + - kind: string + description: | + The default brightness setting. + values: + - value: default + description: | + The default brightness setting. + + The behavior depends on the transfer function: + + - `default`: The maximum brightness of the output. + - `PQ`: 203 cd/m^2 + - kind: number + description: | + The brightness in cd/m^2. diff --git a/wire/jay_randr.txt b/wire/jay_randr.txt index bea7297e..a318047d 100644 --- a/wire/jay_randr.txt +++ b/wire/jay_randr.txt @@ -86,6 +86,15 @@ request set_colors (since = 15) { transfer_function: str, } +request set_brightness (since = 16) { + output: str, + lux: pod(f64), +} + +request unset_brightness (since = 16) { + output: str, +} + # events event global { @@ -182,3 +191,13 @@ event supported_transfer_function (since = 15) { event current_transfer_function (since = 15) { transfer_function: str, } + +event brightness_range (since = 16) { + min: pod(f64), + max: pod(f64), + max_fall: pod(f64), +} + +event brightness (since = 16) { + lux: pod(f64), +}