diff options
| author | gbc dev <gbc@localhost> | 2026-07-04 22:39:34 +0200 |
|---|---|---|
| committer | gbc dev <gbc@localhost> | 2026-07-04 22:39:34 +0200 |
| commit | 894ddbe9bcf2bbd1da3af0429b6ed9550c0d4ce3 (patch) | |
| tree | 4089a0c9bb2a1661f57564e3d2044fcb2df5d23e | |
| parent | docs: note mem_timing-2 passing (diff) | |
| download | sl0pboy-894ddbe9bcf2bbd1da3af0429b6ed9550c0d4ce3.tar.gz sl0pboy-894ddbe9bcf2bbd1da3af0429b6ed9550c0d4ce3.tar.xz sl0pboy-894ddbe9bcf2bbd1da3af0429b6ed9550c0d4ce3.zip | |
renderer: inter-frame diffing to minimize terminal output
Only cells that changed since the previous frame are redrawn, using cursor
positioning to skip unchanged runs. Static screens write nothing after the
first frame (~108x reduction on acid2). SIGWINCH forces a full repaint.
Verified pixel-exact against the framebuffer (11520/11520 cells, 0 mismatch).
| -rw-r--r-- | README.md | 7 | ||||
| -rw-r--r-- | src/render.c | 60 | ||||
| -rw-r--r-- | src/render.h | 1 |
3 files changed, 59 insertions, 9 deletions
@@ -6,6 +6,13 @@ codes and the Unicode upper-half-block trick (`▀`): each character cell packs two vertical pixels (foreground = top pixel, background = bottom pixel), giving a 160×144 GB screen in 160×72 terminal cells. +To keep terminal load low, output is minimized two ways: SGR color escapes +are only emitted when the fg/bg changes, and **inter-frame diffing** redraws +only the cells that changed since the previous frame (jumping over unchanged +runs with `ESC[row;colH`). A fully static screen writes nothing after the +first frame; on the acid2 screens this cut output from ~7.1 MB to ~65 KB +over 2 s (~108×). Terminal resize (`SIGWINCH`) triggers a full repaint. + ## Build & run ```sh diff --git a/src/render.c b/src/render.c index 72e5558..75ce020 100644 --- a/src/render.c +++ b/src/render.c @@ -5,12 +5,15 @@ #include <unistd.h> #include <termios.h> #include <fcntl.h> +#include <signal.h> #define UPPER "\xe2\x96\x80" // ▀ U+2580 static struct termios orig_termios; static bool raw_active = false; +static void on_winch(int s); + void term_restore(void) { if (raw_active) { tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios); @@ -33,22 +36,50 @@ void term_init(void) { // enter alt screen, hide cursor, clear printf("\x1b[?1049h\x1b[?25l\x1b[2J\x1b[H"); fflush(stdout); + signal(SIGWINCH, on_winch); + render_force_full(); atexit(term_restore); } // large reusable output buffer static char outbuf[SCREEN_W * (SCREEN_H/2) * 40 + 4096]; +// previous frame's pixels, for inter-frame diffing +static u8 prev_fb[SCREEN_H][SCREEN_W][3]; +static bool need_full = true; // force a complete repaint +static volatile sig_atomic_t resized = 0; // set by SIGWINCH + +void render_force_full(void) { need_full = true; } +static void on_winch(int s) { (void)s; resized = 1; } + void render_frame(GB *gb) { PPU *p = &gb->ppu; char *o = outbuf; - o += sprintf(o, "\x1b[H"); + + if (resized) { resized = 0; need_full = true; o += sprintf(o, "\x1b[2J"); } + + // Terminal SGR (color) state persists across cursor moves, so we can keep + // one fg/bg tracker for the whole frame even when we jump over unchanged + // cells. Reset each frame since we emit a final \x1b[0m. + int pr = -1, pg = -1, pb = -1; // last-emitted fg + int PR = -1, PG = -1, PB = -1; // last-emitted bg + int cur_row = -2, cur_col = -2; // terminal cell just written (0-indexed) + for (int y = 0; y < SCREEN_H; y += 2) { - int pr = -1, pg = -1, pb = -1; // prev fg - int PR = -1, PG = -1, PB = -1; // prev bg + int row = y / 2; for (int x = 0; x < SCREEN_W; x++) { u8 *top = p->fb[y][x]; u8 *bot = p->fb[y+1][x]; + if (!need_full) { + u8 *pt = prev_fb[y][x], *pb2 = prev_fb[y+1][x]; + if (top[0]==pt[0] && top[1]==pt[1] && top[2]==pt[2] && + bot[0]==pb2[0] && bot[1]==pb2[1] && bot[2]==pb2[2]) + continue; // cell unchanged -> skip + } + // reposition cursor only when we're not already right after the + // previously written cell + if (row != cur_row || x != cur_col + 1) + o += sprintf(o, "\x1b[%d;%dH", row + 1, x + 1); if (top[0] != pr || top[1] != pg || top[2] != pb) { o += sprintf(o, "\x1b[38;2;%d;%d;%dm", top[0], top[1], top[2]); pr = top[0]; pg = top[1]; pb = top[2]; @@ -58,14 +89,25 @@ void render_frame(GB *gb) { PR = bot[0]; PG = bot[1]; PB = bot[2]; } memcpy(o, UPPER, 3); o += 3; + cur_row = row; cur_col = x; } - memcpy(o, "\x1b[0m\r\n", 6); o += 6; } - o += sprintf(o, "\x1b[0m\x1b[38;2;150;150;150m %.*s " - "[wasd/arrows] z:A x:B space:sel enter:start q:quit\x1b[0m\r\n", - 15, gb->cart.title); - fwrite(outbuf, 1, o - outbuf, stdout); - fflush(stdout); + + // status line: static text, only (re)draw on a full repaint + if (need_full) { + o += sprintf(o, "\x1b[0m\x1b[%d;1H\x1b[38;2;150;150;150m %.*s " + "[wasd/arrows] z:A x:B space:sel enter:start q:quit", + SCREEN_H/2 + 1, 15, gb->cart.title); + } + + memcpy(prev_fb, p->fb, sizeof(prev_fb)); + need_full = false; + + if (o != outbuf) { // nothing changed -> write nothing + o += sprintf(o, "\x1b[0m"); + fwrite(outbuf, 1, o - outbuf, stdout); + fflush(stdout); + } } // ---- input with decay-based "hold" emulation ---- diff --git a/src/render.h b/src/render.h index 15f9cda..93ca32a 100644 --- a/src/render.h +++ b/src/render.h @@ -6,6 +6,7 @@ void term_init(void); void term_restore(void); void render_frame(GB *gb); +void render_force_full(void); // force a complete repaint next frame // Poll keyboard, update gb->buttons. Returns false if quit requested. bool input_poll(GB *gb); |
