Refresh lid observations before rendering after resume

This commit is contained in:
Samuel Onoja committed 2026-10-02 09:41:40 +01:00
1 parent 0420e59cbe
commit 428267a714
6 files changed
+407 -53

No files matched your search

+30 -26
View File
@@ -6,6 +6,7 @@ use std::path::Path;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
mod capture; mod capture;
mod lid;
mod output_power; mod output_power;
mod planes; mod planes;
mod redraw; mod redraw;
@@ -53,7 +54,8 @@ use crate::render::{frame_effect_metrics, sampled_output_elements};
pub struct DirectBackendState { pub struct DirectBackendState {
pub session: LibSeatSession, pub session: LibSeatSession,
pub active: bool, pub active: bool,
lid_closed: bool, lid: lid::LidState,
lid_reader: lid::Reader,
topology: Topology, topology: Topology,
pub(crate) low_power: bool, pub(crate) low_power: bool,
devices: HashMap<DrmNode, DirectDevice>, devices: HashMap<DrmNode, DirectDevice>,
@@ -69,7 +71,7 @@ pub struct DirectBackendState {
impl DirectBackendState { impl DirectBackendState {
fn can_render(&self) -> bool { fn can_render(&self) -> bool {
self.active && !self.topology.reconciling self.active && !self.topology.reconciling && !self.lid.pending()
} }
pub(crate) fn dump_scheduling_metrics(&self) { pub(crate) fn dump_scheduling_metrics(&self) {
@@ -239,10 +241,12 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
libinput_context.suspend(); libinput_context.suspend();
} }
let lid_reader = lid::Reader::start(state)?;
state.direct_backend = Some(DirectBackendState { state.direct_backend = Some(DirectBackendState {
session, session,
active: session_active, active: session_active,
lid_closed: initial_lid_closed(), lid: lid::LidState::default(),
lid_reader,
topology: Topology::default(), topology: Topology::default(),
low_power: super::power::low_power(false, super::power::battery_percent()), low_power: super::power::low_power(false, super::power::battery_percent()),
devices: HashMap::new(), devices: HashMap::new(),
@@ -263,8 +267,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
.topology .topology
.devices .devices
.insert(primary_path, Default::default()); .insert(primary_path, Default::default());
reconcile_outputs(state, false); lid::request_refresh(state, false);
sync_battery_timer(state);
event_loop event_loop
.handle() .handle()
@@ -291,6 +294,10 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
let handle = state.loop_handle.clone(); let handle = state.loop_handle.clone();
if let Some(backend) = state.direct_backend.as_mut() { if let Some(backend) = state.direct_backend.as_mut() {
backend.active = false; backend.active = false;
backend.lid.pause();
if let Some(token) = backend.lid_reader.deadline.take() {
handle.remove(token);
}
backend.topology.dirty = true; backend.topology.dirty = true;
if let Some(token) = backend.topology.retry_timer.take() { if let Some(token) = backend.topology.retry_timer.take() {
handle.remove(token); handle.remove(token);
@@ -336,12 +343,8 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
let seat = state.seat.clone(); let seat = state.seat.clone();
state.idle_notifier_state.notify_activity(&seat); state.idle_notifier_state.notify_activity(&seat);
reconcile_outputs(state, true); lid::request_refresh(state, true);
sync_battery_timer(state); tracing::info!("direct session reacquired; refreshing external state");
state.reset_animation_clock();
tracing::info!("direct session activated");
state.lock_input_activity();
render_all(state);
} }
})?; })?;
event_loop.handle().insert_source(udev_backend, |event, _, state| { event_loop.handle().insert_source(udev_backend, |event, _, state| {
@@ -468,7 +471,7 @@ pub(crate) fn validate_live_outputs(state: &Ferese, profiles: &[OutputProfile])
let mut usable = 0; let mut usable = 0;
for device in backend.devices.values() { for device in backend.devices.values() {
let mut scan = select_outputs(&device.drm, profiles, backend.low_power).map_err(|e| e.to_string())?; let mut scan = select_outputs(&device.drm, profiles, backend.low_power).map_err(|e| e.to_string())?;
apply_lid_policy(&mut scan, backend.lid_closed); apply_lid_policy(&mut scan, backend.lid.closed());
usable += scan.selections.len(); usable += scan.selections.len();
} }
if usable == 0 { if usable == 0 {
@@ -864,22 +867,19 @@ pub(crate) fn set_lid_closed(state: &mut Ferese, closed: bool) {
let Some(backend) = state.direct_backend.as_mut() else { let Some(backend) = state.direct_backend.as_mut() else {
return; return;
}; };
if backend.lid_closed == closed { // Even an unchanged switch observation supersedes an in-flight DBus read.
return; if backend.lid.observe(closed) {
tracing::info!(closed, "laptop lid state changed");
reconcile_outputs(state, false);
} }
backend.lid_closed = closed;
tracing::info!(closed, "laptop lid state changed");
reconcile_outputs(state, false);
} }
fn initial_lid_closed() -> bool { pub(crate) fn system_resumed(state: &mut Ferese) {
std::fs::read_dir("/proc/acpi/button/lid") if state.direct_backend.as_ref().is_some_and(|backend| backend.active) {
.ok() let seat = state.seat.clone();
.into_iter() state.idle_notifier_state.notify_activity(&seat);
.flatten() }
.filter_map(Result::ok) lid::request_refresh(state, true);
.filter_map(|entry| std::fs::read_to_string(entry.path().join("state")).ok())
.any(|state| state.split_whitespace().last() == Some("closed"))
} }
fn internal_connector(interface: connector::Interface) -> bool { fn internal_connector(interface: connector::Interface) -> bool {
@@ -1122,7 +1122,10 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) -> Result<bool, ()> {
} }
}; };
let lid_closed = state.direct_backend.as_ref().is_some_and(|backend| backend.lid_closed); let lid_closed = state
.direct_backend
.as_ref()
.is_some_and(|backend| backend.lid.closed());
apply_lid_policy(&mut scan, lid_closed); apply_lid_policy(&mut scan, lid_closed);
device.connected_outputs = scan.connected_outputs; device.connected_outputs = scan.connected_outputs;
let mut complete = true; let mut complete = true;
@@ -1562,6 +1565,7 @@ fn deliver_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle
output.callback_timer = None; output.callback_timer = None;
if !backend.active if !backend.active
|| backend.topology.reconciling || backend.topology.reconciling
|| backend.lid.pending()
|| !device.drm.is_active() || !device.drm.is_active()
|| !output.power.can_render() || !output.power.can_render()
|| state.session_lock.sleeping || state.session_lock.sleeping
+304
View File
@@ -0,0 +1,304 @@
//! Resume reads are asynchronous; newer switch events win over old replies.
use std::sync::mpsc;
use super::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct Probe {
serial: u64,
revision: u64,
reactivate: bool,
}
#[derive(Default)]
pub(super) struct LidState {
value: Option<bool>,
revision: u64,
serial: u64,
pending: Option<Probe>,
}
impl LidState {
pub fn closed(&self) -> bool {
self.value == Some(true)
}
pub fn pending(&self) -> bool {
self.pending.is_some()
}
pub fn observe(&mut self, closed: bool) -> bool {
let changed = self.value != Some(closed);
self.value = Some(closed);
self.revision = self.revision.wrapping_add(1);
changed
}
fn begin(&mut self, reactivate: bool) -> Probe {
self.serial = self.serial.wrapping_add(1);
let probe = Probe {
serial: self.serial,
revision: self.revision,
reactivate: reactivate || self.pending.is_some_and(|pending| pending.reactivate),
};
self.pending = Some(probe);
probe
}
fn complete(&mut self, probe: Probe, value: Option<bool>) -> Option<bool> {
if self.pending != Some(probe) {
return None;
}
self.pending = None;
if probe.revision == self.revision
&& let Some(value) = value
{
self.observe(value);
}
Some(probe.reactivate)
}
pub fn pause(&mut self) {
self.pending = None;
}
}
pub(super) struct Reader {
requests: mpsc::Sender<Probe>,
pub deadline: Option<RegistrationToken>,
}
fn read_logind(connection: &zbus::blocking::Connection) -> zbus::Result<bool> {
// A direct Get avoids retaining a property cache across suspend.
let value: zbus::zvariant::OwnedValue = connection
.call_method(
Some("org.freedesktop.login1"),
"/org/freedesktop/login1",
Some("org.freedesktop.DBus.Properties"),
"Get",
&("org.freedesktop.login1.Manager", "LidClosed"),
)?
.body()
.deserialize()?;
bool::try_from(value).map_err(Into::into)
}
fn read_acpi() -> Option<bool> {
let mut state = None;
for entry in std::fs::read_dir("/proc/acpi/button/lid").ok()?.flatten() {
let Ok(text) = std::fs::read_to_string(entry.path().join("state")) else {
continue;
};
match text.split_whitespace().last() {
Some("closed") => return Some(true),
Some("open") => state = Some(false),
_ => {}
}
}
state
}
impl Reader {
pub fn start(state: &Ferese) -> Result<Self, Box<dyn Error>> {
let (requests, receiver) = mpsc::channel::<Probe>();
let (replies, events) = smithay::reexports::calloop::channel::channel();
state.loop_handle.insert_source(events, |event, _, state| {
if let smithay::reexports::calloop::channel::Event::Msg((probe, value)) = event {
complete_refresh(state, probe, value);
}
})?;
std::thread::Builder::new()
.name("ferese-lid-reader".into())
.spawn(move || {
while let Ok(mut probe) = receiver.recv() {
for newer in receiver.try_iter() {
probe = newer;
}
let value = zbus::blocking::connection::Builder::system()
.and_then(|builder| builder.method_timeout(Duration::from_secs(2)).build())
.and_then(|connection| read_logind(&connection))
.ok()
.or_else(read_acpi);
if replies.send((probe, value)).is_err() {
break;
}
}
})?;
Ok(Self {
requests,
deadline: None,
})
}
}
pub(super) fn request_refresh(state: &mut Ferese, reactivate: bool) {
let Some(backend) = state.direct_backend.as_mut() else {
return;
};
if !backend.active {
backend.topology.dirty = true;
return;
}
if let Some(token) = backend.lid_reader.deadline.take() {
state.loop_handle.remove(token);
}
// A timer that fires while readiness is gated would otherwise leave a
// stale registration token on an unchanged output.
for device in backend.devices.values_mut() {
for output in device.outputs.values_mut() {
cancel_output_timers(&state.loop_handle, output);
}
}
let probe = backend.lid.begin(reactivate);
let sent = backend.lid_reader.requests.send(probe).is_ok();
let timer = state
.loop_handle
.insert_source(Timer::from_duration(Duration::from_secs(3)), move |_, _, state| {
if let Some(backend) = state.direct_backend.as_mut() {
backend.lid_reader.deadline = None;
}
tracing::warn!("lid refresh deadline elapsed; retaining the latest observed state");
complete_refresh(state, probe, None);
TimeoutAction::Drop
});
if sent && let Ok(token) = timer {
state.direct_backend.as_mut().unwrap().lid_reader.deadline = Some(token);
} else {
if let Ok(token) = timer {
state.loop_handle.remove(token);
}
complete_refresh(state, probe, None);
}
}
fn complete_refresh(state: &mut Ferese, probe: Probe, value: Option<bool>) {
let Some(backend) = state.direct_backend.as_mut() else {
return;
};
let Some(reactivate) = backend.lid.complete(probe, value) else {
return;
};
if let Some(token) = backend.lid_reader.deadline.take() {
state.loop_handle.remove(token);
}
if !backend.active {
return;
}
// Waking a locked session may request redraws. Hold them until the fresh
// device/connector snapshot has been reconciled.
backend.topology.reconciling = true;
state.reset_animation_clock();
state.lock_input_activity();
state.direct_backend.as_mut().unwrap().topology.reconciling = false;
reconcile_outputs(state, reactivate);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore = "requires dbus-daemon and private test sockets"]
fn logind_reads_are_fresh_without_property_change_signals() {
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
struct Bus(std::process::Child);
impl Drop for Bus {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
struct Manager(Arc<AtomicBool>);
#[zbus::interface(name = "org.freedesktop.login1.Manager")]
impl Manager {
#[zbus(property)]
fn lid_closed(&self) -> bool {
self.0.load(Ordering::Relaxed)
}
}
let mut bus = Bus(Command::new("dbus-daemon")
.args(["--session", "--nofork", "--print-address=1"])
.stdout(Stdio::piped())
.spawn()
.unwrap());
let mut address = String::new();
BufReader::new(bus.0.stdout.take().unwrap())
.read_line(&mut address)
.unwrap();
let closed = Arc::new(AtomicBool::new(true));
let server = zbus::blocking::connection::Builder::address(address.trim())
.unwrap()
.name("org.freedesktop.login1")
.unwrap()
.serve_at("/org/freedesktop/login1", Manager(closed.clone()))
.unwrap()
.build()
.unwrap();
let client = zbus::blocking::connection::Builder::address(address.trim())
.unwrap()
.method_timeout(Duration::from_secs(1))
.build()
.unwrap();
assert!(read_logind(&client).unwrap());
closed.store(false, Ordering::Relaxed);
assert!(!read_logind(&client).unwrap());
server.release_name("org.freedesktop.login1").unwrap();
assert!(
read_logind(&client).is_err(),
"missing logind must not masquerade as an open lid"
);
}
#[test]
fn newer_input_wins_over_a_delayed_resume_read() {
let mut lid = LidState::default();
lid.observe(true);
let probe = lid.begin(true);
lid.observe(false);
assert_eq!(lid.complete(probe, Some(true)), Some(true));
assert!(!lid.closed());
}
#[test]
fn failed_reads_preserve_known_state_and_unknown_is_not_an_open_observation() {
let mut lid = LidState::default();
let probe = lid.begin(false);
lid.complete(probe, None);
assert_eq!(lid.value, None);
lid.observe(true);
let probe = lid.begin(true);
lid.complete(probe, None);
assert_eq!(lid.value, Some(true));
}
#[test]
fn pause_and_newer_resume_invalidate_old_replies() {
let mut lid = LidState::default();
let old = lid.begin(true);
lid.pause();
assert_eq!(lid.complete(old, Some(true)), None);
let current = lid.begin(true);
assert_eq!(lid.complete(old, Some(true)), None);
assert!(lid.pending());
assert_eq!(lid.complete(current, Some(false)), Some(true));
assert!(!lid.pending());
}
#[test]
fn system_wake_cannot_cancel_pending_session_reactivation() {
let mut lid = LidState::default();
lid.begin(true);
let probe = lid.begin(false);
assert_eq!(lid.complete(probe, Some(false)), Some(true));
}
}
+14 -1
View File
@@ -114,12 +114,15 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
let Some(backend) = state.direct_backend.as_mut() else { let Some(backend) = state.direct_backend.as_mut() else {
return; return;
}; };
if !backend.topology.request(backend.active) { if !backend.topology.request(backend.active && !backend.lid.pending()) {
return; return;
} }
backend.topology.reconciling = true; backend.topology.reconciling = true;
backend.topology.dirty = false; backend.topology.dirty = false;
if reactivate {
backend.presentation.values_mut().for_each(PresentationClock::reset_timing);
}
if let Some(token) = backend.topology.retry_timer.take() { if let Some(token) = backend.topology.retry_timer.take() {
state.loop_handle.remove(token); state.loop_handle.remove(token);
} }
@@ -198,6 +201,15 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
let device = state.direct_backend.as_mut().unwrap().devices.get_mut(&node).unwrap(); let device = state.direct_backend.as_mut().unwrap().devices.get_mut(&node).unwrap();
device.drm.activate(true).map(|()| { device.drm.activate(true).map(|()| {
for (crtc, output) in &mut device.outputs { for (crtc, output) in &mut device.outputs {
cancel_output_timers(&state.loop_handle, output);
output.surface.reset_buffers();
output.primary_commit = None;
output.capture_texture = None;
output.frame_pending = false;
output.lock_frame_pending = false;
output.power.suspended();
state.display_presentation.remove_output(&output.output);
if let Err(error) = output.surface.clear().and_then(|()| output.surface.reset_state()) { if let Err(error) = output.surface.clear().and_then(|()| output.surface.reset_state()) {
tracing::warn!(%error, ?node, ?crtc, "output reset failed; recreating output"); tracing::warn!(%error, ?node, ?crtc, "output reset failed; recreating output");
failed_outputs.push(*crtc); failed_outputs.push(*crtc);
@@ -291,6 +303,7 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) {
} }
} }
state.refresh_idle_inhibition();
sync_battery_timer(state); sync_battery_timer(state);
state.restore_output_focus(); state.restore_output_focus();
state.relayout(); state.relayout();
+2
View File
@@ -27,6 +27,7 @@ mod render;
#[cfg(feature = "resize-metrics")] #[cfg(feature = "resize-metrics")]
mod resize_metrics; mod resize_metrics;
mod resize_transaction; mod resize_transaction;
mod resume;
mod session_lock; mod session_lock;
mod shell_control; mod shell_control;
mod stacking; mod stacking;
@@ -93,6 +94,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut state = Ferese::new(&mut event_loop, display, runtime)?; let mut state = Ferese::new(&mut event_loop, display, runtime)?;
state.config_source = initial_source.filter(|source| source.len() <= 60 * 1024); state.config_source = initial_source.filter(|source| source.len() <= 60 * 1024);
theme::init(&mut event_loop, &mut state, candidate)?; theme::init(&mut event_loop, &mut state, candidate)?;
resume::init(&mut event_loop)?;
overview::init_font_loader(&mut event_loop, &mut state)?; overview::init_font_loader(&mut event_loop, &mut state)?;
backends::init(launch.backend, &mut event_loop, &mut state)?; backends::init(launch.backend, &mut event_loop, &mut state)?;
info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients"); info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients");
+57
View File
@@ -0,0 +1,57 @@
//! System wake is shared by hardware reconciliation and time-dependent UI state.
use std::error::Error;
use std::time::Duration;
use calloop::EventLoop;
use calloop::channel::{Event, channel};
use crate::Ferese;
pub(crate) fn init(event_loop: &mut EventLoop<Ferese>) -> Result<(), Box<dyn Error>> {
let (sender, receiver) = channel();
event_loop.handle().insert_source(receiver, |event, _, state| {
if let Event::Msg(()) = event {
crate::backends::direct::system_resumed(state);
state.refresh_theme();
}
})?;
std::thread::Builder::new()
.name("ferese-system-resume".into())
.spawn(move || {
let mut warned = false;
loop {
let monitor = || -> zbus::Result<bool> {
let connection = zbus::blocking::connection::Builder::system()?
.method_timeout(Duration::from_secs(2))
.build()?;
let proxy = zbus::blocking::Proxy::new(
&connection,
"org.freedesktop.login1",
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
)?;
for message in proxy.receive_signal("PrepareForSleep")? {
let (sleeping,): (bool,) = message.body().deserialize()?;
if !sleeping && sender.send(()).is_err() {
return Ok(false);
}
}
Ok(true)
};
match monitor() {
Ok(false) => break,
Ok(true) => warned = false,
Err(error) if !warned => {
tracing::warn!(%error, "system resume monitor unavailable; retrying connection");
warned = true;
}
Err(_) => {}
}
// Recovery only: a healthy listener blocks on DBus signals.
std::thread::sleep(Duration::from_secs(5));
}
})?;
Ok(())
}
-26
View File
@@ -265,32 +265,6 @@ pub(crate) fn init(
} }
paths.lock().unwrap().insert(PathBuf::from("/etc/localtime")); paths.lock().unwrap().insert(PathBuf::from("/etc/localtime"));
let wakeup = state.loop_signal.clone(); let wakeup = state.loop_signal.clone();
let resume_sender = sender.clone();
let resume_wakeup = wakeup.clone();
std::thread::Builder::new()
.name("ferese-theme-resume".into())
.spawn(move || {
let monitor = || -> zbus::Result<()> {
let connection = zbus::blocking::Connection::system()?;
let proxy = zbus::blocking::Proxy::new(
&connection,
"org.freedesktop.login1",
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
)?;
for message in proxy.receive_signal("PrepareForSleep")? {
let (sleeping,): (bool,) = message.body().deserialize()?;
if !sleeping {
let _ = resume_sender.try_send(());
resume_wakeup.wakeup();
}
}
Ok(())
};
if let Err(error) = monitor() {
tracing::warn!(%error, "theme resume monitor unavailable");
}
})?;
let system_sender = sender.clone(); let system_sender = sender.clone();
let system_wakeup = wakeup.clone(); let system_wakeup = wakeup.clone();
std::thread::Builder::new() std::thread::Builder::new()