Skip to content

WebView2 (DComp swapchain) never presents when the target HWND is a reparented child — JUCE 8 audio-plugin editors #8

Description

@blakebratcher

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

  1. Install the WebView2 Evergreen runtime into the prefix.
  2. Install a JUCE 8 plugin whose editor is a WebBrowserComponent (WebView2).
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions