When a scroll region is set below an image and the region is scrolled up, mintty moves the image too, even though it lies entirely outside the region. Images below the region are also moved, for a related reason.
This came out of tmux/tmux#5507, where it first looked like a tmux bug. The diagnosis and the patch below are the work of Michael Grant (@mgrant0), a tmux maintainer, who traced it to term_do_scroll() and reduced it to a case with no tmux involved. I have verified the reproduction and built mintty master with the patch applied; I am filing here on his suggestion so it reaches the right tracker. Full history is in the tmux issue.
Reproduction
In a 33-row mintty window, with the attached sixels.txt (any sixel image about 19 rows tall will do):
clear; cat sixels.txt; sleep 1 ; printf '\e[21;32r\e[12S\e[r' ; sleep 10
The image occupies rows 1-19. The escape sequence sets a scroll region of rows 21-32, scrolls it up by 12, then resets the region.
Expected: nothing above row 21 changes; the image stays where it is.
Actual: the image jumps up 12 rows, leaving the bottom portion stacked over the top of itself. Forcing a full repaint restores it.
Cause
mintty stores image positions in virtual line coordinates: img->top = term.virtuallines + scrtop (winimg.c:274), and paints at (img->top - term.virtuallines - term.disptop) * cell_height (winimg.c:571). term.virtuallines is therefore a global offset: bumping it slides every image on screen.
The scroll-up branch of term_do_scroll() does exactly that and nothing else (term.c:2237):
else {
int seltop = topline;
term.virtuallines += lines;
For a full-screen scroll that is correct. For a region scroll with topline > 0 it drags along every image on screen, including ones nowhere near the region.
There is a second, related bug just above it. The scroll-down branch does have an image fixup loop, but its bounds test is cur->top - term.virtuallines >= topline, only a lower bound. The selection code immediately adjacent gets this right with p->y >= topline && p->y < botline, so the image loop also wrongly moves images that sit below the region.
Suggested patch
Against master's src/term.c: adds the missing botline bound to the scroll-down loop, and compensates images outside the region in the scroll-up branch.
--- a/src/term.c
+++ b/src/term.c
@@ -2229,7 +2229,8 @@ term_do_scroll(int topline, int botline, int lines, bool sb)
// Move graphics if within the scroll region
for (imglist * cur = term.imgs.first; cur; cur = cur->next) {
- if (cur->top - term.virtuallines >= topline) {
+ int imgtop = cur->top - term.virtuallines;
+ if (imgtop >= topline && imgtop < botline) {
cur->top += lines;
}
}
@@ -2239,6 +2240,14 @@ term_do_scroll(int topline, int botline, int lines, bool sb)
term.virtuallines += lines;
+ // Shifting virtuallines moves every image on the screen; images outside
+ // the scroll region must stay put, so compensate for those.
+ for (imglist * cur = term.imgs.first; cur; cur = cur->next) {
+ int imgtop = cur->top - term.virtuallines + lines;
+ if (imgtop < topline || imgtop >= botline)
+ cur->top += lines;
+ }
+
// Only push lines into the scrollback when scrolling off the top of the
// normal screen and scrollback is actually enabled.
if (sb && topline == 0 && !term.on_alt_screen && cfg.scrollback_lines) {
I have built master (d7132bb) with this applied and it fixes both the reduced case above and the original tmux case. Ordinary full-screen scrolling and scrollback still behave as before, so the added loop does not appear to regress the common path.
Why this shows up in practice
zsh emits a bare \e[J before drawing each prompt, so under tmux this triggers on essentially every command. Any sixel image on screen is corrupted as soon as the next prompt appears.
tmux turns that \e[J into precisely the pattern above. With the cursor on row 20 and an image on rows 1-19, tty_clear_area() in tmux's tty.c clears the rest of the screen by scrolling it away:
^[[21;32r^[[12S^[[20;1H^[[K^[[1;33r
That is legal and correct, and tmux still believes the image is on rows 1-19, so it does not repaint it, which is why the corruption persists until you detach and reattach.
Environment
uname -s: MINGW64_NT-10.0-26200
When a scroll region is set below an image and the region is scrolled up, mintty moves the image too, even though it lies entirely outside the region. Images below the region are also moved, for a related reason.
This came out of tmux/tmux#5507, where it first looked like a tmux bug. The diagnosis and the patch below are the work of Michael Grant (@mgrant0), a tmux maintainer, who traced it to
term_do_scroll()and reduced it to a case with no tmux involved. I have verified the reproduction and built mintty master with the patch applied; I am filing here on his suggestion so it reaches the right tracker. Full history is in the tmux issue.Reproduction
In a 33-row mintty window, with the attached sixels.txt (any sixel image about 19 rows tall will do):
The image occupies rows 1-19. The escape sequence sets a scroll region of rows 21-32, scrolls it up by 12, then resets the region.
Expected: nothing above row 21 changes; the image stays where it is.
Actual: the image jumps up 12 rows, leaving the bottom portion stacked over the top of itself. Forcing a full repaint restores it.
Cause
mintty stores image positions in virtual line coordinates:
img->top = term.virtuallines + scrtop(winimg.c:274), and paints at(img->top - term.virtuallines - term.disptop) * cell_height(winimg.c:571).term.virtuallinesis therefore a global offset: bumping it slides every image on screen.The scroll-up branch of
term_do_scroll()does exactly that and nothing else (term.c:2237):For a full-screen scroll that is correct. For a region scroll with
topline > 0it drags along every image on screen, including ones nowhere near the region.There is a second, related bug just above it. The scroll-down branch does have an image fixup loop, but its bounds test is
cur->top - term.virtuallines >= topline, only a lower bound. The selection code immediately adjacent gets this right withp->y >= topline && p->y < botline, so the image loop also wrongly moves images that sit below the region.Suggested patch
Against master's
src/term.c: adds the missingbotlinebound to the scroll-down loop, and compensates images outside the region in the scroll-up branch.I have built master (d7132bb) with this applied and it fixes both the reduced case above and the original tmux case. Ordinary full-screen scrolling and scrollback still behave as before, so the added loop does not appear to regress the common path.
Why this shows up in practice
zsh emits a bare
\e[Jbefore drawing each prompt, so under tmux this triggers on essentially every command. Any sixel image on screen is corrupted as soon as the next prompt appears.tmux turns that
\e[Jinto precisely the pattern above. With the cursor on row 20 and an image on rows 1-19,tty_clear_area()in tmux'stty.cclears the rest of the screen by scrolling it away:That is legal and correct, and tmux still believes the image is on rows 1-19, so it does not repaint it, which is why the corruption persists until you detach and reattach.
Environment
uname -s: MINGW64_NT-10.0-26200