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 the boundaries of the scrolling commands #2505

Merged
merged 7 commits into from
Sep 11, 2019

Conversation

j4james
Copy link
Collaborator

@j4james j4james commented Aug 21, 2019

Summary of the Pull Request

There are a number of VT escape sequences that rely on the ScrollRegion function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH), and all of them have got the clipping rect or scroll boundaries wrong in some way, resulting in content being scrolled off the screen that should have been clipped, revealed areas not being correctly filled, or parts of the screen not being moved that should have been. This PR attempts to fix all of those issues.

PR Checklist

Detailed Description of the Pull Request / Additional comments

The ScrollRegion function is what ultimately handles the scrolling, but it's typically called via the ApiRoutines::ScrollConsoleScreenBufferWImpl method, and it's the callers of that method that have needed correcting.

One "mistake" that many of these operations made, was in setting a clipping rect that was different from the scrolling rect. This should never have been necessary, since the area being scrolled is also the boundary into which the content needs to be clipped, so the easiest thing to do is just use the same rect for both parameters.

Another common mistake was in clipping the horizontal boundaries to the width of the viewport. But it's really the buffer width that represents the active width of the screen - the viewport width and offset are merely a window on that active area. As such, the viewport should only be used to clip vertically - the horizontal extent should typically be the full buffer width.

On that note, there is really no need to actually calculate the buffer width when we want to set any of the scrolling parameters to that width. The ScrollRegion function already takes care of clipping everything within the buffer boundary, so we can simply set the Left of the rect to 0 and the Right to SHORT_MAX.

More details on individual commands:

  • RI (the DoSrvPrivateReverseLineFeed function)
    This now uses a single rect for both the scroll region and clipping boundary, and the width is set to SHORT_MAX to cover the full buffer width. Also the bottom of the scrolling region is now the bottom of the viewport (rather than bottom-1), otherwise it would be off by one.

  • DL and IL (the DoSrvPrivateModifyLinesImpl function)
    Again this uses a single rect for both the scroll region and clipping boundary, and the width is set to SHORT_MAX to cover the full width. The most significant change, though, is that the bottom boundary is now the viewport bottom rather than the buffer bottom. Using the buffer bottom prevented it clipping the content that scrolled off screen when inserting, and failed to fill the revealed area when deleting.

  • SU and SD (the AdaptDispatch::_ScrollMovement method)
    This was already using a single rect for both the scroll region and clipping boundary, but it was previously constrained to the width of the viewport rather than the buffer width, so some areas of the screen weren't correctly scrolled. Also, the bottom boundary was off by 1, because it was using an exclusive rect while the ScrollRegion function expects inclusive rects.

  • ICH and DCH (the AdaptDispatch::_InsertDeleteHelper method)
    This method has been considerably simplified, because it was reimplementing a lot of functionality that was already provided by the ScrollRegion function. And like many of the other cases, it has been updated to use a single rect for both the scroll region and clipping boundary, and clip to the full buffer width rather than the viewport width.

I should add that if we were following the specs exactly, then the SU and SD commands should technically be panning the viewport over the buffer instead of moving the buffer contents within the viewport boundary. So SU would be the equivalent of a newline at the bottom of the viewport (assuming no margins). And SD would assumedly do the opposite, scrolling the back buffer back into view (an RI at the top of the viewport should do the same).

This doesn't seem to be something that is consistently implemented, though. Some terminals do implement SU as a viewport pan, but I haven't seen anyone implement SD or RI as a pan. If we do want to do something about this, I think it's best addressed as a separate issue.

Validation Steps Performed

There were already existing tests for the SU, SD, ICH, and DCH commands, but they were implemented as adapter tests, which weren't effectively testing anything - the ScrollConsoleScreenBufferW method used in those tests was just a mock (an incomplete reimplementation of the ScrollRegion function), so confirming that the mock produced the correct result told you nothing about the validity of the real code.

To address that, I've now reimplemented those adapter tests as screen buffer tests. For the most part I've tried to duplicate the functionality of the original tests, but there are significant differences to account for the fact that scrolling region now covers the full width of the buffer rather than just the viewport width.

I've also extended those tests with additional coverage for the RI, DL, and IL commands, which are really just a variation of the SU and SD functionality.

