diff --git a/notes/FINDINGS-ISSUE-87-SPLICE-INPUT-2026-08-04.md b/notes/FINDINGS-ISSUE-87-SPLICE-INPUT-2026-08-04.md new file mode 100644 index 00000000..c2bcdbac --- /dev/null +++ b/notes/FINDINGS-ISSUE-87-SPLICE-INPUT-2026-08-04.md @@ -0,0 +1,220 @@ +# Issue 87: Splice panel ignores input after collapse and reopen + +This note records what we know about issue 87, what we changed on the +fix/webview-splice branch, and what test decides the rest. + +Written 2026-08-04, revised 2026-08-05. Branch: fix/webview-splice +(worktree webview-splice, started from origin/main bac1bb0). + +## Current state and what to do next + +Three changes sit on this branch. None is verified against the bug yet. + +1. tools/learnheal.c. learnheal is a small helper that our launcher starts + with Live. It nudges Live's Learn View pane once so the pane draws + correctly. Before this change it also nudged webview windows that belong + to plugins, including the Splice panel, every time Splice rebuilt its + panel. Now it only touches windows whose parents are Live's own pane + windows. The rebuilt learnheal.exe is committed. +2. patches/0070. Fixes three defects in our dxgi patch stack. dxgi is the + Wine library that manages swapchains, the buffers a program draws frames + into. Our stack attaches a helper to some plugin windows there, and that + helper could end up eating all input for a window while the window kept + drawing. The exact conditions are in the patch and in the "Theory 3" + section below. The patch compiles clean but has not run against Live. +3. tools/issue87-routing-trace.patch. A logging patch for wineserver, the + Wine process that decides which window receives each mouse event. It is + not part of the shipped patch series. Build a test runtime with it, record + one hover over the panel while it works and one after it breaks, and the + log names the exact check that fails. Usage steps are in the patch header. + +Next step: build a runtime from this branch and ask a reporter to repeat the +collapse/reopen test. If the panel still breaks, run the trace and read the +log. The reporters reproduce the bug reliably, so one session answers it. + +## What the bug looks like + +Reported by ClickSentinel. Their report labels the runtime "2026.07.23.1" +but describes the 11.13 Wine base with patches 0001 through 0054. That +combination is newer than our v2026.07.23.1 tag, so ask for the exact build +commit. The facts from the report: + +- Collapse the Splice panel in Live's browser, then reopen it. The panel + still draws and still resizes, but clicks, hover and keys stop reaching + it. It stays broken until the plugin reloads or Live restarts. +- The rebuild is real: Splice destroys its webview window and creates a new + one with a new window class name and a new window handle. Of the WebView2 + helper programs, only the renderer process restarts. The browser, GPU and + utility processes keep running. +- During a 10 second hover inside the panel, a message monitor inside Live + counted mouse messages only at Live's main window: 28730 while healthy, + 710 while broken, zero at the panel's windows in both states. +- While broken, every standard check still passes: the window-under-point + query resolves the panel's window, the window answers messages, focus and + capture are clean, window styles and the window tree match the healthy + state, and our patch 0016 instrumentation never fires. +- Clicking Live's back and forward browser arrows repairs the panel. Found + by amenohi2 in issue 34, confirmed by ClickSentinel. + +## What the report gets wrong + +The report concludes that the failure lives inside WebView2's closed-source +input forwarding and cannot be observed from our side. The source code of +CHOC, the library Splice uses to embed its webview, shows otherwise. CHOC is +public at github.com/Tracktion/choc, file choc/gui/choc_WebView.h. + +- CHOC creates one plain window and hands it to WebView2 in windowed mode + (choc_WebView.h lines 1368, 1038, 1452). Windowed mode means WebView2 + creates its own child windows inside that window and receives input + through them. The forwarding API the report describes belongs to a + different mode that CHOC does not use. +- CHOC forwards no input itself. Its window procedure, the function that + receives a window's messages, handles only resize and show messages and + passes everything else to the default handler (lines 1561-1572). + +This changes the whole picture. Nothing in the plugin forwards input, so +while the panel works, mouse events must arrive directly at the child +windows that WebView2 created. Those child windows belong to the +msedgewebview2 browser process, a separate program. The bug is therefore +that this direct delivery stops after the rebuild, and Wine makes that +decision, so Wine can log it. + +Two of the report's measurements do not show what they appear to show: + +- "Zero messages at the panel's windows" came from a monitor inside Live's + process. A monitor in one process cannot see messages delivered to + another process's windows. The zero is expected in both states and rules + nothing out. +- The drop from 28730 to 710 messages at Live's main window is a real + signal. A likely explanation: while the panel works, the browser process + reflects extra messages toward Live's window, and that reflection stops + when delivery stops. The trace patch settles this. + +The report's "reparented into a new host window" reading also falls away. +CHOC builds a complete new webview. The surviving browser and GPU processes +are the shared WebView2 installation reusing one browser per profile, which +is its normal behaviour. + +## How Wine decides who gets a mouse event + +Two different code paths answer "which window is under the pointer", and +they can disagree. That disagreement is why the report's checks passed while +input stayed dead. + +- Delivery. wineserver picks the receiving thread in + window_thread_from_point (server/window.c:1069). It walks down the window + tree and tests each window with is_point_in_window (server/window.c:933): + the window must be visible, not a disabled child, not marked transparent + to input, the point must fall inside the window's visible rectangle after + scaling it by the window's DPI (its display scale factor), and inside the + window's shape region if one is set. +- Queries. The WindowFromPoint function that the report used runs a + different walk with the calling program's own scale factor + (win32u/window.c:2846). + +A window chain can pass the query and still lose delivery. The state that +decides delivery does not appear in any style or tree dump: the visible +rectangle, the shape region, the per-window scale factor, the stacking +order between siblings, and the owning thread. The report compared styles, +rectangles and tree structure, which is exactly the state that both paths +share. + +Keyboard input needs no separate explanation. In windowed mode the keyboard +follows focus, and focus follows a successful click. Dead mouse means dead +keyboard. + +## Theories, strongest first + +### Theory 1: wineserver rejects the rebuilt windows during delivery + +The new child windows fail one of the delivery checks, so every mouse event +lands in Live's own queue and stops at Live's main window. Drawing survives +because our composition path copies frames into the window regardless, and +resizing survives because it travels through COM calls, not through input. + +Why only the rebuild breaks: at first launch the browser takes seconds to +start, so WebView2 attaches its windows into a settled layout. On reopen +the browser is already running, attachment completes in milliseconds, and +the windows are created while the panel is still mid-collapse. Windows the +operating system tolerates that order. Several of our patches record window +state at creation time and could freeze the bad moment. + +Why the arrow buttons repair it: navigation makes the browser rebuild its +input window inside a settled layout, which recreates the state cleanly. + +Test: build with tools/issue87-routing-trace.patch, capture one healthy and +one broken hover, and read which check rejects which window. This is the +one measurement nobody has taken. + +### Theory 2: learnheal nudged the rebuilt panel + +learnheal matched any large titled Chrome_WidgetWin_1 window on the +desktop. WebView2 titles that window after the page, so Splice's panel +matched, and every rebuild produced a fresh window that earned a new nudge +a few seconds later. learnheal's own header records that a badly timed +nudge leaves a webview permanently degraded. + +Weaknesses: learnheal waits for the window's size to settle before it +nudges, and its known damage is resizing, not input. The bug also +reproduces too reliably for a one-second scanner to be the whole story. + +Test: the fix is committed. Reporters can also test the old runtime by +killing learnheal.exe before the repro. + +### Theory 3: our dxgi helper swallowed input after a rebind + +Our dxgi patches attach a helper window procedure to some plugin windows. +Patch 0016 fixed a family of bugs in the equivalent dcomp helper: rebinding +the same window recorded our own helper as the "original" handler, and +losing the recorded original turned the window into one that draws but +ignores every message. The dxgi copy never received those guards, and its +release path could remove the recorded original while the helper stayed +installed. That produces exactly "draws, resizes, ignores input". + +Two details fit issue 87. The reporter's zero readings came from counters +on our full-mode timer path, and a misassigned rebind runs on a different +timer, so their zeros are consistent with this theory rather than evidence +against it. One detail does not fit: the panel appears to draw fresh +frames, and this failure should degrade drawing too. + +Test: patch 0070 is committed. The mode decision already logs a FIXME line, +now including a rebind flag; capture it during a repro. + +### Theory 4: patch 0045 changed how the old panel tears down + +Patch 0045 makes RevokeDragDrop refuse windows of other processes and +return a hard error. If the plugin's cleanup loop stops at the first hard +error, part of the old panel's teardown never runs. That could feed +theory 3's overlap or leave other state behind. This path also touches +issue 34, the Splice drag-and-drop bug, reported by the same people. + +Test: log RevokeDragDrop calls and returns during a collapse. + +## Environment facts specific to Splice + +- Our launcher exports browser flags (software rendering, composition + disabled, sandbox off) for WebView2. Live's own panes override them with + Ableton's flags, but a plugin-created webview inherits ours. Splice + therefore runs a browser configuration nothing else on this stack runs. + The reporter still sees composition windows on the Splice chain, so check + whether the flags reach Splice's browser at all. +- WebView2 version 150 composites pages through a path this stack never + tested (notes/ABLETON-WINE-GPU-RENDERER-WEBVIEW2-DIAGNOSIS.md, fork + issue 8). Record the prefix's WebView2 version in any repro. + +## Rules for changes in this area + +Land no behaviour change here before a probe confirms its mechanism. Every +candidate touches machinery shared with the Learn View pane, Max for Live +and JUCE plugin editors. Each change needs the regression list from +notes/ABLETON-WINE-GPU-RENDERER-WEBVIEW2-DIAGNOSIS.md step 5 (Learn View +open, scroll, close, reopen; both panes at once; Splice editor close) plus +this issue's collapse/reopen cycle. Scratch builds carry no PipeASIO, so a +test runtime has no audio; that does not affect this repro. + +## Data to request from reporters + +- The exact commit their runtime was built from. +- Their desktop scale factor. +- A dump of Live's main window's child windows that preserves sibling + order, taken healthy and broken. diff --git a/patches/0070-dxgi-keep-the-true-original-wndproc-across-dcomp-tar.patch b/patches/0070-dxgi-keep-the-true-original-wndproc-across-dcomp-tar.patch new file mode 100644 index 00000000..bec61a6a --- /dev/null +++ b/patches/0070-dxgi-keep-the-true-original-wndproc-across-dcomp-tar.patch @@ -0,0 +1,232 @@ +Subject: dxgi: keep the true original wndproc across dcomp target re-binds + +Found while reviewing issue 87 (Splice panel input-dead after pane +collapse/reopen). These are the dxgi twins of the defects patch 0016 +fixed in dcomp.dll's target subclass; the dxgi copy in factory.c and +swapchain.c never received the same guards. Not confirmed as the issue +87 mechanism; the failure modes are real on their own. + +1. A WM_WINE_DCOMP_SET_TARGET re-bind for a window we already subclass + (a client recreating its swapchain for the same window: JUCE 8 device + recreation, WebView2 rebuilds) recorded our own subclass as the + "original" wndproc. First message after that recurses through + CallWindowProcW into itself. The re-bind also double-incremented + dcomp_subclassed_target_count and double-pushed the popup stack. + The install path now detects the re-bind, keeps the recorded + original, re-arms only the mode's timer, and skips count and stack. + +2. The popup-mode heuristic counted the window's own live subclass, so + any same-window re-bind saw count > 0 and was demoted to popup mode, + permanently losing the full-mode present timer and the frame-latency + signal that patch 0022 documents as input-critical. The re-bound + window's own subclass is now excluded from the count comparison. + +3. d3d11_swapchain_Release removed __wine_dcomp_orig_wndproc even when + it could not restore the wndproc (foreign subclass on top, or the + prop already gone). An installed subclass without that prop sends + every message to DefWindowProcW while WM_PAINT still blits the comp + buffer: the window paints and resizes but ignores all input, the + exact failure 0016 describes. Release now restores, removes the prop + and decrements the count only when the current wndproc is verifiably + ours (new dcomp_is_subclass_wndproc helper); otherwise the subclass + keeps forwarding through the prop and WM_NCDESTROY finishes the + cleanup, including the decrement, so the count is never taken twice. + +The popup-detect FIXME now also logs the re-bind flag, so a capture of +a WebView2 rebuild shows which mode the new bind took. +--- + dlls/dxgi/dxgi_private.h | 4 ++ + dlls/dxgi/factory.c | 101 ++++++++++++++++++++++++++++++++++------------ + dlls/dxgi/swapchain.c | 18 +++++++- + 3 files changed, 94 insertions(+), 29 deletions(-) + +diff --git a/dlls/dxgi/dxgi_private.h b/dlls/dxgi/dxgi_private.h +--- a/dlls/dxgi/dxgi_private.h ++++ b/dlls/dxgi/dxgi_private.h +@@ -180,6 +180,10 @@ + * un-subclasses a target on its own UI thread. */ + extern LONG dcomp_subclassed_target_count; + ++/* TRUE when proc is one of factory.c's target subclass wndprocs; lets ++ * swapchain.c verify the subclass is still in place before restoring. */ ++BOOL dcomp_is_subclass_wndproc(WNDPROC proc); ++ + /* IDXGISwapChain */ + struct d3d11_swapchain + { +diff --git a/dlls/dxgi/factory.c b/dlls/dxgi/factory.c +--- a/dlls/dxgi/factory.c ++++ b/dlls/dxgi/factory.c +@@ -991,6 +991,11 @@ + : DefWindowProcW(hwnd, msg, wparam, lparam); + } + ++BOOL dcomp_is_subclass_wndproc(WNDPROC proc) ++{ ++ return proc == dcomp_target_wndproc || proc == dcomp_popup_wndproc; ++} ++ + static LRESULT CALLBACK dcomp_swapchain_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) + { + switch (msg) +@@ -1156,34 +1161,63 @@ + /* Transient anchor for a new popup = the previously-opened + * target (top of the popup stack). */ + HWND popup_parent = dcomp_popup_stack_top(); ++ /* Re-bind: this window is already subclassed by us because a ++ * client recreated its swapchain for the same window (JUCE 8 ++ * device recreation, WebView2 rebuilds). The true original ++ * wndproc is already in __wine_dcomp_orig_wndproc: never ++ * record our own subclass as "original" (that turned the ++ * dcomp-side subclass into a permanent DefWindowProc black ++ * hole, painted but ignored all input — patch 0016), never ++ * count or stack-push the window twice, and exclude its own ++ * subclass from the popup-mode count so a re-bound main ++ * window cannot demote itself to a popup. */ ++ WNDPROC cur = (WNDPROC)GetWindowLongPtrW(target_hwnd, GWLP_WNDPROC); ++ BOOL rebind = (cur == dcomp_target_wndproc || cur == dcomp_popup_wndproc); + +- FIXME("DComp popup-detect: target %p style=0x%08lx parent=%p count=%ld.\n", ++ FIXME("DComp popup-detect: target %p style=0x%08lx parent=%p count=%ld rebind=%d.\n", + target_hwnd, (unsigned long)target_style, target_parent, +- dcomp_subclassed_target_count); ++ dcomp_subclassed_target_count, rebind); + +- if (dcomp_subclassed_target_count > 0) ++ if (dcomp_subclassed_target_count > (rebind ? 1 : 0)) + is_popup_mode = TRUE; + + if (is_popup_mode) + { + /* Lightweight popup mode: subclass with minimal wndproc + * (WM_ERASEBKGND + WM_PAINT only), no timer. */ +- WNDPROC orig = (WNDPROC)SetWindowLongPtrW(target_hwnd, GWLP_WNDPROC, +- (LONG_PTR)dcomp_popup_wndproc); +- if (orig) ++ WNDPROC orig; ++ if (rebind) ++ { ++ orig = (WNDPROC)GetPropW(target_hwnd, L"__wine_dcomp_orig_wndproc"); ++ SetWindowLongPtrW(target_hwnd, GWLP_WNDPROC, ++ (LONG_PTR)dcomp_popup_wndproc); ++ KillTimer(target_hwnd, DCOMP_REBLIT_TIMER_ID); ++ } ++ else ++ { ++ orig = (WNDPROC)SetWindowLongPtrW(target_hwnd, GWLP_WNDPROC, ++ (LONG_PTR)dcomp_popup_wndproc); ++ if (orig) ++ SetPropW(target_hwnd, L"__wine_dcomp_orig_wndproc", (HANDLE)orig); ++ } ++ if (orig || rebind) + { +- SetPropW(target_hwnd, L"__wine_dcomp_orig_wndproc", (HANDLE)orig); + SetPropW(target_hwnd, L"__wine_dcomp_swapchain", (HANDLE)iface); +- /* 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. */ +- SetPropW(target_hwnd, L"__wine_dcomp_popup_parent", (HANDLE)popup_parent); + SetTimer(target_hwnd, DCOMP_POPUP_REBLIT_TIMER_ID, 200, NULL); +- InterlockedIncrement(&dcomp_subclassed_target_count); +- dcomp_update_active_prop(); +- dcomp_popup_stack_push(target_hwnd); +- FIXME("DComp POPUP mode: target %p, orig wndproc %p, sc=%p, transient->%p (reblit timer 200ms).\n", +- target_hwnd, orig, iface, popup_parent); ++ if (!rebind) ++ { ++ /* 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. ++ * On a re-bind the recorded anchor stays valid; the stack ++ * top may be the window itself by now. */ ++ SetPropW(target_hwnd, L"__wine_dcomp_popup_parent", (HANDLE)popup_parent); ++ InterlockedIncrement(&dcomp_subclassed_target_count); ++ dcomp_update_active_prop(); ++ dcomp_popup_stack_push(target_hwnd); ++ } ++ FIXME("DComp POPUP mode: target %p (rebind=%d), orig wndproc %p, sc=%p, transient->%p (reblit timer 200ms).\n", ++ target_hwnd, rebind, orig, iface, popup_parent); + } + else + { +@@ -1194,19 +1228,34 @@ + else + { + /* Full mode: subclass + timer + periodic Present for main window */ +- WNDPROC orig = (WNDPROC)SetWindowLongPtrW(target_hwnd, GWLP_WNDPROC, +- (LONG_PTR)dcomp_target_wndproc); +- if (orig) ++ WNDPROC orig; ++ if (rebind) ++ { ++ orig = (WNDPROC)GetPropW(target_hwnd, L"__wine_dcomp_orig_wndproc"); ++ SetWindowLongPtrW(target_hwnd, GWLP_WNDPROC, ++ (LONG_PTR)dcomp_target_wndproc); ++ KillTimer(target_hwnd, DCOMP_POPUP_REBLIT_TIMER_ID); ++ } ++ else ++ { ++ orig = (WNDPROC)SetWindowLongPtrW(target_hwnd, GWLP_WNDPROC, ++ (LONG_PTR)dcomp_target_wndproc); ++ if (orig) ++ SetPropW(target_hwnd, L"__wine_dcomp_orig_wndproc", (HANDLE)orig); ++ } ++ if (orig || rebind) + { +- SetPropW(target_hwnd, L"__wine_dcomp_orig_wndproc", (HANDLE)orig); + SetPropW(target_hwnd, L"__wine_dcomp_swapchain", (HANDLE)iface); + SetTimer(target_hwnd, DCOMP_REBLIT_TIMER_ID, 200, NULL); +- InterlockedIncrement(&dcomp_subclassed_target_count); +- dcomp_update_active_prop(); +- /* Main window: stack base for the first popup's transient anchor. */ +- dcomp_popup_stack_push(target_hwnd); +- FIXME("DComp: subclassed target %p, orig wndproc %p, timer started, sc=%p.\n", +- target_hwnd, orig, iface); ++ if (!rebind) ++ { ++ InterlockedIncrement(&dcomp_subclassed_target_count); ++ dcomp_update_active_prop(); ++ /* Main window: stack base for the first popup's transient anchor. */ ++ dcomp_popup_stack_push(target_hwnd); ++ } ++ FIXME("DComp: subclassed target %p (rebind=%d), orig wndproc %p, timer started, sc=%p.\n", ++ target_hwnd, rebind, orig, iface); + } + else + { +diff --git a/dlls/dxgi/swapchain.c b/dlls/dxgi/swapchain.c +--- a/dlls/dxgi/swapchain.c ++++ b/dlls/dxgi/swapchain.c +@@ -315,17 +315,29 @@ + if (IsWindow(t) && GetWindowThreadProcessId(t, NULL) == GetCurrentThreadId()) + { + WNDPROC orig = (WNDPROC)GetPropW(t, L"__wine_dcomp_orig_wndproc"); ++ WNDPROC cur = (WNDPROC)GetWindowLongPtrW(t, GWLP_WNDPROC); + + KillTimer(t, DCOMP_REBLIT_TIMER_ID); + KillTimer(t, DCOMP_POPUP_REBLIT_TIMER_ID); + KillTimer(t, DCOMP_RESIZE_REBLIT_TIMER_ID); +- if (orig) ++ /* Restore only while the subclass is verifiably still ours and ++ * the original is known. Removing the prop while our subclass ++ * stays installed (someone subclassed on top, or the prop was ++ * lost) turns the wndproc tail into an unconditional ++ * DefWindowProc: the window keeps painting from the comp buffer ++ * but ignores all input (the dcomp-side patch 0016 failure ++ * mode). When the restore is skipped, the subclass keeps ++ * forwarding through the prop and WM_NCDESTROY finishes the ++ * cleanup, including the count decrement. */ ++ if (orig && dcomp_is_subclass_wndproc(cur)) ++ { + SetWindowLongPtrW(t, GWLP_WNDPROC, (LONG_PTR)orig); +- RemovePropW(t, L"__wine_dcomp_orig_wndproc"); ++ RemovePropW(t, L"__wine_dcomp_orig_wndproc"); ++ InterlockedDecrement(&dcomp_subclassed_target_count); ++ } + 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); + } + swapchain->target_hwnd = NULL; + } diff --git a/patches/BASE.txt b/patches/BASE.txt index 61c7cdea..559fa2f5 100644 --- a/patches/BASE.txt +++ b/patches/BASE.txt @@ -251,3 +251,11 @@ Apply them in sequence with the rest of the series. borderless fullscreen, unselected windows, and plugin windows retain Wine's existing behavior. `WINE_WIN32_RESIZABLE_CLASS=off` disables only this fix for comparison. See `notes/ABLETON-WINE-GPU-RENDERER.md`. +- `0070`: dxgi twins of the patch 0016 dcomp subclass guards. A re-bind of a + window dxgi already subclasses (client recreated its swapchain for the same + window) no longer records dxgi's own wndproc as the original, no longer + double-counts the popup-mode heuristic or demotes the window to popup mode, + and `d3d11_swapchain_Release` no longer strands an installed subclass + without its forwarding prop (painted but ignored all input). Found during + the issue 87 review; not confirmed as its mechanism. See + `notes/FINDINGS-ISSUE-87-SPLICE-INPUT-2026-08-04.md`. diff --git a/patches/SERIES.sha256 b/patches/SERIES.sha256 index 828fe3ff..b374cc0a 100644 --- a/patches/SERIES.sha256 +++ b/patches/SERIES.sha256 @@ -62,5 +62,6 @@ a07f7ecb2c3a568d8eeae9a42df558cea4d8e279e5bdeb0af67a6649086973f7 0062-winex11-k 81e02eeac88e6eda5da8044cd5bf74a197b2742f77c46da54e61dbf300031bd3 0064-shell32-route-explorer-folder-open-commands-to-the-h.patch 7931be1771a070325eb0b87f970e51f7089c216f1bad128fb5ecf504e100e005 0065-win32u-normalize-selected-fullscreen-window.patch 4b4ca42a11539df9482e7160141be08c10e8f4b1d1c46e1ce3fa60caa0fb993a 0069-win32u-keep-selected-captioned-monitor-sized-window-resizable.patch +92a8cb5e92241d9b6b6b7eea34dc1e7d48239687cce6412f2854d2baf672ea7f 0070-dxgi-keep-the-true-original-wndproc-across-dcomp-tar.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 55c40aa5..9811bc18 100755 --- a/scripts/build-audit.sh +++ b/scripts/build-audit.sh @@ -145,6 +145,7 @@ FINGERPRINTS=' 0065|ascii|lib/wine/x86_64-unix/win32u.so|WINE_WIN32_FULLSCREEN_CLASS 0065|ascii|lib/wine/x86_64-unix/winex11.so|WINE_WIN32_FULLSCREEN_CLASS 0069|ascii|lib/wine/x86_64-unix/win32u.so|WINE_WIN32_RESIZABLE_CLASS +0070|ascii|lib/wine/x86_64-windows/dxgi.dll|rebind=%d 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/issue87-routing-trace.patch b/tools/issue87-routing-trace.patch new file mode 100644 index 00000000..5b9be429 --- /dev/null +++ b/tools/issue87-routing-trace.patch @@ -0,0 +1,106 @@ +Diagnostic patch for issue 87 (Splice panel input-dead after collapse/reopen). +Log-only wineserver trace of hardware mouse routing. NOT part of the shipped +series; apply on top of patches/ for a scratch diagnosis build. + +What it logs, gated by WINE_I87_TRACE=1 in the wineserver's environment: +- every hardware-input routing decision (window_thread_from_point): the scope + window, raw and client coordinates, the final window and owning thread id +- every child considered during the descent: hit/skip, style, ex-style, + visible_rect, window-region presence, owning thread id + +WindowFromPoint requests stay silent, so the capture contains only the real +routing path the issue's client-side probes could not see. + +Usage: + wineserver -k + WINE_I87_TRACE=1 WINEPREFIX=... wineserver -f -p 2>i87.log & + ; hover the Splice panel ~5 s healthy, break it, hover ~5 s again + grep i87: i87.log +A skip line naming a Chrome/CHOC window during the broken hover, or a route +result landing on Live's main window while the healthy capture landed on a +webview thread, pins the failing check. Validate the capture is non-empty +before trusting a negative (the launcher defaults WINEDEBUG=-all; this trace +does not use WINEDEBUG, but an empty log still means the env var did not +reach wineserver). + +Scratch builds lack PipeASIO; audio is absent in a diagnosis runtime. That +does not affect this repro. +--- +diff --git a/server/window.c b/server/window.c +index 73bad90..3e2f79a 100644 +--- a/server/window.c ++++ b/server/window.c +@@ -22,6 +22,8 @@ + + #include + #include ++#include ++#include + + #include "ntstatus.h" + #include "windef.h" +@@ -997,6 +999,22 @@ static void get_window_list( struct desktop *desktop, struct window *win, struct + } + } + ++/* issue 87 diagnostic: log-only trace of hardware input routing, gated by ++ * WINE_I87_TRACE in the wineserver's environment. Active only inside ++ * window_thread_from_point so WindowFromPoint requests stay silent. */ ++static int issue87_trace_active; ++ ++static int issue87_trace_enabled(void) ++{ ++ static int on = -1; ++ if (on < 0) ++ { ++ const char *e = getenv( "WINE_I87_TRACE" ); ++ on = e && *e && *e != '0'; ++ } ++ return on; ++} ++ + /* find child of 'parent' that contains the given point (in parent-relative coords) */ + static struct window *child_window_from_point( struct window *parent, int x, int y ) + { +@@ -1005,8 +1023,15 @@ static struct window *child_window_from_point( struct window *parent, int x, int + LIST_FOR_EACH_ENTRY( ptr, &parent->children, struct window, entry ) + { + int x_child = x, y_child = y; ++ int hit = is_point_in_window( ptr, &x_child, &y_child, get_window_dpi( parent ) ); + +- if (!is_point_in_window( ptr, &x_child, &y_child, get_window_dpi( parent ) )) continue; /* skip it */ ++ if (issue87_trace_active) ++ fprintf( stderr, "i87: %s win=%08x style=%08x ex=%08x vis=(%d,%d)-(%d,%d) rgn=%d tid=%04x parent_pt=(%d,%d)\n", ++ hit ? "hit " : "skip", ptr->handle, ptr->style, ptr->ex_style, ++ ptr->visible_rect.left, ptr->visible_rect.top, ++ ptr->visible_rect.right, ptr->visible_rect.bottom, ++ !!ptr->win_region, ptr->thread ? ptr->thread->id : 0, x, y ); ++ if (!hit) continue; /* skip it */ + + /* if window is minimized or disabled, return at once */ + if (ptr->style & (WS_MINIMIZE|WS_DISABLED)) return ptr; +@@ -1069,13 +1094,24 @@ user_handle_t shallow_window_from_point( struct desktop *desktop, int x, int y ) + struct thread *window_thread_from_point( user_handle_t scope, int x, int y ) + { + struct window *win = get_user_object( scope, NTUSER_OBJ_WINDOW ); ++ int raw_x = x, raw_y = y; + + if (!win) return NULL; + + map_point_raw_to_virt( win->desktop, &x, &y ); + + screen_to_client( win, &x, &y, no_dpi ); ++ issue87_trace_active = issue87_trace_enabled(); ++ if (issue87_trace_active) ++ fprintf( stderr, "i87: route scope=%08x raw=(%d,%d) client=(%d,%d)\n", ++ scope, raw_x, raw_y, x, y ); + win = child_window_from_point( win, x, y ); ++ if (issue87_trace_active) ++ { ++ fprintf( stderr, "i87: route result win=%08x tid=%04x\n", ++ win->handle, win->thread ? win->thread->id : 0 ); ++ issue87_trace_active = 0; ++ } + if (!win->thread) return NULL; + return (struct thread *)grab_object( win->thread ); + } diff --git a/tools/learnheal.c b/tools/learnheal.c index dad4dbfc..e6d10db1 100644 --- a/tools/learnheal.c +++ b/tools/learnheal.c @@ -93,6 +93,24 @@ static void consider( HWND hwnd ) } } +/* Only Live's own panes (Learn View, doc sidebar): every ancestor below the + * desktop must be one of Live's pane hosts. Plugin webviews (CHOC/Splice, + * JUCE) hang under their own host windows and must never be poked (issue 87). */ +static int live_pane_ancestry( HWND hwnd ) +{ + char cls[128]; + HWND desk = GetDesktopWindow(), p; + + for (p = GetAncestor( hwnd, GA_PARENT ); p && p != desk; p = GetAncestor( p, GA_PARENT )) + { + if (!GetClassNameA( p, cls, sizeof(cls) )) return 0; + if (lstrcmpA( cls, "Ableton Live Window Class" ) + && lstrcmpA( cls, "AbletonWebViewHelperWindow" ) + && lstrcmpA( cls, "Chrome_WidgetWin_0" )) return 0; + } + return 1; +} + static void scan( HWND hwnd ) { char cls[128], title[160]; @@ -103,7 +121,7 @@ static void scan( HWND hwnd ) title[0] = 0; GetWindowTextA( hwnd, title, sizeof(title) ); if (title[0] && find_sub( title, "Ableton Live" )) live_seen = 1; - if (!lstrcmpA( cls, "Chrome_WidgetWin_1" ) && title[0]) + if (!lstrcmpA( cls, "Chrome_WidgetWin_1" ) && title[0] && live_pane_ancestry( hwnd )) { RECT r; GetWindowRect( hwnd, &r ); /* skip tooltips/popups; the lesson pane and doc sidebar are big */ diff --git a/tools/learnheal.exe b/tools/learnheal.exe index 30395949..a797769d 100755 Binary files a/tools/learnheal.exe and b/tools/learnheal.exe differ