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 <noreply@anthropic.com>
This commit is contained in:
Michael Grant
2026-09-20 17:59:51 +01:00
parent ec7f768c3a
commit 18c48cf34a
5 changed files with 129 additions and 17 deletions

View File

@@ -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)

20
image.c
View File

@@ -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)

View File

@@ -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. */

4
tmux.h
View File

@@ -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 *);

23
tty.c
View File

@@ -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);