#include "gb.h" #include "cpu.h" #include "render.h" #include "control.h" #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) 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]]\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 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; } } 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; } 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_run_frame(gb); // 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; }