]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0628: popup image: wrong overlap layering, kitty laggy v9.2.0628
authorYasuhiro Matsumoto <mattn.jp@gmail.com>
Sat, 13 Jun 2026 15:36:13 +0000 (15:36 +0000)
committerChristian Brabandt <cb@256bit.org>
Sat, 13 Jun 2026 15:36:13 +0000 (15:36 +0000)
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 <mattn.jp@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
src/drawscreen.c
src/kitty.c
src/popupwin.c
src/proto/kitty.pro
src/proto/popupwin.pro
src/screen.c
src/structs.h
src/version.c
src/vim.h

index 4119cd2449617983c438626e92f0e8b5147d2204..58b54c3b39c5666cb1eca76bf94897cc048c1987 100644 (file)
@@ -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
 
index 825de78caa6e6959269a4d7f291c31191e70861d..183d17e58350c63790059ca620ede55d5facec34 100644 (file)
@@ -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),
-                       "\033_Ga=T,f=%d,s=%d,v=%d,i=%d,q=2,m=%d;",
-                       fmt, img->width, img->height, id, more ? 1 : 0);
+                       "\033_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),
-                       "\033_Ga=T,f=%d,s=%d,v=%d,q=2,m=%d;",
-                       fmt, img->width, img->height, more ? 1 : 0);
+                       "\033_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
index fdb1d394cdd0245a4d9db705db7e15686a87efda..94ffe89391ff28476e9fd61c89b3d833431062a9 100644 (file)
@@ -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 *)"\033[?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.
index 3630f5a6a7102a761fc809393fd64ffa65f6ee91..11e2ab2f4a97fe58f108b6de4b5397ed3f27578b 100644 (file)
@@ -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 : */
index db917e818116c42cde3c70fb747595b05e6450be..98e3a5f5922bfad9c4a99f1f98c54d19c5285a2f 100644 (file)
@@ -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));
index 7ade748b710e5c0bb9592faab7da7384f8405845..de2def9e0fb906bf574e5a33bbddd3ad35a0c5a1 100644 (file)
@@ -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
index eb0bfe6b3b6fb66330c92af8389996d242629255..8218a80f5bc92b1a1837fd27920eda8b00dede0a 100644 (file)
@@ -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
index 812d66fd2c1a1a7a9836e13a716a855cfe0cd342..90ec1dc438d02ce7e6fe209b7acb58eedcedb9a8 100644 (file)
@@ -754,6 +754,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    628,
 /**/
     627,
 /**/
index 7d1914cd66886fdbfc7dbe4fdf318e7fa46637cc..1f8e7e1a42d0d4f074cdd2b6bd5fa87d03b14db1 100644 (file)
--- 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.