[PATCH 0/2] MR10712: wineandroid: fix desktop sizing and repaint on resolution changes
This series fixes two related issues in wineandroid affecting desktop initialization and repaint behavior when using the virtual desktop mode. First, launching explorer.exe with an empty desktop size ("/desktop=shell,,android") does not behave as intended on wineandroid. When the size is omitted, explorer attempts to determine the desktop resolution via get_default_desktop_size(). If no matching registry entry is found, it falls back to a hardcoded default (typically 800x600). Later, even if the wineandroid backend sets the correct display size through pCreateDesktop, explorer overrides it again during initialize_display_settings(), restoring the default resolution. As a result, the desktop ends up stuck at 800x600 regardless of the actual display size reported by the backend. To address this, the desktop is now launched with a size of "-1x-1". This value is accepted by parse_size(), but does not correspond to a valid resolution. As a result, explorer does not fall back to the default resolution, and also avoids overriding the backend-provided size during initialization. This allows the desktop to retain the correct resolution as configured by wineandroid. Second, window repainting on resolution changes is fixed. Previously, WM_ANDROID_REFRESH used NtUserExposeWindowSurface(), which only exposes existing surface contents without invalidating the window. This prevents WM_PAINT and WM_ERASEBKGND from being generated. When the desktop or other windows are resized (e.g. after applying a new display mode), they may retain stale contents instead of repainting. This is resolved by replacing NtUserExposeWindowSurface() with NtUserRedrawWindow(), ensuring proper invalidation and repaint of window contents. This fixes cases where the desktop background is not redrawn after a resolution change. Together, these changes ensure that the desktop starts with the correct resolution and that windows are properly repainted when display settings change. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712
From: Twaik Yont <9674930+twaik@users.noreply.github.com> Replace NtUserExposeWindowSurface() with NtUserRedrawWindow() when handling WM_ANDROID_REFRESH for non-OpenGL windows. NtUserExposeWindowSurface() only exposes the existing surface contents, but does not invalidate the window or trigger WM_PAINT / WM_ERASEBKGND. As a result, when the display resolution changes, windows (including the desktop window managed by explorer.exe) may be resized without receiving a repaint, leaving stale contents on screen. Using NtUserRedrawWindow() ensures that the window is properly invalidated and repainted, so background and client areas are refreshed correctly after resolution changes. This fixes cases where the desktop background is not redrawn after applying a new display mode. Signed-off-by: Twaik Yont <9674930+twaik@users.noreply.github.com> --- dlls/wineandroid.drv/window.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dlls/wineandroid.drv/window.c b/dlls/wineandroid.drv/window.c index 97eb3004ba8..5ce4828e1a4 100644 --- a/dlls/wineandroid.drv/window.c +++ b/dlls/wineandroid.drv/window.c @@ -1197,9 +1197,7 @@ LRESULT ANDROID_WindowMessage( HWND hwnd, UINT msg, WPARAM wp, LPARAM lp ) detach_client_surfaces( hwnd ); } else - { - NtUserExposeWindowSurface( hwnd, 0, NULL, 0 ); - } + NtUserRedrawWindow( hwnd, NULL, 0, RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN | RDW_FRAME ); return 0; default: FIXME( "got window msg %x hwnd %p wp %lx lp %lx\n", msg, hwnd, (long)wp, lp ); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10712
From: Twaik Yont <9674930+twaik@users.noreply.github.com> Use “-1x-1” instead of an empty size in the /desktop option when launching explorer.exe. An empty size causes get_default_desktop_size() to be used, which falls back to a fixed default (typically 800x600). This prevents the desktop from matching the actual display size provided by the Android backend. Passing “-1x-1” is accepted by parse_size() and results in large unsigned values, which are then clamped by the driver to the current screen size. This effectively requests a dynamically sized desktop matching the Android display. This ensures that the desktop resolution is derived from the backend instead of being forced to the default fixed size. Signed-off-by: Twaik Yont <9674930+twaik@users.noreply.github.com> --- dlls/wineandroid.drv/WineActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlls/wineandroid.drv/WineActivity.java b/dlls/wineandroid.drv/WineActivity.java index 6488188b93a..f0bade27a28 100644 --- a/dlls/wineandroid.drv/WineActivity.java +++ b/dlls/wineandroid.drv/WineActivity.java @@ -169,7 +169,7 @@ private final void runWine( String loader, String cmdline ) { String[] cmd = { loader, "c:\\windows\\system32\\explorer.exe", - "/desktop=shell,,android", + "/desktop=shell,-1x-1,android", cmdline }; String err = wine_init( cmd ); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10712
@julliard Hi, just a gentle ping on this PR — would appreciate a review when you have a moment. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_137712
@julliard Hi, just a gentle ping on this PR — would appreciate a review when you have a moment. I know, the fixes are kinda ugly, but they do their job and seems like they do it fine. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_138721
It doesn't seem right to have to force a repaint. If the window size changed that should have triggered a repaint in win32u already. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_138749
Unfortunately without that desktop window does not obtain repaint message, it is simply not being sent. Or at least I did not catch WM_PAINT or WM_ERASEBKGND on desktop/background window when I intercepted and printed everything it received. {width=708 height=600} -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_138755
Should we send `NtUserExposeWindowSurface` to regular windows and `NtUserRedrawWindow` to desktop window instead of sending `NtUserRedrawWindow` everywhere? -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_138758
Seems like that also works. ``` diff --git a/dlls/wineandroid.drv/device.c b/dlls/wineandroid.drv/device.c index 34424009afd..ad04392a5d3 100644 --- a/dlls/wineandroid.drv/device.c +++ b/dlls/wineandroid.drv/device.c @@ -77,7 +77,7 @@ WINE_DEFAULT_DEBUG_CHANNEL(android); #endif static HWND capture_window; -static HWND desktop_window; +HWND desktop_window; static pthread_mutex_t dispatch_ioctl_lock = PTHREAD_MUTEX_INITIALIZER; diff --git a/dlls/wineandroid.drv/window.c b/dlls/wineandroid.drv/window.c index b3f3aeea955..eb005140193 100644 --- a/dlls/wineandroid.drv/window.c +++ b/dlls/wineandroid.drv/window.c @@ -61,6 +61,7 @@ struct android_win_data #define SWP_AGG_NOPOSCHANGE (SWP_NOSIZE | SWP_NOMOVE | SWP_NOCLIENTSIZE | SWP_NOCLIENTMOVE | SWP_NOZORDER) pthread_mutex_t win_data_mutex; +extern HWND desktop_window; static struct android_win_data *win_data_context[32768]; @@ -457,7 +458,10 @@ static int process_events( DWORD mask ) event->data.surface.client ? "client" : "whole", event->data.surface.width, event->data.surface.height ); - NtUserPostMessage( event->data.surface.hwnd, WM_ANDROID_REFRESH, event->data.surface.client, 0 ); + if ( event->data.surface.hwnd == desktop_window ) + NtUserPostMessage( event->data.surface.hwnd, WM_PAINT, 0, 0 ); + else + NtUserPostMessage( event->data.surface.hwnd, WM_ANDROID_REFRESH, event->data.surface.client, 0 ); break; case MOTION_EVENT: ``` But it is kinda weird. In the case if I send WM_ANDROID_REFRESH or WM_ANDROID_REFRESH+WM_PAINT the desktop window is still white/stale, but in the case if I send only WM_PAINT everything seems to be fine. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_138765
@julliard Hi, just a gentle ping on this PR — would appreciate a review when you have a moment. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_139581
Like I said, you shouldn't need to force a repaint. You'll need to figure out what's going wrong here. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_139584
Thank you. I'll take a deeper look for the reason desktop window is not being repainted after resolution change. What about the second commit? -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_139585
Also, I see the comment in `dce.c` ``` /* desktop window never gets WM_PAINT, only WM_ERASEBKGND */ ``` https://gitlab.winehq.org/wine/wine/-/blob/fc21ae38e6c048a4016244c724f51fcd1... -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_139586
On Tue May 12 14:49:36 2026 +0000, Twaik Yont wrote:
Thank you. I'll take a deeper look for the reason desktop window is not being repainted after resolution change. What about the second commit? That doesn't seem right either.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_139587
On Tue May 12 14:49:36 2026 +0000, Alexandre Julliard wrote:
That doesn't seem right either. So the only acceptable fix will be getting desktop window size before I start wine in order to pass the size in the commandline? Because whatever I will do in the code will be reset after pCreateWindow finishes, because explorer explicitly resets the value I pass to 800x600.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_139588
On Tue May 12 14:52:24 2026 +0000, Twaik Yont wrote:
So the only acceptable fix will be getting desktop window size before I start wine in order to pass the size in the commandline? Because whatever I will do in the code will be reset after pCreateWindow finishes, because explorer explicitly resets the value I pass to 800x600. It's not clear to me what's going on exactly, but if there's a bug in explorer, that should be fixed instead of worked around.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_139604
On Tue May 12 20:28:04 2026 +0000, Alexandre Julliard wrote:
It's not clear to me what's going on exactly, but if there's a bug in explorer, that should be fixed instead of worked around. I explained it more detailed in the PR description. It is not a bug, Android always was some kind of special case. Probably I will try to obtain current main window size from Android and pass it via command line if you prefer this workaround.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_139605
On Tue May 12 21:09:44 2026 +0000, Twaik Yont wrote:
I explained it more detailed in the PR description. It is not a bug, Android always was some kind of special case. Probably I will try to obtain current main window size from Android and pass it via command line if you prefer this workaround. Sorry for the wall of text.
Short version: winex11 has direct drawing and after disabling it I've seen the same white areas instead of valid content. In the case of direct drawing `NtUserExposeWindowSurface` falls back to `NtUserRedrawWindow`. Long version: The difference seems to be that winex11 and wineandroid do not use the same expose path for the desktop window. In winex11, the virtual desktop window is treated as direct drawing. `X11DRV_CreateWindowSurface()` returns a NULL window_surface when the X11 window is the root/desktop window: dlls/winex11.drv/bitblt.c: ``` enable_direct_drawing() if (data->whole_window == root_window) return TRUE; X11DRV_CreateWindowSurface() *surface = NULL; /* indicate that we want to draw directly to the window */ ``` Because the desktop has no real Wine window_surface, NtUserExposeWindowSurface() takes the fallback path in win32u: dlls/win32u/window.c: ``` if (!surface || surface == &dummy_surface) NtUserRedrawWindow( hwnd, rect ? &exposed_rect : NULL, NULL, flags ); ``` So an X11 Expose event with RDW_INVALIDATE eventually creates a normal update region and causes the desktop window to receive WM_ERASEBKGND / WM_PAINT. In wineandroid, the desktop window does have a real window_surface: dlls/wineandroid.drv/window.c: ``` ANDROID_CreateWindowSurface(): *surface = create_surface(data->hwnd, surface_rect); ``` Therefore `NtUserExposeWindowSurface()` does not call `NtUserRedrawWindow()`. It only marks the existing surface bounds dirty: dlls/win32u/window.c: ``` window_surface_lock( surface ); if (!rect) add_bounds_rect( &surface->bounds, &surface->rect ); else { OffsetRect( &exposed_rect, rects.client.left - rects.visible.left, rects.client.top - rects.visible.top ); intersect_rect( &exposed_rect, &exposed_rect, &surface->rect ); add_bounds_rect( &surface->bounds, &exposed_rect ); } window_surface_unlock( surface ); ``` That is not the same as invalidating the window. It does not create an update region and it does not force WM_ERASEBKGND / WM_PAINT. This matters for desktop size changes because the default desktop WM_DISPLAYCHANGE handling resizes the desktop with SWP_DEFERERASE: dlls/win32u/defwnd.c: ``` NtUserSetWindowPos(..., flags | SWP_NOZORDER | SWP_NOACTIVATE | SWP_DEFERERASE); ``` So after the Android desktop surface grows, the newly exposed area is not erased by the normal SetWindowPos path. Since wineandroid has a real backing surface, NtUserExposeWindowSurface() only marks that backing surface dirty. But if explorer never receives WM_ERASEBKGND / WM_PAINT for the newly exposed area, the backing storage is never filled with the desktop background. The Android flush path can only present pixels that already exist in the Wine backing bitmap: dlls/win32u/dce.c: ``` window_surface_flush(): ... color_bits = window_surface_get_color( surface, color_info ))) ... ... ... surface->funcs->flush( surface, &surface->rect, &dirty, color_info, color_bits, shape_changed, shape_info, shape_bits ) ... ``` And after that `android_surface_flush()` copies `color_bits` into `ANativeWindow_Buffer` in `dlls/wineandroid.drv/window.c`. So `android_surface_flush()` cannot fix this by itself. If the backing bitmap was not repainted, flushing it just copies missing/stale/uninitialized contents to the Android surface. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_139665
On Wed May 13 09:17:44 2026 +0000, Twaik Yont wrote:
Sorry for the wall of text. Short version: winex11 has direct drawing and after disabling it I've seen the same white areas instead of valid content. In the case of direct drawing `NtUserExposeWindowSurface` falls back to `NtUserRedrawWindow`. Long version: The difference seems to be that winex11 and wineandroid do not use the same expose path for the desktop window. In winex11, the virtual desktop window is treated as direct drawing. `X11DRV_CreateWindowSurface()` returns a NULL window_surface when the X11 window is the root/desktop window: dlls/winex11.drv/bitblt.c: ``` enable_direct_drawing() if (data->whole_window == root_window) return TRUE; X11DRV_CreateWindowSurface() *surface = NULL; /* indicate that we want to draw directly to the window */ ``` Because the desktop has no real Wine window_surface, NtUserExposeWindowSurface() takes the fallback path in win32u: dlls/win32u/window.c: ``` if (!surface || surface == &dummy_surface) NtUserRedrawWindow( hwnd, rect ? &exposed_rect : NULL, NULL, flags ); ``` So an X11 Expose event with RDW_INVALIDATE eventually creates a normal update region and causes the desktop window to receive WM_ERASEBKGND / WM_PAINT. In wineandroid, the desktop window does have a real window_surface: dlls/wineandroid.drv/window.c: ``` ANDROID_CreateWindowSurface(): *surface = create_surface(data->hwnd, surface_rect); ``` Therefore `NtUserExposeWindowSurface()` does not call `NtUserRedrawWindow()`. It only marks the existing surface bounds dirty: dlls/win32u/window.c: ``` window_surface_lock( surface ); if (!rect) add_bounds_rect( &surface->bounds, &surface->rect ); else { OffsetRect( &exposed_rect, rects.client.left - rects.visible.left, rects.client.top - rects.visible.top ); intersect_rect( &exposed_rect, &exposed_rect, &surface->rect ); add_bounds_rect( &surface->bounds, &exposed_rect ); } window_surface_unlock( surface ); ``` That is not the same as invalidating the window. It does not create an update region and it does not force WM_ERASEBKGND / WM_PAINT. This matters for desktop size changes because the default desktop WM_DISPLAYCHANGE handling resizes the desktop with SWP_DEFERERASE: dlls/win32u/defwnd.c: ``` NtUserSetWindowPos(..., flags | SWP_NOZORDER | SWP_NOACTIVATE | SWP_DEFERERASE); ``` So after the Android desktop surface grows, the newly exposed area is not erased by the normal SetWindowPos path. Since wineandroid has a real backing surface, NtUserExposeWindowSurface() only marks that backing surface dirty. But if explorer never receives WM_ERASEBKGND / WM_PAINT for the newly exposed area, the backing storage is never filled with the desktop background. The Android flush path can only present pixels that already exist in the Wine backing bitmap: dlls/win32u/dce.c: ``` window_surface_flush(): ... color_bits = window_surface_get_color( surface, color_info ))) ... ... ... surface->funcs->flush( surface, &surface->rect, &dirty, color_info, color_bits, shape_changed, shape_info, shape_bits ) ... ``` And after that `android_surface_flush()` copies `color_bits` into `ANativeWindow_Buffer` in `dlls/wineandroid.drv/window.c`. So `android_surface_flush()` cannot fix this by itself. If the backing bitmap was not repainted, flushing it just copies missing/stale/uninitialized contents to the Android surface. @julliard Sorry for the repeated pings.
When you have time, could you please take a look at the previous long comment? I tried to trace the repaint/expose flow more carefully and wrote down the reasoning in more detail there. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_140626
On Tue May 19 17:34:03 2026 +0000, Twaik Yont wrote:
@julliard Sorry for the repeated pings. When you have time, could you please take a look at the previous long comment? I tried to trace the repaint/expose flow more carefully and wrote down the reasoning in more detail there. I still think that win32u should be exposing the desktop correctly without requiring workarounds in the drivers.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_140639
On Tue May 19 21:02:09 2026 +0000, Alexandre Julliard wrote:
I still think that win32u should be exposing the desktop correctly without requiring workarounds in the drivers. It should, but it does not due to `SWP_DEFERERASE`, unfortunately.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_140640
On Tue May 19 21:07:51 2026 +0000, Twaik Yont wrote:
It should, but it does not due to `SWP_DEFERERASE`, unfortunately. SWP_DEFERERASE should not prevent repainting.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_140641
On Tue May 19 21:16:10 2026 +0000, Alexandre Julliard wrote:
SWP_DEFERERASE should not prevent repainting. SWP_DEFERERASE prevents invoking erase_now in set_window_pos. And desktop window never gets WM_PAINT.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_140642
On Tue May 19 21:19:24 2026 +0000, Twaik Yont wrote:
SWP_DEFERERASE prevents invoking erase_now in set_window_pos. And desktop window never gets WM_PAINT. It should still get an update region, which will eventually trigger a repaint. If not, that's a bug that needs to be fixed in the appropriate place.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_140643
On Tue May 19 21:22:12 2026 +0000, Alexandre Julliard wrote:
It should still get an update region, which will eventually trigger a repaint. If not, that's a bug that needs to be fixed in the appropriate place. I see. In that case, where would you suggest I start looking?
From what I can currently trace, the desktop surface grows correctly, but I never see the newly exposed region becoming part of a repaint/update cycle afterward. So I am probably missing the place where the update region is supposed to be generated or propagated. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_140644
On Tue May 19 21:24:47 2026 +0000, Twaik Yont wrote:
I see. In that case, where would you suggest I start looking? From what I can currently trace, the desktop surface grows correctly, but I never see the newly exposed region becoming part of a repaint/update cycle afterward. So I am probably missing the place where the update region is supposed to be generated or propagated. It's probably somewhere between win32u and the server. If you are looking at the x11 side of things, note that in root mode we obviously don't repaint the desktop, but in desktop mode we should.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_140645
On Tue May 19 21:35:14 2026 +0000, Alexandre Julliard wrote:
It's probably somewhere between win32u and the server. If you are looking at the x11 side of things, note that in root mode we obviously don't repaint the desktop, but in desktop mode we should. Yeah, I know. I checked with desktop=shell. Winex11 desktop mode repaints desktop due to direct drawing (which causes NtUserExposeWindowSurface to fall back to NtUserRedrawWindow), without it I see same white rectangles.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_140646
On Tue May 19 21:39:12 2026 +0000, Twaik Yont wrote:
Yeah, I know. I checked with desktop=shell. Winex11 desktop mode repaints desktop due to direct drawing (which causes NtUserExposeWindowSurface to fall back to NtUserRedrawWindow), without it I see same white rectangles. I think I understand the issue better now.
`add_bounds_rect()` in `NtUserExposeWindowSurface()` only updates the surface dirty/present path, but it does not create a normal window update region. So it looks like the newly exposed desktop area never becomes invalidated for the normal repaint path. It also seems like the most appropriate fix would probably be to trigger normal window content invalidation there through `NtUserRedrawWindow()`. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_140648
On Tue May 19 21:50:47 2026 +0000, Twaik Yont wrote:
I think I understand the issue better now. `add_bounds_rect()` in `NtUserExposeWindowSurface()` only updates the surface dirty/present path, but it does not create a normal window update region. So it looks like the newly exposed desktop area never becomes invalidated for the normal repaint path. It also seems like the most appropriate fix would probably be to trigger normal window content invalidation there through `NtUserRedrawWindow()`. NtUserExposeWindowSurface is for getting the bits from the surface to the screen. The update region is to get the app to repaint the bits into the surface. They are two separate things. Forcing a repaint every time we want to copy the bits would of course work, but it's not what we want.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_140651
On Tue May 19 22:04:01 2026 +0000, Alexandre Julliard wrote:
NtUserExposeWindowSurface is for getting the bits from the surface to the screen. The update region is to get the app to repaint the bits into the surface. They are two separate things. Forcing a repaint every time we want to copy the bits would of course work, but it's not what we want. No, I mean we probably should call window content invalidation in set_window_pos in the case if the size was changed, Because IIRC surface/dib backing storage is being erased or recreated.
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10712#note_140668
participants (3)
-
Alexandre Julliard (@julliard) -
Twaik Yont -
Twaik Yont (@twaik)