Skip to content
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

Correct fill attributes when scrolling and erasing #3100

Merged
18 commits merged into from
Dec 10, 2019

Conversation

j4james
Copy link
Collaborator

@j4james j4james commented Oct 6, 2019

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

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).

…ributes correctly, and make sure the full buffer width is covered.
…t the fill attributes correctly, and make sure the full buffer width is affected.
…ribute and ScrollConsoleScreenBufferW methods from the ConGetSet interface.
…FillConsoleOutputAttributeImpl and ScrollConsoleScreenBufferWImpl implementations.
// The VT standard requires that the new row is initialized with
// the current background color, but with no meta attributes set.
auto fillAttributes = _currentAttributes;
fillAttributes.SetMetaAttributes(0);
Copy link
Contributor

Choose a reason for hiding this comment

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

Concerned about this one, actually -- this is used for traditional Windows console apps as well, and I don't know what the expected behavior is for new rows outside of global VT processing mode.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah yes. I should have mentioned I did actually have a look into that. I wrote a little test app that set the attributes with SetConsoleTextAttribute (using COMMON_LVB_UNDERSCORE and COMMON_LVB_REVERSE_VIDEO) and then output some text that would cause the screen to scroll.

In both the legacy console and the v2 console, a line feed doesn't actually fill the new row with the current attributes most of the time - only when it reaches the end of the buffer. That's just plain weird, but it seems that's the way it has always been, and that hasn't changed.

In the legacy console the underscore and reverse attributes don't seem to have any effect anywhere, so naturally that means the new rows filled when scrolling past the end of the buffer will also not have an underscore or have their attributes reversed. So in some ways this update more closely matches that legacy behaviour. It is a change from the current v2 behaviour though.

The bottom line is I'd be very surprised if anyone is relying on this behaviour, considering how strange and inconsistent it is. However, I know backward compatibility is important to you, so if you think it's a concern, I can probably add a check that only resets the attributes when in vt mode or something of the sort.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've now made it so this only applies in the specific case of the line feed scrolling when in vt mode. There are probably more situations where it should be applied, but for now this is the most conservative fix for this particular issue.

@j4james
Copy link
Collaborator Author

j4james commented Oct 7, 2019

There are two other issues I should mention that I'm concerned about in this PR:

  1. I noticed that LockConsole was being used around most calls ScrollRegion (e.g. see here), so I've followed the same pattern in the new PrivateScrollRegion method, as well the in the new PrivateFillRegion method around the SCREEN_INFORMATION::Write call. However, I've also seen places where I would have expected it to be used, but it wasn't (e.g. see here). So I'm unsure now whether it is really necessary to lock these operations, or whether it's only required for specific cases?

  2. I also noticed there's a NotifyAccessibilityEventing call in the FillConsoleOutputAttributeImpl method to indicate changes to the screen buffer (see here), but then it's not used in something like the VtEraseAll method which also updates the buffer (see here). So again I'm not sure when exactly that is required. For now I haven't called it either the new PrivateScrollRegion method or the PrivateFillRegion method, but I suspect that may be wrong.

@miniksa
Copy link
Member

miniksa commented Oct 11, 2019

There are two other issues I should mention that I'm concerned about in this PR:

  1. I noticed that LockConsole was being used around most calls ScrollRegion (e.g. see here), so I've followed the same pattern in the new PrivateScrollRegion method, as well the in the new PrivateFillRegion method around the SCREEN_INFORMATION::Write call. However, I've also seen places where I would have expected it to be used, but it wasn't (e.g. see here). So I'm unsure now whether it is really necessary to lock these operations, or whether it's only required for specific cases?

It is necessary to lock the scroll operation. We are supposed to lock any operation that moves data around in either the input or output buffers. The fact that it's a global lock pains us, but it's just the way it's been.

The latter case with AdjustCursorPosition is probably because it was reasoned out at some previous point in time that that method was never called without already being under LockConsole. That might not be strictly true anymore as the code has organically grown, but you've stumbled upon one of the "inherited things that haven't been fully sussed out yet even 5 years later" things.

Adding a LockConsole with unlock inside AdjustCursorPosition to cover our butts wouldn't be a bad thing. The lock should be re-entrant. If something starts hanging after you add that, then we've identified a long standing problem.

  1. I also noticed there's a NotifyAccessibilityEventing call in the FillConsoleOutputAttributeImpl method to indicate changes to the screen buffer (see here), but then it's not used in something like the VtEraseAll method which also updates the buffer (see here). So again I'm not sure when exactly that is required. For now I haven't called it either the new PrivateScrollRegion method or the PrivateFillRegion method, but I suspect that may be wrong.

That appears to be another mistake that has slipped through the cracks. We're supposed to be notifying eventing when the contents of the buffer changes in a way that is visible on the screen.

Because that's so prone to error, I've asked @carlos-zamora to start down the path of turning accessibility into a renderer instead of having to separately event it all over the place. The events that accessibility needs to know are pretty much the same things that one needs to know to draw on the screen anyway (and we already have a robust system for knowing and doing that).

However, he's doing it for UIA. Where the NotifyAccessibilityEventing method you identified is for MSAA.

In the short term, it would make sense to propagate the NotifyAccessibilityEventing method to the missing one and the new ones.

In the long term, we should also book moving MSAA to a renderer like @carlos-zamora is doing for UIA. I appear to have mentioned that briefly in #410 as a performance concern as well, but I don't see an issue on the tracker to do it. Maybe @DHowett-MSFT has better GitHub-search-fu than I do.

@DHowett-MSFT DHowett-MSFT added Product-Conhost For issues in the Console codebase Area-VT Virtual Terminal sequence support labels Oct 15, 2019
@j4james
Copy link
Collaborator Author

j4james commented Oct 16, 2019

The latter case with AdjustCursorPosition is probably because it was reasoned out at some previous point in time that that method was never called without already being under LockConsole.

I think you're right about that. I added a check to see if that function was ever called without the lock being held and I couldn't get it to fail, so I think it's probably OK. When it's triggered as a result of a write operation it would already have been locked by one of the WriteConsole*Impl methods (e.g. see here), and for input operations it would likely have come via the ConsoleWindowProc method (see here).

But that does suggest that the lock probably shouldn't be needed in any of the private VT methods, since they should already have been locked by the WriteConsole call that triggered them. I'm nervous about removing existing locks, though, so I'm leaving those as they are.

We're supposed to be notifying eventing when the contents of the buffer changes in a way that is visible on the screen.

I've added a NotifyAccessibilityEventing to the new PrivateFillRegion method, but after a further investigation of the VtEraseAll, I think that code is actually fine as it is. The way the screen is cleared is essentially by scrolling down to a clean area of the buffer, and the viewport move already generates a accessibility scroll event for that, so that should be fine.

Copy link
Member

@zadjii-msft zadjii-msft left a comment

Choose a reason for hiding this comment

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

I apologize for not getting back to this PR earlier. I'm happy with this change overall, but I think it might need a little bit of toughing up to deal with the ExtendedAttributes that merged after this PR was introduced.

Once that's fixed, I'll be happy with accepting this PR.

src/host/_output.cpp Show resolved Hide resolved
src/host/getset.cpp Outdated Show resolved Hide resolved
src/host/screenInfo.cpp Outdated Show resolved Hide resolved
src/terminal/adapter/adaptDispatch.cpp Show resolved Hide resolved
src/host/_stream.cpp Outdated Show resolved Hide resolved
@ghost ghost added Needs-Author-Feedback The original author of the issue/PR needs to come back and respond to something and removed Needs-Author-Feedback The original author of the issue/PR needs to come back and respond to something labels Nov 25, 2019
Copy link
Member

@zadjii-msft zadjii-msft left a comment

Choose a reason for hiding this comment

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

Thanks for setting all those meta attributes ☺️

src/host/getset.cpp Outdated Show resolved Hide resolved
src/terminal/adapter/adaptDispatch.cpp Show resolved Hide resolved
@zadjii-msft zadjii-msft added the Needs-Second It's a PR that needs another sign-off label Dec 4, 2019
@ghost ghost requested review from miniksa and carlos-zamora December 4, 2019 17:05
@zadjii-msft zadjii-msft added this to the Terminal-1912 milestone Dec 4, 2019
Copy link
Member

@carlos-zamora carlos-zamora left a comment

Choose a reason for hiding this comment

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

Thanks! Sorry it took a while!

@carlos-zamora
Copy link
Member

@msftbot make sure @miniksa signs off

@ghost ghost added the AutoMerge Marked for automatic merge by the bot when requirements are met label Dec 5, 2019
@ghost
Copy link

ghost commented Dec 5, 2019

Hello @carlos-zamora!

Because you've given me some instructions on how to help merge this pull request, I'll be modifying my merge approach. Here's how I understand your requirements for merging this pull request:

  • I'll only merge this pull request if it's approved by @miniksa

