#include "gb.h" #include "cpu.h" #include "render.h" #include "control.h" #include #include #include #include #include #include #include #include #include #include // Connect the link port to a unix-domain stream socket (the network bridge // lives at the other end). Retries briefly so the launcher's listen()/accept() // can win the race. Returns a connected fd, or -1. static int serial_connect(const char *path) { for (int try = 0; try < 50; try++) { int fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) return -1; struct sockaddr_un a; memset(&a, 0, sizeof a); a.sun_family = AF_UNIX; strncpy(a.sun_path, path, sizeof(a.sun_path) - 1); if (connect(fd, (struct sockaddr *)&a, sizeof a) == 0) return fd; close(fd); struct timespec ts = { 0, 20000000 }; // 20 ms nanosleep(&ts, NULL); } return -1; } static volatile sig_atomic_t running = 1; static void on_sig(int s) { (void)s; running = 0; } static double now_sec(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec + ts.tv_nsec / 1e9; } static void run_frame(GB *gb) { gb->ppu.frame_ready = false; // safety bound in case LCD is off (no frame_ready) u64 budget = gb->cycles + 70224 * 2; while (!gb->ppu.frame_ready && gb->cycles < budget && !gb->poweroff) cpu_step(gb); } // Scripted button taps for testing: each char in `keys` = one token occupying // 20 frames (pressed for the first 10, released for the next 10, so the guest // sees a clean press+release edge). u/d/l/r=dpad, a/b, s=select, e=start, .=wait. static u8 keys_mask(const char *keys, int frame) { int tok = frame / 20; if (tok >= (int)strlen(keys)) return 0; if ((frame % 20) >= 10) return 0; switch (keys[tok]) { case 'u': return BTN_UP; case 'd': return BTN_DOWN; case 'l': return BTN_LEFT; case 'r': return BTN_RIGHT; case 'a': return BTN_A; case 'b': return BTN_B; case 's': return BTN_SELECT; case 'e': return BTN_START; default: return 0; } } static void print_usage(FILE *out, const char *prog) { fprintf(out, "usage: %s [options] \n" "\n" "Emulate a Game Boy / Game Boy Color ROM. The ROM path is the final\n" "argument; all options precede it.\n" "\n" "display:\n" " --sixel [scale] sixel graphics output; scale = pixel zoom 1-6 (def 2)\n" " --chrome draw a Game Boy body around the LCD (sixel display)\n" " --palette NAME recolor the LCD: dmg|green (pea green), pocket|gray\n" " --shrink N shrink the half-block display N x (1-4); capture and\n" " recording stay full-res\n" " --green alias for --palette dmg\n" " --frameskip N draw 1 of every (N+1) frames\n" "\n" "speed:\n" " --fps N emulation speed cap in frames/sec (default 59.73)\n" " --uncapped run as fast as possible (alias: --turbo)\n" "\n" "boot:\n" " --bios [path] run a boot ROM first (default bios/gbc_bios.bin)\n" "\n" "control channels:\n" " --fifo [path] button-input named pipe (default /tmp/sl0pboy.fifo)\n" " --sock [path] control/debug socket (default /tmp/sl0pboy.sock)\n" "\n" "control channels (cont.):\n" " --serial-sock PATH link/serial port over a unix socket (network bridge);\n" " works with a live display, unlike --headless stdio\n" "\n" "headless & testing:\n" " --headless no video; wire serial <-> stdio (OS console)\n" " --test [seconds] run N seconds then exit, logging serial (default 30)\n" " --shot N [file] run N frames, write a PPM screenshot (default shot.ppm)\n" " --keys STR scripted button taps: u d l r a b s(elect) e(start) .\n" "\n" " -h, --help show this help and exit\n", prog); } int main(int argc, char **argv) { const char *prog = argv[0]; // --help / -h anywhere on the line wins. for (int i = 1; i < argc; i++) if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { print_usage(stdout, prog); return 0; } if (argc < 2) { print_usage(stderr, prog); return 1; } // The ROM is the final argument; everything before it is an option. const char *rom = argv[argc - 1]; if (rom[0] == '-') { fprintf(stderr, "%s: no ROM specified (it must be the final argument)\n\n", prog); print_usage(stderr, prog); return 1; } bool test = false; double seconds = 30.0; int shot_frames = -1; const char *shot_path = "shot.ppm"; const char *fifo_path = NULL; const char *sock_path = NULL; double target_fps = 59.73; // emulation speed cap; 0 = uncapped int frameskip = 0; // draw 1 of every (frameskip+1) frames bool sixel = false; // use sixel graphics output int sixel_scale = 2; // integer pixel zoom for sixel bool chrome = false; // draw a Game Boy body around the LCD (sixel) const char *palette = NULL; // LCD recolor: dmg/green, pocket/gray, or off bool headless = false; // no video; serial <-> stdio (OS debug console) const char *bios_path = NULL; // boot ROM to run before the cartridge const char *keys = NULL; // scripted button taps for testing (u d l r a b s e .) const char *serial_sock = NULL; // link/serial port over a unix socket // Parse options in argv[1 .. argc-2]; the final argument is the ROM and is // excluded here so options with optional values never swallow it. for (int i = 1; i < argc - 1; i++) { const char *a = argv[i]; // A value is available only if the next token is still within the // option range (not the ROM) and isn't itself an option. bool has_val = (i + 1 < argc - 1) && argv[i+1][0] != '-'; if (!strcmp(a, "--test")) { test = true; if (has_val) seconds = atof(argv[++i]); } else if (!strcmp(a, "--shot")) { if (!has_val) { fprintf(stderr, "%s: --shot needs a frame count\n", prog); return 1; } shot_frames = atoi(argv[++i]); if ((i + 1 < argc - 1) && argv[i+1][0] != '-') shot_path = argv[++i]; } else if (!strcmp(a, "--keys")) { if (!has_val) { fprintf(stderr, "%s: --keys needs a value\n", prog); return 1; } keys = argv[++i]; } else if (!strcmp(a, "--fifo")) { fifo_path = has_val ? argv[++i] : "/tmp/sl0pboy.fifo"; } else if (!strcmp(a, "--sock") || !strcmp(a, "--socket")) { sock_path = has_val ? argv[++i] : "/tmp/sl0pboy.sock"; } else if (!strcmp(a, "--serial-sock") || !strcmp(a, "--netsock")) { if (!has_val) { fprintf(stderr, "%s: --serial-sock needs a path\n", prog); return 1; } serial_sock = argv[++i]; } else if (!strcmp(a, "--fps")) { if (!has_val) { fprintf(stderr, "%s: --fps needs a value\n", prog); return 1; } target_fps = atof(argv[++i]); } else if (!strcmp(a, "--frameskip")) { if (!has_val) { fprintf(stderr, "%s: --frameskip needs a value\n", prog); return 1; } frameskip = atoi(argv[++i]); } else if (!strcmp(a, "--sixel")) { sixel = true; if (has_val) sixel_scale = atoi(argv[++i]); } else if (!strcmp(a, "--shrink")) { if (!has_val) { fprintf(stderr, "%s: --shrink needs a value\n", prog); return 1; } render_set_shrink(atoi(argv[++i])); } else if (!strcmp(a, "--chrome")) { chrome = true; } else if (!strcmp(a, "--palette")) { palette = has_val ? argv[++i] : "dmg"; } else if (!strcmp(a, "--green")) { palette = "dmg"; } else if (!strcmp(a, "--uncapped") || !strcmp(a, "--turbo")) { target_fps = 0; } else if (!strcmp(a, "--headless")) { headless = true; } else if (!strcmp(a, "--bios") || !strcmp(a, "--boot")) { bios_path = has_val ? argv[++i] : "bios/gbc_bios.bin"; } else { fprintf(stderr, "%s: unknown option '%s'\n\n", prog, a); print_usage(stderr, prog); return 1; } } if (frameskip < 0) frameskip = 0; GB *gb = calloc(1, sizeof(GB)); if (cart_load(&gb->cart, rom) != 0) { free(gb); return 1; } if (bios_path && gb_load_bootrom(gb, bios_path) != 0) { cart_free(&gb->cart); free(gb); return 1; } gb_reset(gb); if (bios_path) fprintf(stderr, "booting through BIOS: %s\n", bios_path); // Link port over a unix socket: wire the serial fds to it (non-blocking, no // EOF-exit). Works in any display mode - the network is independent of the // console, which stays on the LCD + OSK. int serial_fd = -1; if (serial_sock) { serial_fd = serial_connect(serial_sock); if (serial_fd < 0) { fprintf(stderr, "%s: could not connect --serial-sock %s\n", prog, serial_sock); cart_free(&gb->cart); free(gb); return 1; } int fl = fcntl(serial_fd, F_GETFL, 0); if (fl != -1) fcntl(serial_fd, F_SETFL, fl | O_NONBLOCK); gb->serial_in_fd = serial_fd; gb->serial_out_fd = serial_fd; gb->serial_no_eof = true; gb->serial_sock_path = serial_sock; // enables auto-reconnect (gb.c) // A dead peer must degrade the link, not kill the emulator: without // this, a write() after the bridge goes away raises SIGPIPE and the // whole Game Boy silently dies - exactly the "GB left and never came // back" failure mode. signal(SIGPIPE, SIG_IGN); fprintf(stderr, "link port <-> %s\n", serial_sock); } // Display/capture options that also affect screenshots and video recording, // so set them before the --shot / recording paths. (--sixel below is purely // a live-display concern.) if (palette) render_set_palette(palette); if (chrome) render_set_chrome(true); if (test) { gb->serial_log = true; u64 target = (u64)(seconds * 4194304.0); while (gb->cycles < target) cpu_step(gb); fprintf(stderr, "\n[ran %llu cycles]\n", (unsigned long long)gb->cycles); cart_free(&gb->cart); free(gb); return 0; } // Headless: no rendering, no terminal UI. Tie the serial port to stdio so // an OS on the GB gets a plain byte console (TX -> stdout, RX <- stdin). // Paced to native speed unless --uncapped. Piping works for scripted tests: // printf 'ls\n' | sl0pboy rom.gb --headless if (headless) { if (serial_fd < 0) { // --serial-sock overrides stdio serial gb->serial_out_fd = STDOUT_FILENO; gb->serial_in_fd = STDIN_FILENO; gb->serial_no_eof = (keys != NULL); // OSK provides input; don't EOF-exit } // --sock works headless too: a scriptable GB you can also debug // (read/write memory, breakpoints, button injection) is far more // useful than one you can't see into. if (sock_path) { control_open(sock_path); if (control_active()) fprintf(stderr, "control socket: %s\n", sock_path); } int keys_frame = 0; int fl = fcntl(STDIN_FILENO, F_GETFL, 0); if (fl != -1) fcntl(STDIN_FILENO, F_SETFL, fl | O_NONBLOCK); signal(SIGINT, on_sig); signal(SIGTERM, on_sig); // Interactive: if stdin is a terminal, drop line-buffering + local echo // so keystrokes reach the guest immediately and only the guest echoes. // (Piped/redirected stdin is left alone, for scripted input.) struct termios saved_tio; bool raw_tty = isatty(STDIN_FILENO) && tcgetattr(STDIN_FILENO, &saved_tio) == 0; if (raw_tty) { struct termios t = saved_tio; t.c_lflag &= ~(ICANON | ECHO); // char-at-a-time, no echo (keep ISIG) // VMIN=1 (not 0): with O_NONBLOCK, "no data" is EAGAIN, not a 0-byte // read that we'd otherwise misread as EOF. t.c_cc[VMIN] = 1; t.c_cc[VTIME] = 0; tcsetattr(STDIN_FILENO, TCSANOW, &t); fprintf(stderr, "[sl0pboy: interactive serial console - Ctrl-C to quit]\n"); } bool uncapped = (target_fps <= 0); double start_t = now_sec(); u64 start_cyc = gb->cycles; const double HZ = 4194304.0; while (running && !gb->poweroff) { if (keys) { gb->buttons = keys_mask(keys, keys_frame); if (keys_frame++ > (int)strlen(keys) * 20 + 40) break; } if (sock_path && !control_poll(gb)) break; // debug socket quit run_frame(gb); // advance ~a frame's worth of cycles if (!uncapped) { double target = start_t + (gb->cycles - start_cyc) / HZ; double sleep = target - now_sec(); if (sleep > 0) { struct timespec ts = { (time_t)sleep, (long)((sleep - (time_t)sleep) * 1e9) }; nanosleep(&ts, NULL); } else if (sleep < -0.5) { start_t = now_sec(); start_cyc = gb->cycles; // resync } } } if (raw_tty) tcsetattr(STDIN_FILENO, TCSANOW, &saved_tio); control_close(); fprintf(stderr, "[sl0pboy] headless loop exit: %s\n", gb->poweroff ? "guest poweroff" : "signal"); if (shot_frames >= 0) { // --headless --shot FILE: dump final LCD int cw, ch; const u8 *cap = render_capture_frame(gb, &cw, &ch); FILE *pf = fopen(shot_path, "wb"); fprintf(pf, "P6\n%d %d\n255\n", cw, ch); fwrite(cap, 1, (size_t)cw * ch * 3, pf); fclose(pf); } cart_free(&gb->cart); free(gb); return 0; } if (shot_frames >= 0) { for (int f = 0; f < shot_frames; f++) run_frame(gb); int cw, ch; const u8 *cap = render_capture_frame(gb, &cw, &ch); FILE *pf = fopen(shot_path, "wb"); fprintf(pf, "P6\n%d %d\n255\n", cw, ch); fwrite(cap, 1, (size_t)cw * ch * 3, pf); fclose(pf); fprintf(stderr, "wrote %s after %d frames\n", shot_path, shot_frames); cart_free(&gb->cart); free(gb); return 0; } signal(SIGINT, on_sig); signal(SIGTERM, on_sig); if (fifo_path) { input_open_fifo(fifo_path); fprintf(stderr, "control fifo: echo 'a' > %s (a b start select " "up down left right; +x/-x hold/release; release)\n", fifo_path); } if (sock_path) { control_open(sock_path); if (control_active()) fprintf(stderr, "control socket: %s (buttons + debug: cpu read " "write step break watch continue pause; 'help')\n", sock_path); } if (sixel) render_set_sixel(true, sixel_scale); if (chrome && !sixel) fprintf(stderr, "note: --chrome only affects live display in --sixel mode " "(it still applies to --shot / recordings)\n"); // A sixel stream is fragile: any stray stderr text (link reconnect notices, // input debug, cart warnings) lands mid-stream and corrupts the picture. // Silence stderr while the live sixel display is up; restored on exit so // the final stats line still prints. int saved_stderr = -1; if (sixel) { fflush(stderr); saved_stderr = dup(STDERR_FILENO); int devnull = open("/dev/null", O_WRONLY); if (devnull >= 0) { dup2(devnull, STDERR_FILENO); close(devnull); } } term_init(); // The emulator always advances every GB frame at the paced native rate so // games run at correct speed. Frame *skipping* only affects how often we // actually draw to the terminal: rendering is the expensive part, so on a // slow terminal we render 1 of every (frameskip+1) frames while emulation // keeps real-time. In turbo we run uncapped and additionally clamp redraws // to ~60 Hz so the terminal isn't flooded. bool turbo = (target_fps <= 0); double last_render = 0; double start_time = now_sec(); double next_emu = start_time; u64 frames = 0, drawn = 0; int since_render = 0; while (running) { frames++; if (!input_poll(gb)) break; if (!control_poll(gb)) break; // socket may inject input / debug cmds if (input_take_turbo()) turbo = !turbo; frameskip += input_take_frameskip_delta(); if (frameskip < 0) frameskip = 0; if (frameskip > 30) frameskip = 30; render_set_hud(frameskip, turbo); // Emulate a full frame, honoring debugger pause/step/breakpoints when // the control socket is active (otherwise a plain full frame). control_input_frame(gb); // drive input from a replay movie if playing control_run_frame(gb); control_record_frame(gb); // append to the video capture if recording // decide whether to draw this frame bool do_render = (since_render >= frameskip); double t = now_sec(); if (do_render && turbo && (t - last_render) < 1.0 / 60.0) do_render = false; // clamp turbo redraws to ~60 Hz if (do_render) { render_frame(gb); last_render = t; since_render = 0; drawn++; } else { since_render++; } // pace emulation to native speed (skip sleeping in turbo) double emu_dt = turbo ? 0.0 : 1.0 / target_fps; if (emu_dt > 0.0) { next_emu += emu_dt; double sleep = next_emu - now_sec(); if (sleep > 0) { struct timespec ts = { (time_t)sleep, (long)((sleep - (time_t)sleep) * 1e9) }; nanosleep(&ts, NULL); } else if (sleep < -0.1) { next_emu = now_sec(); // fell behind; resync } } else { next_emu = now_sec(); // uncapped: keep baseline fresh } } term_restore(); if (saved_stderr >= 0) { // un-silence stderr (sixel mode) fflush(stderr); dup2(saved_stderr, STDERR_FILENO); close(saved_stderr); } input_close_fifo(); control_close(); double elapsed = now_sec() - start_time; if (elapsed > 0) fprintf(stderr, "emulated %llu frames (%.1f fps), drew %llu (%.1f fps) " "in %.2fs\n", (unsigned long long)frames, frames / elapsed, (unsigned long long)drawn, drawn / elapsed, elapsed); cart_free(&gb->cart); free(gb); return 0; }