FlorianRhiem / pyGLFW

Python bindings for GLFW
MIT License
232 stars 36 forks source link

Add a ctypes call to improve hidpi support with multiple monitors. #48

Closed almarklein closed 4 years ago

almarklein commented 4 years ago

I have a setup with a laptop screen and a 4K external display. I found that get_framebuffer_size and get_window_content_scale produce the correct results when the window is resized. However, when moving the window from monitor to another, the window is not updated. Adding this code fixes that.

This works on Windows 10. I am not sure what it does on older system, but I suppose worst case is that the current behavior is maintained.

FlorianRhiem commented 4 years ago

Hello Almar, thank you for the pull request!

This sounds like it should be part of GLFW, so I've just checked and found this line: win32_init.c, line 572, which calls SetProcessDpiAwareness to PROCESS_PER_MONITOR_DPI_AWARE. This is only done if the Windows version is between Windows 8.10 and the Windows 10 Creators Update (Build 15063), otherwise (if a newer Windows version is used) SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) is called, which seems to be a more advanced API for doing (almost) the same thing.

Therefore, I'm not sure why it'd be necessary to still call SetProcessDpiAwareness and I'm hesitant to include it until we know what the issue with the GLFW call of SetProcessDpiAwarenessContext is.

FlorianRhiem commented 4 years ago

I just wrote a small script to test whether the DPI awareness would be 2 after initializing GLFW. On my system (Windows 10 Build 17763), initializing GLFW is indeed enough for the process DPI awareness to be 2. Without initializing GLFW, it is 0.

import glfw
import ctypes

ctypes.windll.kernel32.GetCurrentProcess.argtypes = []
ctypes.windll.kernel32.GetCurrentProcess.restype = ctypes.c_void_p
ctypes.windll.shcore.GetProcessDpiAwareness.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
ctypes.windll.shcore.GetProcessDpiAwareness.restype = ctypes.c_int
dpi_awareness = ctypes.c_int()

glfw.init()

ctypes.windll.shcore.GetProcessDpiAwareness(ctypes.windll.kernel32.GetCurrentProcess(), ctypes.byref(dpi_awareness))
print(dpi_awareness.value)
almarklein commented 4 years ago

That is interesting. I get a value of 2 as well. Will revisit this on Monday (when I have access to a highres screen again).

almarklein commented 4 years ago

Ok, so what's going on is that on a recent Windows 10, glfw sets to DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, which is a more advanced version of setting it to 2. In this mode, Windows does not automatically scale the window, instead the process gets a notification that the dpi has changed.

So it's a matter of using set_window_content_scale_callback to reset the size when this happens.

@FlorianRhiem thanks for your quick support (and the script to read the DpiAwareness value). Closing!

FlorianRhiem commented 4 years ago

For future reference, what do you need to set/call in the window content scale callback?

almarklein commented 4 years ago

Basically we keep track of the logical size of the window (based on framebuffer size and content scale) and then, when the content scale (a.ka. device pixel ratio) changes we do something like:

        lsize = self._logical_size
        pixel_ratio = glfw.get_window_content_scale(self._window)[0]
        # The current screen size and physical size, and its ratio
        ssize = glfw.get_window_size(self._window)
        psize = glfw.get_framebuffer_size(self._window)
        screen_ratio = ssize[0] / psize[0]
        # Apply
        glfw.set_window_size(
            self._window,
            int(lsize[0] * pixel_ratio / screen_ratio),
            int(lsize[1] * pixel_ratio / screen_ratio),
        )

The fact that what glfw considers "screen size" differers between platforms makes this code more complex :/ Original code here