From 18c48cf34af4710852569d925bdadab337cea7bf Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Sun, 20 Sep 2026 17:59:51 +0100 Subject: [PATCH] sixel: coalesce vertically adjacent rows into one SIXEL write The redraw loop hands images to the sixel backend one grid line at a time, so a placement N rows tall arrives as N separate one-row rectangles - each a full scale/encode pass and its own DCS sequence carrying its own copy of the palette. Hold a run of vertically adjacent rows back in struct sixel_output and write it as a single SIXEL once the run ends (image_draw_flush, called from the redraw loop after each pane/line pass). Also make image_tty_update() report whether the backend actually changed, and skip the client redraw in tty_update_features() when it didn't. That function runs on every DA/secondary DA/extended DA answer, most of which just confirm what's already known; the redraw it triggers repaints from tmux's grid and discards anything a pane wrote via DCS passthrough that tmux doesn't model - visibly flashing images away right after they're drawn. Ported from paranoidi's fork (commit 98cc26d) onto the damage-rectangle based redraw introduced on this branch; the three image_draw_flush() call sites are placed at the equivalent point in redraw_draw_pane_lines(), redraw_draw_lines(), and redraw_draw_damage_rect(). Verified against tmux/tmux#5445. Co-Authored-By: Claude Sonnet 5 --- image-sixel.c | 90 +++++++++++++++++++++++++++++++++++++++++++------ image.c | 20 +++++++++-- screen-redraw.c | 9 +++++ tmux.h | 4 ++- tty.c | 23 +++++++++++-- 5 files changed, 129 insertions(+), 17 deletions(-) diff --git a/image-sixel.c b/image-sixel.c index 661308d4f..9d0921506 100644 --- a/image-sixel.c +++ b/image-sixel.c @@ -109,11 +109,27 @@ struct sixel_image_cache { struct sixel_image_cache *next; }; +/* + * Contiguous rows of one placement, held back so that they can be written as + * a single SIXEL instead of one per row. See sixel_draw_rect. + */ +struct sixel_pending { + struct image *image; + u_int source_x; + u_int source_y; + u_int width; + u_int height; + u_int destination_x; + u_int destination_y; +}; + struct sixel_output { /* Per-terminal cached images and aggregate cache state. */ struct sixel_image_cache *images; size_t size; uint64_t age; + /* Rows waiting to be written as one SIXEL. */ + struct sixel_pending pending; }; struct sixel_hgram { @@ -1457,6 +1473,8 @@ sixel_free_output(struct tty *tty, __unused int send) if (so == NULL) return; + /* The run is dropped, not written: the geometry it was measured at is gone. */ + so->pending.image = NULL; for (cache = so->images; cache != NULL; cache = next) { next = cache->next; sixel_free(cache->si); @@ -1548,24 +1566,26 @@ sixel_image_is_cached(struct tty *tty, struct sixel_image *si) return (0); } -/* Draw an image rectangle with SIXEL output. */ +/* Write the rows held in the pending run as a single SIXEL. */ void -sixel_draw_rect(struct tty *tty, const struct image_rect *rectangle, - __unused const struct tty_style_ctx *style_ctx) +sixel_flush_output(struct tty *tty) { + struct sixel_output *so = tty->image_data; + struct sixel_pending *sp; struct sixel_image *si, *crop; char *data; size_t size; - u_int source_x, source_y, width, height; - u_int destination_x, destination_y; - si = sixel_get_image(tty, image_rect_get_image(rectangle)); + if (so == NULL || so->pending.image == NULL) + return; + sp = &so->pending; + + si = sixel_get_image(tty, sp->image); + sp->image = NULL; if (si == NULL) return; - image_rect_get_coords(rectangle, &source_x, &source_y, &width, - &height, &destination_x, &destination_y); - crop = sixel_scale(si, tty->xpixel, tty->ypixel, - source_x, source_y, width, height, 1); + crop = sixel_scale(si, tty->xpixel, tty->ypixel, sp->source_x, + sp->source_y, sp->width, sp->height, 1); if (!sixel_image_is_cached(tty, si)) sixel_free(si); if (crop == NULL) @@ -1576,7 +1596,7 @@ sixel_draw_rect(struct tty *tty, const struct image_rect *rectangle, return; tty_region_off(tty); tty_margin_off(tty); - tty_cursor(tty, destination_x, destination_y); + tty_cursor(tty, sp->destination_x, sp->destination_y); tty->flags |= TTY_NOBLOCK; tty_putn(tty, data, size, 0); /* SIXEL moves the cursor, but does not change terminal attributes. */ @@ -1584,6 +1604,54 @@ sixel_draw_rect(struct tty *tty, const struct image_rect *rectangle, free(data); } +/* + * Queue an image rectangle for SIXEL output. + * + * The redraw loop hands images to the backend one grid line at a time, so a + * placement 24 rows tall arrives as 24 separate one-row rectangles. Writing + * each of them straight out means 24 scale and encode passes and 24 DCS + * sequences carrying 24 copies of the palette, for what the application sent + * as one image - and terminals have to allocate and composite each one. The + * rows of a placement arrive in order, so hold a run of vertically adjacent + * rows back and write them as one SIXEL when the run ends. + * + * Anything that is not a continuation of the run flushes it first, and + * image_draw_flush at the end of the redraw flushes whatever is left, so no + * other terminal output can be reordered across a pending run. + */ +void +sixel_draw_rect(struct tty *tty, const struct image_rect *rectangle, + __unused const struct tty_style_ctx *style_ctx) +{ + struct sixel_output *so = sixel_get_output(tty); + struct sixel_pending *sp = &so->pending; + struct image *im = image_rect_get_image(rectangle); + u_int source_x, source_y, width, height; + u_int destination_x, destination_y; + + image_rect_get_coords(rectangle, &source_x, &source_y, &width, + &height, &destination_x, &destination_y); + + if (sp->image == im && + sp->source_x == source_x && + sp->width == width && + sp->destination_x == destination_x && + sp->source_y + sp->height == source_y && + sp->destination_y + sp->height == destination_y) { + sp->height += height; + return; + } + + sixel_flush_output(tty); + sp->image = im; + sp->source_x = source_x; + sp->source_y = source_y; + sp->width = width; + sp->height = height; + sp->destination_x = destination_x; + sp->destination_y = destination_y; +} + /* Remove old SIXEL pixels before replaying a dirty image area. */ void sixel_redraw_start(struct tty *tty, u_int x, u_int y, u_int sx, u_int sy) diff --git a/image.c b/image.c index da7290a77..0c240d90f 100644 --- a/image.c +++ b/image.c @@ -120,15 +120,19 @@ image_tty_find_backend(struct tty *tty) return (&image_backend_fallback); } -/* Update a terminal's image backend after its capabilities change. */ -void +/* + * Update a terminal's image backend after its capabilities change. Returns 1 + * if the backend changed (so the caller knows a redraw is actually needed), + * 0 if not. + */ +int image_tty_update(struct tty *tty) { const struct image_backend *backend; backend = image_tty_find_backend(tty); if (tty->image_backend == backend) - return; + return (0); if (tty->image_backend != NULL && tty->image_backend->free != NULL) tty->image_backend->free(tty, !!(tty->flags & TTY_OPENED)); @@ -136,6 +140,7 @@ image_tty_update(struct tty *tty) tty->image_backend = backend; log_debug("%s: %s image backend is %s", __func__, tty->client->name, backend->name); + return (1); } /* Remove Kitty placements which will be replaced by a redraw. */ @@ -150,6 +155,15 @@ image_redraw_start(struct tty *tty, u_int x, u_int y, u_int width, sixel_redraw_start(tty, x, y, width, height); } +/* Write out any image output the backend is still holding back. */ +void +image_draw_flush(struct tty *tty) +{ + image_tty_update(tty); + if (tty->image_backend == &image_backend_sixel) + sixel_flush_output(tty); +} + /* Return the flags for a terminal's image backend. */ int image_backend_flags(struct tty *tty) diff --git a/screen-redraw.c b/screen-redraw.c index 013b04b5b..c591269aa 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1719,6 +1719,9 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp, } } } +#ifdef ENABLE_IMAGES + image_draw_flush(&scene->c->tty); +#endif } } @@ -1783,6 +1786,9 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags) phase); } } +#ifdef ENABLE_IMAGES + image_draw_flush(&scene->c->tty); +#endif } } @@ -2329,6 +2335,9 @@ redraw_draw_damage_rect(struct redraw_draw_ctx *dctx, u_int x, u_int y, } } } +#ifdef ENABLE_IMAGES + image_draw_flush(&scene->c->tty); +#endif } /* SIXEL image output may disturb status cells; compose them last. */ diff --git a/tmux.h b/tmux.h index 9d64ac00f..afffe2311 100644 --- a/tmux.h +++ b/tmux.h @@ -4348,8 +4348,9 @@ void image_redraw_area(struct screen_write_ctx *, u_int, u_int, void image_redraw_all(struct screen_write_ctx *); void image_redraw_scroll(struct screen_write_ctx *, u_int); void image_redraw_start(struct tty *, u_int, u_int, u_int, u_int); +void image_draw_flush(struct tty *); int image_backend_flags(struct tty *); -void image_tty_update(struct tty *); +int image_tty_update(struct tty *); void image_tty_geometry_changed(struct tty *); void image_tty_free(struct tty *, int); void image_draw_line(struct tty *, struct screen *, u_int, u_int, @@ -4412,6 +4413,7 @@ void sixel_draw_rect(struct tty *, const struct image_rect *, const struct tty_style_ctx *); void sixel_redraw_start(struct tty *, u_int, u_int, u_int, u_int); void sixel_free_output(struct tty *, int); +void sixel_flush_output(struct tty *); struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int, u_int); void sixel_free(struct sixel_image *); diff --git a/tty.c b/tty.c index f7a797cef..ea73f0ed7 100644 --- a/tty.c +++ b/tty.c @@ -569,11 +569,15 @@ void tty_update_features(struct tty *tty) { struct client *c = tty->client; + int changed = 0; - if (tty_apply_features(tty->term)) + if (tty_apply_features(tty->term)) { tty_term_apply_overrides(tty->term); + changed = 1; + } #ifdef ENABLE_IMAGES - image_tty_update(tty); + if (image_tty_update(tty)) + changed = 1; #endif if (tty_use_margin(tty)) @@ -588,7 +592,22 @@ tty_update_features(struct tty *tty) /* * Features might have changed since the first draw during attach. For * example, this happens when DA responses are received. + * + * Only redraw when something actually did change. This function is + * called for every DA, secondary DA and extended DA answer, and from + * the start timer when none arrive - answers which usually just + * confirm what is already known, either from a previous answer or + * from terminal-features in the configuration. The redraw is not + * free: it repaints the pane from tmux's own grid, which discards + * anything the pane put on the terminal that tmux does not model, + * notably an image written through DCS passthrough. That makes an + * unnecessary redraw here visible to the user as an image that + * appears and then vanishes a moment later, once per client, with no + * way for the application to detect it and redraw. */ + if (!changed) + return; + server_redraw_client(c); tty_invalidate(tty);