1
0
Fork 0
forked from wry/wry

cmm: enable using the display primaries in SDR mode

This commit is contained in:
Julian Orth 2025-12-04 17:08:05 +01:00
parent 2b7b3b5310
commit 67760e270e
19 changed files with 259 additions and 21 deletions

View file

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

View file

@ -816,6 +816,10 @@ pub enum ClientMessage<'a> {
position: BarPosition,
},
GetBarPosition,
ConnectorSetUseNativeGamut {
connector: Connector,
use_native_gamut: bool,
},
}
#[derive(Serialize, Deserialize, Debug)]

View file

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

View file

@ -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<f64>,
pub blend_space: Option<String>,
pub native_gamut: Option<Primaries>,
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()
}

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

View file

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

View file

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

View file

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

View file

@ -140,6 +140,7 @@ pub struct PersistentOutputState {
pub tearing_mode: Cell<&'static TearingMode>,
pub brightness: Cell<Option<f64>>,
pub blend_space: Cell<BlendSpace>,
pub use_native_gamut: Cell<bool>,
}
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,

View file

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

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(22),
version: s.jay_compositor.1.min(23),
id: id.into(),
});
self.jay_compositor.set(Some(id));

View file

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

View file

@ -366,6 +366,7 @@ pub struct Output {
pub eotf: Option<Eotf>,
pub brightness: Option<Option<f64>>,
pub blend_space: Option<BlendSpace>,
pub use_native_gamut: Option<bool>,
}
#[derive(Debug, Clone)]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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