use sdl3_sys::everything::*;
use std::ffi::CStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DisplayRole {
Playfield,
Backglass,
Dmd,
Topper,
Unused,
}
impl DisplayRole {
pub fn label(&self) -> &'static str {
match self {
Self::Playfield => "role_playfield",
Self::Backglass => "role_backglass",
Self::Dmd => "role_dmd",
Self::Topper => "role_topper",
Self::Unused => "role_unused",
}
}
pub fn all() -> &'static [DisplayRole] {
&[
Self::Playfield,
Self::Backglass,
Self::Dmd,
Self::Topper,
Self::Unused,
]
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct DisplayInfo {
pub name: String,
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
pub refresh_rate: f32,
pub best_mode: Option<(i32, i32, f32)>,
pub is_primary: bool,
pub total_pixels: u64,
pub size_inches: Option<u32>,
pub width_mm: i32,
pub height_mm: i32,
pub detected_width_mm: i32,
pub detected_height_mm: i32,
pub role: DisplayRole,
pub monitor_index: usize,
pub content_scale: f32,
pub native_handle: Option<usize>,
}
#[cfg(target_os = "windows")]
unsafe fn native_handle(id: SDL_DisplayID) -> Option<usize> {
let props = unsafe { SDL_GetDisplayProperties(id) };
if props == 0 {
return None;
}
let h = unsafe {
SDL_GetPointerProperty(
props,
SDL_PROP_DISPLAY_WINDOWS_HMONITOR_POINTER,
std::ptr::null_mut(),
)
};
(!h.is_null()).then_some(h as usize)
}
#[cfg(not(target_os = "windows"))]
unsafe fn native_handle(_id: SDL_DisplayID) -> Option<usize> {
None
}
fn parse_inches_from_name(name: &str) -> Option<u32> {
let trimmed = name.trim().trim_end_matches('"');
trimmed.rsplit(' ').next()?.parse().ok()
}
fn physical_sizes(displays: &[DisplayInfo]) -> Vec<(String, (i32, i32), Option<u32>)> {
let sdl: Vec<crate::display_id::SdlDisplay> = displays
.iter()
.map(|d| crate::display_id::SdlDisplay {
name: d.name.clone(),
x: d.x,
y: d.y,
width: d.width,
height: d.height,
width_mm: 0,
height_mm: 0,
})
.collect();
crate::display_id::correlate(&sdl, &crate::display_id::read_drm_monitors())
.into_iter()
.filter_map(|(name, id)| {
let (w, h) = id.phys_mm?;
let inches = id.diagonal_inches();
Some((
name,
(i32::try_from(w).unwrap_or(0), i32::try_from(h).unwrap_or(0)),
inches,
))
})
.collect()
}
pub fn size_looks_wrong(d: &DisplayInfo) -> bool {
if d.width_mm != d.detected_width_mm || d.height_mm != d.detected_height_mm {
return false;
}
let (w, h) = (d.detected_width_mm, d.detected_height_mm);
if w <= 0 || h <= 0 || w.min(h) >= 900 {
return true;
}
let generic = matches!(
d.name.trim().to_ascii_lowercase().as_str(),
"" | "monitor" | "generic monitor" | "default monitor" | "unknown"
);
let ratio = |a: f64, b: f64| a.max(b) / a.min(b);
let px = ratio(d.width as f64, d.height as f64);
let mm = ratio(w as f64, h as f64);
generic || (px - mm).abs() / px > 0.1
}
fn display_modes(id: SDL_DisplayID) -> Vec<(i32, i32, f32)> {
let mut count = 0;
unsafe {
let modes = SDL_GetFullscreenDisplayModes(id, &mut count);
if modes.is_null() {
return Vec::new();
}
let list = (0..count.max(0) as usize)
.filter_map(|i| (*modes.add(i)).as_ref())
.map(|m| (m.w, m.h, m.refresh_rate))
.collect();
SDL_free(modes as *mut _);
list
}
}
pub fn better_mode(current: (i32, i32, f32), modes: &[(i32, i32, f32)]) -> Option<(i32, i32, f32)> {
let pixels = |m: &(i32, i32, f32)| m.0 as i64 * m.1 as i64;
let best = modes
.iter()
.copied()
.filter(|m| pixels(m) >= pixels(¤t) && m.2 >= current.2 - 1.0)
.max_by(|a, b| {
pixels(a)
.cmp(&pixels(b))
.then(a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal))
})?;
let more_pixels = pixels(&best) > pixels(¤t);
let faster = best.2 > current.2 + 1.0;
(more_pixels || faster).then_some(best)
}
pub fn enumerate_displays() -> Vec<DisplayInfo> {
let mut displays = Vec::new();
unsafe {
if !SDL_InitSubSystem(SDL_INIT_VIDEO) {
let err = CStr::from_ptr(SDL_GetError()).to_string_lossy();
log::warn!("SDL_InitSubSystem(VIDEO) failed: {err}");
return displays;
}
let mut count: i32 = 0;
let display_ids = SDL_GetDisplays(&mut count);
if display_ids.is_null() || count <= 0 {
log::warn!("No displays found (count={count})");
return displays;
}
let primary_id = SDL_GetPrimaryDisplay();
for i in 0..count as usize {
let id = *display_ids.add(i);
let name_ptr = SDL_GetDisplayName(id);
let name = if !name_ptr.is_null() {
CStr::from_ptr(name_ptr).to_string_lossy().into_owned()
} else {
format!("Display {}", i + 1)
};
let mut bounds = SDL_Rect {
x: 0,
y: 0,
w: 0,
h: 0,
};
if !SDL_GetDisplayBounds(id, &mut bounds) {
continue;
}
let mode = SDL_GetDesktopDisplayMode(id);
let refresh_rate = if !mode.is_null() {
(*mode).refresh_rate
} else {
0.0
};
let total_pixels = bounds.w as u64 * bounds.h as u64;
let content_scale = match SDL_GetDisplayContentScale(id) {
s if s > 0.0 => s,
_ => 1.0,
};
let size_inches = parse_inches_from_name(&name);
displays.push(DisplayInfo {
name,
x: bounds.x,
y: bounds.y,
width: bounds.w,
height: bounds.h,
refresh_rate,
best_mode: better_mode((bounds.w, bounds.h, refresh_rate), &display_modes(id)),
is_primary: id == primary_id,
total_pixels,
size_inches,
width_mm: 0,
height_mm: 0,
detected_width_mm: 0,
detected_height_mm: 0,
role: DisplayRole::Unused,
monitor_index: displays.len(),
content_scale,
native_handle: native_handle(id),
});
}
SDL_free(display_ids as *mut _);
#[cfg(target_os = "linux")]
{
let driver_ptr = SDL_GetCurrentVideoDriver();
let is_x11 = !driver_ptr.is_null() && CStr::from_ptr(driver_ptr).to_bytes() == b"x11";
if is_x11 && crate::x11_display::is_plain_x11() {
match_winit_order(&mut displays);
}
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
match_winit_order(&mut displays);
}
for (name, (w, h), inches) in physical_sizes(&displays) {
if let Some(d) = displays.iter_mut().find(|d| d.name == name) {
d.width_mm = w;
d.height_mm = h;
d.detected_width_mm = w;
d.detected_height_mm = h;
if let Some(i) = inches {
d.size_inches = Some(i);
}
}
}
let mut sorted_indices: Vec<usize> = (0..displays.len()).collect();
sorted_indices.sort_by_key(|&i| std::cmp::Reverse(displays[i].total_pixels));
let roles = [
DisplayRole::Playfield,
DisplayRole::Backglass,
DisplayRole::Dmd,
DisplayRole::Topper,
];
for (rank, &idx) in sorted_indices.iter().enumerate() {
displays[idx].role = roles.get(rank).copied().unwrap_or(DisplayRole::Unused);
}
for d in &displays {
log::info!(
"Display: {} | {}x{} @ ({},{}) | {:?} | winit #{} | scale {}",
d.name,
d.width,
d.height,
d.x,
d.y,
d.role,
d.monitor_index,
d.content_scale
);
}
displays
}
const WIZARD_FIT_WIDTH: f32 = 0.90;
const WIZARD_FIT_HEIGHT: f32 = 0.85;
pub fn wizard_window_size(displays: &[DisplayInfo]) -> [f32; 2] {
let logical = |px: i32, scale: f32| {
let scale = if scale.is_finite() && scale > 0.0 {
scale
} else {
1.0
};
px as f32 / scale
};
let smallest = displays
.iter()
.filter(|d| d.width > 0 && d.height > 0)
.map(|d| {
[
logical(d.width, d.content_scale),
logical(d.height, d.content_scale),
]
})
.reduce(|a, b| [a[0].min(b[0]), a[1].min(b[1])]);
match smallest {
Some([w, h]) => [
(w * WIZARD_FIT_WIDTH).round(),
(h * WIZARD_FIT_HEIGHT).round(),
],
None => [1152.0, 680.0],
}
}
pub fn fit_within_monitor(inner: [f32; 2], monitor: [f32; 2]) -> Option<[f32; 2]> {
if !(monitor[0] > 0.0 && monitor[1] > 0.0) {
return None;
}
let max = [
(monitor[0] * WIZARD_FIT_WIDTH).floor(),
(monitor[1] * WIZARD_FIT_HEIGHT).floor(),
];
if inner[0] <= max[0] + 0.5 && inner[1] <= max[1] + 0.5 {
return None;
}
Some([inner[0].min(max[0]), inner[1].min(max[1])])
}
#[cfg(test)]
pub(crate) fn auto_assign_roles(displays: &mut [DisplayInfo]) {
let roles = [
DisplayRole::Playfield,
DisplayRole::Backglass,
DisplayRole::Dmd,
DisplayRole::Topper,
];
for (i, display) in displays.iter_mut().enumerate() {
display.role = roles.get(i).copied().unwrap_or(DisplayRole::Unused);
}
}
#[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))]
fn match_winit_order(displays: &mut [DisplayInfo]) {
let rects: Vec<_> = displays
.iter()
.map(|d| (d.x, d.y, d.width, d.height))
.collect();
let Some(winit) = crate::monitor_order::winit_monitor_rects() else {
log::warn!("Monitor list unavailable: assuming winit lists screens in SDL's order");
return;
};
log::info!("SDL monitor order: {rects:?}");
log::info!("winit monitor order: {winit:?}");
let handles: Option<Vec<usize>> = displays.iter().map(|d| d.native_handle).collect();
let indices = match (handles, crate::monitor_order::winit_monitor_handles()) {
(Some(sdl), Some(winit_handles)) => {
log::info!("joining SDL and winit monitors on their HMONITOR");
crate::monitor_order::handle_indices(&sdl, &winit_handles)
}
_ => crate::monitor_order::monitor_indices(&rects, &winit),
};
for (d, idx) in displays.iter_mut().zip(indices) {
if d.monitor_index != idx {
log::info!(
"Display {} is winit monitor {idx} (SDL lists it at {})",
d.name,
d.monitor_index
);
}
d.monitor_index = idx;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_display(name: &str, w: i32, h: i32) -> DisplayInfo {
DisplayInfo {
name: name.to_string(),
x: 0,
y: 0,
width: w,
height: h,
refresh_rate: 60.0,
best_mode: None,
is_primary: false,
total_pixels: w as u64 * h as u64,
size_inches: None,
width_mm: 0,
height_mm: 0,
detected_width_mm: 0,
detected_height_mm: 0,
role: DisplayRole::Unused,
monitor_index: 0,
content_scale: 1.0,
native_handle: None,
}
}
#[test]
fn a_faster_mode_at_the_same_size_is_proposed() {
let modes = [
(3840, 2160, 59.997),
(3840, 2160, 119.91),
(1920, 1080, 119.91),
];
assert_eq!(
better_mode((3840, 2160, 59.997), &modes),
Some((3840, 2160, 119.91))
);
}
#[test]
fn the_best_mode_already_in_use_proposes_nothing() {
let modes = [(3840, 2160, 119.91), (3840, 2160, 59.997)];
assert_eq!(better_mode((3840, 2160, 119.91), &modes), None);
}
#[test]
fn rounding_between_59_94_and_60_is_not_a_better_mode() {
let modes = [(1920, 1080, 60.0), (1920, 1080, 59.94)];
assert_eq!(better_mode((1920, 1080, 59.94), &modes), None);
}
#[test]
fn a_mode_that_loses_refresh_rate_is_never_proposed() {
let modes = [(3840, 2160, 30.0), (1920, 1080, 144.0)];
assert_eq!(better_mode((1920, 1080, 144.0), &modes), None);
}
fn scaled(w: i32, h: i32, scale: f32) -> DisplayInfo {
DisplayInfo {
content_scale: scale,
..make_display("x", w, h)
}
}
#[test]
fn wizard_fits_the_smallest_screen_of_a_cabinet() {
let displays = [
scaled(2160, 3840, 1.5),
scaled(1920, 1080, 1.0),
scaled(1920, 1080, 1.0),
];
let [w, h] = wizard_window_size(&displays);
assert_eq!([w, h], [1296.0, 918.0]);
assert!(w <= 1920.0 && h <= 1080.0);
assert!(w <= 1440.0 && h <= 2560.0);
}
#[test]
fn wizard_size_respects_each_screen_scale() {
let [w, h] = wizard_window_size(&[scaled(3840, 2160, 2.0)]);
assert_eq!([w, h], [1728.0, 918.0]);
let [w, h] = wizard_window_size(&[scaled(1920, 1080, 0.0)]);
assert_eq!([w, h], [1728.0, 918.0]);
}
#[test]
fn wizard_size_without_displays_is_laptop_sized() {
let [w, h] = wizard_window_size(&[]);
assert!(w <= 1280.0 && h <= 800.0);
}
#[test]
fn fit_within_monitor_shrinks_only_what_overflows() {
assert_eq!(
fit_within_monitor([1152.0, 1728.0], [1920.0, 1080.0]),
Some([1152.0, 918.0])
);
assert_eq!(
fit_within_monitor([2000.0, 600.0], [1920.0, 1080.0]),
Some([1728.0, 600.0])
);
assert_eq!(fit_within_monitor([1000.0, 700.0], [1920.0, 1080.0]), None);
assert_eq!(fit_within_monitor([1728.0, 918.0], [1920.0, 1080.0]), None);
assert_eq!(fit_within_monitor([5000.0, 5000.0], [0.0, 0.0]), None);
}
#[test]
fn display_role_labels() {
assert_eq!(DisplayRole::Playfield.label(), "role_playfield");
assert_eq!(DisplayRole::Backglass.label(), "role_backglass");
assert_eq!(DisplayRole::Dmd.label(), "role_dmd");
assert_eq!(DisplayRole::Topper.label(), "role_topper");
assert_eq!(DisplayRole::Unused.label(), "role_unused");
}
#[test]
fn display_role_all_has_5_entries() {
assert_eq!(DisplayRole::all().len(), 5);
}
#[test]
fn parse_inches_standard() {
assert_eq!(parse_inches_from_name("PL4380UH 42\""), Some(42));
}
#[test]
fn parse_inches_with_trailing_quote() {
assert_eq!(parse_inches_from_name("Samsung U28E590 43\""), Some(43));
}
#[test]
fn parse_inches_no_size() {
assert_eq!(parse_inches_from_name("Generic Monitor"), None);
}
#[test]
fn parse_inches_empty() {
assert_eq!(parse_inches_from_name(""), None);
}
#[test]
fn parse_inches_only_number() {
assert_eq!(parse_inches_from_name("27\""), Some(27));
}
#[test]
fn auto_assign_single_display() {
let mut displays = vec![make_display("Main", 1920, 1080)];
auto_assign_roles(&mut displays);
assert_eq!(displays[0].role, DisplayRole::Playfield);
}
#[test]
fn auto_assign_two_displays() {
let mut displays = vec![
make_display("Big", 3840, 2160),
make_display("Small", 1920, 1080),
];
auto_assign_roles(&mut displays);
assert_eq!(displays[0].role, DisplayRole::Playfield);
assert_eq!(displays[1].role, DisplayRole::Backglass);
}
#[test]
fn auto_assign_three_displays() {
let mut displays = vec![
make_display("4K", 3840, 2160),
make_display("QHD", 2560, 1440),
make_display("FHD", 1920, 1080),
];
auto_assign_roles(&mut displays);
assert_eq!(displays[0].role, DisplayRole::Playfield);
assert_eq!(displays[1].role, DisplayRole::Backglass);
assert_eq!(displays[2].role, DisplayRole::Dmd);
}
#[test]
fn auto_assign_four_displays() {
let mut displays = vec![
make_display("4K", 3840, 2160),
make_display("QHD", 2560, 1440),
make_display("FHD", 1920, 1080),
make_display("HD", 1280, 720),
];
auto_assign_roles(&mut displays);
assert_eq!(displays[0].role, DisplayRole::Playfield);
assert_eq!(displays[1].role, DisplayRole::Backglass);
assert_eq!(displays[2].role, DisplayRole::Dmd);
assert_eq!(displays[3].role, DisplayRole::Topper);
}
#[test]
fn auto_assign_five_displays_extra_is_unused() {
let mut displays = vec![
make_display("A", 3840, 2160),
make_display("B", 2560, 1440),
make_display("C", 1920, 1080),
make_display("D", 1280, 720),
make_display("E", 800, 600),
];
auto_assign_roles(&mut displays);
assert_eq!(displays[4].role, DisplayRole::Unused);
}
#[test]
fn auto_assign_empty() {
let mut displays: Vec<DisplayInfo> = vec![];
auto_assign_roles(&mut displays);
assert!(displays.is_empty());
}
#[test]
fn tv_placeholder_size_looks_wrong() {
let mut d = make_display("LG TV SSCR2", 3840, 2160);
(d.width_mm, d.height_mm) = (1600, 900);
(d.detected_width_mm, d.detected_height_mm) = (1600, 900);
assert!(size_looks_wrong(&d));
(d.width_mm, d.height_mm) = (920, 1640);
(d.detected_width_mm, d.detected_height_mm) = (920, 1640);
assert!(size_looks_wrong(&d));
(d.detected_width_mm, d.detected_height_mm) = (1600, 900);
(d.width_mm, d.height_mm) = (924, 520);
assert!(!size_looks_wrong(&d));
}
#[test]
fn generic_board_and_bad_shape_look_wrong() {
let mut d = make_display("Monitor", 1920, 1080);
(d.width_mm, d.height_mm) = (477, 268);
(d.detected_width_mm, d.detected_height_mm) = (477, 268);
assert!(size_looks_wrong(&d));
d.name = "DELL S2721QS".into();
assert!(!size_looks_wrong(&d));
(d.width_mm, d.height_mm) = (400, 300);
(d.detected_width_mm, d.detected_height_mm) = (400, 300);
assert!(size_looks_wrong(&d));
let mut r = make_display("DELL S2721QS", 1080, 1920);
(r.width_mm, r.height_mm) = (597, 336);
(r.detected_width_mm, r.detected_height_mm) = (597, 336);
assert!(!size_looks_wrong(&r));
}
#[test]
fn missing_size_looks_wrong() {
assert!(size_looks_wrong(&make_display("DELL S2721QS", 1920, 1080)));
}
}