aboutsummaryrefslogtreecommitdiffstats
path: root/src/render.c
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 /src/render.c
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.
Diffstat (limited to 'src/render.c')
-rw-r--r--src/render.c135
1 files changed, 113 insertions, 22 deletions
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;
}