Initialize rows lazily (#15524)

For a 120x9001 terminal, a01500f reduced the private working set of
conhost by roughly 0.7MB, presumably due to tighter `ROW` packing, but
also increased it by 2.1MB due to the addition of the `_charOffsets`
array on each `ROW` instance. An option to fix this would be to only
allocate a `_charOffsets` if the first wide or complex Unicode glyph
is encountered. But on one hand this would be quite western-centric
and unfairly hurt most languages that exist and on another we can get
rid of the `_charOffsets` array entirely in the future by injecting
ZWNJs if a write begins with a combining glyph and just recount each
row from the start. That's still faster than fragmented memory.

This commit goes a different way and instead reduces the working
set of conhost after it launches from 7MB down to just 2MB,
by only committing ROWs when they're first used.

Finally, it adds a "scratchpad" row which can be used to build
more complex contents, for instance to horizontally scroll them.

## Validation Steps Performed
* Traditional resize
  * Horizontal shrinking works 
  * Vertical shrinking works  and cursor stays in the viewport 
* Reflow works 
* Filling the buffer with ASCII works  and no leaks 
* Filling the buffer with complex Unicode works  and no leaks 
* `^[[3J` erases scrollback 
* Test `ScrollRows` with a positive delta 
* I don't know how to test `Reset`.  Unit tests use it though
This commit is contained in:
Leonard Hecker
2023-06-10 15:17:18 +02:00
committed by GitHub
parent 17596d2623
commit 612b00cd44
16 changed files with 375 additions and 255 deletions

View File

@@ -440,6 +440,7 @@ DECMSR
DECNKM
DECNRCM
DECOM
decommit
DECPCTERM
DECPS
DECRARA

View File

@@ -82,10 +82,7 @@ ROW::ROW(wchar_t* charsBuffer, uint16_t* charOffsetsBuffer, uint16_t rowWidth, c
_attr{ rowWidth, fillAttribute },
_columnCount{ rowWidth }
{
if (_chars.data())
{
_init();
}
}
void ROW::SetWrapForced(const bool wrap) noexcept
@@ -147,6 +144,15 @@ void ROW::TransferAttributes(const til::small_rle<TextAttribute, uint16_t, 1>& a
_attr.resize_trailing_extent(gsl::narrow<uint16_t>(newWidth));
}
void ROW::CopyFrom(const ROW& source)
{
til::CoordType begin = 0;
CopyTextFrom(0, til::CoordTypeMax, source, begin, til::CoordTypeMax);
TransferAttributes(source.Attributes(), _columnCount);
_lineRendition = source._lineRendition;
_wrapForced = source._wrapForced;
}
// Returns the previous possible cursor position, preceding the given column.
// Returns 0 if column is less than or equal to 0.
til::CoordType ROW::NavigateToPrevious(til::CoordType column) const noexcept
@@ -445,7 +451,7 @@ catch (...)
charsConsumed = ch - chBeg;
}
til::CoordType ROW::CopyRangeFrom(til::CoordType columnBegin, til::CoordType columnLimit, const ROW& other, til::CoordType& otherBegin, til::CoordType otherLimit)
til::CoordType ROW::CopyTextFrom(til::CoordType columnBegin, til::CoordType columnLimit, const ROW& other, til::CoordType& otherBegin, til::CoordType otherLimit)
try
{
const auto otherColBeg = other._clampedColumnInclusive(otherBegin);
@@ -464,8 +470,11 @@ try
}
WriteHelper h{ *this, columnBegin, columnLimit, chars };
if (!h.IsValid())
// If we were to copy text from ourselves, we'd overwrite
// our _charOffsets and break Finish() which reads from it.
if (!h.IsValid() || this == &other)
{
assert(false); // You probably shouldn't call this function in the first place.
return h.colBeg;
}
// Any valid charOffsets array is at least 2 elements long (the 1st element is the start offset and the 2nd
@@ -477,7 +486,7 @@ try
otherBegin = other.size();
return h.colBeg;
}
h.CopyRangeFrom(charOffsets);
h.CopyTextFrom(charOffsets);
h.Finish();
otherBegin += h.colEnd - h.colBeg;
@@ -489,7 +498,7 @@ catch (...)
throw;
}
[[msvc::forceinline]] void ROW::WriteHelper::CopyRangeFrom(const std::span<const uint16_t>& charOffsets) noexcept
[[msvc::forceinline]] void ROW::WriteHelper::CopyTextFrom(const std::span<const uint16_t>& charOffsets) noexcept
{
// Since our `charOffsets` input is already in columns (just like the `ROW::_charOffsets`),
// we can directly look up the end char-offset, but...

View File

@@ -60,6 +60,26 @@ struct RowWriteState
class ROW final
{
public:
// The implicit agreement between ROW and TextBuffer is that TextBuffer supplies ROW with a charsBuffer of at
// least `columns * sizeof(wchar_t)` bytes and a charOffsetsBuffer of at least `(columns + 1) * sizeof(uint16_t)`
// bytes (see ROW::_charOffsets for why it needs space for 1 additional offset).
// These methods exists to make this agreement explicit and serve as a reminder.
//
// TextBuffer calculates the distance in bytes between two ROWs (_bufferRowStride) as the sum of these values.
// As such it's important that we return sizes with a minimum alignment of alignof(ROW).
static constexpr size_t CalculateRowSize() noexcept
{
return sizeof(ROW);
}
static constexpr size_t CalculateCharsBufferSize(size_t columns) noexcept
{
return (columns * sizeof(wchar_t) + 16) & ~15;
}
static constexpr size_t CalculateCharOffsetsBufferSize(size_t columns) noexcept
{
return (columns * sizeof(uint16_t) + 16) & ~15;
}
ROW() = default;
ROW(wchar_t* charsBuffer, uint16_t* charOffsetsBuffer, uint16_t rowWidth, const TextAttribute& fillAttribute);
@@ -78,6 +98,7 @@ public:
void Reset(const TextAttribute& attr);
void TransferAttributes(const til::small_rle<TextAttribute, uint16_t, 1>& attr, til::CoordType newWidth);
void CopyFrom(const ROW& source);
til::CoordType NavigateToPrevious(til::CoordType column) const noexcept;
til::CoordType NavigateToNext(til::CoordType column) const noexcept;
@@ -88,7 +109,7 @@ public:
void ReplaceAttributes(til::CoordType beginIndex, til::CoordType endIndex, const TextAttribute& newAttr);
void ReplaceCharacters(til::CoordType columnBegin, til::CoordType width, const std::wstring_view& chars);
void ReplaceText(RowWriteState& state);
til::CoordType CopyRangeFrom(til::CoordType columnBegin, til::CoordType columnLimit, const ROW& other, til::CoordType& otherBegin, til::CoordType otherLimit);
til::CoordType CopyTextFrom(til::CoordType columnBegin, til::CoordType columnLimit, const ROW& other, til::CoordType& otherBegin, til::CoordType otherLimit);
til::small_rle<TextAttribute, uint16_t, 1>& Attributes() noexcept;
const til::small_rle<TextAttribute, uint16_t, 1>& Attributes() const noexcept;
@@ -121,7 +142,7 @@ private:
bool IsValid() const noexcept;
void ReplaceCharacters(til::CoordType width) noexcept;
void ReplaceText() noexcept;
void CopyRangeFrom(const std::span<const uint16_t>& charOffsets) noexcept;
void CopyTextFrom(const std::span<const uint16_t>& charOffsets) noexcept;
void Finish();
// Parent pointer.

View File

@@ -42,10 +42,166 @@ TextBuffer::TextBuffer(til::size screenBufferSize,
// Guard against resizing the text buffer to 0 columns/rows, which would break being able to insert text.
screenBufferSize.width = std::max(screenBufferSize.width, 1);
screenBufferSize.height = std::max(screenBufferSize.height, 1);
_charBuffer = _allocateBuffer(screenBufferSize, _currentAttributes, _storage);
_UpdateSize();
_reserve(screenBufferSize, defaultAttributes);
}
TextBuffer::~TextBuffer()
{
if (_buffer)
{
_destroy();
}
}
// I put these functions in a block at the start of the class, because they're the most
// fundamental aspect of TextBuffer: It implements the basic gap buffer text storage.
// It's also fairly tricky code.
#pragma region buffer management
#pragma warning(push)
#pragma warning(disable : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
#pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1).
// MEM_RESERVEs memory sufficient to store height-many ROW structs,
// as well as their ROW::_chars and ROW::_charOffsets buffers.
//
// We use explicit virtual memory allocations to not taint the general purpose allocator
// with our huge allocation, as well as to be able to reduce the private working set of
// the application by only committing what we actually need. This reduces conhost's
// memory usage from ~7MB down to just ~2MB at startup in the general case.
void TextBuffer::_reserve(til::size screenBufferSize, const TextAttribute& defaultAttributes)
{
const auto w = gsl::narrow<uint16_t>(screenBufferSize.width);
const auto h = gsl::narrow<uint16_t>(screenBufferSize.height);
constexpr auto rowSize = ROW::CalculateRowSize();
const auto charsBufferSize = ROW::CalculateCharsBufferSize(w);
const auto charOffsetsBufferSize = ROW::CalculateCharOffsetsBufferSize(w);
const auto rowStride = rowSize + charsBufferSize + charOffsetsBufferSize;
assert(rowStride % alignof(ROW) == 0);
// 65535*65535 cells would result in a allocSize of 8GiB.
// --> Use uint64_t so that we can safely do our calculations even on x86.
// We allocate 1 additional row, which will be used for GetScratchpadRow().
const auto rowCount = ::base::strict_cast<uint64_t>(h) + 1;
const auto allocSize = gsl::narrow<size_t>(rowCount * rowStride);
// NOTE: Modifications to this block of code might have to be mirrored over to ResizeTraditional().
// It constructs a temporary TextBuffer and then extracts the members below, overwriting itself.
_buffer = wil::unique_virtualalloc_ptr<std::byte>{
static_cast<std::byte*>(THROW_LAST_ERROR_IF_NULL(VirtualAlloc(nullptr, allocSize, MEM_RESERVE, PAGE_READWRITE)))
};
_bufferEnd = _buffer.get() + allocSize;
_commitWatermark = _buffer.get();
_initialAttributes = defaultAttributes;
_bufferRowStride = rowStride;
_bufferOffsetChars = rowSize;
_bufferOffsetCharOffsets = rowSize + charsBufferSize;
_width = w;
_height = h;
}
// MEM_COMMITs the memory and constructs all ROWs up to and including the given row pointer.
// It's expected that the caller verifies the parameter. It goes hand in hand with _getRowByOffsetDirect().
//
// Declaring this function as noinline allows _getRowByOffsetDirect() to be inlined,
// which improves overall TextBuffer performance by ~6%. And all it cost is this annotation.
// The compiler doesn't understand the likelihood of our branches. (PGO does, but that's imperfect.)
__declspec(noinline) void TextBuffer::_commit(const std::byte* row)
{
const auto rowEnd = row + _bufferRowStride;
const auto remaining = gsl::narrow_cast<uintptr_t>(_bufferEnd - _commitWatermark);
const auto minimum = gsl::narrow_cast<uintptr_t>(rowEnd - _commitWatermark);
const auto ideal = minimum + _bufferRowStride * _commitReadAheadRowCount;
const auto size = std::min(remaining, ideal);
THROW_LAST_ERROR_IF_NULL(VirtualAlloc(_commitWatermark, size, MEM_COMMIT, PAGE_READWRITE));
_construct(_commitWatermark + size);
}
// Destructs and MEM_DECOMMITs all previously constructed ROWs.
// You can use this (or rather the Reset() method) to fully clear the TextBuffer.
void TextBuffer::_decommit() noexcept
{
_destroy();
VirtualFree(_buffer.get(), 0, MEM_DECOMMIT);
_commitWatermark = _buffer.get();
}
// Constructs ROWs up to (excluding) the ROW pointed to by `until`.
void TextBuffer::_construct(const std::byte* until) noexcept
{
for (; _commitWatermark < until; _commitWatermark += _bufferRowStride)
{
const auto row = reinterpret_cast<ROW*>(_commitWatermark);
const auto chars = reinterpret_cast<wchar_t*>(_commitWatermark + _bufferOffsetChars);
const auto indices = reinterpret_cast<uint16_t*>(_commitWatermark + _bufferOffsetCharOffsets);
std::construct_at(row, chars, indices, _width, _initialAttributes);
}
}
// Destroys all previously constructed ROWs.
// Be careful! This doesn't reset any of the members, in particular the _commitWatermark.
void TextBuffer::_destroy() const noexcept
{
for (auto it = _buffer.get(); it < _commitWatermark; it += _bufferRowStride)
{
std::destroy_at(reinterpret_cast<ROW*>(it));
}
}
// This function is "direct" because it trusts the caller to properly wrap the "offset"
// parameter modulo the _height of the buffer, etc. But keep in mind that a offset=0
// is the GetScratchpadRow() and not the GetRowByOffset(0). That one is offset=1.
ROW& TextBuffer::_getRowByOffsetDirect(size_t offset)
{
const auto row = _buffer.get() + _bufferRowStride * offset;
THROW_HR_IF(E_UNEXPECTED, row < _buffer.get() || row >= _bufferEnd);
if (row >= _commitWatermark)
{
_commit(row);
}
return *reinterpret_cast<ROW*>(row);
}
// Retrieves a row from the buffer by its offset from the first row of the text buffer
// (what corresponds to the top row of the screen buffer).
const ROW& TextBuffer::GetRowByOffset(const til::CoordType index) const
{
// The const_cast is safe because "const" never had any meaning in C++ in the first place.
#pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile (type.3).
return const_cast<TextBuffer*>(this)->GetRowByOffset(index);
}
// Retrieves a row from the buffer by its offset from the first row of the text buffer
// (what corresponds to the top row of the screen buffer).
ROW& TextBuffer::GetRowByOffset(const til::CoordType index)
{
// Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows.
auto offset = (_firstRow + index) % _height;
// Support negative wrap around. This way an index of -1 will
// wrap to _rowCount-1 and make implementing scrolling easier.
if (offset < 0)
{
offset += _height;
}
// We add 1 to the row offset, because row "0" is the one returned by GetScratchpadRow().
return _getRowByOffsetDirect(gsl::narrow_cast<size_t>(offset) + 1);
}
// Returns a row filled with whitespace and the current attributes, for you to freely use.
ROW& TextBuffer::GetScratchpadRow()
{
return _getRowByOffsetDirect(0);
}
#pragma warning(pop)
#pragma endregion
// Routine Description:
// - Copies properties from another text buffer into this one.
// - This is primarily to copy properties that would otherwise not be specified during CreateInstance
@@ -66,35 +222,7 @@ void TextBuffer::CopyProperties(const TextBuffer& OtherBuffer) noexcept
// - Total number of rows in the buffer
til::CoordType TextBuffer::TotalRowCount() const noexcept
{
return gsl::narrow_cast<til::CoordType>(_storage.size());
}
// Routine Description:
// - Retrieves a row from the buffer by its offset from the first row of the text buffer (what corresponds to
// the top row of the screen buffer)
// Arguments:
// - Number of rows down from the first row of the buffer.
// Return Value:
// - const reference to the requested row. Asserts if out of bounds.
const ROW& TextBuffer::GetRowByOffset(const til::CoordType index) const noexcept
{
// Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows.
const auto offsetIndex = gsl::narrow_cast<size_t>(_firstRow + index) % _storage.size();
return til::at(_storage, offsetIndex);
}
// Routine Description:
// - Retrieves a row from the buffer by its offset from the first row of the text buffer (what corresponds to
// the top row of the screen buffer)
// Arguments:
// - Number of rows down from the first row of the buffer.
// Return Value:
// - reference to the requested row. Asserts if out of bounds.
ROW& TextBuffer::GetRowByOffset(const til::CoordType index) noexcept
{
// Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows.
const auto offsetIndex = gsl::narrow_cast<size_t>(_firstRow + index) % _storage.size();
return til::at(_storage, offsetIndex);
return _height;
}
// Routine Description:
@@ -483,7 +611,7 @@ bool TextBuffer::InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttr
// - <none> - Always sets to wrap
//Return Value:
// - <none>
void TextBuffer::_SetWrapOnCurrentRow() noexcept
void TextBuffer::_SetWrapOnCurrentRow()
{
_AdjustWrapOnCurrentRow(true);
}
@@ -495,7 +623,7 @@ void TextBuffer::_SetWrapOnCurrentRow() noexcept
// - fSet - True if this row has a wrap. False otherwise.
//Return Value:
// - <none>
void TextBuffer::_AdjustWrapOnCurrentRow(const bool fSet) noexcept
void TextBuffer::_AdjustWrapOnCurrentRow(const bool fSet)
{
// The vertical position of the cursor represents the current row we're manipulating.
const auto uiCurrentRowOffset = GetCursor().GetPosition().y;
@@ -651,7 +779,7 @@ til::point TextBuffer::GetLastNonSpaceCharacter(std::optional<const Microsoft::C
// Return Value:
// - Coordinate position in screen coordinates of the character just before the cursor.
// - NOTE: Will return 0,0 if already in the top left corner
til::point TextBuffer::_GetPreviousFromCursor() const noexcept
til::point TextBuffer::_GetPreviousFromCursor() const
{
auto coordPosition = GetCursor().GetPosition();
@@ -683,43 +811,7 @@ const til::CoordType TextBuffer::GetFirstRowIndex() const noexcept
const Viewport TextBuffer::GetSize() const noexcept
{
return _size;
}
wil::unique_virtualalloc_ptr<std::byte> TextBuffer::_allocateBuffer(til::size sz, const TextAttribute& attributes, std::vector<ROW>& rows)
{
const auto w = gsl::narrow<uint16_t>(sz.width);
const auto h = gsl::narrow<uint16_t>(sz.height);
const auto charsBytes = w * sizeof(wchar_t);
// The ROW::_indices array stores 1 more item than the buffer is wide.
// That extra column stores the past-the-end _chars pointer.
const auto indicesBytes = w * sizeof(uint16_t) + sizeof(uint16_t);
const auto rowStride = charsBytes + indicesBytes;
// 65535*65535 cells would result in a charsAreaSize of 8GiB.
// --> Use uint64_t so that we can safely do our calculations even on x86.
const auto allocSize = gsl::narrow<size_t>(::base::strict_cast<uint64_t>(rowStride) * ::base::strict_cast<uint64_t>(h));
auto buffer = wil::unique_virtualalloc_ptr<std::byte>{ static_cast<std::byte*>(VirtualAlloc(nullptr, allocSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)) };
THROW_IF_NULL_ALLOC(buffer);
auto data = std::span{ buffer.get(), allocSize }.begin();
rows.resize(h);
for (auto& row : rows)
{
const auto chars = til::bit_cast<wchar_t*>(&*data);
const auto indices = til::bit_cast<uint16_t*>(&*(data + charsBytes));
row = { chars, indices, w, attributes };
data += rowStride;
}
return buffer;
}
void TextBuffer::_UpdateSize()
{
_size = Viewport::FromDimensions({ _storage.at(0).size(), gsl::narrow<til::CoordType>(_storage.size()) });
return Viewport::FromDimensions({ _width, _height });
}
void TextBuffer::_SetFirstRowIndex(const til::CoordType FirstRowIndex) noexcept
@@ -727,27 +819,21 @@ void TextBuffer::_SetFirstRowIndex(const til::CoordType FirstRowIndex) noexcept
_firstRow = FirstRowIndex;
}
void TextBuffer::ScrollRows(const til::CoordType firstRow, const til::CoordType size, const til::CoordType delta)
void TextBuffer::ScrollRows(const til::CoordType firstRow, til::CoordType size, const til::CoordType delta)
{
// If we don't have to move anything, leave early.
if (delta == 0)
{
return;
}
// OK. We're about to play games by moving rows around within the deque to
// scroll a massive region in a faster way than copying things.
// To make this easier, first correct the circular buffer to have the first row be 0 again.
if (_firstRow != 0)
{
// Rotate the buffer to put the first row at the front.
std::rotate(_storage.begin(), _storage.begin() + _firstRow, _storage.end());
// Since the for() loop uses !=, we must ensure that size is positive.
// A negative size doesn't make any sense anyways.
size = std::max(0, size);
// The first row is now at the top.
_firstRow = 0;
}
til::CoordType y = 0;
til::CoordType end = 0;
til::CoordType step = 0;
// Rotate just the subsection specified
if (delta < 0)
{
// The layout is like this:
@@ -757,33 +843,20 @@ void TextBuffer::ScrollRows(const til::CoordType firstRow, const til::CoordType
// | 0 begin
// | 1
// | 2
// | 3 A. begin + firstRow + delta (because delta is negative)
// | 3 A. firstRow + delta (because delta is negative)
// | 4
// | 5 B. begin + firstRow
// | 5 B. firstRow
// | 6
// | 7
// | 8 C. begin + firstRow + size
// | 8 C. firstRow + size
// | 9
// | 10
// | 11
// - end
// We want B to slide up to A (the negative delta) and everything from [B,C) to slide up with it.
// So the final layout will be
// --- (storage) ----
// | 0 begin
// | 1
// | 2
// | 5
// | 6
// | 7
// | 3
// | 4
// | 8
// | 9
// | 10
// | 11
// - end
std::rotate(_storage.begin() + firstRow + delta, _storage.begin() + firstRow, _storage.begin() + firstRow + size);
y = firstRow;
end = firstRow + size;
step = 1;
}
else
{
@@ -796,31 +869,23 @@ void TextBuffer::ScrollRows(const til::CoordType firstRow, const til::CoordType
// | 2
// | 3
// | 4
// | 5 A. begin + firstRow
// | 5 A. firstRow
// | 6
// | 7
// | 8 B. begin + firstRow + size
// | 8 B. firstRow + size
// | 9
// | 10 C. begin + firstRow + size + delta
// | 10 C. firstRow + size + delta
// | 11
// - end
// We want B-1 to slide down to C-1 (the positive delta) and everything from [A, B) to slide down with it.
// So the final layout will be
// --- (storage) ----
// | 0 begin
// | 1
// | 2
// | 3
// | 4
// | 8
// | 9
// | 5
// | 6
// | 7
// | 10
// | 11
// - end
std::rotate(_storage.begin() + firstRow, _storage.begin() + firstRow + size, _storage.begin() + firstRow + size + delta);
y = firstRow + size - 1;
end = firstRow - 1;
step = -1;
}
for (; y != end; y += step)
{
GetRowByOffset(y + delta).CopyFrom(GetRowByOffset(y));
}
}
@@ -869,7 +934,7 @@ void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition, cons
}
}
void TextBuffer::ResetLineRenditionRange(const til::CoordType startRow, const til::CoordType endRow) noexcept
void TextBuffer::ResetLineRenditionRange(const til::CoordType startRow, const til::CoordType endRow)
{
for (auto row = startRow; row < endRow; row++)
{
@@ -877,37 +942,37 @@ void TextBuffer::ResetLineRenditionRange(const til::CoordType startRow, const ti
}
}
LineRendition TextBuffer::GetLineRendition(const til::CoordType row) const noexcept
LineRendition TextBuffer::GetLineRendition(const til::CoordType row) const
{
return GetRowByOffset(row).GetLineRendition();
}
bool TextBuffer::IsDoubleWidthLine(const til::CoordType row) const noexcept
bool TextBuffer::IsDoubleWidthLine(const til::CoordType row) const
{
return GetLineRendition(row) != LineRendition::SingleWidth;
}
til::CoordType TextBuffer::GetLineWidth(const til::CoordType row) const noexcept
til::CoordType TextBuffer::GetLineWidth(const til::CoordType row) const
{
// Use shift right to quickly divide the width by 2 for double width lines.
const auto scale = IsDoubleWidthLine(row) ? 1 : 0;
return GetSize().Width() >> scale;
}
til::point TextBuffer::ClampPositionWithinLine(const til::point position) const noexcept
til::point TextBuffer::ClampPositionWithinLine(const til::point position) const
{
const auto rightmostColumn = GetLineWidth(position.y) - 1;
return { std::min(position.x, rightmostColumn), position.y };
}
til::point TextBuffer::ScreenToBufferPosition(const til::point position) const noexcept
til::point TextBuffer::ScreenToBufferPosition(const til::point position) const
{
// Use shift right to quickly divide the X pos by 2 for double width lines.
const auto scale = IsDoubleWidthLine(position.y) ? 1 : 0;
return { position.x >> scale, position.y };
}
til::point TextBuffer::BufferToScreenPosition(const til::point position) const noexcept
til::point TextBuffer::BufferToScreenPosition(const til::point position) const
{
// Use shift left to quickly multiply the X pos by 2 for double width lines.
const auto scale = IsDoubleWidthLine(position.y) ? 1 : 0;
@@ -917,14 +982,10 @@ til::point TextBuffer::BufferToScreenPosition(const til::point position) const n
// Routine Description:
// - Resets the text contents of this buffer with the default character
// and the default current color attributes
void TextBuffer::Reset()
void TextBuffer::Reset() noexcept
{
const auto attr = GetCurrentAttributes();
for (auto& row : _storage)
{
row.Reset(attr);
}
_decommit();
_initialAttributes = _currentAttributes;
}
// Routine Description:
@@ -941,55 +1002,34 @@ void TextBuffer::Reset()
try
{
til::CoordType TopRow = 0; // new top row of the screen buffer
if (newSize.height <= GetCursor().GetPosition().y)
{
TopRow = GetCursor().GetPosition().y - newSize.height + 1;
}
const auto TopRowIndex = gsl::narrow_cast<size_t>(_firstRow + TopRow) % _storage.size();
TextBuffer newBuffer{ newSize, _currentAttributes, 0, false, _renderer };
const auto cursorRow = GetCursor().GetPosition().y;
const auto copyableRows = std::min<til::CoordType>(_height, newSize.height);
til::CoordType srcRow = 0;
til::CoordType dstRow = 0;
std::vector<ROW> newStorage;
auto newBuffer = _allocateBuffer(newSize, _currentAttributes, newStorage);
// This basically imitates a std::rotate_copy(first, mid, last), but uses ROW::CopyRangeFrom() to do the copying.
if (cursorRow >= newSize.height)
{
const auto first = _storage.begin();
const auto last = _storage.end();
const auto mid = first + TopRowIndex;
auto dest = newStorage.begin();
std::span<ROW> sourceRanges[]{
{ mid, last },
{ first, mid },
};
// Ensure we don't copy more from `_storage` than fit into `newStorage`.
if (sourceRanges[0].size() > newStorage.size())
{
sourceRanges[0] = sourceRanges[0].subspan(0, newStorage.size());
}
if (const auto remaining = newStorage.size() - sourceRanges[0].size(); sourceRanges[1].size() > remaining)
{
sourceRanges[1] = sourceRanges[1].subspan(0, remaining);
srcRow = cursorRow - newSize.height + 1;
}
for (const auto& sourceRange : sourceRanges)
for (; dstRow < copyableRows; ++dstRow, ++srcRow)
{
for (const auto& oldRow : sourceRange)
{
til::CoordType begin = 0;
dest->CopyRangeFrom(0, til::CoordTypeMax, oldRow, begin, til::CoordTypeMax);
dest->TransferAttributes(oldRow.Attributes(), newSize.width);
++dest;
}
}
newBuffer.GetRowByOffset(dstRow).CopyFrom(GetRowByOffset(srcRow));
}
_charBuffer = std::move(newBuffer);
_storage = std::move(newStorage);
// NOTE: Keep this in sync with _reserve().
_buffer = std::move(newBuffer._buffer);
_bufferEnd = newBuffer._bufferEnd;
_commitWatermark = newBuffer._commitWatermark;
_initialAttributes = newBuffer._initialAttributes;
_bufferRowStride = newBuffer._bufferRowStride;
_bufferOffsetChars = newBuffer._bufferOffsetChars;
_bufferOffsetCharOffsets = newBuffer._bufferOffsetCharOffsets;
_width = newBuffer._width;
_height = newBuffer._height;
_SetFirstRowIndex(0);
_UpdateSize();
}
CATCH_RETURN();
@@ -1059,17 +1099,6 @@ void TextBuffer::TriggerNewTextNotification(const std::wstring_view newText)
}
}
// Routine Description:
// - Retrieves the first row from the underlying buffer.
// Arguments:
// - <none>
// Return Value:
// - reference to the first row.
ROW& TextBuffer::_GetFirstRow() noexcept
{
return GetRowByOffset(0);
}
// Method Description:
// - get delimiter class for buffer cell position
// - used for double click selection and uia word navigation
@@ -1078,7 +1107,7 @@ ROW& TextBuffer::_GetFirstRow() noexcept
// - wordDelimiters: the delimiters defined as a part of the DelimiterClass::DelimiterChar
// Return Value:
// - the delimiter class for the given char
DelimiterClass TextBuffer::_GetDelimiterClassAt(const til::point pos, const std::wstring_view wordDelimiters) const noexcept
DelimiterClass TextBuffer::_GetDelimiterClassAt(const til::point pos, const std::wstring_view wordDelimiters) const
{
return GetRowByOffset(pos.y).DelimiterClassAt(pos.x, wordDelimiters);
}
@@ -1144,7 +1173,7 @@ til::point TextBuffer::GetWordStart(const til::point target, const std::wstring_
// - wordDelimiters - what characters are we considering for the separation of words
// Return Value:
// - The til::point for the first character on the current/previous READABLE "word" (inclusive)
til::point TextBuffer::_GetWordStartForAccessibility(const til::point target, const std::wstring_view wordDelimiters) const noexcept
til::point TextBuffer::_GetWordStartForAccessibility(const til::point target, const std::wstring_view wordDelimiters) const
{
auto result = target;
const auto bufferSize = GetSize();
@@ -1189,7 +1218,7 @@ til::point TextBuffer::_GetWordStartForAccessibility(const til::point target, co
// - wordDelimiters - what characters are we considering for the separation of words
// Return Value:
// - The til::point for the first character on the current word or delimiter run (stopped by the left margin)
til::point TextBuffer::_GetWordStartForSelection(const til::point target, const std::wstring_view wordDelimiters) const noexcept
til::point TextBuffer::_GetWordStartForSelection(const til::point target, const std::wstring_view wordDelimiters) const
{
auto result = target;
const auto bufferSize = GetSize();
@@ -1310,7 +1339,7 @@ til::point TextBuffer::_GetWordEndForAccessibility(const til::point target, cons
// - wordDelimiters - what characters are we considering for the separation of words
// Return Value:
// - The til::point for the last character of the current word or delimiter run (stopped by right margin)
til::point TextBuffer::_GetWordEndForSelection(const til::point target, const std::wstring_view wordDelimiters) const noexcept
til::point TextBuffer::_GetWordEndForSelection(const til::point target, const std::wstring_view wordDelimiters) const
{
const auto bufferSize = GetSize();

View File

@@ -72,14 +72,21 @@ public:
const UINT cursorSize,
const bool isActiveBuffer,
Microsoft::Console::Render::Renderer& renderer);
TextBuffer(const TextBuffer& a) = delete;
TextBuffer(const TextBuffer&) = delete;
TextBuffer(TextBuffer&&) = delete;
TextBuffer& operator=(const TextBuffer&) = delete;
TextBuffer& operator=(TextBuffer&&) = delete;
~TextBuffer();
// Used for duplicating properties to another text buffer
void CopyProperties(const TextBuffer& OtherBuffer) noexcept;
// row manipulation
const ROW& GetRowByOffset(const til::CoordType index) const noexcept;
ROW& GetRowByOffset(const til::CoordType index) noexcept;
ROW& GetScratchpadRow();
const ROW& GetRowByOffset(til::CoordType index) const;
ROW& GetRowByOffset(til::CoordType index);
TextBufferCellIterator GetCellDataAt(const til::point at) const;
TextBufferCellIterator GetCellLineDataAt(const til::point at) const;
@@ -129,16 +136,16 @@ public:
void SetCurrentAttributes(const TextAttribute& currentAttributes) noexcept;
void SetCurrentLineRendition(const LineRendition lineRendition, const TextAttribute& fillAttributes);
void ResetLineRenditionRange(const til::CoordType startRow, const til::CoordType endRow) noexcept;
LineRendition GetLineRendition(const til::CoordType row) const noexcept;
bool IsDoubleWidthLine(const til::CoordType row) const noexcept;
void ResetLineRenditionRange(const til::CoordType startRow, const til::CoordType endRow);
LineRendition GetLineRendition(const til::CoordType row) const;
bool IsDoubleWidthLine(const til::CoordType row) const;
til::CoordType GetLineWidth(const til::CoordType row) const noexcept;
til::point ClampPositionWithinLine(const til::point position) const noexcept;
til::point ScreenToBufferPosition(const til::point position) const noexcept;
til::point BufferToScreenPosition(const til::point position) const noexcept;
til::CoordType GetLineWidth(const til::CoordType row) const;
til::point ClampPositionWithinLine(const til::point position) const;
til::point ScreenToBufferPosition(const til::point position) const;
til::point BufferToScreenPosition(const til::point position) const;
void Reset();
void Reset() noexcept;
[[nodiscard]] HRESULT ResizeTraditional(const til::size newSize) noexcept;
@@ -219,23 +226,26 @@ public:
interval_tree::IntervalTree<til::point, size_t> GetPatterns(const til::CoordType firstRow, const til::CoordType lastRow) const;
private:
static wil::unique_virtualalloc_ptr<std::byte> _allocateBuffer(til::size sz, const TextAttribute& attributes, std::vector<ROW>& rows);
void _reserve(til::size screenBufferSize, const TextAttribute& defaultAttributes);
void _commit(const std::byte* row);
void _decommit() noexcept;
void _construct(const std::byte* until) noexcept;
void _destroy() const noexcept;
ROW& _getRowByOffsetDirect(size_t offset);
void _UpdateSize();
void _SetFirstRowIndex(const til::CoordType FirstRowIndex) noexcept;
til::point _GetPreviousFromCursor() const noexcept;
void _SetWrapOnCurrentRow() noexcept;
void _AdjustWrapOnCurrentRow(const bool fSet) noexcept;
til::point _GetPreviousFromCursor() const;
void _SetWrapOnCurrentRow();
void _AdjustWrapOnCurrentRow(const bool fSet);
// Assist with maintaining proper buffer state for Double Byte character sequences
bool _PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute);
bool _AssertValidDoubleByteSequence(const DbcsAttribute dbcsAttribute);
ROW& _GetFirstRow() noexcept;
void _ExpandTextRow(til::inclusive_rect& selectionRow) const;
DelimiterClass _GetDelimiterClassAt(const til::point pos, const std::wstring_view wordDelimiters) const noexcept;
til::point _GetWordStartForAccessibility(const til::point target, const std::wstring_view wordDelimiters) const noexcept;
til::point _GetWordStartForSelection(const til::point target, const std::wstring_view wordDelimiters) const noexcept;
DelimiterClass _GetDelimiterClassAt(const til::point pos, const std::wstring_view wordDelimiters) const;
til::point _GetWordStartForAccessibility(const til::point target, const std::wstring_view wordDelimiters) const;
til::point _GetWordStartForSelection(const til::point target, const std::wstring_view wordDelimiters) const;
til::point _GetWordEndForAccessibility(const til::point target, const std::wstring_view wordDelimiters, const til::point limit) const;
til::point _GetWordEndForSelection(const til::point target, const std::wstring_view wordDelimiters) const noexcept;
til::point _GetWordEndForSelection(const til::point target, const std::wstring_view wordDelimiters) const;
void _PruneHyperlinks();
static void _AppendRTFText(std::ostringstream& contentBuilder, const std::wstring_view& text);
@@ -249,13 +259,67 @@ private:
std::unordered_map<size_t, std::wstring> _idsAndPatterns;
size_t _currentPatternId = 0;
wil::unique_virtualalloc_ptr<std::byte> _charBuffer;
std::vector<ROW> _storage;
// This block describes the state of the underlying virtual memory buffer that holds all ROWs, text and attributes.
// Initially memory is only allocated with MEM_RESERVE to reduce the private working set of conhost.
// ROWs are laid out like this in memory:
// ROW <-- sizeof(ROW), stores
// (padding)
// ROW::_charsBuffer <-- _width * sizeof(wchar_t)
// (padding)
// ROW::_charOffsets <-- (_width + 1) * sizeof(uint16_t)
// (padding)
// ...
// Padding may exist for alignment purposes.
//
// The base (start) address of the memory arena.
wil::unique_virtualalloc_ptr<std::byte> _buffer;
// The past-the-end pointer of the memory arena.
std::byte* _bufferEnd = nullptr;
// The range between _buffer (inclusive) and _commitWatermark (exclusive) is the range of
// memory that has already been committed via MEM_COMMIT and contains ready-to-use ROWs.
//
// The problem is that calling VirtualAlloc(MEM_COMMIT) on each ROW one by one is extremely expensive, which forces
// us to commit ROWs in batches and avoid calling it on already committed ROWs. Let's say we commit memory in
// batches of 128 ROWs. One option to know whether a ROW has already been committed is to allocate a vector<uint8_t>
// of size `(height + 127) / 128` and mark the corresponding slot as 1 if that 128-sized batch has been committed.
// That way we know not to commit it again. But ROWs aren't accessed randomly. Instead, they're usually accessed
// fairly linearly from row 1 to N. As such we can just commit ROWs up to the point of the highest accessed ROW
// plus some read-ahead of 128 ROWs. This is exactly what _commitWatermark stores: The highest accessed ROW plus
// some read-ahead. It's the amount of memory that has been committed and is ready to use.
//
// _commitWatermark will always be a multiple of _bufferRowStride away from _buffer.
// In other words, _commitWatermark itself will either point exactly onto the next ROW
// that should be committed or be equal to _bufferEnd when all ROWs are committed.
std::byte* _commitWatermark = nullptr;
// This will MEM_COMMIT 128 rows more than we need, to avoid us from having to call VirtualAlloc too often.
// This equates to roughly the following commit chunk sizes at these column counts:
// * 80 columns (the usual minimum) = 60KB chunks, 4.1MB buffer at 9001 rows
// * 120 columns (the most common) = 80KB chunks, 5.6MB buffer at 9001 rows
// * 400 columns (the usual maximum) = 220KB chunks, 15.5MB buffer at 9001 rows
// There's probably a better metric than this. (This comment was written when ROW had both,
// a _chars array containing text and a _charOffsets array contain column-to-text indices.)
static constexpr size_t _commitReadAheadRowCount = 128;
// Before TextBuffer was made to use virtual memory it initialized the entire memory arena with the initial
// attributes right away. To ensure it continues to work the way it used to, this stores these initial attributes.
TextAttribute _initialAttributes;
// ROW ---------------+--+--+
// (padding) | | v _bufferOffsetChars
// ROW::_charsBuffer | |
// (padding) | v _bufferOffsetCharOffsets
// ROW::_charOffsets |
// (padding) v _bufferRowStride
size_t _bufferRowStride = 0;
size_t _bufferOffsetChars = 0;
size_t _bufferOffsetCharOffsets = 0;
// The width of the buffer in columns.
uint16_t _width = 0;
// The height of the buffer in rows, excluding the scratchpad row.
uint16_t _height = 0;
TextAttribute _currentAttributes;
til::CoordType _firstRow = 0; // indexes top row (not necessarily 0)
Cursor _cursor;
Microsoft::Console::Types::Viewport _size;
bool _isActiveBuffer = false;

View File

@@ -287,7 +287,7 @@ ptrdiff_t TextBufferCellIterator::operator-(const TextBufferCellIterator& it)
// - Sets the coordinate position that this iterator will inspect within the text buffer on dereference.
// Arguments:
// - newPos - The new coordinate position.
void TextBufferCellIterator::_SetPos(const til::point newPos) noexcept
void TextBufferCellIterator::_SetPos(const til::point newPos)
{
if (newPos.y != _pos.y)
{
@@ -315,7 +315,7 @@ void TextBufferCellIterator::_SetPos(const til::point newPos) noexcept
// - pos - Position inside screen buffer bounds to retrieve row
// Return Value:
// - Pointer to the underlying CharRow structure
const ROW* TextBufferCellIterator::s_GetRow(const TextBuffer& buffer, const til::point pos) noexcept
const ROW* TextBufferCellIterator::s_GetRow(const TextBuffer& buffer, const til::point pos)
{
return &buffer.GetRowByOffset(pos.y);
}

View File

@@ -49,9 +49,9 @@ public:
til::point Pos() const noexcept;
protected:
void _SetPos(const til::point newPos) noexcept;
void _SetPos(const til::point newPos);
void _GenerateView() noexcept;
static const ROW* s_GetRow(const TextBuffer& buffer, const til::point pos) noexcept;
static const ROW* s_GetRow(const TextBuffer& buffer, const til::point pos);
til::small_rle<TextAttribute, uint16_t, 1>::const_iterator _attrIter;
OutputCellView _view;

View File

@@ -180,7 +180,7 @@ public:
ULONG GetCursorHeight() const noexcept override;
ULONG GetCursorPixelWidth() const noexcept override;
CursorType GetCursorStyle() const noexcept override;
bool IsCursorDoubleWidth() const noexcept override;
bool IsCursorDoubleWidth() const override;
const std::vector<Microsoft::Console::Render::RenderOverlay> GetOverlays() const noexcept override;
const bool IsGridLineDrawingAllowed() noexcept override;
const std::wstring GetHyperlinkUri(uint16_t id) const override;

View File

@@ -70,7 +70,7 @@ CursorType Terminal::GetCursorStyle() const noexcept
return _activeBuffer().GetCursor().GetType();
}
bool Terminal::IsCursorDoubleWidth() const noexcept
bool Terminal::IsCursorDoubleWidth() const
{
const auto& buffer = _activeBuffer();
const auto position = buffer.GetCursor().GetPosition();

View File

@@ -246,7 +246,7 @@ const std::vector<Microsoft::Console::Render::RenderOverlay> RenderData::GetOver
// - <none>
// Return Value:
// - true if the cursor should be drawn twice as wide as usual
bool RenderData::IsCursorDoubleWidth() const noexcept
bool RenderData::IsCursorDoubleWidth() const
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
return gci.GetActiveOutputBuffer().CursorIsDoubleWidth();

View File

@@ -36,7 +36,7 @@ public:
ULONG GetCursorHeight() const noexcept override;
CursorType GetCursorStyle() const noexcept override;
ULONG GetCursorPixelWidth() const noexcept override;
bool IsCursorDoubleWidth() const noexcept override;
bool IsCursorDoubleWidth() const override;
const std::vector<Microsoft::Console::Render::RenderOverlay> GetOverlays() const noexcept override;

View File

@@ -2609,7 +2609,7 @@ Viewport SCREEN_INFORMATION::GetVirtualViewport() const noexcept
// - <none>
// Return Value:
// - true if the character at the cursor's current position is wide
bool SCREEN_INFORMATION::CursorIsDoubleWidth() const noexcept
bool SCREEN_INFORMATION::CursorIsDoubleWidth() const
{
const auto& buffer = GetTextBuffer();
const auto position = buffer.GetCursor().GetPosition();

View File

@@ -157,7 +157,7 @@ public:
InputBuffer* const GetActiveInputBuffer() const override;
#pragma endregion
bool CursorIsDoubleWidth() const noexcept;
bool CursorIsDoubleWidth() const;
DWORD OutputMode;
WORD ResizingWindow; // > 0 if we should ignore WM_SIZE messages

View File

@@ -188,7 +188,7 @@ void TextBufferTests::TestWrapFlag()
{
auto& textBuffer = GetTbi();
auto& Row = textBuffer._GetFirstRow();
auto& Row = textBuffer.GetRowByOffset(0);
// no wrap by default
VERIFY_IS_FALSE(Row.WasWrapForced());
@@ -207,7 +207,7 @@ void TextBufferTests::TestWrapThroughWriteLine()
auto& textBuffer = GetTbi();
auto VerifyWrap = [&](bool expected) {
auto& Row = textBuffer._GetFirstRow();
auto& Row = textBuffer.GetRowByOffset(0);
if (expected)
{
@@ -278,7 +278,7 @@ void TextBufferTests::TestDoubleBytePadFlag()
{
auto& textBuffer = GetTbi();
auto& Row = textBuffer._GetFirstRow();
auto& Row = textBuffer.GetRowByOffset(0);
// no padding by default
VERIFY_IS_FALSE(Row.WasDoubleBytePadded());
@@ -300,7 +300,7 @@ void TextBufferTests::DoBoundaryTest(PCWCHAR const pwszInputString,
{
auto& textBuffer = GetTbi();
auto& row = textBuffer._GetFirstRow();
auto& row = textBuffer.GetRowByOffset(0);
// copy string into buffer
for (til::CoordType i = 0; i < cLength; ++i)
@@ -622,7 +622,7 @@ void TextBufferTests::TestIncrementCircularBuffer()
textBuffer._firstRow = iRowToTestIndex;
// fill first row with some stuff
auto& FirstRow = textBuffer._GetFirstRow();
auto& FirstRow = textBuffer.GetRowByOffset(0);
FirstRow.ReplaceCharacters(0, 1, { L"A" });
// ensure it does say that it contains text
@@ -633,7 +633,7 @@ void TextBufferTests::TestIncrementCircularBuffer()
// validate that first row has moved
VERIFY_ARE_EQUAL(textBuffer._firstRow, iNextRowIndex); // first row has incremented
VERIFY_ARE_NOT_EQUAL(textBuffer._GetFirstRow(), FirstRow); // the old first row is no longer the first
VERIFY_ARE_NOT_EQUAL(textBuffer.GetRowByOffset(0), FirstRow); // the old first row is no longer the first
// ensure old first row has been emptied
VERIFY_IS_FALSE(FirstRow.ContainsText());
@@ -1847,7 +1847,7 @@ void TextBufferTests::ResizeTraditionalRotationPreservesHighUnicode()
// This is the negative squared latin capital letter B emoji: 🅱
// It's encoded in UTF-16, as needed by the buffer.
const auto bButton = L"\xD83C\xDD71";
_buffer->_storage[pos.y].ReplaceCharacters(pos.x, 2, bButton);
_buffer->GetRowByOffset(pos.y).ReplaceCharacters(pos.x, 2, bButton);
// Read back the text at that position and ensure that it matches what we wrote.
const auto readBack = _buffer->GetTextDataAt(pos);
@@ -1888,7 +1888,7 @@ void TextBufferTests::ScrollBufferRotationPreservesHighUnicode()
// This is the fire emoji: 🔥
// It's encoded in UTF-16, as needed by the buffer.
const auto fire = L"\xD83D\xDD25";
_buffer->_storage[pos.y].ReplaceCharacters(pos.x, 2, fire);
_buffer->GetRowByOffset(pos.y).ReplaceCharacters(pos.x, 2, fire);
// Read back the text at that position and ensure that it matches what we wrote.
const auto readBack = _buffer->GetTextDataAt(pos);
@@ -1902,11 +1902,7 @@ void TextBufferTests::ScrollBufferRotationPreservesHighUnicode()
// Scroll the row with our data by delta.
_buffer->ScrollRows(pos.y, 1, delta);
// Retrieve the text at the old and new positions.
const auto shouldBeEmptyText = *_buffer->GetTextDataAt(pos);
const auto shouldBeFireText = *_buffer->GetTextDataAt(newPos);
VERIFY_ARE_EQUAL(String(L" "), String(shouldBeEmptyText.data(), gsl::narrow<int>(shouldBeEmptyText.size())));
VERIFY_ARE_EQUAL(String(fire), String(shouldBeFireText.data(), gsl::narrow<int>(shouldBeFireText.size())));
}
@@ -1927,7 +1923,7 @@ void TextBufferTests::ResizeTraditionalHighUnicodeRowRemoval()
// This is the eggplant emoji: 🍆
// It's encoded in UTF-16, as needed by the buffer.
const auto emoji = L"\xD83C\xDF46";
_buffer->_storage[pos.y].ReplaceCharacters(pos.x, 2, emoji);
_buffer->GetRowByOffset(pos.y).ReplaceCharacters(pos.x, 2, emoji);
// Read back the text at that position and ensure that it matches what we wrote.
const auto readBack = _buffer->GetTextDataAt(pos);
@@ -1957,7 +1953,7 @@ void TextBufferTests::ResizeTraditionalHighUnicodeColumnRemoval()
// This is the peach emoji: 🍑
// It's encoded in UTF-16, as needed by the buffer.
const auto emoji = L"\xD83C\xDF51";
_buffer->_storage[pos.y].ReplaceCharacters(pos.x, 2, emoji);
_buffer->GetRowByOffset(pos.y).ReplaceCharacters(pos.x, 2, emoji);
// Read back the text at that position and ensure that it matches what we wrote.
const auto readBack = _buffer->GetTextDataAt(pos);

View File

@@ -325,7 +325,7 @@ public:
return 12ul;
}
bool IsCursorDoubleWidth() const noexcept override
bool IsCursorDoubleWidth() const override
{
return false;
}

View File

@@ -57,7 +57,7 @@ namespace Microsoft::Console::Render
virtual ULONG GetCursorHeight() const noexcept = 0;
virtual CursorType GetCursorStyle() const noexcept = 0;
virtual ULONG GetCursorPixelWidth() const noexcept = 0;
virtual bool IsCursorDoubleWidth() const noexcept = 0;
virtual bool IsCursorDoubleWidth() const = 0;
virtual const std::vector<RenderOverlay> GetOverlays() const noexcept = 0;
virtual const bool IsGridLineDrawingAllowed() noexcept = 0;
virtual const std::wstring_view GetConsoleTitle() const noexcept = 0;