Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Can we have infinite scrollback? #18290

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/cascadia/profiles.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2994,7 +2994,7 @@
"type": "boolean"
},
"historySize": {
"default": 9001,
"default": 65536,
"description": "The number of lines above the ones displayed in the window you can scroll back to.",
"minimum": -1,
"type": "integer"
Expand Down
6 changes: 3 additions & 3 deletions src/buffer/out/OutputCellRect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ OutputCellRect::OutputCellRect(const til::CoordType rows, const til::CoordType c
_rows(rows),
_cols(cols)
{
_storage.resize(gsl::narrow<size_t>(rows * cols));
_storage.resize(gsl::narrow<size_t>(til::HugeCoordType{ rows } * cols));
}

// Routine Description:
Expand Down Expand Up @@ -61,7 +61,7 @@ OutputCellIterator OutputCellRect::GetRowIter(const til::CoordType row) const
// - Pointer to the location in the rectangle that represents the start of the requested row.
OutputCell* OutputCellRect::_FindRowOffset(const til::CoordType row)
{
return &_storage.at(gsl::narrow_cast<size_t>(row * _cols));
return &_storage.at(gsl::narrow<size_t>(til::HugeCoordType{ row } * _cols));
}

// Routine Description:
Expand All @@ -73,7 +73,7 @@ OutputCell* OutputCellRect::_FindRowOffset(const til::CoordType row)
// - Pointer to the location in the rectangle that represents the start of the requested row.
const OutputCell* OutputCellRect::_FindRowOffset(const til::CoordType row) const
{
return &_storage.at(gsl::narrow_cast<size_t>(row * _cols));
return &_storage.at(gsl::narrow<size_t>(til::HugeCoordType{ row } * _cols));
}

// Routine Description:
Expand Down
4 changes: 2 additions & 2 deletions src/buffer/out/Row.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -937,12 +937,12 @@ void ROW::_resizeChars(uint16_t colEndDirty, uint16_t chBegDirty, size_t chEndDi
}
}

til::small_rle<TextAttribute, uint16_t, 1>& ROW::Attributes() noexcept
ROW::AttributesType& ROW::Attributes() noexcept
{
return _attr;
}

const til::small_rle<TextAttribute, uint16_t, 1>& ROW::Attributes() const noexcept
const ROW::AttributesType& ROW::Attributes() const noexcept
{
return _attr;
}
Expand Down
8 changes: 5 additions & 3 deletions src/buffer/out/Row.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ struct CharToColumnMapper
class ROW final
{
public:
using AttributesType = til::small_rle<TextAttribute, uint16_t, 3>;

// The implicit agreement between ROW and TextBuffer is that the `charsBuffer` and `charOffsetsBuffer`
// arrays have a minimum alignment of 16 Bytes and a size of `rowWidth+1`. The former is used to
// implement Reset() efficiently via SIMD and the latter is used to store the past-the-end offset
Expand Down Expand Up @@ -148,8 +150,8 @@ class ROW final
void ReplaceText(RowWriteState& state);
void CopyTextFrom(RowCopyTextFromState& state);

til::small_rle<TextAttribute, uint16_t, 1>& Attributes() noexcept;
const til::small_rle<TextAttribute, uint16_t, 1>& Attributes() const noexcept;
AttributesType& Attributes() noexcept;
const AttributesType& Attributes() const noexcept;
TextAttribute GetAttrByColumn(til::CoordType column) const;
std::vector<uint16_t> GetHyperlinks() const;
ImageSlice* SetImageSlice(ImageSlice::Pointer imageSlice) noexcept;
Expand Down Expand Up @@ -297,7 +299,7 @@ class ROW final
std::span<uint16_t> _charOffsets;
// _attr is a run-length-encoded vector of TextAttribute with a decompressed
// length equal to _columnCount (= 1 TextAttribute per column).
til::small_rle<TextAttribute, uint16_t, 1> _attr;
AttributesType _attr;
// The width of the row in visual columns.
uint16_t _columnCount = 0;
// Stores double-width/height (DECSWL/DECDWL/DECDHL) attributes.
Expand Down
112 changes: 63 additions & 49 deletions src/buffer/out/textBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,14 @@ TextBuffer::TextBuffer(til::size screenBufferSize,
_cursor{ cursorSize, *this },
_isActiveBuffer{ isActiveBuffer }
{
// 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);
_reserve(screenBufferSize, defaultAttributes);
}

TextBuffer::~TextBuffer()
{
if (_buffer)
{
_destroy();
_destroy(_buffer.get());
}
}

Expand All @@ -90,8 +87,9 @@ TextBuffer::~TextBuffer()
// 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);
// Guard against resizing the text buffer to 0 columns/rows, which would break being able to insert text.
const auto w = std::clamp(screenBufferSize.width, 1, 0xffff);
const auto h = std::clamp(screenBufferSize.height, 1, til::CoordTypeMax / 2 + UINT16_MAX);

constexpr auto rowSize = ROW::CalculateRowSize();
const auto charsBufferSize = ROW::CalculateCharsBufferSize(w);
Expand All @@ -102,13 +100,13 @@ void TextBuffer::_reserve(til::size screenBufferSize, const TextAttribute& defau
// 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 rowCount = gsl::narrow_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)))
_buffer = wil::unique_virtualalloc_ptr<uint8_t>{
static_cast<uint8_t*>(THROW_LAST_ERROR_IF_NULL(VirtualAlloc(nullptr, allocSize, MEM_RESERVE, PAGE_READWRITE)))
};
_bufferEnd = _buffer.get() + allocSize;
_commitWatermark = _buffer.get();
Expand All @@ -126,7 +124,7 @@ void TextBuffer::_reserve(til::size screenBufferSize, const TextAttribute& defau
// 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)
__declspec(noinline) void TextBuffer::_commit(const uint8_t* row)
{
assert(row >= _commitWatermark);

Expand All @@ -141,31 +139,59 @@ __declspec(noinline) void TextBuffer::_commit(const std::byte* row)
_construct(_commitWatermark + size);
}

// Destructs and MEM_DECOMMITs all previously constructed ROWs.
// Destructs and MEM_DECOMMITs all rows between [rowsToKeep,_commitWatermark).
// You can use this (or rather the Reset() method) to fully clear the TextBuffer.
void TextBuffer::_decommit() noexcept
void TextBuffer::_decommit(til::CoordType rowsToKeep) noexcept
{
_destroy();
VirtualFree(_buffer.get(), 0, MEM_DECOMMIT);
_commitWatermark = _buffer.get();
SYSTEM_INFO si;
GetSystemInfo(&si);

rowsToKeep = std::clamp(rowsToKeep, 0, _height);

// Amount of bytes that have been allocated with MEM_COMMIT so far.
const auto commitBytes = gsl::narrow_cast<size_t>(_commitWatermark - _buffer.get());
// Offset in bytes to the first row that we were asked to destroy.
// We must ensure that the offset is not past the end of the current _commitWatermark,
// since we don't want to finish with a watermark that's somehow larger than what we started with.
const auto byteOffset = std::min(commitBytes, rowsToKeep * _bufferRowStride);
const auto newWatermark = _buffer.get() + byteOffset;
// Since the last row we were asked to keep may reside in the middle
// of a page, we must round the offset up to the next page boundary.
// That offset will tell us the offset at which we will MEM_DECOMMIT memory.
const auto pageMask = gsl::narrow_cast<size_t>(si.dwPageSize) - 1;
const auto pageOffset = (byteOffset + pageMask) & ~pageMask;

// _destroy() takes care to check that the given pointer is valid.
_destroy(newWatermark);

// MEM_DECOMMIT the memory that we don't need anymore.
if (pageOffset < commitBytes)
{
VirtualFree(_buffer.get() + pageOffset, commitBytes - pageOffset, MEM_DECOMMIT);
}

_commitWatermark = newWatermark;
}

// Constructs ROWs between [_commitWatermark,until).
void TextBuffer::_construct(const std::byte* until) noexcept
void TextBuffer::_construct(const uint8_t* until) noexcept
{
// _width has been validated to fit into uint16_t during reserve().
const auto width = gsl::narrow_cast<uint16_t>(_width);

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);
std::construct_at(row, chars, indices, width, _initialAttributes);
}
}

// Destructs ROWs between [_buffer,_commitWatermark).
void TextBuffer::_destroy() const noexcept
// Destructs ROWs between [it,_commitWatermark).
void TextBuffer::_destroy(uint8_t* it) const noexcept
{
for (auto it = _buffer.get(); it < _commitWatermark; it += _bufferRowStride)
for (; it < _commitWatermark; it += _bufferRowStride)
{
std::destroy_at(reinterpret_cast<ROW*>(it));
}
Expand Down Expand Up @@ -973,7 +999,7 @@ til::point TextBuffer::BufferToScreenPosition(const til::point position) const
// and the default current color attributes
void TextBuffer::Reset() noexcept
{
_decommit();
_decommit(0);
_initialAttributes = _currentAttributes;
}

Expand All @@ -988,29 +1014,20 @@ void TextBuffer::ClearScrollback(const til::CoordType newFirstRow, const til::Co
return;
}
// The new viewport should keep 0 rows? Then just reset everything.
if (rowsToKeep <= 0)
if (rowsToKeep > 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 newFirstRow 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 + newFirstRow;
_firstRow = 0;
ScrollRows(startAbsolute, rowsToKeep, -startAbsolute);
}

ClearMarksInRange(til::point{ 0, 0 }, til::point{ _width, std::max(0, newFirstRow - 1) });

// 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 newFirstRow 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 + newFirstRow;
_firstRow = 0;
ScrollRows(startAbsolute, rowsToKeep, -startAbsolute);

const auto end = _estimateOffsetOfLastCommittedRow();
for (auto y = rowsToKeep; y <= end; ++y)
{
GetMutableRowByOffset(y).Reset(_initialAttributes);
}
_decommit(rowsToKeep);
}

// Routine Description:
Expand Down Expand Up @@ -2015,7 +2032,7 @@ std::string TextBuffer::GenHTML(const CopyRequest& req,
const auto [rowBeg, rowEnd, addLineBreak] = _RowCopyHelper(req, iRow, row);
const auto rowBegU16 = gsl::narrow_cast<uint16_t>(rowBeg);
const auto rowEndU16 = gsl::narrow_cast<uint16_t>(rowEnd);
const auto runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();
const auto& runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();

auto x = rowBegU16;
for (const auto& [attr, length] : runs)
Expand Down Expand Up @@ -2265,7 +2282,7 @@ std::string TextBuffer::GenRTF(const CopyRequest& req,
const auto [rowBeg, rowEnd, addLineBreak] = _RowCopyHelper(req, iRow, row);
const auto rowBegU16 = gsl::narrow_cast<uint16_t>(rowBeg);
const auto rowEndU16 = gsl::narrow_cast<uint16_t>(rowEnd);
const auto runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();
const auto& runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();

auto x = rowBegU16;
for (auto& [attr, length] : runs)
Expand Down Expand Up @@ -2457,7 +2474,7 @@ void TextBuffer::_SerializeRow(const ROW& row, const til::CoordType startX, cons

const auto startXU16 = gsl::narrow_cast<uint16_t>(startX);
const auto endXU16 = gsl::narrow_cast<uint16_t>(endX);
const auto runs = row.Attributes().slice(startXU16, endXU16).runs();
const auto& runs = row.Attributes().slice(startXU16, endXU16).runs();

const auto beg = runs.begin();
const auto end = runs.end();
Expand Down Expand Up @@ -3246,7 +3263,6 @@ MarkExtents TextBuffer::_scrollMarkExtentForRow(const til::CoordType rowOffset,
bool startedPrompt = false;
bool startedCommand = false;
bool startedOutput = false;
MarkKind lastMarkKind = MarkKind::Output;

const auto endThisMark = [&](auto x, auto y) {
if (startedOutput)
Expand All @@ -3273,7 +3289,7 @@ MarkExtents TextBuffer::_scrollMarkExtentForRow(const til::CoordType rowOffset,
// Output attribute.

const auto& row = GetRowByOffset(y);
const auto runs = row.Attributes().runs();
const auto& runs = row.Attributes().runs();
x = 0;
for (const auto& [attr, length] : runs)
{
Expand Down Expand Up @@ -3316,8 +3332,6 @@ MarkExtents TextBuffer::_scrollMarkExtentForRow(const til::CoordType rowOffset,

endThisMark(lastMarkedText.x, lastMarkedText.y);
}
// Otherwise, we've changed from any state -> any state, and it doesn't really matter.
lastMarkKind = markKind;
}
// advance to next run of text
x = nextX;
Expand Down Expand Up @@ -3350,7 +3364,7 @@ std::wstring TextBuffer::_commandForRow(const til::CoordType rowOffset,
// Command attributes. Collect up all of those, till we get to the next
// Output attribute.
const auto& row = GetRowByOffset(y);
const auto runs = row.Attributes().runs();
const auto& runs = row.Attributes().runs();
auto x = 0;
for (const auto& [attr, length] : runs)
{
Expand Down
28 changes: 14 additions & 14 deletions src/buffer/out/textBuffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,10 @@ class TextBuffer final

private:
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;
void _commit(const uint8_t* row);
Copy link
Member

Choose a reason for hiding this comment

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

hmm why these?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is so that cls releases the memory again. I figured that this would become much more important once the scrollback can actually become quite large.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, you meant std::byte vs. uint8_t. We only use the former in a teeny tiny subset of the codebase, of which this usage here is half of it. By using the more traditional uint8_t we make our code simpler and need less specialization. It's also more predictable, because primitive integers have received compiler optimizations over decades whereas std::byte hasn't.
I'm reminded of that one blog post that showed how poorly std::byte was optimized by compilers at the time, but I was unable to find it while writing this comment.

void _decommit(til::CoordType keep) noexcept;
void _construct(const uint8_t* until) noexcept;
void _destroy(uint8_t* it) const noexcept;
ROW& _getRowByOffsetDirect(size_t offset);
ROW& _getRow(til::CoordType y) const;
til::CoordType _estimateOffsetOfLastCommittedRow() const noexcept;
Expand Down Expand Up @@ -357,9 +357,9 @@ class TextBuffer final
// Padding may exist for alignment purposes.
//
// The base (start) address of the memory arena.
wil::unique_virtualalloc_ptr<std::byte> _buffer;
wil::unique_virtualalloc_ptr<uint8_t> _buffer;
// The past-the-end pointer of the memory arena.
std::byte* _bufferEnd = nullptr;
uint8_t* _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.
//
Expand All @@ -375,15 +375,15 @@ class TextBuffer final
// _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.
uint8_t* _commitWatermark = nullptr;
// This will MEM_COMMIT 256 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
// * 80 columns (the usual minimum) = 120KB chunks, 4.1MB buffer at 9001 rows
// * 120 columns (the most common) = 160KB chunks, 5.6MB buffer at 9001 rows
// * 400 columns (the usual maximum) = 440KB 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;
static constexpr size_t _commitReadAheadRowCount = 256;
// 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;
Expand All @@ -397,9 +397,9 @@ class TextBuffer final
size_t _bufferOffsetChars = 0;
size_t _bufferOffsetCharOffsets = 0;
// The width of the buffer in columns.
uint16_t _width = 0;
til::CoordType _width = 0;
// The height of the buffer in rows, excluding the scratchpad row.
uint16_t _height = 0;
til::CoordType _height = 0;

TextAttribute _currentAttributes;
til::CoordType _firstRow = 0; // indexes top row (not necessarily 0)
Expand Down
2 changes: 1 addition & 1 deletion src/buffer/out/textBufferCellIterator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class TextBufferCellIterator
void _GenerateView() noexcept;
static const ROW* s_GetRow(const TextBuffer& buffer, const til::point pos);

til::small_rle<TextAttribute, uint16_t, 1>::const_iterator _attrIter;
ROW::AttributesType::const_iterator _attrIter;
OutputCellView _view;

const ROW* _pRow;
Expand Down
Loading
Loading