@j4james
Copy link
Collaborator Author

j4james commented Aug 22, 2019

@DHowett-MSFT When you've got a moment, could you please take a look at this failing build log and let me know if it's something obvious I've done wrong, or just a transient build failure?

It seems that the build timed out while running the unit tests in the x86 build. And if I've understood the log correctly, the last test to complete was TestResize, and I'm assuming the one that follows is ScreenWidthAndHeightAreClampedToBounds (based on the successful x64 build log).

I don't think either of those have anything to do with the changes I've made, but I thought I might have done something that effected the global state in a way that would break another test. Is that possible?

Neither of those tests fails when run locally - tested with both x86 and x64 builds.

@DHowett-MSFT
Copy link
Contributor

It's more likely that you've hit one of our flaky tests. Let's see.

/azp run

@DHowett-MSFT
Copy link
Contributor

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

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.

This looks fundamentally better. Great job taking an organically grow solution and finding the actually right implementation.

Copy link
Contributor

@DHowett-MSFT DHowett-MSFT left a comment

Choose a reason for hiding this comment

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

This seems reasonable to me, but I trust @miniksa more than myself here.

src/host/ut_host/ScreenBufferTests.cpp Outdated Show resolved Hide resolved
src/host/ut_host/ScreenBufferTests.cpp Outdated Show resolved Hide resolved
src/host/getset.cpp Show resolved Hide resolved
src/host/getset.cpp Show resolved Hide resolved
@miniksa
Copy link
Member

miniksa commented Sep 6, 2019

I'm good with this. Thank you for identifying the testing gap and resolving it at the same time. I appreciate the detailed explanation and reasoning in the issues.

All I left was a comment or two to ask you to embed key pieces of the explanation inside the code for future maintainers.

@j4james
Copy link
Collaborator Author

j4james commented Sep 11, 2019

@DHowett-MSFT I rebased this yesterday so there are no merge conflicts now. Is there anything else I need to do? I'm hoping the force-push isn't a problem, because I don't really know what I'm doing with git half the time.

@DHowett-MSFT
Copy link
Contributor

Perfect! Thanks so much.

Copy link
Contributor

@DHowett-MSFT DHowett-MSFT left a comment

Choose a reason for hiding this comment

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

