diff --git a/scripts/fsp-server.py b/scripts/fsp-server.py index f9641c2..6ffd370 100755 --- a/scripts/fsp-server.py +++ b/scripts/fsp-server.py @@ -64,8 +64,12 @@ class RawTransport: def __init__(self, iface, port): self.port = port + # ETH_P_IP, not ETH_P_ALL: with ALL, every frame on the link -- + # including our own outgoing ones -- is copied to userspace and + # parsed in Python, which adds latency to the very round trip we are + # trying to shorten. self.s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, - socket.htons(ETH_P_ALL)) + socket.htons(ETH_P_IP)) self.s.bind((iface, 0)) self.our_mac = self.s.getsockname()[4][:6] @@ -73,7 +77,7 @@ class RawTransport: """Return (payload, reply_ctx) for a UDP datagram aimed at our port.""" while True: frame = self.s.recv(65535) - if len(frame) < 34 or struct.unpack("!H", frame[12:14])[0] != ETH_P_IP: + if len(frame) < 34: continue ip = frame[14:] ihl = (ip[0] & 0x0F) * 4 @@ -261,6 +265,14 @@ def main(): "which is what made mini-dhcpd work.") ap.add_argument("--bind", default="0.0.0.0") ap.add_argument("--port", type=int, default=21) + ap.add_argument("--max-reply", type=int, default=1440, + help="largest GET_FILE payload, 32-ALIGNED (default 1440 " + "= 1500 MTU - 20 IP - 8 UDP - 12 FSP, rounded down " + "to 32). Streaming is strictly ONE request in " + "flight, so throughput is this divided by the round " + "trip: at ~2 ms, 1024 B gives ~509 KB/s and 1440 B " + "gives ~700 KB/s. fsplib never asks for more than " + "its own FSP_SPACE (1024) so the menu is unaffected.") ap.add_argument("--quiet-reads", action="store_true", help="only log the rate, not every read") ap.add_argument("--trace", action="store_true", @@ -298,6 +310,7 @@ def main(): print(f" {pkt_[:48].hex()}") uploads = {} # peer -> staged bytes, committed by CC_INSTALL + service = [] # per-request service time, to split the round trip served = 0 t_first = None t_report = time.time() @@ -467,7 +480,18 @@ def main(): if cmd == CC_GET_FILE: want = struct.unpack("!H", r["extra"][:2])[0] if len(r["extra"]) >= 2 else 1024 - want = min(want, FSP_SPACE) + # ⚠️ Do NOT clamp to FSP_SPACE (1024). The in-game client asks + # for pmtu - 40 (1460 at a 1500 MTU) and accepts it; only + # fsplib has the 1024 limit, and fsplib never asks for more than + # that, so honouring the request is safe for both. Clamping here + # is what held streaming to 509 KB/s: one request in flight means + # throughput is payload / RTT, nothing else. + want = min(want, a.max_reply) + # Keep it a multiple of 32 unless it is the file tail: the client + # does data_size = MIN(requested, OSRoundDown32B(reply_len)), so a + # non-aligned mid-file reply is silently truncated. + if want >= 32: + want &= ~31 if fh_path != real: if fh: fh.close() @@ -478,12 +502,14 @@ def main(): reply(ctx, pack(CC_ERR, SERVER_KEY, seq, pos, f"{e}\0".encode()), "ERR") continue + t_req = time.time() fh.seek(pos) data = fh.read(want) reply(ctx, pack(CC_GET_FILE, SERVER_KEY, seq, pos, data), "GET_FILE") served += len(data) reads += 1 now = time.time() + service.append(now - t_req) if t_first is None: t_first = now print(f"[{peer[0]}] GET_FILE {path!r} streaming started") @@ -492,9 +518,18 @@ def main(): f"want={want} -> {len(data)} B") if now - t_report >= 2.0: el = max(now - t_first, 1e-9) + svc = sorted(service) + med = svc[len(svc) // 2] * 1000 if svc else 0.0 + rtt = (el / reads) * 1000 if reads else 0.0 print(f" streaming {served/el/1024:7.0f} KB/s " f"{served/2**20:8.1f} MiB in {el:6.1f}s " - f"{reads/el:5.0f} reads/s last pos {pos:#x}") + f"{reads/el:5.0f} reads/s {len(data)} B/reply") + # One request in flight, so the observed interval IS the round + # trip. Splitting it shows whether WE are the slow part. + print(f" round trip {rtt:5.2f} ms, of which this server " + f"{med:5.2f} ms (median) -> " + f"{'SERVER-bound' if med > rtt/2 else 'console-bound'}") + service.clear() t_report = now continue diff --git a/scripts/test-fsp-server.py b/scripts/test-fsp-server.py index 017ae13..ab50ad1 100755 --- a/scripts/test-fsp-server.py +++ b/scripts/test-fsp-server.py @@ -182,6 +182,27 @@ try: else: print("T10 GET_PRO 'swiss/patches' -> CC_ERR (Swiss falls back to root)") + # T13 reply size must follow the REQUEST, not FSP_SPACE. Clamping to + # 1024 is what held streaming to 509 KB/s: one request in flight means + # throughput is payload / RTT. + r = rt(req(CC_GET_FILE, 60, 0, "/dvd.iso", extra=struct.pack("!H", 1460))) + if len(r["data"]) != 1440: + fails.append(f"T13 asked 1460 -> got {len(r['data'])} B, want 1440 " + f"(1500 MTU budget, rounded down to 32)") + elif len(r["data"]) % 32: + fails.append(f"T13 reply {len(r['data'])} B not 32-aligned") + else: + print(f"T13 request 1460 -> 1440 B reply (32-aligned, 1.41x of 1024)") + + # ...and a 1024-byte request (what fsplib asks) still gets exactly 1024, + # so the menu client is unaffected by the larger cap. + r = rt(req(CC_GET_FILE, 61, 0, "/dvd.iso", extra=struct.pack("!H", 1024))) + if len(r["data"]) != 1024: + fails.append(f"T13 fsplib-sized request got {len(r['data'])} B, " + f"want 1024 - must not exceed what was asked") + else: + print("T13 request 1024 -> 1024 B (fsplib unaffected)") + # T11 the write path Swiss needs for patches: MAKE_DIR, then UP_LOAD # (which carries NO filename -- data is raw bytes at pos), then INSTALL # to name the destination. Without this Swiss reports "failed to write diff --git a/swiss/patches/game/8327fab250602eb4437900088e0734ad.bin b/swiss/patches/game/8327fab250602eb4437900088e0734ad.bin new file mode 100644 index 0000000..d108409 Binary files /dev/null and b/swiss/patches/game/8327fab250602eb4437900088e0734ad.bin differ diff --git a/swiss/patches/game/b01886fe9e83ec2b692c02e4531a59db.bin b/swiss/patches/game/b01886fe9e83ec2b692c02e4531a59db.bin new file mode 100644 index 0000000..9833503 Binary files /dev/null and b/swiss/patches/game/b01886fe9e83ec2b692c02e4531a59db.bin differ diff --git a/swiss/patches/game/bbee34150a7af28ad0d2c607ad9a502b.bin b/swiss/patches/game/bbee34150a7af28ad0d2c607ad9a502b.bin new file mode 100644 index 0000000..5b8c2d5 Binary files /dev/null and b/swiss/patches/game/bbee34150a7af28ad0d2c607ad9a502b.bin differ