Skip to content

Commit 4628ceb

Browse files
authored
AtlasEngine: Clip box glyphs to their cells (#15343)
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. 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. ## Validation Steps Performed * With Cascadia Code, printing ``"`u{2593}`n`u{2593}"`` in pwsh doesn't produce a brightened overlap anymore ✅ * ``"`e#3`u{2502}`n`e#4`u{2502}"`` produces a fat vertical line ✅
1 parent 9a4f4ab commit 4628ceb

File tree

3 files changed

+132
-26
lines changed

3 files changed

+132
-26
lines changed

src/inc/til/flat_set.h

+33-3
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,19 @@ namespace til
2828
// A basic, hashmap with linear probing. A `LoadFactor` of 2 equals
2929
// a max. load of roughly 50% and a `LoadFactor` of 4 roughly 25%.
3030
//
31+
// `GrowthExponent` controls how fast the set grows and corresponds to
32+
// a rate of 2^GrowthExponent. In other words, a `GrowthExponent` of 3
33+
// equals a growth rate of 8x every time the capacity has been reached.
34+
//
3135
// It performs best with:
3236
// * small and cheap T
3337
// * >= 50% successful lookups
3438
// * <= 50% load factor (LoadFactor >= 2, which is the minimum anyways)
35-
template<typename T, size_t LoadFactor = 2>
39+
template<typename T, size_t LoadFactor = 2, size_t GrowthExponent = 1>
3640
struct linear_flat_set
3741
{
3842
static_assert(LoadFactor >= 2);
43+
static_assert(GrowthExponent >= 1);
3944

4045
linear_flat_set() = default;
4146

@@ -85,6 +90,30 @@ namespace til
8590
}
8691
}
8792

