]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0828: GTK4: hardware rendering can be improved v9.2.0828
authorFoxe Chen <chen.foxe@gmail.com>
Wed, 22 Jul 2026 09:31:53 +0000 (09:31 +0000)
committerChristian Brabandt <cb@256bit.org>
Wed, 22 Jul 2026 09:31:53 +0000 (09:31 +0000)
Problem:  GTK4: hardware rendering can be improved
Solution: Refactor code (Foxe Chen)

This change does the following:

- Make each cell have its own array of glyphs, that represent it. Before
  glyphs were stored per span of drawn text.
- Background color, invert blend is now stored as a single row pixel
  buffer, that is scaled up to size when rendering. No need to have logic
  for merging visual attributes together.
- Underlines, undercurls, and strikethroughs use a mask outline, and a
  pixel buffer (similar to the above) that is opaque at parts where there
  are under decorations. This means having 100 individual undercurls that
  are all separated by spaces in a row will only result in a single mask
  node.
- Render nodes are cached per row, because creating render nodes are
  cheap. The actual heavy stuff would be shaping text into glyphs, which
  are cached per cell.
- Use PangoAttrFontFeatures to handle guiligatures option, instead of
  manually splitting draw calls, which is buggy and complex (some complex
  unicode characters are not rendered correctly).
- Render block cursor separately, this prevents a full redraw of the text
  for the row the cursor is on (only if glyphs in cell underneath cursor
  did not change).

Complex unicode characters that bleed outside of the cell should look
better, since the background is always rendered before all glyphs,
meaning they are not clipped off.

fixes:  #13435
closes: #20674

Signed-off-by: Foxe Chen <chen.foxe@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
src/gui.c
src/gui_gtk4.c
src/gui_gtk4_da.c
src/gui_gtk4_da.h
src/netbeans.c
src/proto/gui_gtk4.pro
src/version.c

index 7b08f786ff3b331dfa3a1fdc84fd39ab1bba7612..3457c88f13d1af50acce90b5133dc24e4255a922 100644 (file)
--- a/src/gui.c
+++ b/src/gui.c
@@ -24,7 +24,9 @@ static void set_guifontwide(char_u *font_name);
 static void gui_check_pos(void);
 static void gui_reset_scroll_region(void);
 static void gui_outstr(char_u *, int);
+#ifndef USE_GTK4_SNAPSHOT
 static int gui_screenchar(int off, int flags, guicolor_T fg, guicolor_T bg, int back);
+#endif
 static int gui_outstr_nowrap(char_u *s, int len, int flags, guicolor_T fg, guicolor_T bg, int back);
 static void gui_delete_lines(int row, int count);
 static void gui_insert_lines(int row, int count);
@@ -1354,13 +1356,28 @@ gui_update_cursor(
      */
     if (!gui.in_focus)
     {
+#ifdef USE_GTK4_SNAPSHOT
+       gui_gtk4_draw_cursor(cbg, cfg, -1, -1);
+#else
        gui_mch_draw_hollow_cursor(cbg);
+#endif
        return;
     }
 
+#ifdef USE_GTK4_SNAPSHOT
+    // Make sure that character underneath is drawn again in case it is part of
+    // a ligature.
+    gui_redraw_block(gui.row, gui.col, gui.row, gui.col, GUI_MON_NOCLEAR);
+    // gui_redraw_block() will invalidate the cursor
+    gui.cursor_is_valid = true;
+#endif
+
     old_hl_mask = gui.highlight_mask;
     if (shape->shape == SHAPE_BLOCK)
     {
+#ifdef USE_GTK4_SNAPSHOT
+       gui_gtk4_draw_cursor(cbg, cfg, 0, 0);
+#else
        /*
         * Draw the text character with the cursor colors.      Use the
         * character attributes plus the cursor attributes.
@@ -1368,6 +1385,7 @@ gui_update_cursor(
        gui.highlight_mask = (cattr | attr);
        (void)gui_screenchar(LineOffset[gui.row] + gui.col,
                GUI_MON_IS_CURSOR | GUI_MON_NOCLEAR, cfg, cbg, 0);
+#endif
     }
     else
     {
@@ -1406,15 +1424,18 @@ gui_update_cursor(
            }
 #endif
        }
+#ifdef USE_GTK4_SNAPSHOT
+       gui_gtk4_draw_cursor(cbg, cfg, cur_width, cur_height);
+#else
        gui_mch_draw_part_cursor(cur_width, cur_height, cbg);
+#endif
 #if defined(FEAT_RIGHTLEFT)
        if (col_off)
            --gui.col;
 #endif
 
-       // Doesn't seem to work for MSWindows. Not necessary when using
-       // GtkSnapshot, because everything is drawn in order in the snapshot
-       // vfunc.
+       // Doesn't seem to work for MSWindows. We call gui_redraw_block() above
+       // for GtkSnapshot.
 #if !defined(FEAT_GUI_MSWIN) && !defined(USE_GTK4_SNAPSHOT)
        gui.highlight_mask = ScreenAttrs[LineOffset[gui.row] + gui.col];
        (void)gui_screenchar(LineOffset[gui.row] + gui.col,
@@ -2212,6 +2233,7 @@ gui_outstr(char_u *s, int len)
     }
 }
 
+#ifndef USE_GTK4_SNAPSHOT
 /*
  * Output one character (may be one or two display cells).
  * Caller must check for valid "off".
@@ -2248,6 +2270,7 @@ gui_screenchar(
            enc_dbcs ? (*mb_ptr2len)(ScreenLines + off) : 1,
                                                         flags, fg, bg, back);
 }
+#endif
 
 #ifdef FEAT_GUI_GTK
 /*
index a194edd76099bbb406d2333b9a7e216ea0992da1..b29b180d6356ca0e3ddcb2505e197655aabce4bb 100644 (file)
@@ -1439,11 +1439,16 @@ gui_gtk4_update_size(void)
 }
 
 # ifdef FEAT_NETBEANS_INTG
-    cairo_t *
-gui_gtk4_get_multisign_context(int x, int y, int w, int h)
+    void
+gui_gtk4_add_multisign(
+       cairo_surface_t *surf,
+       int             row,
+       int             col,
+       int             width,
+       int             height)
 {
-    return vim_draw_area_get_multisign_cairo(
-           VIM_DRAW_AREA(gui.drawarea), x, y, w, h);
+    vim_draw_area_add_multisign(VIM_DRAW_AREA(gui.drawarea), surf, row, col,
+           width, height);
 }
 # endif
 #else // USE_GTK4_SNAPSHOT
@@ -1762,13 +1767,24 @@ gui_mch_insert_lines(int row, int num_lines)
     gtk_widget_queue_draw(gui.drawarea);
 }
 
+#if defined(USE_GTK4_SNAPSHOT) || defined(PROTO)
+/*
+ * See vim_draw_area_set_cursor() for more information
+ */
+    void
+gui_gtk4_draw_cursor(guicolor_T bg, guicolor_T fg, int w, int h)
+{
+    gui_mch_set_bg_color(bg);
+    gui_mch_set_fg_color(fg);
+    vim_draw_area_set_cursor(VIM_DRAW_AREA(gui.drawarea), w, h);
+    gtk_widget_queue_draw(gui.drawarea);
+}
+#endif
+
+#if !defined(USE_GTK4_SNAPSHOT) || defined(PROTO)
     void
 gui_mch_draw_hollow_cursor(guicolor_T color)
 {
-#ifdef USE_GTK4_SNAPSHOT
-    gui_mch_set_fg_color(color);
-    vim_draw_area_set_hollow_cursor(VIM_DRAW_AREA(gui.drawarea));
-#else
     cairo_t *cr;
     int i = 1;
 
@@ -1788,7 +1804,6 @@ gui_mch_draw_hollow_cursor(guicolor_T color)
            i * gui.char_width - 1, gui.char_height - 1);
     cairo_stroke(cr);
     cairo_destroy(cr);
-#endif
 
     gtk_widget_queue_draw(gui.drawarea);
 }
@@ -1796,10 +1811,6 @@ gui_mch_draw_hollow_cursor(guicolor_T color)
     void
 gui_mch_draw_part_cursor(int w, int h, guicolor_T color)
 {
-#ifdef USE_GTK4_SNAPSHOT
-    gui_mch_set_fg_color(color);
-    vim_draw_area_set_part_cursor(VIM_DRAW_AREA(gui.drawarea), w, h);
-#else
     cairo_t *cr;
 
     if (gui.surface == NULL)
@@ -1818,10 +1829,10 @@ gui_mch_draw_part_cursor(int w, int h, guicolor_T color)
            w, h);
     cairo_fill(cr);
     cairo_destroy(cr);
-#endif
 
     gtk_widget_queue_draw(gui.drawarea);
 }
+#endif
 
     void
 gui_mch_flash(int msec)
@@ -3517,12 +3528,13 @@ gui_mch_destroy_sign(void *sign)
  * ============================================================
  */
 
+#ifndef USE_GTK4_SNAPSHOT
 /*
  * Ligature and text drawing support.
  * Ported from gui_gtk_x11.c (GTK3) to support 'guiligatures' in GTK4.
  */
 
-#define INSERT_PANGO_ATTR(Attribute, AttrList, Start, End)  \
+# define INSERT_PANGO_ATTR(Attribute, AttrList, Start, End)  \
     G_STMT_START{                                          \
        PangoAttribute *tmp_attr_;                          \
        tmp_attr_ = (Attribute);                            \
@@ -3651,7 +3663,6 @@ setup_zero_width_cluster(PangoItem *item, PangoGlyphInfo *glyph,
        glyph->geometry.x_offset = -width + MAX(0, width - ink_rect.width) / 2;
 }
 
-#ifndef USE_GTK4_SNAPSHOT
 /*
  * Draw a single glyph string segment: background, foreground, and fake bold.
  */
@@ -3737,7 +3748,6 @@ draw_under(int flags, int row, int col, int cells, cairo_t *cr)
        cairo_stroke(cr);
     }
 }
-#endif
 
 /*
  * Draw a string of characters on the screen.
@@ -3757,21 +3767,12 @@ gui_gtk_draw_string_ext(
     PangoGlyphString   *glyphs;
     int                        column_offset = 0;
     int                        i;
-#ifdef USE_GTK4_SNAPSHOT
-    gboolean           s_alloced = FALSE;
-#else
     GdkRectangle       area;
     cairo_t            *cr;
-#endif
 
-    if (gui.text_context == NULL
-#ifndef USE_GTK4_SNAPSHOT
-           || gui.surface == NULL
-#endif
-           )
+    if (gui.text_context == NULL || gui.surface == NULL)
        return len;
 
-#ifndef USE_GTK4_SNAPSHOT
     // Restrict all drawing to the current screen line.
     area.x = gui.border_offset;
     area.y = FILL_Y(row);
@@ -3781,7 +3782,6 @@ gui_gtk_draw_string_ext(
     cr = cairo_create(gui.surface);
     cairo_rectangle(cr, area.x, area.y, area.width, area.height);
     cairo_clip(cr);
-#endif
 
     glyphs = pango_glyph_string_new();
 
@@ -3806,12 +3806,7 @@ gui_gtk_draw_string_ext(
            glyphs->log_clusters[i] = i;
        }
 
-#ifdef USE_GTK4_SNAPSHOT
-       vim_draw_area_add_glyphs(VIM_DRAW_AREA(gui.drawarea), row, col, len,
-               flags, gui.ascii_font, glyphs);
-#else
        draw_glyph_string(row, col, len, flags, gui.ascii_font, glyphs, cr);
-#endif
 
        column_offset = len;
     }
@@ -3827,17 +3822,8 @@ not_ascii:;
        // Safety check: pango crashes with invalid utf-8.
        if (!utf_valid_string(s, s + len))
        {
-#ifdef USE_GTK4_SNAPSHOT
-           // vim_draw_area_add_glyphs() also handles under decorations. Make
-           // "str" a string of spaces so that under decorations are still
-           // applied.
-           s = g_malloc(len);
-           memset(s, ' ', len);
-           s_alloced = TRUE;
-#else
            column_offset = len;
            goto skipitall;
-#endif
        }
 
        cluster_width = PANGO_SCALE * gui.char_width;
@@ -3929,14 +3915,8 @@ not_ascii:;
                }
            }
 
-#ifdef USE_GTK4_SNAPSHOT
-           vim_draw_area_add_glyphs(VIM_DRAW_AREA(gui.drawarea),
-                   row, col + column_offset, item_cells,
-                   flags, item->analysis.font, glyphs);
-#else
            draw_glyph_string(row, col + column_offset, item_cells,
-                             flags, item->analysis.font, glyphs, cr);
-#endif
+                   flags, item->analysis.font, glyphs, cr);
 
            pango_item_free(item);
 
@@ -3946,23 +3926,18 @@ not_ascii:;
        pango_attr_list_unref(attr_list);
     }
 
-#ifdef USE_GTK4_SNAPSHOT
-    if (s_alloced)
-       g_free(s);
-#else
 skipitall:
     draw_under(flags, row, col, column_offset, cr);
     cairo_destroy(cr);
-#endif
 
     pango_glyph_string_free(glyphs);
 
-
     if (gui.drawarea != NULL)
        gtk_widget_queue_draw(gui.drawarea);
 
     return column_offset;
 }
+#endif // !USE_GTK4_SNAPSHOT
 
 /*
  * Draw a string of characters on the screen using the current font and colors.
@@ -3974,6 +3949,17 @@ skipitall:
     int
 gui_gtk_draw_string(int row, int col, char_u *s, int len, int flags)
 {
+#ifdef USE_GTK4_SNAPSHOT
+    int cells;
+
+    cells = vim_draw_area_draw_string(VIM_DRAW_AREA(gui.drawarea),
+           row, col, s, len, flags);
+
+    if (gui.drawarea != NULL)
+       gtk_widget_queue_draw(gui.drawarea);
+
+    return cells;
+#else
     char_u     *conv_buf = NULL;
     int                convlen;
     int                len_sum;
@@ -3986,11 +3972,7 @@ gui_gtk_draw_string(int row, int col, char_u *s, int len, int flags)
     int                is_utf8;
     char_u     backup_ch;
 
-    if (gui.text_context == NULL
-#ifndef USE_GTK4_SNAPSHOT
-           || gui.surface == NULL
-#endif
-           )
+    if (gui.text_context == NULL || gui.surface == NULL)
        return len;
 
     if (output_conv.vc_type != CONV_NONE)
@@ -4090,6 +4072,7 @@ gui_gtk_draw_string(int row, int col, char_u *s, int len, int flags)
     }
     vim_free(conv_buf);
     return len_sum;
+#endif
 }
 
     int
index 8db224ea67868ed4b71853b302f9011d46f6ff3f..4a7211fd70773f613fd507d0b73855c77fd67ae6 100644 (file)
 #include <gtk/gtk.h>
 #include "gui_gtk4_da.h"
 
-#define DRAW_NODE_DIRTY 1   // Draw node is dirty
-#define DRAW_NODE_NOBG 2    // Don't create background node
-#define DRAW_NODE_NOINK 4   // Draw node has no ink
-#define DRAW_NODE_UNDER 8   // Has under decorations (for convenience)
-#define DRAW_NODE_CLIP 16   // Text node should be clipped to draw node bounds.
 
+#ifdef FEAT_IMAGE_GDK
+/*
+ * Struct containing information about an image. This is designed to map well
+ * with how Vim handles the kitty graphics protocol.
+ */
 typedef struct
 {
-    int refcount;
-
-    PangoGlyphInfo  *glyphs;
-    int                    n_glyphs;
-    char_u         dnode_flags; // DRAW_NODE_* flags
-    GskRenderNode   *node;  // This is either a text node, or a container node
-                           // (if there is more than one node).
-
-    PangoFont  *font;
-    GdkRGBA    fg_color;
-    GdkRGBA    bg_color;
-    GdkRGBA    sp_color;
-    int                flags;      // DRAW_* flags
+    int id;
+    int zindex;
+    GskRenderNode *node; // Cached clip node, which has the texture node as its
+                        // child. May be NULL
+} DrawImage;
+#endif
 
