nanogui icon indicating copy to clipboard operation
nanogui copied to clipboard

Support for high DPI screens

Open rogerdahl opened this issue 7 years ago • 25 comments

Thank you for NanoGUI, it's awesome!

Are there any plans to add support for scaling on Linux? I wrote an app using NanoGUI and it uses the size passed to Screen directly as pixel size. After upgrading my monitor to 4K and increasing the OS scale factor to match, most apps and GUI elements scaled appropriately, but my NanoGUI app is now tiny...

rogerdahl avatar Jan 23 '17 02:01 rogerdahl

Are you using Screen::pixelRatio() (or its equivalent mPixelRatio member variable)? You should be able to use it to multiply with Widget::mSize, but you may have to deal with float vs int stuff at some point too.

svenevs avatar Jan 23 '17 03:01 svenevs

I checked now and found that Screen::pixelRatio() return 1.

rogerdahl avatar Jan 23 '17 04:01 rogerdahl

That's unfortunate :/ Out of curiosity, if you manually resize your screen class to scale everything by 2 does it appear as you desire? (by 2 to go from 1920x1080 to 3840x2160).

If it does, then I'm not sure what to say. At the top of screen.cpp is where this is getting setup, I'm inclined to assume you may not have gtk or something. One thing you could try is getting rid of the __linux__ part so that it looks like

/* Calculate pixel ratio for hi-dpi devices. */
static float get_pixel_ratio(GLFWwindow *window) {
#if defined(_WIN32)
    HWND hWnd = glfwGetWin32Window(window);
    HMONITOR monitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST);
    /* The following function only exists on Windows 8.1+, but we don't want to make that a dependency */
    static HRESULT (WINAPI *GetDpiForMonitor_)(HMONITOR, UINT, UINT*, UINT*) = nullptr;
    static bool GetDpiForMonitor_tried = false;

    if (!GetDpiForMonitor_tried) {
        auto shcore = LoadLibrary(TEXT("shcore"));
        if (shcore)
            GetDpiForMonitor_ = (decltype(GetDpiForMonitor_)) GetProcAddress(shcore, "GetDpiForMonitor");
        GetDpiForMonitor_tried = true;
    }

    if (GetDpiForMonitor_) {
        uint32_t dpiX, dpiY;
        if (GetDpiForMonitor_(monitor, 0 /* effective DPI */, &dpiX, &dpiY) == S_OK)
            return std::round(dpiX / 96.0);
    }
    return 1.f;
#else
    Vector2i fbSize, size;
    glfwGetFramebufferSize(window, &fbSize[0], &fbSize[1]);
    glfwGetWindowSize(window, &size[0], &size[1]);
    return (float)fbSize[0] / (float)size[0];
#endif
}

There doesn't appear to be anything in there that wouldn't work on linux, and I know this code is reliable for OSX.

svenevs avatar Jan 23 '17 04:01 svenevs

Thank you for your help! The version I'm using does include a path for Linux in get_pixel_ratio():

#elif defined(__linux__)
    (void) window;

    /* Try to read the pixel ratio from GTK */
    FILE *fp = popen("gsettings get org.gnome.desktop.interface scaling-factor", "r");
    if (!fp)
        return 1;

    int ratio = 1;
    if (fscanf(fp, "uint32 %i", &ratio) != 1)
        return 1;

    if (pclose(fp) != 0)
        return 1;

    return ratio >= 1 ? ratio : 1;

I'm on Mate and gsettings get org.gnome.desktop.interface scaling-factor returns 0.

Wonder if there's a way to get that value that works on more Linux desktops.

rogerdahl avatar Jan 23 '17 04:01 rogerdahl

A quick test shows that forcing the return value of get_pixel_ratio() to a value above 1 causes everything to scale nicely.

rogerdahl avatar Jan 23 '17 04:01 rogerdahl

Ahhh I see the problem. Mate != Gnome, so even if you set org.gnome.desktop.interface scaling-factor, it won't matter because you can't run gnome and mate at the same time.

Before investigating more obtuse alternatives, does it work if you get rid of the #elif defined(__linux__) so that the code being executed for you is

Vector2i fbSize, size;
glfwGetFramebufferSize(window, &fbSize[0], &fbSize[1]);
glfwGetWindowSize(window, &size[0], &size[1]);
return (float)fbSize[0] / (float)size[0];

?

If that code is also not working for you, there are alternatives. To help, please paste the output of

  1. xrandr | fgrep '*', and
  2. xdpyinfo | grep dim

svenevs avatar Jan 23 '17 05:01 svenevs

Looks like querying X11 directly could be the way to go.

#include <X11/Xlib.h>

DisplayWidth();
DisplayHeight();
DisplayWidthMM();
DisplayHeightMM();

The returned values seem to make sense. I think the mm sizes are extrapolated from the display resolution and DPI values I've set in the desktop.

