Skip to content

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

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 0x500x53, 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 answers FD and 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:

FC 01 00 30   ⇄   FD 01 00 30        identity handshake
F8 00 <input>  /  F8 <p1> <p2>        per-frame input exchange

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.