From 7b331233948e850f0972becfe3ee394a40481a87 Mon Sep 17 00:00:00 2001 From: Shibco Date: Tue, 11 Aug 2026 20:59:42 +0200 Subject: [PATCH 01/11] Add a wmtrace window-state transition channel (patch 0090) The channel logs one line per transition across winex11, win32u and dxgi: WM requests with their X serials, host acknowledgements, the state handed back to Win32, surface creation, fullscreen normalization, DComp reblit decisions and each swapchain's GL/GDI present choice. tools/wm-capture.sh records the matching host-side snapshot, and the troubleshooting guide now walks reporters through both captures, including the xwayland-satellite 0.8.2 downgrade. --- CHANGELOG.md | 8 + TROUBLESHOOTING.md | 34 +++ ...xgi-add-a-compact-wmtrace-state-tran.patch | 269 ++++++++++++++++++ patches/BASE.txt | 12 +- patches/SERIES.sha256 | 1 + scripts/ableton-live | 9 +- scripts/build-audit.sh | 3 + tools/wm-capture.sh | 73 +++++ 8 files changed, 407 insertions(+), 2 deletions(-) create mode 100644 patches/0090-winex11-win32u-dxgi-add-a-compact-wmtrace-state-tran.patch create mode 100755 tools/wm-capture.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f2aedcf..22edc233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +- 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/BASE.txt b/patches/BASE.txt index f2e4e1a5..79fcdc7e 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 82 files, numbered 0001 through 0090. 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. @@ -371,3 +371,13 @@ 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. diff --git a/patches/SERIES.sha256 b/patches/SERIES.sha256 index 0fe31efc..2b05d583 100644 --- a/patches/SERIES.sha256 +++ b/patches/SERIES.sha256 @@ -79,5 +79,6 @@ 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 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..2a50497e 100755 --- a/scripts/ableton-live +++ b/scripts/ableton-live @@ -12,7 +12,9 @@ # 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), +# 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 +46,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..2a4d86df 100755 --- a/scripts/build-audit.sh +++ b/scripts/build-audit.sh @@ -153,6 +153,9 @@ 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= 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 ' diff --git a/tools/wm-capture.sh b/tools/wm-capture.sh new file mode 100755 index 00000000..2ef76c8a --- /dev/null +++ b/tools/wm-capture.sh @@ -0,0 +1,73 @@ +#!/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; } +command -v xwininfo >/dev/null || { echo "!! xwininfo not found (install xorg-xwininfo)"; exit 1; } +[ -n "${DISPLAY:-}" ] || { echo "!! DISPLAY is not set (X11/XWayland session required)"; exit 1; } + +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) ----------------------- +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 ===" + 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" From 3ca335509c9842e3a244695db965895dc9005f64 Mon Sep 17 00:00:00 2001 From: Shibco Date: Tue, 11 Aug 2026 22:05:55 +0200 Subject: [PATCH 02/11] wm-capture: degrade to xprop-only capture when xwininfo is missing --- tools/wm-capture.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tools/wm-capture.sh b/tools/wm-capture.sh index 2ef76c8a..959cbc23 100755 --- a/tools/wm-capture.sh +++ b/tools/wm-capture.sh @@ -19,9 +19,12 @@ 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; } -command -v xwininfo >/dev/null || { echo "!! xwininfo not found (install xorg-xwininfo)"; exit 1; } +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" @@ -55,7 +58,7 @@ command -v xrandr >/dev/null && xrandr --query > "$OUT/monitors.txt" 2>&1 } > "$OUT/root.txt" 2>&1 # --- complete tree (includes override-redirect windows) ----------------------- -xwininfo -root -tree > "$OUT/tree.txt" 2>&1 +[ "$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]*')" @@ -63,7 +66,7 @@ count=0 for id in $ids; do { echo "=== $id ===" - xwininfo -id "$id" -stats -wm + [ "$HAVE_XWININFO" = 1 ] && xwininfo -id "$id" -stats -wm echo "--- properties ---" xprop -id "$id" } > "$OUT/windows/$id.txt" 2>&1 From 4dfd932b8a2be57037cd604092d377f8c579f6df Mon Sep 17 00:00:00 2001 From: Shibco Date: Wed, 12 Aug 2026 00:54:21 +0200 Subject: [PATCH 03/11] Stop repeated window-activation requests (patches 0091, 0092) --- CHANGELOG.md | 6 ++ ...eat-_NET_ACTIVE_WINDOW-requests-keep.patch | 66 +++++++++++++++++++ ..._ACTIVATE_SOURCE-choose-the-EWMH-act.patch | 47 +++++++++++++ patches/BASE.txt | 14 +++- patches/SERIES.sha256 | 2 + scripts/ableton-live | 3 + scripts/build-audit.sh | 2 + 7 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 patches/0091-winex11-skip-repeat-_NET_ACTIVE_WINDOW-requests-keep.patch create mode 100644 patches/0092-winex11-let-WINE_ACTIVATE_SOURCE-choose-the-EWMH-act.patch diff --git a/CHANGELOG.md b/CHANGELOG.md index 22edc233..2672e515 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +- 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 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/BASE.txt b/patches/BASE.txt index 79fcdc7e..981b1942 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 82 files, numbered 0001 through 0090. +The current Wine series contains 84 files, numbered 0001 through 0092. 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. @@ -381,3 +381,15 @@ Apply them in sequence with the rest of the series. `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. diff --git a/patches/SERIES.sha256 b/patches/SERIES.sha256 index 2b05d583..bc49092f 100644 --- a/patches/SERIES.sha256 +++ b/patches/SERIES.sha256 @@ -80,5 +80,7 @@ ee36bbf26e5ea22b8d6ecd5344cb72fc7883ac59ed733661dfb4e73f773fc18f 0087-win32u-re 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 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 2a50497e..2e0825b7 100755 --- a/scripts/ableton-live +++ b/scripts/ableton-live @@ -13,6 +13,9 @@ # 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_ACTIVATE_RESEND=1 (restore repeated window-activation requests), +# 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 diff --git a/scripts/build-audit.sh b/scripts/build-audit.sh index 2a4d86df..f9aa7a5a 100755 --- a/scripts/build-audit.sh +++ b/scripts/build-audit.sh @@ -156,6 +156,8 @@ FINGERPRINTS=' 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 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 ' From b0051656acba2ae3bc4d1a42a56e0f302f3a67d3 Mon Sep 17 00:00:00 2001 From: Shibco Date: Wed, 12 Aug 2026 00:55:07 +0200 Subject: [PATCH 04/11] Lock host window state while win32u applies it (patch 0093) --- CHANGELOG.md | 6 + ...ock-host-window-state-updates-while-.patch | 175 ++++++++++++++++++ patches/BASE.txt | 10 +- patches/SERIES.sha256 | 1 + scripts/ableton-live | 1 + scripts/build-audit.sh | 1 + 6 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 patches/0093-win32u-winex11-lock-host-window-state-updates-while-.patch diff --git a/CHANGELOG.md b/CHANGELOG.md index 2672e515..1625b7e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +- 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 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/BASE.txt b/patches/BASE.txt index 981b1942..c842e3dc 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 84 files, numbered 0001 through 0092. +The current Wine series contains 85 files, numbered 0001 through 0093. 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. @@ -393,3 +393,11 @@ Apply them in sequence with the rest of the series. 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. diff --git a/patches/SERIES.sha256 b/patches/SERIES.sha256 index bc49092f..076cb796 100644 --- a/patches/SERIES.sha256 +++ b/patches/SERIES.sha256 @@ -82,5 +82,6 @@ efec6729f1663efe78c7efa941b826b2a28d219bf6faa974294a3bfa854b84f5 0088-win32u-al 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 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 2e0825b7..3ea24d36 100755 --- a/scripts/ableton-live +++ b/scripts/ableton-live @@ -14,6 +14,7 @@ # WINE_WIN32_FULLSCREEN_CLASS=off (disable Live fullscreen normalization), # 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_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 diff --git a/scripts/build-audit.sh b/scripts/build-audit.sh index f9aa7a5a..71db324e 100755 --- a/scripts/build-audit.sh +++ b/scripts/build-audit.sh @@ -158,6 +158,7 @@ FINGERPRINTS=' 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 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 ' From e98e9a56d046ef49d9548d25b2cc039209c4eb58 Mon Sep 17 00:00:00 2001 From: Shibco Date: Wed, 12 Aug 2026 00:56:01 +0200 Subject: [PATCH 05/11] Tear down DComp windows completely from any thread (patch 0094) --- CHANGELOG.md | 7 + ...window-teardown-complete-idempotent-.patch | 306 ++++++++++++++++++ patches/BASE.txt | 14 +- patches/SERIES.sha256 | 1 + scripts/ableton-live | 1 + scripts/build-audit.sh | 1 + 6 files changed, 329 insertions(+), 1 deletion(-) create mode 100644 patches/0094-dxgi-make-DComp-window-teardown-complete-idempotent-.patch diff --git a/CHANGELOG.md b/CHANGELOG.md index 1625b7e0..99a3949e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +- 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 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/BASE.txt b/patches/BASE.txt index c842e3dc..083e73cb 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 85 files, numbered 0001 through 0093. +The current Wine series contains 86 files, numbered 0001 through 0094. 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. @@ -401,3 +401,15 @@ Apply them in sequence with the rest of the series. 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. diff --git a/patches/SERIES.sha256 b/patches/SERIES.sha256 index 076cb796..82285201 100644 --- a/patches/SERIES.sha256 +++ b/patches/SERIES.sha256 @@ -83,5 +83,6 @@ d15fbf9a1bb079e972135c252642b2e10029125a67ff52f3584ff063045f2807 0090-winex11-w 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 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 3ea24d36..5c3900c9 100755 --- a/scripts/ableton-live +++ b/scripts/ableton-live @@ -15,6 +15,7 @@ # 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_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 diff --git a/scripts/build-audit.sh b/scripts/build-audit.sh index 71db324e..43f12269 100755 --- a/scripts/build-audit.sh +++ b/scripts/build-audit.sh @@ -159,6 +159,7 @@ FINGERPRINTS=' 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 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 ' From 6edfd81cb36dcbcb2cd38196ccc7d414457b1662 Mon Sep 17 00:00:00 2001 From: Shibco Date: Wed, 12 Aug 2026 00:56:24 +0200 Subject: [PATCH 06/11] Let the patch pipeline reach four-digit patch numbers --- patches/BASE.txt | 4 ++-- scripts/build-audit.sh | 4 ++-- scripts/container-build.sh | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/patches/BASE.txt b/patches/BASE.txt index 083e73cb..7f9b5647 100644 --- a/patches/BASE.txt +++ b/patches/BASE.txt @@ -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. diff --git a/scripts/build-audit.sh b/scripts/build-audit.sh index 43f12269..6b2e5b3b 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. 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" { From 2793d19468878d69e82a96992d913a32b6604795 Mon Sep 17 00:00:00 2001 From: Shibco Date: Wed, 12 Aug 2026 00:56:37 +0200 Subject: [PATCH 07/11] Stop plugin views interfering with each other's windows (patches 0095-0097) --- CHANGELOG.md | 5 + ...count-the-parent-WS_CLIPCHILDREN-bit.patch | 77 +++++++++++ ...-the-desktop-swapchain-property-if-w.patch | 29 +++++ ...pace-the-composition-swapchain-windo.patch | 122 ++++++++++++++++++ patches/BASE.txt | 21 ++- patches/SERIES.sha256 | 3 + scripts/build-audit.sh | 3 + 7 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 patches/0095-dxgi-reference-count-the-parent-WS_CLIPCHILDREN-bit.patch create mode 100644 patches/0096-dxgi-only-remove-the-desktop-swapchain-property-if-w.patch create mode 100644 patches/0097-dxgi-dcomp-namespace-the-composition-swapchain-windo.patch diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a3949e..01ec6b50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- 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 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..f05212cc --- /dev/null +++ b/patches/0095-dxgi-reference-count-the-parent-WS_CLIPCHILDREN-bit.patch @@ -0,0 +1,77 @@ +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. +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 7cc0d08..41b867c 100644 +--- a/dlls/dxgi/factory.c ++++ b/dlls/dxgi/factory.c +@@ -635,17 +635,32 @@ 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); ++ } + } + } + +@@ -1291,10 +1306,21 @@ static LRESULT CALLBACK dcomp_swapchain_wndproc(HWND hwnd, UINT msg, WPARAM wpar + HWND phwnd = GetParent(target_hwnd); + if (phwnd) + { ++ LONG_PTR refs = (LONG_PTR)GetPropW(phwnd, L"__wine_dcomp_clip_refs"); + LONG pstyle = GetWindowLongW(phwnd, GWL_STYLE); +- if (!(pstyle & WS_CLIPCHILDREN)) ++ ++ if (!dcomp_teardown_legacy() && refs > 0) ++ { ++ /* We already own the bit for another target on this ++ * parent: count this one too so the style survives ++ * the first teardown. */ ++ SetPropW(phwnd, L"__wine_dcomp_clip_refs", (HANDLE)(refs + 1)); ++ SetPropW(target_hwnd, L"__wine_dcomp_parent_clip", (HANDLE)phwnd); ++ } ++ else if (!(pstyle & WS_CLIPCHILDREN)) + { + SetWindowLongW(phwnd, GWL_STYLE, pstyle | WS_CLIPCHILDREN); ++ SetPropW(phwnd, L"__wine_dcomp_clip_refs", (HANDLE)1); + SetPropW(target_hwnd, L"__wine_dcomp_parent_clip", (HANDLE)phwnd); + } + } 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/BASE.txt b/patches/BASE.txt index 7f9b5647..7dc63321 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 86 files, numbered 0001 through 0094. +The current Wine series contains 89 files, numbered 0001 through 0097. 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. @@ -413,3 +413,22 @@ Apply them in sequence with the rest of the series. 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. diff --git a/patches/SERIES.sha256 b/patches/SERIES.sha256 index 82285201..c0cf6562 100644 --- a/patches/SERIES.sha256 +++ b/patches/SERIES.sha256 @@ -84,5 +84,8 @@ bf46e82e08c06410d9bc967a85ed68ef07d92a306128f71055c41e4427ab0b56 0091-winex11-s 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 +0a45d3ce6da786c76fe5c64406d75fb028907c3fb70084a61515132cc945c3de 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 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/build-audit.sh b/scripts/build-audit.sh index 6b2e5b3b..6a6ab254 100755 --- a/scripts/build-audit.sh +++ b/scripts/build-audit.sh @@ -160,6 +160,8 @@ FINGERPRINTS=' 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 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 ' @@ -172,6 +174,7 @@ 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) 0002|logic-only (visible-rect gates; adds no string literal) 0004|logic-only (reentrant wpchanged state) 0005|logic-only (NC frame allowance) From 4894a0407f89cbc11773aa08ac5bc9c13a84a2ff Mon Sep 17 00:00:00 2001 From: Shibco Date: Wed, 12 Aug 2026 00:56:48 +0200 Subject: [PATCH 08/11] Clip composition targets out of GDI presents (patches 0098, 0099) --- CHANGELOG.md | 5 + ...clude-composition-targets-from-GDI-p.patch | 151 ++++++++++++++++++ ...lude-dcomp-targets-that-share-our-to.patch | 48 ++++++ patches/BASE.txt | 16 +- patches/SERIES.sha256 | 2 + scripts/build-audit.sh | 2 + 6 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 patches/0098-dcomp-wined3d-exclude-composition-targets-from-GDI-p.patch create mode 100644 patches/0099-wined3d-only-exclude-dcomp-targets-that-share-our-to.patch diff --git a/CHANGELOG.md b/CHANGELOG.md index 01ec6b50..fe4c444e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- 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 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..9bd9e764 --- /dev/null +++ b/patches/0098-dcomp-wined3d-exclude-composition-targets-from-GDI-p.patch @@ -0,0 +1,151 @@ +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. + +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..fabb97a 100644 +--- a/dlls/dcomp/device.c ++++ b/dlls/dcomp/device.c +@@ -1411,6 +1411,42 @@ 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 && free_slot >= 0) ++ { ++ swprintf(prop, ARRAY_SIZE(prop), L"__wine_dcomp_target_%u", free_slot); ++ SetPropW(desktop, prop, (HANDLE)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 +1533,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 +2309,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..fc59728 100644 +--- a/dlls/wined3d/swapchain.c ++++ b/dlls/wined3d/swapchain.c +@@ -28,6 +28,28 @@ 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; ++ ++ if (IsWindowVisible(child) && GetPropW(child, L"__wine_dcomp_child_count") ++ && 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 +1144,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/BASE.txt b/patches/BASE.txt index 7dc63321..04ad7aff 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 89 files, numbered 0001 through 0097. +The current Wine series contains 91 files, numbered 0001 through 0099. 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. @@ -432,3 +432,17 @@ Apply them in sequence with the rest of the series. 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. diff --git a/patches/SERIES.sha256 b/patches/SERIES.sha256 index c0cf6562..2b423172 100644 --- a/patches/SERIES.sha256 +++ b/patches/SERIES.sha256 @@ -87,5 +87,7 @@ dd398cb167a7ef56744c2f009dbb130ba98e13107cc7f96de5cc43c28b0626c0 0094-dxgi-make 0a45d3ce6da786c76fe5c64406d75fb028907c3fb70084a61515132cc945c3de 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 +56f170f6c07dcf8ea47c3e735083d8b821c6841b58ad187722601075bfb3236a 0098-dcomp-wined3d-exclude-composition-targets-from-GDI-p.patch +372a7685948f52d07dc5678336077bd4e800d55eaeaa72d9764340421b544670 0099-wined3d-only-exclude-dcomp-targets-that-share-our-to.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/build-audit.sh b/scripts/build-audit.sh index 6a6ab254..5966ba8c 100755 --- a/scripts/build-audit.sh +++ b/scripts/build-audit.sh @@ -162,6 +162,7 @@ FINGERPRINTS=' 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 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 ' @@ -175,6 +176,7 @@ STAMP_ONLY=' 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) From b104bfb3b3632f1f848fcc1a04fe1823a564d9a5 Mon Sep 17 00:00:00 2001 From: Shibco Date: Wed, 12 Aug 2026 00:57:04 +0200 Subject: [PATCH 09/11] Scope, gate and harden DComp window lifecycle (patches 0100-0103) --- CHANGELOG.md | 12 + ...pe-DComp-popup-inference-by-thread-a.patch | 206 ++++++++++++++++++ ...mp-paint-and-erase-only-with-a-valid.patch | 95 ++++++++ ...e-parked-reblits-on-host-viewability.patch | 91 ++++++++ ...-teardown-on-a-registered-message-no.patch | 163 ++++++++++++++ patches/BASE.txt | 37 +++- patches/SERIES.sha256 | 4 + scripts/ableton-live | 3 + scripts/build-audit.sh | 5 + 9 files changed, 615 insertions(+), 1 deletion(-) create mode 100644 patches/0100-dxgi-winex11-scope-DComp-popup-inference-by-thread-a.patch create mode 100644 patches/0101-dxgi-consume-DComp-paint-and-erase-only-with-a-valid.patch create mode 100644 patches/0102-dxgi-winex11-gate-parked-reblits-on-host-viewability.patch create mode 100644 patches/0103-dxgi-carry-DComp-teardown-on-a-registered-message-no.patch diff --git a/CHANGELOG.md b/CHANGELOG.md index fe4c444e..afb9c291 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ## Unreleased +- 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. + +- 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 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..774555c2 --- /dev/null +++ b/patches/0100-dxgi-winex11-scope-DComp-popup-inference-by-thread-a.patch @@ -0,0 +1,206 @@ +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. + +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 7e28076..2856e5e 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,17 +627,59 @@ 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) ++/* 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) ++{ ++ DWORD tid; ++ int i; ++ ++ if (dcomp_scope_global()) ++ return dcomp_popup_stack_depth > 0 ? dcomp_popup_stack[dcomp_popup_stack_depth - 1] : NULL; ++ ++ 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])) ++ return dcomp_popup_stack[i]; ++ } ++ return NULL; ++} ++ ++/* 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; ++ DWORD tid; ++ int i; ++ ++ if (dcomp_scope_global()) ++ return dcomp_subclassed_target_count > 0; ++ ++ tid = GetWindowThreadProcessId(target, NULL); ++ for (i = 0; i < dcomp_popup_stack_depth; ++i) ++ { ++ if (dcomp_popup_stack_tid[i] == tid && IsWindow(dcomp_popup_stack[i])) ++ return TRUE; ++ } ++ return FALSE; + } + + static void dcomp_popup_stack_push(HWND hwnd) + { + 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); + } +@@ -626,6 +694,8 @@ static void dcomp_popup_stack_remove(HWND 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; + } +@@ -1228,14 +1298,14 @@ static LRESULT CALLBACK dcomp_swapchain_wndproc(HWND hwnd, UINT msg, WPARAM wpar + 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) ++ if (dcomp_thread_has_target(target_hwnd)) + is_popup_mode = TRUE; + + if (is_popup_mode) +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..3a3b2e91 --- /dev/null +++ b/patches/0102-dxgi-winex11-gate-parked-reblits-on-host-viewability.patch @@ -0,0 +1,91 @@ +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. + +diff --git a/dlls/dxgi/factory.c b/dlls/dxgi/factory.c +index c2fb9f6..0b629c0 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..c60c4f2 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; +@@ -2548,6 +2555,9 @@ 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 ); ++ ++ NtUserSetProp( data->hwnd, x11_viewable_prop, ++ (HANDLE)(UINT_PTR)(data->current_state.wm_state == NormalState ? 1 : 2) ); + } + + 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..753ca366 --- /dev/null +++ b/patches/0103-dxgi-carry-DComp-teardown-on-a-registered-message-no.patch @@ -0,0 +1,163 @@ +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. + +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 878e101..3cea4be 100644 +--- a/dlls/dxgi/factory.c ++++ b/dlls/dxgi/factory.c +@@ -390,7 +390,20 @@ 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; ++} + /* 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 +@@ -855,6 +868,12 @@ static LRESULT CALLBACK dcomp_popup_wndproc(HWND hwnd, UINT msg, WPARAM wparam, + { + WNDPROC orig = (WNDPROC)GetPropW(hwnd, L"__wine_dcomp_orig_wndproc"); + ++ if (msg == dcomp_teardown_message()) ++ { ++ dcomp_teardown_window(hwnd); ++ return 0; ++ } ++ + switch (msg) + { + case WM_ERASEBKGND: +@@ -895,10 +914,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) + { +@@ -971,6 +986,12 @@ static LRESULT CALLBACK dcomp_target_wndproc(HWND hwnd, UINT msg, WPARAM wparam, + { + WNDPROC orig = (WNDPROC)GetPropW(hwnd, L"__wine_dcomp_orig_wndproc"); + ++ if (msg == dcomp_teardown_message()) ++ { ++ dcomp_teardown_window(hwnd); ++ return 0; ++ } ++ + switch (msg) + { + case WM_ERASEBKGND: +@@ -1027,10 +1048,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) + { +@@ -1210,15 +1227,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 (msg == dcomp_teardown_message()) ++ { ++ 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 a180e16..cb6413d 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/BASE.txt b/patches/BASE.txt index 04ad7aff..5164b69b 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 91 files, numbered 0001 through 0099. +The current Wine series contains 95 files, numbered 0001 through 0103. 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. @@ -446,3 +446,38 @@ Apply them in sequence with the rest of the series. 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. diff --git a/patches/SERIES.sha256 b/patches/SERIES.sha256 index 2b423172..906c913a 100644 --- a/patches/SERIES.sha256 +++ b/patches/SERIES.sha256 @@ -89,5 +89,9 @@ dd398cb167a7ef56744c2f009dbb130ba98e13107cc7f96de5cc43c28b0626c0 0094-dxgi-make 940963327f288c87b723a82213363e79fa5399afb76bea7ba293283f9f5f843b 0097-dxgi-dcomp-namespace-the-composition-swapchain-windo.patch 56f170f6c07dcf8ea47c3e735083d8b821c6841b58ad187722601075bfb3236a 0098-dcomp-wined3d-exclude-composition-targets-from-GDI-p.patch 372a7685948f52d07dc5678336077bd4e800d55eaeaa72d9764340421b544670 0099-wined3d-only-exclude-dcomp-targets-that-share-our-to.patch +364c66cd1e94dd410f3030cdd981ff6230dd979edfb93966ffcbd9c687b1a5a6 0100-dxgi-winex11-scope-DComp-popup-inference-by-thread-a.patch +b16a8b135278dfc02397c32aea8751d20dfb900f4ae084c3824ad63c5650bb68 0101-dxgi-consume-DComp-paint-and-erase-only-with-a-valid.patch +972d123fbc2bcc039aa5333d34443650f4a5ab3b0fcf560d0acbc4c7ed4d97d7 0102-dxgi-winex11-gate-parked-reblits-on-host-viewability.patch +904e9bf116e9b641ca7f495d848bd15fd53f3b15757a14a88af7b27aeee1fe81 0103-dxgi-carry-DComp-teardown-on-a-registered-message-no.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 5c3900c9..5ee43330 100755 --- a/scripts/ableton-live +++ b/scripts/ableton-live @@ -16,6 +16,9 @@ # 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_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 diff --git a/scripts/build-audit.sh b/scripts/build-audit.sh index 5966ba8c..3b22ae26 100755 --- a/scripts/build-audit.sh +++ b/scripts/build-audit.sh @@ -163,6 +163,11 @@ FINGERPRINTS=' 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 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 ' From a89864acee7a4734e68c84f9df6feba56961cc37 Mon Sep 17 00:00:00 2001 From: Shibco Date: Wed, 12 Aug 2026 01:24:48 +0200 Subject: [PATCH 10/11] Tie window-geometry aliases to Wine's own configure requests (patch 0104) --- CHANGELOG.md | 6 + ...ig-rounding-aliases-to-Wine-s-own-co.patch | 154 ++++++++++++++++++ patches/BASE.txt | 16 +- patches/SERIES.sha256 | 1 + scripts/ableton-live | 1 + scripts/build-audit.sh | 1 + 6 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 patches/0104-winex11-tie-config-rounding-aliases-to-Wine-s-own-co.patch diff --git a/CHANGELOG.md b/CHANGELOG.md index afb9c291..92cdeebd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## 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. + - 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 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..e2288f87 --- /dev/null +++ b/patches/0104-winex11-tie-config-rounding-aliases-to-Wine-s-own-co.patch @@ -0,0 +1,154 @@ +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. + +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 c60c4f2..672cf6b 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 ); + } +@@ -1784,11 +1801,20 @@ static void activate_config_rounding( struct x11drv_win_data *data, const RECT * + 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 +1828,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 +1847,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 +1866,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; +@@ -1835,6 +1885,7 @@ static BOOL track_config_rounding_move( struct x11drv_win_data *data, const RECT + data->config_rounding_serial = 0; + data->config_rounding_settle_serial = 0; + data->config_rounding_generation = data->config_generation; ++ data->config_rounding_dpi = dpi; + data->config_rounding_win32_rect = win32_rect; + activate_config_rounding( data, host_rect ); + TRACE( "window %p/%lx tracking host move as config rounding alias %s -> %s generation %lu\n", +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 5164b69b..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 95 files, numbered 0001 through 0103. +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. @@ -481,3 +481,17 @@ Apply them in sequence with the rest of the series. 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 906c913a..9e3cc2a0 100644 --- a/patches/SERIES.sha256 +++ b/patches/SERIES.sha256 @@ -93,5 +93,6 @@ dd398cb167a7ef56744c2f009dbb130ba98e13107cc7f96de5cc43c28b0626c0 0094-dxgi-make b16a8b135278dfc02397c32aea8751d20dfb900f4ae084c3824ad63c5650bb68 0101-dxgi-consume-DComp-paint-and-erase-only-with-a-valid.patch 972d123fbc2bcc039aa5333d34443650f4a5ab3b0fcf560d0acbc4c7ed4d97d7 0102-dxgi-winex11-gate-parked-reblits-on-host-viewability.patch 904e9bf116e9b641ca7f495d848bd15fd53f3b15757a14a88af7b27aeee1fe81 0103-dxgi-carry-DComp-teardown-on-a-registered-message-no.patch +4a90a710be796a9184a15179b708a040188a0ae472655c3534d96ba159a585de 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 5ee43330..8e83a4a3 100755 --- a/scripts/ableton-live +++ b/scripts/ableton-live @@ -19,6 +19,7 @@ # 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 diff --git a/scripts/build-audit.sh b/scripts/build-audit.sh index 3b22ae26..7df67cbe 100755 --- a/scripts/build-audit.sh +++ b/scripts/build-audit.sh @@ -168,6 +168,7 @@ FINGERPRINTS=' 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 ' From b9a4e5f3eebf7bef2c3fa3bd8ff26385f8f685bf Mon Sep 17 00:00:00 2001 From: Shibco Date: Wed, 12 Aug 2026 12:39:25 +0200 Subject: [PATCH 11/11] Correct six patches the graphics-hardening review found defective The review of patches 0090 to 0104 confirmed six defects. Three left a patch claiming a fix it did not deliver, and one reversed a fix the series already shipped. 0104 stamped the rounding alias with its DPI at two of the three call sites that create one. The third is 0042's own sub-scale alias, so every reader compared 0 against the real DPI and threw the alias away. The rounded host rectangle then went back to Win32, the application re-applied its own size, and the two chased each other, which is the resize jitter 0042 exists to stop. The stamp moves into activate_config_rounding, the only place that turns an alias active. 0098 gated its exclusion on __wine_dcomp_child_count, which dcomp writes only at Commit and writes as 0 when the root visual has no child visuals. Registry membership is now the whole test. The slot claim also reads itself back, because the desktop property list spans the window station and two processes could claim one slot. 0102 published __wine_x11_viewable on the success path alone, while handle_state_change records the new state before refusing an old-serial acknowledgement. A mapped window could keep a stale unviewable mark and freeze every parked reblit beneath it. Both exits publish now. 0095 counted a clip owner per SET_TARGET message rather than per target. The composition window is keyed on the swapchain, so a client that recreates its swapchain rebinds the same target through a new one, and that repeat raised the count without adding an owner that could release it. Triage of the review's plausible set fixed three more. 0103 compared an incoming message against an unregistered id, and WM_NULL is 0. 0100 mutated the popup stack and its thread-id array from several UI threads without a lock, and let a repeat bind subclass a target twice, which would have handed our own proc back as the original. The changelog names the two escape hatches its entries had omitted. Every patch applies to the pristine base with no fuzz, the four touched modules compile clean, and the build audit passes at 127 checks. Live 12 Suite 12.4.3 runs on the result. None of the six symptoms has a deliberate before-and-after test yet. --- CHANGELOG.md | 9 +- ...count-the-parent-WS_CLIPCHILDREN-bit.patch | 80 ++++++++---- ...clude-composition-targets-from-GDI-p.patch | 65 ++++++++-- ...pe-DComp-popup-inference-by-thread-a.patch | 117 +++++++++++++++--- ...e-parked-reblits-on-host-viewability.patch | 48 ++++++- ...-teardown-on-a-registered-message-no.patch | 37 ++++-- ...ig-rounding-aliases-to-Wine-s-own-co.patch | 39 +++--- patches/SERIES.sha256 | 12 +- 8 files changed, 318 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92cdeebd..024c84f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,17 @@ 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. + 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. + 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 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 index f05212cc..c418cd55 100644 --- a/patches/0095-dxgi-reference-count-the-parent-WS_CLIPCHILDREN-bit.patch +++ b/patches/0095-dxgi-reference-count-the-parent-WS_CLIPCHILDREN-bit.patch @@ -7,15 +7,21 @@ 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. +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 7cc0d08..41b867c 100644 +index 07eca32..476c1eb 100644 --- a/dlls/dxgi/factory.c +++ b/dlls/dxgi/factory.c -@@ -635,17 +635,32 @@ static void dcomp_popup_stack_remove(HWND hwnd) +@@ -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 @@ -49,29 +55,57 @@ index 7cc0d08..41b867c 100644 + 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); } -@@ -1291,10 +1306,21 @@ static LRESULT CALLBACK dcomp_swapchain_wndproc(HWND hwnd, UINT msg, WPARAM wpar + 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_PTR refs = (LONG_PTR)GetPropW(phwnd, L"__wine_dcomp_clip_refs"); - LONG pstyle = GetWindowLongW(phwnd, GWL_STYLE); +- 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 (!dcomp_teardown_legacy() && refs > 0) -+ { -+ /* We already own the bit for another target on this -+ * parent: count this one too so the style survives -+ * the first teardown. */ -+ SetPropW(phwnd, L"__wine_dcomp_clip_refs", (HANDLE)(refs + 1)); -+ SetPropW(target_hwnd, L"__wine_dcomp_parent_clip", (HANDLE)phwnd); -+ } -+ else if (!(pstyle & WS_CLIPCHILDREN)) - { - SetWindowLongW(phwnd, GWL_STYLE, pstyle | WS_CLIPCHILDREN); -+ SetPropW(phwnd, L"__wine_dcomp_clip_refs", (HANDLE)1); - 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/0098-dcomp-wined3d-exclude-composition-targets-from-GDI-p.patch b/patches/0098-dcomp-wined3d-exclude-composition-targets-from-GDI-p.patch index 9bd9e764..08c25fd5 100644 --- a/patches/0098-dcomp-wined3d-exclude-composition-targets-from-GDI-p.patch +++ b/patches/0098-dcomp-wined3d-exclude-composition-targets-from-GDI-p.patch @@ -12,14 +12,30 @@ 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..fabb97a 100644 +index 88d1adb..0377fd3 100644 --- a/dlls/dcomp/device.c +++ b/dlls/dcomp/device.c -@@ -1411,6 +1411,42 @@ static void dcomp_visual_try_reparent(struct dcomp_visual *visual) +@@ -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"; @@ -53,16 +69,37 @@ index 88d1adb..fabb97a 100644 + if (add && free_slot < 0 && (!cur || !IsWindow(cur))) + free_slot = (int)i; + } -+ if (add && free_slot >= 0) ++ 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) + { -+ swprintf(prop, ARRAY_SIZE(prop), L"__wine_dcomp_target_%u", free_slot); ++ 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 +1533,7 @@ static ULONG STDMETHODCALLTYPE dcomp_target_Release(IDCompositionTarget *iface) +@@ -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); @@ -70,7 +107,7 @@ index 88d1adb..fabb97a 100644 if ((WNDPROC)GetWindowLongPtrW(target->hwnd, GWLP_WNDPROC) == dcomp_target_wndproc) { WNDPROC orig = (WNDPROC)GetPropW(target->hwnd, dcomp_origproc_prop); -@@ -2272,6 +2309,7 @@ static HRESULT STDMETHODCALLTYPE dcomp_device_CreateTargetForHwnd(IDCompositionD +@@ -2272,6 +2330,7 @@ static HRESULT STDMETHODCALLTYPE dcomp_device_CreateTargetForHwnd(IDCompositionD } } SetPropW(hwnd, dcomp_target_prop, (HANDLE)object); @@ -79,10 +116,10 @@ index 88d1adb..fabb97a 100644 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..fc59728 100644 +index 869fe45..92f27dd 100644 --- a/dlls/wined3d/swapchain.c +++ b/dlls/wined3d/swapchain.c -@@ -28,6 +28,28 @@ WINE_DEFAULT_DEBUG_CHANNEL(d3d); +@@ -28,6 +28,34 @@ WINE_DEFAULT_DEBUG_CHANNEL(d3d); WINE_DECLARE_DEBUG_CHANNEL(d3d_perf); WINE_DECLARE_DEBUG_CHANNEL(winediag); @@ -98,8 +135,14 @@ index 869fe45..fc59728 100644 + struct dcomp_child_exclude_ctx *ctx = (struct dcomp_child_exclude_ctx *)lp; + RECT r; + -+ if (IsWindowVisible(child) && GetPropW(child, L"__wine_dcomp_child_count") -+ && GetWindowRect(child, &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); @@ -111,7 +154,7 @@ index 869fe45..fc59728 100644 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 +1144,37 @@ static void swapchain_blit_gdi(struct wined3d_swapchain *swapchain, +@@ -1122,10 +1150,37 @@ static void swapchain_blit_gdi(struct wined3d_swapchain *swapchain, } else { 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 index 774555c2..6193ddbc 100644 --- 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 @@ -14,11 +14,24 @@ 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 7e28076..2856e5e 100644 +index 9134a50..e338edd 100644 --- a/dlls/dxgi/factory.c +++ b/dlls/dxgi/factory.c @@ -580,13 +580,39 @@ LONG dcomp_subclassed_target_count; @@ -63,7 +76,7 @@ index 7e28076..2856e5e 100644 } /* Popup transient-parent stack: the open DComp targets in open order -@@ -601,17 +627,59 @@ static void dcomp_update_active_prop(void) +@@ -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]; @@ -71,6 +84,20 @@ index 7e28076..2856e5e 100644 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 @@ -78,19 +105,29 @@ index 7e28076..2856e5e 100644 + * 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()) -+ return dcomp_popup_stack_depth > 0 ? dcomp_popup_stack[dcomp_popup_stack_depth - 1] : NULL; -+ -+ 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])) -+ return dcomp_popup_stack[i]; ++ 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; ++ } ++ } + } -+ return NULL; ++ LeaveCriticalSection(&dcomp_popup_stack_cs); ++ return ret; +} + +/* Full-versus-popup mode: the first target on its own UI thread is that @@ -100,6 +137,7 @@ index 7e28076..2856e5e 100644 +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; + @@ -107,16 +145,22 @@ index 7e28076..2856e5e 100644 + 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])) -+ return TRUE; ++ { ++ ret = TRUE; ++ break; ++ } + } -+ return FALSE; ++ 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); @@ -124,17 +168,37 @@ index 7e28076..2856e5e 100644 + } else WARN("DComp popup stack full (%d), not pushing %p.\n", dcomp_popup_stack_depth, hwnd); ++ LeaveCriticalSection(&dcomp_popup_stack_cs); } -@@ -626,6 +694,8 @@ static void dcomp_popup_stack_remove(HWND hwnd) + + 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; +- return; ++ break; } -@@ -1228,14 +1298,14 @@ static LRESULT CALLBACK dcomp_swapchain_wndproc(HWND hwnd, UINT msg, WPARAM wpar + } ++ 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 @@ -148,10 +212,31 @@ index 7e28076..2856e5e 100644 dcomp_subclassed_target_count); - if (dcomp_subclassed_target_count > 0) -+ if (dcomp_thread_has_target(target_hwnd)) ++ /* 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 (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 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 index 3a3b2e91..75abb572 100644 --- a/patches/0102-dxgi-winex11-gate-parked-reblits-on-host-viewability.patch +++ b/patches/0102-dxgi-winex11-gate-parked-reblits-on-host-viewability.patch @@ -10,8 +10,15 @@ 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 c2fb9f6..0b629c0 100644 +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) @@ -62,7 +69,7 @@ index c2fb9f6..0b629c0 100644 { unsigned int w = LOWORD(dims); diff --git a/dlls/winex11.drv/window.c b/dlls/winex11.drv/window.c -index 367be02..c60c4f2 100644 +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[] = @@ -79,13 +86,44 @@ index 367be02..c60c4f2 100644 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; -@@ -2548,6 +2555,9 @@ void window_wm_state_notify( struct x11drv_win_data *data, unsigned long serial, +@@ -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 ); + -+ NtUserSetProp( data->hwnd, x11_viewable_prop, -+ (HANDLE)(UINT_PTR)(data->current_state.wm_state == NormalState ? 1 : 2) ); ++ 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 index 753ca366..a797cf17 100644 --- 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 @@ -11,7 +11,10 @@ number to its own protocol, where toolkits commonly treat it as Register a system-range message id instead, which no application defines, and post only while the target still carries the subclass -marker. +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 @@ -31,10 +34,10 @@ index 9a1b9b9..f6c242f 100644 /* IDXGISwapChain */ struct d3d11_swapchain diff --git a/dlls/dxgi/factory.c b/dlls/dxgi/factory.c -index 878e101..3cea4be 100644 +index e609648..c63242c 100644 --- a/dlls/dxgi/factory.c +++ b/dlls/dxgi/factory.c -@@ -390,7 +390,20 @@ static void STDMETHODCALLTYPE dxgi_factory_UnregisterOcclusionStatus(IWineDXGIFa +@@ -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) @@ -52,15 +55,25 @@ index 878e101..3cea4be 100644 + + 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 -@@ -855,6 +868,12 @@ static LRESULT CALLBACK dcomp_popup_wndproc(HWND hwnd, UINT msg, WPARAM wparam, +@@ -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 (msg == dcomp_teardown_message()) ++ if (dcomp_is_teardown_message(msg)) + { + dcomp_teardown_window(hwnd); + return 0; @@ -69,7 +82,7 @@ index 878e101..3cea4be 100644 switch (msg) { case WM_ERASEBKGND: -@@ -895,10 +914,6 @@ static LRESULT CALLBACK dcomp_popup_wndproc(HWND hwnd, UINT msg, WPARAM wparam, +@@ -961,10 +990,6 @@ static LRESULT CALLBACK dcomp_popup_wndproc(HWND hwnd, UINT msg, WPARAM wparam, return 0; } @@ -80,11 +93,11 @@ index 878e101..3cea4be 100644 case WM_TIMER: if (wparam == DCOMP_POPUP_REBLIT_TIMER_ID) { -@@ -971,6 +986,12 @@ static LRESULT CALLBACK dcomp_target_wndproc(HWND hwnd, UINT msg, WPARAM wparam, +@@ -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 (msg == dcomp_teardown_message()) ++ if (dcomp_is_teardown_message(msg)) + { + dcomp_teardown_window(hwnd); + return 0; @@ -93,7 +106,7 @@ index 878e101..3cea4be 100644 switch (msg) { case WM_ERASEBKGND: -@@ -1027,10 +1048,6 @@ static LRESULT CALLBACK dcomp_target_wndproc(HWND hwnd, UINT msg, WPARAM wparam, +@@ -1093,10 +1124,6 @@ static LRESULT CALLBACK dcomp_target_wndproc(HWND hwnd, UINT msg, WPARAM wparam, return 0; } @@ -104,11 +117,11 @@ index 878e101..3cea4be 100644 case WM_TIMER: if (wparam == DCOMP_REBLIT_TIMER_ID) { -@@ -1210,15 +1227,18 @@ static LRESULT CALLBACK dcomp_target_wndproc(HWND hwnd, UINT msg, WPARAM wparam, +@@ -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 (msg == dcomp_teardown_message()) ++ if (dcomp_is_teardown_message(msg)) + { + DestroyWindow(hwnd); + return 0; @@ -127,7 +140,7 @@ index 878e101..3cea4be 100644 { PAINTSTRUCT ps; diff --git a/dlls/dxgi/swapchain.c b/dlls/dxgi/swapchain.c -index a180e16..cb6413d 100644 +index 3e6d4bb..b60f96f 100644 --- a/dlls/dxgi/swapchain.c +++ b/dlls/dxgi/swapchain.c @@ -28,7 +28,6 @@ WINE_DEFAULT_DEBUG_CHANNEL(dxgi); 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 index e2288f87..5f3e70fa 100644 --- 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 @@ -20,12 +20,17 @@ 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. +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 c60c4f2..672cf6b 100644 +index 450362c..efb0d2c 100644 --- a/dlls/winex11.drv/window.c +++ b/dlls/winex11.drv/window.c @@ -1693,6 +1693,21 @@ enum config_rounding_state @@ -66,7 +71,21 @@ index c60c4f2..672cf6b 100644 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 ); } -@@ -1784,11 +1801,20 @@ static void activate_config_rounding( struct x11drv_win_data *data, const RECT * +@@ -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 ); } @@ -87,7 +106,7 @@ index c60c4f2..672cf6b 100644 EqualRect( visible_rect, &data->config_rounding_win32_rect ) && EqualRect( &data->current_state.rect, &data->config_rounding_host_rect ); } -@@ -1802,6 +1828,7 @@ static BOOL config_rounding_move_pending( const struct x11drv_win_data *data ) +@@ -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 && @@ -95,7 +114,7 @@ index c60c4f2..672cf6b 100644 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 +1847,17 @@ static BOOL track_config_rounding_move( struct x11drv_win_data *data, const 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; @@ -113,7 +132,7 @@ index c60c4f2..672cf6b 100644 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 +1866,18 @@ static BOOL track_config_rounding_move( struct x11drv_win_data *data, const RECT +@@ -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; @@ -132,14 +151,6 @@ index c60c4f2..672cf6b 100644 OffsetRect( &win32_rect, left, top ); if (!config_edges_within_dpi_rounding( data, &win32_rect, host_rect )) return FALSE; -@@ -1835,6 +1885,7 @@ static BOOL track_config_rounding_move( struct x11drv_win_data *data, const RECT - data->config_rounding_serial = 0; - data->config_rounding_settle_serial = 0; - data->config_rounding_generation = data->config_generation; -+ data->config_rounding_dpi = dpi; - data->config_rounding_win32_rect = win32_rect; - activate_config_rounding( data, host_rect ); - TRACE( "window %p/%lx tracking host move as config rounding alias %s -> %s generation %lu\n", diff --git a/dlls/winex11.drv/x11drv.h b/dlls/winex11.drv/x11drv.h index 02154d8..1a7a040 100644 --- a/dlls/winex11.drv/x11drv.h diff --git a/patches/SERIES.sha256 b/patches/SERIES.sha256 index 9e3cc2a0..8f94ac1e 100644 --- a/patches/SERIES.sha256 +++ b/patches/SERIES.sha256 @@ -84,15 +84,15 @@ bf46e82e08c06410d9bc967a85ed68ef07d92a306128f71055c41e4427ab0b56 0091-winex11-s 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 -0a45d3ce6da786c76fe5c64406d75fb028907c3fb70084a61515132cc945c3de 0095-dxgi-reference-count-the-parent-WS_CLIPCHILDREN-bit.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 -56f170f6c07dcf8ea47c3e735083d8b821c6841b58ad187722601075bfb3236a 0098-dcomp-wined3d-exclude-composition-targets-from-GDI-p.patch +5b8c02a7a84d7e70106203887729ae2039f3e762379b61be4c6f74022dffda09 0098-dcomp-wined3d-exclude-composition-targets-from-GDI-p.patch 372a7685948f52d07dc5678336077bd4e800d55eaeaa72d9764340421b544670 0099-wined3d-only-exclude-dcomp-targets-that-share-our-to.patch -364c66cd1e94dd410f3030cdd981ff6230dd979edfb93966ffcbd9c687b1a5a6 0100-dxgi-winex11-scope-DComp-popup-inference-by-thread-a.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 -972d123fbc2bcc039aa5333d34443650f4a5ab3b0fcf560d0acbc4c7ed4d97d7 0102-dxgi-winex11-gate-parked-reblits-on-host-viewability.patch -904e9bf116e9b641ca7f495d848bd15fd53f3b15757a14a88af7b27aeee1fe81 0103-dxgi-carry-DComp-teardown-on-a-registered-message-no.patch -4a90a710be796a9184a15179b708a040188a0ae472655c3534d96ba159a585de 0104-winex11-tie-config-rounding-aliases-to-Wine-s-own-co.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