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 {