Commit: patch 9.2.0962: popup images are not using the kitty protocol properly

Christian Brabandt <[email protected]>
Newsgroups gmane.editors.vim.devel
Message-ID <[email protected]>
patch 9.2.0962: popup images are not using the kitty protocol properly

Commit: https://github.com/vim/vim/commit/89caa43b7882e34067e0ea8e3d64a7d29581744a
Author: Foxe Chen <[email protected]>
Date:   Mon Aug 17 20:21:28 2026 +0000

    patch 9.2.0962: popup images are not using the kitty protocol properly
    
    Problem:  The kitty image backend re-encodes and retransmits the whole
              image on every redraw, since the transmit and the placement are
              sent as a single "a=T" sequence built by popup_encode_image().
    Solution: Transmit the image once with "a=t" and send only a placement
              "a=p" on each redraw, cropping via the protocol's x/y/w/h
              instead of re-cropping the pixel data (Foxe Chen).
    
    closes: #21018
    
    Signed-off-by: Foxe Chen <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

diff --git a/src/kitty.c b/src/kitty.c
index 2b46b1454..279eba842 100644
--- a/src/kitty.c
+++ b/src/kitty.c
@@ -13,163 +13,136 @@
  *	    The popup's image bytes are sent in 4096-byte chunks of base64
  *	    inside ` _G...;<chunk> \` envelopes.
  *	    Spec: https://sw.kovidgoyal.net/kitty/graphics-protocol/
- *	    No external dependency; the base64 alphabet is inlined here.
+ *	    Base64 encoding is shared with misc2 base64_encode()/decode()
  */
 
 #include "vim.h"
 
 #if defined(FEAT_IMAGE_KITTY) || defined(PROTO)
 
-static const char_u kitty_b64_table[] =
-    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+// Max base64 chars per envelope, per the kitty graphics protocol.
+#define KITTY_CHUNK_B64		4096
+// Source bytes that encode into KITTY_CHUNK_B64 base64 chars.
+#define KITTY_CHUNK_SRC		(KITTY_CHUNK_B64 * 3 / 4)
+// header + base64 chunk + " \" trailer + NUL. Use 128 extra bytes padding
+// for header, should be more than enough.
+#define KITTY_BUF_SIZE		(128 + KITTY_CHUNK_B64 + 2 + 1)
 
 /*
- * Append a NUL-terminated string to "ga".  Returns OK / FAIL so the caller
- * can abort on allocation failure (unlike ga_concat(), which silently no-ops
- * and would leave a truncated, invalid kitty APC sequence behind).
+ * Return the kitty image id to use for window id "id".  Kitty image ids are
+ * global to the terminal, so mix in the process id: another Vim in the same
+ * terminal would otherwise use the same ids and its "a=d,d=I" would free our
+ * image data.
  */
     static int
-kitty_ga_concat(garray_T *ga, const char_u *s)
+kitty_image_id(int id)
 {
-    int	    len = (int)STRLEN(s);
+    static int	base = 0;
 
-    if (len == 0)
-	return OK;
-    if (ga_grow(ga, len) == FAIL)
-	return FAIL;
-    mch_memmove((char_u *)ga->ga_data + ga->ga_len, s, (size_t)len);
-    ga->ga_len += len;
-    return OK;
+    if (base == 0)
+	base = (((int)mch_get_pid() & 0x7fff) + 1) << 16;
+    return base | (id & 0xffff);
 }
 
 /*
- * Append base64-encoded bytes from "src[len]" to growarray "ga".
- * Returns OK / FAIL so the caller can propagate OOM.
+ * Transmit an RGB(A) image to the terminal (does not display it!). It will
+ * have an id of "id", so that it can be placed later.
  */
-    static int
-kitty_b64_append(garray_T *ga, char_u *src, long len)
+    int
+kitty_transmit(image_rgb_T *img, int id)
 {
-    long    i;
-    long    out_len = ((len + 2) / 3) * 4;
-    char_u  *dst;
-
-    if (out_len == 0)
-	return OK;
-    if (ga_grow(ga, (int)out_len) == FAIL)
-	return FAIL;
-    dst = (char_u *)ga->ga_data + ga->ga_len;
-    for (i = 0; i < len; i += 3)
-    {
-	unsigned a = src[i];
-	unsigned b = (i + 1 < len) ? src[i + 1] : 0;
-	unsigned c = (i + 2 < len) ? src[i + 2] : 0;
-	unsigned triple = (a << 16) | (b << 8) | c;
-
-	*dst++ = kitty_b64_table[(triple >> 18) & 0x3f];
-	*dst++ = kitty_b64_table[(triple >> 12) & 0x3f];
-	*dst++ = (i + 1 < len)
-			    ? kitty_b64_table[(triple >> 6) & 0x3f] : '=';
-	*dst++ = (i + 2 < len)
-			    ? kitty_b64_table[triple & 0x3f]	    : '=';
-    }
-    ga->ga_len += (int)out_len;
-    return OK;
-}
+    static char buf[KITTY_BUF_SIZE];
 
-/*
- * Encode an RGB(A) image into a kitty graphics protocol APC sequence.
- * Returns a malloced char_u* containing the full sequence
- * (one or more ` _G... \` envelopes), or NULL on OOM.
- *
- * The sequence is emitted with `a=T` (transmit + display), `q=2` (no
- * status responses), `f=24` for RGB or `f=32` for RGBA, and chunked
- * 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, int zindex)
-{
-    garray_T	ga;
     long	pix_bytes;
     long	payload_len;
-    long	b64_total;
     long	offset = 0;
     int		fmt;
     int		first = TRUE;
-    char_u	hdr[80];
 
     if (img == NULL || img->data == NULL || img->width <= 0 || img->height <= 0)
-	return NULL;
+	return FAIL;
 
     pix_bytes = img->has_alpha ? 4 : 3;
     payload_len = (long)img->width * img->height * pix_bytes;
-    b64_total = ((payload_len + 2) / 3) * 4;
     fmt = img->has_alpha ? 32 : 24;
 
-    ga_init2(&ga, 1, (int)b64_total + 256);
-
-    // Emit one envelope per 4096 base64 chars.  The first envelope
-    // carries the full geometry/format header; later envelopes only
-    // need the chunk-continuation marker `m=`.
-    while (offset < b64_total)
+    // Emit one envelope per KITTY_CHUNK_SRC source bytes (= 4096 base64
+    // chars).  The first envelope carries the full geometry/format
+    // header; later envelopes only need the chunk-continuation marker
+    // `m=`.
+    while (offset < payload_len)
     {
-	long	this_chunk = b64_total - offset;
+	long	this_chunk = payload_len - offset;
 	int	more;
+	int	hdr_len;
+	long	b64_len;
 
-	if (this_chunk > 4096)
-	    this_chunk = 4096;
-	more = (offset + this_chunk < b64_total);
+	if (this_chunk > KITTY_CHUNK_SRC)
+	    this_chunk = KITTY_CHUNK_SRC;
+	more = (offset + this_chunk < payload_len);
 
 	if (first)
 	{
-	    if (id != 0)
-		vim_snprintf((char *)hdr, sizeof(hdr),
-			" _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,z=%d,q=2,m=%d;",
-			fmt, img->width, img->height, zindex, more ? 1 : 0);
+	    hdr_len = vim_snprintf(buf, sizeof(buf),
+		    " _Ga=t,i=%d,f=%d,s=%d,v=%d,q=2,m=%d;",
+		    kitty_image_id(id), fmt, img->width,
+		    img->height, more ? 1 : 0);
 	    first = FALSE;
 	}
 	else
-	{
-	    vim_snprintf((char *)hdr, sizeof(hdr),
-		    " _Gm=%d;", more ? 1 : 0);
-	}
-	if (kitty_ga_concat(&ga, hdr) == FAIL)
-	    goto fail;
+	    hdr_len = vim_snprintf(buf, sizeof(buf), " _Gm=%d;",
+		    more ? 1 : 0);
 
-	// Encode the matching slice of the source bytes.  Each base64
-	// chunk consumes (this_chunk / 4) base64 quartets, which means
-	// (this_chunk * 3 / 4) source bytes.
-	{
-	    long	src_offset = offset * 3 / 4;
-	    long	src_len = this_chunk * 3 / 4;
+	b64_len = base64_encode_buf((char_u *)buf + hdr_len,
+					img->data + offset, this_chunk);
 
-	    if (src_offset + src_len > payload_len)
-		src_len = payload_len - src_offset;
-	    if (kitty_b64_append(&ga, img->data + src_offset, src_len) == FAIL)
-		goto fail;
-	}
-
-	if (kitty_ga_concat(&ga, (char_u *)" \") == FAIL)
-	    goto fail;
+	buf[hdr_len + b64_len] = ' ';
+	buf[hdr_len + b64_len + 1] = '\';
+	buf[hdr_len + b64_len + 2] = NUL;
 
+	out_str((char_u *)buf);
 	offset += this_chunk;
     }
 
-    if (ga_append(&ga, NUL) == FAIL)
-	goto fail;
-    return (char_u *)ga.ga_data;
+    out_flush();
+    return OK;
+}
+
+/*
+ * Place the image with the given id, which should have already been
+ * transmitted. Its placement id will always be its image id, so that the image
+ * is moved if it was previously placed.
+ */
+    void
+kitty_place(int id, int row, int col, int src_x, int src_y, int w, int h, int z)
+{
+    vim_snprintf((char *)IObuff, IOSIZE,
+	    " _Ga=p,i=%d,p=%d,x=%d,y=%d,w=%d,h=%d,z=%d,q=2 \",
+	    kitty_image_id(id), kitty_image_id(id), src_x, src_y, w, h, z);
+
+    term_windgoto(row, col);
+    out_str((char_u *)IObuff);
+    screen_start();
+    setcursor_mayforce(TRUE);
+    out_flush();
+}
+
+/*
+ * Delete image placement with image id "id" (which is also its placement id).
+ * If "del_data" is true, then its data will be freed by the terminal (see
+ * https://sw.kovidgoyal.net/kitty/graphics-protocol/#deleting-images).
+ */
+    void
+kitty_delete(int id, bool del_data)
+{
+    char d_key = del_data ? 'I' : 'i';
+
+    vim_snprintf((char *)IObuff, IOSIZE,
+	    " _Ga=d,d=%c,i=%d,p=%d,q=2 \", d_key, kitty_image_id(id),
+	    kitty_image_id(id));
 
-fail:
-    ga_clear(&ga);
-    return NULL;
+    out_str((char_u *)IObuff);
+    out_flush();
 }
 
 /*
@@ -240,25 +213,4 @@ kitty_probe_parse(char *buf, int n)
     return strstr(buf, "_Gi=31;OK") != NULL;
 }
 
-/*
- * Build a kitty "delete image" APC sequence for the placement created
- * by kitty_encode() with the matching `id`.  The caller must
- * vim_free() the returned buffer.  Returns NULL on OOM or id <= 0.
- *
- * Sequence: ` _Ga=d,i=<id>,q=2 \`
- *	a=d  -> action: delete
- *	i=   -> image id (target placement)
- *	q=2  -> suppress status reply
- */
-    char_u *
-kitty_delete(int id)
-{
-    char_u  buf[40];
-
-    if (id <= 0)
-	return NULL;
-    vim_snprintf((char *)buf, sizeof(buf), " _Ga=d,i=%d,q=2 \", id);
-    return vim_strsave(buf);
-}
-
 #endif // FEAT_IMAGE_KITTY || PROTO
diff --git a/src/popupwin.c b/src/popupwin.c
index 07e0b90e2..cbfe72f97 100644
--- a/src/popupwin.c
+++ b/src/popupwin.c
@@ -119,7 +119,7 @@ static void redraw_overlapped_opacity_popups(int winrow, int wincol,
 	int height, int width, int leftoff, int zindex);
 static void redraw_win_under_opacity_popup(win_T *wp);
 #ifdef FEAT_IMAGE_KITTY
-static void popup_image_clear_kitty(win_T *wp);
+static void popup_image_clear_kitty(win_T *wp, bool del_data);
 #endif
 // GDI and cairo paint the image straight into the window, so the area has to
 // be redrawn when the popup goes away.  GTK4 keeps a list of images to render
@@ -974,7 +974,7 @@ apply_general_options(win_T *wp, dict_T *dict)
 		    || wp->w_popup_image_w > 0 || wp->w_popup_image_h > 0)
 	    {
 # ifdef FEAT_IMAGE_KITTY
-		popup_image_clear_kitty(wp);
+		popup_image_clear_kitty(wp, true);
 # endif
 # ifdef FEAT_IMAGE_GDK
 		if (gui.in_use)
@@ -993,7 +993,6 @@ 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) || defined(FEAT_IMAGE_GDK)
 #  ifdef FEAT_GUI
@@ -1054,9 +1053,6 @@ 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)
 		{
@@ -1771,7 +1767,7 @@ popup_compute_clip(win_T *wp, popup_clip_T *cl)
     cl->eff_width = cl->eff_left_extra + w + cl->eff_right_extra;
 }
 
-#ifdef FEAT_IMAGE_SIXEL
+#if defined(FEAT_IMAGE_SIXEL) || defined(FEAT_IMAGE_KITTY)
 /*
  * Re-encode the popup's sixel image so its pixel rows fit above the bottom of
  * the screen.  Many sixel-capable terminals scroll the screen when an image
@@ -1896,12 +1892,12 @@ popup_image_backend(void)
     return detected;
 }
 
+# ifdef FEAT_IMAGE_SIXEL
     static void
 popup_encode_image(win_T *wp)
 {
     image_rgb_T		si;
     int			target_w, target_h;
-    int			backend;
     int			cell_x = 8;
     int			cell_y = 16;
     int			crop_top_px, crop_bot_px;
@@ -1912,30 +1908,28 @@ popup_encode_image(win_T *wp)
     if (wp->w_popup_image_data == NULL
 	    || wp->w_popup_image_w <= 0 || wp->w_popup_image_h <= 0)
 	return;
-# ifdef FEAT_GUI
+#  ifdef FEAT_GUI
     // The GUI backend renders the image from the device bitmap, not from a
     // terminal escape sequence -- skip the encoder entirely in that case.
     if (gui.in_use)
 	return;
-# endif
-
-    backend = popup_image_backend();
+#  endif
 
-# if defined(UNIX) || defined(MSWIN) || defined(VMS) || defined(AMIGA)
+#  if defined(UNIX) || defined(MSWIN) || defined(VMS) || defined(AMIGA)
     {
 	struct cellsize cs;
 
 	cs.cs_xpixel = -1;
 	cs.cs_ypixel = -1;
-#  if defined(UNIX) || defined(MSWIN)
+#   if defined(UNIX) || defined(MSWIN)
 	mch_calc_cell_size(&cs);
-#  endif
+#   endif
 	if (cs.cs_xpixel > 0)
 	    cell_x = cs.cs_xpixel;
 	if (cs.cs_ypixel > 0)
 	    cell_y = cs.cs_ypixel;
     }
-# endif
+#  endif
 
     // For "clipwindow" popups, crop the image to the portion that lies inside
     // the host window.  popup_compute_clip() turns topoff/bottomoff/leftclip/
@@ -1953,7 +1947,6 @@ 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;
     }
 
@@ -1981,11 +1974,10 @@ popup_encode_image(win_T *wp)
 		target_h = host_avail_h;
 	}
 
-	if (backend != IMAGE_BACKEND_KITTY)
 	{
 	    // Reserve the bottom-most cell row to keep the sixel image away
 	    // from the edge that triggers scrolling on terminals with sixel-
-	    // scrolling enabled.  Kitty has no such scroll trigger.
+	    // scrolling enabled.
 	    int sixel_cells = Rows - 1 - img_top_row;
 	    int sixel_cap = sixel_cells > 0 ? sixel_cells * cell_y : 0;
 
@@ -2005,19 +1997,17 @@ 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;
     }
+    // already encoded for this geometry
     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
-	    && wp->w_popup_image_seq_zindex == wp->w_zindex)
-	return;	    // already encoded for this geometry and zindex
+	    && wp->w_popup_image_seq_crop_y == crop_top_px)
+	return;
 
     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),
@@ -2052,15 +2042,8 @@ popup_encode_image(win_T *wp)
     si.width = target_w;
     si.height = target_h;
     si.has_alpha = wp->w_popup_image_alpha;
-# ifdef FEAT_IMAGE_KITTY
-    if (backend == IMAGE_BACKEND_KITTY)
-	// 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_zindex);
-    else
-# endif
-	wp->w_popup_image_seq = sixel_encode(&si);
+
+    wp->w_popup_image_seq = sixel_encode(&si);
 
     vim_free(crop_buf);
 
@@ -2072,7 +2055,6 @@ 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
     {
@@ -2081,7 +2063,8 @@ popup_encode_image(win_T *wp)
 	wp->w_popup_image_seq_cells_h = 0;
     }
 }
-#endif
+# endif // FEAT_IMAGE_SIXEL
+#endif // FEAT_IMAGE_SIXEL || FEAT_IMAGE_KITTY
 
 #ifdef FEAT_IMAGE
 /*
@@ -2420,7 +2403,7 @@ popup_adjust_position(win_T *wp)
 #ifdef FEAT_IMAGE_KITTY
 		    // Kitty placements need to be deleted explicitly before
 		    // the popup goes hidden -- see popup_hide().
-		    popup_image_clear_kitty(wp);
+		    popup_image_clear_kitty(wp, false);
 #endif
 #ifdef FEAT_IMAGE_GDK
 		    if (gui.in_use)
@@ -2962,7 +2945,7 @@ popup_adjust_position(win_T *wp)
 	{
 #ifdef FEAT_IMAGE_KITTY
 	    // delete the kitty placement before hiding, like popup_hide()
-	    popup_image_clear_kitty(wp);
+	    popup_image_clear_kitty(wp, false);
 #endif
 #ifdef FEAT_IMAGE_GDK
 	    if (gui.in_use)
@@ -4344,7 +4327,7 @@ popup_hide(win_T *wp)
     // Sixel pixels disappear when the cells underneath are redrawn, but
     // a kitty placement persists until explicitly deleted -- send the
     // delete APC before hiding so the image goes away with the popup.
-    popup_image_clear_kitty(wp);
+    popup_image_clear_kitty(wp, false);
 #endif
 #ifdef FEAT_IMAGE_GDK
     if (gui.in_use)
@@ -4556,7 +4539,7 @@ popup_free(win_T *wp)
 
 #ifdef FEAT_IMAGE_KITTY
     // Remove the kitty placement before win_free_popup() invalidates wp.
-    popup_image_clear_kitty(wp);
+    popup_image_clear_kitty(wp, true);
 #endif
 #ifdef FEAT_IMAGE_GDK
     if (gui.in_use)
@@ -6940,7 +6923,7 @@ popup_image_gui_clip(
 	|| defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO) \
 	|| defined(FEAT_IMAGE_GDK)
     static void
-popup_invalidate_prev_image_rect(win_T *wp, popup_clip_T *cl)
+popup_invalidate_prev_image_rect(win_T *wp, popup_clip_T *cl UNUSED)
 {
     int old_row, old_col, old_cells_w, old_cells_h;
     int new_row = 0, new_col = 0, new_cells_w = 0, new_cells_h = 0;
@@ -6978,7 +6961,7 @@ popup_invalidate_prev_image_rect(win_T *wp, popup_clip_T *cl)
 	new_cells_h = (draw_h + cell_y - 1) / cell_y;
     }
 #  endif
-#  if defined(FEAT_IMAGE_SIXEL) || defined(FEAT_IMAGE_KITTY)
+#  if defined(FEAT_IMAGE_SIXEL)
 #   if defined(FEAT_GUI) && (defined(FEAT_IMAGE_GDI) || defined(FEAT_IMAGE_CAIRO)) \
     || defined(FEAT_IMAGE_GDK)
     else
@@ -7066,6 +7049,7 @@ popup_emit_image(win_T *wp)
     // leaving the image stuck on screen until the cell is overwritten.
     if (wp->w_popup_flags & POPF_HIDDEN)
 	return;
+
     row = wp->w_winrow + wp->w_popup_border[0] + wp->w_popup_padding[0];
     col = wp->w_wincol + wp->w_popup_border[3] + wp->w_popup_padding[3];
 
@@ -7097,14 +7081,92 @@ popup_emit_image(win_T *wp)
 	return;
     }
 # endif
-# if defined(FEAT_IMAGE_SIXEL) || defined(FEAT_IMAGE_KITTY)
-#  ifdef FEAT_GUI
+# ifdef FEAT_GUI
     // GUI builds without a GUI image backend (e.g. Motif) reach here when
     // gui.in_use is true; emitting sixel/kitty escape sequences via out_str()
     // would print them as raw text on the GUI canvas, so bail out.
     if (gui.in_use)
 	return;
+# endif
+# ifdef FEAT_IMAGE_KITTY
+    if (popup_image_backend() == IMAGE_BACKEND_KITTY)
+    {
+	popup_clip_T	cl;
+	int		cell_x = 8;
+	int		cell_y = 16;
+	int		visible_w;
+	int		visible_h;
+	int		src_x, src_y, w, h;
+
+	if (row < 0 || col < 0)
+	    return;
+
+#  if defined(UNIX) || defined(MSWIN) || defined(VMS) || defined(AMIGA)
+	{
+	    struct cellsize cs;
+
+	    cs.cs_xpixel = -1;
+	    cs.cs_ypixel = -1;
+#   if defined(UNIX) || defined(MSWIN)
+	    mch_calc_cell_size(&cs);
+#   endif
+	    if (cs.cs_xpixel > 0)
+		cell_x = cs.cs_xpixel;
+	    if (cs.cs_ypixel > 0)
+		cell_y = cs.cs_ypixel;
+	}
 #  endif
+	popup_compute_clip(wp, &cl);
+
+	visible_w = wp->w_width - cl.clip_left_content - cl.clip_right_content;
+	visible_h = wp->w_height - cl.clip_top_content - cl.clip_bot_content;
+
+	if (visible_w <= 0 || visible_h <= 0)
+	    return;
+
+	row += cl.clip_top_content;
+	col += cl.clip_left_content;
+
+	src_x = cl.clip_left_content * cell_x;
+	src_y = cl.clip_top_content * cell_y;
+	w = wp->w_popup_image_w - src_x - cl.clip_right_content * cell_x;
+	h = wp->w_popup_image_h - src_y - cl.clip_bot_content * cell_y;
+
+	// Clamp to the popup's actual visible cell box in pixels, so the
+	// crop can never claim more cells than the popup has
+	if (w > visible_w * cell_x)
+	    w = visible_w * cell_x;
+	if (h > visible_h * cell_y)
+	    h = visible_h * cell_y;
+
+	if (w <= 0 || h <= 0)
+	    return;
+
+	// Transmit the image to the terminal if it hasn't already
+	if (!wp->w_popup_image_transmit)
+	{
+	    image_rgb_T si;
+
+	    si.data = wp->w_popup_image_data;
+	    si.width = wp->w_popup_image_w;
+	    si.height = wp->w_popup_image_h;
+	    si.has_alpha = wp->w_popup_image_alpha;
+
+	    if (kitty_transmit(&si, wp->w_id) == FAIL)
+		return;
+	    wp->w_popup_image_transmit = true;
+	}
+	kitty_place(wp->w_id, row, col, src_x, src_y, w, h, wp->w_zindex);
+
+	wp->w_popup_image_emit_row = row;
+	wp->w_popup_image_emit_col = col;
+	wp->w_popup_image_emit_cells_w = (w + cell_x - 1) / cell_x;
+	wp->w_popup_image_emit_cells_h = (h + cell_y - 1) / cell_y;
+	wp->w_popup_image_px_dirty = false;
+	return;
+    }
+# endif
+# ifdef FEAT_IMAGE_SIXEL
     if (wp->w_popup_image_seq == NULL)
 	return;
     // For "clipwindow" popups the encoded sequence already covers only the
@@ -7120,18 +7182,6 @@ 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
@@ -7153,33 +7203,28 @@ popup_emit_image(win_T *wp)
     // 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
+    // the terminal instead of skipping them as unchanged.
+
+    for (int rr = row; rr < row + wp->w_popup_image_seq_cells_h; ++rr)
     {
-	for (int rr = row; rr < row + wp->w_popup_image_seq_cells_h; ++rr)
-	{
-	    if (rr < 0 || rr >= screen_Rows)
-		continue;
+	if (rr < 0 || rr >= screen_Rows)
+	    continue;
 
-	    int off_base = LineOffset[rr];
+	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;
+	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;
+	    int off = off_base + cc;
 
-		ScreenLines[off] = ' ';
-		if (enc_utf8 && ScreenLinesUC != NULL)
-		    ScreenLinesUC[off] = 0;
-		ScreenAttrs[off] = -1;
-	    }
+	    ScreenLines[off] = ' ';
+	    if (enc_utf8 && ScreenLinesUC != NULL)
+		ScreenLinesUC[off] = 0;
+	    ScreenAttrs[off] = -1;
 	}
     }
 
@@ -7192,39 +7237,32 @@ 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;
     wp->w_popup_image_px_dirty = false;
 # endif
 }
 
 # ifdef FEAT_IMAGE_KITTY
 /*
- * Send a kitty `a=d,i=<id>` APC to remove the placement made for "wp"
- * by an earlier kitty_encode().  Called when the popup goes away (via
- * popup_hide / popup_close / textprop scrolling out of view), because
- * unlike sixel pixels -- which the next text overwrite clears -- kitty
- * placements persist until explicitly deleted.
+ * Remove the kitty image placement for "wp".  Called when the popup goes
+ * away.
+ * When "del_data" is true the terminal also frees the transmitted image.
  */
     static void
-popup_image_clear_kitty(win_T *wp)
+popup_image_clear_kitty(win_T *wp, bool del_data)
 {
-    char_u  *seq;
-
 #  ifdef FEAT_GUI
     if (gui.in_use)
 	return;
 #  endif
+
     if (wp == NULL || wp->w_popup_image_data == NULL || wp->w_id <= 0)
 	return;
+
     if (popup_image_backend() != IMAGE_BACKEND_KITTY)
 	return;
-    seq = kitty_delete(wp->w_id);
-    if (seq == NULL)
-	return;
-    out_str(seq);
-    out_flush();
-    vim_free(seq);
-    wp->w_popup_image_emit_valid = false;
+    kitty_delete(wp->w_id, del_data);
+    if (del_data)
+	wp->w_popup_image_transmit = false;
 }
 # endif
 
@@ -7251,27 +7289,6 @@ popup_image_clear_gui(win_T *wp)
 }
 # 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.  Only needed for the GUI, where the cursor redraw and other
diff --git a/src/proto/kitty.pro b/src/proto/kitty.pro
index 8c2970c7d..001e79c31 100644
--- a/src/proto/kitty.pro
+++ b/src/proto/kitty.pro
@@ -1,5 +1,6 @@
 /* kitty.c */
-char_u *kitty_encode(image_rgb_T *img, int id, int zindex);
+int kitty_transmit(image_rgb_T *img, int id);
+void kitty_place(int id, int row, int col, int src_x, int src_y, int w, int h, int z);
+void kitty_delete(int id, bool del_data);
 int kitty_probe_parse(char *buf, int n);
-char_u *kitty_delete(int id);
 /* vim: set ft=c : */
diff --git a/src/proto/popupwin.pro b/src/proto/popupwin.pro
index 758695c35..f29aef9d0 100644
--- a/src/proto/popupwin.pro
+++ b/src/proto/popupwin.pro
@@ -60,7 +60,6 @@ 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 b4cf42ee6..662bc0ed7 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -3636,11 +3636,6 @@ 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 fdba4ee83..d41cd182b 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -4277,7 +4277,7 @@ struct window_S
     // visible under the new frame's transparent pixels.
     bool	w_popup_image_px_dirty;
 #  ifdef FEAT_IMAGE_SIXEL
-    char_u	*w_popup_image_seq;	// cached sixel DCS sequence (terminal)
+    char_u	*w_popup_image_seq;	// cached sixel DCS sequence
     int		w_popup_image_seq_w;	// pixel width of cached seq
     int		w_popup_image_seq_h;	// pixel height used for cached seq;
 					// -1 means cache is invalid
@@ -4285,10 +4285,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_KITTY
+    bool	w_popup_image_transmit;	    // If image has been transmitted to
+					    // 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 2033abfa4..a5c38c915 100644
--- a/src/version.c
+++ b/src/version.c
@@ -763,6 +763,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    962,
 /**/
     961,
 /**/

-- 
-- 
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/E1ww3yC-0091tr-0j%40256bit.org.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.