diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index fd107e82..f7eed6c0 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -28,8 +28,8 @@ use { theme::{Color, colors::Colorable, sized::Resizable}, timer::Timer, video::{ - ColorSpace, Connector, DrmDevice, Eotf, Format, GfxApi, Mode, TearingMode, Transform, - VrrMode, + BlendSpace, ColorSpace, Connector, DrmDevice, Eotf, Format, GfxApi, Mode, TearingMode, + Transform, VrrMode, connector_type::{CON_UNKNOWN, ConnectorType}, }, window::{ @@ -1050,6 +1050,13 @@ impl ConfigClient { }); } + pub fn connector_set_blend_space(&self, connector: Connector, blend_space: BlendSpace) { + self.send(&ClientMessage::ConnectorSetBlendSpace { + connector, + blend_space, + }); + } + pub fn connector_set_brightness(&self, connector: Connector, brightness: Option) { self.send(&ClientMessage::ConnectorSetBrightness { connector, diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 672132ea..aabd1343 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -12,8 +12,8 @@ use { theme::{Color, colors::Colorable, sized::Resizable}, timer::Timer, video::{ - ColorSpace, Connector, DrmDevice, Eotf, Format, GfxApi, TearingMode, Transform, - VrrMode, connector_type::ConnectorType, + BlendSpace, ColorSpace, Connector, DrmDevice, Eotf, Format, GfxApi, TearingMode, + Transform, VrrMode, connector_type::ConnectorType, }, window::{ContentType, TileState, Window, WindowMatcher, WindowType}, workspace::WorkspaceDisplayOrder, @@ -764,6 +764,10 @@ pub enum ClientMessage<'a> { SetWorkspaceDisplayOrder { order: WorkspaceDisplayOrder, }, + ConnectorSetBlendSpace { + connector: Connector, + blend_space: BlendSpace, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/video.rs b/jay-config/src/video.rs index d80b8ff7..56b8a271 100644 --- a/jay-config/src/video.rs +++ b/jay-config/src/video.rs @@ -286,6 +286,13 @@ impl Connector { get!().connector_set_colors(self, color_space, eotf); } + /// Sets the space in which blending is performed for this output. + /// + /// The default is [`BlendSpace::SRGB`] + pub fn set_blend_space(self, blend_space: BlendSpace) { + get!().connector_set_blend_space(self, blend_space); + } + /// Sets the brightness of the output. /// /// By default or when `brightness` is `None`, the brightness depends on the @@ -731,3 +738,16 @@ impl Eotf { /// The PQ EOTF. pub const PQ: Self = Self(1); } + +/// A space in which color blending is performed. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct BlendSpace(pub u32); + +impl BlendSpace { + /// The sRGB blend space with sRGB primaries and gamma22 transfer function. This is + /// the classic desktop blend space. + pub const SRGB: Self = Self(0); + /// The linear blend space performs blending in linear space, which is more physically + /// correct but leads to much lighter output when blending light and dark colors. + pub const LINEAR: Self = Self(1); +} diff --git a/src/backends/metal/present.rs b/src/backends/metal/present.rs index 9dff794b..258627ac 100644 --- a/src/backends/metal/present.rs +++ b/src/backends/metal/present.rs @@ -13,6 +13,7 @@ use { AcquireSync, BufferResv, GfxApiOpt, GfxRenderPass, GfxTexture, ReleaseSync, SyncFile, create_render_pass, }, + ifs::wl_output::BlendSpace, rect::Region, theme::Color, time::Time, @@ -201,7 +202,11 @@ impl MetalConnector { let buffer = &buffers[next_buffer_idx]; let cd = node.global.color_description.get(); - let blend_cd = self.state.color_manager.srgb_gamma22(); + let linear_cd = node.global.linear_color_description.get(); + let blend_cd = match node.global.persistent.blend_space.get() { + BlendSpace::Linear => &linear_cd, + BlendSpace::Srgb => self.state.color_manager.srgb_gamma22(), + }; if self.has_damage.get() > 0 || self.cursor_damage.get() { node.schedule.commit_cursor(); diff --git a/src/cli/randr.rs b/src/cli/randr.rs index 2cde0dd9..cec9887a 100644 --- a/src/cli/randr.rs +++ b/src/cli/randr.rs @@ -3,6 +3,7 @@ use { backend::{BackendColorSpace, BackendEotfs}, cli::GlobalArgs, format::{Format, XRGB8888}, + ifs::wl_output::BlendSpace, scale::Scale, tools::tool_client::{Handle, ToolClient, with_tool_client}, utils::{errorfmt::ErrorFmt, transform_ext::TransformExt}, @@ -164,6 +165,8 @@ pub enum OutputCommand { Colors(ColorsSettings), /// Change the output brightness. Brightness(BrightnessArgs), + /// Change the blend space. + BlendSpace(BlendSpaceArgs), } #[derive(ValueEnum, Debug, Clone)] @@ -407,6 +410,26 @@ fn parse_brightness(s: &str) -> Result { .map_err(|_| ParseBrightnessError) } +#[derive(Args, Debug, Clone)] +pub struct BlendSpaceArgs { + /// The space to blend translucent surfaces in. + #[clap(value_parser = PossibleValuesParser::new(blend_space_possible_values()))] + blend_space: String, +} + +fn blend_space_possible_values() -> Vec { + let mut res = vec![]; + for bs in BlendSpace::variants() { + use BlendSpace::*; + let help = match bs { + Linear => "Linear space, more accurate but brighter", + Srgb => "sRGB space, the classic desktop blend space", + }; + res.push(PossibleValue::new(bs.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() }); @@ -466,6 +489,7 @@ struct Output { pub current_eotf: Option, pub brightness_range: Option<(f64, f64)>, pub brightness: Option, + pub blend_space: Option, } #[derive(Copy, Clone, Debug)] @@ -743,6 +767,16 @@ impl Randr { } } } + OutputCommand::BlendSpace(a) => { + self.handle_error(randr, move |msg| { + eprintln!("Could not set the blend space: {}", msg); + }); + tc.send(jay_randr::SetBlendSpace { + self_id: randr, + output: &args.output, + blend_space: &a.blend_space, + }); + } } tc.round_trip().await; } @@ -975,6 +1009,9 @@ impl Randr { if let Some(lux) = o.brightness { println!(" brightness: {:>10.4} cd/m^2", lux); } + if let Some(bs) = &o.blend_space { + println!(" blend space: {bs}"); + } if o.modes.is_not_empty() && modes { println!(" modes:"); for mode in &o.modes { @@ -1149,6 +1186,12 @@ impl Randr { let output = c.output.as_mut().unwrap(); output.brightness = Some(msg.lux); }); + jay_randr::BlendSpace::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.blend_space = Some(msg.blend_space.to_string()); + }); tc.round_trip().await; data.borrow_mut().clone() } diff --git a/src/compositor.rs b/src/compositor.rs index b8f0b91f..f8e36ced 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::{OutputId, PersistentOutputState, WlOutputGlobal}, + wl_output::{BlendSpace, OutputId, PersistentOutputState, WlOutputGlobal}, wl_seat::handle_position_hint_requests, wl_surface::{ NoneSurfaceExt, xdg_surface::handle_xdg_surface_configure_events, @@ -636,6 +636,7 @@ fn create_dummy_output(state: &Rc) { vrr_cursor_hz: Default::default(), tearing_mode: Cell::new(&TearingMode::Never), brightness: Cell::new(None), + blend_space: Cell::new(BlendSpace::Srgb), }); let mode = backend::Mode { width: 0, diff --git a/src/config/handler.rs b/src/config/handler.rs index 9de37e4e..8b464863 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -17,6 +17,7 @@ use { }, format::config_formats, ifs::{ + wl_output::BlendSpace, wl_seat::{SeatId, WlSeatGlobal}, wp_content_type_v1::ContentTypeExt, }, @@ -69,8 +70,9 @@ use { theme::{colors::Colorable, sized::Resizable}, timer::Timer as JayTimer, video::{ - ColorSpace, Connector, DrmDevice, Eotf as ConfigEotf, Format as ConfigFormat, GfxApi, - TearingMode as ConfigTearingMode, Transform, VrrMode as ConfigVrrMode, + BlendSpace as ConfigBlendSpace, ColorSpace, Connector, DrmDevice, Eotf as ConfigEotf, + Format as ConfigFormat, GfxApi, TearingMode as ConfigTearingMode, Transform, + VrrMode as ConfigVrrMode, }, window::{TileState, Window, WindowMatcher}, workspace::WorkspaceDisplayOrder, @@ -1306,6 +1308,21 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_connector_set_blend_space( + &self, + connector: Connector, + blend_space: ConfigBlendSpace, + ) -> Result<(), CphError> { + let blend_space = match blend_space { + ConfigBlendSpace::SRGB => BlendSpace::Srgb, + ConfigBlendSpace::LINEAR => BlendSpace::Linear, + _ => return Err(CphError::UnknownBlendSpace(blend_space)), + }; + let connector = self.get_output_node(connector)?; + connector.set_blend_space(blend_space); + Ok(()) + } + fn handle_connector_set_brightness( &self, connector: Connector, @@ -3117,6 +3134,12 @@ impl ConfigProxyHandler { ClientMessage::SeatCopyMark { seat, src, dst } => self .handle_seat_copy_mark(seat, src, dst) .wrn("seat_copy_mark")?, + ClientMessage::ConnectorSetBlendSpace { + connector, + blend_space, + } => self + .handle_connector_set_blend_space(connector, blend_space) + .wrn("connector_set_blend_space")?, } Ok(()) } @@ -3226,6 +3249,8 @@ enum CphError { WindowMatcherDoesNotExist(WindowMatcher), #[error("Could not modify the connector state")] ModifyConnectorState(#[source] BackendConnectorTransactionError), + #[error("Unknown blend space {0:?}")] + UnknownBlendSpace(ConfigBlendSpace), } trait WithRequestName { diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 927be203..05aa3064 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 { - 20 + 21 } fn required_caps(&self) -> ClientCaps { diff --git a/src/ifs/jay_randr.rs b/src/ifs/jay_randr.rs index 022cb0a2..89b7fe99 100644 --- a/src/ifs/jay_randr.rs +++ b/src/ifs/jay_randr.rs @@ -4,6 +4,7 @@ use { client::{Client, ClientError}, compositor::MAX_EXTENTS, format::named_formats, + ifs::wl_output, leaks::Tracker, object::{Object, Version}, scale::Scale, @@ -34,6 +35,7 @@ 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); +const BLEND_SPACE_SINCE: Version = Version(21); impl JayRandr { pub fn new(id: JayRandrId, client: &Rc, version: Version) -> Self { @@ -207,6 +209,12 @@ impl JayRandr { }); } } + if self.version >= BLEND_SPACE_SINCE { + self.client.event(BlendSpace { + self_id: self.id, + blend_space: node.global.persistent.blend_space.get().name(), + }); + } } fn send_error(&self, msg: &str) { @@ -526,6 +534,23 @@ impl JayRandrRequestHandler for JayRandr { c.set_brightness(None); Ok(()) } + + fn set_blend_space(&self, req: SetBlendSpace<'_>, _slf: &Rc) -> Result<(), Self::Error> { + let space = 'space: { + for space in wl_output::BlendSpace::variants() { + if space.name() == req.blend_space { + break 'space space; + } + } + self.send_error(&format!("Unknown blend space: {}", req.blend_space)); + return Ok(()); + }; + let Some(c) = self.get_output_node(req.output) else { + return Ok(()); + }; + c.set_blend_space(space); + Ok(()) + } } object_base! { diff --git a/src/ifs/wl_output.rs b/src/ifs/wl_output.rs index 1715e9b9..f0dcd5b4 100644 --- a/src/ifs/wl_output.rs +++ b/src/ifs/wl_output.rs @@ -30,6 +30,7 @@ use { }, ahash::AHashMap, jay_config::video::Transform, + linearize::Linearize, std::{ cell::{Cell, RefCell}, collections::hash_map::Entry, @@ -115,6 +116,21 @@ impl OutputGlobalOpt { } } +#[derive(Copy, Clone, Debug, Eq, PartialEq, Linearize)] +pub enum BlendSpace { + Linear, + Srgb, +} + +impl BlendSpace { + pub fn name(self) -> &'static str { + match self { + BlendSpace::Linear => "linear", + BlendSpace::Srgb => "srgb", + } + } +} + pub struct PersistentOutputState { pub transform: Cell, pub scale: Cell, @@ -123,6 +139,7 @@ pub struct PersistentOutputState { pub vrr_cursor_hz: Cell>, pub tearing_mode: Cell<&'static TearingMode>, pub brightness: Cell>, + pub blend_space: Cell, } impl Default for PersistentOutputState { @@ -135,6 +152,7 @@ impl Default for PersistentOutputState { vrr_cursor_hz: Default::default(), tearing_mode: Cell::new(&TearingMode::Never), brightness: Default::default(), + blend_space: Cell::new(BlendSpace::Srgb), } } } diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index b4ba2cf4..79d707eb 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -9,7 +9,7 @@ use { ifs::{ head_management::{HeadManagers, HeadState}, jay_tray_v1::JayTrayV1Global, - wl_output::{PersistentOutputState, WlOutputGlobal}, + wl_output::{BlendSpace, PersistentOutputState, WlOutputGlobal}, }, output_schedule::OutputSchedule, state::{ConnectorData, OutputData, State}, @@ -183,6 +183,7 @@ impl ConnectorHandler { 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), + blend_space: Cell::new(BlendSpace::Srgb), }); self.state .persistent_output_states diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index 0014d743..82f013ba 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(20), + version: s.jay_compositor.1.min(21), id: id.into(), }); self.jay_compositor.set(Some(id)); diff --git a/src/tree/output.rs b/src/tree/output.rs index bd747e6b..1df18b71 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -13,7 +13,7 @@ use { jay_output::JayOutput, jay_screencast::JayScreencast, wl_buffer::WlBufferStorage, - wl_output::WlOutputGlobal, + wl_output::{BlendSpace, WlOutputGlobal}, wl_seat::{ BTN_LEFT, NodeSeatState, SeatId, WlSeatGlobal, collect_kb_foci2, tablet::{TabletTool, TabletToolChanges, TabletToolId}, @@ -971,6 +971,12 @@ impl OutputNode { } } + pub fn set_blend_space(&self, blend_space: BlendSpace) { + let old = self.global.persistent.blend_space.replace(blend_space); + if old != blend_space { + self.state.damage(self.global.position()); + } + } fn find_stacked_at( &self, stack: &LinkedList>, diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index b0ff875d..9aaca933 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -33,7 +33,7 @@ use { logging::LogLevel, status::MessageFormat, theme::Color, - video::{ColorSpace, Eotf, Format, GfxApi, TearingMode, Transform, VrrMode}, + video::{BlendSpace, ColorSpace, Eotf, Format, GfxApi, TearingMode, Transform, VrrMode}, window::{ContentType, TileState, WindowType}, workspace::WorkspaceDisplayOrder, xwayland::XScalingMode, @@ -349,6 +349,7 @@ pub struct Output { pub color_space: Option, pub eotf: Option, pub brightness: Option>, + pub blend_space: Option, } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/output.rs b/toml-config/src/config/parsers/output.rs index 527d7934..b39cc4f8 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::{ColorSpace, Eotf, Transform}, + jay_config::video::{BlendSpace, ColorSpace, Eotf, Transform}, thiserror::Error, }; @@ -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), + (color_space, eotf, brightness_val, blend_space), ) = ext.extract(( ( opt(str("name")), @@ -69,6 +69,7 @@ impl Parser for OutputParser<'_> { recover(opt(str("color-space"))), recover(opt(str("transfer-function"))), opt(val("brightness")), + recover(opt(str("blend-space"))), ), ))?; let transform = match transform { @@ -177,6 +178,21 @@ impl Parser for OutputParser<'_> { } } } + let blend_space = match blend_space { + None => None, + Some(bs) => match bs.value { + "linear" => Some(BlendSpace::LINEAR), + "srgb" => Some(BlendSpace::SRGB), + _ => { + log::warn!( + "Unknown blend space {}: {}", + bs.value, + self.cx.error3(bs.span) + ); + None + } + }, + }; Ok(Output { name: name.despan().map(|v| v.to_string()), match_: match_val.parse_map(&mut OutputMatchParser(self.cx))?, @@ -191,6 +207,7 @@ impl Parser for OutputParser<'_> { color_space, eotf, brightness, + blend_space, }) } } diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 7f7f916f..cec684db 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -777,6 +777,9 @@ impl Output { if let Some(brightness) = self.brightness { c.set_brightness(brightness); } + if let Some(bs) = self.blend_space { + c.set_blend_space(bs); + } } } diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 43379fa0..174b9f84 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -572,6 +572,14 @@ } ] }, + "BlendSpace": { + "type": "string", + "description": "A color blend space.\n", + "enum": [ + "srgb", + "linear" + ] + }, "Brightness": { "description": "The brightness setting of an output.\n", "anyOf": [ @@ -1655,6 +1663,10 @@ "brightness": { "description": "The brightness of the output.\n\nThis setting has no effect unless the vulkan renderer is used.\n", "$ref": "#/$defs/Brightness" + }, + "blend-space": { + "description": "The blend space of the output.\n\nThe default is `srgb`.\n", + "$ref": "#/$defs/BlendSpace" } }, "required": [ diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index d1e6e4e5..4f6893ea 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -797,6 +797,25 @@ This table is a tagged union. The variant is determined by the `type` field. It The value of this field should be a string. + +### `BlendSpace` + +A color blend space. + +Values of this type should be strings. + +The string should have one of the following values: + +- `srgb`: + + The sRGB blend space. This is the classic desktop blend space. + +- `linear`: + + Linear color space. This is the physically correct blend space. + + + ### `Brightness` @@ -3548,6 +3567,14 @@ The table has the following fields: The value of this field should be a [Brightness](#types-Brightness). +- `blend-space` (optional): + + The blend space of the output. + + The default is `srgb`. + + The value of this field should be a [BlendSpace](#types-BlendSpace). + ### `OutputMatch` diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index d1dcef19..b9239cca 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -1967,6 +1967,13 @@ Output: The brightness of the output. This setting has no effect unless the vulkan renderer is used. + blend-space: + ref: BlendSpace + required: false + description: | + The blend space of the output. + + The default is `srgb`. Transform: @@ -4029,3 +4036,14 @@ WorkspaceDisplayOrder: description: Workspaces are not sorted and can be manually dragged. - value: sorted description: Workspaces are sorted alphabetically and cannot be manually dragged. + + +BlendSpace: + kind: string + description: | + A color blend space. + values: + - value: srgb + description: The sRGB blend space. This is the classic desktop blend space. + - value: linear + description: Linear color space. This is the physically correct blend space. diff --git a/wire/jay_randr.txt b/wire/jay_randr.txt index 2dfba8c3..5450dfaf 100644 --- a/wire/jay_randr.txt +++ b/wire/jay_randr.txt @@ -95,6 +95,11 @@ request unset_brightness (since = 16) { output: str, } +request set_blend_space (since = 21) { + output: str, + blend_space: str, +} + # events event global { @@ -201,3 +206,7 @@ event brightness_range (since = 16) { event brightness (since = 16) { lux: pod(f64), } + +event blend_space (since = 21) { + blend_space: str, +}