From 9c7c777fd962013d22bae203b0f085f16c50099c Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Tue, 4 Aug 2026 08:25:34 +0100 Subject: [PATCH] Keep SIXEL palettes across redraw spans --- image-sixel.c | 184 +++++++++++++++++++++++++++++++++++++++++++++++-- image.c | 23 ++++++- screen-write.c | 5 +- tmux.h | 6 ++ tty.c | 6 ++ 5 files changed, 214 insertions(+), 10 deletions(-) diff --git a/image-sixel.c b/image-sixel.c index 8a7504c24..dc1c4e93a 100644 --- a/image-sixel.c +++ b/image-sixel.c @@ -72,6 +72,22 @@ struct sixel_chunk { char *data; }; +struct sixel_image_cache { + u_int server_id; + u_int xpixel; + u_int ypixel; + size_t size; + uint64_t age; + struct sixel_image *si; + struct sixel_image_cache *next; +}; + +struct sixel_output { + struct sixel_image_cache *images; + size_t size; + uint64_t age; +}; + static int sixel_parse_expand_lines(struct sixel_image *si, u_int y) { @@ -499,6 +515,8 @@ sixel_to_image(struct sixel_image *si) sx, sy, pixels); if (im == NULL) free(pixels); + else + im->sixel = si; return (im); } #endif @@ -1127,21 +1145,175 @@ fail: return (NULL); } +static struct sixel_output * +sixel_get_output(struct tty *tty) +{ + struct sixel_output *so = tty->image_data; + + if (so == NULL) { + so = xcalloc(1, sizeof *so); + tty->image_data = so; + } + return (so); +} + +static size_t +sixel_image_size(struct sixel_image *si) +{ + uint64_t size; + + if ((uint64_t)si->x * si->y > SIZE_MAX / sizeof(uint16_t)) + return (0); + size = (uint64_t)si->x * si->y * sizeof(uint16_t); + if ((uint64_t)si->ncolours * sizeof *si->colours > SIZE_MAX - size) + return (0); + size += (uint64_t)si->ncolours * sizeof *si->colours; + if (size > SIZE_MAX) + return (0); + return (size); +} + +static void +sixel_remove_cache(struct sixel_output *so, struct sixel_image_cache **pp) +{ + struct sixel_image_cache *cache = *pp; + + *pp = cache->next; + so->size -= cache->size; + sixel_free(cache->si); + free(cache); +} + +static void +sixel_collect_images(struct sixel_output *so) +{ + struct sixel_image_cache **pp, *cache; + + for (pp = &so->images; (cache = *pp) != NULL; ) { + if (image_find(cache->server_id) == NULL) + sixel_remove_cache(so, pp); + else + pp = &cache->next; + } +} + +void +sixel_free_output(struct tty *tty, __unused int send) +{ + struct sixel_output *so = tty->image_data; + struct sixel_image_cache *cache, *next; + + if (so == NULL) + return; + for (cache = so->images; cache != NULL; cache = next) { + next = cache->next; + sixel_free(cache->si); + free(cache); + } + free(so); + tty->image_data = NULL; +} + +void +sixel_geometry_changed(struct tty *tty) +{ + sixel_free_output(tty, !!(tty->flags & TTY_OPENED)); +} + +static struct sixel_image * +sixel_render_image(struct image *im, u_int xpixel, u_int ypixel) +{ + /* Preserve SIXEL's original palette and indexed pixels when possible. */ + if (im->sixel != NULL) + return (sixel_scale(im->sixel, xpixel, ypixel, 0, 0, + im->sx, im->sy, 1)); + return (sixel_from_image(im, 0, 0, im->sx, im->sy, xpixel, ypixel)); +} + +static struct sixel_image * +sixel_get_image(struct tty *tty, struct image *im) +{ + struct sixel_output *so = sixel_get_output(tty); + struct sixel_image_cache **pp, *cache, **oldest; + struct sixel_image *si; + size_t size; + + sixel_collect_images(so); + for (cache = so->images; cache != NULL; cache = cache->next) { + if (cache->server_id != im->id || cache->xpixel != tty->xpixel || + cache->ypixel != tty->ypixel) + continue; + cache->age = ++so->age; + return (cache->si); + } + + si = sixel_render_image(im, tty->xpixel, tty->ypixel); + if (si == NULL) + return (NULL); + size = sixel_image_size(si); + if (size == 0 || size > IMAGE_SIZE_LIMIT) { + /* The renderer still has a usable image, but it is not cacheable. */ + return (si); + } + while (so->size > IMAGE_SIZE_LIMIT - size) { + oldest = NULL; + for (pp = &so->images; (cache = *pp) != NULL; + pp = &cache->next) { + if (oldest == NULL || cache->age < (*oldest)->age) + oldest = pp; + } + if (oldest == NULL) + break; + sixel_remove_cache(so, oldest); + } + cache = xcalloc(1, sizeof *cache); + cache->server_id = im->id; + cache->xpixel = tty->xpixel; + cache->ypixel = tty->ypixel; + cache->size = size; + cache->age = ++so->age; + cache->si = si; + cache->next = so->images; + so->images = cache; + so->size += size; + return (si); +} + +static int +sixel_image_is_cached(struct tty *tty, struct sixel_image *si) +{ + struct sixel_output *so = tty->image_data; + struct sixel_image_cache *cache; + + if (so == NULL) + return (0); + for (cache = so->images; cache != NULL; cache = cache->next) { + if (cache->si == si) + return (1); + } + return (0); +} + void sixel_draw_rectangle(struct tty *tty, const struct image_rectangle *rectangle, __unused const struct tty_style_ctx *style_ctx) { - struct sixel_image *si; + struct sixel_image *si, *crop; char *data; size_t size; - si = sixel_from_image(rectangle->image, rectangle->source_x, - rectangle->source_y, rectangle->width, rectangle->height, - tty->xpixel, tty->ypixel); + si = sixel_get_image(tty, rectangle->image); if (si == NULL) return; - data = sixel_print(si, NULL, &size); - sixel_free(si); + crop = sixel_scale(si, tty->xpixel, tty->ypixel, + rectangle->source_x, rectangle->source_y, rectangle->width, + rectangle->height, 1); + if (!sixel_image_is_cached(tty, si)) + sixel_free(si); + if (crop == NULL) + return; + data = sixel_print(crop, NULL, &size); + sixel_free(crop); if (data == NULL) return; tty_region_off(tty); diff --git a/image.c b/image.c index 385c234a6..375dd370b 100644 --- a/image.c +++ b/image.c @@ -36,13 +36,16 @@ struct image_backend { int flags; void (*draw_rectangle)(struct tty *, const struct image_rectangle *, const struct tty_style_ctx *); + void (*free)(struct tty *, int); + void (*geometry_changed)(struct tty *); }; static const struct image_backend image_backend_ascii = { - "ascii", IMAGE_BACKEND_SCROLLS, NULL + "ascii", IMAGE_BACKEND_SCROLLS, NULL, NULL, NULL }; static const struct image_backend image_backend_sixel = { - "sixel", IMAGE_BACKEND_GRAPHICAL, sixel_draw_rectangle + "sixel", IMAGE_BACKEND_GRAPHICAL, sixel_draw_rectangle, + sixel_free_output, sixel_geometry_changed }; static const struct image_backend * @@ -63,6 +66,9 @@ image_tty_update(struct tty *tty) if (tty->image_backend == backend) return; + if (tty->image_backend != NULL && tty->image_backend->free != NULL) + tty->image_backend->free(tty, !!(tty->flags & TTY_OPENED)); + tty->image_data = NULL; tty->image_backend = backend; log_debug("%s: %s image backend is %s", __func__, tty->client->name, backend->name); @@ -86,6 +92,17 @@ void image_tty_geometry_changed(struct tty *tty) { image_tty_update(tty); + if (tty->image_backend->geometry_changed != NULL) + tty->image_backend->geometry_changed(tty); +} + +void +image_tty_free(struct tty *tty, int send) +{ + if (tty->image_backend != NULL && tty->image_backend->free != NULL) + tty->image_backend->free(tty, send); + tty->image_backend = NULL; + tty->image_data = NULL; } static int @@ -249,6 +266,8 @@ image_free(u_int id) log_debug("%s: freeing image %u", __func__, id); RB_REMOVE(images, &images, im); free(im->pixels); + if (im->sixel != NULL) + sixel_free(im->sixel); free(im->cells); free(im); } diff --git a/screen-write.c b/screen-write.c index 6ad1c418c..c11bf94ad 100644 --- a/screen-write.c +++ b/screen-write.c @@ -3029,9 +3029,10 @@ screen_write_sixelimage(struct screen_write_ctx *ctx, struct sixel_image *si, struct image *im; im = sixel_to_image(si); - sixel_free(si); - if (im == NULL) + if (im == NULL) { + sixel_free(si); return; + } image_write(ctx, im, bg); image_free(im->id); } diff --git a/tmux.h b/tmux.h index 32e71c93e..e995b267b 100644 --- a/tmux.h +++ b/tmux.h @@ -1068,6 +1068,8 @@ struct image { size_t stride; size_t size; u_char *pixels; + /* Original indexed SIXEL data, if this image arrived as SIXEL. */ + struct sixel_image *sixel; struct image_cell *cells; /* lazily generated text samples */ RB_ENTRY(image) entry; @@ -1877,6 +1879,7 @@ struct tty { struct tty_key *key_tree; #ifdef ENABLE_IMAGES const struct image_backend *image_backend; + void *image_data; #endif }; @@ -4275,6 +4278,7 @@ int image_tty_is_graphical(struct tty *); int image_tty_scrolls(struct tty *); void 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, u_int, u_int, u_int, const struct tty_style_ctx *); const struct image_cell *image_get_cell(struct image *, u_int, u_int); @@ -4283,6 +4287,8 @@ void image_get_text_cell(struct tty *, struct image *, u_int, const struct tty_style_ctx *); void sixel_draw_rectangle(struct tty *, const struct image_rectangle *, const struct tty_style_ctx *); +void sixel_free_output(struct tty *, int); +void sixel_geometry_changed(struct tty *); #endif #ifdef ENABLE_SIXEL diff --git a/tty.c b/tty.c index 9ae63d0e5..83380930e 100644 --- a/tty.c +++ b/tty.c @@ -517,6 +517,9 @@ tty_close(struct tty *tty) tty_stop_tty(tty); if (tty->flags & TTY_OPENED) { +#ifdef ENABLE_IMAGES + image_tty_free(tty, 1); +#endif evbuffer_free(tty->in); event_del(&tty->event_in); evbuffer_free(tty->out); @@ -533,6 +536,9 @@ void tty_free(struct tty *tty) { tty_close(tty); +#ifdef ENABLE_IMAGES + image_tty_free(tty, 0); +#endif free(tty->r.ranges); }