diff --git a/gc_bba_test/README.md b/gc_bba_test/README.md index f5105ce..e6b9717 100644 --- a/gc_bba_test/README.md +++ b/gc_bba_test/README.md @@ -149,6 +149,22 @@ ring ahead), resyncs and continues rather than parsing untrustworthy bytes, and reports the count on timeout — but detection is not prevention. A busy link will still cost retransmissions. +### Throughput + +The transfer is **round-trip bound**, not bandwidth bound: stop-and-wait means +one block per round trip, so block size sets the speed. + +- `--block` defaults to **1024** (292 blocks for a ~298 KB image). 256 was the + conservative bring-up value; drop back to it if large frames misbehave. +- `--timeout` defaults to **0.1 s**. The round trip is a few ms on a LAN, so + the original 0.5 s made every lost ACK cost half a second. +- The GC polls for TI without sleeping. A `usleep(500) × 20` loop cost up to + 10 ms per ACK even when the transmit succeeded immediately — on every block. + +A missing ACK is not fatal: the host retransmits, and the GC re-ACKs a +duplicate block rather than ignoring it, so a lost ACK recovers in one +timeout. `txto=N` in the status line counts ACKs where TI never set. + ### Stop-and-wait is load-bearing The gateware does not consume RRP, so `RXFrameAssembler` can overwrite ring diff --git a/gc_bba_test/source/netboot.c b/gc_bba_test/source/netboot.c index cf5da8b..0e00c12 100644 --- a/gc_bba_test/source/netboot.c +++ b/gc_bba_test/source/netboot.c @@ -172,14 +172,20 @@ static int send_ack(u16 seq) if (bba_tx_bytes(f, sizeof(f)) < 0) return -1; bba_w8(REG_NCRA, NCRA_SR | NCRA_ST0); - /* Wait briefly for TI. Not proof it reached the wire, but its absence - * is a clear failure, and blocking here is fine: stop-and-wait means the - * host is idle until this ACK lands anyway. */ - for (i = 0; i < 20; i++) { + /* Poll for TI WITHOUT sleeping. usleep(500) x 20 meant every ACK cost + * up to 10 ms even when the transmit succeeded immediately, and the + * transfer is round-trip bound -- that is 10 ms on every one of ~292 + * blocks. A bba_r8 is itself tens of microseconds, so a bare loop polls + * often enough and finishes as soon as TI sets. + * + * Absence of TI is NOT treated as fatal: the frame may still have gone + * out (TI only means TXFrameDrain handed it to the W5100), and if it did + * not, the host's retransmit covers it. Blocking longer here would only + * slow the common case to protect against the rare one. */ + for (i = 0; i < 200; i++) { ir = bba_r8(REG_IR); if (ir >= 0 && (ir & IR_TI)) return 0; - usleep(500); } s_tx_timeouts++; return -1; diff --git a/scripts/send-dol.py b/scripts/send-dol.py index 637194c..22e31a2 100755 --- a/scripts/send-dol.py +++ b/scripts/send-dol.py @@ -25,13 +25,18 @@ import time ETHERTYPE = 0x88B5 MAGIC = b"GCUP" OP_DATA, OP_ACK, OP_DONE = 1, 2, 3 -# Default payload 256 bytes -> a ~296-byte frame, which with the 4-byte -# descriptor occupies TWO of the SPRAM ring's 15 pages. That is deliberately -# close to the only multi-page frame ever proven on this hardware (a 342-byte -# DHCP broadcast). 1024-byte payloads make ~1050-byte frames spanning FIVE -# pages, which has never been exercised -- raise --block once small blocks are -# known to work, not before. -BLOCK_DEFAULT = 256 +# 1024-byte payloads -> ~1050-byte frames, five of the SPRAM ring's 15 pages. +# +# 256 was the conservative bring-up value, chosen to stay near the only +# multi-page frame ever proven on hardware (a 342-byte DHCP broadcast). Small +# blocks are now known to work, and two things make the large ones safe: +# RBFI stops a full ring being overwritten, and stop-and-wait keeps exactly one +# frame in flight, so five pages of a fourteen-page budget is never contended. +# +# This matters for speed: a 298 KB image is 1166 blocks at 256 bytes but only +# 292 at 1024, and the transfer is round-trip bound, not bandwidth bound. Drop +# back with --block 256 if large frames misbehave. +BLOCK_DEFAULT = 1024 HDR = 26 # The GC's MAC comes from PAR0-5, which the gateware pre-populates with a # Nintendo OUI. Discovered from the first ACK, so this is only the initial @@ -53,8 +58,10 @@ def main(): ap = argparse.ArgumentParser() ap.add_argument("iface", help="network interface facing the GameCube") ap.add_argument("dol", help=".dol to send") - ap.add_argument("--timeout", type=float, default=0.5, - help="per-block ACK timeout in seconds (default 0.5)") + ap.add_argument("--timeout", type=float, default=0.1, + help="per-block ACK timeout in seconds (default 0.1). " + "The round trip is a few ms on a LAN, so 0.5 made " + "every lost ACK cost half a second.") ap.add_argument("--retries", type=int, default=20, help="per-block retransmits before giving up (default 20)") ap.add_argument("--dest", default="/gc_bba_test.dol",