-    int start_col;
-    int n_cells;
-} DrawNode;
+#if defined(FEAT_NETBEANS_INTG) || defined(FEAT_SIGN_ICONS)
+/*
+ * Used for sign icons and netbeans multisign indicator. It is in a DrawGlyphs
+ * so that it can move/be manipulated with the text.
+ */
+typedef struct
+{
+    GdkTexture *texture;
+    int                width;
+    int                height;
+} DrawSign;
+#endif
 
-#define END_COL(dn) ((dn)->start_col + (dn)->n_cells - 1)
-#define HAS_INK(r) ((r)->width != 0 || (r)->height != 0)
+/*
+ * Temporary struct when drawing string representing visual attributes of a cell
+ */
+typedef struct
+{
+    uint32_t   bg_color;
+    uint32_t   fg_color;   // Only used for underlines
+    uint32_t   sp_color;
+    int                draw_flags; // DRAW_* flags, only DRAW_UNDERL, DRAW_UNDERC, and
+                           // DRAW_STRIKE are used.
+} DrawAttr;
 
 /*
- * Each cell holds its own reference to a draw node if any. A draw node may span
- * multiple cells, which represents how many cells it takes up on screen.
+ * Represents the glyphs of a single cell.
  */
 typedef struct
 {
-    DrawNode   *dnode; // May be NULL
-    gboolean   invert; // If this cell is inverted
+    int refcount;
+
+    uint32_t   fg_color;
+    PangoFont  *font;
+    // If "font" is NULL, "dsign" is used, otherwise "glyphs" is used.
+    union
+    {
+       PangoGlyphInfo  *glyphs; // May be NULL
+#if defined(FEAT_NETBEANS_INTG) || defined(FEAT_SIGN_ICONS)
+       DrawSign        *dsign;
+#endif
+    };
+    int n_glyphs;
+} DrawGlyphs;
+
+typedef struct
+{
+    DrawGlyphs *dglyphs;
 } DrawCell;
 
-#ifdef FEAT_IMAGE_GDK
+typedef enum
+{
+    DRAW_LAYER_BACKGROUND,  // Background color, buf stores bg color
+    DRAW_LAYER_TEXT,       // Text glyphs + underline, buf stores fg color (for
+                           // underline)
+    DRAW_LAYER_SPECIAL,            // Undercurls and strikethrough, buf store special
+                           // color
+    DRAW_LAYER_OVERLAY,            // Invert blend, buf stores white color
+    N_DRAW_LAYERS
+} DrawLayerType;
+
+typedef struct
+{
+    GskRenderNode   *node;
+    gboolean       dirty;
+
+    // RGBA buffer, pixel size is number of cells in the row. Note that for the
+    // special layer, it is twice the size, first part is for undercurl, second
+    // part for strikethrough.
+    uint32_t *buf;
+} DrawLayer;
+
+#define UNDERC_OFFSET(n_cols) (0)
+#define STRIKE_OFFSET(n_cols) (n_cols)
+
+typedef struct
+{
+    DrawLayer  dlayers[N_DRAW_LAYERS];
+    DrawCell   *cells;
+    int                n_cells; // For convenience
+    int                row;     // For convenience
+
+    // Pre generated mask of of under decorations
+    GskRenderNode *underl_mask;
+    GskRenderNode *underc_mask;
+    GskRenderNode *strike_mask;
+} DrawRow;
+
+#define INSERT_PANGO_ATTR(Attribute, AttrList, Start, End) \
+    do { \
+       PangoAttribute *tmp_attr_; \
+       tmp_attr_ = (Attribute); \
+       tmp_attr_->start_index = (Start); \
+       tmp_attr_->end_index = (End); \
+       pango_attr_list_insert((AttrList), tmp_attr_); \
+    } while (FALSE)
+#define CELLS2PANGO(c) ((c) * gui.char_width * PANGO_SCALE)
+
 /*
- * Struct containing information about an image. This is designed to map well
- * with how Vim handles the kitty graphics protocol.
+ * Attributes of cursor
  */
 typedef struct
 {
-    int id;
-    int zindex;
-    GskRenderNode *node; // Cached clip node, which has the texture node as its
-                        // child. May be NULL
-} DrawImage;
-#endif
+    gboolean draw; // If cursor should be drawn
+    int width;
+    int height;
+    GdkRGBA bg_color;
+    GdkRGBA fg_color;
+} DrawCursor;
 
 struct _VimDrawArea
 {
     GtkWidget parent;
 
-    DrawCell   *cells; // May be NULL, always check!
-    int         n_rows;
-    int                n_cols;
+    DrawRow *rows;
 
-    int                bleed_right;
+    int n_rows;
+    int        n_cols;
 
-    // Used for hollow and part style cursors. For the block cursor, that is
-    // simply rendered as a cell using vim_draw_area_add_glyphs(). May be NULL.
-    GskRenderNode *cursor_node;
+    int        bleed_right;
 
-#if defined(FEAT_SIGN_ICONS)
-    // Queue of sign icon render nodes. Icons at the end of the queue are drawn
-    // ontop of earlier ones.
-    GQueue *signs;
-#endif
+    // Used for snapshot vfunc call when rendering text, kept around so there is
+    // no need to allocate a new one each time.
+    GArray     *glyph_buf;
+    GPtrArray  *node_buf;
 
-#ifdef FEAT_NETBEANS_INTG
-    // Cairo render node for multi sign indicator for Netbeans. May be NULL
-    GskRenderNode *multisign_node;
-#endif
+    DrawCursor cursor;
 
 #ifdef FEAT_IMAGE_GDK
     // Queue of DrawImage structs. Sorted in ascending order of zindex, so that
@@ -99,30 +164,29 @@ struct _VimDrawArea
 #endif
 };
 
-#define GET_ROW(da, n) ((da)->cells + (da)->n_cols * (n))
-
-G_DEFINE_TYPE(VimDrawArea, vim_draw_area, GTK_TYPE_WIDGET)
-
-#ifdef FEAT_IMAGE_GDK
 static void draw_image_free(DrawImage *dimg);
-#endif
+static void draw_row_init(DrawRow *drow, int row, int cols);
+static void draw_row_clear(DrawRow *drow);
+static void draw_row_dirty_layer(DrawRow *drow, DrawLayerType dlayer_t);
 static void vim_draw_area_snapshot(GtkWidget *widget, GtkSnapshot *snapshot);
 
+static GdkRGBA white_rgba = {1, 1, 1, 1};
+
+G_DEFINE_TYPE(VimDrawArea, vim_draw_area, GTK_TYPE_WIDGET)
+
     static void
 vim_draw_area_finalize(GObject *obj)
 {
     VimDrawArea *self = VIM_DRAW_AREA(obj);
 
-    // "multisign_node" and "cursor_node" will be freed in
-    // vim_draw_area_clear_block().
     vim_draw_area_clear(self);
+    for (int r = 0; r < self->n_rows; r++)
+       draw_row_clear(self->rows + r);
+    g_free(self->rows);
+
+    g_array_free(self->glyph_buf, TRUE);
+    g_ptr_array_free(self->node_buf, TRUE);
 
-    g_free(self->cells);
-#ifdef FEAT_SIGN_ICONS
-    // vim_draw_area_clear_block() should have removed all the sign icons
-    assert(g_queue_is_empty(self->signs));
-    g_queue_free(self->signs);
-#endif
 #ifdef FEAT_IMAGE_GDK
     g_queue_free_full(self->images, (GDestroyNotify)draw_image_free);
 #endif
@@ -142,18 +206,19 @@ vim_draw_area_class_init(VimDrawAreaClass *class)
 
     // Add a layout manager so it can handle child popovers
     gtk_widget_class_set_layout_manager_type(widget_class, GTK_TYPE_BIN_LAYOUT);
-
 }
 
     static void
 vim_draw_area_init(VimDrawArea *self)
 {
-#ifdef FEAT_SIGN_ICONS
-    self->signs = g_queue_new();
-#endif
+    self->bleed_right = -1;
 #ifdef FEAT_IMAGE_GDK
     self->images = g_queue_new();
 #endif
+    self->glyph_buf = g_array_new(FALSE, FALSE, sizeof(PangoGlyphInfo));
+    self->node_buf = g_ptr_array_new_with_free_func(
+           (GDestroyNotify)gsk_render_node_unref);
+
 }
 
     GtkWidget *
@@ -168,179 +233,730 @@ vim_draw_area_new(void)
     void
 vim_draw_area_set_size(VimDrawArea *self, int rows, int cols)
 {
-    if (self->cells != NULL && self->n_rows == rows && self->n_cols == cols)
+    if (self->rows != NULL && self->n_rows == rows && self->n_cols == cols)
        return;
     if (rows == 0 || cols == 0)
        return;
 
     vim_draw_area_clear(self);
 
+    for (int r = 0; r < self->n_rows; r++)
+       draw_row_clear(self->rows + r);
+    self->rows = g_realloc_n(self->rows, rows, sizeof(DrawRow));
+    for (int r = 0; r < rows; r++)
+       draw_row_init(self->rows + r, r, cols);
+
     self->n_rows = rows;
     self->n_cols = cols;
-    self->cells = g_realloc_n(self->cells, rows * cols, sizeof(DrawCell));
-    memset(self->cells, 0, rows * (sizeof(DrawCell) * cols));
 }
 
-    static void
-node_unref(GskRenderNode *node)
+/*
+ * Convert the GdkRGBA to a packed uint32_t.
+ */
+    static uint32_t
+rgba_to_u32(const GdkRGBA *rgba)
 {
-    if (node != NULL)
-       gsk_render_node_unref(node);
+    union { uint32_t val; uint8_t bytes[4]; } color;
+
+    color.bytes[0] = (uint8_t)(CLAMP(rgba->red,   0.0, 1.0) * 255.0 + 0.5);
+    color.bytes[1] = (uint8_t)(CLAMP(rgba->green, 0.0, 1.0) * 255.0 + 0.5);
+    color.bytes[2] = (uint8_t)(CLAMP(rgba->blue,  0.0, 1.0) * 255.0 + 0.5);
+    color.bytes[3] = (uint8_t)(CLAMP(rgba->alpha, 0.0, 1.0) * 255.0 + 0.5);
+
+    return color.val;
+}
+
+/*
+ * Convert the 32 bit RGBA value to a GdkRGBA
+ */
+    static GdkRGBA
+u32_to_rgba(uint32_t val)
+{
+    union { uint32_t val; uint8_t bytes[4]; } packed;
+    GdkRGBA color;
+
+    packed.val = val;
+    color.red   = packed.bytes[0] / 255.0;
+    color.green = packed.bytes[1] / 255.0;
+    color.blue  = packed.bytes[2] / 255.0;
+    color.alpha = packed.bytes[3] / 255.0;
+    return color;
 }
 
 /*
- * Return TRUE if "glyphs" take up space (not entirely whitespace).
+ * Return TRUE if "rgb" equals "rgba", ignoring the alpha channel.
  */
     static gboolean
-glyphs_has_ink(PangoFont *font, const PangoGlyphInfo *glyphs, int n_glyphs)
+color_equal_u32(guicolor_T rgb, uint32_t rgba)
 {
-    for (int i = 0; i < n_glyphs; i++)
+    union { uint32_t val; uint8_t bytes[4]; } packed;
+    guicolor_T res;
+
+    packed.val = rgba;
+    res = ((guicolor_T)packed.bytes[0] << 16) |
+       ((guicolor_T)packed.bytes[1] << 8)  |
+       packed.bytes[2];
+    return res == rgb;
+}
+
+/*
+ * Checks if a RGBA pixel buffer is fully transparent.
+ */
+    static inline gboolean
+rgba_is_transparent(const uint32_t *buf, int n_pixels)
+{
+    // Could also compare 2 pixels at once using a 64 bit integer, but not sure
+    // if that might cause alignment issues...
+    for (int i = 0; i < n_pixels; i++)
+       if (buf[i] != 0)
+           return FALSE;
+    return TRUE;
+}
+
+/*
+ * Return TRUE if "glyph" has ink (not whitespace).
+ */
+    static gboolean
+glyph_has_ink(PangoFont *font, PangoGlyphInfo glyph)
+{
+    PangoRectangle ink;
+
+    pango_font_get_glyph_extents (font, glyph.glyph, &ink, NULL);
+
+    return ink.width > 0 && ink.height > 0;
+}
+
+/*
+ * Apply the 'guifontwide' font to double-width characters in the string.
+ */
+    static void
+apply_wide_font_attr(char_u *s, int len, PangoAttrList *attr_list)
+{
+    char_u  *start = NULL;
+    char_u  *p;
+    int            uc;
+
+    for (p = s; p < s + len; p += utf_byte2len(*p))
     {
-       PangoRectangle glyph_ink;
+       uc = utf_ptr2char(p);
 
-       pango_font_get_glyph_extents (font, glyphs[i].glyph, &glyph_ink, NULL);
+       if (start == NULL)
+       {
+           if (uc >= 0x80 && utf_char2cells(uc) == 2)
+               start = p;
+       }
+       else if (uc < 0x80
+               || (utf_char2cells(uc) != 2 && !utf_iscomposing(uc)))
+       {
+           INSERT_PANGO_ATTR(pango_attr_font_desc_new(gui.wide_font),
+                   attr_list, start - s, p - s);
+           start = NULL;
+       }
+    }
 
-       if (HAS_INK(&glyph_ink))
-           return TRUE;
+    if (start != NULL)
+       INSERT_PANGO_ATTR(pango_attr_font_desc_new(gui.wide_font),
+               attr_list, start - s, len);
+}
+
+/*
+ * Count the number of display cells occupied by a glyph cluster.
+ */
+    static int
+count_cluster_cells(
+       char_u              *s,
+       PangoItem           *item,
+       PangoGlyphString    *glyphs,
+       int                 i,
+       int                 *cluster_width,
+       int                 *last_glyph_rbearing)
+{
+    char_u  *p;
+    int            next;
+    int            start, end;
+    int            width;
+    int            uc;
+    int            cellcount = 0;
+
+    width = glyphs->glyphs[i].geometry.width;
+
+    for (next = i + 1; next < glyphs->num_glyphs; ++next)
+    {
+       if (glyphs->glyphs[next].attr.is_cluster_start)
+           break;
+       else if (glyphs->glyphs[next].geometry.width > width)
+           width = glyphs->glyphs[next].geometry.width;
     }
-    return FALSE;
+
+    start = item->offset + glyphs->log_clusters[i];
+    end   = item->offset + ((next < glyphs->num_glyphs) ?
+           glyphs->log_clusters[next] : item->length);
+
+    for (p = s + start; p < s + end; p += utf_byte2len(*p))
+    {
+       uc = utf_ptr2char(p);
+       if (uc < 0x80)
+           ++cellcount;
+       else if (!utf_iscomposing(uc))
+           cellcount += utf_char2cells(uc);
+    }
+
+    if (last_glyph_rbearing != NULL
+           && cellcount > 0 && next == glyphs->num_glyphs)
+    {
+       PangoRectangle ink_rect;
+
+       pango_font_get_glyph_extents(item->analysis.font,
+               glyphs->glyphs[i].glyph,
+               &ink_rect, NULL);
+
+       if (PANGO_RBEARING(ink_rect) > 0)
+           *last_glyph_rbearing = PANGO_RBEARING(ink_rect);
+    }
+
+    if (cellcount > 0)
+       *cluster_width = width;
+
+    return cellcount;
 }
 
 /*
- * Realloc "glyphs" to "n_glyphs" and return the new reallocated pointer.
+ * Handle combining characters that form a zero-width cluster.
  */
