Use grayscale AA always on non-opaque backgrounds (#5277)

## Summary of the Pull Request

When we're on acrylic, we can't have cleartype text unfortunately. This PR changes the DX renderer to force cleartype runs of text that are on a non-opaque background to use grayscale AA instead.

## References

Here are some of the URLS I was referencing as writing this:

* https://stackoverflow.com/q/23587787
* https://docs.microsoft.com/en-us/windows/win32/direct2d/supported-pixel-formats-and-alpha-modes#cleartype-and-alpha-modes
* https://devblogs.microsoft.com/oldnewthing/20150129-00/?p=44803
* https://docs.microsoft.com/en-us/windows/win32/api/d2d1/ne-d2d1-d2d1_layer_options
* https://docs.microsoft.com/en-us/windows/win32/direct2d/direct2d-layers-overview#d2d1_layer_parameters1-and-d2d1_layer_options1
* https://docs.microsoft.com/en-us/windows/win32/api/dcommon/ne-dcommon-d2d1_alpha_mode?redirectedfrom=MSDN#cleartype-and-alpha-modes
* https://stackoverflow.com/a/26523006

Additionally:
* This was introduced in #4711 

## PR Checklist
* [x] Closes #5098
* [x] I work here
* [ ] Tests added/passed
* [n/a] Requires documentation to be updated

## Detailed Description of the Pull Request / Additional comments

Basically, if you use cleartype on a light background, what you'll get today is the text foreground color _added_ to the background. This will make the text look basically invisible. 

So, what I did was use some trickery with `PushLayer` to basically create a layer where the text would be forced to render in grayscale AA. I only enable this layer pushing business when both:
  * The user has enabled cleartype text
  * The background opacity < 1.0

This plumbs some information through from the TermControl to the DX Renderer to make this smooth.

## Validation Steps Performed

Opened both cleartype and grayscale panes SxS, and messed with the opacity liberally.
This commit is contained in:
Mike Griese 2020-04-24 12:16:34 -05:00 committed by GitHub
parent fa58d87761
commit c803893c22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 132 additions and 3 deletions

View file

@ -345,11 +345,23 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
{
RootGrid().Background(acrylic);
}
// GH#5098: Inform the engine of the new opacity of the default text background.
if (_renderEngine)
{
_renderEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast<float>(_settings.TintOpacity()));
}
}
else
{
Media::SolidColorBrush solidColor{};
RootGrid().Background(solidColor);
// GH#5098: Inform the engine of the new opacity of the default text background.
if (_renderEngine)
{
_renderEngine->SetDefaultTextBackgroundOpacity(1.0f);
}
}
if (!_settings.BackgroundImage().empty())
@ -592,6 +604,12 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
break;
}
// GH#5098: Inform the engine of the opacity of the default text background.
if (_settings.UseAcrylic())
{
dxEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast<float>(_settings.TintOpacity()));
}
THROW_IF_FAILED(dxEngine->Enable());
_renderEngine = std::move(dxEngine);
@ -1280,7 +1298,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
try
{
auto acrylicBrush = RootGrid().Background().as<Media::AcrylicBrush>();
acrylicBrush.TintOpacity(acrylicBrush.TintOpacity() + effectiveDelta);
_settings.TintOpacity(acrylicBrush.TintOpacity() + effectiveDelta);
acrylicBrush.TintOpacity(_settings.TintOpacity());
if (acrylicBrush.TintOpacity() == 1.0)
{
_settings.UseAcrylic(false);
@ -1288,6 +1308,14 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
uint32_t bg = _settings.DefaultBackground();
_BackgroundColorChanged(bg);
}
else
{
// GH#5098: Inform the engine of the new opacity of the default text background.
if (_renderEngine)
{
_renderEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast<float>(_settings.TintOpacity()));
}
}
}
CATCH_LOG();
}

View file