If this doesn't seem right to you, you can tell me to cancel these instructions and use the auto-merge policy that has been configured for this repository. Try telling me "forget everything I just told you".

@miniksa
Copy link
Member

miniksa commented Dec 10, 2019

OK, I intend to read this one after lunch today. Sorry for the delay. I've been swamped with internal deliverables.

Copy link
Member

@miniksa miniksa left a comment

Choose a reason for hiding this comment

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

This looks pretty good to me. I just had two small comments before we final approve and merge.

@@ -542,14 +542,22 @@ bool TextBuffer::NewlineCursor()
// - <none>
Copy link
Member

Choose a reason for hiding this comment

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

Arguments comment not updated.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sorry I totally overlooked that. Added in commit 171a4f9

// The buffer needs to be initialized with the standard erase attributes,
// i.e. the current background color, but with no meta attributes set.
auto initAttributes = GetAttributes();
initAttributes.SetExtendedAttributes(ExtendedAttributes::Normal);
Copy link
Member

Choose a reason for hiding this comment

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

I feel like this pattern (SetExtendedAttributes to Normal, set Meta to 0) is common enough that perhaps there should just be a .SetStandardErase() method on the Attributes that does both of these instead of having this pattern everywhere.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Very happy to do this. Makes things a lot neater. Added in commit 786cc36

@ghost ghost removed the AutoMerge Marked for automatic merge by the bot when requirements are met label Dec 10, 2019
Copy link
Member

@miniksa miniksa left a comment

Choose a reason for hiding this comment

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

Excellent, thank you!

// erased area be filled with the current background color, but with
// no additional meta attributes set. For all other cases, we just
// fill with the default attributes.
auto fillAttrs = TextAttribute{};
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: TextAttribute fillAttrs{};?

Copy link
Contributor

Choose a reason for hiding this comment

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

ARGH i apparently wrote this comment eighteen months ago and never published it
literally don't care now
😄

// We don't use the _EraseAreaHelper here because _EraseSingleLineDistanceHelper does it all in one operation
fSuccess = _EraseSingleLineDistanceHelper(coordBelowStartPosition, dwTotalAreaBelow, csbiex.wAttributes);
// Again we need to use the default attributes, hence standardFillAttrs is false.
fSuccess = _conApi->PrivateFillRegion(coordBelowStartPosition, dwTotalAreaBelow, L' ', false);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit

Suggested change
fSuccess = _conApi->PrivateFillRegion(coordBelowStartPosition, dwTotalAreaBelow, L' ', false);
fSuccess = _conApi->PrivateFillRegion(coordBelowStartPosition, dwTotalAreaBelow, UNICODE_SPACE, false);

if we can see UNICODE_SPACE here?

@DHowett-MSFT
Copy link
Contributor

@j4james excellent as usual. There's a couple missing UNICODE_SPACEs, but I have my finger on the merge button anyway ;P

@DHowett-MSFT DHowett-MSFT added the AutoMerge Marked for automatic merge by the bot when requirements are met label Dec 10, 2019
@ghost
Copy link

ghost commented Dec 10, 2019

Hello @DHowett-MSFT!

Because this pull request has the AutoMerge label, I will be glad to assist with helping to merge this pull request once all check-in policies pass.

p.s. you can customize the way I help with merging this pull request, such as holding this pull request until a specific person approves. Simply @mention me (@msftbot) and give me an instruction to get started! Learn more here.

@ghost ghost merged commit 381b115 into microsoft:master Dec 10, 2019
@j4james
Copy link
Collaborator Author

j4james commented Dec 10, 2019

There's a couple missing UNICODE_SPACEs, but I have my finger on the merge button anyway ;P

Just has a check, and it seems UNICODE_SPACE isn't available there anyway. I did actually think I had a reason for using those literals, although it may just have been that I copied them from the previous code.

@j4james j4james deleted the fix-fill-attributes branch December 15, 2019 01:53
@ghost
Copy link

ghost commented Jan 14, 2020

🎉Windows Terminal Preview v0.8.10091.0 has been released which incorporates this pull request.:tada:

Handy links:

This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-VT Virtual Terminal sequence support AutoMerge Marked for automatic merge by the bot when requirements are met Needs-Second It's a PR that needs another sign-off Product-Conhost For issues in the Console codebase
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Commands that erase the screen should clear the rendition attributes
5 participants