Skip to content

Commit 66f4f9d

Browse files
authored
AtlasEngine: Implement LRU invalidation for glyph tiles (#13458)
So far AtlasEngine would only grow the backing texture atlas once it gets full, without the ability to reuse tiles once it gets full. This commit adds LRU capabilities to the glyph-to-tile hashmap, allowing us to reuse the least recently used tiles for new ones once the atlas texture is full. This commit uses a quadratic growth factor with power-of-2 textures, resulting in a backing atlas of 1x to 2x the size of the window. While AtlasEngine is still incapable of shrinking the texture, it'll now at least not grow to 128MB or result in weird glitches under most circumstances. ## Validation Steps Performed * Print `utf8_sequence_0-0x2ffff_assigned_printable_unseparated.txt` from https://github.com/bits/UTF-8-Unicode-Test-Documents * Scroll back up to the top * PowerShell input line is still there rendering as ASCII. ✅
1 parent bbc570d commit 66f4f9d

File tree

3 files changed

+314
-166
lines changed

3 files changed

+314
-166
lines changed

src/renderer/atlas/AtlasEngine.cpp

Lines changed: 34 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,6 @@
2525

2626
using namespace Microsoft::Console::Render;
2727

28-
#pragma warning(push)
29-
#pragma warning(disable : 26447) // The function is declared 'noexcept' but calls function 'operator()()' which may throw exceptions (f.6).
30-
__declspec(noinline) static void showOOMWarning() noexcept
31-
{
32-
[[maybe_unused]] static const auto once = []() {
33-
std::thread t{ []() noexcept {
34-
MessageBoxW(nullptr, L"This application is using a highly experimental text rendering engine and has run out of memory. Text rendering will start to behave irrationally and you should restart this process.", L"Out Of Memory", MB_ICONERROR | MB_OK);
35-
} };
36-
t.detach();
37-
return false;
38-
}();
39-
}
40-
#pragma warning(pop)
41-
4228
struct TextAnalyzer final : IDWriteTextAnalysisSource, IDWriteTextAnalysisSink
4329
{
4430
constexpr TextAnalyzer(const std::vector<wchar_t>& text, std::vector<AtlasEngine::TextAnalyzerResult>& results) noexcept :
@@ -365,12 +351,14 @@ try
365351
}
366352
}
367353

368-
_api.dirtyRect = til::rect{
369-
0,
370-
_api.invalidatedRows.x,
371-
_api.cellCount.x,
372-
_api.invalidatedRows.y,
373-
};
354+
if constexpr (debugGlyphGenerationPerformance)
355+
{
356+
_api.dirtyRect = til::rect{ 0, 0, _api.cellCount.x, _api.cellCount.y };
357+
}
358+
else
359+
{
360+
_api.dirtyRect = til::rect{ 0, _api.invalidatedRows.x, _api.cellCount.x, _api.invalidatedRows.y };
361+
}
374362

375363
return S_OK;
376364
}
@@ -394,7 +382,7 @@ CATCH_RETURN()
394382

395383
[[nodiscard]] bool AtlasEngine::RequiresContinuousRedraw() noexcept
396384
{
397-
return continuousRedraw;
385+
return debugGeneralPerformance;
398386
}
399387

400388
void AtlasEngine::WaitUntilCanRender() noexcept
@@ -559,9 +547,10 @@ try
559547
const auto point = options.coordCursor;
560548
// TODO: options.coordCursor can contain invalid out of bounds coordinates when
561549
// the window is being resized and the cursor is on the last line of the viewport.
562-
const auto x = gsl::narrow_cast<uint16_t>(clamp<int>(point.X, 0, _r.cellCount.x - 1));
563-
const auto y = gsl::narrow_cast<uint16_t>(clamp<int>(point.Y, 0, _r.cellCount.y - 1));
564-
const auto right = gsl::narrow_cast<uint16_t>(x + 1 + (options.fIsDoubleWidth & (options.cursorType != CursorType::VerticalBar)));
550+
const auto x = gsl::narrow_cast<uint16_t>(clamp(point.X, 0, _r.cellCount.x - 1));
551+
const auto y = gsl::narrow_cast<uint16_t>(clamp(point.Y, 0, _r.cellCount.y - 1));
552+
const auto cursorWidth = 1 + (options.fIsDoubleWidth & (options.cursorType != CursorType::VerticalBar));
553+
const auto right = gsl::narrow_cast<uint16_t>(clamp(x + cursorWidth, 0, _r.cellCount.x - 0));
565554
const auto bottom = gsl::narrow_cast<uint16_t>(y + 1);
566555
_setCellFlags({ x, y, right, bottom }, CellFlags::Cursor, CellFlags::Cursor);
567556
}
@@ -775,7 +764,7 @@ void AtlasEngine::_createSwapChain()
775764

