diff --git a/compositor/src/main.rs b/compositor/src/main.rs index 61c2c53..94b4fce 100644 --- a/compositor/src/main.rs +++ b/compositor/src/main.rs @@ -135,9 +135,17 @@ fn main() -> Result<(), Box> { } // All input/Wayland callbacks have returned, releasing seat locks. // Coalesce cursor changes and redraw here, never inside cursor_image. - if std::mem::take(&mut state.cursor_redraw_pending) || wallpaper_changed { + let wallpaper_retry = state.wallpaper.take_retry_wakeup(); + if std::mem::take(&mut state.cursor_redraw_pending) || wallpaper_changed || wallpaper_retry + { backends::direct::render_all(state); } + if let Err(error) = state + .wallpaper + .arm_retry_timer(&reload_handle, std::time::Instant::now()) + { + warn!(%error, "could not schedule wallpaper upload retry"); + } // Registry/sync/configure replies must not depend on a submitted // frame: at startup clients need these before they can draw anything. // This also keeps idle DRM sessions responsive when there is no damage. diff --git a/compositor/src/wallpaper.rs b/compositor/src/wallpaper.rs index 898d90d..9a5f95c 100644 --- a/compositor/src/wallpaper.rs +++ b/compositor/src/wallpaper.rs @@ -16,8 +16,10 @@ use smithay::{ utils::{Buffer, Physical, Rectangle, Size}, }; use std::{ + cell::Cell, collections::HashMap, path::PathBuf, + rc::Rc, sync::mpsc, time::{Duration, Instant}, }; @@ -68,6 +70,8 @@ pub(crate) struct WallpaperState { textures: HashMap, upload_retries: HashMap, wakeup: Option, + retry_timer_pending: Rc>, + retry_wakeup: Rc>, } impl WallpaperState { @@ -114,6 +118,8 @@ impl WallpaperState { textures: HashMap::new(), upload_retries: HashMap::new(), wakeup, + retry_timer_pending: Rc::default(), + retry_wakeup: Rc::default(), } } @@ -163,6 +169,45 @@ impl WallpaperState { .is_none_or(|retry| now >= *retry) } + pub(crate) fn take_retry_wakeup(&self) -> bool { + self.retry_wakeup.replace(false) && !self.upload_retries.is_empty() + } + + fn next_retry_deadline(&self, now: Instant) -> Option { + self.upload_retries.values().copied().min().map(|deadline| { + // A suspended/hidden output may not have attempted an upload when + // the timer fired. Avoid immediately rescheduling an expired timer. + if deadline <= now { + now + UPLOAD_RETRY_DELAY + } else { + deadline + } + }) + } + + pub(crate) fn arm_retry_timer( + &self, + handle: &smithay::reexports::calloop::LoopHandle<'static, Data>, + now: Instant, + ) -> Result<(), Box> { + use smithay::reexports::calloop::timer::{TimeoutAction, Timer}; + if self.retry_timer_pending.get() { + return Ok(()); + } + let Some(deadline) = self.next_retry_deadline(now) else { + return Ok(()); + }; + let pending = self.retry_timer_pending.clone(); + let wakeup = self.retry_wakeup.clone(); + handle.insert_source(Timer::from_deadline(deadline), move |_, _, _| { + pending.set(false); + wakeup.set(true); + TimeoutAction::Drop + })?; + self.retry_timer_pending.set(true); + Ok(()) + } + pub fn poll(&mut self) -> bool { let Some(receiver) = &self.receiver else { return false; @@ -310,6 +355,47 @@ fn image_geometry( mod tests { use super::*; + #[test] + fn failed_upload_retry_wakes_idle_loop_and_does_not_spin_when_output_is_inactive() { + use smithay::backend::renderer::ContextId; + use smithay::reexports::calloop::EventLoop; + let mut event_loop = EventLoop::<()>::try_new().unwrap(); + let mut state = WallpaperState::new(WallpaperConfig { + path: None, + mode: WallpaperMode::Fill, + }); + let context = ContextId::::new().erased(); + let now = Instant::now(); + state + .upload_retries + .insert(context.clone(), now + Duration::from_millis(10)); + state.arm_retry_timer(&event_loop.handle(), now).unwrap(); + state.arm_retry_timer(&event_loop.handle(), now).unwrap(); + assert!(state.retry_timer_pending.get()); + assert!(!state.take_retry_wakeup()); + event_loop + .dispatch(Some(Duration::from_secs(1)), &mut ()) + .unwrap(); + assert!(!state.retry_timer_pending.get()); + assert!( + state.take_retry_wakeup(), + "retry must request redraw without input or client activity" + ); + assert!(!state.take_retry_wakeup()); + let after = Instant::now(); + assert_eq!( + state.next_retry_deadline(after), + Some(after + UPLOAD_RETRY_DELAY) + ); + state.forget_context(&context); + assert_eq!(state.next_retry_deadline(after), None); + state.retry_wakeup.set(true); + assert!( + !state.take_retry_wakeup(), + "cleared failures must not trigger stale redraws" + ); + } + #[test] fn decoder_wakes_an_idle_event_loop() { use smithay::reexports::calloop::{EventLoop, timer::Timer};