Wow, what serendipity! @carlos-zamora is currently looking at how ScrollConsoleScreenBufferWImpl interacts with the alternate buffer (part of #1189). I'm actually wondering now if this changeset damages alt buffer ICH/DCH in the alternate buffer since it makes InsertDeleteHelper use (...)Scroll(...)Impl.

I'm going to mark this one red so nobody merges it before we get a chance to investigate. Really sorry for not catching this sooner!

@ghost ghost added the Needs-Author-Feedback The original author of the issue/PR needs to come back and respond to something label Sep 11, 2019
Copy link
Contributor

@DHowett-MSFT DHowett-MSFT left a comment

Choose a reason for hiding this comment

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

Nevermind. It goes through ScrollConsoleScreenBufferW, which passes ScrollConsoleScreenBufferWImpl the correct output buffer.

@DHowett-MSFT DHowett-MSFT merged commit 12d2e17 into microsoft:master Sep 11, 2019
@j4james
Copy link
Collaborator Author

j4james commented Sep 11, 2019

I was bit worried for a moment there. Glad it's not a problem. Thanks for the merge.

@ghost ghost removed the Needs-Author-Feedback The original author of the issue/PR needs to come back and respond to something label Sep 11, 2019
@j4james j4james deleted the fix-scroll-cliprect branch September 22, 2019 09:31
DHowett-MSFT pushed a commit that referenced this pull request Sep 23, 2019
There are a number of VT escape sequences that rely on the `ScrollRegion`
function to scroll the viewport (RI, DL, IL, SU, SD, ICH, and DCH) , and all of
them have got the clipping rect or scroll boundaries wrong in some way,
resulting in content being scrolled off the screen that should have been
clipped, revealed areas not being correctly filled, or parts of the screen not
being moved that should have been. This PR attempts to fix all of those issues.

The `ScrollRegion` function is what ultimately handles the scrolling, but it's
typically called via the `ApiRoutines::ScrollConsoleScreenBufferWImpl` method,
and it's the callers of that method that have needed correcting.

One "mistake" that many of these operations made, was in setting a clipping
rect that was different from the scrolling rect. This should never have been
necessary, since the area being scrolled is also the boundary into which the
content needs to be clipped, so the easiest thing to do is just use the same
rect for both parameters.

Another common mistake was in clipping the horizontal boundaries to the width
of the viewport. But it's really the buffer width that represents the active
width of the screen - the viewport width and offset are merely a window on that
active area. As such, the viewport should only be used to clip vertically - the
horizontal extent should typically be the full buffer width.

On that note, there is really no need to actually calculate the buffer width
when we want to set any of the scrolling parameters to that width. The
`ScrollRegion` function already takes care of clipping everything within the
buffer boundary, so we can simply set the `Left` of the rect to `0` and the
`Right` to `SHORT_MAX`.

More details on individual commands:

* RI (the `DoSrvPrivateReverseLineFeed` function)
  This now uses a single rect for both the scroll region and clipping boundary,
  and the width is set to `SHORT_MAX` to cover the full buffer width. Also the
  bottom of the scrolling region is now the bottom of the viewport (rather than
  bottom-1), otherwise it would be off by one.

* DL and IL (the `DoSrvPrivateModifyLinesImpl` function)
  Again this uses a single rect for both the scroll region and clipping
  boundary, and the width is set to `SHORT_MAX` to cover the full width. The
  most significant change, though, is that the bottom boundary is now the
  viewport bottom rather than the buffer bottom. Using the buffer bottom
  prevented it clipping the content that scrolled off screen when inserting,
  and failed to fill the revealed area when deleting.

* SU and SD (the `AdaptDispatch::_ScrollMovement` method)
  This was already using a single rect for both the scroll region and clipping
  boundary, but it was previously constrained to the width of the viewport
  rather than the buffer width, so some areas of the screen weren't correctly
  scrolled. Also, the bottom boundary was off by 1, because it was using an
  exclusive rect while the `ScrollRegion` function expects inclusive rects.

* ICH and DCH (the `AdaptDispatch::_InsertDeleteHelper` method)
  This method has been considerably simplified, because it was reimplementing a
  lot of functionality that was already provided by the `ScrollRegion`
  function. And like many of the other cases, it has been updated to use a
  single rect for both the scroll region and clipping boundary, and clip to the
  full buffer width rather than the viewport width.

I should add that if we were following the specs exactly, then the SU and SD
commands should technically be panning the viewport over the buffer instead of
moving the buffer contents within the viewport boundary. So SU would be the
equivalent of a newline at the bottom of the viewport (assuming no margins).
And SD would assumedly do the opposite, scrolling the back buffer back into
view (an RI at the top of the viewport should do the same).

This doesn't seem to be something that is consistently implemented, though.
Some terminals do implement SU as a viewport pan, but I haven't seen anyone
implement SD or RI as a pan. If we do want to do something about this, I think
it's best addressed as a separate issue.

## Validation Steps Performed

There were already existing tests for the SU, SD, ICH, and DCH commands, but
they were implemented as adapter tests, which weren't effectively testing
anything - the `ScrollConsoleScreenBufferW` method used in those tests was just
a mock (an incomplete reimplementation of the `ScrollRegion` function), so
confirming that the mock produced the correct result told you nothing about the
validity of the real code.

To address that, I've now reimplemented those adapter tests as screen buffer
tests. For the most part I've tried to duplicate the functionality of the
original tests, but there are significant differences to account for the fact
that scrolling region now covers the full width of the buffer rather than just
the viewport width.

I've also extended those tests with additional coverage for the RI, DL, and IL
commands, which are really just a variation of the SU and SD functionality.

Closes #2174

(cherry picked from commit 12d2e17)
@ghost
Copy link

ghost commented Sep 24, 2019

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

Handy links:

@DHowett-MSFT
Copy link
Contributor

This went out for conhost in insider build 19002! Thanks 😄

ghost pushed a commit that referenced this pull request Dec 10, 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
* [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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

The ScrollRegion clip rect is almost always wrong
4 participants