Skip to content

Windows: Allow opting back into Direct2D under Wine - #1701

Open
giang17 wants to merge 1 commit into
juce-framework:masterfrom
giang17:wine-direct2d-opt-in
Open

Windows: Allow opting back into Direct2D under Wine#1701
giang17 wants to merge 1 commit into
juce-framework:masterfrom
giang17:wine-direct2d-opt-in

Conversation

@giang17

@giang17 giang17 commented Aug 10, 2026

Copy link
Copy Markdown

What this does

5179690ff7 made JUCE fall back to the software renderer whenever it detects Wine, because
stock Wine does not implement the Direct2D 1.3 / DirectComposition surface the Direct2D
renderer needs. That is the right default and this PR does not change it.

It does add a way to turn the fallback off, for Wine builds that do implement that surface:

  • JUCE_ALLOW_DIRECT2D_UNDER_WINE=1 at build time, or
  • an environment variable of the same name set to something other than 0 at run time.

Both are off by default, so behaviour is unchanged unless someone asks for it. Wine stays
unsupported either way — the point is only that people who have solved the underlying problem
no longer have to patch JUCE to benefit from it.

The two Wine checks that select the renderer (Component::createNewPeer and
NativeImageType::create) now go through a single juce_shouldAvoidDirect2DUnderWine().
The font handling introduced in the same commit is deliberately left alone.

Why the run-time switch as well as the build-time one

The build-time macro only helps plugin developers. The people who most need this are Wine
users and distributions, who have the binaries but not the build. The environment variable
lets them enable it for plug-ins they did not compile — which is exactly the group currently
forced to maintain patched JUCE copies.

Testing

Built JUCE 8.0.15 with this patch (MinGW cross-compile) and ran the resulting GUI app under a
Wine build that implements the Direct2D and DirectComposition path — giang17/wine, branch
d2d1-dcomp-11.0
, which adds the
Direct2D effect surface, dcomp/DCompositionCreateDevice and
CreateSwapChainForComposition — counting d2d1 entry points via WINEDEBUG=+d2d:

Run JUCE_ALLOW_DIRECT2D_UNDER_WINE d2d1 calls
A unset 23
B 1 2,135,717
C 0 23

The 23 calls in A and C are just factory/device creation and teardown — no
d2d_device_context_* at all, i.e. nothing is drawn through Direct2D, which is the existing
fallback behaviour. In B there are ~299k device-context operations and the app renders
correctly (all widgets, animation running, no black window).

For reference, the same measurement on an unpatched build: JUCE 8.0.12 produces ~2.7M d2d1
calls on this Wine build, 8.0.13+ produces 23. So the capability is present and the fallback
is what stands between it and the renderer.

Context

This comes out of the discussion in
Juce8 Direct2D WINE/yabridge.
The stated hope there was to "reduce the number of JUCE forks being used by people" — for the
subset of Wine builds that implement Direct2D 1.3 and CreateSwapChainForComposition, the
unconditional fallback currently has the opposite effect, since the only way to get the
renderer back is to patch JUCE.

Such builds exist and are in use: giang17/wine d2d1-dcomp-11.0
is the one measured above, and it has downstream adopters (distributions and plug-in vendors
shipping or recommending it). Before 8.0.13 those users ran JUCE 8 plug-ins on the Direct2D
path without patching anything; since 8.0.13 they cannot, which is what this PR is about.

One concrete cost: WebBrowserComponent loses its content

WebBrowserComponent embeds WebView2 into the peer's window. On the Direct2D path that window
has a composition swapchain and Chromium presents into it; under the GDI fallback there is no
swapchain, so the page renders and the frame has nowhere to go.

