Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
134 changes: 134 additions & 0 deletions examples/damage.rs
Original file line number Diff line number Diff line change
@@ -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<Rect> {
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<Rect> = 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);
}
118 changes: 116 additions & 2 deletions src/backends/win32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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<Rect> {
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<Rect> {
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.
Expand Down Expand Up @@ -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);
}
}
7 changes: 7 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading