Skip to content

Windows multi-window redraw starvation: unfocused windows do not present after state changes #3320

@schizza

Description

@schizza

Is your issue REALLY a bug?

  • My issue is indeed a bug!
  • I am not crazy! I will not fill out this form just to ask a question or request a feature. Pinky promise.

Is there an existing issue for this?

  • I have searched the existing issues.

Is this issue related to iced?

  • My hardware is compatible and my graphics drivers are up-to-date.

What happened?

In a multi-window iced application on Windows, unfocused/background windows can fail to visually update after their state changes.

The application state updates correctly. view() is rebuilt for all iced windows. request_redraw() is also called for all native windows. However, the actual RedrawRequested / present path only runs for the focused or most recently focused window.

The result is that background windows keep showing stale content until they receive focus or Windows eventually sends them another redraw event.

This was reproduced while developing Snapdash, a cross-platform desktop widget app built with Rust + iced. Snapdash creates small transparent undecorated widget windows and updates them from Home Assistant sensor state changes.

The same application works correctly on macOS and Linux. The issue appears Windows-specific.

Environment

  • OS: Windows
  • App: Snapdash
  • iced: fork based on 0.14.0
  • Window setup:
    • multiple windows
    • transparent
    • undecorated
    • DWM shadow / rounded corners on Windows
  • Backend path: iced_winit / winit

Observed Behavior

When sensor data arrives, Snapdash updates application state and iced rebuilds the views for all windows.

Example log:

2026-04-27T14:13:16.323246Z DEBUG snapdash::app: view() called window=Id(1) theme=MacDark
2026-04-27T14:13:16.323264Z DEBUG snapdash::app: view() called window=Id(3) theme=MacDark
2026-04-27T14:13:16.323274Z DEBUG snapdash::app: view() called window=Id(2) theme=MacDark

At this point, iced/winit also requests redraw for all three native windows:

2026-04-27T14:13:16.323284Z DEBUG iced_winit::window: request_raw_redraw window=WindowId(7344172)
2026-04-27T14:13:16.323416Z DEBUG iced_winit::window: request_raw_redraw window=WindowId(21430518)
2026-04-27T14:13:16.323510Z DEBUG iced_winit::window: request_raw_redraw window=WindowId(3149896)

However, only one iced window reaches the present path:

2026-04-27T14:13:16.323638Z DEBUG iced_winit: presenting window window=Id(1)

The same pattern repeats:

2026-04-27T14:13:16.354578Z DEBUG snapdash::app: WindowRedraw event arrived window=Id(1)
2026-04-27T14:13:16.354599Z DEBUG snapdash::app: view() called window=Id(1) theme=MacDark
2026-04-27T14:13:16.354617Z DEBUG snapdash::app: view() called window=Id(3) theme=MacDark
2026-04-27T14:13:16.354626Z DEBUG snapdash::app: view() called window=Id(2) theme=MacDark
2026-04-27T14:13:16.354636Z DEBUG iced_winit::window: request_raw_redraw window=WindowId(7344172)
2026-04-27T14:13:16.354777Z DEBUG iced_winit::window: request_raw_redraw window=WindowId(21430518)
2026-04-27T14:13:16.354870Z DEBUG iced_winit::window: request_raw_redraw window=WindowId(3149896)
2026-04-27T14:13:16.354995Z DEBUG iced_winit: presenting window window=Id(1)

So from the application’s point of view:

  • data arrived
  • state changed
  • all views were rebuilt
  • all native windows were asked to redraw
  • But only Id(1) presented.

Focus Changes Which Window Updates

When focus changes, the window that receives RedrawRequested / present changes too.
For example, after focusing another widget, the same pattern starts happening for Id(2):

2026-04-27T14:22:53.197897Z DEBUG snapdash::app: view() called window=Id(1) theme=MacDark
2026-04-27T14:22:53.197915Z DEBUG snapdash::app: view() called window=Id(3) theme=MacDark
2026-04-27T14:22:53.197924Z DEBUG snapdash::app: view() called window=Id(2) theme=MacDark

2026-04-27T14:22:53.197933Z DEBUG iced_winit::window: request_raw_redraw window=WindowId(414518186)
2026-04-27T14:22:53.198083Z DEBUG iced_winit::window: request_raw_redraw window=WindowId(5574518)
2026-04-27T14:22:53.198178Z DEBUG iced_winit::window: request_raw_redraw window=WindowId(23465014)

Additional diagnostic logging shows the raw WindowId to iced Id mapping:

2026-04-27T14:22:53.198297Z DEBUG iced_winit::window: redraw state before present requested=Some(Id(2)) window=Id(1) raw=WindowId(414518186) redraw_at=None
2026-04-27T14:22:53.198303Z DEBUG iced_winit::window: redraw state before present requested=Some(Id(2)) window=Id(2) raw=WindowId(5574518) redraw_at=None
2026-04-27T14:22:53.198306Z DEBUG iced_winit::window: redraw state before present requested=Some(Id(2)) window=Id(3) raw=WindowId(23465014) redraw_at=None

