Add support for custom box drawing and powerline glyphs (#16729)

This adds support for drawing our own box drawing, block element,
and basic Powerline (U+E0Bx) glyphs in AtlasEngine.

This PR consists of 4 parts:
* AtlasEngine was refactored to simplify `_drawGlyph` because I've
  wanted to do that for ~1 year now and never got a chance.
  Well, now I'm doing it and you all will review it muahahaha.
  The good news is that it removes a goto usage that even for my
  standards was rather dangerous. Now it's gone and the risk with it.
* AtlasEngine was further refactored to properly parse out text that
  we want to handle different from regular text. Previously, we only
  did that for soft fonts, but now we want to do that for a lot more,
  so a refactor was in order. The new code is still extremely
  disgusting, because I now stuff `wchar_t`s into an array that's
  intended for glyph indices, but that's the best way to make it fast
  and not blow up the complexity of the code even further.
* Finally this adds a huge LUT for all the aforementioned glyphs.
  The LUT has 4 "drawing instruction" entries per glyph which describe
  the shape (rectangle, circle, lines, etc.) and the start/end coord.
  With a lot of bit packing each entry is only 4 bytes large.
* Finally-finally a `builtinGlyphs` setting was added to the font
  object and it defaults to `true`.

Closes #5897

## Validation Steps Performed
* RenderingTests with soft fonts 
* All the aforementioned glyphs 
* ...with color 
* `customGlyphs` setting can be toggled on and off 
This commit is contained in:
Leonard Hecker
2024-02-23 22:40:29 +01:00
committed by GitHub
parent 6b29ef51e3
commit a6a0e44088
32 changed files with 1787 additions and 472 deletions

View File

@@ -860,6 +860,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
const auto lock = _terminal->LockForWriting();
_builtinGlyphs = _settings->EnableBuiltinGlyphs();
_cellWidth = CSSLengthPercentage::FromString(_settings->CellWidth().c_str());
_cellHeight = CSSLengthPercentage::FromString(_settings->CellHeight().c_str());
_runtimeOpacity = std::nullopt;
@@ -1038,6 +1039,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
_actualFont = { fontFace, 0, fontWeight.Weight, _desiredFont.GetEngineSize(), CP_UTF8, false };
_actualFontFaceName = { fontFace };
_desiredFont.SetEnableBuiltinGlyphs(_builtinGlyphs);
_desiredFont.SetCellSize(_cellWidth, _cellHeight);
const auto before = _actualFont.GetSize();

View File

@@ -316,6 +316,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
FontInfoDesired _desiredFont;
FontInfo _actualFont;
winrt::hstring _actualFontFaceName;
bool _builtinGlyphs = true;
CSSLengthPercentage _cellWidth;
CSSLengthPercentage _cellHeight;

View File

@@ -41,6 +41,7 @@ namespace Microsoft.Terminal.Control
String Padding { get; };
Windows.Foundation.Collections.IMap<String, UInt32> FontFeatures { get; };
Windows.Foundation.Collections.IMap<String, Single> FontAxes { get; };
Boolean EnableBuiltinGlyphs { get; };
String CellWidth { get; };
String CellHeight { get; };

View File

@@ -87,6 +87,7 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation
OBSERVABLE_PROJECTED_SETTING(_appearance.SourceProfile().FontInfo(), FontFace);
OBSERVABLE_PROJECTED_SETTING(_appearance.SourceProfile().FontInfo(), FontSize);
OBSERVABLE_PROJECTED_SETTING(_appearance.SourceProfile().FontInfo(), FontWeight);
OBSERVABLE_PROJECTED_SETTING(_appearance.SourceProfile().FontInfo(), EnableBuiltinGlyphs);
OBSERVABLE_PROJECTED_SETTING(_appearance, RetroTerminalEffect);
OBSERVABLE_PROJECTED_SETTING(_appearance, CursorShape);

View File

@@ -41,6 +41,7 @@ namespace Microsoft.Terminal.Settings.Editor
OBSERVABLE_PROJECTED_APPEARANCE_SETTING(Single, FontSize);
OBSERVABLE_PROJECTED_APPEARANCE_SETTING(Double, LineHeight);
OBSERVABLE_PROJECTED_APPEARANCE_SETTING(Windows.UI.Text.FontWeight, FontWeight);
OBSERVABLE_PROJECTED_APPEARANCE_SETTING(Boolean, EnableBuiltinGlyphs);
OBSERVABLE_PROJECTED_APPEARANCE_SETTING(String, DarkColorSchemeName);
OBSERVABLE_PROJECTED_APPEARANCE_SETTING(String, LightColorSchemeName);

View File

@@ -287,6 +287,15 @@
</StackPanel>
</local:SettingContainer>
<!-- Builtin Glyphs -->
<local:SettingContainer x:Uid="Profile_EnableBuiltinGlyphs"
ClearSettingValue="{x:Bind Appearance.ClearEnableBuiltinGlyphs}"
HasSettingValue="{x:Bind Appearance.HasEnableBuiltinGlyphs, Mode=OneWay}"
SettingOverrideSource="{x:Bind Appearance.EnableBuiltinGlyphsOverrideSource, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind Appearance.EnableBuiltinGlyphs, Mode=TwoWay}"
Style="{StaticResource ToggleSwitchInExpanderStyle}" />
</local:SettingContainer>
<!-- Retro Terminal Effect -->
<local:SettingContainer x:Uid="Profile_RetroTerminalEffect"
ClearSettingValue="{x:Bind Appearance.ClearRetroTerminalEffect}"

View File

@@ -886,6 +886,14 @@
<value>1.2</value>
<comment>"1.2" is a decimal number.</comment>
</data>
<data name="Profile_EnableBuiltinGlyphs.Header" xml:space="preserve">
<value>Builtin Glyphs</value>
<comment>The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones.</comment>
</data>
<data name="Profile_EnableBuiltinGlyphs.HelpText" xml:space="preserve">
<value>When enabled, the terminal draws custom glyphs for block element and box drawing characters instead of using the font. This feature only works when GPU Acceleration is available.</value>
<comment>A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification.</comment>
</data>
<data name="Profile_FontWeightComboBox.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Font weight</value>
<comment>Name for a control to select the weight (i.e. bold, thin, etc.) of the text in the app.</comment>

View File

@@ -20,6 +20,7 @@ namespace Microsoft.Terminal.Settings.Model
INHERITABLE_FONT_SETTING(Windows.UI.Text.FontWeight, FontWeight);
INHERITABLE_FONT_SETTING(Windows.Foundation.Collections.IMap<String COMMA UInt32>, FontFeatures);
INHERITABLE_FONT_SETTING(Windows.Foundation.Collections.IMap<String COMMA Single>, FontAxes);
INHERITABLE_FONT_SETTING(Boolean, EnableBuiltinGlyphs);
INHERITABLE_FONT_SETTING(String, CellWidth);
INHERITABLE_FONT_SETTING(String, CellHeight);
}

View File

@@ -114,6 +114,7 @@ Author(s):
X(winrt::Windows::UI::Text::FontWeight, FontWeight, "weight", DEFAULT_FONT_WEIGHT) \
X(IFontAxesMap, FontAxes, "axes") \
X(IFontFeatureMap, FontFeatures, "features") \
X(bool, EnableBuiltinGlyphs, "builtinGlyphs", true) \
X(winrt::hstring, CellWidth, "cellWidth") \
X(winrt::hstring, CellHeight, "cellHeight")

View File

@@ -289,6 +289,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
_FontWeight = fontInfo.FontWeight();
_FontFeatures = fontInfo.FontFeatures();
_FontAxes = fontInfo.FontAxes();
_EnableBuiltinGlyphs = fontInfo.EnableBuiltinGlyphs();
_CellWidth = fontInfo.CellWidth();
_CellHeight = fontInfo.CellHeight();
_Padding = profile.Padding();

View File

@@ -129,6 +129,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
INHERITABLE_SETTING(Model::TerminalSettings, winrt::Windows::UI::Text::FontWeight, FontWeight);
INHERITABLE_SETTING(Model::TerminalSettings, IFontAxesMap, FontAxes);
INHERITABLE_SETTING(Model::TerminalSettings, IFontFeatureMap, FontFeatures);
INHERITABLE_SETTING(Model::TerminalSettings, bool, EnableBuiltinGlyphs, true);
INHERITABLE_SETTING(Model::TerminalSettings, hstring, CellWidth);
INHERITABLE_SETTING(Model::TerminalSettings, hstring, CellHeight);

View File

@@ -63,6 +63,7 @@
X(winrt::Windows::UI::Text::FontWeight, FontWeight) \
X(IFontFeatureMap, FontFeatures) \
X(IFontAxesMap, FontAxes) \
X(bool, EnableBuiltinGlyphs, true) \
X(winrt::hstring, CellWidth) \
X(winrt::hstring, CellHeight) \
X(winrt::Microsoft::Terminal::Control::IKeyBindings, KeyBindings, nullptr) \

View File

@@ -64,6 +64,7 @@ SCREEN_INFORMATION::SCREEN_INFORMATION(
{
OutputMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
}
_desiredFont.SetEnableBuiltinGlyphs(gci.GetEnableBuiltinGlyphs());
}
// Routine Description:
@@ -539,7 +540,10 @@ void SCREEN_INFORMATION::RefreshFontWithRenderer()
void SCREEN_INFORMATION::UpdateFont(const FontInfo* const pfiNewFont)
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
FontInfoDesired fiDesiredFont(*pfiNewFont);
fiDesiredFont.SetEnableBuiltinGlyphs(gci.GetEnableBuiltinGlyphs());
GetDesiredFont() = fiDesiredFont;

View File

@@ -776,3 +776,8 @@ bool Settings::GetCopyColor() const noexcept
{
return _fCopyColor;
}
bool Settings::GetEnableBuiltinGlyphs() const noexcept
{
return _fEnableBuiltinGlyphs;
}

View File

@@ -171,6 +171,7 @@ public:
bool GetUseDx() const noexcept;
bool GetCopyColor() const noexcept;
bool GetEnableBuiltinGlyphs() const noexcept;
private:
RenderSettings _renderSettings;
@@ -214,6 +215,7 @@ private:
DWORD _dwVirtTermLevel;
bool _fUseDx;
bool _fCopyColor;
bool _fEnableBuiltinGlyphs = true;
// this is used for the special STARTF_USESIZE mode.
bool _fUseWindowSizePixels;

