Skip to content

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

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

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

Conversation

@giang17

@giang17 giang17 commented Aug 10, 2026 •

Copy link
Copy Markdown

What this does

5179690ff7 ("Restore Wine functionality"; the commit message says nothing more) made JUCE fall
back to the software renderer whenever it detects Wine. The reason is plain enough on stock Wine
11.0: CreateSwapChainForComposition and DCompositionCreateDevice, which the Direct2D peer
presents through, are FIXME stubs
returning E_NOTIMPL there (dlls/dxgi/factory.c, dlls/dcomp/device.c), so the Direct2D path
cannot put anything on screen. That is the right default and this PR does not
change it.

It adds 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 two Wine checks that select the renderer (Component::createNewPeer and
NativeImageType::create) now go through a single juce_shouldAvoidDirect2DUnderWine().
The same commit also removed the Wine-specific default font names in
juce_DirectWriteTypeface_windows.cpp; that part is left alone.

Why — the two checks have to move together

The peer's public API already switches the renderer back: under Wine
getAvailableRenderingEngines() lists Direct2D and setCurrentRenderingEngine (1) constructs
the D2DRenderContext. What it cannot do is switch NativeImageType::create, which has no peer
and no override. That leaves a Direct2D context whose images are all SoftwarePixelData, and
the context does not handle that: PagesAndArea::make converts through NativeImageType, gets
software pixel data again, finds no bitmap pages and gives up. Measured on 8.0.15 with a
fifteen-panel probe: every drawImage, every setBufferedToImage child, setTiledImageFill,
DropShadow::drawForPath, DropShadowEffect and GlowEffect come out empty or as a solid
rectangle in that state (DropShadow::drawForRectangle is unaffected — it draws gradients, not
an image). Details, with the code path, are in #1721.

So the choice is not "which renderer" but "one switch for both halves or none". This PR is the
smallest form of that switch. The environment variable exists because the people who need it —
Wine users and distributions — have the binaries but not the build.

What it is not

This is not a claim that the software fallback loses anything visible. I measured that
directly (same probe: fallback versus both checks bypassed, Wine detection untouched): across
knob bodies, value rings, gradients, PNG bitmaps with alpha, drop shadows, transparency layers,
path clipping, tiled fills, buffered children and effect filters, nothing is missing under the
fallback. The differences are anti-aliasing, text hinting, and — in the other direction — two
rendering gaps in Wine's own d2d1 (a GaussianBlur effect that is created but never drawn, and
cubic Béziers approximated by a single quadratic). Those are Wine's to fix, and they are why the
switch is opt-in and unsupported: whoever enables it owns the result.

An earlier revision of this description argued that WebBrowserComponent loses its page content
under the fallback. That was wrong and is retracted in the comments below; WebView2 presents into
its own composition swapchain and does not depend on the peer's renderer.

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
— counting d2d1 entry
points via WINEDEBUG=+d2d over 16-second runs:

Run JUCE_ALLOW_DIRECT2D_UNDER_WINE d2d1 calls of which d2d_device_context_*
A unset 28 0
B 1 45,761 19,595
C 0 28 0

A and C are factory/device creation and teardown only — the existing fallback, and their
screenshots match within capture noise. B renders the full panel set with Image (Image::ARGB, …)
backed by Direct2DPixelData; its only deviations from the software renderer are the two
Wine-side d2d1 gaps named above.

Context

This comes out of the discussion in
Juce8 Direct2D WINE/yabridge.
The stated hope there (t0m, 9 February 2026) was "I hope we can reduce the number of JUCE forks
being used by people" — for the subset of Wine builds that implement the surface, the
unconditional fallback currently works against that: a plug-in developer can only reach a
consistent Direct2D state by patching JUCE, and a user of an already-compiled plug-in only by
hiding Wine from the whole process (a Wine-side patch that hides wine_get_version), which flips
every Wine-aware component at once (that is
exactly what produced the mistaken WebView2 claim retracted below).

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

Warning

Retracted (2026-08-29). The central claim below — that WebBrowserComponent
loses its content because the fallback leaves no composition swapchain for Chromium
to present into — is wrong. I have since tested the peer renderer in isolation, with
Wine detection left untouched, and the page content is identical whether the peer uses
the software renderer or Direct2D: WebView2 is hosted windowed and presents into its
own composition swapchain in its own process. The measurements that led me here changed
a process-wide setting, not JUCE's renderer choice. Details and numbers are in the
comments that follow. Leaving the original text below rather
than editing it away.


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.

@giang17
giang17 changed the base branch from master to develop August 29, 2026 17:42
@giang17

giang17 commented Aug 29, 2026 •

Copy link
Copy Markdown
Author

Correcting myself: the WebView2 argument above does not hold

