VT renderer works incorrectly when cursor is moved between writes #21713

Closed
opened 2026-01-31 07:52:50 +00:00 by claunia · 10 comments
Owner

Originally created by @alabuzhev on GitHub (May 15, 2024).

Windows Terminal version

Latest source

Windows build number

10.0.19045.4355

Other Software

No

Steps to reproduce

The original issue is described here: https://github.com/FarGroup/FarManager/issues/841
According to the comments, it regressed somewhere between 1.18.3181.0 - 1.19.11213.0

MRE: VtRendererBug.zip
STR: compile, run, follow instructions

Expected Behavior

The vertical bar moves right without affecting the underlying text.

It works as expected in OpenConsole.

Actual Behavior

The vertical bar and the underlying text are broken.
The third line is written into the second.

It doesn't work as expected in WT and other terminals that use OpenConsole, e.g. WezTerm.


I've managed to trace it down to f62d2d5d2c/src/renderer/vt/XtermEngine.cpp (L281-L282)

It looks like specifying the position explicitly fixes the issue:

-                std::string seq = "\r\n";
-                hr = _Write(seq);
+                hr = _CursorPosition(coord);

Which could mean that the cursor position in _lastText is tracked incorrectly / unreliably.

Originally created by @alabuzhev on GitHub (May 15, 2024). ### Windows Terminal version Latest source ### Windows build number 10.0.19045.4355 ### Other Software No ### Steps to reproduce The original issue is described here: https://github.com/FarGroup/FarManager/issues/841 According to the comments, it regressed somewhere between 1.18.3181.0 - 1.19.11213.0 MRE: [VtRendererBug.zip](https://github.com/microsoft/terminal/files/15314733/VtRendererBug.zip) STR: compile, run, follow instructions ### Expected Behavior The vertical bar moves right without affecting the underlying text. It works as expected in OpenConsole. ### Actual Behavior The vertical bar and the underlying text are broken. The third line is written into the second. It doesn't work as expected in WT and other terminals that use OpenConsole, e.g. WezTerm. --- I've managed to trace it down to https://github.com/microsoft/terminal/blob/f62d2d5d2c7b4188daf1d46f48faacf5ee2c717d/src/renderer/vt/XtermEngine.cpp#L281-L282 It looks like specifying the position explicitly fixes the issue: ```DIFF - std::string seq = "\r\n"; - hr = _Write(seq); + hr = _CursorPosition(coord); ``` Which could mean that the cursor position in `_lastText` is tracked incorrectly / unreliably.
claunia added the Area-RenderingIssue-BugNeeds-Tag-FixProduct-Conpty labels 2026-01-31 07:52:50 +00:00
Author
Owner

@github-actions[bot] commented on GitHub (May 15, 2024):

Hi I'm an AI powered bot that finds similar issues based off the issue title.

Please view the issues below to see if they solve your problem, and if the issue describes your problem please consider closing this one and thumbs upping the other issue to help us prioritize it. Thank you!

Closed similar issues:

Note: You can give me feedback by thumbs upping or thumbs downing this comment.

@github-actions[bot] commented on GitHub (May 15, 2024): Hi I'm an AI powered bot that finds similar issues based off the issue title. Please view the issues below to see if they solve your problem, and if the issue describes your problem please consider closing this one and thumbs upping the other issue to help us prioritize it. Thank you! ### Closed similar issues: - [Issues with cursor invalidation in GDI (#17150)](https://github.com/microsoft/terminal/issues/17150), similarity score: 0.75 > Note: You can give me feedback by thumbs upping or thumbs downing this comment.
Author
Owner

@alabuzhev commented on GitHub (May 16, 2024):

MRE: VtRendererBug.zip
STR: compile, run, follow instructions

P.S. I think the provided example reproduces the same issue.
Just in case if not, STR for the original one:

  1. Get Far - https://github.com/FarGroup/FarManager/releases/
  2. Run it.
  3. In F9 - Options - Interface settings
    uncheck:
    [ ] Use Virtual Terminal for rendering
    [ ] Fullwidth-aware rendering
    check:
    [x] ClearType-friendly redraw (can be slow)
  4. Edit any file (F4)
  5. In F11 - FarColorer - Configure - Main settings
    check:
    [x] Cross
    set Cross style to both
  6. Press CtrlHome to move the cursor to the upper left corner and then Right a few times.
