#include "gb.h" #include "cpu.h" #include "render.h" #include "control.h" #include #include #include #include #include #include #include #include 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); } int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "usage: %s [--test seconds] [--sixel [scale]] " "[--frameskip n] [--fps n] [--uncapped] [--fifo [path]] " "[--sock [path]] [--headless]\n", argv[0]); return 1; } const char *rom = argv[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 headless = false; // no video; serial <-> stdio (OS debug console) for (int i = 2; i < argc; i++) { if (!strcmp(argv[i], "--test")) { test = true; if (i + 1 < argc) seconds = atof(argv[++i]); } else if (!strcmp(argv[i], "--shot")) { if (i + 1 < argc) shot_frames = atoi(argv[++i]); if (i + 1 < argc && argv[i+1][0] != '-') shot_path = argv[++i]; } else if (!strcmp(argv[i], "--fifo")) { fifo_path = (i + 1 < argc && argv[i+1][0] != '-') ? argv[++i] : "/tmp/gbc.fifo"; } else if (!strcmp(argv[i], "--sock") || !strcmp(argv[i], "--socket")) { sock_path = (i + 1 < argc && argv[i+1][0] != '-') ? argv[++i] : "/tmp/gbc.sock"; } else if (!strcmp(argv[i], "--fps")) { if (i + 1 < argc) target_fps = atof(argv[++i]); } else if (!strcmp(argv[i], "--frameskip")) { if (i + 1 < argc) frameskip = atoi(argv[++i]); } else if (!strcmp(argv[i], "--sixel")) { sixel = true; if (i + 1 < argc && argv[i+1][0] != '-') sixel_scale = atoi(argv[++i]); } else if (!strcmp(argv[i], "--uncapped") || !strcmp(argv[i], "--turbo")) { target_fps = 0; } else if (!strcmp(argv[i], "--headless")) { headless = true; } } if (frameskip < 0) frameskip = 0; GB *gb = calloc(1, sizeof(GB)); if (cart_load(&gb->cart, rom) != 0) { free(gb); return 1; } gb_reset(gb); 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' | gbc rom.gb --headless if (headless) { gb->serial_out_fd = STDOUT_FILENO; gb->serial_in_fd = STDIN_FILENO; 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, "[gbc: 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) { 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); cart_free(&gb->cart); free(gb); return 0; } if (shot_frames >= 0) { for (int f = 0; f < shot_frames; f++) run_frame(gb); FILE *pf = fopen(shot_path, "wb"); fprintf(pf, "P6\n%d %d\n255\n", SCREEN_W, SCREEN_H); for (int y = 0; y < SCREEN_H; y++) for (int x = 0; x < SCREEN_W; x++) fwrite(gb->ppu.fb[y][x], 1, 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); 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(); 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; }