From 54081ce90bf2b4cad803ab78db65b91c1217cc83 Mon Sep 17 00:00:00 2001 From: gbc dev Date: Sat, 4 Jul 2026 22:46:11 +0200 Subject: 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. --- src/main.c | 10 +++++ src/render.c | 135 +++++++++++++++++++++++++++++++++++++++++++++++++---------- src/render.h | 7 +++- 3 files changed, 129 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/main.c b/src/main.c index 273d6b7..173ab5b 100644 --- a/src/main.c +++ b/src/main.c @@ -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 #include #include +#include +#include +#include #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 -- cgit v1.3.1-sl0p