sublimehq / sublime_text

Issue tracker for Sublime Text
https://www.sublimetext.com
809 stars 39 forks source link

DirectDraw glyph spacing is too condensed, text less legible than in Visual Studio - fixed width font glyphs are not pixel-aligned #2350

Open nanoant opened 6 years ago

nanoant commented 6 years ago

I think there is something wrong with DirectWrite rendering. I have to use it since GDI is essentially broken since PerMonitorDpi changes (see #2242).

Long story short Sublime Text is far less legible than one generated by Visual Studio, or Electron based editors, which causes eye strain on my (non HDPI) workstation.

Visual Studio 2015 (top) vs SublimeText 3176 (bottom)

directwrite-dpiscale125-vsvsst-consolas-8pt

Same image at 400%: directwrite-dpiscale125-vsvsst-consolas-8pt-400pct

(make sure you view these images with your browser set at 100% - so no browser upscaling breaks images above)

Observations

  1. Text is more condensed in Sublime (top), Visual Studio does more spacing between glyphs (bottom), I have also looked in Word, Chrome, and in all of them text is less condensed as in Sublime
  2. Glyph in sublime has non-pixel aligned width (top) - and depending on the position each glyph looks differently. There are 3 r (small R) glyph, Sublime Text renders each of them differently! while in Visual Studio each r looks exactly the same.

Honestly I don't understand this behavior when working with fixed width font (point 2). This looks like a bug to me. If there are some options in DirectWrite that control this it would be nice if there were exposed to developers, at least in dev builds.

P.S. Sorry to be such an annoying guy about the font rendering, but I spend really many hours in front on the monitor πŸ™„

Environment

wbond commented 6 years ago

It sounds to me like Visual Studio is rounding glyph widths. Different apps do this differently, and even different sizes with certain apps on specific platforms do this differently. On some apps on Mac, monospace fonts size 16 or below have rounded glyph widths. Other apps don't.

Currently we are obeying the glyph shaping output from Windows as to the advances for each glyph. Clearly they are not all integers, and thus letters are subpixel positioned, resulting in different pixel representations of the lowercase r. I would say that there is no bug here, just that we do it differently that Visual Studio does.

Microsoft is actually really bad, in regards to consistency, about their font rendering. Notepad always does 100% cleartype subpixel color anti-aliasing even if you have it turned off or down. Edge properly respects ClearType tuning, but for the actual HTML content, and not for elements in the chrome of the browser. It seems Visual Studio rounds glyph widths. It is kind of a crap shoot.

This should be tagged a feature request, but ultimately Jon will be the one to decide if he wants to support rounded glyph sizing across the board.

nanoant commented 6 years ago

@wbond Got it! Thanks for the explanation and good to see you back.

nanoant commented 6 years ago

@wbond Dear Will, I gave a latest builds a testdrive, but so far new DirectDraw with PerDPI scaling rendering is too me inferior, not sure if it is not pixel aligned or too condensed glyph spacing or something else, but it gives me eye strain, hence I switched back to GDI and build 3157 and this is really a relief. Moreover this build does not suffer ... truncation of autocomplete #2279.

Like I said before I have an impression that Visual Studio or Jupyer (Chrome) gives far better clarity on Windows (work non-HiDPI workstation). I have Retina Mac home, so to me it is Windows specific problem.

I am checking everyday for a new dev build, but it seems that development has ceased for a while after 3.1.1 release.

wbond commented 6 years ago

Have you tried enabling DPI backwards compatibility on sublime_text.exe set to Windows 7? That should enable system-mode dpi scaling.

We are in the middle of working on a chunk of work, so there is no dev release imminent. Unfortunately also I don’t believe we’ll be making DPI-related changes on Windows soon other than fixing the autocomplete window width.

nanoant commented 6 years ago

Note sure which exactly option you are suggesting, but... I tried all of "Override...", Application gives no effect, while System is using 100% scaling the upscales the bitmap to 125%, also tried "Compatibility mode". Nothing really makes it look as it used to be in 3157.

image

nanoant commented 6 years ago

@wbond Dear Will, I managed to play a little with Skia sources, and I was able to reproduce 1:1 Visual Studio rendering, no special code, just DirectWrite flags πŸ˜ƒ

Using both DWRITE_RENDERING_MODE_GDI_CLASSIC and DWRITE_MEASURING_MODE_GDI_CLASSIC, together with DWRITE_TEXTURE_CLEARTYPE_3x1 leads to 1:1 pixel match as in Visual Studio (except to gamma, see below):

skia-vs-visualstudio

Top to bottom:

  1. Skia default LCD rendering
  2. Skia forced DW GDI classic rendering and measuring
  3. Visual Studio (Visual Studio seems to use different gamma, but pixels layout and relative values are the same - zoom to see)

Sublime Text last build behavior

So, with all I have said above, I have strong impression that ST maps both dwrite_cleartype_classic and dwrite_cleartype_natural into same DWRITE_RENDERING_MODE_GDI_NATURAL - I don't see any difference switching between these two ST settings. Therefore I strongly suspect DWRITE_RENDERING_MODE_GDI_CLASSIC is nowhere exposed.

Proposed solution

Hence the little request, can you please make sure that dwrite_cleartype_classic enabled both DWRITE_RENDERING_MODE_GDI_CLASSIC and DWRITE_MEASURING_MODE_GDI_CLASSIC correctly.

This will finally solve my issues, and bring best experience (IMHO) for Windows developers working on regular screens, where as stated in DirectWrite docs the GDI rendering favors more screen pixels (sharpness) vs overall font shape preserved by clear type natural rendering and measuring. With such a change, I believe plain old "gdi" rendering is obsolete in ST.

nanoant commented 6 years ago

@wbond If it makes it easier I have also create a demonstration program at https://gist.github.com/nanoant/dde75805132561140ed5e38f4048f5c1 to demonstrate various DirectWrite rendering and glyph measuring method (layout).

image

  1. DirectWrite default
  2. DWRITE_RENDERING_MODE_GDI_CLASSIC + DWRITE_MEASURING_MODE_GDI_CLASSIC
  3. DWRITE_RENDERING_MODE_GDI_NATURAL + DWRITE_MEASURING_MODE_GDI_NATURAL
  4. DWRITE_RENDERING_MODE_NATURAL + DWRITE_MEASURING_MODE_NATURAL - looks the same as 1. and latest ST DirectWrite rendering
  5. Same as 4. but with default->GetEnhancedContrast() * 0.5f - somewhat similar to ST dwrite_cleartype_classic & dwrite_cleartype_natural πŸ™„
  6. Same as 4. but with DWRITE_MEASURING_MODE_GDI_CLASSIC and default->GetEnhancedContrast() * 0.5f
  7. Same as 4. but with DWRITE_MEASURING_MODE_GDI_CLASSIC and default->GetEnhancedContrast() * 2.0f - somewhat similar to Visual Studio
  8. Same as 4. but with DWRITE_MEASURING_MODE_GDI_CLASSIC and default->GetGamma() * 2.0f
  9. Same as 4. but with DWRITE_MEASURING_MODE_GDI_CLASSIC and default->GetGamma() * 0.5f

Like I said before I don't think ST does expose number 2 or 3, moreover I have an impression that dwrite_cleartype_classic and dwrite_cleartype_natural does something else than advertised - use different CreateCustomRenderingParams gamma and/or enhanced contrast as point 5. looks like it, which unexpected since it was supposed to alter only rendering (and measuring) method - I would expect look of point 2 or 3.

(Btw. am I correct that ST tweaks this in SkScalerContext_DW::SkScalerContext_DW? Do you use SK_FONT_HOST_USE_SYSTEM_SETTINGS?)

Therefore maybe the most straightforward solution is to have dwrite_rendering_mode_gdi_classic dwrite_measuring_mode_gdi_classic separate flags that only affect rendering and measuring method (not anything else), instead dwrite_cleartype_classic/natural that apparently do something else.

Additionally you may also consider exposing enhanced contrast setting e.g. under dwrite_enhanced_contrast_scale as a factor for pDefaultRenderingParams->GetEnhancedContrast() * value (with 1.0 default - meaning same as default), so users can tweak it to their needs which may be appreciated by some dark theme users complaining about text being too thin or thick.

With all this, I don't think old gdi is needed anymore since dwrite_rendering/measuring_mode_gdi_classic does what gdi used to do before it got broken #2242, or maybe gdi then should become alias for these two enabled and internal GDI implementation should be removed completely?

Cheers, and please let me know if this makes sense.

wbond commented 6 years ago

I am not working on this part of the app at the moment, but just so you know, we do not use Skia for any text rendering any longer. We used to use it on Windows under certain circumstances, but our glyph rendering is now 100% homegrown, using the Windows API methods.

When the user specifies dwrite_cleartype_classic or dwrite_cleartype_natural, we utilize CreateGdiCompatibleTextLayout() for layout, otherwise CreateTextLayout(). We pass useGdiNatural as TRUE only if the user specifies dwrite_cleartype_natural.

We used to use CreateGlyphRunAnalysis() and CreateAlphaTexture(), but that doesn't respect the rendering params. Now we utilize CreateBitmapRenderTarget() and DrawGlyphRun() since they respect the rendering params. We store the output as a black and white pixel buffer and colorize as needed. We have different fractional X positions that we rasterize each char at so we can do proper subpixel positioning.

I have confirmed that there is slightly different rendering between dwrite_cleartype_classic and dwrite_cleartype_natural. This likely depends on the font and size, if I recall correctly.

Unfortunately I am not 100% familiar with the Skia directwrite implementation, so I don't know what APIs they use under the hood, but I am fairly sure they are different since they don't respect clear type tuning parameters, and I have seen PRs on Chromium to try and fix the issue (which I think were stalled out of disinterest from Google). Based on that, my hunch is that they were using CreateAlphaTexture() and a certain gamma with the output.

wbond commented 6 years ago

Here is an example of the rendering and measuring modes we use (slightly modified to omit implementation details):

DWRITE_RENDERING_MODE rmode = DWRITE_RENDERING_MODE_CLEARTYPE_NATURAL_SYMMETRIC;
DWRITE_MEASURING_MODE mmode = DWRITE_MEASURING_MODE_NATURAL;

if (aliased)
{
    rmode = DWRITE_RENDERING_MODE_ALIASED;
}
else if (dwrite_cleartype_classic)
{
    rmode = DWRITE_RENDERING_MODE_CLEARTYPE_GDI_CLASSIC;
    mmode = DWRITE_MEASURING_MODE_GDI_CLASSIC;
}
else if (dwrite_cleartype_natural)
{
    rmode = DWRITE_RENDERING_MODE_CLEARTYPE_GDI_NATURAL;
    mmode = DWRITE_MEASURING_MODE_GDI_NATURAL;
}
nanoant commented 6 years ago

@wbond Thanks for the detailed feedback. I trust your description, however something is definitely wrong if I compare output of my application with the output of ST 3176 (see below) and I hope we can get to the root cause of it.

(please look at this image at 100% scaling (no browser up or downscaling)) image

Images above the line (top to bottom) -- Sublime Text rendering

A. Sublime Text 3157 gdi (last version that produces IMHO correct GDI glyph layout) B. Sublime Text 3176 gdi (latest build, known problem incorrect GDI layout according to #2242) C. Sublime Text 3176 directwrite D. Sublime Text 3176 directwrite + dwrite_cleartype_classic E. Sublime Text 3176 directwrite + dwrite_cleartype_natural

Images below the line (top to bottom) -- DirectWrite example app rendering (see above)

  1. DirectWrite Example default DWRITE_RENDERING_MODE_NATURAL + DWRITE_MEASURING_MODE_NATURAL
  2. DirectWrite Example DWRITE_RENDERING_MODE_GDI_CLASSIC + DWRITE_MEASURING_MODE_GDI_CLASSIC

Observations

Questions

@wbond Can you please explain why?

wbond commented 6 years ago

What font and size are you using? I tried all sorts of different variations of Sublime Text versions and could not replicate what you were seeing, so I would need more details to fully go through your post. Also, what DPI scaling mode is Sublime Text running under? Is it system, or per-monitor? If it is per-monitor, what DPI scale is the display your are taking the screenshots on? The Sublime Text console output will show DPI mode:. If the value is not DPI mode: system and you are using a scaling of anything other than 100%, you are not going to like GDI, and it will look wrong.

Until this info is established, we are comparing apples to oranges.

I am going to take a little more time to outline some things below, but this thread is taking up way too much time that could go to other things. Unfortunately Windows has a very complicated and convoluted way to handle high DPI that works with modern displays, and has like 20 different ways to use DirectWrite and GDI with different factories, rendering modes, rasterization, clear type tuning and so forth.

Why do I say that? Because your example code uses completely different APIs that we use, and I don't see any details about your dpi awareness you used to compile your app, or the environment it was run on. I don't doubt you are getting the results you are, but that doesn't mean we aren't using the APIs we are using properly.


Furthermore, the code that shapes gdi text didn't change between build 3157 and 3158. There was a significant change when we dropped Skia for font rendering in build 3146. We replaced some divergent font code in different situations with our own shaping and rasterizing library that allowed us to selectively handle ligatures, do glyph clipping (the reason Skia was used before) and simplify and standardize the font handling across all three supported platforms.

What did change in 3158 is that we added support for per-monitor DPI scaling, which required switching to logical coordinates for the whole application. Before this we were using system DPI scale, so it was hard-coded across the whole app at startup, and all pixel dimensions were multiplied by the system scaling. Thus, a system DPI of 125% would result is every dimension and font size being multiplied by 1.25. Most dimensions and font sizes were then rounded to whole pixel values, so everything was "pixel perfect".


We never use DrawText() anywhere, so there isn't a way to compare directly to the output to that. We use GetTextExtentPoint() when "font_options": ["gdi"] is set, and when not we use either CreateTextLayout() (or CreateGdiCompatibleTextLayout() if a dwrite gdi option is set) and use the DWRITE_GLYPH_RUN glyphOffsets advanceOffset to measure the X axis size. Since we are getting consistent dimensions from both the gdi APIs and the dwrite APIs, that makes me think we are getting back the correct extent information.

My hunch is that you are running in per-monitor DPI mode, thus using logical coordinates. More details about the Sublime Text codebase: we only store one set of layout information for the text buffer. However, a text buffer can be displayed in different tabs on different monitors at the same time. We can only store the width of each token in logical coordinates. Since we don't know what displays you may plug in, or what monitor a tab may get dragged on to, we store the layout of GDI at 1.0 scale. This will be incorrect for any other scale because GDI does heavy pixel snapping. It works for DirectWrite since it doesn't snap the layout in the same way. However, if you are using GDI and have a 100% scale display (as most GDI users do), everything will look normal.

However, if you have your scale set to 125% or 150%, the layout of text is going to be incorrect when using GDI, or the GDI-compatible modes of DirectWrite. Things are going to look either squished, or too far apart depending on the font and size.

I believe that if you want the behavior before we introduced per-monitor DPI scales, that you need to set the "Run this program in compatibility mode for:" to the value of "Windows 7". Once Sublime Text is closed and restarted, you should see in the console that the DPI mode: system and DPI scale: 1.25 (or whatever your scale is set to) should be present. This should trigger the old code paths that are used on Windows 7 to multiply the dimensions and font sizes by the scaling factor. All of the new logical coordinate system should fall back to thinking the scaling is 1.0 and you should get crisp GDI text at any scale. The downside is that you wouldn't be able to use that with mixed DPI monitor configurations, and I don't think it will respond to scale changes without and log out and log back in.


In terms of the anti-aliasing weight of various options, we use a gamma value that is tweaked based on the ClearType tuning values since the Windows API we use (CreateBitmapRenderTarget() and DrawGlyphRun()) ignores some rendering parameters. There is no formula that Microsoft published anywhere the shows exactly how they apply the various clear type tuning parameters in terms of rendering, so I did some experimentation with Microsoft Edge (since it respects cleartype) and came up with gamma modification parameters that mimic the effects seen in Edge.

nanoant commented 6 years ago

@wbond Please see my answers below your text.

What font and size are you using? I tried all sorts of different variations of Sublime Text versions and could not replicate what you were seeing, (...) Also, what DPI scaling mode is Sublime Text running under? Is it system, or per-monitor? (...)

Because your example code uses completely different APIs that we use, and I don't see any details about your dpi awareness you used to compile your app, or the environment it was run on.

My example app runs at my machine, so the environment is the same as stated above. It calls SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) in wWinMain and I believe that it is producing correct and expected results.

My hunch is that you are running in per-monitor DPI mode, thus using logical coordinates.

At very beginning of this thread I stated I am at Windows 10 and I use 125 DPI scaling. So yes, this means per-monitor scaling is active.

More details about the Sublime Text codebase: we only store one set of layout information for the text buffer. (...), we store the layout of GDI at 1.0 scale. This will be incorrect for any other scale because GDI does heavy pixel snapping.

Okay, I believe this explains everything. However, does it mean then that ST layout is compliant only when on 100% scaling? 😒 and at every other scale it is just an approximation?

Also I suspect you still need to round you logical coordinates to subpixel boundaries at ~100%, otherwise you will smear your alpha glyph textures. That could explain my weird GDI spacing as caused by snapping to nearest subpixel.

It works for DirectWrite since it doesn't snap the layout in the same way.

So DirectWrite does not do such aggressive pixel snapping as GDI, but I believe pixel hinting is still there, so I am not really sure if glyph measurement is completely DPI independent.

However, if you are using GDI and have a 100% scale display (as most GDI users do), everything will look normal.

I am confused by this statement. Wasn't the idea of per-monitor DPI support to make ST look perfect on each DPI and each monitor. But now it turns to be only fine for 100% scale displays, so then what is the purpose of all this effort after 3157?

I believe that if you want the behavior before we introduced per-monitor DPI scales, that you need to set the "Run this program in compatibility mode for:" to the value of "Windows 7". (...) This should trigger the old code paths (...) you should get crisp GDI text at any scale.

I would be happy if this workaround was working, I could definitely sacrifice per-monitor scaling for that, but it doesn't work for me. I have already tried that (as you have suggested that once above), and unfortunately this does not produce good GDI rendering as you described. In fact I see absolutely not difference in terms of the rendering, even that Sublime reports now DPI mode: system and DPI scale: 1.25 like you described. See below:

ST 3176 default (per monitor) and gdi

st3176-permonitor-nocompat

ST 3176 with Windows 7 compatibility and gdi

st3176-system-windows7compat

If the "old" rendering path is still there, can we have instead a settings switch (might require restart - I don't care). I believe "Windows 7" compatibility is not only affecting how scaling is handled, but may also affect other ST functionality.

wbond commented 6 years ago

My example app runs at my machine, so the environment is the same as stated above. It calls SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) in wWinMain and I believe that it is producing correct and expected results.

We don't use DrawText(), so it isn't comparable. We create glyph runs and measure the advances returned by the Windows API.

Okay, I believe this explains everything. However, does it mean then that ST layout is compliant only when on 100% scaling? 😒 and at every other scale it is just an approximation?

With GDI, yes. GDI is very legacy, and we did what we could to maintain support for it, but we'd much rather have all modern laptops with high DPI screens not look fuzzy than try to make GDI look pixel perfect at scales other than 100%.

Also I suspect you still need to round you logical coordinates to subpixel boundaries at ~100%, otherwise you will smear your alpha glyph textures. That could explain my weird GDI spacing as caused by snapping to nearest subpixel.

Yes, Windows approach of fractional scaling is troublesome to deal with, especially when you need to deal with mixed scale monitor environments. The only way to get things pixel perfect is for every element and window to know the display scale and round every dimension at layout time. But then you run into weird issues where rounding individual components results in sums too large for containers. Apple did a good job where everything is 200%, and then the output pixel buffer resized on the fly to the display.

If you want things to look good on Windows, using 100% or 200% is your best bet. Sublime Text 3176 allows you to run at 100% and then use the UI scale to replicate something like a 125% scale (but also 1.25 the UI size on a 200% DPI screen), however I don't know if Windows allows forcing the scale of an app to 100%. You'd have to dig into the compatibility options and try things.

So DirectWrite does not do such aggressive pixel snapping as GDI, but I believe pixel hinting is still there, so I am not really sure if glyph measurement is completely DPI independent.

Not a single person, to my knowledge, has noticed or complained of layout issues with DirectWrite, so I think it is "good enough." πŸ˜„

I am confused by this statement. Wasn't the idea of per-monitor DPI support to make ST look perfect on each DPI and each monitor. But now it turns to be only fine for 100% scale displays, so then what is the purpose of all this effort after 3157?

Yes, per-monitor DPI works just fine with DirectWrite, the issue is GDI. It significantly changes the "shape" of text at different scales. Since GDI is legacy, and basically unsupported by Microsoft at this point, we decided having proper per-monitor DPI scale so that things look crisp (as opposed to the old muddy mess) was far more noticeable and important than supporting the legacy GDI text layout. If you haven't seen build 3157 on a 200% laptop screen and a 100% external screen, it was horrible. Windows was trying to scale things itself, so everything other than the Window decorations looked like it was upscaled in a photo editing program.

I would be happy if this workaround was working, I could definitely sacrifice per-monitor scaling for that, but it doesn't work for me. I have already tried that (as you have suggested that once above), and unfortunately this does not produce good GDI rendering as you described. In fact I see absolutely not difference in terms of the rendering, even that Sublime reports now DPI mode: system and DPI scale: 1.25 like you described. See below:

Here is a screenshot I just took with 3176 (above) in Windows 7 compatibility mode using Consolas at size 8 in GDI mode with 150% scaling and 3157 (lower) with Consolas size 8 in GDI mode with 150% scaling. The layout is identical, however the font weight is different since 3176 respects cleartype tuning.

consolas-8-150-scale-windows7compat

I am not sure why your machine isn't doing this properly. Perhaps since I am running Windows 10 build 1734?

If the "old" rendering path is still there, can we have instead a settings switch (might require restart - I don't care). I believe "Windows 7" compatibility is not only affecting how scaling is handled, but may also affect other ST functionality.

No, this functionality is determined by the manifest attached to the exe (otherwise there are weird bugs where certain dialogs aren't scaled properly). This is the only way Microsoft ensures that your app works properly with per-monitor scaling. I attempted the code-based approach instead of a manifest during development, but we did run into issues where sometimes windows wouldn't get the appropriate scale info in time and things would be the wrong size. I can only presume this is why Microsoft says the manifest is the only way to get 100% support.

Either way, with the manifest, the scaling is determined by Windows at exe launch time, hence you have to use the compatibility settings to accomplish it.

nanoant commented 6 years ago

@wbond Thank you for comprehensive response. I will try to "hack" the manifest with some resourse editor to see if I can trigger old code path tomorrow. Cheers. (P.S. Responded from my private Retina MacBook at 200% DPI scaling 😎)

nanoant commented 6 years ago

@wbond I tried today hacking the binary with:

path = r'SublimeText.dev\\sublime_text.exe'

with open(path, 'rb') as f:
    b = f.read()
    b = b.replace(rb'>true/PM</dpiAware>',
                  rb'>true</dpiAware>   ')
    b = b.replace(rb'>PerMonitorV2, PerMonitor</dpiAwareness>',
                  rb'>System</dpiAwareness>                  ')
with open(path, 'wb') as f:
    f.write(b)

Similar effect as above, Sublime states: DPI mode: system and DPI scale: 1.25 but GDI layout is still broken: image

According to your description DPI mode: system should trigger old rendering path, but apparently it does not.

Just for comparison version 3157 with correct layout: image

Therefore, sorry but I have no idea how to trigger the good old rendering path with new builds. I can try on Windows 7 with 125% scale to see if it is even possible there.