Summary
A JUCE 8 WebBrowserComponent (WebView2) plugin editor renders nothing — the
host sees only the plugin's own placeholder background — when the editor HWND has
been reparented into a host window by an audio-plugin bridge (yabridge).
The same WebView2 runtime, same wine build, same prefix renders perfectly for a
normal top-level WebView2 app (Tauri/Minimal Hub). So the runtime and the d2d1/dcomp
stack work; the failure is specific to the composition target being an already-
reparented child window.
The web content itself is fully alive — this is purely a presentation failure:
document.readyState = complete, 351 elements, stylesheets loaded (98 + 3 rules)
document.visibilityState = visible (not an occlusion suspend)
- WebGL 2.0 works:
clearColor(1,0,0,1) + readPixels returns 255,0,0,255
Page.captureScreenshot via CDP returns an all-black image
(also with fromSurface:false and captureBeyondViewport:true)
So raster works, but the composed frame never reaches the target window.
Environment
|
|
| wine |
wine-11.0-167-g46c43a2db62, branch d2d1-dcomp-11.0 |
| WebView2 runtime |
Evergreen 150.0.4078.83 |
| Plugin |
Minimal Audio "EPROM - Memory Rites" 1.1.0, VST3, JUCE 8.0.13 |
| Bridge |
yabridge 5.1.1-54-g48ea9749 (VST3, individually hosted) |
| Hosts |
Carla 2.x and Bitwig Studio (identical symptom) |
| Prefix |
Bottles, DXVK/VKD3D off (wined3d) |
| GPU |
NVIDIA RTX 3090, proprietary driver |
Reproduction
- Install the WebView2 Evergreen runtime into the prefix.
- Install a JUCE 8 plugin whose editor is a
WebBrowserComponent (WebView2).
- Bridge it with yabridge and open its editor in any host.
Editor shows the plugin's placeholder background only; web content never appears.
Control that works: run a normal top-level WebView2 app (e.g. a Tauri app) in the
same prefix with the same runner — it renders correctly. Only the embedded/reparented
plugin-editor case fails.
Relevant trace (WINEDEBUG=+dcomp)
dxgi:dxgi_factory_CreateSwapChainForComposition Created composition window 00000000000900F8 (1100x720) for swapchain 000000000D261A90.
dcomp:dcomp_device_CreateTargetForHwnd Created composition target ... for hwnd 000000000008010E (subclassed, orig_wndproc ...).
dcomp:dcomp_target_SetRoot iface ..., visual ..., target hwnd 000000000008010E.
dcomp:dcomp_visual_SetContent iface ..., content 000000000D261A90 (visual: target_hwnd 000000000008010E).
dcomp:dcomp_visual_try_reparent Reparent skipped: content 0000000000000000, target_hwnd 000000000008010E.
dcomp:dcomp_visual_try_reparent Requested swapchain window switch from 00000000000900F8 to target 000000000008010E.
dcomp:dcomp_visual_try_reparent Switching swapchain window from 00000000000900F8 to target 000000000008010E.
dxgi:dcomp_swapchain_wndproc DComp popup-detect: target 000000000008010E style=0x46000000 parent=0000000000080104 count=0.
dxgi:dcomp_swapchain_wndproc DComp: subclassed target 000000000008010E, orig wndproc ..., timer started, sc=...
dxgi:dcomp_swapchain_wndproc DComp: target 000000000008010E client=1100x720 screen=(0,0)-(1100,720) parent=0000000000080104 sc=1100x720.
dxgi:dcomp_swapchain_wndproc DComp: target 000000000008010E now 1100x720, swapchain switched.
dxgi:dcomp_target_wndproc WM_PAINT #1: hwnd 000000000008010E 1100x720, comp_dc=0000000003410060.
dxgi:dcomp_target_wndproc WM_PAINT #2: hwnd 000000000008010E 1100x720, comp_dc=0000000003410060.
d3d:swapchain_blit_gdi Using GDI present.
Notes on the above:
- The target
0x8010E is not top-level: parent=0x80104, style 0x46000000
(WS_CHILD|WS_VISIBLE|WS_CLIPCHILDREN|WS_CLIPSIBLINGS). yabridge has reparented
the editor into the host's window before WebView2 creates its composition target.
- One
try_reparent call reports Reparent skipped: content 0000000000000000, i.e.
it ran before SetContent supplied a swapchain.
- Only two
WM_PAINT events are ever delivered to the subclassed target; after
that nothing further presents, even as the page animates.
Ruled out
Tested via the AdditionalBrowserArguments policy key (the
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS env var is ignored here because JUCE supplies
its own options object), each with a fresh browser process — WebView2 shares one
browser process per user-data-dir and silently ignores argument changes otherwise:
--disable-features=CalculateNativeWinOcclusion, --disable-backgrounding-occluded-windows,
--disable-renderer-backgrounding — no change (and visibilityState was already visible)
--disable-gpu, --disable-gpu-compositing — no change
--disable-direct-composition — no change; a DComp Swapchain window is still created
--use-angle=gl / --use-gl=angle — no change (WebGL still reports the D3D11 ANGLE backend)
--use-angle=swiftshader — WebView2 fails to start entirely
- yabridge
editor_double_embed — no change
Hypothesis
The dcomp/dxgi presentation path assumes the composition target is a top-level window
it can subclass and drive via WM_PAINT + GDI blit. When the target is already a
reparented child of a foreign (host) window, the swapchain window switch completes but
the subsequent present/paint cycle does not, so the composed frame is never blitted to
the visible window.
Happy to test patches or gather more traces — this reproduces 100% of the time.
Summary
A JUCE 8
WebBrowserComponent(WebView2) plugin editor renders nothing — thehost sees only the plugin's own placeholder background — when the editor HWND has
been reparented into a host window by an audio-plugin bridge (yabridge).
The same WebView2 runtime, same wine build, same prefix renders perfectly for a
normal top-level WebView2 app (Tauri/Minimal Hub). So the runtime and the d2d1/dcomp
stack work; the failure is specific to the composition target being an already-
reparented child window.
The web content itself is fully alive — this is purely a presentation failure:
document.readyState=complete, 351 elements, stylesheets loaded (98 + 3 rules)document.visibilityState=visible(not an occlusion suspend)clearColor(1,0,0,1)+readPixelsreturns255,0,0,255Page.captureScreenshotvia CDP returns an all-black image(also with
fromSurface:falseandcaptureBeyondViewport:true)So raster works, but the composed frame never reaches the target window.
Environment
wine-11.0-167-g46c43a2db62, branchd2d1-dcomp-11.0150.0.4078.835.1.1-54-g48ea9749(VST3, individually hosted)Reproduction
WebBrowserComponent(WebView2).Editor shows the plugin's placeholder background only; web content never appears.
Control that works: run a normal top-level WebView2 app (e.g. a Tauri app) in the
same prefix with the same runner — it renders correctly. Only the embedded/reparented
plugin-editor case fails.
Relevant trace (
WINEDEBUG=+dcomp)Notes on the above:
0x8010Eis not top-level:parent=0x80104, style0x46000000(
WS_CHILD|WS_VISIBLE|WS_CLIPCHILDREN|WS_CLIPSIBLINGS). yabridge has reparentedthe editor into the host's window before WebView2 creates its composition target.
try_reparentcall reportsReparent skipped: content 0000000000000000, i.e.it ran before
SetContentsupplied a swapchain.WM_PAINTevents are ever delivered to the subclassed target; afterthat nothing further presents, even as the page animates.
Ruled out
Tested via the
AdditionalBrowserArgumentspolicy key (theWEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTSenv var is ignored here because JUCE suppliesits own options object), each with a fresh browser process — WebView2 shares one
browser process per user-data-dir and silently ignores argument changes otherwise:
--disable-features=CalculateNativeWinOcclusion,--disable-backgrounding-occluded-windows,--disable-renderer-backgrounding— no change (andvisibilityStatewas alreadyvisible)--disable-gpu,--disable-gpu-compositing— no change--disable-direct-composition— no change; aDComp Swapchainwindow is still created--use-angle=gl/--use-gl=angle— no change (WebGL still reports the D3D11 ANGLE backend)--use-angle=swiftshader— WebView2 fails to start entirelyeditor_double_embed— no changeHypothesis
The dcomp/dxgi presentation path assumes the composition target is a top-level window
it can subclass and drive via
WM_PAINT+ GDI blit. When the target is already areparented child of a foreign (host) window, the swapchain window switch completes but
the subsequent present/paint cycle does not, so the composed frame is never blitted to
the visible window.
Happy to test patches or gather more traces — this reproduces 100% of the time.