Skip to content

Commit e6acccf

Browse files
committed
Can we have infinite scrollback?
1 parent 0b492ab commit e6acccf

27 files changed

+270
-250
lines changed

doc/cascadia/profiles.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2994,7 +2994,7 @@
29942994
"type": "boolean"
29952995
},
29962996
"historySize": {
2997-
"default": 9001,
2997+
"default": 65536,
29982998
"description": "The number of lines above the ones displayed in the window you can scroll back to.",
29992999
"minimum": -1,
30003000
"type": "integer"

src/buffer/out/OutputCellRect.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ OutputCellRect::OutputCellRect(const til::CoordType rows, const til::CoordType c
2525
_rows(rows),
2626
_cols(cols)
2727
{
28-
_storage.resize(gsl::narrow<size_t>(rows * cols));
28+
_storage.resize(gsl::narrow<size_t>(til::HugeCoordType{ rows } * cols));
2929
}
3030

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

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

7979
// Routine Description:

src/buffer/out/Row.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -937,12 +937,12 @@ void ROW::_resizeChars(uint16_t colEndDirty, uint16_t chBegDirty, size_t chEndDi
937937
}
938938
}
939939

940-
til::small_rle<TextAttribute, uint16_t, 1>& ROW::Attributes() noexcept
940+
ROW::AttributesType& ROW::Attributes() noexcept
941941
{
942942
return _attr;
943943
}
944944

945-
const til::small_rle<TextAttribute, uint16_t, 1>& ROW::Attributes() const noexcept
945+
const ROW::AttributesType& ROW::Attributes() const noexcept
946946
{
947947
return _attr;
948948
}

src/buffer/out/Row.hpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ struct CharToColumnMapper
9292
class ROW final
9393
{
9494
public:
95+
using AttributesType = til::small_rle<TextAttribute, uint16_t, 3>;
96+
9597
// The implicit agreement between ROW and TextBuffer is that the `charsBuffer` and `charOffsetsBuffer`
9698
// arrays have a minimum alignment of 16 Bytes and a size of `rowWidth+1`. The former is used to
9799
// implement Reset() efficiently via SIMD and the latter is used to store the past-the-end offset
@@ -148,8 +150,8 @@ class ROW final
148150
void ReplaceText(RowWriteState& state);
149151
void CopyTextFrom(RowCopyTextFromState& state);
150152

151-
til::small_rle<TextAttribute, uint16_t, 1>& Attributes() noexcept;
152-
const til::small_rle<TextAttribute, uint16_t, 1>& Attributes() const noexcept;
153+
AttributesType& Attributes() noexcept;
154+
const AttributesType& Attributes() const noexcept;
153155
TextAttribute GetAttrByColumn(til::CoordType column) const;
154156
std::vector<uint16_t> GetHyperlinks() const;
155157
ImageSlice* SetImageSlice(ImageSlice::Pointer imageSlice) noexcept;
@@ -297,7 +299,7 @@ class ROW final
297299
std::span<uint16_t> _charOffsets;
298300
// _attr is a run-length-encoded vector of TextAttribute with a decompressed
299301
// length equal to _columnCount (= 1 TextAttribute per column).
300-
til::small_rle<TextAttribute, uint16_t, 1> _attr;
302+
AttributesType _attr;
301303
// The width of the row in visual columns.
302304
uint16_t _columnCount = 0;
303305
// Stores double-width/height (DECSWL/DECDWL/DECDHL) attributes.

src/buffer/out/textBuffer.cpp

Lines changed: 72 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,14 @@ TextBuffer::TextBuffer(til::size screenBufferSize,
5959
_cursor{ cursorSize, *this },
6060
_isActiveBuffer{ isActiveBuffer }
6161
{
62-
// Guard against resizing the text buffer to 0 columns/rows, which would break being able to insert text.
63-
screenBufferSize.width = std::max(screenBufferSize.width, 1);
64-
screenBufferSize.height = std::max(screenBufferSize.height, 1);
6562
_reserve(screenBufferSize, defaultAttributes);
6663
}
6764

6865
TextBuffer::~TextBuffer()
6966
{
7067
if (_buffer)
7168
{
72-
_destroy();
69+
_destroy(_buffer.get());
7370
}
7471
}
7572

@@ -88,10 +85,11 @@ TextBuffer::~TextBuffer()
8885
// with our huge allocation, as well as to be able to reduce the private working set of
8986
// the application by only committing what we actually need. This reduces conhost's
9087
// memory usage from ~7MB down to just ~2MB at startup in the general case.
91-
void TextBuffer::_reserve(til::size screenBufferSize, const TextAttribute& defaultAttributes)
88+
__declspec(noinline) void TextBuffer::_reserve(til::size screenBufferSize, const TextAttribute& defaultAttributes)
9289
{
93-
const auto w = gsl::narrow<uint16_t>(screenBufferSize.width);
94-
const auto h = gsl::narrow<uint16_t>(screenBufferSize.height);
90+
// Guard against resizing the text buffer to 0 columns/rows, which would break being able to insert text.
91+
const auto w = std::clamp(screenBufferSize.width, 1, 0xffff);
92+
const auto h = std::clamp(screenBufferSize.height, 1, til::CoordTypeMax / 2 + UINT16_MAX);
9593

9694
constexpr auto rowSize = ROW::CalculateRowSize();
9795
const auto charsBufferSize = ROW::CalculateCharsBufferSize(w);
@@ -102,13 +100,13 @@ void TextBuffer::_reserve(til::size screenBufferSize, const TextAttribute& defau
102100
// 65535*65535 cells would result in a allocSize of 8GiB.
103101
// --> Use uint64_t so that we can safely do our calculations even on x86.
104102
// We allocate 1 additional row, which will be used for GetScratchpadRow().
105-
const auto rowCount = ::base::strict_cast<uint64_t>(h) + 1;
103+
const auto rowCount = gsl::narrow_cast<uint64_t>(h) + 1;
106104
const auto allocSize = gsl::narrow<size_t>(rowCount * rowStride);
107105

108106
// NOTE: Modifications to this block of code might have to be mirrored over to ResizeTraditional().
109107
// It constructs a temporary TextBuffer and then extracts the members below, overwriting itself.
110-
_buffer = wil::unique_virtualalloc_ptr<std::byte>{
111-
static_cast<std::byte*>(THROW_LAST_ERROR_IF_NULL(VirtualAlloc(nullptr, allocSize, MEM_RESERVE, PAGE_READWRITE)))
108+
_buffer = wil::unique_virtualalloc_ptr<uint8_t>{
109+
static_cast<uint8_t*>(THROW_LAST_ERROR_IF_NULL(VirtualAlloc(nullptr, allocSize, MEM_RESERVE, PAGE_READWRITE)))
112110
};
113111
_bufferEnd = _buffer.get() + allocSize;
114112
_commitWatermark = _buffer.get();
@@ -126,7 +124,7 @@ void TextBuffer::_reserve(til::size screenBufferSize, const TextAttribute& defau
126124
// Declaring this function as noinline allows _getRowByOffsetDirect() to be inlined,
127125
// which improves overall TextBuffer performance by ~6%. And all it cost is this annotation.
128126
// The compiler doesn't understand the likelihood of our branches. (PGO does, but that's imperfect.)
129-
__declspec(noinline) void TextBuffer::_commit(const std::byte* row)
127+
__declspec(noinline) void TextBuffer::_commit(const uint8_t* row)
130128
{
131129
assert(row >= _commitWatermark);
132130

@@ -143,29 +141,62 @@ __declspec(noinline) void TextBuffer::_commit(const std::byte* row)
143141

144142
// Destructs and MEM_DECOMMITs all previously constructed ROWs.
145143
// You can use this (or rather the Reset() method) to fully clear the TextBuffer.
146-
void TextBuffer::_decommit() noexcept
144+
void TextBuffer::_decommit(til::CoordType keep) noexcept
147145
{
148-
_destroy();
149-
VirtualFree(_buffer.get(), 0, MEM_DECOMMIT);
150-
_commitWatermark = _buffer.get();
146+
SYSTEM_INFO si;
147+
GetSystemInfo(&si);
148+
149+
keep = std::clamp(keep, 0, _height);
150+
keep += _commitReadAheadRowCount;
151+
keep = std::min(keep, _height);
152+
153+
// Amount of bytes that have been allocated with MEM_COMMIT so far.
154+
const auto commitBytes = gsl::narrow_cast<size_t>(_commitWatermark - _buffer.get());
155+
// Offset in bytes to the first row that we were asked to destroy.
156+
// We must ensure that the offset is not past the end of the current _commitWatermark,
157+
// since we don't want to finish with a watermark that's somehow larger than what we started with.
158+
const auto byteOffset = std::min(commitBytes, keep * _bufferRowStride);
159+
const auto newWatermark = _buffer.get() + byteOffset;
160+
// Since the last row we were asked to keep may reside in the middle
161+
// of a page, we must round the offset up to the next page boundary.
162+
// That offset will tell us the offset at which we will MEM_DECOMMIT memory.
163+
const auto pageMask = gsl::narrow_cast<size_t>(si.dwPageSize) - 1;
164+
const auto pageOffset = (byteOffset + pageMask) & ~pageMask;
165+
166+
// _destroy() takes care to check that the given pointer is valid.
167+
_destroy(newWatermark);
168+
169+
// MEM_DECOMMIT the memory that we don't need anymore.
170+
if (pageOffset < commitBytes)
171+
{
172+
VirtualFree(_buffer.get() + pageOffset, commitBytes - pageOffset, MEM_DECOMMIT);
173+
}
174+
175+
_commitWatermark = newWatermark;
151176
}
152177

153178
// Constructs ROWs between [_commitWatermark,until).
154-
void TextBuffer::_construct(const std::byte* until) noexcept
179+
void TextBuffer::_construct(const uint8_t* until) noexcept
155180
{
156-
for (; _commitWatermark < until; _commitWatermark += _bufferRowStride)
181+
// _width has been validated to fit into uint16_t during reserve().
182+
const auto width = gsl::narrow_cast<uint16_t>(_width);
183+
auto wm = _commitWatermark;
184+
185+
for (; wm < until; wm += _bufferRowStride)
157186
{
158-
const auto row = reinterpret_cast<ROW*>(_commitWatermark);
159-
const auto chars = reinterpret_cast<wchar_t*>(_commitWatermark + _bufferOffsetChars);
160-
const auto indices = reinterpret_cast<uint16_t*>(_commitWatermark + _bufferOffsetCharOffsets);
161-
std::construct_at(row, chars, indices, _width, _initialAttributes);
187+
const auto row = reinterpret_cast<ROW*>(wm);
188+
const auto chars = reinterpret_cast<wchar_t*>(wm + _bufferOffsetChars);
189+
const auto indices = reinterpret_cast<uint16_t*>(wm + _bufferOffsetCharOffsets);
190+
std::construct_at(row, chars, indices, width, _initialAttributes);
162191
}
192+
193+
_commitWatermark = wm;
163194
}
164195

165-
// Destructs ROWs between [_buffer,_commitWatermark).
166-
void TextBuffer::_destroy() const noexcept
196+
// Destructs ROWs between [it,_commitWatermark).
197+
void TextBuffer::_destroy(uint8_t* it) const noexcept
167198
{
168-
for (auto it = _buffer.get(); it < _commitWatermark; it += _bufferRowStride)
199+
for (; it < _commitWatermark; it += _bufferRowStride)
169200
{
170201
std::destroy_at(reinterpret_cast<ROW*>(it));
171202
}
@@ -973,7 +1004,7 @@ til::point TextBuffer::BufferToScreenPosition(const til::point position) const
9731004
// and the default current color attributes
9741005
void TextBuffer::Reset() noexcept
9751006
{
976-
_decommit();
1007+
_decommit(0);
9771008
_initialAttributes = _currentAttributes;
9781009
}
9791010

@@ -988,29 +1019,20 @@ void TextBuffer::ClearScrollback(const til::CoordType newFirstRow, const til::Co
9881019
return;
9891020
}
9901021
// The new viewport should keep 0 rows? Then just reset everything.
991-
if (rowsToKeep <= 0)
1022+
if (rowsToKeep > 0)
9921023
{
993-
_decommit();
994-
return;
1024+
// Our goal is to move the viewport to the absolute start of the underlying memory buffer so that we can
1025+
// MEM_DECOMMIT the remaining memory. _firstRow is used to make the TextBuffer behave like a circular buffer.
1026+
// The newFirstRow parameter is relative to the _firstRow. The trick to get the content to the absolute start
1027+
// is to simply add _firstRow ourselves and then reset it to 0. This causes ScrollRows() to write into
1028+
// the absolute start while reading from relative coordinates. This works because GetRowByOffset()
1029+
// operates modulo the buffer height and so the possibly-too-large startAbsolute won't be an issue.
1030+
const auto startAbsolute = _firstRow + newFirstRow;
1031+
_firstRow = 0;
1032+
ScrollRows(startAbsolute, rowsToKeep, -startAbsolute);
9951033
}
9961034

997-
ClearMarksInRange(til::point{ 0, 0 }, til::point{ _width, std::max(0, newFirstRow - 1) });
998-
999-
// Our goal is to move the viewport to the absolute start of the underlying memory buffer so that we can
1000-
// MEM_DECOMMIT the remaining memory. _firstRow is used to make the TextBuffer behave like a circular buffer.
1001-
// The newFirstRow parameter is relative to the _firstRow. The trick to get the content to the absolute start
1002-
// is to simply add _firstRow ourselves and then reset it to 0. This causes ScrollRows() to write into
1003-
// the absolute start while reading from relative coordinates. This works because GetRowByOffset()
1004-
// operates modulo the buffer height and so the possibly-too-large startAbsolute won't be an issue.
1005-
const auto startAbsolute = _firstRow + newFirstRow;
1006-
_firstRow = 0;
1007-
ScrollRows(startAbsolute, rowsToKeep, -startAbsolute);
1008-
1009-
const auto end = _estimateOffsetOfLastCommittedRow();
1010-
for (auto y = rowsToKeep; y <= end; ++y)
1011-
{
1012-
GetMutableRowByOffset(y).Reset(_initialAttributes);
1013-
}
1035+
_decommit(rowsToKeep);
10141036
}
10151037

10161038
// Routine Description:
@@ -2015,7 +2037,7 @@ std::string TextBuffer::GenHTML(const CopyRequest& req,
20152037
const auto [rowBeg, rowEnd, addLineBreak] = _RowCopyHelper(req, iRow, row);
20162038
const auto rowBegU16 = gsl::narrow_cast<uint16_t>(rowBeg);
20172039
const auto rowEndU16 = gsl::narrow_cast<uint16_t>(rowEnd);
2018-
const auto runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();
2040+
const auto& runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();
20192041

20202042
auto x = rowBegU16;
20212043
for (const auto& [attr, length] : runs)
@@ -2265,7 +2287,7 @@ std::string TextBuffer::GenRTF(const CopyRequest& req,
22652287
const auto [rowBeg, rowEnd, addLineBreak] = _RowCopyHelper(req, iRow, row);
22662288
const auto rowBegU16 = gsl::narrow_cast<uint16_t>(rowBeg);
22672289
const auto rowEndU16 = gsl::narrow_cast<uint16_t>(rowEnd);
2268-
const auto runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();
2290+
const auto& runs = row.Attributes().slice(rowBegU16, rowEndU16).runs();
22692291

22702292
auto x = rowBegU16;
22712293
for (auto& [attr, length] : runs)
@@ -2457,7 +2479,7 @@ void TextBuffer::_SerializeRow(const ROW& row, const til::CoordType startX, cons
24572479

24582480
const auto startXU16 = gsl::narrow_cast<uint16_t>(startX);
24592481
const auto endXU16 = gsl::narrow_cast<uint16_t>(endX);
2460-
const auto runs = row.Attributes().slice(startXU16, endXU16).runs();
2482+
const auto& runs = row.Attributes().slice(startXU16, endXU16).runs();
24612483

24622484
const auto beg = runs.begin();
24632485
const auto end = runs.end();
@@ -3246,7 +3268,6 @@ MarkExtents TextBuffer::_scrollMarkExtentForRow(const til::CoordType rowOffset,
32463268
bool startedPrompt = false;
32473269
bool startedCommand = false;
32483270
bool startedOutput = false;
3249-
MarkKind lastMarkKind = MarkKind::Output;
32503271

32513272
const auto endThisMark = [&](auto x, auto y) {
32523273
if (startedOutput)
@@ -3273,7 +3294,7 @@ MarkExtents TextBuffer::_scrollMarkExtentForRow(const til::CoordType rowOffset,
32733294
// Output attribute.
32743295

32753296
const auto& row = GetRowByOffset(y);
3276-
const auto runs = row.Attributes().runs();
3297+
const auto& runs = row.Attributes().runs();
32773298
x = 0;
32783299
for (const auto& [attr, length] : runs)
32793300
{
@@ -3316,8 +3337,6 @@ MarkExtents TextBuffer::_scrollMarkExtentForRow(const til::CoordType rowOffset,
33163337

33173338
endThisMark(lastMarkedText.x, lastMarkedText.y);
33183339
}
3319-
// Otherwise, we've changed from any state -> any state, and it doesn't really matter.
3320-
lastMarkKind = markKind;
33213340
}
33223341
// advance to next run of text
33233342
x = nextX;
@@ -3350,7 +3369,7 @@ std::wstring TextBuffer::_commandForRow(const til::CoordType rowOffset,
33503369
// Command attributes. Collect up all of those, till we get to the next
33513370
// Output attribute.
33523371
const auto& row = GetRowByOffset(y);
3353-
const auto runs = row.Attributes().runs();
3372+
const auto& runs = row.Attributes().runs();
33543373
auto x = 0;
33553374
for (const auto& [attr, length] : runs)
33563375
{

src/buffer/out/textBuffer.hpp

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -311,10 +311,10 @@ class TextBuffer final
311311

312312
private:
313313
void _reserve(til::size screenBufferSize, const TextAttribute& defaultAttributes);
314-
void _commit(const std::byte* row);
315-
void _decommit() noexcept;
316-
void _construct(const std::byte* until) noexcept;
317-
void _destroy() const noexcept;
314+
void _commit(const uint8_t* row);
315+
void _decommit(til::CoordType keep) noexcept;
316+
void _construct(const uint8_t* until) noexcept;
317+
void _destroy(uint8_t* it) const noexcept;
318318
ROW& _getRowByOffsetDirect(size_t offset);
319319
ROW& _getRow(til::CoordType y) const;
320320
til::CoordType _estimateOffsetOfLastCommittedRow() const noexcept;
@@ -357,9 +357,9 @@ class TextBuffer final
357357
// Padding may exist for alignment purposes.
358358
//
359359
// The base (start) address of the memory arena.
360-
wil::unique_virtualalloc_ptr<std::byte> _buffer;
360+
wil::unique_virtualalloc_ptr<uint8_t> _buffer;
361361
// The past-the-end pointer of the memory arena.
362-
std::byte* _bufferEnd = nullptr;
362+
uint8_t* _bufferEnd = nullptr;
363363
// The range between _buffer (inclusive) and _commitWatermark (exclusive) is the range of
364364
// memory that has already been committed via MEM_COMMIT and contains ready-to-use ROWs.
365365
//
@@ -375,15 +375,15 @@ class TextBuffer final
375375
// _commitWatermark will always be a multiple of _bufferRowStride away from _buffer.
376376
// In other words, _commitWatermark itself will either point exactly onto the next ROW
377377
// that should be committed or be equal to _bufferEnd when all ROWs are committed.
378-
std::byte* _commitWatermark = nullptr;
379-
// This will MEM_COMMIT 128 rows more than we need, to avoid us from having to call VirtualAlloc too often.
378+
uint8_t* _commitWatermark = nullptr;
379+
// This will MEM_COMMIT 256 rows more than we need, to avoid us from having to call VirtualAlloc too often.
380380
// This equates to roughly the following commit chunk sizes at these column counts:
381-
// * 80 columns (the usual minimum) = 60KB chunks, 4.1MB buffer at 9001 rows
382-
// * 120 columns (the most common) = 80KB chunks, 5.6MB buffer at 9001 rows
383-
// * 400 columns (the usual maximum) = 220KB chunks, 15.5MB buffer at 9001 rows
381+
// * 80 columns (the usual minimum) = 120KB chunks, 4.1MB buffer at 9001 rows
382+
// * 120 columns (the most common) = 160KB chunks, 5.6MB buffer at 9001 rows
383+
// * 400 columns (the usual maximum) = 440KB chunks, 15.5MB buffer at 9001 rows
384384
// There's probably a better metric than this. (This comment was written when ROW had both,
385385
// a _chars array containing text and a _charOffsets array contain column-to-text indices.)
386-
static constexpr size_t _commitReadAheadRowCount = 128;
386+
static constexpr size_t _commitReadAheadRowCount = 256;
387387
// Before TextBuffer was made to use virtual memory it initialized the entire memory arena with the initial
388388
// attributes right away. To ensure it continues to work the way it used to, this stores these initial attributes.
389389
TextAttribute _initialAttributes;
@@ -397,9 +397,9 @@ class TextBuffer final
397397
size_t _bufferOffsetChars = 0;
398398
size_t _bufferOffsetCharOffsets = 0;
399399
// The width of the buffer in columns.
400-
uint16_t _width = 0;
400+
til::CoordType _width = 0;
401401
// The height of the buffer in rows, excluding the scratchpad row.
402-
uint16_t _height = 0;
402+
til::CoordType _height = 0;
403403

404404
TextAttribute _currentAttributes;
405405
til::CoordType _firstRow = 0; // indexes top row (not necessarily 0)

src/buffer/out/textBufferCellIterator.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class TextBufferCellIterator
5353
void _GenerateView() noexcept;
5454
static const ROW* s_GetRow(const TextBuffer& buffer, const til::point pos);
5555

56-
til::small_rle<TextAttribute, uint16_t, 1>::const_iterator _attrIter;
56+
ROW::AttributesType::const_iterator _attrIter;
5757
OutputCellView _view;
5858

5959
const ROW* _pRow;

0 commit comments

Comments
 (0)