Skip to content

Commit 1e4b8dc

Browse files
committed
AtlasEngine: Clip box glyphs to their cells
1 parent 952ed38 commit 1e4b8dc

File tree

3 files changed

+121
-11
lines changed

3 files changed

+121
-11
lines changed

src/inc/til/flat_set.h

+36-5
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);
@@ -161,12 +191,13 @@ namespace til
161191
}
162192

163193
static constexpr auto digits = std::numeric_limits<size_t>::digits;
194+
// This results in an initial capacity of 8 items, independent of the LoadFactor.
195+
static constexpr auto initialShift = digits - LoadFactor - 1;
164196

165197
std::unique_ptr<T[]> _map;
166198
size_t _capacity = 0;
167199
size_t _load = 0;
168-
// This results in an initial capacity of 8 items, independent of the LoadFactor.
169-
size_t _shift = digits - LoadFactor - 1;
200+
size_t _shift = initialShift;
170201
size_t _mask = 0;
171202
};
172203
}

src/renderer/atlas/BackendD3D.cpp

+81-6
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
{
@@ -1246,7 +1288,7 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const BackendD3D::AtlasFo
12461288
bool isColorGlyph = false;
12471289
D2D1_RECT_F bounds = GlyphRunEmptyBounds;
12481290

1249-
const auto cleanup = wil::scope_exit([&]() {
1291+
const auto antialiasingCleanup = wil::scope_exit([&]() {
12501292
if (isColorGlyph)
12511293
{
12521294
_d2dRenderTarget4->SetTextAntialiasMode(static_cast<D2D1_TEXT_ANTIALIAS_MODE>(p.s->font->antialiasingMode));
@@ -1273,7 +1315,23 @@ bool BackendD3D::_drawGlyph(const RenderingPayload& p, const BackendD3D::AtlasFo
12731315
}
12741316
}
12751317

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

13101368
_d2dBeginDrawing();
13111369

1370+
if (isBoxGlyph)
1371+
{
1372+
const D2D1_RECT_F clipRect{
1373+
static_cast<f32>(rect.x),
1374+
static_cast<f32>(rect.y),
1375+
static_cast<f32>(rect.x + rect.w),
1376+
static_cast<f32>(rect.y + rect.h),
1377+
};
1378+
_d2dRenderTarget4->PushAxisAlignedClip(&clipRect, D2D1_ANTIALIAS_MODE_ALIASED);
1379+
}
1380+
const auto boxGlyphCleanup = wil::scope_exit([&]() {
1381+
if (isBoxGlyph)
1382+
{
1383+
_d2dRenderTarget4->PopAxisAlignedClip();
1384+
}
1385+
});
1386+
13121387
if (!isColorGlyph)
13131388
{
13141389
_d2dRenderTarget->DrawGlyphRun(baselineOrigin, &glyphRun, _brush.get(), DWRITE_MEASURING_MODE_NATURAL);

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)