diff options
| author | gbc dev <gbc@localhost> | 2026-07-04 23:07:50 +0200 |
|---|---|---|
| committer | gbc dev <gbc@localhost> | 2026-07-04 23:07:50 +0200 |
| commit | f4ba890eb4b98e1a65a3d9fc8ba8f62dda66895b (patch) | |
| tree | cb1cccb2dfd0f1b59e59e8e86454849179c88cb2 | |
| parent | input: external control channel via FIFO (diff) | |
| download | sl0pboy-f4ba890eb4b98e1a65a3d9fc8ba8f62dda66895b.tar.gz sl0pboy-f4ba890eb4b98e1a65a3d9fc8ba8f62dda66895b.tar.xz sl0pboy-f4ba890eb4b98e1a65a3d9fc8ba8f62dda66895b.zip | |
speed control: frame skipping to decouple terminal draw from emulation
Emulation always runs at native rate (correct game speed); --frameskip N draws
only 1 of every N+1 frames so a slow terminal no longer bottlenecks the loop.
Adds --fps cap, --uncapped/--turbo, live [ ] and f keys, HUD readout, and an
exit summary of emulated-vs-drawn fps. Replaces the confusing --render-fps.
| -rw-r--r-- | README.md | 26 | ||||
| -rw-r--r-- | src/main.c | 72 | ||||
| -rw-r--r-- | src/render.c | 29 | ||||
| -rw-r--r-- | src/render.h | 6 |
4 files changed, 120 insertions, 13 deletions
@@ -25,6 +25,30 @@ make Requires a truecolor-capable terminal (`COLORTERM=truecolor`) at least 160 columns wide. +### Speed & frame skipping + +The emulator always advances every Game Boy frame at the correct rate, so +games run at their intended speed. Because writing a frame to the terminal is +by far the most expensive step, drawing can be *decoupled* from emulation via +frame skipping — the machine keeps running in real time while the terminal is +refreshed less often: + +```sh +./build/gbc rom.gb --frameskip 2 # emulate 60 fps, draw every 3rd frame (20 fps) +./build/gbc rom.gb --fps 120 # run the game at 2x speed +./build/gbc rom.gb --uncapped # run as fast as possible (turbo) +``` + +| Flag | Meaning | +|-------------------|-----------------------------------------------------| +| `--frameskip N` | draw 1 of every `N+1` emulated frames (default 0) | +| `--fps N` | emulation speed cap in fps (default 59.73 native) | +| `--uncapped`/`--turbo` | run emulation as fast as possible | + +Frame skip and turbo are also adjustable live with `[` / `]` and `f`; in turbo +mode terminal redraws are additionally clamped to ~60 Hz. On exit the emulator +prints how many frames it emulated vs. actually drew. + ## Controls | Key | Button | @@ -34,6 +58,8 @@ Requires a truecolor-capable terminal (`COLORTERM=truecolor`) at least | `x` / `k` | B | | enter | Start | | space | Select | +| `f` | toggle fast-forward (turbo) | +| `[` / `]` | decrease / increase frame skip | | `q` / Ctrl-C | Quit | Since terminals don't report key-release, buttons are held for a few frames @@ -35,6 +35,8 @@ int main(int argc, char **argv) { int shot_frames = -1; const char *shot_path = "shot.ppm"; const char *fifo_path = NULL; + double target_fps = 59.73; // emulation speed cap; 0 = uncapped + int frameskip = 0; // draw 1 of every (frameskip+1) frames for (int i = 2; i < argc; i++) { if (!strcmp(argv[i], "--test")) { test = true; @@ -45,8 +47,15 @@ int main(int argc, char **argv) { } 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], "--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], "--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; } @@ -83,26 +92,67 @@ int main(int argc, char **argv) { } term_init(); - const double frame_time = 1.0 / 59.73; - double next = now_sec(); + // 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; - run_frame(gb); - render_frame(gb); + 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); - next += frame_time; + run_frame(gb); // always emulate a full frame + + // decide whether to draw this frame + bool do_render = (since_render >= frameskip); double t = now_sec(); - double sleep = next - t; - if (sleep > 0) { - struct timespec ts = { (time_t)sleep, (long)((sleep - (time_t)sleep) * 1e9) }; - nanosleep(&ts, NULL); - } else if (sleep < -0.1) { - next = t; // fell behind; resync + 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(); + 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; diff --git a/src/render.c b/src/render.c index f659da3..df39c99 100644 --- a/src/render.c +++ b/src/render.c @@ -51,8 +51,19 @@ static char outbuf[SCREEN_W * (SCREEN_H/2) * 40 + 4096]; static u8 prev_fb[SCREEN_H][SCREEN_W][3]; static bool need_full = true; // force a complete repaint static volatile sig_atomic_t resized = 0; // set by SIGWINCH +static int hud_frameskip = 0; +static bool hud_turbo = false; void render_force_full(void) { need_full = true; } + +// 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) { + hud_frameskip = frameskip; + hud_turbo = turbo; + need_full = true; + } +} static void on_winch(int s) { (void)s; resized = 1; } void render_frame(GB *gb) { @@ -99,8 +110,9 @@ void render_frame(GB *gb) { // status line: static text, only (re)draw on a full repaint if (need_full) { o += sprintf(o, "\x1b[0m\x1b[%d;1H\x1b[38;2;150;150;150m %.*s " - "[wasd/arrows] z:A x:B space:sel enter:start q:quit", - SCREEN_H/2 + 1, 15, gb->cart.title); + "skip:%d%s [f]ast [ ][ ] q:quit\x1b[K", + SCREEN_H/2 + 1, 12, gb->cart.title, + hud_frameskip, hud_turbo ? " TURBO" : ""); } memcpy(prev_fb, p->fb, sizeof(prev_fb)); @@ -119,8 +131,18 @@ void render_frame(GB *gb) { // decay[] : momentary presses (keyboard, or FIFO taps), count down per frame static int decay[8]; // frames remaining for each BTN bit position static u8 held; // explicitly-held buttons (FIFO) +static int turbo_toggles; // pending fast-forward toggles (consumed by main) +static int frameskip_delta;// pending frameskip adjustment (consumed by main) #define HOLD_FRAMES 4 +bool input_take_turbo(void) { + if (turbo_toggles > 0) { turbo_toggles--; return true; } + return false; +} +int input_take_frameskip_delta(void) { + int d = frameskip_delta; frameskip_delta = 0; return d; +} + static int mask_index(int mask) { for (int i = 0; i < 8; i++) if ((1 << i) == mask) return i; return -1; @@ -226,6 +248,9 @@ bool input_poll(GB *gb) { case 'x': case 'k': tap(BTN_B, HOLD_FRAMES); break; case '\r': case '\n': tap(BTN_START, HOLD_FRAMES); break; case ' ': tap(BTN_SELECT, HOLD_FRAMES); break; + case 'f': turbo_toggles++; break; // toggle fast-forward + case '[': frameskip_delta--; break; // fewer skipped frames + case ']': frameskip_delta++; break; // more skipped frames case 'q': case 3: quit = true; break; } } diff --git a/src/render.h b/src/render.h index 27da080..3b6c7e4 100644 --- a/src/render.h +++ b/src/render.h @@ -7,6 +7,7 @@ void term_init(void); void term_restore(void); void render_frame(GB *gb); void render_force_full(void); // force a complete repaint next frame +void render_set_hud(int frameskip, bool turbo); // update HUD readout // Poll keyboard + control FIFO, update gb->buttons. False if quit requested. bool input_poll(GB *gb); @@ -15,4 +16,9 @@ bool input_poll(GB *gb); void input_open_fifo(const char *path); void input_close_fifo(void); +// Returns true once for each pending fast-forward (turbo) toggle keypress. +bool input_take_turbo(void); +// Net frameskip adjustment requested via '[' / ']' since last call. +int input_take_frameskip_delta(void); + #endif |
