perf(animation): do not build a picture to make a window paint itself

Making a window paint itself (a render of it is synchronous) went through
Snapshot::capture, which also allocated and filled a second picture of the
visible frame, copied it out of the first and cut its corners, to throw it all
away: about a quarter of what taking a picture costs, on the thread that
gates showing the window again.

Split rendering a window out of taking a picture of it and make paint_now only
render. And when taking a picture, do not run over millions of pixels to make
them opaque when a window has rendered them that way, as it does: look at a
grid of them across the picture, and only fall back to making every pixel
opaque if one of those is not.
This commit is contained in:
LGUG2Z committed 2026-10-30 15:45:37 -07:00
1 parent 126acaa074
commit cd1c9ce9ce
1 file changed
+161 -49
+161 -49
View File
@@ -132,15 +132,32 @@ fn copy_area(
}
}
/// Make every pixel opaque, except for the ones outside the rounded corners of
/// a window, which are made fully transparent. (Premultiplied, so those are
/// all zero.)
fn round_corners(pixels: &mut [u32], width: i32, height: i32, radius: i32) {
let radius = radius.min(width / 2).min(height / 2).max(0);
/// Make every pixel opaque. (Premultiplied, so the colour is as it was.)
fn make_opaque(pixels: &mut [u32]) {
for pixel in pixels.iter_mut() {
*pixel |= 0xFF00_0000;
}
}
/// Whether the pixels of a picture are all opaque, as far as a grid of them
/// across the picture says, which is a lot cheaper than looking at every one
/// of the millions there can be. The picture a window renders of itself
/// normally is, and then there is nothing to do.
fn looks_opaque(pixels: &[u32], width: i32, height: i32) -> bool {
let step_x = (width / 32).max(1) as usize;
let step_y = (height / 32).max(1) as usize;
(0..height as usize).step_by(step_y).all(|y| {
(0..width as usize)
.step_by(step_x)
.all(|x| pixels[y * width as usize + x] >> 24 == 0xFF)
})
}
/// Make the pixels outside the rounded corners of a window, which has opaque
/// pixels, fully transparent. (Premultiplied, so those are all zero.)
fn round_corners(pixels: &mut [u32], width: i32, height: i32, radius: i32) {
let radius = radius.min(width / 2).min(height / 2).max(0);
for corner_y in 0..radius {
for corner_x in 0..radius {
@@ -163,24 +180,28 @@ fn round_corners(pixels: &mut [u32], width: i32, height: i32, radius: i32) {
}
}
/// Ask a window to paint itself, as it is now. Taking a picture of a window
/// makes it render synchronously (see `Snapshot::capture`), which is also a
/// way to have a window that has just been resized lay itself out at its new
/// size, without waiting for it to get round to it.
pub fn paint_now(hwnd: isize) {
let start = std::time::Instant::now();
let _ = Snapshot::capture(hwnd);
tracing::trace!(
"snapshot: hwnd {hwnd} painted itself in {:.1} ms",
start.elapsed().as_secs_f64() * 1000.0
);
/// What a window rendered of itself, and where in that its visible frame is
struct Rendered {
bitmap: HBITMAP,
pixels: &'static [u32],
window_width: i32,
x: i32,
y: i32,
width: i32,
height: i32,
}
impl Snapshot {
/// Take a picture of a window as it is now. This asks the window to
/// render itself, so it can take a while for one that is busy, and it is
/// not for the thread that handles komorebi's events.
pub fn capture(hwnd: isize) -> eyre::Result<Self> {
impl Drop for Rendered {
fn drop(&mut self) {
unsafe {
let _ = DeleteObject(self.bitmap.into());
}
}
}
/// Have a window render itself, which it does synchronously, whatever it takes
/// it to do so, into a bitmap of the size of the window.
fn render(hwnd: isize) -> eyre::Result<Rendered> {
let handle = HWND(hwnd as *mut c_void);
let mut window = RECT::default();
@@ -206,43 +227,101 @@ impl Snapshot {
let dc = CreateCompatibleDC(Some(screen));
let result = (|| {
let (full, full_pixels) = create_dib(dc, window_width, window_height)?;
let (bitmap, pixels) = create_dib(dc, window_width, window_height)?;
let allocated = start.elapsed();
let previous = SelectObject(dc, full.into());
let previous = SelectObject(dc, bitmap.into());
let printed = PrintWindow(handle, dc, PRINT_WINDOW_FLAGS(PW_RENDERFULLCONTENT));
SelectObject(dc, previous);
let rendered = start.elapsed();
let result = if printed.as_bool() {
create_dib(dc, width, height).map(|(bitmap, pixels)| {
copy_area(full_pixels, window_width, x, y, pixels, width, height);
round_corners(pixels, width, height, CORNER_RADIUS);
Self {
bitmap,
width,
height,
}
})
} else {
Err(eyre::eyre!("hwnd {hwnd} would not render itself"))
};
let _ = DeleteObject(full.into());
tracing::trace!(
"snapshot: hwnd {hwnd} {window_width}x{window_height}: allocate {:.1} ms, \
PrintWindow {:.1} ms, picture {:.1} ms",
PrintWindow {:.1} ms",
allocated.as_secs_f64() * 1000.0,
(rendered - allocated).as_secs_f64() * 1000.0,
(start.elapsed() - rendered).as_secs_f64() * 1000.0,
(start.elapsed() - allocated).as_secs_f64() * 1000.0,
);
result
let rendered = Rendered {
bitmap,
pixels,
window_width,
x,
y,
width,
height,
};
if printed.as_bool() {
Ok(rendered)
} else {
Err(eyre::eyre!("hwnd {hwnd} would not render itself"))
}
})();
let _ = DeleteDC(dc);
ReleaseDC(None, screen);
result
}
}
/// Ask a window to paint itself, as it is now. Having a window render itself
/// is synchronous, which makes it a way to have a window that has just been
/// resized lay itself out at its new size, without waiting for it to get round
/// to it. What it renders is of no use here, so nothing is made of it.
pub fn paint_now(hwnd: isize) {
let start = std::time::Instant::now();
let _ = render(hwnd);
tracing::trace!(
"snapshot: hwnd {hwnd} painted itself in {:.1} ms",
start.elapsed().as_secs_f64() * 1000.0
);
}
impl Snapshot {
/// Take a picture of a window as it is now. This asks the window to
/// render itself, so it can take a while for one that is busy, and it is
/// not for the thread that handles komorebi's events.
pub fn capture(hwnd: isize) -> eyre::Result<Self> {
let rendered = render(hwnd)?;
let start = std::time::Instant::now();
unsafe {
let screen = GetDC(None);
let dc = CreateCompatibleDC(Some(screen));
let result = create_dib(dc, rendered.width, rendered.height).map(|(bitmap, pixels)| {
copy_area(
rendered.pixels,
rendered.window_width,
rendered.x,
rendered.y,
pixels,
rendered.width,
rendered.height,
);
if !looks_opaque(pixels, rendered.width, rendered.height) {
make_opaque(pixels);
}
round_corners(pixels, rendered.width, rendered.height, CORNER_RADIUS);
Self {
bitmap,
width: rendered.width,
height: rendered.height,
}
});
let _ = DeleteDC(dc);
ReleaseDC(None, screen);
tracing::trace!(
"snapshot: hwnd {hwnd}: picture {:.1} ms",
start.elapsed().as_secs_f64() * 1000.0
);
result
}
}
@@ -387,9 +466,9 @@ mod tests {
use super::*;
#[test]
fn corners_are_cut_out_and_the_rest_is_opaque() {
fn corners_are_cut_out_and_the_rest_is_kept() {
let (width, height) = (40, 30);
let mut pixels = vec![0x0010_2030_u32; (width * height) as usize];
let mut pixels = vec![0xFF10_2030_u32; (width * height) as usize];
round_corners(&mut pixels, width, height, 10);
@@ -401,7 +480,7 @@ mod tests {
assert_eq!(at(0, height - 1), 0);
assert_eq!(at(width - 1, height - 1), 0);
// the middle and the straight edges are kept, and opaque
// the middle and the straight edges are kept
assert_eq!(at(20, 15), 0xFF10_2030);
assert_eq!(at(20, 0), 0xFF10_2030);
assert_eq!(at(0, 15), 0xFF10_2030);
@@ -414,7 +493,7 @@ mod tests {
#[test]
fn corners_of_a_tiny_picture_do_not_run_out_of_bounds() {
let mut pixels = vec![0x00FF_FFFF_u32; 9];
let mut pixels = vec![0xFFFF_FFFF_u32; 9];
round_corners(&mut pixels, 3, 3, 10);
assert!(
pixels
@@ -423,6 +502,39 @@ mod tests {
);
}
#[test]
fn pixels_are_made_opaque_without_changing_their_colour() {
let mut pixels = vec![0x0010_2030_u32, 0x0000_0000, 0x7F11_2233];
make_opaque(&mut pixels);
assert_eq!(pixels, vec![0xFF10_2030, 0xFF00_0000, 0xFF11_2233]);
}
#[test]
fn a_picture_looks_opaque_when_the_pixels_across_it_are() {
let (width, height) = (100, 80);
let mut pixels = vec![0xFF00_FF00_u32; (width * height) as usize];
assert!(looks_opaque(&pixels, width, height));
// a pixel on the grid that is not
pixels[0] = 0x0000_FF00;
assert!(!looks_opaque(&pixels, width, height));
pixels[0] = 0xFF00_FF00;
// a whole row of them, on the grid
for x in 0..width {
pixels[(4 * width + x) as usize] = 0;
}
assert!(!looks_opaque(&pixels, width, height));
// pictures too small for a grid are looked at pixel by pixel
assert!(looks_opaque(&[0xFF00_0000_u32; 6], 3, 2));
assert!(!looks_opaque(
&[0xFF00_0000, 0xFF00_0000, 0x0, 0xFF00_0000],
2,
2
));
}
#[test]
fn an_area_is_copied_out_of_a_picture() {
// 4x3, each pixel numbered by where it is