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