776765
// D3D swap chain setup (the thing that allows us to present frames on the screen)
777766
{
778-
const auto supportsFrameLatencyWaitableObject = IsWindows8Point1OrGreater();
767+
const auto supportsFrameLatencyWaitableObject = !debugGeneralPerformance && IsWindows8Point1OrGreater();
779768

780769
// With C++20 we'll finally have designated initializers.
781770
DXGI_SWAP_CHAIN_DESC1 desc{};
@@ -899,6 +888,7 @@ void AtlasEngine::_recreateSizeDependentResources()
899888
// (40x on AMD Zen1-3, which have a rep movsb performance issue. MSFT:33358259.)
900889
_r.cells = Buffer<Cell, 32>{ totalCellCount };
901890
_r.cellCount = _api.cellCount;
891+
_r.tileAllocator.setMaxArea(_api.sizeInPixel);
902892

903893
// .clear() doesn't free the memory of these buffers.
904894
// This code allows them to shrink again.
@@ -947,32 +937,14 @@ void AtlasEngine::_recreateFontDependentResources()
947937

948938
// D3D
949939
{
950-
// TODO: Consider using IDXGIAdapter3::QueryVideoMemoryInfo() and IDXGIAdapter3::RegisterVideoMemoryBudgetChangeNotificationEvent()
951-
// That way we can make better to use of a user's available video memory.
952-
953-
static constexpr size_t sizePerPixel = 4;
954-
static constexpr size_t sizeLimit = D3D10_REQ_RESOURCE_SIZE_IN_MEGABYTES * 1024 * 1024;
955-
const size_t dimensionLimit = _r.device->GetFeatureLevel() >= D3D_FEATURE_LEVEL_11_0 ? D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION : D3D10_REQ_TEXTURE2D_U_OR_V_DIMENSION;
956-
const size_t csx = _api.fontMetrics.cellSize.x;
957-
const size_t csy = _api.fontMetrics.cellSize.y;
958-
const auto xLimit = (dimensionLimit / csx) * csx;
959-
const auto pixelsPerCellRow = xLimit * csy;
960-
const auto yLimitDueToDimension = (dimensionLimit / csy) * csy;
961-
const auto yLimitDueToSize = ((sizeLimit / sizePerPixel) / pixelsPerCellRow) * csy;
962-
const auto yLimit = std::min(yLimitDueToDimension, yLimitDueToSize);
963940
const auto scaling = GetScaling();
964941

965942
_r.cellSizeDIP.x = static_cast<float>(_api.fontMetrics.cellSize.x) / scaling;
966943
_r.cellSizeDIP.y = static_cast<float>(_api.fontMetrics.cellSize.y) / scaling;
967944
_r.cellSize = _api.fontMetrics.cellSize;
968945
_r.cellCount = _api.cellCount;
969-
// x/yLimit are strictly smaller than dimensionLimit, which is smaller than a u16.
970-
_r.atlasSizeInPixelLimit = u16x2{ gsl::narrow_cast<u16>(xLimit), gsl::narrow_cast<u16>(yLimit) };
971946
_r.atlasSizeInPixel = { 0, 0 };
972-
// The first Cell at {0, 0} is always our cursor texture.
973-
// --> The first glyph starts at {1, 0}.
974-
_r.atlasPosition.x = _api.fontMetrics.cellSize.x;
975-
_r.atlasPosition.y = 0;
947+
_r.tileAllocator = TileAllocator{ _r.cellSize, _api.sizeInPixel };
976948

977949
_r.glyphs = {};
978950
_r.glyphQueue = {};
@@ -1118,26 +1090,6 @@ void AtlasEngine::_setCellFlags(u16r coords, CellFlags mask, CellFlags bits) noe
11181090
}
11191091
}
11201092

