feat: add BroadcastChannel JS bridge for browser-based link cable multiplayer#1
feat: add BroadcastChannel JS bridge for browser-based link cable multiplayer#1
Conversation
…tiplayer Add a lightweight JavaScript bridge that enables GBA link cable multiplayer between browser tabs using the BroadcastChannel API - no server required for same-origin local multiplayer. libretro/libretro.c: - Add gpsp JS bridge exports (gpsp_bridge_set_client_id/count) - Add js_gpsp_bridge_has/send/poll EM_JS functions for BroadcastChannel - Add gpsp_bridge_poll_receive() to dispatch packets from JS bridge - Guard netpacket_start() to not clobber bridge-configured values - Add netpacket_poll_receive() for SERIAL_MODE_SERIAL_POKE in retro_run() serial_proto.c: - Add netpacket_poll_receive() per-tick in serialpoke_update() - Fix CONNECTED to HANDSHAKE re-transition: reset offset/checksum, notify peer, fire IRQ on slave Tested: Pokemon Fire Red trading works end-to-end via BroadcastChannel
There was a problem hiding this comment.
Pull request overview
This pull request adds BroadcastChannel JavaScript bridge support for browser-based GBA link cable multiplayer and includes important bug fixes for the Pokemon serial protocol.
Changes:
- Adds Emscripten JS bridge (
globalThis.__gpspLinkBridge) for cross-tab multiplayer using BroadcastChannel API - Fixes Pokemon link cable handshake re-transition bug (CONNECTED → HANDSHAKE) by resetting state variables and notifying peers
- Fixes missing
netpacket_poll_receive()call for SERIAL_MODE_SERIAL_POKE that was preventing Pokemon packets from being delivered - Adds always-on AW_TRACE diagnostic logging for AW protocol debugging
- Adds heartbeat command handling for AW protocol to improve state synchronization
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| libretro/libretro.c | Adds Emscripten JS bridge functions (send/poll/has), KEEPALIVE exports for client ID/count configuration, bridge polling in netpacket_poll_receive(), guard logic to preserve bridge-configured values, and missing netpacket_poll_receive() calls in retro_run() |
| serial_proto.c | Adds AW_TRACE macro for diagnostic logging, heartbeat_cmd field for AW protocol state sync, fixes handshake re-transition in both master/slave paths, adds netpacket_poll_receive() calls for low-latency packet delivery, clears SIOCNT busy bit, and adds diagnostic counters |
| bios_data.S | Adds ELF .type and .size directives for BIOS symbols |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const u16 ste = (flags >> 8) & 0xff; // Peer state. | ||
| const u16 cnt = flags & 0x00ff; // Number of words to follow. | ||
|
|
||
| static int aw_recv_log_count = 0; | ||
| if (aw_recv_log_count++ < 30) | ||
| AW_TRACE("[AW-RECV] from=%d st=%d cmd=%04x cnt=%d myid=%d\n", | ||
| client_id, ste, cmd, cnt, netplay_client_id); | ||
|
|
||
| serstate.aw.peer[client_id].timeout = 0; | ||
| serstate.aw.recv_count++; | ||
|
|
||
| /* When the peer changes state, discard any leftover heartbeat from | ||
| the previous state — it would confuse the local FSM. For example | ||
| a game command cached while in PACKETXG must not leak through | ||
| after the peer moves to SYNC. */ | ||
| if (ste != serstate.aw.peer[client_id].state) | ||
| serstate.aw.peer[client_id].heartbeat_cmd = CMD_NONE; | ||
|
|
||
| serstate.aw.peer[client_id].state = ste; | ||
|
|
||
| /* For heartbeat packets (cnt=0) store the command so the other side | ||
| can see it in SIOMULTI on the next transfer / fake-IRQ. Each value | ||
| is consumed once by process_awpeer(). | ||
| cmd==0 is a dummy placeholder used in state-notification sends | ||
| (e.g. serialaw_senddata(0, STATE_SYNC, NULL, 0)) — it must NOT | ||
| be forwarded or the game would see 0x0000 in SIOMULTI, which it | ||
| interprets as corrupt data → linking error. */ | ||
| if (cnt == 0 && cmd != 0) { | ||
| serstate.aw.peer[client_id].heartbeat_cmd = cmd; | ||
| } | ||
|
|
||
| SRPT_DEBUG_LOG("Got packet with state %d cmd %04x and size %d.\n", ste, cmd, cnt); | ||
|
|
||
| if (ste == STATE_INTERSYNC) { |
There was a problem hiding this comment.
Missing bounds check on client_id parameter. The client_id is used directly as an array index (e.g., lines 800, 807, 808, 810, 820) without validation, but the peer array only has 4 elements (indices 0-3). When using the JavaScript bridge, a malicious or buggy bridge implementation could provide a client_id >= 4, causing out-of-bounds memory access. Add a bounds check at the start of the function after validating the header: if (client_id >= 4) return;
| case SERIAL_MODE_SERIAL_AW1: | ||
| case SERIAL_MODE_SERIAL_AW2: | ||
| netpacket_poll_receive(); | ||
| break; |
There was a problem hiding this comment.
Inconsistent indentation in switch case statement. The cases for SERIAL_MODE_SERIAL_AW1 and SERIAL_MODE_SERIAL_AW2 use one space for indentation while other cases use two spaces. The body also uses three spaces instead of the expected indentation pattern. This should match the indentation pattern used for SERIAL_MODE_SERIAL_POKE case above.
| case SERIAL_MODE_SERIAL_AW1: | |
| case SERIAL_MODE_SERIAL_AW2: | |
| netpacket_poll_receive(); | |
| break; | |
| case SERIAL_MODE_SERIAL_AW1: | |
| case SERIAL_MODE_SERIAL_AW2: | |
| netpacket_poll_receive(); | |
| break; |
| EM_JS(void, js_gpsp_bridge_send, (int dst, const void *data_ptr, int len), { | ||
| const bridge = globalThis.__gpspLinkBridge; | ||
| if (!bridge || typeof bridge.send !== 'function') return; | ||
| if (!len || len <= 0) return; |
There was a problem hiding this comment.
Redundant condition check. The condition "!len || len <= 0" is redundant because "len <= 0" already covers the case when len is 0. The "!len" check is unnecessary and should be removed. The condition should be simplified to just "if (len <= 0) return;"
| if (!len || len <= 0) return; | |
| if (len <= 0) return; |
| /* Always-on lightweight state-transition trace for AW protocol debugging. | ||
| Uses printf which maps to console.log in Emscripten. */ | ||
| #define AW_TRACE(...) printf(__VA_ARGS__) |
There was a problem hiding this comment.
AW_TRACE macro is always enabled and will print diagnostic messages to console in production builds. While the comment indicates this is intentional for "AW protocol debugging", several trace calls are unlimited (lines 557, 589, 733, 735-736, 852) and could print frequently during gameplay, potentially impacting performance and cluttering console output. Consider either: (1) guarding AW_TRACE with a compile-time flag similar to SRPT_DEBUG_LOG, or (2) adding rate limiting to all trace calls, or (3) documenting that these will be removed/disabled before merging to main.
BroadcastChannel JS Bridge for Browser-Based Link Cable Multiplayer
Add a lightweight JavaScript bridge that enables GBA link cable multiplayer between browser tabs using the BroadcastChannel API — no server required for same-origin local multiplayer.
What this does
Adds an Emscripten JS bridge (
globalThis.__gpspLinkBridge) that lets a web frontend configure and drive gpSP's netplay serial protocols viasend(dst, payload)/poll()calls, using BroadcastChannel for cross-tab communication.Changes
libretro/libretro.cgpsp_bridge_set_client_id()/gpsp_bridge_set_client_count()EMSCRIPTEN_KEEPALIVE exports for configuringnetplay_client_idandnetplay_num_clientsfrom JavaScriptjs_gpsp_bridge_has/send/pollEM_JS functions for BroadcastChannel communication viaglobalThis.__gpspLinkBridgegpsp_bridge_poll_receive()that polls the JS bridge and dispatches packets to serial protocol handlersnetpacket_start()to not clobber bridge-configured values when the JS bridge is already activenetpacket_poll_receive()call forSERIAL_MODE_SERIAL_POKEinretro_run()— was completely missing, causing Pokemon link cable packets to never be deliveredserial_proto.cnetpacket_poll_receive()per-tick inserialpoke_update()for low-latency packet delivery (matching the pattern already used by AW mode)offset/checksum, notify peer via network packet, fire IRQ on slave side (fixes "Communication error" when exiting Pokemon trade room)Testing
mul_poke(Pokemon link cable protocol)