Compat TODO item 2, the last silent-corruption risk. RXFrameAssembler took the RX ring as a compile-time constant (pages 1-15), so a driver configuring a SMALLER ring was ignored and we kept writing pages it would never read, with nothing reporting an error. The bounds now come from bp/rhbp, wired in BBATop from the GC's BP (0x0A/0B) and RHBP (0x1A/1B) registers, and both the page wrap and the free-space calculation use them. Garbage is rejected rather than obeyed: the driver's values are used only when bp >= 1 (page 0 is the TX buffer), rhbp <= 0x0F and rhbp > bp. Otherwise the hardcoded layout stands and geom_honoured goes low, so a mismatch is observable. Without that check BP=0 or an RHBP below BP gives a zero or negative ring size and the assembler writes outside it. ⚠️ Recorded as a known limitation: a ring must be LARGER than _MAX_FRAME_PAGES (6) or nothing is ever stored, because a frame's length is unknown until EOF so the worst case (1518+4 = 6 pages) is reserved up front. Found the honest way -- the test was written with RHBP=4 first and passed VACUOUSLY, storing nothing at all. Making that work needs a length-aware reservation, which is a bigger change than this one. T7 (8-page ring honoured, frames only in pages 1-8, and at least one actually stored) and T7b (all three garbage cases rejected, fallback still stores). Mutation-tested: wrapping at the hardcoded page 15 writes pages 9-11 and fails T7; removing the sanity check fails all four T7b assertions. Item 1 (RX filtering) remains the only open gap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8.8 KiB
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.cin emukidid/swiss-gc. Note that directory holds one file per adapter (bba.cis the real DOL-015;enc28j60.c,w5500.c,w6100.c,w6300.care other hardware) — check which file you are in. - libogc2 —
lwip/arch/gc/netif/gcif.cin extremscorner/libogc2. This is what Swiss's own menu networking uses, viaif_configex(). - stock devkitPro libogc — same shapes as libogc2; read out of
/opt/devkitpro/libogc/lib/cube/libbba.a(gcif.o) withobjdump.
⚠️ 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:
- decode
NCRB(see item 3) soPR/PMcan steer the filter; - refresh
SHARwhen PAR0–5 changes, not only at init; - re-open socket 0 when the filter mode changes (
Sn_MRis 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: ✅ DONE 2026-10-10. RXFrameAssembler takes the ring bounds from
bp/rhbp, wired from the GC's BP (0x0A/0B) and RHBP (0x1A/1B) registers
in BBATop. The wrap and the free-space calculation both use them, so a
driver's smaller ring is respected instead of ignored.
Garbage is rejected, not obeyed. The bounds are used only when
bp >= 1 (page 0 is the TX buffer), rhbp <= 0x0F and rhbp > bp;
otherwise the hardcoded pages 1–15 stand and geom_honoured goes low, so a
mismatch is observable rather than silent. Without that check, BP = 0 or an
RHBP below BP gives a zero or negative ring size and the assembler writes
outside it.
⚠️ A ring must be LARGER than _MAX_FRAME_PAGES (6) or nothing is ever
stored. A frame's length is unknown until EOF, so the assembler reserves the
worst case (1518 + 4 bytes = 6 pages) before accepting one. With a 4-page ring
free can never reach 6, so RBFI drops every frame — correct, conservative,
and useless. This was found by writing the test with RHBP = 4 first, where it
passed vacuously because nothing was stored at all. If a real driver ever
configures a ring that small, the reservation has to become length-aware
(accept a frame whose actual length fits), which is a bigger change than this
one.
Tests: rx_frame_assembler T7 (an 8-page ring is honoured, frames land
only in pages 1–8) and T7b (all three garbage cases rejected, and the
fallback layout still stores). Mutation-tested: wrapping at the hardcoded page
15 writes pages 9–11 and fails T7; removing the sanity check fails all four
T7b assertions.
For the record, libogc2 writes TLBP = 0x0000, BP = 0x0001,
RHBP = 0x000F — exactly the old hardcoded layout, which is why this never
showed up in practice.
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 implementbba_calc_responseemulation. - 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
0x0Fmay 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.