From f2934adf5d269f060a82c0fb94470b1eda8b7d55 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 8 Jun 2023 13:25:55 +0200 Subject: [PATCH] Initialize rows lazily --- .github/actions/spelling/expect/expect.txt | 1 + src/buffer/out/Row.cpp | 25 +- src/buffer/out/Row.hpp | 18 +- src/buffer/out/textBuffer.cpp | 420 ++++++++++-------- src/buffer/out/textBuffer.hpp | 65 ++- src/buffer/out/textBufferCellIterator.cpp | 4 +- src/buffer/out/textBufferCellIterator.hpp | 4 +- src/cascadia/TerminalCore/Terminal.hpp | 2 +- .../TerminalCore/terminalrenderdata.cpp | 2 +- src/host/renderData.cpp | 2 +- src/host/renderData.hpp | 2 +- src/host/screenInfo.cpp | 2 +- src/host/screenInfo.hpp | 2 +- src/host/ut_host/TextBufferTests.cpp | 24 +- src/host/ut_host/VtIoTests.cpp | 2 +- src/renderer/inc/IRenderData.hpp | 2 +- 16 files changed, 325 insertions(+), 252 deletions(-) diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 496ffcf079f..a7383e66e73 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -439,6 +439,7 @@ DECMSR DECNKM DECNRCM DECOM +decommit DECPCTERM DECPS DECRARA diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp index 9d883b09502..737c59d4107 100644 --- a/src/buffer/out/Row.cpp +++ b/src/buffer/out/Row.cpp @@ -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(); - } + _init(); } void ROW::SetWrapForced(const bool wrap) noexcept @@ -147,6 +144,15 @@ void ROW::TransferAttributes(const til::small_rle& a _attr.resize_trailing_extent(gsl::narrow(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& charOffsets) noexcept +[[msvc::forceinline]] void ROW::WriteHelper::CopyTextFrom(const std::span& 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... diff --git a/src/buffer/out/Row.hpp b/src/buffer/out/Row.hpp index c86312984e9..39d790eab82 100644 --- a/src/buffer/out/Row.hpp +++ b/src/buffer/out/Row.hpp @@ -60,6 +60,19 @@ struct RowWriteState class ROW final { public: + static constexpr size_t CalculateRowSize() noexcept + { + return sizeof(ROW); + } + static constexpr size_t CalculateCharsBufferSize(size_t columns) noexcept + { + return columns * sizeof(wchar_t); + } + static constexpr size_t CalculateCharOffsetsBufferSize(size_t columns) noexcept + { + return (columns + 1) * sizeof(uint16_t); + } + ROW() = default; ROW(wchar_t* charsBuffer, uint16_t* charOffsetsBuffer, uint16_t rowWidth, const TextAttribute& fillAttribute); @@ -78,6 +91,7 @@ class ROW final void Reset(const TextAttribute& attr); void TransferAttributes(const til::small_rle& 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 +102,7 @@ class ROW final 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& Attributes() noexcept; const til::small_rle& Attributes() const noexcept; @@ -121,7 +135,7 @@ class ROW final bool IsValid() const noexcept; void ReplaceCharacters(til::CoordType width) noexcept; void ReplaceText() noexcept; - void CopyRangeFrom(const std::span& charOffsets) noexcept; + void CopyTextFrom(const std::span& charOffsets) noexcept; void Finish(); // Parent pointer. diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index b3c2e590e37..9d6dbd28ded 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -42,10 +42,173 @@ 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(screenBufferSize.width); + const auto h = gsl::narrow(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; + // 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(h) + 1; + const auto allocSize = gsl::narrow(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{ + static_cast(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(_bufferEnd - _commitWatermark); + const auto minimum = gsl::narrow_cast(rowEnd - _commitWatermark); + // 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, + // an _chars array containing text and a _charOffsets array contain column-to-text indices.) + const auto ideal = minimum + _bufferRowStride * 128; + 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(_commitWatermark); + const auto chars = reinterpret_cast(_commitWatermark + _bufferOffsetChars); + const auto indices = reinterpret_cast(_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(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); +} + +// 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(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(offset) + 1); +} + +// Returns a row filled with whitespace and the current attributes, for you to freely use. +ROW& TextBuffer::GetScratchpadRow() +{ + auto& row = _getRowByOffsetDirect(0); + row.Reset(_currentAttributes); + return row; +} + +#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 +229,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(_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(_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(_firstRow + index) % _storage.size(); - return til::at(_storage, offsetIndex); + return _height; } // Routine Description: @@ -483,7 +618,7 @@ bool TextBuffer::InsertCharacter(const wchar_t wch, const DbcsAttribute dbcsAttr // - - Always sets to wrap //Return Value: // - -void TextBuffer::_SetWrapOnCurrentRow() noexcept +void TextBuffer::_SetWrapOnCurrentRow() { _AdjustWrapOnCurrentRow(true); } @@ -495,7 +630,7 @@ void TextBuffer::_SetWrapOnCurrentRow() noexcept // - fSet - True if this row has a wrap. False otherwise. //Return Value: // - -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; @@ -658,7 +793,7 @@ til::point TextBuffer::GetLastNonSpaceCharacter(std::optional TextBuffer::_allocateBuffer(til::size sz, const TextAttribute& attributes, std::vector& rows) -{ - const auto w = gsl::narrow(sz.width); - const auto h = gsl::narrow(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(::base::strict_cast(rowStride) * ::base::strict_cast(h)); - - auto buffer = wil::unique_virtualalloc_ptr{ static_cast(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(&*data); - const auto indices = til::bit_cast(&*(data + charsBytes)); - row = { chars, indices, w, attributes }; - data += rowStride; - } - - return buffer; -} - -void TextBuffer::_UpdateSize() -{ - _size = Viewport::FromDimensions({ _storage.at(0).size(), gsl::narrow(_storage.size()) }); + return Viewport::FromDimensions({ _width, _height }); } void TextBuffer::_SetFirstRowIndex(const til::CoordType FirstRowIndex) noexcept @@ -734,27 +833,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: @@ -764,33 +857,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 { @@ -803,31 +883,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)); } } @@ -878,7 +950,7 @@ void TextBuffer::SetCurrentLineRendition(const LineRendition lineRendition) } } -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++) { @@ -886,37 +958,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; @@ -926,14 +998,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: @@ -950,55 +1018,34 @@ void TextBuffer::Reset() try { - til::CoordType TopRow = 0; // new top row of the screen buffer - if (newSize.height <= GetCursor().GetPosition().y) + TextBuffer newBuffer{ newSize, _currentAttributes, 0, false, _renderer }; + const auto cursorRow = GetCursor().GetPosition().y; + const auto copyableRows = std::min(_height, newSize.height); + til::CoordType srcRow = 0; + til::CoordType dstRow = 0; + + if (cursorRow >= newSize.height) { - TopRow = GetCursor().GetPosition().y - newSize.height + 1; + srcRow = cursorRow - newSize.height + 1; } - const auto TopRowIndex = gsl::narrow_cast(_firstRow + TopRow) % _storage.size(); - - std::vector 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. + for (; dstRow < copyableRows; ++dstRow, ++srcRow) { - const auto first = _storage.begin(); - const auto last = _storage.end(); - const auto mid = first + TopRowIndex; - auto dest = newStorage.begin(); - - std::span 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); - } - - for (const auto& sourceRange : sourceRanges) - { - 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(); @@ -1068,17 +1115,6 @@ void TextBuffer::TriggerNewTextNotification(const std::wstring_view newText) } } -// Routine Description: -// - Retrieves the first row from the underlying buffer. -// Arguments: -// - -// 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 @@ -1087,7 +1123,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); } @@ -1153,7 +1189,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(); @@ -1198,7 +1234,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(); @@ -1319,7 +1355,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(); diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index 555531a25aa..cb6c4372985 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -72,14 +72,21 @@ class TextBuffer final 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 @@ class TextBuffer final void SetCurrentAttributes(const TextAttribute& currentAttributes) noexcept; void SetCurrentLineRendition(const LineRendition lineRendition); - 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 @@ class TextBuffer final interval_tree::IntervalTree GetPatterns(const til::CoordType firstRow, const til::CoordType lastRow) const; private: - static wil::unique_virtualalloc_ptr _allocateBuffer(til::size sz, const TextAttribute& attributes, std::vector& 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,20 @@ class TextBuffer final std::unordered_map _idsAndPatterns; size_t _currentPatternId = 0; - wil::unique_virtualalloc_ptr _charBuffer; - std::vector _storage; + wil::unique_virtualalloc_ptr _buffer; + std::byte* _bufferEnd = nullptr; + std::byte* _commitWatermark = nullptr; + TextAttribute _initialAttributes; + size_t _bufferRowStride = 0; + size_t _bufferOffsetChars = 0; + size_t _bufferOffsetCharOffsets = 0; + uint16_t _width = 0; + 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; diff --git a/src/buffer/out/textBufferCellIterator.cpp b/src/buffer/out/textBufferCellIterator.cpp index f19b8a285bf..8a177dfec6e 100644 --- a/src/buffer/out/textBufferCellIterator.cpp +++ b/src/buffer/out/textBufferCellIterator.cpp @@ -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); } diff --git a/src/buffer/out/textBufferCellIterator.hpp b/src/buffer/out/textBufferCellIterator.hpp index 198bdac6ce1..30276f2e13c 100644 --- a/src/buffer/out/textBufferCellIterator.hpp +++ b/src/buffer/out/textBufferCellIterator.hpp @@ -49,9 +49,9 @@ class TextBufferCellIterator 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::const_iterator _attrIter; OutputCellView _view; diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 1c8e36d72b4..2b7b113a7da 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -180,7 +180,7 @@ class Microsoft::Terminal::Core::Terminal final : 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 GetOverlays() const noexcept override; const bool IsGridLineDrawingAllowed() noexcept override; const std::wstring GetHyperlinkUri(uint16_t id) const override; diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp index b4994777c71..69ea72d764f 100644 --- a/src/cascadia/TerminalCore/terminalrenderdata.cpp +++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp @@ -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(); diff --git a/src/host/renderData.cpp b/src/host/renderData.cpp index 80fbf9c8eba..1f1dadda001 100644 --- a/src/host/renderData.cpp +++ b/src/host/renderData.cpp @@ -246,7 +246,7 @@ const std::vector RenderData::GetOver // - // 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(); diff --git a/src/host/renderData.hpp b/src/host/renderData.hpp index 8263f35cd70..460cda32589 100644 --- a/src/host/renderData.hpp +++ b/src/host/renderData.hpp @@ -36,7 +36,7 @@ class RenderData final : 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 GetOverlays() const noexcept override; diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index 789020ebfc9..563c3726506 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -2633,7 +2633,7 @@ Viewport SCREEN_INFORMATION::GetVirtualViewport() const noexcept // - // 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(); diff --git a/src/host/screenInfo.hpp b/src/host/screenInfo.hpp index d67e3f5684c..56c3b977614 100644 --- a/src/host/screenInfo.hpp +++ b/src/host/screenInfo.hpp @@ -157,7 +157,7 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console 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 diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index 823736ea30f..bcb4cec4a99 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -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(shouldBeEmptyText.size()))); VERIFY_ARE_EQUAL(String(fire), String(shouldBeFireText.data(), gsl::narrow(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); diff --git a/src/host/ut_host/VtIoTests.cpp b/src/host/ut_host/VtIoTests.cpp index 2b4d2fe5b39..329dd3f46ec 100644 --- a/src/host/ut_host/VtIoTests.cpp +++ b/src/host/ut_host/VtIoTests.cpp @@ -325,7 +325,7 @@ class MockRenderData : public IRenderData return 12ul; } - bool IsCursorDoubleWidth() const noexcept override + bool IsCursorDoubleWidth() const override { return false; } diff --git a/src/renderer/inc/IRenderData.hpp b/src/renderer/inc/IRenderData.hpp index e3007ab2449..e45dc388ac7 100644 --- a/src/renderer/inc/IRenderData.hpp +++ b/src/renderer/inc/IRenderData.hpp @@ -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 GetOverlays() const noexcept = 0; virtual const bool IsGridLineDrawingAllowed() noexcept = 0; virtual const std::wstring_view GetConsoleTitle() const noexcept = 0;