This is not specific to our setup — it was reported independently in the yabridge thread three
months ago, about Synthesizer V 2 on a stock-Wine setup
(#386, 2026-05-18):

works for me as a plugin (minus the web view bits which aren't needed for functionality)

There the loss was incidental. Where the editor is a web view it is total: a JUCE 8.0.13
plug-in of that kind (Minimal Audio "EPROM - Memory Rites", licensed copy) shows only its
placeholder background under the fallback — no artwork, no icons, no content. With the renderer
forced back to Direct2D on the same binary it renders completely, matching a Windows reference
capture element for element, and three symptoms we had tracked separately (content appearing
only after a resize, flicker, unresponsive clicks) disappeared along with it.

Worth noting for the diagnosis cost: the switch is invisible from the outside. No error, no log
line, and a commit title that reads as an improvement. We spent weeks instrumenting the
composition path — probes in SetContent, thirteen causal experiments, all correctly measured
— before realising the plug-in had stopped entering that path in the first place.

Independently reproduced

A third party has since confirmed the whole picture on their own rig (yabridge 5.1.1 + Carla,
offscreen X, four commercial plug-ins checked against their compiled JUCE_VERSION) —
giang17/wine#11 (comment):

  • Minimal Audio Evoke (8.0.13): under the fallback, a skeleton UI of labels and thin
    strokes; with the renderer switched back, the complete UI — knob bodies, value rings,
    spectrum panel, keyboard strip, icons, every readout. Before/after captures in the comment.
    A phantom hover tooltip that had looked like a separate window bug went with it.
  • EPROM (8.0.13, WebView2 fixed-version 150): placeholder card under the fallback; full
    start screen presenting at rest with the renderer back.
  • Their control plug-ins on 8.0.12 were unaffected either way, matching the version boundary.

Their measurement is also a cleaner probe than raw call volume: app-side rasterization can
drive millions of d2d1 calls regardless of the peer's renderer, but
d2d_device_create_device_context separates the two states sharply — 4 creations in 45 s
with the fallback active versus 2,512 with the Direct2D peer restored.

Happy to adjust the naming, drop the environment variable if a build-time switch alone is
preferred, or gate it behind a deliberately unappealing name if that helps signal that it is
unsupported.

JUCE falls back to the software renderer whenever it detects Wine, because
stock Wine does not implement the Direct2D 1.3 and DirectComposition surface
the Direct2D renderer needs. Some Wine builds do implement it, and there the
fallback is not wanted; today those users have to patch JUCE to get the
Direct2D path back.

Route both Wine checks that select the renderer through a single
juce_shouldAvoidDirect2DUnderWine(), which can be turned off either at build
time via JUCE_ALLOW_DIRECT2D_UNDER_WINE or at run time via an environment
variable of the same name, so a Wine distribution can enable it for binaries
it did not build itself.

Both are off by default, so the fallback and the unsupported status of Wine
are unchanged. The font handling is deliberately left alone.
@giang17

giang17 commented Aug 10, 2026

Copy link
Copy Markdown
Author

A concrete case has since turned up that makes this less abstract than the original
description, so I'm adding it here rather than opening a separate issue.

WebView2-based plugins lose their content entirely under the fallback

WebBrowserComponent embeds WebView2 into the peer's window. On the Direct2D path that window
has a composition swapchain, and Chromium presents into it. Under the GDI fallback there is no
swapchain — so the page renders, and the frame has nowhere to go.

The plugin we hit this with is Minimal Audio's "EPROM - Memory Rites" (JUCE 8.0.13, licensed
copy, WebView2 UI). Under the fallback its editor shows the placeholder background only: no
artwork, no icons, no page content. With the renderer forced back to Direct2D on the same
binary, the login splash and the account screen render completely, matching a Windows reference
capture element for element. In the same step the content stopped appearing only after a resize,
the flicker disappeared, and clicks on the login page started working again — symptoms we had
been tracking as three separate problems.

For scale, WINEDEBUG=+d2d on a JUCE 8.0.13 plugin: 23 d2d1 entry points with the fallback
active — factory and device creation only, not one d2d_device_context_*, so nothing is drawn
through Direct2D — against 2,653,764 with it disabled, plus the composition swapchain window
that otherwise never exists.

I'll add that this cost us several weeks. We instrumented SetContent, measured a
dynamic_count of zero, established a permanently punch-black root leaf and ran thirteen causal
experiments, all of them correctly measured and all of them describing a pipeline the plugin had
stopped entering in June. Nothing pointed at the renderer choice, because the switch is not
visible from the outside: no error, no log line, and a commit title that reads as an improvement.

Why this argues for the opt-in rather than against the fallback

None of this is a case against the fallback. On stock Wine it is a clear improvement — the UI
becomes visible where it previously wasn't. The problem is only that it is unconditional. On a
Wine build that implements the Direct2D and DirectComposition surface, it takes away something
that demonstrably worked, and there is no way to say so.

That is exactly what this PR adds, and it changes nothing for anyone who doesn't ask for it: the
default stays as it is, and Wine stays unsupported. It just stops forcing the subset of users who
solved the underlying problem into maintaining patched copies of JUCE — the outcome the fallback
was meant to reduce.

Happy to adjust anything about the shape of it: drop the environment variable if a build-time
switch alone is preferred, rename it, or gate it behind a deliberately unappealing name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant