aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgbc dev <gbc@localhost>2026-09-22 19:49:47 +0200
committergbc dev <gbc@localhost>2026-09-22 19:49:47 +0200
commit1537eff38cd86e1d0b1980bf8f4412dfa2a11056 (patch)
tree6783e69a449db2bfa1488e4517228077283036fb
parentweb: boot a demo ROM on arrival, and a deploy script for gbc.sl0p.foo (diff)
downloadsl0pboy-1537eff38cd86e1d0b1980bf8f4412dfa2a11056.tar.gz
sl0pboy-1537eff38cd86e1d0b1980bf8f4412dfa2a11056.tar.xz
sl0pboy-1537eff38cd86e1d0b1980bf8f4412dfa2a11056.zip
wasm: expose the link port as host-drained byte queues
A browser has no file descriptors, so the wasm build's link port was inert. Give it a pair of ring buffers instead: the guest's transmitted bytes pile up in one, the host pushes received bytes into the other, and the external-clock RX path pops from it (staying pending when empty, exactly like a quiet link). gbw_serial_drain / gbw_serial_push hand them to JS through a fixed staging buffer, so nothing allocates per frame. That is enough to be a real link. SLIP-framed bytes are IP packets; unframed bytes are console I/O - the same split tools/gbhub makes natively, which is what lets a page type into the link port and reach the shell's stdin. One machine per wasm instance, so the queues are plain statics: several Game Boys on a page are several instantiations of the one compiled module, each with its own linear memory. The native link is untouched: the fd paths sit in the #else and, preprocessed for a native target, gb.c comes out byte-identical to the previous commit (verified with gcc -E). blargg cpu_instrs still passes and the golden-frame tests are still pixel-exact.
-rw-r--r--src/gb.c56
-rw-r--r--src/gb.h8
-rw-r--r--src/wasm.c28
3 files changed, 88 insertions, 4 deletions
diff --git a/src/gb.c b/src/gb.c
index 89a792b..4b6f2ab 100644
--- a/src/gb.c
+++ b/src/gb.c
@@ -25,11 +25,46 @@
// instead: the link is simply always down (serial_*_fd stay -1, so the serial
// paths below never touch a descriptor) and the call sites stay identical.
#ifdef __wasm__
-static bool serial_check(GB *gb) {
- (void)gb;
- return false;
+// ---------- link port <-> host queues (wasm) ----------
+// A browser has no file descriptors, so the link port becomes a pair of byte
+// queues the host drains and fills. That is enough to be a real link: SLIP-
+// framed bytes are network traffic, unframed bytes are console I/O - exactly
+// the split tools/gbhub makes on the native side.
+//
+// One machine per wasm instance, so plain statics are per-Game-Boy: each one
+// on the page is its own module instantiation with its own linear memory.
+#define SER_Q 8192 /* power of two: mask instead of modulo */
+static u8 ser_tx[SER_Q];
+static u32 ser_tx_w, ser_tx_r;
+static u8 ser_rx[SER_Q];
+static u32 ser_rx_w, ser_rx_r;
+static u32 ser_rx_dropped; /* host pushed faster than the guest consumed */
+
+static void ser_tx_push(u8 b) {
+ if (ser_tx_w - ser_tx_r >= SER_Q) return; /* host isn't draining; drop */
+ ser_tx[ser_tx_w++ & (SER_Q - 1)] = b;
+}
+static bool ser_rx_pop(u8 *out) {
+ if (ser_rx_r == ser_rx_w) return false;
+ *out = ser_rx[ser_rx_r++ & (SER_Q - 1)];
+ return true;
+}
+
+void gb_serial_rx_push(const u8 *src, int n) {
+ for (int i = 0; i < n; i++) {
+ if (ser_rx_w - ser_rx_r >= SER_Q) {
+ ser_rx_dropped++;
+ return;
+ }
+ ser_rx[ser_rx_w++ & (SER_Q - 1)] = src[i];
+ }
+}
+int gb_serial_tx_drain(u8 *dst, int max) {
+ int n = 0;
+ while (n < max && ser_tx_r != ser_tx_w) dst[n++] = ser_tx[ser_tx_r++ & (SER_Q - 1)];
+ return n;
}
-static void serial_mark_dead(GB *gb) { (void)gb; }
+int gb_serial_rx_dropped(void) { return (int)ser_rx_dropped; }
#else
static double mono_sec(void) {
@@ -196,6 +231,9 @@ void gb_write(GB *gb, u16 addr, u8 val) {
// internal clock (GB is master): transmit gb->sb. Completes now.
u8 outb = gb->sb;
if (gb->serial_log) { fputc(outb, stderr); fflush(stderr); }
+#ifdef __wasm__
+ ser_tx_push(outb);
+#else
if (gb->serial_sock_path && gb->serial_sock_dead) serial_check(gb);
if (gb->serial_out_fd >= 0) {
unsigned char c = outb;
@@ -204,6 +242,7 @@ void gb_write(GB *gb, u16 addr, u8 val) {
errno == EBADF || errno == ENOTCONN))
serial_mark_dead(gb);
}
+#endif
gb->sb = 0xFF; // nothing shifted in on a pure TX
gb->sc &= ~0x80;
gb_request_interrupt(gb, INT_SERIAL);
@@ -212,6 +251,14 @@ void gb_write(GB *gb, u16 addr, u8 val) {
// Complete only when an input byte is available - and do NOT echo
// it to stdout - so a program can poll for input without emitting
// junk. Stays pending (bit7 set) while stdin has nothing.
+#ifdef __wasm__
+ u8 rxb;
+ if (ser_rx_pop(&rxb)) {
+ gb->sb = rxb;
+ gb->sc &= ~0x80;
+ gb_request_interrupt(gb, INT_SERIAL);
+ } // else: stay pending, exactly like a quiet link
+#else
if (gb->serial_sock_path && gb->serial_sock_dead) serial_check(gb);
if (gb->serial_in_fd >= 0) {
unsigned char c;
@@ -234,6 +281,7 @@ void gb_write(GB *gb, u16 addr, u8 val) {
}
// other n < 0 (EAGAIN on a live link): stay pending, poll again
}
+#endif
}
return;
}
diff --git a/src/gb.h b/src/gb.h
index a03204f..e95048b 100644
--- a/src/gb.h
+++ b/src/gb.h
@@ -152,4 +152,12 @@ void gb_tick(GB *gb, int tcycles); // advance ppu/timer/dma by t-cycles
void gb_request_interrupt(GB *gb, u8 flag);
+#ifdef __wasm__
+// Link port as host-drained byte queues (no file descriptors in a browser).
+// SLIP-framed bytes are network traffic, unframed bytes are console I/O.
+void gb_serial_rx_push(const u8 *src, int n); // host -> guest
+int gb_serial_tx_drain(u8 *dst, int max); // guest -> host, returns count
+int gb_serial_rx_dropped(void); // overflow counter, for debugging
+#endif
+
#endif
diff --git a/src/wasm.c b/src/wasm.c
index e1051a1..cf7e74e 100644
--- a/src/wasm.c
+++ b/src/wasm.c
@@ -124,3 +124,31 @@ int EXPORT(gbw_sram_load)(const u8 *data, int len) {
// Total T-cycles since power-on; handy for pacing checks and debugging.
double EXPORT(gbw_cycles)(void) { return (double)gb.cycles; }
+
+// ---- link cable -----------------------------------------------------------
+// The page plays the part tools/gbhub plays natively: drain what this Game Boy
+// transmitted, decide where it goes, push bytes back in. SLIP-framed bytes are
+// IP packets; unframed bytes are console I/O (which is why typing into the
+// link port lands on the shell's stdin).
+//
+// A fixed staging buffer avoids allocating per frame; the page reads it
+// through a typed array right after the call.
+static u8 serial_buf[2048];
+
+// Bytes this Game Boy put on the wire since the last drain. Returns the count;
+// the data sits at gbw_serial_buf().
+int EXPORT(gbw_serial_drain)(void) {
+ return gb_serial_tx_drain(serial_buf, (int)sizeof serial_buf);
+}
+u8 *EXPORT(gbw_serial_buf)(void) { return serial_buf; }
+
+// Hand bytes to this Game Boy's link port (write them into gbw_serial_buf()
+// first, then call this with the length).
+void EXPORT(gbw_serial_push)(int n) {
+ if (n > 0)
+ gb_serial_rx_push(serial_buf, n > (int)sizeof serial_buf ? (int)sizeof serial_buf : n);
+}
+
+// RX bytes dropped because the guest wasn't consuming - a wedged link shows up
+// here as a climbing number instead of silently losing packets.
+int EXPORT(gbw_serial_dropped)(void) { return gb_serial_rx_dropped(); }