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.

Raspunde prin e-mail lui