diff options
| author | gbc dev <gbc@localhost> | 2026-07-17 00:27:18 +0200 |
|---|---|---|
| committer | gbc dev <gbc@localhost> | 2026-07-17 00:27:18 +0200 |
| commit | 6c0eaf8469a3f4876cb067e11824a030bf37bcac (patch) | |
| tree | 731f75c98d3da0ea420817f8116895d994924039 | |
| parent | gb: boot ROM (BIOS) support (diff) | |
| download | sl0pboy-6c0eaf8469a3f4876cb067e11824a030bf37bcac.tar.gz sl0pboy-6c0eaf8469a3f4876cb067e11824a030bf37bcac.tar.xz sl0pboy-6c0eaf8469a3f4876cb067e11824a030bf37bcac.zip | |
render: Game Boy chrome, LCD palette recolor, and frame capture
- --chrome: draw a DMG-style 'sl0pboy' shell around the LCD in sixel mode,
with a bitmap-font silk-screen (logo, labels), speaker grille, power LED,
and buttons that light up while held.
- LCD palette recolor (render_set_palette): remap the LCD to a 4-shade
palette by luminance for the classic monochrome look ('dmg'/'green' pea
green, 'pocket'/'gray'); applies to every render path.
- Refactor the sixel encoder into generic build_palette_buf/encode_sixel_buf
operating on any RGB888 buffer, shared by the bare LCD and the chrome canvas.
- render_capture_size/frame: produce the exact displayed frame (recolor +
chrome) as an RGB buffer, for screenshots and video recording.
| -rw-r--r-- | src/render.c | 371 | ||||
| -rw-r--r-- | src/render.h | 16 |
2 files changed, 348 insertions, 39 deletions
diff --git a/src/render.c b/src/render.c index 76a4ad0..4f612ae 100644 --- a/src/render.c +++ b/src/render.c @@ -55,7 +55,18 @@ 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; @@ -66,7 +77,8 @@ void render_set_sixel(bool on, int scale) { sixel_enabled = on; sixel_scale = scale; if (on) { - int W2 = SCREEN_W * scale, H2 = SCREEN_H * scale; + // 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; @@ -79,8 +91,55 @@ void render_set_sixel(bool on, int scale) { 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) { @@ -93,50 +152,48 @@ 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. +// 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]; -static u8 idxmap[SCREEN_H][SCREEN_W]; // palette index per source pixel -static int build_palette(PPU *p) { +// 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; - 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]; + 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; } } -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; - +// 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) @@ -146,14 +203,15 @@ static void render_frame_sixel(GB *gb) { (pal_b[i] * 100 + 127) / 255); static u8 present[256]; - static u8 sx[SCREEN_W * 6]; // sixel value per column for the active color + 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; - for (int x = 0; x < SCREEN_W; x++) present[idxmap[sy][x]] = 1; + 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++) { @@ -164,7 +222,7 @@ static void render_frame_sixel(GB *gb) { 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); + if (idx[((by + k) / sc) * w + sx0] == c) bits |= (1 << k); sx[X2] = (u8)(0x3f + bits); } o += sprintf(o, "#%d", c); @@ -181,7 +239,18 @@ static void render_frame_sixel(GB *gb) { *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; @@ -189,9 +258,233 @@ static void render_frame_sixel(GB *gb) { 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) { render_frame_sixel(gb); return; } + 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"); } @@ -206,8 +499,8 @@ void render_frame(GB *gb) { 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]; + 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] && @@ -239,7 +532,7 @@ void render_frame(GB *gb) { hud_frameskip, hud_turbo ? " TURBO" : ""); } - memcpy(prev_fb, p->fb, sizeof(prev_fb)); + memcpy(prev_fb, fb, sizeof(prev_fb)); need_full = false; if (o != outbuf) { // nothing changed -> write nothing diff --git a/src/render.h b/src/render.h index 03d6c7c..1be1290 100644 --- a/src/render.h +++ b/src/render.h @@ -11,6 +11,22 @@ void render_set_hud(int frameskip, bool turbo); // update HUD readout // Switch terminal output to sixel graphics (scale = integer pixel zoom, >=1). // Pass on=false to use the default Unicode half-block renderer. void render_set_sixel(bool on, int scale); +// Draw a Game Boy body/frame around the LCD (sixel output only); buttons on the +// drawn shell light up while pressed. No effect unless sixel mode is enabled. +void render_set_chrome(bool on); +// Recolor the LCD output to a 4-shade palette by luminance, for the classic +// monochrome-LCD look. Names: "dmg"/"green" (DMG-01 pea green), "pocket"/"gray" +// (grayscale). NULL or unknown name = passthrough (true colors). Applies to +// every render path (half-block, sixel, chrome). +void render_set_palette(const char *name); + +// Frame capture for screenshots / video recording: produces the same pixels the +// user sees, applying the LCD recolor palette and the Game Boy chrome when they +// are enabled. render_capture_size reports the resulting dimensions (they only +// change when chrome is toggled); render_capture_frame returns a pointer to an +// internal contiguous RGB888 buffer (w*h*3 bytes, caller must not free). +void render_capture_size(int *w, int *h); +const u8 *render_capture_frame(GB *gb, int *w, int *h); // Poll keyboard + control FIFO, update gb->buttons. False if quit requested. bool input_poll(GB *gb); |
