Measure the throughput limit: window x RTT, not the adapter
Swiss dumping a disc over the adapter measures 850 KB/s (6.97 Mbit/s, 25.8% of the 27 Mbit/s EXI ceiling). The capture decomposes it completely: 18395 data frames / 9200 ACKs = 2.0 frames per ACK 18.84 MB / 9200 = 2048 B per round trip 22.78 s / 9200 = 2.48 ms per round trip 2048 B / 2.48 ms = 826 KB/s ~= the measured 850 So the flow is window x RTT bound and nothing else. Payload per frame is exactly 1024 B (wire 1078 minus 54 of headers) against a 2920 B window, so two segments fit in flight. Zero retransmissions means the 15-page ring never overflowed; 0.2% stall time means nothing waits on the disc; our W5100 bus engine sits at ~17% utilisation. The adapter is not the constraint. Levers, and the first two are not ours: a larger lwIP TCP_SND_BUF (scales almost linearly while window-bound), and full-MSS segments -- Swiss writes 1024 B at a time, so the same window and RTT at 1460 B would give ~1150 KB/s, a 1.43x gain for free. Third is a shorter round trip, which is GC-side interrupt dispatch plus lwIP, plus EXI lock contention: libogc2's bba_event_handler defers when EXI_Lock fails, so RX queues behind TX on the single channel. ⚠️ This REVERSES an earlier recommendation in the TODO. RX interrupt coalescing (NCRB.RXINTC, RXINTT) was listed as a throughput lever; for a window-bound flow it makes things worse, because delaying the ACK's interrupt lengthens the round trip and throughput is bytes-per-round-trip / RTT. The measurement showed the guess was backwards. Noted in item 3 as "implement for fidelity, not for speed". analyse-pcap.py now derives frames-per-ACK, bytes-per-round-trip and the implied RTT itself, identifies a window-bound flow, quantifies the full-MSS gain, and warns against coalescing -- so the next person gets the conclusion rather than the raw numbers. Self-test G replays this exact shape. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -168,6 +168,54 @@ 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.
|
||||
|
||||
## Throughput: measured 850 KB/s, and it is NOT the adapter
|
||||
|
||||
**Measured 2026-10-10**, Swiss dumping a disc over the adapter, captured on
|
||||
the PC and analysed with `scripts/analyse-pcap.py`:
|
||||
|
||||
```
|
||||
GC -> PC 18395 frames, 19.83 MB, 850 KB/s (6.97 Mbit/s, 25.8% of the EXI ceiling)
|
||||
807 frames/s, mean frame 1078 B, retransmissions 0
|
||||
PC -> GC 9200 frames, mean 54 B (pure ACKs)
|
||||
GC advertised window 2920 B; gaps >5 ms total 56 ms (0.2% of 22.78 s)
|
||||
```
|
||||
|
||||
**The flow is window × RTT bound and nothing else:**
|
||||
|
||||
| derived | value |
|
||||
|---|---|
|
||||
| data frames per ACK | 2.0 |
|
||||
| payload per round trip | 2048 B |
|
||||
| implied RTT | 2.48 ms |
|
||||
| 2048 B / 2.48 ms | **826 KB/s** ≈ the measured 850 |
|
||||
| payload per frame | **1024 B** (wire 1078 − 54 of headers) |
|
||||
| window ÷ segment | 2920 / 1024 → **2 segments in flight** |
|
||||
|
||||
So the sender can have 2048 bytes outstanding and each round trip costs
|
||||
2.48 ms. **Zero retransmissions** means the 15-page ring never overflowed and
|
||||
we drop nothing; **0.2% stall time** means nothing is waiting on the disc.
|
||||
Our W5100 bus engine runs at roughly 17% utilisation at this rate. The adapter
|
||||
is not the constraint.
|
||||
|
||||
The levers, in order of effect, and **none of the first two are ours**:
|
||||
|
||||
1. **A larger TCP send buffer on the GC** (lwIP `TCP_SND_BUF`). Throughput
|
||||
scales with it almost linearly while the flow is window-bound.
|
||||
2. **Full-MSS segments.** Swiss writes 1024 bytes at a time, so a 2920 B
|
||||
window carries 2048 B of payload. At 1460 B segments the same window and
|
||||
RTT would give ~**1150 KB/s, a 1.43× gain** for free.
|
||||
3. **A shorter round trip** — throughput scales 1/RTT. The 2.48 ms is GC-side
|
||||
interrupt dispatch and lwIP, plus EXI lock contention: libogc2's
|
||||
`bba_event_handler` bails out if `EXI_Lock` fails and defers until the TX
|
||||
path unlocks, so RX handling queues behind TX on the single channel.
|
||||
|
||||
⚠️ **RX interrupt coalescing would make this WORSE, and that reverses an
|
||||
earlier recommendation in this file.** `NCRB.RXINTC` asks for 2 packets per
|
||||
interrupt and `RXINTT` adds a timer; both were listed as throughput levers.
|
||||
For a **window-bound** flow they are not: delaying the ACK's interrupt
|
||||
lengthens the round trip, and throughput is bytes-per-round-trip ÷ RTT. The
|
||||
measurement is what showed this — the guess was backwards.
|
||||
|
||||
## 3. `NCRB` (0x01) is not decoded at all
|
||||
|
||||
**Status: ✅ DECODED 2026-10-10** — stored, readable, and exported as
|
||||
@@ -186,6 +234,12 @@ Bits, from libogc2:
|
||||
We interrupt per frame where libogc2 asks for every 2 packets, which is benign
|
||||
(more interrupts than requested). `PR`/`PM`/`AB` feed item 1.
|
||||
|
||||
⚠️ **Do NOT implement `RXINTC`/`RXINTT` coalescing as a throughput
|
||||
optimisation.** Measurement showed the bulk flow is window × RTT bound (see
|
||||
"Throughput" above), so delaying an interrupt lengthens the round trip and
|
||||
*lowers* throughput. Implement it for fidelity if a driver depends on the
|
||||
behaviour, not for speed.
|
||||
|
||||
`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.
|
||||
|
||||
Reference in New Issue
Block a user