From d53602faf399b86e5f207d280f9d94633284880d Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sun, 13 Sep 2026 13:57:26 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2 --- kernel/dev/vt.c | 66 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/kernel/dev/vt.c b/kernel/dev/vt.c index 18c02e5..87d5dca 100644 --- a/kernel/dev/vt.c +++ b/kernel/dev/vt.c @@ -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)); }