And only Id(2) presents:

2026-04-27T14:22:53.198319Z DEBUG iced_winit: presenting window window=Id(2)

This strongly suggests that the issue is not application state, view rebuilding, entity mapping, or request emission. The starvation happens between request_redraw() and present().

What is the expected behavior?

If multiple iced windows have updated state and request redraw, all affected windows should eventually be presented, even when they are not focused.

A background window should not need focus in order to visually update.

Actual Behavior

Only the focused or most recently focused window receives timely RedrawRequested handling and reaches present.

Other windows may have updated iced state and rebuilt views, but their swapchain/surface is not presented until focus changes or Windows later decides to deliver another redraw event.

Application-Level Workarounds Tried

Before investigating iced/winit, Snapdash tried several application-level workarounds:

  • explicitly requesting redraw for all windows
  • forcing redraw when sensor state changes
  • forcing redraw only for the mapped widget window
  • moving/nudging windows to force invalidation
  • trying stronger Win32 invalidation paths

These did not reliably fix the issue, because the actual visual update depends on iced_winit entering the render/present path. The app can call request_redraw, but if Windows/winit only delivers RedrawRequested for one window, only that window gets presented.

Current Fork Workaround

In my iced fork, I added a Windows-only workaround inside the RedrawRequested handling path.

When one window receives RedrawRequested, iced first renders/presents that requested window normally. Then, on Windows only, it also draws and presents the other open windows once during the current event batch.

After this change, inactive windows update correctly.

Relevant log after the workaround:

2026-04-27T14:28:59.609267Z DEBUG iced_winit: presenting window window=Id(5)
2026-04-27T14:28:59.636150Z DEBUG iced_winit: presenting extra window window=Id(1)
2026-04-27T14:28:59.666673Z DEBUG iced_winit: presenting extra window window=Id(2)
2026-04-27T14:28:59.697869Z DEBUG iced_winit: presenting extra window window=Id(3)

Another event batch:

2026-04-27T14:28:59.729241Z DEBUG iced_winit: presenting window window=Id(3)
2026-04-27T14:28:59.760201Z DEBUG iced_winit: presenting extra window window=Id(1)
2026-04-27T14:28:59.791488Z DEBUG iced_winit: presenting extra window window=Id(2)
2026-04-27T14:28:59.822975Z DEBUG iced_winit: presenting extra window window=Id(5)

With this workaround, the application behaves correctly on Windows: background widget windows visually update as soon as sensor data changes.

To avoid a cascade where every later RedrawRequested presents all windows again, the workaround is throttled so the extra-present path runs at most once between NewEvents batches.

Why I think this seems like an iced_winit-Level Issue

The application layer does not seem to have a clean, reliable fix.

The app can:

  • update state
  • rebuild views
  • call request_redraw
  • request redraw for one window or all windows

But the visual update only happens when iced_winit reaches its render/present path.

Log shows:

view() called window=Id(1)
view() called window=Id(3)
view() called window=Id(2)

request_raw_redraw window=WindowId(...)
request_raw_redraw window=WindowId(...)
request_raw_redraw window=WindowId(...)

presenting window window=Id(2)

So iced has the updated UI, and redraw requests are emitted, but only one window is presented.

That makes this look like redraw starvation in iced_winit’s Windows event/present handling rather than a Snapdash-specific bug.

Suggested Upstream Direction

The current fork workaround is intentionally broad because it was used to prove the diagnosis. It works, but it may not be the ideal final upstream shape.

A better upstream fix could track a per-window pending redraw / pending present flag inside iced_winit.

Suggested behavior:

  1. When iced requests RedrawRequest::NextFrame for a window, mark that window as needing present.
  2. On Windows, when any WindowEvent::RedrawRequested arrives:
  • render/present the window that received the event normally
  • additionally render/present other windows whose pending-present flag is set
  • clear the flag after successful present
  1. Avoid presenting completely idle windows.

Conceptually:

// Windows-only concept:
//
// When any RedrawRequested arrives:
//   present(requested_window)
//
//   for each other window:
//       if window.needs_present:
//           present(window)
//           window.needs_present = false

This would avoid unnecessary presents for idle windows while still preventing background windows from being starved behind focus.

Reproduction idea:

A minimal repro could be an iced example that:

  1. Opens 3 transparent undecorated windows.
  2. Uses a timer/subscription to update visible text in all windows every second.
  3. Logs:
    • view(window_id)
    • request_redraw(raw_window_id)
    • present(window_id)
  4. Keeps focus on one window.

Expected failure on Windows:

  • all windows rebuild views
  • redraw is requested for all windows
  • only the focused / last-focused window presents repeatedly
  • background windows visually stay stale until focused

Additional notes

After moving the workaround into iced_winit, Snapdash no longer needs application-level force redraw hacks. The app can simply update state and rely on iced to eventually present the affected windows.

This supports the idea that the fix belongs in iced_winit’s Windows redraw/present coordination.

Version

crates.io release

Operating System

Windows

Do you have any log output?

see above

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions