aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgbc dev <gbc@localhost>2026-07-04 22:46:11 +0200
committergbc dev <gbc@localhost>2026-07-04 22:46:11 +0200
commit54081ce90bf2b4cad803ab78db65b91c1217cc83 (patch)
treef5e51723c2e9a40265b5251b07870b48c81d048d
parentrenderer: inter-frame diffing to minimize terminal output (diff)
downloadsl0pboy-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.md30
-rw-r--r--src/main.c10
-rw-r--r--src/render.c135
-rw-r--r--src/render.h7
4 files changed, 159 insertions, 23 deletions
diff --git a/README.md b/README.md
index 947211d..620f4c9 100644
--- a/README.md
+++ b/README.md
@@ -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 |
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 <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