patch 9.2.0628: popup image: wrong overlap layering, kitty laggy
Commit:
https://github.com/vim/vim/commit/0dce2b9c8bf93ec0908accf2e450e9f03f8cfb54
Author: Yasuhiro Matsumoto <[email protected]>
Date: Sat Jun 13 15:36:13 2026 +0000
patch 9.2.0628: popup image: wrong overlap layering, kitty laggy
Problem: popup image: wrong overlap layering, kitty laggy
Solution: Make the end-of-redraw re-emit pass GUI-only, handle zindex
correctly (Yasuhiro Matsumoto).
Emitting every popup image again at the end of each redraw painted lower
zindex images over higher popups and re-sent the multi-MB kitty sequence
on every cursor movement. Make update_popup_images() GUI-only; in
terminal mode the zindex-ordered emit in update_popups() suffices, with
ScreenLines invalidated for cells a higher popup draws over an emitted
sixel image. Kitty placements persist and are now layered with z=zindex,
so retransmission is skipped while the placement is still current.
closes: #20474
Signed-off-by: Yasuhiro Matsumoto <[email protected]>
Signed-off-by: Christian Brabandt <[email protected]>
diff --git a/src/drawscreen.c b/src/drawscreen.c
index 4119cd244..58b54c3b3 100644
--- a/src/drawscreen.c
+++ b/src/drawscreen.c
@@ -449,11 +449,10 @@ update_screen(int type_arg)
}
#endif
-#ifdef FEAT_IMAGE
- // Popup images are blitted by update_popups(), but later steps in
- // update_screen() such as the intro message, GUI cursor redraw, and other
- // final overlays may paint on top of them. Re-emit the popup images once
- // here at the end of every redraw so the image layer is restored.
+#if defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO)
+ // GUI only: the cursor redraw and other late blits paint directly onto
+ // the canvas and may damage the popup images blitted by update_popups();
+ // restore the image layer. No-op in terminal mode.
update_popup_images();
#endif
diff --git a/src/kitty.c b/src/kitty.c
index 825de78ca..183d17e58 100644
--- a/src/kitty.c
+++ b/src/kitty.c
@@ -86,9 +86,11 @@ kitty_b64_append(garray_T *ga, char_u *src, long len)
* via `m=1`/`m=0` so the per-envelope payload stays under kitty's
* 4096-byte limit. When "id" is non-zero it is sent as `i=<id>` so
* the resulting placement can later be removed via kitty_delete().
+ * "zindex" is sent as `z=<zindex>` so overlapping placements stack in
+ * popup zindex order no matter in which order they were (re)created.
*/
char_u *
-kitty_encode(image_rgb_T *img, int id)
+kitty_encode(image_rgb_T *img, int id, int zindex)
{
garray_T ga;
long pix_bytes;
@@ -125,12 +127,13 @@ kitty_encode(image_rgb_T *img, int id)
{
if (id != 0)
vim_snprintf((char *)hdr, sizeof(hdr),
- " _Ga=T,f=%d,s=%d,v=%d,i=%d,q=2,m=%d;",
- fmt, img->width, img->height, id, more ? 1 : 0);
+ " _Ga=T,f=%d,s=%d,v=%d,i=%d,z=%d,q=2,m=%d;",
+ fmt, img->width, img->height, id, zindex,
+ more ? 1 : 0);
else
vim_snprintf((char *)hdr, sizeof(hdr),
- " _Ga=T,f=%d,s=%d,v=%d,q=2,m=%d;",
- fmt, img->width, img->height, more ? 1 : 0);
+ " _Ga=T,f=%d,s=%d,v=%d,z=%d,q=2,m=%d;",
+ fmt, img->width, img->height, zindex, more ? 1 : 0);
first = FALSE;
}
else
diff --git a/src/popupwin.c b/src/popupwin.c
index fdb1d394c..94ffe8939 100644
--- a/src/popupwin.c
+++ b/src/popupwin.c
@@ -957,6 +957,7 @@ apply_general_options(win_T *wp, dict_T *dict)
wp->w_popup_image_seq_crop_y = 0;
wp->w_popup_image_seq_cells_w = 0;
wp->w_popup_image_seq_cells_h = 0;
+ wp->w_popup_image_emit_valid = false;
# endif
# if defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO)
# ifdef FEAT_GUI
@@ -1014,6 +1015,9 @@ apply_general_options(win_T *wp, dict_T *dict)
# ifdef FEAT_IMAGE_SIXEL
VIM_CLEAR(wp->w_popup_image_seq);
wp->w_popup_image_seq_h = -1;
+# endif
+# ifdef FEAT_IMAGE_KITTY
+ wp->w_popup_image_emit_valid = false;
# endif
if (wp->w_popup_image_data != NULL)
{
@@ -1951,6 +1955,7 @@ popup_encode_image(win_T *wp)
{
VIM_CLEAR(wp->w_popup_image_seq);
wp->w_popup_image_seq_h = 0;
+ wp->w_popup_image_emit_valid = false;
return;
}
@@ -2002,16 +2007,19 @@ popup_encode_image(win_T *wp)
{
VIM_CLEAR(wp->w_popup_image_seq);
wp->w_popup_image_seq_h = 0;
+ wp->w_popup_image_emit_valid = false;
return;
}
if (wp->w_popup_image_seq != NULL
&& wp->w_popup_image_seq_w == target_w
&& wp->w_popup_image_seq_h == target_h
&& wp->w_popup_image_seq_crop_x == crop_left_px
- && wp->w_popup_image_seq_crop_y == crop_top_px)
- return; // already encoded for this geometry
+ && wp->w_popup_image_seq_crop_y == crop_top_px
+ && wp->w_popup_image_seq_zindex == wp->w_zindex)
+ return; // already encoded for this geometry and zindex
VIM_CLEAR(wp->w_popup_image_seq);
+ wp->w_popup_image_emit_valid = false;
// The sixel/kitty encoders read data tightly packed as width*height
// pixels. When the source row width changes (left or right clipped),
@@ -2051,7 +2059,7 @@ popup_encode_image(win_T *wp)
// Use the popup's window-id as the kitty image id so that
// popup_image_clear_kitty() can target the placement when the
// popup is later hidden or closed.
- wp->w_popup_image_seq = kitty_encode(&si, wp->w_id);
+ wp->w_popup_image_seq = kitty_encode(&si, wp->w_id, wp->w_zindex);
else
# endif
wp->w_popup_image_seq = sixel_encode(&si);
@@ -2066,6 +2074,7 @@ popup_encode_image(win_T *wp)
wp->w_popup_image_seq_crop_y = crop_top_px;
wp->w_popup_image_seq_cells_w = (target_w + cell_x - 1) / cell_x;
wp->w_popup_image_seq_cells_h = (target_h + cell_y - 1) / cell_y;
+ wp->w_popup_image_seq_zindex = wp->w_zindex;
}
else
{
@@ -6912,6 +6921,18 @@ popup_emit_image(win_T *wp)
}
if (row < 0 || col < 0)
return;
+# ifdef FEAT_IMAGE_KITTY
+ // A kitty placement persists on the terminal and is drawn above the
+ // text layer, so when it is already showing at this position there is
+ // nothing to repair: skip the (potentially multi-MB) retransmission.
+ // The flag is reset when the image is re-encoded, the placement is
+ // deleted, or the terminal screen is cleared.
+ if (popup_image_backend() == IMAGE_BACKEND_KITTY
+ && wp->w_popup_image_emit_valid
+ && wp->w_popup_image_emit_row == row
+ && wp->w_popup_image_emit_col == col)
+ return;
+# endif
// Hide the cursor across the move + image emit, then restore it to
// the current text-cursor position before showing it; otherwise the
// cursor can briefly flicker below its scrolled-to position because
@@ -6929,6 +6950,40 @@ popup_emit_image(win_T *wp)
out_str((char_u *)" [?25h");
out_flush();
+ // The sixel bytes just painted over every cell of the emitted rectangle,
+ // including cells that a higher zindex popup draws on top of this image.
+ // Invalidate those cells in ScreenLines so the higher popup's draw,
+ // later in this same update_popups() walk, actually rewrites them to
+ // the terminal instead of skipping them as unchanged. Not needed for
+ // kitty, where the placement is layered by its z= value instead.
+# ifdef FEAT_IMAGE_KITTY
+ if (popup_image_backend() != IMAGE_BACKEND_KITTY)
+# endif
+ {
+ for (int rr = row; rr < row + wp->w_popup_image_seq_cells_h; ++rr)
+ {
+ if (rr < 0 || rr >= screen_Rows)
+ continue;
+
+ int off_base = LineOffset[rr];
+
+ for (int cc = col; cc < col + wp->w_popup_image_seq_cells_w; ++cc)
+ {
+ if (cc < 0 || cc >= screen_Columns)
+ continue;
+ if (popup_mask[rr * screen_Columns + cc] <= wp->w_zindex)
+ continue;
+
+ int off = off_base + cc;
+
+ ScreenLines[off] = ' ';
+ if (enc_utf8 && ScreenLinesUC != NULL)
+ ScreenLinesUC[off] = 0;
+ ScreenAttrs[off] = -1;
+ }
+ }
+ }
+
// Remember where the image was emitted so the next redraw can invalidate
// ScreenLines/ScreenAttrs for cells that move out from under the image
// (e.g. body -> top padding when the clip shrinks). Otherwise screen_fill
@@ -6938,6 +6993,7 @@ popup_emit_image(win_T *wp)
wp->w_popup_image_emit_col = col;
wp->w_popup_image_emit_cells_w = wp->w_popup_image_seq_cells_w;
wp->w_popup_image_emit_cells_h = wp->w_popup_image_seq_cells_h;
+ wp->w_popup_image_emit_valid = true;
# endif
}
@@ -6964,25 +7020,56 @@ popup_image_clear_kitty(win_T *wp)
out_str(seq);
out_flush();
vim_free(seq);
+ wp->w_popup_image_emit_valid = false;
+}
+# endif
+
+# if defined(FEAT_IMAGE_SIXEL) || defined(FEAT_IMAGE_KITTY)
+/*
+ * Called after the terminal screen has been cleared: kitty deletes
+ * placements that intersect the erased area, so the cached "already on
+ * screen" state no longer holds and the next popup_emit_image() must
+ * retransmit.
+ */
+ void
+popup_images_invalidate(void)
+{
+ win_T *wp;
+ tabpage_T *tp;
+
+ FOR_ALL_POPUPWINS(wp)
+ wp->w_popup_image_emit_valid = false;
+ FOR_ALL_TABPAGES(tp)
+ FOR_ALL_POPUPWINS_IN_TAB(tp, wp)
+ wp->w_popup_image_emit_valid = false;
}
# endif
/*
* Re-paint every popup's image after the rest of the screen update has
- * settled. Called from update_screen() after the intro message and the
- * GUI cursor have had their say, otherwise those would clobber the image
- * we just blitted onto the canvas.
+ * settled. Only needed for the GUI, where the cursor redraw and other
+ * late blits paint directly onto the canvas and can damage the images.
+ * Walk the popups in zindex order, lowest first, so that where images
+ * overlap the higher zindex popup's image ends up on top.
+ * In terminal mode there is nothing to repair: everything drawn after
+ * update_popups() goes through ScreenLines writers that respect
+ * popup_mask, so the images emitted there are still intact. Re-emitting
+ * here would instead paint a lower zindex image over the cells of a
+ * higher zindex popup drawn on top of it.
*/
+# if defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO)
void
update_popup_images(void)
{
win_T *wp;
- FOR_ALL_POPUPWINS(wp)
- popup_emit_image(wp);
- FOR_ALL_POPUPWINS_IN_TAB(curtab, wp)
+ if (!gui.in_use)
+ return;
+ popup_reset_handled(POPUP_HANDLED_5);
+ while ((wp = find_next_popup(TRUE, POPUP_HANDLED_5)) != NULL)
popup_emit_image(wp);
}
+# endif
# ifdef FEAT_IMAGE_GDI
static void
@@ -7644,12 +7731,12 @@ update_popups(void (*win_update)(win_T *wp))
#ifdef FEAT_IMAGE
// Emit the popup image right after this popup's decorations land in
- // ScreenLines: the image must sit on top of its own border/padding so
- // it is visible while update_popups() walks the remaining popups.
- // A second pass from update_popup_images() runs at the end of redraw
- // to re-emit on top of any late overlays (intro message, cursor, ...);
- // the cost there is one out_str() per image -- correctness wins over
- // shaving a redundant write that only happens once per redraw cycle.
+ // ScreenLines. Popups are walked in zindex order, so a higher
+ // zindex popup drawn later paints its cells over this image where
+ // they overlap, and its own image lands on top of those again:
+ // correct layering at cell granularity. This is the only emit pass
+ // in terminal mode; update_popup_images() at the end of redraw is a
+ // GUI-only repair.
// The topoff shift is undone above so popup_emit_image() sees the
// popup's logical winrow. Otherwise the clip-top adjustment
// overshoots by topoff and the image lands below its correct row.
diff --git a/src/proto/kitty.pro b/src/proto/kitty.pro
index 3630f5a6a..11e2ab2f4 100644
--- a/src/proto/kitty.pro
+++ b/src/proto/kitty.pro
@@ -1,4 +1,4 @@
/* kitty.c */
-char_u *kitty_encode(image_rgb_T *img, int id);
+char_u *kitty_encode(image_rgb_T *img, int id, int zindex);
char_u *kitty_delete(int id);
/* vim: set ft=c : */
diff --git a/src/proto/popupwin.pro b/src/proto/popupwin.pro
index db917e818..98e3a5f59 100644
--- a/src/proto/popupwin.pro
+++ b/src/proto/popupwin.pro
@@ -59,6 +59,7 @@ void may_update_popup_mask(int type);
void may_update_popup_position(void);
int popup_get_base_screen_cell(int row, int col, schar_T *linep, int *attrp,
u8char_T *ucp);
void popup_set_base_screen_cell(int row, int col, schar_T line, int attr,
u8char_T uc);
+void popup_images_invalidate(void);
void update_popup_images(void);
void update_popup_images_rect(int left, int top, int right, int bottom);
void update_popups(void (*win_update)(win_T *wp));
diff --git a/src/screen.c b/src/screen.c
index 7ade748b7..de2def9e0 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -3628,6 +3628,11 @@ screenclear2(int doclear)
if (suppressed_cells != NULL)
vim_memset(suppressed_cells, 0,
(size_t)suppressed_rows * suppressed_cols);
+#endif
+#if defined(FEAT_IMAGE_SIXEL) || defined(FEAT_IMAGE_KITTY)
+ // Clearing the display removes kitty image placements; force the
+ // next redraw to retransmit popup images.
+ popup_images_invalidate();
#endif
}
else
diff --git a/src/structs.h b/src/structs.h
index eb0bfe6b3..8218a80f5 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -4274,6 +4274,10 @@ struct window_S
int w_popup_image_seq_crop_y; // pixel offset (top) into
source
int w_popup_image_seq_cells_w; // cell width spanning seq
pixels
int w_popup_image_seq_cells_h; // cell height spanning seq
pixels
+ int w_popup_image_seq_zindex; // zindex encoded into seq
(kitty z=)
+ bool w_popup_image_emit_valid; // true while the kitty placement
+ // emitted at w_popup_image_emit_*
+ // is still on the terminal
# endif
# ifdef FEAT_IMAGE_GDI
// Pre-built Windows GUI image cache. The bitmap is a 32-bit top-down
diff --git a/src/version.c b/src/version.c
index 812d66fd2..90ec1dc43 100644
--- a/src/version.c
+++ b/src/version.c
@@ -754,6 +754,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 628,
/**/
627,
/**/
diff --git a/src/vim.h b/src/vim.h
index 7d1914cd6..1f8e7e1a4 100644
--- a/src/vim.h
+++ b/src/vim.h
@@ -697,7 +697,8 @@ extern int (*dyn_libintl_wputenv)(const wchar_t *envstring);
#define POPUP_HANDLED_2 0x02 // used by popup_do_filter()
#define POPUP_HANDLED_3 0x04 // used by popup_check_cursor_pos()
#define POPUP_HANDLED_4 0x08 // used by may_update_popup_mask()
-#define POPUP_HANDLED_5 0x10 // used by update_popups()
+#define POPUP_HANDLED_5 0x10 // used by update_popups() and
+ // update_popup_images()
/*
* Terminal highlighting attribute bits.
--
--
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php
---
You received this message because you are subscribed to the Google Groups
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion visit
https://groups.google.com/d/msgid/vim_dev/E1wYQmC-00DEit-L2%40256bit.org.