gfx: stop redrawing what already looks right

The console got slower the day TIOCGWINSZ started telling the truth.

`ESC[J` -- erase to end of screen -- arrives after every shell prompt, and zsh
redraws the prompt on every keystroke. While the terminal claimed to be 80x24
that erased 1920 cells; once it reported the real 160x50 it erased 8000, and
every one was written to the framebuffer whether or not it already looked
exactly like that.

Measured first, which killed the obvious theory: hoisting the bounds checks
out of gfx_draw_pixel changed a full-screen clear by 1%. The cost is not the
per-pixel function call, it is the framebuffer -- about 128 MB/s here, so four
megabytes is 32 ms of writing, and on a real window with QEMU rendering each
frame it is worse than that.

So write less rather than write faster. The grid already knows what is on
screen: erase_run compares before it draws and skips any row span already
blank in exactly those colours, and vt_clear does the same for the whole
screen.

    100x erase-to-end-of-screen, before   0.101s
    100x erase-to-end-of-screen, after    0.017s

Six times, with -display none where the framebuffer is ordinary RAM. This is
what the backing store is for: comparing is free, writing is not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
Nexoniarz 2026-09-13 13:57:26 +02:00
commit d53602faf3

View file

@ -373,12 +373,52 @@ vt_putc(struct vt *v, int glyph)
}
// Fill a run of cells with the current background, wrapping at the right edge.
//
// The grid is updated cell by cell -- it has to be -- but the *drawing* is
// done as rectangles, one per row span. Blanking a cell through the glyph
// path writes 128 pixels through the generic pixel routine; blanking a whole
// row as one fill writes the same pixels with the bounds checks hoisted out,
// and the console erases to end of screen on every shell prompt. At 160x50
// that is the difference between a million checked pixel writes per prompt and
// fifty row fills.
static void
erase_run(struct vt *v, int col, int row, int count)
{
while (count-- > 0 && row < v->rows) {
put_blank_at(v, col, row);
if (++col >= v->cols) { col = 0; row++; }
int active = is_active(v);
while (count > 0 && row < v->rows) {
int n = v->cols - col; // to the end of this row
if (n > count)
n = count;
// Only draw what actually changes.
//
// This is the whole reason a terminal keeps a grid. `ESC[J` -- erase to
// end of screen -- arrives after every shell prompt, and almost every
// time the screen below the cursor is *already* blank in exactly the
// colours it is being asked to blank to. Drawing it anyway means writing
// four megabytes to a framebuffer that runs at about 128 MB/s, which is
// 32 ms of nothing, on every prompt, and zsh redraws the prompt on every
// keystroke.
//
// The grid already knows. Comparing is free; writing is not.
int dirty = 0;
for (int i = 0; i < n; i++) {
struct vtcell *k = cell_at(v, col + i, row);
if (k->ch != ' ' || k->fg != v->fg || k->bg != v->bg || k->attr != 0)
dirty = 1;
k->ch = ' ';
k->fg = v->fg;
k->bg = v->bg;
k->attr = 0;
}
if (active && dirty)
gfx_fill_rect(col * gfx_char_w(), row * gfx_char_h(),
n * gfx_char_w(), gfx_char_h(), vt_rgb(v->bg));
count -= n;
col = 0;
row++;
}
}
@ -407,11 +447,29 @@ vt_erase_line(struct vt *v, int mode)
void
vt_clear(struct vt *v)
{
int dirty = 0;
for (int r = 0; r < v->rows; r++) {
for (int c = 0; c < v->cols; c++) {
struct vtcell *k = cell_at(v, c, r);
if (k->ch != ' ' || k->fg != v->fg || k->bg != v->bg || k->attr != 0) {
dirty = 1;
break;
}
}
if (dirty)
break;
}
for (int r = 0; r < v->rows; r++)
blank_row(v, r);
v->cx = v->cy = 0;
v->wrap_pending = 0;
vt_repaint(v);
// A screen fill, not a repaint. vt_repaint draws every cell through the
// glyph path -- eight thousand of them -- to produce a screen that is
// entirely one colour.
if (is_active(v) && dirty)
gfx_clear(vt_rgb(v->bg));
}