@ -286,6 +286,43 @@ using namespace Microsoft::Console::Render;
d2dContext->FillRectangle(rect, drawingContext->backgroundBrush);
// GH#5098: If we're rendering with cleartype text, we need to always render
// onto an opaque background. If our background _isn't_ opaque, then we need
// to use grayscale AA for this run of text.
//
// We can force grayscale AA for just this run of text by pushing a new
// layer onto the d2d context. We'll only need to do this for cleartype
// text, when our eventual background isn't actually opaque. See
// DxEngine::PaintBufferLine and DxEngine::UpdateDrawingBrushes for more
// details.
//
// DANGER: Layers slow us down. Only do this in the specific case where
// someone has chosen the slower ClearType antialiasing (versus the faster
// grayscale antialiasing).
// First, create the scope_exit to pop the layer. If we don't need the
// layer, we'll just gracefully release it.
auto popLayer = wil::scope_exit([&d2dContext]() noexcept {
d2dContext->PopLayer();
});
if (drawingContext->forceGrayscaleAA)
{
// Mysteriously, D2D1_LAYER_OPTIONS_INITIALIZE_FOR_CLEARTYPE actually
// gets us the behavior we want, which is grayscale.
d2dContext->PushLayer(D2D1::LayerParameters(rect,
nullptr,
D2D1_ANTIALIAS_MODE_ALIASED,
D2D1::IdentityMatrix(),
1.0,
nullptr,
D2D1_LAYER_OPTIONS_INITIALIZE_FOR_CLEARTYPE),
nullptr);
}
else
{
popLayer.release();
}
// Now go onto drawing the text.
// First check if we want a color font and try to extract color emoji first.

View file

