aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorgbc dev <gbc@localhost>2026-07-17 00:27:46 +0200
committergbc dev <gbc@localhost>2026-07-17 00:27:46 +0200
commit6c906a750675910323710b293f939a5f26dadd58 (patch)
treed60d58afe47dfb6077cc12924cf84a9be5d91d93 /src
parentrecord: capture the displayed frame (chrome + palette) (diff)
downloadsl0pboy-6c906a750675910323710b293f939a5f26dadd58.tar.gz
sl0pboy-6c906a750675910323710b293f939a5f26dadd58.tar.xz
sl0pboy-6c906a750675910323710b293f939a5f26dadd58.zip
main: options-first CLI with --help, and wire up display/boot flags
- Rewrite argument handling: the ROM is now the final argument and options precede it (parsed in argv[1..argc-2] so optional-value flags never swallow the ROM). Add grouped --help/-h, unknown-option errors, and clear 'missing value'/'no ROM' diagnostics. - Add --chrome, --palette/--green, and --bios flags; set the display/capture options before the --shot and recording paths so screenshots and recordings honor them. Make --shot dump the composited capture frame. - gbctl: pass the ROM last when spawning the emulator. - gitignore the (copyrighted) bios/ directory; document all new flags.
Diffstat (limited to 'src')
-rw-r--r--src/main.c152
1 files changed, 116 insertions, 36 deletions
diff --git a/src/main.c b/src/main.c
index 40efe29..1f3a868 100644
--- a/src/main.c
+++ b/src/main.c
@@ -44,15 +44,59 @@ static u8 keys_mask(const char *keys, int frame) {
}
}
+static void print_usage(FILE *out, const char *prog) {
+ fprintf(out,
+"usage: %s [options] <rom>\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"
+" --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/gbc.fifo)\n"
+" --sock [path] control/debug socket (default /tmp/gbc.sock)\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) {
- if (argc < 2) {
- fprintf(stderr, "usage: %s <rom> [--test seconds] [--sixel [scale]] "
- "[--frameskip n] [--fps n] [--uncapped] [--fifo [path]] "
- "[--sock [path]] [--headless]\n",
- argv[0]);
+ 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;
}
- const char *rom = argv[1];
bool test = false;
double seconds = 30.0;
int shot_frames = -1;
@@ -63,41 +107,76 @@ int main(int argc, char **argv) {
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 .)
- for (int i = 2; i < argc; i++) {
- if (!strcmp(argv[i], "--test")) {
+
+ // 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 (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], "--keys")) {
- if (i + 1 < argc) keys = 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")) {
+ 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/gbc.fifo";
+ } else if (!strcmp(a, "--sock") || !strcmp(a, "--socket")) {
+ sock_path = has_val ? argv[++i] : "/tmp/gbc.sock";
+ } 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 (i + 1 < argc && argv[i+1][0] != '-') sixel_scale = atoi(argv[++i]);
- } else if (!strcmp(argv[i], "--uncapped") || !strcmp(argv[i], "--turbo")) {
+ if (has_val) sixel_scale = 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(argv[i], "--headless")) {
+ } 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);
+
+ // 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;
@@ -161,11 +240,10 @@ int main(int argc, char **argv) {
}
if (raw_tty) tcsetattr(STDIN_FILENO, TCSANOW, &saved_tio);
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", 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);
+ 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;
@@ -173,11 +251,10 @@ int main(int argc, char **argv) {
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", 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);
+ 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;
@@ -198,6 +275,9 @@ int main(int argc, char **argv) {
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");
term_init();
// The emulator always advances every GB frame at the paced native rate so