-    static PangoGlyphInfo *
-glyphs_resize(PangoGlyphInfo *glyphs, int n_glyphs)
+    static void
+setup_zero_width_cluster(
+       PangoItem       *item,
+       PangoGlyphInfo  *glyph,
+       int             last_cellcount,
+       int             last_cluster_width,
+       int             last_glyph_rbearing)
 {
-    return g_realloc_n(glyphs, n_glyphs, sizeof(PangoGlyphInfo));
+    PangoRectangle  ink_rect;
+    PangoRectangle  logical_rect;
+    int                    width;
+
+    width = last_cellcount * gui.char_width * PANGO_SCALE;
+    glyph->geometry.x_offset = -width + MAX(0, width - last_cluster_width) / 2;
+    glyph->geometry.width = 0;
+
+    pango_font_get_glyph_extents(item->analysis.font,
+           glyph->glyph,
+           &ink_rect, &logical_rect);
+    if (ink_rect.x < 0)
+    {
+       glyph->geometry.x_offset += last_glyph_rbearing;
+       glyph->geometry.y_offset  = logical_rect.height
+           - (gui.char_height - p_linespace) * PANGO_SCALE;
+    }
+    else
+       glyph->geometry.x_offset = -width + MAX(0, width - ink_rect.width) / 2;
+}
+
+#ifdef FEAT_IMAGE_GDK
+    static void
+draw_image_free(DrawImage *dimg)
+{
+    gsk_render_node_unref(dimg->node);
+    g_free(dimg);
 }
+#endif
+
 
+#if defined(FEAT_NETBEANS_INTG) || defined(FEAT_SIGN_ICONS)
 /*
- * Return TRUE if "bg" is the same as the default background color.
+ * Create a new draw sign icon with the given texture.
  */
-    static gboolean
-color_is_default_bg(const GdkRGBA *bg)
+    static DrawSign *
+draw_sign_new(GdkTexture *texture, int width, int height)
 {
-    guicolor_T bgc = ((guicolor_T)(bg->red * 255) << 16)
-       | ((guicolor_T)(bg->green * 255) << 8)
-       |  (guicolor_T)(bg->blue * 255);
-    return bgc == gui.back_pixel;
+    DrawSign *dsign = g_new0(DrawSign, 1);
+
+    dsign->texture = g_object_ref(texture);
+    dsign->width = width;
+    dsign->height = height;
+
+    return dsign;
+}
+
+    static void
+draw_sign_free(DrawSign *dsign)
+{
+    g_object_unref(dsign->texture);
+    g_free(dsign);
+}
+#endif
+
+    static void
+draw_attr_init(
+       DrawAttr        *dattr,
+       const GdkRGBA   *bg_color,
+       const GdkRGBA   *fg_color,
+       const GdkRGBA   *sp_color,
+       int             draw_flags)
+{
+    dattr->bg_color = rgba_to_u32(bg_color);
+    dattr->fg_color = rgba_to_u32(fg_color);
+    dattr->sp_color = rgba_to_u32(sp_color);
+    dattr->draw_flags = draw_flags & (DRAW_UNDERL | DRAW_UNDERC | DRAW_STRIKE);
 }
 
 /*
- * Convert the given cell offset into an index in the "glyphs" array.
+ * Apply the draw attr to the row for the given col/cell. Only dirty layers that
+ * have actually changed.
  */
-    static int
-cell_offset_to_glyph(const PangoGlyphInfo *glyphs, int n_glyphs, int cell_offset)
+    static void
+draw_attr_apply(DrawAttr *dattr, int col, DrawRow *drow)
 {
-    int cells_seen = 0;
+    uint32_t   *bg_cell = drow->dlayers[DRAW_LAYER_BACKGROUND].buf + col;
+    uint32_t   *text_cell = drow->dlayers[DRAW_LAYER_TEXT].buf + col;
+    uint32_t   *underc_cell;
+    uint32_t   *strike_cell;
+    uint32_t   fg_color = 0, bg_color = 0, sp_color = 0;
+    int                draw_flags = 0;
+    gboolean   bg_is_default = TRUE;
+
+    underc_cell = drow->dlayers[DRAW_LAYER_SPECIAL].buf +
+       col + UNDERC_OFFSET(drow->n_cells);
+    strike_cell = drow->dlayers[DRAW_LAYER_SPECIAL].buf +
+       col + STRIKE_OFFSET(drow->n_cells);
+
+    // If "dattr" is NULL, then make the pixels transparent.
+    if (dattr != NULL)
+    {
+       fg_color = dattr->fg_color;
+       bg_color = dattr->bg_color;
+       sp_color = dattr->sp_color;
+       draw_flags = dattr->draw_flags;
+       bg_is_default = color_equal_u32(gui.back_pixel, bg_color);
+    }
 
-    for (int i = 0; i < n_glyphs; i++)
+    if (*bg_cell != bg_color && !(*bg_cell == 0 && bg_is_default))
     {
-       const PangoGlyphInfo *glyph = glyphs + i;
+       draw_row_dirty_layer(drow, DRAW_LAYER_BACKGROUND);
 
-       if (cells_seen >= cell_offset)
-           return i;
+       // If "bg_color" is same as global background color, then use
+       // transparent color.
+       if (bg_is_default)
+           *bg_cell = 0;
+       else
+           *bg_cell = bg_color;
+    }
+    if ((draw_flags & DRAW_UNDERL && *text_cell != fg_color)
+           || (!(draw_flags & DRAW_UNDERL) && *text_cell != 0))
+    {
+       draw_row_dirty_layer(drow, DRAW_LAYER_TEXT);
+       if (draw_flags & DRAW_UNDERL)
+           *text_cell = fg_color;
+       else
+           *text_cell = 0;
+    }
+    if ((draw_flags & DRAW_UNDERC && *underc_cell != sp_color)
+           || (!(draw_flags & DRAW_UNDERC) && *underc_cell != 0))
+    {
+       draw_row_dirty_layer(drow, DRAW_LAYER_SPECIAL);
+       if (draw_flags & DRAW_UNDERC)
+           *underc_cell = sp_color;
+       else
+           *underc_cell = 0;
+    }
+    if ((draw_flags & DRAW_STRIKE && *strike_cell != sp_color)
+           || (!(draw_flags & DRAW_STRIKE) && *strike_cell != 0))
+    {
+       draw_row_dirty_layer(drow, DRAW_LAYER_SPECIAL);
+       if (draw_flags & DRAW_STRIKE)
+           *strike_cell = sp_color;
+       else
+           *strike_cell = 0;
+    }
+}
 
-       cells_seen += glyph->geometry.width / (gui.char_width * PANGO_SCALE);
+    static DrawGlyphs *
+draw_glyphs_new(
+       PangoFont       *font,
+       PangoGlyphInfo  *glyphs,
+       int             n_glyphs,
+       uint32_t        fg_color)
+{
+    DrawGlyphs *dglyphs = g_new0(DrawGlyphs, 1);
+
+    dglyphs->refcount = 1;
+    dglyphs->font = g_object_ref(font);
+    dglyphs->glyphs = g_memdup2(glyphs, sizeof(*glyphs) * n_glyphs);
+    dglyphs->n_glyphs = n_glyphs;
+    dglyphs->fg_color = fg_color;
+
+    return dglyphs;
+}
+
+#if defined(FEAT_NETBEANS_INTG) || defined(FEAT_SIGN_ICONS)
+/*
+ * Create a new draw glyphs that shows the given sign. Note that ownership of
+ * "dsign" is taken.
+ */
+    static DrawGlyphs *
+draw_glyphs_new_sign(DrawSign *dsign)
+{
+    DrawGlyphs *dglyphs = g_new0(DrawGlyphs, 1);
+
+    dglyphs->refcount = 1;
+    dglyphs->dsign = dsign;
+
+    return dglyphs;
+}
+#endif
+
+    static DrawGlyphs *
+draw_glyphs_ref(DrawGlyphs *dglyphs)
+{
+    dglyphs->refcount++;
+    return dglyphs;
+}
+
+    static void
+draw_glyphs_unref(DrawGlyphs *dglyphs)
+{
+    if (--dglyphs->refcount <= 0)
+    {
+#if defined(FEAT_NETBEANS_INTG) || defined(FEAT_SIGN_ICONS)
+       if (dglyphs->font == NULL)
+           draw_sign_free(dglyphs->dsign);
+       else
+#endif
+       {
+           g_free(dglyphs->glyphs);
+           g_object_unref(dglyphs->font);
+       }
+       g_free(dglyphs);
     }
-    return n_glyphs;
 }
 
 /*
- * Create a new under decoration node with the given flags. Returns NULL if no
- * under decorations are needed.
+ * Return TRUE if "a" and "b" are same. If either are NULL, return FALSE, if
+ * both are NULL, return TRUE.
+ */
+    static gboolean
+draw_glyphs_same(DrawGlyphs *a, DrawGlyphs *b)
+{
+    if (a == NULL && b == NULL)
+       return TRUE;
+    if (a == NULL || b == NULL)
+       return FALSE;
+    if (a->fg_color != b->fg_color)
+       return FALSE;
+    if (a->n_glyphs != b->n_glyphs || a->font != b->font)
+       return FALSE;
+    for (int i = 0; i < a->n_glyphs; i++)
+       if (a->glyphs[i].glyph != b->glyphs[i].glyph
+               || a->glyphs[i].geometry.width != b->glyphs[i].geometry.width
+               || a->glyphs[i].geometry.x_offset != b->glyphs[i].geometry.x_offset
+               || a->glyphs[i].geometry.y_offset != b->glyphs[i].geometry.y_offset
+               || a->glyphs[i].attr.is_cluster_start != b->glyphs[i].attr.is_cluster_start)
+           return FALSE;
+    return TRUE;
+}
+
+/*
+ * Convert the buffer for the given layer to a texture node, scaled up to the
+ * correct size. "off" is the pixel offset in the buffer to start from, and
+ * "bleed" is TRUE, then expand the buffer by 1 cell (clipped by draw area
+ * width), using the color of the rightmost cell.
  */
     static GskRenderNode *
-create_under_decor_node(
-       int             row,
-       int             start_col,
-       int             n_cells,
-       int             flags,
-       const GdkRGBA   *fg_color,
-       const GdkRGBA   *sp_color)
+draw_layer_get_texture(
+       DrawLayer   *dlayer,
+       int         row,
+       VimDrawArea *da,
+       int         off,
+       gboolean    bleed)
 {
-    GskRenderNode   *nodes[3];
-    int                    n_nodes = 0;
-    GskRenderNode   *container;
+    GBytes         *bytes;
+    GdkTexture     *texture;
+    GskRenderNode   *node;
+
+    if (bleed)
+    {
+       GByteArray *arr = g_byte_array_new();
+
+       g_byte_array_append(arr, (uint8_t *)(dlayer->buf + off),
+               sizeof(uint32_t) * da->n_cols);
+       g_byte_array_append(arr,
+               (uint8_t *)(dlayer->buf + off + (da->n_cols - 1)),
+               sizeof(uint32_t)); // Add bleed using last pixel of buffer
+       bytes = g_byte_array_free_to_bytes(arr);
+    }
+    else
+       bytes = g_bytes_new(dlayer->buf + off, sizeof(uint32_t) * da->n_cols);
+
+    texture = gdk_memory_texture_new(da->n_cols + bleed, 1,
+           GDK_MEMORY_R8G8B8A8, bytes, (da->n_cols + bleed) * 4);
+    g_bytes_unref(bytes);
+
+    // Scale texture to actual size
+    node = gsk_texture_scale_node_new(texture,
+           &GRAPHENE_RECT_INIT(FILL_X(0), FILL_Y(row),
+               (da->n_cols + bleed) * gui.char_width, gui.char_height),
+           GSK_SCALING_FILTER_NEAREST);
+    if (bleed)
+    {
+       GskRenderNode *new;
+
+       new = gsk_clip_node_new(node,
+               &GRAPHENE_RECT_INIT(FILL_X(0), FILL_Y(row),
+                   da->n_cols * gui.char_width + da->bleed_right,
+                   gui.char_height));
+       gsk_render_node_unref(node);
+       node = new;
+    }
+    g_object_unref(texture);
+    return node;
+}
+
+    static void
+draw_row_init(DrawRow *drow, int row, int cols)
+{
+    drow->cells = g_malloc0_n(cols, sizeof(DrawCell));
+    drow->n_cells = cols;
+    drow->row = row;
+    drow->underl_mask = drow->underc_mask = drow->strike_mask = NULL;
+
+    memset(drow->dlayers, 0, sizeof(DrawLayer) * N_DRAW_LAYERS);
+    for (int l = 0; l < N_DRAW_LAYERS; l++)
+    {
+       DrawLayer *dlayer = drow->dlayers + l;
+
+       // Make the buffer transparent initially
+       if (l == DRAW_LAYER_SPECIAL)
+           dlayer->buf = g_malloc0_n(cols * 2, 4);
+       else
+           dlayer->buf = g_malloc0_n(cols, 4);
+    }
+}
+
+    static void
+draw_row_clear(DrawRow *drow)
+{
+    g_free(drow->cells);
+    for (int l = 0; l < N_DRAW_LAYERS; l++)
+    {
+       DrawLayer *dlayer = drow->dlayers + l;
+
+       if (dlayer->node != NULL)
+           gsk_render_node_unref(dlayer->node);
+       g_free(dlayer->buf);
+    }
+    g_clear_pointer(&drow->underl_mask, gsk_render_node_unref);
+    g_clear_pointer(&drow->underc_mask, gsk_render_node_unref);
+    g_clear_pointer(&drow->strike_mask, gsk_render_node_unref);
+}
+
+/*
+ * Dirty the given layer for the row so it is rendered again.
+ */
+    static void
+draw_row_dirty_layer(DrawRow *drow, DrawLayerType dlayer_t)
+{
+    DrawLayer *dlayer = drow->dlayers + dlayer_t;
+
+    dlayer->dirty = TRUE;
+    g_clear_pointer(&dlayer->node, gsk_render_node_unref);
+}
+
+/*
+ * If the cell at "col" is inverted, then uninvert and set the layer as dirty.
+ * Otherwise do nothing.
+ */
+    static void
+draw_row_uninvert_cell(DrawRow *drow, int col)
+{
+    uint32_t *buf = drow->dlayers[DRAW_LAYER_OVERLAY].buf;
+
+    // If pixel is white, then it is inverted.
+    if (buf[col] == 0xffffffff)
+    {
+       draw_row_dirty_layer(drow, DRAW_LAYER_OVERLAY);
+       buf[col] = 0;
+    }
+}
+
+/*
+ * Set the cell in the row to the glyphs and attrs (that represents the cell).
+ * This will dirty the changed layers of the row.
+ */
+    static void
+draw_row_set_cell(DrawRow *drow, int col, DrawGlyphs *dglyphs, DrawAttr *dattr)
+{
+    DrawCell *dcell = drow->cells + col;
+
+    if (!draw_glyphs_same(dglyphs, dcell->dglyphs))
+       draw_row_dirty_layer(drow, DRAW_LAYER_TEXT);
+
+    // When drawing double width characters in the snapshot vfunc, pointer
+    // equality of the DrawGlyphs is used. Must make sure to always keep the
+    // pointer in sync, even if glyphs are technically the "same".
+    if (dcell->dglyphs != dglyphs)
+    {
+       if (dcell->dglyphs != NULL)
+           draw_glyphs_unref(dcell->dglyphs);
+       dcell->dglyphs = dglyphs == NULL ? NULL : draw_glyphs_ref(dglyphs);
+    }
+
+    draw_attr_apply(dattr, col, drow);
+    draw_row_uninvert_cell(drow, col);
+}
+
+/*
+ * Same as draw_row_set_cell(), but fills in the cells between "col1" and "col2"
+ * (inclusive).
+ */
+    static void
+draw_row_fill(
+       DrawRow     *drow,
+       int         col1,
+       int         col2,
+       DrawGlyphs  *dglyphs,
+       DrawAttr    *dattr,
+       DrawCursor  *cursor)
+{
+    for (int c = col1; c <= col2; c++)
+       draw_row_set_cell(drow, c, dglyphs, dattr);
+
+    // Clear cursor if its in the region
+    if (gui.cursor_row == drow->row && gui.cursor_col >= col1
+           && gui.cursor_col <= col2)
+       cursor->draw = FALSE;
+}
 
