Menu

#2344 DirectWrite rendering looks blurry with DPI unaware apps

Bug
closed
nobody
None
5
2024-01-21
2022-08-16
No

Windows by default performs GDI scaling for applications which are DPI unaware when using a display with device scaling set to more than 100 %.

My MFC app is DPI unaware. While text with SC_TECHNOLOGY_DEFAULT looks crisp with GDI scaling at 200 %, text looks very blurry when using DirectWrite as the rendering technology. Obviously Windows continues rendering text at 96 dpi and then scales this bitmap.

I've enhanced ScintillaWin::EnsureRenderTarget() to take the scale factor of the monitor into account:

#include <shellscalingapi.h>
#pragma comment(lib, "Shcore.lib")

void ScintillaWin::EnsureRenderTarget(HDC hdc) {
  if (!renderTargetValid) {
    DropRenderTarget();
    renderTargetValid = true;
  }
  if (!pRenderTarget) {
    HWND hw = MainHWND();
    RECT rc;
    ::GetClientRect(hw, &rc);

    float scaleFactor = 1.0f;

    if (DPI_AWARENESS_UNAWARE == ::GetAwarenessFromDpiAwarenessContext(::GetWindowDpiAwarenessContext(hw)))
    {
      DEVICE_SCALE_FACTOR deviceScaleFactor;
      if (S_OK == ::GetScaleFactorForMonitor(hCurrentMonitor, &deviceScaleFactor))
        scaleFactor = deviceScaleFactor / 100.0f;
    }

    const auto size = D2D1::SizeU((UINT32)(scaleFactor * (rc.right - rc.left)), (UINT32)(scaleFactor * (rc.bottom - rc.top)));
    const auto windowDpi = scaleFactor * dpi;

    // Create a Direct2D render target.
    D2D1_RENDER_TARGET_PROPERTIES drtp {};
    drtp.type = D2D1_RENDER_TARGET_TYPE_DEFAULT;
    drtp.pixelFormat.format = DXGI_FORMAT_UNKNOWN;
    drtp.pixelFormat.alphaMode = D2D1_ALPHA_MODE_UNKNOWN;
    drtp.dpiX = drtp.dpiY = windowDpi;
    drtp.usage = D2D1_RENDER_TARGET_USAGE_NONE;
    drtp.minLevel = D2D1_FEATURE_LEVEL_DEFAULT;
...

This patch makes text looks crisp again. Alas, this is not the only place to make adjustments: the calltip appears slightly blurry and the autocompletion listbox looks very blurry. At a quick glance, I couldn't find similar Direct2D code for calltips and the listbox.

Is this the correct way to tackle the issue? What do you think?

Related

Bugs: #2450
Feature Requests: #1432

Discussion

1 2 3 4 > >> (Page 1 of 4)
  • Zufu Liu

    Zufu Liu - 2022-08-16

    (not tested) What about leave dpiX and dpiY as zero, e.g. "Using Default DPI Settings":
    https://docs.microsoft.com/en-us/windows/win32/api/d2d1/ns-d2d1-d2d1_render_target_properties#using-default-dpi-settings

     
    • Markus Nißl

      Markus Nißl - 2022-08-16

      Because of the app being DPI unaware, the DPI remains effectively at 96 dpi.

       
  • Neil Hodgson

    Neil Hodgson - 2022-08-16

    Wouldn't you be better off making your app DPI-aware?

     
    • Markus Nißl

      Markus Nißl - 2022-08-17

      You highly underestimate the work of making a nearly 30 year old MFC enterprise application with 500.000 lines of code DPI-aware.

      If it were that simple, Microsoft would not have come up with the concept of Mixed-Mode DPI Scaling and DPI-aware APIs either.

      Taken from High DPI Desktop Application Development on Windows:

      When updating an application to support per-monitor DPI awareness, it can sometimes become impractical or impossible to update every window in the application in one go. This can simply be due to the time and effort required to update and test all UI, or because you do not own all of the UI code that you need to run (if your application perhaps loads third-party UI). In these situations, Windows offers a way to ease into the world of per-monitor awareness by letting you run some of your application windows (top-level only) in their original DPI-awareness mode while you focus your time and energy updating the more important parts of your UI.

       
  • Neil Hodgson

    Neil Hodgson - 2022-08-16

    After turning off DPI awareness in SciTE by editing the win32\SciTE.exe.manifest file, the results with the patch appear quite poor at 200% and 125% to me.

    200%

    125%

    These could be because of something unusual in SciTE but it doesn't look like the proposal is a simple fix for all.

    Since Scintilla still supports Windows XP and the GetWindowDpiAwarenessContext, GetAwarenessFromDpiAwarenessContext and GetScaleFactorForMonitor APIs are not available there, those calls should be enabled through (GetModuleHandle/LoadLibraryEx)+DLLFunction.

     
    • Markus Nißl

      Markus Nißl - 2022-08-17

      My patch gave me excellent results in my app. I will take a look at Scite tomorrow to see why things don't work out there.

      My patch was not a 100% "turn in as is" patch. I wanted to make you aware of the fact that DirectWrite looks very blurry when used with a GDI scaled app. I'm also aware of the fact those newer APIs are not available on all versions of Windows that Scintilla supports. I first wanted to know whether this would be the correct approach that you approve of before providing you with code to check in.

      Would you please point me to the pieces of code where the calltip and the listbox are setup Direct2D wise? They obviously also need some size and DPI treatment as they looked (very) blurry while buffer text looked awesome.

       
      • Neil Hodgson

        Neil Hodgson - 2022-08-17

        where the calltip and the listbox are setup Direct2D

        Autocompletion lists draw text with GDI, not DirectWrite but they do draw images with Direct2D.

        All Direct2D setup creates a render target so grepping Create.*RenderTarget will find them. For calltips the call is in ScintillaWin::CTWndProc and for autocompletion lists in ListBoxX::Draw.

         
    • Markus Nißl

      Markus Nißl - 2022-08-19

      If Scintilla still wants to support Windows XP, then WINVER and _WIN32_WINNT should be defined as _WIN32_WINNT_WINXP instead of _WIN32_WINNT_WIN10 (= 0x0A00) in ScintillaWin.cxx and PlatWin.cxx.

      In ScintillaDll.cxx, the constants point to _WIN32_WINNT_WIN2K (= 0x0500).

      See Update WINVER and _WIN32_WINNT.

       
      • Markus Nißl

        Markus Nißl - 2022-08-19

        Setting WINVER to an older version also excludes type definitions which are required for the function pointer signature, hence staying with the latest version is fine. ;-)

         
  • Zufu Liu

    Zufu Liu - 2022-08-17

    screenshots for SciTE seems does not has GDI scaling enabled in manifest.

    <asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
        <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2017/WindowsSettings">
            <gdiScaling>true</gdiScaling>
        </asmv3:windowsSettings>
    </asmv3:application>
    

    I think function like FLOAT GetDPIUnawareScaleFactor(HWND hwnd, HMONITOR monitor) noexcept could be added into PlatWin.cxx (where shcore.dll is loaded). however it appears to me the improvement of the code is not much but I can't test higher scaling like 200%.

     
  • Markus Nißl

    Markus Nißl - 2022-08-18

    You can quickly change DPI awareness settings via the properties dialog of the application file in File Explorer, tab "Compatibility": see section Configuring a local app to run with enhanced system scaling for a screenshot in English (I'm running the German locale of Windows).

    When selecting "System (Enhanced)", Windows performs GDI scaling. The official Scite 5.2.4 with "Cascadia Mono" as the monospace font then looks like this (200 %):

    With my patch, the rendering is crisp:

    I was wrong in my original post that Windows performs GDI scaling by default for DPI-unaware apps: Windows does perform scaling (option "System"), but it simply stretches the bitmap that was rendered at 96 dpi, while you explicitly have to opt into GDI scaling where it renders with an integral scaling factor (2x, 3x ...) and optionally scales it down to a lower DPI (e.g. for 150 %).

    With my patch, I observe artefacs with non integral scale factors when changing the scaling from 100 % to a non integral value. Here's an example at 225%:

    A simple redraw (by scrolling the buffer) fixes this issue, but the artefacts keep coming back after resizing the window. Obviously, another redraw is required here too.

     
    • Neil Hodgson

      Neil Hodgson - 2022-08-18

      artefacs with non integral scale factors

      That could be caused by the text rectangles not quite meeting and showing background which has been seen in buggy versions before but its not normally as consistent as the picture which appears to be exactly one pixel for each run. Maybe there is rounding occurring below where it can be seen by Scintilla.

       
      • Markus Nißl

        Markus Nißl - 2022-08-19

        In my merge request I rounded up the scaled rectangles, alas to no avail.

         
        • Neil Hodgson

          Neil Hodgson - 2022-08-20

          Its possible that cached measurements aren't being removed from cache (InvalidateStyleRedraw) when DPI set. Check that the handling of WM_DPICHANGED and WM_DPICHANGED_AFTERPARENT are behaving correctly with the change.

           
          • Markus Nißl

            Markus Nißl - 2022-08-20

            As GDI scaling works transparently to the application, the application does not receive WM_DPICHANGED.

             
            • Neil Hodgson

              Neil Hodgson - 2022-08-20

              As the scale factor changes, the render target should be recreated to match the new value. Is there a message that indicates the scale factor has changed?

               
              • Markus Nißl

                Markus Nißl - 2022-08-21

                The artefacts do not result from changing DPI: they are already visible initially (with non-integral scale factors) when the render target is created.

                When the application window is moved from one monitor to another one featuring a different device scale factor, Windows resizes the application window. ScintillaWin::SizeWindow() reacts to this event by dropping the render target. ScintillaWin::EnsureRenderTarget() in turn recreates the render target where my patch gets to see the new device scale factor.

                As already stated, resizing the window on a monitor with non integral scale factor also makes the artefacts reappear until a redraw happens (initiated e.g. by scrolling or moving the caret between lines).

                 

                Last edit: Markus Nißl 2022-08-21
      • Neil Hodgson

        Neil Hodgson - 2022-08-23

        Scintilla relies on the background being completely drawn by rectangular tiles. In these non-integral modes, it appears the initial drawing surface state is leaking through on the edges of the rectangles. The initial state may be black or grey and may be transparent.

        There isn't really a single background colour for the whole window but StyleDefault can be used as an approximation that may sometimes work poorly. Filling with this background before calling Paint decreases the artefacts.

        @@ -943,6 +955,7 @@
                    if (surfaceWindow) {
                        SetRenderingParams(surfaceWindow);
                        pRenderTarget->BeginDraw();
        
        +               surfaceWindow->FillRectangle(rcPaint, vs.styles[StyleDefault].back);
                        Paint(surfaceWindow, rcPaint);
                        surfaceWindow->Release();
                        const HRESULT hr = pRenderTarget->EndDraw();
        

        This will be slower and could have visual effects - it may need to take the update region into account instead of just the update rectangle. It should only be run when needed. The runtime cost of the scaling check should be examined and potentially cached.

         
        • Markus Nißl

          Markus Nißl - 2022-08-23

          The correct way to fix this issue was to enhance PixelAlign() in Geometry.cxx to support non-integral scale factors.

          The virtual method PixelDivisions() now returns the factor as hundreth; the implementation of SurfaceGDIreturns 100 whereas SurfaceD2D returns the device scale factor in case of "system enhanced scaling". It is up to you to provide the correct implementions for the other platforms.

          This fix is included in my second merge request where changes in ScintillaWin.cxx fix wrong DPI values for DPI-aware applications that introduced with my first merge request.

          Credits also go to my colleague and brother Reinhard Nißl.

           
  • Markus Nißl

    Markus Nißl - 2022-08-18

    I have an update on the blurry autompletion list: it looks absolutely fine without registered images:

    Alas, when the listbox shows images, it really blows up:

    This behaviour also shows without my code changes (i.e. Direct2D with GDI scaling). As expected, Technology::Default with GDI scaling looks fine.

     
    • Markus Nißl

      Markus Nißl - 2022-08-19

      The blown up autocompletion list is not the result of the actual drawing of the bitmaps: even without the call surfaceItem->DrawRGBAImage() the list items are blurred. No idea why.

      My workaround consists of limiting the canvas which is exposed to Direct2D to the bitmap strip at the left hand side of the list box which prevents the text from getting blurred.

       
  • Markus Nißl

    Markus Nißl - 2022-08-18

    Taking the same approach in ScintillaWin::CTWndProc() as already in ScintillaWin::EnsureRenderTarget(), the calltip shows no longer blurry (225%):

     
  • Markus Nißl

    Markus Nißl - 2022-08-19

    While investigating the issue of the blurry autocompletion list, I had a look at SurfaceD2D::DrawRGBAImage() which features this line:

    D2D1_BITMAP_PROPERTIES props = {{DXGI_FORMAT_B8G8R8A8_UNORM,
                D2D1_ALPHA_MODE_PREMULTIPLIED}, 72.0, 72.0};
    

    Shouldn't the DPI be set to 96 instead of 72?

     
    • Neil Hodgson

      Neil Hodgson - 2022-08-20

      Does it make a difference?

       
      • Markus Nißl

        Markus Nißl - 2022-08-20

        I cannot spot any difference for a 16x16 image. It only feels wrong to see some code using 72 and other code using 96 dpi.

         
1 2 3 4 > >> (Page 1 of 4)

Log in to post a comment.

MongoDB Logo MongoDB