Skip to content

Netplay: the faithful link transport

The netplay bridge carries the EXT link over the network. To be game-agnostic it emulates the hardware transport the link runs on — two TMP95C061 SIO channel-0 endpoints joined by a null-modem crossover cable with hardware /CTS + software /RTS flow control — rather than any single game's protocol. Every link-capable cartridge then works with zero per-game code.

Getting there took modelling the link at two levels, because NGPC games reach the link two different ways.

The project originally assumed "games never touch the SIO registers — all link I/O goes through the BIOS COM services." That is true for many games but not all. A static sweep of the library (tools/link_isr_scan.py) finds two mechanisms:

Class How it moves bytes Examples
BIOS-COM Only the COM services (COMGETDATA/COMSENDSTART/…). Samurai Shodown 2, and the large majority of link games.
Own-ISR Calls COMINIT to set up SIO0, then installs its own serial RX/TX interrupt handlers that read/write SC0BUF directly into the BIOS COM RX ring, and reads it back via COMGETDATA. SNK vs Capcom (MOTM), KOF R-1, KOF R-2.

Only three games run their own serial ISR, and all three use byte-identical SNK link code with the same RX ring (0x6cc0 data, 0x6d01 count, 0x6d03 read-pointer). See game compatibility.

The model

The bridge is one endpoint of the wire (link_transport.c), with the peer reached over a WebRTC DataChannel (or, for the in-page test harness, the other core in the same page):

  • TX / RX FIFOs. COMSENDSTATUS reports real TX occupancy (back-pressure); COMRECIVESTATUS reports RX occupancy.
  • /RTS-/CTS flow control. A byte only leaves our TX FIFO while the peer's /RTS is asserted (our /CTS). COMONRTS/COMOFFRTS (and direct writes to I/O 0xB2) change our /RTS, and the edge is signalled to the peer over the DataChannel, so the peer's transmitter gates in turn. This is what enforces half-duplex turn-taking, and what makes back-pressure real.
  • Register views. 0xB1 (status) and 0xB2 (/RTS-/CTS) reads return values consistent with the modelled line, for games that poll them directly.

On top of that sits the SIO device, for the own-ISR games:

  • SC0BUF / SC0CR. A byte arriving from the peer is presented in SC0BUF and raises INTRX0; the CPU vectors (through the BIOS interrupt table already synthesised by the core) to the RAM slot 0x6FE4 — the game's own RX ISR — which reads SC0BUF into its ring. Writes to SC0BUF transmit and raise INTTX0 (RAM slot 0x6FE8) to pace the game's TX ISR.
  • COMGETDATA bridged to the ring. An own-ISR game reads received bytes back through COMGETDATA, which the emulator HLEs. So COMGETDATA reads from the same BIOS COM RX ring the game's ISR fills, draining 0x6d01. Everything is gated on the game having installed a custom ISR, so BIOS-COM-only games read the transport queue directly and are untouched.

End-to-end for an own-ISR game: peer → transport → SC0BUF → INTRX0 → game RX ISR → RX ring → COMGETDATA → game.

Wire format

The DataChannel is reliable + ordered and multiplexes two message kinds behind a 1-byte tag: 0x00 = payload bytes, 0x01 = an /RTS edge. The /RTS messages are essential — without them the peer's transmitter never drains.

Status

Game Path Status
Samurai Shodown 2 BIOS-COM ✅ links (in-page + online)
SNK vs Capcom (MOTM) Own-ISR ✅ links (in-page)
KOF R-1 / R-2 Own-ISR 🟢 identical link code, boots — expected
~24 other link games BIOS-COM 🟢 expected via the transport

Building

The link-enabled cores are separate from the solo build:

cd emulator/race-wasm
make net    # web/ngpc-net.{js,wasm}   — online client (index.html, play.html)
make link   # web/ngpc-link.{js,wasm}  — in-page two-core harness (link.html)

Both compile with -DLINK_COM and export the link seam (_link_rx, _link_peer_rts). The native flow-control model has a headless test:

make test-transport   # pure gcc, no emscripten