#include "render.h" #include #include #include #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 static int hud_frameskip = 0; static bool hud_turbo = false; // ---- sixel output ---- static bool sixel_enabled = false; static int sixel_scale = 2; static char *sixel_buf = NULL; // reusable output buffer static size_t sixel_buf_cap = 0; void render_set_sixel(bool on, int scale) { if (scale < 1) scale = 1; if (scale > 6) scale = 6; sixel_enabled = on; sixel_scale = scale; if (on) { int W2 = SCREEN_W * scale, H2 = SCREEN_H * scale; int bands = (H2 + 5) / 6; // worst case: every band emits every palette color across full width size_t cap = (size_t)bands * 256 * (W2 + 8) + 65536; if (cap > sixel_buf_cap) { free(sixel_buf); sixel_buf = malloc(cap); sixel_buf_cap = sixel_buf ? cap : 0; } } need_full = true; } void render_force_full(void) { need_full = true; } // Update the on-screen HUD values; forces a status-line repaint on change. void render_set_hud(int frameskip, bool turbo) { if (frameskip != hud_frameskip || turbo != hud_turbo) { hud_frameskip = frameskip; hud_turbo = turbo; need_full = true; } } static void on_winch(int s) { (void)s; resized = 1; } // ---- sixel encoder --------------------------------------------------------- // The GB framebuffer is truecolor RGB but any given frame uses few distinct // colors (4 shades on DMG, a modest set on CGB). We build a per-frame palette // (<=256 entries), quantizing more coarsely only if a frame overflows, then // emit standard sixel bands. static int pal_r[256], pal_g[256], pal_b[256]; static int pal_key[256]; static u8 idxmap[SCREEN_H][SCREEN_W]; // palette index per source pixel static int build_palette(PPU *p) { static int htab[8192]; for (int shift = 0; ; shift++) { memset(htab, -1, sizeof(htab)); int mask = (0xFF << shift) & 0xFF; int n = 0, overflow = 0; for (int y = 0; y < SCREEN_H && !overflow; y++) { for (int x = 0; x < SCREEN_W; x++) { int r = p->fb[y][x][0] & mask; int g = p->fb[y][x][1] & mask; int b = p->fb[y][x][2] & mask; int key = (r << 16) | (g << 8) | b; unsigned h = ((unsigned)key * 2654435761u) & 8191u; while (htab[h] != -1 && pal_key[htab[h]] != key) h = (h + 1) & 8191u; if (htab[h] == -1) { if (n >= 256) { overflow = 1; break; } htab[h] = n; pal_key[n] = key; pal_r[n] = r; pal_g[n] = g; pal_b[n] = b; n++; } idxmap[y][x] = (u8)htab[h]; } } if (!overflow || shift >= 7) return n; } } static void render_frame_sixel(GB *gb) { PPU *p = &gb->ppu; if (!sixel_buf) return; int sc = sixel_scale; int W2 = SCREEN_W * sc, H2 = SCREEN_H * sc; int ncol = build_palette(p); char *o = sixel_buf; // home cursor, start sixel with 1:1 pixel aspect + raster size o += sprintf(o, "\x1b[H\x1bPq\"1;1;%d;%d", W2, H2); for (int i = 0; i < ncol; i++) // palette (0-100 percent) o += sprintf(o, "#%d;2;%d;%d;%d", i, (pal_r[i] * 100 + 127) / 255, (pal_g[i] * 100 + 127) / 255, (pal_b[i] * 100 + 127) / 255); static u8 present[256]; static u8 sx[SCREEN_W * 6]; // sixel value per column for the active color for (int by = 0; by < H2; by += 6) { // which palette colors appear in this 6-row band? memset(present, 0, ncol); int rows = (H2 - by < 6) ? (H2 - by) : 6; for (int k = 0; k < rows; k++) { int sy = (by + k) / sc; for (int x = 0; x < SCREEN_W; x++) present[idxmap[sy][x]] = 1; } int emitted = 0; for (int c = 0; c < ncol; c++) { if (!present[c]) continue; if (emitted) *o++ = '$'; // graphics CR: back to band start emitted = 1; // build this color's 6-bit column values for (int X2 = 0; X2 < W2; X2++) { int sx0 = X2 / sc, bits = 0; for (int k = 0; k < rows; k++) if (idxmap[(by + k) / sc][sx0] == c) bits |= (1 << k); sx[X2] = (u8)(0x3f + bits); } o += sprintf(o, "#%d", c); // run-length encode int X2 = 0; while (X2 < W2) { int run = 1; while (X2 + run < W2 && sx[X2 + run] == sx[X2]) run++; if (run >= 4) o += sprintf(o, "!%d%c", run, sx[X2]); else for (int j = 0; j < run; j++) *o++ = sx[X2]; X2 += run; } } *o++ = '-'; // next band } o += sprintf(o, "\x1b\\"); // ST: end sixel // In sixel mode we emit only the graphics - no status line / HUD text, so // the terminal shows nothing but the game image. need_full = false; fwrite(sixel_buf, 1, o - sixel_buf, stdout); fflush(stdout); } void render_frame(GB *gb) { if (sixel_enabled) { render_frame_sixel(gb); return; } 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 " "skip:%d%s [f]ast [ ][ ] q:quit\x1b[K", SCREEN_H/2 + 1, 12, gb->cart.title, hud_frameskip, hud_turbo ? " TURBO" : ""); } 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: keyboard (decay-based holds) + optional FIFO control channel ---- // Two independent sources are merged each frame: // held : buttons explicitly held via the FIFO (+x / -x), no decay // decay[] : momentary presses (keyboard, or FIFO taps), count down per frame static int decay[8]; // frames remaining for each BTN bit position static u8 held; // explicitly-held buttons (FIFO) static int turbo_toggles; // pending fast-forward toggles (consumed by main) static int frameskip_delta;// pending frameskip adjustment (consumed by main) #define HOLD_FRAMES 4 bool input_take_turbo(void) { if (turbo_toggles > 0) { turbo_toggles--; return true; } return false; } int input_take_frameskip_delta(void) { int d = frameskip_delta; frameskip_delta = 0; return d; } static int mask_index(int mask) { for (int i = 0; i < 8; i++) if ((1 << i) == mask) return i; return -1; } static void tap(int mask, int frames) { int i = mask_index(mask); if (i >= 0) decay[i] = frames > 0 ? frames : HOLD_FRAMES; } // ---- FIFO control channel ---- static int fifo_fd = -1; static bool fifo_created = false; static char fifo_path[512]; static char linebuf[256]; static int linelen = 0; static int name_mask(const char *s) { if (!strcasecmp(s, "a")) return BTN_A; if (!strcasecmp(s, "b")) return BTN_B; if (!strcasecmp(s, "start")) return BTN_START; if (!strcasecmp(s, "select") || !strcasecmp(s, "sel")) return BTN_SELECT; if (!strcasecmp(s, "up")) return BTN_UP; if (!strcasecmp(s, "down")) return BTN_DOWN; if (!strcasecmp(s, "left")) return BTN_LEFT; if (!strcasecmp(s, "right")) return BTN_RIGHT; return 0; } // Parse one token: "+a" hold, "-a" release, "a" tap, "a:20" tap N frames, // "release" clears all held. static void handle_token(char *t) { if (!*t) return; if (!strcasecmp(t, "release") || !strcasecmp(t, "none")) { held = 0; return; } if (t[0] == '+') { held |= name_mask(t + 1); return; } if (t[0] == '-') { held &= ~name_mask(t + 1); return; } int frames = HOLD_FRAMES; char *colon = strchr(t, ':'); if (colon) { *colon = 0; frames = atoi(colon + 1); } int m = name_mask(t); if (m) tap(m, frames); } static void handle_line(char *line) { for (char *t = strtok(line, " \t,"); t; t = strtok(NULL, " \t,")) handle_token(t); } // Public entry: process a line of button tokens from any control channel. void input_handle_command(const char *line) { char tmp[256]; snprintf(tmp, sizeof(tmp), "%s", line); handle_line(tmp); } void input_open_fifo(const char *path) { snprintf(fifo_path, sizeof(fifo_path), "%s", path); if (mkfifo(fifo_path, 0666) == 0) fifo_created = true; else if (errno != EEXIST) { perror("mkfifo"); return; } // O_RDWR keeps a writer end open so we never see persistent EOF and the // open never blocks waiting for an external writer. fifo_fd = open(fifo_path, O_RDWR | O_NONBLOCK); if (fifo_fd < 0) perror("open fifo"); } void input_close_fifo(void) { if (fifo_fd >= 0) { close(fifo_fd); fifo_fd = -1; } if (fifo_created) { unlink(fifo_path); fifo_created = false; } } static void poll_fifo(void) { if (fifo_fd < 0) return; char buf[512]; int n; while ((n = read(fifo_fd, buf, sizeof(buf))) > 0) { for (int i = 0; i < n; i++) { char c = buf[i]; if (c == '\n' || c == '\r') { linebuf[linelen] = 0; if (linelen) handle_line(linebuf); linelen = 0; } else if (linelen < (int)sizeof(linebuf) - 1) { linebuf[linelen++] = c; } } } } 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': tap(BTN_UP, HOLD_FRAMES); break; case 'B': tap(BTN_DOWN, HOLD_FRAMES); break; case 'C': tap(BTN_RIGHT, HOLD_FRAMES); break; case 'D': tap(BTN_LEFT, HOLD_FRAMES); break; } i += 2; continue; } switch (ch) { case 'w': tap(BTN_UP, HOLD_FRAMES); break; case 's': tap(BTN_DOWN, HOLD_FRAMES); break; case 'a': tap(BTN_LEFT, HOLD_FRAMES); break; case 'd': tap(BTN_RIGHT, HOLD_FRAMES); break; case 'z': case 'j': tap(BTN_A, HOLD_FRAMES); break; case 'x': case 'k': tap(BTN_B, HOLD_FRAMES); break; case '\r': case '\n': tap(BTN_START, HOLD_FRAMES); break; case ' ': tap(BTN_SELECT, HOLD_FRAMES); break; case 'f': turbo_toggles++; break; // toggle fast-forward case '[': frameskip_delta--; break; // fewer skipped frames case ']': frameskip_delta++; break; // more skipped frames case 'q': case 3: quit = true; break; } } poll_fifo(); // merge held + decaying momentary presses into the joypad state u8 b = held; for (int i = 0; i < 8; i++) if (decay[i] > 0) { decay[i]--; b |= (1 << i); } if (b & ~gb->buttons) gb_request_interrupt(gb, INT_JOYPAD); static bool dbg_init = false, dbg = false; if (!dbg_init) { dbg = getenv("GBC_INPUT_DEBUG") != NULL; dbg_init = true; } if (dbg && b != gb->buttons) fprintf(stderr, "[input] %c%c%c%c %c%c%c%c\n", b&BTN_UP?'U':'.', b&BTN_DOWN?'D':'.', b&BTN_LEFT?'L':'.', b&BTN_RIGHT?'R':'.', b&BTN_A?'A':'.', b&BTN_B?'B':'.', b&BTN_SELECT?'s':'.', b&BTN_START?'S':'.'); gb->buttons = b; return !quit; }