View File

@@ -4,8 +4,9 @@
#pragma once
#pragma warning(push)
#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4).
#pragma warning(disable : 26409) // Avoid calling new and delete explicitly, use std::make_unique<T> instead (r.11).
#pragma warning(disable : 26432) // If you define or delete any default operation in the type '...', define or delete them all (c.21).
#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4).
namespace til
{
@@ -36,7 +37,7 @@ namespace til
// * small and cheap T
// * >= 50% successful lookups
// * <= 50% load factor (LoadFactor >= 2, which is the minimum anyways)
template<typename T, size_t LoadFactor = 2, size_t GrowthExponent = 1>
template<typename T, typename Traits, size_t LoadFactor = 2, size_t GrowthExponent = 1>
struct linear_flat_set
{
static_assert(LoadFactor >= 2);
@@ -98,27 +99,28 @@ namespace til
return nullptr;
}
const auto hash = ::std::hash<T>{}(key) >> _shift;
const auto hash = Traits::hash(key) >> _shift;
for (auto i = hash;; ++i)
{
auto& slot = _map[i & _mask];
if (!slot)
if (!Traits::occupied(slot))
{
return nullptr;
}
if (slot == key) [[likely]]
if (Traits::equals(slot, key)) [[likely]]
{
return &slot;
}
}
}
// NOTE: It also does not initialize the returned slot.
// You must do that yourself in way that ensures that Traits::occupied(slot) now returns true.
// Use lookup() to check if the item already exists.
template<typename U>
std::pair<T&, bool> insert(U&& key)
std::pair<T*, bool> insert(U&& key)
{
// Putting this into the lookup path is a little pessimistic, but it
// allows us to default-construct this hashmap with a size of 0.
if (_load >= _capacity) [[unlikely]]
{
_bumpSize();
@@ -129,20 +131,20 @@ namespace til
// many times in literature that such a scheme performs the best on average.
// As such, we perform the divide here to get the topmost bits down.
// See flat_set_hash_integer.
const auto hash = ::std::hash<T>{}(key) >> _shift;
const auto hash = Traits::hash(key) >> _shift;
for (auto i = hash;; ++i)
{
auto& slot = _map[i & _mask];
if (!slot)
if (!Traits::occupied(slot))
{
slot = std::forward<U>(key);
_load += LoadFactor;
return { slot, true };
Traits::assign(slot, key);
return { &slot, true };
}
if (slot == key) [[likely]]
if (Traits::equals(slot, key)) [[likely]]
{
return { slot, false };
return { &slot, false };
}
}
}
@@ -166,17 +168,17 @@ namespace til
// This mirrors the insert() function, but without the lookup part.
for (auto& oldSlot : container())
{
if (!oldSlot)
if (!Traits::occupied(oldSlot))
{
continue;
}
const auto hash = ::std::hash<T>{}(oldSlot) >> newShift;
const auto hash = Traits::hash(oldSlot) >> newShift;
for (auto i = hash;; ++i)
{
auto& slot = newMap[i & newMask];
if (!slot)
if (!Traits::occupied(slot))
{
slot = std::move_if_noexcept(oldSlot);
break;

View File

@@ -10,21 +10,28 @@ namespace til
inline constexpr wchar_t UNICODE_REPLACEMENT = 0xFFFD;
}
static constexpr bool is_surrogate(const wchar_t wch) noexcept
constexpr bool is_surrogate(const auto wch) noexcept
{
return (wch & 0xF800) == 0xD800;
}
static constexpr bool is_leading_surrogate(const wchar_t wch) noexcept
constexpr bool is_leading_surrogate(const auto wch) noexcept
{
return (wch & 0xFC00) == 0xD800;
}
static constexpr bool is_trailing_surrogate(const wchar_t wch) noexcept
constexpr bool is_trailing_surrogate(const auto wch) noexcept
{
return (wch & 0xFC00) == 0xDC00;
}
constexpr char32_t combine_surrogates(const auto lead, const auto trail)
{
// Ah, I love these bracketed C-style casts. I use them in C all the time. Yep.
#pragma warning(suppress : 26493) // Don't use C-style casts (type.4).
return (char32_t{ lead } << 10) - 0x35FDC00 + char32_t{ trail };
}
// Verifies the beginning of the given UTF16 string and returns the first UTF16 sequence
// or U+FFFD otherwise. It's not really useful and at the time of writing only a
// single caller uses this. It's best to delete this if you read this comment.

View File

@@ -61,7 +61,10 @@ const RegistrySerialization::_RegPropertyMap RegistrySerialization::s_PropertyMa
{ _RegPropertyType::Boolean, CONSOLE_REGISTRY_INTERCEPTCOPYPASTE, SET_FIELD_AND_SIZE(_fInterceptCopyPaste) },
{ _RegPropertyType::Boolean, CONSOLE_REGISTRY_TERMINALSCROLLING, SET_FIELD_AND_SIZE(_TerminalScrolling) },
{ _RegPropertyType::Boolean, CONSOLE_REGISTRY_USEDX, SET_FIELD_AND_SIZE(_fUseDx) },
{ _RegPropertyType::Boolean, CONSOLE_REGISTRY_COPYCOLOR, SET_FIELD_AND_SIZE(_fCopyColor) }
{ _RegPropertyType::Boolean, CONSOLE_REGISTRY_COPYCOLOR, SET_FIELD_AND_SIZE(_fCopyColor) },
#if TIL_FEATURE_CONHOSTATLASENGINE_ENABLED
{ _RegPropertyType::Boolean, L"EnableBuiltinGlyphs", SET_FIELD_AND_SIZE(_fEnableBuiltinGlyphs) },
#endif
// Special cases that are handled manually in Registry::LoadFromRegistry:
// - CONSOLE_REGISTRY_WINDOWPOS

View File

@@ -295,8 +295,8 @@ CATCH_RETURN()
/* fontStyle */ DWRITE_FONT_STYLE_NORMAL,
/* fontStretch */ DWRITE_FONT_STRETCH_NORMAL,
/* fontSize */ _api.s->font->fontSize,
/* localeName */ L"",
/* textFormat */ textFormat.put()));
/* localeName */ _p.userLocaleName.c_str(),
/* textFormat */ textFormat.addressof()));
wil::com_ptr<IDWriteTextLayout> textLayout;
RETURN_IF_FAILED(_p.dwriteFactory->CreateTextLayout(glyph.data(), gsl::narrow_cast<uint32_t>(glyph.size()), textFormat.get(), FLT_MAX, FLT_MAX, textLayout.addressof()));
@@ -774,5 +774,7 @@ void AtlasEngine::_resolveFontMetrics(const wchar_t* requestedFaceName, const Fo
fontMetrics->doubleUnderline[0] = { doubleUnderlinePosTopU16, thinLineWidthU16 };
fontMetrics->doubleUnderline[1] = { doubleUnderlinePosBottomU16, thinLineWidthU16 };
fontMetrics->overline = { 0, underlineWidthU16 };
fontMetrics->builtinGlyphs = fontInfoDesired.GetEnableBuiltinGlyphs();
}
}

View File

