diff --git a/CLAUDE.md b/CLAUDE.md index 1e7d388..654c794 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1334,6 +1334,25 @@ parallel master is the default — see "W5100 vs W5500".) --- +## ⚠️ Open compatibility gaps — see `docs/bba_compat_todo.md` + +Known differences between this implementation and what real GC software +expects, with the evidence for each, are tracked in +**`docs/bba_compat_todo.md`**. Summary of what is still OPEN: + +1. **No hardware RX filtering** — we take every frame on the segment; the real + BBA delivers own-MAC + broadcast. Causes the ring pressure already seen on + hardware. Needs a decision, not just code. +2. **Ring geometry hardcoded** — `RHBP`/`BP`/`TLBP` writes ignored; a game + using a smaller RX ring corrupts silently. +3. **`NCRB` (0x01) not decoded** — filtering and interrupt-coalescing bits. +4. **Registers reading `0x00` that have non-zero hardware defaults** — + `NWAYC` `0x84`, `MISC` `0x3C`, and the rest of the map. + +Keep that file updated in the same commit whenever one is closed. + +--- + ## ⚠️ Which driver actually talks to us — and the TWO interrupt levels **Swiss has several ethernet back-ends, and `cube/patches/bba/` holds one file diff --git a/docs/bba_compat_todo.md b/docs/bba_compat_todo.md new file mode 100644 index 0000000..a0a4636 --- /dev/null +++ b/docs/bba_compat_todo.md @@ -0,0 +1,142 @@ +# BBA compatibility TODO + +Gaps between this implementation and what real GC software expects, found by +reading the three drivers that actually drive a BBA: + +- **Swiss (in-game patches)** — `cube/patches/bba/bba.c` in + [emukidid/swiss-gc](https://github.com/emukidid/swiss-gc). Note that + directory holds one file per adapter (`bba.c` is the real DOL-015; + `enc28j60.c`, `w5500.c`, `w6100.c`, `w6300.c` are other hardware) — check + which file you are in. +- **libogc2** — `lwip/arch/gc/netif/gcif.c` in + [extremscorner/libogc2](https://github.com/extremscorner/libogc2). This is + what Swiss's own menu networking uses, via `if_configex()`. +- **stock devkitPro libogc** — same shapes as libogc2; read out of + `/opt/devkitpro/libogc/lib/cube/libbba.a` (`gcif.o`) with `objdump`. + +⚠️ **Games use the official Nintendo SDK driver, which none of us can read.** +Everything below is derived from the three open drivers plus the register map +they document. A game may exercise a mechanism no open driver touches, so +"no open driver does this" is weaker evidence than "the hardware has no such +register". + +The register map referenced throughout is the one in libogc2's `gcif.c` +(`BBA_*` defines), which is the official one. + +--- + +## 1. No hardware RX filtering — **the consequential one** + +**Status: OPEN. Needs a decision, not just code.** + +The real BBA filters received frames. libogc2 configures +`NCRB = AB|CA|2_PACKETS_PER_INT` = `0x52`, i.e. `PR=0` (not promiscuous), +`PM=0` (no multicast), `AB=1` (accept broadcast) — so the device delivers +**own-MAC + broadcast only**. + +We run the W5100 in MACRAW with the **MF (MAC filter) bit clear** +(`_S0_MR_MACRAW = 0x04`, `w5100_parallel_master.py`), so **every frame on the +segment enters the SPRAM ring.** + +Observed consequence, already seen on hardware: the test app's ring filled to +page 15 from what looked like "ambient broadcast traffic" but was in fact all +unicast traffic on the segment too. Ring pressure makes RBFI drop frames we +DO want, and a game's lean driver may not expect foreign unicast at all. + +⚠️ **Setting MF is not a one-line fix.** MF filters against the W5100's +`SHAR`, and we write `SHAR` only during the init sequence. libogc2 *reads* the +MAC out of PAR0–5 and never writes it, so SHAR never diverges there — but a +game that WRITES PAR0–5 would leave SHAR stale and MF would then drop +everything. Doing it properly needs: + +1. decode `NCRB` (see item 3) so `PR`/`PM` can steer the filter; +2. refresh `SHAR` when PAR0–5 changes, not only at init; +3. re-open socket 0 when the filter mode changes (`Sn_MR` is sampled at OPEN). + +**The decision:** filter correctly (matches real hardware, fixes the ring +pressure) at the cost of that machinery, or stay a permissive superset where +nothing is ever lost but the ring fills with other hosts' traffic. + +## 2. Ring geometry is hardcoded — silent corruption risk + +**Status: OPEN.** + +We fix TX = page 0 and RX = pages 1–15 (`_RX_PAGE_FIRST`, `_RX_PAGE_LAST`, +`_PAGES_TOTAL`). libogc2 writes `TLBP = 0x0000`, `BP = 0x0001`, +`RHBP = 0x000F`, which matches us exactly — but **we ignore those writes.** + +A game choosing a smaller RX ring (`RHBP < 0x0F`) would read pages the +assembler is still writing, with nothing reporting an error. The minimum +honest fix is to honour `RHBP`/`BP`, or failing that to detect a value that +disagrees with our layout and surface it (a status bit, or at least the UART +console) rather than corrupt silently. + +## 3. `NCRB` (0x01) is not decoded at all + +**Status: OPEN.** Bits, from libogc2: + +| bit | name | meaning | +|---|---|---| +| 0 | `PR` | promiscuous mode | +| 1 | `CA` | capture effect mode | +| 2 | `PM` | pass multicast | +| 3 | `PB` | pass bad frame | +| 4 | `AB` | accept broadcast | +| 6–7 | `RXINTC` | RX interrupt counter: 1 / 2 / … packets per interrupt | + +We interrupt per frame where libogc2 asks for every 2 packets, which is benign +(more interrupts than requested). `PR`/`PM`/`AB` feed item 1. + +## 4. Registers that read `0x00` but have non-zero hardware defaults + +**Status: OPEN.** A driver doing read-modify-write, or sanity-checking a +default, sees zero. libogc2 *does* read `NWAYC` (0x30) before writing it. + +| reg | name | hardware default | +|---|---|---| +| 0x01 | `NCRB` | — | +| 0x04 | `LTPS` | last transmitted packet status, RO | +| 0x05 | `LRPS` | last received packet status, RO | +| 0x0A/0B | `BP` | boundary page pointer | +| 0x0C/0D | `TLBP` | TX low boundary page | +| 0x0E/0F | `TWP` | TX buffer write page | +| 0x12/13 | `TRP` | TX buffer read page | +| 0x14/15 | `RXINTT` | RX interrupt timer | +| 0x1A/1B | `RHBP` | RX high boundary page | +| 0x30 | `NWAYC` | **0x84** | +| 0x32 | `GCA` | 0x00 | +| 0x3D | `MISC` | **0x3C** | +| 0x3E/3F | `TXFIFOCNT` | TX FIFO counter, RO | +| 0x50 | `MISC2` | 0x00 | +| 0x58–0x5F | SI regs | libogc2 writes `80 00 03 83 32 FE 1F 1F` | +| 0x60 | `SI_ACTRL2` | libogc2 writes `0x00` then `0x74` | + +## 5. Two of our registers do not exist on real hardware + +**Status: KNOWN, low risk, do not "fix" blindly.** + +`0x34`/`0x35` (`TWD_LO`/`TWD_HI`, our TX length latch) and `0x3A` (`HIPR`, +returning `0x01`) are **not in the official register map** — `0x3A` is +unassigned there and `MISC` is `0x3D`. They are our own inventions, and it is +now confirmed that **no driver writes TWD** (the TX length is counted at +TXDATA instead — see CLAUDE.md). They are harmless unless a game writes those +addresses for some other purpose and trips our TX-length latch. + +Removing them would break `gc_bba_test`, which does write TWD, so keep them +but do not add more inventions. + +## 6. Lower risk / no action + +- **Challenge/response** (command regs 0x08/0x09/0x0B) is **device-initiated** + — gated on command status bit `0x10`, which we never raise, so the path + cannot be entered. Do NOT implement `bba_calc_response` emulation. +- **No EEPROM**: the MAC is a fixed reset value in PAR0–5. Since libogc2 reads + the MAC from there rather than writing it, that value is what the console + uses. Games keying anything off the MAC see ours. +- **`RXINTT`** (RX interrupt timer) is interrupt coalescing; ignoring it means + we interrupt earlier than asked, which is safe. +- **Command register `0x0F`** may be a reset strobe rather than a register: + `bba_cmd_in8_slow(0x0F)` holds CS for 200 µs between two 10 ms delays and + discards the result. Implemented as storage; the NCRA reset follows + immediately in that sequence so ignoring any side effect costs nothing + today.