#include "render.h" #include #include #include #include #include #include #include #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); raw_active = false; } // show cursor, leave alt screen, reset colors printf("\x1b[0m\x1b[?25h\x1b[?1049l"); fflush(stdout); } void term_init(void) { tcgetattr(STDIN_FILENO, &orig_termios); struct termios raw = orig_termios; raw.c_lflag &= ~(ECHO | ICANON | ISIG); raw.c_iflag &= ~(IXON | ICRNL); raw.c_cc[VMIN] = 0; raw.c_cc[VTIME] = 0; tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); raw_active = true; // 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; 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 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]; } if (bot[0] != PR || bot[1] != PG || bot[2] != PB) { o += sprintf(o, "\x1b[48;2;%d;%d;%dm", bot[0], bot[1], bot[2]); PR = bot[0]; PG = bot[1]; PB = bot[2]; } memcpy(o, UPPER, 3); o += 3; cur_row = row; cur_col = x; } } // 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 ---- static int hold[8]; // frames remaining for each BTN bit position #define HOLD_FRAMES 4 static void press(int bit) { for (int i = 0; i < 8; i++) if ((1 << i) == bit) hold[i] = HOLD_FRAMES; } bool input_poll(GB *gb) { unsigned char buf[64]; int n = read(STDIN_FILENO, buf, sizeof(buf)); bool quit = false; for (int i = 0; i < n; i++) { unsigned char ch = buf[i]; if (ch == 0x1b && i + 2 < n && buf[i+1] == '[') { switch (buf[i+2]) { case 'A': press(BTN_UP); break; case 'B': press(BTN_DOWN); break; case 'C': press(BTN_RIGHT); break; case 'D': press(BTN_LEFT); break; } i += 2; continue; } switch (ch) { case 'w': press(BTN_UP); break; case 's': press(BTN_DOWN); break; case 'a': press(BTN_LEFT); break; case 'd': press(BTN_RIGHT); break; case 'z': case 'j': press(BTN_A); break; case 'x': case 'k': press(BTN_B); break; case '\r': case '\n': press(BTN_START); break; case ' ': press(BTN_SELECT); break; case 'q': case 3: quit = true; break; } } // build button state from decay counters u8 b = 0; for (int i = 0; i < 8; i++) { if (hold[i] > 0) { hold[i]--; b |= (1 << i); } } gb->buttons = b; if (b) gb_request_interrupt(gb, INT_JOYPAD); return !quit; }