@@ -4,7 +4,10 @@
#include "pch.h"
#include "AtlasEngine.h"
#include <til/unicode.h>
#include "Backend.h"
#include "BuiltinGlyphs.h"
#include "DWriteTextAnalysis.h"
#include "../../interactivity/win32/CustomWindowMessages.h"
@@ -20,6 +23,7 @@
#pragma warning(disable : 26446) // Prefer to use gsl::at() instead of unchecked subscript operator (bounds.4).
#pragma warning(disable : 26459) // You called an STL function '...' with a raw pointer parameter at position '...' that may be unsafe [...].
#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
#pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1).
#pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2).
using namespace Microsoft::Console::Render::Atlas;
@@ -70,8 +74,9 @@ try
_handleSettingsUpdate();
}
if constexpr (ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION)
if (ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION || _hackTriggerRedrawAll)
{
_hackTriggerRedrawAll = false;
_api.invalidatedRows = invalidatedRowsAll;
_api.scrollOffset = 0;
}
@@ -575,7 +580,7 @@ void AtlasEngine::_recreateFontDependentResources()
memcpy(&localeName[0], L"en-US", 12);
}
_api.userLocaleName = std::wstring{ &localeName[0] };
_p.userLocaleName = std::wstring{ &localeName[0] };
}
if (_p.s->font->fontAxisValues.empty())
@@ -606,6 +611,8 @@ void AtlasEngine::_recreateFontDependentResources()
_api.textFormatAxes[i] = { fontAxisValues.data(), fontAxisValues.size() };
}
}
_hackWantsBuiltinGlyphs = _p.s->font->builtinGlyphs && !_hackIsBackendD2D;
}
void AtlasEngine::_recreateCellCountDependentResources()
@@ -666,14 +673,65 @@ void AtlasEngine::_flushBufferLine()
// This would seriously blow us up otherwise.
Expects(_api.bufferLineColumn.size() == _api.bufferLine.size() + 1);
const auto beg = _api.bufferLine.data();
const auto len = _api.bufferLine.size();
size_t segmentBeg = 0;
size_t segmentEnd = 0;
bool custom = false;
if (!_hackWantsBuiltinGlyphs)
{
_mapRegularText(0, len);
return;
}
while (segmentBeg < len)
{
segmentEnd = segmentBeg;
do
{
auto i = segmentEnd;
char32_t codepoint = beg[i++];
if (til::is_leading_surrogate(codepoint) && i < len)
{
codepoint = til::combine_surrogates(codepoint, beg[i++]);
}
const auto c = BuiltinGlyphs::IsBuiltinGlyph(codepoint) || BuiltinGlyphs::IsSoftFontChar(codepoint);
if (custom != c)
{
break;
}
segmentEnd = i;
} while (segmentEnd < len);
if (segmentBeg != segmentEnd)
{
if (custom)
{
_mapBuiltinGlyphs(segmentBeg, segmentEnd);
}
else
{
_mapRegularText(segmentBeg, segmentEnd);
}
}
segmentBeg = segmentEnd;
custom = !custom;
}
}
void AtlasEngine::_mapRegularText(size_t offBeg, size_t offEnd)
{
auto& row = *_p.rows[_api.lastPaintBufferLineCoord.y];
#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)
for (u32 idx = gsl::narrow_cast<u32>(offBeg), mappedEnd = 0; idx < offEnd; idx = mappedEnd)
{
u32 mappedLength = 0;
wil::com_ptr<IDWriteFontFace2> mappedFontFace;
_mapCharacters(_api.bufferLine.data() + idx, gsl::narrow_cast<u32>(_api.bufferLine.size()) - idx, &mappedLength, mappedFontFace.addressof());
_mapCharacters(_api.bufferLine.data() + idx, gsl::narrow_cast<u32>(offEnd - idx), &mappedLength, mappedFontFace.addressof());
mappedEnd = idx + mappedLength;
if (!mappedFontFace)
@@ -697,7 +755,7 @@ void AtlasEngine::_flushBufferLine()
_api.glyphProps = Buffer<DWRITE_SHAPING_GLYPH_PROPERTIES>{ size };
}
if (_api.s->font->fontFeatures.empty())
if (_p.s->font->fontFeatures.empty())
{
// We can reuse idx here, as it'll be reset to "idx = mappedEnd" in the outer loop anyways.
for (u32 complexityLength = 0; idx < mappedEnd; idx += complexityLength)
@@ -712,10 +770,10 @@ void AtlasEngine::_flushBufferLine()
for (size_t i = 0; i < complexityLength; ++i)
{
const size_t col1 = _api.bufferLineColumn[idx + i + 0];
const size_t col2 = _api.bufferLineColumn[idx + i + 1];
const auto col1 = _api.bufferLineColumn[idx + i + 0];
const auto col2 = _api.bufferLineColumn[idx + i + 1];
const auto glyphAdvance = (col2 - col1) * _p.s->font->cellSize.x;
const auto fg = colors[col1 << shift];
const auto fg = colors[static_cast<size_t>(col1) << shift];
row.glyphIndices.emplace_back(_api.glyphIndices[i]);
row.glyphAdvances.emplace_back(static_cast<f32>(glyphAdvance));
row.glyphOffsets.emplace_back();
@@ -750,9 +808,31 @@ void AtlasEngine::_flushBufferLine()
}
}
void AtlasEngine::_mapBuiltinGlyphs(size_t offBeg, size_t offEnd)
{
auto& row = *_p.rows[_api.lastPaintBufferLineCoord.y];
auto initialIndicesCount = row.glyphIndices.size();
const auto shift = gsl::narrow_cast<u8>(row.lineRendition != LineRendition::SingleWidth);
const auto colors = _p.foregroundBitmap.begin() + _p.colorBitmapRowStride * _api.lastPaintBufferLineCoord.y;
const auto base = reinterpret_cast<const u16*>(_api.bufferLine.data());
const auto len = offEnd - offBeg;
row.glyphIndices.insert(row.glyphIndices.end(), base + offBeg, base + offEnd);
row.glyphAdvances.insert(row.glyphAdvances.end(), len, static_cast<f32>(_p.s->font->cellSize.x));
row.glyphOffsets.insert(row.glyphOffsets.end(), len, {});
for (size_t i = offBeg; i < offEnd; ++i)
{
const auto col = _api.bufferLineColumn[i];
row.colors.emplace_back(colors[static_cast<size_t>(col) << shift]);
}
row.mappings.emplace_back(nullptr, gsl::narrow_cast<u32>(initialIndicesCount), gsl::narrow_cast<u32>(row.glyphIndices.size()));
}
void AtlasEngine::_mapCharacters(const wchar_t* text, const u32 textLength, u32* mappedLength, IDWriteFontFace2** mappedFontFace) const
{
TextAnalysisSource analysisSource{ _api.userLocaleName.c_str(), text, textLength };
TextAnalysisSource analysisSource{ _p.userLocaleName.c_str(), text, textLength };
const auto& textFormatAxis = _api.textFormatAxes[static_cast<size_t>(_api.attributes)];
// We don't read from scale anyways.
@@ -807,7 +887,7 @@ void AtlasEngine::_mapComplex(IDWriteFontFace2* mappedFontFace, u32 idx, u32 len
{
_api.analysisResults.clear();
TextAnalysisSource analysisSource{ _api.userLocaleName.c_str(), _api.bufferLine.data(), gsl::narrow<UINT32>(_api.bufferLine.size()) };
TextAnalysisSource analysisSource{ _p.userLocaleName.c_str(), _api.bufferLine.data(), gsl::narrow<UINT32>(_api.bufferLine.size()) };
TextAnalysisSink analysisSink{ _api.analysisResults };
THROW_IF_FAILED(_p.textAnalyzer->AnalyzeScript(&analysisSource, idx, length, &analysisSink));
@@ -852,7 +932,7 @@ void AtlasEngine::_mapComplex(IDWriteFontFace2* mappedFontFace, u32 idx, u32 len
/* isSideways */ false,
/* isRightToLeft */ 0,
/* scriptAnalysis */ &a.analysis,
/* localeName */ _api.userLocaleName.c_str(),
/* localeName */ _p.userLocaleName.c_str(),
/* numberSubstitution */ nullptr,
/* features */ &features,
/* featureRangeLengths */ &featureRangeLengths,
@@ -904,7 +984,7 @@ void AtlasEngine::_mapComplex(IDWriteFontFace2* mappedFontFace, u32 idx, u32 len
/* isSideways */ false,
/* isRightToLeft */ 0,
/* scriptAnalysis */ &a.analysis,
/* localeName */ _api.userLocaleName.c_str(),
/* localeName */ _p.userLocaleName.c_str(),
/* features */ &features,
/* featureRangeLengths */ &featureRangeLengths,
/* featureRanges */ featureRanges,
@@ -979,53 +1059,31 @@ void AtlasEngine::_mapReplacementCharacter(u32 from, u32 to, ShapedRow& row)
return;
}
auto pos1 = from;
auto pos2 = pos1;
size_t col1 = _api.bufferLineColumn[from];
size_t col2 = col1;
auto pos = from;
auto col1 = _api.bufferLineColumn[from];
auto initialIndicesCount = row.glyphIndices.size();
const auto softFontAvailable = !_p.s->font->softFontPattern.empty();
auto currentlyMappingSoftFont = isSoftFontChar(_api.bufferLine[pos1]);
const auto shift = gsl::narrow_cast<u8>(row.lineRendition != LineRendition::SingleWidth);
const auto colors = _p.foregroundBitmap.begin() + _p.colorBitmapRowStride * _api.lastPaintBufferLineCoord.y;
while (pos2 < to)
while (pos < to)
{
col2 = _api.bufferLineColumn[++pos2];
const auto col2 = _api.bufferLineColumn[++pos];
if (col1 == col2)
{
continue;
}
const auto cols = col2 - col1;
const auto ch = static_cast<u16>(_api.bufferLine[pos1]);
const auto nowMappingSoftFont = isSoftFontChar(ch);
row.glyphIndices.emplace_back(nowMappingSoftFont ? ch : _api.replacementCharacterGlyphIndex);
row.glyphAdvances.emplace_back(static_cast<f32>(cols * _p.s->font->cellSize.x));
row.glyphIndices.emplace_back(_api.replacementCharacterGlyphIndex);
row.glyphAdvances.emplace_back(static_cast<f32>((col2 - col1) * _p.s->font->cellSize.x));
row.glyphOffsets.emplace_back();
row.colors.emplace_back(colors[col1 << shift]);
row.colors.emplace_back(colors[static_cast<size_t>(col1) << shift]);
if (currentlyMappingSoftFont != nowMappingSoftFont)
{
const auto indicesCount = row.glyphIndices.size();
const auto fontFace = currentlyMappingSoftFont && softFontAvailable ? nullptr : _api.replacementCharacterFontFace.get();
if (indicesCount > initialIndicesCount)
{
row.mappings.emplace_back(fontFace, gsl::narrow_cast<u32>(initialIndicesCount), gsl::narrow_cast<u32>(indicesCount));
initialIndicesCount = indicesCount;
}
}
pos1 = pos2;
col1 = col2;
currentlyMappingSoftFont = nowMappingSoftFont;
}
{
const auto indicesCount = row.glyphIndices.size();
const auto fontFace = currentlyMappingSoftFont && softFontAvailable ? nullptr : _api.replacementCharacterFontFace.get();
const auto fontFace = _api.replacementCharacterFontFace.get();
if (indicesCount > initialIndicesCount)
{

View File

@@ -86,6 +86,8 @@ namespace Microsoft::Console::Render::Atlas
void _recreateFontDependentResources();
void _recreateCellCountDependentResources();
void _flushBufferLine();
void _mapRegularText(size_t offBeg, size_t offEnd);
void _mapBuiltinGlyphs(size_t offBeg, size_t offEnd);
void _mapCharacters(const wchar_t* text, u32 textLength, u32* mappedLength, IDWriteFontFace2** mappedFontFace) const;
void _mapComplex(IDWriteFontFace2* mappedFontFace, u32 idx, u32 length, ShapedRow& row);
ATLAS_ATTR_COLD void _mapReplacementCharacter(u32 from, u32 to, ShapedRow& row);
@@ -117,6 +119,18 @@ namespace Microsoft::Console::Render::Atlas
std::unique_ptr<IBackend> _b;
RenderingPayload _p;
// _p.s->font->builtinGlyphs is the setting which decides whether we should map box drawing glyphs to
// our own builtin versions. There's just one problem: BackendD2D doesn't have this functionality.
// But since AtlasEngine shapes the text before it's handed to the backends, it would need to know
// whether BackendD2D is in use, before BackendD2D even exists. These two flags solve the issue
// by triggering a complete, immediate redraw whenever the backend type changes.
//
// The proper solution is to move text shaping into the backends.
// Someone just needs to write a generic "TextBuffer to DWRITE_GLYPH_RUN" function.
bool _hackIsBackendD2D = false;
bool _hackWantsBuiltinGlyphs = true;
bool _hackTriggerRedrawAll = false;
struct ApiState
{
GenerationalSettings s = DirtyGenerationalSettings();
@@ -133,8 +147,6 @@ namespace Microsoft::Console::Render::Atlas
std::vector<wchar_t> bufferLine;
std::vector<u16> bufferLineColumn;
std::wstring userLocaleName;
std::array<Buffer<DWRITE_FONT_AXIS_VALUE>, 4> textFormatAxes;
std::vector<TextAnalysisSinkResult> analysisResults;
Buffer<u16> clusterMap;

View File

@@ -32,7 +32,7 @@ using namespace Microsoft::Console::Render::Atlas;
[[nodiscard]] HRESULT AtlasEngine::Present() noexcept
try
{
if (!_p.dxgi.adapter || !_p.dxgi.factory->IsCurrent())
if (!_p.dxgi.adapter)
{
_recreateAdapter();
}
@@ -77,7 +77,7 @@ CATCH_RETURN()
[[nodiscard]] bool AtlasEngine::RequiresContinuousRedraw() noexcept
{
return ATLAS_DEBUG_CONTINUOUS_REDRAW || (_b && _b->RequiresContinuousRedraw());
return ATLAS_DEBUG_CONTINUOUS_REDRAW || (_b && _b->RequiresContinuousRedraw()) || _hackTriggerRedrawAll;
}
void AtlasEngine::WaitUntilCanRender() noexcept
@@ -276,12 +276,14 @@ void AtlasEngine::_recreateBackend()
_b = std::make_unique<BackendD3D>(_p);
}
// !!! NOTE !!!
// Normally the viewport is indirectly marked as dirty by `AtlasEngine::_handleSettingsUpdate()` whenever
// the settings change, but the `!_p.dxgi.factory->IsCurrent()` check is not part of the settings change
// flow and so we have to manually recreate how AtlasEngine.cpp marks viewports as dirty here.
// This ensures that the backends redraw their entire viewports whenever a new swap chain is created.
// This ensures that the backends redraw their entire viewports whenever a new swap chain is created,
// EVEN IF we got called when no actual settings changed (i.e. rendering failure, etc.).
_p.MarkAllAsDirty();
const auto hackWantsBuiltinGlyphs = _p.s->font->builtinGlyphs && !d2dMode;
_hackTriggerRedrawAll = _hackWantsBuiltinGlyphs != hackWantsBuiltinGlyphs;
_hackIsBackendD2D = d2dMode;
_hackWantsBuiltinGlyphs = hackWantsBuiltinGlyphs;
}
void AtlasEngine::_handleSwapChainUpdate()
@@ -319,13 +321,15 @@ void AtlasEngine::_createSwapChain()
// 3 buffers seems to guarantee a stable framerate at display frequency at all times.
.BufferCount = 3,
.Scaling = DXGI_SCALING_NONE,
// DXGI_SWAP_EFFECT_FLIP_DISCARD is a mode that was created at a time were display drivers
// lacked support for Multiplane Overlays (MPO) and were copying buffers was expensive.
// DXGI_SWAP_EFFECT_FLIP_DISCARD is the easiest to use, because it's fast and uses little memory.
// But it's a mode that was created at a time were display drivers lacked support
// for Multiplane Overlays (MPO) and were copying buffers was expensive.
// This allowed DWM to quickly draw overlays (like gamebars) on top of rendered content.
// With faster GPU memory in general and with support for MPO in particular this isn't
// really an advantage anymore. Instead DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL allows for a
// more "intelligent" composition and display updates to occur like Panel Self Refresh
// (PSR) which requires dirty rectangles (Present1 API) to work correctly.
// We were asked by DWM folks to use DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL for this reason (PSR).
.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL,
// If our background is opaque we can enable "independent" flips by setting DXGI_ALPHA_MODE_IGNORE.
// As our swap chain won't have to compose with DWM anymore it reduces the display latency dramatically.

View File

@@ -74,11 +74,6 @@ namespace Microsoft::Console::Render::Atlas
return val < min ? min : (max < val ? max : val);
}
constexpr bool isSoftFontChar(wchar_t ch) noexcept
{
return ch >= 0xEF20 && ch < 0xEF80;
}
inline constexpr D2D1_RECT_F GlyphRunEmptyBounds{ 1e38f, 1e38f, -1e38f, -1e38f };
void GlyphRunAccumulateBounds(const ID2D1DeviceContext* d2dRenderTarget, D2D1_POINT_2F baselineOrigin, const DWRITE_GLYPH_RUN* glyphRun, D2D1_RECT_F& bounds);

View File

@@ -4,11 +4,14 @@
#include "pch.h"
#include "BackendD3D.h"
#include <til/unicode.h>
#include <custom_shader_ps.h>
#include <custom_shader_vs.h>
#include <shader_ps.h>
#include <shader_vs.h>
#include "BuiltinGlyphs.h"
#include "dwrite.h"
#include "../../types/inc/ColorFix.hpp"
@@ -42,45 +45,8 @@ TIL_FAST_MATH_BEGIN
using namespace Microsoft::Console::Render::Atlas;
template<>
struct std::hash<u16>
{
constexpr size_t operator()(u16 key) const noexcept
{
return til::flat_set_hash_integer(key);
}
};
template<>
struct std::hash<BackendD3D::AtlasGlyphEntry>
{
constexpr size_t operator()(u16 key) const noexcept
{
return til::flat_set_hash_integer(key);
}
constexpr size_t operator()(const BackendD3D::AtlasGlyphEntry& slot) const noexcept
{
return til::flat_set_hash_integer(slot.glyphIndex);
}
};
template<>
struct std::hash<BackendD3D::AtlasFontFaceEntry>
{
using T = BackendD3D::AtlasFontFaceEntry;
size_t operator()(const BackendD3D::AtlasFontFaceKey& key) const noexcept
{
return til::flat_set_hash_integer(std::bit_cast<uintptr_t>(key.fontFace) | static_cast<u8>(key.lineRendition));
}
size_t operator()(const BackendD3D::AtlasFontFaceEntry& slot) const noexcept
{
const auto& inner = *slot.inner;
return til::flat_set_hash_integer(std::bit_cast<uintptr_t>(inner.fontFace.get()) | static_cast<u8>(inner.lineRendition));
}
};
static constexpr D2D1_MATRIX_3X2_F identityTransform{ .m11 = 1, .m22 = 1 };
static constexpr D2D1_COLOR_F whiteColor{ 1, 1, 1, 1 };
BackendD3D::BackendD3D(const RenderingPayload& p)
{
@@ -329,7 +295,11 @@ void BackendD3D::_updateFontDependents(const RenderingPayload& p)
_fontChangedResetGlyphAtlas = true;
_textShadingType = font.antialiasingMode == AntialiasingMode::ClearType ? ShadingType::TextClearType : ShadingType::TextGrayscale;
// _ligatureOverhangTriggerLeft/Right are essentially thresholds for a glyph's width at
// which point we consider it wider than allowed and "this looks like a coding ligature".
// See _drawTextOverlapSplit for more information about what this does.
{
// No ligatures -> No thresholds.
auto ligaturesDisabled = false;
for (const auto& feature : font.fontFeatures)
{
@@ -752,11 +722,15 @@ void BackendD3D::_resetGlyphAtlas(const RenderingPayload& p)
// It's not great, but it's not terrible.
for (auto& slot : _glyphAtlasMap.container())
{
if (slot.inner)
for (auto& glyphs : slot.glyphs)
{
slot.inner->glyphs.clear();
glyphs.clear();
}
}
for (auto& glyphs : _builtinGlyphs.glyphs)
{
glyphs.clear();
}
_d2dBeginDrawing();
_d2dRenderTarget->Clear();
@@ -797,9 +771,6 @@ void BackendD3D::_resizeGlyphAtlas(const RenderingPayload& p, const u16 u, const
_d2dRenderTarget.try_query_to(_d2dRenderTarget4.addressof());
_d2dRenderTarget->SetUnitMode(D2D1_UNIT_MODE_PIXELS);
// We don't really use D2D for anything except DWrite, but it
// can't hurt to ensure that everything it does is pixel aligned.
_d2dRenderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
// Ensure that D2D uses the exact same gamma as our shader uses.
_d2dRenderTarget->SetTextRenderingParams(_textRenderingParams.get());
@@ -821,9 +792,8 @@ void BackendD3D::_resizeGlyphAtlas(const RenderingPayload& p, const u16 u, const
}
{
static constexpr D2D1_COLOR_F color{ 1, 1, 1, 1 };
THROW_IF_FAILED(_d2dRenderTarget->CreateSolidColorBrush(&color, nullptr, _emojiBrush.put()));
THROW_IF_FAILED(_d2dRenderTarget->CreateSolidColorBrush(&color, nullptr, _brush.put()));
THROW_IF_FAILED(_d2dRenderTarget->CreateSolidColorBrush(&whiteColor, nullptr, _emojiBrush.put()));
THROW_IF_FAILED(_d2dRenderTarget->CreateSolidColorBrush(&whiteColor, nullptr, _brush.put()));
}
ID3D11ShaderResourceView* resources[]{ _backgroundBitmapView.get(), _glyphAtlasView.get() };
@@ -955,7 +925,7 @@ void BackendD3D::_drawBackground(const RenderingPayload& p)
}
_appendQuad() = {
.shadingType = ShadingType::Background,
.shadingType = static_cast<u16>(ShadingType::Background),
.size = p.s->targetSize,
};
}
@@ -1013,65 +983,65 @@ void BackendD3D::_drawText(RenderingPayload& p)
for (const auto& m : row->mappings)
{
auto x = m.glyphsFrom;
const AtlasFontFaceKey fontFaceKey{
.fontFace = m.fontFace.get(),
.lineRendition = row->lineRendition,
};
const auto glyphsTo = m.glyphsTo;
const auto fontFace = m.fontFace.get();
// This goto label exists to allow us to retry rendering a glyph if the glyph atlas was full.
// We need to goto here, because a retry will cause the atlas texture as well as the
// _glyphCache hashmap to be cleared, and so we'll have to call insert() again.
drawGlyphRetry:
const auto [fontFaceEntryOuter, fontFaceInserted] = _glyphAtlasMap.insert(fontFaceKey);
auto& fontFaceEntry = *fontFaceEntryOuter.inner;
if (fontFaceInserted)
// The lack of a fontFace indicates a soft font.
AtlasFontFaceEntry* fontFaceEntry = &_builtinGlyphs;
if (fontFace) [[likely]]
{
_initializeFontFaceEntry(fontFaceEntry);
fontFaceEntry = _glyphAtlasMap.insert(fontFace).first;
}
while (x < m.glyphsTo)
{
const auto [glyphEntry, inserted] = fontFaceEntry.glyphs.insert(row->glyphIndices[x]);
const auto& glyphs = fontFaceEntry->glyphs[WI_EnumValue(row->lineRendition)];
if (inserted && !_drawGlyph(p, fontFaceEntry, glyphEntry))
while (x < glyphsTo)
{
size_t dx = 1;
u32 glyphIndex = row->glyphIndices[x];
// Note: !fontFace is only nullptr for builtin glyphs which then use glyphIndices for UTF16 code points.
// In other words, this doesn't accidentally corrupt any actual glyph indices.
if (!fontFace && til::is_leading_surrogate(glyphIndex))
{
// A deadlock in this retry loop is detected in _drawGlyphPrepareRetry.
//
// Yes, I agree, avoid goto. Sometimes. It's not my fault that C++ still doesn't
// have a `continue outerloop;` like other languages had it for decades. :(
#pragma warning(suppress : 26438) // Avoid 'goto' (es.76).
#pragma warning(suppress : 26448) // Consider using gsl::finally if final action is intended (gsl.util).
goto drawGlyphRetry;
glyphIndex = til::combine_surrogates(glyphIndex, row->glyphIndices[x + 1]);
dx = 2;
}
if (glyphEntry.data.shadingType != ShadingType::Default)
auto glyphEntry = glyphs.lookup(glyphIndex);
if (!glyphEntry)
{
glyphEntry = _drawGlyph(p, *row, *fontFaceEntry, glyphIndex);
}
// A shadingType of 0 (ShadingType::Default) indicates a glyph that is whitespace.
if (glyphEntry->shadingType != ShadingType::Default)
{
auto l = static_cast<til::CoordType>(lrintf((baselineX + row->glyphOffsets[x].advanceOffset) * scaleX));
auto t = static_cast<til::CoordType>(lrintf((baselineY - row->glyphOffsets[x].ascenderOffset) * scaleY));
l += glyphEntry.data.offset.x;
t += glyphEntry.data.offset.y;
l += glyphEntry->offset.x;
t += glyphEntry->offset.y;
row->dirtyTop = std::min(row->dirtyTop, t);
row->dirtyBottom = std::max(row->dirtyBottom, t + glyphEntry.data.size.y);
row->dirtyBottom = std::max(row->dirtyBottom, t + glyphEntry->size.y);
_appendQuad() = {
.shadingType = glyphEntry.data.shadingType,
.shadingType = static_cast<u16>(glyphEntry->shadingType),
.position = { static_cast<i16>(l), static_cast<i16>(t) },
.size = glyphEntry.data.size,
.texcoord = glyphEntry.data.texcoord,
.size = glyphEntry->size,
.texcoord = glyphEntry->texcoord,
.color = row->colors[x],
};
if (glyphEntry.data.overlapSplit)
if (glyphEntry->overlapSplit)
{
_drawTextOverlapSplit(p, y);
}
}
baselineX += row->glyphAdvances[x];
++x;
x += dx;
}
}
@@ -1189,71 +1159,22 @@ void BackendD3D::_drawTextOverlapSplit(const RenderingPayload& p, u16 y)
}
}
void BackendD3D::_initializeFontFaceEntry(AtlasFontFaceEntryInner& fontFaceEntry)
BackendD3D::AtlasGlyphEntry* BackendD3D::_drawGlyph(const RenderingPayload& p, const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, u32 glyphIndex)
{
// The lack of a fontFace indicates a soft font.
if (!fontFaceEntry.fontFace)
{
return;
}
ALLOW_UNINITIALIZED_BEGIN
std::array<u32, 0x100> codepoints;
std::array<u16, 0x100> indices;
ALLOW_UNINITIALIZED_END
for (u32 i = 0; i < codepoints.size(); ++i)
{
codepoints[i] = 0x2500 + i;
}
THROW_IF_FAILED(fontFaceEntry.fontFace->GetGlyphIndicesW(codepoints.data(), codepoints.size(), indices.data()));
for (u32 i = 0; i < indices.size(); ++i)
{
if (const auto idx = indices[i])
{
fontFaceEntry.boxGlyphs.insert(idx);
}
}
}
bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry)
{
if (!fontFaceEntry.fontFace)
{
return _drawSoftFontGlyph(p, fontFaceEntry, glyphEntry);
return _drawBuiltinGlyph(p, row, fontFaceEntry, glyphIndex);
}
const auto glyphIndexU16 = static_cast<u16>(glyphIndex);
const DWRITE_GLYPH_RUN glyphRun{
.fontFace = fontFaceEntry.fontFace.get(),
.fontEmSize = p.s->font->fontSize,
.glyphCount = 1,
.glyphIndices = &glyphEntry.glyphIndex,
.glyphIndices = &glyphIndexU16,
};
// To debug issues with this function it may be helpful to know which file
// a given font face corresponds to. This code works for most cases.
#if 0
wchar_t filePath[MAX_PATH]{};
{
UINT32 fileCount = 1;
wil::com_ptr<IDWriteFontFile> file;
if (SUCCEEDED(fontFaceEntry.fontFace->GetFiles(&fileCount, file.addressof())))
{
wil::com_ptr<IDWriteFontFileLoader> loader;
THROW_IF_FAILED(file->GetLoader(loader.addressof()));
if (const auto localLoader = loader.try_query<IDWriteLocalFontFileLoader>())
{
void const* fontFileReferenceKey;
UINT32 fontFileReferenceKeySize;
THROW_IF_FAILED(file->GetReferenceKey(&fontFileReferenceKey, &fontFileReferenceKeySize));
THROW_IF_FAILED(localLoader->GetFilePathFromKey(fontFileReferenceKey, fontFileReferenceKeySize, &filePath[0], MAX_PATH));
}
}
}
#endif
// It took me a while to figure out how to rasterize glyphs manually with DirectWrite without depending on Direct2D.
// The benefits are a reduction in memory usage, an increase in performance under certain circumstances and most
// importantly, the ability to debug the renderer more easily, because many graphics debuggers don't support Direct2D.
@@ -1322,16 +1243,13 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryI
// The buffer now contains a grayscale alpha mask.
#endif
const auto lineRendition = static_cast<LineRendition>(fontFaceEntry.lineRendition);
const auto needsTransform = lineRendition != LineRendition::SingleWidth;
static constexpr D2D1_MATRIX_3X2_F identityTransform{ .m11 = 1, .m22 = 1 };
const int scale = row.lineRendition != LineRendition::SingleWidth;
D2D1_MATRIX_3X2_F transform = identityTransform;
if (needsTransform)
if (scale)
{
transform.m11 = 2.0f;
transform.m22 = lineRendition >= LineRendition::DoubleHeightTop ? 2.0f : 1.0f;
transform.m22 = row.lineRendition >= LineRendition::DoubleHeightTop ? 2.0f : 1.0f;
_d2dRenderTarget->SetTransform(&transform);
}
@@ -1387,26 +1305,10 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryI
}
}
// Overhangs for box glyphs can produce unsightly effects, where the antialiased edges of horizontal
// and vertical lines overlap between neighboring glyphs and produce "boldened" intersections.
// It looks a little something like this:
// ---+---+---
// This avoids the issue in most cases by simply clipping the glyph to the size of a single cell.
// The downside is that it fails to work well for custom line heights, etc.
const auto isBoxGlyph = fontFaceEntry.boxGlyphs.lookup(glyphEntry.glyphIndex) != nullptr;
if (isBoxGlyph)
{
// NOTE: As mentioned above, the "origin" of a glyph's coordinate system is its baseline.
bounds.left = std::max(bounds.left, 0.0f);
bounds.top = std::max(bounds.top, static_cast<f32>(-p.s->font->baseline) * transform.m22);
bounds.right = std::min(bounds.right, static_cast<f32>(p.s->font->cellSize.x) * transform.m11);
bounds.bottom = std::min(bounds.bottom, static_cast<f32>(p.s->font->descender) * transform.m22);
}
// The bounds may be empty if the glyph is whitespace.
if (bounds.left >= bounds.right || bounds.top >= bounds.bottom)
{
return true;
return _drawGlyphAllocateEntry(row, fontFaceEntry, glyphIndex);
}
const auto bl = lrintf(bounds.left);
@@ -1418,37 +1320,15 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryI
.w = br - bl,
.h = bb - bt,
};
if (!stbrp_pack_rects(&_rectPacker, &rect, 1))
{
_drawGlyphPrepareRetry(p);
return false;
}
_drawGlyphAtlasAllocate(p, rect);
_d2dBeginDrawing();
const D2D1_POINT_2F baselineOrigin{
static_cast<f32>(rect.x - bl),
static_cast<f32>(rect.y - bt),
};
_d2dBeginDrawing();
if (isBoxGlyph)
{
const D2D1_RECT_F clipRect{
static_cast<f32>(rect.x) / transform.m11,
static_cast<f32>(rect.y) / transform.m22,
static_cast<f32>(rect.x + rect.w) / transform.m11,
static_cast<f32>(rect.y + rect.h) / transform.m22,
};
_d2dRenderTarget->PushAxisAlignedClip(&clipRect, D2D1_ANTIALIAS_MODE_ALIASED);
}
const auto boxGlyphCleanup = wil::scope_exit([&]() {
if (isBoxGlyph)
{
_d2dRenderTarget->PopAxisAlignedClip();
}
});
if (needsTransform)
if (scale)
{
transform.dx = (1.0f - transform.m11) * baselineOrigin.x;
transform.dy = (1.0f - transform.m22) * baselineOrigin.y;
@@ -1474,56 +1354,85 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryI
//
// The former condition makes sure to exclude diacritics and such from being considered a ligature,
// while the latter condition-pair makes sure to exclude regular BMP wide glyphs that overlap a little.
const auto horizontalScale = lineRendition != LineRendition::SingleWidth ? 2 : 1;
const auto triggerLeft = _ligatureOverhangTriggerLeft * horizontalScale;
const auto triggerRight = _ligatureOverhangTriggerRight * horizontalScale;
const auto triggerLeft = _ligatureOverhangTriggerLeft << scale;
const auto triggerRight = _ligatureOverhangTriggerRight << scale;
const auto overlapSplit = rect.w >= p.s->font->cellSize.x && (bl <= triggerLeft || br >= triggerRight);
glyphEntry.data.shadingType = isColorGlyph ? ShadingType::TextPassthrough : _textShadingType;
glyphEntry.data.overlapSplit = overlapSplit;
glyphEntry.data.offset.x = bl;
glyphEntry.data.offset.y = bt;
glyphEntry.data.size.x = rect.w;
glyphEntry.data.size.y = rect.h;
glyphEntry.data.texcoord.x = rect.x;
glyphEntry.data.texcoord.y = rect.y;
const auto glyphEntry = _drawGlyphAllocateEntry(row, fontFaceEntry, glyphIndex);
glyphEntry->shadingType = isColorGlyph ? ShadingType::TextPassthrough : _textShadingType;
glyphEntry->overlapSplit = overlapSplit;
glyphEntry->offset.x = bl;
glyphEntry->offset.y = bt;
glyphEntry->size.x = rect.w;
glyphEntry->size.y = rect.h;
glyphEntry->texcoord.x = rect.x;
glyphEntry->texcoord.y = rect.y;
if (lineRendition >= LineRendition::DoubleHeightTop)
if (row.lineRendition >= LineRendition::DoubleHeightTop)
{
_splitDoubleHeightGlyph(p, fontFaceEntry, glyphEntry);
_splitDoubleHeightGlyph(p, row, fontFaceEntry, glyphEntry);
}
return true;
return glyphEntry;
}
bool BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry)
BackendD3D::AtlasGlyphEntry* BackendD3D::_drawBuiltinGlyph(const RenderingPayload& p, const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, u32 glyphIndex)
{
auto baseline = p.s->font->baseline;
stbrp_rect rect{
.w = p.s->font->cellSize.x,
.h = p.s->font->cellSize.y,
};
const auto lineRendition = static_cast<LineRendition>(fontFaceEntry.lineRendition);
auto baseline = p.s->font->baseline;
if (lineRendition != LineRendition::SingleWidth)
if (row.lineRendition != LineRendition::SingleWidth)
{
const auto heightShift = static_cast<u8>(lineRendition >= LineRendition::DoubleHeightTop);
const auto heightShift = static_cast<u8>(row.lineRendition >= LineRendition::DoubleHeightTop);
rect.w <<= 1;
rect.h <<= heightShift;
baseline <<= heightShift;
}
if (!stbrp_pack_rects(&_rectPacker, &rect, 1))
_drawGlyphAtlasAllocate(p, rect);
_d2dBeginDrawing();
if (BuiltinGlyphs::IsSoftFontChar(glyphIndex))
{
_drawGlyphPrepareRetry(p);
return false;
_drawSoftFontGlyph(p, rect, glyphIndex);
}
else
{
const D2D1_RECT_F r{
static_cast<f32>(rect.x),
static_cast<f32>(rect.y),
static_cast<f32>(rect.x + rect.w),
static_cast<f32>(rect.y + rect.h),
};
BuiltinGlyphs::DrawBuiltinGlyph(p.d2dFactory.get(), _d2dRenderTarget.get(), _brush.get(), r, glyphIndex);
}
const auto glyphEntry = _drawGlyphAllocateEntry(row, fontFaceEntry, glyphIndex);
glyphEntry->shadingType = ShadingType::TextGrayscale;
glyphEntry->overlapSplit = 0;
glyphEntry->offset.x = 0;
glyphEntry->offset.y = -baseline;
glyphEntry->size.x = rect.w;
glyphEntry->size.y = rect.h;
glyphEntry->texcoord.x = rect.x;
glyphEntry->texcoord.y = rect.y;
if (row.lineRendition >= LineRendition::DoubleHeightTop)
{
_splitDoubleHeightGlyph(p, row, fontFaceEntry, glyphEntry);
}
return glyphEntry;
}
void BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const stbrp_rect& rect, u32 glyphIndex)
{
if (!_softFontBitmap)
{
// Allocating such a tiny texture is very wasteful (min. texture size on GPUs
// right now is 64kB), but this is a seldom used feature so it's fine...
// right now is 64kB), but this is a seldomly used feature so it's fine...
const D2D1_SIZE_U size{
static_cast<UINT32>(p.s->font->softFontCellSize.width),
static_cast<UINT32>(p.s->font->softFontCellSize.height),
@@ -1536,6 +1445,30 @@ bool BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFa
THROW_IF_FAILED(_d2dRenderTarget->CreateBitmap(size, nullptr, 0, &bitmapProperties, _softFontBitmap.addressof()));
}
{
const auto width = static_cast<size_t>(p.s->font->softFontCellSize.width);
const auto height = static_cast<size_t>(p.s->font->softFontCellSize.height);
auto bitmapData = Buffer<u32>{ width * height };
const auto softFontIndex = glyphIndex - 0xEF20u;
auto src = p.s->font->softFontPattern.begin() + height * softFontIndex;
auto dst = bitmapData.begin();
for (size_t y = 0; y < height; y++)
{
auto srcBits = *src++;
for (size_t x = 0; x < width; x++)
{
const auto srcBitIsSet = (srcBits & 0x8000) != 0;
*dst++ = srcBitIsSet ? 0xffffffff : 0x00000000;
srcBits <<= 1;
}
}
const auto pitch = static_cast<UINT32>(width * sizeof(u32));
THROW_IF_FAILED(_softFontBitmap->CopyFromMemory(nullptr, bitmapData.data(), pitch));
}
const auto interpolation = p.s->font->antialiasingMode == AntialiasingMode::Aliased ? D2D1_INTERPOLATION_MODE_NEAREST_NEIGHBOR : D2D1_INTERPOLATION_MODE_HIGH_QUALITY_CUBIC;
const D2D1_RECT_F dest{
static_cast<f32>(rect.x),
@@ -1545,118 +1478,67 @@ bool BackendD3D::_drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFa
};
_d2dBeginDrawing();
_drawSoftFontGlyphInBitmap(p, glyphEntry);
_d2dRenderTarget->DrawBitmap(_softFontBitmap.get(), &dest, 1, interpolation, nullptr, nullptr);
glyphEntry.data.shadingType = ShadingType::TextGrayscale;
glyphEntry.data.overlapSplit = 0;
glyphEntry.data.offset.x = 0;
glyphEntry.data.offset.y = -baseline;
glyphEntry.data.size.x = rect.w;
glyphEntry.data.size.y = rect.h;
glyphEntry.data.texcoord.x = rect.x;
glyphEntry.data.texcoord.y = rect.y;
if (lineRendition >= LineRendition::DoubleHeightTop)
{
_splitDoubleHeightGlyph(p, fontFaceEntry, glyphEntry);
}
return true;
}
void BackendD3D::_drawSoftFontGlyphInBitmap(const RenderingPayload& p, const AtlasGlyphEntry& glyphEntry) const
void BackendD3D::_drawGlyphAtlasAllocate(const RenderingPayload& p, stbrp_rect& rect)
{
if (!isSoftFontChar(glyphEntry.glyphIndex))
if (stbrp_pack_rects(&_rectPacker, &rect, 1))
{
// AtlasEngine::_mapReplacementCharacter should have filtered inputs with isSoftFontChar already.
assert(false);
return;
}
const auto width = static_cast<size_t>(p.s->font->softFontCellSize.width);
const auto height = static_cast<size_t>(p.s->font->softFontCellSize.height);
const auto& softFontPattern = p.s->font->softFontPattern;
// The isSoftFontChar() range is [0xEF20,0xEF80).
const auto offset = glyphEntry.glyphIndex - 0xEF20u;
const auto offsetBeg = offset * height;
const auto offsetEnd = offsetBeg + height;
if (offsetEnd > softFontPattern.size())
{
// Out of range values should not occur, but they do and it's unknown why: GH#15553
assert(false);
return;
}
Buffer<u32> bitmapData{ width * height };
auto dst = bitmapData.begin();
auto it = softFontPattern.begin() + offsetBeg;
const auto end = softFontPattern.begin() + offsetEnd;
while (it != end)
{
auto srcBits = *it++;
for (size_t x = 0; x < width; x++)
{
const auto srcBitIsSet = (srcBits & 0x8000) != 0;
*dst++ = srcBitIsSet ? 0xffffffff : 0x00000000;
srcBits <<= 1;
}
}
const auto pitch = static_cast<UINT32>(width * sizeof(u32));
THROW_IF_FAILED(_softFontBitmap->CopyFromMemory(nullptr, bitmapData.data(), pitch));
}
void BackendD3D::_drawGlyphPrepareRetry(const RenderingPayload& p)
{
THROW_HR_IF_MSG(E_UNEXPECTED, _glyphAtlasMap.empty(), "BackendD3D::_drawGlyph deadlock");
_d2dEndDrawing();
_flushQuads(p);
_resetGlyphAtlas(p);
if (!stbrp_pack_rects(&_rectPacker, &rect, 1))
{
THROW_HR(HRESULT_FROM_WIN32(ERROR_POSSIBLE_DEADLOCK));
}
}
BackendD3D::AtlasGlyphEntry* BackendD3D::_drawGlyphAllocateEntry(const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, u32 glyphIndex)
{
const auto glyphEntry = fontFaceEntry.glyphs[WI_EnumValue(row.lineRendition)].insert(glyphIndex).first;
glyphEntry->shadingType = ShadingType::Default;
return glyphEntry;
}
// If this is a double-height glyph (DECDHL), we need to split it into 2 glyph entries:
// One for the top/bottom half each, because that's how DECDHL works. This will clip the
// `glyphEntry` to only contain the one specified by `fontFaceEntry.lineRendition`
// and create a second entry in our glyph cache hashmap that contains the other half.
void BackendD3D::_splitDoubleHeightGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry)
void BackendD3D::_splitDoubleHeightGlyph(const RenderingPayload& p, const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, AtlasGlyphEntry* glyphEntry)
{
// Twice the line height, twice the descender gap. For both.
glyphEntry.data.offset.y -= p.s->font->descender;
glyphEntry->offset.y -= p.s->font->descender;
const auto isTop = fontFaceEntry.lineRendition == LineRendition::DoubleHeightTop;
const auto isTop = row.lineRendition == LineRendition::DoubleHeightTop;
const auto otherLineRendition = isTop ? LineRendition::DoubleHeightBottom : LineRendition::DoubleHeightTop;
const auto entry2 = fontFaceEntry.glyphs[WI_EnumValue(otherLineRendition)].insert(glyphEntry->glyphIndex).first;
const AtlasFontFaceKey key2{
.fontFace = fontFaceEntry.fontFace.get(),
.lineRendition = isTop ? LineRendition::DoubleHeightBottom : LineRendition::DoubleHeightTop,
};
*entry2 = *glyphEntry;
auto& glyphCache = _glyphAtlasMap.insert(key2).first.inner->glyphs;
auto& entry2 = glyphCache.insert(glyphEntry.glyphIndex).first;
entry2.data = glyphEntry.data;
const auto top = isTop ? glyphEntry : entry2;
const auto bottom = isTop ? entry2 : glyphEntry;
const auto topSize = clamp(-glyphEntry->offset.y - p.s->font->baseline, 0, static_cast<int>(glyphEntry->size.y));
auto& top = isTop ? glyphEntry : entry2;
auto& bottom = isTop ? entry2 : glyphEntry;
const auto topSize = clamp(-glyphEntry.data.offset.y - p.s->font->baseline, 0, static_cast<int>(glyphEntry.data.size.y));
top.data.offset.y += p.s->font->cellSize.y;
top.data.size.y = topSize;
bottom.data.offset.y += topSize;
bottom.data.size.y = std::max(0, bottom.data.size.y - topSize);
bottom.data.texcoord.y += topSize;
top->offset.y += p.s->font->cellSize.y;
top->size.y = topSize;
bottom->offset.y += topSize;
bottom->size.y = std::max(0, bottom->size.y - topSize);
bottom->texcoord.y += topSize;
// Things like diacritics might be so small that they only exist on either half of the
// double-height row. This effectively turns the other (unneeded) side into whitespace.
if (!top.data.size.y)
if (!top->size.y)
{
top.data.shadingType = ShadingType::Default;
top->shadingType = ShadingType::Default;
}
if (!bottom.data.size.y)
if (!bottom->size.y)
{
bottom.data.shadingType = ShadingType::Default;
bottom->shadingType = ShadingType::Default;
}
}
@@ -1691,7 +1573,7 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y)
for (; posX < end; posX += textCellWidth)
{
_appendQuad() = {
.shadingType = ShadingType::SolidLine,
.shadingType = static_cast<u16>(ShadingType::SolidLine),
.position = { static_cast<i16>(posX), rowTop },
.size = { width, p.s->font->cellSize.y },
.color = r.gridlineColor,
@@ -1713,7 +1595,7 @@ void BackendD3D::_drawGridlines(const RenderingPayload& p, u16 y)
if (rt < rb)
{
_appendQuad() = {
.shadingType = shadingType,
.shadingType = static_cast<u16>(shadingType),
.renditionScale = { static_cast<u8>(1 << horizontalShift), static_cast<u8>(1 << verticalShift) },
.position = { left, static_cast<i16>(rt) },
.size = { width, static_cast<u16>(rb - rt) },
@@ -1803,8 +1685,8 @@ void BackendD3D::_drawCursorBackground(const RenderingPayload& p)
}
const i16x2 position{
p.s->font->cellSize.x * x0,
p.s->font->cellSize.y * p.cursorRect.top,
static_cast<i16>(p.s->font->cellSize.x * x0),
static_cast<i16>(p.s->font->cellSize.y * p.cursorRect.top),
};
const u16x2 size{
static_cast<u16>(p.s->font->cellSize.x * (x1 - x0)),
@@ -1887,7 +1769,7 @@ void BackendD3D::_drawCursorBackground(const RenderingPayload& p)
for (const auto& c : _cursorRects)
{
_appendQuad() = {
.shadingType = ShadingType::Cursor,
.shadingType = static_cast<u16>(ShadingType::Cursor),
.position = c.position,
.size = c.size,
.color = c.background,
@@ -1911,7 +1793,7 @@ void BackendD3D::_drawCursorForeground()
// start and end of this "block" here in advance.
for (; instancesOffset < instancesCount; ++instancesOffset)
{
const auto shadingType = _instances[instancesOffset].shadingType;
const auto shadingType = static_cast<ShadingType>(_instances[instancesOffset].shadingType);
if (shadingType >= ShadingType::TextDrawingFirst && shadingType <= ShadingType::TextDrawingLast)
{
break;
@@ -1931,7 +1813,7 @@ void BackendD3D::_drawCursorForeground()
// Now do the same thing as above, but backwards from the end.
for (; instancesCount > instancesOffset; --instancesCount)
{
const auto shadingType = _instances[instancesCount - 1].shadingType;
const auto shadingType = static_cast<ShadingType>(_instances[instancesCount - 1].shadingType);
if (shadingType >= ShadingType::TextDrawingFirst && shadingType <= ShadingType::TextDrawingLast)
{
break;
@@ -1990,7 +1872,7 @@ size_t BackendD3D::_drawCursorForegroundSlowPath(const CursorRect& c, size_t off
// There's one special exception to the rule: Emojis. We currently don't really support inverting
// (or reversing) colored glyphs like that, so we can return early here and avoid cutting them up.
// It'd be too expensive to check for these rare glyph types inside the _drawCursorForeground() loop.
if (it.shadingType == ShadingType::TextPassthrough)
if (static_cast<ShadingType>(it.shadingType) == ShadingType::TextPassthrough)
{
return 0;
}
@@ -2121,14 +2003,14 @@ void BackendD3D::_drawSelection(const RenderingPayload& p)
else
{
_appendQuad() = {
.shadingType = ShadingType::Selection,
.shadingType = static_cast<u16>(ShadingType::Selection),
.position = {
p.s->font->cellSize.x * row->selectionFrom,
p.s->font->cellSize.y * y,
static_cast<i16>(p.s->font->cellSize.x * row->selectionFrom),
static_cast<i16>(p.s->font->cellSize.y * y),
},
.size = {
static_cast<u16>(p.s->font->cellSize.x * (row->selectionTo - row->selectionFrom)),
p.s->font->cellSize.y,
static_cast<u16>(p.s->font->cellSize.y),
},
.color = p.s->misc->selectionColor,
};

View File

@@ -57,7 +57,7 @@ namespace Microsoft::Console::Render::Atlas
#pragma warning(suppress : 4324) // 'CustomConstBuffer': structure was padded due to alignment specifier
};
enum class ShadingType : u16
enum class ShadingType : u8 // u8 so that it packs perfectly into AtlasGlyphEntry
{
Default = 0,
Background = 0,
@@ -89,7 +89,7 @@ namespace Microsoft::Console::Render::Atlas
// impact on performance and power draw. If (when?) displays with >32k resolution make their
// appearance in the future, this should be changed to f32x2. But if you do so, please change
// all other occurrences of i16x2 positions/offsets throughout the class to keep it consistent.
alignas(u16) ShadingType shadingType;
alignas(u16) u16 shadingType; // not using ShadingType here to avoid struct padding which causes optimization failures with MSVC
alignas(u16) u8x2 renditionScale;
alignas(u32) i16x2 position;
alignas(u32) u16x2 size;
@@ -97,8 +97,12 @@ namespace Microsoft::Console::Render::Atlas
alignas(u32) u32 color;
};
struct alignas(u32) AtlasGlyphEntryData
// NOTE: Don't initialize any members in this struct. This ensures that no
// zero-initialization needs to occur when we allocate large buffers of this object.
struct AtlasGlyphEntry
{
u32 glyphIndex;
u8 occupied;
ShadingType shadingType;
u16 overlapSplit;
i16x2 offset;
@@ -106,80 +110,71 @@ namespace Microsoft::Console::Render::Atlas
u16x2 texcoord;
};
// NOTE: Don't initialize any members in this struct. This ensures that no
// zero-initialization needs to occur when we allocate large buffers of this object.
struct AtlasGlyphEntry
struct AtlasGlyphEntryHashTrait
{
u16 glyphIndex;
// All data in QuadInstance is u32-aligned anyways, so this simultaneously serves as padding.
u16 _occupied;
AtlasGlyphEntryData data;
constexpr bool operator==(u16 key) const noexcept
static constexpr bool occupied(const AtlasGlyphEntry& entry) noexcept
{
return glyphIndex == key;
return entry.occupied != 0;
}
constexpr operator bool() const noexcept
static constexpr size_t hash(const u16 glyphIndex) noexcept
{
return _occupied != 0;
return til::flat_set_hash_integer(glyphIndex);
}
constexpr AtlasGlyphEntry& operator=(u16 key) noexcept
static constexpr size_t hash(const AtlasGlyphEntry& entry) noexcept
{
glyphIndex = key;
_occupied = 1;
return *this;
return til::flat_set_hash_integer(entry.glyphIndex);
}
static constexpr bool equals(const AtlasGlyphEntry& entry, u16 glyphIndex) noexcept
{
return entry.glyphIndex == glyphIndex;
}
static constexpr void assign(AtlasGlyphEntry& entry, u16 glyphIndex) noexcept
{
entry.glyphIndex = glyphIndex;
entry.occupied = 1;
}
};
// This exists so that we can look up a AtlasFontFaceEntry without AddRef()/Release()ing fontFace first.
struct AtlasFontFaceKey
{
IDWriteFontFace2* fontFace;
LineRendition lineRendition;
};
struct AtlasFontFaceEntryInner
struct AtlasFontFaceEntry
{
// BODGY: At the time of writing IDWriteFontFallback::MapCharacters returns the same IDWriteFontFace instance
// for the same font face variant as long as someone is holding a reference to the instance (see ActiveFaceCache).
// This allows us to hash the value of the pointer as if it was uniquely identifying the font face variant.
wil::com_ptr<IDWriteFontFace2> fontFace;
LineRendition lineRendition = LineRendition::SingleWidth;
til::linear_flat_set<AtlasGlyphEntry> glyphs;
// boxGlyphs gets an increased growth rate of 2^2 = 4x, because presumably fonts either contain very
// few or almost all of the box glyphs. This reduces the cost of _initializeFontFaceEntry quite a bit.
til::linear_flat_set<u16, 2, 2> boxGlyphs;
// The 4 entries map to the 4 corresponding LineRendition enum values.
til::linear_flat_set<AtlasGlyphEntry, AtlasGlyphEntryHashTrait> glyphs[4];
};
struct AtlasFontFaceEntry
struct AtlasFontFaceEntryHashTrait
{
// This being a heap allocated allows us to insert into `glyphs` in `_splitDoubleHeightGlyph`
// (which might resize the hashmap!), while the caller `_drawText` is holding onto `glyphs`.
// If it wasn't heap allocated, all pointers into `linear_flat_set` would be invalidated.
std::unique_ptr<AtlasFontFaceEntryInner> inner;
bool operator==(const AtlasFontFaceKey& key) const noexcept
static bool occupied(const AtlasFontFaceEntry& entry) noexcept
{
const auto& i = *inner;
return i.fontFace.get() == key.fontFace && i.lineRendition == key.lineRendition;
return static_cast<bool>(entry.fontFace);
}
operator bool() const noexcept
static constexpr size_t hash(const IDWriteFontFace2* fontFace) noexcept
{
return static_cast<bool>(inner);
return til::flat_set_hash_integer(std::bit_cast<uintptr_t>(fontFace));
}
AtlasFontFaceEntry& operator=(const AtlasFontFaceKey& key)
static size_t hash(const AtlasFontFaceEntry& entry) noexcept
{
inner = std::make_unique<AtlasFontFaceEntryInner>();
auto& i = *inner;
i.fontFace = key.fontFace;
i.lineRendition = key.lineRendition;
return *this;
return hash(entry.fontFace.get());
}
static bool equals(const AtlasFontFaceEntry& entry, const IDWriteFontFace2* fontFace) noexcept
{
return entry.fontFace.get() == fontFace;
}
static void assign(AtlasFontFaceEntry& entry, IDWriteFontFace2* fontFace) noexcept
{
entry.fontFace = fontFace;
}
};
@@ -216,12 +211,12 @@ namespace Microsoft::Console::Render::Atlas
void _uploadBackgroundBitmap(const RenderingPayload& p);
void _drawText(RenderingPayload& p);
ATLAS_ATTR_COLD void _drawTextOverlapSplit(const RenderingPayload& p, u16 y);
ATLAS_ATTR_COLD static void _initializeFontFaceEntry(AtlasFontFaceEntryInner& fontFaceEntry);
[[nodiscard]] ATLAS_ATTR_COLD bool _drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry);
bool _drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry);
void _drawSoftFontGlyphInBitmap(const RenderingPayload& p, const AtlasGlyphEntry& glyphEntry) const;
void _drawGlyphPrepareRetry(const RenderingPayload& p);
void _splitDoubleHeightGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry);
[[nodiscard]] ATLAS_ATTR_COLD AtlasGlyphEntry* _drawGlyph(const RenderingPayload& p, const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, u32 glyphIndex);
AtlasGlyphEntry* _drawBuiltinGlyph(const RenderingPayload& p, const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, u32 glyphIndex);
void _drawSoftFontGlyph(const RenderingPayload& p, const stbrp_rect& rect, u32 glyphIndex);
void _drawGlyphAtlasAllocate(const RenderingPayload& p, stbrp_rect& rect);
static AtlasGlyphEntry* _drawGlyphAllocateEntry(const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, u32 glyphIndex);
static void _splitDoubleHeightGlyph(const RenderingPayload& p, const ShapedRow& row, AtlasFontFaceEntry& fontFaceEntry, AtlasGlyphEntry* glyphEntry);
void _drawGridlines(const RenderingPayload& p, u16 y);
void _drawCursorBackground(const RenderingPayload& p);
ATLAS_ATTR_COLD void _drawCursorForeground();
@@ -258,7 +253,8 @@ namespace Microsoft::Console::Render::Atlas
wil::com_ptr<ID3D11Texture2D> _glyphAtlas;
wil::com_ptr<ID3D11ShaderResourceView> _glyphAtlasView;
til::linear_flat_set<AtlasFontFaceEntry> _glyphAtlasMap;
til::linear_flat_set<AtlasFontFaceEntry, AtlasFontFaceEntryHashTrait> _glyphAtlasMap;
AtlasFontFaceEntry _builtinGlyphs;
Buffer<stbrp_node> _rectPackerData;
stbrp_context _rectPacker{};
til::CoordType _ligatureOverhangTriggerLeft = 0;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "common.h"
namespace Microsoft::Console::Render::Atlas::BuiltinGlyphs
{
bool IsBuiltinGlyph(char32_t codepoint) noexcept;
void DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_RECT_F& rect, char32_t codepoint);
// This is just an extra. It's not actually implemented as part of BuiltinGlyphs.cpp.
constexpr bool IsSoftFontChar(char32_t ch) noexcept
{
return ch >= 0xEF20 && ch < 0xEF80;
}
}

View File

@@ -16,6 +16,7 @@
<ClCompile Include="Backend.cpp" />
<ClCompile Include="BackendD2D.cpp" />
<ClCompile Include="BackendD3D.cpp" />
<ClCompile Include="BuiltinGlyphs.cpp" />
<ClCompile Include="dwrite.cpp" />
<ClCompile Include="DWriteTextAnalysis.cpp" />
<ClCompile Include="pch.cpp">
@@ -28,6 +29,7 @@
<ClInclude Include="Backend.h" />
<ClInclude Include="BackendD2D.h" />
<ClInclude Include="BackendD3D.h" />
<ClInclude Include="BuiltinGlyphs.h" />
<ClInclude Include="colorbrewer.h" />
<ClInclude Include="common.h" />
<ClInclude Include="dwrite.h" />

View File

@@ -360,6 +360,7 @@ namespace Microsoft::Console::Render::Atlas
u16 dpi = 96;
AntialiasingMode antialiasingMode = DefaultAntialiasingMode;
bool builtinGlyphs = false;
std::vector<uint16_t> softFontPattern;
til::size softFontCellSize;
@@ -420,8 +421,8 @@ namespace Microsoft::Console::Render::Atlas
struct FontMapping
{
wil::com_ptr<IDWriteFontFace2> fontFace;
u32 glyphsFrom = 0;
u32 glyphsTo = 0;
size_t glyphsFrom = 0;
size_t glyphsTo = 0;
};
struct GridLineRange
@@ -450,11 +451,18 @@ namespace Microsoft::Console::Render::Atlas
dirtyBottom = dirtyTop + cellHeight;
}
// Each mappings from/to range indicates the range of indices/advances/offsets/colors this fontFace is to be used for.
std::vector<FontMapping> mappings;
// Stores glyph indices of the corresponding mappings.fontFace, unless fontFace is nullptr,
// in which case this stores UTF16 because we're dealing with a custom glyph (box glyph, etc.).
std::vector<u16> glyphIndices;
std::vector<f32> glyphAdvances; // same size as glyphIndices
std::vector<DWRITE_GLYPH_OFFSET> glyphOffsets; // same size as glyphIndices
std::vector<u32> colors; // same size as glyphIndices
// Same size as glyphIndices.
std::vector<f32> glyphAdvances;
// Same size as glyphIndices.
std::vector<DWRITE_GLYPH_OFFSET> glyphOffsets;
// Same size as glyphIndices.
std::vector<u32> colors;
std::vector<GridLineRange> gridLineRanges;
LineRendition lineRendition = LineRendition::SingleWidth;
u16 selectionFrom = 0;
@@ -499,6 +507,7 @@ namespace Microsoft::Console::Render::Atlas
//// Parameters which change seldom.
GenerationalSettings s;
std::wstring userLocaleName;
//// Parameters which change every frame.
// This is the backing buffer for `rows`.

View File

@@ -29,6 +29,11 @@ void FontInfoDesired::SetCellSize(const CSSLengthPercentage& cellWidth, const CS
_cellHeight = cellHeight;
}
void FontInfoDesired::SetEnableBuiltinGlyphs(bool builtinGlyphs) noexcept
{
_builtinGlyphs = builtinGlyphs;
}
const CSSLengthPercentage& FontInfoDesired::GetCellWidth() const noexcept
{
return _cellWidth;
@@ -39,6 +44,11 @@ const CSSLengthPercentage& FontInfoDesired::GetCellHeight() const noexcept
return _cellHeight;
}
bool FontInfoDesired::GetEnableBuiltinGlyphs() const noexcept
{
return _builtinGlyphs;
}
float FontInfoDesired::GetFontSize() const noexcept
{
return _fontSize;

View File

@@ -35,9 +35,11 @@ public:
bool operator==(const FontInfoDesired& other) = delete;
void SetCellSize(const CSSLengthPercentage& cellWidth, const CSSLengthPercentage& cellHeight) noexcept;
void SetEnableBuiltinGlyphs(bool builtinGlyphs) noexcept;
const CSSLengthPercentage& GetCellWidth() const noexcept;
const CSSLengthPercentage& GetCellHeight() const noexcept;
bool GetEnableBuiltinGlyphs() const noexcept;
float GetFontSize() const noexcept;
til::size GetEngineSize() const noexcept;
bool IsDefaultRasterFont() const noexcept;
@@ -47,4 +49,5 @@ private:
float _fontSize;
CSSLengthPercentage _cellWidth;
CSSLengthPercentage _cellHeight;
bool _builtinGlyphs = false;
};

View File

@@ -12,38 +12,35 @@ using namespace WEX::TestExecution;
struct Data
{
static constexpr auto emptyMarker = std::numeric_limits<size_t>::max();
constexpr operator bool() const noexcept
{
return value != emptyMarker;
}
constexpr bool operator==(int key) const noexcept
{
return value == static_cast<size_t>(key);
}
constexpr Data& operator=(int key) noexcept
{
value = static_cast<size_t>(key);
return *this;
}
size_t value = emptyMarker;
};
template<>
struct ::std::hash<Data>
struct DataHashTrait
{
constexpr size_t operator()(int key) const noexcept
static constexpr bool occupied(const Data& d) noexcept
{
return til::flat_set_hash_integer(static_cast<size_t>(key));
return d.value != Data::emptyMarker;
}
constexpr size_t operator()(Data d) const noexcept
static constexpr size_t hash(const size_t key) noexcept
{
return til::flat_set_hash_integer(key);
}
static constexpr size_t hash(const Data& d) noexcept
{
return til::flat_set_hash_integer(d.value);
}
static constexpr bool equals(const Data& d, size_t key) noexcept
{
return d.value == key;
}
static constexpr void assign(Data& d, size_t key) noexcept
{
d.value = key;
}
};
class FlatSetTests
@@ -52,7 +49,7 @@ class FlatSetTests
TEST_METHOD(Basic)
{
til::linear_flat_set<Data> set;
til::linear_flat_set<Data, DataHashTrait> set;
// This simultaneously demonstrates how the class can't just do "heterogeneous lookups"
// like STL does, but also insert items with a different type.
@@ -62,7 +59,7 @@ class FlatSetTests
const auto [entry2, inserted2] = set.insert(123);
VERIFY_IS_FALSE(inserted2);
VERIFY_ARE_EQUAL(&entry1, &entry2);
VERIFY_ARE_EQUAL(123u, entry2.value);
VERIFY_ARE_EQUAL(entry1, entry2);
VERIFY_ARE_EQUAL(123u, entry2->value);
}
};