imgui icon indicating copy to clipboard operation
imgui copied to clipboard

HDPI support

Open rokups opened this issue 4 years ago • 17 comments

This PR is supposed to solve HDPI issues once and for all (and close #1786). This is still a work in progress and incomplete.

With this PR user is expected to render UI at 96 DPI. ImGuiStyle::ScaleAllSizes() becomes obsolete in this case. Mouse position is scaled according to DPI window is using. When window is dragged between screens of different DPIs - DPI switch is performed when dragging stops (window rescales). Fonts are duplicated for each DPI and switched automatically when window moves between screens of different DPIs.

  • [x] Implement per-dpi fonts
  • [x] Implement support in OpenGL examples
  • [x] Implement support in Vulkan examples
  • [x] Implement support in windows examples
  • [x] Implement support in MacOS / iOS examples
  • [x] Fix shape instability problem
  • ~~Implement some logic for when to use what DPI. For example system may report monitor with 1.1 DPI which makes window scaling pointless and we should just use DPI scale of 1.0.~~
  • [x] Verify DPI-awareness handling on GLFW and remove manually enabling it on windows if needed.
  • [x] Verify DPI-awareness handling on SDL and remove manually enabling it on windows if needed.
  • [x] Password character masking broken
  • [ ] Popup window positioning broken
  • [ ] Multi-monitor handling on MacOS broken
  • [ ] Mouse position is not recognized correctly when window has DPI of monitor A, but also spans into monitor B and mouse is on monitor B.

Thanks to @themd for anti-alias fringe scale implementation. Thanks to @mosra for fleshing out this method and giving me an example on how to proceed.


I am also researching a different concept proposed by @ocornut. Idea is that we create fonts per each different DPI (like this in this PR) and do automatic font swapping. There are no virtual grids and scaling is reflected in font->FontSize property. In addition to that - style is also automatically scaled according to current window DPI. There are no virtual grids, everything is still measured in pixels. Therefore user must do extra work for their application to support HDPI screens. Essentially user must not use hardcoded sizes. Everything must be relative to something else, most of the time relative to the font size or values in the style.

https://github.com/rokups/imgui/tree/hdpi-support-fontscale-master (based on master branch) https://github.com/rokups/imgui/tree/hdpi-support-fontscale-viewport (based on docking branch)

rokups avatar Oct 02 '19 14:10 rokups

Still battling widget shaking.

Few observations:

  • Shaking is obviously caused due to misalignment to the pixel grid.
  • Disabling all the rounding (basically making ImFloor noop) and translating window by fractional coordinates (like window->Pos.x += 1.123) will result in shaking even if DPI=1.0f.
  • MacOS avoids this issue purely because retina screens use 2x scale. DPI=2.0f results in no shaking on other platforms as well.
  • Aligning window coordinates/size to physical pixel grid solves shaking of the window edges, however widgets within the window keep shaking.

This is a lead to work with, however i dont yet see how i could easily align all sizes to pixel grid. The main difference is that to move window by one pixel user expects to write window->Pos.x += 1.f``, but we actually need window->Pos.x += 1.f / DPI(or multiple of1.f / DPI) to keep everything aligned. What is worse - window size/pos is easy enough to fix automatically, but we need same alignment for every single point used in rendering. Modifying ImDrawList` functions to do this alignment could be an option maybe.

rokups avatar Oct 07 '19 09:10 rokups

...brainstorming continued. So pixel grind misalignment... Black grid is physical pixels. Red grid is our virtual 96 DPI grid (where DPI=1.5f). Problem is rather obvious now. image Position (1.f, 1.f) ends up at the center of physical pixel at (2, 2).

Or maybe we can approach the problem from another angle. As we know fractional scaling works "fine" in magnum's web demo: https://magnum.graphics/showcase/imgui/?magnum-dpi-scaling=1.5 You can see that there is no shaking when window is moved even though it is upscaled. I made a few screenshots of same area, the only difference is window moved down by 1 pixel. imgui They look identical at the first sight, however you may notice some extra antialiasing on letter "e". Opening image in some application and zooming in clearly shows that there is some kind of antialiasing going on. @mosra do you have any idea where antialiasing is coming from in your web demo? Is it set up in magnum or is it browser's doing?

Edit: This looks relevant. https://www.opengl.org/archives/resources/code/samples/advanced/advanced97/notes/node63.html

Edit: This bit enables similar behavior like in magnum webdemo. Default font jitters somewhat due to it's pixely nature, but other fonts (like Roboto-Medium) look quite ok.

    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 8);

rokups avatar Oct 07 '19 14:10 rokups

Shaking fix is in place. I also made screenshots of docking branch vs hdpi branch to see if my changes do not break stuff in default setting. I am fairly happy with results. You may compare imgui_capture-docking vs imgui_capture-hdpi using something like diffimg. There are some differences in text rendering and widget sizes which mainly come from here and here. Rounding here eliminates shaking.

Approach i have taken is to do rounding and flooring to values that directly map to physical pixels on the screen. This makes flooring and rounding operations expensive as they now need access to DPI of current window and now are fat and ugly. I will still rework and clean up these once we figure out if this is a correct approach.

round(f * dpi + 0.0001f) / dpi;

This hack. In some cases rounding to multiples of physical pixel is supposed to yield value with fraction part being exactly 0.5f. However after performing rounding to dpi we end up with fractional part being 0.4999999.f all due to floating point imprecision. This particular instance resulted in a very rare widget shaking that happens only in some parts of the screen. A better solution would be great, but i am unsure if there is one.

rokups avatar Oct 29 '19 15:10 rokups

I think we are at the point where we can start taking a close look at current implementation and what we need to do further. Here are some things to consider:

ImGuiConfigFlags_DpiEnableScaleFonts flag was removed as it no longer makes sense.

ImGuiConfigFlags_DpiEnableScaleViewports flag could be either removed or implemented. At the moment DPI scaling is enabled when viewports are enabled.

io.DisplayFramebufferScale works as before when viewports are disabled and is ignored when viewports are enabled. draw_data->FramebufferScale is populated with {viewport->DpiScale, viewport->DpiScale} when viewports are enabled. This essentially gave us HDPI support in all samples that already supported io.DisplayFramebufferScale without changing them.

On MacOS user sets up io.DisplaySize and uses native coordinates in native window manipulation as before. Library will scale coordinates behind the scenes as necessary.

A workaround for SDL was added as library reports invalid DPI on retina screens (bug).

Need to decide what we do about this bit. So far i could not think of a better solution.

I also added lower limit of 1.0f to DPI scale. I do not think it is worth it to concern us with ultra low DPI screens at least for now.

rokups avatar Nov 12 '19 14:11 rokups

I implemented support for ImGuiConfigFlags_DpiEnableScaleViewports, but i am unsure of it's usefulness. Do we want to keep this flag or should it be removed altogether and enabling viewports should imply dpi scaling?

At this point i am fairly certain that:

  1. IO.DisplayFramebufferScale should only be used when viewports are not enabled. Enabling viewports provides dpi information with platform monitor information.
  2. draw_data->FramebufferScale gets monitor dpi scale value or IO.DisplayFramebufferScale value depending on settings. Backend code does not need any changes.

Now there is a small discrepancy. IO.DisplayFramebufferScale is ImVec2 and monitor->DpiScale is float. I am not really aware of systems where different scales for each axis would make sense. However at least SDL does report per-axis dpi scales and they are different on my machine:

horizontal = {float} 108.917923
vertical = {float} 108.85714

We should use same type for dpi scales everywhere, so we either switch to float or to ImVec2. I suspect float is enough and we should use it. We do not really deal with pixels of non-square shapes. Any objections?

rokups avatar Dec 02 '19 10:12 rokups

Hey really nice PR !

We are near something usable I guess !

Milerius avatar Dec 05 '19 13:12 Milerius

ImGuiInputTextFlags_Password seems broken somehow (this pushes a temporary font).

ocornut avatar Dec 25 '19 16:12 ocornut

ImGuiInputTextFlags_Password seems broken somehow (this pushes a temporary font).

Only the hiding with *** part is broken. Otherwise it doesn't let you copy / cut.

naezith avatar Dec 25 '19 16:12 naezith

Hello it seems that when I use several monitors when moving the application window from one monitor to another part of the window becomes invisible.

Main monitor:

Capture d’écran 2019-12-27 à 13 47 50

When switching to the second monitor: Capture d’écran 2019-12-27 à 13 48 41

The problem persists after restarting the application

SDL2 + HIGH DPI + Multiviewport. OSX 10.15.1 Catalina. MacBook Pro 2016.

Milerius avatar Dec 27 '19 15:12 Milerius

I was annoyed by keeping to squint at my screen, so here I am. : )

This PR is nice reference, thanks! My personal goal here is to reduce amount of changes in widgets and imgui internals.

I tried a bit different approach trying to keep DPI problem on ImDrawXXX side. So far I managed to patch font generation to handle ImFontConfig::DpiScale. Generated font isn't aware of DPI and all metrics like for regular fonts. This way I don't have to touch any drawing routine.

Font is setup to be oversampled if DPI is > 1. This should look good as long as there is one pixel border around every glyph.

    float scaling = ImGui_ImplWin32_GetDpiScaleForHwnd(hwnd);

    ImFontConfig fontConfig;
    fontConfig.SizePixels = 13.0f;
    fontConfig.DpiScale = scaling;
    fontConfig.OversampleH = fontConfig.OversampleV = scaling > 1 ? 2 : 1;
    fontConfig.PixelSnapH = scaling > 1.0f ? false : true;
    io.Fonts->AddFontDefault(&fontConfig);

Result: image

Widgets drifting is still a thing (on my side). PR is trying to deal with that with large amount of rounding. I think problem is rather in simplistic implementation of draw routines, ImDrawList::AddRect and friends. Adding a fringe to most primitives should remove sharp edges on half-pixel boundaries. I will probably experiment with this approach.

@rokups You can use ImDrawData::ScaleClipRects instead of modifying clip rects calculations in backends.

thedmd avatar Jan 03 '20 04:01 thedmd

Hey awesome someone else is looking into this and exploring alternative solutions! I overlooked font oversampling. Need to investigate it again. But would it work in a situation where we have multiple monitors of different DPIs? I assume font would have to be oversampled for the monitor with highest pixel density. I need to try this on this PR code.

I too aimed to solve unstable widget positions in an "universal" way somewhere deeper when drawing everything. It proved to be a failure. This is actually more than one problem at play here. At the moment entire codebase is built around assumption that 1.0f == 1 pixel. When it comes to DPI scaling this is no longer true. We still want to move windows by 1 pixel granularity which means that we have to store fractional positions now. Consider retina screens. DPI scale is 2 there. So in order to move a window by 1 pixel we have to move it by 0.5f. This creates a new problem. Pervasive coordinate flooring across the codebase makes widgets reposition when they are moved every 2 pixels. Consider window_pos_x=0.0f, widget_pos_x=1.0f. ImFloor(0.0f + 1.0f) == 1.0f. Move window by 0.5f - ImFloor(0.5f + 1.0f) == 1.0f. Window moved by one pixel, widget stays in place. You could say that we may just bite the bullet and move windows in increments of DPI=2, so every 2 pixels. DPI is so high that nobody would notice, right? That would be easy way out, however it collapses the moment we start testing this on screens with lower DPI. For example on windows 125% scaling is quite common today and this gets reported to us as 1.25f DPI. Widget position instability is inevitable in this scenario. There just is nothing to do here... We absolutely need fractional window positions, which ripples through codebase introducing a requirement of fractional positions everywhere.

All of this makes one's head spin. If you have any questions or want to chat on the subject you can ping @rokups in gitter or rk#8724 on discord.

rokups avatar Jan 03 '20 08:01 rokups

Thanks, I will get intermediate discussion into Discord.

thedmd avatar Jan 03 '20 08:01 thedmd

I am exploding different HDPI support approach. Please see end of the first post for a detailed description and link to the branch if you want to test it.

rokups avatar Jan 29 '20 10:01 rokups

Any update on this? I only have a 4k screen and I wrote an imgui based app. How can I upscale it? This is very important for me, the small font is hardly readable..

Boscop avatar Aug 11 '20 15:08 Boscop

Any update on this? I only have a 4k screen and I wrote an imgui based app. How can I upscale it? This is very important for me, the small font is hardly readable..

The difficulty of what we are trying to do is in order to support multiple-varying DPI scale simultaneously (for multi-viewports over multi-monitor with different scale). Outside of that specific case, handling hi-dpi/4k is pretty much a matter of loading your font scaled by the right amount + calling ImGuiStyle::ScaleAllSizes() on your style. You should be able to do that already without any extra patch/work. Make sure to round your font size to nearest integer.

ocornut avatar Aug 11 '20 15:08 ocornut

@ocornut Thanks! I would already be happy to only support one DPI (not multiple monitors with different DPIs for the same window). I'm using the Rust bindings, loading my font like this now: https://github.com/Gekkio/imgui-rs/blob/d5be602f73c51896838318958cd6c930f18cc8c3/imgui-examples/examples/support/mod.rs#L51-L71

It seems to work, but it would be more correct to round font_size to the nearest int, right? Do I need to change rasterizer_multiply at all? And do I still need to call ScaleAllSizes after this?

Boscop avatar Aug 14 '20 04:08 Boscop

If anyone is interested - there is another experimental branch implementing a different approach we are exploring.

This approach implements multiple font atlases for each unique DPI value and automatic style rescaling. Naturally solution is not fleshed out and there will surely be dragons. We would appreciate any feedback. I also linked a gist describing some issues from which you can infer changes that are needed to support HDPI. Only SDL OpenGL3 example has automatic DPI scaling enabled.

https://github.com/rokups/imgui/tree/hdpi-support-fonstscale-viewport-3 https://gist.github.com/rokups/a322dff8ee885f75ee8326207ea0bf75

I would already be happy to only support one DPI

@Boscop to do that you should multiply font size by your desired DPI scale and also use style.ScaleAllSizes(dpi_scale). It wont be perfect, but about 95% good.

rokups avatar Aug 31 '20 12:08 rokups