Ask the platform what colour the screen actually is
FR-DSP-8's acquisition half. `dr_plat::display` surveys the session's
displays and reduces each one's profile to an output space the pipeline
can encode into, stating the mechanism per display server as
FR-PLAT-LIN-2 requires:
- X11 reads the `_ICC_PROFILE` / `_ICC_PROFILE_<n>` root-window
properties, enumerating and numbering the outputs through RandR,
which also yields the rectangles a window move is measured against.
- Wayland binds `wp_color_manager_v1` and asks each `wl_output` for
its image description, accepting either an ICC profile on a file
descriptor or primaries stated as chromaticities.
- Where neither answers, sRGB is assumed and the reason travels with
it as data rather than into a log, so the About page can say which
path the session is on.
A display profile is a measurement of one panel and is none of the four
spaces the pipeline knows. Rather than grow an ICC engine, the profile
is reduced to D50-adapted colorants and matched against the four; a
match that is merely nearest is marked as such and shown as such.
Verified on this machine: mutter 50 advertises the colour-management
global and reports eDP-1 as sRGB, and the same session forced onto X11
enumerates the output through RandR and correctly finds no atom.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
//! TRACES: FR-DSP-8 | FR-PLAT-LIN-2
|
||||
//! Acquisition on X11: ICC profiles as root-window properties.
|
||||
//!
|
||||
//! The mechanism is the ICC Profiles in X specification, and it predates
|
||||
//! everything else in this module by two decades. A colour manager — colord
|
||||
//! under GNOME, `xiccd`, `xcalib`, or the desktop's own — loads the profile
|
||||
//! for each output and publishes its bytes on the *root window* as a property:
|
||||
//! `_ICC_PROFILE` for the first output, `_ICC_PROFILE_1`, `_ICC_PROFILE_2` and
|
||||
//! so on for the rest, numbered by the output's position in RandR's list.
|
||||
//!
|
||||
//! Any client can read them, which is exactly why this works and exactly why
|
||||
//! [`super::icc::read_profile`] validates so carefully: a root-window property
|
||||
//! is untyped shared state that anything on the display may write.
|
||||
//!
|
||||
//! # Geometry comes from RandR, and it is what makes a move detectable
|
||||
//!
|
||||
//! X11 will also say where each output sits in the desktop's coordinate space,
|
||||
//! and will tell a client where its own window is. Those two facts together
|
||||
//! are the whole of FR-DSP-8's "updates when the window moves between
|
||||
//! displays" on this display server — the window's centre falls in one
|
||||
//! rectangle or another, and the answer changes as it is dragged. Wayland
|
||||
//! gives neither, which is why `wayland.rs` says what it says.
|
||||
|
||||
use x11rb::connection::Connection;
|
||||
use x11rb::protocol::randr::ConnectionExt as RandrExt;
|
||||
use x11rb::protocol::xproto::{AtomEnum, ConnectionExt as XExt};
|
||||
|
||||
use super::{
|
||||
nearest_by_colorants, Bounds, DisplayInfo, DisplayProfile, FallbackReason, ProfileSource,
|
||||
};
|
||||
|
||||
/// Every output the X server has, with whatever profile is published for it.
|
||||
pub fn probe() -> Result<Vec<DisplayInfo>, FallbackReason> {
|
||||
let (conn, screen_num) =
|
||||
x11rb::connect(None).map_err(|e| FallbackReason::NoConnection(format!("X11: {e}")))?;
|
||||
let root = conn
|
||||
.setup()
|
||||
.roots
|
||||
.get(screen_num)
|
||||
.ok_or_else(|| FallbackReason::NoConnection("X11: no screen".to_string()))?
|
||||
.root;
|
||||
|
||||
// RandR describes the outputs. Its absence is survivable: a server without
|
||||
// it still has a root window with `_ICC_PROFILE` on it, so fall through to
|
||||
// a single unnamed display carrying the first property rather than giving
|
||||
// up on colour management entirely.
|
||||
let outputs = enumerate(&conn, root).unwrap_or_default();
|
||||
let outputs = if outputs.is_empty() {
|
||||
vec![(String::from("display"), None)]
|
||||
} else {
|
||||
outputs
|
||||
};
|
||||
|
||||
Ok(outputs
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, (name, bounds))| DisplayInfo {
|
||||
name,
|
||||
bounds,
|
||||
profile: profile_for(&conn, root, index),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// An output's connector name and the rectangle it occupies, where RandR
|
||||
/// would say. Named so that the order of the two is fixed in one place.
|
||||
type Output = (String, Option<Bounds>);
|
||||
|
||||
/// The connected outputs, in RandR order, with their desktop rectangles.
|
||||
///
|
||||
/// Order is load-bearing rather than cosmetic: it is what numbers the
|
||||
/// `_ICC_PROFILE_<n>` properties, so reordering this list would hand every
|
||||
/// monitor its neighbour's profile.
|
||||
fn enumerate(conn: &impl Connection, root: u32) -> Result<Vec<Output>, Box<dyn std::error::Error>> {
|
||||
let resources = conn.randr_get_screen_resources_current(root)?.reply()?;
|
||||
let stamp = resources.config_timestamp;
|
||||
let mut found = Vec::new();
|
||||
for output in resources.outputs {
|
||||
let Ok(info) = conn.randr_get_output_info(output, stamp)?.reply() else {
|
||||
continue;
|
||||
};
|
||||
// `crtc == 0` is an output with no CRTC driving it: a port with
|
||||
// nothing plugged in, or a disabled monitor. It occupies no part of
|
||||
// the desktop and takes no place in the `_ICC_PROFILE_<n>` numbering.
|
||||
if info.crtc == 0 {
|
||||
continue;
|
||||
}
|
||||
let name = String::from_utf8_lossy(&info.name).to_string();
|
||||
let bounds = conn
|
||||
.randr_get_crtc_info(info.crtc, stamp)
|
||||
.ok()
|
||||
.and_then(|c| c.reply().ok())
|
||||
.map(|c| Bounds {
|
||||
x: i32::from(c.x),
|
||||
y: i32::from(c.y),
|
||||
width: u32::from(c.width),
|
||||
height: u32::from(c.height),
|
||||
});
|
||||
found.push((name, bounds));
|
||||
}
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
/// The property this output's profile would be published on, read and reduced.
|
||||
fn profile_for(conn: &impl Connection, root: u32, index: usize) -> DisplayProfile {
|
||||
// The specification's numbering: the first output's property carries no
|
||||
// suffix. Writing `_ICC_PROFILE_0` instead reads nothing on every desktop.
|
||||
let name = if index == 0 {
|
||||
"_ICC_PROFILE".to_string()
|
||||
} else {
|
||||
format!("_ICC_PROFILE_{index}")
|
||||
};
|
||||
|
||||
let bytes = match fetch(conn, root, &name) {
|
||||
Ok(Some(bytes)) => bytes,
|
||||
// No property. The overwhelmingly common case on a stock desktop with
|
||||
// no calibration installed, and not a fault of any kind.
|
||||
Ok(None) => return DisplayProfile::fallback(FallbackReason::NoProfileForDisplay),
|
||||
Err(e) => return DisplayProfile::fallback(FallbackReason::Unreadable(format!("X11: {e}"))),
|
||||
};
|
||||
|
||||
match super::icc::read_profile(&bytes) {
|
||||
Ok(summary) => {
|
||||
let (space, exact) = nearest_by_colorants(&summary.colorants);
|
||||
DisplayProfile {
|
||||
space,
|
||||
source: ProfileSource::X11RootProperty(name),
|
||||
described_as: summary.description,
|
||||
approximated: !exact,
|
||||
}
|
||||
}
|
||||
Err(why) => DisplayProfile::fallback(FallbackReason::Unreadable(why)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a root-window property whole.
|
||||
///
|
||||
/// The length is asked for in 32-bit units, which is X11's unit for this call
|
||||
/// and a trap worth naming: a display profile is a few kilobytes and a request
|
||||
/// stated in bytes would truncate it into a shape `read_profile` then rejects
|
||||
/// as corrupt. `u32::MAX / 4` asks for all of it.
|
||||
fn fetch(
|
||||
conn: &impl Connection,
|
||||
root: u32,
|
||||
name: &str,
|
||||
) -> Result<Option<Vec<u8>>, Box<dyn std::error::Error>> {
|
||||
let atom = conn.intern_atom(true, name.as_bytes())?.reply()?.atom;
|
||||
// `only_if_exists` above: an atom that no one has interned is a property
|
||||
// no one has set, and interning it ourselves would litter the server with
|
||||
// atoms for the monitors this desktop does not have.
|
||||
if atom == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let reply = conn
|
||||
.get_property(false, root, atom, AtomEnum::CARDINAL, 0, u32::MAX / 4)?
|
||||
.reply()?;
|
||||
if reply.value.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(reply.value))
|
||||
}
|
||||
Reference in New Issue
Block a user