Skip to content

Commit 37a63b7

Browse files
committed
AtlasEngine: Clip box glyphs to their cells
1 parent 457bc65 commit 37a63b7

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)