Description
Hi! This picks up a thread from #1041, where exposing the damage report was agreed to be worthwhile and split off into a separate change that never happened.
RedrawRequested fires both when the application called request_redraw and when the system asked for part of the window to be repainted, and the application can't tell which, or which area. Renderers that repaint only what changed in their own state (Slint's software renderer, or anything built on softbuffer's buffer age) answer a system request with a frame that covers only their own dirty region. If nothing in the UI changed, that is nothing at all, and the part of the window the system asked for keeps whatever it showed: black, or another window's leftovers.
To be clear about the direction: this is about what the system tells the application. It is unrelated to the damage an application sends to the compositor, which came up in #2412.
Here is what reaches the application today, measured with winit 0.30.13 and a small softbuffer app that presents only what it changed.
On Windows 11, the update region is still readable when RedrawRequested is delivered, because the WM_PAINT handler calls DefWindowProcW only afterwards. Restoring the window from minimized, maximizing it and resizing it each invalidated the whole client area. Moving it back from half off screen invalidated just the part that had been off screen (491x600 of 800x600). Redraws from request_redraw carry no update region, since winit uses RDW_INTERNALPAINT, and none appeared during steady animation. Occluded isn't reported on Windows, so there is no other hint either. Slint reads the region with GetUpdateRect inside the handler to work around this (slint-ui/slint#13568, where @ogoffart noted it as a winit deficiency).
On X11 (Xvfb with openbox), the server sends the exposed rectangles with every Expose, but expose() only turns the last event of a series into RedrawRequested and drops them. After a restore or a map, and whenever the visibility changes, winit sends Occluded(false) just before that redraw, which is enough for a toolkit to repaint everything, and Slint relies on it. An exposure without a visibility change gets no hint, though. With no compositor running and two windows over ours, moving one of them away made the server send Expose (440,372) 186x133 and no VisibilityNotify. The application received a bare RedrawRequested, indistinguishable from its own request_redraw, and the uncovered area kept the other window's image.
What I'd propose is a new WindowEvent variant, emitted just before the RedrawRequested it belongs to:
/// Parts of the surface whose presented contents the system discarded.
///
/// The application should present these areas again in the `RedrawRequested` that follows.
/// Rectangles are in physical pixels relative to the top-left of the surface, and may cover
/// more than what was actually lost.
SurfaceInvalidated { rects: Vec<(PhysicalPosition<u32>, PhysicalSize<u32>)> },
It is additive now that WindowEvent is #[non_exhaustive] (#4647), so existing RedrawRequested handlers keep working unchanged. Platforms that keep the window's contents (Wayland, Web, Orbital) would never emit it, and their docs would say so. No event means the system lost nothing, rather than "unknown, repaint everything", so it never forces full repaints where they aren't needed.
For the implementation:
- Windows:
GetUpdateRgn and GetRegionData in the WM_PAINT arm, before DefWindowProcW. On the buffered path and in the drag-source arm, the rectangles are kept in the window state and go out with the next RedrawRequested, so they aren't lost when DefWindowProcW validates the region, and the event still arrives right before its redraw.
- X11: record the rectangle of every
Expose per window and emit them just before that window's RedrawRequested.
- macOS:
drawRect: receives a rectangle that winit ignores today. Whether it's ever smaller than the view on a layer-backed view needs checking first, so I'd leave macOS for a follow-up.
I also looked at a field on RedrawRequested, as suggested in #1041 and #2883. It would be a single event, but it breaks every pattern that matches WindowEvent::RedrawRequested, and each backend would need per-window state to merge regions across coalesced redraws. A method on Window that's only valid inside the handler would be easy on Windows, but Window is Send + Sync and callable from anywhere, which makes it easy to misuse.
I've implemented this for Windows and X11 on a branch (diff against master) and tested it on both. A test app that presents only what it changed came back fully intact after restores, uncovering and maps, against 0.4 to 83 % intact without the event, and redraws from request_redraw never produced it. I'll open it as a PR if the shape works for you. Does a separate event work, and is there a name you'd prefer?
Related:
Relevant platforms
Windows, X11
Description
Hi! This picks up a thread from #1041, where exposing the damage report was agreed to be worthwhile and split off into a separate change that never happened.
RedrawRequestedfires both when the application calledrequest_redrawand when the system asked for part of the window to be repainted, and the application can't tell which, or which area. Renderers that repaint only what changed in their own state (Slint's software renderer, or anything built on softbuffer's buffer age) answer a system request with a frame that covers only their own dirty region. If nothing in the UI changed, that is nothing at all, and the part of the window the system asked for keeps whatever it showed: black, or another window's leftovers.To be clear about the direction: this is about what the system tells the application. It is unrelated to the damage an application sends to the compositor, which came up in #2412.
Here is what reaches the application today, measured with winit 0.30.13 and a small softbuffer app that presents only what it changed.
On Windows 11, the update region is still readable when
RedrawRequestedis delivered, because theWM_PAINThandler callsDefWindowProcWonly afterwards. Restoring the window from minimized, maximizing it and resizing it each invalidated the whole client area. Moving it back from half off screen invalidated just the part that had been off screen (491x600 of 800x600). Redraws fromrequest_redrawcarry no update region, since winit usesRDW_INTERNALPAINT, and none appeared during steady animation.Occludedisn't reported on Windows, so there is no other hint either. Slint reads the region withGetUpdateRectinside the handler to work around this (slint-ui/slint#13568, where @ogoffart noted it as a winit deficiency).On X11 (Xvfb with openbox), the server sends the exposed rectangles with every
Expose, butexpose()only turns the last event of a series intoRedrawRequestedand drops them. After a restore or a map, and whenever the visibility changes, winit sendsOccluded(false)just before that redraw, which is enough for a toolkit to repaint everything, and Slint relies on it. An exposure without a visibility change gets no hint, though. With no compositor running and two windows over ours, moving one of them away made the server sendExpose (440,372) 186x133and noVisibilityNotify. The application received a bareRedrawRequested, indistinguishable from its ownrequest_redraw, and the uncovered area kept the other window's image.What I'd propose is a new
WindowEventvariant, emitted just before theRedrawRequestedit belongs to:It is additive now that
WindowEventis#[non_exhaustive](#4647), so existingRedrawRequestedhandlers keep working unchanged. Platforms that keep the window's contents (Wayland, Web, Orbital) would never emit it, and their docs would say so. No event means the system lost nothing, rather than "unknown, repaint everything", so it never forces full repaints where they aren't needed.For the implementation:
GetUpdateRgnandGetRegionDatain theWM_PAINTarm, beforeDefWindowProcW. On the buffered path and in the drag-source arm, the rectangles are kept in the window state and go out with the nextRedrawRequested, so they aren't lost whenDefWindowProcWvalidates the region, and the event still arrives right before its redraw.Exposeper window and emit them just before that window'sRedrawRequested.drawRect:receives a rectangle that winit ignores today. Whether it's ever smaller than the view on a layer-backed view needs checking first, so I'd leave macOS for a follow-up.I also looked at a field on
RedrawRequested, as suggested in #1041 and #2883. It would be a single event, but it breaks every pattern that matchesWindowEvent::RedrawRequested, and each backend would need per-window state to merge regions across coalesced redraws. A method onWindowthat's only valid inside the handler would be easy on Windows, butWindowisSend + Syncand callable from anywhere, which makes it easy to misuse.I've implemented this for Windows and X11 on a branch (diff against master) and tested it on both. A test app that presents only what it changed came back fully intact after restores, uncovering and maps, against 0.4 to 83 % intact without the event, and redraws from
request_redrawnever produced it. I'll open it as a PR if the shape works for you. Does a separate event work, and is there a name you'd prefer?Related:
GetUpdateRectpresent_with_damagemarked the whole window painted after copying only the damageRelevant platforms
Windows, X11