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.

Raspunde prin e-mail lui