diff --git a/docs/aminet-survey/own-tools.tsv b/docs/aminet-survey/own-tools.tsv index 22414fd7f..f1a07707c 100644 --- a/docs/aminet-survey/own-tools.tsv +++ b/docs/aminet-survey/own-tools.tsv @@ -5,7 +5,7 @@ offset lvo aminet_callers our_tool_mentions -48 accept 245 40 -54 connect 600 21 -60 sendto 217 1 --66 send 758 83 +-66 send 758 84 -72 recvfrom 198 2 -78 recv 696 17 -84 shutdown 198 8 diff --git a/docs/user/AmiNetXDuo.guide b/docs/user/AmiNetXDuo.guide index 53fbf2652..8b148028b 100644 --- a/docs/user/AmiNetXDuo.guide +++ b/docs/user/AmiNetXDuo.guide @@ -467,6 +467,17 @@ ring overflowed 42 times in ten seconds and the card received 2.8 Mbit/s. + @{b}TCPACKMAX@{ub} + Maximum received TCP bytes between requested ACKs on + connections using this interface, 1 to 65535. Smaller + values send more ACKs and may improve Wi-Fi receive speed + at a cost in transmit work. Leave it out for the device + default: 11680 for anxwifipi.device and wifipi.device, + 50176 for other devices. An explicit value wins on this + interface only; editing the file takes effect when the + interface is next attached. This is a TCP stack setting, + not a SANA-II driver command. + 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 aed79df6e..217a9904c 100644 --- a/include/aminetxduo/config.h +++ b/include/aminetxduo/config.h @@ -54,6 +54,7 @@ extern "C" { #define AMINETXDUO_TX_SLOTS 32 #endif #define AMI_CFG_WRITEREQUESTS_MAX AMINETXDUO_TX_SLOTS +#define AMI_CFG_TCP_ACK_MAX 65535UL #define AMI_CFG_PATH_LEN 128 /* The default domain gets its own cap: SetDefaultDomainName()'s autodoc says @@ -199,6 +200,9 @@ typedef struct AmiIfConfig { * hardware is known gets a default by name. A number here wins. */ ULONG rx_buffer; + /* TCPACKMAX: maximum unacknowledged receive bytes before an ACK is + requested on connections using this interface. 0 = device default. */ + ULONG tcp_ack_max; 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 b744fe162..fb311cc26 100644 --- a/include/aminetxduo/config_advice.h +++ b/include/aminetxduo/config_advice.h @@ -64,5 +64,6 @@ #define AMI_CFG_ADVICE_RXBUFFER_IS_THE 49 #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 #endif /* AMINETXDUO_CONFIG_ADVICE_H */ diff --git a/include/aminetxduo/sana2.h b/include/aminetxduo/sana2.h index 2ba224d5b..c57689e97 100644 --- a/include/aminetxduo/sana2.h +++ b/include/aminetxduo/sana2.h @@ -72,6 +72,11 @@ UWORD ami_sana2_retained_count(VOID); refuses such a pair with AMI_NET_ERR_RETAINED before OpenDevice(). */ ULONG ami_sana2_retained_holds(const char *device, ULONG unit); +/* TCP ACK ceiling when TCPACKMAX is absent: WiFiPi needs the measured + one-GRO-run cadence, other devices use the NetX port default. Zero means + use that port default. Names are compared by basename, ignoring case. */ +ULONG ami_sana2_default_tcp_ack_max(const char *device); + /* * Collect whatever the devices have given back since, without waiting, and * CloseDevice() and free every interface nothing holds any more. Under diff --git a/install/Install-AmiNetXDuo b/install/Install-AmiNetXDuo index 28e28f930..ff96f111a 100644 --- a/install/Install-AmiNetXDuo +++ b/install/Install-AmiNetXDuo @@ -2238,6 +2238,8 @@ (append "; keyword is absent. The responder costs\n") (append "; real time on a slow machine, even when\n") (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 ";\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/config/config_advice.c b/src/config/config_advice.c index 61ee91634..7ee1daf90 100644 --- a/src/config/config_advice.c +++ b/src/config/config_advice.c @@ -126,6 +126,9 @@ static const char *const ami_cfg_advice_text[] = "FILTER is LOCAL, IPANDARP or EVERYTHING. The first two are wha" "t happens anyway. EVERYTHING opens the driver promiscuous, for" " a card whose multicast filter lets nothing through.", + "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.", }; const char *ami_cfg_advice(UWORD code) diff --git a/src/config/config_parse.c b/src/config/config_parse.c index 8808d285b..a93680b45 100644 --- a/src/config/config_parse.c +++ b/src/config/config_parse.c @@ -43,6 +43,7 @@ typedef enum IF_KEY_ARPREQUESTS, IF_KEY_WRITEREQUESTS, IF_KEY_RXBUFFER, + IF_KEY_TCPACKMAX, IF_KEY_PRIORITY } IfKey; @@ -76,6 +77,7 @@ ami_if_keywords[] = { "arprequests", IF_KEY_ARPREQUESTS }, { "writerequests", IF_KEY_WRITEREQUESTS }, { "rxbuffer", IF_KEY_RXBUFFER }, + { "tcpackmax", IF_KEY_TCPACKMAX }, { "priority", IF_KEY_PRIORITY }, /* Roadshow's, and PRI */ { "pri", IF_KEY_PRIORITY }, @@ -763,6 +765,21 @@ LONG ami_cfg_parse_interface(const char *name, char *buf, AmiIfConfig *out) } break; + case IF_KEY_TCPACKMAX: + if (ami_cfg_parse_ulong(value, &n) && + n != 0 && n <= AMI_CFG_TCP_ACK_MAX) + { + out->tcp_ack_max = n; + } + else + { + AMI_WARN("config: %s: bad TCPACKMAX '%s'", out->name, value); + report_bad_value(lineno, AMI_CFG_PROBLEM_WARN, + "TCPACKMAX", value, + AMI_CFG_ADVICE_TCPACKMAX_IS_ACK_BYTES); + } + 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 8f6edb884..026a574a5 100644 --- a/src/config/test/test_config.c +++ b/src/config/test/test_config.c @@ -1117,6 +1117,39 @@ static void test_request_counts_have_ceilings(void) CHECK(seen[1].line == 4); CHECK(seen[1].severity == AMI_CFG_PROBLEM_WARN); } +static void test_interface_tcp_ack_max(void) +{ + AmiIfConfig iface; + char *buf; + + printf("interface: TCPACKMAX is optional and bounded per interface\n"); + + buf = dup_text("device = anxwifipi.device\nconfigure = dhcp\n" + "tcpackmax = 11680\n"); + CHECK(ami_cfg_parse_interface("wifipi", buf, &iface) == AMI_CFG_OK); + CHECK(iface.tcp_ack_max == 11680UL); + 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" + "tcpackmax = 0\n"); + CHECK(ami_cfg_parse_interface("wifipi", buf, &iface) == AMI_CFG_OK); + CHECK(iface.tcp_ack_max == 0); + CHECK(seen_count == 1 && seen[0].line == 3); + free(buf); + + seen_count = 0; + buf = dup_text("device = anxwifipi.device\nconfigure = dhcp\n" + "tcpackmax = 65536\n"); + CHECK(ami_cfg_parse_interface("wifipi", buf, &iface) == AMI_CFG_OK); + CHECK(iface.tcp_ack_max == 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; @@ -2934,6 +2967,7 @@ int main(int argc, char **argv) test_problem_reporter(); test_inert_keywords_are_notes(); test_request_counts_have_ceilings(); + test_interface_tcp_ack_max(); test_interface_priority(); test_interface_ipv6_only(); #ifdef AMINETXDUO_IPV6 diff --git a/src/netstack/netstack.c b/src/netstack/netstack.c index 08f63d7a0..83c5bad9a 100644 --- a/src/netstack/netstack.c +++ b/src/netstack/netstack.c @@ -532,10 +532,20 @@ static VOID ami_ns_park_unaddressed(AmiNetStack *ns, UWORD index) * keep attach order, which without this line is the whole rule -- a 3c589 in * slot 0 carried a subnet an anxgenet.device in slot 1 shares. */ -static VOID ami_ns_apply_priority(AmiNetStack *ns, UWORD index) +static VOID ami_ns_apply_interface_policy(AmiNetStack *ns, UWORD index) { ns->ns_Ip.nx_ip_interface[index].nx_interface_priority = (INT)ns->ns_Config.interfaces[index].priority; + + /* A TCP connection keeps its NX_INTERFACE for its lifetime. Publish the + ACK policy before bringing this interface online; zero leaves NetX at + the port default (50176), while WiFiPi defaults to the measured 11680. + An explicit TCPACKMAX always wins for this interface alone. */ + ns->ns_Ip.nx_ip_interface[index].nx_interface_tcp_ack_threshold_max = + (ns->ns_Config.interfaces[index].tcp_ack_max != 0) + ? ns->ns_Config.interfaces[index].tcp_ack_max + : ami_sana2_default_tcp_ack_max( + ns->ns_Config.interfaces[index].device); } /* @@ -809,7 +819,7 @@ static LONG ami_ns_create_ip(AmiNetStack *ns) for (i = 0; i < ns->ns_IfaceCount; i++) { ami_ns_park_unaddressed(ns, i); - ami_ns_apply_priority(ns, i); + ami_ns_apply_interface_policy(ns, i); if (ns->ns_Ip.nx_ip_interface[i].nx_interface_link_up == NX_FALSE) ami_event(NETEVENT_LINK_DOWN, i, 0UL); @@ -3270,7 +3280,7 @@ static LONG ami_ns_interface_add_locked(const AmiIfConfig *cfg, ns->ns_IfaceCount = (UWORD)(slot + 1); ami_ns_park_unaddressed(ns, (UWORD)slot); - ami_ns_apply_priority(ns, (UWORD)slot); + ami_ns_apply_interface_policy(ns, (UWORD)slot); ns->ns_Ip.nx_ip_interface[slot].nx_interface_ip_conflict_notify_handler = ami_ns_ip_conflict; diff --git a/src/sana2/CMakeLists.txt b/src/sana2/CMakeLists.txt index 1db2149f2..b51d1f8cd 100644 --- a/src/sana2/CMakeLists.txt +++ b/src/sana2/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(aminetxduo_sana2 STATIC sana2_device.c + sana2_policy.c sana2_copy.c sana2_rx.c sana2_tx.c diff --git a/src/sana2/sana2_policy.c b/src/sana2/sana2_policy.c new file mode 100644 index 000000000..0be350fd9 --- /dev/null +++ b/src/sana2/sana2_policy.c @@ -0,0 +1,38 @@ +/* Pure device-name defaults shared by the driver and host netstack tests. */ +#include "aminetxduo/sana2.h" + +static char lower_ascii(char c) +{ + if (c >= 'A' && c <= 'Z') + return (char)(c + ('a' - 'A')); + return c; +} + +static int name_equal(const char *a, const char *b) +{ + while (*a != '\0' && *b != '\0') + { + if (lower_ascii(*a++) != lower_ascii(*b++)) + return 0; + } + return *a == *b; +} + +ULONG ami_sana2_default_tcp_ack_max(const char *device) +{ + const char *base = device; + const char *p; + + if (device == NULL) + return 0; + + for (p = device; *p != '\0'; p++) + if (*p == '/' || *p == ':') + base = p + 1; + + if (name_equal(base, "anxwifipi.device") || + name_equal(base, "wifipi.device")) + return 11680UL; + + return 0; +} diff --git a/tests/netstack/CMakeLists.txt b/tests/netstack/CMakeLists.txt index 18a76b20d..8611efec7 100644 --- a/tests/netstack/CMakeLists.txt +++ b/tests/netstack/CMakeLists.txt @@ -963,6 +963,7 @@ if(NOT CMAKE_CROSSCOMPILING) # because that is the shipping default and its handful of entry points are # already answered. set(NETSTACK_HOST_TU + "${CMAKE_SOURCE_DIR}/src/sana2/sana2_policy.c" "${CMAKE_SOURCE_DIR}/src/netstack/netstack.c" "${CMAKE_SOURCE_DIR}/src/netstack/netstack_caller.c" "${CMAKE_SOURCE_DIR}/src/netstack/netstack_claim.c" diff --git a/tests/netstack/host/test_config_routes_host.c b/tests/netstack/host/test_config_routes_host.c index 9dfde411e..817e1c562 100644 --- a/tests/netstack/host/test_config_routes_host.c +++ b/tests/netstack/host/test_config_routes_host.c @@ -118,11 +118,58 @@ static void t_loopback_then_specific_route(void) h_down(); } +static void t_interface_tcp_ack_policy(void) +{ + static const struct { + const char *device; + ULONG requested; + ULONG expected; + } cases[] = { + { "RAM:anxwifipi.device", 0, 11680UL }, + { "genet.device", 0, 0 }, + { "genet.device", 8192UL, 8192UL }, + { "RAM:anxwifipi.device", 4096UL, 4096UL } + }; + AmiIfConfig cfg; + UWORD index; + unsigned int i; + + printf("interface: TCP ACK policy follows the attached device and override\n"); + + for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) + { + nsh_reset(); + CHECK(netstack_startup_loopback() == AMI_NET_OK, + "the loopback stack starts for the ACK policy case"); + + memset(&cfg, 0, sizeof(cfg)); + strcpy(cfg.name, "testif"); + strcpy(cfg.device, cases[i].device); + cfg.iptype = AMI_IPTYPE_STATIC; + cfg.address = 0xC0A80132UL; + cfg.netmask = 0xFFFFFF00UL; + cfg.up = TRUE; + cfg.configured = TRUE; + cfg.tcp_ack_max = cases[i].requested; + index = 99; + + CHECK(netstack_interface_start(&cfg, &index) == AMI_NET_OK && index == 0, + "the selected interface attaches in slot zero"); + if (index == 0) + CHECK(netstack_get()->ns_Ip.nx_ip_interface[index] + .nx_interface_tcp_ack_threshold_max == cases[i].expected, + "the attached interface carries its own ACK ceiling"); + + h_down(); + } +} + int main(void) { t_startup_installs(); t_late_address_and_operator_delete(); t_loopback_then_specific_route(); + t_interface_tcp_ack_policy(); printf("\n%lu checks, %lu failure(s)\n", h_checks, h_failures); return h_failures == 0 ? 0 : 1; diff --git a/tests/sana2/CMakeLists.txt b/tests/sana2/CMakeLists.txt index e80054a57..43b06e6b9 100644 --- a/tests/sana2/CMakeLists.txt +++ b/tests/sana2/CMakeLists.txt @@ -249,6 +249,7 @@ add_test(NAME sana2_driver COMMAND test_sana2_driver) 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") target_compile_options(test_sana2_device PRIVATE diff --git a/tests/sana2/host/test_sana2_device_host.c b/tests/sana2/host/test_sana2_device_host.c index 1e196ad5a..6b898b4ea 100644 --- a/tests/sana2/host/test_sana2_device_host.c +++ b/tests/sana2/host/test_sana2_device_host.c @@ -1810,6 +1810,14 @@ static void case_bursty_wifi_reads(void) h_check(ami_sana2_default_ip_reads(NULL) == 0, "an unnamed driver keeps the ordinary BPS ladder"); + h_check(ami_sana2_default_tcp_ack_max("DEVS:Networks/anxwifipi.device") == 11680UL, + "the shipped WiFiPi driver gets the measured ACK ceiling"); + h_check(ami_sana2_default_tcp_ack_max("WIFIPI.DEVICE") == 11680UL, + "the upstream WiFiPi name gets the same ceiling"); + h_check(ami_sana2_default_tcp_ack_max("genet.device") == 0 && + ami_sana2_default_tcp_ack_max(NULL) == 0, + "wired and unnamed drivers keep the port ACK ceiling"); + /* The DEVICE= lines Install-AmiNetXDuo writes for Wi-Fi, system layout and self-contained drawer: neither carries IPREQUESTS. */ h_check(ami_sana2_default_ip_reads("DEVS:Networks/anxwifipi.device") == 128 && diff --git a/third_party/netxduo b/third_party/netxduo index c4d40bdf9..02a6f37e3 160000 --- a/third_party/netxduo +++ b/third_party/netxduo @@ -1 +1 @@ -Subproject commit c4d40bdf964ed12be4d3b6907dbf9314aa20818e +Subproject commit 02a6f37e3d52ab957aea4ccda2bb5fbcf875203d diff --git a/tools/check-image-size.sh b/tools/check-image-size.sh index 363566d0f..2ab303f83 100755 --- a/tools/check-image-size.sh +++ b/tools/check-image-size.sh @@ -71,7 +71,10 @@ BUDGETS=( # 364,664 with the bounded S2_OFFLINE: timer-bounded reply wait, abort, # and an abandoned request kept with its interface until the device # replies (#90, 5473e26e), 2026-09-26 (micro 204,052, minimal 238,240). - "default:src/bsdsocket/bsdsocket.library:365000" + # 365,468 with per-interface TCP ACK policy (#109): config parsing and + # publishing the WiFi/default-or-explicit ceiling into each NX_INTERFACE. + # This adds 468 bytes over the previous resident-image budget. + "default:src/bsdsocket/bsdsocket.library:366000" # 41,412 after stateless receive-checksum verification was added to the # EL3 and word/long NE2000 direct paths, 2026-09-15. 43,620 with # ANXD_CMD_RX_BATCH in the shell (claim, completion, staging copy, the