Implement NGP link in your emulator¶
This page is for emulator authors. It shows how to add real, game-agnostic online multiplayer to any Neo Geo Pocket / Color emulator by bridging the link cable — so, in time, every NGP emulator can offer netplay.
The one idea to take away
A game talks to the cable only through a small set of BIOS communication services (or, at the register level, the on-chip UART). If you carry those raw bytes to a peer faithfully, the game's own protocol does everything else — handshake, roles, input exchange, teardown. You do not implement the link protocol; the ROM already contains it. That is why one bridge works for every link-capable cartridge, NGP (mono) and NGPC (color) alike.
Pick an integration point¶
A — HLE the BIOS COM services (easiest, recommended)¶
Most NGP emulators (NeoPop, RACE, Mednafen…) high-level-emulate the BIOS. The link is a group of BIOS services the game calls; implement them as a byte transport.
The full ABI is in BIOS COM ABI. The minimum you must implement:
| Service | ID | Your implementation |
|---|---|---|
COMINIT |
10 |
reset your TX/RX buffers; return success (RA3=0) |
COMSENDSTART |
11 |
flush queued TX bytes to the peer |
COMCREATEDATA |
13 |
queue one TX byte (from RB3) |
COMGETDATA |
14 |
pop one RX byte into RB3; RA3=0 if a byte was ready, 1 if empty |
COMSENDSTATUS |
17 |
pending TX count in RWA3 (return 0 if you send instantly) |
COMRECIVESTATUS |
18 |
available RX byte count in RWA3 |
COMCREATEBUFDATA |
19 |
send RB3 bytes starting at XHL3 |
COMGETBUFDATA |
1a |
receive up to RB3 bytes into XHL3; return the count |
COMONRTS / COMOFFRTS |
15 / 16 |
flow control — safe to no-op for a faithful byte bridge |
That's the entire bridge. In pseudocode:
// TX side: game builds a frame with COMCREATEDATA*, then COMSENDSTART
void COMCREATEDATA(u8 b) { tx_buf.push(b); }
void COMSENDSTART() { link_send(tx_buf); tx_buf.clear(); } // -> to peer
// RX side: bytes arrive from the peer into rx_buf
int COMGETDATA(u8* out) { if (rx_buf.empty()) return 1; *out = rx_buf.pop(); return 0; }
int COMRECIVESTATUS() { return rx_buf.size(); }
// the network callback
void on_peer_bytes(const u8* p, int n) { rx_buf.push(p, n); }
Wire those into wherever your emulator dispatches BIOS calls (in RACE it is
doBios() in tlcs900h.c; the reference bridge is com_* in emulator/race-wasm/web.c).
B — Emulate the SIO0 UART and carry the serial (accurate)¶
If your emulator runs the real BIOS and models the TMP95C061, the link is the
on-chip SIO channel 0: registers SC0BUF/SC0CR/SC0MOD/BR0CR at
0x50–0x53, 19200 8N1, INTTX0 on transmit-buffer-empty, 16× receive
oversampling, hardware CTS, and RTS as a software GPO on I/O 0xB2. Serialize
each transmitted byte to the peer and feed received bytes back into SC0BUF with the
right interrupts. More work, but exact, and it also covers the rare game that pokes
SIO directly. See BIOS COM ABI and the
research notebook for the register details.
The transport (netplay)¶
- Run two independent emulator instances — one per player, each a full console. The connection between them is the cable. (Do not try to share one instance; the two consoles have separate state and screens.)
- The transport must be reliable, ordered, and bidirectional — a real cable never drops or reorders bytes. TCP or a WebRTC reliable-ordered DataChannel both fit.
- Relay every byte faithfully. Do not parse, frame, or transform it. Your TX bridge emits bytes → send to peer; peer bytes → your RX bridge.
Timing, latency, roles — things you get for free¶
- No rollback, no determinism engineering. The two machines only ever communicate through the cable, so faithful relay cannot desync. The game's lockstep does the sync.
- Latency degrades gracefully. The exchange is lockstep (~30 Hz); under network latency the game runs in slight slow-motion and stays perfectly in sync, disconnecting only past a very generous watchdog (~500 ms+ RTT). See Netplay latency.
- Roles resolve themselves. Both sides advertise
FC; the first accepted becomes Player 1, the peer answersFDand becomes Player 2. You implement none of this — just don't drop or duplicate bytes. If both connect at the same instant, stagger slightly. - Flow control (
CTS/RTS) can be ignored by a byte-faithful HLE bridge — the game's queues and timeouts cope. Honor it only if you emulate the UART at the register level.
Verify your implementation¶
The wire is deterministic, so you can check it directly. Link two of your instances, enter a game's link/VS mode, and you should see:
Cross-check byte values against Identity handshake and
Gameplay exchange. If your two instances complete the FC/FD
handshake and then exchange F8 input frames, you have working NGP netplay — for
every link game at once.
Reference implementation¶
emulator/race-link/— a headless byte-bridge + a validator that runs the real ROM's link code and asserts the protocol (all layers PASS).emulator/race-wasm/— the full core in WebAssembly with the bridge carried over WebRTC, plus a Cloudflare Workers signaling/lobby server.
Both are MIT-glue over the GPL-2.0 core; reuse freely.