Raw patch for getting DPI scaling working in plugins on Windows

Hi,

I’ve ran into issues with Juce wrt it not picking up the DPI awareness of a DPI aware host application (Premiere Pro) on Windows. The result was teeny unscaled dialogs. On Mac the Juce framework correctly scales under the same host application. Moreover, standalone Juce apps work fine on Windows as well.

I’ve delved a bit in the Juce code, and in the WIndows version of Desktop::getDefaultMasterScale() the DPI scaling is only looked at when we’re a JUCEApplicationBase::isStandaloneApp, which I guess (my) plugins aren’t. Just removing that test and always doing the DPI calculation fixes my plugins. And, yes, I know, that’s not the correct solution to the problem :slight_smile:

I’ve modified my copy of Juce 5.3.2 to fix this more elegantly. When running as a standalone Juce app, the code invokes the DPI awareness setting functions under Windows as it did before. However, if we’re not a standalone app, we can request the current state of our host app’s DPI awareness via similar functions. Using these I added code to the else branch of the JUCEApplicationBase::isStandaloneApp logic, to look if we should scale or not. And in my rudimentary test cases it works out nicely; my windows are now always DPI scaled correctly, no matter if I’m under Premiere or in a standalone app.

I basically:

  • added DLL imports for the IsProcessDPIAware and GetProcessDPIAwareness functions,
  • modfified setDPIAwareness() into initializeDPIAwareness(), which now not only sets it for standalone apps but also checks it for the system-DPI aware setting in plugins, and caches the result,
  • added isDPIAware() which checks the cached state above, and if it’s per-monitor aware it re-evaluates the state on each call (per-monitor can only be derived at when the host used SetProcessDPIAwareness(), and in that case I think the host is free to change it later on? def. don’t know for sure though)

I also spotted the check on getDPIForMonitor() being available in setDPIAwareness(), but not being used in that function. Probably because it is used in enumMonitorsProc() which depends on setDPIAwareness() to load the API function. There’s however room for improvement there too I think. As well as for per-monitor DPI awareness; I already saw commented-out code to set it in setDPIAwareness(), but it needs extra handling by catching the “DPI changed” messages when users drag the window onto other monitors etc. I already added commented-out accompanying code for the cached state though.

Also note that I mentioned “rudimentary tests”. I’m sure I’ve failed to think of lots of edge cases here, and I only tested on Windows 10 under Premiere. So extra testing is definitely needed.

I hope anyone finds it of use, and I hope this could get the Juce team onto the right track here as well. Any intermediate feedback is also very welcome; I intent to use my own patch in my own product, so if anyone spots opportunities for improvement that would be great!

Anyway, as for the patch (again, for Juce 5.3.2); all changes are made in modules\juce_gui_basics\native\juce_win32_Windowing.cpp

=================================

--- juce_win32_Windowing.cpp	Thu Nov 01 13:33:07 2018
+++ juce_win32_Windowing.cpp	Tue Jun 04 17:25:21 2019
@@ -253,7 +253,9 @@
 typedef BOOL (WINAPI* GetTouchInputInfoFunc) (HTOUCHINPUT, UINT, TOUCHINPUT*, int);
 typedef BOOL (WINAPI* CloseTouchInputHandleFunc) (HTOUCHINPUT);
 typedef BOOL (WINAPI* GetGestureInfoFunc) (HGESTUREINFO, GESTUREINFO*);
+typedef BOOL (WINAPI* IsProcessDPIAwareFunc)();
 typedef BOOL (WINAPI* SetProcessDPIAwareFunc)();
+typedef BOOL (WINAPI* GetProcessDPIAwarenessFunc) (HANDLE, Process_DPI_Awareness*);
 typedef BOOL (WINAPI* SetProcessDPIAwarenessFunc) (Process_DPI_Awareness);
 typedef HRESULT (WINAPI* GetDPIForMonitorFunc) (HMONITOR, Monitor_DPI_Type, UINT*, UINT*);
 
