From: Yasuhiro Matsumoto Date: Sat, 13 Jun 2026 15:36:13 +0000 (+0000) Subject: patch 9.2.0628: popup image: wrong overlap layering, kitty laggy X-Git-Tag: v9.2.0628^0 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0dce2b9c8bf93ec0908accf2e450e9f03f8cfb54;p=thirdparty%2Fvim.git 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 Signed-off-by: Christian Brabandt --- diff --git a/src/drawscreen.c b/src/drawscreen.c index 4119cd2449..58b54c3b39 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 825de78caa..183d17e583 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=` so * the resulting placement can later be removed via kitty_delete(). + * "zindex" is sent as `z=` 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 diff --git a/src/popupwin.c b/src/popupwin.c index fdb1d394cd..94ffe89391 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 *)"\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. diff --git a/src/proto/kitty.pro b/src/proto/kitty.pro index 3630f5a6a7..11e2ab2f4a 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 db917e8181..98e3a5f592 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 7ade748b71..de2def9e0f 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 eb0bfe6b3b..8218a80f5b 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 812d66fd2c..90ec1dc438 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 7d1914cd66..1f8e7e1a42 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.