@alabuzhev commented on GitHub (May 16, 2024): > MRE: [VtRendererBug.zip](https://github.com/microsoft/terminal/files/15314733/VtRendererBug.zip) > STR: compile, run, follow instructions P.S. I *think* the provided example reproduces the same issue. Just in case if not, STR for the original one: 1. Get Far - https://github.com/FarGroup/FarManager/releases/ 2. Run it. 3. In F9 - Options - Interface settings uncheck: `[ ] Use Virtual Terminal for rendering` `[ ] Fullwidth-aware rendering` check: `[x] ClearType-friendly redraw (can be slow)` 4. Edit any file (F4) 5. In F11 - FarColorer - Configure - Main settings check: `[x] Cross` set `Cross style` to `both` 6. Press `CtrlHome` to move the cursor to the upper left corner and then `Right` a few times.
Author
Owner

@lhecker commented on GitHub (May 16, 2024):

We've talked internally about reverting #15500 for the 1.21 release. It's the source for all these issues. While the fix you found may fix this particular issue, it's not unlikely that there will be more.

On the current main branch, which targets version 1.22, we're thinking of potentially removing VtEngine in its entirety. This would then also fix the cursor invalidation issue introduced in #15500. Unfortunately, this means that this issue would intentionally exist on main for up to a couple months.

@lhecker commented on GitHub (May 16, 2024): We've talked internally about reverting #15500 for the 1.21 release. It's the source for all these issues. While the fix you found may fix this particular issue, it's not unlikely that there will be more. On the current main branch, which targets version 1.22, we're thinking of potentially removing VtEngine in its entirety. This would then also fix the cursor invalidation issue introduced in #15500. Unfortunately, this means that this issue would intentionally exist on main for up to a couple months.
Author
Owner

@j4james commented on GitHub (May 16, 2024):

FYI, I did a git bisect with the VtRendererBug test case, and for me the issue first showed up in commit 72b44888b5. However, I suspect it may be one of those things that's timing dependent, and that commit just exposed a bug that has always existed.

@j4james commented on GitHub (May 16, 2024): FYI, I did a git bisect with the VtRendererBug test case, and for me the issue first showed up in commit 72b44888b5487a41cf2f030de86daea34b30e9c0. However, I suspect it may be one of those things that's timing dependent, and that commit just exposed a bug that has always existed.
Author
Owner

@j4james commented on GitHub (May 16, 2024):

I've just confirmed that reverting commit 72b44888b5 on the main branch is enough to fix the issue for me (i.e. moving the WaitUntilCanRender call back down to the bottom of the loop, before PaintFrame). And I've also now tested the cursor movement in Far itself, and the same fix works there too.

Again I'm not sure that's a real solution to the problem, but it might be worth considering as a quick fix, assuming it works for everyone else and not just me.

@j4james commented on GitHub (May 16, 2024): I've just confirmed that reverting commit 72b44888b5487a41cf2f030de86daea34b30e9c0 on the main branch is enough to fix the issue for me (i.e. moving the `WaitUntilCanRender` call back down to the bottom of the loop, before `PaintFrame`). And I've also now tested the cursor movement in Far itself, and the same fix works there too. Again I'm not sure that's a real solution to the problem, but it might be worth considering as a quick fix, assuming it works for everyone else and not just me.
Author
Owner

@alabuzhev commented on GitHub (May 16, 2024):

Thanks James, reverting 72b44888b5 fixed it for me as well.

@alabuzhev commented on GitHub (May 16, 2024): Thanks James, reverting 72b44888b5487a41cf2f030de86daea34b30e9c0 fixed it for me as well.
Author
Owner

@j4james commented on GitHub (May 16, 2024):

As for what's going wrong, this is what I've been able to establish:

  1. When you write a line of text that extends to the last column, we set the _wrapForced flag, even though the line hasn't actually wrapped (this is partially tracked in #15602, although that's referencing the AdaptDispatch class, while this particular case is in one of the console APIs).

  2. When the VtEngine outputs that line, and it sees that flag is set, it records the apparent wrapped state in the _wrappedRow field, again despite the fact that it hasn't actually wrapped.

  3. At this point in time, the cursor is meant to be at the start of line 2, because that's where it was manually positioned, and the WriteConsoleOutput API doesn't change that. So at the end of the frame, the VtEngine tries to move the cursor there. However, because of _wrappedRow being set to the line above, it thinks there is nothing to do (the previousLineWrapped test here).

  4. Even though the _MoveCursor method didn't do anything in the previous step, it still updates the _lastText field to where it mistakenly thought the cursor was (it thinks it's at the start of line 2, when it's actually at the end of line 1). This is why the subsequent output ends up the wrong location.

