Files
feresaria/compositor/src/output_policy.rs
T

772 lines
26 KiB
Rust

//! Pure monitor selection. DRM handles and side effects never enter this module.
use std::collections::{BTreeSet, HashSet};
use crate::config::{LidPolicy, OutputLayout, OutputProfile, OutputSettings, OutputTransform};
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Monitor {
/// Qualified hardware connector (GPU identity + connector), never a runtime Output handle.
pub key: String,
pub connector: String,
pub identity: String,
pub aliases: Vec<String>,
pub internal: bool,
pub usable: bool,
}
impl Monitor {
pub fn matches(&self, selector: &str) -> bool {
selector == self.connector
|| selector == self.identity
|| self.aliases.iter().any(|alias| alias == selector)
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct DesiredOutput {
pub key: String,
pub settings: OutputSettings,
/// A mirror target has no workspace ownership of its own.
pub mirror_source: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct DesiredOutputConfiguration {
pub profile: Option<String>,
pub outputs: Vec<DesiredOutput>,
pub suspend: bool,
}
/// Overrides expire on a *material* topology/lid change, independent of event ordering.
#[derive(Clone, Debug, Default)]
pub(crate) struct ManualOverride {
pub profile: Option<String>,
pub internal: Option<bool>,
pub layout: Option<OutputLayout>,
topology: Option<(BTreeSet<String>, bool)>,
}
impl ManualOverride {
pub fn observe(&mut self, monitors: &[Monitor], lid_closed: bool, profiles: &[OutputProfile]) {
let topology = (
monitors
.iter()
.map(|monitor| format!("{}={}", monitor.key, monitor.identity))
.collect(),
lid_closed,
);
if self.topology.as_ref().is_some_and(|old| old != &topology) {
self.profile = None;
self.internal = None;
self.layout = None;
}
if self.profile.as_ref().is_some_and(|name| {
!profiles.iter().any(|profile| {
profile.name == *name && profile_matches(profile, monitors, lid_closed)
})
}) {
self.profile = None;
}
self.topology = Some(topology);
}
}
fn profile_matches(profile: &OutputProfile, monitors: &[Monitor], lid_closed: bool) -> bool {
let mut matched = HashSet::new();
if profile.outputs.iter().any(|settings| {
monitors
.iter()
.filter(|monitor| monitor.matches(&settings.matcher))
.any(|monitor| !matched.insert(&monitor.key))
}) {
return false;
}
profile
.lid_closed
.is_none_or(|required| required == lid_closed)
&& profile
.outputs
.iter()
.filter(|settings| settings.required)
.all(|settings| {
// Ambiguous selectors cannot silently configure two monitors as one.
monitors
.iter()
.filter(|monitor| monitor.matches(&settings.matcher))
.count()
== 1
})
}
pub(crate) fn defaults(matcher: String) -> OutputSettings {
OutputSettings {
auto_refresh: false,
matcher,
required: true,
enabled: true,
mode: None,
scale: 1.0,
transform: OutputTransform::Normal,
position: None,
}
}
/// Most required monitors wins; equal specificity retains config order.
/// Unlisted outputs keep working in extend profiles. Exclusive layouts deliberately filter them.
pub(crate) fn select_profile(
monitors: &[Monitor],
lid_closed: bool,
profiles: &[OutputProfile],
manual: &ManualOverride,
) -> DesiredOutputConfiguration {
let selected = manual
.profile
.as_ref()
.and_then(|name| {
profiles.iter().find(|profile| {
profile.name == *name && profile_matches(profile, monitors, lid_closed)
})
})
.or_else(|| {
profiles
.iter()
.enumerate()
.filter(|(_, profile)| profile_matches(profile, monitors, lid_closed))
.max_by_key(|(index, profile)| {
(
profile
.outputs
.iter()
.filter(|output| output.required)
.count(),
std::cmp::Reverse(*index),
)
})
.map(|(_, profile)| profile)
});
let layout = manual
.layout
.unwrap_or_else(|| selected.map_or(OutputLayout::Extend, |profile| profile.layout));
let manage_lid = selected.is_none_or(|profile| profile.lid_policy == LidPolicy::DockOrSuspend);
let external = monitors
.iter()
.any(|monitor| monitor.usable && !monitor.internal);
let mut outputs = monitors
.iter()
.map(|monitor| {
let mut settings = selected
.and_then(|profile| {
profile
.outputs
.iter()
.find(|settings| monitor.matches(&settings.matcher))
})
.cloned()
.unwrap_or_else(|| defaults(monitor.identity.clone()));
if manual.layout.is_some() {
settings.enabled = true;
}
settings.enabled &= monitor.usable;
settings.enabled &= match layout {
OutputLayout::InternalOnly => monitor.internal,
OutputLayout::ExternalOnly => !monitor.internal,
OutputLayout::Extend | OutputLayout::Mirror => true,
};
if monitor.internal {
if let Some(enabled) = manual.internal {
settings.enabled = enabled && monitor.usable;
} else if manage_lid && lid_closed && external {
settings.enabled = false;
}
}
DesiredOutput {
key: monitor.key.clone(),
settings,
mirror_source: None,
}
})
.collect::<Vec<_>>();
if !outputs.iter().any(|output| output.settings.enabled) {
// Policy never disables the last usable display, even for an inapplicable exclusive layout.
if let Some((index, _)) = monitors
.iter()
.enumerate()
.filter(|(_, monitor)| monitor.usable)
.min_by_key(|(_, monitor)| (!monitor.internal, &monitor.key))
{
outputs[index].settings = defaults(monitors[index].identity.clone());
}
}
if layout == OutputLayout::Mirror {
let source = selected
.and_then(|profile| profile.mirror_source.as_ref())
.and_then(|selector| {
monitors
.iter()
.zip(&outputs)
.find(|(monitor, output)| monitor.matches(selector) && output.settings.enabled)
.map(|(_, output)| output.key.clone())
})
.or_else(|| {
outputs
.iter()
.filter(|output| output.settings.enabled)
.map(|output| output.key.clone())
.min()
});
if let Some(source) = source {
for output in &mut outputs {
if output.settings.enabled && output.key != source {
output.mirror_source = Some(source.clone());
}
}
}
}
DesiredOutputConfiguration {
profile: selected.map(|profile| profile.name.clone()),
outputs,
suspend: manage_lid && lid_closed && !external,
}
}
/// Restore actual settings only for surviving hardware; new monitors remain safe fallbacks.
pub(crate) fn restore_configuration(
previous: &DesiredOutputConfiguration,
monitors: &[Monitor],
) -> DesiredOutputConfiguration {
let mut restored = previous.clone();
restored
.outputs
.retain(|output| monitors.iter().any(|monitor| monitor.key == output.key));
for monitor in monitors {
if !restored
.outputs
.iter()
.any(|output| output.key == monitor.key)
{
let mut settings = defaults(monitor.identity.clone());
settings.enabled = false;
restored.outputs.push(DesiredOutput {
key: monitor.key.clone(),
settings,
mirror_source: None,
});
}
}
let source_keys = restored
.outputs
.iter()
.filter(|output| output.settings.enabled)
.map(|output| output.key.clone())
.collect::<HashSet<_>>();
for output in &mut restored.outputs {
if output
.mirror_source
.as_ref()
.is_some_and(|source| !source_keys.contains(source))
{
output.mirror_source = None;
}
}
if !restored
.outputs
.iter()
.any(|output| output.settings.enabled)
{
return select_profile(monitors, false, &[], &ManualOverride::default());
}
restored
}
/// A diff carries only affected connectors; removing a connector is an explicit disable.
pub(crate) fn changed_outputs<'a>(
old: &'a DesiredOutputConfiguration,
new: &'a DesiredOutputConfiguration,
) -> Vec<&'a str> {
let mut keys = HashSet::new();
old.outputs
.iter()
.chain(&new.outputs)
.filter_map(|output| {
let key = output.key.as_str();
if !keys.insert(key) {
return None;
}
let previous = old.outputs.iter().find(|candidate| candidate.key == key);
let next = new.outputs.iter().find(|candidate| candidate.key == key);
let same = match (previous, next) {
(Some(previous), Some(next)) => {
previous.settings.same_configuration(&next.settings)
&& previous.mirror_source == next.mirror_source
}
(None, None) => true,
_ => false,
};
(!same).then_some(key)
})
.collect()
}
#[derive(Default)]
pub(crate) struct Debouncer {
deadline: Option<std::time::Duration>,
}
impl Debouncer {
pub fn request(
&mut self,
now: std::time::Duration,
active: bool,
) -> Option<std::time::Duration> {
self.deadline = active.then(|| now + std::time::Duration::from_millis(150));
self.deadline
}
pub fn clear(&mut self) {
self.deadline = None;
}
pub fn take_due(&mut self, now: std::time::Duration) -> bool {
if self.deadline.is_some_and(|deadline| deadline <= now) {
self.deadline = None;
true
} else {
false
}
}
}
pub(crate) struct Placement {
pub key: String,
pub size: (i32, i32),
pub previous: Option<[i32; 2]>,
}
/// Explicit positions win. Preserve an automatic output's old position when
/// it still fits; append new/conflicting outputs in stable hardware-key order.
pub(crate) fn arrange_positions(
desired: &mut DesiredOutputConfiguration,
placements: &[Placement],
) -> Result<(), String> {
let mut occupied = Vec::new();
let mut automatic = Vec::new();
for (index, output) in desired.outputs.iter().enumerate() {
if !output.settings.enabled || output.mirror_source.is_some() {
continue;
}
let placement = placements
.iter()
.find(|placement| placement.key == output.key)
.ok_or("missing output dimensions")?;
if let Some(position) = output.settings.position {
occupied.push((position[0], position[1], placement.size.0, placement.size.1));
} else {
automatic.push((index, placement));
}
}
automatic.sort_by(|(_, a), (_, b)| {
(a.previous.is_none(), &a.key).cmp(&(b.previous.is_none(), &b.key))
});
for (index, placement) in automatic {
let fits = |position: [i32; 2]| {
let (x, y, w, h) = (
position[0] as i64,
position[1] as i64,
placement.size.0 as i64,
placement.size.1 as i64,
);
!occupied.iter().any(|&(ox, oy, ow, oh)| {
x < ox as i64 + ow as i64
&& x + w > ox as i64
&& y < oy as i64 + oh as i64
&& y + h > oy as i64
})
};
let previous = placement.previous.filter(|position| fits(*position));
let position = if let Some(previous) = previous {
previous
} else {
let right = occupied
.iter()
.map(|&(x, _, w, _)| x as i64 + w as i64)
.max()
.unwrap_or(0)
.max(0);
let x = i32::try_from(right).map_err(|_| "automatic output position overflows")?;
x.checked_add(placement.size.0)
.ok_or("automatic output geometry overflows")?;
[x, 0]
};
desired.outputs[index].settings.position = Some(position);
occupied.push((position[0], position[1], placement.size.0, placement.size.1));
}
let source_positions = desired
.outputs
.iter()
.filter(|output| output.mirror_source.is_none())
.map(|output| (output.key.clone(), output.settings.position))
.collect::<std::collections::HashMap<_, _>>();
for output in &mut desired.outputs {
if let Some(source) = &output.mirror_source {
output.settings.position = source_positions.get(source).copied().flatten();
}
}
Ok(())
}
#[cfg(test)]
mod placement_tests {
use super::*;
#[test]
fn automatic_placement_is_stable_and_only_moves_overlapping_neighbors() {
let monitors = [
Monitor {
key: "a".into(),
connector: "a".into(),
identity: "a".into(),
aliases: vec![],
internal: true,
usable: true,
},
Monitor {
key: "b".into(),
connector: "b".into(),
identity: "b".into(),
aliases: vec![],
internal: false,
usable: true,
},
];
let mut desired = select_profile(&monitors, false, &[], &ManualOverride::default());
let placements = [
Placement {
key: "b".into(),
size: (100, 100),
previous: Some([100, 0]),
},
Placement {
key: "a".into(),
size: (100, 100),
previous: Some([0, 0]),
},
];
arrange_positions(&mut desired, &placements).unwrap();
assert_eq!(desired.outputs[0].settings.position, Some([0, 0]));
assert_eq!(desired.outputs[1].settings.position, Some([100, 0]));
let mut expanded = select_profile(&monitors, false, &[], &ManualOverride::default());
let placements = [
Placement {
key: "a".into(),
size: (150, 100),
previous: Some([0, 0]),
},
Placement {
key: "b".into(),
size: (100, 100),
previous: Some([100, 0]),
},
];
arrange_positions(&mut expanded, &placements).unwrap();
assert_eq!(expanded.outputs[0].settings.position, Some([0, 0]));
assert_eq!(expanded.outputs[1].settings.position, Some([150, 0]));
}
}
#[cfg(test)]
mod tests {
use super::*;
fn monitor(name: &str, internal: bool) -> Monitor {
Monitor {
key: name.into(),
connector: name.into(),
identity: format!("edid:{name}"),
aliases: vec![],
internal,
usable: true,
}
}
fn profile(name: &str, outputs: &[&str], layout: OutputLayout) -> OutputProfile {
OutputProfile {
name: name.into(),
outputs: outputs
.iter()
.map(|name| defaults((*name).into()))
.collect(),
layout,
confirm_timeout: 15,
lid_policy: LidPolicy::DockOrSuspend,
lid_closed: None,
mirror_source: None,
}
}
#[test]
fn manual_layouts_reuse_profiles_and_expire_on_monitor_changes() {
let connected = [monitor("eDP-1", true), monitor("DP-1", false)];
let mut settings = defaults("DP-1".into());
settings.enabled = false;
settings.scale = 1.5;
let mut mobile = profile("mobile", &["eDP-1"], OutputLayout::InternalOnly);
mobile.outputs.push(settings);
let profiles = [mobile];
for layout in [
OutputLayout::InternalOnly,
OutputLayout::ExternalOnly,
OutputLayout::Extend,
OutputLayout::Mirror,
] {
let mut manual = ManualOverride {
layout: Some(layout),
..ManualOverride::default()
};
manual.observe(&connected, false, &profiles);
let desired = select_profile(&connected, false, &profiles, &manual);
assert_eq!(
desired.outputs[0].settings.enabled,
layout != OutputLayout::ExternalOnly
);
assert_eq!(
desired.outputs[1].settings.enabled,
layout != OutputLayout::InternalOnly
);
assert_eq!(
desired.outputs[1].settings.scale, 1.5,
"manual layout must preserve configured scale"
);
assert_eq!(
desired
.outputs
.iter()
.filter(|output| output.mirror_source.is_some())
.count(),
usize::from(layout == OutputLayout::Mirror)
);
manual.observe(&connected[..1], false, &profiles);
assert_eq!(manual.layout, None);
assert!(
select_profile(&connected[..1], false, &profiles, &manual).outputs[0]
.settings
.enabled
);
}
}
#[test]
fn specificity_and_config_order() {
let monitors = [monitor("eDP-1", true), monitor("DP-1", false)];
let profiles = [
profile("mobile", &["eDP-1"], OutputLayout::Extend),
profile("desk", &["eDP-1", "DP-1"], OutputLayout::Extend),
profile("tie", &["DP-1", "eDP-1"], OutputLayout::Extend),
];
assert_eq!(
select_profile(&monitors, false, &profiles, &ManualOverride::default())
.profile
.as_deref(),
Some("desk")
);
}
#[test]
fn layouts_unknown_outputs_and_safe_fallback() {
let monitors = [
monitor("eDP-1", true),
monitor("DP-1", false),
monitor("HDMI-A-1", false),
];
for (layout, count) in [
(OutputLayout::InternalOnly, 1),
(OutputLayout::ExternalOnly, 2),
(OutputLayout::Extend, 3),
(OutputLayout::Mirror, 3),
] {
let desired = select_profile(
&monitors,
false,
&[profile("test", &["eDP-1"], layout)],
&ManualOverride::default(),
);
assert_eq!(
desired
.outputs
.iter()
.filter(|output| output.settings.enabled)
.count(),
count
);
assert_eq!(
desired
.outputs
.iter()
.filter(|output| output.mirror_source.is_some())
.count(),
if layout == OutputLayout::Mirror { 2 } else { 0 }
);
}
let missing = [profile("absent", &["DP-9"], OutputLayout::ExternalOnly)];
let fallback = select_profile(&monitors, false, &missing, &ManualOverride::default());
assert_eq!(fallback.profile, None);
assert!(
fallback
.outputs
.iter()
.all(|output| output.settings.enabled)
);
let internal = &monitors[..1];
let mut disabled = profile("disabled", &["eDP-1"], OutputLayout::Extend);
disabled.outputs[0].enabled = false;
assert!(
select_profile(internal, false, &[disabled], &ManualOverride::default()).outputs[0]
.settings
.enabled
);
}
#[test]
fn override_expires_only_on_material_change_or_invalid_reload() {
let monitors = [monitor("eDP-1", true), monitor("DP-1", false)];
let profiles = [profile("mobile", &["eDP-1"], OutputLayout::InternalOnly)];
let mut manual = ManualOverride::default();
manual.observe(&monitors, false, &profiles);
manual.profile = Some("mobile".into());
manual.observe(&monitors, false, &profiles);
assert_eq!(manual.profile.as_deref(), Some("mobile"));
manual.observe(&monitors, true, &profiles);
assert_eq!(manual.profile, None);
manual.profile = Some("mobile".into());
manual.observe(&monitors, true, &[]);
assert_eq!(manual.profile, None);
}
#[test]
fn lid_dock_undock_and_internal_toggle_never_disable_last_output() {
let monitors = [monitor("eDP-1", true), monitor("DP-1", false)];
let docked = select_profile(&monitors, true, &[], &ManualOverride::default());
assert!(!docked.suspend);
assert!(!docked.outputs[0].settings.enabled);
assert!(docked.outputs[1].settings.enabled);
let manual = ManualOverride {
internal: Some(false),
..ManualOverride::default()
};
let undocked = select_profile(&monitors[..1], true, &[], &manual);
assert!(undocked.suspend);
assert!(undocked.outputs[0].settings.enabled);
}
#[test]
fn desired_diff_is_local() {
let monitors = [monitor("eDP-1", true), monitor("DP-1", false)];
let old = select_profile(&monitors, false, &[], &ManualOverride::default());
assert!(changed_outputs(&old, &old).is_empty());
let mut new = old.clone();
new.outputs[1].settings.scale = 1.5;
assert_eq!(changed_outputs(&old, &new), vec!["DP-1"]);
}
#[test]
fn mirror_removal_and_extend_restore_single_ownership_plan() {
let monitors = [monitor("eDP-1", true), monitor("DP-1", false)];
let mut mirror = profile("mirror", &["eDP-1", "DP-1"], OutputLayout::Mirror);
mirror.mirror_source = Some("eDP-1".into());
let old = select_profile(
&monitors,
false,
&[mirror.clone()],
&ManualOverride::default(),
);
assert_eq!(old.outputs[1].mirror_source.as_deref(), Some("eDP-1"));
let removed = select_profile(&monitors[..1], false, &[mirror], &ManualOverride::default());
assert!(removed.outputs[0].mirror_source.is_none());
assert!(removed.outputs[0].settings.enabled);
let extended = select_profile(&monitors, false, &[], &ManualOverride::default());
assert!(
extended
.outputs
.iter()
.all(|output| output.mirror_source.is_none())
);
assert_eq!(changed_outputs(&old, &extended), vec!["DP-1"]);
}
#[test]
fn optional_outputs_and_lid_specific_profiles() {
let monitors = [monitor("eDP-1", true)];
let mut mobile = profile("mobile", &["eDP-1", "DP-1"], OutputLayout::Extend);
mobile.outputs[1].required = false;
mobile.lid_closed = Some(false);
assert_eq!(
select_profile(
&monitors,
false,
&[mobile.clone()],
&ManualOverride::default()
)
.profile
.as_deref(),
Some("mobile")
);
assert_eq!(
select_profile(&monitors, true, &[mobile], &ManualOverride::default()).profile,
None
);
}
#[test]
fn swapping_monitor_on_same_connector_expires_override() {
let mut monitors = [monitor("eDP-1", true), monitor("DP-1", false)];
let profiles = [profile("mobile", &["eDP-1"], OutputLayout::InternalOnly)];
let mut manual = ManualOverride::default();
manual.observe(&monitors, false, &profiles);
manual.profile = Some("mobile".into());
monitors[1].identity = "edid:new-monitor".into();
manual.observe(&monitors, false, &profiles);
assert!(manual.profile.is_none());
}
#[test]
fn restoration_survives_disconnect_and_keeps_new_monitor_as_emergency_fallback() {
let monitors = [monitor("eDP-1", true), monitor("DP-1", false)];
let previous = select_profile(&monitors, false, &[], &ManualOverride::default());
let restored = restore_configuration(&previous, &monitors[..1]);
assert_eq!(restored.outputs.len(), 1);
assert!(restored.outputs[0].settings.enabled);
let unknown = [monitor("HDMI-A-2", false)];
assert!(
restore_configuration(&previous, &unknown).outputs[0]
.settings
.enabled
);
}
}
#[cfg(test)]
mod debounce_tests {
use super::*;
use std::time::Duration;
#[test]
fn a_dock_burst_produces_one_reconciliation_after_last_event() {
let mut debounce = Debouncer::default();
for time in [0, 40, 110] {
debounce.request(Duration::from_millis(time), true);
}
assert!(!debounce.take_due(Duration::from_millis(150)));
assert!(!debounce.take_due(Duration::from_millis(259)));
assert!(debounce.take_due(Duration::from_millis(260)));
assert!(!debounce.take_due(Duration::from_millis(400)));
}
#[test]
fn inactivity_cancels_deadlines_and_resume_uses_fresh_state() {
let mut debounce = Debouncer::default();
debounce.request(Duration::ZERO, true);
assert_eq!(debounce.request(Duration::from_millis(30), false), None);
assert!(!debounce.take_due(Duration::from_secs(10)));
debounce.request(Duration::from_secs(10), true);
assert!(debounce.take_due(Duration::from_millis(10150)));
}
}