Skip to content

Commit ecb5e37

Browse files
authored
Use new row primitives for ResizeTraditional (#15105)
This will allow us to share the same fundamental text insertion logic for both `ResizeTraditional` and `Reflow`, because both can be implemented with `ROW::CopyRangeFrom`. It also replaces the `BufferAllocator` struct with a `_allocateBuffer` function which will help us allocate scratch buffer rows in the future. Closes #14696 ## PR Checklist * Disable reflow resize in conhost * Print "zhwik8.txt" - a enwik8.txt equivalent of Chinese Wikipedia * Run `color 80` in cmd * Resize windows from 120 to 119 columns * Wide glyphs disappear and are replaced with whitespace ✅ * Resizing the window to >120 columns adds gray whitespace ✅
1 parent aea0477 commit ecb5e37

File tree

4 files changed

+73
-188
lines changed

4 files changed

+73
-188
lines changed

Diff for: src/buffer/out/Row.cpp

-85
Original file line numberDiff line numberDiff line change
@@ -88,19 +88,6 @@ ROW::ROW(wchar_t* charsBuffer, uint16_t* charOffsetsBuffer, uint16_t rowWidth, c
8888
}
8989
}
9090

91-
void swap(ROW& lhs, ROW& rhs) noexcept
92-
{
93-
std::swap(lhs._charsBuffer, rhs._charsBuffer);
94-
std::swap(lhs._charsHeap, rhs._charsHeap);
95-
std::swap(lhs._chars, rhs._chars);
96-
std::swap(lhs._charOffsets, rhs._charOffsets);
97-
std::swap(lhs._attr, rhs._attr);
98-
std::swap(lhs._columnCount, rhs._columnCount);
99-
std::swap(lhs._lineRendition, rhs._lineRendition);
100-
std::swap(lhs._wrapForced, rhs._wrapForced);
101-
std::swap(lhs._doubleBytePadded, rhs._doubleBytePadded);
102-
}
103-
10491
void ROW::SetWrapForced(const bool wrap) noexcept
10592
{
10693
_wrapForced = wrap;
@@ -154,78 +141,6 @@ void ROW::_init() noexcept
154141
std::iota(_charOffsets.begin(), _charOffsets.end(), uint16_t{ 0 });
155142
}
156143

157-
// Routine Description:
158-
// - resizes ROW to new width
159-
// Arguments:
160-
// - charsBuffer - a new backing buffer to use for _charsBuffer
161-
// - charOffsetsBuffer - a new backing buffer to use for _charOffsets
162-
// - rowWidth - the new width, in cells
163-
// - fillAttribute - the attribute to use for any newly added, trailing cells
164-
void ROW::Resize(wchar_t* charsBuffer, uint16_t* charOffsetsBuffer, uint16_t rowWidth, const TextAttribute& fillAttribute)
165-
{
166-
// A default-constructed ROW has no cols/chars to copy.
167-
// It can be detected by the lack of a _charsBuffer (among others).
168-
//
169-
// Otherwise, this block figures out how much we can copy into the new `rowWidth`.
170-
uint16_t colsToCopy = 0;
171-
uint16_t charsToCopy = 0;
172-
if (_charsBuffer)
173-
{
174-
colsToCopy = std::min(rowWidth, _columnCount);
175-
// Safety: colsToCopy is [0, _columnCount].
176-
charsToCopy = _uncheckedCharOffset(colsToCopy);
177-
// Safety: colsToCopy is [0, _columnCount] due to colsToCopy != 0.
178-
for (; colsToCopy != 0 && _uncheckedIsTrailer(colsToCopy); --colsToCopy)
179-
{
180-
}
181-
}
182-
183-
// If we grow the row width, we have to append a bunch of whitespace.
184-
// `trailingWhitespace` stores that amount.
185-
// Safety: The preceding block left colsToCopy in the range [0, rowWidth].
186-
const uint16_t trailingWhitespace = rowWidth - colsToCopy;
187-
188-
// Allocate memory for the new `_chars` array.
189-
// Use the provided charsBuffer if possible, otherwise allocate a `_charsHeap`.
190-
std::unique_ptr<wchar_t[]> charsHeap;
191-
std::span chars{ charsBuffer, rowWidth };
192-
const std::span charOffsets{ charOffsetsBuffer, ::base::strict_cast<size_t>(rowWidth) + 1u };
193-
if (const uint16_t charsCapacity = charsToCopy + trailingWhitespace; charsCapacity > rowWidth)
194-
{
195-
charsHeap = std::make_unique_for_overwrite<wchar_t[]>(charsCapacity);
196-
chars = { charsHeap.get(), charsCapacity };
197-
}
198-
199-
// Copy chars and charOffsets over.
200-
{
201-
const auto it = std::copy_n(_chars.begin(), charsToCopy, chars.begin());
202-
std::fill_n(it, trailingWhitespace, L' ');
203-
}
204-
{
205-
const auto it = std::copy_n(_charOffsets.begin(), colsToCopy, charOffsets.begin());
206-
// The _charOffsets array is 1 wider than newWidth indicates.
207-
// This is because the extra column contains the past-the-end index into _chars.
208-
iota_n(it, trailingWhitespace + 1u, charsToCopy);
209-
}
210-
211-
_charsBuffer = charsBuffer;
212-
_charsHeap = std::move(charsHeap);
213-
_chars = chars;
214-
_charOffsets = charOffsets;
215-
_columnCount = rowWidth;
216-
217-
// .resize_trailing_extent() doesn't work if the vector is empty,
218-
// since there's no trailing item that could be extended.
219-
if (_attr.empty())
220-
{
221-
_attr = { rowWidth, fillAttribute };
222-
}
223-
else
224-
{
225-
_attr.resize_trailing_extent(rowWidth);
226-
}
227-
}
228-
229144
void ROW::TransferAttributes(const til::small_rle<TextAttribute, uint16_t, 1>& attr, til::CoordType newWidth)
230145
{
231146
_attr = attr;

Diff for: src/buffer/out/Row.hpp

+1-4
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,9 @@ class ROW final
6666
ROW(const ROW& other) = delete;
6767
ROW& operator=(const ROW& other) = delete;
6868

69-
explicit ROW(ROW&& other) = default;
69+
ROW(ROW&& other) = default;
7070
ROW& operator=(ROW&& other) = default;
7171

72-
friend void swap(ROW& lhs, ROW& rhs) noexcept;
73-
7472
void SetWrapForced(const bool wrap) noexcept;
7573
bool WasWrapForced() const noexcept;
7674
void SetDoubleBytePadded(const bool doubleBytePadded) noexcept;
@@ -79,7 +77,6 @@ class ROW final
7977
LineRendition GetLineRendition() const noexcept;
8078

8179
void Reset(const TextAttribute& attr);
82-
void Resize(wchar_t* charsBuffer, uint16_t* charOffsetsBuffer, uint16_t rowWidth, const TextAttribute& fillAttribute);
8380
void TransferAttributes(const til::small_rle<TextAttribute, uint16_t, 1>& attr, til::CoordType newWidth);
8481

8582
til::CoordType NavigateToPrevious(til::CoordType column) const noexcept;

Diff for: src/buffer/out/textBuffer.cpp

+70-99
Original file line numberDiff line numberDiff line change
@@ -13,75 +13,6 @@
1313
#include "../types/inc/convert.hpp"
1414
#include "../../types/inc/GlyphWidth.hpp"
1515

16-
namespace
17-
{
18-
struct BufferAllocator
19-
{
20-
BufferAllocator(til::size sz)
21-
{
22-
const auto w = gsl::narrow<uint16_t>(sz.width);
23-
const auto h = gsl::narrow<uint16_t>(sz.height);
24-
25-
const auto charsBytes = w * sizeof(wchar_t);
26-
// The ROW::_indices array stores 1 more item than the buffer is wide.
27-
// That extra column stores the past-the-end _chars pointer.
28-
const auto indicesBytes = w * sizeof(uint16_t) + sizeof(uint16_t);
29-
const auto rowStride = charsBytes + indicesBytes;
30-
// 65535*65535 cells would result in a charsAreaSize of 8GiB.
31-
// --> Use uint64_t so that we can safely do our calculations even on x86.
32-
const auto allocSize = gsl::narrow<size_t>(::base::strict_cast<uint64_t>(rowStride) * ::base::strict_cast<uint64_t>(h));
33-
34-
_buffer = wil::unique_virtualalloc_ptr<std::byte>{ static_cast<std::byte*>(VirtualAlloc(nullptr, allocSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)) };
35-
THROW_IF_NULL_ALLOC(_buffer);
36-
37-
_data = std::span{ _buffer.get(), allocSize }.begin();
38-
_rowStride = rowStride;
39-
_indicesOffset = charsBytes;
40-
_width = w;
41-
_height = h;
42-
}
43-
44-
BufferAllocator& operator++() noexcept
45-
{
46-
_data += _rowStride;
47-
return *this;
48-
}
49-
50-
wchar_t* chars() const noexcept
51-
{
52-
return til::bit_cast<wchar_t*>(&*_data);
53-
}
54-
55-
uint16_t* indices() const noexcept
56-
{
57-
return til::bit_cast<uint16_t*>(&*(_data + _indicesOffset));
58-
}
59-
60-
uint16_t width() const noexcept
61-
{
62-
return _width;
63-
}
64-
65-
uint16_t height() const noexcept
66-
{
67-
return _height;
68-
}
69-
70-
wil::unique_virtualalloc_ptr<std::byte>&& take() noexcept
71-
{
72-
return std::move(_buffer);
73-
}
74-
75-
private:
76-
wil::unique_virtualalloc_ptr<std::byte> _buffer;
77-
std::span<std::byte>::iterator _data;
78-
size_t _rowStride;
79-
size_t _indicesOffset;
80-
uint16_t _width;
81-
uint16_t _height;
82-
};
83-
}
84-
8516
using namespace Microsoft::Console;
8617
using namespace Microsoft::Console::Types;
8718

@@ -111,16 +42,7 @@ TextBuffer::TextBuffer(til::size screenBufferSize,
11142
// Guard against resizing the text buffer to 0 columns/rows, which would break being able to insert text.
11243
screenBufferSize.width = std::max(screenBufferSize.width, 1);
11344
screenBufferSize.height = std::max(screenBufferSize.height, 1);
114-
115-
BufferAllocator allocator{ screenBufferSize };
116-
117-
_storage.reserve(allocator.height());
118-
for (til::CoordType i = 0; i < screenBufferSize.height; ++i, ++allocator)
119-
{
120-
_storage.emplace_back(allocator.chars(), allocator.indices(), allocator.width(), _currentAttributes);
121-
}
122-
123-
_charBuffer = allocator.take();
45+
_charBuffer = _allocateBuffer(screenBufferSize, _currentAttributes, _storage);
12446
_UpdateSize();
12547
}
12648

@@ -775,6 +697,37 @@ const Viewport TextBuffer::GetSize() const noexcept
775697
return _size;
776698
}
777699

