use super::{Conn, MonitorId, MonitorInfo, PhysicalRect, X11Shell, ffi};
struct Crtc {
id: u32,
x: i16,
y: i16,
width: u16,
height: u16,
mode: u32,
output: Option<u32>,
refresh_millihertz: u32,
}
impl X11Shell {
pub(super) fn enumerate_monitors(&mut self) -> Vec<MonitorInfo> {
let scale = self.scale_factor;
let work_area = self.conn.read_work_area();
let Some(crtcs) = self.conn.read_crtcs() else {
return vec![self.fallback_monitor(scale, work_area)];
};
if crtcs.is_empty() {
return vec![self.fallback_monitor(scale, work_area)];
}
let primary_output = self.conn.read_primary_output();
let mut monitors = Vec::with_capacity(crtcs.len());
for crtc in crtcs {
let bounds = PhysicalRect::new(
i32::from(crtc.x),
i32::from(crtc.y),
u32::from(crtc.width),
u32::from(crtc.height),
);
let (name, is_primary) = crtc.output.map_or_else(
|| (format!("CRTC-{}", crtc.id), false),
|output| {
(
self.conn.read_output_name(output),
primary_output == Some(output),
)
},
);
monitors.push(MonitorInfo {
id: self.monitor_id_for(crtc.id),
name,
bounds,
work_area: work_area.map_or(bounds, |area| intersect(bounds, area)),
scale_factor: scale,
refresh_millihertz: crtc.refresh_millihertz,
is_primary,
});
}
if !monitors.iter().any(|monitor| monitor.is_primary)
&& let Some(first) = monitors.first_mut()
{
first.is_primary = true;
}
monitors
}
fn fallback_monitor(&mut self, scale: f64, work_area: Option<PhysicalRect>) -> MonitorInfo {
let bounds = PhysicalRect::new(
0,
0,
self.conn.screen_size.width,
self.conn.screen_size.height,
);
MonitorInfo {
id: self.monitor_id_for(0),
name: "screen".to_string(),
bounds,
work_area: work_area.map_or(bounds, |area| intersect(bounds, area)),
scale_factor: scale,
refresh_millihertz: 0,
is_primary: true,
}
}
fn monitor_id_for(&mut self, crtc: u32) -> MonitorId {
if let Some((_, id)) = self.monitor_ids.iter().find(|(known, _)| *known == crtc) {
return *id;
}
let id = MonitorId(self.next_monitor_id);
self.next_monitor_id += 1;
self.monitor_ids.push((crtc, id));
id
}
}
fn refresh_millihertz(info: &ffi::RandrModeInfo) -> u32 {
let total = u64::from(info.htotal) * u64::from(info.vtotal);
if total == 0 {
return 0;
}
const INTERLACE: u32 = 1 << 4;
const DOUBLE_SCAN: u32 = 1 << 5;
let mut millihertz = u64::from(info.dot_clock) * 1_000 / total;
if info.mode_flags & DOUBLE_SCAN != 0 {
millihertz /= 2;
}
if info.mode_flags & INTERLACE != 0 {
millihertz *= 2;
}
u32::try_from(millihertz).unwrap_or(0)
}
impl Conn {
fn read_crtcs(&self) -> Option<Vec<Crtc>> {
let randr = self.ext.randr?;
self.randr_event_base?;
let resources = unsafe {
let cookie = (randr.get_screen_resources_current)(self.raw(), self.root);
(randr.get_screen_resources_current_reply)(self.raw(), cookie, core::ptr::null_mut())
};
if resources.is_null() {
return None;
}
let (config_timestamp, ids) = unsafe {
let count = usize::from((*resources).num_crtcs);
let crtcs = (randr.resources_crtcs)(resources);
let ids = if count > 0 && !crtcs.is_null() {
core::slice::from_raw_parts(crtcs, count).to_vec()
} else {
Vec::new()
};
((*resources).config_timestamp, ids)
};
let modes: Vec<(u32, u32)> = unsafe {
let count = usize::from((*resources).num_modes);
let modes = (randr.resources_modes)(resources);
if count == 0 || modes.is_null() {
Vec::new()
} else {
core::slice::from_raw_parts(modes, count)
.iter()
.map(|info| (info.id, refresh_millihertz(info)))
.collect()
}
};
unsafe { ffi::free_reply(resources) };
let cookies: Vec<(u32, ffi::Cookie)> = ids
.iter()
.map(|id| {
(*id, unsafe {
(randr.get_crtc_info)(self.raw(), *id, config_timestamp)
})
})
.collect();
let mut crtcs = Vec::new();
for (id, cookie) in cookies {
let reply =
unsafe { (randr.get_crtc_info_reply)(self.raw(), cookie, core::ptr::null_mut()) };
if reply.is_null() {
continue;
}
let crtc = unsafe {
let info = *reply;
let outputs = (randr.crtc_outputs)(reply);
let output = (info.num_outputs > 0 && !outputs.is_null()).then(|| *outputs);
Crtc {
id,
x: info.x,
y: info.y,
width: info.width,
height: info.height,
mode: info.mode,
output,
refresh_millihertz: modes
.iter()
.find(|(id, _)| *id == info.mode)
.map_or(0, |(_, millihertz)| *millihertz),
}
};
unsafe { ffi::free_reply(reply) };
if crtc.mode != 0 && crtc.width > 0 && crtc.height > 0 {
crtcs.push(crtc);
}
}
Some(crtcs)
}
fn read_output_name(&self, output: u32) -> String {
let Some(randr) = self.ext.randr else {
return format!("output-{output}");
};
let reply = unsafe {
let cookie = (randr.get_output_info)(self.raw(), output, 0);
(randr.get_output_info_reply)(self.raw(), cookie, core::ptr::null_mut())
};
if reply.is_null() {
return format!("output-{output}");
}
let name = unsafe {
let length = usize::from((*reply).name_len);
let bytes = (randr.output_name)(reply);
if length == 0 || bytes.is_null() {
None
} else {
Some(
core::slice::from_raw_parts(bytes, length)
.iter()
.map(|byte| char::from(*byte))
.collect::<String>(),
)
}
};
unsafe { ffi::free_reply(reply) };
name.unwrap_or_else(|| format!("output-{output}"))
}
fn read_primary_output(&self) -> Option<u32> {
let randr = self.ext.randr?;
let reply = unsafe {
let cookie = (randr.get_output_primary)(self.raw(), self.root);
(randr.get_output_primary_reply)(self.raw(), cookie, core::ptr::null_mut())
};
if reply.is_null() {
return None;
}
let output = unsafe { (*reply).output };
unsafe { ffi::free_reply(reply) };
(output != 0).then_some(output)
}
fn read_work_area(&self) -> Option<PhysicalRect> {
let words = self.get_property_words(self.root, self.atoms.net_workarea);
let [x, y, width, height, ..] = words[..] else {
return None;
};
if width == 0 || height == 0 {
return None;
}
Some(PhysicalRect::new(
i32::try_from(x).unwrap_or(0),
i32::try_from(y).unwrap_or(0),
width,
height,
))
}
}
fn intersect(bounds: PhysicalRect, area: PhysicalRect) -> PhysicalRect {
let left = bounds.x.max(area.x);
let top = bounds.y.max(area.y);
let right = (bounds.x.saturating_add_unsigned(bounds.width))
.min(area.x.saturating_add_unsigned(area.width));
let bottom = (bounds.y.saturating_add_unsigned(bounds.height))
.min(area.y.saturating_add_unsigned(area.height));
if right <= left || bottom <= top {
return bounds;
}
PhysicalRect::new(left, top, right.abs_diff(left), bottom.abs_diff(top))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_work_area_clips_the_monitor_it_overlaps() {
let bounds = PhysicalRect::new(0, 0, 1920, 1080);
let area = PhysicalRect::new(0, 32, 1920, 1048);
assert_eq!(
intersect(bounds, area),
PhysicalRect::new(0, 32, 1920, 1048)
);
}
#[test]
fn a_second_monitor_outside_the_work_area_keeps_its_full_bounds() {
let right = PhysicalRect::new(1920, 0, 1280, 1024);
let area = PhysicalRect::new(0, 32, 1920, 1048);
assert_eq!(intersect(right, area), right);
}
#[test]
fn a_partial_overlap_keeps_only_the_overlapping_part() {
let bounds = PhysicalRect::new(-800, -100, 1600, 1200);
let area = PhysicalRect::new(0, 0, 1920, 1080);
assert_eq!(intersect(bounds, area), PhysicalRect::new(0, 0, 800, 1080));
}
fn mode_with(dot_clock: u32, htotal: u16, vtotal: u16, mode_flags: u32) -> ffi::RandrModeInfo {
ffi::RandrModeInfo {
id: 1,
width: 1920,
height: 1080,
dot_clock,
hsync_start: 0,
hsync_end: 0,
htotal,
hskew: 0,
vsync_start: 0,
vsync_end: 0,
vtotal,
name_len: 0,
mode_flags,
}
}
fn mode(dot_clock: u32, htotal: u16, vtotal: u16) -> ffi::RandrModeInfo {
mode_with(dot_clock, htotal, vtotal, 0)
}
#[test]
fn the_refresh_of_a_59_94_mode_is_not_60() {
assert_eq!(refresh_millihertz(&mode(148_352_000, 2200, 1125)), 59_940);
assert_eq!(refresh_millihertz(&mode(148_500_000, 2200, 1125)), 60_000);
}
#[test]
fn interlace_doubles_the_rate_and_double_scan_halves_it() {
const INTERLACE: u32 = 1 << 4;
const DOUBLE_SCAN: u32 = 1 << 5;
let plain = refresh_millihertz(&mode(148_500_000, 2200, 1125));
assert_eq!(
refresh_millihertz(&mode_with(148_500_000, 2200, 1125, INTERLACE)),
plain * 2,
);
assert_eq!(
refresh_millihertz(&mode_with(148_500_000, 2200, 1125, DOUBLE_SCAN)),
plain / 2,
);
assert_eq!(
refresh_millihertz(&mode_with(148_500_000, 2200, 1125, INTERLACE | DOUBLE_SCAN)),
plain,
);
}
#[test]
fn a_mode_with_no_timing_reports_zero() {
assert_eq!(refresh_millihertz(&mode(148_500_000, 0, 1125)), 0);
assert_eq!(refresh_millihertz(&mode(148_500_000, 2200, 0)), 0);
assert_eq!(refresh_millihertz(&mode(0, 2200, 1125)), 0);
}
}