Reset _wrapForced when erasing scrollback (#16610)

#15541 changed `AdaptDispatch::_FillRect` which caused it to not affect
the `ROW::_wrapForced` flag anymore. This change in behavior was not
noticeable as `TextBuffer::GetLastNonSpaceCharacter` had a bug where
rows of only whitespace text would always be treated as empty.
This would then affect `AdaptDispatch::_EraseAll` to accidentally
correctly guess the last row with text despite the `_FillRect` change.

#15701 then fixed `GetLastNonSpaceCharacter` indirectly by fixing
`ROW::MeasureRight` which now made the previous change apparent.
`_EraseAll` would now guess the last row of text incorrectly,
because it would find the rows that `_FillRect` cleared but still
had `_wrapForced` set to `true`.

This PR fixes the issue by replacing the `_FillRect` usage to clear
rows with direct calls to `ROW::Reset()`. In the future this could be
extended by also `MEM_DECOMMIT`ing the now unused underlying memory.

Closes #16603

## Validation Steps Performed
* Enter WSL and resize the window to <40 columns
* Execute
  ```sh
  cd /bin
  ls -la
  printf "\e[3J"
  ls -la
  printf "\e[3J"
  printf "\e[2J"
  ```
* Only one viewport-height-many lines of whitespace exist between the
  current prompt line and the previous scrollback contents 

(cherry picked from commit 5f71cf3e94)
Service-Card-Id: 91707937
Service-Version: 1.19
This commit is contained in:
Leonard Hecker 2024-01-29 23:49:42 +01:00 committed by Dustin L. Howett
parent 29895e1c2d
commit bc48eda022
4 changed files with 51 additions and 23 deletions

View File

@ -126,6 +126,8 @@ void TextBuffer::_reserve(til::size screenBufferSize, const TextAttribute& defau
// 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)
{
assert(row >= _commitWatermark);
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);
@ -146,7 +148,7 @@ void TextBuffer::_decommit() noexcept
_commitWatermark = _buffer.get();
}
// Constructs ROWs up to (excluding) the ROW pointed to by `until`.
// Constructs ROWs between [_commitWatermark,until).
void TextBuffer::_construct(const std::byte* until) noexcept
{
for (; _commitWatermark < until; _commitWatermark += _bufferRowStride)
@ -158,8 +160,7 @@ void TextBuffer::_construct(const std::byte* until) noexcept
}
}
// Destroys all previously constructed ROWs.
// Be careful! This doesn't reset any of the members, in particular the _commitWatermark.
// Destructs ROWs between [_buffer,_commitWatermark).
void TextBuffer::_destroy() const noexcept
{
for (auto it = _buffer.get(); it < _commitWatermark; it += _bufferRowStride)
@ -168,9 +169,8 @@ void TextBuffer::_destroy() const noexcept
}
}
// 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.
// This function is "direct" because it trusts the caller to properly
// wrap the "offset" parameter modulo the _height of the buffer.
ROW& TextBuffer::_getRowByOffsetDirect(size_t offset)
{
const auto row = _buffer.get() + _bufferRowStride * offset;
@ -184,6 +184,7 @@ ROW& TextBuffer::_getRowByOffsetDirect(size_t offset)
return *reinterpret_cast<ROW*>(row);
}
// See GetRowByOffset().
ROW& TextBuffer::_getRow(til::CoordType y) const
{
// Rows are stored circularly, so the index you ask for is offset by the start position and mod the total of rows.
@ -197,6 +198,7 @@ ROW& TextBuffer::_getRow(til::CoordType y) const
}
// We add 1 to the row offset, because row "0" is the one returned by GetScratchpadRow().
// See GetScratchpadRow() for more explanation.
#pragma warning(suppress : 26492) // Don't use const_cast to cast away const or volatile (type.3).
return const_cast<TextBuffer*>(this)->_getRowByOffsetDirect(gsl::narrow_cast<size_t>(offset) + 1);
}
@ -238,6 +240,9 @@ ROW& TextBuffer::GetScratchpadRow()
// Returns a row filled with whitespace and the given attributes, for you to freely use.
ROW& TextBuffer::GetScratchpadRow(const TextAttribute& attributes)
{
// The scratchpad row is mapped to the underlying index 0, whereas all regular rows are mapped to
// index 1 and up. We do it this way instead of the other way around (scratchpad row at index _height),
// because that would force us to MEM_COMMIT the entire buffer whenever this function is called.
auto& r = _getRowByOffsetDirect(0);
r.Reset(attributes);
return r;
@ -902,15 +907,14 @@ til::point TextBuffer::GetLastNonSpaceCharacter(const Viewport* viewOptional) co
// If the X coordinate turns out to be -1, the row was empty, we need to search backwards for the real end of text.
const auto viewportTop = viewport.Top();
auto fDoBackUp = (coordEndOfText.x < 0 && coordEndOfText.y > viewportTop); // this row is empty, and we're not at the top
while (fDoBackUp)
// while (this row is empty, and we're not at the top)
while (coordEndOfText.x < 0 && coordEndOfText.y > viewportTop)
{
coordEndOfText.y--;
const auto& backupRow = GetRowByOffset(coordEndOfText.y);
// We need to back up to the previous row if this line is empty, AND there are more rows
coordEndOfText.x = backupRow.MeasureRight() - 1;
fDoBackUp = (coordEndOfText.x < 0 && coordEndOfText.y > viewportTop);
}
// don't allow negative results
@ -1146,6 +1150,39 @@ void TextBuffer::Reset() noexcept
_initialAttributes = _currentAttributes;
}
void TextBuffer::ClearScrollback(const til::CoordType start, const til::CoordType height)
{
if (start <= 0)
{
return;
}
if (height <= 0)
{
_decommit();
return;
}
// Our goal is to move the viewport to the absolute start of the underlying memory buffer so that we can
// MEM_DECOMMIT the remaining memory. _firstRow is used to make the TextBuffer behave like a circular buffer.
// The start parameter is relative to the _firstRow. The trick to get the content to the absolute start
// is to simply add _firstRow ourselves and then reset it to 0. This causes ScrollRows() to write into
// the absolute start while reading from relative coordinates. This works because GetRowByOffset()
// operates modulo the buffer height and so the possibly-too-large startAbsolute won't be an issue.
const auto startAbsolute = _firstRow + start;
_firstRow = 0;
ScrollRows(startAbsolute, height, -startAbsolute);
const auto end = _estimateOffsetOfLastCommittedRow();
for (auto y = height; y <= end; ++y)
{
GetMutableRowByOffset(y).Reset(_initialAttributes);
}
ScrollMarks(-start);
ClearMarksInRange(til::point{ 0, height }, til::point{ _width, _height });
}
// Routine Description:
// - This is the legacy screen resize with minimal changes
// Arguments:

