diff options
| author | gbc dev <gbc@localhost> | 2026-09-22 19:49:47 +0200 |
|---|---|---|
| committer | gbc dev <gbc@localhost> | 2026-09-22 19:49:47 +0200 |
| commit | 1537eff38cd86e1d0b1980bf8f4412dfa2a11056 (patch) | |
| tree | 6783e69a449db2bfa1488e4517228077283036fb | |
| parent | web: boot a demo ROM on arrival, and a deploy script for gbc.sl0p.foo (diff) | |
| download | sl0pboy-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.c | 56 | ||||
| -rw-r--r-- | src/gb.h | 8 | ||||
| -rw-r--r-- | src/wasm.c | 28 |
3 files changed, 88 insertions, 4 deletions
@@ -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; } @@ -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 @@ -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(); } |
