Merge pull request #7 from josephdunn/perf/lock-idle-redraw-dirty-tracking

perf: only redraw the lock screen when something changes
This commit is contained in:
2026-06-20 14:54:35 +02:00
committed by GitHub
4 changed files with 79 additions and 15 deletions
+65 -12
View File
@@ -24,6 +24,12 @@ pub struct LockedSurface {
wayland_surface: Option<wl_surface::WlSurface>, wayland_surface: Option<wl_surface::WlSurface>,
output: wl_output::WlOutput, output: wl_output::WlOutput,
configured: bool, configured: bool,
/// Set whenever rendered state changes (keystroke, status, clock minute,
/// animation step). update() renders only when this is set or an animation
/// is in flight, so an idle lock screen does no per-frame cairo work.
dirty: bool,
/// Last clock minute (unix-minute) we rendered, to detect %H:%M rollover.
last_minute: i64,
} }
impl LockedSurface { impl LockedSurface {
@@ -54,6 +60,8 @@ impl LockedSurface {
wayland_surface: None, wayland_surface: None,
output, output,
configured: false, configured: false,
dirty: true,
last_minute: i64::MIN,
}) })
} }
@@ -62,6 +70,7 @@ impl LockedSurface {
log::debug!("LockedSurface: Configured, starting animation"); log::debug!("LockedSurface: Configured, starting animation");
self.configured = true; self.configured = true;
self.start_time = Instant::now(); self.start_time = Instant::now();
self.dirty = true;
} }
/// Check if this surface matches the given Wayland surface /// Check if this surface matches the given Wayland surface
@@ -72,12 +81,15 @@ impl LockedSurface {
.is_some_and(|ws| ws.id() == surface.id()) .is_some_and(|ws| ws.id() == surface.id())
} }
/// Update the surface state (called on each frame) /// Update the surface state (called on each frame). Returns `true` if the
pub fn update(&mut self) { /// surface was re-rendered and therefore needs to be committed. An idle
/// surface (no input, no animation, same clock minute) returns `false` and
/// does no cairo work, which keeps a locked session near-zero CPU.
pub fn update(&mut self) -> bool {
self.input_handler.update(); self.input_handler.update();
if !self.configured { if !self.configured {
return; return false;
} }
// Update fade animation // Update fade animation
@@ -92,9 +104,22 @@ impl LockedSurface {
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0 1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
}; };
let new_alpha = eased_t.min(1.0); let new_alpha = eased_t.min(1.0);
if (new_alpha - self.fade_alpha).abs() > 0.001 { if t >= 1.0 {
// The eased curve only approaches 1.0 asymptotically, and the
// 0.001 throttle below suppresses the tiny final steps — which
// would leave fade_alpha stuck just under 1.0 forever. Since
// `fade_alpha < 1.0` is our "still animating" signal, that would
// force a full render every frame. Snap to exactly 1.0 once the
// fade duration has elapsed so the animation cleanly completes.
if self.fade_alpha != 1.0 {
self.fade_alpha = 1.0;
self.renderer.set_fade_alpha(1.0);
self.dirty = true;
}
} else if (new_alpha - self.fade_alpha).abs() > 0.001 {
self.fade_alpha = new_alpha; self.fade_alpha = new_alpha;
self.renderer.set_fade_alpha(self.fade_alpha); self.renderer.set_fade_alpha(self.fade_alpha);
self.dirty = true;
} }
} }
@@ -102,6 +127,7 @@ impl LockedSurface {
if self.input_handler.should_show_wrong_password() && !self.wrong_password_shown { if self.input_handler.should_show_wrong_password() && !self.wrong_password_shown {
self.renderer.show_wrong_password(); self.renderer.show_wrong_password();
self.wrong_password_shown = true; self.wrong_password_shown = true;
self.dirty = true;
} else if !self.input_handler.should_show_wrong_password() && self.wrong_password_shown { } else if !self.input_handler.should_show_wrong_password() && self.wrong_password_shown {
self.wrong_password_shown = false; self.wrong_password_shown = false;
} }
@@ -110,12 +136,16 @@ impl LockedSurface {
if self.input_handler.should_show_key_highlight() && !self.key_highlight_shown { if self.input_handler.should_show_key_highlight() && !self.key_highlight_shown {
self.renderer.show_key_highlight(); self.renderer.show_key_highlight();
self.key_highlight_shown = true; self.key_highlight_shown = true;
self.dirty = true;
} else if !self.input_handler.should_show_key_highlight() && self.key_highlight_shown { } else if !self.input_handler.should_show_key_highlight() && self.key_highlight_shown {
self.key_highlight_shown = false; self.key_highlight_shown = false;
} }
// Update caps lock state in renderer // Update caps lock state in renderer
self.renderer.caps_lock = self.input_handler.caps_lock(); if self.renderer.caps_lock != self.input_handler.caps_lock() {
self.renderer.caps_lock = self.input_handler.caps_lock();
self.dirty = true;
}
// Set background if available and not already applied // Set background if available and not already applied
if !self.background_applied { if !self.background_applied {
@@ -123,14 +153,34 @@ impl LockedSurface {
log::info!("Applying background image to renderer"); log::info!("Applying background image to renderer");
self.renderer.set_background(background.clone()); self.renderer.set_background(background.clone());
self.background_applied = true; self.background_applied = true;
self.dirty = true;
} }
} }
// The clock displays %H:%M, so it only needs a redraw once per minute.
if self.config.clock {
let minute = chrono::Local::now().timestamp().div_euclid(60);
if self.last_minute != minute {
self.last_minute = minute;
self.dirty = true;
}
}
// Keep emitting frames while an animation is in flight so it can run to
// completion even though no new event arrives.
let animating = self.fade_alpha < 1.0 || self.renderer.is_animating();
if !self.dirty && !animating {
return false;
}
self.renderer self.renderer
.set_password_display(self.input_handler.password_length()); .set_password_display(self.input_handler.password_length());
self.renderer self.renderer
.set_cursor_position(self.input_handler.cursor_position()); .set_cursor_position(self.input_handler.cursor_position());
self.renderer.render(); self.renderer.render();
self.dirty = false;
true
} }
/// Commit the rendered frame to the Wayland surface /// Commit the rendered frame to the Wayland surface
@@ -163,6 +213,7 @@ impl LockedSurface {
} }
self.renderer.resize(width, height); self.renderer.resize(width, height);
self.background_applied = false; self.background_applied = false;
self.dirty = true;
} }
pub fn show_wrong_password(&mut self) { pub fn show_wrong_password(&mut self) {
@@ -178,6 +229,10 @@ impl LockedSurface {
.input_handler .input_handler
.handle_key_event(event.keysym, event.utf8, modifiers); .handle_key_event(event.keysym, event.utf8, modifiers);
// Any key event may change the password display, cursor or caps state,
// so request a redraw on the next update().
self.dirty = true;
match action { match action {
InputAction::PasswordChanged => { InputAction::PasswordChanged => {
self.input_handler.set_key_highlight(); self.input_handler.set_key_highlight();
@@ -207,10 +262,14 @@ impl LockedSurface {
pub fn set_background(&mut self, surface: ImageSurface) { pub fn set_background(&mut self, surface: ImageSurface) {
self.background = Some(surface); self.background = Some(surface);
self.background_applied = false; self.background_applied = false;
self.dirty = true;
} }
pub fn set_system_status(&mut self, status: SystemStatus) { pub fn set_system_status(&mut self, status: SystemStatus) {
self.renderer.system_status = status; if self.renderer.system_status != status {
self.renderer.system_status = status;
self.dirty = true;
}
} }
} }
@@ -237,12 +296,6 @@ impl LockManager {
} }
} }
pub fn update(&mut self) {
for surface in &mut self.surfaces {
surface.update();
}
}
pub fn surface_count(&self) -> usize { pub fn surface_count(&self) -> usize {
self.surfaces.len() self.surfaces.len()
} }
+5 -2
View File
@@ -830,9 +830,12 @@ fn main() -> Result<(), Box<dyn Error>> {
if let Ok(mut lm) = state.lock_manager.lock() { if let Ok(mut lm) = state.lock_manager.lock() {
lm.set_system_status(status); lm.set_system_status(status);
lm.update(); // Only commit surfaces that actually re-rendered this tick. An idle
// lock screen renders nothing and commits nothing.
for surface in &mut lm.surfaces { for surface in &mut lm.surfaces {
let _ = surface.commit(&mut state.pool); if surface.update() {
let _ = surface.commit(&mut state.pool);
}
} }
} }
if state.exit { if state.exit {
+8
View File
@@ -121,6 +121,14 @@ impl Renderer {
} }
} }
/// Whether any feedback animation is currently in flight and therefore
/// requires continued per-frame redraws until it finishes.
pub(crate) fn is_animating(&self) -> bool {
self.wrong_password_start.is_some()
|| self.key_highlight_start.is_some()
|| self.cleared_feedback_start.is_some()
}
pub(crate) fn update_feedback_timers(&mut self) { pub(crate) fn update_feedback_timers(&mut self) {
self.update_uptime(); self.update_uptime();
if let Some(start) = self.wrong_password_start { if let Some(start) = self.wrong_password_start {
+1 -1
View File
@@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use zbus::Connection; use zbus::Connection;
#[derive(Clone, Default)] #[derive(Clone, Default, PartialEq)]
pub struct SystemStatus { pub struct SystemStatus {
pub battery_percent: Option<f64>, pub battery_percent: Option<f64>,
pub is_charging: bool, pub is_charging: bool,