Skip to content

Report the area the system invalidated along with redraws #4719

Description

@bigtree108-dmytro

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

Activity

  1. bigtree108-dmytro commented on Sep 27, 2026

    @bigtree108-dmytro
    Author

    The implementation I mentioned is now on a branch in my fork, so the proposal can be judged against real code: master...bigtree108-dmytro:winit:surface-invalidated

    It's rebased on current master: one commit touching winit-core (the event and its docs), winit-win32, winit-x11 and the changelog, with unit tests for the Windows region handling. I haven't opened a pull request, since CONTRIBUTING asks for new API to be discussed in an issue first. I'll open one from this branch once we agree on the shape, and I'm happy to rename or restructure it however you prefer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions