Skip to content

Add an efficient text stream write function #14821

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

Merged
merged 18 commits into from
Mar 24, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
355 changes: 291 additions & 64 deletions src/buffer/out/Row.cpp

Large diffs are not rendered by default.

66 changes: 63 additions & 3 deletions src/buffer/out/Row.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ Revision History:

#pragma once

#include <span>

#include <til/rle.h>

#include "LineRendition.hpp"
Expand All @@ -37,6 +35,34 @@ enum class DelimiterClass
RegularChar
};

struct RowTextIterator
{
RowTextIterator(std::span<const wchar_t> chars, std::span<const uint16_t> charOffsets, uint16_t offset) noexcept;

bool operator==(const RowTextIterator& other) const noexcept;
RowTextIterator& operator++() noexcept;
const RowTextIterator& operator*() const noexcept;

std::wstring_view Text() const noexcept;
til::CoordType Cols() const noexcept;
DbcsAttribute DbcsAttr() const noexcept;

private:
uint16_t _uncheckedCharOffset(size_t col) const noexcept;
bool _uncheckedIsTrailer(size_t col) const noexcept;

// To simplify the detection of wide glyphs, we don't just store the simple character offset as described
// for _charOffsets. Instead we use the most significant bit to indicate whether any column is the
// trailing half of a wide glyph. This simplifies many implementation details via _uncheckedIsTrailer.
static constexpr uint16_t CharOffsetsTrailer = 0x8000;
static constexpr uint16_t CharOffsetsMask = 0x7fff;

std::span<const wchar_t> _chars;
std::span<const uint16_t> _charOffsets;
uint16_t _beg;
uint16_t _end;
};

