image: never let a Kitty image drop to zero placements mid-redraw

Every redraw that replaces an image's placements (a pane revealed after
an overlapping floating pane closes, a full window redraw, etc.)
deleted all of an image's existing placements before creating their
replacements. That briefly left the image with none at all - and some
Kitty implementations free an image's underlying pixel data once it
has no placements left, so the replacement placements just placed
would then reference already-discarded data and render as nothing: an
empty hole where the image used to be. Not reproducible with SIXEL,
which has no separate placement/data lifecycle to race.

kitty_redraw_start() now only marks placements intersecting a redraw
area as stale instead of deleting them immediately; a new
kitty_redraw_finish() (called via image_redraw_finish() after a
redraw's replacement placements have already been created) deletes
whatever is still marked. The image now always has at least one live
placement throughout the transition.
This commit is contained in:
Michael Grant
2026-09-23 16:35:28 +01:00
parent 54634fad24
commit bbb2dd9454
4 changed files with 74 additions and 11 deletions

View File

@@ -131,6 +131,7 @@ struct kitty_placement_cache {
u_int y;
u_int width;
u_int height;
int pending_delete;
struct kitty_placement_cache *next;
};
@@ -177,13 +178,49 @@ kitty_free_placements(struct kitty_image_cache *entry)
entry->placements = NULL;
}
/* Delete Kitty placements intersecting a redraw area. */
/*
* 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.
*/
void
kitty_redraw_start(struct tty *tty, u_int x, u_int y, u_int width,
u_int height)
{
struct kitty_output *ko = tty->image_data;
struct kitty_image_cache *entry;
struct kitty_output *ko = tty->image_data;
struct kitty_image_cache *entry;
struct kitty_placement_cache *placement;
if (ko == NULL)
return;
for (entry = ko->images; entry != NULL; entry = entry->next) {
for (placement = entry->placements; placement != NULL;
placement = placement->next) {
if (placement->x >= x + width ||
placement->x + placement->width <= x ||
placement->y >= y + height ||
placement->y + placement->height <= y)
continue;
placement->pending_delete = 1;
}
}
}
/*
* Delete placements marked stale by kitty_redraw_start() - called once any
* replacement placements have already been created, so an image already
* placed elsewhere in the same redraw is never left with none at all in
* between the two.
*/
void
kitty_redraw_finish(struct tty *tty)
{
struct kitty_output *ko = tty->image_data;
struct kitty_image_cache *entry;
struct kitty_placement_cache **pp, *placement;
char s[64];
@@ -191,15 +228,12 @@ kitty_redraw_start(struct tty *tty, u_int x, u_int y, u_int width,
return;
for (entry = ko->images; entry != NULL; entry = entry->next) {
for (pp = &entry->placements; (placement = *pp) != NULL; ) {
if (placement->x >= x + width ||
placement->x + placement->width <= x ||
placement->y >= y + height ||
placement->y + placement->height <= y) {
if (!placement->pending_delete) {
pp = &placement->next;
continue;
}
xsnprintf(s, sizeof s,
"\033_Ga=d,d=i,i=%u,p=%u,q=2\033\\", entry->kitty_id,
}
xsnprintf(s, sizeof s,
"\033_Ga=d,d=i,i=%u,p=%u,q=2\033\\", entry->kitty_id,
placement->id);
tty_puts(tty, s);
*pp = placement->next;

14
image.c
View File

@@ -156,6 +156,20 @@ image_redraw_start(struct tty *tty, u_int x, u_int y, u_int width,
sixel_redraw_start(tty, x, y, width, height);
}
/*
* Delete placements marked stale by image_redraw_start() - called once any
* replacement placements a redraw is making have already been created. Only
* meaningful for Kitty (see kitty_redraw_finish()) - SIXEL has no separate
* placement/data distinction for sixel_redraw_start()'s plain erase to
* leave dangling.
*/
void
image_redraw_finish(struct tty *tty)
{
if (tty->image_backend == &image_backend_kitty)
kitty_redraw_finish(tty);
}
/* Write out any image output the backend is still holding back. */
void
image_draw_flush(struct tty *tty)

View File

@@ -1806,6 +1806,10 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp,
image_draw_flush(&scene->c->tty);
#endif
}
#ifdef ENABLE_IMAGES
if (flags & REDRAW_PANE)
image_redraw_finish(&scene->c->tty);
#endif
}
/* Draw lines. */
@@ -2139,8 +2143,13 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
if (wp != NULL)
redraw_draw_pane_lines(&dctx, wp, flags);
else
else {
redraw_draw_lines(&dctx, flags);
#ifdef ENABLE_IMAGES
if (flags & REDRAW_PANE)
image_redraw_finish(tty);
#endif
}
#ifdef ENABLE_IMAGES
if ((flags & REDRAW_PANE) &&
(image_backend_flags(tty) &
@@ -2541,6 +2550,10 @@ redraw_draw_damage_rect(struct redraw_draw_ctx *dctx, u_int x, u_int y,
image_draw_flush(&scene->c->tty);
#endif
}
#ifdef ENABLE_IMAGES
if (!skip_images)
image_redraw_finish(&scene->c->tty);
#endif
/* SIXEL image output may disturb status cells; compose them last. */
for (yy = y; yy < y + sy; yy++) {

2
tmux.h
View File

@@ -4341,6 +4341,7 @@ 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_redraw_finish(struct tty *);
void image_draw_flush(struct tty *);
int image_backend_flags(struct tty *);
int image_tty_update(struct tty *);
@@ -4398,6 +4399,7 @@ void kitty_free_state(void *);
void kitty_draw_rect(struct tty *,
const struct image_rect *, const struct tty_style_ctx *);
void kitty_redraw_start(struct tty *, u_int, u_int, u_int, u_int);
void kitty_redraw_finish(struct tty *);
void kitty_free_output_state(struct tty *, int);
#endif