1121-
AtlasEngine::u16x2 AtlasEngine::_allocateAtlasTile() noexcept
1122-
{
1123-
const auto ret = _r.atlasPosition;
1124-
1125-
_r.atlasPosition.x += _r.cellSize.x;
1126-
if (_r.atlasPosition.x >= _r.atlasSizeInPixelLimit.x)
1127-
{
1128-
_r.atlasPosition.x = 0;
1129-
_r.atlasPosition.y += _r.cellSize.y;
1130-
if (_r.atlasPosition.y >= _r.atlasSizeInPixelLimit.y)
1131-
{
1132-
_r.atlasPosition.x = _r.cellSize.x;
1133-
_r.atlasPosition.y = 0;
1134-
showOOMWarning();
1135-
}
1136-
}
1137-
1138-
return ret;
1139-
}
1140-
11411093
void AtlasEngine::_flushBufferLine()
11421094
{
11431095
if (_api.bufferLine.empty())
@@ -1449,11 +1401,10 @@ void AtlasEngine::_emplaceGlyph(IDWriteFontFace* fontFace, size_t bufferPos1, si
14491401
auto attributes = _api.attributes;
14501402
attributes.cellCount = cellCount;
14511403

1452-
const auto [it, inserted] = _r.glyphs.emplace(std::piecewise_construct, std::forward_as_tuple(attributes, gsl::narrow<u16>(charCount), chars), std::forward_as_tuple());
1453-
const auto& key = it->first;
1454-
auto& value = it->second;
1404+
AtlasKey key{ attributes, gsl::narrow<u16>(charCount), chars };
1405+
const AtlasValue* valueRef = _r.glyphs.find(key);
14551406

1456-
if (inserted)
1407+
if (!valueRef)
14571408
{
14581409
// Do fonts exist *in practice* which contain both colored and uncolored glyphs? I'm pretty sure...
14591410
// However doing it properly means using either of:
@@ -1481,17 +1432,28 @@ void AtlasEngine::_emplaceGlyph(IDWriteFontFace* fontFace, size_t bufferPos1, si
14811432
WI_SetFlagIf(flags, CellFlags::ColoredGlyph, fontFace2 && fontFace2->IsColorFont());
14821433
}
14831434

1484-
const auto coords = value.initialize(flags, cellCount);
1435+
// The AtlasValue constructor fills the `coords` variable with a pointer to an array
1436+
// of at least `cellCount` elements. I did this so that I don't have to type out
1437+
// `value.data()->coords` again, despite the constructor having all the data necessary.
1438+
u16x2* coords;
1439+
AtlasValue value{ flags, cellCount, &coords };
1440+
14851441
for (u16 i = 0; i < cellCount; ++i)
14861442
{
1487-
coords[i] = _allocateAtlasTile();
1443+
coords[i] = _r.tileAllocator.allocate(_r.glyphs);
14881444
}
14891445

1490-
_r.glyphQueue.push_back(AtlasQueueItem{ &key, &value });
1446+
const auto it = _r.glyphs.insert(std::move(key), std::move(value));
1447+
valueRef = &it->second;
1448+
_r.glyphQueue.emplace_back(&it->first, &it->second);
14911449
_r.maxEncounteredCellCount = std::max(_r.maxEncounteredCellCount, cellCount);
14921450
}
14931451

1494-
const auto valueData = value.data();
1452+
// For some reason MSVC doesn't understand that valueRef is overwritten in the branch above, resulting in:
1453+
// C26430: Symbol 'valueRef' is not tested for nullness on all paths (f.23).
1454+
__assume(valueRef != nullptr);
1455+
1456+
const auto valueData = valueRef->data();
14951457
const auto coords = &valueData->coords[0];
14961458
const auto data = _getCell(x1, _api.lastPaintBufferLineCoord.y);
14971459

0 commit comments

Comments
 (0)