class ROW final
{
public:
Expand All @@ -57,16 +83,23 @@ class ROW final
bool WasDoubleBytePadded() const noexcept;
void SetLineRendition(const LineRendition lineRendition) noexcept;
LineRendition GetLineRendition() const noexcept;
RowTextIterator Begin() const noexcept;
RowTextIterator End() const noexcept;

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

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

void ClearCell(til::CoordType column);
OutputCellIterator WriteCells(OutputCellIterator it, til::CoordType columnBegin, std::optional<bool> wrap = std::nullopt, std::optional<til::CoordType> limitRight = std::nullopt);
bool SetAttrToEnd(til::CoordType columnBegin, TextAttribute attr);
void ReplaceAttributes(til::CoordType beginIndex, til::CoordType endIndex, const TextAttribute& newAttr);
void ReplaceCharacters(til::CoordType columnBegin, til::CoordType width, const std::wstring_view& chars);
til::CoordType Write(til::CoordType columnBegin, til::CoordType columnLimit, std::wstring_view& chars);
til::CoordType WriteWithOffsets(til::CoordType columnBegin, til::CoordType columnLimit, std::wstring_view& chars, std::span<const uint16_t>& charOffsetsPtr);

const til::small_rle<TextAttribute, uint16_t, 1>& Attributes() const noexcept;
TextAttribute GetAttrByColumn(til::CoordType column) const;
Expand All @@ -89,6 +122,30 @@ class ROW final
#endif

private:
// WriteHelper exists because other forms of abstracting this functionality away (like templates with lambdas)
// where only very poorly optimized by MSVC as it failed to inline the templates.
struct WriteHelper
{
explicit WriteHelper(ROW& row, til::CoordType columnBegin, til::CoordType columnLimit, const std::wstring_view& chars) noexcept;
bool IsValid() const noexcept;
void ReplaceCharacters(til::CoordType width) noexcept;
void Write() noexcept;
void WriteWithOffsets(const std::span<const uint16_t>& charOffsets) noexcept;
void Finish();

ROW& row;
const std::wstring_view& chars;
uint16_t colBeg;
uint16_t colLimit;
uint16_t chExtBeg;
uint16_t colExtBeg;
uint16_t leadingSpaces;
uint16_t chBeg;
uint16_t colEnd;
uint16_t colExtEnd;
size_t charsConsumed;
};

// To simplify the detection of wide glyphs, we don't just store the simple character offset as described
// for _charOffsets. Instead we use the most significant bit to indicate whether any column is the
// trailing half of a wide glyph. This simplifies many implementation details via _uncheckedIsTrailer.
Expand All @@ -102,13 +159,16 @@ class ROW final
template<typename T>
constexpr uint16_t _clampedColumnInclusive(T v) const noexcept;

uint16_t _adjustBackward(uint16_t column) const noexcept;
uint16_t _adjustForward(uint16_t column) const noexcept;

wchar_t _uncheckedChar(size_t off) const noexcept;
uint16_t _charSize() const noexcept;
uint16_t _uncheckedCharOffset(size_t col) const noexcept;
bool _uncheckedIsTrailer(size_t col) const noexcept;

void _init() noexcept;
void _resizeChars(uint16_t colExtEnd, uint16_t chExtBeg, uint16_t chExtEnd, size_t chExtEndNew);
void _resizeChars(uint16_t colExtEnd, uint16_t chExtBeg, uint16_t chExtEndOld, size_t chExtEndNew);

// These fields are a bit "wasteful", but it makes all this a bit more robust against
// programming errors during initial development (which is when this comment was written).
Expand Down
12 changes: 12 additions & 0 deletions src/buffer/out/textBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,18 @@ bool TextBuffer::_PrepareForDoubleByteSequence(const DbcsAttribute dbcsAttribute
return fSuccess;
}

til::CoordType TextBuffer::Write(til::CoordType row, til::CoordType columnBegin, til::CoordType columnLimit, bool wrapAtEOL, const TextAttribute& attributes, std::wstring_view& chars)
{
auto& r = GetRowByOffset(row);

const auto columnEnd = r.Write(columnBegin, columnLimit, chars);
r.ReplaceAttributes(columnBegin, columnEnd, attributes);
r.SetWrapForced(wrapAtEOL && columnEnd == r.size());

TriggerRedraw(Viewport::FromExclusive({ columnBegin, row, columnEnd, row + 1 }));
return columnEnd;
}

// Routine Description:
// - Writes cells to the output buffer. Writes at the cursor.
// Arguments:
Expand Down
2 changes: 2 additions & 0 deletions src/buffer/out/textBuffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ class TextBuffer final
TextBufferTextIterator GetTextDataAt(const til::point at, const Microsoft::Console::Types::Viewport limit) const;

// Text insertion functions
til::CoordType Write(til::CoordType row, til::CoordType columnBegin, til::CoordType columnLimit, bool wrapAtEOL, const TextAttribute& attributes, std::wstring_view& chars);

OutputCellIterator Write(const OutputCellIterator givenIt);

OutputCellIterator Write(const OutputCellIterator givenIt,
Expand Down
46 changes: 46 additions & 0 deletions src/host/ut_host/TextBufferTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ class TextBufferTests

TEST_METHOD(TestBurrito);
TEST_METHOD(TestOverwriteChars);
TEST_METHOD(TestRowWrite);

TEST_METHOD(TestAppendRTFText);

Expand Down Expand Up @@ -2046,6 +2047,51 @@ void TextBufferTests::TestOverwriteChars()
#undef complex1
}

void TextBufferTests::TestRowWrite()
{
til::size bufferSize{ 10, 3 };
UINT cursorSize = 12;
TextAttribute attr{ 0x7f };
TextBuffer buffer{ bufferSize, attr, cursorSize, false, _renderer };
auto& row = buffer.GetRowByOffset(0);
std::wstring_view str;
til::CoordType pos = 0;

#define complex L"\U0001F41B"

// Not enough space -> early exit
str = complex;
pos = row.Write(2, 2, str);
VERIFY_ARE_EQUAL(2, pos);
VERIFY_ARE_EQUAL(complex, str);
VERIFY_ARE_EQUAL(L" ", row.GetText());

// Writing with the exact right amount of space
str = complex;
pos = row.Write(2, 4, str);
VERIFY_ARE_EQUAL(4, pos);
VERIFY_ARE_EQUAL(L"", str);
VERIFY_ARE_EQUAL(L" " complex L" ", row.GetText());

// Overwrite a wide character, but with not enough space left
str = complex complex;
pos = row.Write(0, 3, str);
// It's not quite clear what Write() should return in that case,
// so this simply asserts on what it happens to return right now.
VERIFY_ARE_EQUAL(4, pos);
VERIFY_ARE_EQUAL(complex, str);
VERIFY_ARE_EQUAL(complex L" ", row.GetText());

// Various text, too much to fit into the row
str = L"a" complex L"b" complex L"c" complex L"foo";
pos = row.Write(1, til::CoordTypeMax, str);
VERIFY_ARE_EQUAL(10, pos);
VERIFY_ARE_EQUAL(L"foo", str);
VERIFY_ARE_EQUAL(L" a" complex L"b" complex L"c" complex, row.GetText());

#undef complex
}

void TextBufferTests::TestAppendRTFText()
{
{
Expand Down
43 changes: 7 additions & 36 deletions src/inc/test/CommonState.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -256,49 +256,20 @@ class CommonState
std::unique_ptr<TextBuffer> m_backupTextBufferInfo;
std::unique_ptr<INPUT_READ_HANDLE_DATA> m_readHandle;

struct TestString
{
std::wstring_view string;
bool wide = false;
};

static void applyTestString(ROW* pRow, const auto& testStrings)
{
uint16_t x = 0;
for (const auto& t : testStrings)
{
if (t.wide)
{
pRow->ReplaceCharacters(x, 2, t.string);
x += 2;
}
else
{
for (const auto& ch : t.string)
{
pRow->ReplaceCharacters(x, 1, { &ch, 1 });
x += 1;
}
}
}
}

void FillRow(ROW* pRow, bool wrapForced)
{
// fill a row
// 9 characters, 6 spaces. 15 total
// か = \x304b
// き = \x304d

static constexpr std::array testStrings{
TestString{ L"AB" },
TestString{ L"\x304b", true },
TestString{ L"C" },
TestString{ L"\x304d", true },
TestString{ L"DE " },
};

applyTestString(pRow, testStrings);
uint16_t column = 0;
for (const auto& ch : std::wstring_view{ L"AB\u304bC\u304dDE " })
{
const uint16_t width = ch >= 0x80 ? 2 : 1;
pRow->ReplaceCharacters(column, width, { &ch, 1 });
column += width;
}
Comment on lines +266 to +272
Copy link
Member Author

Choose a reason for hiding this comment

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

I forgot why I made this change... But it's much shorter now, so there's that at least.


// A = bright red on dark gray
// This string starts at index 0
Expand Down
2 changes: 2 additions & 0 deletions src/inc/til/point.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
using CoordType = int32_t;
inline constexpr CoordType CoordTypeMin = INT32_MIN;
inline constexpr CoordType CoordTypeMax = INT32_MAX;

namespace details
{
Expand Down
32 changes: 9 additions & 23 deletions src/terminal/adapter/adaptDispatch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string)

// The width at which we wrap is determined by the line rendition attribute.
auto lineWidth = textBuffer.GetLineWidth(cursorPosition.y);
auto stringIterator = string;

auto stringPosition = string.cbegin();
while (stringPosition < string.cend())
while (!stringIterator.empty())
{
if (cursor.IsDelayedEOLWrap() && wrapAtEOL)
{
Expand All @@ -101,45 +101,31 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string)
}
}

const OutputCellIterator it(std::wstring_view{ stringPosition, string.cend() }, attributes);
if (_modes.test(Mode::InsertReplace))
{
// If insert-replace mode is enabled, we first measure how many cells
// the string will occupy, and scroll the target area right by that
// amount to make space for the incoming text.
const OutputCellIterator it(stringIterator, attributes);
auto measureIt = it;
while (measureIt && measureIt.GetCellDistance(it) < lineWidth)
{
measureIt++;
++measureIt;
}
const auto row = cursorPosition.y;
const auto cellCount = measureIt.GetCellDistance(it);
_ScrollRectHorizontally(textBuffer, { cursorPosition.x, row, lineWidth, row + 1 }, cellCount);
}
const auto itEnd = textBuffer.WriteLine(it, cursorPosition, wrapAtEOL, lineWidth - 1);

if (itEnd.GetInputDistance(it) == 0)
{
// If we haven't written anything out because there wasn't enough space,
// we move the cursor to the end of the line so that it's forced to wrap.
cursorPosition.x = lineWidth;
// But if wrapping is disabled, we also need to move to the next string
// position, otherwise we'll be stuck in this loop forever.
if (!wrapAtEOL)
{
stringPosition++;
}
}
else
const auto newPosX = textBuffer.Write(cursorPosition.y, cursorPosition.x, til::CoordTypeMax, wrapAtEOL, attributes, stringIterator);

if (const til::rect changedRect{ cursorPosition.x, cursorPosition.y, newPosX, cursorPosition.y + 1 })
{
const auto cellCount = itEnd.GetCellDistance(it);
const auto changedRect = til::rect{ cursorPosition, til::size{ cellCount, 1 } };
_api.NotifyAccessibilityChange(changedRect);

stringPosition += itEnd.GetInputDistance(it);
cursorPosition.x += cellCount;
}

cursorPosition.x = newPosX;

if (cursorPosition.x >= lineWidth)
{
// If we're past the end of the line, we need to clamp the cursor
Expand Down
27 changes: 4 additions & 23 deletions tools/ConsoleTypes.natvis
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,12 @@
<DisplayString>{{LT({Left}, {Top}) RB({Right}, {Bottom}) In:[{Right-Left+1} x {Bottom-Top+1}] Ex:[{Right-Left} x {Bottom-Top}]}}</DisplayString>
</Type>

<Type Name="CharRowCell">
<DisplayString Condition="_attr._glyphStored">Stored Glyph, go to UnicodeStorage.</DisplayString>
<DisplayString Condition="_attr._attribute == 0">{_wch,X} Single</DisplayString>
<DisplayString Condition="_attr._attribute == 1">{_wch,X} Lead</DisplayString>
<DisplayString Condition="_attr._attribute == 2">{_wch,X} Trail</DisplayString>
</Type>

<Type Name="ATTR_ROW">
<Expand>
<ExpandedItem>_data</ExpandedItem>
</Expand>
</Type>

<Type Name="CharRow">
<DisplayString>{{ wrap={_wrapForced} padded={_doubleBytePadded} }}</DisplayString>
<Expand>
<ExpandedItem>_data</ExpandedItem>
</Expand>
</Type>

<Type Name="ROW">
<DisplayString>{{ id={_id} width={_rowWidth} }}</DisplayString>
<DisplayString>{_chars.data(),[_charOffsets[_columnCount]]}</DisplayString>
<StringView>_chars.data(),[_charOffsets[_columnCount]]</StringView>
<Expand>
<Item Name="_charRow">_charRow</Item>
<Item Name="_attrRow">_attrRow</Item>
<Item Name="_chars">_chars.data(),[_charOffsets[_columnCount]]</Item>
<Item Name="_charOffsets">_charOffsets.data(),[_charOffsets.size()]</Item>
</Expand>
</Type>

Expand Down