From 79f584a920bd4d1452e9097b07a43408a2fd4478 Mon Sep 17 00:00:00 2001 From: Shibco Date: Wed, 5 Aug 2026 00:09:36 +0200 Subject: [PATCH 1/3] learnheal: only poke webviews inside Live's own pane ancestry The healer matched any titled Chrome_WidgetWin_1 of 200x200 or larger anywhere on the desktop, so it also poked plugin-hosted webviews it was never meant for: the Splice panel rebuilds its CHOC webview on every pane collapse/reopen (issue 87) and got a cross-process SetWindowPos nudge a few scans after each rebuild. Require every ancestor below the desktop to be one of Live's pane hosts (Ableton Live Window Class, AbletonWebViewHelperWindow, Chrome_WidgetWin_0); the Learn View and the doc sidebar keep their heal in both the open and the parked topology, plugin webviews are never touched. Rebuilt binary included. --- tools/learnheal.c | 20 +++++++++++++++++++- tools/learnheal.exe | Bin 5632 -> 6144 bytes 2 files changed, 19 insertions(+), 1 deletion(-) 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 303959494a5e86d6313e0af7c75ed06bb06ea329..a797769d94dbd37287d835853b27ea90add1d977 100755 GIT binary patch delta 1969 zcmcgtZ%kWN6hE&HC@WId?h)NT%KC~JR-{H)MpCyIihE05kT^1k0)@$AR13w@jeWA+ zR*WxQBkT@C7Nf-M!?F)^+JrD7+JPB52_ek6Vf?^cX2_TZmYA^3fN|jCATva=nfi?5g#i5ng5h?7jnN~H0na0o;iExUT6tJejq_F3SdGM<3YUB}P?)w1kVm!pP z2r*|XdWmuOy=Q5I&ICmDv>+mM+?ni81lvhOgF|QyIDnfvDCh@#V zuqttVjWG8OrS5A!RTi)Ogaw6+f;=hgSv~rh5U=AztXW}Ku+x}_t1IzvqPyaIEYW=0 zizkt5K(5xUQX*1g3NA73c-;x$N-o*k+e@{p+mYF9NbVx0=PzmGT1xy}o9yRoV!Yf`M!nd*IjPzIMsfU>=I~1zo=d~m)9^$Z9{Ugc zm1=itH7kybX$L<47fjyE{)j}fBbsbIv(dVrKkS!6fi9cqJ0{uceO+yVUR#yFwWkL( zOTBdXkWcCrB!9OQR2`tIBN*tEy!F1eBT@)CZyB*{e6?~hHB~xUfZIC(=+|W*%G$b% z+$`dbr?_S0RtpCT^TS5m(Uq@hPSdYwTF`WzrrR`qOw%VdeMZx2G=NVvd`Z(Inx4@6 zg<(cd!2es26(Fr5m>~nE?XF)l_e&wZ>usqg6bQl}6r~1K)7mNV09lAVQqQ}gK({s> zGHyyRKms{-h9@NF9+!Fr@Hz}A&!cR^f9y8=8oqJRbFg9jK~Ztx#K}Z)?gF#rZ@hw8 zwA@9>v*VkAx{C#P&Yp1P-C#&9V`c_PhQ*#ygK>@%r=?)`dEDJ*)aWz=&<6)D>zE#7 z^6*KauL^!}agCEx7Q1%`dVQ$#ypOgEIz~ZeQO96m(63`oAw$3NG{0IyCGLjF9ZNwt z7x|>lb_;E@I_5Gmt2$;1nFSrwg3PFnq1_CvGcV)L>-BYZdvPM#C=ipdmCe!yM55N( PP3t~uGfmvEE-?Q9H;5;I delta 1522 zcmc(fU1$_n6vxlqx@**RXRAADW3|~Nvo$nEvn_tKCE(PtnUyg{6KK>m$rcT1+(L+3 zDk|)jW+}sF=%!aiq(~kLeG8O@qEcl0foVhVt7xfxu(U)hwh{%Qf^z$x$tcdg^syJt zoO^!f+;h&oGjnq-xmCqn_3v8mAT1$^5Ya5v(;{DpC@^+>n04?4>}`Lvjwm-)_X5wc zs&)N-M1>&%$UMkXBTCe_dU$UtlOp=u2M2%?vyh)-D_Mx&WZU=*TUye67g@FcSt8w9 zac2#YQCQms%P6!y&zt96*>-}#WnR#&qe>W4^>hx2gd9TMd|4V<^9<2>O&P-SE@enJ z@2aQAod7=qq_5x^DQEWUmZ2QJ9h7P{-s4YZx34B*x+QPH=5%Y+Ni5{AB`0M*k9KoM zkWv~sgsGj_Kw=?xBA}otT}_%|wElo9(&$bE0CfX(xacjQvjW4gVl^lfFW+8)!bIlO zR&=aQ>HI;U(^qWf?ek-7jA!P5kgahV=~9yR2E^4uQrTNbD+5hQCE1i#`pmQvHLSJ2 zw%FaslR+Ghc{gFss^%|3ezchA%ze9{Y`?68ISHqbU$j$BYDyG+l7aoF4fBCv-c!x8 zkpILQYZF7>MiU`?uxin=FThYfer0pKygB}(Dz2UB##V$p!9S_`EPEMM^?W(=l5U>Y z3$5S6>@Onz*1?BDK%Ye{iJxt6zWn}8{lo9=H@ACFZ;zT(_ozK;?|0MFB6bMtrk@NE z8?LYiE9`e%J3F^V#CBB#?5MD}RM>C+hy8km|CI{+7^-Kl@>*$z z_xVN@=UT#ceUKW+a{LL)@qzE?)H-h@wkIC-9{aM?82*hd8^ZoB;n)8IGJ^Rm^eNnC zqxGfulAjpwU=mAnf26^`9(kg?Ia0g+FlIw+F1uY}?)uX`BncYE9cN#2xNAs>SjJ>(R~K@UlQba;r}Y0DF` UfLn4CzbJ1k)m7hMIMl8G0@EvkY5)KL From abcd1d26a40d3337243540761084b4dd28ce7d46 Mon Sep 17 00:00:00 2001 From: Shibco Date: Wed, 5 Aug 2026 00:09:49 +0200 Subject: [PATCH 2/3] Patch 0070: dxgi subclass guards; issue 87 theory review and diagnostics Issue 87: the Splice panel keeps rendering and resizing but ignores all input after a pane collapse/reopen rebuilds its CHOC WebView2. The review (notes/FINDINGS-ISSUE-87-SPLICE-INPUT-2026-08-04.md) reframes the report: CHOC is windowed hosting with no input forwarding of its own, so working input means wineserver routes hardware messages straight to the browser-process windows, and the reporter's in-process probes could not see that path in either state. Patch 0070 closes the dxgi twins of the dcomp patch 0016 defects found during the review: a swapchain re-bind for an already-subclassed window recorded dxgi's own wndproc as the original, double-counted the popup-mode heuristic and demoted the window to popup mode, and d3d11_swapchain_Release could strand an installed subclass without its forwarding prop, the exact painted-but-input-dead shape. Compiled clean (dxgi.dll, wineserver) in a scratch tree off the series; not yet run against Live, and not confirmed as the issue 87 mechanism. tools/issue87-routing-trace.patch is a log-only wineserver trace, kept out of the series, whose healthy-vs-broken capture decides the routing theory; usage in its header. The 0070 slot may need renumbering when the reserved 0066-0068 series and the other in-flight branches land. --- ...NDINGS-ISSUE-87-SPLICE-INPUT-2026-08-04.md | 264 ++++++++++++++++++ ...ue-original-wndproc-across-dcomp-tar.patch | 232 +++++++++++++++ patches/BASE.txt | 8 + patches/SERIES.sha256 | 1 + scripts/build-audit.sh | 1 + tools/issue87-routing-trace.patch | 106 +++++++ 6 files changed, 612 insertions(+) create mode 100644 notes/FINDINGS-ISSUE-87-SPLICE-INPUT-2026-08-04.md create mode 100644 patches/0070-dxgi-keep-the-true-original-wndproc-across-dcomp-tar.patch create mode 100644 tools/issue87-routing-trace.patch 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..ecc43abc --- /dev/null +++ b/notes/FINDINGS-ISSUE-87-SPLICE-INPUT-2026-08-04.md @@ -0,0 +1,264 @@ +# Issue 87: Splice panel input-dead after collapse/reopen. Theory review, 2026-08-04 + +Status 2026-08-05: fix attempt implemented on this branch, runtime verification +pending. Three changes: + +1. tools/learnheal.c: the resident healer now only pokes webviews whose whole + ancestry is Live's own pane hosts (Ableton Live Window Class, + AbletonWebViewHelperWindow, Chrome_WidgetWin_0). Plugin webviews (CHOC, + JUCE) are never touched. Rebuilt learnheal.exe and posteresize.exe are + committed. Addresses T2 below. +2. patches/0070: dxgi twins of the patch 0016 subclass guards (re-bind records + its own wndproc as original; popup-mode self-demotion on re-bind; Release + stranding an installed subclass without its forwarding prop, which is the + exact painted-but-input-dead shape). Addresses T3's fixable half. Applies + cleanly on the series; dxgi.dll and wineserver compiled warning-free in a + scratch tree; NOT run against Live yet. +3. tools/issue87-routing-trace.patch: log-only wineserver trace for the T1 + probe (not in the shipped series). One healthy-vs-broken hover capture + decides T1; usage in the patch header. + +None of this is verified against the repro. If the reporters still reproduce +with 1+2, the trace capture from 3 is the next step and pins T1. + +Branch: fix/webview-splice (worktree webview-splice, off origin/main bac1bb0). + +## 1. The reported facts + +From ClickSentinel's report (runtime label "2026.07.23.1", but the stated base is +d2d1-dcomp-11.13 / 5c23dd1c with patches 0001-0053 plus 0054, which is later than +our v2026.07.23.1 tag; go by the stated patch list, and ask for the exact commit): + +- Collapse and reopen the Splice panel. The panel keeps rendering and keeps + responding to resize, but no mouse or keyboard input reaches it, permanently. +- The whole CHOC webview host is destroyed and recreated: new unique window class + (CHOCWebView117967303 to CHOCWebView118003504), new HWND. Only the WebView2 + renderer process respawns; browser, GPU and utility processes survive. +- During a 10 s in-panel hover, mouse messages observed from Live's process go + only to Live's main window in both states: 28730 healthy, 710 broken, 0 to the + child chain either way. +- In the broken state: WindowFromPoint resolves 20/20 in-panel points to the + webview child; WM_NCHITTEST replies HTCLIENT; capture, focus, styles, tree all + measure identical to healthy; dcomp props balanced; patch 0016 instrumentation + never fires; reblit timer counters read 0. +- Clicking Live's back/forward browser-navigation arrows restores input + (found by amenohi2 on issue 34, confirmed by ClickSentinel). + +## 2. The report's framing is wrong about the input path + +The issue concludes the failure sits in WebView2/CHOC's composition-hosting input +forwarding (SendMouseInput re-wiring), closed-source and unobservable. CHOC's +source says otherwise (github.com/Tracktion/choc, choc/gui/choc_WebView.h): + +- CHOC creates a plain WS_POPUP window ("CHOCWebView" class plus unique suffix) + and calls CreateCoreWebView2Controller(hwnd, ...) on it: windowed hosting + (choc_WebView.h lines 1368, 1038, 1452). Its vendored WebView2 header contains + no ICoreWebView2CompositionController and no SendMouseInput at all. +- CHOC's wndproc handles only WM_SIZE and WM_SHOWWINDOW; everything else goes to + DefWindowProcW (lines 1561-1572). CHOC forwards no input, ever. +- WS_EX_NOREDIRECTIONBITMAP on Chrome_WidgetWin_1 does not imply composition + hosting: our own Learn View panes are windowed-hosted and carry the same + styles (tools/dpispy.txt, tools/swamprobe.txt). + +Consequence, and this is the load-bearing deduction: since CHOC forwards nothing +and windowed hosting has no host-side input API, the only way input can work at +all is direct Win32 delivery to Chromium's own windows (Chrome_WidgetWin_1 / +Chrome_RenderWidgetHostHWND), which are owned by the msedgewebview2 browser +process, cross-process children inside Live's tree. Healthy input works, so that +delivery works. The bug is therefore that this delivery stops for the recreated +chain. That is a Wine-observable, Wine-instrumentable failure, not a +closed-source one. + +Two measurement blind spots follow: + +- "0 messages to the child chain" was measured from Live's process. Message + hooks are per-process; deliveries to the browser process's windows are + invisible from Live. The 0 proves nothing about the real input path. +- The healthy 28730-vs-broken 710 rate at Live's main window (a 40x drop for the + same physical hover) is unexplained under the report's model. Under ours, a + plausible reading is that the healthy stream is reflected traffic from + Chromium's legacy-window forwarding (LegacyRenderWidgetHostHWND passes mouse + messages up the parent chain), which disappears exactly when direct delivery + stops, leaving only the raw ~70/s motion rate. Unproven; the wineserver trace + below decides it. + +The "reparented into a new host window, put_ParentWindow unsupported" reading is +also unsupported: CHOC recreates the whole webview from scratch. The surviving +browser/GPU processes are the shared Evergreen browser cluster for Splice's +user-data folder, and a renderer respawn for a new CoreWebView2 in a shared +cluster is the normal pattern, not evidence of reparenting. + +## 3. How input actually routes, and why the reporter's checks do not cover it + +In the 11.13 base, hardware mouse routing and WindowFromPoint are different code +paths that consult the same server data differently: + +- Routing: winex11 in Live's process posts the event against Live's top-level; + the server picks the destination thread via window_thread_from_point + (server/window.c:1069), which maps raw to virtual coordinates, then descends + with child_window_from_point using is_point_in_window (server/window.c:933) at + each step: WS_VISIBLE, disabled-child skip, LAYERED+TRANSPARENT skip, + per-window DPI point mapping, visible_rect containment, win_region + containment. The deepest window's thread gets the message; the receiving + process then re-resolves the final HWND client-side (win32u/message.c:2625). +- WindowFromPoint: a get_window_children_from_point server request with the + caller's thread DPI, then a client-side walk (win32u/window.c:2846) that stops + at the first cross-thread window without consulting its wndproc. + +So a window chain can pass WindowFromPoint from a prober's context and still +lose hardware routing. The state that can differ is exactly the state absent +from the reporter's dumps: visible_rect (driver-adjusted at SetWindowPos time; +the NSPA base patches 0002/0003 gate it), win_region, per-window DPI (Live +creates VST3 editor windows under a DPI-UNAWARE thread context, see +tools/webviewclose.c header; a re-attach that latches a different per-window DPI +than the first attach shifts the descent's point mapping), sibling z-order, and +the owning thread. Styles, rects and tree dumps show none of these. + +Keyboard death needs no separate mechanism: windowed-hosting keyboard input +follows focus, focus follows a successful click, so mouse death implies +keyboard death. + +## 4. Theories, ranked + +### T1. Server-side routing state on the recreated chain (primary frame) + +The recreated Chromium windows fail some is_point_in_window check server-side, +so window_thread_from_point resolves to Live's thread; Live's client-side +re-resolution can only land on Live-owned windows; input pools at Live's main +window and dies. Rendering is unaffected because presentation is the dcomp/GDI +blit path, and resize is unaffected because it flows through COM (put_Bounds), +neither touching input routing. + +Why recreate differs from first create: at first launch the browser cluster +takes seconds to spawn, so the controller attaches into a settled window tree. +On reopen the cluster already runs and CreateCoreWebView2Controller completes in +milliseconds, so Chromium's SetParent/SetWindowPos sequence lands mid-collapse +layout (panel width animating, CHOC host possibly still an unmapped WS_POPUP). +Windows tolerates that ordering; our stack has several places that latch +attach-time state (NSPA visible-rect gates, layered-map delay noted in patch +0041's message, per-window DPI at creation). + +Heal fit: back/forward navigation makes Chromium rebuild its +RenderWidgetHostView and legacy HWND inside a settled tree, recreating the +server-side state cleanly. + +Deciding probe: a log-only wineserver patch in window_thread_from_point / +is_point_in_window printing, for each descent step during a broken-state hover, +which window was rejected by which check (visibility, transparency, DPI-mapped +point vs visible_rect, region), plus a server-eye dump of visible_rect, +win_region, per-window DPI and owning thread for the chain, healthy vs broken. +Nobody has looked at this layer; all prior instrumentation was client-side or +dcomp-side. + +### T2. learnheal.exe pokes the rebuilt webview at bind time + +Present and active in the reporter's environment (launcher starts it +unconditionally since 2026.07.21.2). Its matcher is any Chrome_WidgetWin_1, +anywhere, with a non-empty title and rect at least 200x200 (tools/learnheal.c:99-112); +WebView2 mirrors the page title onto Chrome_WidgetWin_1, so Splice's webview +qualifies once its page loads. A recreated webview is a new HWND, so it gets a +fresh tracked slot and a cross-process SetWindowPos nudge roughly 1-3 s after it +appears, which on a reopen is exactly bind time. learnheal's own header records +that bind-time nudges leave Chromium permanently degraded ("Nudging at bind +time leaves Chromium unable to handle later resizes", tools/learnheal.c:5-7). +The documented degradation is resize handling, not input, so the symptom match +is imperfect; but this is a resident cross-process actor firing during the +exact failure window, entirely under our control. + +Deciding probe: suppress learnheal, repeat the repro. Cheapest test available; +zero build. If implicated, the low-risk fix is scoping its matcher (title or +ancestor restricted to Live's own panes), not removing the heal. + +### T3. dxgi popup-mode misassignment via the process-global subclass count + +The base dxgi carries a deliberate process-global heuristic +(dcomp_subclassed_target_count, factory.c:440,844-921): if any target is still +subclassed when a new WM_WINE_DCOMP_SET_TARGET bind arrives, the new target is +permanently classified as a popup. The count's increment is unconditional but +its decrement is conditional (only on the window's own thread, only from paths +that may not run in a Release-first teardown), so both a teardown overlap +(Chromium's GPU thread releasing the old swapchain lazily while the new panel +binds) and a genuine leak put the rebuilt Splice target into popup mode. + +Popup mode installs a different wndproc with a different timer id and no +frame-latency signalling. Two observations make this attractive: + +- The reporter's "reblit counters read 0" was measured on the full-mode timer + path. A popup-mode target produces that reading by construction, so the + negative result is consistent with this theory rather than evidence against + dxgi involvement. +- The base's own comment warns a misclassified main window "loses its present + timer". + +Points against: the frame-latency stall arm predicts degraded or frozen frame +production, while the report describes correct rendering and live resize. It +needs the probe to decide whether the panel's apparent liveness is real frames +or GDI re-blits of the last comp buffer. + +Deciding probes: the base already logs the mode decision +(FIXME "DComp popup-detect: target %p ... count=%ld", factory.c:865-867); +capture it across the repro with WINEDEBUG active (mind the launcher's +WINEDEBUG=-all default, issue 87's own gotcha). Also dump +GetProp(desktop, "__wine_dcomp_active"), and which of DCOMP_REBLIT_TIMER_ID / +DCOMP_POPUP_REBLIT_TIMER_ID is armed on the new target. + +Related latent defect found during this review, independent of issue 87: the +dxgi subclass never received patch 0016's re-entry guards. A second SET_TARGET +on the same HWND records dxgi's own wndproc as "original" +(factory.c:902-907), and a missing __wine_dcomp_orig_wndproc prop sends every +message to DefWindowProcW (factory.c:748-749): the exact input-dead black hole +0016 fixed in dcomp.dll, still open in dxgi. The usual target (Intermediate D3D +Window) is input-transparent by design, so this is probably not issue 87, but +it should be fixed on its own merits, with regression runs for the Learn View +park/reopen cycle, M4L and JUCE/SWAM editors. + +### T4. Patch 0045's return-code change steering host teardown + +RevokeDragDrop on a foreign window now returns DRAGDROP_E_INVALIDHWND, a hard +error, including for foreign windows that were never registered (previously +DRAGDROP_E_NOTREGISTERED). If the JUCE-style teardown sweep in Live's process +treats a hard error as fatal and aborts its per-window loop, the old editor's +teardown completes only partially, which could feed the T3 overlap or leave +other per-window teardown undone. Also the marshalled drop-target data in the +helper process now leaks on every close. Directly relevant to issue 34 +(Splice drag-and-drop, closed but reportedly still broken, same commenter). +Probe: log RevokeDragDrop calls and returns during a collapse. + +## 5. Environment factors specific to Splice + +- Our launcher's WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS (SwiftShader ANGLE, + --disable-gpu-compositing, --disable-direct-composition, --no-sandbox) are + overridden by Ableton's own flags for Live's panes, but a plugin-created + environment inherits them for real. Splice therefore runs a browser + configuration nothing else on this stack runs. Yet the reporter still + observes dcomp props and an Intermediate D3D Window on the Splice chain, so + the flag set deserves its own check (does --disable-direct-composition reach + Splice's cluster at all, and what does the stack look like with the flags + cleared). +- WebView2 Evergreen 150 uses delegated compositing, a path this stack was + never tested against (notes/ABLETON-WINE-GPU-RENDERER-WEBVIEW2-DIAGNOSIS.md, + fork issue 8). Record the prefix's WebView2 version in any repro. + +## 6. Diagnostic plan, cheap first + +1. learnheal off, repro on. No build. (T2) +2. Repro with WINEDEBUG explicitly set, capture the existing popup-detect FIXME + and RevokeDragDrop returns across a collapse/reopen. Validate the capture is + non-empty before trusting it. No build. (T3, T4) +3. Prop/timer spy on the rebuilt chain: desktop __wine_dcomp_active, wndproc vs + __wine_dcomp_orig_wndproc, armed timer ids. Small tool, no runtime change. (T3) +4. Log-only wineserver + win32u tracing of the routing descent during a broken + hover, plus server-eye state dump of the chain (visible_rect, win_region, + per-window DPI, owning thread, z-order) healthy vs broken. Scratch build, + no behavior change; note scratch builds lack PipeASIO for hand-over runs. (T1) +5. Ask the reporter for the exact build commit, desktop scale factor, and a + z-order-preserving sibling dump of Live's main window children in both + states. + +No fix should land before one of these probes confirms a mechanism. Every +candidate change (learnheal matcher scoping, dxgi 0016-parity guards, popup-mode +re-evaluation, routing-state fix) touches machinery shared with the Learn View +park/reopen path, M4L and JUCE editors, so each needs the regression set 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. 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 ); + } From 0ddee82640b856b59c81cb281505bf14e1e5e472 Mon Sep 17 00:00:00 2001 From: Shibco Date: Wed, 5 Aug 2026 00:26:39 +0200 Subject: [PATCH 3/3] Findings note: plain-language revision Same facts, rewritten for a first-time reader: action and current state first, every technical term defined where it appears, shorter sentences, theories ordered with their tests. No content changes. --- ...NDINGS-ISSUE-87-SPLICE-INPUT-2026-08-04.md | 484 ++++++++---------- 1 file changed, 220 insertions(+), 264 deletions(-) diff --git a/notes/FINDINGS-ISSUE-87-SPLICE-INPUT-2026-08-04.md b/notes/FINDINGS-ISSUE-87-SPLICE-INPUT-2026-08-04.md index ecc43abc..c2bcdbac 100644 --- a/notes/FINDINGS-ISSUE-87-SPLICE-INPUT-2026-08-04.md +++ b/notes/FINDINGS-ISSUE-87-SPLICE-INPUT-2026-08-04.md @@ -1,264 +1,220 @@ -# Issue 87: Splice panel input-dead after collapse/reopen. Theory review, 2026-08-04 - -Status 2026-08-05: fix attempt implemented on this branch, runtime verification -pending. Three changes: - -1. tools/learnheal.c: the resident healer now only pokes webviews whose whole - ancestry is Live's own pane hosts (Ableton Live Window Class, - AbletonWebViewHelperWindow, Chrome_WidgetWin_0). Plugin webviews (CHOC, - JUCE) are never touched. Rebuilt learnheal.exe and posteresize.exe are - committed. Addresses T2 below. -2. patches/0070: dxgi twins of the patch 0016 subclass guards (re-bind records - its own wndproc as original; popup-mode self-demotion on re-bind; Release - stranding an installed subclass without its forwarding prop, which is the - exact painted-but-input-dead shape). Addresses T3's fixable half. Applies - cleanly on the series; dxgi.dll and wineserver compiled warning-free in a - scratch tree; NOT run against Live yet. -3. tools/issue87-routing-trace.patch: log-only wineserver trace for the T1 - probe (not in the shipped series). One healthy-vs-broken hover capture - decides T1; usage in the patch header. - -None of this is verified against the repro. If the reporters still reproduce -with 1+2, the trace capture from 3 is the next step and pins T1. - -Branch: fix/webview-splice (worktree webview-splice, off origin/main bac1bb0). - -## 1. The reported facts - -From ClickSentinel's report (runtime label "2026.07.23.1", but the stated base is -d2d1-dcomp-11.13 / 5c23dd1c with patches 0001-0053 plus 0054, which is later than -our v2026.07.23.1 tag; go by the stated patch list, and ask for the exact commit): - -- Collapse and reopen the Splice panel. The panel keeps rendering and keeps - responding to resize, but no mouse or keyboard input reaches it, permanently. -- The whole CHOC webview host is destroyed and recreated: new unique window class - (CHOCWebView117967303 to CHOCWebView118003504), new HWND. Only the WebView2 - renderer process respawns; browser, GPU and utility processes survive. -- During a 10 s in-panel hover, mouse messages observed from Live's process go - only to Live's main window in both states: 28730 healthy, 710 broken, 0 to the - child chain either way. -- In the broken state: WindowFromPoint resolves 20/20 in-panel points to the - webview child; WM_NCHITTEST replies HTCLIENT; capture, focus, styles, tree all - measure identical to healthy; dcomp props balanced; patch 0016 instrumentation - never fires; reblit timer counters read 0. -- Clicking Live's back/forward browser-navigation arrows restores input - (found by amenohi2 on issue 34, confirmed by ClickSentinel). - -## 2. The report's framing is wrong about the input path - -The issue concludes the failure sits in WebView2/CHOC's composition-hosting input -forwarding (SendMouseInput re-wiring), closed-source and unobservable. CHOC's -source says otherwise (github.com/Tracktion/choc, choc/gui/choc_WebView.h): - -- CHOC creates a plain WS_POPUP window ("CHOCWebView" class plus unique suffix) - and calls CreateCoreWebView2Controller(hwnd, ...) on it: windowed hosting - (choc_WebView.h lines 1368, 1038, 1452). Its vendored WebView2 header contains - no ICoreWebView2CompositionController and no SendMouseInput at all. -- CHOC's wndproc handles only WM_SIZE and WM_SHOWWINDOW; everything else goes to - DefWindowProcW (lines 1561-1572). CHOC forwards no input, ever. -- WS_EX_NOREDIRECTIONBITMAP on Chrome_WidgetWin_1 does not imply composition - hosting: our own Learn View panes are windowed-hosted and carry the same - styles (tools/dpispy.txt, tools/swamprobe.txt). - -Consequence, and this is the load-bearing deduction: since CHOC forwards nothing -and windowed hosting has no host-side input API, the only way input can work at -all is direct Win32 delivery to Chromium's own windows (Chrome_WidgetWin_1 / -Chrome_RenderWidgetHostHWND), which are owned by the msedgewebview2 browser -process, cross-process children inside Live's tree. Healthy input works, so that -delivery works. The bug is therefore that this delivery stops for the recreated -chain. That is a Wine-observable, Wine-instrumentable failure, not a -closed-source one. - -Two measurement blind spots follow: - -- "0 messages to the child chain" was measured from Live's process. Message - hooks are per-process; deliveries to the browser process's windows are - invisible from Live. The 0 proves nothing about the real input path. -- The healthy 28730-vs-broken 710 rate at Live's main window (a 40x drop for the - same physical hover) is unexplained under the report's model. Under ours, a - plausible reading is that the healthy stream is reflected traffic from - Chromium's legacy-window forwarding (LegacyRenderWidgetHostHWND passes mouse - messages up the parent chain), which disappears exactly when direct delivery - stops, leaving only the raw ~70/s motion rate. Unproven; the wineserver trace - below decides it. - -The "reparented into a new host window, put_ParentWindow unsupported" reading is -also unsupported: CHOC recreates the whole webview from scratch. The surviving -browser/GPU processes are the shared Evergreen browser cluster for Splice's -user-data folder, and a renderer respawn for a new CoreWebView2 in a shared -cluster is the normal pattern, not evidence of reparenting. - -## 3. How input actually routes, and why the reporter's checks do not cover it - -In the 11.13 base, hardware mouse routing and WindowFromPoint are different code -paths that consult the same server data differently: - -- Routing: winex11 in Live's process posts the event against Live's top-level; - the server picks the destination thread via window_thread_from_point - (server/window.c:1069), which maps raw to virtual coordinates, then descends - with child_window_from_point using is_point_in_window (server/window.c:933) at - each step: WS_VISIBLE, disabled-child skip, LAYERED+TRANSPARENT skip, - per-window DPI point mapping, visible_rect containment, win_region - containment. The deepest window's thread gets the message; the receiving - process then re-resolves the final HWND client-side (win32u/message.c:2625). -- WindowFromPoint: a get_window_children_from_point server request with the - caller's thread DPI, then a client-side walk (win32u/window.c:2846) that stops - at the first cross-thread window without consulting its wndproc. - -So a window chain can pass WindowFromPoint from a prober's context and still -lose hardware routing. The state that can differ is exactly the state absent -from the reporter's dumps: visible_rect (driver-adjusted at SetWindowPos time; -the NSPA base patches 0002/0003 gate it), win_region, per-window DPI (Live -creates VST3 editor windows under a DPI-UNAWARE thread context, see -tools/webviewclose.c header; a re-attach that latches a different per-window DPI -than the first attach shifts the descent's point mapping), sibling z-order, and -the owning thread. Styles, rects and tree dumps show none of these. - -Keyboard death needs no separate mechanism: windowed-hosting keyboard input -follows focus, focus follows a successful click, so mouse death implies -keyboard death. - -## 4. Theories, ranked - -### T1. Server-side routing state on the recreated chain (primary frame) - -The recreated Chromium windows fail some is_point_in_window check server-side, -so window_thread_from_point resolves to Live's thread; Live's client-side -re-resolution can only land on Live-owned windows; input pools at Live's main -window and dies. Rendering is unaffected because presentation is the dcomp/GDI -blit path, and resize is unaffected because it flows through COM (put_Bounds), -neither touching input routing. - -Why recreate differs from first create: at first launch the browser cluster -takes seconds to spawn, so the controller attaches into a settled window tree. -On reopen the cluster already runs and CreateCoreWebView2Controller completes in -milliseconds, so Chromium's SetParent/SetWindowPos sequence lands mid-collapse -layout (panel width animating, CHOC host possibly still an unmapped WS_POPUP). -Windows tolerates that ordering; our stack has several places that latch -attach-time state (NSPA visible-rect gates, layered-map delay noted in patch -0041's message, per-window DPI at creation). - -Heal fit: back/forward navigation makes Chromium rebuild its -RenderWidgetHostView and legacy HWND inside a settled tree, recreating the -server-side state cleanly. - -Deciding probe: a log-only wineserver patch in window_thread_from_point / -is_point_in_window printing, for each descent step during a broken-state hover, -which window was rejected by which check (visibility, transparency, DPI-mapped -point vs visible_rect, region), plus a server-eye dump of visible_rect, -win_region, per-window DPI and owning thread for the chain, healthy vs broken. -Nobody has looked at this layer; all prior instrumentation was client-side or -dcomp-side. - -### T2. learnheal.exe pokes the rebuilt webview at bind time - -Present and active in the reporter's environment (launcher starts it -unconditionally since 2026.07.21.2). Its matcher is any Chrome_WidgetWin_1, -anywhere, with a non-empty title and rect at least 200x200 (tools/learnheal.c:99-112); -WebView2 mirrors the page title onto Chrome_WidgetWin_1, so Splice's webview -qualifies once its page loads. A recreated webview is a new HWND, so it gets a -fresh tracked slot and a cross-process SetWindowPos nudge roughly 1-3 s after it -appears, which on a reopen is exactly bind time. learnheal's own header records -that bind-time nudges leave Chromium permanently degraded ("Nudging at bind -time leaves Chromium unable to handle later resizes", tools/learnheal.c:5-7). -The documented degradation is resize handling, not input, so the symptom match -is imperfect; but this is a resident cross-process actor firing during the -exact failure window, entirely under our control. - -Deciding probe: suppress learnheal, repeat the repro. Cheapest test available; -zero build. If implicated, the low-risk fix is scoping its matcher (title or -ancestor restricted to Live's own panes), not removing the heal. - -### T3. dxgi popup-mode misassignment via the process-global subclass count - -The base dxgi carries a deliberate process-global heuristic -(dcomp_subclassed_target_count, factory.c:440,844-921): if any target is still -subclassed when a new WM_WINE_DCOMP_SET_TARGET bind arrives, the new target is -permanently classified as a popup. The count's increment is unconditional but -its decrement is conditional (only on the window's own thread, only from paths -that may not run in a Release-first teardown), so both a teardown overlap -(Chromium's GPU thread releasing the old swapchain lazily while the new panel -binds) and a genuine leak put the rebuilt Splice target into popup mode. - -Popup mode installs a different wndproc with a different timer id and no -frame-latency signalling. Two observations make this attractive: - -- The reporter's "reblit counters read 0" was measured on the full-mode timer - path. A popup-mode target produces that reading by construction, so the - negative result is consistent with this theory rather than evidence against - dxgi involvement. -- The base's own comment warns a misclassified main window "loses its present - timer". - -Points against: the frame-latency stall arm predicts degraded or frozen frame -production, while the report describes correct rendering and live resize. It -needs the probe to decide whether the panel's apparent liveness is real frames -or GDI re-blits of the last comp buffer. - -Deciding probes: the base already logs the mode decision -(FIXME "DComp popup-detect: target %p ... count=%ld", factory.c:865-867); -capture it across the repro with WINEDEBUG active (mind the launcher's -WINEDEBUG=-all default, issue 87's own gotcha). Also dump -GetProp(desktop, "__wine_dcomp_active"), and which of DCOMP_REBLIT_TIMER_ID / -DCOMP_POPUP_REBLIT_TIMER_ID is armed on the new target. - -Related latent defect found during this review, independent of issue 87: the -dxgi subclass never received patch 0016's re-entry guards. A second SET_TARGET -on the same HWND records dxgi's own wndproc as "original" -(factory.c:902-907), and a missing __wine_dcomp_orig_wndproc prop sends every -message to DefWindowProcW (factory.c:748-749): the exact input-dead black hole -0016 fixed in dcomp.dll, still open in dxgi. The usual target (Intermediate D3D -Window) is input-transparent by design, so this is probably not issue 87, but -it should be fixed on its own merits, with regression runs for the Learn View -park/reopen cycle, M4L and JUCE/SWAM editors. - -### T4. Patch 0045's return-code change steering host teardown - -RevokeDragDrop on a foreign window now returns DRAGDROP_E_INVALIDHWND, a hard -error, including for foreign windows that were never registered (previously -DRAGDROP_E_NOTREGISTERED). If the JUCE-style teardown sweep in Live's process -treats a hard error as fatal and aborts its per-window loop, the old editor's -teardown completes only partially, which could feed the T3 overlap or leave -other per-window teardown undone. Also the marshalled drop-target data in the -helper process now leaks on every close. Directly relevant to issue 34 -(Splice drag-and-drop, closed but reportedly still broken, same commenter). -Probe: log RevokeDragDrop calls and returns during a collapse. - -## 5. Environment factors specific to Splice - -- Our launcher's WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS (SwiftShader ANGLE, - --disable-gpu-compositing, --disable-direct-composition, --no-sandbox) are - overridden by Ableton's own flags for Live's panes, but a plugin-created - environment inherits them for real. Splice therefore runs a browser - configuration nothing else on this stack runs. Yet the reporter still - observes dcomp props and an Intermediate D3D Window on the Splice chain, so - the flag set deserves its own check (does --disable-direct-composition reach - Splice's cluster at all, and what does the stack look like with the flags - cleared). -- WebView2 Evergreen 150 uses delegated compositing, a path this stack was - never tested against (notes/ABLETON-WINE-GPU-RENDERER-WEBVIEW2-DIAGNOSIS.md, - fork issue 8). Record the prefix's WebView2 version in any repro. - -## 6. Diagnostic plan, cheap first - -1. learnheal off, repro on. No build. (T2) -2. Repro with WINEDEBUG explicitly set, capture the existing popup-detect FIXME - and RevokeDragDrop returns across a collapse/reopen. Validate the capture is - non-empty before trusting it. No build. (T3, T4) -3. Prop/timer spy on the rebuilt chain: desktop __wine_dcomp_active, wndproc vs - __wine_dcomp_orig_wndproc, armed timer ids. Small tool, no runtime change. (T3) -4. Log-only wineserver + win32u tracing of the routing descent during a broken - hover, plus server-eye state dump of the chain (visible_rect, win_region, - per-window DPI, owning thread, z-order) healthy vs broken. Scratch build, - no behavior change; note scratch builds lack PipeASIO for hand-over runs. (T1) -5. Ask the reporter for the exact build commit, desktop scale factor, and a - z-order-preserving sibling dump of Live's main window children in both - states. - -No fix should land before one of these probes confirms a mechanism. Every -candidate change (learnheal matcher scoping, dxgi 0016-parity guards, popup-mode -re-evaluation, routing-state fix) touches machinery shared with the Learn View -park/reopen path, M4L and JUCE editors, so each needs the regression set 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. +# 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.