-    if (flags & DRAW_UNDERL)
-       nodes[n_nodes++] = gsk_color_node_new(fg_color,
-               &GRAPHENE_RECT_INIT(FILL_X(start_col),
-                   FILL_Y(row + 1) - 1,
-                   FILL_X(start_col + n_cells) - FILL_X(start_col), 1));
+/*
+ * Move the cells between "col1" and "col2" from "src" to "dest", overwriting
+ * the existing cells. This will clear the source cells.
+ */
+    static void
+draw_row_move_to(
+       DrawRow     *dest_row,
+       DrawRow     *src_row,
+       int         col1,
+       int         col2,
+       VimDrawArea *da)
+{
+    int move_size = (col2 - col1 + 1) * sizeof(DrawCell);
 
-    if (flags & DRAW_STRIKE)
-       nodes[n_nodes++] = gsk_color_node_new(fg_color,
-               &GRAPHENE_RECT_INIT(FILL_X(start_col),
-                   FILL_Y(row) + (int)(gui.char_height / 2),
-                   FILL_X(start_col + n_cells) - FILL_X(start_col), 1));
+    draw_row_fill(dest_row, col1, col2, NULL, NULL, &da->cursor);
+    memmove(dest_row->cells + col1, src_row->cells + col1, move_size);
+    // NULL the draw cells so we don't double unreference.
+    memset(src_row->cells + col1, 0, move_size);
+    draw_row_dirty_layer(src_row, DRAW_LAYER_TEXT);
 
-    if (flags & DRAW_UNDERC)
+    // Move the layer buffers
+    move_size = (col2 - col1 + 1) * 4;
+    for (int l = 0; l < N_DRAW_LAYERS; l++)
+    {
+       uint32_t *src = src_row->dlayers[l].buf;
+       uint32_t *dest = dest_row->dlayers[l].buf;
+
+       if (l == DRAW_LAYER_SPECIAL)
+       {
+           memmove(dest + col1 + UNDERC_OFFSET(dest_row->n_cells),
+                   src + col1 + UNDERC_OFFSET(src_row->n_cells), move_size);
+           memmove(dest + col1 + STRIKE_OFFSET(dest_row->n_cells),
+                   src + col1 + STRIKE_OFFSET(src_row->n_cells), move_size);
+       }
+       else
+           memmove(dest + col1, src + col1, move_size);
+       draw_row_dirty_layer(dest_row, l);
+    }
+}
+
+/*
+ * If background layer is dirty, then render it.
+ */
+    static void
+draw_row_render_background(DrawRow *drow, VimDrawArea *da)
+{
+    DrawLayer *dlayer = drow->dlayers + DRAW_LAYER_BACKGROUND;
+
+    if (!dlayer->dirty)
+       return;
+
+    dlayer->dirty = FALSE;
+
+    // If background is transparent, don't add a render node.
+    if (rgba_is_transparent(dlayer->buf, da->n_cols))
+       return;
+
+    // Add a 1 cell bleed, then use a clip node to clip it to the draw area
+    // width. This just makes things like status bars look a bit nicer when draw
+    // area size is not an exact multiple of the cell size.
+    dlayer->node = draw_layer_get_texture(dlayer, drow->row, da, 0, TRUE);
+}
+
+/*
+ * Ensure the given under decorations are generated.
+ */
+    static void
+draw_row_ensure_decor(DrawRow *drow, int flags)
+{
+    if (flags & DRAW_UNDERL && drow->underl_mask == NULL)
+       drow->underl_mask = gsk_color_node_new(&white_rgba,
+               &GRAPHENE_RECT_INIT(FILL_X(0), FILL_Y(drow->row + 1) - 1,
+                   FILL_X(drow->n_cells), 1));
+
+    if (flags & DRAW_STRIKE && drow->strike_mask == NULL)
+       drow->strike_mask = gsk_color_node_new(&white_rgba,
+               &GRAPHENE_RECT_INIT(FILL_X(0),
+                   FILL_Y(drow->row) + (int)(gui.char_height / 2),
+                   FILL_X(drow->n_cells), 1));
+
+    if (flags & DRAW_UNDERC && drow->underc_mask == NULL)
     {
-       int             y = FILL_Y(row + 1) - 1; // Top of underneath line,
-                                                // upwards by one pixel.
-       int             x_start = FILL_X(start_col);
-       int             x_end = FILL_X(start_col + n_cells);
+       static const int val[8] = {1, 0, 0, 0, 1, 2, 2, 2};
+
+       int y = FILL_Y(drow->row + 1) - 1;
+       int x_start = FILL_X(0);
+       int x_end = FILL_X(drow->n_cells);
 
        // GskPath was added in GSK 4.14, otherwise use cairo
 #if GTK_CHECK_VERSION(4, 14, 0)
        GskPathBuilder  *builder;
        GskPath         *path;
-       GskStroke       *stroke;
+       GskStroke       *stroke;
        GskRenderNode   *color_node;
        graphene_rect_t bounds;
 
-       const int       half_wave = 4;  // Half-cycle width (e.g., 4px up, 4px
-                                       // down)
-       const int       amplitude = 2;  // Peak height from baseline
-       int             toggle = -1;    // Start by pulling up (-Y is up in GTK)
-
        builder = gsk_path_builder_new();
-       gsk_path_builder_move_to(builder, x_start, y);
 
-       // Each cycle contains two quadratic bezier curves, one going up and one
-       // going down.
-       for (int x = x_start; x < x_end; x += half_wave)
-       {
-           int current_half = half_wave;
-           if (x + current_half > x_end)
-           {
-               current_half = x_end - x;
-           }
-
-           // The control point sits exactly halfway horizontally through the arc
-           int cp_x = x + (current_half / 2);
-           int cp_y = y + (toggle * amplitude);
-           int end_x = x + current_half;
+       gsk_path_builder_move_to(builder,
+               x_start + 1,
+               y - 2 + 0.5);
 
-           gsk_path_builder_quad_to(builder, cp_x, cp_y, end_x, y);
+       for (int i = x_start + 1; i < x_end; i++)
+       {
+           int offset = val[i % 8];
 
-           toggle = -toggle; // Flip direction for the next half-wave
+           gsk_path_builder_line_to(builder,
+                   i, y - offset + 0.5);
        }
 
        path = gsk_path_builder_free_to_path(builder);
-       stroke = gsk_stroke_new(1.0f);
+
+       stroke = gsk_stroke_new(1.0);
 
        gsk_path_get_stroke_bounds (path, stroke, &bounds);
-       color_node = gsk_color_node_new(sp_color, &bounds);
+       color_node = gsk_color_node_new(&white_rgba, &bounds);
 
-       nodes[n_nodes++] = gsk_stroke_node_new(color_node, path, stroke);
+       drow->underc_mask = gsk_stroke_node_new(color_node, path, stroke);
        gsk_stroke_free(stroke);
        gsk_path_unref(path);
        gsk_render_node_unref(color_node);
 #else
-       static const int    val[8] = {1, 0, 0, 0, 1, 2, 2, 2};
-       cairo_t             *cr;
-       GskRenderNode       *node;
+       cairo_t         *cr;
+       GskRenderNode   *node;
 
        node = gsk_cairo_node_new(
                &GRAPHENE_RECT_INIT(x_start, y - 3, x_end - x_start, 5));
        cr = gsk_cairo_node_get_draw_context(node);
 
        cairo_set_line_width(cr, 1.0);
-       cairo_set_source_rgba(cr, sp_color->red, sp_color->green,
-               sp_color->blue, sp_color->alpha);
+       cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0);
 
        cairo_move_to(cr, x_start + 1, y - 2 + 0.5);
 
@@ -352,656 +968,533 @@ create_under_decor_node(
 
        cairo_stroke(cr);
        cairo_destroy(cr);
-       nodes[n_nodes++] = node;
+       drow->underc_mask = node;
 #endif
     }
-
-    if (n_nodes == 0)
-       return NULL;
-    if (n_nodes == 1)
-       return nodes[0];
-
-    container = gsk_container_node_new(nodes, n_nodes);
-    for (int i = 0; i < n_nodes; i++)
-       // Container node takes its own reference to each.
-       gsk_render_node_unref(nodes[i]);
-    return container;
 }
 
 /*
- * Create a new draw node with a reference count of 1. Note that this may be
- * NULL if creating a new draw node is not necessary.
+ * If the text layer of the draw row is dirty, then render it and set the render
+ * node. Note that the render node may still be NULL if there is nothing to
+ * display.
  */
-    static DrawNode *
-draw_node_new(
-       PangoFont               *font,
-       const PangoGlyphInfo    *glyphs,
-       int                     n_glyphs,
-       const GdkRGBA           *bg_color,
-       const GdkRGBA           *fg_color,
-       const GdkRGBA           *sp_color,
-       int                     flags,
-       int                     start_col,
-       int                     n_cells)
-{
-    DrawNode   *dnode;
-    gboolean   has_ink = glyphs_has_ink(font, glyphs, n_glyphs);
-    gboolean   is_def_bg = color_is_default_bg(bg_color);
-    gboolean   has_under = flags & (DRAW_UNDERL | DRAW_UNDERC | DRAW_STRIKE);
-
-    // If there is no ink to be displayed, and the background color is the same
-    // as the default background color (the color that will be displayed behind
-    // everything), then there is no point in creating a new draw node.
-    if (!has_ink && !has_under && (flags & DRAW_TRANSP || is_def_bg))
-       return NULL;
-
-    dnode = g_new0(DrawNode, 1);
-
-    dnode->refcount = 1;
-
-    dnode->glyphs = g_memdup2(glyphs, sizeof(PangoGlyphInfo) * n_glyphs);
-    dnode->n_glyphs = n_glyphs;
-    dnode->dnode_flags |= DRAW_NODE_DIRTY;
-    if (is_def_bg || flags & DRAW_TRANSP)
-       dnode->dnode_flags |= DRAW_NODE_NOBG;
-    if (!has_ink)
-       dnode->dnode_flags |= DRAW_NODE_NOINK;
-    if (has_under)
-       dnode->dnode_flags |= DRAW_NODE_UNDER;
-
-    dnode->font = g_object_ref(font);
-    dnode->bg_color = *bg_color;
-    dnode->fg_color = *fg_color;
-    dnode->sp_color = *sp_color;
-    dnode->flags = flags;
-
-    dnode->start_col = start_col;
-    dnode->n_cells = n_cells;
-
-    return dnode;
-}
-
-    static DrawNode *
-draw_node_ref(DrawNode *dnode)
-{
-    dnode->refcount++;
-    return dnode;
-}
-
     static void
-draw_node_unref(DrawNode *dnode)
+draw_row_render_text(DrawRow *drow, VimDrawArea *da)
 {
-    if (dnode != NULL && --dnode->refcount <= 0)
-    {
-       g_free(dnode->glyphs);
-       node_unref(dnode->node);
-       g_object_unref(dnode->font);
-       g_free(dnode);
-    }
-}
-
-/*
- * Dirty the draw node. This will remove the render node if any, and mark it to
- * have a new render node created for it on the next snapshot vfunc call.
- * Returns TRUE if draw node is not necessary anymore.
- */
-    static gboolean
-draw_node_make_dirty(DrawNode *dnode)
-{
-    int flags = dnode->dnode_flags;
-
-    g_clear_pointer(&dnode->node, gsk_render_node_unref);
-    dnode->dnode_flags |= DRAW_NODE_DIRTY;
-
-    return (flags & DRAW_NODE_NOINK) && !(flags & (DRAW_NODE_UNDER))
-       && flags & DRAW_NODE_NOBG;
-}
-
-    static DrawNode *
-draw_node_copy(DrawNode *dnode)
-{
-    DrawNode *copy = draw_node_new(
-           dnode->font, dnode->glyphs, dnode->n_glyphs,
-           &dnode->bg_color, &dnode->fg_color, &dnode->sp_color,
-           dnode->flags, dnode->start_col, dnode->n_cells
-           );
+    DrawLayer  *dlayer = drow->dlayers + DRAW_LAYER_TEXT;
+    GArray     *glyph_buf;
+    GPtrArray  *nodes;
+    PangoFont  *cur_font = NULL;
+    int                start_col = 0;
+    int                empty_cells = 0;
+    uint32_t   cur_fg = 0;
+    int                c;
+
+    if (!dlayer->dirty)
+       return;
 
-    // "copy" should never be NULL, so we don't need to check for NULL.
-    if (unlikely(dnode->dnode_flags & DRAW_NODE_CLIP))
-       copy->dnode_flags |= DRAW_NODE_CLIP;
+    glyph_buf = da->glyph_buf;
+    nodes = da->node_buf;
+
+    g_array_set_size(glyph_buf, 0);
+    g_ptr_array_set_size(nodes, 0);
+
+    // GskTextNode does not use the "log_clusters" array of PangoGlyphString, so
+    // we don't need to set it.
+#define FLUSH_NODE() \
+    do { \
+       if (cur_font != NULL) \
+       { \
+           PangoGlyphString    glyph_str; \
+           GskRenderNode       *node; \
+           GdkRGBA             fg_color = u32_to_rgba(cur_fg); \
+           glyph_str.glyphs = (PangoGlyphInfo *)glyph_buf->data; \
+           glyph_str.num_glyphs = glyph_buf->len; \
+           node = gsk_text_node_new(cur_font, &glyph_str, &fg_color, \
+                   &GRAPHENE_POINT_INIT(TEXT_X(start_col), \
+                       TEXT_Y(drow->row))); \
+           if (node != NULL) \
+               g_ptr_array_add(nodes, node); \
+           g_array_set_size(glyph_buf, 0); \
+           cur_font = NULL; \
+       } \
+    } while (FALSE)
+
+    for (c = 0; c < da->n_cols; c++)
+    {
+       DrawCell    *dcell = drow->cells + c;
+       DrawGlyphs  *dglyphs = dcell->dglyphs;
+       int         prev_len;
 
-    return copy;
-}
+       if (dglyphs == NULL)
+       {
+           empty_cells++;
+           continue;
+       }
+       else if (dglyphs->font == NULL)
+       {
+           // Add sign icon
+           GskRenderNode   *snode;
+           int             w;
+
+           FLUSH_NODE();
+
+           snode = gsk_texture_scale_node_new(dglyphs->dsign->texture,
+                   &GRAPHENE_RECT_INIT(FILL_X(c), FILL_Y(drow->row),
+                       dglyphs->dsign->width, dglyphs->dsign->height),
+                   GSK_SCALING_FILTER_TRILINEAR);
+           w = (dglyphs->dsign->width + gui.char_width - 1) / gui.char_width;
+
+           if (snode != NULL)
+               g_ptr_array_add(nodes, snode);
+           // Skip the cells the draw sign covers.
+           c += w - 1; // Subtract one because we increment by one in the for
+                       // loop
+           continue;
+       }
+       else if (dglyphs->font != cur_font || cur_fg != dglyphs->fg_color)
+       {
+           FLUSH_NODE();
+           cur_font = dglyphs->font;
+           start_col = c;
+           cur_fg = dglyphs->fg_color;
+       }
 
-/*
- * Split the draw node at the given cell offset in place (exclusive). If
- * "keep_left" is TRUE, then keep the left halve (discard right halve), and vice
- * versa. This will dirty the draw node.
- *
- * Returns TRUE if the new split draw node is not necessary anymore (see
- * draw_node_new()), otherwise FALSE.
- */
-    static gboolean
-draw_node_split(DrawNode *dnode, int cell_offset, gboolean keep_left)
-{
-    int                glyph_offset;
-    gboolean   split = TRUE;
-    gboolean   clip = FALSE;
-
-    glyph_offset = cell_offset_to_glyph(dnode->glyphs,
-           dnode->n_glyphs, cell_offset);
-
-    // Some fonts emulate ligatures by having spacer glyphs followed by a glyph
-    // that contains all the ink. If we tried splitting this type of ligature,
-    // then one side will incorrectly be empty.
-    //
-    // To handle this case, always clip the draw node so that the extra ink does
-    // not bleed out. If we are keeping the left side, then do not split,
-    // because we want to keep the glyph with all the ink. If we are keeping the
-    // right side, then we can split because the glyph with the ink will be on
-    // the right side always anyways.
-    for (int i = glyph_offset; i < dnode->n_glyphs; i++)
-    {
-       PangoRectangle ink;
+       // Don't want to render double width characters twice.
+       if (c > 0 && drow->cells[c - 1].dglyphs == dglyphs)
+           continue;
 
-       pango_font_get_glyph_extents(dnode->font, dnode->glyphs[i].glyph,
-               &ink, NULL);
+       prev_len = glyph_buf->len;
+       g_array_append_vals(glyph_buf, dglyphs->glyphs, dglyphs->n_glyphs);
 
-       if (HAS_INK(&ink))
+       // Inject the accumulated space width into the glyphs that
+       // have ink.
+       if (empty_cells > 0)
        {
-           if (ink.x < 0)
+           if (prev_len > 0)
            {
-               split = !keep_left;
-               clip = TRUE;
+               // Spaces occurred between valid text: extend
+               // previous character's advance
+               PangoGlyphInfo *last = &g_array_index(
+                       glyph_buf, PangoGlyphInfo,
+                       prev_len - 1);
+               last->geometry.width += CELLS2PANGO(empty_cells);
            }
-           break;
+           empty_cells = 0; // Width consumed
        }
     }
 
-    if (unlikely(clip))
-       dnode->dnode_flags |= DRAW_NODE_CLIP;
+    FLUSH_NODE();
 
-    if (keep_left)
-    {
-       if (likely(split))
-           dnode->n_glyphs = glyph_offset;
-       dnode->n_cells = cell_offset;
-    }
-    else
+#undef FLUSH_NODE
+
+    if (nodes->len > 0)
     {
-       if (likely(split))
+       // Add underline mask (if needed).
+       GskRenderNode *source;
+       GskRenderNode *node;
+
+       draw_row_ensure_decor(drow, DRAW_UNDERL);
+       if (!rgba_is_transparent(dlayer->buf, drow->n_cells))
        {
-           // If this results in zero, then glyphs_has_ink() will return FALSE
-           // so it is fine.
-           dnode->n_glyphs -= glyph_offset;
-           // Shift glyphs after offset to beginning
-           memmove(dnode->glyphs, dnode->glyphs + glyph_offset,
-                   sizeof(PangoGlyphInfo) * dnode->n_glyphs);
+           source = draw_layer_get_texture(dlayer, drow->row, da, 0, FALSE);
+           node = gsk_mask_node_new(source, drow->underl_mask,
+                   GSK_MASK_MODE_ALPHA);
+           gsk_render_node_unref(source);
+           g_ptr_array_add(nodes, node);
        }
 
-       dnode->n_cells -= cell_offset;
-       dnode->start_col += cell_offset;
-    }
-
-    if (likely(split))
-    {
-       dnode->glyphs = glyphs_resize(dnode->glyphs, dnode->n_glyphs);
-
-       // Recheck if new split glyphs has ink
-       if (glyphs_has_ink(dnode->font, dnode->glyphs, dnode->n_glyphs))
-           dnode->dnode_flags &= ~DRAW_NODE_NOINK;
-       else
-           dnode->dnode_flags |= DRAW_NODE_NOINK;
+       dlayer->node = gsk_container_node_new(
+               (GskRenderNode **)nodes->pdata, nodes->len);
     }
-
-    return draw_node_make_dirty(dnode);
+    dlayer->dirty = FALSE;
 }
 
 /*
- * If "dnode" is dirty, create a new render node for it at the given row and
- * store it, then undirty it.
+ * Render undercurl and strikethough, which use the special colour.
  */
     static void
-draw_node_render(DrawNode *dnode, int row, VimDrawArea *da)
+draw_row_render_special(DrawRow *drow, VimDrawArea *da)
 {
-    GskRenderNode      *nodes[3];
-    int                        n_nodes = 0;
-    GskRenderNode      *decor_node;
+    DrawLayer      *dlayer = drow->dlayers + DRAW_LAYER_SPECIAL;
+    GskRenderNode   *source;
+    GskRenderNode   *nodes[2];
+    int                    n_nodes = 0;
 
-    if (!(dnode->dnode_flags & DRAW_NODE_DIRTY))
+    if (!dlayer->dirty)
        return;
 
-    if (!(dnode->dnode_flags & DRAW_NODE_NOBG))
-    {
-       int width = dnode->n_cells * gui.char_width;
+    draw_row_ensure_decor(drow, DRAW_UNDERC | DRAW_STRIKE);
 
-       // If this draw node touches the end of the draw area. Bleed its
-       // background to the right if the space the draw area covers is slightly
-       // bigger than its actual visible area (that all cells cover). This just
-       // makes things like status bars look a bit nicer
-       //
-       // Don't do this for the bottom, because that will make the cursor in
-       // the cmdline look weird. Instead only bleed downwards when drawing the
-       // global background color (see vim_draw_area_snapshot())
-       if (END_COL(dnode) == da->n_cols - 1)
-           width += gui.bleed_right;
-
-       nodes[n_nodes++] = gsk_color_node_new(&dnode->bg_color,
-               &GRAPHENE_RECT_INIT(FILL_X(dnode->start_col), FILL_Y(row),
-                   width, gui.char_height));
+    if (!rgba_is_transparent(dlayer->buf + UNDERC_OFFSET(drow->n_cells),
+               drow->n_cells))
+    {
+       source = draw_layer_get_texture(dlayer, drow->row, da,
+               UNDERC_OFFSET(drow->n_cells), FALSE);
+       nodes[n_nodes++] = gsk_mask_node_new(source, drow->underc_mask,
+               GSK_MASK_MODE_ALPHA);
+       gsk_render_node_unref(source);
     }
 
-    if (!(dnode->dnode_flags & DRAW_NODE_NOINK))
+    if (!rgba_is_transparent(dlayer->buf + STRIKE_OFFSET(drow->n_cells),
+               drow->n_cells))
     {
-       GskRenderNode       *text_node;
-       PangoGlyphString    glyphs_str;
-
-       // gsk_text_node_new() only uses the "glyphs" field, don't need to worry
-       // about the "log_clusters" array.
-       glyphs_str.glyphs = dnode->glyphs;
-       glyphs_str.num_glyphs = dnode->n_glyphs;
-       text_node = gsk_text_node_new(dnode->font, &glyphs_str, &dnode->fg_color,
-               &GRAPHENE_POINT_INIT(TEXT_X(dnode->start_col), TEXT_Y(row)));
-       // Should never be NULL since we check beforehand if there is ink.
-       assert(text_node != NULL);
-
-       if (dnode->dnode_flags & DRAW_NODE_CLIP)
-       {
-           GskRenderNode *old = text_node;
-
-           text_node = gsk_clip_node_new(text_node,
-                   &GRAPHENE_RECT_INIT(FILL_X(dnode->start_col), FILL_Y(row),
-                       dnode->n_cells * gui.char_width, gui.char_height));
-           gsk_render_node_unref(old);
-           assert(text_node != NULL);
-       }
-
-       nodes[n_nodes++] = text_node;
+       source = draw_layer_get_texture(dlayer, drow->row, da,
+               STRIKE_OFFSET(drow->n_cells), FALSE);
+       nodes[n_nodes++] = gsk_mask_node_new(source, drow->strike_mask,
+               GSK_MASK_MODE_ALPHA);
+       gsk_render_node_unref(source);
     }
 
-    decor_node = create_under_decor_node(row, dnode->start_col, dnode->n_cells,
-           dnode->flags, &dnode->fg_color, &dnode->sp_color);
-    if (decor_node != NULL)
-       nodes[n_nodes++] = decor_node;
-
-    // Should never be zero
-    assert(n_nodes > 0);
-
-    if (likely(n_nodes == 1))
-       dnode->node = nodes[0];
-    else
+    if (n_nodes == 1)
+    {
+       dlayer->node = nodes[0];
+    }
+    else if (n_nodes > 0)
     {
-       dnode->node = gsk_container_node_new(nodes, n_nodes);
-       // gsk_container_node_new() takes its own reference
-       for (int i = 0; i < n_nodes; i++)
+       dlayer->node = gsk_container_node_new(nodes, 2);
+       // gsk_container_node_new() takes its own ref
+       for (int i = 0; i < ARRAY_LENGTH(nodes); i++)
            gsk_render_node_unref(nodes[i]);
     }
 
-    dnode->dnode_flags &= ~DRAW_NODE_DIRTY;
+    dlayer->dirty = FALSE;
 }
 
 /*
- * Returns true if "dnode" matches "font" + "flags" in terms of
- * color/visual attributes.
+ * Render white color blocks used for blend to invert cells.
  */
-    static gboolean
-draw_node_match(DrawNode *dnode, PangoFont *font, int flags)
+    static void
+draw_row_render_overlay(DrawRow *drow, VimDrawArea *da)
 {
-    if (dnode->flags != flags)
-       return FALSE;
+    DrawLayer *dlayer = drow->dlayers + DRAW_LAYER_OVERLAY;
 
-    if (!(flags & DRAW_TRANSP)
-           && !gdk_rgba_equal(&dnode->bg_color, gui.bgcolor))
-       return FALSE;
+    if (!dlayer->dirty)
+       return;
 
-    if (!gdk_rgba_equal(&dnode->fg_color, gui.fgcolor))
-       return FALSE;
+    dlayer->dirty = FALSE;
 
-    // Special color is only used for undercurls
-    if (flags & DRAW_UNDERC && !gdk_rgba_equal(&dnode->sp_color, gui.spcolor))
-       return FALSE;
+    // If no invert regions, don't do anything.
+    if (rgba_is_transparent(dlayer->buf, da->n_cols))
+       return;
 
-    // This may not work all the time, but creating two PangoFontDescription
-    // each time to compare equality seems slow...
-    return dnode->font == font;
+    dlayer->node = draw_layer_get_texture(dlayer, drow->row, da, 0, TRUE);
 }
 
 /*
- * Append or prepend the given glyphs to the draw node. If "start" is TRUE, then
- * prepend, otherwise append. This will invalidate the draw node. Note that
- * prepending does not update "start_col" or "n_cells".
+ * Add the glyph cluster to the raw area with given attributes and font.
  */
     static void
-draw_node_extend(
-       DrawNode                *dnode,
-       const PangoGlyphInfo    *glyphs,
-       int                     n_glyphs,
-       bool                    start)
+draw_row_add_cluster(
+       DrawRow         *drow,
+       DrawAttr        *dattr,
+       int             col,
+       int             n_cells,
+       PangoFont       *font,
+       PangoGlyphInfo  *glyphs,
+       int             n_glyphs,
+       VimDrawArea     *da)
 {
-    dnode->glyphs = glyphs_resize(dnode->glyphs, dnode->n_glyphs + n_glyphs);
-
-    if (start)
-    {
-       // Move the existing glyphs forward first
-       memmove(dnode->glyphs + n_glyphs, dnode->glyphs,
-               dnode->n_glyphs * sizeof(PangoGlyphInfo));
-       memcpy(dnode->glyphs, glyphs, n_glyphs * sizeof(PangoGlyphInfo));
-    }
-    else
-       memcpy(dnode->glyphs + dnode->n_glyphs, glyphs,
-               n_glyphs * sizeof(PangoGlyphInfo));
+    DrawGlyphs *dglyphs = NULL;
 
-    dnode->n_glyphs += n_glyphs;
+    if (col + n_cells > da->n_cols)
+       return;
 
-    if (glyphs_has_ink(dnode->font, dnode->glyphs, dnode->n_glyphs))
-       dnode->dnode_flags &= ~DRAW_NODE_NOINK;
-    else
-       dnode->dnode_flags |= DRAW_NODE_NOINK;
-    (void)draw_node_make_dirty(dnode);
-}
+    // If there is no ink ("glyphs" is NULL), then just set the "dglyphs" of
+    // each cell to NULL.
+    if (glyphs != NULL)
+       dglyphs = draw_glyphs_new(font, glyphs, n_glyphs, dattr->fg_color);
 
-/*
- * Set the given cell to the draw node (which may be NULL), adding a new
- * reference to it.
- */
-    static void
-draw_cell_set(DrawCell *dcell, DrawNode *dnode)
-{
-    draw_node_unref(dcell->dnode);
-    dcell->dnode = dnode == NULL ? NULL : draw_node_ref(dnode);
-    dcell->invert = FALSE;
-}
+    draw_row_fill(drow, col, col + n_cells - 1, dglyphs, dattr, &da->cursor);
 
-/*
- * Set the cells between "col1" and "col2" (inclusive) to "dnode" (which may be
- * NULL).
- */
-    static void
-draw_row_fill(DrawCell *drow, int col1, int col2, DrawNode *dnode)
-{
-    for (int c = col1; c <= col2; c++)
-       draw_cell_set(drow + c, dnode);
+    if (dglyphs != NULL)
+       draw_glyphs_unref(dglyphs);
 }
 
 /*
- * Same as draw_row_fill(), but also handle truncating/splitting any draw nodes
- * that overlap onto the set region. If "split" is TRUE, then only
- * truncating/splitting is done.
- *
- * If "copy" is TRUE, then "dnode" is ignored and instead any draw nodes in the
- * region that overlap outside of it are copied and clipped in addition to
- * truncating draw nodes outside the region.
+ * Add the string to the draw area. This will handle the 'guiligatures' option.
  */
-    static void
-draw_row_set(
-       DrawCell    *drow,
-       int         col1,
-       int         col2,
-       DrawNode    *dnode,
-       gboolean    copy,
-       gboolean    split)
+    static int
+vim_draw_area_add_string(
+       VimDrawArea *self,
+       DrawRow     *drow,
+       DrawAttr    *dattr,
+       int         col,
+       char_u      *s,
+       int         len,
+       int         draw_flags)
 {
-    DrawNode   *ldnode = drow[col1].dnode;
-    DrawNode   *rdnode = drow[col2].dnode;
-    DrawNode   *new_dnode = NULL;
-
-    if (ldnode != NULL && ldnode == rdnode
-           && (ldnode->start_col != col1 || END_COL(ldnode) > col2))
+    PangoGlyphString   *glyphs;
+    PangoAttrList      *attr_list = NULL;
+    int                        start_idx = -1;
+    GList              *item_list;
+    int                        column_offset = 0;
+    int                        cluster_width;
+    int                        last_glyph_rbearing;
+    int                        cells = 0;
+    int                        i = 0;
+    gboolean           has_unicode = FALSE;
+    gboolean           needs_shaping = FALSE;
+
+    // Scan the string, and find spans where ligatures should not happen. We
+    // must ensure that attribute boundaries do not split a base character from
+    // its combining marks, as Pango breaks items at attribute boundaries.
+    while (i < len)
     {
-       // Region in completely inside a single draw node. Truncate the existing
-       // draw node, and create a new draw node to be used as the right split.
-       if (END_COL(ldnode) > col2)
+       int     clen = 1;
+       int     is_composing = 0;
+       bool    want_disable;
+
+       if (s[i] >= 0x80)
        {
-           rdnode = draw_node_copy(ldnode);
-           draw_row_fill(drow, col2 + 1, END_COL(rdnode), rdnode);
-           draw_node_unref(rdnode);
+           clen = utf_ptr2len(s + i);
+           is_composing = utf_iscomposing(utf_ptr2char(s + i));
+           has_unicode = TRUE;
        }
-       else
-           // "ldnode" does not extend past "col2", no point in creating a new
-           // draw node on the right.
-           rdnode = NULL;
-
-       if (copy)
-           // Make another copy for the new draw node inside the set region.
-           // Must fill it in the row after, since "ldnode" may be unreferenced
-           // fully.
-           new_dnode = draw_node_copy(ldnode);
-    }
 
-    if (ldnode != NULL && ldnode->start_col != col1)
-    {
-       if (copy && new_dnode == NULL)
+       if (is_composing)
+           // Do not change state mid-cluster. Inherit the previous character's
+           // state.
+           want_disable = (start_idx != -1);
+       else
+           // Base character: disable ligatures if it's an ASCII non-ligature
+           // char.
+           want_disable = (s[i] < 0x80 && !gui.ligatures_map[s[i]]);
+
+       // If any base character actually wants ligature/contextual shaping,
+       // the string must go through Pango's shaping engine -- the ASCII
+       // fast path below draws cached glyphs one-by-one and can never
+       // produce a ligature, regardless of 'guiligatures'.
+       if (!is_composing && !want_disable)
+           needs_shaping = TRUE;
+
+       if (want_disable)
        {
-           // Make a copy for the right halve.
-           DrawNode *new_right = draw_node_copy(ldnode);
-
-           if (draw_node_split(new_right,  col1 - ldnode->start_col, FALSE))
-               g_clear_pointer(&new_right, draw_node_unref);
-           draw_row_fill(drow, col1, END_COL(ldnode), new_right);
-           draw_node_unref(new_right);
+           if (start_idx == -1)
+               start_idx = i;
        }
-
-       // Leftmost draw node overlaps onto region, split it and discard right
-       // halve.
-       if (draw_node_split(ldnode, col1 - ldnode->start_col, TRUE))
-           // Draw node is not necessary anymore, clear it from the row.
-           draw_row_fill(drow, ldnode->start_col, col1 - 1, NULL);
-    }
-    if (rdnode != NULL && END_COL(rdnode) > col2)
-    {
-       if (copy && new_dnode == NULL)
+       else if (start_idx != -1)
        {
-           // Make a copy for the left halve.
-           DrawNode *new_left = draw_node_copy(rdnode);
-
-           if (draw_node_split(new_left,  col2 - rdnode->start_col + 1, TRUE))
-               g_clear_pointer(&new_left, draw_node_unref);
-           draw_row_fill(drow, rdnode->start_col, col2, new_left);
-           draw_node_unref(new_left);
+           if (attr_list == NULL)
+               attr_list = pango_attr_list_new();
+           INSERT_PANGO_ATTR(pango_attr_font_features_new("liga 0, calt 0"),
+                   attr_list, start_idx, i);
+           start_idx = -1;
        }
-
-       // Rightmost draw node overlaps onto region, split it and discard left
-       // halve.
-       if (draw_node_split(rdnode, col2 - rdnode->start_col + 1, FALSE))
-           draw_row_fill(drow, col2 + 1, END_COL(rdnode), NULL);
+       i += clen;
     }
 
-    if (copy)
+    // Fast path for pure ASCII: use cached glyph table. Skip this path when
+    // there are non-ascii characters in the string, font attributes, or if
+    // theres a possible ligature.
+    if (!(draw_flags & DRAW_ITALIC)
+           && !((draw_flags & DRAW_BOLD) && gui.font_can_bold)
+           && gui.ascii_glyphs != NULL
+           && !has_unicode
+           && !needs_shaping)
     {
-       if (new_dnode != NULL)
+       for (i = 0; i < len; ++i)
        {
-           if (draw_node_split(new_dnode, col1 - new_dnode->start_col, FALSE)
-                   || draw_node_split(new_dnode,
-                       col2 - new_dnode->start_col + 1, TRUE))
-               g_clear_pointer(&new_dnode, draw_node_unref);
+           PangoGlyphInfo  glyph = gui.ascii_glyphs->glyphs[2 * s[i]];
+           gboolean        has_ink;
+
+           has_ink = glyph_has_ink(gui.ascii_font, glyph);
 
-           draw_row_fill(drow, col1, col2, new_dnode);
-           draw_node_unref(new_dnode);
+           draw_row_add_cluster(drow, dattr, col + i, 1,
+                   gui.ascii_font, has_ink ? &glyph : NULL, 1, self);
        }
+
+       column_offset = len;
+       goto exit;
     }
-    else if (!split)
-       draw_row_fill(drow, col1, col2, dnode);
-}
 
-/*
- * Move the cells between "col1" and "col2" from "src" to "dest", overwriting
- * the existing cells. This will handle clipping any draw nodes.
- */
-    static void
-draw_row_move_to(DrawCell *dest_row, DrawCell *src_row, int col1, int col2)
-{
-    int move_size = (col2 - col1 + 1) * sizeof(DrawCell);
+    glyphs = pango_glyph_string_new();
 
-    // Make sure that we free/truncate any draw nodes before we overwrite
-    // them.
-    draw_row_set(dest_row, col1, col2, NULL, FALSE, FALSE);
+    if (attr_list == NULL)
+       attr_list = pango_attr_list_new();
 
-    // Make sure that draw nodes at the "col1" and "col2" of "src_row" are
-    // clipped so that they all fit in the region being moved.
-    draw_row_set(src_row, col1, col2, NULL, TRUE, FALSE);
+    if (start_idx != -1)
+       INSERT_PANGO_ATTR(pango_attr_font_features_new("liga 0, calt 0"),
+               attr_list, start_idx, len);
 
-    memmove(dest_row + col1, src_row + col1, move_size);
+    cluster_width = PANGO_SCALE * gui.char_width;
+    last_glyph_rbearing = PANGO_SCALE * gui.char_width;
 
-    // Dirty the moved cells
-    for (int c = col1; c <= col2;)
-       if (dest_row[c].dnode != NULL)
-       {
-           (void)draw_node_make_dirty(dest_row[c].dnode);
-           c += dest_row[c].dnode->n_cells;
-       }
-       else
-           c++;
+    // If 'guifontwide' is set then use that for double-width characters.
+    if (gui.wide_font != NULL)
+       apply_wide_font_attr(s, len, attr_list);
 
-    // NULL the draw nodes so we don't double unreference.
-    memset(src_row + col1, 0, (col2 - col1 + 1) * sizeof(DrawCell));
-}
+    if ((draw_flags & DRAW_BOLD) && gui.font_can_bold)
+       INSERT_PANGO_ATTR(pango_attr_weight_new(PANGO_WEIGHT_BOLD),
+               attr_list, 0, len);
+    if (draw_flags & DRAW_ITALIC)
+       INSERT_PANGO_ATTR(pango_attr_style_new(PANGO_STYLE_ITALIC),
+               attr_list, 0, len);
 
-/*
- * Should be called after modifying draw nodes within the given region.
- */
-static void
-vim_draw_area_check_bounds(
-       VimDrawArea *self,
-       int         row1,
-       int         row2,
-       int         col1,
-       int         col2)
-{
-#if defined(FEAT_SIGN_ICONS) || defined(FEAT_NETBEANS_INTG)
-    graphene_rect_t bounds = GRAPHENE_RECT_INIT(
-           FILL_X(col1), FILL_Y(row1),
-           gui.char_width * (col2 - col1 + 1),
-           gui.char_height * (row2 - row1 + 1));
-#endif
+    item_list = pango_itemize(gui.text_context,
+           (const char *)s, 0, len, attr_list, NULL);
 
-    if (self->cursor_node != NULL)
-       // Check if cursor node is within the the updated region. If so, then
-       // remove the render node. This only applies to the part and hollow
-       // cursor, the block cursor will be cleared in draw_row_make_space().
-       if (gui.row >= row1 && gui.row <= row2
-               && gui.col >= col1 && gui.col <= col2)
-           g_clear_pointer(&self->cursor_node, gsk_render_node_unref);
-
-#ifdef FEAT_SIGN_ICONS
-    // Clear any sign icons within the modified block if any
-    for (GList *s = self->signs->head; s != NULL;)
+    while (item_list != NULL)
     {
-       GList           *next = s->next;
-       graphene_rect_t rect;
+       PangoItem   *item = item_list->data;
+       int         item_cells = 0;
+       int         cluster_start_idx = 0;
+       int         current_cluster_col = col + column_offset;
+       int         current_cluster_cells = 0;
+       gboolean    current_cluster_ink = FALSE;
+
+       item_list = g_list_delete_link(item_list, item_list);
 
-       gsk_render_node_get_bounds(s->data, &rect);
+       // Force LTR direction; Vim handles bidi on its own.
+       item->analysis.level = (item->analysis.level + 1) & (~1U);
 
-       if (graphene_rect_contains_rect(&bounds, &rect))
+       pango_shape_full((const char *)s + item->offset, item->length,
+               (const char *)s, len, &item->analysis, glyphs);
+
+       // Fixed-width hack: assign a fixed width to each glyph based on the
+       // number of cells it occupies, handling composing characters and
+       // cluster boundaries properly.
+       //
+       // At the end of each cluster, add it to the draw area.
+       for (i = 0; i < glyphs->num_glyphs; ++i)
        {
-           // Keep going in case there are multiple sign icons within this
-           // block.
-           gsk_render_node_unref(s->data);
-           g_queue_delete_link(self->signs, s);
+           PangoGlyphInfo *glyph;
+
+           glyph = &glyphs->glyphs[i];
+
+           if (glyph->attr.is_cluster_start)
+           {
+               int cellcount;
+
+               cellcount = count_cluster_cells(
+                       s, item, glyphs, i, &cluster_width,
+                       (item_list != NULL) ? &last_glyph_rbearing : NULL);
+
+               if (i > 0 && cellcount > 0)
+               {
+                   draw_row_add_cluster(drow, dattr,
+                           current_cluster_col, current_cluster_cells,
+                           item->analysis.font,
+                           current_cluster_ink ?
+                           glyphs->glyphs + cluster_start_idx : NULL,
+                           i - cluster_start_idx, self);
+
+                   current_cluster_col += current_cluster_cells;
+                   cluster_start_idx = i;
+                   current_cluster_ink = FALSE;
+                   current_cluster_cells = 0;
+               }
+
+               if (cellcount > 0)
+               {
+                   int width;
+
+                   width = cellcount * gui.char_width * PANGO_SCALE;
+                   glyph->geometry.x_offset +=
+                       MAX(0, width - cluster_width) / 2;
+                   glyph->geometry.width = width;
+               }
+               else
+                   setup_zero_width_cluster(item, glyph, cells,
+                           cluster_width,
+                           last_glyph_rbearing);
+
+               item_cells += cellcount;
+               cells = cellcount;
+               current_cluster_cells += cellcount;
+           }
+           else if (i > 0)
+           {
+               int width;
+
+               if (glyph->geometry.x_offset >= 0)
+               {
+                   glyphs->glyphs[i].geometry.width =
+                       glyphs->glyphs[i - 1].geometry.width;
+                   glyphs->glyphs[i - 1].geometry.width = 0;
+               }
+               width = cells * gui.char_width * PANGO_SCALE;
+               glyph->geometry.x_offset +=
+                   MAX(0, width - cluster_width) / 2;
+           }
+           else
+               glyph->geometry.width = 0;
+
+
+           if (!current_cluster_ink)
+               current_cluster_ink = glyph_has_ink(
+                       item->analysis.font, *glyph);
        }
-       s = next;
+       if (glyphs->num_glyphs > 0)
+           draw_row_add_cluster(drow, dattr,
+                   current_cluster_col, current_cluster_cells,
+                   item->analysis.font,
+                   current_cluster_ink ?
+                   glyphs->glyphs + cluster_start_idx : NULL,
+                   glyphs->num_glyphs - cluster_start_idx, self);
+
+       pango_item_free(item);
+       column_offset += item_cells;
     }
-#endif
-#ifdef FEAT_NETBEANS_INTG
-    // Remove multi sign indicator if it is within the modified region.
-    if (self->multisign_node != NULL)
-    {
-       graphene_rect_t rect;
 
-       gsk_render_node_get_bounds(self->multisign_node, &rect);
-       if (graphene_rect_contains_rect(&bounds, &rect))
-           g_clear_pointer(&self->multisign_node, gsk_render_node_unref);
-    }
-#endif
+    pango_glyph_string_free(glyphs);
+
+exit:
+    if (attr_list != NULL)
+       pango_attr_list_unref(attr_list);
+
+    return column_offset;
 }
 
 /*
- * Add the glyph string starting at column "col" in row "row". This will handle
- * any background colours, fake bold, and under decorations. This does not queue
- * a redraw for the widget.
+ * Draw the given string at position (col, row) on the draw area.
  */
-    void
-vim_draw_area_add_glyphs(
+    int
+vim_draw_area_draw_string(
        VimDrawArea *self,
-       int row,
-       int col,
-       int num_cells,
-       int flags,
-       PangoFont *font,
-       PangoGlyphString *glyphs)
+       int         row,
+       int         col,
+       char_u      *s,
+       int         len,
+       int         flags)
 {
-    DrawCell   *drow;
-    DrawNode   *dnode = NULL;
-    int                end_col = col + num_cells - 1;
+    DrawRow    *drow;
+    DrawAttr   dattr;
+    char_u     *conv_buf = NULL;
+    int                convlen;
+    int                n_cells ;
 
-    if (unlikely(self->cells == NULL
+    if (unlikely(self->rows == NULL
                || row >= self->n_rows
-               || col >= self->n_cols
-               || col + num_cells > self->n_cols))
-       return;
-
-    drow = GET_ROW(self, row);
-
-    draw_row_set(drow, col, end_col, NULL, FALSE, TRUE);
+               || col >= self->n_cols))
+       return len;
 
-    // Check if leftmost draw node (if any) has the same visual
-    // attributes/colours as the glyph string being added. If so, then just
-    // extend that draw node with the new glyphs.
-    if (col > 0)
+    if (output_conv.vc_type != CONV_NONE)
     {
-       DrawNode *ldnode = drow[col - 1].dnode;
-
-       // Don't want to try merging draw nodes that are clipped, because the
-       // glyphs in them may not match one to one with the actual bounds of the
-       // draw node.
-       if (ldnode != NULL && !(ldnode->dnode_flags & DRAW_NODE_CLIP)
-               && draw_node_match(ldnode, font, flags))
+       convlen = len;
+       conv_buf = string_convert(&output_conv, s, &convlen);
+       if (conv_buf != NULL)
        {
-           draw_node_extend(ldnode, glyphs->glyphs, glyphs->num_glyphs, FALSE);
-           draw_row_fill(drow, col, end_col, ldnode);
-           ldnode->n_cells += num_cells;
-           dnode = ldnode;
+           s = conv_buf;
+           len = convlen;
        }
     }
 
-    // Check if we can use the existing draw node on the right. If so, then shift
-    // "rdnode" to the "col", and extend it. If we merged the left draw node, then
-    // instead extend it normally and unreference the right draw node.
-    if (col + num_cells < self->n_cols)
+    // Safety check: pango crashes with invalid utf-8.
+    if (!utf_valid_string(s, s + len))
     {
-       DrawNode *rdnode = drow[col + num_cells].dnode;
-
-       if (rdnode != NULL && !(rdnode->dnode_flags & DRAW_NODE_CLIP)
-               && draw_node_match(rdnode, font, flags))
-       {
-           if (dnode != NULL)
-           {
-               assert(rdnode->start_col == col + num_cells);
-               draw_node_extend(dnode, rdnode->glyphs, rdnode->n_glyphs, FALSE);
-               dnode->n_cells += rdnode->n_cells;
-               draw_row_fill(drow, rdnode->start_col, END_COL(rdnode), dnode);
-           }
-           else
-           {
-               draw_node_extend(rdnode, glyphs->glyphs, glyphs->num_glyphs, TRUE);
-               draw_row_fill(drow, col, end_col, rdnode);
-               rdnode->start_col = col;
-               rdnode->n_cells += num_cells;
-               dnode = rdnode;
-           }
-       }
+       vim_free(conv_buf);
+       return len;
     }
 
-    if (dnode != NULL)
-       return;
+    drow = self->rows + row;
+    draw_attr_init(&dattr, gui.bgcolor, gui.fgcolor, gui.spcolor, flags);
 
-    dnode = draw_node_new(
-           font, glyphs->glyphs, glyphs->num_glyphs, gui.bgcolor,
-           gui.fgcolor, gui.spcolor, flags, col, num_cells
-           );
-    draw_row_fill(drow, col, end_col, dnode);
-    draw_node_unref(dnode);
+    n_cells = vim_draw_area_add_string(self, drow, &dattr, col, s, len, flags);
 
-    vim_draw_area_check_bounds(self, row, row, col, col + num_cells - 1);
+    vim_free(conv_buf);
+
+    return n_cells;
 }
 
 /*
@@ -1015,7 +1508,7 @@ vim_draw_area_clear_block(
        int             row2,
        int             col2)
 {
-    if (unlikely(self->cells == NULL
+    if (unlikely(self->rows == NULL
                || row1 >= self->n_rows
                || col1 >= self->n_cols
                || row2 >= self->n_rows
@@ -1023,9 +1516,7 @@ vim_draw_area_clear_block(
        return;
 
     for (int r = row1; r <= row2; r++)
-       draw_row_set(GET_ROW(self, r), col1, col2, NULL, FALSE, FALSE);
-
-    vim_draw_area_check_bounds(self, row1, row2, col1, col2);
+       draw_row_fill(self->rows + r, col1, col2, NULL, NULL, &self->cursor);
 }
 
 /*
@@ -1051,17 +1542,9 @@ vim_draw_area_move_block(
        int         col1,
        int         col2)
 {
+    int offset = row2 - row1;
 
-    int                    offset = row2 - row1;
-#if defined(FEAT_SIGN_ICONS) || defined(FEAT_NETBEANS_INTG)
-    graphene_rect_t bounds = GRAPHENE_RECT_INIT(
-           FILL_X(col1), FILL_Y(row1),
-           gui.char_width * (col2 - col1 + 1),
-           gui.char_height * (row2 - row1 + 1));
-    graphene_rect_t clear_rect;
-#endif
-
-    if (unlikely(self->cells == NULL
+    if (unlikely(self->rows == NULL
                || row1 >= self->n_rows
                || row2 >= self->n_rows
                || to >= self->n_rows
@@ -1078,8 +1561,8 @@ vim_draw_area_move_block(
        // "row1" is below "to", start moving rows starting at "row1". Rows are
        // being shifted upwards.
        for (int o = 0; o <= offset; o++)
-           draw_row_move_to(GET_ROW(self, to + o), GET_ROW(self, row1 + o),
-                   col1, col2);
+           draw_row_move_to(self->rows + to + o, self->rows + row1 + o,
+                   col1, col2, self);
     }
     else
     {
@@ -1089,158 +1572,27 @@ vim_draw_area_move_block(
            if (to + o >= self->n_rows)
                // "src_row" is being "moved" off the screen, no need to move
                // it physically.
-               gui_clear_block(row1 + o, col1, row1 + o, col2);
-           else
-               draw_row_move_to(GET_ROW(self, to + o), GET_ROW(self, row1 + o),
-                       col1, col2);
-    }
-
-    // Do not call vim_draw_area_check_bounds(), because we moved cells, not
-    // modified them.
-
-#if defined(FEAT_SIGN_ICONS) || defined(FEAT_NETBEANS_INTG)
-    if (row1 > to)
-       clear_rect = GRAPHENE_RECT_INIT(
-               FILL_X(col1), FILL_Y(to),
-               gui.char_width * (col2 - col1 + 1),
-               gui.char_height * (row1 - to));
-    else
-       clear_rect = GRAPHENE_RECT_INIT(
-               FILL_X(col1), FILL_Y(row2 + 1),
-               gui.char_width * (col2 - col1 + 1),
-               gui.char_height * (to - row1));
-#endif
-
-#ifdef FEAT_SIGN_ICONS
-    // Move sign icons if they are in the moved region
-    for (GList *s = self->signs->head; s != NULL;)
-    {
-       GList           *next = s->next;
-       GskRenderNode   *node = s->data;
-       graphene_rect_t  rect;
-
-       gsk_render_node_get_bounds(node, &rect);
-
-       // Check if icon moved off screen, if so then remove it.
-       if (graphene_rect_contains_rect(&clear_rect, &rect))
-       {
-           gsk_render_node_unref(s->data);
-           g_queue_delete_link(self->signs, s);
-           s = next;
-           continue;
-       }
-
-       if (graphene_rect_contains_rect(&bounds, &rect))
-       {
-           GdkTexture    *texture;
-           GskRenderNode *new;
-           float          new_y;
-
-           texture = gsk_texture_scale_node_get_texture(node);
-           new_y = graphene_rect_get_y(&rect) - graphene_rect_get_y(&bounds);
-           new_y += FILL_Y(to);
-
-           if (new_y >= 0 && new_y < gtk_widget_get_height(GTK_WIDGET(self)))
-           {
-               rect.origin.y = new_y;
-               new = gsk_texture_scale_node_new(texture, &rect,
-                       GSK_SCALING_FILTER_TRILINEAR);
-               gsk_render_node_unref(node);
-               s->data = new;
-           }
-           else
-           {
-               gsk_render_node_unref(s->data);
-               g_queue_delete_link(self->signs, s);
-           }
-       }
-       s = next;
-    }
-#endif
-#ifdef FEAT_NETBEANS_INTG
-    // Move multisign indicator node if needed
-    if (self->multisign_node != NULL)
-    {
-       graphene_rect_t rect;
-
-       gsk_render_node_get_bounds(self->multisign_node, &rect);
-
-       if (graphene_rect_contains_rect(&clear_rect, &rect))
-           g_clear_pointer(&self->multisign_node, gsk_render_node_unref);
-       else if (graphene_rect_contains_rect(&bounds, &rect))
-       {
-           float new_y =
-               graphene_rect_get_y(&rect) - graphene_rect_get_y(&bounds);
-
-           new_y += FILL_Y(to);
-
-           if (new_y >= 0 && new_y < gtk_widget_get_height(GTK_WIDGET(self)))
-           {
-               cairo_surface_t *surface;
-               GskRenderNode   *new;
-               cairo_t         *cr;
-
-               surface = gsk_cairo_node_get_surface(self->multisign_node);
-               rect.origin.y = new_y;
-               new = gsk_cairo_node_new(&rect);
-               cr = gsk_cairo_node_get_draw_context(new);
-               cairo_set_source_surface(cr, surface, 0, 0);
-               cairo_paint(cr);
-               cairo_destroy(cr);
-
-               gsk_render_node_unref(self->multisign_node);
-               self->multisign_node = new;
-           }
+               vim_draw_area_clear_block(self, row1 + o, col1, row1 + o, col2);
            else
-               g_clear_pointer(&self->multisign_node, gsk_render_node_unref);
-       }
+               draw_row_move_to(self->rows + to + o, self->rows + row1 + o,
+                       col1, col2, self);
     }
-#endif
-}
-
-/*
- * Draw a hollow cursor at the cursor position using the current foreground
- * color. Note that this does not queue a redraw
- */
-    void
-vim_draw_area_set_hollow_cursor(VimDrawArea *self)
-{
-    GskRoundedRect     outline;
-    int                        i = 1;
-    static const float border[4] = {1.0f, 1.0f, 1.0f, 1.0f};
-    const GdkRGBA color[4] = {
-       *gui.fgcolor, *gui.fgcolor,
-       *gui.fgcolor, *gui.fgcolor
-    } ;
-
-    // Double cursor width if double width character
-    if (mb_lefthalve(gui.row, gui.col))
-       i = 2;
-
-    gsk_rounded_rect_init_from_rect(&outline,
-           &GRAPHENE_RECT_INIT(FILL_X(gui.col), FILL_Y(gui.row),
-               i * gui.char_width, gui.char_height),
-           0.0f);
-
-    node_unref(self->cursor_node);
-    self->cursor_node = gsk_border_node_new(&outline, border, color);
 }
 
 /*
- * Draw a part cursor with width "w" and height "h". Note that this does not
- * queue a redraw
+ * Set the cursor to the given width and height, use "gui.bgcolor" and
+ * "gui.fgcolor" for the colors as well. If "w" and "h" is zero, then a full
+ * block cursor is used, if "w" and "h" is -1 then a hollow cursor is used,
+ * otherwise a part cursor with dimensions "w" and "h" is used.
  */
     void
-vim_draw_area_set_part_cursor(VimDrawArea *self, int w, int h)
+vim_draw_area_set_cursor(VimDrawArea *self, int w, int h)
 {
-    node_unref(self->cursor_node);
-    self->cursor_node = gsk_color_node_new(gui.fgcolor,
-           &GRAPHENE_RECT_INIT(
-#ifdef FEAT_RIGHTLEFT
-               CURSOR_BAR_RIGHT ? FILL_X(gui.col + 1) - w :
-#endif
-               FILL_X(gui.col), FILL_Y(gui.row) + gui.char_height - h,
-               w, h));
+    self->cursor.draw = TRUE;
+    self->cursor.width = w;
+    self->cursor.height = h;
+    self->cursor.bg_color = *gui.bgcolor;
+    self->cursor.fg_color = *gui.fgcolor;
 }
 
 /*
@@ -1254,7 +1606,7 @@ vim_draw_area_invert_block(
        int             nrows,
        int             ncols)
 {
-    if (unlikely(self->cells == NULL
+    if (unlikely(self->rows == NULL
                || row >= self->n_rows
                || col >= self->n_cols
                || row + nrows - 1 >= self->n_rows
@@ -1263,14 +1615,12 @@ vim_draw_area_invert_block(
 
     for (int r = row; r < row + nrows; r++)
     {
-       DrawCell *drow = GET_ROW(self, r);
-
-       for (int c = col; c < col + ncols; c++)
-       {
-           DrawCell *dcell = drow + c;
+       DrawRow     *drow = self->rows + r;
+       DrawLayer   *dlayer = drow->dlayers + DRAW_LAYER_OVERLAY;
 
-           dcell->invert = !dcell->invert;
-       }
+       for (int c = 0; c < ncols; c++)
+           dlayer->buf[c + col] = ~dlayer->buf[c + col];
+       draw_row_dirty_layer(drow, DRAW_LAYER_OVERLAY);
     }
 }
 
@@ -1282,41 +1632,83 @@ vim_draw_area_invert_block(
     void
 vim_draw_area_add_sign(
        VimDrawArea *self,
-       GdkTexture *sign,
-       int row,
-       int col,
-       int width,
-       int height)
+       GdkTexture  *sign,
+       int         row,
+       int         col,
+       int         width,
+       int         height)
 {
-    GskRenderNode   *node;
+    DrawRow    *drow;
+    DrawSign   *dsign;
+    DrawGlyphs *dglyphs;
+    int                cells;
 
-    if (unlikely(self->cells == NULL
+    if (unlikely(self->rows == NULL
                || row >= self->n_rows
                || col >= self->n_cols))
        return;
 
-    node = gsk_texture_scale_node_new(sign,
-           &GRAPHENE_RECT_INIT(FILL_X(col), FILL_Y(row), width, height),
-           GSK_SCALING_FILTER_TRILINEAR);
-    if (node == NULL)
-       return;
-    g_queue_push_tail(self->signs, node);
+    drow = self->rows + row;
+    dsign = draw_sign_new(sign, width, height);
+    dglyphs = draw_glyphs_new_sign(dsign);
+    cells = (width + gui.char_width - 1) / gui.char_width;
+
+    draw_row_fill(drow, col, col + cells - 1, dglyphs, NULL, &self->cursor);
+    draw_glyphs_unref(dglyphs);
 }
 #endif
 
 #ifdef FEAT_NETBEANS_INTG
-    cairo_t *
-vim_draw_area_get_multisign_cairo(VimDrawArea *self, int x, int y, int w, int h)
+/*
+ * Add a multi sign indicator at the given position. "surf" should be in ARGB
+ * format.
+ */
+    void
+vim_draw_area_add_multisign(
+       VimDrawArea     *self,
+       cairo_surface_t *surf,
+       int             row,
+       int             col,
+       int             width,
+       int             height)
 {
-    node_unref(self->multisign_node);
-    self->multisign_node = gsk_cairo_node_new(
-           &GRAPHENE_RECT_INIT( x, y, w, h));
-    return gsk_cairo_node_get_draw_context(self->multisign_node);
+    DrawRow    *drow;
+    DrawSign   *dsign;
+    DrawGlyphs *dglyphs;
+    int                cells;
+    GdkTexture *texture;
+    GBytes     *bytes;
+
+    if (unlikely(self->rows == NULL
+               || row >= self->n_rows
+               || col >= self->n_cols))
+       return;
+
+    cairo_surface_flush(surf);
+    bytes = g_bytes_new_with_free_func(cairo_image_surface_get_data(surf),
+           cairo_image_surface_get_height(surf)
+           * cairo_image_surface_get_stride(surf),
+           (GDestroyNotify)cairo_surface_destroy,
+           cairo_surface_reference(surf));
+
+    texture = gdk_memory_texture_new(cairo_image_surface_get_width(surf),
+           cairo_image_surface_get_height(surf),
+           GDK_MEMORY_A8R8G8B8_PREMULTIPLIED,
+           bytes, cairo_image_surface_get_stride(surf));
+    g_bytes_unref(bytes);
+
+    drow = self->rows + row;
+    dsign = draw_sign_new(texture, width, height);
+    g_object_unref(texture);
+    dglyphs = draw_glyphs_new_sign(dsign);
+    cells = (width + gui.char_width - 1) / gui.char_width;
+
+    draw_row_fill(drow, col, col + cells - 1, dglyphs, NULL, &self->cursor);
+    draw_glyphs_unref(dglyphs);
 }
 #endif
 
 #ifdef FEAT_IMAGE_GDK
-
 /*
  * Get the draw image with the given id, return NULL if not exists.
  */
@@ -1381,7 +1773,7 @@ vim_draw_area_add_image(
     GList          *link;
     DrawImage      *dimg;
 
-    if (unlikely(self->cells == NULL
+    if (unlikely(self->rows == NULL
                || row >= self->n_rows
                || col >= self->n_cols))
        return;
@@ -1433,13 +1825,6 @@ vim_draw_area_add_image(
     vim_draw_area_queue_image(self, link);
 }
 
-    static void
-draw_image_free(DrawImage *dimg)
-{
-    gsk_render_node_unref(dimg->node);
-    g_free(dimg);
-}
-
 /*
  * Remove the image with the given id if it exists
  */
@@ -1456,122 +1841,167 @@ vim_draw_area_remove_image(VimDrawArea *self, int id)
 }
 #endif
 
+/*
+ * Add the cursor to the snapshot.
+ */
     static void
-flush_invert_ga(garray_T *invert_ga, int row, int start, int len)
+vim_draw_area_snapshot_cursor(
+       VimDrawArea     *self,
+       GskRenderNode   *text_node,     // May be NULL
+       GskRenderNode   *special_node,  // May be NULL
+       GtkSnapshot     *snapshot)
 {
-    if (ga_grow(invert_ga, 1) == OK)
+    DrawCursor     *cursor = &self->cursor;
+    int                    w = cursor->width;
+    int                    h = cursor->height;
+    graphene_rect_t rect;
+
+    if (!cursor->draw)
+       return;
+
+    if (cursor->width <= 0 && cursor->height <= 0)
+    {
+       // Double width if double width character
+       w += gui.char_width * (1 + mb_lefthalve(gui.row, gui.col));
+       h = gui.char_height;
+    }
+
+    graphene_rect_init(&rect,
+#ifdef FEAT_RIGHTLEFT
+           CURSOR_BAR_RIGHT ? FILL_X(gui.cursor_col + 1) - w :
+#endif
+           FILL_X(gui.cursor_col),
+           FILL_Y(gui.cursor_row) + gui.char_height - h, w, h);
+
+
+    if (cursor->width == -1 && cursor->height == -1)
+    {
+       // Create hollow cursor
+       GskRoundedRect      outline;
+       static const float  border[4] = {1.0f, 1.0f, 1.0f, 1.0f};
+       const GdkRGBA       color[4] = {
+           cursor->bg_color, cursor->bg_color,
+           cursor->bg_color, cursor->bg_color
+       } ;
+
+       gsk_rounded_rect_init_from_rect(&outline, &rect, 0.0f);
+       gtk_snapshot_append_border(snapshot, &outline, border, color);
+    }
+    else
     {
-       graphene_rect_t *arr = (graphene_rect_t *)invert_ga->ga_data;
+       // Create block style cursor, by masking the text + under decorations
+       // with the fg color.
+       gtk_snapshot_append_color(snapshot, &cursor->bg_color, &rect);
 
-       graphene_rect_init(arr + invert_ga->ga_len++,
-               FILL_X(start), FILL_Y(row),
-               len * gui.char_width, gui.char_height);
+       gtk_snapshot_push_mask(snapshot, GSK_MASK_MODE_ALPHA);
+
+       // Used as an "outline" for the fg color.
+       if (text_node != NULL)
+           gtk_snapshot_append_node(snapshot, text_node);
+       if (special_node != NULL)
+           gtk_snapshot_append_node(snapshot, special_node);
+
+       gtk_snapshot_pop(snapshot);
+
+       gtk_snapshot_append_color(snapshot, &cursor->fg_color, &rect);
+       gtk_snapshot_pop(snapshot);
     }
 }
 
     static void
 vim_draw_area_snapshot(GtkWidget *widget, GtkSnapshot *snapshot)
 {
-    VimDrawArea                    *self = VIM_DRAW_AREA(widget);
-    int                            height, width;
-    static const GdkRGBA    white = {1, 1, 1, 1};
-    garray_T               invert_ga;
+    VimDrawArea            *self = VIM_DRAW_AREA(widget);
+    int                    height, width;
+    GtkSnapshot            *body_snapshot;
+    GtkSnapshot            *invert_snapshot = NULL;
+    GskRenderNode   *body_node;
+    GskRenderNode   *invert_node = NULL;
 
     gui_mch_set_bg_color(gui.back_pixel);
     height = gtk_widget_get_height(widget) + gui.bleed_bot;
     width = gtk_widget_get_width(widget) + gui.bleed_right;
 
-    if (self->cells == NULL)
+    if (self->rows == NULL)
     {
        gtk_snapshot_append_color(snapshot, gui.bgcolor,
                &GRAPHENE_RECT_INIT(0, 0, width, height));
        return;
     }
 
-    // If number of pixels to bleed has changed, then dirty the nodes at the
-    // right edge of the draw area.
-    if (self->bleed_right != gui.bleed_right)
+    // If number of pixels to bleed has changed, then dirty the background and
+    // overlay layers of all rows.
+    if (self->bleed_right == -1 || self->bleed_right != gui.bleed_right)
     {
        self->bleed_right = gui.bleed_right;
        for (int r = 0; r < self->n_rows; r++)
        {
-           DrawCell *dcell = &GET_ROW(self, r)[self->n_cols - 1];
-
-           if (dcell->dnode != NULL)
-           {
-               (void)draw_node_make_dirty(dcell->dnode);
-               draw_node_render(dcell->dnode, r, self);
-           }
+           draw_row_dirty_layer(self->rows + r, DRAW_LAYER_BACKGROUND);
+           draw_row_dirty_layer(self->rows + r, DRAW_LAYER_OVERLAY);
        }
     }
 
-    // For inverted cells, we first build an array of bounds that represent
-    // blocks of inverted cells. Then we apply a white color to each of those
-    // bounds and then finish the blend.
-    gtk_snapshot_push_blend(snapshot, GSK_BLEND_MODE_DIFFERENCE);
-    ga_init2(&invert_ga, sizeof(graphene_rect_t), 8);
-
-    gtk_snapshot_append_color(snapshot, gui.bgcolor,
-           &GRAPHENE_RECT_INIT(0, 0, width, height));
+    // First append everything that should be inverted to another snapshot, then
+    // free that snapshot into a node so it can be blended (if needed).
+    body_snapshot = gtk_snapshot_new();
 
     for (int r = 0; r < self->n_rows; r++)
     {
-       DrawCell    *drow = GET_ROW(self, r);
-       int         inv_len = 0;
-       int         inv_start;
-
-       for (int c = 0; c < self->n_cols; c++)
-       {
-           DrawCell    *dcell = drow + c;
-           DrawNode    *dnode = dcell->dnode;
+       DrawRow *drow = self->rows + r;
 
-           // Batch inverted cells as single row rectangles.
-           if (dcell->invert)
-           {
-               if (inv_len == 0)
-                   inv_start = c;
-               inv_len++;
-           }
-           else if (!dcell->invert && inv_len > 0)
-           {
-               flush_invert_ga(&invert_ga, r, inv_start, inv_len);
-               inv_len = 0;
-           }
+       draw_row_render_background(drow, self);
+       draw_row_render_text(drow, self);
+       draw_row_render_special(drow, self);
+       draw_row_render_overlay(drow, self);
 
-           if (dnode == NULL)
-               continue;
+       for (int l = 0; l < N_DRAW_LAYERS; l++)
+       {
+           DrawLayer *dlayer = drow->dlayers + l;
 
-           if (dnode->start_col == c)
+           if (dlayer->node != NULL)
            {
-               draw_node_render(dnode, r, self);
-               assert(dnode->node != NULL);
-               gtk_snapshot_append_node(snapshot, dnode->node);
+               if (l == DRAW_LAYER_OVERLAY)
+               {
+                   if (invert_snapshot == NULL)
+                       invert_snapshot = gtk_snapshot_new();
+                   gtk_snapshot_append_node(invert_snapshot, dlayer->node);
+               }
+               else
+                   gtk_snapshot_append_node(body_snapshot, dlayer->node);
            }
        }
-       // Flush trailing inverted blocks at end of row loop
-       if (inv_len > 0)
-           flush_invert_ga(&invert_ga, r, inv_start, inv_len);
+
+       if (r == gui.cursor_row)
+           vim_draw_area_snapshot_cursor(self,
+                   drow->dlayers[DRAW_LAYER_TEXT].node,
+                   drow->dlayers[DRAW_LAYER_SPECIAL].node,
+                   body_snapshot);
     }
 
-#ifdef FEAT_SIGN_ICONS
-    // Order of where the sign icon should be placed shouldn't matter,
-    // since caller will add whitespace padding in the region it covers.
-    // Probably should put it behind cursor though.
-    for (GList *s = self->signs->head; s != NULL; s = s->next)
-       gtk_snapshot_append_node(snapshot, s->data);
-#endif
+    if (invert_snapshot != NULL)
+       gtk_snapshot_push_blend(snapshot, GSK_BLEND_MODE_DIFFERENCE);
 
-    if (self->cursor_node != NULL)
-       gtk_snapshot_append_node(snapshot, self->cursor_node);
+    gtk_snapshot_append_color(snapshot, gui.bgcolor,
+           &GRAPHENE_RECT_INIT(0, 0, width, height));
+    body_node = gtk_snapshot_free_to_node(body_snapshot);
+    if (body_node != NULL)
+    {
+       gtk_snapshot_append_node(snapshot, body_node);
+       gsk_render_node_unref(body_node);
+    }
 
-    gtk_snapshot_pop(snapshot);
-    for (int i = 0; i < invert_ga.ga_len; i++)
+    if (invert_snapshot != NULL)
     {
-       graphene_rect_t *rect = &((graphene_rect_t *)invert_ga.ga_data)[i];
-       gtk_snapshot_append_color(snapshot, &white, rect);
+       gtk_snapshot_pop(snapshot);
+       invert_node = gtk_snapshot_free_to_node(invert_snapshot);
+       // Not sure if it can be NULL but still check
+       if (invert_node != NULL)
+       {
+           gtk_snapshot_append_node(snapshot, invert_node);
+           gsk_render_node_unref(invert_node);
+       }
+       gtk_snapshot_pop(snapshot);
     }
-    gtk_snapshot_pop(snapshot);
-    ga_clear(&invert_ga);
 
 #ifdef FEAT_IMAGE_GDK
     // Draw images after any possible inversions
index 100d1dba06970a845d4d01ededd6664448b184dd..5cd1cdc5529362c9e46e13a7454e0ae1d683881b 100644 (file)
@@ -21,17 +21,22 @@ G_DECLARE_FINAL_TYPE(VimDrawArea, vim_draw_area, VIM, DRAW_AREA, GtkWidget)
 
 GtkWidget *vim_draw_area_new(void);
 void vim_draw_area_set_size(VimDrawArea *self, int rows, int cols);
-void vim_draw_area_add_glyphs(VimDrawArea *self, int row, int col, int num_cells, int flags, PangoFont *font, PangoGlyphString *glyphs);
+int vim_draw_area_draw_string(VimDrawArea *self, int row, int col, char_u *s, int len, int flags);
 void vim_draw_area_clear_block(VimDrawArea *self, int row1, int col1, int row2, int col2);
 void vim_draw_area_clear(VimDrawArea *self);
 void vim_draw_area_move_block(VimDrawArea *self, int to, int row1, int row2, int col1, int col2);
-void vim_draw_area_set_hollow_cursor(VimDrawArea *self);
-void vim_draw_area_set_part_cursor(VimDrawArea *self, int w, int h);
+void vim_draw_area_set_cursor(VimDrawArea *self, int w, int h);
 void vim_draw_area_invert_block(VimDrawArea *self, int row, int col, int nrows, int ncols);
+# if defined(FEAT_SIGN_ICONS)
 void vim_draw_area_add_sign(VimDrawArea *self, GdkTexture *sign, int row, int col, int width, int height);
-cairo_t *vim_draw_area_get_multisign_cairo(VimDrawArea *self, int x, int y, int w, int h);
-void vim_draw_area_add_image(VimDrawArea *self, GdkTexture  *image, int row, int col, int src_x, int src_y, int draw_w, int draw_h, int zindex, int id);
+# endif
+# ifdef FEAT_NETBEANS_INTG
+void vim_draw_area_add_multisign(VimDrawArea *self, cairo_surface_t *surf, int row, int col, int width, int height);
+# endif
+# ifdef FEAT_IMAGE_GDK
+void vim_draw_area_add_image(VimDrawArea *self, GdkTexture *image, int row, int col, int src_x, int src_y, int draw_w, int draw_h, int zindex, int id);
 void vim_draw_area_remove_image(VimDrawArea *self, int id);
+# endif
 
 #endif
 
index 99fa9549c25457e1b19782f483d57dc7ece78805..abe5d11bc5cac963b15affea61576bf86469a84f 100644 (file)
@@ -3100,16 +3100,25 @@ netbeans_draw_multisign_indicator(int row)
 # else
     GdkDrawable *drawable = gui.drawarea->window;
 # endif
+# ifdef USE_GTK4_SNAPSHOT
+    cairo_surface_t *surf;
+# endif
 
     if (!NETBEANS_OPEN)
        return;
 
     x = 0;
+# ifdef USE_GTK4_SNAPSHOT
+    y = 2;
+# else
     y = row * gui.char_height + 2;
+# endif
 
 # if GTK_CHECK_VERSION(3,0,0)
 #  ifdef USE_GTK4_SNAPSHOT
-    cr = gui_gtk4_get_multisign_context(x, y, 5, gui.char_height);
+    surf = cairo_image_surface_create(
+           CAIRO_FORMAT_ARGB32, 5, gui.char_height);
+    cr = cairo_create(surf);
 #  else
     cr = cairo_create(gui.surface);
 #  endif
@@ -3143,9 +3152,17 @@ netbeans_draw_multisign_indicator(int row)
     gdk_draw_point(drawable, gui.text_gc, x+2, y);
 # endif
 
+# ifdef USE_GTK4_SNAPSHOT
+    cairo_fill(cr);
+    gui_gtk4_add_multisign(surf, row, 0, 5, gui.char_height);
+# endif
+
 # if GTK_CHECK_VERSION(3,0,0)
     cairo_destroy(cr);
 # endif
+# ifdef USE_GTK4_SNAPSHOT
+    cairo_surface_destroy(surf);
+# endif
 }
 #endif // FEAT_GUI_GTK
 
index fddaec21a422f2da22c96bbcca432d1ca32c4621..af55b50fcfd524f15f4c7846c60fa05affcae9ea 100644 (file)
@@ -39,7 +39,7 @@ void gui_mch_set_bg_color(guicolor_T color);
 void gui_mch_set_sp_color(guicolor_T color);
 guicolor_T gui_mch_get_rgb(guicolor_T pixel);
 void gui_gtk4_update_size(void);
-cairo_t *gui_gtk4_get_multisign_context(int x, int y, int w, int h);
+void gui_gtk4_add_multisign(cairo_surface_t *surf, int row, int col, int width, int height);
 void gui_mch_clear_block(int row1, int col1, int row2, int col2);
 void gui_mch_clear_all(void);
 void gui_gtk4_remove_image(win_T *wp);
@@ -48,6 +48,7 @@ bool gui_mch_update_popup_image_pixels(win_T *wp);
 void gui_mch_draw_popup_image(win_T *wp, int row, int col, int src_x, int src_y, int draw_w, int draw_h);
 void gui_mch_delete_lines(int row, int num_lines);
 void gui_mch_insert_lines(int row, int num_lines);
+void gui_gtk4_draw_cursor(guicolor_T bg, guicolor_T fg, int w, int h);
 void gui_mch_draw_hollow_cursor(guicolor_T color);
 void gui_mch_draw_part_cursor(int w, int h, guicolor_T color);
 void gui_mch_flash(int msec);
index 9c342a64190172e86a282347bf634ed225c797e6..182dd1d2405dee094b530c5543baf31a68587c6b 100644 (file)
@@ -759,6 +759,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    828,
 /**/
     827,
 /**/