nanogui
nanogui copied to clipboard
Support for high DPI screens
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...
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.
I checked now and found that Screen::pixelRatio()
return 1
.
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.
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.
A quick test shows that forcing the return value of get_pixel_ratio()
to a value above 1
causes everything to scale nicely.
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
-
xrandr | fgrep '*'
, and -
xdpyinfo | grep dim
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.
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.
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: Would you mind checking if there is a way to do this on a KDE environment?
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:
-
xdpyinfo | grep dimensions
-
printenv | grep -i desktop
This still only solves single-monitor scenarios BTW. Good enough for a hotfix for KDE though :wink:
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
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?
This looks reasonable. But what if std::getenv
returns nullptr
?
The pclose(fp)
can be factored out.
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
returnsnullptr
?
Then we can't do anything, in which case just return scale of 1
?
How about using xdpyinfo
on the platforms where it returns correct resolution and physical size? Then calculate DPI.
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.
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.
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.
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.
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
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 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.
Awesome, thanks! I'm hoping to finish it this weekend assuming everything goes according to plan.