Skip to content

Commit

Permalink
Can we have infinite scrollback?
Browse files Browse the repository at this point in the history
  • Loading branch information
lhecker committed Dec 11, 2024
1 parent 0b492ab commit 3567827
Show file tree
Hide file tree
Showing 27 changed files with 260 additions and 246 deletions.
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);
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

0 comments on commit 3567827

Please sign in to comment.