rogerdahl avatar Jan 23 '17 05:01 rogerdahl

Removing the Linux path and using the glfwGet functions did not work (got the small size). But the xrandr commands you suggested show what's happening. There is also a 1080p monitor hooked up to the box and that monitor is showing up as device 0. The primary 4k monitor is showing up below in the list. So I think it comes down to pulling the values for the monitor on which the NanoGUI window is going to appear instead of the monitor at device 0.

rogerdahl avatar Jan 23 '17 05:01 rogerdahl

There is also an issue when using KDE as a desktop. KDE does not use the gsettings file and therefore nanogui does not scale with high DPI screens when using KDE. I am not sure though how to retrieve the UI scaling factor in KDE.

dvicini avatar Sep 27 '17 07:09 dvicini

@dvicini: Would you mind checking if there is a way to do this on a KDE environment?

wjakob avatar Sep 27 '17 08:09 wjakob

Would you mind checking if there is a way to do this on a KDE environment?

It's kreadconfig.

However, maybe we can just use xdpyinfo? My understanding is that is part of X server, unrelated to the desktop manager. - Note: awk '{ print $2 }' is not safe, e.g. if grep is aliased. Easy solution is to /usr/bin/grep (almost certainly available), better solution would probably be an explicit regex for dimensions:\s+(\d+x d+)\s+

Otherwise, we need a way of detecting things. I think the variables that need to be relied on here are the XDG_* variants:

$ printenv | grep -i desktop
DESKTOP_SESSION=gnome
GNOME_DESKTOP_SESSION_ID=this-is-deprecated
XDG_SESSION_DESKTOP=gnome
XDG_CURRENT_DESKTOP=GNOME

I believe the promise that desktop managers make is to always set XDG_CURRENT_DESKTOP, kind of like how xdg-open filename.txt (if you look at the actual script) just cycles through all of the managers.

This approach (I believe) rules out anybody on linux who built their own window manager, though.

@dvicini can you post back the output of these two commands:

  1. xdpyinfo | grep dimensions
  2. printenv | grep -i desktop

This still only solves single-monitor scenarios BTW. Good enough for a hotfix for KDE though :wink:

svenevs avatar Sep 27 '17 08:09 svenevs

Hi,

Thanks for the input. So I ran the commands you suggested and get

dimensions:    3840x2160 pixels (613x352 millimeters)

and

QT_QUICK_CONTROLS_STYLE=org.kde.desktop
DESKTOP_SESSION=plasma
XDG_SESSION_DESKTOP=plasma
XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat0
XDG_CURRENT_DESKTOP=KDE
XDG_SESSION_PATH=/org/freedesktop/DisplayManager/Session0

i don't think the dimensions from xdpyinfo really help us here. For KDE, the suggested way of scaling the display content on high dpi screens is to go through the display settings: https://wiki.archlinux.org/index.php/HiDPI#Using_KDE_system_settings

If I change these, the dimensions output from xdpyinfo does not change (the size in millimeters is always the physical size of my screen).

I also found the kreadconfig command, but I don't know how to access the display settings using that, as there does not seem to be a list of available settings?

UPDATE: I found where the setting in question is stored. In my case it is in ~/.config/kdeglobals. I can now retrieve the scaling factor I set in the settings by using kreadconfig5 --group KScreen --key ScaleFactor So I guess this would be the right command to use?

Delio

dvicini avatar Sep 27 '17 09:09 dvicini

I think I figured out how to detect which desktop environment is used. I changed the relevant section in screen.cpp to:

    /* Try to read the pixel ratio from GTK */
    float ratio = 1.0f;
    if (std::getenv("XDG_CURRENT_DESKTOP") == std::string("KDE")) {
        FILE *fp = popen("kreadconfig5 --group KScreen --key ScaleFactor", "r");
        if (!fp)
            return 1;
    
        if (fscanf(fp, "%f", &ratio) != 1)
            return 1;    
        if (pclose(fp) != 0)
            return 1; 
    } else {
        FILE *fp = popen("gsettings get org.gnome.desktop.interface scaling-factor", "r");
        if (!fp)
            return 1;
    
        int ratioInt = 1;
        if (fscanf(fp, "uint32 %i", &ratioInt) != 1)
            return 1;
        ratio = ratioInt;
    
        if (pclose(fp) != 0)
            return 1;
    }
    return ratio >= 1 ? ratio : 1;

This works for me now, but it's not a very general solution, as it only works for KDE5 I guess. Should I make a pull request or are you looking for a more general solution?

dvicini avatar Sep 27 '17 10:09 dvicini

This looks reasonable. But what if std::getenv returns nullptr?

wjakob avatar Sep 27 '17 10:09 wjakob

The pclose(fp) can be factored out.

wjakob avatar Sep 27 '17 10:09 wjakob

