From b078ddde5b0e4495f44e598b39b9248e97579ce6 Mon Sep 17 00:00:00 2001 From: bigtree108-dmytro Date: Sun, 27 Sep 2026 07:15:05 +0300 Subject: [PATCH] win32: present what the system invalidated along with the damage When a window is restored from minimized, Windows invalidates its whole client area. The window keeps showing its old contents until it is painted again, and from then on only what was painted since the restore is visible, the rest is black. present_with_damage copied only the damage rectangles and then validated the whole window, so Windows was told the area had been repainted and never asked again. The buffer still holds the last frame there, so read the window's update rectangle and copy it along with the damage before validating. That only works while the request is pending: an application that skips presenting because nothing changed loses the request once the paint message is handled, so the docs now say to present with empty damage in that case. The new damage example presents only a moving square each frame. Restored from minimized four times, its window came back 2.2 to 2.6 % intact before this change and 100 % after it. --- CHANGELOG.md | 1 + examples/damage.rs | 134 ++++++++++++++++++++++++++++++++++++++++++ src/backends/win32.rs | 118 ++++++++++++++++++++++++++++++++++++- src/lib.rs | 7 +++ 4 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 examples/damage.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e6cc5807..ef1bb81a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - **Breaking:** Removed `DamageOutOfRange` error case. If the damage value is greater than the backend supports, it is instead clamped to an appropriate value. - **Breaking:** Removed `SurfaceExtWeb` and the associated `NoDisplayHandle` and `NoWindowHandle` helpers. Use `RawWindowHandle::WebCanvas` or `RawWindowHandle::WebOffscreenCanvas` instead. - Fixed `present_with_damage` with bounds out of range on Windows, Web and X11. +- On Windows, fix `present_with_damage` leaving the parts of the window that the system invalidated unpainted, for example after restoring the window from minimized. # 0.4.8 diff --git a/examples/damage.rs b/examples/damage.rs new file mode 100644 index 00000000..9023ec41 --- /dev/null +++ b/examples/damage.rs @@ -0,0 +1,134 @@ +//! Animates a small square and presents only the area that changed each frame. +//! +//! The rest of the window must stay intact even when the system discards what the window shows, +//! for example when it is minimized and restored, or moved to a screen with another scale factor. + +use softbuffer::{Buffer, Context, Pixel, Rect, Surface}; +use std::num::NonZeroU32; +use winit::event::{KeyEvent, WindowEvent}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::keyboard::{Key, NamedKey}; + +mod util; + +const SQUARE_SIZE: u32 = 40; +const SQUARE_STEP: u32 = 4; +const SQUARE_COLOR: Pixel = Pixel::new_rgb(0xff, 0xff, 0xff); + +fn background(x: u32, y: u32) -> Pixel { + Pixel::new_rgb(0x40 + (x % 0xc0) as u8, 0x60, 0x40 + (y % 0xc0) as u8) +} + +fn square_at(frame: u32, width: NonZeroU32, height: NonZeroU32) -> Option { + let size = SQUARE_SIZE.min(width.get()).min(height.get()); + let travel = width.get() - size + 1; + Some(Rect { + x: frame.wrapping_mul(SQUARE_STEP) % travel, + y: (height.get() - size) / 2, + width: NonZeroU32::new(size)?, + height: NonZeroU32::new(size)?, + }) +} + +fn fill(buffer: &mut Buffer<'_>, rect: Rect, pixel: impl Fn(u32, u32) -> Pixel) { + let columns = rect.x..rect.x + rect.width.get(); + for (y, row) in buffer + .pixel_rows() + .enumerate() + .skip(rect.y as usize) + .take(rect.height.get() as usize) + { + for x in columns.clone() { + row[x as usize] = pixel(x, y as u32); + } + } +} + +fn main() { + util::setup(); + + let event_loop = EventLoop::new().unwrap(); + let context = Context::new(event_loop.owned_display_handle()).unwrap(); + + let app = util::WinitAppBuilder::with_init( + |elwt| { + let window = util::make_window(elwt, |w| w.with_title("Partial presentation")); + let frame = 0u32; + let previous_square: Option = None; + (window, frame, previous_square) + }, + move |_elwt, (window, _, _)| Surface::new(&context, window.clone()).unwrap(), + ) + .with_event_handler(|state, surface, window_id, event, elwt| { + let (window, frame, previous_square) = state; + + elwt.set_control_flow(ControlFlow::Wait); + + if window_id != window.id() { + return; + } + + match event { + WindowEvent::Resized(size) => { + let Some(surface) = surface else { + tracing::error!("Resized fired before Resumed or after Suspended"); + return; + }; + + if let (Some(width), Some(height)) = + (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) + { + surface.resize(width, height).unwrap(); + } + } + WindowEvent::RedrawRequested => { + let Some(surface) = surface else { + tracing::error!("RedrawRequested fired before Resumed or after Suspended"); + return; + }; + + let mut buffer = surface.next_buffer().unwrap(); + let (width, height) = (buffer.width(), buffer.height()); + let square = square_at(*frame, width, height); + + match (buffer.age(), *previous_square, square) { + (1, Some(previous), Some(current)) + if previous.x + previous.width.get() <= width.get() + && previous.y + previous.height.get() <= height.get() => + { + fill(&mut buffer, previous, background); + fill(&mut buffer, current, |_, _| SQUARE_COLOR); + buffer.present_with_damage(&[previous, current]).unwrap(); + } + _ => { + for (x, y, pixel) in buffer.pixels_iter() { + *pixel = background(x, y); + } + if let Some(current) = square { + fill(&mut buffer, current, |_, _| SQUARE_COLOR); + } + buffer.present().unwrap(); + } + } + + *previous_square = square; + *frame = frame.wrapping_add(1); + window.request_redraw(); + } + WindowEvent::CloseRequested + | WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: Key::Named(NamedKey::Escape), + .. + }, + .. + } => { + elwt.exit(); + } + _ => {} + } + }); + + util::run_app(event_loop, app); +} diff --git a/src/backends/win32.rs b/src/backends/win32.rs index 566fbff9..54c9f9cc 100644 --- a/src/backends/win32.rs +++ b/src/backends/win32.rs @@ -15,7 +15,7 @@ use std::slice; use std::sync::{mpsc, Mutex, OnceLock}; use std::thread; -use windows_sys::Win32::Foundation::HWND; +use windows_sys::Win32::Foundation::{HWND, RECT}; use windows_sys::Win32::Graphics::Gdi; use windows_sys::Win32::UI::ColorSystem::LCS_WINDOWS_COLOR_SPACE; @@ -320,8 +320,9 @@ impl BufferInterface for BufferImpl<'_> { } fn present_with_damage(self, damage: &[Rect]) -> Result<(), SoftBufferError> { + let invalidated = self.invalidated_rect(); unsafe { - for rect in damage { + for rect in damage.iter().chain(&invalidated) { let rect = util::clamp_rect( *rect, self.buffer.width.try_into().unwrap(), @@ -355,6 +356,40 @@ impl BufferInterface for BufferImpl<'_> { } } +impl BufferImpl<'_> { + /// The part of the window that the system asked to repaint, within the buffer. + /// + /// Windows discards what a window shows when it is restored from minimized or moved to a + /// screen with another scale factor. The buffer still holds the last frame there, so it is + /// presented along with the damage instead of being validated away unpainted. + fn invalidated_rect(&self) -> Option { + let mut rect = RECT { + left: 0, + top: 0, + right: 0, + bottom: 0, + }; + if unsafe { Gdi::GetUpdateRect(self.window.0, &mut rect, false.into()) } == 0 { + return None; + } + clip_to_buffer(rect, self.buffer.width, self.buffer.height) + } +} + +/// The part of `rect` that lies within a buffer of the given size, if any. +fn clip_to_buffer(rect: RECT, width: NonZeroI32, height: NonZeroI32) -> Option { + let left = rect.left.max(0); + let top = rect.top.max(0); + let right = rect.right.min(width.get()); + let bottom = rect.bottom.min(height.get()); + Some(Rect { + x: u32::try_from(left).ok()?, + y: u32::try_from(top).ok()?, + width: NonZeroU32::new(u32::try_from(right.saturating_sub(left)).ok()?)?, + height: NonZeroU32::new(u32::try_from(bottom.saturating_sub(top)).ok()?)?, + }) +} + /// Allocator for device contexts. /// /// Device contexts can only be allocated or freed on the thread that originated them. @@ -513,3 +548,82 @@ fn byte_stride(width: u32, bit_count: u32) -> u32 { // When `bit_count == 32`, this is always just equal to `width * 4`. ((width * bit_count + 31) & !31) >> 3 } + +#[cfg(test)] +mod tests { + use super::*; + + const WIDTH: i32 = 800; + const HEIGHT: i32 = 600; + + fn clip(left: i32, top: i32, right: i32, bottom: i32) -> Option<(u32, u32, u32, u32)> { + let rect = RECT { + left, + top, + right, + bottom, + }; + let width = NonZeroI32::new(WIDTH).unwrap(); + let height = NonZeroI32::new(HEIGHT).unwrap(); + clip_to_buffer(rect, width, height) + .map(|rect| (rect.x, rect.y, rect.width.get(), rect.height.get())) + } + + #[test] + fn rect_inside_the_buffer_is_kept() { + assert_eq!(clip(10, 20, 110, 70), Some((10, 20, 100, 50))); + } + + #[test] + fn rect_covering_exactly_the_buffer_is_kept() { + assert_eq!(clip(0, 0, WIDTH, HEIGHT), Some((0, 0, 800, 600))); + } + + #[test] + fn one_pixel_rects_in_the_corners_are_kept() { + assert_eq!(clip(0, 0, 1, 1), Some((0, 0, 1, 1))); + assert_eq!( + clip(WIDTH - 1, HEIGHT - 1, WIDTH, HEIGHT), + Some((799, 599, 1, 1)) + ); + } + + #[test] + fn rect_larger_than_the_buffer_is_clipped_to_it() { + assert_eq!(clip(0, 0, 1000, 900), Some((0, 0, 800, 600))); + assert_eq!(clip(-50, -50, 1000, 900), Some((0, 0, 800, 600))); + } + + #[test] + fn rect_crossing_an_edge_is_clipped_to_the_buffer() { + assert_eq!(clip(700, 500, 900, 700), Some((700, 500, 100, 100))); + assert_eq!(clip(-10, -20, 30, 40), Some((0, 0, 30, 40))); + } + + #[test] + fn rect_outside_the_buffer_is_dropped() { + assert_eq!(clip(WIDTH, 0, WIDTH + 10, 10), None); + assert_eq!(clip(0, HEIGHT, 10, HEIGHT + 10), None); + assert_eq!(clip(-20, 0, 0, 10), None); + assert_eq!(clip(0, -20, 10, 0), None); + assert_eq!(clip(900, 700, 1000, 800), None); + } + + #[test] + fn empty_or_inverted_rect_is_dropped() { + assert_eq!(clip(10, 10, 10, 50), None); + assert_eq!(clip(10, 10, 50, 10), None); + assert_eq!(clip(50, 50, 10, 10), None); + } + + #[test] + fn extreme_coordinates_do_not_overflow() { + assert_eq!( + clip(i32::MIN, i32::MIN, i32::MAX, i32::MAX), + Some((0, 0, 800, 600)) + ); + assert_eq!(clip(i32::MAX, i32::MAX, i32::MIN, i32::MIN), None); + assert_eq!(clip(i32::MIN, i32::MIN, i32::MIN, i32::MIN), None); + assert_eq!(clip(i32::MAX, i32::MAX, i32::MAX, i32::MAX), None); + } +} diff --git a/src/lib.rs b/src/lib.rs index 5fd3f778..a8ed6f35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -376,6 +376,13 @@ impl Buffer<'_> { /// - Web /// /// Otherwise this is equivalent to [`Self::present`]. + /// + /// ## Win32 + /// + /// Parts of the window that the system asked to repaint, for example after the window was + /// restored from minimized, are presented from the buffer along with `damage` while that + /// request is still pending. To have them repainted when nothing else changed, present with + /// empty `damage` while handling the request (winit's `RedrawRequested`). pub fn present_with_damage(mut self, damage: &[Rect]) -> Result<(), SoftBufferError> { // Verify that pixels are set as opaque if the alpha mode requires it. if cfg!(debug_assertions) && self.alpha_mode == AlphaMode::Opaque {