feat(animation): keep the picture of a window that leaves

Keep the picture that is taken of a window as its ghost is made, for the ghost
of the window to show when it is back, so that a window that is hidden is
not asked to render itself to get one. Asking a hidden Chromium or Electron
window to has been seen to leave it blank, or drawn off its place, until its
process is restarted.

At most 12 pictures, 256 MiB and ten minutes of them are kept, and a picture
is only given back for a window that is within a few pixels of the size it
was. Nothing uses them yet.
This commit is contained in:
LGUG2Z committed 2026-10-30 15:45:37 -07:00
1 parent 730f5d82ec
commit 38e4f04900
1 file changed
+205
+205
View File
@@ -8,8 +8,13 @@
//! window of our own, which DWM can scale on the GPU like any other.
use color_eyre::eyre;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::ffi::c_void;
use std::sync::OnceLock;
use std::time::Duration;
use std::time::Instant;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Foundation::HWND;
use windows::Win32::Foundation::POINT;
@@ -67,6 +72,8 @@ const CORNER_RADIUS: i32 = 10;
/// borders it has around it.
pub struct Snapshot {
bitmap: HBITMAP,
/// Where the pixels of the bitmap are, which is as long as the bitmap is
pixels: usize,
pub width: i32,
pub height: i32,
}
@@ -309,6 +316,7 @@ impl Snapshot {
Self {
bitmap,
pixels: pixels.as_ptr() as usize,
width: rendered.width,
height: rendered.height,
}
@@ -339,11 +347,132 @@ impl Snapshot {
Self {
bitmap,
pixels: pixels.as_ptr() as usize,
width,
height,
}
}
}
/// Another picture that is the same as this one
pub fn duplicate(&self) -> eyre::Result<Self> {
unsafe {
let screen = GetDC(None);
let dc = CreateCompatibleDC(Some(screen));
let result = create_dib(dc, self.width, self.height).map(|(bitmap, pixels)| {
let source = std::slice::from_raw_parts(
self.pixels as *const u32,
(self.width * self.height) as usize,
);
pixels.copy_from_slice(source);
Self {
bitmap,
pixels: pixels.as_ptr() as usize,
width: self.width,
height: self.height,
}
});
let _ = DeleteDC(dc);
ReleaseDC(None, screen);
result
}
}
fn bytes(&self) -> usize {
self.width as usize * self.height as usize * 4
}
}
/// How many pictures are kept of windows that left, at most
const REMEMBERED_MAX: usize = 12;
/// How much memory the pictures that are kept can take up, at most
const REMEMBERED_MAX_BYTES: usize = 256 * 1024 * 1024;
/// How long a picture is kept: what a window looked like that long ago is not
/// what it looks like now, and the window is not shown for long as it was.
const REMEMBERED_MAX_AGE: Duration = Duration::from_secs(10 * 60);
/// How much bigger or smaller than a window a picture of it can be, in pixels,
/// and be shown in its place: it is not scaled to anything much different
const REMEMBERED_SIZE_TOLERANCE: i32 = 4;
/// The pictures of windows as they left, for the ghosts of them to show when
/// they are back. A window that is hidden is not to be asked to render itself:
/// a picture of it can only be taken by having it paint something it is not
/// shown for, and Chromium and Electron windows that have been asked to have
/// been seen to be left blank or drawn off their place until their process is
/// restarted.
#[derive(Default)]
struct Remembered {
pictures: HashMap<isize, (Instant, Snapshot)>,
}
impl Remembered {
fn remember(&mut self, hwnd: isize, picture: Snapshot, now: Instant) {
self.pictures.insert(hwnd, (now, picture));
self.prune(now);
}
/// The picture of the window, which is not kept any more, if there is one
/// of about this size that is not too old
fn recall(&mut self, hwnd: isize, size: (i32, i32), now: Instant) -> Option<Snapshot> {
let (since, picture) = self.pictures.remove(&hwnd)?;
let fits = (picture.width - size.0).abs() <= REMEMBERED_SIZE_TOLERANCE
&& (picture.height - size.1).abs() <= REMEMBERED_SIZE_TOLERANCE;
(fits && now.saturating_duration_since(since) < REMEMBERED_MAX_AGE).then_some(picture)
}
/// Get rid of what is too old, and then of the oldest while there is too
/// much of it
fn prune(&mut self, now: Instant) {
self.pictures
.retain(|_, (since, _)| now.saturating_duration_since(*since) < REMEMBERED_MAX_AGE);
while self.pictures.len() > REMEMBERED_MAX
|| self
.pictures
.values()
.map(|(_, picture)| picture.bytes())
.sum::<usize>()
> REMEMBERED_MAX_BYTES
{
let Some(oldest) = self
.pictures
.iter()
.min_by_key(|(_, (since, _))| *since)
.map(|(hwnd, _)| *hwnd)
else {
break;
};
self.pictures.remove(&oldest);
}
}
}
lazy_static! {
static ref REMEMBERED: Mutex<Remembered> = Mutex::new(Remembered::default());
}
/// Keep a picture of a window that is about to leave
pub fn remember(hwnd: isize, picture: &Snapshot) {
match picture.duplicate() {
Ok(copy) => REMEMBERED.lock().remember(hwnd, copy, Instant::now()),
Err(error) => tracing::trace!("snapshot: could not keep a picture of hwnd {hwnd}: {error}"),
}
}
/// The picture that was kept of a window that is coming back, if it is one that
/// can be shown for it. `size` is what it is to be shown as.
pub fn recall(hwnd: isize, size: (i32, i32)) -> Option<Snapshot> {
REMEMBERED.lock().recall(hwnd, size, Instant::now())
}
extern "system" fn snapshot_wnd_proc(
@@ -627,4 +756,80 @@ mod tests {
// stretched, so it reaches the corner of the host as well
assert_eq!(far_corner & 0xFF, 0xFF, "red, got {far_corner:#08x}");
}
fn picture(width: i32, height: i32) -> Snapshot {
Snapshot::solid(width, height, 0x0010_2030)
}
#[test]
fn a_picture_that_was_kept_is_given_back_once() {
let mut remembered = Remembered::default();
let now = Instant::now();
remembered.remember(1, picture(100, 50), now);
let back = remembered.recall(1, (100, 50), now).unwrap();
assert_eq!((back.width, back.height), (100, 50));
assert!(remembered.recall(1, (100, 50), now).is_none());
}
#[test]
fn a_picture_is_not_shown_for_a_window_of_another_size_or_much_later() {
let mut remembered = Remembered::default();
let now = Instant::now();
// a few pixels either way is the same window
remembered.remember(1, picture(100, 50), now);
assert!(remembered.recall(1, (104, 46), now).is_some());
// but a window that has been given a different shape is not the picture
remembered.remember(2, picture(100, 50), now);
assert!(remembered.recall(2, (200, 50), now).is_none());
assert!(
remembered.recall(2, (100, 50), now).is_none(),
"and it is gone"
);
// and what a window looked like long ago is not what it looks like
remembered.remember(3, picture(100, 50), now);
assert!(
remembered
.recall(3, (100, 50), now + REMEMBERED_MAX_AGE)
.is_none()
);
}
#[test]
fn the_oldest_pictures_are_the_first_to_go_when_there_are_too_many() {
let mut remembered = Remembered::default();
let start = Instant::now();
for hwnd in 0..(REMEMBERED_MAX as isize + 3) {
remembered.remember(
hwnd,
picture(10, 10),
start + Duration::from_millis(hwnd as u64),
);
}
assert_eq!(remembered.pictures.len(), REMEMBERED_MAX);
// the first three were the oldest
for hwnd in 0..3 {
assert!(!remembered.pictures.contains_key(&hwnd));
}
assert!(remembered.pictures.contains_key(&3));
}
#[test]
fn a_duplicate_is_the_same_picture() {
let original = picture(20, 10);
let copy = original.duplicate().unwrap();
assert_eq!((copy.width, copy.height), (20, 10));
let pixels = |picture: &Snapshot| unsafe {
std::slice::from_raw_parts(picture.pixels as *const u32, 200).to_vec()
};
assert_eq!(pixels(&original), pixels(&copy));
assert_ne!(original.pixels, copy.pixels);
}
}