View File

@ -194,6 +194,7 @@ public:
til::point BufferToScreenPosition(const til::point position) const;
void Reset() noexcept;
void ClearScrollback(const til::CoordType start, const til::CoordType height);
void ResizeTraditional(const til::size newSize);

View File

@ -4515,6 +4515,7 @@ void ScreenBufferTests::EraseScrollbackTests()
auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
auto& stateMachine = si.GetStateMachine();
const auto& cursor = si.GetTextBuffer().GetCursor();
const auto initialAttributes = si.GetAttributes();
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
const auto bufferWidth = si.GetBufferSize().Width();
@ -4571,7 +4572,7 @@ void ScreenBufferTests::EraseScrollbackTests()
}
Log::Comment(L"The rest of the buffer should be cleared with default attributes.");
VERIFY_IS_TRUE(_ValidateLinesContain(viewportLine, bufferHeight, L' ', TextAttribute{}));
VERIFY_IS_TRUE(_ValidateLinesContain(viewportLine, bufferHeight, L' ', initialAttributes));
}
void ScreenBufferTests::EraseTests()

View File

@ -3200,18 +3200,7 @@ bool AdaptDispatch::_EraseScrollback()
auto& cursor = textBuffer.GetCursor();
const auto row = cursor.GetPosition().y;
// Clear all the marks below the new viewport position.
textBuffer.ClearMarksInRange(til::point{ 0, height },
til::point{ bufferSize.width, bufferSize.height });
// Then scroll all the remaining marks up. This will trim ones that are now "outside" the buffer
textBuffer.ScrollMarks(-top);
// Scroll the viewport content to the top of the buffer.
textBuffer.ScrollRows(top, height, -top);
// Clear everything after the viewport.
_FillRect(textBuffer, { 0, height, bufferSize.width, bufferSize.height }, whitespace, {});
// Also reset the line rendition for all of the cleared rows.
textBuffer.ResetLineRenditionRange(height, bufferSize.height);
textBuffer.ClearScrollback(top, height);
// Move the viewport
_api.SetViewportPosition({ viewport.left, 0 });
// Move the cursor to the same relative location.