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