diff --git a/screen-redraw.c b/screen-redraw.c index 7ea30a8a4..2a2156d86 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -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); + } +} diff --git a/screen-write.c b/screen-write.c index 150eab7db..6123930d4 100644 --- a/screen-write.c +++ b/screen-write.c @@ -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. */ diff --git a/tmux.h b/tmux.h index c27e7f23f..01515b433 100644 --- a/tmux.h +++ b/tmux.h @@ -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 */ diff --git a/window.c b/window.c index 3680ad80f..e5e26fd4c 100644 --- a/window.c +++ b/window.c @@ -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);