@ -12,6 +12,7 @@ namespace Microsoft::Console::Render
DrawingContext(ID2D1RenderTarget* renderTarget,
ID2D1Brush* foregroundBrush,
ID2D1Brush* backgroundBrush,
bool forceGrayscaleAA,
IDWriteFactory* dwriteFactory,
const DWRITE_LINE_SPACING spacing,
const D2D_SIZE_F cellSize,
@ -20,6 +21,7 @@ namespace Microsoft::Console::Render
this->renderTarget = renderTarget;
this->foregroundBrush = foregroundBrush;
this->backgroundBrush = backgroundBrush;
this->forceGrayscaleAA = forceGrayscaleAA;
this->dwriteFactory = dwriteFactory;
this->spacing = spacing;
this->cellSize = cellSize;
@ -29,6 +31,7 @@ namespace Microsoft::Console::Render
ID2D1RenderTarget* renderTarget;
ID2D1Brush* foregroundBrush;
ID2D1Brush* backgroundBrush;
bool forceGrayscaleAA;
IDWriteFactory* dwriteFactory;
DWRITE_LINE_SPACING spacing;
D2D_SIZE_F cellSize;

View file

@ -84,6 +84,7 @@ DxEngine::DxEngine() :
_haveDeviceResources{ false },
_retroTerminalEffects{ false },
_antialiasingMode{ D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE },
_defaultTextBackgroundOpacity{ 1.0f },
_hwndTarget{ static_cast<HWND>(INVALID_HANDLE_VALUE) },
_sizeTarget{},
_dpi{ USER_DEFAULT_SCREEN_DPI },
@ -1235,10 +1236,36 @@ try
DWRITE_LINE_SPACING spacing;
RETURN_IF_FAILED(_dwriteTextFormat->GetLineSpacing(&spacing.method, &spacing.height, &spacing.baseline));
// GH#5098: If we're rendering with cleartype text, we need to always
// render onto an opaque background. If our background's opacity is
// 1.0f, that's great, we can use that. Otherwise, we need to force the
// text renderer to render this text in grayscale. In
// UpdateDrawingBrushes, we'll set the backgroundColor's a channel to
// 1.0 if we're in cleartype mode and the background's opacity is 1.0.
// Otherwise, at this point, the _backgroundColor's alpha is <1.0.
//
// Currently, only text with the default background color uses an alpha
// of 0, every other background uses 1.0
//
// DANGER: Layers slow us down. Only do this in the specific case where
// someone has chosen the slower ClearType antialiasing (versus the faster
// grayscale antialiasing)
const bool usingCleartype = _antialiasingMode == D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE;
const bool usingTransparency = _defaultTextBackgroundOpacity != 1.0f;
// Another way of naming "bgIsDefault" is "bgHasTransparency"
const auto bgIsDefault = (_backgroundColor.a == _defaultBackgroundColor.a) &&
(_backgroundColor.r == _defaultBackgroundColor.r) &&
(_backgroundColor.g == _defaultBackgroundColor.g) &&
(_backgroundColor.b == _defaultBackgroundColor.b);
const bool forceGrayscaleAA = usingCleartype &&
usingTransparency &&
bgIsDefault;
// Assemble the drawing context information
DrawingContext context(_d2dRenderTarget.Get(),
_d2dBrushForeground.Get(),
_d2dBrushBackground.Get(),
forceGrayscaleAA,
_dwriteFactory.Get(),
spacing,
_glyphCell,
@ -1525,8 +1552,19 @@ CATCH_RETURN()
const ExtendedAttributes /*extendedAttrs*/,
bool const isSettingDefaultBrushes) noexcept
{
_foregroundColor = _ColorFFromColorRef(colorForeground);
_backgroundColor = _ColorFFromColorRef(colorBackground);
// GH#5098: If we're rendering with cleartype text, we need to always render
// onto an opaque background. If our background's opacity is 1.0f, that's
// great, we can actually use cleartype in that case. In that scenario
// (cleartype && opacity == 1.0), we'll force the opacity bits of the
// COLORREF to 0xff so we draw as cleartype. In any other case, leave the
// opacity bits unchanged. PaintBufferLine will later do some logic to
// determine if we should paint the text as grayscale or not.
const bool usingCleartype = _antialiasingMode == D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE;
const bool usingTransparency = _defaultTextBackgroundOpacity != 1.0f;
const bool forceOpaqueBG = usingCleartype && !usingTransparency;
_foregroundColor = _ColorFFromColorRef(OPACITY_OPAQUE | colorForeground);
_backgroundColor = _ColorFFromColorRef((forceOpaqueBG ? OPACITY_OPAQUE : 0) | colorBackground);
_d2dBrushForeground->SetColor(_foregroundColor);
_d2dBrushBackground->SetColor(_backgroundColor);
@ -2124,3 +2162,23 @@ void DxEngine::SetAntialiasingMode(const D2D1_TEXT_ANTIALIAS_MODE antialiasingMo
{
_antialiasingMode = antialiasingMode;
}
// Method Description:
// - Update our tracker of the opacity of our background. We can only
// effectively render cleartype text onto fully-opaque backgrounds. If we're
// rendering onto a transparent surface (like acrylic), then cleartype won't
// work correctly, and will actually just additively blend with the
// background. This is here to support GH#5098.
// Arguments:
// - opacity: the new opacity of our background, on [0.0f, 1.0f]
// Return Value:
// - <none>
void DxEngine::SetDefaultTextBackgroundOpacity(const float opacity) noexcept
{
_defaultTextBackgroundOpacity = opacity;
// Make sure we redraw all the cells, to update whether they're actually
// drawn with cleartype or not.
// We don't terribly care if this fails.
LOG_IF_FAILED(InvalidateAll());
}

View file

@ -106,6 +106,7 @@ namespace Microsoft::Console::Render
void SetSelectionBackground(const COLORREF color) noexcept;
void SetAntialiasingMode(const D2D1_TEXT_ANTIALIAS_MODE antialiasingMode) noexcept;
void SetDefaultTextBackgroundOpacity(const float opacity) noexcept;
protected:
[[nodiscard]] HRESULT _DoUpdateTitle(_In_ const std::wstring& newTitle) noexcept override;
@ -190,6 +191,8 @@ namespace Microsoft::Console::Render
D2D1_TEXT_ANTIALIAS_MODE _antialiasingMode;
float _defaultTextBackgroundOpacity;
// DirectX constant buffers need to be a multiple of 16; align to pad the size.
__declspec(align(16)) struct
{