From ae31c3cd82ba53454b8ab49d3f9d8ca385560d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sun, 19 Aug 2018 13:34:54 +0200 Subject: [PATCH] Platform: initial HiDPI support in SDL2 app on Linux and Emscripten. This is quite complex, actually. The end goal is: when I request an 800x600 window, it should create a window of the same physical size as an 800x600 window would have on a system default DPI. After that, the actual window size (for events), framebuffer size and DPI scaling value (to correctly scale the contents relative to window size) are platform-dependent. On macOS and iOS, the DPI scaling is done simply by having the framebuffer twice the size while the window size (for events) remains the same. Easy to support. On Linux, a non-DPI-aware app is simply having a really tiny window. The worst behavior of all systems. Next to that, SDL_GetDisplayDPI() returns physical DPI, which is quite useless as the value is usually coming from Xorg display autodetection and is usually just 96, unless one goes extra lengths and supplies a correct value via an xorg.conf. The DE is using a different, user-configurable value for scaling the visuals and this one is available through a Xft.dpi property. To get it, we dlopen() self and dlsym() X11 symbols to get this property. If this fails, it might mean the app doesn't run on X11 (maybe Wayland, maybe something's just messed up, who knows) and then we fall back to SDL_GetDisplayDPI(). Which is usually very wrong, so this is also why I'm implementing two ways to override this -- either via the app Configuration or via a command-line / environment variable. On Emscripten / HTML5, all that's needed is querying device pixel ratio and then requesting canvas size scaled by that. The event coordinates are relative to this size, so there's not much more to handle. Physical canvas size on the page is controlled via CSS, so no issues with stuff being too big or too small apply -- in the worst case, things may be blurry. On Windows, the DPI scaling is something in-between -- if the app presents itself as DPI-aware, window size is treated as real pixels (so one gets really what is asked for, i.e. an 800x600 window on a system with 240 DPI is maybe four centimeters wide). If not, the window is upscaled (and blurried) by the compositor. In order to have correct behavior, I first need to query if the app is DPI-aware and then either scale the requested size or not (to avoid extra huge windows when the app is not marked as DPI aware). That will be done in a later commit. --- doc/changelog.dox | 2 + modules/FindMagnum.cmake | 5 + src/Magnum/Platform/AndroidApplication.h | 6 +- src/Magnum/Platform/CMakeLists.txt | 11 + .../Platform/Implementation/dpiScaling.hpp | 131 ++++++++ src/Magnum/Platform/Sdl2Application.cpp | 149 +++++++++- src/Magnum/Platform/Sdl2Application.h | 281 +++++++++++++++++- 7 files changed, 564 insertions(+), 21 deletions(-) create mode 100644 src/Magnum/Platform/Implementation/dpiScaling.hpp diff --git a/doc/changelog.dox b/doc/changelog.dox index 47fe5f62c..0618753af 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -86,6 +86,8 @@ See also: @subsubsection changelog-latest-new-platform Platform libraries +- Initial HiDPI support for Linux and Emscripten in + @ref Platform::Sdl2Application - Implemented @ref Platform::GlfwApplication::MouseMoveEvent::buttons() for feature parity with @ref Platform::Sdl2Application - Added @ref Platform::Sdl2Application::GLConfiguration::setColorBufferSize() "GLConfiguration::setColorBufferSize()", diff --git a/modules/FindMagnum.cmake b/modules/FindMagnum.cmake index 165c45ead..d0c7aec12 100644 --- a/modules/FindMagnum.cmake +++ b/modules/FindMagnum.cmake @@ -639,6 +639,11 @@ foreach(_component ${Magnum_FIND_COMPONENTS}) find_package(SDL2) set_property(TARGET Magnum::${_component} APPEND PROPERTY INTERFACE_LINK_LIBRARIES SDL2::SDL2) + if(CORRADE_TARGET_UNIX AND NOT CORRADE_TARGET_APPLE) + # Needed for opt-in DPI queries + set_property(TARGET Magnum::${_component} APPEND PROPERTY + INTERFACE_LINK_LIBRARIES ${CMAKE_DL_LIBS}) + endif() # With GLVND (since CMake 3.11) we need to explicitly link to # GLX/EGL because libOpenGL doesn't provide it. For EGL we have diff --git a/src/Magnum/Platform/AndroidApplication.h b/src/Magnum/Platform/AndroidApplication.h index e0fd9d8ed..102bb663d 100644 --- a/src/Magnum/Platform/AndroidApplication.h +++ b/src/Magnum/Platform/AndroidApplication.h @@ -300,7 +300,11 @@ class AndroidApplication { /** @{ @name Screen handling */ - /** @copydoc Sdl2Application::windowSize() */ + /** + * @brief Window size + * + * Window size to which all input event coordinates can be related. + */ Vector2i windowSize(); /** diff --git a/src/Magnum/Platform/CMakeLists.txt b/src/Magnum/Platform/CMakeLists.txt index 4e0dae6cd..ac5e8ec48 100644 --- a/src/Magnum/Platform/CMakeLists.txt +++ b/src/Magnum/Platform/CMakeLists.txt @@ -222,6 +222,17 @@ if(WITH_SDL2APPLICATION) ${MagnumSomeContext_LIBRARY}) endif() + # If there is X11, ask it for DPI + if(CORRADE_TARGET_UNIX AND NOT CORRADE_TARGET_APPLE) + find_package(X11) + if(X11_FOUND) + # Not linking to X11, we dlopen() instead + target_include_directories(MagnumSdl2Application PRIVATE ${X11_X11_INCLUDE_PATH}) + target_link_libraries(MagnumSdl2Application PUBLIC ${CMAKE_DL_LIBS}) + target_compile_definitions(MagnumSdl2Application PRIVATE "_MAGNUM_PLATFORM_USE_X11") + endif() + endif() + install(FILES ${MagnumSdl2Application_HEADERS} DESTINATION ${MAGNUM_INCLUDE_INSTALL_DIR}/Platform) install(TARGETS MagnumSdl2Application RUNTIME DESTINATION ${MAGNUM_BINARY_INSTALL_DIR} diff --git a/src/Magnum/Platform/Implementation/dpiScaling.hpp b/src/Magnum/Platform/Implementation/dpiScaling.hpp new file mode 100644 index 000000000..3ca96aa41 --- /dev/null +++ b/src/Magnum/Platform/Implementation/dpiScaling.hpp @@ -0,0 +1,131 @@ +#ifndef Magnum_Platform_Implementation_dpiScaling_hpp +#define Magnum_Platform_Implementation_dpiScaling_hpp +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 + Vladimír Vondruš + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + +#include + +#include "Magnum/Magnum.h" + +#ifdef _MAGNUM_PLATFORM_USE_X11 +#include +#include +#include +#undef None +#endif + +#ifdef CORRADE_TARGET_EMSCRIPTEN +#include +#endif + +namespace Magnum { namespace Platform { namespace Implementation { namespace { + +inline Utility::Arguments windowScalingArguments() { + Utility::Arguments args{"magnum"}; + args.addOption("dpi-scaling", "virtual") + .setFromEnvironment("dpi-scaling", "default") + #ifdef CORRADE_TARGET_APPLE + .setHelp("dpi-scaling", "\n window DPI scaling", "default|framebuffer||\" \"") + #elif !defined(CORRADE_TARGET_EMSCRIPTEN) && !defined(CORRADE_TARGET_ANDROID) + .setHelp("dpi-scaling", "\n window DPI scaling", "default|virtual|physical||\" \"") + #else + .setHelp("dpi-scaling", "\n window DPI scaling", "default|physical||\" \"") + #endif + ; + return args; +} + +#ifdef _MAGNUM_PLATFORM_USE_X11 +/* Returns DPI scaling for current X11 instance. Because X11 (as opposed to + Wayland) doesn't have per-monitor scaling, it's fetched from the default + display. */ +inline Float x11DpiScaling() { + /* If the end app links to X11, these symbols will be available in a global + scope and we can use that to query the DPI. If not, then those symbols + won't be and that's okay -- it may be using Wayland or something else. */ + void* xlib = dlopen(nullptr, RTLD_NOW|RTLD_GLOBAL); + Containers::ScopedExit closeXlib{xlib, dlclose}; + #ifdef __GNUC__ /* http://www.mr-edd.co.uk/blog/supressing_gcc_warnings */ + __extension__ + #endif + auto xOpenDisplay = reinterpret_cast(dlsym(xlib, "XOpenDisplay")); + #ifdef __GNUC__ /* http://www.mr-edd.co.uk/blog/supressing_gcc_warnings */ + __extension__ + #endif + auto xCloseDisplay = reinterpret_cast(dlsym(xlib, "XCloseDisplay")); + #ifdef __GNUC__ /* http://www.mr-edd.co.uk/blog/supressing_gcc_warnings */ + __extension__ + #endif + auto xResourceManagerString = reinterpret_cast(dlsym(xlib, "XResourceManagerString")); + #ifdef __GNUC__ /* http://www.mr-edd.co.uk/blog/supressing_gcc_warnings */ + __extension__ + #endif + auto xrmGetStringDatabase = reinterpret_cast(dlsym(xlib, "XrmGetStringDatabase")); + #ifdef __GNUC__ /* http://www.mr-edd.co.uk/blog/supressing_gcc_warnings */ + __extension__ + #endif + auto xrmGetResource = reinterpret_cast(dlsym(xlib, "XrmGetResource")); + #ifdef __GNUC__ /* http://www.mr-edd.co.uk/blog/supressing_gcc_warnings */ + __extension__ + #endif + auto xrmDestroyDatabase = reinterpret_cast(dlsym(xlib, "XrmDestroyDatabase")); + if(!xOpenDisplay || !xCloseDisplay || !xResourceManagerString || !xrmGetStringDatabase || !xrmGetResource || !xrmDestroyDatabase) { + Warning{} << "Platform: can't load X11 symbols for getting virtual DPI scaling, falling back to physical DPI"; + return {}; + } + + Display* display = xOpenDisplay(nullptr); + Containers::ScopedExit closeDisplay{display, xCloseDisplay}; + + const char* rms = xResourceManagerString(display); + CORRADE_INTERNAL_ASSERT(rms); + XrmDatabase db = xrmGetStringDatabase(rms); + CORRADE_INTERNAL_ASSERT(db); + Containers::ScopedExit closeDb{db, xrmDestroyDatabase}; + + XrmValue value; + char* type{}; + if(xrmGetResource(db, "Xft.dpi", "Xft.Dpi", &type, &value)) { + if(type && strcmp(type, "String") == 0) { + const float scaling = std::stof(value.addr)/96.0f; + CORRADE_INTERNAL_ASSERT(scaling); + return scaling; + } + } + + Warning{} << "Platform: can't get Xft.dpi property for virtual DPI scaling, falling back to physical DPI"; + return {}; +} +#endif + +#ifdef CORRADE_TARGET_EMSCRIPTEN +inline Float emscriptenDpiScaling() { + return Float(emscripten_get_device_pixel_ratio()); +} +#endif + +}}}} + +#endif diff --git a/src/Magnum/Platform/Sdl2Application.cpp b/src/Magnum/Platform/Sdl2Application.cpp index 15b3fb695..24085574a 100644 --- a/src/Magnum/Platform/Sdl2Application.cpp +++ b/src/Magnum/Platform/Sdl2Application.cpp @@ -34,6 +34,7 @@ #include "Magnum/Math/Range.h" #include "Magnum/Platform/ScreenedApplication.hpp" +#include "Magnum/Platform/Implementation/dpiScaling.hpp" #ifdef MAGNUM_TARGET_GL #include "Magnum/GL/Version.h" @@ -78,18 +79,43 @@ Sdl2Application::Sdl2Application(const Arguments& arguments, NoCreateT): _minimalLoopPeriod{0}, #endif #ifdef MAGNUM_TARGET_GL - _glContext{nullptr}, _context{new GLContext{NoCreate, arguments.argc, arguments.argv}}, + _glContext{nullptr}, #endif _flags{Flag::Redraw} { + Utility::Arguments args{Implementation::windowScalingArguments()}; + #ifdef MAGNUM_TARGET_GL + _context.reset(new GLContext{NoCreate, args, arguments.argc, arguments.argv}); + #else + args.parse(arguments.argc, arguments.argv); + #endif + if(SDL_Init(SDL_INIT_VIDEO) < 0) { Error() << "Cannot initialize SDL."; std::exit(1); } - #ifndef MAGNUM_TARGET_GL - static_cast(arguments); + /* Save command-line arguments */ + if(args.value("log") == "verbose") _verboseLog = true; + const std::string dpiScaling = args.value("dpi-scaling"); + if(dpiScaling == "default") + _commandLineDpiScalingPolicy = Implementation::DpiScalingPolicy::Default; + #ifdef CORRADE_TARGET_APPLE + else if(dpiScaling == "framebuffer") + _commandLineDpiScalingPolicy = Implementation::DpiScalingPolicy::Framebuffer; + #endif + #ifndef CORRADE_TARGET_APPLE + #if !defined(CORRADE_TARGET_EMSCRIPTEN) && !defined(CORRADE_TARGET_ANDROID) + else if(dpiScaling == "virtual") + _commandLineDpiScalingPolicy = Implementation::DpiScalingPolicy::Virtual; #endif + else if(dpiScaling == "physical") + _commandLineDpiScalingPolicy = Implementation::DpiScalingPolicy::Physical; + #endif + else if(dpiScaling.find_first_of(" \t\n") != std::string::npos) + _commandLineDpiScaling = args.value("dpi-scaling"); + else + _commandLineDpiScaling = Vector2{args.value("dpi-scaling")}; } void Sdl2Application::create() { @@ -106,12 +132,96 @@ void Sdl2Application::create(const Configuration& configuration, const GLConfigu } #endif +Vector2 Sdl2Application::dpiScaling(const Configuration& configuration) const { + std::ostream* verbose = _verboseLog ? Debug::output() : nullptr; + + /* Use values from the configuration only if not overriden on command line. + In any case explicit scaling has a precedence before the policy. */ + Implementation::DpiScalingPolicy dpiScalingPolicy{}; + if(!_commandLineDpiScaling.isZero()) { + Debug{verbose} << "Platform::Sdl2Application: user-defined DPI scaling" << _commandLineDpiScaling.x(); + return _commandLineDpiScaling; + } else if(UnsignedByte(_commandLineDpiScalingPolicy)) { + dpiScalingPolicy = _commandLineDpiScalingPolicy; + } else if(!configuration.dpiScaling().isZero()) { + Debug{verbose} << "Platform::Sdl2Application: app-defined DPI scaling" << _commandLineDpiScaling.x(); + return configuration.dpiScaling(); + } else { + dpiScalingPolicy = configuration.dpiScalingPolicy(); + } + + /* There's no choice on Apple, it's all controlled by the plist file. So + unless someone specified custom scaling via config or command-line + above, return the default. */ + #ifdef CORRADE_TARGET_APPLE + return Vector2{1.0f}; + + /* Otherwise there's a choice between virtual and physical DPI scaling */ + #else + /* Try to get virtual DPI scaling first, if supported and requested */ + #if !defined(CORRADE_TARGET_EMSCRIPTEN) && !defined(CORRADE_TARGET_ANDROID) + if(dpiScalingPolicy == Implementation::DpiScalingPolicy::Virtual) { + /* Use Xft.dpi on X11 */ + #ifdef _MAGNUM_PLATFORM_USE_X11 + const Vector2 dpiScaling{Implementation::x11DpiScaling()}; + if(!dpiScaling.isZero()) { + Debug{verbose} << "Platform::Sdl2Application: virtual DPI scaling" << dpiScaling.x(); + return dpiScaling; + } + + /* Otherwise ¯\_(ツ)_/¯ */ + #else + Debug{verbose} << "Platform::Sdl2Application: sorry, virtual DPI scaling not implemented on this platform yet"; + return Vector2{1.0f}; + #endif + } + #endif + + /* At this point, either the virtual DPI query failed or a physical DPI + scaling is requested */ + #if !defined(CORRADE_TARGET_EMSCRIPTEN) && !defined(CORRADE_TARGET_ANDROID) + CORRADE_INTERNAL_ASSERT(dpiScalingPolicy == Implementation::DpiScalingPolicy::Virtual || dpiScalingPolicy == Implementation::DpiScalingPolicy::Physical); + #else + CORRADE_INTERNAL_ASSERT(dpiScalingPolicy == Implementation::DpiScalingPolicy::Physical); + #endif + + /* Take device pixel ratio on Emscripten */ + #ifdef CORRADE_TARGET_EMSCRIPTEN + const Vector2 dpiScaling{Implementation::emscriptenDpiScaling()}; + Debug{verbose} << "Platform::Sdl2Application: physical DPI scaling" << dpiScaling.x(); + return dpiScaling; + + /* Take display DPI elsewhere. Enable only on Linux for now, I need to + test this properly on Windows first. Also only since SDL 2.0.4. */ + #elif defined(CORRADE_TARGET_UNIX) && SDL_VERSION_ATLEAST(2, 0, 4) + Vector2 dpi; + if(SDL_GetDisplayDPI(0, nullptr, &dpi.x(), &dpi.y()) == 0) { + const Vector2 dpiScaling{dpi/96.0f}; + Debug{verbose} << "Platform::Sdl2Application: physical DPI scaling" << dpiScaling; + return dpiScaling; + } + + Warning{} << "Platform::Sdl2Application: can't get physical display DPI, falling back to no scaling:" << SDL_GetError(); + return Vector2{1.0f}; + + /* Not implemented otherwise */ + #else + Debug{verbose} << "Platform::Sdl2Application: sorry, physical DPI scaling not implemented on this platform yet"; + return Vector2{1.0f}; + #endif + #endif +} + bool Sdl2Application::tryCreate(const Configuration& configuration) { #ifdef MAGNUM_TARGET_GL if(!(configuration.windowFlags() & Configuration::WindowFlag::Contextless)) return tryCreate(configuration, GLConfiguration{}); #endif + /* Scale window based on DPI */ + _dpiScaling = dpiScaling(configuration); + const Vector2i scaledWindowSize = configuration.size()*_dpiScaling; + #ifndef CORRADE_TARGET_EMSCRIPTEN /* Create window */ if(!(_window = SDL_CreateWindow( @@ -121,7 +231,7 @@ bool Sdl2Application::tryCreate(const Configuration& configuration) { nullptr, #endif SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, - configuration.size().x(), configuration.size().y(), + scaledWindowSize.x(), scaledWindowSize.y(), Uint32(configuration.windowFlags()&~Configuration::WindowFlag::Contextless)))) { Error() << "Platform::Sdl2Application::tryCreate(): cannot create window:" << SDL_GetError(); @@ -129,7 +239,7 @@ bool Sdl2Application::tryCreate(const Configuration& configuration) { } #else /* Emscripten-specific initialization */ - if(!(_glContext = SDL_SetVideoMode(configuration.size().x(), configuration.size().y(), 24, SDL_OPENGL|SDL_HWSURFACE|SDL_DOUBLEBUF))) { + if(!(_glContext = SDL_SetVideoMode(scaledWindowSize.x(), scaledWindowSize.y(), 24, SDL_OPENGL|SDL_HWSURFACE|SDL_DOUBLEBUF))) { Error() << "Platform::Sdl2Application::tryCreate(): cannot create window:" << SDL_GetError(); return false; } @@ -184,6 +294,10 @@ bool Sdl2Application::tryCreate(const Configuration& configuration, const GLConf SDL_GL_SetAttribute(SDL_GL_FRAMEBUFFER_SRGB_CAPABLE, glConfiguration.isSRGBCapable()); #endif + /* Scale window based on DPI */ + _dpiScaling = dpiScaling(configuration); + const Vector2i scaledWindowSize = configuration.size()*_dpiScaling; + /** @todo Remove when Emscripten has proper SDL2 support */ #ifndef CORRADE_TARGET_EMSCRIPTEN /* Set context version, if user-specified */ @@ -241,7 +355,7 @@ bool Sdl2Application::tryCreate(const Configuration& configuration, const GLConf nullptr, #endif SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, - configuration.size().x(), configuration.size().y(), + scaledWindowSize.x(), scaledWindowSize.y(), SDL_WINDOW_OPENGL|SDL_WINDOW_HIDDEN|Uint32(configuration.windowFlags())))) { Error() << "Platform::Sdl2Application::tryCreate(): cannot create window:" << SDL_GetError(); @@ -294,7 +408,7 @@ bool Sdl2Application::tryCreate(const Configuration& configuration, const GLConf if(!(_window = SDL_CreateWindow(configuration.title().data(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, - configuration.size().x(), configuration.size().y(), + scaledWindowSize.x(), scaledWindowSize.y(), SDL_WINDOW_OPENGL|SDL_WINDOW_HIDDEN|Uint32(configuration.windowFlags()&~Configuration::WindowFlag::Contextless)))) { Error() << "Platform::Sdl2Application::tryCreate(): cannot create window:" << SDL_GetError(); @@ -325,7 +439,7 @@ bool Sdl2Application::tryCreate(const Configuration& configuration, const GLConf #endif #else /* Emscripten-specific initialization */ - if(!(_glContext = SDL_SetVideoMode(configuration.size().x(), configuration.size().y(), 24, SDL_OPENGL|SDL_HWSURFACE|SDL_DOUBLEBUF))) { + if(!(_glContext = SDL_SetVideoMode(scaledWindowSize.x(), scaledWindowSize.y(), 24, SDL_OPENGL|SDL_HWSURFACE|SDL_DOUBLEBUF))) { Error() << "Platform::Sdl2Application::tryCreate(): cannot create context:" << SDL_GetError(); return false; } @@ -354,7 +468,7 @@ bool Sdl2Application::tryCreate(const Configuration& configuration, const GLConf } #endif -Vector2i Sdl2Application::windowSize() { +Vector2i Sdl2Application::windowSize() const { #ifndef CORRADE_TARGET_EMSCRIPTEN Vector2i size; SDL_GetWindowSize(_window, &size.x(), &size.y()); @@ -364,6 +478,16 @@ Vector2i Sdl2Application::windowSize() { #endif } +Vector2i Sdl2Application::framebufferSize() const { + #ifndef CORRADE_TARGET_EMSCRIPTEN + Vector2i size; + SDL_GL_GetDrawableSize(_window, &size.x(), &size.y()); + return size; + #else + return {_glContext->w, _glContext->h}; + #endif +} + void Sdl2Application::swapBuffers() { #ifndef CORRADE_TARGET_EMSCRIPTEN SDL_GL_SwapWindow(_window); @@ -633,12 +757,13 @@ Sdl2Application::Configuration::Configuration(): _title("Magnum SDL2 Application"), #endif #ifdef CORRADE_TARGET_EMSCRIPTEN - _size{640, 480} + _size{640, 480}, #elif !defined(CORRADE_TARGET_IOS) - _size{800, 600} + _size{800, 600}, #else - _size{} /* SDL2 detects someting for us */ + _size{}, /* SDL2 detects someting for us */ #endif + _dpiScalingPolicy{DpiScalingPolicy::Default} #if defined(MAGNUM_BUILD_DEPRECATED) && defined(MAGNUM_TARGET_GL) , _sampleCount(0) #ifndef CORRADE_TARGET_EMSCRIPTEN diff --git a/src/Magnum/Platform/Sdl2Application.h b/src/Magnum/Platform/Sdl2Application.h index 4d538256c..e9de03375 100644 --- a/src/Magnum/Platform/Sdl2Application.h +++ b/src/Magnum/Platform/Sdl2Application.h @@ -56,6 +56,10 @@ namespace Magnum { namespace Platform { +namespace Implementation { + enum class DpiScalingPolicy: UnsignedByte; +} + /** @nosubgrouping @brief SDL2 application @@ -236,6 +240,89 @@ a particular value for details: - @ref Configuration::WindowFlag::Borderless hides the menu bar - @ref Configuration::WindowFlag::Resizable makes the application respond to device orientation changes + +@section Platform-Sdl2Application-dpi DPI awareness + +On displays that match the platform default DPI (96 or 72), +@ref Configuration::setSize() will create the window in exactly the requested +size and the framebuffer pixels will match display pixels 1:1. On displays that +have different DPI, there are three possible scenarios, listed below. It's +possible to fine tune the behavior either using extra parameters passed to +@ref Configuration::setSize() or via the `--magnum-dpi-scaling` command-line +option. + +- Framebuffer DPI scaling. The window is created with exactly the requested + size and all event coordinates are reported also relative to that size. + However, the window backing framebuffer has a different size. This is only + supported on macOS and iOS. See @ref platforms-macos-hidpi for details how + to enable it. Equivalent to passing + @ref Configuration::DpiScalingPolicy::Framebuffer to + @ref Configuration::setSize() or `framebuffer` on command line. +- Virtual DPI scaling. Scales the window based on DPI scaling setting in the + system. For example if a 800x600 window is requested and DPI scaling is set + to 200%, the resulting window will have 1600x1200 pixels. The backing + framebuffer will have the same size. This is supported on Linux and + Windows. Equivalent to passing @ref Configuration::DpiScalingPolicy::Virtual + to @ref Configuration::setSize() or `virtual` on command line. +- Physical DPI scaling. Takes the requested window size as a physical size + that a window would have on platform's default DPI and scales it to have + the same physical size on given display physical DPI. So, for example on a + display with 240 DPI the window size will be 2000x1500 in pixels, but it + will be 21 centimeters wide, the same as a 800x600 window would be on a 96 + DPI display. On platforms that don't have a concept of a window (such + as mobile platforms or @ref CORRADE_TARGET_EMSCRIPTEN "Emscripten"), it + causes the framebuffer to match display pixels 1:1 without any scaling. + This is supported on Linux, Windows, all mobile platforms except iOS and + Emscripten. Equivalent to passing + @ref Configuration::DpiScalingPolicy::Physical to + @ref Configuration::setSize() or `physical` on command line. + +Besides the above, it's possible to supply a custom DPI scaling value to +@ref Configuration::setSize() or the `--magnum-dpi-scaling` command-line +option. Using `--magnum-dpi-scaling <number>` will make the scaling +same in both directions, with `--magnum-dpi-scaling " "` +the scaling will be different in each direction. On desktop systems custom DPI +scaling value will affect physical window size (with the content being scaled), +on mobile and web it will affect sharpness of the contents. + +The default is depending on the platform: + +- On macOS and iOS, the default and only supported option is + @ref Configuration::DpiScalingPolicy::Framebuffer. On this platform, + @ref windowSize() and @ref framebufferSize() will differ depending on + whether `NSHighResolutionCapable` is enabled in the `*.plist` file or not. + By default, @ref dpiScaling() is @cpp 1.0f @ce in both dimensions but it + can be overriden using custom DPI scaling. +- On Windows, the default is @ref Configuration::DpiScalingPolicy::Framebuffer. + The @ref windowSize() and @ref framebufferSize() is always the same. + Depending on whether the DPI awareness was enabled in the manifest file or + set by the `SetProcessDpiAwareness()` API, @ref dpiScaling() is either + @cpp 1.0f @ce in both dimensions, indicating a low-DPI screen or a + non-DPI-aware app, or some other value for HiDPI screens. In both cases the + value can be overriden using custom DPI scaling. +- On Linux, the default is @ref Configuration::DpiScalingPolicy::Virtual, + taken from the `Xft.dpi` property. If the property is not available, it + falls back to @ref Configuration::DpiScalingPolicy::Physical, querying the + monitor DPI value. The @ref windowSize() and @ref framebufferSize() is + always the same, @ref dpiScaling() contains the queried DPI scaling value. + The value can be overriden using custom DPI scaling. +- On @ref CORRADE_TARGET_EMSCRIPTEN "Emscripten", the default is physical DPI + scaling, taken from [Window.getDevicePixelRatio()](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio). The + @ref windowSize() and @ref framebufferSize() is always the same, + @ref dpiScaling() contains the queried DPI scaling value. The value can be + overriden using custom DPI scaling. + +If your application is saving and restoring window size, it's advisable to take +@ref dpiScaling() into account: + +- Either divide the window size by the DPI scaling value and use that to + restore the window next time --- but note this might accumulate slight + differences in window sizes over time, especially if fractional scaling is + involved. +- Or save the scaled size and use @ref Configuration::setSize(const Vector2i&, const Vector2&) + with @cpp 1.0f @ce as custom DPI scaling next time --- but this doesn't + properly handle cases where the window is opened on a display with + different DPI. */ class Sdl2Application { public: @@ -472,15 +559,60 @@ class Sdl2Application { /** @{ @name Screen handling */ + public: /** * @brief Window size * * Window size to which all input event coordinates can be related. - * Note that especially on HiDPI systems the reported window size might - * not be the same as framebuffer size. + * Note that, especially on HiDPI systems, it may be different from + * @ref framebufferSize(). If a window is not created yet, returns + * zero vector. See @ref Platform-Sdl2Application-dpi for more + * information. + * @see @ref dpiScaling() + */ + Vector2i windowSize() const; + + #if defined(MAGNUM_TARGET_GL) || defined(DOXYGEN_GENERATING_OUTPUT) + /** + * @brief Framebuffer size + * + * Size of the default framebuffer. Note that, especially on HiDPI + * systems, it may be different from @ref windowSize(). If a window is + * not created yet, returns zero vector. See + * @ref Platform-Sdl2Application-dpi for more information. + * + * @note This function is available only if Magnum is compiled with + * @ref MAGNUM_TARGET_GL enabled (done by default). See + * @ref building-features for more information. + * + * @see @ref dpiScaling() + */ + Vector2i framebufferSize() const; + #endif + + /** + * @brief DPI scaling + * + * How the content should be scaled relative to system defaults for + * given @ref windowSize(). If a window is not created yet, returns + * zero vector, use @ref dpiScaling(const Configuration&) const for + * calculating a value independently. See @ref Platform-Sdl2Application-dpi + * for more information. + * @see @ref framebufferSize() */ - Vector2i windowSize(); + Vector2 dpiScaling() const { return _dpiScaling; } + /** + * @brief DPI scaling for given configuration + * + * Calculates DPI scaling that would be used when creating a window + * with given @p configuration. Takes into account DPI scaling policy + * and custom scaling specified on the command-line. See + * @ref Platform-Sdl2Application-dpi for more information. + */ + Vector2 dpiScaling(const Configuration& configuration) const; + + protected: /** * @brief Swap buffers * @@ -739,6 +871,13 @@ class Sdl2Application { typedef Containers::EnumSet Flags; CORRADE_ENUMSET_FRIEND_OPERATORS(Flags) + /* These are saved from command-line arguments */ + bool _verboseLog{}; + Implementation::DpiScalingPolicy _commandLineDpiScalingPolicy{}; + Vector2 _commandLineDpiScaling; + + Vector2 _dpiScaling; + #ifndef CORRADE_TARGET_EMSCRIPTEN SDL_Window* _window; UnsignedInt _minimalLoopPeriod; @@ -949,6 +1088,33 @@ CORRADE_ENUMSET_OPERATORS(Sdl2Application::GLConfiguration::Flags) #endif #endif +namespace Implementation { + enum class DpiScalingPolicy: UnsignedByte { + /* Using 0 for an "unset" value */ + + #ifdef CORRADE_TARGET_APPLE + Framebuffer = 1, + #endif + + #ifndef CORRADE_TARGET_APPLE + #if !defined(CORRADE_TARGET_EMSCRIPTEN) && !defined(CORRADE_TARGET_ANDROID) + Virtual = 2, + #endif + + Physical = 3, + #endif + + Default + #ifdef CORRADE_TARGET_APPLE + = Framebuffer + #elif !defined(CORRADE_TARGET_EMSCRIPTEN) && !defined(CORRADE_TARGET_ANDROID) + = Virtual + #else + = Physical + #endif + }; +} + /** @brief Configuration @@ -1025,6 +1191,62 @@ class Sdl2Application::Configuration { typedef Containers::EnumSet WindowFlags; #endif + /** + * @brief DPI scaling policy + * + * DPI scaling policy when requesting a particular window size. Can + * be overriden on command-line using `--magnum-dpi-scaling` or via + * the `MAGNUM_DPI_SCALING` environment variable. + * @see @ref setSize(), @ref Platform-Sdl2Application-dpi + */ + #ifdef DOXYGEN_GENERATING_OUTPUT + enum class DpiScalingPolicy: UnsignedByte { + /** + * Framebuffer DPI scaling. The window will have the same size as + * requested, but the framebuffer size will be different. Supported + * only on macOS and iOS and is also the only supported value + * there. + */ + Framebuffer, + + /** + * Virtual DPI scaling. Scales the window based on UI scaling + * setting in the system. Falls back to + * @ref DpiScalingPolicy::Physical on platforms that don't support + * it. Supported only on desktop platforms (except macOS) and it's + * the default there. + * + * Equivalent to `--magnum-dpi-scaling virtual` passed on + * command-line. + */ + Virtual, + + /** + * Physical DPI scaling. Takes the requested window size as a + * physical size that a window would have on platform's default DPI + * and scales it to have the same size on given display physical + * DPI. On platforms that don't have a concept of a window it + * causes the framebuffer to match screen pixels 1:1 without any + * scaling. Supported on desktop platforms except macOS and on + * mobile and web. Default on mobile and web. + * + * Equivalent to `--magnum-dpi-scaling physical` passed on + * command-line. + */ + Physical, + + /** + * Default policy for current platform. Alias to one of + * @ref DpiScalingPolicy::Framebuffer, @ref DpiScalingPolicy::Virtual + * or @ref DpiScalingPolicy::Physical depending on platform. See + * @ref Platform-Sdl2Application-dpi for details. + */ + Default + }; + #else + typedef Implementation::DpiScalingPolicy DpiScalingPolicy; + #endif + /*implicit*/ Configuration(); ~Configuration(); @@ -1060,18 +1282,59 @@ class Sdl2Application::Configuration { /** @brief Window size */ Vector2i size() const { return _size; } + /** + * @brief DPI scaling policy + * + * If @ref dpiScaling() is non-zero, it has a priority over this value. + * The `--magnum-dpi-scaling` command-line option has a priority over + * any application-set value. + * @see @ref setSize(const Vector2i&, DpiScalingPolicy) + */ + DpiScalingPolicy dpiScalingPolicy() const { return _dpiScalingPolicy; } + + /** + * @brief Custom DPI scaling + * + * If zero, then @ref dpiScalingPolicy() has a priority over this + * value. The `--magnum-dpi-scaling` command-line option has a priority + * over any application-set value. + * @see @ref setSize(const Vector2i&, const Vector2&) + */ + Vector2 dpiScaling() const { return _dpiScaling; } + /** * @brief Set window size + * @param size Desired window size + * @param dpiScalingPolicy Policy based on which DPI scaling will be set * @return Reference to self (for method chaining) * * Default is @cpp {800, 600} @ce and @cpp {640, 480} @ce on - * Emscripten. On iOS it defaults to a "reasonable" size based on - * whether HiDPI support is enabled using @ref WindowFlag::AllowHighDpi, - * but not necessarily native display resolution (you have to set it - * explicitly). + * Emscripten with @p dpiScalingPolicy set to + * @ref DpiScalingPolicy::Default. On iOS it defaults to a size that + * matches display resolution. See @ref Platform-Sdl2Application-dpi + * for more information. + * @see @ref setSize(const Vector2i&, const Vector2&) + */ + Configuration& setSize(const Vector2i& size, DpiScalingPolicy dpiScalingPolicy = DpiScalingPolicy::Default) { + _size = size; + _dpiScalingPolicy = dpiScalingPolicy; + return *this; + } + + /** + * @brief Set window size with custom DPI scaling + * @param size Desired window size + * @param dpiScaling Custom DPI scaling value + * + * Compared to @ref setSize(const Vector2i&, DpiScalingPolicy) which + * autodetects the DPI scaling value according to given policy, this + * function sets the DPI scaling directly. The resulting + * @ref Sdl2Application::windowSize() is @cpp size*dpiScaling @ce and + * @ref Sdl2Application::dpiScaling() is @p dpiScaling. */ - Configuration& setSize(const Vector2i& size) { + Configuration& setSize(const Vector2i& size, const Vector2& dpiScaling) { _size = size; + _dpiScaling = dpiScaling; return *this; } @@ -1156,7 +1419,9 @@ class Sdl2Application::Configuration { std::string _title; #endif Vector2i _size; + DpiScalingPolicy _dpiScalingPolicy; WindowFlags _windowFlags; + Vector2 _dpiScaling; #if defined(MAGNUM_BUILD_DEPRECATED) && defined(MAGNUM_TARGET_GL) Int _sampleCount; #ifndef CORRADE_TARGET_EMSCRIPTEN