diff --git a/src/backend/kms/device.rs b/src/backend/kms/device.rs index 694e7e757..e1a04efcf 100644 --- a/src/backend/kms/device.rs +++ b/src/backend/kms/device.rs @@ -2,7 +2,7 @@ use crate::{ backend::render::{output_elements, CursorMode, GlMultiRenderer, CLEAR_COLOR}, - config::{AdaptiveSync, OutputConfig, OutputState}, + config::{AdaptiveSync, EdidProduct, OutputConfig, OutputState}, shell::Shell, utils::prelude::*, wayland::protocols::screencopy::Frame as ScreencopyFrame, @@ -739,7 +739,7 @@ fn create_output_for_conn(drm: &mut DrmDevice, conn: connector::Handle) -> Resul .ok(); let (phys_w, phys_h) = conn_info.size().unwrap_or((0, 0)); - Ok(Output::new( + let output = Output::new( interface, PhysicalProperties { size: (phys_w as i32, phys_h as i32).into(), @@ -760,7 +760,13 @@ fn create_output_for_conn(drm: &mut DrmDevice, conn: connector::Handle) -> Resul .and_then(|info| info.model()) .unwrap_or_else(|| String::from("Unknown")), }, - )) + ); + if let Some(edid) = edid_info.as_ref().and_then(|x| x.edid()) { + output + .user_data() + .insert_if_missing(|| EdidProduct::from(edid.vendor_product())); + } + Ok(output) } fn populate_modes( diff --git a/src/config/mod.rs b/src/config/mod.rs index 652d7d4d9..2f5da0b1d 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -29,7 +29,7 @@ pub use smithay::{ }; use std::{ cell::RefCell, - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, BTreeSet, HashMap}, fs::OpenOptions, io::Write, path::PathBuf, @@ -73,23 +73,104 @@ pub struct DynamicConfig { #[derive(Debug, Deserialize, Serialize)] pub struct OutputsConfig { - pub config: HashMap, Vec>, + pub config: HashMap, Vec>, } +impl OutputsConfig { + fn match_configs(&self, infos: &BTreeSet) -> Option<&Vec> { + if let Some(exact_match) = self.config.get(infos) { + return Some(exact_match); + } + // TODO: disambiguate multiple matches by connector? + for (config_infos, configs) in &self.config { + if config_infos.len() == infos.len() + && config_infos + .iter() + .zip(infos) + .all(|(config_info, info)| config_info.matches_info(info)) + { + return Some(configs); + } + } + None + } +} + +// Fields are ordered so derived order will prioritize edid, and consider connector last. +// TODO: custom eq/ord? #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct OutputInfo { - pub connector: String, + pub edid: Option, pub make: String, pub model: String, + pub connector: String, +} + +impl OutputInfo { + /// An output matches this if it has the exact same edid manufacturer/product + /// information, or it has no edid information but has the same connector name + /// as the output's name. + fn matches_output(&self, output: &Output) -> bool { + let output_edid = output.edid(); + self.edid == output_edid && (self.edid.is_some() || self.connector == output.name()) + } + + fn matches_info(&self, other_info: &Self) -> bool { + self.edid == other_info.edid + && (self.edid.is_some() || self.connector == other_info.connector) + } + + fn find_output_match<'a>(&self, outputs: &'a [Output]) -> Option<&'a Output> { + let matches: Vec<&Output> = outputs + .into_iter() + .filter(|o| self.matches_output(o)) + .collect(); + if matches.len() > 1 { + // If there are multiple edid matches, favor output with the same + // connector. + // + // Even identical monitors *should* differ by serial number, but + // implementations vary. + if let Some(output) = matches.iter().find(|o| o.name() == self.connector) { + return Some(*output); + } + } + matches.into_iter().next() + } } impl From for OutputInfo { fn from(o: Output) -> OutputInfo { let physical = o.physical_properties(); + let edid = o.edid(); OutputInfo { connector: o.name(), make: physical.make, model: physical.model, + edid, + } + } +} + +#[derive(Debug, Deserialize, Serialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct EdidProduct { + pub manufacturer: [char; 3], + pub product: u16, + pub serial: Option, + pub manufacture_week: i32, + pub manufacture_year: i32, + pub model_year: Option, +} + +impl From for EdidProduct { + fn from(vp: libdisplay_info::edid::VendorProduct) -> Self { + Self { + manufacturer: vp.manufacturer, + product: vp.product, + serial: vp.serial, + manufacture_week: vp.manufacture_week, + manufacture_year: vp.manufacture_year, + model_year: vp.model_year, } } } @@ -106,6 +187,7 @@ pub enum OutputState { Enabled, #[serde(rename = "false")] Disabled, + // TODO store edid info, for better matching? Mirroring(String), } @@ -459,13 +541,13 @@ impl Config { clock: &Clock, ) { let outputs = output_state.outputs().collect::>(); - let mut infos = outputs + let infos = outputs .iter() .cloned() .map(Into::::into) - .collect::>(); - infos.sort(); - if let Some(configs) = self.dynamic_conf.outputs().config.get(&infos).cloned() { + .collect::>(); + // XXX only matches exact config? as `Vec` + if let Some(configs) = self.dynamic_conf.outputs().match_configs(&infos).cloned() { let known_good_configs = outputs .iter() .map(|output| { diff --git a/src/shell/workspace.rs b/src/shell/workspace.rs index f386caa58..ac0f0a6eb 100644 --- a/src/shell/workspace.rs +++ b/src/shell/workspace.rs @@ -84,6 +84,7 @@ pub struct Workspace { pub handle: WorkspaceHandle, pub focus_stack: FocusStacks, pub screencopy: ScreencopySessions, + // TODO edid info pub output_stack: VecDeque, pub pending_tokens: HashSet, pub(super) backdrop_id: Id, diff --git a/src/utils/prelude.rs b/src/utils/prelude.rs index 94dd6fcbe..bb21856ae 100644 --- a/src/utils/prelude.rs +++ b/src/utils/prelude.rs @@ -9,7 +9,7 @@ pub use crate::shell::{SeatExt, Shell, Workspace}; pub use crate::state::{Common, State}; pub use crate::wayland::handlers::xdg_shell::popup::update_reactive_popups; use crate::{ - config::{AdaptiveSync, OutputConfig, OutputState}, + config::{AdaptiveSync, EdidProduct, OutputConfig, OutputState}, shell::zoom::OutputZoomState, }; @@ -35,6 +35,8 @@ pub trait OutputExt { fn is_enabled(&self) -> bool; fn config(&self) -> Ref<'_, OutputConfig>; fn config_mut(&self) -> RefMut<'_, OutputConfig>; + + fn edid(&self) -> Option; } struct Vrr(AtomicU8); @@ -158,4 +160,8 @@ impl OutputExt for Output { .unwrap() .borrow_mut() } + + fn edid(&self) -> Option { + self.user_data().get().copied() + } }