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.

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

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/O 0x50), SC0CR (0x51). A byte from the peer goes into SC0BUF; a write to SC0BUF transmits. 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 slot 0x6FE4, the game's RX ISR, which reads SC0BUF. After a transmit, raise INTTX0 (RAM slot 0x6FE8) to pace the game's TX ISR. Enable is gated by INTES0 (I/O 0x77).
  • The BIOS COM RX ring. The game's RX ISR stores received bytes into the standard BIOS ring — 0x6cc0 data (64 B), 0x6d01 pending count, 0x6d03 read pointer — and then reads them back through COMGETDATA. So your COMGETDATA must read from that ring (draining 0x6d01), not from a separate queue, whenever the game has installed its own ISR (RAM vector 0x6FE4 ≠ the default stub). Otherwise the ring fills but the game's protocol sees nothing.
  • Flow control is real here. Model /RTS (I/O 0xB2, software GPO) ↔ peer /CTS: a byte only transmits while the peer's /RTS is asserted, and signal your /RTS edges to the peer. This gives half-duplex turn-taking and genuine COMSENDSTATUS back-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 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 for BIOS-COM games — their queues and timeouts cope. But the own-ISR games (Option B) depend on it: they pace on COMSENDSTATUS and 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:

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. The faithful transport is link_transport.c (UART + /RTS-/CTS FIFO model, with a make test-transport gcc unit test) plus the SIO device + COMGETDATA ring bridge in web.c.

Both are MIT-glue over the GPL-2.0 core; reuse freely.