#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 ---- // The GB body "chrome" (see below) surrounds the LCD, so the sixel canvas can // be larger than the raw screen. Size all buffers for the chrome canvas. #define CH_SIDE 40 // body margin left/right of the LCD (unscaled px) #define CH_TOP 46 // body area above the LCD #define CH_BOTTOM 172 // body area below the LCD (branding + controls live here) #define CHROME_W (SCREEN_W + 2*CH_SIDE) // 240 #define CHROME_H (SCREEN_H + CH_TOP + CH_BOTTOM) // 362 #define LCD_X0 CH_SIDE #define LCD_Y0 CH_TOP static bool sixel_enabled = false; static bool chrome_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) { // Size for the largest canvas we might emit (the chrome frame). int W2 = CHROME_W * scale, H2 = CHROME_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; } // Enable the drawn Game Boy body/frame around the LCD (sixel mode only). void render_set_chrome(bool on) { chrome_enabled = on; need_full = true; } void render_force_full(void) { need_full = true; } // ---- LCD recolor filter ---------------------------------------------------- // Optionally remap every LCD pixel to a 4-shade palette by luminance, giving // the authentic monochrome-LCD look (and greening color CGB games for that // retro vibe). Applied to every render path (half-block, sixel, chrome). static bool lcd_recolor = false; static u8 lcd_pal[4][3]; void render_set_palette(const char *name) { // classic DMG-01 "pea green" (#9bbc0f ramp) and a GB-Pocket-ish grayscale static const u8 dmg_green[4][3] = { {155,188,15}, {139,172,15}, {48,98,48}, {15,56,15} }; static const u8 pocket_gray[4][3] = { {224,224,224}, {168,168,168}, {104,104,104}, {40,40,40} }; if (!name) { lcd_recolor = false; return; } if (!strcasecmp(name, "dmg") || !strcasecmp(name, "green")) { memcpy(lcd_pal, dmg_green, sizeof(lcd_pal)); lcd_recolor = true; } else if (!strcasecmp(name, "pocket") || !strcasecmp(name, "gray") || !strcasecmp(name, "grey")) { memcpy(lcd_pal, pocket_gray, sizeof(lcd_pal)); lcd_recolor = true; } else { lcd_recolor = false; // unknown name -> passthrough } need_full = true; } // Return the framebuffer to draw: the raw PPU output, or a recolored copy. static u8 fb_scratch[SCREEN_H][SCREEN_W][3]; static u8 (*lcd_fb(PPU *p))[SCREEN_W][3] { if (!lcd_recolor) return p->fb; for (int y = 0; y < SCREEN_H; y++) for (int x = 0; x < SCREEN_W; x++) { const u8 *in = p->fb[y][x]; int lum = (in[0]*77 + in[1]*150 + in[2]*29) >> 8; // 0..255 int idx = lum >= 192 ? 0 : lum >= 128 ? 1 : lum >= 64 ? 2 : 3; fb_scratch[y][x][0] = lcd_pal[idx][0]; fb_scratch[y][x][1] = lcd_pal[idx][1]; fb_scratch[y][x][2] = lcd_pal[idx][2]; } return fb_scratch; } // 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; the chrome adds a handful of // flat body/button colors). We build a per-frame palette (<=256 entries), // quantizing more coarsely only if a frame overflows, then emit standard // sixel bands. The encoder works on any RGB888 buffer so the same path serves // both the bare LCD and the full body-chrome canvas. static int pal_r[256], pal_g[256], pal_b[256]; static int pal_key[256]; // Build a palette from an RGB888 buffer (w*h pixels, row-major, 3 bytes/pixel) // writing one palette index per pixel into idx[]. Returns the color count. static int build_palette_buf(const u8 *rgb, int w, int h, u8 *idx) { static int htab[8192]; for (int shift = 0; ; shift++) { memset(htab, -1, sizeof(htab)); int mask = (0xFF << shift) & 0xFF; int n = 0, overflow = 0, npix = w * h; for (int i = 0; i < npix && !overflow; i++) { int r = rgb[i*3+0] & mask; int g = rgb[i*3+1] & mask; int b = rgb[i*3+2] & mask; int key = (r << 16) | (g << 8) | b; unsigned hh = ((unsigned)key * 2654435761u) & 8191u; while (htab[hh] != -1 && pal_key[htab[hh]] != key) hh = (hh + 1) & 8191u; if (htab[hh] == -1) { if (n >= 256) { overflow = 1; break; } htab[hh] = n; pal_key[n] = key; pal_r[n] = r; pal_g[n] = g; pal_b[n] = b; n++; } idx[i] = (u8)htab[hh]; } if (!overflow || shift >= 7) return n; } } // Emit a full sixel image for a w*h palettized buffer at integer scale sc. // Returns the advanced output pointer. static char *encode_sixel_buf(char *o, int w, int h, int sc, int ncol, const u8 *idx) { int W2 = w * sc, H2 = h * sc; // 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[CHROME_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; const u8 *row = &idx[sy * w]; for (int x = 0; x < w; x++) present[row[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 (idx[((by + k) / sc) * w + 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 return o; } static u8 screen_idx[SCREEN_W * SCREEN_H]; static void render_frame_sixel(GB *gb) { PPU *p = &gb->ppu; if (!sixel_buf) return; u8 (*fb)[SCREEN_W][3] = lcd_fb(p); int ncol = build_palette_buf(&fb[0][0][0], SCREEN_W, SCREEN_H, screen_idx); char *o = encode_sixel_buf(sixel_buf, SCREEN_W, SCREEN_H, sixel_scale, ncol, screen_idx); // 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); } // ---- Game Boy body chrome -------------------------------------------------- // A flat-shaded DMG-style shell drawn around the LCD. Flat colors keep the // per-frame palette tiny. Buttons brighten while the matching key is held. static u8 chrome_canvas[CHROME_W * CHROME_H * 3]; static u8 chrome_idx[CHROME_W * CHROME_H]; static inline void put_px(u8 *cv, int x, int y, int r, int g, int b) { if ((unsigned)x >= (unsigned)CHROME_W || (unsigned)y >= (unsigned)CHROME_H) return; u8 *p = &cv[(y * CHROME_W + x) * 3]; p[0] = (u8)r; p[1] = (u8)g; p[2] = (u8)b; } static void fill_rect(u8 *cv, int x0, int y0, int w, int h, int r, int g, int b) { for (int y = y0; y < y0 + h; y++) for (int x = x0; x < x0 + w; x++) put_px(cv, x, y, r, g, b); } static void fill_circle(u8 *cv, int cx, int cy, int rad, int r, int g, int b) { for (int y = cy - rad; y <= cy + rad; y++) for (int x = cx - rad; x <= cx + rad; x++) { int dx = x - cx, dy = y - cy; if (dx*dx + dy*dy <= rad*rad) put_px(cv, x, y, r, g, b); } } // rounded rect, uniform corner radius static void fill_rrect(u8 *cv, int x0, int y0, int w, int h, int rad, int r, int g, int b) { if (rad < 0) rad = 0; for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { int cx = x < rad ? rad : (x >= w - rad ? w - 1 - rad : x); int cy = y < rad ? rad : (y >= h - rad ? h - 1 - rad : y); int dx = x - cx, dy = y - cy; if (dx*dx + dy*dy <= rad*rad) put_px(cv, x0 + x, y0 + y, r, g, b); } } // ---- tiny 5x7 bitmap font (column-major: 5 bytes, bit0=top row) ---- // Only the glyphs we actually silk-screen onto the shell are defined; the rest // render blank. Indexed by (ch - 32), covering ASCII space .. 'z'. static const u8 font5x7[95][5] = { [' '-32] = {0x00,0x00,0x00,0x00,0x00}, ['('-32] = {0x00,0x1C,0x22,0x41,0x00}, [')'-32] = {0x00,0x41,0x22,0x1C,0x00}, ['.'-32] = {0x00,0x40,0x60,0x00,0x00}, ['0'-32] = {0x3E,0x51,0x49,0x45,0x3E}, ['1'-32] = {0x00,0x42,0x7F,0x40,0x00}, ['2'-32] = {0x42,0x61,0x51,0x49,0x46}, ['3'-32] = {0x21,0x41,0x45,0x4B,0x31}, ['4'-32] = {0x18,0x14,0x12,0x7F,0x10}, ['5'-32] = {0x27,0x45,0x45,0x45,0x39}, ['6'-32] = {0x3C,0x4A,0x49,0x49,0x30}, ['7'-32] = {0x01,0x71,0x09,0x05,0x03}, ['8'-32] = {0x36,0x49,0x49,0x49,0x36}, ['9'-32] = {0x06,0x49,0x49,0x29,0x1E}, ['A'-32] = {0x7E,0x11,0x11,0x11,0x7E}, ['B'-32] = {0x7F,0x49,0x49,0x49,0x36}, ['C'-32] = {0x3E,0x41,0x41,0x41,0x22}, ['D'-32] = {0x7F,0x41,0x41,0x22,0x1C}, ['E'-32] = {0x7F,0x49,0x49,0x49,0x41}, ['F'-32] = {0x7F,0x09,0x09,0x09,0x01}, ['G'-32] = {0x3E,0x41,0x49,0x49,0x7A}, ['H'-32] = {0x7F,0x08,0x08,0x08,0x7F}, ['I'-32] = {0x00,0x41,0x7F,0x41,0x00}, ['L'-32] = {0x7F,0x40,0x40,0x40,0x40}, ['M'-32] = {0x7F,0x02,0x0C,0x02,0x7F}, ['N'-32] = {0x7F,0x04,0x08,0x10,0x7F}, ['O'-32] = {0x3E,0x41,0x41,0x41,0x3E}, ['P'-32] = {0x7F,0x09,0x09,0x09,0x06}, ['R'-32] = {0x7F,0x09,0x19,0x29,0x46}, ['S'-32] = {0x46,0x49,0x49,0x49,0x31}, ['T'-32] = {0x01,0x01,0x7F,0x01,0x01}, ['U'-32] = {0x3F,0x40,0x40,0x40,0x3F}, ['W'-32] = {0x7F,0x20,0x18,0x20,0x7F}, ['X'-32] = {0x63,0x14,0x08,0x14,0x63}, ['Y'-32] = {0x07,0x08,0x70,0x08,0x07}, ['b'-32] = {0x7F,0x48,0x44,0x44,0x38}, ['l'-32] = {0x00,0x41,0x7F,0x40,0x00}, ['o'-32] = {0x38,0x44,0x44,0x44,0x38}, ['p'-32] = {0x7C,0x14,0x14,0x14,0x08}, ['s'-32] = {0x48,0x54,0x54,0x54,0x20}, ['y'-32] = {0x0C,0x50,0x50,0x50,0x3C}, }; static void draw_char(u8 *cv, int x, int y, char ch, int sc, int r, int g, int b) { if (ch < 32 || ch > 126) return; const u8 *gl = font5x7[ch - 32]; for (int col = 0; col < 5; col++) for (int row = 0; row < 7; row++) if (gl[col] & (1 << row)) fill_rect(cv, x + col*sc, y + row*sc, sc, sc, r, g, b); } static int text_w(const char *s, int sc) { return (int)strlen(s) * 6 * sc - sc; } static void draw_text(u8 *cv, int x, int y, const char *s, int sc, int r, int g, int b) { for (; *s; s++, x += 6*sc) draw_char(cv, x, y, *s, sc, r, g, b); } // draw centered on cx static void draw_text_c(u8 *cv, int cx, int y, const char *s, int sc, int r, int g, int b) { draw_text(cv, cx - text_w(s, sc)/2, y, s, sc, r, g, b); } // Composite the full Game Boy shell + (recolored) LCD + lit buttons into // chrome_canvas. Shared by the live sixel renderer and the frame capture path. static void build_chrome_canvas(GB *gb) { PPU *p = &gb->ppu; u8 *cv = chrome_canvas; u8 btn = gb->buttons; u8 (*fb)[SCREEN_W][3] = lcd_fb(p); // backdrop + body shell (classic "brick" gray) with a touch of depth fill_rect (cv, 0, 0, CHROME_W, CHROME_H, 26, 26, 30); fill_rrect(cv, 5, 5, CHROME_W - 10, CHROME_H - 10, 22, 201, 200, 186); fill_rect (cv, 14, 11, CHROME_W - 28, 2, 224, 223, 210); // top highlight fill_rect (cv, 16, 20, CHROME_W - 32, 2, 178, 177, 164); // ridge line fill_rect (cv, 12, CHROME_H - 15, CHROME_W - 24, 3, 176, 175, 162); // shadow // screen bezel (dark plum-gray) with the DMG accent stripe int bx = LCD_X0 - 16, by = LCD_Y0 - 20; int bw = SCREEN_W + 32, bh = SCREEN_H + 44; fill_rrect(cv, bx, by, bw, bh, 10, 74, 72, 88); fill_rect (cv, bx + 12, by + 8, 3, 5, 150, 40, 70); // red accent fill_rect (cv, bx + 17, by + 8, 3, 5, 40, 60, 140); // blue accent // power LED + vertical label in the narrow left margin (BATTERY-style) fill_circle(cv, 15, LCD_Y0 + 2, 3, 210, 60, 60); fill_circle(cv, 15, LCD_Y0 + 2, 1, 255, 180, 180); { const char *pw = "POWER"; int yy = LCD_Y0 + 12; for (const char *c = pw; *c; c++, yy += 8) draw_char(cv, 10, yy, *c, 1, 120, 118, 108); } // blit the live LCD into the bezel window for (int y = 0; y < SCREEN_H; y++) for (int x = 0; x < SCREEN_W; x++) { u8 *s = fb[y][x]; put_px(cv, LCD_X0 + x, LCD_Y0 + y, s[0], s[1], s[2]); } // subtitle under the LCD, inside the bezel draw_text_c(cv, LCD_X0 + SCREEN_W/2, LCD_Y0 + SCREEN_H + 8, "DOT MATRIX WITH STEREO SOUND", 1, 150, 148, 165); // ---- brand: "sl0pboy" ---- int logo_y = LCD_Y0 + SCREEN_H + 32; draw_text_c(cv, CHROME_W/2, logo_y, "sl0pboy", 2, 48, 46, 92); int lw = text_w("sl0pboy", 2); fill_rect(cv, CHROME_W/2 - lw/2, logo_y + 17, lw/2, 2, 210, 40, 120); // magenta fill_rect(cv, CHROME_W/2, logo_y + 17, lw/2, 2, 40, 190, 200); // cyan // ---- controls ---- int base_dk = 52, base_dg = 52, base_db = 60; // dpad / neutral dark int lit_dk = 120, lit_dg = 120, lit_db = 135; // pressed dpad highlight int ctrl_y = LCD_Y0 + SCREEN_H + 84; // D-pad (left): a dark cross, arms brighten when held int dcx = LCD_X0 + 26, dcy = ctrl_y; fill_rect(cv, dcx - 33, dcy - 11, 66, 22, base_dk, base_dg, base_db); fill_rect(cv, dcx - 11, dcy - 33, 22, 66, base_dk, base_dg, base_db); if (btn & BTN_UP) fill_rect(cv, dcx-11, dcy-33, 22, 22, lit_dk, lit_dg, lit_db); if (btn & BTN_DOWN) fill_rect(cv, dcx-11, dcy+11, 22, 22, lit_dk, lit_dg, lit_db); if (btn & BTN_LEFT) fill_rect(cv, dcx-33, dcy-11, 22, 22, lit_dk, lit_dg, lit_db); if (btn & BTN_RIGHT) fill_rect(cv, dcx+11, dcy-11, 22, 22, lit_dk, lit_dg, lit_db); fill_rect(cv, dcx - 6, dcy - 6, 12, 12, 40, 40, 46); // hub dimple // A / B buttons (right, diagonal): maroon, brighten when held int acx = LCD_X0 + SCREEN_W - 2, acy = ctrl_y - 12; int bcx = LCD_X0 + SCREEN_W - 36, bcy = ctrl_y + 4; fill_circle(cv, acx, acy, 15, 92, 92, 104); // socket ring fill_circle(cv, bcx, bcy, 15, 92, 92, 104); if (btn & BTN_A) fill_circle(cv, acx, acy, 13, 240, 96, 150); else fill_circle(cv, acx, acy, 13, 150, 30, 74); if (btn & BTN_B) fill_circle(cv, bcx, bcy, 13, 240, 96, 150); else fill_circle(cv, bcx, bcy, 13, 150, 30, 74); draw_char(cv, acx + 18, acy - 3, 'A', 1, 90, 88, 100); draw_char(cv, bcx + 18, bcy - 3, 'B', 1, 90, 88, 100); // Select / Start pills (center bottom), angled + labelled int py = ctrl_y + 30; int selx = CHROME_W/2 - 34, stax = CHROME_W/2 + 6; if (btn & BTN_SELECT) fill_rrect(cv, selx, py, 28, 9, 4, 150,150,165); else fill_rrect(cv, selx, py, 28, 9, 4, 92,92,104); if (btn & BTN_START) fill_rrect(cv, stax, py, 28, 9, 4, 150,150,165); else fill_rrect(cv, stax, py, 28, 9, 4, 92,92,104); draw_text_c(cv, selx + 14, py + 13, "SELECT", 1, 78, 76, 88); draw_text_c(cv, stax + 14, py + 13, "START", 1, 78, 76, 88); // speaker grille (bottom-right): slanted rows of dark dots int sgx = CHROME_W - 78, sgy = CHROME_H - 56; for (int i = 0; i < 5; i++) for (int j = 0; j < 6; j++) fill_circle(cv, sgx + j*8 + i*4, sgy + i*8, 2, 168, 167, 154); } static void render_frame_chrome(GB *gb) { if (!sixel_buf) return; build_chrome_canvas(gb); int ncol = build_palette_buf(chrome_canvas, CHROME_W, CHROME_H, chrome_idx); char *o = encode_sixel_buf(sixel_buf, CHROME_W, CHROME_H, sixel_scale, ncol, chrome_idx); need_full = false; fwrite(sixel_buf, 1, o - sixel_buf, stdout); fflush(stdout); } // ---- frame capture (for screenshots / video recording) --------------------- // Produce the exact frame the user sees - LCD recolor plus the Game Boy chrome // if enabled - as a contiguous RGB888 buffer, so captures match the display. void render_capture_size(int *w, int *h) { if (w) *w = chrome_enabled ? CHROME_W : SCREEN_W; if (h) *h = chrome_enabled ? CHROME_H : SCREEN_H; } const u8 *render_capture_frame(GB *gb, int *w, int *h) { render_capture_size(w, h); if (chrome_enabled) { build_chrome_canvas(gb); return chrome_canvas; } u8 (*fb)[SCREEN_W][3] = lcd_fb(&gb->ppu); return &fb[0][0][0]; } void render_frame(GB *gb) { if (sixel_enabled) { if (chrome_enabled) render_frame_chrome(gb); else render_frame_sixel(gb); return; } PPU *p = &gb->ppu; u8 (*fb)[SCREEN_W][3] = lcd_fb(p); 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 = fb[y][x]; u8 *bot = 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, 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; }