The 850 KB/s dump measurement does not bound ROM streaming, and the tool could
not have measured streaming at all: it only counted TCP payload, so a
streaming capture would have reported ~zero throughput.
Swiss's ROM streaming runs in the game via the patch driver's own stack
(cube/patches/bba/tcpip.c), which contains no TCP whatsoever -- UDP + FSP,
plus ICMP and ARP. Reads are sized to the path MTU rather than 1024:
_fsp.data_length = MIN(length, env->pmtu - (fsp->data - eth->data));
so ~1446 B of payload per datagram at a 1500 MTU, with QUEUE_SIZE 2
outstanding and udp->checksum = 0x0000 (no payload checksum). None of the
menu path's TCP window, lwIP overhead or 1024-byte writes apply.
The analyser now counts UDP payload and picks the bulk direction by which way
the payload actually flows -- streaming is PC -> GC, the opposite of a disc
dump -- with --bulk to_gc to steer the gap analysis. Self-test H replays the
streaming shape (1446 B datagrams, 2 per request) and checks the bulk
direction is identified and the payload counted.
Recorded in the TODO that "can it stream?" is still OPEN, but open on the GC's
side: the dump established the adapter has headroom (zero retransmissions,
~17% W5100 bus utilisation, 26% of the EXI ceiling). The hard ceiling is
3375 KB/s and the GC's disc delivers ~2000-3100 KB/s, so full disc rate has no
margin -- but games read in bursts, not continuously at peak.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Swiss dumps a disc over the adapter at 740-780 KB/s, which is ~23% of the
27 Mbit/s EXI ceiling (3375 KB/s), and no published real-BBA figure exists to
compare against. Rather than reason about where the rest goes, measure it.
./scripts/analyse-pcap.py /tmp/bba.pcap --gc-ip 10.0.0.64 reports, per
direction: throughput as a fraction of the EXI ceiling, MEAN FRAME SIZE,
retransmissions, the GC's advertised TCP window, zero-window spans, and the
largest gaps between data frames. Each one discriminates a different
bottleneck:
mean frame far below 1514 -> per-frame overhead (interrupts, EXI
transactions, socket bookkeeping) dominates
retransmissions -> we are dropping frames, e.g. the 15-page RX
ring overflowing (RBFI)
small advertised window -> capped at window/RTT regardless of the wire
zero-window spans -> the GC cannot drain: CPU/stack bound
large gaps, little sending -> the limit is upstream of the wire entirely
(disc read, CPU, stack), not the adapter
Classic pcap only, stdlib only, no root. pcapng is detected and refused with
a conversion hint rather than misparsed.
scripts/test-analyse-pcap.py synthesises captures with known properties and
checks each conclusion is reached: 1000 full frames in 2 s reports ~22% of the
ceiling, 200-byte payloads raise the overhead warning, a repeated seq counts 9
retransmissions, one 300 ms hole in 100 frames is found and attributed, a
zero-window span is called CPU/stack bound, and a pcapng header is rejected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>