@@ -261,7 +263,9 @@
 static GetTouchInputInfoFunc      getTouchInputInfo = nullptr;
 static CloseTouchInputHandleFunc  closeTouchInputHandle = nullptr;
 static GetGestureInfoFunc         getGestureInfo = nullptr;
+static IsProcessDPIAwareFunc      isProcessDPIAware = nullptr;
 static SetProcessDPIAwareFunc     setProcessDPIAware = nullptr;
+static GetProcessDPIAwarenessFunc getProcessDPIAwareness = nullptr;
 static SetProcessDPIAwarenessFunc setProcessDPIAwareness = nullptr;
 static GetDPIForMonitorFunc       getDPIForMonitor = nullptr;
 
@@ -329,41 +333,88 @@
 }
 
 //==============================================================================
+enum DPI_Awareness_State
+{
+    DPI_Awareness_State_Unset,
+    DPI_Awareness_State_Unaware,
+    DPI_Awareness_State_Aware,
+    DPI_Awareness_State_PerMonitor
+};
+static DPI_Awareness_State dpiAwarenessState = DPI_Awareness_State_Unset;
+static void initializeDPIAwareness()
+{
+    if (dpiAwarenessState == DPI_Awareness_State_Unset) {
+        dpiAwarenessState = DPI_Awareness_State_Unaware;
+        HMODULE shcoreModule = GetModuleHandleA("SHCore.dll");
+        if (shcoreModule != 0)
+        {
+            getDPIForMonitor = (GetDPIForMonitorFunc)GetProcAddress(shcoreModule, "GetDpiForMonitor");
+            setProcessDPIAwareness = (SetProcessDPIAwarenessFunc)GetProcAddress(shcoreModule, "SetProcessDpiAwareness");
+            setProcessDPIAware = (SetProcessDPIAwareFunc)getUser32Function("SetProcessDPIAware");
+            isProcessDPIAware = (IsProcessDPIAwareFunc)getUser32Function("IsProcessDPIAware");
+            getProcessDPIAwareness = (GetProcessDPIAwarenessFunc)GetProcAddress(shcoreModule, "GetProcessDpiAwareness");
+            if (JUCEApplicationBase::isStandaloneApp())
+            {
+                if (setProcessDPIAwareness != nullptr && getDPIForMonitor != nullptr
+//                    && SUCCEEDED(setProcessDPIAwareness(Process_Per_Monitor_DPI_Aware)))
+                    && SUCCEEDED(setProcessDPIAwareness(Process_System_DPI_Aware))) // (keep using this mode temporarily..)
+//                    dpiAwarenessState = DPI_Awareness_State_PerMonitor;
+                    dpiAwarenessState = DPI_Awareness_State_Aware;
+                else if (setProcessDPIAware != nullptr
+                    && setProcessDPIAware())
+                    dpiAwarenessState = DPI_Awareness_State_Aware;
+                else
+                    dpiAwarenessState = DPI_Awareness_State_Unaware;
+            }
+            else
+            {
+                Process_DPI_Awareness processDPIAwareness;
+                if (getProcessDPIAwareness != nullptr
+                    && SUCCEEDED(getProcessDPIAwareness(NULL, &processDPIAwareness)))
+                {
+                    if (processDPIAwareness == Process_Per_Monitor_DPI_Aware)
+                        dpiAwarenessState = DPI_Awareness_State_PerMonitor;
+                    else if (processDPIAwareness == Process_System_DPI_Aware)
+                        dpiAwarenessState = DPI_Awareness_State_Aware;
+                    else
+                        dpiAwarenessState = DPI_Awareness_State_Unaware;
+                }
+                else if (isProcessDPIAware != nullptr)
+                    if (isProcessDPIAware())
+                        dpiAwarenessState = DPI_Awareness_State_Aware;
+                    else
+                        dpiAwarenessState = DPI_Awareness_State_Unaware;
+            }
+        }
+    }
+}
+
+static bool isDPIAware()
+{
+    bool isAware = false;
+#if ! JUCE_DISABLE_WIN32_DPI_AWARENESS
+    initializeDPIAwareness();
+    if (dpiAwarenessState == DPI_Awareness_State_Aware)
+        isAware = true;
+    else if (dpiAwarenessState == DPI_Awareness_State_PerMonitor)
+    {
+        if (JUCEApplicationBase::isStandaloneApp())
+            isAware = false;
+        else
+        {
+            Process_DPI_Awareness processDPIAwareness;
+            if (getProcessDPIAwareness != nullptr
+                && SUCCEEDED(getProcessDPIAwareness(NULL, &processDPIAwareness)))
+                isAware = processDPIAwareness != Process_DPI_Unaware;
+        }
+    }
+#endif
+    return isAware;
+}
-static void setDPIAwareness()
-{
-   #if ! JUCE_DISABLE_WIN32_DPI_AWARENESS
-    if (JUCEApplicationBase::isStandaloneApp())
-    {
-        if (setProcessDPIAwareness == nullptr)
-        {
-            HMODULE shcoreModule = GetModuleHandleA ("SHCore.dll");
-
-            if (shcoreModule != 0)
-            {
-                setProcessDPIAwareness = (SetProcessDPIAwarenessFunc) GetProcAddress (shcoreModule, "SetProcessDpiAwareness");
-                getDPIForMonitor = (GetDPIForMonitorFunc) GetProcAddress (shcoreModule, "GetDpiForMonitor");
-
-                if (setProcessDPIAwareness != nullptr && getDPIForMonitor != nullptr
-//                     && SUCCEEDED (setProcessDPIAwareness (Process_Per_Monitor_DPI_Aware)))
-                     && SUCCEEDED (setProcessDPIAwareness (Process_System_DPI_Aware))) // (keep using this mode temporarily..)
-                    return;
-            }
-
-            if (setProcessDPIAware == nullptr)
-            {
-                setProcessDPIAware = (SetProcessDPIAwareFunc) getUser32Function ("SetProcessDPIAware");
-
-                if (setProcessDPIAware != nullptr)
-                    setProcessDPIAware();
-            }
-        }
-    }
-   #endif
-}
 
 static double getGlobalDPI()
 {
-    setDPIAwareness();
+    initializeDPIAwareness();
 
     HDC dc = GetDC (0);
     const double dpi = (GetDeviceCaps (dc, LOGPIXELSX)
@@ -374,8 +425,8 @@
 
 double Desktop::getDefaultMasterScale()
 {
-    return JUCEApplicationBase::isStandaloneApp() ? getGlobalDPI() / 96.0
-                                                  : 1.0;
+    return isDPIAware() ? getGlobalDPI() / 96.0
+                        : 1.0;
 }
 
 bool Desktop::canUseSemiTransparentWindows() noexcept       { return true; }
@@ -1826,7 +1877,7 @@
             if (canUseMultiTouch())
                 registerTouchWindow (hwnd, 0);
 
-            setDPIAwareness();
+            initializeDPIAwareness();
             setMessageFilter();
             updateBorderSize();
             checkForPointerAPI();
@@ -4034,7 +4085,7 @@
 
 void Desktop::Displays::findDisplays (float masterScale)
 {
-    setDPIAwareness();
+    initializeDPIAwareness();
 
     Array<MonitorInfo> monitors;
     EnumDisplayMonitors (0, 0, &enumMonitorsProc, (LPARAM) &monitors);

Have you tried this with the latest tip of the develop branch? There have been a lot of DPI-related changes in the last few months, and your patch looks like it’s against a very old version of that file…

No, not yet, since I’m working against a fixed version of Juce for now (to not introduce unnecessary variability close to my next release).

But thanks for the heads up! I’ll take a look at the latest version then tomorrow and see what has changed in this regard.

I’ve looked at the latest stable release (5.4.3), and while this one adds a lot of extra per-monitor DPI handling, it doesn’t utilize any of the “GetDPIAware”-type functions that can be used to tell in what mode the host process operates. Compared to the latest commit on the development branch, nothing significant has changed concerning this part of the DPI code.

Some observations on the latest 5.4.3 release:

  • getDefaultMasterScale() always returns 1.0 for plugins.
  • findDisplays() correctly swaps out the current context for a high-dpi-aware context and thus finds the DPI settings correctly, but IIRC this doesn’t work on Windows 7 so there I assume we’d not get the correct DPIs?
  • In my plugin, during createWindow() isPerMonitorDPIAwareThread() returns false because I guess the host process itself is not per-monitor DPI aware, and thus scaleFactor remains set at 1.0.
  • getPlatformScaleFactor(), getWindowRect() etc. for the same reason thus conclude we’re not DPI aware in any way and work with default 1.0 scaling to not interfere with any assumed default scaling by Windows.

Thus:

  • If we’re a Juce application then we set our own DPI awareness and all is fine.
  • If the host is DPI unaware this results in us also not scaling anything and we rely on the upscaling from Windows.
  • If the host is per-monitor DPI aware the new Juce code probably works OK and we scale ourselves accordingly.
  • However, if the host is DPI aware but just not per-monitor DPI aware (or if we’re running on Windows 7 or such which doesn’t have per-monitor DPI scaling IIRC?)), then we don’t do any scaling but Windows also won’t, resulting in dialogs that are far too small.

The best approach here again as with my original patch above is to not give up on determining the scaling if we’re a plugin under a non-per-monitor-dpi-aware host (and return 1.0 as the scaling factor), but to just query Windows on what our process’ status is. This can be done by looking if GetProcessDPIAwareness() returns Process_System_DPI_Aware, and if that function isn’t available, to fall back on checking if IsProcessDPIAware() returns false. In those cases our process is only system-dpi aware, which means Windows will not scale our hosts but also not our own windows, and we thus need to set the master scale factor to the monitor’s DPI / 96. If the user drage our window to another monitor with a different DPI, then Windows will do the appropriate conversion scaling for us at that time, so we’d also just need to keep using that same master scaling. One nice thing about this is that at least the older SetProcessDPIAware/IsProcessDPIAware combo is set for life; i.e. the app calls SetProcessDPIAware once and cannot change it’s mind afterwards; IsProcessDPIAware() thus supposedly always returns the same answer which can be cached in the first call to e.g. setDPIAwareness().

Or at least that’s what I make of the MSDN docs and some testing :slight_smile:

Is it worth while to update my patch to work against 5.4.3? The type of change needed is still the same as in my patch for 5.3.2, and I read patches are vivisected first and not taken onboard verbatim anyway?

1 Like

So, to be clear, your changes are dealing with the case where the host is system DPI aware and not per-monitor DPI aware (V1 or V2)?

If you could that would be great and I’ll take a look. Also, if you could upload the .diff file directly instead of pasting it into your post that would be useful as copying it across to a file seems to lose the trailing spaces resulting in a corrupt file.

Yes, that’s ocrrect. For unaware apps the best thing to do is to not sale at all, what Juce does in that case. And for per-monitor aware the latest Juce version also works nice I think. It’s the middle option (system DPI aware) where Juce could use some improvement: Windows does not scale for you (unless the DPI changes during the app’s life), so we need to arrange for scaling just like for per-monitor DPI aware, but the DPI if fixed from the start and any other scaling needed to be done after the fact is arranged by Windows. So a check once what scaling is needed is all that is missing; this could be done in the one-time init routine Juce already has for initializing the DPI setting in the first place. And the Windows API calls to use are GetProcessDPIAwareness (new) and IsProcessDPIAware (old). At least, that’s what I make of it all right now.

And I’d love to update my patch, but unfortunately it’s crunch time here; we’re right in front of a major release, so I doubt if I got the time for an updated patch soon…