My comment of 10 August claimed that WebBrowserComponent loses its page content under the
fallback, because the peer's window would have no composition swapchain for Chromium to
present into. I have now built the isolation test that claim needed, and it does not survive
it. Retracting it here rather than letting someone else find it.

Setup. A JUCE 8.0.15 app with a WebBrowserComponent, built from the 8.0.15 tag plus nine MinGW compile fixes that do not touch renderer selection
(without this PR's patch), switching the peer renderer through the existing public
setCurrentRenderingEngine(). Wine detection is never touched — wine_get_version stays
visible in every run — so JUCE's renderer is the only variable, and every other Wine-aware
component in the process behaves identically. The page carries a solid magenta block and the
screenshots are scored by counting that colour, so the verdict does not rest on my reading of
a picture. Runs are 40 s each, on the same Wine build and GPU.

Peer renderer d2d1 calls d2d_device_context_* magenta in window
peer's own choice (software) 28 0 30.28 %
setCurrentRenderingEngine (0) 28 0 30.28 %
setCurrentRenderingEngine (1) 889 590 30.28 %

The marker block is 220 px of a 700 px window, so ~31 % is a fully presented page — text, JS
timestamp and all. Identical across all three, to three decimals. The 28/0 rows confirm the
software renderer really was active (same signature as the 23 calls reported earlier: factory
and device creation, no device context at all), and 889/590 confirms the switch to Direct2D
really took effect.

Why it was never going to depend on the peer. The window list, same app, two runs:

Direct2D peer:   "DComp Swapchain" msedgewebview2.exe 900x660  +  "DComp Swapchain" <app> 900x700
software peer:   "DComp Swapchain" msedgewebview2.exe 900x660     (the app has none)

JUCE hosts WebView2 windowed, via CreateCoreWebView2Controller (hwnd). Chromium creates its
own composition swapchain in its own process and presents into that. It does not use the
peer's render context, so the peer's renderer cannot decide whether the page appears.

What that means for the earlier evidence. Those observations were made by hiding
wine_get_version, which is process-wide: it changes the behaviour of every Wine-aware
component in the process, not just JUCE's renderer choice. The visible change was real, but
attributing it to the peer renderer was not warranted by that setup. The same caveat applies
to the independently reproduced results in
giang17/wine#11 (comment) —
that report is about JUCE's own drawing rather than WebView2, so the test above neither
confirms nor refutes it, but it used the same process-wide switch and so is not cleanly
isolated either.

What is unaffected. The measurements under "Testing" in the PR description vary only
JUCE_ALLOW_DIRECT2D_UNDER_WINE, i.e. only JUCE's renderer selection, and stand as they are.

What the test turned up instead. Under Wine, getAvailableRenderingEngines() returns both
engines and setCurrentRenderingEngine (1) is accepted and builds the D2DRenderContext —
only createNewPeer's initial choice is forced. So the public API advertises and grants a
renderer that createNewPeer will never pick and that NativeImageType::create — which has
no peer and therefore no override at all — will never honour. That is filed separately as
#1721, and it holds regardless of what happens to this PR.

@giang17

giang17 commented Aug 29, 2026 •

Copy link
Copy Markdown
Author

Description rewritten — the case for this PR is consistency, not visual loss

I have measured the remaining open question from my previous comment: whether JUCE's own
drawing loses anything under the software fallback. It does not. Fifteen static panels (knob
bodies and value rings, spectrum fills, keyboard strips, cubic curves, Image via both pixel-data
types, PNG with alpha, DropShadow, transparency layers, path clipping, text, tiled fills,
setBufferedToImage, DropShadowEffect, GlowEffect), Wine detection untouched, screenshots
diffed per panel:

fallback vs. both checks bypassed pixels differing > 35 %
12 of 15 panels ≤ 0.6 % (edge anti-aliasing)
text 3.9 % — DirectWrite-on-D2D vs JUCE's rasteriser, hinting only
cubic curve 2.8 % — wrong under Direct2D: Wine's d2d1 approximates a cubic with one quadratic
GlowEffect gone under Direct2D: Wine's d2d1 creates CLSID_D2D1GaussianBlur but never draws it

So on today's Wine the fallback is, if anything, the more correct renderer for JUCE's own
drawing, and I have dropped every suggestion to the contrary from the description. Those two
gaps are Wine's, not JUCE's, and I am filing them on that side.

What the same probe did show is the half state reachable through the public API — Direct2D
peer via setCurrentRenderingEngine (1) with NativeImageType still software: every image draw
is dropped and alpha-masked fills become solid rectangles. That is written up in #1721 and is
now the whole argument for this patch: the two Wine checks need to move together, and this is
the smallest switch that does it.

The earlier third-party observations on Evoke were made with a process-wide switch and remain
unisolated; I am not relying on them.

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