700+
wil::unique_virtualalloc_ptr<std::byte> TextBuffer::_allocateBuffer(til::size sz, const TextAttribute& attributes, std::vector<ROW>& rows)
701+
{
702+
const auto w = gsl::narrow<uint16_t>(sz.width);
703+
const auto h = gsl::narrow<uint16_t>(sz.height);
704+
705+
const auto charsBytes = w * sizeof(wchar_t);
706+
// The ROW::_indices array stores 1 more item than the buffer is wide.
707+
// That extra column stores the past-the-end _chars pointer.
708+
const auto indicesBytes = w * sizeof(uint16_t) + sizeof(uint16_t);
709+
const auto rowStride = charsBytes + indicesBytes;
710+
// 65535*65535 cells would result in a charsAreaSize of 8GiB.
711+
// --> Use uint64_t so that we can safely do our calculations even on x86.
712+
const auto allocSize = gsl::narrow<size_t>(::base::strict_cast<uint64_t>(rowStride) * ::base::strict_cast<uint64_t>(h));
713+
714+
auto buffer = wil::unique_virtualalloc_ptr<std::byte>{ static_cast<std::byte*>(VirtualAlloc(nullptr, allocSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)) };
715+
THROW_IF_NULL_ALLOC(buffer);
716+
717+
auto data = std::span{ buffer.get(), allocSize }.begin();
718+
719+
rows.resize(h);
720+
for (auto& row : rows)
721+
{
722+
const auto chars = til::bit_cast<wchar_t*>(&*data);
723+
const auto indices = til::bit_cast<uint16_t*>(&*(data + charsBytes));
724+
row = { chars, indices, w, attributes };
725+
data += rowStride;
726+
}
727+
728+
return buffer;
729+
}
730+
778731
void TextBuffer::_UpdateSize()
779732
{
780733
_size = Viewport::FromDimensions({ _storage.at(0).size(), gsl::narrow<til::CoordType>(_storage.size()) });
@@ -1001,37 +954,55 @@ void TextBuffer::Reset()
1001954

1002955
try
1003956
{
1004-
BufferAllocator allocator{ newSize };
1005-
1006-
const auto currentSize = GetSize().Dimensions();
1007-
const auto attributes = GetCurrentAttributes();
1008-
1009957
til::CoordType TopRow = 0; // new top row of the screen buffer
1010958
if (newSize.height <= GetCursor().GetPosition().y)
1011959
{
1012960
TopRow = GetCursor().GetPosition().y - newSize.height + 1;
1013961
}
1014-
const auto TopRowIndex = (GetFirstRowIndex() + TopRow) % currentSize.height;
962+
const auto TopRowIndex = gsl::narrow_cast<size_t>(_firstRow + TopRow) % _storage.size();
1015963

1016-
// rotate rows until the top row is at index 0
1017-
std::rotate(_storage.begin(), _storage.begin() + TopRowIndex, _storage.end());
1018-
_SetFirstRowIndex(0);
1019-
1020-
// realloc in the Y direction
1021-
// remove rows if we're shrinking
1022-
_storage.resize(allocator.height());
964+
std::vector<ROW> newStorage;
965+
auto newBuffer = _allocateBuffer(newSize, _currentAttributes, newStorage);
1023966

1024-
// realloc in the X direction
1025-
for (auto& it : _storage)
967+
// This basically imitates a std::rotate_copy(first, mid, last), but uses ROW::CopyRangeFrom() to do the copying.
1026968
{
1027-
it.Resize(allocator.chars(), allocator.indices(), allocator.width(), attributes);
1028-
++allocator;
969+
const auto first = _storage.begin();
970+
const auto last = _storage.end();
971+
const auto mid = first + TopRowIndex;
972+
auto dest = newStorage.begin();
973+
974+
std::span<ROW> sourceRanges[]{
975+
{ mid, last },
976+
{ first, mid },
977+
};
978+
979+
// Ensure we don't copy more from `_storage` than fit into `newStorage`.
980+
if (sourceRanges[0].size() > newStorage.size())
981+
{
982+
sourceRanges[0] = sourceRanges[0].subspan(0, newStorage.size());
983+
}
984+
if (const auto remaining = newStorage.size() - sourceRanges[0].size(); sourceRanges[1].size() > remaining)
985+
{
986+
sourceRanges[1] = sourceRanges[1].subspan(0, remaining);
987+
}
988+
989+
for (const auto& sourceRange : sourceRanges)
990+
{
991+
for (const auto& oldRow : sourceRange)
992+
{
993+
til::CoordType begin = 0;
994+
dest->CopyRangeFrom(0, til::CoordTypeMax, oldRow, begin, til::CoordTypeMax);
995+
dest->TransferAttributes(oldRow.Attributes(), newSize.width);
996+
++dest;
997+
}
998+
}
1029999
}
10301000

1031-
// Update the cached size value
1032-
_UpdateSize();
1001+
_charBuffer = std::move(newBuffer);
1002+
_storage = std::move(newStorage);
10331003

1034-
_charBuffer = allocator.take();
1004+
_SetFirstRowIndex(0);
1005+
_UpdateSize();
10351006
}
10361007
CATCH_RETURN();
10371008

Diff for: src/buffer/out/textBuffer.hpp

+2
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ class TextBuffer final
219219
interval_tree::IntervalTree<til::point, size_t> GetPatterns(const til::CoordType firstRow, const til::CoordType lastRow) const;
220220

221221
private:
222+
static wil::unique_virtualalloc_ptr<std::byte> _allocateBuffer(til::size sz, const TextAttribute& attributes, std::vector<ROW>& rows);
223+
222224
void _UpdateSize();
223225
void _SetFirstRowIndex(const til::CoordType FirstRowIndex) noexcept;
224226
til::point _GetPreviousFromCursor() const noexcept;

0 commit comments

Comments
 (0)