This commit is contained in:
Michael Grant
2026-09-28 15:05:32 +01:00
parent 593f0af4cd
commit 45dc8ea40d
7 changed files with 112 additions and 204 deletions

View File

@@ -237,17 +237,12 @@ kitty_redraw_keep(struct tty *tty, struct kitty_image_cache *entry,
} }
/* /*
* Mark Kitty placements intersecting a redraw area as stale, without * Mark placements intersecting a redraw area as stale rather than deleting
* deleting them yet - see kitty_redraw_finish(). Deleting immediately here * them yet (see kitty_redraw_finish()) - some implementations free an
* would, for an image whose only placements are in this area, leave it * image's pixel data once its last placement is gone, so an image only
* with none at all until the replacement is placed - some Kitty * placed here would go blank before its replacement lands. A placement only
* implementations free an image's pixel data once it has no placements * partly inside the area is deleted as a whole, so place its outside parts
* left, which would leave the replacement referencing already-discarded * again first or they would vanish from cells nothing redraws.
* data and render as nothing.
*
* A placement only partly inside the area is deleted as a whole, but the
* redraw only replaces the part inside, so the parts outside are placed
* again first - otherwise they would vanish from cells nothing redraws.
*/ */
void void
kitty_redraw_start(struct tty *tty, u_int x, u_int y, u_int width, kitty_redraw_start(struct tty *tty, u_int x, u_int y, u_int width,

View File

@@ -1605,19 +1605,12 @@ sixel_flush_output(struct tty *tty)
} }
/* /*
* Queue an image rectangle for SIXEL output. * Queue an image rectangle for SIXEL output. The redraw loop hands images to
* * the backend one grid line at a time, so hold back a run of vertically
* The redraw loop hands images to the backend one grid line at a time, so a * adjacent rows and write them as one SIXEL instead of one per row.
* placement 24 rows tall arrives as 24 separate one-row rectangles. Writing * Anything that is not a continuation flushes the run first, and
* each of them straight out means 24 scale and encode passes and 24 DCS * image_draw_flush() flushes what's left at the end of the redraw, so no
* sequences carrying 24 copies of the palette, for what the application sent * other terminal output is reordered across a pending run.
* 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 void
sixel_draw_rect(struct tty *tty, const struct image_rect *rectangle, sixel_draw_rect(struct tty *tty, const struct image_rect *rectangle,

56
image.c
View File

@@ -1563,26 +1563,10 @@ image_extend_row(struct image_line *line, struct image_placement *placement,
} }
/* /*
* Extend existing image placements to reveal more of their original width * Extend existing image placements to reveal more of an image after a pane
* after a pane has grown wider. * grows wider. image_write() only creates spans for as much as fit at the
* * time - unlike height, there is no "scroll right" to recover a clipped
* image_write() (below) only creates spans for as much of an image as fit * width later, so this is the only way the rest is ever shown.
* in the pane at the time it was placed - the rest of the image's pixels
* are still retained (struct image is immutable and kept for as long as
* any placement references it), but nothing ever revisits that clipping
* decision, so a pane that was too narrow when an image was displayed
* stays clipped forever, even after growing wide enough to fit the rest.
* Unlike height, which recovers via ordinary scrollback (image_write()
* scrolls rather than clips when a placement is taller than the pane),
* there is no equivalent "scroll right" - this is the only way the extra
* width is ever recovered.
*
* For every grid row with image spans, this finds each distinct placement
* referenced there, works out how far its spans already reach (source_x +
* width) and its origin column (a span's x - source_x, which is the same
* for every span of the same placement), and adds spans for any newly
* revealed columns up to whichever is smaller: the image's own full width
* or the new pane width.
*/ */
void void
image_grid_resize_width(struct grid *gd, u_int new_sx) image_grid_resize_width(struct grid *gd, u_int new_sx)
@@ -1673,16 +1657,7 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg,
} else if (screen_size_y(s) - cy <= sy) { } else if (screen_size_y(s) - cy <= sy) {
lines = sy - (screen_size_y(s) - cy) + 1; lines = sy - (screen_size_y(s) - cy) + 1;
/* /* screen_write_scrollup() clamps lines per call, so loop it. */
* screen_write_scrollup() clamps its own lines argument to
* at most one scroll region height per call, so a single
* call cannot push more than that much history however
* large lines is - call it repeatedly to actually push the
* full amount, so the history-row loop below (which is
* itself bounded against gd->hsize, so it is safe even if
* this loop's assumptions are ever wrong) has real rows to
* use instead of just whatever fit in one call.
*/
region_height = s->rlower - s->rupper + 1; region_height = s->rlower - s->rupper + 1;
if (region_height == 0) if (region_height == 0)
region_height = 1; region_height = 1;
@@ -1706,23 +1681,10 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg,
placement = image_placement_create(gd, im, input, app_image_id, placement = image_placement_create(gd, im, input, app_image_id,
app_placement_id, z); app_placement_id, z);
/* /*
* The rows above origin_y were only ever needed to make the image * Give the origin_y rows scrolled into history spans too, so they
* fit on screen at all - screen_write_scrollup() already pushed * show when scrolled back to instead of appearing blank - cap to
* blank rows into history to make room, so those absolute grid rows * gd->hsize since screen_write_scrollup() may not have created a
* exist and are otherwise unused. Give them spans too, instead of * history row for every one of them.
* silently discarding that part of the image: unlike width, which
* has no "scroll right" to recover a permanent clip, height already
* has ordinary scrollback - it would be wasted if these rows were
* left with nothing to show when scrolled back to.
*
* screen_write_scrollup() clamps its own lines argument to at most
* one screen height per call, however many were requested, so it
* may not have created a history row for every one of the origin_y
* rows this is trying to place - cap to however many actually
* exist (gd->hsize) to avoid reading before the start of the grid,
* and place the rows closest to the visible area (the highest
* source rows below origin_y) in whatever history space there is,
* since those are the ones the scrolled-off area would show first.
*/ */
hist_origin_y = origin_y; hist_origin_y = origin_y;
if (hist_origin_y > gd->hsize) if (hist_origin_y > gd->hsize)

View File

@@ -1633,6 +1633,7 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp,
struct redraw_line *line; struct redraw_line *line;
struct redraw_spans *spans; struct redraw_spans *spans;
struct redraw_span *span; struct redraw_span *span;
enum redraw_image_phase phase;
u_int cy; u_int cy;
int y, top, bottom; int y, top, bottom;
@@ -1670,8 +1671,8 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp,
} }
#endif #endif
for (enum redraw_image_phase phase = REDRAW_IMAGES_BEFORE; for (phase = REDRAW_IMAGES_BEFORE; phase <= REDRAW_IMAGES_AFTER;
phase <= REDRAW_IMAGES_AFTER; phase++) { phase++) {
for (y = top; y < bottom; y++) { for (y = top; y < bottom; y++) {
line = &scene->lines[y]; line = &scene->lines[y];
if (dctx->flags & REDRAW_STATUS_TOP) if (dctx->flags & REDRAW_STATUS_TOP)
@@ -1714,10 +1715,11 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags)
struct redraw_line *line; struct redraw_line *line;
struct redraw_spans *spans; struct redraw_spans *spans;
struct redraw_span *span; struct redraw_span *span;
enum redraw_image_phase phase;
u_int y, cy, type; u_int y, cy, type;
for (enum redraw_image_phase phase = REDRAW_IMAGES_BEFORE; for (phase = REDRAW_IMAGES_BEFORE; phase <= REDRAW_IMAGES_AFTER;
phase <= REDRAW_IMAGES_AFTER; phase++) { phase++) {
for (y = 0; y < scene->sy; y++) { for (y = 0; y < scene->sy; y++) {
line = &scene->lines[y]; line = &scene->lines[y];
if (dctx->flags & REDRAW_STATUS_TOP) if (dctx->flags & REDRAW_STATUS_TOP)
@@ -1725,45 +1727,46 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags)
else else
cy = y; cy = y;
for (type = 0; type < REDRAW_SPAN_TYPES; type++) { for (type = 0; type < REDRAW_SPAN_TYPES; type++) {
if (phase != REDRAW_TEXT && type != REDRAW_SPAN_PANE) if (phase != REDRAW_TEXT &&
type != REDRAW_SPAN_PANE)
continue; continue;
if (!REDRAW_IS_ALL(flags)) { if (!REDRAW_IS_ALL(flags)) {
switch (type) { switch (type) {
case REDRAW_SPAN_PANE: case REDRAW_SPAN_PANE:
if (~flags & REDRAW_PANE) if (~flags & REDRAW_PANE)
continue;
break;
case REDRAW_SPAN_OUTSIDE:
if (~flags & REDRAW_OUTSIDE)
continue;
break;
case REDRAW_SPAN_EMPTY:
if (~flags & REDRAW_EMPTY)
continue;
break;
case REDRAW_SPAN_BORDER:
if (~flags & REDRAW_PANE_BORDER)
continue;
break;
case REDRAW_SPAN_STATUS:
if (~flags & REDRAW_PANE_STATUS)
continue;
break;
case REDRAW_SPAN_SCROLLBAR:
if (~flags & REDRAW_PANE_SCROLLBAR)
continue;
break;
case REDRAW_SPAN_MENU:
if (~flags & REDRAW_MENU)
continue;
break;
default:
continue; continue;
break; }
case REDRAW_SPAN_OUTSIDE:
if (~flags & REDRAW_OUTSIDE)
continue;
break;
case REDRAW_SPAN_EMPTY:
if (~flags & REDRAW_EMPTY)
continue;
break;
case REDRAW_SPAN_BORDER:
if (~flags & REDRAW_PANE_BORDER)
continue;
break;
case REDRAW_SPAN_STATUS:
if (~flags & REDRAW_PANE_STATUS)
continue;
break;
case REDRAW_SPAN_SCROLLBAR:
if (~flags & REDRAW_PANE_SCROLLBAR)
continue;
break;
case REDRAW_SPAN_MENU:
if (~flags & REDRAW_MENU)
continue;
break;
default:
continue;
} }
} spans = &line->spans[type];
spans = &line->spans[type]; TAILQ_FOREACH(span, spans, entry)
TAILQ_FOREACH(span, spans, entry) redraw_draw_span(dctx, span, cy, phase);
redraw_draw_span(dctx, span, cy, phase);
} }
} }
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
@@ -2210,6 +2213,7 @@ redraw_draw_damage_rectangle(struct redraw_draw_ctx *dctx, u_int x, u_int y,
struct redraw_line *line; struct redraw_line *line;
struct redraw_spans *spans; struct redraw_spans *spans;
struct redraw_span *span; struct redraw_span *span;
enum redraw_image_phase phase;
u_int cy, yy, type; u_int cy, yy, type;
if (x >= scene->sx || y >= scene->sy) if (x >= scene->sx || y >= scene->sy)
@@ -2223,18 +2227,12 @@ redraw_draw_damage_rectangle(struct redraw_draw_ctx *dctx, u_int x, u_int y,
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
/* /*
* Remove any stale Kitty placements this redraw is about to replace, * Remove stale Kitty placements this redraw is about to replace, the
* the same as redraw_draw_pane_lines() does for a full pane redraw - * same as redraw_draw_pane_lines() does for a full pane redraw - a
* unlike a plain overwrite of SIXEL pixels, a Kitty placement is a * placement persists until explicitly deleted, unlike a plain SIXEL
* discrete object that persists until explicitly deleted, so without * overwrite. Every span type is included, not just panes, since a
* this a scroll-triggered redraw (this function, not the full-pane * moved floating pane can leave a placement over cells that now
* path) leaves every previous placement behind, all still visible * belong to something else.
* and now overlapping the newly placed ones.
*
* Every span type is included, not just panes: when a floating pane
* moves, cells where its image was placed can now belong to a border
* (or anything else), and a Kitty placement left there would stay
* drawn over it.
*/ */
for (yy = y; yy < y + sy; yy++) { for (yy = y; yy < y + sy; yy++) {
line = &scene->lines[yy]; line = &scene->lines[yy];
@@ -2258,8 +2256,8 @@ redraw_draw_damage_rectangle(struct redraw_draw_ctx *dctx, u_int x, u_int y,
} }
#endif #endif
for (enum redraw_image_phase phase = REDRAW_IMAGES_BEFORE; for (phase = REDRAW_IMAGES_BEFORE; phase <= REDRAW_IMAGES_AFTER;
phase <= REDRAW_IMAGES_AFTER; phase++) { phase++) {
for (yy = y; yy < y + sy; yy++) { for (yy = y; yy < y + sy; yy++) {
line = &scene->lines[yy]; line = &scene->lines[yy];
if (dctx->flags & REDRAW_STATUS_TOP) if (dctx->flags & REDRAW_STATUS_TOP)
@@ -2267,28 +2265,26 @@ redraw_draw_damage_rectangle(struct redraw_draw_ctx *dctx, u_int x, u_int y,
else else
cy = yy; cy = yy;
for (type = 0; type < REDRAW_SPAN_TYPES; type++) { for (type = 0; type < REDRAW_SPAN_TYPES; type++) {
if (phase != REDRAW_TEXT && type != REDRAW_SPAN_PANE) if (phase != REDRAW_TEXT &&
type != REDRAW_SPAN_PANE)
continue; continue;
if (type == REDRAW_SPAN_STATUS) if (type == REDRAW_SPAN_STATUS)
continue; continue;
spans = &line->spans[type]; spans = &line->spans[type];
TAILQ_FOREACH(span, spans, entry) { TAILQ_FOREACH(span, spans, entry) {
if (span->x >= x + sx) if (span->x >= x + sx)
continue; continue;
if (span->x + span->width <= x) if (span->x + span->width <= x)
continue; continue;
if (type == REDRAW_SPAN_STATUS) { redraw_draw_span(dctx, span, cy, phase);
redraw_damage_refresh_status(dctx, if (phase == REDRAW_TEXT &&
span->data.st.wp); type == REDRAW_SPAN_PANE) {
} redraw_damage_draw_pane_prompt(
redraw_draw_span(dctx, span, cy, phase); dctx, span, cy);
if (phase == REDRAW_TEXT && type == REDRAW_SPAN_PANE) { }
redraw_damage_draw_pane_prompt(dctx,
span, cy);
} }
} }
} }
}
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
image_draw_flush(&scene->c->tty); image_draw_flush(&scene->c->tty);
#endif #endif

6
tmux.h
View File

@@ -1815,12 +1815,12 @@ struct tty_term {
#define TERM_SIXEL 0x40 #define TERM_SIXEL 0x40
#define TERM_INVALIDMS 0x80 #define TERM_INVALIDMS 0x80
#define TERM_KITTY 0x100 #define TERM_KITTY 0x100
#define TERM_NOREPLACE 0x200
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
#define TERM_IMAGE_QUADRANTS 0x200 #define TERM_IMAGE_QUADRANTS 0x400
#define TERM_IMAGE_SEXTANTS 0x400 #define TERM_IMAGE_SEXTANTS 0x800
#define TERM_IMAGESCROLL 0x1000 #define TERM_IMAGESCROLL 0x1000
#endif #endif
#define TERM_NOREPLACE 0x800
int flags; int flags;
LIST_ENTRY(tty_term) entry; LIST_ENTRY(tty_term) entry;

20
tty.c
View File

@@ -585,20 +585,12 @@ tty_update_features(struct tty *tty)
tty_puts(tty, tty_term_string(tty->term, TTYC_ENESC)); tty_puts(tty, tty_term_string(tty->term, TTYC_ENESC));
/* /*
* Features might have changed since the first draw during attach. For * Features might have changed since the first draw during attach, for
* example, this happens when DA responses are received. * example on a DA response - but only redraw if something actually
* * did change, since this is called for every DA answer (which are
* Only redraw when something actually did change. This function is * usually confirming what is already known) and a redraw repaints
* called for every DA, secondary DA and extended DA answer, and from * from tmux's own grid, discarding anything the pane put on the
* the start timer when none arrive - answers which usually just * terminal that tmux does not model (e.g. a DCS-passthrough image).
* 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) if (!changed)
return; return;

View File

@@ -5340,21 +5340,10 @@ window_copy_write_one(struct window_mode_entry *wme,
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
/* /*
* Write image-covered cells directly into the grid, * Write image-covered cells directly into the grid,
* skipping both window_copy_update_style() (so a * skipping window_copy_update_style() (a highlight
* selection, current-line or search-mark highlight * must not sweep over the image) and
* never sweeps visibly over the image before it is * screen_write_cell() (its image-damage call would
* recomposited separately - see the * re-damage the image on every redraw for nothing).
* image_redraw_area() call below) and
* screen_write_cell(), whose built-in
* screen_write_image_damage() call (screen-write.c)
* fires on every write regardless of whether
* anything actually changed, which would needlessly
* re-damage - and so retransmit - the image on every
* redraw. The cell must still land in the grid as
* normal: a non-graphical client's ASCII fallback for
* the image is an ordinary character here, not
* something recomposited separately, and depends on
* this write the same as any other cell.
*/ */
if (image_grid_check_area(gd, fx, fy, gc.data.width, if (image_grid_check_area(gd, fx, fy, gc.data.width,
1)) { 1)) {
@@ -5615,18 +5604,11 @@ window_copy_write_line(struct window_mode_entry *wme,
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
/* /*
* Copy the backing line's image layers separately from its text * Copy the backing line's image layers separately: the text write
* cells. This is not implied by the text cells just written above: * above knows nothing about image content. Only redraw them when the
* those go via the pane's normal (frequently fast, direct-write) * view has actually moved (data->image_refresh) - otherwise they are
* path, which knows nothing about image content. * already correct and redrawing would just flash them on every
* * unrelated redraw.
* Only report it as needing a redraw when the view has actually
* moved (data->image_refresh, set once per window_copy_redraw_lines
* call) - text writes never touch image-covered cells (see
* window_copy_write_one), so if the view is unmoved this row's
* images are already exactly as they should be and redrawing them
* anyway just flashes the image on every unrelated redraw (e.g.
* every step of a selection drag) for no visible benefit.
*/ */
image_grid_free_line(s->grid, image_grid_free_line(s->grid,
&s->grid->linedata[s->grid->hsize + py]); &s->grid->linedata[s->grid->hsize + py]);
@@ -5689,17 +5671,10 @@ window_copy_redraw_selection(struct window_mode_entry *wme, u_int old_y)
#ifdef ENABLE_IMAGES #ifdef ENABLE_IMAGES
/* /*
* Only rows whose underlying history position has moved since the last * Only rows whose underlying history position has moved since the last
* call need their images recomposited - most redraws are just a selection * call need their images recomposited, to avoid flashing them on every
* or cursor-line style change with the view otherwise unmoved, and that * unrelated redraw. Every caller of window_copy_write_line()/
* never touches image-covered cells (see window_copy_write_one()), so * window_copy_write_lines() must call this first - it is not implied by
* redrawing images for it is needless: on a fast drag it is visible as the * them, since some write directly rather than via
* image briefly flashing on every step even though nothing about it
* actually changed.
*
* Every place that calls window_copy_write_line()/window_copy_write_lines()
* must call this first - it is not implied by them, since some (the
* initial full-screen draw on entering copy mode, window_copy_scroll_up(),
* window_copy_scroll_down()) write directly rather than going through
* window_copy_redraw_lines(). * window_copy_redraw_lines().
*/ */
static void static void
@@ -5715,14 +5690,9 @@ window_copy_update_image_refresh(struct window_copy_mode_data *data)
/* /*
* Whether any part of the currently visible backing range carries image * Whether any part of the currently visible backing range carries image
* data. A terminal's line insert/delete only shifts character cells - the * data. A scrolled insert/delete-line fast path only shifts character
* pixels of a sixel or Kitty image already on screen stay exactly where * cells, leaving image content stale, so callers should fall back to a
* they were sent, so a scroll that uses that fast path (see * full window_copy_redraw_screen() when this returns true.
* window_copy_scroll_up()/window_copy_scroll_down()) leaves stale or
* missing image content for every row except the single new one it
* explicitly rewrites. When this returns true, callers should fall back to
* a full window_copy_redraw_screen() instead, so every visible row's image
* is recomposited at its correct new position.
*/ */
static int static int
window_copy_visible_has_images(struct window_copy_mode_data *data) window_copy_visible_has_images(struct window_copy_mode_data *data)