From 65f41914d25f41481ef03b370f424d0d4eafc30f Mon Sep 17 00:00:00 2001 From: Tinic Uro Date: Sun, 27 Sep 2026 22:05:54 -0700 Subject: [PATCH 1/3] Configure TCP receive-window grow threshold per interface (#89) TCPGROWRTT= in an interface file sets the handshake round trip at or above which a socket on that interface grows its receive window from BSD_TCP_WINDOW_LAN to its maximum. 1..65535, bad values are warned and ignored like TCPACKMAX. Unset is 0, which keeps the built-in 10 ms on every device, WiFiPi included; there is no per-device default. The value rides on AmiSana2If (set at open beside RXBUFFER) and bsd_tcp_window_settle reads it from the socket's own connect interface, so accepted sockets see it too. ami_bsd_tcp_window_settle and ami_bsd_tcp_window_burst_bound both take the threshold. Co-Authored-By: Claude Opus 5.5 --- docs/user/AmiNetXDuo.guide | 9 +++ include/aminetxduo/config.h | 4 ++ include/aminetxduo/config_advice.h | 1 + include/aminetxduo/sana2.h | 3 + install/Install-AmiNetXDuo | 2 + src/bsdsocket/bsdsocket_window.c | 16 ++++-- src/bsdsocket/bsdsocket_window.h | 14 +++-- src/bsdsocket/socket.c | 5 +- src/config/config_advice.c | 3 + src/config/config_parse.c | 17 ++++++ src/config/test/test_config.c | 53 +++++++++++++++++ src/sana2/sana2_device.c | 6 ++ src/sana2/sana2_internal.h | 2 + tests/netstack/host/test_pool_window_host.c | 64 ++++++++++++++------- tests/sana2/host/test_sana2_device_host.c | 29 ++++++++++ 15 files changed, 197 insertions(+), 31 deletions(-) diff --git a/docs/user/AmiNetXDuo.guide b/docs/user/AmiNetXDuo.guide index 8b148028b..7e2518f89 100644 --- a/docs/user/AmiNetXDuo.guide +++ b/docs/user/AmiNetXDuo.guide @@ -478,6 +478,15 @@ interface is next attached. This is a TCP stack setting, not a SANA-II driver command. + @{b}TCPGROWRTT@{ub} + Connect round trip in milliseconds, 1 to 65535, at or + above which a TCP receive window on this interface grows + from 100352 bytes to its maximum. Leave it out for 10 on + every device. A smaller value grows the window on shorter + paths, including a local network. The value applies to + this interface only; editing the file takes effect when + the interface is next attached. + Interface files written for Roadshow usually carry other keywords: COPYMODE, ALIAS, METRIC, LEASE, DHCPUNICAST, LINKSTATUSCOMMAND, MULTICAST, POINTTOPOINT, DESTINATION and BROADCASTADDRESS among diff --git a/include/aminetxduo/config.h b/include/aminetxduo/config.h index 217a9904c..2591a6105 100644 --- a/include/aminetxduo/config.h +++ b/include/aminetxduo/config.h @@ -55,6 +55,7 @@ extern "C" { #endif #define AMI_CFG_WRITEREQUESTS_MAX AMINETXDUO_TX_SLOTS #define AMI_CFG_TCP_ACK_MAX 65535UL +#define AMI_CFG_TCP_GROW_RTT_MAX 65535UL #define AMI_CFG_PATH_LEN 128 /* The default domain gets its own cap: SetDefaultDomainName()'s autodoc says @@ -203,6 +204,9 @@ typedef struct AmiIfConfig { /* TCPACKMAX: maximum unacknowledged receive bytes before an ACK is requested on connections using this interface. 0 = device default. */ ULONG tcp_ack_max; + /* TCPGROWRTT: handshake round trip in ms at or above which a socket on + this interface grows its receive window. 0 = BSD_TCP_WINDOW_GROW_RTT_MS. */ + ULONG tcp_grow_rtt; BYTE priority; /* PRIORITY=, -128..127, 0 unset: the highest wins a route two interfaces could carry */ diff --git a/include/aminetxduo/config_advice.h b/include/aminetxduo/config_advice.h index fb311cc26..c2e21fb99 100644 --- a/include/aminetxduo/config_advice.h +++ b/include/aminetxduo/config_advice.h @@ -65,5 +65,6 @@ #define AMI_CFG_ADVICE_PRIORITY_IS_A_SMALL 50 #define AMI_CFG_ADVICE_FILTER_IS_LOCAL_IPANDARP 51 #define AMI_CFG_ADVICE_TCPACKMAX_IS_ACK_BYTES 52 +#define AMI_CFG_ADVICE_TCPGROWRTT_IS_MILLISECONDS 53 #endif /* AMINETXDUO_CONFIG_ADVICE_H */ diff --git a/include/aminetxduo/sana2.h b/include/aminetxduo/sana2.h index 91716e013..ce9660da7 100644 --- a/include/aminetxduo/sana2.h +++ b/include/aminetxduo/sana2.h @@ -102,6 +102,9 @@ ULONG ami_sana2_get_bps(const AmiSana2If *iface); /* Bytes the card holds from the wire with nobody draining it, 0 = not stated (aminetxduo/anxs2ext.h, ANXD_CMD_RX_CAPACITY). */ ULONG ami_sana2_get_hw_rx_bytes(const AmiSana2If *iface); +/* The interface file's TCPGROWRTT in ms, 0 = not set (bsdsocket_window.h, + BSD_TCP_WINDOW_GROW_RTT_MS). */ +ULONG ami_sana2_get_tcp_grow_rtt(const AmiSana2If *iface); BOOL ami_sana2_is_online(const AmiSana2If *iface); /* diff --git a/install/Install-AmiNetXDuo b/install/Install-AmiNetXDuo index ff96f111a..8cefdf8cb 100644 --- a/install/Install-AmiNetXDuo +++ b/install/Install-AmiNetXDuo @@ -2240,6 +2240,8 @@ (append "; nothing asks it anything.\n") (append "; TCPACKMAX= caps received TCP bytes between requested ACKs.\n") (append "; Leave it out for this device's default.\n") + (append "; TCPGROWRTT= is the connect round trip in ms at which a\n") + (append "; TCP receive window grows. Leave it out for 10.\n") (append ";\n") (append "; An IPv6 build also understands CONFIGURE6 (OFF, LINKLOCAL,\n") (append "; AUTO, DHCP or STATIC), ADDRESS6 and GATEWAY6. A build\n") diff --git a/src/bsdsocket/bsdsocket_window.c b/src/bsdsocket/bsdsocket_window.c index 19dcd4f76..63f24315a 100644 --- a/src/bsdsocket/bsdsocket_window.c +++ b/src/bsdsocket/bsdsocket_window.c @@ -75,11 +75,18 @@ ULONG ami_bsd_tcp_window_fit(ULONG window, ULONG hw_bytes, ULONG mss) return (fit < window) ? fit : window; } +/* TCPGROWRTT, or the built-in line where the interface sets none. */ +static ULONG bsd_grow_rtt(ULONG grow_rtt_ms) +{ + return (grow_rtt_ms != 0UL) ? grow_rtt_ms + : (ULONG)BSD_TCP_WINDOW_GROW_RTT_MS; +} + ULONG ami_bsd_tcp_window_settle(ULONG created, ULONG maximum, ULONG bps, - ULONG rtt_ms) + ULONG rtt_ms, ULONG grow_rtt_ms) { /* A long path: the window is the rate, bursts are paced far away. */ - if (rtt_ms >= (ULONG)BSD_TCP_WINDOW_GROW_RTT_MS) + if (rtt_ms >= bsd_grow_rtt(grow_rtt_ms)) return (maximum > created) ? maximum : created; /* A short path on a link that can put the whole window on the wire at @@ -97,9 +104,10 @@ ULONG ami_bsd_tcp_window_settle(ULONG created, ULONG maximum, ULONG bps, return created; } -BOOL ami_bsd_tcp_window_burst_bound(ULONG bps, ULONG rtt_ms) +BOOL ami_bsd_tcp_window_burst_bound(ULONG bps, ULONG rtt_ms, + ULONG grow_rtt_ms) { - if (rtt_ms >= (ULONG)BSD_TCP_WINDOW_GROW_RTT_MS && + if (rtt_ms >= bsd_grow_rtt(grow_rtt_ms) && bps >= (ULONG)BSD_TCP_WINDOW_FAST_BPS) return FALSE; diff --git a/src/bsdsocket/bsdsocket_window.h b/src/bsdsocket/bsdsocket_window.h index fba2597d0..7f38746c4 100644 --- a/src/bsdsocket/bsdsocket_window.h +++ b/src/bsdsocket/bsdsocket_window.h @@ -55,7 +55,8 @@ * pool: 262,144 on a gigabit LAN is the burst the GENET's ring was measured * to take, 1 MB on a long path is 300 Mbit/s at 26 ms (below). The 10 ms * threshold is a coarse but safe line: the real A1200's LAN round trip is - * 1-5 ms, the emulator's under 1, the Internet's 20 and up. + * 1-5 ms, the emulator's under 1, the Internet's 20 and up. An interface + * file's TCPGROWRTT replaces it on that interface alone (#89). */ #ifndef BSD_TCP_WINDOW_LAN #define BSD_TCP_WINDOW_LAN 100352UL /* (512 / 8) * 1568, the old ceiling */ @@ -143,10 +144,11 @@ * The window a socket settles at once its handshake is done. `created` is * what it opened with, `maximum` what it may grow to, `bps` the link it came * up on (0 = unknown), `rtt_ms` the handshake's round trip (0 = not - * measured, which a passive socket cannot). Pure arithmetic, host-tested. + * measured, which a passive socket cannot), `grow_rtt_ms` the interface's + * TCPGROWRTT (0 = BSD_TCP_WINDOW_GROW_RTT_MS). Pure arithmetic, host-tested. */ ULONG ami_bsd_tcp_window_settle(ULONG created, ULONG maximum, ULONG bps, - ULONG rtt_ms); + ULONG rtt_ms, ULONG grow_rtt_ms); /* * The window a card can absorb: `hw_bytes` is what the driver holds from @@ -174,9 +176,11 @@ ULONG ami_bsd_tcp_window_fit(ULONG window, ULONG hw_bytes, ULONG mss); * path is still burst-bound -- the Internet is faster than a 100 Mbit card, * so its bursts arrive at the card's own line rate and a 25 MHz 68030 * behind a 13 KB ring cannot take them (anxs2ext.h, ANXD_CMD_RX_CAPACITY). - * Pure arithmetic, host-tested. + * `grow_rtt_ms` as for ami_bsd_tcp_window_settle(). Pure arithmetic, + * host-tested. */ -BOOL ami_bsd_tcp_window_burst_bound(ULONG bps, ULONG rtt_ms); +BOOL ami_bsd_tcp_window_burst_bound(ULONG bps, ULONG rtt_ms, + ULONG grow_rtt_ms); #ifndef BSD_TCP_WINDOW_CEILING #ifdef AMINETXDUO_TCP_WINDOW_SCALING diff --git a/src/bsdsocket/socket.c b/src/bsdsocket/socket.c index 192b06ebc..bf4e5a106 100644 --- a/src/bsdsocket/socket.c +++ b/src/bsdsocket/socket.c @@ -205,9 +205,10 @@ VOID bsd_tcp_window_settle(NX_TCP_SOCKET *tcp, ULONG rtt_ms) ? (AmiSana2If *)nxif->nx_interface_additional_link_info : NULL; ULONG bps = (sana != NULL) ? ami_sana2_get_bps(sana) : 0UL; + ULONG grow = ami_sana2_get_tcp_grow_rtt(sana); /* 0 = built-in line */ ULONG cur = tcp->nx_tcp_socket_rx_window_default; ULONG want = ami_bsd_tcp_window_settle(cur, bsd_tcp_window_top(tcp), bps, - rtt_ms); + rtt_ms, grow); /* ... and never more than the card behind this interface can hold from the wire at once (bsdsocket_window.h, ami_bsd_tcp_window_fit), where @@ -215,7 +216,7 @@ VOID bsd_tcp_window_settle(NX_TCP_SOCKET *tcp, ULONG rtt_ms) the path (ami_bsd_tcp_window_burst_bound). The segment size is the one the handshake settled: nx_tcp_socket_mss is what the application asked for and stays 0 on an accepted socket. */ - if (sana != NULL && ami_bsd_tcp_window_burst_bound(bps, rtt_ms)) + if (sana != NULL && ami_bsd_tcp_window_burst_bound(bps, rtt_ms, grow)) want = ami_bsd_tcp_window_fit(want, ami_sana2_get_hw_rx_bytes(sana), tcp->nx_tcp_socket_connect_mss); diff --git a/src/config/config_advice.c b/src/config/config_advice.c index 7ee1daf90..4b668a306 100644 --- a/src/config/config_advice.c +++ b/src/config/config_advice.c @@ -129,6 +129,9 @@ static const char *const ami_cfg_advice_text[] = "TCPACKMAX is the maximum number of received TCP bytes to wait " "before requesting an ACK, 1 to 65535. Leave it out for the " "device default; smaller values send more ACKs.", + "TCPGROWRTT is the connect round trip in milliseconds at or above " + "which a TCP receive window grows to its maximum, 1 to 65535. " + "Leave it out for 10.", }; const char *ami_cfg_advice(UWORD code) diff --git a/src/config/config_parse.c b/src/config/config_parse.c index a93680b45..d3d4eabdd 100644 --- a/src/config/config_parse.c +++ b/src/config/config_parse.c @@ -44,6 +44,7 @@ typedef enum IF_KEY_WRITEREQUESTS, IF_KEY_RXBUFFER, IF_KEY_TCPACKMAX, + IF_KEY_TCPGROWRTT, IF_KEY_PRIORITY } IfKey; @@ -78,6 +79,7 @@ ami_if_keywords[] = { "writerequests", IF_KEY_WRITEREQUESTS }, { "rxbuffer", IF_KEY_RXBUFFER }, { "tcpackmax", IF_KEY_TCPACKMAX }, + { "tcpgrowrtt", IF_KEY_TCPGROWRTT }, { "priority", IF_KEY_PRIORITY }, /* Roadshow's, and PRI */ { "pri", IF_KEY_PRIORITY }, @@ -780,6 +782,21 @@ LONG ami_cfg_parse_interface(const char *name, char *buf, AmiIfConfig *out) } break; + case IF_KEY_TCPGROWRTT: + if (ami_cfg_parse_ulong(value, &n) && + n != 0 && n <= AMI_CFG_TCP_GROW_RTT_MAX) + { + out->tcp_grow_rtt = n; + } + else + { + AMI_WARN("config: %s: bad TCPGROWRTT '%s'", out->name, value); + report_bad_value(lineno, AMI_CFG_PROBLEM_WARN, + "TCPGROWRTT", value, + AMI_CFG_ADVICE_TCPGROWRTT_IS_MILLISECONDS); + } + break; + case IF_KEY_IPREQUESTS: case IF_KEY_ARPREQUESTS: case IF_KEY_WRITEREQUESTS: diff --git a/src/config/test/test_config.c b/src/config/test/test_config.c index 026a574a5..664b9f730 100644 --- a/src/config/test/test_config.c +++ b/src/config/test/test_config.c @@ -1150,6 +1150,58 @@ static void test_interface_tcp_ack_max(void) ami_config_set_reporter(NULL, NULL); } +static void test_interface_tcp_grow_rtt(void) +{ + AmiIfConfig iface; + char *buf; + + printf("interface: TCPGROWRTT is optional and bounded per interface\n"); + + buf = dup_text("device = anxwifipi.device\nconfigure = dhcp\n"); + CHECK(ami_cfg_parse_interface("wifipi", buf, &iface) == AMI_CFG_OK); + CHECK(iface.tcp_grow_rtt == 0); + free(buf); + + buf = dup_text("device = anxwifipi.device\nconfigure = dhcp\n" + "tcpgrowrtt = 2\n"); + CHECK(ami_cfg_parse_interface("wifipi", buf, &iface) == AMI_CFG_OK); + CHECK(iface.tcp_grow_rtt == 2UL); + free(buf); + + buf = dup_text("device = genet.device\nconfigure = dhcp\n" + "TCPGROWRTT=65535\n"); + CHECK(ami_cfg_parse_interface("eth", buf, &iface) == AMI_CFG_OK); + CHECK(iface.tcp_grow_rtt == 65535UL); + free(buf); + + seen_count = 0; + ami_config_set_reporter(collect, NULL); + ami_cfg_problem_file("DEVS:NetInterfaces/wifipi"); + buf = dup_text("device = anxwifipi.device\nconfigure = dhcp\n" + "tcpgrowrtt = 0\n"); + CHECK(ami_cfg_parse_interface("wifipi", buf, &iface) == AMI_CFG_OK); + CHECK(iface.tcp_grow_rtt == 0); + CHECK(seen_count == 1 && seen[0].line == 3); + free(buf); + + seen_count = 0; + buf = dup_text("device = anxwifipi.device\nconfigure = dhcp\n" + "tcpgrowrtt = 65536\n"); + CHECK(ami_cfg_parse_interface("wifipi", buf, &iface) == AMI_CFG_OK); + CHECK(iface.tcp_grow_rtt == 0); + CHECK(seen_count == 1 && seen[0].line == 3); + free(buf); + + seen_count = 0; + buf = dup_text("device = anxwifipi.device\nconfigure = dhcp\n" + "tcpgrowrtt = fast\n"); + CHECK(ami_cfg_parse_interface("wifipi", buf, &iface) == AMI_CFG_OK); + CHECK(iface.tcp_grow_rtt == 0); + CHECK(seen_count == 1 && seen[0].line == 3); + free(buf); + ami_config_set_reporter(NULL, NULL); +} + static void test_interface_priority(void) { AmiIfConfig iface; @@ -2968,6 +3020,7 @@ int main(int argc, char **argv) test_inert_keywords_are_notes(); test_request_counts_have_ceilings(); test_interface_tcp_ack_max(); + test_interface_tcp_grow_rtt(); test_interface_priority(); test_interface_ipv6_only(); #ifdef AMINETXDUO_IPV6 diff --git a/src/sana2/sana2_device.c b/src/sana2/sana2_device.c index b455b26f5..ad7d56b36 100644 --- a/src/sana2/sana2_device.c +++ b/src/sana2/sana2_device.c @@ -1320,6 +1320,7 @@ AmiSana2If *ami_sana2_open(const AmiIfConfig *cfg, LONG *err) * ask for); then a vendor driver whose hardware is known gets its number * by name. 0 is no cap. */ + iface->tcp_grow_rtt = cfg->tcp_grow_rtt; if (cfg->rx_buffer != 0) iface->hw_rx_bytes = cfg->rx_buffer; else if ((iface->extension.Accepted & ANXD_S2F_RX_CAPACITY) != 0) @@ -1607,6 +1608,11 @@ ULONG ami_sana2_get_hw_rx_bytes(const AmiSana2If *iface) return (iface != NULL) ? iface->hw_rx_bytes : 0; } +ULONG ami_sana2_get_tcp_grow_rtt(const AmiSana2If *iface) +{ + return (iface != NULL) ? iface->tcp_grow_rtt : 0; +} + /* * What a vendor driver's card holds from the wire, by the driver's name, * for the drivers whose hardware is known: they cannot be asked. The diff --git a/src/sana2/sana2_internal.h b/src/sana2/sana2_internal.h index e9ab1cd31..6f3044656 100644 --- a/src/sana2/sana2_internal.h +++ b/src/sana2/sana2_internal.h @@ -860,6 +860,8 @@ struct AmiSana2If nobody draining it, 0 = not stated. The TCP window a socket on this interface settles at stays inside it (bsdsocket_window.h). */ ULONG hw_rx_bytes; + /* TCPGROWRTT in ms, 0 = BSD_TCP_WINDOW_GROW_RTT_MS (bsdsocket_window.h). */ + ULONG tcp_grow_rtt; /* The interface file's IPREQUESTS, ARPREQUESTS (0 = the plan decides) and WRITEREQUESTS (1..AMI_SANA2_TX_SLOTS: how many of tx[] are diff --git a/tests/netstack/host/test_pool_window_host.c b/tests/netstack/host/test_pool_window_host.c index 7c3c4bbdc..0c56cfec6 100644 --- a/tests/netstack/host/test_pool_window_host.c +++ b/tests/netstack/host/test_pool_window_host.c @@ -460,62 +460,86 @@ static void i_the_window_settles_for_the_path_and_the_link(void) const ULONG rtt = (ULONG)BSD_TCP_WINDOW_GROW_RTT_MS; /* A long path grows to the maximum on any link, even a gigabit one. */ - h_check(ami_bsd_tcp_window_settle(lan, max, gbit, rtt) == max, + h_check(ami_bsd_tcp_window_settle(lan, max, gbit, rtt, 0UL) == max, "a long path on a gigabit link does not grow"); - h_check(ami_bsd_tcp_window_settle(lan, max, hundm, 22UL) == max, + h_check(ami_bsd_tcp_window_settle(lan, max, hundm, 22UL, 0UL) == max, "a 22 ms path on 100 Mbit does not grow"); - h_check(ami_bsd_tcp_window_settle(lan, max, 0UL, 95UL) == max, + h_check(ami_bsd_tcp_window_settle(lan, max, 0UL, 95UL, 0UL) == max, "a long path on an unknown link does not grow"); /* ... but never past what it was created with when there is no maximum (a peer with no scaling, a small machine). */ - h_check(ami_bsd_tcp_window_settle(lan, lan, gbit, rtt) == lan, + h_check(ami_bsd_tcp_window_settle(lan, lan, gbit, rtt, 0UL) == lan, "a long path grew past a maximum equal to the created window"); - h_check(ami_bsd_tcp_window_settle(8192UL, 8192UL, 0UL, 300UL) == 8192UL, + h_check(ami_bsd_tcp_window_settle(8192UL, 8192UL, 0UL, 300UL, 0UL) == 8192UL, "a small machine's floor window moved on a long path"); /* A LAN round trip on a gigabit link: the size the burst was measured at -- the driver's ring and 128 posted reads back it (202 Mbit/s at 262,144 against 186 at 65,535, no frame lost, bsdsocket_window.h) -- and no further, whatever the maximum: there the window is one burst. */ - h_check(ami_bsd_tcp_window_settle(lan, max, gbit, rtt - 1UL) == lmax, + h_check(ami_bsd_tcp_window_settle(lan, max, gbit, rtt - 1UL, 0UL) == lmax, "a gigabit LAN socket did not settle at the LAN maximum"); - h_check(ami_bsd_tcp_window_settle(lan, max, gbit, 0UL) == lmax, + h_check(ami_bsd_tcp_window_settle(lan, max, gbit, 0UL, 0UL) == lmax, "a passive gigabit socket did not settle at the LAN maximum"); - h_check(ami_bsd_tcp_window_settle(50176UL, max, gbit, 0UL) == lmax, + h_check(ami_bsd_tcp_window_settle(50176UL, max, gbit, 0UL, 0UL) == lmax, "a small created window on a gigabit LAN did not grow"); - h_check(ami_bsd_tcp_window_settle(lan, 200000UL, gbit, 0UL) == 200000UL, + h_check(ami_bsd_tcp_window_settle(lan, 200000UL, gbit, 0UL, 0UL) == 200000UL, "a gigabit LAN socket grew past a maximum under the LAN maximum"); /* ... while the same socket on a long path takes the whole maximum. */ - h_check(ami_bsd_tcp_window_settle(lan, max, gbit, rtt) == max && + h_check(ami_bsd_tcp_window_settle(lan, max, gbit, rtt, 0UL) == max && max > lmax, "a long path over a gigabit link stopped at the LAN maximum"); /* Whether the card must hold the window: a LAN round trip always, a slower card on any path; a gigabit card on a long path is drained at wire speed into posted reads while the far link paces the data. */ - h_check(ami_bsd_tcp_window_burst_bound(gbit, rtt - 1UL) == TRUE, + h_check(ami_bsd_tcp_window_burst_bound(gbit, rtt - 1UL, 0UL) == TRUE, "a gigabit LAN socket is not burst-bound"); - h_check(ami_bsd_tcp_window_burst_bound(gbit, 0UL) == TRUE, + h_check(ami_bsd_tcp_window_burst_bound(gbit, 0UL, 0UL) == TRUE, "a passive gigabit socket is not burst-bound"); - h_check(ami_bsd_tcp_window_burst_bound(gbit, rtt) == FALSE, + h_check(ami_bsd_tcp_window_burst_bound(gbit, rtt, 0UL) == FALSE, "a gigabit card on a long path is burst-bound"); - h_check(ami_bsd_tcp_window_burst_bound(hundm, 26UL) == TRUE, + h_check(ami_bsd_tcp_window_burst_bound(hundm, 26UL, 0UL) == TRUE, "a 100 Mbit card on a long path is not burst-bound"); - h_check(ami_bsd_tcp_window_burst_bound(tenm, 300UL) == TRUE, + h_check(ami_bsd_tcp_window_burst_bound(tenm, 300UL, 0UL) == TRUE, "a 10 Mbit card on a long path is not burst-bound"); - h_check(ami_bsd_tcp_window_burst_bound(0UL, 95UL) == TRUE, + h_check(ami_bsd_tcp_window_burst_bound(0UL, 95UL, 0UL) == TRUE, "an unknown link on a long path is not burst-bound"); - h_check(ami_bsd_tcp_window_settle(lan, lan, gbit, 0UL) == lan, + h_check(ami_bsd_tcp_window_settle(lan, lan, gbit, 0UL, 0UL) == lan, "a gigabit LAN socket grew past a maximum equal to its window"); /* A LAN round trip on 10 or 100 Mbit: exactly what it opened with. */ - h_check(ami_bsd_tcp_window_settle(lan, max, hundm, 1UL) == lan, + h_check(ami_bsd_tcp_window_settle(lan, max, hundm, 1UL, 0UL) == lan, "a 100 Mbit LAN socket did not keep the LAN window"); - h_check(ami_bsd_tcp_window_settle(lan, max, tenm, 5UL) == lan, + h_check(ami_bsd_tcp_window_settle(lan, max, tenm, 5UL, 0UL) == lan, "a 10 Mbit LAN socket did not keep the LAN window"); - h_check(ami_bsd_tcp_window_settle(lan, max, 0UL, 0UL) == lan, + h_check(ami_bsd_tcp_window_settle(lan, max, 0UL, 0UL, 0UL) == lan, "an unknown link on a LAN did not keep the LAN window"); + /* TCPGROWRTT moves the line for its interface alone; 0 is the built-in + 10 ms, which is also what a socket with no interface gets. */ + h_check(ami_bsd_tcp_window_settle(lan, max, hundm, 2UL, 2UL) == max, + "TCPGROWRTT=2 did not grow a 2 ms path"); + h_check(ami_bsd_tcp_window_settle(lan, max, hundm, 1UL, 2UL) == lan, + "TCPGROWRTT=2 grew a 1 ms path"); + h_check(ami_bsd_tcp_window_settle(lan, max, hundm, 2UL, 0UL) == lan, + "the default line grew a 2 ms path"); + h_check(ami_bsd_tcp_window_settle(lan, max, hundm, rtt - 1UL, 0UL) == lan && + ami_bsd_tcp_window_settle(lan, max, hundm, rtt, 0UL) == max, + "an unset TCPGROWRTT is not the 10 ms line"); + h_check(ami_bsd_tcp_window_settle(lan, max, hundm, 22UL, 50UL) == lan, + "TCPGROWRTT=50 grew a 22 ms path"); + h_check(ami_bsd_tcp_window_settle(lan, max, gbit, 2UL, 2UL) == max && + ami_bsd_tcp_window_settle(lan, max, gbit, 2UL, 0UL) == lmax, + "TCPGROWRTT did not lift a gigabit LAN past the LAN maximum"); + h_check(ami_bsd_tcp_window_burst_bound(gbit, 2UL, 2UL) == FALSE && + ami_bsd_tcp_window_burst_bound(gbit, 2UL, 0UL) == TRUE, + "TCPGROWRTT did not move the burst-bound line with it"); + h_check(ami_bsd_tcp_window_burst_bound(gbit, rtt, 50UL) == TRUE, + "a raised TCPGROWRTT left a 10 ms gigabit path unbounded"); + h_check(ami_bsd_tcp_window_burst_bound(hundm, 2UL, 2UL) == TRUE, + "TCPGROWRTT unbounded a 100 Mbit card"); + /* The card's own memory caps whatever settled: an X-Surf 100's 13 KB ring (52 pages) holds eight 1536-byte frames of 1460 payload each; the GENET's 256 KB holds more than the LAN window; a card that says diff --git a/tests/sana2/host/test_sana2_device_host.c b/tests/sana2/host/test_sana2_device_host.c index 6b898b4ea..5916371b7 100644 --- a/tests/sana2/host/test_sana2_device_host.c +++ b/tests/sana2/host/test_sana2_device_host.c @@ -1457,6 +1457,34 @@ static void case_request_counts(void) (VOID)ami_sana2_close(iface); } +/* TCPGROWRTT reaches the interface at open; unsaid, or no interface at all, + is 0, which bsdsocket reads as the built-in 10 ms. */ +static void case_tcp_grow_rtt(void) +{ + AmiSana2If *iface; + LONG err = 0; + + printf(" the interface file's TCPGROWRTT reaches the interface\n"); + + h_config(); + h_cfg.tcp_grow_rtt = 2; + iface = ami_sana2_open(&h_cfg, &err); + h_check(iface != NULL && ami_sana2_get_tcp_grow_rtt(iface) == 2UL, + "TCPGROWRTT=2 is carried to the interface"); + if (iface != NULL) + (VOID)ami_sana2_close(iface); + + h_config(); + iface = ami_sana2_open(&h_cfg, &err); + h_check(iface != NULL && ami_sana2_get_tcp_grow_rtt(iface) == 0, + "an unsaid TCPGROWRTT is 0"); + if (iface != NULL) + (VOID)ami_sana2_close(iface); + + h_check(ami_sana2_get_tcp_grow_rtt(NULL) == 0, + "no interface is 0"); +} + /* FILTER=EVERYTHING is SANA2OPF_PROM at OpenDevice(), and nothing else in the file touches the flags. */ static void case_filter_everything(void) @@ -1868,6 +1896,7 @@ int main(void) case_stats_request(); case_keeps_online(); case_request_counts(); + case_tcp_grow_rtt(); case_filter_everything(); case_offline_bounded_sync(); case_offline_bounded_late_reply(); From fe0667b2738ae79ebc955e1002036a99335430d2 Mon Sep 17 00:00:00 2001 From: Tinic Uro Date: Sun, 27 Sep 2026 22:12:54 -0700 Subject: [PATCH 2/3] Test the settle path with a NULL interface; UWORD TCPGROWRTT test_sana2_device composes ami_sana2_get_tcp_grow_rtt() with the window functions the way bsd_tcp_window_settle does: a NULL interface or an unset key keeps the rtt >= 10 line, TCPGROWRTT=2 grows a 2 ms connect. The field is a UWORD (1..65535) and the advice text is shorter, for the resident budget. Co-Authored-By: Claude Opus 5.5 --- include/aminetxduo/config.h | 2 +- src/config/config_advice.c | 4 +--- src/config/config_parse.c | 2 +- src/sana2/sana2_internal.h | 2 +- tests/sana2/CMakeLists.txt | 4 +++- tests/sana2/host/test_sana2_device_host.c | 20 ++++++++++++++++++++ 6 files changed, 27 insertions(+), 7 deletions(-) diff --git a/include/aminetxduo/config.h b/include/aminetxduo/config.h index 2591a6105..6b7f3f24d 100644 --- a/include/aminetxduo/config.h +++ b/include/aminetxduo/config.h @@ -206,7 +206,7 @@ typedef struct AmiIfConfig { ULONG tcp_ack_max; /* TCPGROWRTT: handshake round trip in ms at or above which a socket on this interface grows its receive window. 0 = BSD_TCP_WINDOW_GROW_RTT_MS. */ - ULONG tcp_grow_rtt; + UWORD tcp_grow_rtt; BYTE priority; /* PRIORITY=, -128..127, 0 unset: the highest wins a route two interfaces could carry */ diff --git a/src/config/config_advice.c b/src/config/config_advice.c index 4b668a306..122a6ecae 100644 --- a/src/config/config_advice.c +++ b/src/config/config_advice.c @@ -129,9 +129,7 @@ static const char *const ami_cfg_advice_text[] = "TCPACKMAX is the maximum number of received TCP bytes to wait " "before requesting an ACK, 1 to 65535. Leave it out for the " "device default; smaller values send more ACKs.", - "TCPGROWRTT is the connect round trip in milliseconds at or above " - "which a TCP receive window grows to its maximum, 1 to 65535. " - "Leave it out for 10.", + "TCPGROWRTT is milliseconds, 1 to 65535. Leave it out for 10.", }; const char *ami_cfg_advice(UWORD code) diff --git a/src/config/config_parse.c b/src/config/config_parse.c index d3d4eabdd..2de5ebd47 100644 --- a/src/config/config_parse.c +++ b/src/config/config_parse.c @@ -786,7 +786,7 @@ LONG ami_cfg_parse_interface(const char *name, char *buf, AmiIfConfig *out) if (ami_cfg_parse_ulong(value, &n) && n != 0 && n <= AMI_CFG_TCP_GROW_RTT_MAX) { - out->tcp_grow_rtt = n; + out->tcp_grow_rtt = (UWORD)n; } else { diff --git a/src/sana2/sana2_internal.h b/src/sana2/sana2_internal.h index 6f3044656..b3568a10c 100644 --- a/src/sana2/sana2_internal.h +++ b/src/sana2/sana2_internal.h @@ -861,7 +861,7 @@ struct AmiSana2If interface settles at stays inside it (bsdsocket_window.h). */ ULONG hw_rx_bytes; /* TCPGROWRTT in ms, 0 = BSD_TCP_WINDOW_GROW_RTT_MS (bsdsocket_window.h). */ - ULONG tcp_grow_rtt; + UWORD tcp_grow_rtt; /* The interface file's IPREQUESTS, ARPREQUESTS (0 = the plan decides) and WRITEREQUESTS (1..AMI_SANA2_TX_SLOTS: how many of tx[] are diff --git a/tests/sana2/CMakeLists.txt b/tests/sana2/CMakeLists.txt index 43b06e6b9..8ee227099 100644 --- a/tests/sana2/CMakeLists.txt +++ b/tests/sana2/CMakeLists.txt @@ -250,7 +250,8 @@ add_executable(test_sana2_device host/test_sana2_device_host.c "${CMAKE_SOURCE_DIR}/src/sana2/sana2_device.c" "${CMAKE_SOURCE_DIR}/src/sana2/sana2_policy.c" - "${CMAKE_SOURCE_DIR}/src/sana2/sana2_txmeta.c") + "${CMAKE_SOURCE_DIR}/src/sana2/sana2_txmeta.c" + "${CMAKE_SOURCE_DIR}/src/bsdsocket/bsdsocket_window.c") target_compile_options(test_sana2_device PRIVATE -include "${CMAKE_CURRENT_SOURCE_DIR}/host/shim/host_regargs.h") @@ -264,6 +265,7 @@ target_include_directories(test_sana2_device PRIVATE "${CMAKE_SOURCE_DIR}/src/config/test/shim" "${CMAKE_SOURCE_DIR}/src/sana2" "${CMAKE_SOURCE_DIR}/src/net68k" + "${CMAKE_SOURCE_DIR}/src/bsdsocket" "${CMAKE_SOURCE_DIR}/include" "${CMAKE_SOURCE_DIR}/port/threadx-amiga/inc" "${AMINETXDUO_NETXDUO}/ports/linux/gnu/inc" diff --git a/tests/sana2/host/test_sana2_device_host.c b/tests/sana2/host/test_sana2_device_host.c index 5916371b7..b00f3e1a1 100644 --- a/tests/sana2/host/test_sana2_device_host.c +++ b/tests/sana2/host/test_sana2_device_host.c @@ -2,6 +2,7 @@ SPDX-License-Identifier: MIT */ #include "sana2_internal.h" +#include "bsdsocket_window.h" #include "aminetxduo/netstack.h" #include "aminetxduo/netstatus.h" @@ -1461,6 +1462,8 @@ static void case_request_counts(void) is 0, which bsdsocket reads as the built-in 10 ms. */ static void case_tcp_grow_rtt(void) { + const ULONG lan = (ULONG)BSD_TCP_WINDOW_LAN; + const ULONG max = (ULONG)BSD_TCP_WINDOW_MAX; AmiSana2If *iface; LONG err = 0; @@ -1471,6 +1474,13 @@ static void case_tcp_grow_rtt(void) iface = ami_sana2_open(&h_cfg, &err); h_check(iface != NULL && ami_sana2_get_tcp_grow_rtt(iface) == 2UL, "TCPGROWRTT=2 is carried to the interface"); + /* What socket.c:bsd_tcp_window_settle does with it. */ + h_check(iface != NULL && + ami_bsd_tcp_window_settle(lan, max, 0UL, 2UL, + ami_sana2_get_tcp_grow_rtt(iface)) == max && + ami_bsd_tcp_window_settle(lan, max, 0UL, 1UL, + ami_sana2_get_tcp_grow_rtt(iface)) == lan, + "TCPGROWRTT=2 grows a 2 ms connect and not a 1 ms one"); if (iface != NULL) (VOID)ami_sana2_close(iface); @@ -1483,6 +1493,16 @@ static void case_tcp_grow_rtt(void) h_check(ami_sana2_get_tcp_grow_rtt(NULL) == 0, "no interface is 0"); + /* A NULL interface takes the built-in rtt >= 10 line, as socket.c. */ + h_check(ami_bsd_tcp_window_settle(lan, max, 0UL, 9UL, + ami_sana2_get_tcp_grow_rtt(NULL)) == lan && + ami_bsd_tcp_window_settle(lan, max, 0UL, 10UL, + ami_sana2_get_tcp_grow_rtt(NULL)) == max && + ami_bsd_tcp_window_burst_bound(1000000000UL, 9UL, + ami_sana2_get_tcp_grow_rtt(NULL)) == TRUE && + ami_bsd_tcp_window_burst_bound(1000000000UL, 10UL, + ami_sana2_get_tcp_grow_rtt(NULL)) == FALSE, + "a NULL interface keeps the 10 ms line"); } /* FILTER=EVERYTHING is SANA2OPF_PROM at OpenDevice(), and nothing else in From 44d1bb408fd11fbd54f694138015d03dd8e820f1 Mon Sep 17 00:00:00 2001 From: claudecode Date: Sun, 27 Sep 2026 22:16:14 -0700 Subject: [PATCH 3/3] TCPGROWRTT: changelog entry; "handshake round trip" in the guide and installer The window setting applies to accepted sockets as well as connects, so the guide and the installer comment name the handshake round trip. The guide no longer promises growth from exactly 100352 bytes: both sizes depend on the pool and on window scaling. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 +++++ docs/user/AmiNetXDuo.guide | 7 ++++--- install/Install-AmiNetXDuo | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e254dda2..f4b623b75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ version at the top when it merges. ## Unreleased +- `TCPGROWRTT=` in an interface file sets the handshake round trip + (1 to 65,535 ms) at or above which a TCP receive window on that interface + grows to its maximum. Unset, every device keeps 10 ms. The key adds 100 + bytes, bringing the resident `bsdsocket.library` image to 365,416 bytes; + its budget is 366,000 bytes. - TCP ACK batching can be set per interface with `TCPACKMAX`. WiFiPi defaults to an 11,680-byte ceiling when the setting is absent; other devices retain the 50,176-byte port default. This changes ACK cadence, not the unresolved diff --git a/docs/user/AmiNetXDuo.guide b/docs/user/AmiNetXDuo.guide index 7e2518f89..80038275b 100644 --- a/docs/user/AmiNetXDuo.guide +++ b/docs/user/AmiNetXDuo.guide @@ -479,10 +479,11 @@ not a SANA-II driver command. @{b}TCPGROWRTT@{ub} - Connect round trip in milliseconds, 1 to 65535, at or + Handshake round trip in milliseconds, 1 to 65535, at or above which a TCP receive window on this interface grows - from 100352 bytes to its maximum. Leave it out for 10 on - every device. A smaller value grows the window on shorter + from its starting size to the largest the socket may + use. Both sizes depend on the packet pool and on window + scaling. Leave it out for 10 on every device. A smaller value grows the window on shorter paths, including a local network. The value applies to this interface only; editing the file takes effect when the interface is next attached. diff --git a/install/Install-AmiNetXDuo b/install/Install-AmiNetXDuo index 8cefdf8cb..b283559f9 100644 --- a/install/Install-AmiNetXDuo +++ b/install/Install-AmiNetXDuo @@ -2240,7 +2240,7 @@ (append "; nothing asks it anything.\n") (append "; TCPACKMAX= caps received TCP bytes between requested ACKs.\n") (append "; Leave it out for this device's default.\n") - (append "; TCPGROWRTT= is the connect round trip in ms at which a\n") + (append "; TCPGROWRTT= is the handshake round trip in ms at which a\n") (append "; TCP receive window grows. Leave it out for 10.\n") (append ";\n") (append "; An IPv6 build also understands CONFIGURE6 (OFF, LINKLOCAL,\n")