From 699f31f8bc041f18f496c04e394fb84eeb953e4f Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Wed, 28 Oct 2026 11:25:48 -0700 Subject: [PATCH] perf(animation): do not update a ghost that has not changed Asking DWM to put a thumbnail where it already is, as opaque as it already is, still has it compose the frame again, and an eased animation spends its first and last frames where it is. Remember where a ghost was last put and how opaque it was made, and leave it alone when that is asked of it again. --- komorebi/src/animation/ghost.rs | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/komorebi/src/animation/ghost.rs b/komorebi/src/animation/ghost.rs index e0fd1a5a..b55b27a6 100644 --- a/komorebi/src/animation/ghost.rs +++ b/komorebi/src/animation/ghost.rs @@ -17,6 +17,7 @@ use color_eyre::eyre; use crossbeam_channel::Sender; use crossbeam_channel::bounded; use crossbeam_channel::unbounded; +use parking_lot::Mutex; use std::collections::HashMap; use std::ffi::c_void; use std::sync::OnceLock; @@ -490,6 +491,8 @@ pub struct GhostWindow { hthumb: isize, /// Where the host's client area starts in screen coordinates origin: (i32, i32), + /// Where the ghost was last put, and how opaque it was made + shown: Mutex>, disposed: bool, } @@ -521,6 +524,8 @@ impl GhostWindow { Ok(Self { hthumb, origin, + // it is registered where the window is, and opaque + shown: Mutex::new(Some((start_rect, 255))), disposed: false, }) } @@ -528,7 +533,14 @@ impl GhostWindow { /// Move and scale the ghost to `rect`, and make it `opacity` opaque. /// Updating a registered thumbnail has no thread affinity, so this is /// applied on the calling thread and is done by the time it returns. + /// + /// A ghost that is already there is left alone: asking DWM for what is + /// already so still has it compose the frame again. pub fn update(&self, rect: Rect, opacity: u8) -> eyre::Result<()> { + if !needs_update(&mut self.shown.lock(), rect, opacity) { + return Ok(()); + } + WindowsApi::dwm_update_thumbnail_properties( self.hthumb, &thumbnail_properties(destination(rect, self.origin), opacity), @@ -571,6 +583,17 @@ impl Drop for GhostWindow { } } +/// Whether putting a ghost at `rect` with `opacity` changes anything, given +/// where it was last put, which is remembered if it does +fn needs_update(shown: &mut Option<(Rect, u8)>, rect: Rect, opacity: u8) -> bool { + if *shown == Some((rect, opacity)) { + return false; + } + + *shown = Some((rect, opacity)); + true +} + /// How many frames a ghost is faded out over once its animation is over const FADE_OUT_FRAMES: u32 = 6; @@ -679,6 +702,33 @@ mod tests { assert!(!is_swap_chain_class("")); } + #[test] + fn a_ghost_is_only_updated_when_it_changes() { + let rect = |left| Rect { + left, + top: 0, + right: 100, + bottom: 50, + }; + let mut shown = Some((rect(0), 255)); + + // where it is already + assert!(!needs_update(&mut shown, rect(0), 255)); + + // moved, or faded, or both + assert!(needs_update(&mut shown, rect(10), 255)); + assert!(!needs_update(&mut shown, rect(10), 255)); + assert!(needs_update(&mut shown, rect(10), 200)); + assert!(needs_update(&mut shown, rect(20), 100)); + + // and back, which is a change from where it was last + assert!(needs_update(&mut shown, rect(10), 200)); + + // one that was never put anywhere is put somewhere + let mut never = None; + assert!(needs_update(&mut never, rect(0), 255)); + } + #[test] fn the_owner_thread_polls_slowly_when_there_are_no_ghosts() { assert_eq!(poll_interval(false), BUSY_POLL_INTERVAL);