From 77449c0e5c226d5f3b8e3e66009b03341e7bfc4f Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 14 Oct 2026 15:12:45 +0100 Subject: [PATCH 1/2] Remove some code that will not be needed. --- image-sixel.c | 61 ++++++++++----------------------------------------- input.c | 7 ++---- tmux-image.h | 8 +------ 3 files changed, 15 insertions(+), 61 deletions(-) diff --git a/image-sixel.c b/image-sixel.c index 4d500a690..984e42b38 100644 --- a/image-sixel.c +++ b/image-sixel.c @@ -55,7 +55,7 @@ struct sixel_image { u_int x; u_int y; - /* Terminal cell pixel dimensions used for scaling. */ + /* Terminal cell pixel size used for decoding. */ u_int xpixel; u_int ypixel; @@ -65,12 +65,10 @@ struct sixel_image { u_int used_colours; /* SIXEL raster attributes. */ - u_int set_ra; u_int ra_x; u_int ra_y; - /* DCS parameters. */ - u_int p1; + /* DCS background parameter. */ u_int p2; /* Current parser position and colour register. */ @@ -168,6 +166,8 @@ struct sixel_source { u_int out_h; }; +static void sixel_free(struct sixel_image *); + /* Grow a SIXEL image to contain a line. */ static int sixel_parse_expand_lines(struct sixel_image *si, u_int y) @@ -452,8 +452,7 @@ sixel_parse_repeat(struct sixel_image *si, const char *cp, const char *end) /* Parse SIXEL data into an indexed image. */ struct sixel_image * -sixel_parse(const char *buf, size_t len, u_int p1, u_int p2, u_int xpixel, - u_int ypixel) +sixel_parse(const char *buf, size_t len, u_int p2, u_int xpixel, u_int ypixel) { struct sixel_image *si; const char *cp = buf, *end = buf + len; @@ -467,7 +466,6 @@ sixel_parse(const char *buf, size_t len, u_int p1, u_int p2, u_int xpixel, si = xcalloc(1, sizeof *si); si->xpixel = xpixel; si->ypixel = ypixel; - si->p1 = p1; si->p2 = p2; while (cp != end) { @@ -517,7 +515,7 @@ bad: } /* Free a SIXEL image. */ -void +static void sixel_free(struct sixel_image *si) { u_int y; @@ -541,33 +539,6 @@ sixel_size_in_cells(struct sixel_image *si, u_int *x, u_int *y) image_size_in_cells(si->x, si->y, si->xpixel, si->ypixel, x, y); } -/* Log a SIXEL image. */ -void -sixel_log(struct sixel_image *si) -{ - struct sixel_line *sl; - char s[SIXEL_WIDTH_LIMIT + 1]; - u_int i, x, y, cx, cy; - - sixel_size_in_cells(si, &cx, &cy); - log_debug("%s: image %ux%u (%ux%u)", __func__, si->x, si->y, cx, cy); - for (i = 0; i < si->ncolours; i++) - log_debug("%s: colour %u is %07x", __func__, i, si->colours[i]); - for (y = 0; y < si->y; y++) { - sl = &si->lines[y]; - for (x = 0; x < si->x; x++) { - if (x >= sl->x) - s[x] = '_'; - else if (sl->data[x] != 0) - s[x] = '0' + (sl->data[x] - 1) % 10; - else - s[x] = '.'; - } - s[x] = '\0'; - log_debug("%s: %4u: %s", __func__, y, s); - } -} - /* Convert one HLS component to RGB. */ static double sixel_hue(double p, double q, double t) @@ -620,7 +591,7 @@ sixel_colour_to_rgb(u_int c, u_char *r, u_char *g, u_char *b) } /* Convert a SIXEL image into the common RGBA format. */ -struct image * +static struct image * sixel_to_image(struct sixel_image *si) { struct image *im; @@ -785,14 +756,11 @@ sixel_print(struct sixel_image *si, size_t *size) len = 8192; buf = xmalloc(len); - tmplen = xsnprintf(tmp, sizeof tmp, "\033P%u;%uq", si->p1, si->p2); - sixel_print_add(&buf, &len, &used, tmp, tmplen); + sixel_print_add(&buf, &len, &used, "\033P9;1q", 6); - if (si->set_ra) { - tmplen = xsnprintf(tmp, sizeof tmp, "\"1;1;%u;%u", si->ra_x, - si->ra_y); - sixel_print_add(&buf, &len, &used, tmp, tmplen); - } + tmplen = xsnprintf(tmp, sizeof tmp, "\"1;1;%u;%u", si->ra_x, + si->ra_y); + sixel_print_add(&buf, &len, &used, tmp, tmplen); /* Track colour planes containing pixels in the current six-row band. */ planes = xcalloc(used_colours, sizeof *planes); @@ -1221,11 +1189,6 @@ sixel_make_image(struct image *im, u_int cx, u_int cy, u_int csx, u_int csy, /* Create the indexed image with a SIXEL RGB palette. */ si = xcalloc(1, sizeof *si); - si->xpixel = cw; - si->ypixel = ch; - si->p1 = 9; - si->p2 = 1; - si->set_ra = 1; si->ra_x = sx; si->ra_y = sy; si->ncolours = si->used_colours = nc; @@ -1273,7 +1236,7 @@ fail: } /* Encode part of an image as SIXEL for a terminal cell size. */ -char * +static char * sixel_from_image(struct image *im, u_int x, u_int y, u_int sx, u_int sy, u_int cw, u_int ch, size_t *size) { diff --git a/input.c b/input.c index 0cc3085e8..5759e976d 100644 --- a/input.c +++ b/input.c @@ -2632,7 +2632,7 @@ input_dcs_dispatch(struct input_ctx *ictx) #ifdef ENABLE_IMAGES struct window *w; struct sixel_image *si; - int p1, p2; + int p2; #endif if (wp == NULL) @@ -2650,13 +2650,10 @@ input_dcs_dispatch(struct input_ctx *ictx) w = wp->window; if (input_split(ictx) != 0) return (0); - p1 = input_get(ictx, 0, 0, 0); - if (p1 == -1) - p1 = 0; p2 = input_get(ictx, 1, 0, 0); if (p2 == -1) p2 = 0; - si = sixel_parse(buf, len, p1, p2, w->xpixel, w->ypixel); + si = sixel_parse(buf, len, p2, w->xpixel, w->ypixel); if (si != NULL) sixel_write(sctx, si, ictx->cell.cell.bg); } diff --git a/tmux-image.h b/tmux-image.h index e3873a8e7..625a67211 100644 --- a/tmux-image.h +++ b/tmux-image.h @@ -146,15 +146,9 @@ const struct grid_cell *fallback_map_cell(struct tty *, struct grid *, u_int, #define SIXEL_COLOUR_REGISTERS 1024 int sixel_select(struct tty *); int sixel_get_flags(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 *); -void sixel_log(struct sixel_image *); -struct image *sixel_to_image(struct sixel_image *); +struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int); void sixel_write(struct screen_write_ctx *, struct sixel_image *, u_int); -char *sixel_from_image(struct image *, u_int, u_int, u_int, u_int, - u_int, u_int, size_t *); void sixel_draw_rect(struct tty *, struct image_rect *); void sixel_start_draw(struct tty *, u_int, u_int, u_int, u_int); void sixel_free_pending(struct tty *); From 5213cbf64442fe801f03ae99d4adf2b8a1970cfa Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Wed, 14 Oct 2026 15:31:25 +0100 Subject: [PATCH 2/2] Add SIXEL cache. --- image-sixel.c | 177 ++++++++++++++++++++++++++++++++++- regress/image-sixel-cache.sh | 78 +++++++++++++++ 2 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 regress/image-sixel-cache.sh diff --git a/image-sixel.c b/image-sixel.c index 984e42b38..0eb2f5eb5 100644 --- a/image-sixel.c +++ b/image-sixel.c @@ -40,6 +40,9 @@ #define SIXEL_HGRAM_SIZE \ (SIXEL_HGRAM_LEVELS * SIXEL_HGRAM_LEVELS * SIXEL_HGRAM_LEVELS) +/* Maximum number of bytes in the SIXEL cache. */ +#define SIXEL_CACHE_SIZE_LIMIT IMAGE_SIZE_LIMIT + /* Line of a SIXEL image. */ struct sixel_line { /* Number of pixels in this row. */ @@ -115,6 +118,25 @@ struct sixel_pending { u_int dst_y; }; +/* Item in the cache. */ +struct sixel_item { + u_int id; + struct sixel_image *si; + size_t size; + + u_int cw; + u_int ch; + + RB_ENTRY(sixel_item) tree_entry; + TAILQ_ENTRY(sixel_item) entry; +}; + +RB_HEAD(sixel_tree, sixel_item); +TAILQ_HEAD(sixel_items, sixel_item); +static struct sixel_tree sixel_tree = RB_INITIALIZER(&sixel_tree); +static struct sixel_items sixel_cache = TAILQ_HEAD_INITIALIZER(sixel_cache); +static size_t sixel_cache_size; + /* RGB histogram entry. */ struct sixel_hgram { u_int count; @@ -168,6 +190,26 @@ struct sixel_source { static void sixel_free(struct sixel_image *); +/* SIXEL cache item comparison. */ +static int +sixel_cache_cmp(struct sixel_item *a, struct sixel_item *b) +{ + if (a->id < b->id) + return (-1); + if (a->id > b->id) + return (1); + if (a->cw < b->cw) + return (-1); + if (a->cw > b->cw) + return (1); + if (a->ch < b->ch) + return (-1); + if (a->ch > b->ch) + return (1); + return (0); +} +RB_GENERATE_STATIC(sixel_tree, sixel_item, tree_entry, sixel_cache_cmp); + /* Grow a SIXEL image to contain a line. */ static int sixel_parse_expand_lines(struct sixel_image *si, u_int y) @@ -1235,15 +1277,144 @@ fail: return (NULL); } -/* Encode part of an image as SIXEL for a terminal cell size. */ +/* Return the number of bytes allocated for an indexed SIXEL image. */ +static size_t +sixel_image_size(struct sixel_image *si) +{ + return (sizeof *si + si->y * sizeof *si->lines + si->allocated + + si->ncolours * sizeof *si->colours); +} + +/* Remove an image from the global SIXEL output cache. */ +static void +sixel_remove_cache(struct sixel_item *cache) +{ + RB_REMOVE(sixel_tree, &sixel_tree, cache); + TAILQ_REMOVE(&sixel_cache, cache, entry); + sixel_cache_size -= cache->size; + sixel_free(cache->si); + free(cache); +} + +/* Drop SIXEL cache entries whose source images have gone away. */ +static void +sixel_collect_images(void) +{ + struct sixel_item *cache, *next; + + TAILQ_FOREACH_SAFE(cache, &sixel_cache, entry, next) { + if (image_find(cache->id) == NULL) + sixel_remove_cache(cache); + } +} + +/* Get a cached image at the given cell size. */ +static struct sixel_image * +sixel_get_image(struct image *im, u_int cw, u_int ch) +{ + struct sixel_image *si; + size_t size; + u_int id = image_get_id(im), sx, sy; + struct sixel_item *cache, find = { .id = id, .cw = cw, .ch = ch }; + + cache = RB_FIND(sixel_tree, &sixel_tree, &find); + if (cache != NULL) { + TAILQ_REMOVE(&sixel_cache, cache, entry); + TAILQ_INSERT_TAIL(&sixel_cache, cache, entry); + return (cache->si); + } + image_get_size_in_cells(im, &sx, &sy); + + si = sixel_make_image(im, 0, 0, sx, sy, cw, ch); + if (si == NULL) + return (NULL); + + size = (sizeof *cache) + sixel_image_size(si); + if (size > SIXEL_CACHE_SIZE_LIMIT) { + sixel_free(si); + return (NULL); + } + if (sixel_cache_size > SIXEL_CACHE_SIZE_LIMIT - size) + sixel_collect_images(); + while (sixel_cache_size > SIXEL_CACHE_SIZE_LIMIT - size) + sixel_remove_cache(TAILQ_FIRST(&sixel_cache)); + + cache = xcalloc(1, sizeof *cache); + cache->id = id; + cache->cw = cw; + cache->ch = ch; + cache->size = size; + cache->si = si; + RB_INSERT(sixel_tree, &sixel_tree, cache); + TAILQ_INSERT_TAIL(&sixel_cache, cache, entry); + + sixel_cache_size += size; + return (si); +} + +/* Copy a rectangle from a SIXEL image. */ +static struct sixel_image * +sixel_crop(struct sixel_image *si, u_int cx, u_int cy, u_int csx, u_int csy, + u_int cw, u_int ch) +{ + struct sixel_image *new; + struct sixel_line *src, *dst; + uint64_t left, right, top, bottom; + u_int x, y, w, h, i; + + left = (uint64_t)cx * cw; + top = (uint64_t)cy * ch; + right = ((uint64_t)cx + csx) * cw; + bottom = ((uint64_t)cy + csy) * ch; + if (right > si->ra_x) + right = si->ra_x; + if (bottom > si->ra_y) + bottom = si->ra_y; + if (right <= left || bottom <= top) + return (NULL); + x = left; + y = top; + w = right - left; + h = bottom - top; + + new = xcalloc(1, sizeof *new); + new->x = new->ra_x = w; + new->y = new->ra_y = h; + new->lines = xcalloc(h, sizeof *new->lines); + for (i = 0; i < h; i++) { + if (y + i >= si->y) + break; + src = &si->lines[y + i]; + if (x >= src->x) + continue; + dst = &new->lines[i]; + dst->x = src->x - x; + if (dst->x > w) + dst->x = w; + dst->data = xmalloc(dst->x * sizeof *dst->data); + memcpy(dst->data, src->data + x, dst->x * sizeof *dst->data); + new->allocated += dst->x * sizeof *dst->data; + } + new->used_colours = si->used_colours; + new->ncolours = si->ncolours; + new->colours = xmalloc(new->ncolours * sizeof *new->colours); + memcpy(new->colours, si->colours, new->ncolours * sizeof *new->colours); + return (new); +} + +/* Encode part of an image as SIXEL. */ static char * sixel_from_image(struct image *im, u_int x, u_int y, u_int sx, u_int sy, u_int cw, u_int ch, size_t *size) { - struct sixel_image *si; + struct sixel_image *si, *cached; char *data; - si = sixel_make_image(im, x, y, sx, sy, cw, ch); + cached = sixel_get_image(im, cw, ch); + if (cached != NULL) + si = sixel_crop(cached, x, y, sx, sy, cw, ch); + else + si = sixel_make_image(im, x, y, sx, sy, cw, ch); if (si == NULL) return (NULL); data = sixel_print(si, size); diff --git a/regress/image-sixel-cache.sh b/regress/image-sixel-cache.sh new file mode 100644 index 000000000..6eb8a3123 --- /dev/null +++ b/regress/image-sixel-cache.sh @@ -0,0 +1,78 @@ +#!/bin/sh + +# Check that cached colours and dithering survive clipping and client changes, +# and that a visible crop is drawn when the whole scaled image is too large. + +PATH=/bin:/usr/bin +TERM=screen +export TERM +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +python3 -B - "$TEST_TMUX" <<'PY' +import sys +from image_test import image_test, sixel, check_image + +levels = [0, 14, 29, 43, 57, 71, 86, 100] +palette = [(r, g, b) for r in levels for g in levels for b in levels] +wire = sixel(32, 16, palette, list(range(512))) + +with image_test(sys.argv[1]) as test: + first = test.attach() + test.emit(wire) + original = first.images() + assert len(original) == 1 and original[0][:2] == (32, 16) + + # Clients with the same cell size draw identical indexed pixels. + second = test.attach() + test.redraw() + assert first.images() == second.images() == original + + # A different cell size must not replace the first clients' result. + small = test.attach(cw=4, ch=8) + test.redraw() + scaled = small.images() + assert len(scaled) == 1 and scaled[0][:2] == (16, 8) + assert first.images() == second.images() == original + + # Overwrite the first cell. The remaining pixels retain the full palette + # and dithering, rather than converting the smaller rectangle again. + test.emit(b'\033[HX') + expected = [original[0][2][y * 32 + x] + for y in range(16) for x in range(8, 32)] + for client in (first, second): + images = client.images() + assert len(images) == 1 + check_image(images[0], 24, 16, expected) + small_expected = [scaled[0][2][y * 16 + x] + for y in range(8) for x in range(4, 16)] + images = small.images() + assert len(images) == 1 + check_image(images[0], 12, 8, small_expected) + + # Changing back to an earlier cell size reuses the same indexed pixels. + test.resize(small, 40, 16) + test.redraw() + for client in (first, second, small): + images = client.images() + assert len(images) == 1 + check_image(images[0], 24, 16, expected) + + # Replacing the source must not draw pixels from the deleted image. + test.emit(b'\033[2J\033[H' + + sixel(8, 16, [(100, 0, 0)], [0] * (8 * 16))) + for client in (first, second, small): + images = client.images() + assert len(images) == 1 + check_image(images[0], 8, 16, [(255, 0, 0)] * (8 * 16)) + +with image_test(sys.argv[1], sx=1) as test: + client = test.attach() + test.emit(sixel(16, 16, [(100, 0, 0)], [0] * (16 * 16))) + # The whole image would be 12000 pixels wide, beyond the SIXEL limit. + # Its visible first cell is still valid and must be converted directly. + test.resize(client, 1, 16, cw=6000, ch=16) + test.redraw() + images = client.images() + assert len(images) == 1 + check_image(images[0], 6000, 16, [(255, 0, 0)] * (6000 * 16)) +PY