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.
Two ways games drive the link¶
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.
COMSENDSTATUSreports real TX occupancy (back-pressure);COMRECIVESTATUSreports RX occupancy. /RTS-/CTSflow control. A byte only leaves our TX FIFO while the peer's/RTSis asserted (our/CTS).COMONRTS/COMOFFRTS(and direct writes to I/O0xB2) 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) and0xB2(/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 inSC0BUFand raisesINTRX0; the CPU vectors (through the BIOS interrupt table already synthesised by the core) to the RAM slot0x6FE4— the game's own RX ISR — which readsSC0BUFinto its ring. Writes toSC0BUFtransmit and raiseINTTX0(RAM slot0x6FE8) to pace the game's TX ISR.COMGETDATAbridged to the ring. An own-ISR game reads received bytes back throughCOMGETDATA, which the emulator HLEs. SoCOMGETDATAreads from the same BIOS COM RX ring the game's ISR fills, draining0x6d01. 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: