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>
162 lines
6.5 KiB
Rust
162 lines
6.5 KiB
Rust
//! 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))
|
|
}
|