diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f2aedcf..024c84f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,66 @@ ## Unreleased +- Tiling window managers get their exact geometry again. When a tiler + handed Live a one-pixel size correction, Live could read it as a move + and keep its old size, leaving a gap or an overlap in the tile. Moving + a window to a display with different scaling no longer carries the old + scaling correction with it. If window sizing behaves worse than + before, launch with `env WINE_CONFIG_ROUNDING=legacy ableton-live` and + report it. + +- Newly exposed parts of a plugin popup no longer show leftover pixels + while the popup has nothing ready to draw, and a Live that is + minimised or on another workspace stops redrawing parked browser + views nobody can see. If popups flash when they open, launch with + `env WINE_DCOMP_PAINT=legacy ableton-live` and report it. If a + restored window comes back blank, use + `env WINE_DCOMP_VIEWABILITY=off ableton-live` and report that. + +- One plugin's popup menus no longer attach to another plugin's window, + and opening a second plugin no longer costs its editor the redraw + drive the first one has. A plugin view in one program also no longer + turns unrelated programs' small tool windows into menus. Launch with + `env WINE_DCOMP_SCOPE=global ableton-live` to restore the old + behaviour and report it if popups misbehave. + +- Live's main display no longer paints over a browser view sharing its + window, which made the view flicker while both redrew. A plugin + editor dragged across the browser panel also no longer loses its + content where the two overlap. + +- Plugin views no longer interfere with each other's windows. Closing + one of two views inside the same window kept breaking the survivor's + drawing, and two plugin processes could clash over the same internal + window key and blank one of them. + +- Closing a plugin or browser view now cleans up its window machinery + completely, wherever in Live the close came from. Leftover redraw + timers and a stuck popup mode could survive before, leaving stale + rectangles and menu-like behaviour on unrelated windows. If plugin + windows behave worse after closing views, launch with + `env WINE_DCOMP_TEARDOWN=legacy ableton-live` and report it. + +- Restoring or un-minimising a window no longer races the desktop with + stale minimise and resize requests, which some desktops answered with + a wrong size or a re-minimised window. If restore behaves worse than + before, launch with `env WINE_HOST_STATE_LOCK=off ableton-live` and + report it. + +- On KDE, switching windows no longer leaves a window unmovable with the + wrong taskbar focus. Live now asks for activation once instead of + repeating the request on every input. If focus stops following your + clicks, launch with `env WINE_ACTIVATE_RESEND=1 ableton-live` and + report it. + +- Window problems are now easier to report. Launching with + `env ABLETON_WM_TRACE=1 ableton-live` logs every window change that + Live and the desktop exchange, and `tools/wm-capture.sh` records how + the desktop sees Live's windows while a problem is on screen. The + troubleshooting guide explains both captures in a new section on + windows that will not close, and names the xwayland-satellite 0.8.2 + bug that hides some dialogs, with its downgrade workaround. + - Link setup records its version marker only when the service step completed, so a host where that step failed retries it on the next update instead of counting itself configured. The version moves to 5. diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 16e238a4..b280647b 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -244,6 +244,36 @@ If fullscreen is still wrong after the update, launch once with [open an issue](https://github.com/shibco/ableton-linux/issues) and include your desktop environment and whether that launch behaved differently. +## A window will not close or never appears + +If your desktop runs xwayland-satellite (niri does), check its version: + +```bash +xwayland-satellite --version +``` + +Version 0.8.2 hides some of Live's dialogs, the Trial window included. +Downgrade to 0.8.1 and try again. + +On other desktops, record what yours is doing and open an issue. Run +this while the stuck window is on screen: + +```bash +curl -fsSLO https://raw.githubusercontent.com/shibco/ableton-linux/main/tools/wm-capture.sh +bash wm-capture.sh +``` + +Then start Live once with the window log switched on, repeat the +problem, and quit Live: + +```bash +env ABLETON_WM_TRACE=1 ableton-live +``` + +Attach the folder that wm-capture.sh printed and the file +`~/.log/ableton-wine/live.log` to the issue, and name your desktop +environment. + ## GNOME handles a Live shortcut instead of Live GNOME uses Ctrl+Alt+Up and Ctrl+Alt+Down for workspace switching. These keys @@ -274,3 +304,7 @@ Use the [GitHub issue form](https://github.com/shibco/ableton-linux/issues/new/c Include the Live edition, this project's release number, Linux distribution, desktop environment, and the exact action that failed. Do not attach Ableton installers, authorization files, licence keys, projects, or plugin credentials. + +For window problems (wrong size or position, stuck fullscreen, black or +frozen areas, a window that will not close) also attach the two captures +described in "A window will not close or never appears" above. diff --git a/patches/0090-winex11-win32u-dxgi-add-a-compact-wmtrace-state-tran.patch b/patches/0090-winex11-win32u-dxgi-add-a-compact-wmtrace-state-tran.patch new file mode 100644 index 00000000..51efcdac --- /dev/null +++ b/patches/0090-winex11-win32u-dxgi-add-a-compact-wmtrace-state-tran.patch @@ -0,0 +1,269 @@ +Subject: winex11/win32u/dxgi: add a compact wmtrace state-transition channel + +Diagnosing window-management faults (wrong fullscreen exits, stale +DComp rectangles, undismissable dialogs, resize disobedience) needs +one capture that shows the whole lifecycle: what Win32 asked for, +what was sent to the window manager, what the host acknowledged, and +what flowed back into Win32 - plus whether a backing surface existed +and which present path a swapchain chose at the time. Today that +story is spread across +x11drv, +win and +dxgi, which are far too +chatty to leave enabled and drown the transitions in event noise. + +Add a dedicated wmtrace channel with one line per transition: + +- REQ-WMSTATE / REQ-CONFIG / REQ-NETWMSTATE / REQ-TYPE / REQ-MOTIF / + REQ-ACTIVATE[-SKIP]: every request Wine sends to the host WM, with + HWND/XID, owner, styles, geometry, DPI and the X request serial. +- ALIAS: a sub-scale config request aliased to the settled host rect + (patch 0042) instead of being forwarded. +- ACK / ACK-FORCED / ACK-IGNORED: every host acknowledgement through + handle_state_change, with the pending serial it matched or missed. +- WIN32-APPLY: the state/geometry X11DRV_GetWindowStateUpdates hands + back to Win32. +- SURFACE-CREATE: window-surface creation (surface validity). +- FS-NORMALIZE / FS-EXIT: the 0065 fullscreen geometry policy firing. +- REBLIT / REBLIT-SKIP: parked-DComp reblit decisions (0041/0056), + with the skip cause (hidden ancestry / stale buffer). +- PRESENT-PATH: the GL-vs-GDI present decision per swapchain (0055). + +Capture with WINEDEBUG=-all,+wmtrace (add +winediag to keep the 0071 +fallback telemetry). No behaviour change: every hunk is a TRACE_() +call or channel declaration, all off unless the channel is enabled. + +diff --git a/dlls/dxgi/factory.c b/dlls/dxgi/factory.c +index d88ddf8..726c13d 100644 +--- a/dlls/dxgi/factory.c ++++ b/dlls/dxgi/factory.c +@@ -20,6 +20,7 @@ + #include "dxgi_private.h" + + WINE_DEFAULT_DEBUG_CHANNEL(dxgi); ++WINE_DECLARE_DEBUG_CHANNEL(wmtrace); + + static inline struct dxgi_factory *impl_from_IWineDXGIFactory(IWineDXGIFactory *iface) + { +@@ -516,6 +517,7 @@ static void dcomp_reblit_comp_buffer(HWND hwnd, const char *reason) + ++hidden_count; + if (hidden_count <= 3 || !(hidden_count % 500)) + FIXME("Re-blit skipped (hidden ancestry): hwnd %p reason=%s.\n", hwnd, reason); ++ TRACE_(wmtrace)("REBLIT-SKIP hwnd=%p cause=hidden reason=%s\n", hwnd, reason); + return; + } + +@@ -532,6 +534,7 @@ static void dcomp_reblit_comp_buffer(HWND hwnd, const char *reason) + if (stale_count <= 5 || !(stale_count % 200)) + FIXME("Re-blit skipped (stale %ux%u buffer): hwnd %p reason=%s.\n", + w, h, hwnd, reason); ++ TRACE_(wmtrace)("REBLIT-SKIP hwnd=%p cause=stale size=%ux%u reason=%s\n", hwnd, w, h, reason); + dcomp_arm_delayed_reblit(hwnd); + return; + } +@@ -543,6 +546,7 @@ static void dcomp_reblit_comp_buffer(HWND hwnd, const char *reason) + if (reblit_count <= 5 || !(reblit_count % 200)) + FIXME("Re-blit #%u: hwnd %p %ux%u reason=%s.\n", + reblit_count, hwnd, w, h, reason); ++ TRACE_(wmtrace)("REBLIT hwnd=%p size=%ux%u reason=%s\n", hwnd, w, h, reason); + BitBlt(hdc, 0, 0, w, h, comp_dc, 0, 0, SRCCOPY); + ReleaseDC(hwnd, hdc); + } +diff --git a/dlls/dxgi/swapchain.c b/dlls/dxgi/swapchain.c +index 3fd784e..e768bed 100644 +--- a/dlls/dxgi/swapchain.c ++++ b/dlls/dxgi/swapchain.c +@@ -26,6 +26,7 @@ + + WINE_DEFAULT_DEBUG_CHANNEL(dxgi); + WINE_DECLARE_DEBUG_CHANNEL(winediag); ++WINE_DECLARE_DEBUG_CHANNEL(wmtrace); + + static UINT dxgi_get_display_refresh_rate(void) + { +@@ -1216,6 +1217,12 @@ HRESULT d3d11_swapchain_init(struct d3d11_swapchain *swapchain, struct dxgi_devi + wined3d_swapchain_set_prefer_gl_present(swapchain->wined3d_swapchain, TRUE); + FIXME("Top-level device window %p (style %#lx), preferring GL present.\n", + desc->device_window, style); ++ TRACE_(wmtrace)("PRESENT-PATH hwnd=%p style=%#lx path=gl\n", desc->device_window, style); ++ } ++ else ++ { ++ TRACE_(wmtrace)("PRESENT-PATH hwnd=%p style=%#lx path=gdi disabled=%d\n", ++ desc->device_window, style, disabled); + } + } + +diff --git a/dlls/win32u/dce.c b/dlls/win32u/dce.c +index 71db6ec..8962d9a 100644 +--- a/dlls/win32u/dce.c ++++ b/dlls/win32u/dce.c +@@ -33,6 +33,7 @@ + #include "wine/debug.h" + + WINE_DEFAULT_DEBUG_CHANNEL(win); ++WINE_DECLARE_DEBUG_CHANNEL(wmtrace); + + struct dce + { +@@ -581,6 +582,7 @@ struct window_surface *window_surface_create( UINT size, const struct window_sur + memset( window_surface_get_color( surface, info ), 0x00, info->bmiHeader.biSizeImage ); + + TRACE( "created surface %p for hwnd %p rect %s\n", surface, hwnd, wine_dbgstr_rect( &surface->rect ) ); ++ TRACE_(wmtrace)( "SURFACE-CREATE hwnd=%p surface=%p rect=%s\n", hwnd, surface, wine_dbgstr_rect( &surface->rect ) ); + return surface; + } + +diff --git a/dlls/win32u/window.c b/dlls/win32u/window.c +index 5110d70..7e4bbc8 100644 +--- a/dlls/win32u/window.c ++++ b/dlls/win32u/window.c +@@ -34,6 +34,7 @@ + #include "wine/debug.h" + + WINE_DEFAULT_DEBUG_CHANNEL(win); ++WINE_DECLARE_DEBUG_CHANNEL(wmtrace); + + #define USER_HANDLE_TO_INDEX(hwnd) ((LOWORD(hwnd) - FIRST_USER_HANDLE) >> 1) + #define USER_HANDLE_FROM_INDEX(index, generation) UlongToHandle( (index << 1) + FIRST_USER_HANDLE + (generation << 16) ) +@@ -4390,6 +4391,8 @@ BOOL WINAPI NtUserSetWindowPos( HWND hwnd, HWND after, INT x, INT y, INT cx, INT + winpos.cx = mon_width; + winpos.cy = mon_height; + winpos.flags &= ~(SWP_NOMOVE | SWP_NOSIZE); ++ TRACE_(wmtrace)( "FS-NORMALIZE hwnd=%p req=%s mon=%s\n", winpos.hwnd, ++ wine_dbgstr_rect( &requested ), wine_dbgstr_rect( &mi.rcMonitor ) ); + } + else + { +@@ -4397,6 +4400,8 @@ BOOL WINAPI NtUserSetWindowPos( HWND hwnd, HWND after, INT x, INT y, INT cx, INT + /* The fullscreen client override survives a no-size exit + * unless the ordinary non-client area is recalculated. */ + if (was_fullscreen) winpos.flags |= SWP_FRAMECHANGED; ++ if (was_fullscreen) TRACE_(wmtrace)( "FS-EXIT hwnd=%p framechanged req=%s\n", winpos.hwnd, ++ wine_dbgstr_rect( &requested ) ); + } + } + } +diff --git a/dlls/winex11.drv/window.c b/dlls/winex11.drv/window.c +index c19a0ec..aca972c 100644 +--- a/dlls/winex11.drv/window.c ++++ b/dlls/winex11.drv/window.c +@@ -55,6 +55,7 @@ + + WINE_DEFAULT_DEBUG_CHANNEL(x11drv); + WINE_DECLARE_DEBUG_CHANNEL(systray); ++WINE_DECLARE_DEBUG_CHANNEL(wmtrace); + + #define _NET_WM_MOVERESIZE_SIZE_TOPLEFT 0 + #define _NET_WM_MOVERESIZE_SIZE_TOP 1 +@@ -1117,6 +1118,8 @@ static void window_set_mwm_hints( struct x11drv_win_data *data, const MwmHints * + data->mwm_hints_serial = NextRequest( data->display ); + TRACE( "window %p/%lx, requesting _MOTIF_WM_HINTS %s serial %lu\n", data->hwnd, data->whole_window, + debugstr_mwm_hints(&data->pending_state.mwm_hints), data->mwm_hints_serial ); ++ TRACE_(wmtrace)( "REQ-MOTIF hwnd=%p xid=%lx hints=%s serial=%lu\n", data->hwnd, data->whole_window, ++ debugstr_mwm_hints(&data->pending_state.mwm_hints), data->mwm_hints_serial ); + XChangeProperty( data->display, data->whole_window, x11drv_atom(_MOTIF_WM_HINTS), x11drv_atom(_MOTIF_WM_HINTS), + 32, PropModeReplace, (unsigned char *)new_hints, sizeof(*new_hints) / sizeof(long) ); + } +@@ -1189,6 +1192,8 @@ static void window_set_net_wm_window_type( struct x11drv_win_data *data, enum x1 + data->pending_state.net_wm_window_type = new_type; + TRACE( "window %p/%lx, requesting _NET_WM_WINDOW_TYPE %lx (%s) serial %lu\n", data->hwnd, data->whole_window, + new_type, X11DRV_atom_names[atom - FIRST_XATOM], NextRequest( data->display ) ); ++ TRACE_(wmtrace)( "REQ-TYPE hwnd=%p xid=%lx type=%s\n", data->hwnd, data->whole_window, ++ X11DRV_atom_names[atom - FIRST_XATOM] ); + XChangeProperty( data->display, data->whole_window, x11drv_atom(_NET_WM_WINDOW_TYPE), XA_ATOM, + 32, PropModeReplace, (unsigned char *)&new_type, 1 ); + } +@@ -1603,6 +1608,8 @@ static void window_set_net_wm_state( struct x11drv_win_data *data, UINT new_stat + data->net_wm_state_serial = NextRequest( data->display ); + TRACE( "window %p/%lx, requesting _NET_WM_STATE %#x serial %lu\n", data->hwnd, data->whole_window, + data->pending_state.net_wm_state, data->net_wm_state_serial ); ++ TRACE_(wmtrace)( "REQ-NETWMSTATE hwnd=%p xid=%lx state=%#x serial=%lu direct=1\n", data->hwnd, ++ data->whole_window, data->pending_state.net_wm_state, data->net_wm_state_serial ); + XChangeProperty( data->display, data->whole_window, x11drv_atom(_NET_WM_STATE), XA_ATOM, + 32, PropModeReplace, (unsigned char *)atoms, count ); + } +@@ -1634,6 +1641,8 @@ static void window_set_net_wm_state( struct x11drv_win_data *data, UINT new_stat + data->net_wm_state_serial = NextRequest( data->display ); + TRACE( "window %p/%lx, requesting _NET_WM_STATE %#x serial %lu\n", data->hwnd, data->whole_window, + data->pending_state.net_wm_state, data->net_wm_state_serial ); ++ TRACE_(wmtrace)( "REQ-NETWMSTATE hwnd=%p xid=%lx state=%#x serial=%lu direct=0\n", data->hwnd, ++ data->whole_window, data->pending_state.net_wm_state, data->net_wm_state_serial ); + XSendEvent( data->display, DefaultRootWindow( data->display ), False, + SubstructureRedirectMask | SubstructureNotifyMask, &xev ); + } +@@ -1867,6 +1876,8 @@ static void window_set_config( struct x11drv_win_data *data, RECT rect, BOOL abo + TRACE( "window %p/%lx aliasing sub-scale app request %s to host %s, not forwarded\n", + data->hwnd, data->whole_window, wine_dbgstr_rect(new_rect), + wine_dbgstr_rect(&data->current_state.rect) ); ++ TRACE_(wmtrace)( "ALIAS hwnd=%p xid=%lx win32=%s host=%s\n", data->hwnd, data->whole_window, ++ wine_dbgstr_rect(new_rect), wine_dbgstr_rect(&data->current_state.rect) ); + return; + } + +@@ -1912,6 +1923,8 @@ static void window_set_config( struct x11drv_win_data *data, RECT rect, BOOL abo + track_config_rounding_request( data, data->configure_serial, new_rect ); + TRACE( "window %p/%lx, requesting config %s mask %#x above %u, serial %lu\n", data->hwnd, data->whole_window, + wine_dbgstr_rect(new_rect), mask, above, data->configure_serial ); ++ TRACE_(wmtrace)( "REQ-CONFIG hwnd=%p xid=%lx rect=%s mask=%#x above=%u serial=%lu\n", data->hwnd, ++ data->whole_window, wine_dbgstr_rect(new_rect), mask, above, data->configure_serial ); + XReconfigureWMWindow( data->display, data->whole_window, data->vis.screen, mask, &changes ); + } + +@@ -2132,6 +2145,16 @@ static void window_set_wm_state( struct x11drv_win_data *data, UINT new_state, B + data->wm_state_serial = NextRequest( data->display ); + TRACE( "window %p/%lx, requesting WM_STATE %#x -> %#x serial %lu, foreground %p, activate %u\n", data->hwnd, data->whole_window, + old_state, new_state, data->wm_state_serial, NtUserGetForegroundWindow(), activate ); ++ if (TRACE_ON(wmtrace)) ++ { ++ UINT trace_style = NtUserGetWindowLongW( data->hwnd, GWL_STYLE ); ++ UINT trace_ex_style = NtUserGetWindowLongW( data->hwnd, GWL_EXSTYLE ); ++ TRACE_(wmtrace)( "REQ-WMSTATE hwnd=%p xid=%lx %#x->%#x serial=%lu activate=%u style=%08x ex=%08x " ++ "owner=%p managed=%u rect=%s dpi=%u\n", data->hwnd, data->whole_window, old_state, ++ new_state, data->wm_state_serial, activate, trace_style, trace_ex_style, ++ NtUserGetWindowRelative( data->hwnd, GW_OWNER ), data->managed, ++ wine_dbgstr_rect( &data->rects.window ), NtUserGetDpiForWindow( data->hwnd ) ); ++ } + + switch (MAKELONG(old_state, new_state)) + { +@@ -2403,6 +2426,8 @@ BOOL X11DRV_GetWindowStateUpdates( HWND hwnd, UINT *state_cmd, UINT *swp_flags, + if (!*state_cmd && !*swp_flags && !*foreground) return FALSE; + TRACE( "hwnd %p, returning state_cmd %#x, swp_flags %#x, rect %s, foreground %p\n", + hwnd, *state_cmd, *swp_flags, wine_dbgstr_rect(rect), *foreground ); ++ TRACE_(wmtrace)( "WIN32-APPLY hwnd=%p state_cmd=%#x swp=%#x rect=%s foreground=%p\n", ++ hwnd, *state_cmd, *swp_flags, wine_dbgstr_rect(rect), *foreground ); + return TRUE; + } + +@@ -2414,6 +2439,7 @@ static BOOL handle_state_change( unsigned long serial, unsigned long *expect_ser + if (reason) + { + WARN( "Ignoring %s%s%s%s\n", prefix, reason, received, expected ); ++ TRACE_(wmtrace)( "ACK-IGNORED %s%s%s%s\n", prefix, reason, received, expected ); + memcpy( current, value, size ); + return FALSE; + } +@@ -2428,6 +2454,7 @@ static BOOL handle_state_change( unsigned long serial, unsigned long *expect_ser + memcpy( desired, value, size ); + memcpy( pending, value, size ); + } ++ TRACE_(wmtrace)( "ACK%s %s%s%s%s\n", reason ? "-FORCED" : "", prefix, reason ? reason : "", received, expected ); + + memcpy( current, value, size ); + *expect_serial = 0; +@@ -2730,6 +2757,8 @@ void set_net_active_window( HWND hwnd, HWND previous ) + { + /* workaround requesting activation of withdrawn windows which breaks some WM, assume the window will soon be mapped */ + WARN( "skipping _NET_ACTIVE_WINDOW for withdrawn window %p/%lx serial %lu\n", hwnd, window, data->net_active_window_serial ); ++ TRACE_(wmtrace)( "REQ-ACTIVATE-SKIP hwnd=%p xid=%lx withdrawn serial=%lu\n", hwnd, window, ++ data->net_active_window_serial ); + XNoOp( data->display ); + return; + } +@@ -2737,6 +2766,8 @@ void set_net_active_window( HWND hwnd, HWND previous ) + + TRACE( "requesting _NET_ACTIVE_WINDOW %p/%lx serial %lu time %lu\n", hwnd, window, + data->net_active_window_serial, data->net_active_window_time ); ++ TRACE_(wmtrace)( "REQ-ACTIVATE hwnd=%p xid=%lx prev=%p serial=%lu time=%lu\n", hwnd, window, previous, ++ data->net_active_window_serial, data->net_active_window_time ); + XSendEvent( data->display, DefaultRootWindow( data->display ), False, + SubstructureRedirectMask | SubstructureNotifyMask, &xev ); + } diff --git a/patches/0091-winex11-skip-repeat-_NET_ACTIVE_WINDOW-requests-keep.patch b/patches/0091-winex11-skip-repeat-_NET_ACTIVE_WINDOW-requests-keep.patch new file mode 100644 index 00000000..ba1433b1 --- /dev/null +++ b/patches/0091-winex11-skip-repeat-_NET_ACTIVE_WINDOW-requests-keep.patch @@ -0,0 +1,66 @@ +Subject: winex11: skip repeat _NET_ACTIVE_WINDOW requests, keep real timestamps + +Patch 0017 fixed mutter's focus-stealing prevention dropping our +timestamp-0 activation requests, and also relaxed the duplicate skip so +a newer input timestamp could retry an ignored request. Under KWin every +activation produces feedback that yields fresh input timestamps, so the +retries ping-pong: wrong taskbar focus and unmovable, unsizable windows. +giang17/wine hit the same regression porting 0017 and dropped the retry +in a641db69 (issue 76); the non-zero timestamp alone fixes the mutter +deadlock the retry was written for. + +Return to the single pending-window skip and keep the timestamp work +unchanged. WINE_ACTIVATE_RESEND=1 restores the relaxed skip for +comparison. + +diff --git a/dlls/winex11.drv/window.c b/dlls/winex11.drv/window.c +index aca972c..c6ceb33 100644 +--- a/dlls/winex11.drv/window.c ++++ b/dlls/winex11.drv/window.c +@@ -2699,6 +2699,19 @@ void net_active_window_init( struct x11drv_thread_data *data ) + data->current_state.net_active_window = window; + } + ++/* WINE_ACTIVATE_RESEND=1 restores patch 0017's relaxed duplicate skip. */ ++static BOOL net_active_window_resend(void) ++{ ++ static int mode = -1; ++ ++ if (mode == -1) ++ { ++ const char *env = getenv( "WINE_ACTIVATE_RESEND" ); ++ mode = env && !strcmp( env, "1" ); ++ } ++ return mode; ++} ++ + static BOOL window_set_pending_activate( HWND hwnd, BOOL *withdrawn ) + { + struct x11drv_win_data *data; +@@ -2729,12 +2742,20 @@ void set_net_active_window( HWND hwnd, HWND previous ) + if (EVENT_x11_time_to_win32_time( 0 )) + time = NtUserGetThreadState( UserThreadStateMessageTime ) - EVENT_x11_time_to_win32_time( 0 ); + +- /* Skip only exact repeats: if the WM ignored a previous request for this window +- * (e.g. it carried an older timestamp than the WM's focus-stealing cutoff), a new +- * user interaction yields a fresh timestamp and must be allowed to retry, else the +- * window can never be activated again for the rest of the session. */ +- if (data->current_state.net_active_window == window && !data->net_active_window_serial) return; +- if (data->pending_state.net_active_window == window && time == data->net_active_window_time) return; ++ /* Patch 0017 also relaxed this skip so a newer input timestamp could retry ++ * an ignored request. Under KWin every activation produces feedback that ++ * yields fresh input timestamps, so retries ping-pong: wrong taskbar focus ++ * and unmovable/unsizable windows (the fork dropped the retry in a641db69 ++ * for the same reason, issue 76). The non-zero timestamp alone fixes the ++ * mutter deadlock the retry was written for, so skip any repeat for the ++ * pending window. WINE_ACTIVATE_RESEND=1 restores the relaxed skip for ++ * comparison. */ ++ if (net_active_window_resend()) ++ { ++ if (data->current_state.net_active_window == window && !data->net_active_window_serial) return; ++ if (data->pending_state.net_active_window == window && time == data->net_active_window_time) return; ++ } ++ else if (data->pending_state.net_active_window == window) return; + if (window_set_pending_activate( hwnd, &withdrawn )) return; + + xev.xclient.type = ClientMessage; diff --git a/patches/0092-winex11-let-WINE_ACTIVATE_SOURCE-choose-the-EWMH-act.patch b/patches/0092-winex11-let-WINE_ACTIVATE_SOURCE-choose-the-EWMH-act.patch new file mode 100644 index 00000000..5a2448fa --- /dev/null +++ b/patches/0092-winex11-let-WINE_ACTIVATE_SOURCE-choose-the-EWMH-act.patch @@ -0,0 +1,47 @@ +Subject: winex11: let WINE_ACTIVATE_SOURCE choose the EWMH activation source + +The base sends every _NET_ACTIVE_WINDOW request with source indication +2 (pager / direct user action), which asks the WM to activate +unconditionally. EWMH lets a WM treat source 1 (application) requests +with its normal focus-stealing rules instead, which may suit WMs where +unconditional activation fights the user. WINE_ACTIVATE_SOURCE= +application sends 1; anything else keeps the base behaviour. Default +off so the 0091 activation change stays a single variable. + +diff --git a/dlls/winex11.drv/window.c b/dlls/winex11.drv/window.c +index c6ceb33..7cbf5ad 100644 +--- a/dlls/winex11.drv/window.c ++++ b/dlls/winex11.drv/window.c +@@ -2699,6 +2699,23 @@ void net_active_window_init( struct x11drv_thread_data *data ) + data->current_state.net_active_window = window; + } + ++/* EWMH source indication for our _NET_ACTIVE_WINDOW requests. The base has ++ * always sent 2 (pager / direct user action), which asks the WM to activate ++ * unconditionally. WINE_ACTIVATE_SOURCE=application sends 1 instead, so the ++ * WM applies its normal focus-stealing rules to app-initiated activation; ++ * default-off experiment. */ ++static long net_active_window_source(void) ++{ ++ static long source = -1; ++ ++ if (source == -1) ++ { ++ const char *env = getenv( "WINE_ACTIVATE_SOURCE" ); ++ source = (env && !strcmp( env, "application" )) ? 1 : 2; ++ } ++ return source; ++} ++ + /* WINE_ACTIVATE_RESEND=1 restores patch 0017's relaxed duplicate skip. */ + static BOOL net_active_window_resend(void) + { +@@ -2765,7 +2782,7 @@ void set_net_active_window( HWND hwnd, HWND previous ) + xev.xclient.display = data->display; + xev.xclient.send_event = True; + xev.xclient.format = 32; +- xev.xclient.data.l[0] = 2; /* source: pager */ ++ xev.xclient.data.l[0] = net_active_window_source(); /* source: pager, or application via WINE_ACTIVATE_SOURCE */ + xev.xclient.data.l[1] = time; + xev.xclient.data.l[2] = X11DRV_get_whole_window( previous ); /* current active */ + xev.xclient.data.l[3] = 0; diff --git a/patches/0093-win32u-winex11-lock-host-window-state-updates-while-.patch b/patches/0093-win32u-winex11-lock-host-window-state-updates-while-.patch new file mode 100644 index 00000000..88569d64 --- /dev/null +++ b/patches/0093-win32u-winex11-lock-host-window-state-updates-while-.patch @@ -0,0 +1,175 @@ +Subject: win32u/winex11: lock host window state updates while they are applied + +Re-entrant SetWindowPos processing during WM_WINE_WINDOW_STATE_CHANGED +could request stale minimise, restore, geometry and Motif state while +the host state was still being applied. Picky window managers saw +spurious minimise/restore cycles, and an unlucky schedule could apply +the transient state before SC_RESTORE. + +GetWindowStateUpdates now locks the window's host state before +computing updates; win32u unlocks it with a null-argument call after +applying them, and requests parked in desired_state flush on unlock. + +Backport of wine-mirror 852e39a89c0d (Remi Bernon) onto this tree, +adapted around the wmtrace lines and the 0042 config-rounding state, +with LOCK/UNLOCK wmtrace lines added and an underflow guard on the +unlock path. WINE_HOST_STATE_LOCK=off disables the lock for +comparison. + +diff --git a/dlls/win32u/message.c b/dlls/win32u/message.c +index a0f0e58..6a14a66 100644 +--- a/dlls/win32u/message.c ++++ b/dlls/win32u/message.c +@@ -2235,7 +2235,7 @@ static LRESULT handle_internal_message( HWND hwnd, UINT msg, WPARAM wparam, LPAR + RECT window_rect; + HWND foreground; + +- if (!user_driver->pGetWindowStateUpdates( hwnd, &state_cmd, &swp_flags, &window_rect, &foreground )) return 0; ++ if (!user_driver->pGetWindowStateUpdates( hwnd, &state_cmd, &swp_flags, &window_rect, &foreground )) goto unlock; + window_rect = map_rect_raw_to_virt( window_rect, get_thread_dpi() ); + + if (foreground) set_foreground_window( foreground, FALSE, TRUE ); +@@ -2258,6 +2258,8 @@ static LRESULT handle_internal_message( HWND hwnd, UINT msg, WPARAM wparam, LPAR + break; + } + ++ unlock: ++ user_driver->pGetWindowStateUpdates( hwnd, NULL, NULL, NULL, NULL ); /* unlock the host state */ + return 0; + } + case WM_WINE_UPDATEWINDOWSTATE: +diff --git a/dlls/winex11.drv/window.c b/dlls/winex11.drv/window.c +index 7cbf5ad..8cfe055 100644 +--- a/dlls/winex11.drv/window.c ++++ b/dlls/winex11.drv/window.c +@@ -1103,6 +1103,7 @@ static void window_set_mwm_hints( struct x11drv_win_data *data, const MwmHints * + const MwmHints *old_hints = &data->pending_state.mwm_hints; + + data->desired_state.mwm_hints = *new_hints; ++ if (data->state_locks) return; /* win32 state is being updated, delay the change */ + if (!data->whole_window || !data->managed || data->embedded) return; /* no window or not managed, nothing to update */ + if (!memcmp( old_hints, new_hints, sizeof(*new_hints) )) return; /* hints are the same, nothing to update */ + +@@ -1523,6 +1524,7 @@ static void window_set_net_wm_fullscreen_monitors( struct x11drv_win_data *data, + data->desired_state.monitors = *new_monitors; + + if (!(data->pending_state.net_wm_state & (1 << NET_WM_STATE_FULLSCREEN)) || is_virtual_desktop()) return; /* window isn't fullscreen, delay updating */ ++ if (data->state_locks) return; /* win32 state is being updated, delay the change */ + if (!data->whole_window || !data->managed || data->embedded) return; /* no window or not managed, nothing to update */ + if (!memcmp( old_monitors, new_monitors, sizeof(*new_monitors) )) return; /* states are the same, nothing to update */ + +@@ -1580,6 +1582,7 @@ static void window_set_net_wm_state( struct x11drv_win_data *data, UINT new_stat + + new_state &= x11drv_init_thread_data()->net_wm_state_mask; + data->desired_state.net_wm_state = new_state; ++ if (data->state_locks) return; /* win32 state is being updated, delay the change */ + if (!data->whole_window || !data->managed || data->embedded) return; /* no window or not managed, nothing to update */ + if (data->wm_state_serial) return; /* another WM_STATE update is pending, wait for it to complete */ + /* we ignore and override previous _NET_WM_STATE update requests */ +@@ -1848,6 +1851,7 @@ static void window_set_config( struct x11drv_win_data *data, RECT rect, BOOL abo + + data->desired_state.rect = *new_rect; + data->desired_state.above = above; ++ if (data->state_locks) return; /* win32 state is being updated, delay the change */ + if (!data->whole_window) return; /* no window, nothing to update */ + if (EqualRect( old_rect, new_rect ) && (old_above || !above || data->managed)) return; /* rects are the same, no need to be raised, nothing to update */ + if (window_needs_config_change_delay( data )) +@@ -2070,6 +2074,7 @@ static void window_set_wm_state( struct x11drv_win_data *data, UINT new_state, B + + data->desired_state.wm_state = new_state; + data->desired_state.activate = activate; ++ if (data->state_locks) return; /* win32 state is being updated, delay the change */ + if (!data->whole_window) return; /* no window, nothing to update */ + if (data->wm_state_serial && !data->current_state.wm_state != !data->pending_state.wm_state) + return; /* another map/unmap WM_STATE update is pending, wait for it to complete */ +@@ -2391,6 +2396,28 @@ static UINT window_update_client_config( struct x11drv_win_data *data ) + return flags; + } + ++static void window_request_desired_state( struct x11drv_win_data *data ) ++{ ++ window_set_wm_state( data, data->desired_state.wm_state, data->desired_state.activate ); ++ window_set_net_wm_state( data, data->desired_state.net_wm_state ); ++ window_set_net_wm_fullscreen_monitors( data, &data->desired_state.monitors ); ++ window_set_mwm_hints( data, &data->desired_state.mwm_hints ); ++ window_set_config( data, data->desired_state.rect, FALSE ); ++} ++ ++/* WINE_HOST_STATE_LOCK=off disables the host-state lock for comparison. */ ++static BOOL host_state_lock_enabled(void) ++{ ++ static int enabled = -1; ++ ++ if (enabled == -1) ++ { ++ const char *env = getenv( "WINE_HOST_STATE_LOCK" ); ++ enabled = !env || (strcmp( env, "off" ) && strcmp( env, "0" )); ++ } ++ return enabled; ++} ++ + /*********************************************************************** + * GetWindowStateUpdates (X11DRV.@) + */ +@@ -2400,6 +2427,21 @@ BOOL X11DRV_GetWindowStateUpdates( HWND hwnd, UINT *state_cmd, UINT *swp_flags, + struct x11drv_win_data *data; + HWND old_foreground; + ++ if (!state_cmd) ++ { ++ if (host_state_lock_enabled() && (data = get_win_data( hwnd ))) ++ { ++ if (data->state_locks && !--data->state_locks) ++ { ++ TRACE( "Unlocked window %p/%lx state\n", data->hwnd, data->whole_window ); ++ TRACE_(wmtrace)( "UNLOCK hwnd=%p xid=%lx\n", data->hwnd, data->whole_window ); ++ } ++ window_request_desired_state( data ); ++ release_win_data( data ); ++ } ++ return FALSE; ++ } ++ + *state_cmd = *swp_flags = 0; + *foreground = 0; + +@@ -2414,6 +2456,11 @@ BOOL X11DRV_GetWindowStateUpdates( HWND hwnd, UINT *state_cmd, UINT *swp_flags, + + if ((data = get_win_data( hwnd ))) + { ++ if (host_state_lock_enabled() && !data->state_locks++) ++ { ++ TRACE( "Locked window %p/%lx state\n", data->hwnd, data->whole_window ); ++ TRACE_(wmtrace)( "LOCK hwnd=%p xid=%lx\n", data->hwnd, data->whole_window ); ++ } + *state_cmd = window_update_client_state( data ); + *swp_flags = window_update_client_config( data ); + if (*swp_flags && config_rounding_move_pending( data )) +@@ -2461,15 +2508,6 @@ static BOOL handle_state_change( unsigned long serial, unsigned long *expect_ser + return TRUE; + } + +-static void window_request_desired_state( struct x11drv_win_data *data ) +-{ +- window_set_wm_state( data, data->desired_state.wm_state, data->desired_state.activate ); +- window_set_net_wm_state( data, data->desired_state.net_wm_state ); +- window_set_net_wm_fullscreen_monitors( data, &data->desired_state.monitors ); +- window_set_mwm_hints( data, &data->desired_state.mwm_hints ); +- window_set_config( data, data->desired_state.rect, FALSE ); +-} +- + void window_wm_state_notify( struct x11drv_win_data *data, unsigned long serial, UINT value, Time time ) + { + UINT *desired = &data->desired_state.wm_state, *pending = &data->pending_state.wm_state, *current = &data->current_state.wm_state; +diff --git a/dlls/winex11.drv/x11drv.h b/dlls/winex11.drv/x11drv.h +index 34b53ce..02154d8 100644 +--- a/dlls/winex11.drv/x11drv.h ++++ b/dlls/winex11.drv/x11drv.h +@@ -715,6 +715,7 @@ struct x11drv_win_data + unsigned long wm_normal_hints_serial;/* serial of last pending WM_NORMAL_HINTS request */ + unsigned long configure_serial; /* serial of last pending configure request */ + unsigned long net_wm_icon_serial; /* serial of last pending _NET_WM_ICON request */ ++ unsigned long state_locks; /* X11 state requests lock while updating win32 state */ + unsigned long config_generation; /* generation of the current Win32 window rect */ + unsigned long config_rounding_serial; /* configure request which may be rounded by the WM */ + unsigned long config_rounding_settle_serial; /* last related request which may adjust the config */ diff --git a/patches/0094-dxgi-make-DComp-window-teardown-complete-idempotent-.patch b/patches/0094-dxgi-make-DComp-window-teardown-complete-idempotent-.patch new file mode 100644 index 00000000..e2f865ec --- /dev/null +++ b/patches/0094-dxgi-make-DComp-window-teardown-complete-idempotent-.patch @@ -0,0 +1,306 @@ +Subject: dxgi: make DComp window teardown complete, idempotent and owning-thread + +Releasing a swapchain from a thread other than its target window's +skipped every window-owning cleanup: the reblit timers kept firing, the +subclass stayed installed, the composition properties leaked, and the +subclassed-target count never fell, so winex11 kept classifying +ownerless tool popups as menus for the rest of the session. A +same-thread release also left the popup-stack entry, the desktop active +property and the parent clip bit behind, and a release followed by +window destruction could tear down twice. + +Factor the whole teardown into dcomp_teardown_window: timers, subclass +restore with ownership validation, all composition properties, the +popup-stack entry, the active count and the parent clip, one-shot via a +__wine_dcomp_subclassed marker. Both NCDESTROY handlers, same-thread +release and a new WM_WINE_DCOMP_TEARDOWN message use it; a cross-thread +release posts that message to the owning thread instead of skipping, +and the comp window destroy marshals the same way. +WINE_DCOMP_TEARDOWN=legacy restores the old release path for +comparison. + +diff --git a/dlls/dxgi/dxgi_private.h b/dlls/dxgi/dxgi_private.h +index 039105b..9a1b9b9 100644 +--- a/dlls/dxgi/dxgi_private.h ++++ b/dlls/dxgi/dxgi_private.h +@@ -179,6 +179,8 @@ struct dxgi_adapter *unsafe_impl_from_IDXGIAdapter(IDXGIAdapter *iface); + * Controls full-vs-popup mode; d3d11_swapchain_Release() decrements it when it + * un-subclasses a target on its own UI thread. */ + extern LONG dcomp_subclassed_target_count; ++extern void dcomp_teardown_window(HWND hwnd); ++extern BOOL dcomp_teardown_legacy(void); + + /* IDXGISwapChain */ + struct d3d11_swapchain +diff --git a/dlls/dxgi/factory.c b/dlls/dxgi/factory.c +index 726c13d..7cc0d08 100644 +--- a/dlls/dxgi/factory.c ++++ b/dlls/dxgi/factory.c +@@ -390,6 +390,7 @@ static void STDMETHODCALLTYPE dxgi_factory_UnregisterOcclusionStatus(IWineDXGIFa + + #define WM_WINE_DCOMP_SET_TARGET (WM_USER + 0x100) + #define WM_WINE_DCOMP_SET_CHILD_MODE (WM_USER + 0x101) ++#define WM_WINE_DCOMP_TEARDOWN (WM_USER + 0x102) + /* SET_TARGET wparam flag set by dlls/dcomp when the binding dcomp device has + * no d2d1 device (mirrored there, like the message id itself). */ + #define DCOMP_SET_TARGET_NULL_D2D1_DEVICE 0x1 +@@ -648,6 +649,71 @@ static void dcomp_restore_parent_clip(HWND hwnd) + } + } + ++static LRESULT CALLBACK dcomp_popup_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); ++static LRESULT CALLBACK dcomp_target_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); ++ ++/* WINE_DCOMP_TEARDOWN=legacy restores the pre-0094 swapchain-release ++ * teardown (window-owning cleanup only on the owning thread, skipped ++ * otherwise) for comparison. */ ++BOOL dcomp_teardown_legacy(void) ++{ ++ static int legacy = -1; ++ ++ if (legacy == -1) ++ { ++ WCHAR val[8] = {0}; ++ legacy = GetEnvironmentVariableW(L"WINE_DCOMP_TEARDOWN", val, ARRAY_SIZE(val)) ++ && !wcscmp(val, L"legacy"); ++ } ++ return legacy; ++} ++ ++/* Complete, idempotent teardown of a subclassed DComp target window: ++ * timers, subclass, window properties, popup-stack entry, active count ++ * and parent clip restore. Must run on the window's owning thread ++ * (KillTimer and SetWindowLongPtrW are thread-affine); a cross-thread ++ * swapchain Release marshals here via WM_WINE_DCOMP_TEARDOWN. The ++ * __wine_dcomp_subclassed marker makes the count/stack/clip effects ++ * one-shot however many of Release, the marshalled message and ++ * WM_NCDESTROY run for the same window. */ ++void dcomp_teardown_window(HWND hwnd) ++{ ++ WNDPROC orig = (WNDPROC)GetPropW(hwnd, L"__wine_dcomp_orig_wndproc"); ++ WNDPROC current; ++ ++ if (!GetPropW(hwnd, L"__wine_dcomp_subclassed")) return; /* already torn down */ ++ RemovePropW(hwnd, L"__wine_dcomp_subclassed"); ++ ++ KillTimer(hwnd, DCOMP_REBLIT_TIMER_ID); ++ KillTimer(hwnd, DCOMP_POPUP_REBLIT_TIMER_ID); ++ KillTimer(hwnd, DCOMP_RESIZE_REBLIT_TIMER_ID); ++ ++ /* Only unhook while one of our procs is still the current wndproc: ++ * if something subclassed the window after us, restoring orig here ++ * would cut that outer chain. Leave the chain and the orig property ++ * in place so our proc keeps forwarding. */ ++ current = (WNDPROC)GetWindowLongPtrW(hwnd, GWLP_WNDPROC); ++ if (orig && (current == dcomp_target_wndproc || current == dcomp_popup_wndproc)) ++ { ++ SetWindowLongPtrW(hwnd, GWLP_WNDPROC, (LONG_PTR)orig); ++ RemovePropW(hwnd, L"__wine_dcomp_orig_wndproc"); ++ } ++ ++ InterlockedDecrement(&dcomp_subclassed_target_count); ++ dcomp_update_active_prop(); ++ dcomp_popup_stack_remove(hwnd); ++ dcomp_restore_parent_clip(hwnd); ++ ++ RemovePropW(hwnd, L"__wine_dcomp_comp_dc"); ++ RemovePropW(hwnd, L"__wine_dcomp_comp_size"); ++ RemovePropW(hwnd, L"__wine_dcomp_comp_bits"); ++ RemovePropW(hwnd, L"__wine_dcomp_last_present"); ++ RemovePropW(hwnd, L"__wine_dcomp_reblit_tries"); ++ RemovePropW(hwnd, L"__wine_dcomp_popup_parent"); ++ dcomp_clear_null_device_props(hwnd); ++ TRACE_(wmtrace)("DCOMP-TEARDOWN hwnd=%p\n", hwnd); ++} ++ + /* Lightweight subclass for DComp popup windows (menus, tooltips). + * Only blocks WM_ERASEBKGND and re-blits composition content on WM_PAINT. + * NO timer, NO periodic Present — avoids dual-swapchain +@@ -679,6 +745,10 @@ static LRESULT CALLBACK dcomp_popup_wndproc(HWND hwnd, UINT msg, WPARAM wparam, + return 0; + } + ++ case WM_WINE_DCOMP_TEARDOWN: ++ dcomp_teardown_window(hwnd); ++ return 0; ++ + case WM_TIMER: + if (wparam == DCOMP_POPUP_REBLIT_TIMER_ID) + { +@@ -696,12 +766,6 @@ static LRESULT CALLBACK dcomp_popup_wndproc(HWND hwnd, UINT msg, WPARAM wparam, + LRESULT result; + IDXGISwapChain4 *sc = (IDXGISwapChain4 *)GetPropW(hwnd, L"__wine_dcomp_swapchain"); + +- InterlockedDecrement(&dcomp_subclassed_target_count); +- dcomp_update_active_prop(); +- dcomp_popup_stack_remove(hwnd); +- KillTimer(hwnd, DCOMP_POPUP_REBLIT_TIMER_ID); +- KillTimer(hwnd, DCOMP_RESIZE_REBLIT_TIMER_ID); +- + /* Before the popup window is destroyed, switch the swapchain back + * to its composition window. This drains the CS queue (no more + * presents to the dead popup), releases the popup's DC, and +@@ -736,15 +800,10 @@ static LRESULT CALLBACK dcomp_popup_wndproc(HWND hwnd, UINT msg, WPARAM wparam, + } + } + +- dcomp_restore_parent_clip(hwnd); ++ dcomp_teardown_window(hwnd); + result = orig ? CallWindowProcW(orig, hwnd, msg, wparam, lparam) + : DefWindowProcW(hwnd, msg, wparam, lparam); +- RemovePropW(hwnd, L"__wine_dcomp_orig_wndproc"); + RemovePropW(hwnd, L"__wine_dcomp_swapchain"); +- RemovePropW(hwnd, L"__wine_dcomp_comp_dc"); +- RemovePropW(hwnd, L"__wine_dcomp_comp_size"); +- RemovePropW(hwnd, L"__wine_dcomp_reblit_tries"); +- dcomp_clear_null_device_props(hwnd); + return result; + } + } +@@ -807,6 +866,10 @@ static LRESULT CALLBACK dcomp_target_wndproc(HWND hwnd, UINT msg, WPARAM wparam, + return 0; + } + ++ case WM_WINE_DCOMP_TEARDOWN: ++ dcomp_teardown_window(hwnd); ++ return 0; ++ + case WM_TIMER: + if (wparam == DCOMP_REBLIT_TIMER_ID) + { +@@ -968,25 +1031,14 @@ static LRESULT CALLBACK dcomp_target_wndproc(HWND hwnd, UINT msg, WPARAM wparam, + { + LRESULT result; + IDXGISwapChain4 *sc = (IDXGISwapChain4 *)GetPropW(hwnd, L"__wine_dcomp_swapchain"); +- InterlockedDecrement(&dcomp_subclassed_target_count); +- dcomp_update_active_prop(); +- dcomp_popup_stack_remove(hwnd); +- KillTimer(hwnd, DCOMP_REBLIT_TIMER_ID); +- KillTimer(hwnd, DCOMP_RESIZE_REBLIT_TIMER_ID); + /* Window dies first: clear the back-pointer so a later + * d3d11_swapchain_Release won't touch this (recycled) HWND. */ + if (sc) + d3d11_swapchain_from_IDXGISwapChain4(sc)->target_hwnd = NULL; +- dcomp_restore_parent_clip(hwnd); ++ dcomp_teardown_window(hwnd); + result = orig ? CallWindowProcW(orig, hwnd, msg, wparam, lparam) + : DefWindowProcW(hwnd, msg, wparam, lparam); +- RemovePropW(hwnd, L"__wine_dcomp_orig_wndproc"); + RemovePropW(hwnd, L"__wine_dcomp_swapchain"); +- RemovePropW(hwnd, L"__wine_dcomp_comp_dc"); +- RemovePropW(hwnd, L"__wine_dcomp_comp_size"); +- RemovePropW(hwnd, L"__wine_dcomp_last_present"); +- RemovePropW(hwnd, L"__wine_dcomp_reblit_tries"); +- dcomp_clear_null_device_props(hwnd); + return result; + } + } +@@ -1003,6 +1055,9 @@ static LRESULT CALLBACK dcomp_swapchain_wndproc(HWND hwnd, UINT msg, WPARAM wpar + return 1; + case WM_NCHITTEST: + return HTTRANSPARENT; ++ case WM_WINE_DCOMP_TEARDOWN: ++ DestroyWindow(hwnd); ++ return 0; + case WM_PAINT: + { + PAINTSTRUCT ps; +@@ -1178,6 +1233,7 @@ static LRESULT CALLBACK dcomp_swapchain_wndproc(HWND hwnd, UINT msg, WPARAM wpar + { + SetPropW(target_hwnd, L"__wine_dcomp_orig_wndproc", (HANDLE)orig); + SetPropW(target_hwnd, L"__wine_dcomp_swapchain", (HANDLE)iface); ++ SetPropW(target_hwnd, L"__wine_dcomp_subclassed", (HANDLE)1); + /* Anchor transient_for at the previously-opened target + * (read by winex11 set_style_hints) instead of + * get_active_window → nested hierarchy, no main-window pull. */ +@@ -1204,6 +1260,7 @@ static LRESULT CALLBACK dcomp_swapchain_wndproc(HWND hwnd, UINT msg, WPARAM wpar + { + SetPropW(target_hwnd, L"__wine_dcomp_orig_wndproc", (HANDLE)orig); + SetPropW(target_hwnd, L"__wine_dcomp_swapchain", (HANDLE)iface); ++ SetPropW(target_hwnd, L"__wine_dcomp_subclassed", (HANDLE)1); + SetTimer(target_hwnd, DCOMP_REBLIT_TIMER_ID, 200, NULL); + InterlockedIncrement(&dcomp_subclassed_target_count); + dcomp_update_active_prop(); +diff --git a/dlls/dxgi/swapchain.c b/dlls/dxgi/swapchain.c +index e768bed..93d7d98 100644 +--- a/dlls/dxgi/swapchain.c ++++ b/dlls/dxgi/swapchain.c +@@ -28,6 +28,8 @@ WINE_DEFAULT_DEBUG_CHANNEL(dxgi); + WINE_DECLARE_DEBUG_CHANNEL(winediag); + WINE_DECLARE_DEBUG_CHANNEL(wmtrace); + ++#define WM_WINE_DCOMP_TEARDOWN (WM_USER + 0x102) ++ + static UINT dxgi_get_display_refresh_rate(void) + { + DEVMODEW mode; +@@ -313,20 +315,37 @@ static ULONG STDMETHODCALLTYPE d3d11_swapchain_Release(IDXGISwapChain4 *iface) + HWND t = swapchain->target_hwnd; + + RemovePropW(t, L"__wine_dcomp_swapchain"); +- if (IsWindow(t) && GetWindowThreadProcessId(t, NULL) == GetCurrentThreadId()) ++ if (dcomp_teardown_legacy()) ++ { ++ /* Pre-0094 teardown: window-owning cleanup only on the owning ++ * thread; a cross-thread release leaked the timers, the ++ * subclass, the popup-stack entry and the active count. */ ++ if (IsWindow(t) && GetWindowThreadProcessId(t, NULL) == GetCurrentThreadId()) ++ { ++ WNDPROC orig = (WNDPROC)GetPropW(t, L"__wine_dcomp_orig_wndproc"); ++ ++ KillTimer(t, DCOMP_REBLIT_TIMER_ID); ++ KillTimer(t, DCOMP_POPUP_REBLIT_TIMER_ID); ++ KillTimer(t, DCOMP_RESIZE_REBLIT_TIMER_ID); ++ if (orig) ++ SetWindowLongPtrW(t, GWLP_WNDPROC, (LONG_PTR)orig); ++ RemovePropW(t, L"__wine_dcomp_orig_wndproc"); ++ RemovePropW(t, L"__wine_dcomp_comp_dc"); ++ RemovePropW(t, L"__wine_dcomp_comp_size"); ++ RemovePropW(t, L"__wine_dcomp_comp_bits"); ++ InterlockedDecrement(&dcomp_subclassed_target_count); ++ } ++ } ++ else if (IsWindow(t)) + { +- WNDPROC orig = (WNDPROC)GetPropW(t, L"__wine_dcomp_orig_wndproc"); +- +- KillTimer(t, DCOMP_REBLIT_TIMER_ID); +- KillTimer(t, DCOMP_POPUP_REBLIT_TIMER_ID); +- KillTimer(t, DCOMP_RESIZE_REBLIT_TIMER_ID); +- if (orig) +- SetWindowLongPtrW(t, GWLP_WNDPROC, (LONG_PTR)orig); +- RemovePropW(t, L"__wine_dcomp_orig_wndproc"); +- RemovePropW(t, L"__wine_dcomp_comp_dc"); +- RemovePropW(t, L"__wine_dcomp_comp_size"); +- RemovePropW(t, L"__wine_dcomp_comp_bits"); +- InterlockedDecrement(&dcomp_subclassed_target_count); ++ /* Complete teardown on the window's owning thread: directly ++ * when that is us, marshalled through the subclass proc ++ * otherwise (KillTimer and SetWindowLongPtrW are ++ * thread-affine). */ ++ if (GetWindowThreadProcessId(t, NULL) == GetCurrentThreadId()) ++ dcomp_teardown_window(t); ++ else if (!PostMessageW(t, WM_WINE_DCOMP_TEARDOWN, 0, 0)) ++ WARN("Could not marshal DComp teardown to the thread of %p.\n", t); + } + swapchain->target_hwnd = NULL; + } +@@ -338,9 +357,15 @@ static ULONG STDMETHODCALLTYPE d3d11_swapchain_Release(IDXGISwapChain4 *iface) + L"__wine_dcomp_wnd_%I64x", (UINT_PTR)iface); + RemovePropW(GetDesktopWindow(), prop_name); + RemovePropW(swapchain->comp_wnd, L"__wine_dcomp_swapchain"); +- if (IsWindow(swapchain->comp_wnd) +- && GetWindowThreadProcessId(swapchain->comp_wnd, NULL) == GetCurrentThreadId()) +- DestroyWindow(swapchain->comp_wnd); ++ if (IsWindow(swapchain->comp_wnd)) ++ { ++ if (GetWindowThreadProcessId(swapchain->comp_wnd, NULL) == GetCurrentThreadId()) ++ DestroyWindow(swapchain->comp_wnd); ++ else if (!dcomp_teardown_legacy() ++ && !PostMessageW(swapchain->comp_wnd, WM_WINE_DCOMP_TEARDOWN, 0, 0)) ++ WARN("Could not marshal comp-window destroy to the thread of %p.\n", ++ swapchain->comp_wnd); ++ } + swapchain->comp_wnd = NULL; + } + IWineDXGIFactory_Release(swapchain->factory); diff --git a/patches/0095-dxgi-reference-count-the-parent-WS_CLIPCHILDREN-bit.patch b/patches/0095-dxgi-reference-count-the-parent-WS_CLIPCHILDREN-bit.patch new file mode 100644 index 00000000..c418cd55 --- /dev/null +++ b/patches/0095-dxgi-reference-count-the-parent-WS_CLIPCHILDREN-bit.patch @@ -0,0 +1,111 @@ +Subject: dxgi: reference-count the parent WS_CLIPCHILDREN bit + +Two composition targets sharing one parent recorded the added +WS_CLIPCHILDREN bit only on the first: the parent already carried the +bit when the second arrived, so nothing was recorded for it, and the +first teardown stripped the style from under the surviving target, +letting the parent paint over it. + +Count owners in a __wine_dcomp_clip_refs property on the parent and +restore the style only when the last recorded target lets go. The count +follows the target, not the message. The composition window is keyed on +the swapchain, so a client that recreates its swapchain rebinds the same +target through a new one, and SET_TARGET can run more than once for a +single target. Counting each message would then raise the count without +adding an owner that can ever release it. A target that moves to another +parent releases the old one first. +WINE_DCOMP_TEARDOWN=legacy keeps the old single-owner behaviour +together with the rest of the legacy teardown path. + +diff --git a/dlls/dxgi/factory.c b/dlls/dxgi/factory.c +index 07eca32..476c1eb 100644 +--- a/dlls/dxgi/factory.c ++++ b/dlls/dxgi/factory.c +@@ -635,18 +635,65 @@ static void dcomp_popup_stack_remove(HWND hwnd) + /* Restore a parent's style if we OR-ed in WS_CLIPCHILDREN for this target + * (see WM_WINE_DCOMP_SET_TARGET). Keyed off the __wine_dcomp_parent_clip prop, + * which is set only when we actually added the bit, so a parent that already had +- * WS_CLIPCHILDREN is left untouched. Avoids a permanent host-window style change. */ ++ * WS_CLIPCHILDREN is left untouched. The bit is reference-counted on the ++ * parent (__wine_dcomp_clip_refs): two targets sharing one parent used to ++ * record only the first, and the first teardown stripped the bit from under ++ * the survivor. The style is only restored when the last recorded target ++ * lets go. Runs on the targets' UI thread like the popup stack; a shared ++ * parent means the same plugin UI thread. */ + static void dcomp_restore_parent_clip(HWND hwnd) + { + HWND phwnd = (HWND)GetPropW(hwnd, L"__wine_dcomp_parent_clip"); + + if (phwnd) + { +- LONG pstyle = GetWindowLongW(phwnd, GWL_STYLE); +- if (pstyle & WS_CLIPCHILDREN) +- SetWindowLongW(phwnd, GWL_STYLE, pstyle & ~WS_CLIPCHILDREN); ++ LONG_PTR refs = (LONG_PTR)GetPropW(phwnd, L"__wine_dcomp_clip_refs"); ++ + RemovePropW(hwnd, L"__wine_dcomp_parent_clip"); ++ if (!dcomp_teardown_legacy() && refs > 1) ++ { ++ SetPropW(phwnd, L"__wine_dcomp_clip_refs", (HANDLE)(refs - 1)); ++ } ++ else ++ { ++ LONG pstyle = GetWindowLongW(phwnd, GWL_STYLE); ++ RemovePropW(phwnd, L"__wine_dcomp_clip_refs"); ++ if (pstyle & WS_CLIPCHILDREN) ++ SetWindowLongW(phwnd, GWL_STYLE, pstyle & ~WS_CLIPCHILDREN); ++ } ++ } ++} ++ ++/* Take a reference on the parent's WS_CLIPCHILDREN bit for this target, adding ++ * the bit if we are its first owner. The count follows the target, not the ++ * message: the composition window is keyed on the swapchain, so a client that ++ * recreates its swapchain rebinds the same target through a new one and ++ * SET_TARGET can run twice for one target. Counting each message would raise ++ * the count without adding an owner that can ever release it, and the parent ++ * would keep a bit outliving its last target, clipping its remaining children ++ * out of its own painting. A target that moved parent releases the old one. */ ++static void dcomp_take_parent_clip(HWND target_hwnd, HWND phwnd) ++{ ++ HWND held = (HWND)GetPropW(target_hwnd, L"__wine_dcomp_parent_clip"); ++ LONG_PTR refs; ++ LONG pstyle; ++ ++ if (held == phwnd) return; ++ if (held) dcomp_restore_parent_clip(target_hwnd); ++ ++ refs = (LONG_PTR)GetPropW(phwnd, L"__wine_dcomp_clip_refs"); ++ pstyle = GetWindowLongW(phwnd, GWL_STYLE); ++ ++ if (!dcomp_teardown_legacy() && refs > 0) ++ SetPropW(phwnd, L"__wine_dcomp_clip_refs", (HANDLE)(refs + 1)); ++ else if (!(pstyle & WS_CLIPCHILDREN)) ++ { ++ SetWindowLongW(phwnd, GWL_STYLE, pstyle | WS_CLIPCHILDREN); ++ SetPropW(phwnd, L"__wine_dcomp_clip_refs", (HANDLE)1); + } ++ else return; /* the parent carries the bit itself, leave it alone */ ++ ++ SetPropW(target_hwnd, L"__wine_dcomp_parent_clip", (HANDLE)phwnd); + } + + static LRESULT CALLBACK dcomp_popup_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); +@@ -1289,15 +1336,8 @@ static LRESULT CALLBACK dcomp_swapchain_wndproc(HWND hwnd, UINT msg, WPARAM wpar + * teardown (avoids a permanent host-window style change). */ + { + HWND phwnd = GetParent(target_hwnd); +- if (phwnd) +- { +- LONG pstyle = GetWindowLongW(phwnd, GWL_STYLE); +- if (!(pstyle & WS_CLIPCHILDREN)) +- { +- SetWindowLongW(phwnd, GWL_STYLE, pstyle | WS_CLIPCHILDREN); +- SetPropW(target_hwnd, L"__wine_dcomp_parent_clip", (HANDLE)phwnd); +- } +- } ++ ++ if (phwnd) dcomp_take_parent_clip(target_hwnd, phwnd); + } + ValidateRect(target_hwnd, NULL); + diff --git a/patches/0096-dxgi-only-remove-the-desktop-swapchain-property-if-w.patch b/patches/0096-dxgi-only-remove-the-desktop-swapchain-property-if-w.patch new file mode 100644 index 00000000..cc0bd998 --- /dev/null +++ b/patches/0096-dxgi-only-remove-the-desktop-swapchain-property-if-w.patch @@ -0,0 +1,29 @@ +Subject: dxgi: only remove the desktop swapchain property if we still own it + +The property key is the swapchain object's address; the heap can hand +the same address to a swapchain created after ours was released. +Removing the key unconditionally then strands the live window: the +dcomp lookup finds nothing and its content never reaches the screen. + +Ported from giang17/wine 81e8d12e. + +diff --git a/dlls/dxgi/swapchain.c b/dlls/dxgi/swapchain.c +index 93d7d98..ca496b8 100644 +--- a/dlls/dxgi/swapchain.c ++++ b/dlls/dxgi/swapchain.c +@@ -355,7 +355,14 @@ static ULONG STDMETHODCALLTYPE d3d11_swapchain_Release(IDXGISwapChain4 *iface) + + swprintf(prop_name, ARRAY_SIZE(prop_name), + L"__wine_dcomp_wnd_%I64x", (UINT_PTR)iface); +- RemovePropW(GetDesktopWindow(), prop_name); ++ /* Only drop the mapping if it still points at our own window. The ++ * key is this object's address, and the heap hands the same address ++ * out again once we are gone: a swapchain created after ours was ++ * released can already own the key by the time this runs, and ++ * removing it then strands a live window - the lookup in dcomp ++ * finds nothing and its content never reaches the screen. */ ++ if (GetPropW(GetDesktopWindow(), prop_name) == (HANDLE)swapchain->comp_wnd) ++ RemovePropW(GetDesktopWindow(), prop_name); + RemovePropW(swapchain->comp_wnd, L"__wine_dcomp_swapchain"); + if (IsWindow(swapchain->comp_wnd)) + { diff --git a/patches/0097-dxgi-dcomp-namespace-the-composition-swapchain-windo.patch b/patches/0097-dxgi-dcomp-namespace-the-composition-swapchain-windo.patch new file mode 100644 index 00000000..ed4abf82 --- /dev/null +++ b/patches/0097-dxgi-dcomp-namespace-the-composition-swapchain-windo.patch @@ -0,0 +1,122 @@ +Subject: dxgi/dcomp: namespace the composition-swapchain window property by pid + +The composition swapchain to composition window mapping is published as +a property on the desktop window, keyed by the swapchain object's +address. The desktop window's property list is shared across the whole +window station, so two processes that allocate a swapchain at the same +address (a host app and its WebView2 child, Live's exact shape) collide +on the key, and a lookup in one resolves another process's window. + +Key the property by the owning process id together with the address. +The format string lives once in include/wine/winedxgi.idl so the dxgi +setter and the dcomp getter cannot drift apart silently. + +Ported from giang17/wine df092626; six of its seven sites exist in this +tree, the seventh belongs to the fork's tree compositor. + +diff --git a/dlls/dcomp/device.c b/dlls/dcomp/device.c +index 059b3de..88d1adb 100644 +--- a/dlls/dcomp/device.c ++++ b/dlls/dcomp/device.c +@@ -32,6 +32,7 @@ + #include "d2d1_1.h" + #include "dcomp.h" + #include "wine/debug.h" ++#include "wine/winedxgi.h" + + WINE_DEFAULT_DEBUG_CHANNEL(dcomp); + +@@ -98,7 +99,7 @@ static void dcomp_send_child_mode(IUnknown *content) + return; + + swprintf(prop_name, ARRAY_SIZE(prop_name), +- L"__wine_dcomp_wnd_%I64x", (UINT_PTR)content); ++ WINE_DCOMP_WND_PROP_FMT, GetCurrentProcessId(), (UINT_PTR)content); + comp_wnd = (HWND)GetPropW(GetDesktopWindow(), prop_name); + if (comp_wnd) + { +@@ -1378,13 +1379,13 @@ static void dcomp_visual_try_reparent(struct dcomp_visual *visual) + } + + swprintf(prop_name, ARRAY_SIZE(prop_name), +- L"__wine_dcomp_wnd_%I64x", (UINT_PTR)visual->content); ++ WINE_DCOMP_WND_PROP_FMT, GetCurrentProcessId(), (UINT_PTR)visual->content); + comp_wnd = (HWND)GetPropW(GetDesktopWindow(), prop_name); + + if (!comp_wnd) + { +- FIXME("Composition window NOT FOUND for content %p (prop: __wine_dcomp_wnd_%I64x).\n", +- visual->content, (UINT_PTR)visual->content); ++ FIXME("Composition window NOT FOUND for content %p (prop: %s).\n", ++ visual->content, debugstr_w(prop_name)); + return; + } + +@@ -1721,7 +1722,7 @@ static void dcomp_commit_visual_tree(HWND target_hwnd, struct dcomp_visual *root + /* Swapchain content: look up the child's composition window */ + /* Look up the child's composition window (same mechanism as try_reparent) */ + swprintf(prop_name, ARRAY_SIZE(prop_name), +- L"__wine_dcomp_wnd_%I64x", (UINT_PTR)child->content); ++ WINE_DCOMP_WND_PROP_FMT, GetCurrentProcessId(), (UINT_PTR)child->content); + child_comp_wnd = (HWND)GetPropW(GetDesktopWindow(), prop_name); + if (!child_comp_wnd) + continue; +diff --git a/dlls/dxgi/factory.c b/dlls/dxgi/factory.c +index 41b867c..7e28076 100644 +--- a/dlls/dxgi/factory.c ++++ b/dlls/dxgi/factory.c +@@ -797,7 +797,7 @@ static LRESULT CALLBACK dcomp_popup_wndproc(HWND hwnd, UINT msg, WPARAM wparam, + swapchain->target_hwnd = NULL; + + swprintf(prop_name, ARRAY_SIZE(prop_name), +- L"__wine_dcomp_wnd_%I64x", (UINT_PTR)sc); ++ WINE_DCOMP_WND_PROP_FMT, GetCurrentProcessId(), (UINT_PTR)sc); + comp_wnd = (HWND)GetPropW(GetDesktopWindow(), prop_name); + + wined3d_swapchain_set_prefer_gl_present(swapchain->wined3d_swapchain, FALSE); +@@ -1419,7 +1419,7 @@ static HRESULT STDMETHODCALLTYPE dxgi_factory_CreateSwapChainForComposition(IWin + if (SUCCEEDED(hr)) + { + swprintf(prop_name, ARRAY_SIZE(prop_name), +- L"__wine_dcomp_wnd_%I64x", (UINT_PTR)*swapchain); ++ WINE_DCOMP_WND_PROP_FMT, GetCurrentProcessId(), (UINT_PTR)*swapchain); + SetPropW(GetDesktopWindow(), prop_name, (HANDLE)window); + /* Store back-reference for WM_WINE_DCOMP_SET_TARGET handler */ + SetPropW(window, L"__wine_dcomp_swapchain", (HANDLE)*swapchain); +diff --git a/dlls/dxgi/swapchain.c b/dlls/dxgi/swapchain.c +index ca496b8..a180e16 100644 +--- a/dlls/dxgi/swapchain.c ++++ b/dlls/dxgi/swapchain.c +@@ -354,7 +354,7 @@ static ULONG STDMETHODCALLTYPE d3d11_swapchain_Release(IDXGISwapChain4 *iface) + WCHAR prop_name[64]; + + swprintf(prop_name, ARRAY_SIZE(prop_name), +- L"__wine_dcomp_wnd_%I64x", (UINT_PTR)iface); ++ WINE_DCOMP_WND_PROP_FMT, GetCurrentProcessId(), (UINT_PTR)iface); + /* Only drop the mapping if it still points at our own window. The + * key is this object's address, and the heap hands the same address + * out again once we are gone: a swapchain created after ours was +diff --git a/include/wine/winedxgi.idl b/include/wine/winedxgi.idl +index fff00de..d59013e 100644 +--- a/include/wine/winedxgi.idl ++++ b/include/wine/winedxgi.idl +@@ -20,6 +20,19 @@ + + import "dxgi1_6.idl"; + ++/* Desktop-property name format mapping a composition swapchain to its ++ * composition window. Shared verbatim by dxgi (setter) and dcomp (getter); ++ * keeping it in one place here is the only guard against the two DLLs' ++ * copies diverging silently. The key is the swapchain pointer together with ++ * the owning process id: the desktop window's property list is shared across ++ * the whole window station, so an address-only key can collide between ++ * processes that happen to allocate a swapchain at the same address (e.g. a ++ * host app and its webview2 child). The process id scopes each process's ++ * address space apart; within a single process the ownership check in ++ * d3d11_swapchain_Release keeps a recycled address from stranding a live ++ * window. */ ++cpp_quote("#define WINE_DCOMP_WND_PROP_FMT L\"__wine_dcomp_wnd_%lu_%I64x\"") ++ + [ + object, + local, diff --git a/patches/0098-dcomp-wined3d-exclude-composition-targets-from-GDI-p.patch b/patches/0098-dcomp-wined3d-exclude-composition-targets-from-GDI-p.patch new file mode 100644 index 00000000..08c25fd5 --- /dev/null +++ b/patches/0098-dcomp-wined3d-exclude-composition-targets-from-GDI-p.patch @@ -0,0 +1,194 @@ +Subject: dcomp/wined3d: exclude composition targets from GDI present blits + +A DComp composition target (WebView2) owns its window area: on Windows +it renders into its own redirection surface and the DWM composes it +above everything else, so no present ever paints there. Our GDI present +blit shares the drawable with all windows of the top level, including +sibling branches. Ableton Live hosts its WebView2 tree next to, not +under, its D3D window, and WebView2 reparents it between top levels at +runtime, so window-hierarchy scans cannot find the target reliably. + +Register target hwnds in desktop-window property slots on +CreateTargetForHwnd and Release, and exclude every live registered +target from the present blit clip region. + +Registry membership is the whole test. The fork also reads +__wine_dcomp_child_count on the candidate; we deliberately do not. +dcomp writes that property only at Commit, and writes 0 when the root +visual has no child visuals, which a reader cannot tell apart from a +property nobody ever set. Reading it left every pane built that way +unexcluded, which is the flicker this patch exists to remove. + +The desktop's property list spans the whole window station, and the +scan-then-claim that picks a slot is not atomic across processes. Live +and a WebView2 or plugin child process can see the same free slot and +both write it, and the loser's registration is lost with no error. A +window property has no atomic claim, so a writer now claims a slot and +reads it back, moving to the next free slot when the claim did not +stick. Sixteen slots still cap the window station rather than one +process. + +Ported from giang17/wine 8e223552 (its issue 121), adapted to this +tree's dcomp target release path. + +diff --git a/dlls/dcomp/device.c b/dlls/dcomp/device.c +index 88d1adb..0377fd3 100644 +--- a/dlls/dcomp/device.c ++++ b/dlls/dcomp/device.c +@@ -1411,6 +1411,63 @@ static void dcomp_visual_try_reparent(struct dcomp_visual *visual) + + /* Property name for storing target pointer on HWND (Phase 5 subclass) */ + static const WCHAR dcomp_target_prop[] = L"__wine_dcomp_target"; ++ ++/* Global composition-target registry on the desktop window, so presenters in ++ * other modules/processes (wined3d) can exclude target areas from their window ++ * blits without knowing the window hierarchy - WebView2 reparents its target ++ * between top levels at runtime (issue 121). Slots stale after window death ++ * are skipped by readers via IsWindow(). */ ++#define DCOMP_TARGET_REGISTRY_SLOTS 16 ++ ++static void dcomp_target_registry_set(HWND hwnd, BOOL add) ++{ ++ HWND desktop = GetDesktopWindow(); ++ WCHAR prop[32]; ++ unsigned int i; ++ int free_slot = -1; ++ ++ for (i = 0; i < DCOMP_TARGET_REGISTRY_SLOTS; ++i) ++ { ++ HWND cur; ++ ++ swprintf(prop, ARRAY_SIZE(prop), L"__wine_dcomp_target_%u", i); ++ cur = (HWND)GetPropW(desktop, prop); ++ if (cur == hwnd) ++ { ++ if (!add) ++ RemovePropW(desktop, prop); ++ return; ++ } ++ if (add && free_slot < 0 && (!cur || !IsWindow(cur))) ++ free_slot = (int)i; ++ } ++ if (!add) return; ++ if (free_slot < 0) ++ { ++ WARN("DComp target registry full, %p not registered.\n", hwnd); ++ return; ++ } ++ ++ /* The desktop's property list spans the whole window station, and this ++ * scan-then-claim is not atomic across processes: Live and a WebView2 or ++ * plugin child process can see the same free slot and both write it, and ++ * the loser's registration is lost with no error, so its pane keeps ++ * being overpainted by GDI presents. A window property has no atomic ++ * claim, so claim optimistically and read back. A slot that no longer ++ * holds us went to somebody else, so move on to the next free one. */ ++ for (i = (unsigned int)free_slot; i < DCOMP_TARGET_REGISTRY_SLOTS; ++i) ++ { ++ HWND cur; ++ ++ swprintf(prop, ARRAY_SIZE(prop), L"__wine_dcomp_target_%u", i); ++ cur = (HWND)GetPropW(desktop, prop); ++ if (cur && cur != hwnd && IsWindow(cur)) continue; /* taken since the scan */ ++ ++ SetPropW(desktop, prop, (HANDLE)hwnd); ++ if ((HWND)GetPropW(desktop, prop) == hwnd) return; /* the claim stuck */ ++ } ++ WARN("DComp target registry full, %p not registered.\n", hwnd); ++} + /* The window's TRUE pre-subclass WndProc. Kept as a window property (not only in + * the target struct) so that (a) a second CreateTargetForHwnd on the same HWND + * never records our own subclass as "original" and (b) the subclass can still +@@ -1497,6 +1554,7 @@ static ULONG STDMETHODCALLTYPE dcomp_target_Release(IDCompositionTarget *iface) + (struct dcomp_target *)GetPropW(target->hwnd, dcomp_target_prop) == target) + { + RemovePropW(target->hwnd, dcomp_target_prop); ++ dcomp_target_registry_set(target->hwnd, FALSE); + if ((WNDPROC)GetWindowLongPtrW(target->hwnd, GWLP_WNDPROC) == dcomp_target_wndproc) + { + WNDPROC orig = (WNDPROC)GetPropW(target->hwnd, dcomp_origproc_prop); +@@ -2272,6 +2330,7 @@ static HRESULT STDMETHODCALLTYPE dcomp_device_CreateTargetForHwnd(IDCompositionD + } + } + SetPropW(hwnd, dcomp_target_prop, (HANDLE)object); ++ dcomp_target_registry_set(hwnd, TRUE); + + FIXME("Created composition target %p for hwnd %p (subclassed, orig_wndproc %p).\n", + object, hwnd, object->orig_wndproc); +diff --git a/dlls/wined3d/swapchain.c b/dlls/wined3d/swapchain.c +index 869fe45..92f27dd 100644 +--- a/dlls/wined3d/swapchain.c ++++ b/dlls/wined3d/swapchain.c +@@ -28,6 +28,34 @@ WINE_DEFAULT_DEBUG_CHANNEL(d3d); + WINE_DECLARE_DEBUG_CHANNEL(d3d_perf); + WINE_DECLARE_DEBUG_CHANNEL(winediag); + ++struct dcomp_child_exclude_ctx ++{ ++ HDC dc; ++ HWND parent; ++ unsigned int excluded; ++}; ++ ++static BOOL CALLBACK wined3d_exclude_dcomp_children_proc(HWND child, LPARAM lp) ++{ ++ struct dcomp_child_exclude_ctx *ctx = (struct dcomp_child_exclude_ctx *)lp; ++ RECT r; ++ ++ /* Registry membership already proves this window is a live composition ++ * target, so do not ask for __wine_dcomp_child_count as well. dcomp writes ++ * that property only at Commit, and writes 0 when the root visual has no ++ * child visuals; a reader cannot tell that 0 apart from a property nobody ++ * ever set. Asking for it left every pane built that way unexcluded, ++ * which is the flicker this exists to remove. (The fork's 8e223552 still ++ * asks; we deliberately do not.) */ ++ if (IsWindowVisible(child) && GetWindowRect(child, &r)) ++ { ++ MapWindowPoints(NULL, ctx->parent, (POINT *)&r, 2); ++ ExcludeClipRect(ctx->dc, r.left, r.top, r.right, r.bottom); ++ ctx->excluded++; ++ } ++ return TRUE; ++} ++ + static BOOL set_window_present_rect(HWND hwnd, UINT x, UINT y, UINT width, UINT height) + { + RECT rect = {x, y, x + width, y + height}; +@@ -1122,10 +1150,37 @@ static void swapchain_blit_gdi(struct wined3d_swapchain *swapchain, + } + else + { ++ /* A DComp composition target (WebView2) owns its window area: on ++ * Windows it renders into its own redirection surface and the DWM ++ * composes it above everything else, so no present ever paints ++ * there. Our GDI blit shares the drawable with all windows of the ++ * top level, including sibling branches (Ableton Live hosts the ++ * WebView2 tree next to, not under, its D3D window, and WebView2 ++ * reparents it at runtime). Exclude every target from the dcomp ++ * desktop-window registry, or this present overpaints its content ++ * at 60 Hz while the dcomp tree timer paints it back at 10 Hz: ++ * visible flicker (issue 121). Rects outside our window map to ++ * empty clip areas and are harmless. */ ++ struct dcomp_child_exclude_ctx ctx = { swapchain->dc, swapchain->win_handle, 0 }; ++ HWND desktop_wnd = GetDesktopWindow(); ++ WCHAR treg[32]; ++ unsigned int ti; ++ ++ for (ti = 0; ti < 16; ++ti) ++ { ++ HWND t; ++ ++ swprintf(treg, ARRAY_SIZE(treg), L"__wine_dcomp_target_%u", ti); ++ t = (HWND)GetPropW(desktop_wnd, treg); ++ if (t && t != swapchain->win_handle && IsWindow(t)) ++ wined3d_exclude_dcomp_children_proc(t, (LPARAM)&ctx); ++ } + /* Always BitBlt full comp buffer to window — survives any surface reset. */ + if (!BitBlt(swapchain->dc, dst_rect->left, dst_rect->top, dst_w, dst_h, + swapchain->comp_dc, 0, 0, SRCCOPY)) + WARN("Failed to blit composition buffer to window.\n"); ++ if (ctx.excluded) ++ SelectClipRgn(swapchain->dc, NULL); + } + + /* Store comp_dc as window property so WM_PAINT can re-blit. Don't diff --git a/patches/0099-wined3d-only-exclude-dcomp-targets-that-share-our-to.patch b/patches/0099-wined3d-only-exclude-dcomp-targets-that-share-our-to.patch new file mode 100644 index 00000000..a319b48a --- /dev/null +++ b/patches/0099-wined3d-only-exclude-dcomp-targets-that-share-our-to.patch @@ -0,0 +1,48 @@ +Subject: wined3d: only exclude dcomp targets that share our top level (issue 145) + +A present excludes every window listed in the dcomp target registry from its +clip, so that it does not overpaint a composition target at 60 Hz while the +tree timer paints it back at 10 Hz. That is right for targets living in our +own top level, whose windows all share its drawable. + +The registry lives on the desktop window and is therefore desktop-wide: it +also lists targets of entirely different top levels. Their rects are not +"outside our window" as the comment assumed -- MapWindowPoints puts them +wherever the two windows happen to overlap on screen, and ExcludeClipRect then +punches that overlap out of our own present. + +A plugin editor dragged over Ableton Live's WebView2 panel loses its content in +exactly the intersection, down to the pixel, and keeps the hole: the visible +region stays complete, so nothing looks wrong from the window's own side, and +closing the panel does not help because the target stays registered. Resizing +repaints through another path and hides it until the next time. Seen on a +Vintage Prom 3 editor and reproduced with a JUCE test plugin. + +Only windows sharing our top level share our drawable, so ask for that. The +flicker fix keeps working: same top level, same GA_ROOT, same exclusion as +before. + +Ported from giang17/wine c2cf448f on top of the 0098 registry port. + +diff --git a/dlls/wined3d/swapchain.c b/dlls/wined3d/swapchain.c +index fc59728..59abbc6 100644 +--- a/dlls/wined3d/swapchain.c ++++ b/dlls/wined3d/swapchain.c +@@ -1166,7 +1166,16 @@ static void swapchain_blit_gdi(struct wined3d_swapchain *swapchain, + + swprintf(treg, ARRAY_SIZE(treg), L"__wine_dcomp_target_%u", ti); + t = (HWND)GetPropW(desktop_wnd, treg); +- if (t && t != swapchain->win_handle && IsWindow(t)) ++ /* Only targets that share our top level share our drawable. ++ * The registry is desktop-wide, so it also lists targets of ++ * other top levels - a plugin editor over Live's WebView2 ++ * panel, say. Their rects are not "outside our window": they ++ * map to wherever the two windows overlap on screen, and ++ * excluding them punches that overlap out of our present. The ++ * hole then survives, because nothing invalidates us when the ++ * other top level changes. */ ++ if (t && t != swapchain->win_handle && IsWindow(t) ++ && GetAncestor(t, GA_ROOT) == GetAncestor(swapchain->win_handle, GA_ROOT)) + wined3d_exclude_dcomp_children_proc(t, (LPARAM)&ctx); + } + /* Always BitBlt full comp buffer to window — survives any surface reset. */ diff --git a/patches/0100-dxgi-winex11-scope-DComp-popup-inference-by-thread-a.patch b/patches/0100-dxgi-winex11-scope-DComp-popup-inference-by-thread-a.patch new file mode 100644 index 00000000..6193ddbc --- /dev/null +++ b/patches/0100-dxgi-winex11-scope-DComp-popup-inference-by-thread-a.patch @@ -0,0 +1,291 @@ +Subject: dxgi/winex11: scope DComp popup inference by thread and process + +The popup transient anchor and the full-versus-popup mode choice used +one process-global stack and count: a second plugin's first target +became a popup of the first plugin (losing its present timer), and its +popups could anchor to the wrong plugin's window. Both now key on the +target's UI thread; a plugin creates its popups on its own thread, so +thread affinity relates a popup to its plugin. + +The desktop-wide __wine_dcomp_active flag becomes per-process +(__wine_dcomp_active_): the desktop property list spans the window +station, so the session-wide flag gave unrelated processes' ownerless +tool popups a menu role while any DComp GUI existed anywhere, the +undismissable-dialog class of faults. In-process behaviour is +unchanged. + +The thread keying rests on two more things, so this patch fixes both. +Every plugin pushes and removes on its own UI thread, so a lock now +moves the stack, its new thread-id array and the depth together. Without +one, two plugins opening editors at the same moment tear the arrays +apart and a popup anchors to an unrelated plugin's window. + +A repeat SET_TARGET also stops subclassing a target twice. +SetWindowLongPtrW would hand back our own proc as the original, our proc +would then forward to itself on every message, and teardown would +install it as the window's own wndproc with the application's proc lost. +That second install also inflated the subclassed count and the popup +stack, which teardown unwinds only once. + +WINE_DCOMP_SCOPE=global restores the process-global stack, count and +session-wide flag for comparison. + +diff --git a/dlls/dxgi/factory.c b/dlls/dxgi/factory.c +index 9134a50..e338edd 100644 +--- a/dlls/dxgi/factory.c ++++ b/dlls/dxgi/factory.c +@@ -580,13 +580,39 @@ LONG dcomp_subclassed_target_count; + * IFX list, embedded). This flag lets winex11 mark them DROPDOWN_MENU instead. + * Set while count > 0, removed at 0 (SetProp with value 0 is not distinguishable + * from "unset" — see the SetPropW (HANDLE)1/2 lesson). */ ++/* WINE_DCOMP_SCOPE=global restores the session-wide active flag and the ++ * process-global popup anchoring for comparison. */ ++static BOOL dcomp_scope_global(void) ++{ ++ static int global = -1; ++ ++ if (global == -1) ++ { ++ WCHAR val[16] = {0}; ++ global = GetEnvironmentVariableW(L"WINE_DCOMP_SCOPE", val, ARRAY_SIZE(val)) ++ && !wcscmp(val, L"global"); ++ } ++ return global; ++} ++ ++static void dcomp_active_prop_name(WCHAR *name, SIZE_T count) ++{ ++ if (dcomp_scope_global()) ++ swprintf(name, count, L"__wine_dcomp_active"); ++ else ++ swprintf(name, count, L"__wine_dcomp_active_%u", GetCurrentProcessId()); ++} ++ + static void dcomp_update_active_prop(void) + { ++ WCHAR name[64]; + LONG count = dcomp_subclassed_target_count; ++ ++ dcomp_active_prop_name(name, ARRAY_SIZE(name)); + if (count > 0) +- SetPropW(GetDesktopWindow(), L"__wine_dcomp_active", (HANDLE)(LONG_PTR)count); ++ SetPropW(GetDesktopWindow(), name, (HANDLE)(LONG_PTR)count); + else +- RemovePropW(GetDesktopWindow(), L"__wine_dcomp_active"); ++ RemovePropW(GetDesktopWindow(), name); + } + + /* Popup transient-parent stack: the open DComp targets in open order +@@ -601,35 +627,113 @@ static void dcomp_update_active_prop(void) + * Global for now (shared across plugins); per-plugin scoping ties into #11. */ + #define DCOMP_POPUP_STACK_MAX 16 + static HWND dcomp_popup_stack[DCOMP_POPUP_STACK_MAX]; ++static DWORD dcomp_popup_stack_tid[DCOMP_POPUP_STACK_MAX]; + static int dcomp_popup_stack_depth; + +-static HWND dcomp_popup_stack_top(void) ++/* The stack, its thread-id array and the depth are process-global, and every ++ * plugin pushes and removes from its own UI thread, so the three have to move ++ * together under a lock. Two plugins opening or closing editors at the same ++ * moment would otherwise tear the arrays apart and anchor a popup to an ++ * unrelated plugin's window. */ ++static CRITICAL_SECTION dcomp_popup_stack_cs; ++static CRITICAL_SECTION_DEBUG dcomp_popup_stack_cs_debug = ++{ ++ 0, 0, &dcomp_popup_stack_cs, ++ { &dcomp_popup_stack_cs_debug.ProcessLocksList, &dcomp_popup_stack_cs_debug.ProcessLocksList }, ++ 0, 0, { (DWORD_PTR)(__FILE__ ": dcomp_popup_stack_cs") } ++}; ++static CRITICAL_SECTION dcomp_popup_stack_cs = { &dcomp_popup_stack_cs_debug, -1, 0, 0, 0, 0 }; ++ ++/* Transient anchor for a new target: the previously-opened target on the SAME ++ * UI thread. A plugin creates its popups on its own thread, so thread ++ * affinity relates a popup to its plugin; the old process-global top could ++ * hand plugin B's popup an anchor belonging to plugin A. ++ * WINE_DCOMP_SCOPE=global restores the plain top-of-stack. */ ++static HWND dcomp_popup_stack_top_for(HWND target) ++{ ++ HWND ret = NULL; ++ DWORD tid; ++ int i; ++ ++ EnterCriticalSection(&dcomp_popup_stack_cs); ++ if (dcomp_scope_global()) ++ { ++ if (dcomp_popup_stack_depth > 0) ret = dcomp_popup_stack[dcomp_popup_stack_depth - 1]; ++ } ++ else ++ { ++ tid = GetWindowThreadProcessId(target, NULL); ++ for (i = dcomp_popup_stack_depth - 1; i >= 0; --i) ++ { ++ if (dcomp_popup_stack_tid[i] == tid && IsWindow(dcomp_popup_stack[i])) ++ { ++ ret = dcomp_popup_stack[i]; ++ break; ++ } ++ } ++ } ++ LeaveCriticalSection(&dcomp_popup_stack_cs); ++ return ret; ++} ++ ++/* Full-versus-popup mode: the first target on its own UI thread is that ++ * plugin's main window; later targets on the thread are its popups. The old ++ * process-global count made the first target of a SECOND plugin a popup, ++ * costing it the present timer. */ ++static BOOL dcomp_thread_has_target(HWND target) + { +- return dcomp_popup_stack_depth > 0 ? dcomp_popup_stack[dcomp_popup_stack_depth - 1] : NULL; ++ BOOL ret = FALSE; ++ DWORD tid; ++ int i; ++ ++ if (dcomp_scope_global()) ++ return dcomp_subclassed_target_count > 0; ++ ++ tid = GetWindowThreadProcessId(target, NULL); ++ EnterCriticalSection(&dcomp_popup_stack_cs); ++ for (i = 0; i < dcomp_popup_stack_depth; ++i) ++ { ++ if (dcomp_popup_stack_tid[i] == tid && IsWindow(dcomp_popup_stack[i])) ++ { ++ ret = TRUE; ++ break; ++ } ++ } ++ LeaveCriticalSection(&dcomp_popup_stack_cs); ++ return ret; + } + + static void dcomp_popup_stack_push(HWND hwnd) + { ++ EnterCriticalSection(&dcomp_popup_stack_cs); + if (dcomp_popup_stack_depth < DCOMP_POPUP_STACK_MAX) ++ { ++ dcomp_popup_stack_tid[dcomp_popup_stack_depth] = GetWindowThreadProcessId(hwnd, NULL); + dcomp_popup_stack[dcomp_popup_stack_depth++] = hwnd; ++ } + else + WARN("DComp popup stack full (%d), not pushing %p.\n", dcomp_popup_stack_depth, hwnd); ++ LeaveCriticalSection(&dcomp_popup_stack_cs); + } + + static void dcomp_popup_stack_remove(HWND hwnd) + { + int i; + ++ EnterCriticalSection(&dcomp_popup_stack_cs); + for (i = 0; i < dcomp_popup_stack_depth; ++i) + { + if (dcomp_popup_stack[i] == hwnd) + { + memmove(&dcomp_popup_stack[i], &dcomp_popup_stack[i + 1], + (dcomp_popup_stack_depth - i - 1) * sizeof(*dcomp_popup_stack)); ++ memmove(&dcomp_popup_stack_tid[i], &dcomp_popup_stack_tid[i + 1], ++ (dcomp_popup_stack_depth - i - 1) * sizeof(*dcomp_popup_stack_tid)); + --dcomp_popup_stack_depth; +- return; ++ break; + } + } ++ LeaveCriticalSection(&dcomp_popup_stack_cs); + } + + /* Restore a parent's style if we OR-ed in WS_CLIPCHILDREN for this target +@@ -1256,21 +1360,38 @@ static LRESULT CALLBACK dcomp_swapchain_wndproc(HWND hwnd, UINT msg, WPARAM wpar + * *stacking* (transient anchor) is handled separately via the popup + * stack below, not by this count. */ + { +- BOOL is_popup_mode = FALSE; ++ BOOL is_popup_mode = FALSE, already_subclassed; + LONG target_style = GetWindowLongW(target_hwnd, GWL_STYLE); + HWND target_parent = GetParent(target_hwnd); + /* Transient anchor for a new popup = the previously-opened +- * target (top of the popup stack). */ +- HWND popup_parent = dcomp_popup_stack_top(); ++ * target on the same UI thread (see dcomp_popup_stack_top_for). */ ++ HWND popup_parent = dcomp_popup_stack_top_for(target_hwnd); + + FIXME("DComp popup-detect: target %p style=0x%08lx parent=%p count=%ld.\n", + target_hwnd, (unsigned long)target_style, target_parent, + dcomp_subclassed_target_count); + +- if (dcomp_subclassed_target_count > 0) ++ /* The composition window is keyed on the swapchain, so a ++ * client that recreates its swapchain rebinds the same ++ * target through a new one and this handler can run more ++ * than once per target. Never subclass such a window ++ * twice: SetWindowLongPtrW would hand back our own proc as ++ * the "original", our proc would then forward to itself on ++ * every message, and teardown would install it as the ++ * window's own wndproc with the application's proc lost. ++ * It would also inflate the count and the popup stack. */ ++ already_subclassed = GetPropW(target_hwnd, L"__wine_dcomp_subclassed") != NULL; ++ ++ if (!already_subclassed && dcomp_thread_has_target(target_hwnd)) + is_popup_mode = TRUE; + +- if (is_popup_mode) ++ if (already_subclassed) ++ { ++ SetPropW(target_hwnd, L"__wine_dcomp_swapchain", (HANDLE)iface); ++ FIXME("DComp: target %p already subclassed, rebound swapchain to %p.\n", ++ target_hwnd, iface); ++ } ++ else if (is_popup_mode) + { + /* Lightweight popup mode: subclass with minimal wndproc + * (WM_ERASEBKGND + WM_PAINT only), no timer. */ +diff --git a/dlls/winex11.drv/window.c b/dlls/winex11.drv/window.c +index 8cfe055..367be02 100644 +--- a/dlls/winex11.drv/window.c ++++ b/dlls/winex11.drv/window.c +@@ -111,13 +111,28 @@ static const WCHAR *dcomp_swapchain_prop = dcomp_swapchain_propW; + static const WCHAR dcomp_popup_parent_propW[] = + {'_','_','w','i','n','e','_','d','c','o','m','p','_','p','o','p','u','p','_','p','a','r','e','n','t',0}; + static const WCHAR *dcomp_popup_parent_prop = dcomp_popup_parent_propW; +-/* Set on the desktop window by dxgi while a DComp plugin GUI is hosted in this +- * session. Used to mark embedded ownerless TOOLWINDOW popups as DROPDOWN_MENU +- * (see set_style_hints) — they anchor to the host window, so the per-window +- * dcomp discriminators below cannot see they belong to a DComp plugin. */ +-static const WCHAR dcomp_active_propW[] = +- {'_','_','w','i','n','e','_','d','c','o','m','p','_','a','c','t','i','v','e',0}; +-static const WCHAR *dcomp_active_prop = dcomp_active_propW; ++/* Set on the desktop window by dxgi while a DComp plugin GUI is hosted by ++ * THIS process. Used to mark embedded ownerless TOOLWINDOW popups as ++ * DROPDOWN_MENU (see set_style_hints): they anchor to the host window, so ++ * the per-window dcomp discriminators below cannot see they belong to a ++ * DComp plugin. The name is scoped by process id: the desktop property ++ * list spans the window station, and the old session-wide flag gave ++ * unrelated processes' ownerless popups a menu role while any DComp GUI ++ * existed anywhere. WINE_DCOMP_SCOPE=global restores the session-wide ++ * name for comparison. */ ++static void get_dcomp_active_prop_name( WCHAR *name, size_t count ) ++{ ++ const char *env = getenv( "WINE_DCOMP_SCOPE" ); ++ char ascii[64]; ++ size_t i; ++ ++ if (env && !strcmp( env, "global" )) ++ snprintf( ascii, sizeof(ascii), "__wine_dcomp_active" ); ++ else ++ snprintf( ascii, sizeof(ascii), "__wine_dcomp_active_%u", (UINT)GetCurrentProcessId() ); ++ for (i = 0; ascii[i] && i < count - 1; i++) name[i] = (WCHAR)ascii[i]; ++ name[i] = 0; ++} + + static BOOL selected_fullscreen_class( HWND hwnd ) + { +@@ -1355,8 +1370,10 @@ static void set_style_hints( struct x11drv_win_data *data, DWORD style, DWORD ex + * flag catches them. It is deliberately broad (any TOOLWINDOW popup while + * a DComp GUI is open) — validated against Serum2 (embedded dropdowns must + * not regress). */ ++ WCHAR active_prop[64]; ++ get_dcomp_active_prop_name( active_prop, ARRAY_SIZE(active_prop) ); + if (NtUserGetProp( data->hwnd, dcomp_popup_parent_prop ) || anchor_is_dcomp +- || NtUserGetProp( NtUserGetDesktopWindow(), dcomp_active_prop )) ++ || NtUserGetProp( NtUserGetDesktopWindow(), active_prop )) + window_set_net_wm_window_type( data, XATOM__NET_WM_WINDOW_TYPE_DROPDOWN_MENU ); + else + /* Issue 84: ownerless TOOLWINDOW popups of non-DComp apps (KM88 diff --git a/patches/0101-dxgi-consume-DComp-paint-and-erase-only-with-a-valid.patch b/patches/0101-dxgi-consume-DComp-paint-and-erase-only-with-a-valid.patch new file mode 100644 index 00000000..0352f1e4 --- /dev/null +++ b/patches/0101-dxgi-consume-DComp-paint-and-erase-only-with-a-valid.patch @@ -0,0 +1,95 @@ +Subject: dxgi: consume DComp popup paint and erase only with a valid buffer + +The popup wndproc swallowed WM_PAINT and WM_ERASEBKGND whatever the +composition buffer's state, validating the window with nothing drawn; +newly exposed popup pixels then kept undefined content behind the +background_pixmap=None choices (kept, each guards a documented flash +regression). Popups now forward paint and erase to the app's own +wndproc when no valid current-size buffer exists. + +The target wndproc keeps its consume-without-drawing stale path. +Forwarding there causes the Learn View revert, and a deterministic +fill is worse: the currency check dips whenever Chromium resizes its +swapchain, which mouse-driven hover animation does constantly, and a +fill repainted valid on-screen pixels as white flashes (first live +test, Learn View and Splice). The pixels already on screen are the +last presented frame, and keeping them is the deterministic choice for +a covered region. WINE_DCOMP_PAINT=legacy restores the unconditional +popup consumption for comparison. + +diff --git a/dlls/dxgi/factory.c b/dlls/dxgi/factory.c +index 2856e5e..402fc18 100644 +--- a/dlls/dxgi/factory.c ++++ b/dlls/dxgi/factory.c +@@ -580,6 +580,21 @@ LONG dcomp_subclassed_target_count; + * IFX list, embedded). This flag lets winex11 mark them DROPDOWN_MENU instead. + * Set while count > 0, removed at 0 (SetProp with value 0 is not distinguishable + * from "unset" — see the SetPropW (HANDLE)1/2 lesson). */ ++/* WINE_DCOMP_PAINT=legacy restores the unconditional paint/erase ++ * consumption for comparison. */ ++static BOOL dcomp_paint_legacy(void) ++{ ++ static int legacy = -1; ++ ++ if (legacy == -1) ++ { ++ WCHAR val[16] = {0}; ++ legacy = GetEnvironmentVariableW(L"WINE_DCOMP_PAINT", val, ARRAY_SIZE(val)) ++ && !wcscmp(val, L"legacy"); ++ } ++ return legacy; ++} ++ + /* WINE_DCOMP_SCOPE=global restores the session-wide active flag and the + * process-global popup anchoring for comparison. */ + static BOOL dcomp_scope_global(void) +@@ -810,7 +825,18 @@ static LRESULT CALLBACK dcomp_popup_wndproc(HWND hwnd, UINT msg, WPARAM wparam, + switch (msg) + { + case WM_ERASEBKGND: ++ { ++ HDC comp_dc = (HDC)GetPropW(hwnd, L"__wine_dcomp_comp_dc"); ++ LPARAM dims = (LPARAM)GetPropW(hwnd, L"__wine_dcomp_comp_size"); ++ ++ /* Only consume the erase when WM_PAINT below can cover the area ++ * from a valid current-size buffer. A swallowed erase with ++ * nothing to blit leaves newly exposed pixels undefined. */ ++ if (!dcomp_paint_legacy() && !(comp_dc && dims ++ && dcomp_comp_buffer_current(hwnd, LOWORD(dims), HIWORD(dims)))) ++ break; /* forward: the app's own wndproc erases deterministically */ + return 1; ++ } + + case WM_PAINT: + { +@@ -818,6 +844,12 @@ static LRESULT CALLBACK dcomp_popup_wndproc(HWND hwnd, UINT msg, WPARAM wparam, + HDC comp_dc = (HDC)GetPropW(hwnd, L"__wine_dcomp_comp_dc"); + LPARAM dims = (LPARAM)GetPropW(hwnd, L"__wine_dcomp_comp_size"); + ++ /* No valid current-size buffer: do not consume the paint with ++ * nothing drawn. Forward so the app's own wndproc paints. */ ++ if (!dcomp_paint_legacy() && !(comp_dc && dims ++ && dcomp_comp_buffer_current(hwnd, LOWORD(dims), HIWORD(dims)))) ++ break; ++ + BeginPaint(hwnd, &ps); + if (comp_dc && dims) + { +@@ -945,6 +977,17 @@ static LRESULT CALLBACK dcomp_target_wndproc(HWND hwnd, UINT msg, WPARAM wparam, + if (paint_null <= 3) + FIXME("WM_PAINT: no comp buffer for hwnd %p (dc=%p dims=%#Ix).\n", + hwnd, comp_dc, (ULONG_PTR)dims); ++ /* Consume without drawing: forwarding is not safe here (the ++ * orig proc erases the surface and the sibling software frame ++ * shows through, the Learn View revert), and filling is ++ * worse. The currency check dips whenever Chromium resizes ++ * its swapchain, which mouse-driven hover animation does ++ * constantly, and a fill repainted valid on-screen pixels as ++ * white flashes (first live test, Learn View and Splice). ++ * The pixels already on screen are the last presented frame: ++ * keeping them IS the deterministic choice for a covered ++ * region, and a newly exposed region while the buffer is ++ * stale repaints on the next present or reblit. */ + } + EndPaint(hwnd, &ps); + ValidateRect(hwnd, NULL); diff --git a/patches/0102-dxgi-winex11-gate-parked-reblits-on-host-viewability.patch b/patches/0102-dxgi-winex11-gate-parked-reblits-on-host-viewability.patch new file mode 100644 index 00000000..75abb572 --- /dev/null +++ b/patches/0102-dxgi-winex11-gate-parked-reblits-on-host-viewability.patch @@ -0,0 +1,129 @@ +Subject: dxgi/winex11: gate parked reblits on host viewability, not WS_VISIBLE + +IsWindowVisible walks the WS_VISIBLE chain, which stays set while the +top level is iconified or parked on another workspace, so the parked +reblit timers kept stamping frames into windows nobody could see. +winex11 now publishes the host state on each managed top level +(__wine_x11_viewable: 1 mapped NormalState, 2 not, absent unknown), +maintained from WM_STATE acknowledgements, and the reblit gate skips +when the root reports 2. Absent stays a fallback to the WS_VISIBLE +check, covering unmanaged windows and the virtual desktop. +WINE_DCOMP_VIEWABILITY=off keeps the old gate for comparison. + +Both exits of handle_state_change publish. It records the new state +before it refuses an old-serial or transient acknowledgement, and a +window manager that iconifies or restores while one of our requests is +in flight can send WM_STATE below our serial. Publishing on the success +path alone left a mapped window marked unviewable, with every parked +reblit under it stopped. + +diff --git a/dlls/dxgi/factory.c b/dlls/dxgi/factory.c +index f7fc76e..e609648 100644 +--- a/dlls/dxgi/factory.c ++++ b/dlls/dxgi/factory.c +@@ -492,6 +492,20 @@ static BOOL dcomp_comp_buffer_current(HWND hwnd, unsigned int w, unsigned int h) + * View revert). Liveness is signalled by teardown instead: Release/NCDESTROY + * kill the timer and remove the props, and 0036's null-device guard still + * suspends the timer for never-presenting dead devices. */ ++/* WINE_DCOMP_VIEWABILITY=off keeps the reblit gate on WS_VISIBLE alone. */ ++static BOOL dcomp_viewability_enabled(void) ++{ ++ static int enabled = -1; ++ ++ if (enabled == -1) ++ { ++ WCHAR val[8] = {0}; ++ enabled = !(GetEnvironmentVariableW(L"WINE_DCOMP_VIEWABILITY", val, ARRAY_SIZE(val)) ++ && (!wcscmp(val, L"off") || !wcscmp(val, L"0"))); ++ } ++ return enabled; ++} ++ + static void dcomp_reblit_comp_buffer(HWND hwnd, const char *reason) + { + HDC comp_dc = (HDC)GetPropW(hwnd, L"__wine_dcomp_comp_dc"); +@@ -522,6 +536,25 @@ static void dcomp_reblit_comp_buffer(HWND hwnd, const char *reason) + return; + } + ++ /* IsWindowVisible only walks WS_VISIBLE, which stays set while the top ++ * level is iconified or parked on another workspace. winex11 publishes ++ * the host state on the root window (1 viewable, 2 not, absent unknown: ++ * fall back to the WS_VISIBLE check above). */ ++ if (dcomp_viewability_enabled()) ++ { ++ HWND root = GetAncestor(hwnd, GA_ROOT); ++ ++ if (root && (UINT_PTR)GetPropW(root, L"__wine_x11_viewable") == 2) ++ { ++ static unsigned int unmapped_count; ++ ++unmapped_count; ++ if (unmapped_count <= 3 || !(unmapped_count % 500)) ++ FIXME("Re-blit skipped (host unmapped): hwnd %p reason=%s.\n", hwnd, reason); ++ TRACE_(wmtrace)("REBLIT-SKIP hwnd=%p cause=unmapped reason=%s\n", hwnd, reason); ++ return; ++ } ++ } ++ + if (comp_dc && dims) + { + unsigned int w = LOWORD(dims); +diff --git a/dlls/winex11.drv/window.c b/dlls/winex11.drv/window.c +index 367be02..450362c 100644 +--- a/dlls/winex11.drv/window.c ++++ b/dlls/winex11.drv/window.c +@@ -108,6 +108,13 @@ static const WCHAR focus_time_prop[] = + static const WCHAR dcomp_swapchain_propW[] = + {'_','_','w','i','n','e','_','d','c','o','m','p','_','s','w','a','p','c','h','a','i','n',0}; + static const WCHAR *dcomp_swapchain_prop = dcomp_swapchain_propW; ++/* Host viewability, published for the dcomp reblit gate: 1 = mapped ++ * NormalState, 2 = not; absent = unknown (unmanaged, virtual desktop), so ++ * readers fall back to WS_VISIBLE alone. Value 2 instead of removal: a ++ * removed property cannot be told apart from one never set. */ ++static const WCHAR x11_viewable_propW[] = ++ {'_','_','w','i','n','e','_','x','1','1','_','v','i','e','w','a','b','l','e',0}; ++static const WCHAR *x11_viewable_prop = x11_viewable_propW; + static const WCHAR dcomp_popup_parent_propW[] = + {'_','_','w','i','n','e','_','d','c','o','m','p','_','p','o','p','u','p','_','p','a','r','e','n','t',0}; + static const WCHAR *dcomp_popup_parent_prop = dcomp_popup_parent_propW; +@@ -2525,6 +2532,20 @@ static BOOL handle_state_change( unsigned long serial, unsigned long *expect_ser + return TRUE; + } + ++/* Publish host viewability for the dcomp reblit gate: 1 mapped NormalState, ++ * 2 not. handle_state_change copies the new value into current_state before it ++ * refuses an old-serial or transient acknowledgement, so both of its exits have ++ * to publish. A window manager that iconifies or restores while one of our ++ * requests is in flight can send WM_STATE below our serial; skipping the ++ * publish there leaves a mapped window marked unviewable, and every parked ++ * reblit under that top level stops until an unrelated transition happens to ++ * refresh the property. */ ++static void window_publish_viewability( struct x11drv_win_data *data ) ++{ ++ NtUserSetProp( data->hwnd, x11_viewable_prop, ++ (HANDLE)(UINT_PTR)(data->current_state.wm_state == NormalState ? 1 : 2) ); ++} ++ + void window_wm_state_notify( struct x11drv_win_data *data, unsigned long serial, UINT value, Time time ) + { + UINT *desired = &data->desired_state.wm_state, *pending = &data->pending_state.wm_state, *current = &data->current_state.wm_state; +@@ -2539,7 +2560,10 @@ void window_wm_state_notify( struct x11drv_win_data *data, unsigned long serial, + + if (!handle_state_change( serial, expect_serial, sizeof(value), &value, desired, pending, + current, expected, prefix, received, reason )) ++ { ++ window_publish_viewability( data ); + return; ++ } + data->current_state.activate = data->pending_state.activate; + data->reparenting = 0; + +@@ -2548,6 +2572,8 @@ void window_wm_state_notify( struct x11drv_win_data *data, unsigned long serial, + + if (data->current_state.wm_state == NormalState) NtUserSetProp( data->hwnd, focus_time_prop, (HANDLE)time ); + else if (!data->wm_state_serial) NtUserRemoveProp( data->hwnd, focus_time_prop ); ++ ++ window_publish_viewability( data ); + } + + void window_net_wm_state_notify( struct x11drv_win_data *data, unsigned long serial, UINT value ) diff --git a/patches/0103-dxgi-carry-DComp-teardown-on-a-registered-message-no.patch b/patches/0103-dxgi-carry-DComp-teardown-on-a-registered-message-no.patch new file mode 100644 index 00000000..a797cf17 --- /dev/null +++ b/patches/0103-dxgi-carry-DComp-teardown-on-a-registered-message-no.patch @@ -0,0 +1,176 @@ +Subject: dxgi: carry DComp teardown on a registered message, not WM_USER space + +Patch 0094 marshals teardown to the window's owning thread with +WM_USER+0x102. The other two dcomp WM_USER messages only ever reach our +own composition-window class, but this one is posted to windows the +application owns: Chromium's intermediate D3D window and plugin +editors. WM_USER+n is application-defined space, so a window that no +longer carries our subclass when the posted message arrives hands the +number to its own protocol, where toolkits commonly treat it as +"invoke this callback" with a pointer argument. + +Register a system-range message id instead, which no application +defines, and post only while the target still carries the subclass +marker. The wndprocs compare through a helper that checks the id is +non-zero first. RegisterWindowMessage returns 0 when it fails, and +WM_NULL is 0, so a bare comparison would turn every routine ping into a +teardown. + +Found after a page fault on execute access to a low address in a +helper process during a plugin scan (first full PR 5 test). Whether +that fault came through this path is unproven; the collision is a +defect either way. + +diff --git a/dlls/dxgi/dxgi_private.h b/dlls/dxgi/dxgi_private.h +index 9a1b9b9..f6c242f 100644 +--- a/dlls/dxgi/dxgi_private.h ++++ b/dlls/dxgi/dxgi_private.h +@@ -181,6 +181,7 @@ struct dxgi_adapter *unsafe_impl_from_IDXGIAdapter(IDXGIAdapter *iface); + extern LONG dcomp_subclassed_target_count; + extern void dcomp_teardown_window(HWND hwnd); + extern BOOL dcomp_teardown_legacy(void); ++extern UINT dcomp_teardown_message(void); + + /* IDXGISwapChain */ + struct d3d11_swapchain +diff --git a/dlls/dxgi/factory.c b/dlls/dxgi/factory.c +index e609648..c63242c 100644 +--- a/dlls/dxgi/factory.c ++++ b/dlls/dxgi/factory.c +@@ -390,7 +390,30 @@ static void STDMETHODCALLTYPE dxgi_factory_UnregisterOcclusionStatus(IWineDXGIFa + + #define WM_WINE_DCOMP_SET_TARGET (WM_USER + 0x100) + #define WM_WINE_DCOMP_SET_CHILD_MODE (WM_USER + 0x101) +-#define WM_WINE_DCOMP_TEARDOWN (WM_USER + 0x102) ++/* Teardown is marshalled to windows owned by the APPLICATION (Chromium's ++ * intermediate D3D window, plugin editors), unlike the WM_USER messages ++ * above which only ever reach our own comp-window class. WM_USER+n is ++ * application-defined space: a window that no longer carries our subclass ++ * hands the number to its own protocol, where toolkits commonly treat it as ++ * "invoke this callback" with a pointer argument. RegisterWindowMessage ++ * returns an id in the system range that no application defines. */ ++UINT dcomp_teardown_message(void) ++{ ++ static UINT msg; ++ ++ if (!msg) msg = RegisterWindowMessageW(L"__wine_dcomp_teardown"); ++ return msg; ++} ++ ++/* RegisterWindowMessage returns 0 when it fails, and WM_NULL is 0. Comparing ++ * an incoming message straight against the id would then turn every WM_NULL, ++ * the routine ping a hung-window check sends, into a teardown. */ ++static BOOL dcomp_is_teardown_message(UINT msg) ++{ ++ UINT teardown = dcomp_teardown_message(); ++ ++ return teardown && msg == teardown; ++} + /* SET_TARGET wparam flag set by dlls/dcomp when the binding dcomp device has + * no d2d1 device (mirrored there, like the message id itself). */ + #define DCOMP_SET_TARGET_NULL_D2D1_DEVICE 0x1 +@@ -921,6 +944,12 @@ static LRESULT CALLBACK dcomp_popup_wndproc(HWND hwnd, UINT msg, WPARAM wparam, + { + WNDPROC orig = (WNDPROC)GetPropW(hwnd, L"__wine_dcomp_orig_wndproc"); + ++ if (dcomp_is_teardown_message(msg)) ++ { ++ dcomp_teardown_window(hwnd); ++ return 0; ++ } ++ + switch (msg) + { + case WM_ERASEBKGND: +@@ -961,10 +990,6 @@ static LRESULT CALLBACK dcomp_popup_wndproc(HWND hwnd, UINT msg, WPARAM wparam, + return 0; + } + +- case WM_WINE_DCOMP_TEARDOWN: +- dcomp_teardown_window(hwnd); +- return 0; +- + case WM_TIMER: + if (wparam == DCOMP_POPUP_REBLIT_TIMER_ID) + { +@@ -1037,6 +1062,12 @@ static LRESULT CALLBACK dcomp_target_wndproc(HWND hwnd, UINT msg, WPARAM wparam, + { + WNDPROC orig = (WNDPROC)GetPropW(hwnd, L"__wine_dcomp_orig_wndproc"); + ++ if (dcomp_is_teardown_message(msg)) ++ { ++ dcomp_teardown_window(hwnd); ++ return 0; ++ } ++ + switch (msg) + { + case WM_ERASEBKGND: +@@ -1093,10 +1124,6 @@ static LRESULT CALLBACK dcomp_target_wndproc(HWND hwnd, UINT msg, WPARAM wparam, + return 0; + } + +- case WM_WINE_DCOMP_TEARDOWN: +- dcomp_teardown_window(hwnd); +- return 0; +- + case WM_TIMER: + if (wparam == DCOMP_REBLIT_TIMER_ID) + { +@@ -1276,15 +1303,18 @@ static LRESULT CALLBACK dcomp_target_wndproc(HWND hwnd, UINT msg, WPARAM wparam, + + static LRESULT CALLBACK dcomp_swapchain_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) + { ++ if (dcomp_is_teardown_message(msg)) ++ { ++ DestroyWindow(hwnd); ++ return 0; ++ } ++ + switch (msg) + { + case WM_ERASEBKGND: + return 1; + case WM_NCHITTEST: + return HTTRANSPARENT; +- case WM_WINE_DCOMP_TEARDOWN: +- DestroyWindow(hwnd); +- return 0; + case WM_PAINT: + { + PAINTSTRUCT ps; +diff --git a/dlls/dxgi/swapchain.c b/dlls/dxgi/swapchain.c +index 3e6d4bb..b60f96f 100644 +--- a/dlls/dxgi/swapchain.c ++++ b/dlls/dxgi/swapchain.c +@@ -28,7 +28,6 @@ WINE_DEFAULT_DEBUG_CHANNEL(dxgi); + WINE_DECLARE_DEBUG_CHANNEL(winediag); + WINE_DECLARE_DEBUG_CHANNEL(wmtrace); + +-#define WM_WINE_DCOMP_TEARDOWN (WM_USER + 0x102) + + static UINT dxgi_get_display_refresh_rate(void) + { +@@ -344,7 +343,12 @@ static ULONG STDMETHODCALLTYPE d3d11_swapchain_Release(IDXGISwapChain4 *iface) + * thread-affine). */ + if (GetWindowThreadProcessId(t, NULL) == GetCurrentThreadId()) + dcomp_teardown_window(t); +- else if (!PostMessageW(t, WM_WINE_DCOMP_TEARDOWN, 0, 0)) ++ /* Only message a window still carrying our subclass: the ++ * marker is removed by whichever teardown ran first, and a ++ * window we no longer own may belong to another component ++ * (or be a recycled handle) by the time this posts. */ ++ else if (GetPropW(t, L"__wine_dcomp_subclassed") ++ && !PostMessageW(t, dcomp_teardown_message(), 0, 0)) + WARN("Could not marshal DComp teardown to the thread of %p.\n", t); + } + swapchain->target_hwnd = NULL; +@@ -369,7 +373,7 @@ static ULONG STDMETHODCALLTYPE d3d11_swapchain_Release(IDXGISwapChain4 *iface) + if (GetWindowThreadProcessId(swapchain->comp_wnd, NULL) == GetCurrentThreadId()) + DestroyWindow(swapchain->comp_wnd); + else if (!dcomp_teardown_legacy() +- && !PostMessageW(swapchain->comp_wnd, WM_WINE_DCOMP_TEARDOWN, 0, 0)) ++ && !PostMessageW(swapchain->comp_wnd, dcomp_teardown_message(), 0, 0)) + WARN("Could not marshal comp-window destroy to the thread of %p.\n", + swapchain->comp_wnd); + } diff --git a/patches/0104-winex11-tie-config-rounding-aliases-to-Wine-s-own-co.patch b/patches/0104-winex11-tie-config-rounding-aliases-to-Wine-s-own-co.patch new file mode 100644 index 00000000..5f3e70fa --- /dev/null +++ b/patches/0104-winex11-tie-config-rounding-aliases-to-Wine-s-own-co.patch @@ -0,0 +1,165 @@ +Subject: winex11: tie config-rounding aliases to Wine's own configure requests + +Patch 0042 aliases a window-manager geometry grant that differs from our +request only by sub-scale rounding, so a scale-aligned application does +not chase the difference forever. Three things let that alias outlive +the request it belongs to, now that 0093 serialises host state. + +An unsolicited configure could create an alias outright. There is no +Wine request to alias against on that path, so a window manager placing +a window of its own accord could pin the Win32 size to whatever it +happened to hold; it may now only carry an existing alias along a move. + +axis_is_host_translation accepts the two edges of an axis differing by +up to one scale unit, so at 200% a grant of left+3/right+4 reads as a +pure translation while the width actually grew a pixel, and the alias +then hid that resize from Win32 behind SWP_NOSIZE. Tilers hand out +exactly that when they redistribute a spare pixel, so an unsolicited +configure must now leave both dimensions exactly unchanged. The refusal +is traced as ALIAS-DECLINE. + +The alias is arithmetic against one scale factor and survived a DPI +change, so it is stamped with the DPI it was computed for and ignored +once that changes, which also covers a move to another monitor. The +stamp goes in activate_config_rounding, the one place that turns an +alias active, rather than at each call site. The sub-scale alias in +window_set_config is 0042's own mechanism, and a call site that forgets +the stamp leaves every reader comparing 0 against the real DPI and +throwing the alias away. + +WINE_CONFIG_ROUNDING=legacy restores the old policy for comparison. + +diff --git a/dlls/winex11.drv/window.c b/dlls/winex11.drv/window.c +index 450362c..efb0d2c 100644 +--- a/dlls/winex11.drv/window.c ++++ b/dlls/winex11.drv/window.c +@@ -1693,6 +1693,21 @@ enum config_rounding_state + CONFIG_ROUNDING_ACTIVE, + }; + ++/* WINE_CONFIG_ROUNDING=legacy restores the pre-lock alias policy: aliases ++ * created from unsolicited window-manager configures, sub-scale size changes ++ * accepted as translations, and aliases outliving a DPI change. */ ++static BOOL config_rounding_legacy(void) ++{ ++ static int legacy = -1; ++ ++ if (legacy == -1) ++ { ++ const char *env = getenv( "WINE_CONFIG_ROUNDING" ); ++ legacy = env && !strcmp( env, "legacy" ); ++ } ++ return legacy; ++} ++ + static void clear_config_rounding( struct x11drv_win_data *data, const char *reason ) + { + if (data->config_rounding_state == CONFIG_ROUNDING_NONE) return; +@@ -1704,6 +1719,7 @@ static void clear_config_rounding( struct x11drv_win_data *data, const char *rea + data->config_rounding_serial = 0; + data->config_rounding_settle_serial = 0; + data->config_rounding_generation = 0; ++ data->config_rounding_dpi = 0; + } + + static void track_config_rounding_request( struct x11drv_win_data *data, unsigned long serial, +@@ -1715,6 +1731,7 @@ static void track_config_rounding_request( struct x11drv_win_data *data, unsigne + data->config_rounding_generation = data->config_generation; + data->config_rounding_win32_rect = *rect; + data->config_rounding_host_rect = *rect; ++ data->config_rounding_dpi = NtUserGetDpiForWindow( data->hwnd ); + TRACE( "window %p/%lx tracking config %s request %lu generation %lu\n", data->hwnd, + data->whole_window, wine_dbgstr_rect(rect), serial, data->config_generation ); + } +@@ -1778,17 +1795,34 @@ static void activate_config_rounding( struct x11drv_win_data *data, const RECT * + { + data->config_rounding_state = CONFIG_ROUNDING_ACTIVE; + data->config_rounding_host_rect = *host_rect; ++ /* Stamp the DPI here, the one place that turns an alias active, rather than ++ * at each call site: the sub-scale alias in window_set_config is 0042's own ++ * mechanism and left the stamp at 0, so every reader compared 0 against the ++ * real DPI and threw the alias away. The rounded host rect then goes back ++ * to Win32, the application re-applies its size, and the two chase: the ++ * resize jitter 0042 exists to stop. The arithmetic that qualified this ++ * alias ran against the current DPI, so that is what it belongs to. */ ++ data->config_rounding_dpi = NtUserGetDpiForWindow( data->hwnd ); + TRACE( "window %p/%lx activating config rounding alias %s -> %s request %lu generation %lu\n", + data->hwnd, data->whole_window, wine_dbgstr_rect(&data->config_rounding_win32_rect), + wine_dbgstr_rect(host_rect), data->config_rounding_serial, + data->config_rounding_generation ); + } + ++/* The alias is arithmetic against one scale factor, so it means nothing once ++ * the window's DPI changes (moving to another monitor, a scale change). */ ++static BOOL config_rounding_dpi_current( const struct x11drv_win_data *data ) ++{ ++ return config_rounding_legacy() || ++ data->config_rounding_dpi == NtUserGetDpiForWindow( data->hwnd ); ++} ++ + static BOOL config_rounding_alias_matches_rect( const struct x11drv_win_data *data, + const RECT *visible_rect ) + { + return data->config_rounding_state == CONFIG_ROUNDING_ACTIVE && + data->config_rounding_generation == data->config_generation && ++ config_rounding_dpi_current( data ) && + EqualRect( visible_rect, &data->config_rounding_win32_rect ) && + EqualRect( &data->current_state.rect, &data->config_rounding_host_rect ); + } +@@ -1802,6 +1836,7 @@ static BOOL config_rounding_move_pending( const struct x11drv_win_data *data ) + { + return data->config_rounding_state == CONFIG_ROUNDING_ACTIVE && + data->config_rounding_generation == data->config_generation && ++ config_rounding_dpi_current( data ) && + EqualRect( &data->current_state.rect, &data->config_rounding_host_rect ) && + !EqualRect( &data->rects.visible, &data->config_rounding_win32_rect ) && + rect_sizes_equal( &data->rects.visible, &data->config_rounding_win32_rect ); +@@ -1820,6 +1855,17 @@ static BOOL track_config_rounding_move( struct x11drv_win_data *data, const RECT + if (dpi < USER_DEFAULT_SCREEN_DPI || dpi % USER_DEFAULT_SCREEN_DPI) return FALSE; + scale = dpi / USER_DEFAULT_SCREEN_DPI; + ++ /* An unsolicited configure carries no Wine request to alias against, so it ++ * may only carry an existing alias along a move, never invent one: without ++ * this a window manager placing a window of its own accord could pin the ++ * Win32 size to whatever it happened to hold. */ ++ if (!config_rounding_legacy() && ++ (data->config_rounding_state != CONFIG_ROUNDING_ACTIVE || ++ data->config_rounding_generation != data->config_generation || ++ data->config_rounding_dpi != dpi || ++ !EqualRect( &old_host_rect, &data->config_rounding_host_rect ))) ++ return FALSE; ++ + left = host_rect->left - old_host_rect.left; + top = host_rect->top - old_host_rect.top; + right = host_rect->right - old_host_rect.right; +@@ -1828,6 +1874,18 @@ static BOOL track_config_rounding_move( struct x11drv_win_data *data, const RECT + !axis_is_host_translation( left, right, scale ) || + !axis_is_host_translation( top, bottom, scale )) + return FALSE; ++ /* axis_is_host_translation tolerates the two edges of an axis differing by ++ * up to one scale unit, so a genuine one-pixel resize reads as a move and ++ * the alias then hides it from Win32 behind SWP_NOSIZE. Tilers hand out ++ * exactly that when they redistribute a spare pixel, so an unsolicited ++ * configure must leave both dimensions exactly as they were. */ ++ if (!config_rounding_legacy() && (left != right || top != bottom)) ++ { ++ TRACE_(wmtrace)( "ALIAS-DECLINE hwnd=%p xid=%lx host=%s resize dx=%d,%d dy=%d,%d\n", ++ data->hwnd, data->whole_window, wine_dbgstr_rect(host_rect), ++ left, right, top, bottom ); ++ return FALSE; ++ } + + OffsetRect( &win32_rect, left, top ); + if (!config_edges_within_dpi_rounding( data, &win32_rect, host_rect )) return FALSE; +diff --git a/dlls/winex11.drv/x11drv.h b/dlls/winex11.drv/x11drv.h +index 02154d8..1a7a040 100644 +--- a/dlls/winex11.drv/x11drv.h ++++ b/dlls/winex11.drv/x11drv.h +@@ -722,6 +722,7 @@ struct x11drv_win_data + unsigned long config_rounding_generation; /* Win32 rect generation of the configure request */ + RECT config_rounding_win32_rect; /* Win32 rect associated with a rounded configure request */ + RECT config_rounding_host_rect; /* host rect acknowledged for config_rounding_win32_rect */ ++ UINT config_rounding_dpi; /* window DPI the rounding alias was computed for */ + UINT config_rounding_state; /* state of the rounded configure request */ + }; + diff --git a/patches/BASE.txt b/patches/BASE.txt index f2e4e1a5..d560d0f3 100644 --- a/patches/BASE.txt +++ b/patches/BASE.txt @@ -11,7 +11,7 @@ narrative — 5 merge conflicts, three build breaks the clean rebase didn't catch, a build.sh/pipefail tooling gotcha — is in `notes/ABLETON-WINE-11.11-TO-11.13-BASE-BUMP.md`. -The current Wine series contains 81 files, numbered 0001 through 0089. +The current Wine series contains 96 files, numbered 0001 through 0104. Patches 0027 and 0044 are intentionally absent. Numbers 0066 through 0068 and 0072 through 0074 are reserved by open pull requests and will enter this branch as those merge. @@ -20,11 +20,11 @@ branch as those merge. Run `./build.sh`. Inside the Podman image, `scripts/container-build.sh` unpacks `vendor/wine-base-5c23dd1c.tar.zst`, creates a Git repository, and -applies `patches/00*.patch` in lexical order with `git am --3way`. +applies `patches/0*.patch` in lexical order with `git am --3way`. Some early patch files have no `From` or `Date` header. The build script adds a fixed identity and date before passing those files to `git am`. A bare -`git am patches/00*.patch` command does not reproduce this step. +`git am patches/0*.patch` command does not reproduce this step. Patches 0008 through 0013 contain title-bar experiments and their reverts. Apply them in sequence with the rest of the series. @@ -371,3 +371,127 @@ Apply them in sequence with the rest of the series. expression mixed an unsigned grayscale value with a lower channel sample, so subtraction underflow could saturate that channel to 255 instead of reducing it. +- `0090`: add a `wmtrace` debug channel across winex11, win32u and dxgi. + One line per window-state transition: WM requests with their X serials, + host acknowledgements (matched, forced, or ignored), the state and + geometry handed back to Win32, window-surface creation, the 0065 + fullscreen normalization firing, parked-DComp reblit decisions with + their skip cause, and each swapchain's GL/GDI present choice. Pure + tracing, no behaviour change. Enable with + `WINEDEBUG=-all,+wmtrace,+winediag`, or `ABLETON_WM_TRACE=1` through + the launcher; `tools/wm-capture.sh` records the matching host-side + view. +- `0091`: skip repeat `_NET_ACTIVE_WINDOW` requests for the pending + window, keeping 0017's real timestamps. 0017's relaxed skip retried on + every fresh input timestamp; under KWin each activation produces + feedback with fresh timestamps, so retries ping-pong: wrong taskbar + focus and unmovable windows. The fork dropped the same retry in + `a641db69` (its issue 76); the non-zero timestamp alone fixes the + mutter deadlock. `WINE_ACTIVATE_RESEND=1` restores the relaxed skip. +- `0092`: let `WINE_ACTIVATE_SOURCE=application` send EWMH source + indication 1 instead of 2 in `_NET_ACTIVE_WINDOW` requests, so the WM + applies its focus-stealing rules to app-initiated activation. + Default-off experiment, separate from 0091 so activation changes stay + one variable at a time. +- `0093`: lock a window's host state while win32u applies an update the + window manager initiated. Re-entrant SetWindowPos processing could + request stale minimise, restore, geometry and Motif state mid-apply; + requests made while locked park in the desired state and flush on + unlock. Backport of Wine `852e39a8` (Remi Bernon), adapted around the + 0042 config-rounding state, with wmtrace LOCK/UNLOCK lines and an + underflow guard on the unlock path. `WINE_HOST_STATE_LOCK=off` + disables the lock for comparison. +- `0094`: make DComp window teardown complete, idempotent and + owning-thread. A cross-thread swapchain release skipped every + window-owning cleanup: timers kept firing, the subclass and + composition properties leaked, and the subclassed-target count never + fell, so winex11 kept classifying ownerless tool popups as menus. A + same-thread release also left the popup-stack entry, the desktop + active property and the parent clip bit behind. One marker-gated + helper now runs the whole teardown from both NCDESTROY handlers, + same-thread release, and a new WM_WINE_DCOMP_TEARDOWN message that + cross-thread release posts to the owning thread. The wmtrace channel + logs each teardown. `WINE_DCOMP_TEARDOWN=legacy` restores the old + release path for comparison. +- `0095`: reference-count the parent `WS_CLIPCHILDREN` bit + (`__wine_dcomp_clip_refs` on the parent). Two composition targets + sharing one parent recorded only the first owner, and the first + teardown stripped the style from under the survivor, letting the + parent paint over it. The teardown now restores the style only when + the last recorded target lets go. `WINE_DCOMP_TEARDOWN=legacy` keeps + the old single-owner behaviour. +- `0096`: only remove the desktop swapchain property when it still + points at our own composition window. The key is the swapchain's heap + address, which can be recycled into a newer swapchain; removing the + key unconditionally then strands the live window. Ported from + `giang17/wine` `81e8d12e`. +- `0097`: key that property by process id as well as address + (`__wine_dcomp_wnd_%lu_%I64x`, format shared through + `include/wine/winedxgi.idl`). The desktop property list spans the + window station, so a host and its WebView2 child could collide on the + same address and resolve each other's windows. Ported from + `giang17/wine` `df092626`; six of its seven sites exist here, the + seventh belongs to the fork's tree compositor. +- `0098`: exclude DComp composition targets from GDI present blits. On + Windows the DWM composes a target above everything, so no present + paints there; our GDI blit covered the whole top level, overpainting + a WebView2 pane at 60 Hz while the composition timer painted it back: + visible flicker. dcomp registers target windows in desktop property + slots (`__wine_dcomp_target_%u`) and wined3d clips them out of the + blit. Ported from `giang17/wine` `8e223552` (its issue 121), adapted + to this tree's target release path. +- `0099`: only exclude targets that share the present's own top level. + The registry is desktop-wide, and MapWindowPoints lands a foreign + top-level's target wherever the two windows overlap on screen, so a + plugin editor dragged over Live's WebView2 panel lost its content in + exactly the intersection. Ported from `giang17/wine` `c2cf448f` (its + issue 145); applied cleanly on the 0098 port. +- `0100`: scope DComp popup inference by thread and process. The popup + transient anchor and the full-versus-popup mode choice keyed on one + process-global stack and count, so a second plugin's first target + became a popup of the first (losing its present timer) and popups + could anchor to the wrong plugin; both now key on the target's UI + thread. The desktop-wide `__wine_dcomp_active` flag becomes + per-process (`__wine_dcomp_active_`), so a DComp GUI in one + process no longer gives unrelated processes' ownerless tool popups a + menu role, the undismissable-dialog class of faults. + `WINE_DCOMP_SCOPE=global` restores the old behaviour for comparison. +- `0101`: consume DComp popup WM_PAINT and WM_ERASEBKGND only with a + valid current-size composition buffer; forward to the app's wndproc + otherwise, so newly exposed popup pixels stop keeping undefined + content. The target wndproc keeps its consume-without-drawing stale + path: forwarding there causes the Learn View revert, and a fill is + worse, since the currency check dips whenever Chromium resizes its + swapchain under mouse hover and a fill flashed white over valid + pixels (first live test). The on-screen pixels are the last presented + frame, the deterministic choice for a covered region. + `WINE_DCOMP_PAINT=legacy` restores the old popup consumption. +- `0102`: gate parked DComp reblits on host viewability. IsWindowVisible + keeps reporting TRUE while the top level is iconified or on another + workspace, so parked reblit timers kept stamping invisible frames. + winex11 publishes `__wine_x11_viewable` on each managed top level + from WM_STATE acknowledgements (1 mapped, 2 not, absent unknown with + WS_VISIBLE fallback) and the reblit gate skips on 2. + `WINE_DCOMP_VIEWABILITY=off` keeps the old gate. +- `0103`: carry 0094's teardown marshalling on a registered window + message instead of `WM_USER + 0x102`. The other dcomp `WM_USER` + messages only reach our own composition-window class, but this one is + posted to windows the application owns, and `WM_USER + n` is + application-defined space: a window that no longer carries our + subclass hands the number to its own protocol, where toolkits + commonly treat it as "invoke this callback" with a pointer argument. + Also posts only while the target still carries the subclass marker. +- `0104`: tie 0042's config-rounding aliases to Wine's own configure + requests, now that 0093 serialises host state. An unsolicited + configure could create an alias outright, letting a window manager + pin the Win32 size to whatever it happened to hold; it may now only + carry an existing alias along a move. `axis_is_host_translation` + accepts an axis whose two edges differ by up to one scale unit, so at + 200% a grant of left+3/right+4 read as a translation while the width + grew a pixel, and the alias hid that resize behind `SWP_NOSIZE`; + tilers redistributing a spare pixel hand out exactly that, so an + unsolicited configure must now leave both dimensions exactly + unchanged, traced as ALIAS-DECLINE. The alias is also stamped with + the DPI it was computed for and ignored once that changes, covering a + move to another monitor. `WINE_CONFIG_ROUNDING=legacy` restores the + old policy. diff --git a/patches/SERIES.sha256 b/patches/SERIES.sha256 index 0fe31efc..8f94ac1e 100644 --- a/patches/SERIES.sha256 +++ b/patches/SERIES.sha256 @@ -79,5 +79,20 @@ ca9c10db2c1d77d48c206aaca598f3583758eccbdbdbb3096e98b7417a7b4b62 0079-dxgi-skip ee36bbf26e5ea22b8d6ecd5344cb72fc7883ac59ed733661dfb4e73f773fc18f 0087-win32u-resolve-font-smoothing-by-source-precedence.patch efec6729f1663efe78c7efa941b826b2a28d219bf6faa974294a3bfa854b84f5 0088-win32u-allow-a-semantic-desktop-UI-stock-font.patch 10d34c085b2a1b554457524978e8a380d3c15aaafd290657e5ef8ca9874221c2 0089-d2d1-avoid-unsigned-ClearType-coverage-underflow.patch +d15fbf9a1bb079e972135c252642b2e10029125a67ff52f3584ff063045f2807 0090-winex11-win32u-dxgi-add-a-compact-wmtrace-state-tran.patch +bf46e82e08c06410d9bc967a85ed68ef07d92a306128f71055c41e4427ab0b56 0091-winex11-skip-repeat-_NET_ACTIVE_WINDOW-requests-keep.patch +c925817b1a6a843d4200dc4b87ba470827970b04e030f5d61f0e00cf2a082d8d 0092-winex11-let-WINE_ACTIVATE_SOURCE-choose-the-EWMH-act.patch +b0b08ec1a0e5411423f26a2b38ac4f1597bb35861481599f6b09cf4369a9a947 0093-win32u-winex11-lock-host-window-state-updates-while-.patch +dd398cb167a7ef56744c2f009dbb130ba98e13107cc7f96de5cc43c28b0626c0 0094-dxgi-make-DComp-window-teardown-complete-idempotent-.patch +48a6a06b17d2f67204853744ae359796f457ca1e9728740085f479bb1e5bd45f 0095-dxgi-reference-count-the-parent-WS_CLIPCHILDREN-bit.patch +892e69cfa2611653fb4a4b836cfa433c10fbd3ab24181b61f9285d89613e985b 0096-dxgi-only-remove-the-desktop-swapchain-property-if-w.patch +940963327f288c87b723a82213363e79fa5399afb76bea7ba293283f9f5f843b 0097-dxgi-dcomp-namespace-the-composition-swapchain-windo.patch +5b8c02a7a84d7e70106203887729ae2039f3e762379b61be4c6f74022dffda09 0098-dcomp-wined3d-exclude-composition-targets-from-GDI-p.patch +372a7685948f52d07dc5678336077bd4e800d55eaeaa72d9764340421b544670 0099-wined3d-only-exclude-dcomp-targets-that-share-our-to.patch +22dc3c4f6a6b3c6aa184b426e9bb7b5acf217f304b03161c0c47c756c12506a1 0100-dxgi-winex11-scope-DComp-popup-inference-by-thread-a.patch +b16a8b135278dfc02397c32aea8751d20dfb900f4ae084c3824ad63c5650bb68 0101-dxgi-consume-DComp-paint-and-erase-only-with-a-valid.patch +5d2a180f5276a22765060058c4e8229bed847f37f7069288d1e772af69a294be 0102-dxgi-winex11-gate-parked-reblits-on-host-viewability.patch +eea2decb49b7aa55ccfd8ab4470b16c0e92a39ea95a05f370995115b2f351132 0103-dxgi-carry-DComp-teardown-on-a-registered-message-no.patch +21ed4f1858a2185c4062410b52f1d067b25ef0e12226d1fe0c4187a98a65c2fa 0104-winex11-tie-config-rounding-aliases-to-Wine-s-own-co.patch b2d30d77078263b8141199195ea16b0e6df7eac04b939b9e56cd53552cfb9175 pipeasio/0001-asio-keep-graph-sample-rate-instead-of-ASE_NoClock.patch f25d5b4c3ee71b7e9491f91c462e2b7ddaca377b8b94c7bd95df7734f0a4b563 pipeasio/0002-asio-report-timeGetTime-in-ASIO-systemTime.patch diff --git a/scripts/ableton-live b/scripts/ableton-live index 455c3606..8e83a4a3 100755 --- a/scripts/ableton-live +++ b/scripts/ableton-live @@ -12,7 +12,18 @@ # ABLETON_LINKD=/path/to/ableton-linkd (Ableton Link session anchor override), # WINE_X11_FORCE_OFFSCREEN_CLASS=off (disable the M4L selection-flicker fix), # WINE_WIN32_FULLSCREEN_CLASS=off (disable Live fullscreen normalization), -# WINE_WIN32_RESIZABLE_CLASS=off (disable the monitor-sized resizability fix). +# WINE_WIN32_RESIZABLE_CLASS=off (disable the monitor-sized resizability fix), +# WINE_ACTIVATE_RESEND=1 (restore repeated window-activation requests), +# WINE_HOST_STATE_LOCK=off (disable the host window-state lock), +# WINE_DCOMP_TEARDOWN=legacy (restore the old plugin-window teardown), +# WINE_DCOMP_SCOPE=global (restore session-wide plugin-popup inference), +# WINE_DCOMP_PAINT=legacy (restore unconditional plugin-paint consumption), +# WINE_DCOMP_VIEWABILITY=off (gate parked redraws on WS_VISIBLE alone), +# WINE_CONFIG_ROUNDING=legacy (restore the old window-geometry alias policy), +# WINE_ACTIVATE_SOURCE=application (ask the desktop to apply its +# focus-stealing rules to Live's activation requests), +# ABLETON_WM_TRACE=1 (log every window-state transition on the wmtrace +# channel for window-management reports; pair with tools/wm-capture.sh). set -u # Keep diagnostics from the current launch at one stable path. Tee preserves @@ -44,6 +55,11 @@ export PATH="$WINE_ROOT/bin:$PATH" # Keep the winediag channel on: it carries only rare one-line notices that a # person should read, such as the present-fallback warning from patch 0071. export WINEDEBUG="${WINEDEBUG:--all,+winediag}" +# Window-management transition trace (patch 0090): one line per +# window-state transition. Opt-in because the log grows with every +# window operation. Lines land in ~/.log/ableton-wine/live.log with the +# rest of stderr; tools/wm-capture.sh records the host-side snapshot. +if [ "${ABLETON_WM_TRACE:-0}" = "1" ]; then export WINEDEBUG="$WINEDEBUG,+wmtrace"; fi export WINE_D3D_CONFIG="${WINE_D3D_CONFIG:-csmt=0x1}" export WINED3D_DCOMP_FORCE_FULL_REDRAW="${WINED3D_DCOMP_FORCE_FULL_REDRAW:-1}" # M4L child visibility otherwise makes winex11 detach and reattach Live's whole diff --git a/scripts/build-audit.sh b/scripts/build-audit.sh index 6179dad6..7df67cbe 100755 --- a/scripts/build-audit.sh +++ b/scripts/build-audit.sh @@ -12,7 +12,7 @@ fail() { printf '!! %s\n' "$*" >&2; exit 1; } # --- --freeze: (re)generate the frozen series manifest ------------------------ if [ "${1:-}" = --freeze ]; then - new="$(cd "$root/patches" && sha256sum 00*.patch pipeasio/*.patch)" + new="$(cd "$root/patches" && sha256sum 0*.patch pipeasio/*.patch)" if [ -f "$SERIES" ]; then say "== freeze diff (old -> new) ==" diff -u "$SERIES" <(printf '%s\n' "$new") && say " (no changes)" @@ -64,7 +64,7 @@ while read -r sum file; do sha_ok["$file"]=0 fi done < "$SERIES" -extras="$(cd "$root/patches" && ls 00*.patch pipeasio/*.patch 2>/dev/null | grep -vxF -f <(awk '{print $2}' "$SERIES") || true)" +extras="$(cd "$root/patches" && ls 0*.patch pipeasio/*.patch 2>/dev/null | grep -vxF -f <(awk '{print $2}' "$SERIES") || true)" [ -z "$extras" ] && ok "no unlisted patches" "" || bad "unlisted patches present" "$extras" # Retired numbers stay retired (renumbering would break cross-references in patch # titles and notes/); a gap is fine if documented here, a dropped patch is not. @@ -153,6 +153,22 @@ FINGERPRINTS=' 0080|ascii|lib/wine/x86_64-windows/ninput.dll|pointer_count %u 0084|ascii|lib/wine/x86_64-unix/win32u.so|WINE_DISABLE_PREFIX_FONT_SMOOTHING 0088|ascii|lib/wine/x86_64-unix/win32u.so|DesktopUIFont +0090|ascii|lib/wine/x86_64-unix/winex11.so|REQ-WMSTATE hwnd= +0090|ascii|lib/wine/x86_64-unix/win32u.so|SURFACE-CREATE hwnd= +0090|ascii|lib/wine/x86_64-windows/dxgi.dll|PRESENT-PATH hwnd= +0091|ascii|lib/wine/x86_64-unix/winex11.so|WINE_ACTIVATE_RESEND +0092|ascii|lib/wine/x86_64-unix/winex11.so|WINE_ACTIVATE_SOURCE +0093|ascii|lib/wine/x86_64-unix/winex11.so|WINE_HOST_STATE_LOCK +0094|wide|lib/wine/x86_64-windows/dxgi.dll|WINE_DCOMP_TEARDOWN +0095|wide|lib/wine/x86_64-windows/dxgi.dll|__wine_dcomp_clip_refs +0097|wide|lib/wine/x86_64-windows/dxgi.dll|__wine_dcomp_wnd_%lu_%I64x +0098|wide|lib/wine/x86_64-windows/wined3d.dll|__wine_dcomp_target_%u +0100|wide|lib/wine/x86_64-windows/dxgi.dll|WINE_DCOMP_SCOPE +0100|ascii|lib/wine/x86_64-unix/winex11.so|WINE_DCOMP_SCOPE +0101|wide|lib/wine/x86_64-windows/dxgi.dll|WINE_DCOMP_PAINT +0102|wide|lib/wine/x86_64-windows/dxgi.dll|WINE_DCOMP_VIEWABILITY +0103|wide|lib/wine/x86_64-windows/dxgi.dll|__wine_dcomp_teardown +0104|ascii|lib/wine/x86_64-unix/winex11.so|WINE_CONFIG_ROUNDING pipeasio/0001|ascii|lib/wine/x86_64-unix/pipeasio64.dll.so|pipeasio-clamp-sample-rate pipeasio/0002|ascii|lib/wine/x86_64-unix/pipeasio64.dll.so|pipeasio-midi-timebase ' @@ -165,6 +181,8 @@ STAMP_ONLY=' 0086|logic-only (pixel geometry and target policy for ClearType; adds no string literal) 0087|logic-only (smoothing resolved by source precedence; adds no string literal) 0089|logic-only (signed ClearType coverage interpolation; adds no string literal) +0096|logic-only (desktop swapchain property removed only when still owned; adds no string literal) +0099|logic-only (exclusion restricted to the present top level via GetAncestor; adds no string literal) 0002|logic-only (visible-rect gates; adds no string literal) 0004|logic-only (reentrant wpchanged state) 0005|logic-only (NC frame allowance) diff --git a/scripts/container-build.sh b/scripts/container-build.sh index bab5ca58..46fafee8 100755 --- a/scripts/container-build.sh +++ b/scripts/container-build.sh @@ -16,7 +16,7 @@ CONFIGURE_PREFIX="${INSTALL_PREFIX:?build.sh must pass INSTALL_PREFIX}" } DESTDIR="$WORK/stage" PREFIX_ROOT="$DESTDIR$CONFIGURE_PREFIX" -npatch="$(ls "$SRC"/patches/00*.patch | wc -l)" +npatch="$(ls "$SRC"/patches/0*.patch | wc -l)" echo "== [1/8] unpack pristine Wine base (giang17 d2d1-dcomp-11.13 @ 5c23dd1c) ==" mkdir -p "$WORK/wine-src" @@ -34,7 +34,7 @@ git -c user.email=build@localhost -c user.name=dist commit -q -m "base 5c23dd1c" # The series ships without From:/Date: mail headers; git am refuses to commit # with an empty author, so supply a fixed neutral ident (fixed date keeps the # apply reproducible). Patches that still carry headers keep their own. -for p in "$SRC"/patches/00*.patch; do +for p in "$SRC"/patches/0*.patch; do if head -8 "$p" | grep -q '^From: '; then git -c user.email=build@localhost -c user.name=dist am --3way "$p" else @@ -206,7 +206,7 @@ echo " PipeASIO: PE $pipeasio_pe_sha / Unix $pipeasio_unix_sha" echo "== [6/8] package ==" # Stamp per-patch sha256s into the tree; build-audit.sh diffs this against patches/SERIES.sha256. stack_stamp="$PREFIX_ROOT/ABLETON-WINE-PATCH-STACK.txt" -( cd "$SRC/patches" && sha256sum 00*.patch pipeasio/*.patch ) > "$stack_stamp" +( cd "$SRC/patches" && sha256sum 0*.patch pipeasio/*.patch ) > "$stack_stamp" stack_sha="$(sha256sum "$stack_stamp" | awk '{print $1}')" build_info="$PREFIX_ROOT/ABLETON-WINE-BUILD-INFO.txt" { diff --git a/tools/wm-capture.sh b/tools/wm-capture.sh new file mode 100755 index 00000000..959cbc23 --- /dev/null +++ b/tools/wm-capture.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Capture the host-side window state needed to classify a stuck, missing, +# or misbehaving window (for example a Trial window that will not close): +# tools/wm-capture.sh [outdir] +# +# Records, for every managed X client: geometry, map/viewable and +# override-redirect state, WM_CLASS/WM_NAME, WM_TRANSIENT_FOR, group +# leader, _NET_WM_WINDOW_TYPE, _MOTIF_WM_HINTS, WM_STATE, _NET_WM_STATE +# and _NET_WM_DESKTOP - plus the root window's active-window/desktop +# state, the window manager's identity, monitor layout, and the complete +# window tree (which also lists override-redirect windows the client +# list omits). Run it WHILE the problem is on screen. +# +# Pair it with the Wine-side transition trace (patch 0090), which lands +# in ~/.log/ableton-wine/live.log: +# env ABLETON_WM_TRACE=1 ableton-live + +set -uo pipefail + +OUT="${1:-$PWD/wm-capture-$(date +%Y%m%dT%H%M%S)}" + +command -v xprop >/dev/null || { echo "!! xprop not found (install xorg-xprop)"; exit 1; } +[ -n "${DISPLAY:-}" ] || { echo "!! DISPLAY is not set (X11/XWayland session required)"; exit 1; } +# Capture what the installed tools allow: xprop alone still records every +# property. xwininfo adds geometry/map state and the full window tree. +HAVE_XWININFO=1 +command -v xwininfo >/dev/null || { HAVE_XWININFO=0; echo "!! xwininfo not found (install xorg-xwininfo); capturing properties only"; } + +mkdir -p "$OUT/windows" || exit 1 +echo "==> output: $OUT" + +# --- session environment ------------------------------------------------------ +{ + echo "date: $(date -Is)" + echo "DISPLAY=$DISPLAY" + echo "WAYLAND_DISPLAY=${WAYLAND_DISPLAY:-}" + echo "XDG_SESSION_TYPE=${XDG_SESSION_TYPE:-}" + echo "XDG_CURRENT_DESKTOP=${XDG_CURRENT_DESKTOP:-}" + # Under a Wayland compositor without a built-in XWayland WM, note the + # xwayland-satellite version: 0.8.2 misclassifies some transient dialogs + # as popups (xwayland-satellite issue 470). + if command -v xwayland-satellite >/dev/null; then + echo "xwayland-satellite: $(xwayland-satellite --version 2>&1 | head -1)" + fi + pgrep -a -f 'xwayland-satellite|Xwayland' 2>/dev/null | sed 's/^/proc: /' +} > "$OUT/env.txt" + +command -v xrandr >/dev/null && xrandr --query > "$OUT/monitors.txt" 2>&1 + +# --- root window: WM identity, active window, desktops, supported hints ------- +{ + xprop -root _NET_SUPPORTING_WM_CHECK _NET_ACTIVE_WINDOW _NET_CURRENT_DESKTOP \ + _NET_NUMBER_OF_DESKTOPS _NET_CLIENT_LIST _NET_CLIENT_LIST_STACKING + wmwin="$(xprop -root _NET_SUPPORTING_WM_CHECK 2>/dev/null | grep -o '0x[0-9a-f]*' | head -1)" + [ -n "$wmwin" ] && { echo "--- WM check window $wmwin ---"; xprop -id "$wmwin" _NET_WM_NAME; } + echo "--- _NET_SUPPORTED ---" + xprop -root -notype _NET_SUPPORTED +} > "$OUT/root.txt" 2>&1 + +# --- complete tree (includes override-redirect windows) ----------------------- +[ "$HAVE_XWININFO" = 1 ] && xwininfo -root -tree > "$OUT/tree.txt" 2>&1 + +# --- per-window detail for every managed client ------------------------------- +ids="$(xprop -root _NET_CLIENT_LIST 2>/dev/null | grep -o '0x[0-9a-f]*')" +count=0 +for id in $ids; do + { + echo "=== $id ===" + [ "$HAVE_XWININFO" = 1 ] && xwininfo -id "$id" -stats -wm + echo "--- properties ---" + xprop -id "$id" + } > "$OUT/windows/$id.txt" 2>&1 + count=$((count + 1)) +done +echo "==> captured $count managed windows, root state, and the window tree" +echo "==> attach the whole directory to the report: $OUT"