Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AtlasEngine: Implement LRU invalidation for glyph tiles #13458

Merged
5 commits merged into from
Jul 11, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/renderer/atlas/AtlasEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1402,7 +1402,7 @@ void AtlasEngine::_emplaceGlyph(IDWriteFontFace* fontFace, size_t bufferPos1, si
attributes.cellCount = cellCount;

AtlasKey key{ attributes, gsl::narrow<u16>(charCount), chars };
auto valueRef = _r.glyphs.find(key);
const AtlasValue* valueRef = _r.glyphs.find(key);

if (!valueRef)
{
Expand Down Expand Up @@ -1432,8 +1432,12 @@ void AtlasEngine::_emplaceGlyph(IDWriteFontFace* fontFace, size_t bufferPos1, si
WI_SetFlagIf(flags, CellFlags::ColoredGlyph, fontFace2 && fontFace2->IsColorFont());
}

// The AtlasValue constructor fills the `coords` variable with a pointer to an array
// of at least `cellCount` elements. I did this so that I don't have to type out
// `value.data()->coords` again, despite the constructor having all the data necessary.
u16x2* coords;
AtlasValue value{ flags, cellCount, &coords };

for (u16 i = 0; i < cellCount; ++i)
{
coords[i] = _r.tileAllocator.allocate(_r.glyphs);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... it can split a multi-cell glyph into different pieces??? :O

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least, I thought it could because you give it the opportunity to allocate once per cell, and during fragmentation it might not get two tiles next to eachother

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah that totally works. It needs to, because of very long ligatures. My initial version of this engine failed to render any ligatures wider than 2 cells because of this reason until I figured I could just split glyphs into independent tiles that aren't necessarily next to each other, thereby solving the texture fragmentation issue.

Expand Down
53 changes: 46 additions & 7 deletions src/renderer/atlas/AtlasEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,7 @@ namespace Microsoft::Console::Render
const auto it = _map.find(key);
if (it != _map.end())
{
// Move the key to the head of the LRU queue.
_lru.splice(_lru.begin(), _lru, *it);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

holy crap, this is what does the LRU magic

return &(*it)->second;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this particular indirection?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it's &((*it)->second), not (&(*it))->second

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's a bit ugly, because the _map iterator is pointing to another iterator (namely a list iterator), so we have to dereference it twice - once with * and once with ->.

}
Expand All @@ -568,8 +569,10 @@ namespace Microsoft::Console::Render

std::list<std::pair<AtlasKey, AtlasValue>>::iterator insert(AtlasKey&& key, AtlasValue&& value)
{
// && decays to & when passed as an argument to emplace().
// What a fantastic language.
// Insert the key/value right at the head of the LRU queue, just like find().
//
// && decays to & if the argument is named, because C++ is a simple language
// and so you have to std::move it again, because C++ is a simple language.
_lru.emplace_front(std::move(key), std::move(value));
auto it = _lru.begin();
_map.emplace(it);
Expand Down Expand Up @@ -602,6 +605,18 @@ namespace Microsoft::Console::Render
std::unordered_set<std::list<std::pair<AtlasKey, AtlasValue>>::iterator, AtlasKeyHasher, AtlasKeyEq> _map;
};

// TileAllocator yields `tileSize`-sized tiles for our texture atlas.
// While doing so it'll grow the atlas size() by a factor of 2 if needed.
// Once the setMaxArea() is exceeded it'll stop growing and instead
// snatch tiles back from the oldest TileHashMap entries.
//
// The quadratic growth works by alternating the size()
// between an 1:1 and 2:1 aspect ratio, like so:
// (64,64) -> (128,64) -> (128,128) -> (256,128) -> (256,256)
// These initial tile positions allocate() returns are in a Z
// pattern over the available space in the atlas texture.
// You can log the `return _pos;` in allocate() using "Tracepoint"s
// in Visual Studio if you'd like to understand the Z pattern better.
struct TileAllocator
{
TileAllocator() = default;
Expand All @@ -624,7 +639,7 @@ namespace Microsoft::Console::Render
{
// _generate() uses a quadratic growth factor for _size's area.
// Once it exceeds the _maxArea, it'll start snatching tiles back from the
// TileHashMap using it's LRU queue. Since _size will at least reach half
// TileHashMap using its LRU queue. Since _size will at least reach half
// of _maxSize (because otherwise it could still grow by a factor of 2)
// and by ensuring that _maxArea is at least twice the window size
// we make it impossible* for _generate() to return false before
Expand All @@ -637,7 +652,10 @@ namespace Microsoft::Console::Render

void setMaxArea(size_t max) noexcept
{
_maxArea = clamp(max, _absoluteMinArea, _absoluteMaxArea);
// We need to reserve at least 1 extra `tileArea`, because the tile
// at position {0,0} is already reserved for the cursor texture.
const auto tileArea = static_cast<size_t>(_tileSize.x) * static_cast<size_t>(_tileSize.y);
_maxArea = clamp(max + tileArea, _absoluteMinArea, _absoluteMaxArea);
_updateCanGenerate();
}

Expand All @@ -659,13 +677,20 @@ namespace Microsoft::Console::Render
}

private:
// This method generates the Z pattern coordinates
// described above in the TileAllocator comment.
bool _generate() noexcept
{
if (!_canGenerate)
{
return false;
}

// We need to backup _pos/_size in case our resize below exceeds _maxArea.
// In that case we have to restore _pos/_size so that if _maxArea is increased
// (window resize for instance), we can pick up were we previously left off.
lhecker marked this conversation as resolved.
Show resolved Hide resolved
const auto pos = _pos;

_pos.x += _tileSize.x;
if (_pos.x <= _limit.x)
{
Expand All @@ -679,9 +704,12 @@ namespace Microsoft::Console::Render
return true;
}

// Same as for pos.
const auto size = _size;

// This implements a quadratic growth factor for _size, by
// alternating between an 1:1 and 2:1 aspect ratio, like so:
// (64,64) -> (128,64) -> (128,128) -> (256,128) -> (256,256)
// (64,64) -> (128,64) -> (128,128) -> (256,128) -> (256,256)
// This behavior is strictly dependent on setMaxArea(u16x2)'s
// behavior. See it's comment for an explanation.
if (_size.x == _size.y)
Expand All @@ -694,10 +722,19 @@ namespace Microsoft::Console::Render
_size.y *= 2;
_pos.x = 0;
}
_limit = { gsl::narrow_cast<u16>(_size.x - _tileSize.x), gsl::narrow_cast<u16>(_size.y - _tileSize.y) };
_originX = _pos.x;

_updateCanGenerate();
if (_canGenerate)
{
_limit = { gsl::narrow_cast<u16>(_size.x - _tileSize.x), gsl::narrow_cast<u16>(_size.y - _tileSize.y) };
_originX = _pos.x;
}
else
{
_size = size;
_pos = pos;
}

return _canGenerate;
}

Expand All @@ -721,6 +758,8 @@ namespace Microsoft::Console::Render
// Coincidentially that's exactly what we want as the cursor texture lives at {0, 0}.
u16x2 _pos;
u16 _originX = 0;
// Indicates whether we've exhausted our Z pattern across the atlas texture.
// If this is false, we have to snatch tiles back from TileHashMap.
bool _canGenerate = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aka "has space"?

};

Expand Down