This commit is contained in:
Michael Grant
2026-09-28 15:05:32 +01:00
parent 3ade9c0d45
commit bfa03e9ef3
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
* deleting them yet - see kitty_redraw_finish(). Deleting immediately here
* would, for an image whose only placements are in this area, leave it
* with none at all until the replacement is placed - some Kitty
* implementations free an image's pixel data once it has no placements
* left, which would leave the replacement referencing already-discarded
* 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.
* Mark placements intersecting a redraw area as stale rather than deleting
* them yet (see kitty_redraw_finish()) - some implementations free an
* image's pixel data once its last placement is gone, so an image only
* placed here would go blank before its replacement lands. A placement only
* partly inside the area is deleted as a whole, so place its outside parts
* again first or they would vanish from cells nothing redraws.
*/
void
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.
*
* 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.
* 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
* adjacent rows and write them as one SIXEL instead of one per row.
* Anything that is not a continuation flushes the run first, and
* image_draw_flush() flushes what's left at the end of the redraw, so no
* other terminal output is reordered across a pending run.
*/
void
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
* after a pane has grown wider.
*
* image_write() (below) only creates spans for as much of an image as fit
* 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.
* Extend existing image placements to reveal more of an image after a pane
* 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
* width later, so this is the only way the rest is ever shown.
*/
void
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) {
lines = sy - (screen_size_y(s) - cy) + 1;
/*
* 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.
*/
/* screen_write_scrollup() clamps lines per call, so loop it. */
region_height = s->rlower - s->rupper + 1;
if (region_height == 0)
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,
app_placement_id, z);
/*
* The rows above origin_y were only ever needed to make the image
* fit on screen at all - screen_write_scrollup() already pushed
* blank rows into history to make room, so those absolute grid rows
* exist and are otherwise unused. Give them spans too, instead of
* 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.
* Give the origin_y rows scrolled into history spans too, so they
* show when scrolled back to instead of appearing blank - cap to
* gd->hsize since screen_write_scrollup() may not have created a
* history row for every one of them.
*/
hist_origin_y = origin_y;
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_spans *spans;
struct redraw_span *span;
enum redraw_image_phase phase;
u_int cy;
int y, top, bottom;
@@ -1670,8 +1671,8 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp,
}
#endif
for (enum redraw_image_phase phase = REDRAW_IMAGES_BEFORE;
phase <= REDRAW_IMAGES_AFTER; phase++) {
for (phase = REDRAW_IMAGES_BEFORE; phase <= REDRAW_IMAGES_AFTER;
phase++) {
for (y = top; y < bottom; y++) {
line = &scene->lines[y];
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_spans *spans;
struct redraw_span *span;
enum redraw_image_phase phase;
u_int y, cy, type;
for (enum redraw_image_phase phase = REDRAW_IMAGES_BEFORE;
phase <= REDRAW_IMAGES_AFTER; phase++) {
for (phase = REDRAW_IMAGES_BEFORE; phase <= REDRAW_IMAGES_AFTER;
phase++) {
for (y = 0; y < scene->sy; y++) {
line = &scene->lines[y];
if (dctx->flags & REDRAW_STATUS_TOP)
@@ -1725,45 +1727,46 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags)
else
cy = y;
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;
if (!REDRAW_IS_ALL(flags)) {
switch (type) {
case REDRAW_SPAN_PANE:
if (~flags & REDRAW_PANE)
if (!REDRAW_IS_ALL(flags)) {
switch (type) {
case REDRAW_SPAN_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;
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];
TAILQ_FOREACH(span, spans, entry)
redraw_draw_span(dctx, span, cy, phase);
spans = &line->spans[type];
TAILQ_FOREACH(span, spans, entry)
redraw_draw_span(dctx, span, cy, phase);
}
}
#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_spans *spans;
struct redraw_span *span;
enum redraw_image_phase phase;
u_int cy, yy, type;
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
/*
* Remove any stale Kitty placements this redraw is about to replace,
* the same as redraw_draw_pane_lines() does for a full pane redraw -
* unlike a plain overwrite of SIXEL pixels, a Kitty placement is a
* discrete object that persists until explicitly deleted, so without
* this a scroll-triggered redraw (this function, not the full-pane
* path) leaves every previous placement behind, all still visible
* 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.
* Remove stale Kitty placements this redraw is about to replace, the
* same as redraw_draw_pane_lines() does for a full pane redraw - a
* placement persists until explicitly deleted, unlike a plain SIXEL
* overwrite. Every span type is included, not just panes, since a
* moved floating pane can leave a placement over cells that now
* belong to something else.
*/
for (yy = y; yy < y + sy; 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
for (enum redraw_image_phase phase = REDRAW_IMAGES_BEFORE;
phase <= REDRAW_IMAGES_AFTER; phase++) {
for (phase = REDRAW_IMAGES_BEFORE; phase <= REDRAW_IMAGES_AFTER;
phase++) {
for (yy = y; yy < y + sy; yy++) {
line = &scene->lines[yy];
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
cy = yy;
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;
if (type == REDRAW_SPAN_STATUS)
continue;
spans = &line->spans[type];
TAILQ_FOREACH(span, spans, entry) {
if (span->x >= x + sx)
continue;
if (span->x + span->width <= x)
continue;
if (type == REDRAW_SPAN_STATUS) {
redraw_damage_refresh_status(dctx,
span->data.st.wp);
}
redraw_draw_span(dctx, span, cy, phase);
if (phase == REDRAW_TEXT && type == REDRAW_SPAN_PANE) {
redraw_damage_draw_pane_prompt(dctx,
span, cy);
spans = &line->spans[type];
TAILQ_FOREACH(span, spans, entry) {
if (span->x >= x + sx)
continue;
if (span->x + span->width <= x)
continue;
redraw_draw_span(dctx, span, cy, phase);
if (phase == REDRAW_TEXT &&
type == REDRAW_SPAN_PANE) {
redraw_damage_draw_pane_prompt(
dctx, span, cy);
}
}
}
}
}
#ifdef ENABLE_IMAGES
image_draw_flush(&scene->c->tty);
#endif

6
tmux.h
View File

@@ -1805,12 +1805,12 @@ struct tty_term {
#define TERM_SIXEL 0x40
#define TERM_INVALIDMS 0x80
#define TERM_KITTY 0x100
#define TERM_NOREPLACE 0x200
#ifdef ENABLE_IMAGES
#define TERM_IMAGE_QUADRANTS 0x200
#define TERM_IMAGE_SEXTANTS 0x400
#define TERM_IMAGE_QUADRANTS 0x400
#define TERM_IMAGE_SEXTANTS 0x800
#define TERM_IMAGESCROLL 0x1000
#endif
#define TERM_NOREPLACE 0x800
int flags;
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));
/*
* 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.
* Features might have changed since the first draw during attach, for
* example on a DA response - but only redraw if something actually
* did change, since this is called for every DA answer (which are
* usually confirming what is already known) and a redraw repaints
* from tmux's own grid, discarding anything the pane put on the
* terminal that tmux does not model (e.g. a DCS-passthrough image).
*/
if (!changed)
return;

View File

@@ -5241,21 +5241,10 @@ window_copy_write_one(struct window_mode_entry *wme,
#ifdef ENABLE_IMAGES
/*
* Write image-covered cells directly into the grid,
* skipping both window_copy_update_style() (so a
* selection, current-line or search-mark highlight
* never sweeps visibly over the image before it is
* recomposited separately - see the
* 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.
* skipping window_copy_update_style() (a highlight
* must not sweep over the image) and
* screen_write_cell() (its image-damage call would
* re-damage the image on every redraw for nothing).
*/
if (image_grid_check_area(gd, fx, fy, gc.data.width,
1)) {
@@ -5516,18 +5505,11 @@ window_copy_write_line(struct window_mode_entry *wme,
#ifdef ENABLE_IMAGES
/*
* Copy the backing line's image layers separately from its text
* cells. This is not implied by the text cells just written above:
* those go via the pane's normal (frequently fast, direct-write)
* path, which knows nothing about image content.
*
* 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.
* Copy the backing line's image layers separately: the text write
* above knows nothing about image content. Only redraw them when the
* view has actually moved (data->image_refresh) - otherwise they are
* already correct and redrawing would just flash them on every
* unrelated redraw.
*/
image_grid_free_line(s->grid,
&s->grid->linedata[s->grid->hsize + py]);
@@ -5600,17 +5582,10 @@ window_copy_redraw_selection(struct window_mode_entry *wme, u_int old_y)
#ifdef ENABLE_IMAGES
/*
* Only rows whose underlying history position has moved since the last
* call need their images recomposited - most redraws are just a selection
* or cursor-line style change with the view otherwise unmoved, and that
* never touches image-covered cells (see window_copy_write_one()), so
* redrawing images for it is needless: on a fast drag it is visible as the
* 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
* call need their images recomposited, to avoid flashing them on every
* unrelated redraw. Every caller of window_copy_write_line()/
* window_copy_write_lines() must call this first - it is not implied by
* them, since some write directly rather than via
* window_copy_redraw_lines().
*/
static void
@@ -5626,14 +5601,9 @@ window_copy_update_image_refresh(struct window_copy_mode_data *data)
/*
* Whether any part of the currently visible backing range carries image
* data. A terminal's line insert/delete only shifts character cells - the
* pixels of a sixel or Kitty image already on screen stay exactly where
* they were sent, so a scroll that uses that fast path (see
* 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.
* data. A scrolled insert/delete-line fast path only shifts character
* cells, leaving image content stale, so callers should fall back to a
* full window_copy_redraw_screen() when this returns true.
*/
static int
window_copy_visible_has_images(struct window_copy_mode_data *data)