aboutsummaryrefslogtreecommitdiffstats
path: root/src/render.c
diff options
context:
space:
mode:
authorgbc dev <gbc@localhost>2026-07-04 22:39:34 +0200
committergbc dev <gbc@localhost>2026-07-04 22:39:34 +0200
commit894ddbe9bcf2bbd1da3af0429b6ed9550c0d4ce3 (patch)
tree4089a0c9bb2a1661f57564e3d2044fcb2df5d23e /src/render.c
parentdocs: note mem_timing-2 passing (diff)
downloadsl0pboy-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).
Diffstat (limited to 'src/render.c')
-rw-r--r--src/render.c60
1 files changed, 51 insertions, 9 deletions
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 ----