Ignoring bug number 1 for the moment, it seems to me that point 3 is also a bug, because the _wrappedRow field being set does not actually indicate that the cursor is on the next line, only that it doesn't need to be moved there if you're about to write a character. If you're just painting the cursor at that point, it needs to be moved explicitly.

So another possible way to fix this issue would be to set _wrappedRow = std::nullopt in the VtEngine::PaintCursor method before calling _MoveCursor.

That said, the conpty code is complicated, and I really don't know it very well, so there may be more to it than that. I'm just throwing this out there as potential solution.

@j4james commented on GitHub (May 16, 2024): As for what's going wrong, this is what I've been able to establish: 1. When you write a line of text that extends to the last column, we set the `_wrapForced` flag, even though the line hasn't actually wrapped (this is partially tracked in #15602, although that's referencing the `AdaptDispatch` class, while this particular case is in one of the console APIs). 2. When the VtEngine outputs that line, and it sees that flag is set, it records the apparent wrapped state in the `_wrappedRow` field, again despite the fact that it hasn't actually wrapped. 3. At this point in time, the cursor is meant to be at the start of line 2, because that's where it was manually positioned, and the `WriteConsoleOutput` API doesn't change that. So at the end of the frame, the VtEngine tries to move the cursor there. However, because of `_wrappedRow` being set to the line above, it thinks there is nothing to do (the `previousLineWrapped` test [here](https://github.com/microsoft/terminal/blob/183a8956f699e1967d4cb4712184d0dbc395a0e5/src/renderer/vt/XtermEngine.cpp#L265-L278)). 4. Even though the `_MoveCursor` method didn't do anything in the previous step, it still updates the `_lastText` field to where it mistakenly thought the cursor was (it thinks it's at the start of line 2, when it's actually at the end of line 1). This is why the subsequent output ends up the wrong location. Ignoring bug number 1 for the moment, it seems to me that point 3 is also a bug, because the `_wrappedRow` field being set does not actually indicate that the cursor is on the next line, only that it doesn't need to be moved there if you're about to write a character. If you're just painting the cursor at that point, it needs to be moved explicitly. So another possible way to fix this issue would be to set `_wrappedRow = std::nullopt` in the `VtEngine::PaintCursor` method before calling `_MoveCursor`. That said, the conpty code is complicated, and I really don't know it very well, so there may be more to it than that. I'm just throwing this out there as potential solution.
Author
Owner

@j4james commented on GitHub (May 16, 2024):

Another simple test case that can be run from a WSL bash shell:

printf '\033[2J\033[1;999H*'; sleep 1; printf '\033[2H\033[65;1;1;1;999$x'; sleep 1; printf '\033[3HThis should be line 3\n'

The text "This should be line 3", should be on line 3 😄, but it usually ends up on line 2.

This one is not fixed by reverting 72b44888b5, but it is fixed by the _wrappedRow patch suggested above.

@j4james commented on GitHub (May 16, 2024): Another simple test case that can be run from a WSL bash shell: ``` printf '\033[2J\033[1;999H*'; sleep 1; printf '\033[2H\033[65;1;1;1;999$x'; sleep 1; printf '\033[3HThis should be line 3\n' ``` The text "This should be line 3", should be on line 3 😄, but it usually ends up on line 2. This one is not fixed by reverting 72b44888b5487a41cf2f030de86daea34b30e9c0, but it is fixed by the `_wrappedRow` patch suggested above.
Author
Owner

@DHowett commented on GitHub (May 17, 2024):

Whichever the fix ends up being, I'm going to defer it until the next servicing wave for 1.21 - thanks for all the great investigations, everyone 😄

@DHowett commented on GitHub (May 17, 2024): Whichever the fix ends up being, I'm going to defer it until the next servicing wave for 1.21 - thanks for all the great investigations, everyone 😄
Author
Owner

@j4james commented on GitHub (May 25, 2024):

I've just realised that issue #17013 is another variant of this bug. And that scenario is also fixed by PR #17290.

@j4james commented on GitHub (May 25, 2024): I've just realised that issue #17013 is another variant of this bug. And that scenario is also fixed by PR #17290.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/terminal#21713