i don't think the dimensions from xdpyinfo really help us here

~~Aren't we just checking the ratio of width / 1920 and height / 1080? I was under the impression thats what the NanoGUI reference resolution is.~~ No, that's not how DPI works sorry. However, an excerpt from OSX:

default screen number:    0
number of screens:    1

screen #0:
  dimensions:    1440x878 pixels (381x232 millimeters)
  resolution:    96x96 dots per inch

Thus xdpyinfo tabled for another day...

But what if std::getenv returns nullptr?

Then we can't do anything, in which case just return scale of 1?

svenevs avatar Sep 27 '17 10:09 svenevs

How about using xdpyinfo on the platforms where it returns correct resolution and physical size? Then calculate DPI.

rogerdahl avatar Sep 27 '17 11:09 rogerdahl

I prefer the scale factor approach. This is largely also a user choice -- a user might prefer 1.5x or 2.5x on a given screen regardless of the actual DPI value.

wjakob avatar Sep 27 '17 11:09 wjakob

Yes, the scale factor is user specific and should be accounted for, as the rest of the user interface is scaled by this factor. I will clean up the code (add nullptr check, factor out pclose) and create a pull request.

dvicini avatar Sep 27 '17 12:09 dvicini

First @wjakob, great work on this incredible library. Thank you.

(This is also related to #347 )

A few weeks ago I did some work on this as I was also having trouble with scaling on a stock X11 Ubuntu 18.04. I think the problem was that Gnome itself had a bug where gsettings get org.gnome.desktop.interface scaling-factor always returned 0 (despite what was set in the display settings UI) so get_pixel_ratio() in screen.cpp always returned 1.

GLFW 3.3 had not yet been completed but forcing nanogui to use a dev 3.3 version allowed me to use their new glfwGetWindowContentScale() which seems like a great platform agnostic solution. My testing showed this to work on that Ubuntu 18.04, macOS 10.14 (current, I think), and Windows 10.0.18362.

Now that GLFW 3.3 has landed, my thinking is that something like the following could replace everything currently in get_pixel_ratio().

/* Calculate pixel ratio for hi-dpi devices. */
static float get_pixel_ratio(GLFWwindow *window) {

    (void) window;

    float ratio = 1.0f;
    float xscale;
    float yscale;

    glfwGetWindowContentScale(window, &xscale, &yscale);

    ratio = xscale;

    return ratio;
}

Some things to think about (and things I'm not sure of):

  • I'm not sure if glfwGetWindowContentScale() or glfwGetMonitorContentScale() is more appropriate and in what cases those scales would differ.
  • I've not tested anything running KDE or Wayland.
  • I set it up to return the xscale as it is for _WIN32. I'm not sure if the yscale being different from xscale has a valid use case that should be accounted for.
  • There's also a fancy new function glfwSetWindowContentScaleCallback() that might be a nice add to nanogui to deal with a user's scale change or moving the window between differently scaled displays.

dbird137 avatar Apr 29 '19 06:04 dbird137

glfwGetWindowContentScale() looks great but before moving to it, it would be a good idea to take a look at how it works and determine how reliable it is likely to be across all the platforms that NanoGUI runs on.

rogerdahl avatar Apr 29 '19 15:04 rogerdahl

glfwGetWindowContentScale() branches out to platform specific functions. This is the one for X:

https://github.com/glfw/glfw/blob/a337c568486025d22443535b7c047a563bc34373/src/x11_monitor.c#L336-L343

I'm not sure if this ends up getting the user specified "logical" DPI or the hardware DPI. The description for glfwGetWindowContentScale() is unclear to me as well:

The content scale is the ratio between the current DPI and the platform's default DPI.

Relevant GLFW ticket: https://github.com/glfw/glfw/issues/1019

rogerdahl avatar Apr 29 '19 16:04 rogerdahl

Hey @rogerdahl thanks for letting us know, this looks promising. I've been slowly iterating on updating nanogui's build system for modern cmake, it'll be a one-two punch. We've updated every other dependency, but the GLFW stuff needs to be updated with / after the new cmake stuff. I'm getting much closer.

Would you be interested in taking charge of (after we update GLFW / enable external GLFW) putting together a PR to use the new GLFW method? I'll be able to help test hidpi on all three major platforms. If so, I'll CC you on the PR that will enable it. It will be a pretty big PR that we'll need lots of user testing for, but it'll give you a base that you can work from independently.

svenevs avatar May 06 '19 19:05 svenevs

@svenevs Sounds good. I'd be happy to help. Just give me a ping when the PR is ready. I can also help with testing on Linux and Windows.

rogerdahl avatar May 07 '19 03:05 rogerdahl

Awesome, thanks! I'm hoping to finish it this weekend assuming everything goes according to plan.

svenevs avatar May 07 '19 12:05 svenevs