Skip to content

win32: present what the system invalidated along with the damage - #375

Open
bigtree108-dmytro wants to merge 1 commit into
rust-windowing:masterfrom
bigtree108-dmytro:win32-repaint-invalidated-region
Open

bigtree108-dmytro wants to merge 1 commit into
rust-windowing:masterfrom
bigtree108-dmytro:win32-repaint-invalidated-region

Conversation

@bigtree108-dmytro

Copy link
Copy Markdown

Tested on:

  • Win32 (tier 1)

Hi, and thanks for softbuffer!

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. On Windows, present_with_damage copies the damage rectangles and then calls ValidateRect(hwnd, NULL), which tells Windows that the whole window has been repainted. So an application that presents only what changed ends up with a black window apart from its damage, and Windows never asks again.

The buffer still holds the last frame in those areas, which is what an age of 1 promises, so this change reads the window's update rectangle with GetUpdateRect and copies it along with the damage before validating. When Windows has nothing pending, GetUpdateRect returns false and presenting works exactly as before. A redraw requested through winit's request_redraw doesn't create an update region either (winit uses RDW_INTERNALPAINT), so the extra copy only happens when the system actually asked for a repaint.

This only works while the request is still pending. winit delivers RedrawRequested before it calls DefWindowProcW, so presenting from that handler, or from anywhere before it, picks the area up. An application that skips presenting because nothing changed loses it once the paint message has been handled, so I added a short Win32 note to the present_with_damage docs: present with empty damage while handling the request.

The new damage example animates a small square and presents only the two rectangles it touches each frame. I minimized and restored its window four times from a script and counted how much of the window on screen matched the expected image afterwards: 2.2 to 2.6 % on master, 100 % with this change.

To check the other cases, I ran a separate test app through the same script with different ways of presenting, on master and on this branch. Restore from minimized, three times each:

How the app presents master this PR
Every frame, from RedrawRequested 2.8 to 3.8 % 100 %
Every frame, from about_to_wait (outside WM_PAINT) 2.4 to 2.9 % 100 %
Every frame, into a 400x300 buffer smaller than the window 14.5 % of the buffer area 100 %
Every 500 ms, with empty damage on the other redraw requests 0.5 % 100 %
Every 500 ms, skipping the other redraw requests 0.4 to 0.5 % 0.5 %

The last row is the limit described above. For that case the application needs to know which area was lost, and winit doesn't report that today; I've proposed it in rust-windowing/winit#4719.

In the same runs, maximize and restore, growing and shrinking the window, covering it with another window, and moving it half off screen and back all stayed at 100 % with this change, with no panics. On master they did too, except in the small-buffer run, which never recovered from the first restore because its buffer never changes size and so never gets a full present. The window had no pending update region at any point during three seconds of steady animation, so nothing extra was copied, and none while it was covered. A GetUpdateRect call took about 3 µs in a release build, while a present took several milliseconds.

The rectangle is clipped to the buffer in a small function with unit tests for the edge cases: inside, exactly the buffer, one pixel in each corner, larger than the buffer, crossing each edge, fully outside on every side, empty, inverted, and extreme coordinates.

Tested on Windows 11. Locally, cargo fmt, cargo clippy --all-targets -- -Dwarnings for x86_64-pc-windows-msvc and i686-pc-windows-msvc, cargo test on both, cargo doc, and the MSRV check with 1.71.1 and minimal versions for x86_64-pc-windows-msvc and x86_64-pc-windows-gnu all pass. Not tested: moving the window to a screen with another scale factor, since only one screen was connected, and Windows 10.

One more note: we ran into this through Slint (slint-ui/slint#13537), which is on softbuffer 0.4. The change applies to 0.4.8 as it is, so if a 0.4.x patch release is something you'd consider, I'm happy to open a backport PR against a release branch.

Related:

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant