1
0
Fork 0
forked from wry/wry

config: make the blend space configurable

This commit is contained in:
Julian Orth 2025-09-05 19:19:54 +02:00
parent 991b212120
commit 39c770f6e2
20 changed files with 257 additions and 15 deletions

View file

@ -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<f64>) {
self.send(&ClientMessage::ConnectorSetBrightness {
connector,

View file

@ -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)]

View file

@ -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);
}

View file

@ -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();

View file

@ -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<Brightness, ParseBrightnessError> {
.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<PossibleValue> {
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<String>,
pub brightness_range: Option<(f64, f64)>,
pub brightness: Option<f64>,
pub blend_space: Option<String>,
}
#[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()
}

View file

@ -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<State>) {
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,

View file

@ -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 {

View file

@ -79,7 +79,7 @@ impl Global for JayCompositorGlobal {
}
fn version(&self) -> u32 {
20
21
}
fn required_caps(&self) -> ClientCaps {

View file

@ -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<Client>, 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<Self>) -> 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! {

View file

@ -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<Transform>,
pub scale: Cell<crate::scale::Scale>,
@ -123,6 +139,7 @@ pub struct PersistentOutputState {
pub vrr_cursor_hz: Cell<Option<f64>>,
pub tearing_mode: Cell<&'static TearingMode>,
pub brightness: Cell<Option<f64>>,
pub blend_space: Cell<BlendSpace>,
}
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),
}
}
}

View file

@ -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

View file

@ -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));

View file

@ -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<Rc<dyn StackedNode>>,

View file

@ -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<ColorSpace>,
pub eotf: Option<Eotf>,
pub brightness: Option<Option<f64>>,
pub blend_space: Option<BlendSpace>,
}
#[derive(Debug, Clone)]

View file

@ -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,
})
}
}

View file

@ -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);
}
}
}

View file

@ -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": [

View file

@ -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.
<a name="types-BlendSpace"></a>
### `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.
<a name="types-Brightness"></a>
### `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).
<a name="types-OutputMatch"></a>
### `OutputMatch`

View file

@ -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.

View file

@ -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,
}