screen-redraw, screen-write: add damage-rectangle tracking and composition

Introduce a per-window list of damaged rectangles (struct redraw_damage)
and redraw_damage_window() to record them, redraw_client_damage() to
consume them by composing exactly the damaged cells (via a new
redraw_draw_damage_rect(), which also force-refreshes any pane-status
span it touches, since window_make_pane_status()'s content-diff check
has no way to know the physical cells were disturbed by something else).

redraw_draw_span() now takes an explicit [clip_x, clip_x + clip_n) range
instead of always drawing a span's full width, so a damage rectangle can
redraw just the portion of a span it actually covers.

screen_write_redraw_cb() - the fallback when a write can't be applied
directly to the terminal - now reports damage for just the affected row
via this mechanism, instead of unconditionally flagging the whole pane
for a full redraw.
This commit is contained in:
Michael Grant
2026-08-22 15:45:29 +01:00
parent faba411289
commit d6074895bb
4 changed files with 293 additions and 14 deletions

View File

@@ -205,6 +205,22 @@ struct redraw_scene {
u_int oy;
};
/* A single damaged window-coordinate rectangle. */
struct redraw_damage {
u_int x;
u_int y;
u_int sx;
u_int sy;
TAILQ_ENTRY(redraw_damage) entry;
};
/*
* Cap on the number of pending damage rectangles per window before they are
* collapsed into a single rectangle covering their union.
*/
#define REDRAW_DAMAGE_MAX 16
/* Cell for building the scene. */
struct redraw_build_cell {
struct redraw_span_data data;
@@ -1064,6 +1080,114 @@ redraw_invalidate_scene(struct window *w)
w->redraw_scene_generation++;
}
/* Free all pending damage for a window. */
void
redraw_free_damage(struct window *w)
{
struct redraw_damage *rd, *rd1;
TAILQ_FOREACH_SAFE(rd, &w->damage, entry, rd1) {
TAILQ_REMOVE(&w->damage, rd, entry);
free(rd);
}
w->damage_count = 0;
}
/* Collapse all pending damage for a window into one rectangle - its union. */
static void
redraw_collapse_damage(struct window *w)
{
struct redraw_damage *rd, *rd1, *first;
u_int x0, y0, x1, y1;
first = TAILQ_FIRST(&w->damage);
if (first == NULL)
return;
x0 = first->x;
y0 = first->y;
x1 = first->x + first->sx;
y1 = first->y + first->sy;
TAILQ_FOREACH_SAFE(rd, &w->damage, entry, rd1) {
if (rd->x < x0)
x0 = rd->x;
if (rd->y < y0)
y0 = rd->y;
if (rd->x + rd->sx > x1)
x1 = rd->x + rd->sx;
if (rd->y + rd->sy > y1)
y1 = rd->y + rd->sy;
if (rd != first) {
TAILQ_REMOVE(&w->damage, rd, entry);
free(rd);
}
}
first->x = x0;
first->y = y0;
first->sx = x1 - x0;
first->sy = y1 - y0;
w->damage_count = 1;
}
/*
* Record a damaged window-coordinate rectangle. Clips it to the window,
* merges it with an existing rectangle where doing so does not make the
* result substantially larger than the two combined, and collapses the
* whole list to its union once it grows past a modest cap.
*
* This only records damage - nothing consumes it yet.
*/
void
redraw_damage_window(struct window *w, u_int x, u_int y, u_int sx, u_int sy)
{
struct redraw_damage *rd;
u_int x0, y0, x1, y1, area, union_area;
if (x >= w->sx || y >= w->sy)
return;
if (x + sx > w->sx)
sx = w->sx - x;
if (y + sy > w->sy)
sy = w->sy - y;
if (sx == 0 || sy == 0)
return;
TAILQ_FOREACH(rd, &w->damage, entry) {
/* Skip unless overlapping or directly adjacent. */
if (x > rd->x + rd->sx || rd->x > x + sx ||
y > rd->y + rd->sy || rd->y > y + sy)
continue;
x0 = (x < rd->x) ? x : rd->x;
y0 = (y < rd->y) ? y : rd->y;
x1 = (x + sx > rd->x + rd->sx) ? x + sx : rd->x + rd->sx;
y1 = (y + sy > rd->y + rd->sy) ? y + sy : rd->y + rd->sy;
area = sx * sy + rd->sx * rd->sy;
union_area = (x1 - x0) * (y1 - y0);
if (union_area > 2 * area)
continue;
rd->x = x0;
rd->y = y0;
rd->sx = x1 - x0;
rd->sy = y1 - y0;
return;
}
rd = xcalloc(1, sizeof *rd);
rd->x = x;
rd->y = y;
rd->sx = sx;
rd->sy = sy;
TAILQ_INSERT_TAIL(&w->damage, rd, entry);
w->damage_count++;
if (w->damage_count > REDRAW_DAMAGE_MAX)
redraw_collapse_damage(w);
}
/* Mark all cached redraw scenes as out of date. */
void
redraw_invalidate_all_scenes(void)
@@ -1376,10 +1500,15 @@ redraw_draw_menu_span(struct redraw_draw_ctx *dctx,
tty_draw_line(tty, s, px, span->data.m.py, n, x, y, NULL);
}
/* Draw a span. */
/*
* Draw a span, restricted to [clip_x, clip_x + clip_n) - a caller drawing
* the whole span passes the span's own x/width here; a caller drawing only
* a damaged sub-range passes that range instead. Overlay clipping (menus,
* popups) is then applied on top of this, exactly as before.
*/
static void
redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span,
u_int y)
u_int y, u_int clip_x, u_int clip_n)
{
struct redraw_scene *scene = dctx->scene;
struct redraw_span_data *data = &span->data;
@@ -1393,7 +1522,7 @@ redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span,
if (type == REDRAW_SPAN_STATUS && ~data->st.wp->flags & PANE_NEWSTATUS)
return;
r = tty_check_overlay_range(tty, span->x, y, span->width);
r = tty_check_overlay_range(tty, clip_x, y, clip_n);
for (i = 0; i < r->used; i++) {
rr = &r->ranges[i];
if (rr->nx == 0)
@@ -1453,15 +1582,19 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp,
if (flags & REDRAW_PANE) {
spans = &line->spans[REDRAW_SPAN_PANE];
TAILQ_FOREACH(span, spans, entry) {
if (span->data.p.wp == wp)
redraw_draw_span(dctx, span, cy);
if (span->data.p.wp == wp) {
redraw_draw_span(dctx, span, cy,
span->x, span->width);
}
}
}
if (flags & REDRAW_PANE_SCROLLBAR) {
spans = &line->spans[REDRAW_SPAN_SCROLLBAR];
TAILQ_FOREACH(span, spans, entry) {
if (span->data.sb.wp == wp)
redraw_draw_span(dctx, span, cy);
if (span->data.sb.wp == wp) {
redraw_draw_span(dctx, span, cy,
span->x, span->width);
}
}
}
}
@@ -1519,8 +1652,10 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags)
}
}
spans = &line->spans[type];
TAILQ_FOREACH(span, spans, entry)
redraw_draw_span(dctx, span, cy);
TAILQ_FOREACH(span, spans, entry) {
redraw_draw_span(dctx, span, cy, span->x,
span->width);
}
}
}
}
@@ -1540,8 +1675,10 @@ redraw_draw_menu_lines(struct redraw_draw_ctx *dctx)
cy = dctx->status_lines + y;
else
cy = y;
TAILQ_FOREACH(span, &line->spans[REDRAW_SPAN_MENU], entry)
redraw_draw_span(dctx, span, cy);
TAILQ_FOREACH(span, &line->spans[REDRAW_SPAN_MENU], entry) {
redraw_draw_span(dctx, span, cy, span->x,
span->width);
}
}
}
@@ -1905,3 +2042,125 @@ redraw_pane_scrollbar(struct client *c, struct window_pane *wp)
{
redraw_draw(c, wp, REDRAW_PANE_SCROLLBAR);
}
/*
* A REDRAW_SPAN_STATUS span within a damaged rectangle needs its content
* rebuilt and force-drawn regardless of whether that content has logically
* changed. window_make_pane_status()'s grid_compare() only tells us
* whether the *content* changed, not whether the physical cells were
* disturbed by something else (e.g. a floating pane sliding across this
* row) - and being inside a damage rectangle already proves that happened.
* Without this, redraw_draw_span() silently skips REDRAW_SPAN_STATUS spans
* whenever PANE_NEWSTATUS is not set, leaving a pane's border-status title
* blank until some unrelated redraw happens to touch it (e.g. a focus
* change or window resize).
*/
static void
redraw_damage_refresh_status(struct redraw_draw_ctx *dctx,
struct window_pane *wp)
{
struct redraw_span *first;
u_int width;
if (wp->flags & PANE_NEWSTATUS)
return;
width = redraw_pane_status_width(dctx, wp, &first);
if (width == 0)
return;
window_make_pane_status(wp, dctx->scene->c, width, first);
wp->flags |= PANE_NEWSTATUS;
}
/*
* Compose exactly the cells within a damaged rectangle (already in this
* client's own scene coordinates), rather than a whole pane. For each row
* in range, every span of every type whose x-range intersects the
* rectangle is drawn restricted to just the intersected sub-range.
*/
static void
redraw_draw_damage_rect(struct redraw_draw_ctx *dctx, u_int x, u_int y,
u_int sx, u_int sy)
{
struct redraw_scene *scene = dctx->scene;
struct redraw_line *line;
struct redraw_spans *spans;
struct redraw_span *span;
u_int cy, yy, clip_x, clip_end, type;
if (x >= scene->sx || y >= scene->sy)
return;
if (x + sx > scene->sx)
sx = scene->sx - x;
if (y + sy > scene->sy)
sy = scene->sy - y;
if (sx == 0 || sy == 0)
return;
for (yy = y; yy < y + sy; yy++) {
line = &scene->lines[yy];
if (dctx->flags & REDRAW_STATUS_TOP)
cy = dctx->status_lines + yy;
else
cy = yy;
for (type = 0; type < REDRAW_SPAN_TYPES; type++) {
spans = &line->spans[type];
TAILQ_FOREACH(span, spans, entry) {
clip_x = (span->x > x) ? span->x : x;
clip_end = (span->x + span->width < x + sx) ?
span->x + span->width : x + sx;
if (clip_end <= clip_x)
continue;
if (type == REDRAW_SPAN_STATUS) {
redraw_damage_refresh_status(dctx,
span->data.st.wp);
}
redraw_draw_span(dctx, span, cy, clip_x,
clip_end - clip_x);
}
}
}
}
/*
* Consume a client's window's pending damage by composing exactly the
* damaged cells, after clipping each rectangle to what this client can see
* and translating it into this client's own scene coordinates.
*
* Unlike redraw_pane(), this does not redraw a whole pane's worth of cells
* for a small disturbance - only the cells within the (clipped) rectangle
* are touched, via redraw_draw_damage_rect().
*/
void
redraw_client_damage(struct client *c)
{
struct window *w = c->session->curw->window;
struct redraw_scene *scene;
struct redraw_draw_ctx dctx;
struct redraw_damage *rd;
u_int ox, oy, sx, sy, x0, y0, x1, y1;
if (TAILQ_EMPTY(&w->damage))
return;
scene = redraw_get_scene(c);
if (scene == NULL)
return;
redraw_set_draw_context(&dctx, scene);
redraw_get_window_offset(c, &ox, &oy, &sx, &sy);
tty_sync_start(&c->tty);
tty_update_mode(&c->tty, c->tty.mode & ~CURSOR_MODES, NULL);
TAILQ_FOREACH(rd, &w->damage, entry) {
x0 = (rd->x > ox) ? rd->x : ox;
y0 = (rd->y > oy) ? rd->y : oy;
x1 = (rd->x + rd->sx < ox + sx) ? rd->x + rd->sx : ox + sx;
y1 = (rd->y + rd->sy < oy + sy) ? rd->y + rd->sy : oy + sy;
if (x0 >= x1 || y0 >= y1)
continue;
log_debug("%s: %s composing damage %u,%u %ux%u", __func__,
c->name, x0 - ox, y0 - oy, x1 - x0, y1 - y0);
redraw_draw_damage_rect(&dctx, x0 - ox, y0 - oy, x1 - x0,
y1 - y0);
}
}

