Extend output profiles with monitor reconciliation and mirroring

This commit is contained in:
Samuel Onoja committed 2026-10-02 12:08:19 +01:00
1 parent ac184576f6
commit d14cd3cbd2
23 files changed
+3614 -474

No files matched your search

File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -30,7 +30,7 @@ pub(super) fn capture_output(
Ok(())
}
fn render_capture<'a, E: RenderElement<GlesRenderer>>(
pub(super) fn render_capture<'a, E: RenderElement<GlesRenderer>>(
renderer: &mut GlesRenderer,
texture: &'a mut Option<GlesTexture>,
output: &Output,
+171 -3
View File
@@ -1,5 +1,5 @@
//! Resume reads are asynchronous; newer switch events win over old replies.
use std::sync::mpsc;
use std::sync::{Arc, Mutex, mpsc};
use super::*;
@@ -63,8 +63,51 @@ impl LidState {
}
}
struct InhibitorLease<T> {
active: bool,
generation: u64,
descriptor: Option<T>,
suspend: bool,
}
impl<T> Default for InhibitorLease<T> {
fn default() -> Self {
Self {
active: false,
generation: 0,
descriptor: None,
suspend: false,
}
}
}
impl<T> InhibitorLease<T> {
fn activate(&mut self, active: bool) -> u64 {
self.generation = self.generation.wrapping_add(1);
self.active = active;
self.suspend = false;
// Closing here releases logind immediately, independently of worker progress.
self.descriptor = None;
self.generation
}
fn accept(&mut self, generation: u64, descriptor: T) {
if self.active && self.generation == generation {
self.descriptor = Some(descriptor);
}
}
}
enum PolicyRequest {
Activate(u64),
Suspend,
}
pub(super) struct Reader {
requests: mpsc::Sender<Probe>,
suspend: mpsc::Sender<PolicyRequest>,
lease: Arc<Mutex<InhibitorLease<zbus::zvariant::OwnedFd>>>,
suspend_requested: bool,
pub deadline: Option<RegistrationToken>,
}
@@ -99,7 +142,94 @@ fn read_acpi() -> Option<bool> {
}
impl Reader {
pub fn set_active(&mut self, active: bool) {
let generation = self.lease.lock().unwrap().activate(active);
self.suspend_requested = false;
if active {
let _ = self.suspend.send(PolicyRequest::Activate(generation));
}
}
pub fn start(state: &Ferese) -> Result<Self, Box<dyn Error>> {
let (suspend, suspend_requests) = mpsc::channel::<PolicyRequest>();
let lease = Arc::new(Mutex::new(InhibitorLease::default()));
let worker_lease = lease.clone();
std::thread::Builder::new()
.name("ferese-lid-policy".into())
.spawn(move || {
let connection = zbus::blocking::connection::Builder::system()
.and_then(|builder| builder.method_timeout(Duration::from_secs(5)).build());
let Ok(connection) = connection else { return };
while let Ok(request) = suspend_requests.recv() {
if let PolicyRequest::Activate(generation) = request {
let current = worker_lease.lock().unwrap();
if !current.active || current.generation != generation {
continue;
}
drop(current);
let inhibitor = connection
.call_method(
Some("org.freedesktop.login1"),
"/org/freedesktop/login1",
Some("org.freedesktop.login1.Manager"),
"Inhibit",
&(
"handle-lid-switch",
"Ferese",
"Reconcile monitors before lid action",
"block",
),
)
.and_then(|reply| reply.body().deserialize::<zbus::zvariant::OwnedFd>());
match inhibitor {
Ok(descriptor) => worker_lease.lock().unwrap().accept(generation, descriptor),
Err(error) => tracing::warn!(%error, "could not own logind lid policy"),
}
continue;
}
let current = worker_lease.lock().unwrap();
if !current.active || !current.suspend {
continue;
}
drop(current);
if read_logind(&connection).ok() != Some(true) {
continue;
}
let allowed = connection
.call_method(
Some("org.freedesktop.login1"),
"/org/freedesktop/login1",
Some("org.freedesktop.DBus.Properties"),
"Get",
&("org.freedesktop.login1.Manager", "BlockInhibited"),
)
.and_then(|reply| reply.body().deserialize::<zbus::zvariant::OwnedValue>())
.and_then(|value| String::try_from(value).map_err(Into::into));
if allowed
.as_ref()
.is_ok_and(|inhibitors| inhibitors.split(':').any(|kind| kind == "sleep"))
{
tracing::debug!("lid suspend prevented by sleep inhibitor");
continue;
}
let current = worker_lease.lock().unwrap();
let requested = current.active && current.suspend;
drop(current);
if !requested {
continue;
}
if let Err(error) = connection.call_method(
Some("org.freedesktop.login1"),
"/org/freedesktop/login1",
Some("org.freedesktop.login1.Manager"),
"Suspend",
&(false,),
) {
tracing::warn!(%error, "logind rejected lid suspend");
}
}
worker_lease.lock().unwrap().activate(false);
})?;
let (requests, receiver) = mpsc::channel::<Probe>();
let (replies, events) = smithay::reexports::calloop::channel::channel();
state.loop_handle.insert_source(events, |event, _, state| {
@@ -126,10 +256,15 @@ impl Reader {
}
})?;
Ok(Self {
let mut reader = Self {
requests,
suspend,
lease,
suspend_requested: false,
deadline: None,
})
};
reader.set_active(true);
Ok(reader)
}
}
@@ -152,6 +287,8 @@ pub(super) fn request_refresh(state: &mut Ferese, reactivate: bool) {
cancel_output_timers(&state.loop_handle, output);
}
}
backend.lid_reader.suspend_requested = false;
backend.lid_reader.lease.lock().unwrap().suspend = false;
let probe = backend.lid.begin(reactivate);
let sent = backend.lid_reader.requests.send(probe).is_ok();
let timer = state
@@ -191,6 +328,7 @@ fn complete_refresh(state: &mut Ferese, probe: Probe, value: Option<bool>) {
// Waking a locked session may request redraws. Hold them until the fresh
// device/connector snapshot has been reconciled.
backend.lid_reader.suspend_requested = false;
backend.topology.reconciling = true;
state.reset_animation_clock();
state.lock_input_activity();
@@ -198,10 +336,40 @@ fn complete_refresh(state: &mut Ferese, probe: Probe, value: Option<bool>) {
reconcile_outputs(state, reactivate);
}
/// Only logind requests suspend. Inactive events wait for fresh resume topology.
pub(super) fn apply_suspend_policy(state: &mut Ferese) {
let Some(backend) = state.direct_backend.as_mut() else {
return;
};
let requested = backend.active && backend.desired_outputs.suspend && backend.lid.closed();
backend.lid_reader.lease.lock().unwrap().suspend = requested;
if backend.lid_reader.suspend_requested != requested {
backend.lid_reader.suspend_requested = requested;
let _ = backend.lid_reader.suspend.send(PolicyRequest::Suspend);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pause_releases_inhibitor_and_rejects_delayed_acquisition() {
let mut lease = InhibitorLease::default();
let first = lease.activate(true);
lease.accept(first, "fd");
assert_eq!(lease.descriptor, Some("fd"));
lease.activate(false);
assert_eq!(lease.descriptor, None);
lease.accept(first, "late fd");
assert_eq!(lease.descriptor, None);
let next = lease.activate(true);
lease.accept(first, "old session fd");
assert_eq!(lease.descriptor, None);
lease.accept(next, "new session fd");
assert_eq!(lease.descriptor, Some("new session fd"));
}
#[test]
#[ignore = "requires dbus-daemon and private test sockets"]
fn logind_reads_are_fresh_without_property_change_signals() {
+193
View File
@@ -0,0 +1,193 @@
//! A target displays a composition of the source, with no second workspace or input domain.
//! Source alone releases client callbacks/presentation feedback. Targets have independent KMS clocks.
use super::*;
use smithay::backend::renderer::Texture;
use smithay::utils::{Buffer, Physical, Rectangle};
/// Fit the complete source into the transformed target; black letterbox, never crop.
fn fit(source: (i32, i32), target: (i32, i32)) -> Rectangle<i32, Physical> {
let scale = (target.0 as f64 / source.0.max(1) as f64).min(target.1 as f64 / source.1.max(1) as f64);
let width = (source.0 as f64 * scale).round().max(1.0) as i32;
let height = (source.1 as f64 * scale).round().max(1.0) as i32;
Rectangle::new(
((target.0 - width) / 2, (target.1 - height) / 2).into(),
(width, height).into(),
)
}
pub(super) fn elements(
state: &mut Ferese,
renderer: &mut GlesRenderer,
source: &Output,
frame: &crate::state::FrameScene,
target: &mut DirectOutput,
) -> Result<Vec<crate::render::AnimatedWindowRenderElement>, smithay::backend::renderer::gles::GlesError> {
let elements = sampled_output_elements(state, renderer, source, true, frame);
// Target renderer owns this texture, including when source and target use different GPUs.
let canvas = target.mirror_canvas.get_or_insert_with(|| {
Output::new(
"mirror-canvas".into(),
PhysicalProperties {
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "Ferese".into(),
model: "Mirror composition".into(),
},
)
});
compose(renderer, &mut target.mirror_texture, canvas, source, &elements)?;
let texture = target
.mirror_texture
.as_ref()
.expect("source composition has a texture")
.clone();
target.mirror_commit.increment();
Ok(vec![
fitted_texture(texture, &target.output, target.mirror_id.clone(), target.mirror_commit).into(),
])
}
fn compose(
renderer: &mut GlesRenderer,
texture: &mut Option<GlesTexture>,
canvas: &Output,
source: &Output,
elements: &[crate::render::AnimatedWindowRenderElement],
) -> Result<(), smithay::backend::renderer::gles::GlesError> {
let source_mode = source.current_mode().expect("source has a mode");
canvas.change_current_state(
Some(OutputMode {
size: source.current_transform().transform_size(source_mode.size),
refresh: source_mode.refresh,
}),
Some(Transform::Normal),
Some(source.current_scale()),
None,
);
let framebuffer = capture::render_capture(renderer, texture, canvas, elements)?;
drop(framebuffer);
Ok(())
}
fn fitted_texture(
texture: GlesTexture,
target: &Output,
id: smithay::backend::renderer::element::Id,
commit: CommitCounter,
) -> crate::presentation::NativeTextureElement {
let source_size = texture.size();
let size = target
.current_transform()
.transform_size(target.current_mode().expect("target has a mode").size);
crate::presentation::NativeTextureElement {
id,
commit,
source: Rectangle::<f64, Buffer>::from_size(source_size.to_f64()),
texture,
geometry: fit((source_size.w, source_size.h), (size.w, size.h)),
alpha: 1.0,
program: None,
uniforms: Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn different_native_sizes_fit_without_cropping() {
assert_eq!(
fit((1920, 1080), (1280, 1024)),
Rectangle::new((0, 152).into(), (1280, 720).into())
);
assert_eq!(
fit((1920, 1080), (1080, 1920)),
Rectangle::new((0, 656).into(), (1080, 608).into())
);
}
#[test]
#[ignore = "requires an EGL rendering device"]
fn mirrored_pixels_fit_rotated_source_and_target_without_cropping() {
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
use smithay::backend::renderer::element::{Id, Kind};
use smithay::backend::renderer::{Color32F, ExportMem};
let device = EGLDevice::enumerate().unwrap().last().expect("EGL device");
let display = unsafe { EGLDisplay::new(device).unwrap() };
let context = EGLContext::new(&display).unwrap();
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
let output = |name: &str| {
Output::new(
name.into(),
PhysicalProperties {
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
)
};
let source = output("source");
source.change_current_state(
Some(OutputMode {
size: (64, 32).into(),
refresh: 60_000,
}),
Some(Transform::_90),
Some(Scale::Fractional(2.0)),
None,
);
let canvas = output("canvas");
let colors: Vec<crate::render::AnimatedWindowRenderElement> = [(0, [1., 0., 0., 1.]), (32, [0., 0., 1., 1.])]
.into_iter()
.map(|(y, color)| {
SolidColorRenderElement::new(
Id::new(),
Rectangle::new((0, y).into(), (32, 32).into()),
CommitCounter::default(),
Color32F::from(color),
Kind::Unspecified,
)
.into()
})
.collect();
let mut texture = None;
compose(&mut renderer, &mut texture, &canvas, &source, &colors).unwrap();
assert_eq!(texture.as_ref().unwrap().size(), (32, 64).into());
let target = output("target");
for (transform, colored_area) in [(Transform::Normal, 2048), (Transform::_90, 4608)] {
target.change_current_state(
Some(OutputMode {
size: (96, 64).into(),
refresh: 120_000,
}),
Some(transform),
Some(Scale::Fractional(1.5)),
None,
);
let element = fitted_texture(
texture.as_ref().unwrap().clone(),
&target,
Id::new(),
CommitCounter::default(),
);
let mut target_texture = None;
let framebuffer = capture::render_capture(&mut renderer, &mut target_texture, &target, &[element]).unwrap();
let mapping = renderer
.copy_framebuffer(&framebuffer, Rectangle::from_size((96, 64).into()), Fourcc::Abgr8888)
.unwrap();
let pixels = renderer.map_texture(&mapping).unwrap();
let red = pixels
.chunks_exact(4)
.filter(|pixel| pixel[0] > 100 && pixel[0] > pixel[2])
.count();
let blue = pixels
.chunks_exact(4)
.filter(|pixel| pixel[2] > 100 && pixel[2] > pixel[0])
.count();
assert_eq!(red, colored_area / 2, "{transform:?}");
assert_eq!(blue, colored_area / 2, "{transform:?}");
}
}
}
+187 -85
View File
@@ -78,6 +78,8 @@ pub(super) struct Topology {
pub reconciling: bool,
pub dirty: bool,
pub retry_timer: Option<RegistrationToken>,
pub settle_timer: Option<RegistrationToken>,
pub coalescer: crate::output_policy::Debouncer,
}
impl Topology {
@@ -93,73 +95,6 @@ impl Topology {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inactive_changes_coalesce_and_reconciliation_cannot_reenter() {
let mut topology = Topology::default();
assert!(!topology.request(false));
assert!(!topology.request(false));
assert!(topology.dirty);
assert!(topology.request(true));
topology.reconciling = true;
assert!(!topology.request(true));
}
#[test]
fn replug_matches_hardware_even_when_card_numbers_change() {
let original = Path::new("/dev/dri/card0");
let identity = PathBuf::from("/sys/devices/pci0000:00/0000:00:02.0");
let mut slot = DeviceState {
identity: Some(identity.clone()),
..DeviceState::default()
};
slot.failed(Instant::now());
slot.missing();
let observed = vec![
(
PathBuf::from("/dev/dri/card0"),
Some(PathBuf::from("/sys/devices/other-gpu")),
),
(PathBuf::from("/dev/dri/card2"), Some(identity)),
];
assert_eq!(
slot.current_path(original, &observed),
Some(Path::new("/dev/dri/card2"))
);
assert!(slot.current_path(original, &observed[..1]).is_none());
assert!(slot.current_path(original, &[]).is_none());
}
#[test]
fn unavailable_devices_retry_independently_with_capped_backoff() {
let now = Instant::now();
let mut topology = Topology::default();
topology.devices.insert("/dev/dri/card0".into(), DeviceState::default());
let healthy = DrmNode::from_dev_id(libc::makedev(226, 1)).unwrap();
let mut second = DeviceState::default();
second.ready(healthy);
topology.devices.insert("/dev/dri/card1".into(), second);
let device = topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap();
device.failed(now);
assert_eq!(device.retry_at, Some(now + Duration::from_secs(1)));
for _ in 0..20 {
device.failed(now);
}
assert_eq!(device.retry_at, Some(now + Duration::from_secs(32)));
assert_eq!(
topology.devices[Path::new("/dev/dri/card1")].availability,
Availability::Ready(healthy)
);
assert!(topology.retry_deadline(false).is_none());
assert_eq!(topology.retry_deadline(true), Some(now + Duration::from_secs(32)));
topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap().missing();
assert!(topology.retry_deadline(true).is_none());
}
}
fn slot_mut<'a>(state: &'a mut Ferese, path: &Path) -> &'a mut DeviceState {
state
.direct_backend
@@ -180,8 +115,13 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
return;
}
if let Some(token) = backend.topology.settle_timer.take() {
state.loop_handle.remove(token);
}
backend.topology.coalescer.clear();
backend.topology.reconciling = true;
backend.topology.dirty = false;
backend.output_error = None;
if reactivate {
backend
.presentation
@@ -191,7 +131,7 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
if let Some(token) = backend.topology.retry_timer.take() {
state.loop_handle.remove(token);
}
let paths = backend.topology.devices.keys().cloned().collect::<Vec<_>>();
let mut paths = backend.topology.devices.keys().cloned().collect::<Vec<_>>();
let invalidated = std::mem::take(&mut backend.topology.invalidated);
let observed = all_gpus(backend.session.seat()).map(|paths| {
paths
@@ -209,6 +149,23 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
backend.low_power =
automatic && super::super::power::low_power(backend.low_power, super::super::power::battery_percent());
if let Ok(observed) = &observed {
for (path, identity) in observed {
if !backend
.topology
.devices
.values()
.any(|slot| slot.identity == *identity && identity.is_some())
&& !backend.topology.devices.contains_key(path)
{
backend.topology.devices.insert(path.clone(), DeviceState::known(path));
paths.push(path.clone());
}
}
}
paths.sort();
let mut ready = Vec::new();
for node in invalidated {
remove_device(state, node);
}
@@ -301,7 +258,9 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
.remove(&crtc)
.unwrap();
cancel_output_timers(&state.loop_handle, &mut output);
state.display_handle.disable_global::<Ferese>(output.global);
if let Some(global) = output.global.take() {
state.display_handle.disable_global::<Ferese>(global);
}
state.unregister_output(&output.output);
state
.direct_backend
@@ -311,28 +270,65 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
.remove(&(node, crtc));
}
let complete = rescan_device(state, node);
if complete.is_err() {
remove_device(state, node);
ready.push((path, node));
}
let known_good = state.direct_backend.as_ref().unwrap().applied_outputs.clone();
if let Err(error) = refresh_monitor_policy(state).and_then(|()| {
transaction::retire_disconnected(state);
validate_desired_outputs(state)
}) {
tracing::warn!(%error, "monitor policy validation failed; retaining applied outputs");
state.direct_backend.as_mut().unwrap().output_error = Some(error);
for (path, _) in &ready {
slot_mut(state, path).failed(Instant::now());
}
let entry = slot_mut(state, &path);
if complete == Ok(true) {
entry.ready(node);
} else {
entry.failed(Instant::now());
// A partial connector failure must not take down working outputs.
if complete == Ok(false) {
entry.availability = Availability::Degraded(node);
} else {
// Activate destinations before disabling former owners, including across GPUs.
ready.sort_by_key(|(_, node)| {
let backend = state.direct_backend.as_ref().unwrap();
let prefix = format!("{}/", drm_device_key(&backend.devices[node].drm));
let desired = backend
.desired_outputs
.outputs
.iter()
.filter(|output| output.settings.enabled && output.key.starts_with(&prefix));
let logical_sources = desired.clone().filter(|output| output.mirror_source.is_none()).count();
std::cmp::Reverse((logical_sources, desired.count()))
});
let mut succeeded = Vec::new();
for (path, node) in &ready {
let complete = rescan_device(state, *node);
let entry = slot_mut(state, path);
if complete == Ok(true) {
entry.ready(*node);
succeeded.push(*node);
} else {
entry.failed(Instant::now());
entry.availability = Availability::Degraded(*node);
// Device boundaries are not cross-GPU atomic. Compensate already
// applied devices using their last actual hardware settings.
if !known_good.outputs.is_empty() {
let backend = state.direct_backend.as_mut().unwrap();
let requested = backend.desired_outputs.clone();
backend.desired_outputs =
crate::output_policy::restore_configuration(&known_good, &backend.monitors);
for previous in succeeded {
let _ = rescan_device(state, previous);
}
state.direct_backend.as_mut().unwrap().desired_outputs = requested;
}
break;
}
}
if state.direct_backend.as_ref().unwrap().output_error.is_none() {
remember_applied_configuration(state);
}
}
transaction::recover_last_output(state);
refresh_connected_info(state);
let backend = state.direct_backend.as_mut().unwrap();
backend.connected_outputs = backend
.devices
.values()
.flat_map(|device| device.connected_outputs.clone())
.collect();
backend.topology.reconciling = false;
let retry = backend.topology.retry_deadline(backend.active);
if let Some(deadline) = retry {
@@ -355,4 +351,110 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
state.restore_output_focus();
state.relayout();
render_all(state);
lid::apply_suspend_policy(state);
}
const SETTLE: Duration = Duration::from_millis(150);
/// Trailing-edge coalescing; inactive events retain dirtiness without arming a timer.
pub(super) fn hotplug(state: &mut Ferese) {
let Some(backend) = state.direct_backend.as_mut() else {
return;
};
backend.topology.dirty = true;
if let Some(token) = backend.topology.settle_timer.take() {
state.loop_handle.remove(token);
}
let deadline = backend
.topology
.coalescer
.request(monotonic_now(), backend.active && !backend.lid.pending());
if deadline.is_none() {
return;
}
match state
.loop_handle
.insert_source(Timer::from_duration(SETTLE), |_, _, state| {
if let Some(backend) = state.direct_backend.as_mut() {
backend.topology.settle_timer = None;
if !backend.topology.coalescer.take_due(monotonic_now()) {
return TimeoutAction::Drop;
}
}
reconcile_outputs(state, false);
TimeoutAction::Drop
}) {
Ok(token) => state.direct_backend.as_mut().unwrap().topology.settle_timer = Some(token),
Err(error) => {
tracing::warn!(%error, "could not debounce output events");
reconcile_outputs(state, false);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inactive_changes_coalesce_and_reconciliation_cannot_reenter() {
let mut topology = Topology::default();
assert!(!topology.request(false));
assert!(!topology.request(false));
assert!(topology.dirty);
assert!(topology.request(true));
topology.reconciling = true;
assert!(!topology.request(true));
}
#[test]
fn replug_matches_hardware_even_when_card_numbers_change() {
let original = Path::new("/dev/dri/card0");
let identity = PathBuf::from("/sys/devices/pci0000:00/0000:00:02.0");
let mut slot = DeviceState {
identity: Some(identity.clone()),
..DeviceState::default()
};
slot.failed(Instant::now());
slot.missing();
let observed = vec![
(
PathBuf::from("/dev/dri/card0"),
Some(PathBuf::from("/sys/devices/other-gpu")),
),
(PathBuf::from("/dev/dri/card2"), Some(identity)),
];
assert_eq!(
slot.current_path(original, &observed),
Some(Path::new("/dev/dri/card2"))
);
assert!(slot.current_path(original, &observed[..1]).is_none());
assert!(slot.current_path(original, &[]).is_none());
}
#[test]
fn unavailable_devices_retry_independently_with_capped_backoff() {
let now = Instant::now();
let mut topology = Topology::default();
topology.devices.insert("/dev/dri/card0".into(), DeviceState::default());
let healthy = DrmNode::from_dev_id(libc::makedev(226, 1)).unwrap();
let mut second = DeviceState::default();
second.ready(healthy);
topology.devices.insert("/dev/dri/card1".into(), second);
let device = topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap();
device.failed(now);
assert_eq!(device.retry_at, Some(now + Duration::from_secs(1)));
for _ in 0..20 {
device.failed(now);
}
assert_eq!(device.retry_at, Some(now + Duration::from_secs(32)));
assert_eq!(
topology.devices[Path::new("/dev/dri/card1")].availability,
Availability::Ready(healthy)
);
assert!(topology.retry_deadline(false).is_none());
assert_eq!(topology.retry_deadline(true), Some(now + Duration::from_secs(32)));
topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap().missing();
assert!(topology.retry_deadline(true).is_none());
}
}
@@ -0,0 +1,669 @@
//! Validate a device-wide desired topology, then use Smithay for every real commit.
//! Smithay exposes TEST_ONLY per surface, but not a combined CRTC request. The
//! one raw request here is validation only; mode/plane ownership stays in Smithay.
use super::*;
use smithay::backend::allocator::Allocator;
use smithay::backend::drm::exporter::{ExportBuffer, ExportFramebuffer};
use smithay::reexports::drm::control::{AtomicCommitFlags, ResourceHandle, atomic::AtomicModeReq};
fn property<D: ControlDevice, H: ResourceHandle>(drm: &D, handle: H, name: &str) -> io::Result<property::Handle> {
for (property, _) in drm.get_properties(handle)?.iter() {
if drm.get_property(*property)?.name().to_bytes() == name.as_bytes() {
return Ok(*property);
}
}
Err(io::Error::other(format!("missing DRM property {name}")))
}
fn add<H: ResourceHandle>(
drm: &DrmDevice,
request: &mut AtomicModeReq,
handle: H,
name: &str,
value: u64,
) -> io::Result<()> {
request.add_raw_property(handle.into(), property(drm, handle, name)?, value);
Ok(())
}
fn clear_secondary_planes(
drm: &DrmDevice,
request: &mut AtomicModeReq,
crtc: crtc::Handle,
) -> Result<(), Box<dyn Error>> {
let planes = drm.planes(&crtc)?;
for plane in planes.cursor.iter().chain(&planes.overlay) {
let properties = drm.get_properties(plane.handle)?;
let owned = properties.iter().any(|(handle, value)| {
drm.get_property(*handle)
.is_ok_and(|info| info.name().to_bytes() == b"CRTC_ID" && *value == u32::from(crtc) as u64)
});
if owned {
add(drm, request, plane.handle, "FB_ID", 0)?;
add(drm, request, plane.handle, "CRTC_ID", 0)?;
}
}
Ok(())
}
struct Blobs {
fd: DrmDeviceFd,
ids: Vec<u64>,
}
impl Drop for Blobs {
fn drop(&mut self) {
for id in &self.ids {
let _ = self.fd.destroy_property_blob(*id);
}
}
}
pub(super) fn validate_device(device: &DirectDevice, scan: &OutputScan) -> Result<(), Box<dyn Error>> {
if scan.selections.iter().all(|selection| {
device
.outputs
.get(&selection.crtc)
.is_some_and(|output| output.connector == selection.connector.handle() && output.mode == selection.mode)
}) && scan.selections.len() == device.outputs.len()
{
return Ok(());
}
// Legacy KMS has no atomic test. Smithay validates its surface/mode as far as
// the API permits; commit failures still go through the restoration path.
if !device.drm.is_atomic() {
return Ok(());
}
let drm = &device.drm;
let mut request = AtomicModeReq::new();
let mut blobs = Blobs {
fd: drm.device_fd().clone(),
ids: Vec::new(),
};
let mut buffers = Vec::new();
let mut framebuffers = Vec::new();
let mut allocator = GbmAllocator::new(device.gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
let exporter = GbmFramebufferExporter::new(device.gbm.clone(), Some(device.render_node));
let mut used_planes = HashSet::new();
let desired_crtcs = scan
.selections
.iter()
.map(|selection| selection.crtc)
.collect::<HashSet<_>>();
let desired_connectors = scan
.selections
.iter()
.map(|selection| selection.connector.handle())
.collect::<HashSet<_>>();
for output in device.outputs.values() {
if !desired_connectors.contains(&output.connector) {
add(drm, &mut request, output.connector, "CRTC_ID", 0)?;
}
if !desired_crtcs.contains(&output.surface.crtc()) {
clear_secondary_planes(drm, &mut request, output.surface.crtc())?;
add(drm, &mut request, output.surface.crtc(), "ACTIVE", 0)?;
add(drm, &mut request, output.surface.crtc(), "MODE_ID", 0)?;
add(drm, &mut request, output.surface.plane(), "FB_ID", 0)?;
add(drm, &mut request, output.surface.plane(), "CRTC_ID", 0)?;
}
}
for selection in &scan.selections {
clear_secondary_planes(drm, &mut request, selection.crtc)?;
let planes = drm.planes(&selection.crtc)?;
let primary = device
.outputs
.get(&selection.crtc)
.map(|output| output.surface.plane())
.or_else(|| {
planes
.primary
.iter()
.find(|plane| !used_planes.contains(&plane.handle))
.map(|plane| plane.handle)
})
.ok_or("no usable primary plane")?;
used_planes.insert(primary);
if let Ok(rotation) = property(drm, primary, "rotation") {
request.add_raw_property(primary.into(), rotation, 1);
}
let blob: u64 = drm.create_property_blob(&selection.mode)?.into();
blobs.ids.push(blob);
add(drm, &mut request, selection.crtc, "MODE_ID", blob)?;
add(drm, &mut request, selection.crtc, "ACTIVE", 1)?;
add(
drm,
&mut request,
selection.connector.handle(),
"CRTC_ID",
u32::from(selection.crtc) as u64,
)?;
let (width, height) = selection.mode.size();
let formats = &planes
.primary
.iter()
.find(|plane| plane.handle == primary)
.ok_or("primary plane disappeared")?
.formats;
let mut allocated = None;
for format in formats.iter().filter(|format| {
matches!(
format.code,
Fourcc::Argb8888 | Fourcc::Abgr8888 | Fourcc::Xrgb8888 | Fourcc::Xbgr8888
)
}) {
if let Ok(buffer) = allocator.create_buffer(width.into(), height.into(), format.code, &[format.modifier])
&& let Ok(Some(fb)) = exporter.add_framebuffer(drm.device_fd(), ExportBuffer::Allocator(&buffer), false)
{
allocated = Some((buffer, fb));
break;
}
}
let (buffer, fb) = allocated.ok_or("unable to allocate a DRM validation buffer")?;
add(drm, &mut request, primary, "FB_ID", u32::from(*fb.as_ref()) as u64)?;
add(drm, &mut request, primary, "CRTC_ID", u32::from(selection.crtc) as u64)?;
for (name, value) in [
("SRC_X", 0),
("SRC_Y", 0),
("SRC_W", (width as u64) << 16),
("SRC_H", (height as u64) << 16),
("CRTC_X", 0),
("CRTC_Y", 0),
("CRTC_W", width as u64),
("CRTC_H", height as u64),
] {
add(drm, &mut request, primary, name, value)?;
}
buffers.push(buffer);
framebuffers.push(fb);
}
drm.atomic_commit(AtomicCommitFlags::TEST_ONLY | AtomicCommitFlags::ALLOW_MODESET, request)?;
Ok(())
}
fn prepare(
surface: &mut OutputCompositor,
renderer: &mut GlesRenderer,
mode: DrmMode,
settings: &OutputSettings,
) -> Result<(), Box<dyn Error>> {
surface.use_mode(mode)?;
surface.set_output_mode_source(OutputModeSource::Static {
size: OutputMode::from(mode).size,
scale: settings.scale.into(),
transform: output_transform(settings.transform),
});
surface.reset_buffer_ages();
let elements: Vec<crate::render::AnimatedWindowRenderElement> = Vec::new();
let result = surface.render_frame(
renderer,
&elements,
[0.0, 0.0, 0.0, 1.0],
smithay::backend::drm::compositor::FrameFlags::empty(),
)?;
if let PrimaryPlaneElement::Swapchain(primary) = &result.primary_element {
primary.sync.wait()?;
}
Ok(())
}
/// Retire a logical output only after its hardware is unavailable or confirmed disabled.
fn retire(state: &mut Ferese, output: &mut DirectOutput, node: DrmNode, crtc: crtc::Handle) {
cancel_output_timers(&state.loop_handle, output);
if let Some(global) = output.global.take() {
state.display_handle.disable_global::<Ferese>(global);
}
state.session_lock.output_removed(&output.output);
state.unregister_output(&output.output);
state
.direct_backend
.as_mut()
.unwrap()
.presentation
.remove(&(node, crtc));
}
struct DeviceTransaction<'a> {
state: &'a mut Ferese,
device: &'a mut DirectDevice,
selections: &'a HashMap<crtc::Handle, OutputSelection>,
staged: HashMap<crtc::Handle, DirectOutput>,
replaced: HashSet<crtc::Handle>,
}
impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
type Key = crtc::Handle;
fn stage(&mut self, crtc: crtc::Handle) -> Result<(), String> {
let selection = &self.selections[&crtc];
if self
.device
.outputs
.get(&crtc)
.is_none_or(|output| output.connector != selection.connector.handle())
{
let output = create_direct_output(
self.state,
&mut self.device.drm,
&self.device.gbm,
&self.device.renderer,
selection.clone(),
false,
)
.map_err(|error| error.to_string())?;
self.staged.insert(crtc, output);
}
let output = self
.staged
.get_mut(&crtc)
.or_else(|| self.device.outputs.get_mut(&crtc))
.unwrap();
output
.surface
.set_connectors(&[selection.connector.handle()])
.map_err(|error| error.to_string())?;
prepare(
&mut output.surface,
&mut self.device.renderer,
selection.mode,
&selection.settings,
)
.map_err(|error| error.to_string())
}
fn commit(&mut self, crtc: crtc::Handle) -> Result<(), String> {
if self.staged.contains_key(&crtc) && self.device.outputs.contains_key(&crtc) {
// Connector and mode cannot be changed together through use_mode /
// set_connectors: each tests against the other's old value. Stage a
// replacement surface, retire the old one *before* its first commit
// (Smithay clears the CRTC on surface drop), then publish metadata.
let replacement = self.staged.remove(&crtc).unwrap();
let previous = self.device.outputs.get_mut(&crtc).unwrap();
previous.surface = replacement.surface;
self.replaced.insert(crtc);
}
self.staged
.get_mut(&crtc)
.or_else(|| self.device.outputs.get_mut(&crtc))
.unwrap()
.surface
.commit_frame()
.map_err(|error| error.to_string())
}
fn disable(&mut self, crtc: crtc::Handle) -> Result<(), String> {
self.device
.outputs
.get_mut(&crtc)
.unwrap()
.surface
.clear()
.map_err(|error| error.to_string())
}
fn abort_staged(&mut self) {
for output in self.staged.values_mut() {
let _ = output.surface.clear();
}
self.staged.clear();
}
fn restore(&mut self, crtc: crtc::Handle) -> Result<(), String> {
if self.replaced.contains(&crtc) {
let output = &self.device.outputs[&crtc];
let connector = self
.device
.drm
.get_connector(output.connector, true)
.map_err(|error| error.to_string())?;
if connector.state() != connector::State::Connected {
return Err("previous connector is no longer available".into());
}
let selection = OutputSelection {
connector,
crtc,
mode: output.mode,
settings: output.settings.clone(),
identity: output.identity.clone(),
mirror_source: output.mirror_source.clone(),
};
let mut replacement = create_direct_output(
self.state,
&mut self.device.drm,
&self.device.gbm,
&self.device.renderer,
selection,
false,
)
.map_err(|error| error.to_string())?;
prepare(
&mut replacement.surface,
&mut self.device.renderer,
replacement.mode,
&replacement.settings,
)
.map_err(|error| error.to_string())?;
let previous = self.device.outputs.get_mut(&crtc).unwrap();
previous.surface = replacement.surface;
previous.surface.commit_frame().map_err(|error| error.to_string())?;
}
let Some(output) = self.device.outputs.get_mut(&crtc) else {
return Ok(());
};
output.surface.reset_state().map_err(|error| error.to_string())?;
let restored = output
.surface
.set_connectors(&[output.connector])
.map_err(|error| error.to_string())
.and_then(|()| {
prepare(
&mut output.surface,
&mut self.device.renderer,
output.mode,
&output.settings,
)
.map_err(|error| error.to_string())
})
.and_then(|()| output.surface.commit_frame().map_err(|error| error.to_string()));
self.state.session_lock.output_added(&output.output);
output.lock_frame_pending = false;
output.surface.set_output_mode_source((&output.output).into());
cancel_output_timers(&self.state.loop_handle, output);
output.frame_pending = false;
restored
}
}
/// All allocations/tests precede commits. Logical publication follows every
/// successful commit on this device; other devices are separately rollbackable.
pub(super) fn apply_device(
state: &mut Ferese,
node: DrmNode,
device: &mut DirectDevice,
scan: OutputScan,
) -> Result<(), String> {
let selections = scan
.selections
.into_iter()
.map(|selection| (selection.crtc, selection))
.collect::<HashMap<_, _>>();
let mut changed = selections
.iter()
.filter_map(|(crtc, selection)| {
let same = device.outputs.get(crtc).is_some_and(|output| {
output.connector == selection.connector.handle()
&& output.identity == selection.identity
&& output.mode == selection.mode
&& output.settings.same_configuration(&selection.settings)
&& output.mirror_source == selection.mirror_source
});
(!same).then_some(*crtc)
})
.collect::<Vec<_>>();
changed.sort_by_key(|crtc| (selections[crtc].mirror_source.is_some(), u32::from(*crtc)));
let removals = device
.outputs
.keys()
.filter(|crtc| !selections.contains_key(crtc))
.copied()
.collect::<Vec<_>>();
let hardware_changes = changed
.iter()
.filter(|crtc| {
!device.outputs.get(crtc).is_some_and(|output| {
let selection = &selections[crtc];
output.connector == selection.connector.handle() && output.mode == selection.mode
})
})
.copied()
.collect::<Vec<_>>();
let mut driver = DeviceTransaction {
state,
device,
selections: &selections,
staged: HashMap::new(),
replaced: HashSet::new(),
};
let result = crate::output_transaction::apply(&mut driver, &hardware_changes, &removals);
let mut staged = std::mem::take(&mut driver.staged);
if let Err(failure) = result {
for crtc in failure.unavailable {
if let Some(mut output) = device.outputs.remove(&crtc) {
retire(state, &mut output, node, crtc);
}
}
return Err(failure.error);
}
for crtc in &changed {
let selection = &selections[crtc];
let mut output = staged.remove(crtc).or_else(|| device.outputs.remove(crtc)).unwrap();
let old_geometry = state.space.output_geometry(&output.output);
let old_id = state.output_id(&output.output);
if output.connector != selection.connector.handle() || output.identity != selection.identity {
retire(state, &mut output, node, *crtc);
let (handle, _) = create_output(
state,
&selection.connector,
selection.mode,
selection.identity.clone(),
&selection.settings,
false,
);
output.output = handle;
}
if selection.mirror_source.is_some() && output.global.is_some() {
retire(state, &mut output, node, *crtc);
}
output.connector = selection.connector.handle();
output.internal = internal_connector(selection.connector.interface());
output.identity = selection.identity.clone();
output.settings = selection.settings.clone();
output.mode = selection.mode;
output.mirror_source = selection.mirror_source.clone();
let position = selection.settings.position.unwrap_or_else(|| {
old_geometry
.map(|geometry| [geometry.loc.x, geometry.loc.y])
.unwrap_or_else(|| {
[
state
.space
.outputs()
.filter_map(|output| state.space.output_geometry(output))
.map(|geometry| geometry.loc.x + geometry.size.w)
.max()
.unwrap_or(0),
0,
]
})
});
output.output.change_current_state(
Some(OutputMode::from(output.mode)),
Some(output_transform(output.settings.transform)),
Some(Scale::Fractional(output.settings.scale)),
Some((position[0], position[1]).into()),
);
if output.mirror_source.is_none() {
if output.global.is_none() {
output.global = Some(output.output.create_global::<Ferese>(&state.display_handle));
}
state.space.map_output(&output.output, (position[0], position[1]));
if state.output_id(&output.output).is_none() {
state.register_output(&output.output, output.identity.clone());
}
if let Some(id) = state.output_id(&output.output)
&& let Some(geometry) = state.space.output_geometry(&output.output)
{
state.output_workspaces.update_geometry(
id,
ferese_core::OutputGeometry::new(geometry.loc.x, geometry.loc.y, geometry.size.w, geometry.size.h),
);
if old_id == Some(id)
&& let Some(old) = old_geometry
{
state.reposition_output_floats(
id,
ferese_layout::Rect::new(
old.loc.x as f64,
old.loc.y as f64,
old.size.w as f64,
old.size.h as f64,
),
ferese_layout::Rect::new(
geometry.loc.x as f64,
geometry.loc.y as f64,
geometry.size.w as f64,
geometry.size.h as f64,
),
);
}
}
}
state.session_lock.output_added(&output.output);
output.lock_frame_pending = false;
output.surface.set_output_mode_source((&output.output).into());
cancel_output_timers(&state.loop_handle, &mut output);
output.frame_pending = false;
output.primary_commit = None;
output.capture_texture = None;
output.power.suspended();
let clock = state
.direct_backend
.as_mut()
.unwrap()
.presentation
.entry((node, *crtc))
.or_default();
clock.set_refresh(OutputMode::from(output.mode).refresh);
clock.reset_timing();
device.outputs.insert(*crtc, output);
}
// Add/register destinations before removing former owners, reusing evacuation.
for crtc in removals {
let mut output = device.outputs.remove(&crtc).unwrap();
retire(state, &mut output, node, crtc);
}
device.connected_outputs = scan.connected_outputs;
update_applied_info(device);
Ok(())
}
pub(super) fn update_applied_info(device: &mut DirectDevice) {
for info in &mut device.connected_outputs {
info.enabled = false;
info.current_mode = None;
if let Some(output) = device.outputs.values().find(|output| output.identity == info.identity) {
info.enabled = true;
info.current_mode = Some(connected_mode_info(output.mode));
info.scale = output.settings.scale;
info.transform = output.settings.transform;
info.configured_position = output.settings.position;
}
}
}
/// Disconnected hardware cannot remain logically live, even if a new policy fails validation.
pub(super) fn retire_disconnected(state: &mut Ferese) {
let missing = state
.direct_backend
.as_ref()
.unwrap()
.devices
.iter()
.flat_map(|(node, device)| {
device.outputs.iter().filter_map(|(crtc, output)| {
device
.drm
.get_connector(output.connector, true)
.ok()
.filter(|connector| connector.state() != connector::State::Connected)
.map(|_| (*node, *crtc))
})
})
.collect::<Vec<_>>();
for (node, crtc) in missing {
if let Some(mut output) = state
.direct_backend
.as_mut()
.unwrap()
.devices
.get_mut(&node)
.unwrap()
.outputs
.remove(&crtc)
{
let _ = output.surface.clear();
retire(state, &mut output, node, crtc);
}
}
}
/// If initial activation or rollback leaves no usable desktop, try a preferred
/// mode on each physical monitor independently. Never disable a surviving output.
pub(super) fn recover_last_output(state: &mut Ferese) {
if state
.direct_backend
.as_ref()
.unwrap()
.devices
.values()
.any(|device| device.outputs.values().any(|output| output.global.is_some()))
{
return;
}
let backend = state.direct_backend.as_ref().unwrap();
let requested = backend.desired_outputs.clone();
let original_error = backend.output_error.clone();
let mut candidates = backend
.monitors
.iter()
.filter(|monitor| monitor.usable)
.cloned()
.collect::<Vec<_>>();
candidates.sort_by_key(|monitor| (!monitor.internal, monitor.key.clone()));
for candidate in candidates {
let backend = state.direct_backend.as_mut().unwrap();
backend.desired_outputs = DesiredOutputConfiguration {
profile: None,
suspend: requested.suspend,
outputs: backend
.monitors
.iter()
.map(|monitor| {
let mut settings = crate::output_policy::defaults(monitor.identity.clone());
settings.enabled = monitor.key == candidate.key;
crate::output_policy::DesiredOutput {
key: monitor.key.clone(),
settings,
mirror_source: None,
}
})
.collect(),
};
let nodes = backend
.devices
.iter()
.filter(|(_, device)| candidate.key.starts_with(&format!("{}/", drm_device_key(&device.drm))))
.map(|(node, _)| *node)
.collect::<Vec<_>>();
if validate_desired_outputs(state).is_ok() {
for node in nodes {
let _ = rescan_device(state, node);
}
if state
.direct_backend
.as_ref()
.unwrap()
.devices
.values()
.any(|device| device.outputs.values().any(|output| output.global.is_some()))
{
remember_applied_configuration(state);
state.direct_backend.as_mut().unwrap().output_error = original_error;
tracing::warn!(identity = %candidate.identity, "recovered last usable output with preferred mode");
return;
}
}
}
let backend = state.direct_backend.as_mut().unwrap();
backend.desired_outputs = requested;
backend.output_error = original_error;
}