From: Yasuhiro Matsumoto Date: Sat, 13 Jun 2026 19:02:29 +0000 (+0000) Subject: patch 9.2.0636: popup image: stale pixels under RGBA animation frames X-Git-Tag: v9.2.0636^0 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1f096d6b8f207673aa29e8ab156f85e97f9c711f;p=thirdparty%2Fvim.git patch 9.2.0636: popup image: stale pixels under RGBA animation frames Problem: Sixel P2=1 transparency and cairo OPERATOR_OVER composite onto the previous emit, so swapping RGBA frames of the same size leaves stale pixels under the new frame's transparent areas. Solution: Track pixel swaps with w_popup_image_px_dirty and repaint the cells under the image before re-emitting. In a terminal the repaint is wrapped in a DECSET 2026 synchronized update so the swap does not flicker; terminals without mode 2026 ignore it (Yasuhiro Matsumoto) closes: #20478 Signed-off-by: Yasuhiro Matsumoto Signed-off-by: Christian Brabandt --- diff --git a/src/popupwin.c b/src/popupwin.c index 183dff6081..ac2158f78b 100644 --- a/src/popupwin.c +++ b/src/popupwin.c @@ -120,6 +120,9 @@ static void redraw_overlapped_opacity_popups(int winrow, int wincol, #ifdef FEAT_IMAGE_KITTY static void popup_image_clear_kitty(win_T *wp); #endif +#ifdef FEAT_IMAGE +static bool popup_image_composites_frames(void); +#endif /* * Get option value for "key", which is "line" or "col". @@ -953,6 +956,7 @@ apply_general_options(win_T *wp, dict_T *dict) wp->w_popup_image_w = 0; wp->w_popup_image_h = 0; wp->w_popup_image_alpha = FALSE; + wp->w_popup_image_px_dirty = false; # ifdef FEAT_IMAGE_SIXEL VIM_CLEAR(wp->w_popup_image_seq); wp->w_popup_image_seq_w = 0; @@ -1016,6 +1020,9 @@ apply_general_options(win_T *wp, dict_T *dict) wp->w_popup_image_h = ih; wp->w_popup_image_alpha = has_alpha; } + // The next redraw must clear the previously emitted frame + // before re-emitting; see popup_invalidate_prev_image_rect(). + wp->w_popup_image_px_dirty = true; # ifdef FEAT_IMAGE_SIXEL VIM_CLEAR(wp->w_popup_image_seq); wp->w_popup_image_seq_h = -1; @@ -1047,8 +1054,14 @@ apply_general_options(win_T *wp, dict_T *dict) // Only the image overlay needs refreshing, which happens from // update_popup_images() at the end of redraw and from the // targeted GUI repair paths for cursor/WM_PAINT damage. - redraw_win_later(wp, - same_size_update ? UPD_VALID : UPD_NOT_VALID); + // Exception: an RGBA frame swap on a backend that + // composites onto the previous emit needs the popup's + // text rows redrawn so the cells under the image are + // repainted before the re-emit, clearing the previous + // frame; see popup_invalidate_prev_image_rect(). + redraw_win_later(wp, same_size_update + && !(has_alpha && popup_image_composites_frames()) + ? UPD_VALID : UPD_NOT_VALID); if (must_redraw < UPD_VALID) must_redraw = UPD_VALID; @@ -2042,6 +2055,79 @@ popup_encode_image(win_T *wp) } #endif +#ifdef FEAT_IMAGE +/* + * Return TRUE when the active image backend composites a new frame on top + * of the previously emitted one instead of replacing it: sixel uses P2=1 + * transparency (unpainted pixels keep their previous on-screen contents) + * and cairo paints with OPERATOR_OVER. For an RGBA image this leaves the + * previous frame visible under the new frame's transparent pixels, so the + * cells underneath must be repainted between frame swaps. Kitty replaces + * the whole placement and GDI blits with SRCCOPY; neither leaves residue. + */ + static bool +popup_image_composites_frames(void) +{ +# ifdef FEAT_GUI + if (gui.in_use) +# ifdef FEAT_IMAGE_CAIRO + // Cairo paints the image with OPERATOR_OVER onto gui.surface, so + // a swapped-in RGBA frame needs the cells repainted underneath. + // The surface is composed off-screen before it is exposed, so the + // repaint cannot flicker there. + return true; +# else + // GDI blits with SRCCOPY: a full replace, no residue. + return false; +# endif +# endif +# ifdef FEAT_IMAGE_SIXEL + // Sixel P2=1 transparency: unpainted pixels keep their previous + // on-screen contents, so the cells under the image must be repainted + // between frame swaps. Kitty replaces the whole placement. + return popup_image_backend() == IMAGE_BACKEND_SIXEL; +# else + return false; +# endif +} + +# if defined(FEAT_IMAGE_SIXEL) || defined(FEAT_IMAGE_KITTY) +// TRUE while a DEC synchronized-update block (DECSET 2026) is open around +// a popup image residue clear + re-emit. +static int popup_sync_update_open = FALSE; + +/* + * Begin a synchronized update before the cells under a popup image are + * repainted for an RGBA frame swap. Without it the terminal can render + * the freshly painted background cells before the new sixel frame + * arrives, making the animation flicker. Terminals that do not support + * mode 2026 ignore it. + */ + static void +popup_sync_update_start(void) +{ + if (popup_sync_update_open) + return; +# ifdef FEAT_GUI + if (gui.in_use) + return; +# endif + out_str((char_u *)"\033[?2026h"); + popup_sync_update_open = TRUE; +} + + static void +popup_sync_update_end(void) +{ + if (!popup_sync_update_open) + return; + out_str((char_u *)"\033[?2026l"); + out_flush(); + popup_sync_update_open = FALSE; +} +# endif +#endif + // Snapshot of the popup window geometry that update_popups() temporarily // mutates so that win_update() draws within the host-window clip rectangle. // Saved before the clip is applied, restored after win_update() returns so @@ -6797,7 +6883,31 @@ popup_invalidate_prev_image_rect(win_T *wp, popup_clip_T *cl) old_col = wp->w_popup_image_emit_col; if (new_row == old_row && new_col == old_col && new_cells_w == old_cells_w && new_cells_h == old_cells_h) - return; + { + bool need_clear = false; + + // Unchanged rectangle: normally the previous emit still matches what + // the popup is about to draw and nothing needs invalidating. + // Exception: the pixel buffer was swapped (animation frame) and the + // image has an alpha channel. Sixel uses P2=1 transparency + // (unpainted pixels keep their previous on-screen contents) and + // cairo composites with OPERATOR_OVER, so the previous frame would + // stay visible under the new frame's transparent pixels. Repaint + // the cells underneath so the residue is cleared before the new + // frame is emitted. Kitty replaces the whole placement and GDI + // blits with SRCCOPY; neither leaves residue. + if (wp->w_popup_image_px_dirty && wp->w_popup_image_alpha) + need_clear = popup_image_composites_frames(); + if (!need_clear) + return; +# if defined(FEAT_IMAGE_SIXEL) || defined(FEAT_IMAGE_KITTY) + // Make the repaint-then-re-emit atomic on terminals that support + // synchronized updates, so the background never shows through + // between animation frames. Closed right after this popup's + // image is re-emitted in update_popups(). + popup_sync_update_start(); +# endif + } for (rr = old_row; rr < old_row + old_cells_h; ++rr) { @@ -6869,6 +6979,7 @@ popup_emit_image(win_T *wp) wp->w_popup_image_emit_col = col; wp->w_popup_image_emit_cells_w = (draw_w + cell_x - 1) / cell_x; wp->w_popup_image_emit_cells_h = (draw_h + cell_y - 1) / cell_y; + wp->w_popup_image_px_dirty = false; return; } # endif @@ -6968,6 +7079,7 @@ popup_emit_image(win_T *wp) 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 } @@ -7722,7 +7834,14 @@ update_popups(void (*win_update)(win_T *wp)) # ifdef FEAT_GUI if (!gui.in_use) # endif + { popup_emit_image(wp); +# if defined(FEAT_IMAGE_SIXEL) || defined(FEAT_IMAGE_KITTY) + // Close the synchronized-update block a residue clear for this + // popup may have opened in popup_invalidate_prev_image_rect(). + popup_sync_update_end(); +# endif + } #endif } diff --git a/src/structs.h b/src/structs.h index 92d4441bea..99123f309a 100644 --- a/src/structs.h +++ b/src/structs.h @@ -4268,6 +4268,12 @@ struct window_S int w_popup_image_emit_col; int w_popup_image_emit_cells_w; int w_popup_image_emit_cells_h; + // TRUE when the pixel buffer was replaced after the last emit. For + // RGBA images the backends that composite onto the previous emit + // instead of replacing it (sixel P2=1 transparency, cairo OPERATOR_OVER) + // must repaint the cells underneath first, or the old frame stays + // 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) int w_popup_image_seq_w; // pixel width of cached seq diff --git a/src/version.c b/src/version.c index e99db49712..d2dcaafb5f 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 636, /**/ 635, /**/