Since 8.0.13, loading a JUCE-based plugin can change the host process’s DPI awareness on Windows.
Commit 57c739c444 (“Windows: Remove isStandaloneApp checks”) removed the isStandaloneApp() guard inside setDPIAwareness(). That function calls SetProcessDpiAwarenessContext / SetProcessDpiAwareness / SetProcessDPIAware at process scope, and it runs from the HWNDComponentPeer constructor, so it now fires when a plugin editor window is created.
On a host that already declared its DPI awareness via manifest these calls are no-ops, so most modern DAWs are fine. But on a host that deliberately stays DPI-unaware and relies on Windows’ default scaling, the plugin now forces the whole host process to become DPI-aware. A concrete case is FL Studio’s “FL64 (scaled).exe”, which runs unaware on purpose so Windows bitmap-scales the UI to the system display setting - loading a JUCE plugin breaks that scaling. Before this commit, plugins were explicitly excluded from the process-wide path.
The query/response changes in the same commit look correct (a plugin should reflect the host’s real awareness), but the process-wide setter arguably shouldn’t run from inside a plugin - that’s what the thread-scoped ScopedThreadDPIAwarenessSetter is for.
Was removing the standalone guard on the setter intentional? It seems like the guard could stay on the SetProcess* calls while keeping the rest of the change.
Follow-up with a possible fix.(with the help of Claude)
A window’s DPI awareness is fixed at CreateWindow time from the calling thread’s context, so JUCE doesn’t need the process-wide call at all - it can make its own windows per-monitor aware at creation and leave the host process alone. Windows supports a per-monitor-aware child window inside an unaware host (mixed-mode DPI), which is exactly the FL64 (scaled) case.
Rough shape:
setDPIAwareness(): only run the SetProcess* calls when isStandaloneApp(), otherwise return early. Plugins stop touching the host.
In createWindowOnMessageThread(), wrap the CreateWindowEx call so the thread is PER_MONITOR_AWARE_V2 for that call and restored right after (the ScopedThreadDpiAwarenessEnablement added in 7e8366f5 already does this). The window is then born per-monitor aware regardless of the process.
Do the same around the hidden message window’s CreateWindow, so it matches the peer’s context (the jassert in createWindowOnMessageThread). That one lives in juce_events, which can’t depend on juce_gui_basics, so the SetThreadDpiAwarenessContext / restore has to be done inline there.
End result: JUCE’s windows are all per-monitor aware and consistent with each other, rendering works the same as it does now, and the host’s process awareness is never changed.
It was, since ‘is standalone’ doesn’t do a great job of capturing the situations where we’d want to enable dpi awareness. Another plausible usecase is building JUCE UI into a dynamic library that is linked to a standalone app that doesn’t use the JUCEApplicationBase. The app would still want to be DPI aware, but the ‘is standalone’ check would bypass some relevant branches.
You must call this API before you call any APIs that depend on the DPI awareness (including before creating any UI in your process). Once API awareness is set for an app, any future calls to this API will fail.
I’d interpreted that to mean that attempting to set the process’s DPI mode will fail once any UI had been created, but it sounds like that’s not the case in practice.
Please could you provide the following information:
Have you tested with the develop branch? I (literally just) pushed some changes that fixed some lingering issues in 8.0.13, so maybe the problems you’re seeing have already been resolved.
If the problems are still present on develop, please could you provide a description of the problem beyond ‘broken scaling’ - are windows appearing in the wrong place, or at the wrong size, or something else? A screenshot might help.
It would also be helpful to know whether the issue can be reproduced in any of the JUCE example plugins.