Skip to content

Commit 381b115

Browse files
j4jamesmsftbot[bot]
authored andcommitted
Correct fill attributes when scrolling and erasing (#3100)
## Summary of the Pull Request Operations that erase areas of the screen are typically meant to do so using the current color attributes, but with the rendition attributes reset (what we refer to as meta attributes). This also includes scroll operations that have to clear the area of the screen that has scrolled into view. The only exception is the _Erase Scrollback_ operation, which needs to reset the buffer with the default attributes. This PR updates all of these cases to apply the correct attributes when scrolling and erasing. ## PR Checklist * [x] Closes #2553 * [x] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA * [x] Tests added/passed * [ ] Requires documentation to be updated * [ ] I've not really discussed this with core contributors. I'm ready to accept this work might be rejected in favor of a different grand plan. ## Detailed Description of the Pull Request / Additional comments My initial plan was to use a special case legacy attribute value to indicate the "standard erase attribute" which could safely be passed through the legacy APIs. But this wouldn't cover the cases that required default attributes to be used. And then with the changes in PR #2668 and #2987, it became clear that our requirements could be better achieved with a couple of new private APIs that wouldn't have to depend on legacy attribute hacks at all. To that end, I've added the `PrivateFillRegion` and `PrivateScrollRegion` APIs to the `ConGetSet` interface. These are just thin wrappers around the existing `SCREEN_INFORMATION::Write` method and the `ScrollRegion` function respectively, but with a simple boolean parameter to choose between filling with default attributes or the standard erase attributes (i.e the current colors but with meta attributes reset). With those new APIs in place, I could then update most scroll operations to use `PrivateScrollRegion`, and most erase operations to use `PrivateFillRegion`. The functions affected by scrolling included: * `DoSrvPrivateReverseLineFeed` (the RI command) * `DoSrvPrivateModifyLinesImpl` (the IL and DL commands) * `AdaptDispatch::_InsertDeleteHelper` (the ICH and DCH commands) * `AdaptDispatch::_ScrollMovement` (the SU and SD commands) The functions affected by erasing included: * `AdaptDispatch::_EraseSingleLineHelper` (the EL command, and most ED variants) * `AdaptDispatch::EraseCharacters` (the ECH command) While updating these erase methods, I noticed that both of them also required boundary fixes similar to those in PR #2505 (i.e. the horizontal extent of the erase operation should apply to the full width of the buffer, and not just the current viewport width), so I've addressed that at the same time. In addition to the changes above, there were also a few special cases, the first being the line feed handling, which required updating in a number of places to use the correct erase attributes: * `SCREEN_INFORMATION::InitializeCursorRowAttributes` - this is used to initialise the rows that pan into view when the viewport is moved down the buffer. * `TextBuffer::IncrementCircularBuffer` - this occurs when we scroll passed the very end of the buffer, and a recycled row now needs to be reinitialised. * `AdjustCursorPosition` - when within margin boundaries, this relies on a couple of direct calls to `ScrollRegion` which needed to be passed the correct fill attributes. The second special case was the full screen erase sequence (`ESC 2 J`), which is handled separately from the other ED sequences. This required updating the `SCREEN_INFORMATION::VtEraseAll` method to use the standard erase attributes, and also required changes to the horizontal extent of the filled area, since it should have been clearing the full buffer width (the same issue as the other erase operations mentioned above). Finally, there was the `AdaptDispatch::_EraseScrollback` method, which uses both scroll and fill operations, which could now be handled by the new `PrivateScrollRegion` and `PrivateFillRegion` APIs. But in this case we needed to fill with the default attributes rather than the standard erase attributes. And again this implementation needed some changes to make sure the full width of the active area was retained after the erase, similar to the horizontal boundary issues with the other erase operations. Once all these changes were made, there were a few areas of the code that could then be simplified quite a bit. The `FillConsoleOutputCharacterW`, `FillConsoleOutputAttribute`, and `ScrollConsoleScreenBufferW` were no longer needed in the `ConGetSet` interface, so all of that code could now be removed. The `_EraseSingleLineDistanceHelper` and `_EraseAreaHelper` methods in the `AdaptDispatch` class were also no longer required and could be removed. Then there were the hacks to handle legacy default colors in the `FillConsoleOutputAttributeImpl` and `ScrollConsoleScreenBufferWImpl` implementations. Since those hacks were only needed for VT operations, and the VT code no longer calls those methods, there was no longer a need to retain that behaviour (in fact there are probably some edge cases where that behaviour might have been considered a bug when reached via the public console APIs). ## Validation Steps Performed For most of the scrolling operations there were already existing tests in place, and those could easily be extended to check that the meta attributes were correctly reset when filling the revealed lines of the scrolling region. In the screen buffer tests, I made updates of that sort to the `ScrollOperations` method (handling SU, SD, IL, DL, and RI), the `InsertChars` and `DeleteChars` methods (ICH and DCH), and the `VtNewlinePastViewport` method (LF). I also added a new `VtNewlinePastEndOfBuffer` test to check the case where the line feed causes the viewport to pan past the end of the buffer. The erase operations, however, were being covered by adapter tests, and those aren't really suited for this kind of functionality (the same sort of issue came up in PR #2505). As a result I've had to reimplement those tests as screen buffer tests. Most of the erase operations are covered by the `EraseTests` method, except the for the scrollback erase which has a dedicated `EraseScrollbackTests` method. I've also had to replace the `HardReset` adapter test, but that was already mostly covered by the `HardResetBuffer` screen buffer test, which I've now extended slightly (it could do with some more checks, but I think that can wait for a future PR when we're fixing other RIS issues).
1 parent 5bbf7e2 commit 381b115

File tree

17 files changed

+637
-859
lines changed

17 files changed

+637
-859
lines changed

src/buffer/out/TextAttribute.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,12 @@ bool TextAttribute::BackgroundIsDefault() const noexcept
266266
{
267267
return _background.IsDefault();
268268
}
269+
270+
// Routine Description:
271+
// - Resets the meta and extended attributes, which is what the VT standard
272+
// requires for most erasing and filling operations.
273+
void TextAttribute::SetStandardErase() noexcept
274+
{
275+
SetExtendedAttributes(ExtendedAttributes::Normal);
276+
SetMetaAttributes(0);
277+
}

src/buffer/out/TextAttribute.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ class TextAttribute final
156156
bool ForegroundIsDefault() const noexcept;
157157
bool BackgroundIsDefault() const noexcept;
158158

159+
void SetStandardErase() noexcept;
160+
159161
constexpr bool IsRgb() const noexcept
160162
{
161163
return _foreground.IsRgb() || _background.IsRgb();

src/buffer/out/textBuffer.cpp

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -539,17 +539,24 @@ bool TextBuffer::NewlineCursor()
539539
//Routine Description:
540540
// - Increments the circular buffer by one. Circular buffer is represented by FirstRow variable.
541541
//Arguments:
542-
// - <none>
542+
// - inVtMode - set to true in VT mode, so standard erase attributes are used for the new row.
543543
//Return Value:
544544
// - true if we successfully incremented the buffer.
545-
bool TextBuffer::IncrementCircularBuffer()
545+
bool TextBuffer::IncrementCircularBuffer(const bool inVtMode)
546546
{
547547
// FirstRow is at any given point in time the array index in the circular buffer that corresponds
548548
// to the logical position 0 in the window (cursor coordinates and all other coordinates).
549549
_renderTarget.TriggerCircling();
550550

551551
// First, clean out the old "first row" as it will become the "last row" of the buffer after the circle is performed.
552-
const bool fSuccess = _storage.at(_firstRow).Reset(_currentAttributes);
552+
auto fillAttributes = _currentAttributes;
553+
if (inVtMode)
554+
{
555+
// The VT standard requires that the new row is initialized with
556+
// the current background color, but with no meta attributes set.
557+
fillAttributes.SetStandardErase();
558+
}
559+
const bool fSuccess = _storage.at(_firstRow).Reset(fillAttributes);
553560
if (fSuccess)
554561
{
555562
// Now proceed to increment.

src/buffer/out/textBuffer.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ class TextBuffer final
101101
bool NewlineCursor();
102102

103103
// Scroll needs access to this to quickly rotate around the buffer.
104-
bool IncrementCircularBuffer();
104+
bool IncrementCircularBuffer(const bool inVtMode = false);
105105

106106
COORD GetLastNonSpaceCharacter() const;
107107
COORD GetLastNonSpaceCharacter(const Microsoft::Console::Types::Viewport viewport) const;

src/host/_output.cpp

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -219,24 +219,6 @@ void WriteToScreen(SCREEN_INFORMATION& screenInfo, const Viewport& region)
219219
try
220220
{
221221
TextAttribute useThisAttr(attribute);
222-
223-
// Here we're being a little clever -
224-
// Because RGB color can't roundtrip the API, certain VT sequences will forget the RGB color
225-
// because their first call to GetScreenBufferInfo returned a legacy attr.
226-
// If they're calling this with the default attrs, they likely wanted to use the RGB default attrs.
227-
// This could create a scenario where someone emitted RGB with VT,
228-
// THEN used the API to FillConsoleOutput with the default attrs, and DIDN'T want the RGB color
229-
// they had set.
230-
if (screenBuffer.InVTMode())
231-
{
232-
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
233-
auto bufferLegacy = gci.GenerateLegacyAttributes(screenBuffer.GetAttributes());
234-
if (bufferLegacy == attribute)
235-
{
236-
useThisAttr = TextAttribute(screenBuffer.GetAttributes());
237-
}
238-
}
239-
240222
const OutputCellIterator it(useThisAttr, lengthToWrite);
241223
const auto done = screenBuffer.Write(it, startingCoordinate);
242224

src/host/_stream.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
7474
}
7575
}
7676

77-
const auto bufferAttributes = screenInfo.GetAttributes();
77+
// The VT standard requires the lines revealed when scrolling are filled
78+
// with the current background color, but with no meta attributes set.
79+
auto fillAttributes = screenInfo.GetAttributes();
80+
fillAttributes.SetStandardErase();
7881

7982
const auto relativeMargins = screenInfo.GetRelativeScrollMargins();
8083
auto viewport = screenInfo.GetViewport();
@@ -144,7 +147,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
144147

145148
try
146149
{
147-
ScrollRegion(screenInfo, scrollRect, std::nullopt, newPostMarginsOrigin, UNICODE_SPACE, bufferAttributes);
150+
ScrollRegion(screenInfo, scrollRect, std::nullopt, newPostMarginsOrigin, UNICODE_SPACE, fillAttributes);
148151
}
149152
CATCH_LOG();
150153

@@ -193,7 +196,7 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
193196

194197
try
195198
{
196-
ScrollRegion(screenInfo, scrollRect, scrollRect, dest, UNICODE_SPACE, bufferAttributes);
199+
ScrollRegion(screenInfo, scrollRect, scrollRect, dest, UNICODE_SPACE, fillAttributes);
197200
}
198201
CATCH_LOG();
199202

src/host/getset.cpp

Lines changed: 108 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -836,32 +836,6 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont
836836
auto& buffer = context.GetActiveBuffer();
837837

838838
TextAttribute useThisAttr(fillAttribute);
839-
840-
// Here we're being a little clever - similar to FillConsoleOutputAttributeImpl
841-
// Because RGB/default color can't roundtrip the API, certain VT
842-
// sequences will forget the RGB color because their first call to
843-
// GetScreenBufferInfo returned a legacy attr.
844-
// If they're calling this with the legacy attrs version of our current
845-
// attributes, they likely wanted to use the full version of
846-
// our current attributes, whether that be RGB or _default_ colored.
847-
// This could create a scenario where someone emitted RGB with VT,
848-
// THEN used the API to ScrollConsoleOutput with the legacy attrs,
849-
// and DIDN'T want the RGB color. As in FillConsoleOutputAttribute,
850-
// this scenario is highly unlikely, and we can reasonably do this
851-
// on their behalf.
852-
// see MSFT:19853701
853-
854-
if (buffer.InVTMode())
855-
{
856-
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
857-
const auto currentAttributes = buffer.GetAttributes();
858-
const auto bufferLegacy = gci.GenerateLegacyAttributes(currentAttributes);
859-
if (bufferLegacy == fillAttribute)
860-
{
861-
useThisAttr = currentAttributes;
862-
}
863-
}
864-
865839
ScrollRegion(buffer, source, clip, target, fillCharacter, useThisAttr);
866840

867841
return S_OK;
@@ -1422,25 +1396,12 @@ void DoSrvPrivateAllowCursorBlinking(SCREEN_INFORMATION& screenInfo, const bool
14221396
coordDestination.X = 0;
14231397
coordDestination.Y = viewport.Top + 1;
14241398

1425-
// Here we previously called to ScrollConsoleScreenBufferWImpl to
1426-
// perform the scrolling operation. However, that function only
1427-
// accepts a WORD for the fill attributes. That means we'd lose
1428-
// 256/RGB fidelity for fill attributes. So instead, we'll just call
1429-
// ScrollRegion ourselves, with the same params that
1430-
// ScrollConsoleScreenBufferWImpl would have.
1431-
// See microsoft/terminal#832, #2702 for more context.
1432-
try
1433-
{
1434-
LockConsole();
1435-
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
1436-
ScrollRegion(screenInfo,
1437-
srScroll,
1438-
srScroll,
1439-
coordDestination,
1440-
UNICODE_SPACE,
1441-
screenInfo.GetAttributes());
1442-
}
1443-
CATCH_LOG();
1399+
// Note the revealed lines are filled with the standard erase attributes.
1400+
Status = NTSTATUS_FROM_HRESULT(DoSrvPrivateScrollRegion(screenInfo,
1401+
srScroll,
1402+
srScroll,
1403+
coordDestination,
1404+
true));
14441405
}
14451406
}
14461407
return Status;
@@ -2145,25 +2106,12 @@ void DoSrvPrivateModifyLinesImpl(const unsigned int count, const bool insert)
21452106
coordDestination.Y = (cursorPosition.Y) - gsl::narrow<short>(count);
21462107
}
21472108

2148-
// Here we previously called to ScrollConsoleScreenBufferWImpl to
2149-
// perform the scrolling operation. However, that function only accepts
2150-
// a WORD for the fill attributes. That means we'd lose 256/RGB fidelity
2151-
// for fill attributes. So instead, we'll just call ScrollRegion
2152-
// ourselves, with the same params that ScrollConsoleScreenBufferWImpl
2153-
// would have.
2154-
// See microsoft/terminal#832 for more context.
2155-
try
2156-
{
2157-
LockConsole();
2158-
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
2159-
ScrollRegion(screenInfo,
2160-
srScroll,
2161-
srScroll,
2162-
coordDestination,
2163-
UNICODE_SPACE,
2164-
screenInfo.GetAttributes());
2165-
}
2166-
CATCH_LOG();
2109+
// Note the revealed lines are filled with the standard erase attributes.
2110+
LOG_IF_FAILED(DoSrvPrivateScrollRegion(screenInfo,
2111+
srScroll,
2112+
srScroll,
2113+
coordDestination,
2114+
true));
21672115

21682116
// The IL and DL controls are also expected to move the cursor to the left margin.
21692117
// For now this is just column 0, since we don't yet support DECSLRM.
@@ -2287,3 +2235,99 @@ void DoSrvPrivateMoveToBottom(SCREEN_INFORMATION& screenInfo)
22872235
}
22882236
CATCH_RETURN();
22892237
}
2238+
2239+
// Routine Description:
2240+
// - A private API call for filling a region of the screen buffer.
2241+
// Arguments:
2242+
// - screenInfo - Reference to screen buffer info.
2243+
// - startPosition - The position to begin filling at.
2244+
// - fillLength - The number of characters to fill.
2245+
// - fillChar - Character to fill the target region with.
2246+
// - standardFillAttrs - If true, fill with the standard erase attributes.
2247+
// If false, fill with the default attributes.
2248+
// Return value:
2249+
// - S_OK or failure code from thrown exception
2250+
[[nodiscard]] HRESULT DoSrvPrivateFillRegion(SCREEN_INFORMATION& screenInfo,
2251+
const COORD startPosition,
2252+
const size_t fillLength,
2253+
const wchar_t fillChar,
2254+
const bool standardFillAttrs) noexcept
2255+
{
2256+
try
2257+
{
2258+
if (fillLength == 0)
2259+
{
2260+
return S_OK;
2261+
}
2262+
2263+
LockConsole();
2264+
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
2265+
2266+
// For most VT erasing operations, the standard requires that the
2267+
// erased area be filled with the current background color, but with
2268+
// no additional meta attributes set. For all other cases, we just
2269+
// fill with the default attributes.
2270+
auto fillAttrs = TextAttribute{};
2271+
if (standardFillAttrs)
2272+
{
2273+
fillAttrs = screenInfo.GetAttributes();
2274+
fillAttrs.SetStandardErase();
2275+
}
2276+
2277+
const auto fillData = OutputCellIterator{ fillChar, fillAttrs, fillLength };
2278+
screenInfo.Write(fillData, startPosition, false);
2279+
2280+
// Notify accessibility
2281+
auto endPosition = startPosition;
2282+
const auto bufferSize = screenInfo.GetBufferSize();
2283+
bufferSize.MoveInBounds(fillLength - 1, endPosition);
2284+
screenInfo.NotifyAccessibilityEventing(startPosition.X, startPosition.Y, endPosition.X, endPosition.Y);
2285+
return S_OK;
2286+
}
2287+
CATCH_RETURN();
2288+
}
2289+
2290+
// Routine Description:
2291+
// - A private API call for moving a block of data in the screen buffer,
2292+
// optionally limiting the effects of the move to a clipping rectangle.
2293+
// Arguments:
2294+
// - screenInfo - Reference to screen buffer info.
2295+
// - scrollRect - Region to copy/move (source and size).
2296+
// - clipRect - Optional clip region to contain buffer change effects.
2297+
// - destinationOrigin - Upper left corner of target region.
2298+
// - standardFillAttrs - If true, fill with the standard erase attributes.
2299+
// If false, fill with the default attributes.
2300+
// Return value:
2301+
// - S_OK or failure code from thrown exception
2302+
[[nodiscard]] HRESULT DoSrvPrivateScrollRegion(SCREEN_INFORMATION& screenInfo,
2303+
const SMALL_RECT scrollRect,
2304+
const std::optional<SMALL_RECT> clipRect,
2305+
const COORD destinationOrigin,
2306+
const bool standardFillAttrs) noexcept
2307+
{
2308+
try
2309+
{
2310+
LockConsole();
2311+
auto Unlock = wil::scope_exit([&] { UnlockConsole(); });
2312+
2313+
// For most VT scrolling operations, the standard requires that the
2314+
// erased area be filled with the current background color, but with
2315+
// no additional meta attributes set. For all other cases, we just
2316+
// fill with the default attributes.
2317+
auto fillAttrs = TextAttribute{};
2318+
if (standardFillAttrs)
2319+
{
2320+
fillAttrs = screenInfo.GetAttributes();
2321+
fillAttrs.SetStandardErase();
2322+
}
2323+
2324+
ScrollRegion(screenInfo,
2325+
scrollRect,
2326+
clipRect,
2327+
destinationOrigin,
2328+
UNICODE_SPACE,
2329+
fillAttrs);
2330+
return S_OK;
2331+
}
2332+
CATCH_RETURN();
2333+
}

