diff options
| author | gbc dev <gbc@localhost> | 2026-07-04 22:46:11 +0200 |
|---|---|---|
| committer | gbc dev <gbc@localhost> | 2026-07-04 22:46:11 +0200 |
| commit | 54081ce90bf2b4cad803ab78db65b91c1217cc83 (patch) | |
| tree | f5e51723c2e9a40265b5251b07870b48c81d048d | |
| parent | renderer: inter-frame diffing to minimize terminal output (diff) | |
| download | sl0pboy-54081ce90bf2b4cad803ab78db65b91c1217cc83.tar.gz sl0pboy-54081ce90bf2b4cad803ab78db65b91c1217cc83.tar.xz sl0pboy-54081ce90bf2b4cad803ab78db65b91c1217cc83.zip | |
input: external control channel via FIFO
Adds --fifo [path] opening a named pipe that accepts button commands from any
process. Supports momentary taps (a, start, name:N), deterministic hold/release
(+name / -name), and 'release'. Merges with keyboard input; held state carries
true key-up semantics the terminal can't provide. GBC_INPUT_DEBUG traces state.
| -rw-r--r-- | README.md | 30 | ||||
| -rw-r--r-- | src/main.c | 10 | ||||
| -rw-r--r-- | src/render.c | 135 | ||||
| -rw-r--r-- | src/render.h | 7 |
4 files changed, 159 insertions, 23 deletions
@@ -39,6 +39,36 @@ Requires a truecolor-capable terminal (`COLORTERM=truecolor`) at least Since terminals don't report key-release, buttons are held for a few frames after each keypress (relying on the terminal's key-repeat for sustained holds). +### External control channel (FIFO) + +Start with `--fifo [path]` (default `/tmp/gbc.fifo`) to open a named pipe that +accepts button commands from any process. Unlike the terminal keyboard, this +can send true key-**release** events, so it gives deterministic input for +scripting and automated testing. + +```sh +./build/gbc rom.gb --fifo /tmp/gbc.fifo & +echo 'a' > /tmp/gbc.fifo # momentary tap of A +echo 'start' > /tmp/gbc.fifo # tap Start +echo '+left' > /tmp/gbc.fifo # press and HOLD Left +echo 'a b' > /tmp/gbc.fifo # tap A and B together (Left still held) +echo '-left' > /tmp/gbc.fifo # release Left +echo 'release' > /tmp/gbc.fifo # release everything held +echo 'a:30' > /tmp/gbc.fifo # tap A, held for 30 frames +``` + +Commands (one line, space/comma separated tokens, case-insensitive): + +| Token | Effect | +|----------------|-----------------------------------------| +| `a b start select up down left right` | momentary tap | +| `name:N` | tap held for N frames | +| `+name` | press and hold until released | +| `-name` | release a held button | +| `release` | release all held buttons | + +Set `GBC_INPUT_DEBUG=1` to trace resulting joypad state to stderr. + ## Architecture | File | Responsibility | @@ -34,6 +34,7 @@ int main(int argc, char **argv) { double seconds = 30.0; int shot_frames = -1; const char *shot_path = "shot.ppm"; + const char *fifo_path = NULL; for (int i = 2; i < argc; i++) { if (!strcmp(argv[i], "--test")) { test = true; @@ -41,6 +42,9 @@ int main(int argc, char **argv) { } 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"; } } @@ -72,6 +76,11 @@ int main(int argc, char **argv) { 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); + } term_init(); const double frame_time = 1.0 / 59.73; @@ -93,6 +102,7 @@ int main(int argc, char **argv) { } term_restore(); + input_close_fifo(); cart_free(&gb->cart); free(gb); return 0; diff --git a/src/render.c b/src/render.c index 75ce020..f659da3 100644 --- a/src/render.c +++ b/src/render.c @@ -6,6 +6,9 @@ #include <termios.h> #include <fcntl.h> #include <signal.h> +#include <strings.h> +#include <sys/stat.h> +#include <errno.h> #define UPPER "\xe2\x96\x80" // ▀ U+2580 @@ -110,12 +113,92 @@ void render_frame(GB *gb) { } } -// ---- input with decay-based "hold" emulation ---- -static int hold[8]; // frames remaining for each BTN bit position +// ---- input: keyboard (decay-based holds) + optional FIFO control channel ---- +// Two independent sources are merged each frame: +// held : buttons explicitly held via the FIFO (+x / -x), no decay +// 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) #define HOLD_FRAMES 4 -static void press(int bit) { - for (int i = 0; i < 8; i++) if ((1 << i) == bit) hold[i] = HOLD_FRAMES; +static int mask_index(int mask) { + for (int i = 0; i < 8; i++) if ((1 << i) == mask) return i; + return -1; +} +static void tap(int mask, int frames) { + int i = mask_index(mask); + if (i >= 0) decay[i] = frames > 0 ? frames : HOLD_FRAMES; +} + +// ---- FIFO control channel ---- +static int fifo_fd = -1; +static bool fifo_created = false; +static char fifo_path[512]; +static char linebuf[256]; +static int linelen = 0; + +static int name_mask(const char *s) { + if (!strcasecmp(s, "a")) return BTN_A; + if (!strcasecmp(s, "b")) return BTN_B; + if (!strcasecmp(s, "start")) return BTN_START; + if (!strcasecmp(s, "select") || !strcasecmp(s, "sel")) return BTN_SELECT; + if (!strcasecmp(s, "up")) return BTN_UP; + if (!strcasecmp(s, "down")) return BTN_DOWN; + if (!strcasecmp(s, "left")) return BTN_LEFT; + if (!strcasecmp(s, "right")) return BTN_RIGHT; + return 0; +} + +// Parse one token: "+a" hold, "-a" release, "a" tap, "a:20" tap N frames, +// "release" clears all held. +static void handle_token(char *t) { + if (!*t) return; + if (!strcasecmp(t, "release") || !strcasecmp(t, "none")) { held = 0; return; } + if (t[0] == '+') { held |= name_mask(t + 1); return; } + if (t[0] == '-') { held &= ~name_mask(t + 1); return; } + int frames = HOLD_FRAMES; + char *colon = strchr(t, ':'); + if (colon) { *colon = 0; frames = atoi(colon + 1); } + int m = name_mask(t); + if (m) tap(m, frames); +} + +static void handle_line(char *line) { + for (char *t = strtok(line, " \t,"); t; t = strtok(NULL, " \t,")) + handle_token(t); +} + +void input_open_fifo(const char *path) { + snprintf(fifo_path, sizeof(fifo_path), "%s", path); + if (mkfifo(fifo_path, 0666) == 0) fifo_created = true; + else if (errno != EEXIST) { perror("mkfifo"); return; } + // O_RDWR keeps a writer end open so we never see persistent EOF and the + // open never blocks waiting for an external writer. + fifo_fd = open(fifo_path, O_RDWR | O_NONBLOCK); + if (fifo_fd < 0) perror("open fifo"); +} + +void input_close_fifo(void) { + if (fifo_fd >= 0) { close(fifo_fd); fifo_fd = -1; } + if (fifo_created) { unlink(fifo_path); fifo_created = false; } +} + +static void poll_fifo(void) { + if (fifo_fd < 0) return; + char buf[512]; + int n; + while ((n = read(fifo_fd, buf, sizeof(buf))) > 0) { + for (int i = 0; i < n; i++) { + char c = buf[i]; + if (c == '\n' || c == '\r') { + linebuf[linelen] = 0; + if (linelen) handle_line(linebuf); + linelen = 0; + } else if (linelen < (int)sizeof(linebuf) - 1) { + linebuf[linelen++] = c; + } + } + } } bool input_poll(GB *gb) { @@ -126,32 +209,40 @@ bool input_poll(GB *gb) { unsigned char ch = buf[i]; if (ch == 0x1b && i + 2 < n && buf[i+1] == '[') { switch (buf[i+2]) { - case 'A': press(BTN_UP); break; - case 'B': press(BTN_DOWN); break; - case 'C': press(BTN_RIGHT); break; - case 'D': press(BTN_LEFT); break; + case 'A': tap(BTN_UP, HOLD_FRAMES); break; + case 'B': tap(BTN_DOWN, HOLD_FRAMES); break; + case 'C': tap(BTN_RIGHT, HOLD_FRAMES); break; + case 'D': tap(BTN_LEFT, HOLD_FRAMES); break; } i += 2; continue; } switch (ch) { - case 'w': press(BTN_UP); break; - case 's': press(BTN_DOWN); break; - case 'a': press(BTN_LEFT); break; - case 'd': press(BTN_RIGHT); break; - case 'z': case 'j': press(BTN_A); break; - case 'x': case 'k': press(BTN_B); break; - case '\r': case '\n': press(BTN_START); break; - case ' ': press(BTN_SELECT); break; + case 'w': tap(BTN_UP, HOLD_FRAMES); break; + case 's': tap(BTN_DOWN, HOLD_FRAMES); break; + case 'a': tap(BTN_LEFT, HOLD_FRAMES); break; + case 'd': tap(BTN_RIGHT, HOLD_FRAMES); break; + case 'z': case 'j': tap(BTN_A, HOLD_FRAMES); break; + 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 'q': case 3: quit = true; break; } } - // build button state from decay counters - u8 b = 0; - for (int i = 0; i < 8; i++) { - if (hold[i] > 0) { hold[i]--; b |= (1 << i); } - } + + poll_fifo(); + + // merge held + decaying momentary presses into the joypad state + u8 b = held; + for (int i = 0; i < 8; i++) + if (decay[i] > 0) { decay[i]--; b |= (1 << i); } + if (b & ~gb->buttons) gb_request_interrupt(gb, INT_JOYPAD); + static bool dbg_init = false, dbg = false; + if (!dbg_init) { dbg = getenv("GBC_INPUT_DEBUG") != NULL; dbg_init = true; } + if (dbg && b != gb->buttons) + fprintf(stderr, "[input] %c%c%c%c %c%c%c%c\n", + b&BTN_UP?'U':'.', b&BTN_DOWN?'D':'.', b&BTN_LEFT?'L':'.', b&BTN_RIGHT?'R':'.', + b&BTN_A?'A':'.', b&BTN_B?'B':'.', b&BTN_SELECT?'s':'.', b&BTN_START?'S':'.'); gb->buttons = b; - if (b) gb_request_interrupt(gb, INT_JOYPAD); return !quit; } diff --git a/src/render.h b/src/render.h index 93ca32a..27da080 100644 --- a/src/render.h +++ b/src/render.h @@ -7,7 +7,12 @@ void term_init(void); void term_restore(void); void render_frame(GB *gb); void render_force_full(void); // force a complete repaint next frame -// Poll keyboard, update gb->buttons. Returns false if quit requested. +// Poll keyboard + control FIFO, update gb->buttons. False if quit requested. bool input_poll(GB *gb); +// Optional external control channel: a named pipe (FIFO) that accepts button +// commands (see README). Safe to call once at startup. +void input_open_fifo(const char *path); +void input_close_fifo(void); + #endif |
