patch 9.2.0636: popup image: stale pixels under RGBA animation frames
Commit:
https://github.com/vim/vim/commit/1f096d6b8f207673aa29e8ab156f85e97f9c711f
Author: Yasuhiro Matsumoto <[email protected]>
Date: Sat Jun 13 19:02:29 2026 +0000
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 <[email protected]>
Signed-off-by: Christian Brabandt <[email protected]>
diff --git a/src/popupwin.c b/src/popupwin.c
index 183dff608..ac2158f78 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 *)" [?2026h");
+ popup_sync_update_open = TRUE;
+}
+
+ static void
+popup_sync_update_end(void)
+{
+ if (!popup_sync_update_open)
+ return;
+ out_str((char_u *)" [?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 92d4441be..99123f309 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 e99db4971..d2dcaafb5 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,
/**/
--
--
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/E1wYTot-00DSjz-QC%40256bit.org.