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?
(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
Because of the app being DPI unaware, the DPI remains effectively at 96 dpi.
Wouldn't you be better off making your app DPI-aware?
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:
After turning off DPI awareness in SciTE by editing the
win32\SciTE.exe.manifestfile, the results with the patch appear quite poor at 200% and 125% to me.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,GetAwarenessFromDpiAwarenessContextandGetScaleFactorForMonitorAPIs are not available there, those calls should be enabled through (GetModuleHandle/LoadLibraryEx)+DLLFunction.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.
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.*RenderTargetwill find them. For calltips the call is inScintillaWin::CTWndProcand for autocompletion lists inListBoxX::Draw.If Scintilla still wants to support Windows XP, then
WINVERand_WIN32_WINNTshould be defined as_WIN32_WINNT_WINXPinstead of_WIN32_WINNT_WIN10(=0x0A00) inScintillaWin.cxxandPlatWin.cxx.In
ScintillaDll.cxx, the constants point to_WIN32_WINNT_WIN2K(=0x0500).See Update WINVER and _WIN32_WINNT.
Setting
WINVERto an older version also excludes type definitions which are required for the function pointer signature, hence staying with the latest version is fine. ;-)screenshots for SciTE seems does not has GDI scaling enabled in manifest.
I think function like
FLOAT GetDPIUnawareScaleFactor(HWND hwnd, HMONITOR monitor) noexceptcould 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%.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.
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.
In my merge request I rounded up the scaled rectangles, alas to no avail.
Its possible that cached measurements aren't being removed from cache (
InvalidateStyleRedraw) when DPI set. Check that the handling ofWM_DPICHANGEDandWM_DPICHANGED_AFTERPARENTare behaving correctly with the change.As GDI scaling works transparently to the application, the application does not receive
WM_DPICHANGED.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?
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
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
StyleDefaultcan be used as an approximation that may sometimes work poorly. Filling with this background before callingPaintdecreases the artefacts.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.
The correct way to fix this issue was to enhance
PixelAlign()inGeometry.cxxto support non-integral scale factors.The virtual method
PixelDivisions()now returns the factor as hundreth; the implementation ofSurfaceGDIreturns 100 whereasSurfaceD2Dreturns 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.cxxfix 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.
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.
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.
Taking the same approach in
ScintillaWin::CTWndProc()as already inScintillaWin::EnsureRenderTarget(), the calltip shows no longer blurry (225%):While investigating the issue of the blurry autocompletion list, I had a look at
SurfaceD2D::DrawRGBAImage()which features this line:Shouldn't the DPI be set to 96 instead of 72?
Does it make a difference?
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.