93+
template<typename U>
94+
T* lookup(U&& key) const noexcept
95+
{
96+
if (!_map)
97+
{
98+
return nullptr;
99+
}
100+
101+
const auto hash = ::std::hash<T>{}(key) >> _shift;
102+
103+
for (auto i = hash;; ++i)
104+
{
105+
auto& slot = _map[i & _mask];
106+
if (!slot)
107+
{
108+
return nullptr;
109+
}
110+
if (slot == key) [[likely]]
111+
{
112+
return &slot;
113+
}
114+
}
115+
}
116+
88117
template<typename U>
89118
std::pair<T&, bool> insert(U&& key)
90119
{
@@ -121,14 +150,15 @@ namespace til
121150
private:
122151
__declspec(noinline) void _bumpSize()
123152
{
153+
// For instance at a GrowthExponent of 1:
124154
// A _shift of 0 would result in a newShift of 0xfffff...
125155
// A _shift of 1 would result in a newCapacity of 0
126-
if (_shift < 2)
156+
if (_shift <= GrowthExponent)
127157
{
128158
throw std::bad_array_new_length{};
129159
}
130160

131-
const auto newShift = _shift - 1;
161+
const auto newShift = _shift - GrowthExponent;
132162
const auto newCapacity = size_t{ 1 } << (digits - newShift);
133163
const auto newMask = newCapacity - 1;
134164
auto newMap = std::make_unique<T[]>(newCapacity);

src/renderer/atlas/BackendD3D.cpp

+95-23
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,23 @@ TIL_FAST_MATH_BEGIN
3636
#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
3737
#pragma warning(disable : 26482) // Only index into arrays using constant expressions (bounds.2).
3838

39+
// Initializing large arrays can be very costly compared to how cheap some of these functions are.
40+
#define ALLOW_UNINITIALIZED_BEGIN _Pragma("warning(push)") _Pragma("warning(disable : 26494)")
41+
#define ALLOW_UNINITIALIZED_END _Pragma("warning(pop)")
42+
3943
using namespace Microsoft::Console::Render::Atlas;
4044

4145
template<>
42-
struct ::std::hash<BackendD3D::AtlasGlyphEntry>
46+
struct std::hash<u16>
47+
{
48+
constexpr size_t operator()(u16 key) const noexcept
49+
{
50+
return til::flat_set_hash_integer(key);
51+
}
52+
};
53+
54+
template<>
55+
struct std::hash<BackendD3D::AtlasGlyphEntry>
4356
{
4457
constexpr size_t operator()(u16 key) const noexcept
4558
{
@@ -53,7 +66,7 @@ struct ::std::hash<BackendD3D::AtlasGlyphEntry>
5366
};
5467

5568
template<>
56-
struct ::std::hash<BackendD3D::AtlasFontFaceEntry>
69+
struct std::hash<BackendD3D::AtlasFontFaceEntry>
5770
{
5871
using T = BackendD3D::AtlasFontFaceEntry;
5972

@@ -982,7 +995,13 @@ void BackendD3D::_drawText(RenderingPayload& p)
982995
// We need to goto here, because a retry will cause the atlas texture as well as the
983996
// _glyphCache hashmap to be cleared, and so we'll have to call insert() again.
984997
drawGlyphRetry:
985-
auto& fontFaceEntry = *_glyphAtlasMap.insert(fontFaceKey).first.inner;
998+
const auto [fontFaceEntryOuter, fontFaceInserted] = _glyphAtlasMap.insert(fontFaceKey);
999+
auto& fontFaceEntry = *fontFaceEntryOuter.inner;
1000+
1001+
if (fontFaceInserted)
1002+
{
1003+
_initializeFontFaceEntry(fontFaceEntry);
1004+
}
9861005

9871006
while (x < m.glyphsTo)
9881007
{
@@ -1124,7 +1143,30 @@ void BackendD3D::_drawTextOverlapSplit(const RenderingPayload& p, u16 y)
11241143
}
11251144
}
11261145

1127-
bool BackendD3D::_drawGlyph(const RenderingPayload& p, const BackendD3D::AtlasFontFaceEntryInner& fontFaceEntry, BackendD3D::AtlasGlyphEntry& glyphEntry)
1146+
void BackendD3D::_initializeFontFaceEntry(AtlasFontFaceEntryInner& fontFaceEntry)
1147+
{
1148+
ALLOW_UNINITIALIZED_BEGIN
1149+
std::array<u32, 0x100> codepoints;
1150+
std::array<u16, 0x100> indices;
1151+
ALLOW_UNINITIALIZED_END
1152+
1153+
for (u32 i = 0; i < codepoints.size(); ++i)
1154+
{
1155+
codepoints[i] = 0x2500 + i;
1156+
}
1157+
1158+
THROW_IF_FAILED(fontFaceEntry.fontFace->GetGlyphIndicesW(codepoints.data(), codepoints.size(), indices.data()));
1159+
1160+
for (u32 i = 0; i < indices.size(); ++i)
1161+
{
1162+
if (const auto idx = indices[i])
1163+
{
1164+
fontFaceEntry.boxGlyphs.insert(idx);
1165+
}
1166+
}
1167+
}
1168+
1169+
bool BackendD3D::_drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry)
11281170
{
11291171
if (!fontFaceEntry.fontFace)
11301172
{
@@ -1207,22 +1249,20 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const BackendD3D::AtlasFo
12071249
#endif
12081250

12091251
const auto lineRendition = static_cast<LineRendition>(fontFaceEntry.lineRendition);
1210-
std::optional<D2D1_MATRIX_3X2_F> transform;
1252+
const auto needsTransform = lineRendition != LineRendition::SingleWidth;
12111253

1212-
if (lineRendition != LineRendition::SingleWidth)
1254+
static constexpr D2D1_MATRIX_3X2_F identityTransform{ .m11 = 1, .m22 = 1 };
1255+
D2D1_MATRIX_3X2_F transform = identityTransform;
1256+
1257+
if (needsTransform)
12131258
{
1214-
auto& t = transform.emplace();
1215-
t.m11 = 2.0f;
1216-
t.m22 = lineRendition >= LineRendition::DoubleHeightTop ? 2.0f : 1.0f;
1217-
_d2dRenderTarget->SetTransform(&t);
1259+
transform.m11 = 2.0f;
1260+
transform.m22 = lineRendition >= LineRendition::DoubleHeightTop ? 2.0f : 1.0f;
1261+
_d2dRenderTarget->SetTransform(&transform);
12181262
}
12191263

12201264
const auto restoreTransform = wil::scope_exit([&]() noexcept {
1221-
if (transform)
1222-
{
1223-
static constexpr D2D1_MATRIX_3X2_F identity{ .m11 = 1, .m22 = 1 };
1224-
_d2dRenderTarget->SetTransform(&identity);
1225-
}
1265+
_d2dRenderTarget->SetTransform(&identityTransform);
12261266
});
12271267

12281268
// This calculates the black box of the glyph, or in other words,
@@ -1246,7 +1286,7 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const BackendD3D::AtlasFo
12461286
bool isColorGlyph = false;
12471287
D2D1_RECT_F bounds = GlyphRunEmptyBounds;
12481288

1249-
const auto cleanup = wil::scope_exit([&]() {
1289+
const auto antialiasingCleanup = wil::scope_exit([&]() {
12501290
if (isColorGlyph)
12511291
{
12521292
_d2dRenderTarget4->SetTextAntialiasMode(static_cast<D2D1_TEXT_ANTIALIAS_MODE>(p.s->font->antialiasingMode));
@@ -1273,7 +1313,23 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const BackendD3D::AtlasFo
12731313
}
12741314
}
12751315

1276-
// box may be empty if the glyph is whitespace.
1316+
// Overhangs for box glyphs can produce unsightly effects, where the antialiased edges of horizontal
1317+
// and vertical lines overlap between neighboring glyphs and produce "boldened" intersections.
1318+
// It looks a little something like this:
1319+
// ---+---+---
1320+
// This avoids the issue in most cases by simply clipping the glyph to the size of a single cell.
1321+
// The downside is that it fails to work well for custom line heights, etc.
1322+
const auto isBoxGlyph = fontFaceEntry.boxGlyphs.lookup(glyphEntry.glyphIndex) != nullptr;
1323+
if (isBoxGlyph)
1324+
{
1325+
// NOTE: As mentioned above, the "origin" of a glyph's coordinate system is its baseline.
1326+
bounds.left = std::max(bounds.left, 0.0f);
1327+
bounds.top = std::max(bounds.top, static_cast<f32>(-p.s->font->baseline) * transform.m22);
1328+
bounds.right = std::min(bounds.right, static_cast<f32>(p.s->font->cellSize.x) * transform.m11);
1329+
bounds.bottom = std::min(bounds.bottom, static_cast<f32>(p.s->font->descender) * transform.m22);
1330+
}
1331+
1332+
// The bounds may be empty if the glyph is whitespace.
12771333
if (bounds.left >= bounds.right || bounds.top >= bounds.bottom)
12781334
{
12791335
return true;
@@ -1299,15 +1355,31 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const BackendD3D::AtlasFo
12991355
static_cast<f32>(rect.y - bt),
13001356
};
13011357

1302-
if (transform)
1358+
_d2dBeginDrawing();
1359+
1360+
if (isBoxGlyph)
13031361
{
1304-
auto& t = *transform;
1305-
t.dx = (1.0f - t.m11) * baselineOrigin.x;
1306-
t.dy = (1.0f - t.m22) * baselineOrigin.y;
1307-
_d2dRenderTarget->SetTransform(&t);
1362+
const D2D1_RECT_F clipRect{
1363+
static_cast<f32>(rect.x) / transform.m11,
1364+
static_cast<f32>(rect.y) / transform.m22,
1365+
static_cast<f32>(rect.x + rect.w) / transform.m11,
1366+
static_cast<f32>(rect.y + rect.h) / transform.m22,
1367+
};
1368+
_d2dRenderTarget4->PushAxisAlignedClip(&clipRect, D2D1_ANTIALIAS_MODE_ALIASED);
13081369
}
1370+
const auto boxGlyphCleanup = wil::scope_exit([&]() {
1371+
if (isBoxGlyph)
1372+
{
1373+
_d2dRenderTarget4->PopAxisAlignedClip();
1374+
}
1375+
});
13091376

1310-
_d2dBeginDrawing();
1377+
if (needsTransform)
1378+
{
1379+
transform.dx = (1.0f - transform.m11) * baselineOrigin.x;
1380+
transform.dy = (1.0f - transform.m22) * baselineOrigin.y;
1381+
_d2dRenderTarget->SetTransform(&transform);
1382+
}
13111383

13121384
if (!isColorGlyph)
13131385
{

src/renderer/atlas/BackendD3D.h

+4
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ namespace Microsoft::Console::Render::Atlas
151151
LineRendition lineRendition = LineRendition::SingleWidth;
152152

153153
til::linear_flat_set<AtlasGlyphEntry> glyphs;
154+
// boxGlyphs gets an increased growth rate of 2^2 = 4x, because presumably fonts either contain very
155+
// few or almost all of the box glyphs. This reduces the cost of _initializeFontFaceEntry quite a bit.
156+
til::linear_flat_set<u16, 2, 2> boxGlyphs;
154157
};
155158

156159
struct AtlasFontFaceEntry
@@ -214,6 +217,7 @@ namespace Microsoft::Console::Render::Atlas
214217
void _uploadBackgroundBitmap(const RenderingPayload& p);
215218
void _drawText(RenderingPayload& p);
216219
ATLAS_ATTR_COLD void _drawTextOverlapSplit(const RenderingPayload& p, u16 y);
220+
ATLAS_ATTR_COLD static void _initializeFontFaceEntry(AtlasFontFaceEntryInner& fontFaceEntry);
217221
ATLAS_ATTR_COLD [[nodiscard]] bool _drawGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry);
218222
bool _drawSoftFontGlyph(const RenderingPayload& p, const AtlasFontFaceEntryInner& fontFaceEntry, AtlasGlyphEntry& glyphEntry);
219223
void _drawGlyphPrepareRetry(const RenderingPayload& p);

0 commit comments

Comments
 (0)