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.
Two ways games reach the link
Most games use only the BIOS COM services (Option A below). A few — SNK vs
Capcom, KOF R-1, KOF R-2 — call COMINIT to set up the UART, then install
their own serial interrupt handlers and drive SC0BUF directly. To be
truly game-agnostic you must handle both; Option B covers the second case.
See game compatibility for which is which, and
Netplay transport for the reference design.
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 + serial interrupts (needed for own-ISR games)¶
Some games don't use the BIOS COM services for the data path. They call COMINIT
(to configure the UART), then install their own serial RX/TX interrupt
handlers and move bytes through the raw SIO channel-0 registers. If you only HLE
Option A, these games set up the link, see no bytes, and never connect. Handle
them with these pieces (all validated against SNK vs Capcom; KOF R-1/R-2 use
byte-identical code):
SC0BUF(I/O0x50),SC0CR(0x51). A byte from the peer goes intoSC0BUF; a write toSC0BUFtransmits.SC0CR's error bits must read clean.- Serial interrupts. When a received byte is ready, raise
INTRX0; the CPU vectors (via the BIOS interrupt table) to the RAM slot0x6FE4, the game's RX ISR, which readsSC0BUF. After a transmit, raiseINTTX0(RAM slot0x6FE8) to pace the game's TX ISR. Enable is gated byINTES0(I/O0x77). - The BIOS COM RX ring. The game's RX ISR stores received bytes into the
standard BIOS ring —
0x6cc0data (64 B),0x6d01pending count,0x6d03read pointer — and then reads them back throughCOMGETDATA. So yourCOMGETDATAmust read from that ring (draining0x6d01), not from a separate queue, whenever the game has installed its own ISR (RAM vector0x6FE4≠ the default stub). Otherwise the ring fills but the game's protocol sees nothing. - Flow control is real here. Model
/RTS(I/O0xB2, software GPO) ↔ peer/CTS: a byte only transmits while the peer's/RTSis asserted, and signal your/RTSedges to the peer. This gives half-duplex turn-taking and genuineCOMSENDSTATUSback-pressure.
Line params: 19200 8N1, INTTX0 on transmit-buffer-empty. See the reference
implementation emulator/race-wasm/link_transport.c + the SIO device in web.c,
Netplay transport, and the register details in the
research notebook.
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 for BIOS-COM games — their queues and timeouts cope. But the own-ISR games (Option B) depend on it: they pace onCOMSENDSTATUSand gate transmission on the peer's/RTS, so model the line for those.
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. The faithful transport islink_transport.c(UART +/RTS-/CTSFIFO model, with amake test-transportgcc unit test) plus the SIO device +COMGETDATAring bridge inweb.c.
Both are MIT-glue over the GPL-2.0 core; reuse freely.