src/host/getset.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,15 @@ void DoSrvPrivateMoveToBottom(SCREEN_INFORMATION& screenInfo);
9292
[[nodiscard]] HRESULT DoSrvPrivateSetDefaultForegroundColor(const COLORREF value) noexcept;
9393

9494
[[nodiscard]] HRESULT DoSrvPrivateSetDefaultBackgroundColor(const COLORREF value) noexcept;
95+
96+
[[nodiscard]] HRESULT DoSrvPrivateFillRegion(SCREEN_INFORMATION& screenInfo,
97+
const COORD startPosition,
98+
const size_t fillLength,
99+
const wchar_t fillChar,
100+
const bool standardFillAttrs) noexcept;
101+
102+
[[nodiscard]] HRESULT DoSrvPrivateScrollRegion(SCREEN_INFORMATION& screenInfo,
103+
const SMALL_RECT scrollRect,
104+
const std::optional<SMALL_RECT> clipRect,
105+
const COORD destinationOrigin,
106+
const bool standardFillAttrs) noexcept;

src/host/output.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,8 @@ static void _ScrollScreen(SCREEN_INFORMATION& screenInfo, const Viewport& source
296296
bool StreamScrollRegion(SCREEN_INFORMATION& screenInfo)
297297
{
298298
// Rotate the circular buffer around and wipe out the previous final line.
299-
bool fSuccess = screenInfo.GetTextBuffer().IncrementCircularBuffer();
299+
const bool inVtMode = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);
300+
bool fSuccess = screenInfo.GetTextBuffer().IncrementCircularBuffer(inVtMode);
300301
if (fSuccess)
301302
{
302303
// Trigger a graphical update if we're active.

0 commit comments

Comments
 (0)