View File

@@ -120,14 +120,23 @@ screen_write_set_cursor(struct screen_write_ctx *ctx, int cx, int cy)
evtimer_add(&w->offset_timer, &tv);
}
/* Do a full redraw. */
/*
* Called when a write could not be applied directly to the terminal and
* needs a redraw instead. Report damage for just the row the write was
* targeting rather than the whole pane - ttyctx->ocy is the cursor row
* within the pane's own screen at the point the write was issued, and
* wp->yoff is already adjusted past any top pane-border-status row, so
* wp->yoff + ocy is the correct window-coordinate row.
*/
static void
screen_write_redraw_cb(const struct tty_ctx *ttyctx)
{
struct window_pane *wp = ttyctx->arg;
if (wp != NULL)
wp->flags |= PANE_REDRAW;
if (wp == NULL)
return;
redraw_damage_window(wp->window, wp->xoff, wp->yoff + ttyctx->ocy,
wp->sx, 1);
}
/* Update context for client. */

9
tmux.h
View File

@@ -68,6 +68,7 @@ struct options_array_item;
struct options_entry;
struct prompt;
struct window_pane_prompt;
struct redraw_damage;
struct redraw_scene;
struct redraw_span;
struct screen_write_citem;
@@ -1422,6 +1423,7 @@ struct window_pane {
TAILQ_HEAD(window_panes, window_pane);
TAILQ_HEAD(window_panes_zindex, window_pane);
RB_HEAD(window_pane_tree, window_pane);
TAILQ_HEAD(redraw_damages, redraw_damage);
/* Window structure. */
struct window {
@@ -1464,6 +1466,10 @@ struct window {
uint64_t redraw_scene_generation;
struct redraw_damages damage;
u_int damage_count;
int redraw_deferred;
struct menu_data *menu;
u_int menu_last_px;
u_int menu_last_py;
@@ -3650,6 +3656,9 @@ void redraw_pane_scrollbar(struct client *, struct window_pane *);
void redraw_free_scene(struct redraw_scene *);
void redraw_invalidate_scene(struct window *);
void redraw_invalidate_all_scenes(void);
void redraw_damage_window(struct window *, u_int, u_int, u_int, u_int);
void redraw_free_damage(struct window *);
void redraw_client_damage(struct client *);
int redraw_get_status_border_cell_type(struct redraw_span **, u_int);
/* screen.c */

View File

@@ -415,6 +415,7 @@ window_create(u_int sx, u_int sy, u_int xpixel, u_int ypixel)
TAILQ_INIT(&w->panes);
TAILQ_INIT(&w->z_index);
TAILQ_INIT(&w->last_panes);
TAILQ_INIT(&w->damage);
w->active = NULL;
w->lastlayout = -1;
@@ -460,6 +461,7 @@ window_destroy(struct window *w)
menu_destroy(w);
window_destroy_panes(w);
redraw_free_damage(w);
if (event_initialized(&w->name_event))
evtimer_del(&w->name_event);