4a455bbe88
Compat TODO items 3 and 4. Every register libogc2 writes now stores writes and reads back, with the documented hardware reset defaults: NCRB (0x01), BP (0x0A/0B), TLBP (0x0C/0D), RXINTT (0x14/15), RHBP (0x1A/1B), NWAYC (0x30, default 0x84), GCA (0x32), MISC (0x3D, default 0x3C), MISC2 (0x50). BP and RHBP reset to OUR layout (page 1, page 15) so a driver reading them back without writing sees something consistent with where the ring actually is. Previously all of these read 0x00 and dropped writes, so a driver doing read-modify-write or sanity-checking a default saw zero -- and libogc2 does read NWAYC before writing it. Deliberately still reading 0x00: LTPS, LRPS, TWP, TRP, TXFIFOCNT. Those are STATUS registers, not configuration, so storing what was written to them would be a lie rather than a default. Drivers read LRPS/LTPS only on error bits and 0x00 reads as "no error", which is truthful for a device that does not detect them. NCRB, BP and RHBP are also exported as BBARegisterFile.ncrb/.bp/.rhbp for the RX-filtering work (item 1) and the ring-geometry check (item 2). Nothing consumes them yet -- the decode lands first so the behaviour that uses it is a separate, testable change. T15 checks the defaults, replays libogc2's twelve init writes in its order and reads every one back, and asserts the NCRB export is 0x52 with PR=0, PM=0, AB=1 -- i.e. that the real BBA delivers own-MAC + broadcast only, which is the fact item 1 has to act on. Mutation-tested: dropping NWAYC's 0x84 default fails it, as does dropping NCRB writes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
165 lines
7.6 KiB
Markdown
165 lines
7.6 KiB
Markdown
# 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: ✅ DECODED 2026-10-10** — stored, readable, and exported as
|
||
`BBARegisterFile.ncrb`. Nothing consumes the bits yet; that is item 1.
|
||
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.
|
||
|
||
`bba_register_file` **T15** asserts the export is `0x52` with `PR=0`, `PM=0`,
|
||
`AB=1` — i.e. the real BBA would deliver own-MAC + broadcast only — which is
|
||
the fact item 1 has to act on.
|
||
|
||
## 4. Registers that read `0x00` but have non-zero hardware defaults
|
||
|
||
**Status: ✅ DONE 2026-10-10** for every register libogc2 writes. They now
|
||
store writes and read back, with the documented hardware defaults at reset.
|
||
`bba_register_file` **T15** checks the defaults, replays libogc2's twelve
|
||
init writes and reads every one back; mutation-tested (dropping `NWAYC`'s
|
||
`0x84` default fails it, as does dropping `NCRB` writes).
|
||
|
||
Still reading `0x00`, deliberately: `LTPS` (0x04), `LRPS` (0x05), `TWP`
|
||
(0x0E/0F), `TRP` (0x12/13) and `TXFIFOCNT` (0x3E/3F). These are **status**
|
||
registers, not configuration — a driver reads them to learn what the device
|
||
did, so storing what was written to them would be a lie rather than a default.
|
||
Drivers read `LRPS`/`LTPS` only on error bits, and `0x00` reads as "no error",
|
||
which is the truthful answer for a device that does not detect those errors.
|
||
`TWP`/`TRP`/`TXFIFOCNT` would need the TX engine to actually report its page
|
||
pointers and FIFO depth; worth doing if a driver is ever seen to poll them.
|
||
|
||
The original problem for reference — a driver doing read-modify-write, or
|
||
sanity-checking a default, saw 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.
|