From 4ccfe0bb8b464f94fc1730068c8c4a439206b3d9 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 27 Jan 2022 20:27:11 +0100 Subject: [PATCH] AtlasEngine: Implement ClearType blending (#12242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit extracts DirectWrite related shader code into dwrite.hlsl and adds support for ClearType blending. Additionally the following changes are piggybacked into this commit: * Some incorrect code around fallback glyph sizing was removed as this is already accomplished by `CreateTextLayout` internally * Hot-reload failed to work with dwrite.hlsl as the `pFileName` parameter was missing * Legibility of the dotted underline was improved by increasing the line gap from 1:1 to 3:1 Part of #9999. ## PR Checklist * [x] I work here * [x] Tests added/passed ## Validation Steps Performed * Types are clear ✅ --- src/renderer/atlas/AtlasEngine.cpp | 29 ++--- src/renderer/atlas/AtlasEngine.h | 11 +- src/renderer/atlas/AtlasEngine.r.cpp | 52 ++------ src/renderer/atlas/atlas.vcxproj | 7 +- src/renderer/atlas/dwrite.cpp | 171 +++++++++++++++++++++++++++ src/renderer/atlas/dwrite.h | 16 +++ src/renderer/atlas/dwrite.hlsl | 131 ++++++++++++++++++++ src/renderer/atlas/shader_ps.hlsl | 59 +++------ 8 files changed, 364 insertions(+), 112 deletions(-) create mode 100644 src/renderer/atlas/dwrite.cpp create mode 100644 src/renderer/atlas/dwrite.h create mode 100644 src/renderer/atlas/dwrite.hlsl diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index f84a8200f10..4ef5567724d 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -265,22 +265,10 @@ try try { static const auto compile = [](const std::filesystem::path& path, const char* target) { - const wil::unique_hfile fileHandle{ CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr) }; - THROW_LAST_ERROR_IF(!fileHandle); - - const auto fileSize = GetFileSize(fileHandle.get(), nullptr); - const wil::unique_handle mappingHandle{ CreateFileMappingW(fileHandle.get(), nullptr, PAGE_READONLY, 0, fileSize, nullptr) }; - THROW_LAST_ERROR_IF(!mappingHandle); - - const wil::unique_mapview_ptr dataBeg{ MapViewOfFile(mappingHandle.get(), FILE_MAP_READ, 0, 0, 0) }; - THROW_LAST_ERROR_IF(!dataBeg); - wil::com_ptr error; wil::com_ptr blob; - const auto hr = D3DCompile( - /* pSrcData */ dataBeg.get(), - /* SrcDataSize */ fileSize, - /* pFileName */ nullptr, + const auto hr = D3DCompileFromFile( + /* pFileName */ path.c_str(), /* pDefines */ nullptr, /* pInclude */ D3D_COMPILE_STANDARD_FILE_INCLUDE, /* pEntrypoint */ "main", @@ -1201,10 +1189,9 @@ void AtlasEngine::_flushBufferLine() #pragma warning(suppress : 26494) // Variable 'mappedEnd' is uninitialized. Always initialize an object (type.5). for (u32 idx = 0, mappedEnd; idx < _api.bufferLine.size(); idx = mappedEnd) { - float scale = 1.0f; - if (_sr.systemFontFallback) { + float scale = 1.0f; u32 mappedLength = 0; if (textFormatAxis) @@ -1268,7 +1255,7 @@ void AtlasEngine::_flushBufferLine() { if (const auto col2 = _api.bufferLineColumn[pos2]; col1 != col2) { - _emplaceGlyph(nullptr, scale, pos1, pos2); + _emplaceGlyph(nullptr, pos1, pos2); pos1 = pos2; col1 = col2; } @@ -1306,7 +1293,7 @@ void AtlasEngine::_flushBufferLine() { for (size_t i = 0; i < complexityLength; ++i) { - _emplaceGlyph(mappedFontFace.get(), scale, idx + i, idx + i + 1u); + _emplaceGlyph(mappedFontFace.get(), idx + i, idx + i + 1u); } } else @@ -1390,7 +1377,7 @@ void AtlasEngine::_flushBufferLine() { if (_api.textProps[i].canBreakShapingAfter) { - _emplaceGlyph(mappedFontFace.get(), scale, a.textPosition + beg, a.textPosition + i + 1); + _emplaceGlyph(mappedFontFace.get(), a.textPosition + beg, a.textPosition + i + 1); beg = i + 1; } } @@ -1400,7 +1387,7 @@ void AtlasEngine::_flushBufferLine() } } -void AtlasEngine::_emplaceGlyph(IDWriteFontFace* fontFace, float scale, size_t bufferPos1, size_t bufferPos2) +void AtlasEngine::_emplaceGlyph(IDWriteFontFace* fontFace, size_t bufferPos1, size_t bufferPos2) { static constexpr auto replacement = L'\uFFFD'; @@ -1459,7 +1446,7 @@ void AtlasEngine::_emplaceGlyph(IDWriteFontFace* fontFace, float scale, size_t b coords[i] = _allocateAtlasTile(); } - _r.glyphQueue.push_back(AtlasQueueItem{ &key, &value, scale }); + _r.glyphQueue.push_back(AtlasQueueItem{ &key, &value }); _r.maxEncounteredCellCount = std::max(_r.maxEncounteredCellCount, cellCount); } diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h index e11d3fb3c1a..cc8a8162264 100644 --- a/src/renderer/atlas/AtlasEngine.h +++ b/src/renderer/atlas/AtlasEngine.h @@ -396,7 +396,6 @@ namespace Microsoft::Console::Render Inlined = 0x00000001, ColoredGlyph = 0x00000002, - ThinFont = 0x00000004, Cursor = 0x00000008, Selected = 0x00000010, @@ -528,7 +527,6 @@ namespace Microsoft::Console::Render { const AtlasKey* key; const AtlasValue* value; - float scale; }; struct CachedCursorOptions @@ -556,8 +554,8 @@ namespace Microsoft::Console::Render // This means a structure like {u32; u32; u32; u32x2} would require // padding so that it is {u32; u32; u32; <4 byte padding>; u32x2}. alignas(sizeof(f32x4)) f32x4 viewport; - alignas(sizeof(f32x4)) f32x4 gammaRatios; - alignas(sizeof(f32)) f32 grayscaleEnhancedContrast = 0; + alignas(sizeof(f32x4)) f32 gammaRatios[4]{}; + alignas(sizeof(f32)) f32 enhancedContrast = 0; alignas(sizeof(u32)) u32 cellCountX = 0; alignas(sizeof(u32x2)) u32x2 cellSize; alignas(sizeof(u32x2)) u32x2 underlinePos; @@ -565,6 +563,7 @@ namespace Microsoft::Console::Render alignas(sizeof(u32)) u32 backgroundColor = 0; alignas(sizeof(u32)) u32 cursorColor = 0; alignas(sizeof(u32)) u32 selectionColor = 0; + alignas(sizeof(u32)) u32 useClearType = 0; #pragma warning(suppress : 4324) // 'ConstBuffer': structure was padded due to alignment specifier }; @@ -612,14 +611,13 @@ namespace Microsoft::Console::Render void _setCellFlags(SMALL_RECT coords, CellFlags mask, CellFlags bits) noexcept; u16x2 _allocateAtlasTile() noexcept; void _flushBufferLine(); - void _emplaceGlyph(IDWriteFontFace* fontFace, float scale, size_t bufferPos1, size_t bufferPos2); + void _emplaceGlyph(IDWriteFontFace* fontFace, size_t bufferPos1, size_t bufferPos2); // AtlasEngine.api.cpp void _resolveFontMetrics(const FontInfoDesired& fontInfoDesired, FontInfo& fontInfo, FontMetrics* fontMetrics = nullptr) const; // AtlasEngine.r.cpp void _setShaderResources() const; - static f32x4 _getGammaRatios(float gamma) noexcept; void _updateConstantBuffer() const noexcept; void _adjustAtlasSize(); void _reserveScratchpadSize(u16 minWidth); @@ -696,6 +694,7 @@ namespace Microsoft::Console::Render std::vector glyphQueue; f32 gamma = 0; + f32 cleartypeEnhancedContrast = 0; f32 grayscaleEnhancedContrast = 0; u32 backgroundColor = 0xff000000; u32 selectionColor = 0x7fffffff; diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp index 4a3f3a2390a..633274ebc4b 100644 --- a/src/renderer/atlas/AtlasEngine.r.cpp +++ b/src/renderer/atlas/AtlasEngine.r.cpp @@ -4,6 +4,8 @@ #include "pch.h" #include "AtlasEngine.h" +#include "dwrite.h" + // #### NOTE #### // If you see any code in here that contains "_api." you might be seeing a race condition. // The AtlasEngine::Present() method is called on a background thread without any locks, @@ -109,42 +111,17 @@ void AtlasEngine::_setShaderResources() const _r.deviceContext->PSSetShaderResources(0, gsl::narrow_cast(resources.size()), resources.data()); } -AtlasEngine::f32x4 AtlasEngine::_getGammaRatios(float gamma) noexcept -{ - static constexpr f32x4 gammaIncorrectTargetRatios[13]{ - { 0.0000f / 4.f, 0.0000f / 4.f, 0.0000f / 4.f, 0.0000f / 4.f }, // gamma = 1.0 - { 0.0166f / 4.f, -0.0807f / 4.f, 0.2227f / 4.f, -0.0751f / 4.f }, // gamma = 1.1 - { 0.0350f / 4.f, -0.1760f / 4.f, 0.4325f / 4.f, -0.1370f / 4.f }, // gamma = 1.2 - { 0.0543f / 4.f, -0.2821f / 4.f, 0.6302f / 4.f, -0.1876f / 4.f }, // gamma = 1.3 - { 0.0739f / 4.f, -0.3963f / 4.f, 0.8167f / 4.f, -0.2287f / 4.f }, // gamma = 1.4 - { 0.0933f / 4.f, -0.5161f / 4.f, 0.9926f / 4.f, -0.2616f / 4.f }, // gamma = 1.5 - { 0.1121f / 4.f, -0.6395f / 4.f, 1.1588f / 4.f, -0.2877f / 4.f }, // gamma = 1.6 - { 0.1300f / 4.f, -0.7649f / 4.f, 1.3159f / 4.f, -0.3080f / 4.f }, // gamma = 1.7 - { 0.1469f / 4.f, -0.8911f / 4.f, 1.4644f / 4.f, -0.3234f / 4.f }, // gamma = 1.8 - { 0.1627f / 4.f, -1.0170f / 4.f, 1.6051f / 4.f, -0.3347f / 4.f }, // gamma = 1.9 - { 0.1773f / 4.f, -1.1420f / 4.f, 1.7385f / 4.f, -0.3426f / 4.f }, // gamma = 2.0 - { 0.1908f / 4.f, -1.2652f / 4.f, 1.8650f / 4.f, -0.3476f / 4.f }, // gamma = 2.1 - { 0.2031f / 4.f, -1.3864f / 4.f, 1.9851f / 4.f, -0.3501f / 4.f }, // gamma = 2.2 - }; - static constexpr auto norm13 = static_cast(static_cast(0x10000) / (255 * 255) * 4); - static constexpr auto norm24 = static_cast(static_cast(0x100) / (255) * 4); - - gamma = clamp(gamma, 1.0f, 2.2f); - - const size_t index = gsl::narrow_cast(std::round((gamma - 1.0f) / 1.2f * 12.0f)); - const auto& ratios = gammaIncorrectTargetRatios[index]; - return { norm13 * ratios.x, norm24 * ratios.y, norm13 * ratios.z, norm24 * ratios.w }; -} - void AtlasEngine::_updateConstantBuffer() const noexcept { + const auto useClearType = _api.antialiasingMode == D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE; + ConstBuffer data; data.viewport.x = 0; data.viewport.y = 0; data.viewport.z = static_cast(_r.cellCount.x * _r.cellSize.x); data.viewport.w = static_cast(_r.cellCount.y * _r.cellSize.y); - data.gammaRatios = _getGammaRatios(_r.gamma); - data.grayscaleEnhancedContrast = _r.grayscaleEnhancedContrast; + DWrite_GetGammaRatios(_r.gamma, data.gammaRatios); + data.enhancedContrast = useClearType ? _r.cleartypeEnhancedContrast : _r.grayscaleEnhancedContrast; data.cellCountX = _r.cellCount.x; data.cellSize.x = _r.cellSize.x; data.cellSize.y = _r.cellSize.y; @@ -155,6 +132,7 @@ void AtlasEngine::_updateConstantBuffer() const noexcept data.backgroundColor = _r.backgroundColor; data.cursorColor = _r.cursorOptions.cursorColor; data.selectionColor = _r.selectionColor; + data.useClearType = useClearType; #pragma warning(suppress : 26447) // The function is declared 'noexcept' but calls function '...' which may throw exceptions (f.6). _r.deviceContext->UpdateSubresource(_r.constantBuffer.get(), 0, nullptr, &data, 0, 0); } @@ -282,13 +260,8 @@ void AtlasEngine::_reserveScratchpadSize(u16 minWidth) { const auto surface = _r.atlasScratchpad.query(); - wil::com_ptr defaultParams; - THROW_IF_FAILED(_sr.dwriteFactory->CreateRenderingParams(reinterpret_cast(defaultParams.addressof()))); wil::com_ptr renderingParams; - THROW_IF_FAILED(_sr.dwriteFactory->CreateCustomRenderingParams(1.0f, 0.0f, 0.0f, defaultParams->GetClearTypeLevel(), defaultParams->GetPixelGeometry(), defaultParams->GetRenderingMode(), renderingParams.addressof())); - - _r.gamma = defaultParams->GetGamma(); - _r.grayscaleEnhancedContrast = defaultParams->GetGrayscaleEnhancedContrast(); + DWrite_GetRenderParams(_sr.dwriteFactory.get(), &_r.gamma, &_r.cleartypeEnhancedContrast, &_r.grayscaleEnhancedContrast, renderingParams.addressof()); D2D1_RENDER_TARGET_PROPERTIES props{}; props.type = D2D1_RENDER_TARGET_TYPE_DEFAULT; @@ -300,11 +273,9 @@ void AtlasEngine::_reserveScratchpadSize(u16 minWidth) // We don't really use D2D for anything except DWrite, but it // can't hurt to ensure that everything it does is pixel aligned. _r.d2dRenderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); + _r.d2dRenderTarget->SetTextAntialiasMode(static_cast(_api.antialiasingMode)); // Ensure that D2D uses the exact same gamma as our shader uses. _r.d2dRenderTarget->SetTextRenderingParams(renderingParams.get()); - // We can't set the antialiasingMode here, as D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE - // will force the alpha channel to be 0 for _all_ text. - //_r.d2dRenderTarget->SetTextAntialiasMode(static_cast(_api.antialiasingMode)); } { static constexpr D2D1_COLOR_F color{ 1, 1, 1, 1 }; @@ -344,11 +315,6 @@ void AtlasEngine::_drawGlyph(const AtlasQueueItem& item) const // See D2DFactory::DrawText wil::com_ptr textLayout; THROW_IF_FAILED(_sr.dwriteFactory->CreateTextLayout(&key->chars[0], charsLength, textFormat, cells * _r.cellSizeDIP.x, _r.cellSizeDIP.y, textLayout.addressof())); - if (item.scale != 1.0f) - { - const auto f = textFormat->GetFontSize(); - textLayout->SetFontSize(f * item.scale, { 0, charsLength }); - } if (_r.typography) { textLayout->SetTypography(_r.typography.get(), { 0, charsLength }); diff --git a/src/renderer/atlas/atlas.vcxproj b/src/renderer/atlas/atlas.vcxproj index af6dcda8b10..5b257695e0e 100644 --- a/src/renderer/atlas/atlas.vcxproj +++ b/src/renderer/atlas/atlas.vcxproj @@ -12,16 +12,21 @@ + Create + + + true + Pixel 4.1 @@ -52,4 +57,4 @@ $(OutDir)$(ProjectName)\;%(AdditionalIncludeDirectories) - + \ No newline at end of file diff --git a/src/renderer/atlas/dwrite.cpp b/src/renderer/atlas/dwrite.cpp new file mode 100644 index 00000000000..66d50dfd42c --- /dev/null +++ b/src/renderer/atlas/dwrite.cpp @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "dwrite.h" + +#pragma warning(disable : 26429) // Symbol '...' is never tested for nullness, it can be marked as not_null (f.23). + +template +constexpr T clamp(T v, T min, T max) noexcept +{ + return std::max(min, std::min(max, v)); +} + +// The gamma and grayscaleEnhancedContrast values are required for DWrite_GetGrayscaleCorrectedAlpha(). +// in shader.hlsl later and can be passed in your cbuffer for instance. +// The returned linearParams object can be passed to various DirectWrite/D2D +// methods, like ID2D1RenderTarget::SetTextRenderingParams for instance. +// +// DirectWrite's alpha blending is gamma corrected and thus text color dependent. +// In order to do such blending in our shader we have to disable gamma compensation inside DirectWrite/Direct2D. +// If we didn't we'd apply the correction twice and the result would look wrong. +// +// Under Windows applications aren't expected to refresh the rendering params after startup, +// allowing you to cache these values for the lifetime of your application. +void DWrite_GetRenderParams(IDWriteFactory1* factory, float* gamma, float* cleartypeEnhancedContrast, float* grayscaleEnhancedContrast, IDWriteRenderingParams1** linearParams) +{ + // If you're concerned with crash resilience don't use reinterpret_cast + // and use .query() or ->QueryInterface() instead. + wil::com_ptr defaultParams; + THROW_IF_FAILED(factory->CreateRenderingParams(reinterpret_cast(defaultParams.addressof()))); + + *gamma = defaultParams->GetGamma(); + *cleartypeEnhancedContrast = defaultParams->GetEnhancedContrast(); + *grayscaleEnhancedContrast = defaultParams->GetGrayscaleEnhancedContrast(); + + THROW_IF_FAILED(factory->CreateCustomRenderingParams(1.0f, 0.0f, 0.0f, defaultParams->GetClearTypeLevel(), defaultParams->GetPixelGeometry(), defaultParams->GetRenderingMode(), linearParams)); +} + +// This function produces 4 magic constants for DWrite_ApplyAlphaCorrection() in dwrite.hlsl +// and are required as an argument for DWrite_GetGrayscaleCorrectedAlpha(). +// gamma should be set to the return value of DWrite_GetRenderParams() or (pseudo-code): +// IDWriteRenderingParams* defaultParams; +// dwriteFactory->CreateRenderingParams(&defaultParams); +// gamma = defaultParams->GetGamma(); +// +// gamma is chosen using the gamma value you pick in the "Adjust ClearType text" application. +// The default value for this are the 1.8 gamma ratios, which equates to: +// 0.148054421f, -0.894594550f, 1.47590804f, -0.324668258f +void DWrite_GetGammaRatios(float gamma, float (&out)[4]) noexcept +{ + static constexpr float gammaIncorrectTargetRatios[13][4]{ + { 0.0000f / 4.f, 0.0000f / 4.f, 0.0000f / 4.f, 0.0000f / 4.f }, // gamma = 1.0 + { 0.0166f / 4.f, -0.0807f / 4.f, 0.2227f / 4.f, -0.0751f / 4.f }, // gamma = 1.1 + { 0.0350f / 4.f, -0.1760f / 4.f, 0.4325f / 4.f, -0.1370f / 4.f }, // gamma = 1.2 + { 0.0543f / 4.f, -0.2821f / 4.f, 0.6302f / 4.f, -0.1876f / 4.f }, // gamma = 1.3 + { 0.0739f / 4.f, -0.3963f / 4.f, 0.8167f / 4.f, -0.2287f / 4.f }, // gamma = 1.4 + { 0.0933f / 4.f, -0.5161f / 4.f, 0.9926f / 4.f, -0.2616f / 4.f }, // gamma = 1.5 + { 0.1121f / 4.f, -0.6395f / 4.f, 1.1588f / 4.f, -0.2877f / 4.f }, // gamma = 1.6 + { 0.1300f / 4.f, -0.7649f / 4.f, 1.3159f / 4.f, -0.3080f / 4.f }, // gamma = 1.7 + { 0.1469f / 4.f, -0.8911f / 4.f, 1.4644f / 4.f, -0.3234f / 4.f }, // gamma = 1.8 + { 0.1627f / 4.f, -1.0170f / 4.f, 1.6051f / 4.f, -0.3347f / 4.f }, // gamma = 1.9 + { 0.1773f / 4.f, -1.1420f / 4.f, 1.7385f / 4.f, -0.3426f / 4.f }, // gamma = 2.0 + { 0.1908f / 4.f, -1.2652f / 4.f, 1.8650f / 4.f, -0.3476f / 4.f }, // gamma = 2.1 + { 0.2031f / 4.f, -1.3864f / 4.f, 1.9851f / 4.f, -0.3501f / 4.f }, // gamma = 2.2 + }; + static constexpr auto norm13 = static_cast(static_cast(0x10000) / (255 * 255) * 4); + static constexpr auto norm24 = static_cast(static_cast(0x100) / (255) * 4); + +#pragma warning(suppress : 26451) // Arithmetic overflow: Using operator '+' on a 4 byte value and then casting the result to a 8 byte value. + const auto index = clamp(static_cast(gamma * 10.0f + 0.5f), 10, 22) - 10; +#pragma warning(suppress : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4). +#pragma warning(suppress : 26482) // Only index into arrays using constant expressions (bounds.2). + const auto& ratios = gammaIncorrectTargetRatios[index]; + + out[0] = norm13 * ratios[0]; + out[1] = norm24 * ratios[1]; + out[2] = norm13 * ratios[2]; + out[3] = norm24 * ratios[3]; +} + +// This belongs to isThinFontFamily(). +// Keep this in alphabetical order, or the loop will break. +// Keep thinFontFamilyNamesMaxWithNull updated. +static constexpr std::array thinFontFamilyNames{ + L"Courier New", + L"Fixed Miriam Transparent", + L"Miriam Fixed", + L"Rod", + L"Rod Transparent", + L"Simplified Arabic Fixed" +}; +static constexpr size_t thinFontFamilyNamesMaxLengthWithNull = 25; + +// DWrite_IsThinFontFamily returns true if the specified family name is in our hard-coded list of "thin fonts". +// These are fonts that require special rendering because their strokes are too thin. +// +// The history of these fonts is interesting. The glyph outlines were originally created by digitizing the typeballs of +// IBM Selectric typewriters. Digitizing the metal typeballs yielded very precise outlines. However, the strokes are +// consistently too thin in comparison with the corresponding typewritten characters because the thickness of the +// typewriter ribbon was not accounted for. This didn't matter in the earliest versions of Windows because the screen +// resolution was not that high and you could not have a stroke thinner than one pixel. However, with the introduction +// of anti-aliasing the thin strokes manifested in text that was too light. By this time, it was too late to change +// the fonts so instead a special case was added to render these fonts differently. +// +// --- +// +// The canonical family name is a font's family English name, when +// * There's a corresponding font face name with the same language ID +// * If multiple such pairs exist, en-us is preferred +// * Otherwise (if en-us is not a translation) it's the lowest LCID +// +// However my (lhecker) understanding is that none of the thinFontFamilyNames come without an en-us translation. +// As such you can simply get the en-us name of the font from a IDWriteFontCollection for instance. +// See the overloaded alternative version of isThinFontFamily. +bool DWrite_IsThinFontFamily(const wchar_t* canonicalFamilyName) noexcept +{ + int n = 0; + + // Check if the given canonicalFamilyName is a member of the set of thinFontFamilyNames. + // Binary search isn't helpful here, as it doesn't really reduce the number of average comparisons. + for (const auto familyName : thinFontFamilyNames) + { + n = wcscmp(canonicalFamilyName, familyName); + if (n <= 0) + { + break; + } + } + + return n == 0; +} + +// The actual DWrite_IsThinFontFamily() expects you to pass a "canonical" family name, +// which technically isn't that trivial to determine. This function might help you with that. +// Just give it the font collection you use and any family name from that collection. +// (For instance from IDWriteFactory::GetSystemFontCollection.) +bool DWrite_IsThinFontFamily(IDWriteFontCollection* fontCollection, const wchar_t* familyName) +{ + UINT32 index; + BOOL exists; + if (FAILED(fontCollection->FindFamilyName(familyName, &index, &exists)) || !exists) + { + return false; + } + + wil::com_ptr fontFamily; + THROW_IF_FAILED(fontCollection->GetFontFamily(index, fontFamily.addressof())); + + wil::com_ptr localizedFamilyNames; + THROW_IF_FAILED(fontFamily->GetFamilyNames(localizedFamilyNames.addressof())); + + THROW_IF_FAILED(localizedFamilyNames->FindLocaleName(L"en-US", &index, &exists)); + if (!exists) + { + return false; + } + + UINT32 length; + THROW_IF_FAILED(localizedFamilyNames->GetStringLength(index, &length)); + + if (length >= thinFontFamilyNamesMaxLengthWithNull) + { + return false; + } + + wchar_t enUsFamilyName[thinFontFamilyNamesMaxLengthWithNull]; + THROW_IF_FAILED(localizedFamilyNames->GetString(index, &enUsFamilyName[0], thinFontFamilyNamesMaxLengthWithNull)); + + return DWrite_IsThinFontFamily(&enUsFamilyName[0]); +} diff --git a/src/renderer/atlas/dwrite.h b/src/renderer/atlas/dwrite.h new file mode 100644 index 00000000000..9c61e2cba7f --- /dev/null +++ b/src/renderer/atlas/dwrite.h @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +// Exclude stuff from we don't need. +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN + +#include + +// See .cpp file for documentation. +void DWrite_GetRenderParams(IDWriteFactory1* factory, float* gamma, float* cleartypeEnhancedContrast, float* grayscaleEnhancedContrast, IDWriteRenderingParams1** linearParams); +void DWrite_GetGammaRatios(float gamma, float (&out)[4]) noexcept; +bool DWrite_IsThinFontFamily(const wchar_t* canonicalFamilyName) noexcept; +bool DWrite_IsThinFontFamily(IDWriteFontCollection* fontCollection, const wchar_t* familyName); diff --git a/src/renderer/atlas/dwrite.hlsl b/src/renderer/atlas/dwrite.hlsl new file mode 100644 index 00000000000..561b395684b --- /dev/null +++ b/src/renderer/atlas/dwrite.hlsl @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +float3 DWrite_UnpremultiplyColor(float4 color) +{ + if (color.a != 0) + { + color.rgb /= color.a; + } + return color.rgb; +} + +float DWrite_ApplyLightOnDarkContrastAdjustment(float grayscaleEnhancedContrast, float3 color) +{ + // The following 1 line is the same as this direct translation of the + // original code, but simplified to reduce the number of instructions: + // float lightness = dot(color, float3(0.30f, 0.59f, 0.11f); + // float multiplier = saturate(4.0f * (0.75f - lightness)); + // return grayscaleEnhancedContrast * multiplier; + return grayscaleEnhancedContrast * saturate(dot(color, float3(0.30f, 0.59f, 0.11f) * -4.0f) + 3.0f); +} + +float DWrite_CalcColorIntensity(float3 color) +{ + return dot(color, float3(0.25f, 0.5f, 0.25f)); +} + +float DWrite_EnhanceContrast(float alpha, float k) +{ + return alpha * (k + 1.0f) / (alpha * k + 1.0f); +} + +float3 DWrite_EnhanceContrast3(float3 alpha, float k) +{ + return alpha * (k + 1.0f) / (alpha * k + 1.0f); +} + +float DWrite_ApplyAlphaCorrection(float a, float f, float4 g) +{ + return a + a * (1 - a) * ((g.x * f + g.y) * a + (g.z * f + g.w)); +} + +float3 DWrite_ApplyAlphaCorrection3(float3 a, float3 f, float4 g) +{ + return a + a * (1 - a) * ((g.x * f + g.y) * a + (g.z * f + g.w)); +} + +// Call this function to get the same gamma corrected alpha blending effect +// as DirectWrite's native algorithm for gray-scale anti-aliased glyphs. +// +// The result is a premultiplied color value, resulting +// out of the blending of foregroundColor with glyphAlpha. +// +// gammaRatios: +// Magic constants produced by DWrite_GetGammaRatios() in dwrite.cpp. +// The default value for this are the 1.8 gamma ratios, which equates to: +// 0.148054421f, -0.894594550f, 1.47590804f, -0.324668258f +// grayscaleEnhancedContrast: +// An additional contrast boost, making the font lighter/darker. +// The default value for this is 1.0f. +// This value should be set to the return value of DWrite_GetRenderParams() or (pseudo-code): +// IDWriteRenderingParams1* defaultParams; +// dwriteFactory->CreateRenderingParams(&defaultParams); +// gamma = defaultParams->GetGrayscaleEnhancedContrast(); +// isThinFont: +// This constant is true for certain fonts that are simply too thin for AA. +// Unlike the previous two values, this value isn't a constant and can change per font family. +// If you only use modern fonts (like Roboto) you can safely assume that it's false. +// If you draw your glyph atlas with any DirectWrite method except IDWriteGlyphRunAnalysis::CreateAlphaTexture +// then you must set this to false as well, as not even tricks like setting the +// gamma to 1.0 disables the internal thin-font contrast-boost inside DirectWrite. +// Applying the contrast-boost twice would then look incorrectly. +// foregroundColor: +// The text's foreground color in premultiplied alpha. +// glyphAlpha: +// The alpha value of the current glyph pixel in your texture atlas. +float4 DWrite_GrayscaleBlend(float4 gammaRatios, float grayscaleEnhancedContrast, bool isThinFont, float4 foregroundColor, float glyphAlpha) +{ + float3 foregroundStraight = DWrite_UnpremultiplyColor(foregroundColor); + float contrastBoost = isThinFont ? 0.5f : 0.0f; + float blendEnhancedContrast = contrastBoost + DWrite_ApplyLightOnDarkContrastAdjustment(grayscaleEnhancedContrast, foregroundStraight); + float intensity = DWrite_CalcColorIntensity(foregroundColor.rgb); + float contrasted = DWrite_EnhanceContrast(glyphAlpha, blendEnhancedContrast); + return foregroundColor * DWrite_ApplyAlphaCorrection(contrasted, intensity, gammaRatios); +} + +// Call this function to get the same gamma corrected alpha blending effect +// as DirectWrite's native algorithm for ClearType anti-aliased glyphs. +// +// The result is a color value with alpha = 1, resulting out of the blending +// of foregroundColor and backgroundColor using glyphColor to do sub-pixel AA. +// +// gammaRatios: +// Magic constants produced by DWrite_GetGammaRatios() in dwrite.cpp. +// The default value for this are the 1.8 gamma ratios, which equates to: +// 0.148054421f, -0.894594550f, 1.47590804f, -0.324668258f +// enhancedContrast: +// An additional contrast boost, making the font lighter/darker. +// The default value for this is 0.5f. +// This value should be set to the return value of DWrite_GetRenderParams() or (pseudo-code): +// IDWriteRenderingParams* defaultParams; +// dwriteFactory->CreateRenderingParams(&defaultParams); +// gamma = defaultParams->GetEnhancedContrast(); +// isThinFont: +// This constant is true for certain fonts that are simply too thin for AA. +// Unlike the previous two values, this value isn't a constant and can change per font family. +// If you only use modern fonts (like Roboto) you can safely assume that it's false. +// If you draw your glyph atlas with any DirectWrite method except IDWriteGlyphRunAnalysis::CreateAlphaTexture +// then you must set this to false as well, as not even tricks like setting the +// gamma to 1.0 disables the internal thin-font contrast-boost inside DirectWrite. +// Applying the contrast-boost twice would then look incorrectly. +// backgroundColor: +// The background color in premultiplied alpha (the color behind the text pixel). +// foregroundColor: +// The text's foreground color in premultiplied alpha. +// glyphAlpha: +// The RGB color of the current glyph pixel in your texture atlas. +// The A value is ignored, because ClearType doesn't work with alpha blending. +// RGB is required because ClearType performs sub-pixel AA. The most common ClearType drawing type is 6x1 +// overscale (meaning: the glyph is rasterized with 6x the required resolution in the X axis) and thus +// only 7 different RGB combinations can exist in this texture (black/white and 5 states in between). +// If you wanted to you could just store these in a A8 texture and restore the RGB values in this shader. +float4 DWrite_CleartypeBlend(float4 gammaRatios, float enhancedContrast, bool isThinFont, float4 backgroundColor, float4 foregroundColor, float4 glyphColor) +{ + float3 foregroundStraight = DWrite_UnpremultiplyColor(foregroundColor); + float contrastBoost = isThinFont ? 0.5f : 0.0f; + float blendEnhancedContrast = contrastBoost + DWrite_ApplyLightOnDarkContrastAdjustment(enhancedContrast, foregroundStraight); + float3 contrasted = DWrite_EnhanceContrast3(glyphColor.rgb, blendEnhancedContrast); + float3 alphaCorrected = DWrite_ApplyAlphaCorrection3(contrasted, foregroundStraight, gammaRatios); + return float4(lerp(backgroundColor.rgb, foregroundStraight, alphaCorrected * foregroundColor.a), 1.0f); +} diff --git a/src/renderer/atlas/shader_ps.hlsl b/src/renderer/atlas/shader_ps.hlsl index 36090fd6cef..15a24ae6c0e 100644 --- a/src/renderer/atlas/shader_ps.hlsl +++ b/src/renderer/atlas/shader_ps.hlsl @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +#include "dwrite.hlsl" + #define INVALID_COLOR 0xffffffff // These flags are shared with AtlasEngine::CellFlags. @@ -10,7 +12,6 @@ #define CellFlags_Inlined 0x00000001 #define CellFlags_ColoredGlyph 0x00000002 -#define CellFlags_ThinFont 0x00000004 #define CellFlags_Cursor 0x00000008 #define CellFlags_Selected 0x00000010 @@ -39,7 +40,7 @@ cbuffer ConstBuffer : register(b0) { float4 viewport; float4 gammaRatios; - float grayscaleEnhancedContrast; + float enhancedContrast; uint cellCountX; uint2 cellSize; uint2 underlinePos; @@ -47,17 +48,14 @@ cbuffer ConstBuffer : register(b0) uint backgroundColor; uint cursorColor; uint selectionColor; + uint useClearType; }; StructuredBuffer cells : register(t0); Texture2D glyphs : register(t1); float4 decodeRGBA(uint i) { - uint r = i & 0xff; - uint g = (i >> 8) & 0xff; - uint b = (i >> 16) & 0xff; - uint a = i >> 24; - float4 c = float4(r, g, b, a) / 255.0f; + float4 c = (i >> uint4(0, 8, 16, 24) & 0xff) / 255.0f; // Convert to premultiplied alpha for simpler alpha blending. c.rgb *= c.a; return c; @@ -70,30 +68,8 @@ uint2 decodeU16x2(uint i) float4 alphaBlendPremultiplied(float4 bottom, float4 top) { - float ia = 1 - top.a; - return float4(bottom.rgb * ia + top.rgb, bottom.a * ia + top.a); -} - -float applyLightOnDarkContrastAdjustment(float3 color) -{ - float lightness = 0.30f * color.r + 0.59f * color.g + 0.11f * color.b; - float multiplier = saturate(4.0f * (0.75f - lightness)); - return grayscaleEnhancedContrast * multiplier; -} - -float calcColorIntensity(float3 color) -{ - return (color.r + color.g + color.g + color.b) / 4.0f; -} - -float enhanceContrast(float alpha, float k) -{ - return alpha * (k + 1.0f) / (alpha * k + 1.0f); -} - -float applyAlphaCorrection(float a, float f, float4 g) -{ - return a + a * (1 - a) * ((g.x * f + g.y) * a + (g.z * f + g.w)); + bottom *= 1 - top.a; + return bottom + top; } // clang-format off @@ -138,7 +114,7 @@ float4 main(float4 pos: SV_Position): SV_Target { color = alphaBlendPremultiplied(color, fg); } - if ((cell.flags & CellFlags_UnderlineDotted) && cellPos.y >= underlinePos.x && cellPos.y < underlinePos.y && (viewportPos.x / (underlinePos.y - underlinePos.x) & 1)) + if ((cell.flags & CellFlags_UnderlineDotted) && cellPos.y >= underlinePos.x && cellPos.y < underlinePos.y && (viewportPos.x / (underlinePos.y - underlinePos.x) & 3) == 0) { color = alphaBlendPremultiplied(color, fg); } @@ -146,17 +122,18 @@ float4 main(float4 pos: SV_Position): SV_Target { float4 glyph = glyphs[decodeU16x2(cell.glyphPos) + cellPos]; - if (!(cell.flags & CellFlags_ColoredGlyph)) + if (cell.flags & CellFlags_ColoredGlyph) { - float contrastBoost = (cell.flags & CellFlags_ThinFont) == 0 ? 0.0f : 0.5f; - float enhancedContrast = contrastBoost + applyLightOnDarkContrastAdjustment(fg.rgb); - float intensity = calcColorIntensity(fg.rgb); - float contrasted = enhanceContrast(glyph.a, enhancedContrast); - float correctedAlpha = applyAlphaCorrection(contrasted, intensity, gammaRatios); - glyph = fg * correctedAlpha; + color = alphaBlendPremultiplied(color, glyph); + } + else if (useClearType) + { + color = DWrite_CleartypeBlend(gammaRatios, enhancedContrast, false, color, fg, glyph); + } + else + { + color = alphaBlendPremultiplied(color, DWrite_GrayscaleBlend(gammaRatios, enhancedContrast, false, fg, glyph.a)); } - - color = alphaBlendPremultiplied(color, glyph); } // Step 3: Lines, but not "under"lines if ((cell.flags & CellFlags_Strikethrough) && cellPos.y >= strikethroughPos.x && cellPos.y < strikethroughPos.y)