Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ version at the top when it merges.

## Unreleased

- The micro stack keeps no list of the names it has resolved: every
`gethostbyname`/`gethostbyaddr` asks the name server again. It also neither
splits nor joins an IPv4 packet, so a datagram larger than the link MTU is
refused with `EMSGSIZE` instead of being fragmented -- from `sendto` and
from a raw socket -- and a fragment that arrives is dropped. TCP is
unaffected in both directions. The two come to 5,184 bytes of
`bsdsocket.library` and 2 KB of the stack's resident memory.
- `bsdsocket.library` is 3,228 bytes smaller in the minimal drawer, whose
eight hottest NetX Duo translation units no longer build at `-O2` after the
`-Os` the rest of the tree uses.
- `WaitSelect` re-arms a large timeout as the true remainder instead of a
wrapped 32-bit value. A wait accepted for more than ~71 minutes, when a kept
timer request fired early and the call had to re-arm the balance, multiplied
Expand Down
102 changes: 101 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,42 @@ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
message(STATUS "No CMAKE_BUILD_TYPE given; using Release")
endif()

# THE REST OF THE FLAG SPACE, swept 2026-09-30 against the micro drawer in its
# shipped shape (201,672 bytes at 994aa0ed with the preset's DNS-cache and
# fragmentation options off). Every number is one flag apart from that build
# on the pinned toolchain, m68k-amigaos-gcc 16.2.0b, and each flag was read
# back out of compile_commands.json rather than out of the CMake cache:
#
# -Oz 201,672, exactly nothing. Byte-identical output.
# -msmall-code 202,472, 800 bytes WORSE.
# -fbaserel does not link. cmake/check-pcrel-branches.cmake
# refuses anxwifipi.device: a 32-bit PC-relative branch
# lands inside a function (docs/RESEARCH.md 25).
# A library has no a4 on entry either.
# -mregparm 189,872, 11,800 bytes smaller, and unusable as a flag:
# it passes arguments in registers for EVERY call,
# including calls out of this code to Exec, DOS and
# SANA-II devices, which take them on the stack. The
# disassembly of `extern void ext(int,int); ext(1,2);`
# is moveq #2,d1 / moveq #1,d0 / jmp ext under it and
# pea 2 / pea 1 / jsr ext / addq.l #8,sp without.
# __attribute__((regparm(N))) does exist and is selective
# -- an annotated function takes d0/d1 while an
# unannotated one beside it still reads 4(sp) -- so a
# chosen set of functions can take it without moving the
# ABI of anything they call. That is a code change and a
# measurement, not a flag.
# -fomit-frame-pointer already stated in CMAKE_C_FLAGS_INIT, and -Os implies
# it: removing it from the command line gives 201,672,
# the same bytes to the byte.
#
# Floating point was checked the same day and there is none: the shipped
# disassembly contains no FPU instruction (the same scan finds 16,594 integer
# ones), no libgcc float member is pulled into the link (the map's archive
# members), and the only float or double in the tree that builds into an Amiga
# image is none -- the hosts in src/rfb/host are not images, and the rest of
# the hits are the word in comments.

# ---------------------------------------------------------------- options ---

option(AMINETXDUO_IPV6 "Build the IPv6 dual stack" ON)
Expand Down Expand Up @@ -431,6 +467,23 @@ set(AMINETXDUO_DNS_PACKETS "4" CACHE STRING
add_compile_definitions(
"NX_DNS_PACKET_POOL_SIZE=(${AMINETXDUO_DNS_PACKETS}*(NX_DNS_PACKET_PAYLOAD+sizeof(NX_PACKET)))")

# Keep the answers the resolver has already been given, for their TTL.
#
# NX_DNS_CACHE_ENABLE is what compiles the lookup table in, and
# nx_dns_cache_initialize() is what arms it -- without that call the code is
# inert, which is why src/netstack/netstack_dns.c guards the call on the same
# macro. OFF in the micro drawer: 3,236 bytes of the shipped bsdsocket.library
# (206,856 -> 203,620, micro's preset shape, one flag apart, 2026-09-30), and
# 3,224 of minimal's (238,060 -> 234,836, HOT_O2 already off in both). It
# also takes the 2 KB cache buffer out of AmiNetStack, which is RAM the machine
# keeps for as long as the stack is up. The price is a name-server round trip
# per lookup instead of per TTL, which a micro machine on a slow link pays on
# every gethostbyname, so it is a size decision and not a free one.
option(AMINETXDUO_DNS_CACHE "Keep resolved names for their TTL" ON)
if(NOT AMINETXDUO_DNS_CACHE)
add_compile_definitions(AMINETXDUO_DNS_CACHE_OFF)
endif()

# Half-open connections held with their options recorded exactly.
#
# NetX Duo's own number is 512, chosen against the four megabytes this project
Expand Down Expand Up @@ -843,6 +896,40 @@ else()
add_compile_definitions(NX_DISABLE_IGMP)
endif()

# IPv4 fragmentation and reassembly, both directions.
#
# NX_DISABLE_FRAGMENTATION removes the transmit splitter and the receive
# reassembler together, because NetX Duo has no receive-only arm. That is
# 1,892 bytes of the shipped bsdsocket.library in micro's shape (206,856 ->
# 204,964), 2,024 in minimal's (241,288 -> 236,036, which is 3,228 less again
# for HOT_O2) and 3,560 in the full drawer's (368,828 -> 365,268), each one flag
# apart in the same tree on 2026-09-30. The three are net of the shim's
# refusal below, which the micro figure includes. What it costs is two
# different things:
#
# o a datagram larger than the interface MTU can no longer be SENT; sendto
# and the raw send both report EMSGSIZE where they used to split. That
# refusal is the shim's: NetX releases an oversize packet without an
# error once there is no fragmenter to take it, so a sender would
# otherwise be told the bytes had gone. bsd_nofrag_maxdgram()
# (src/bsdsocket/transfer.c) is that refusal, and it is why the size
# below is the option's cost net of a few dozen bytes of shim.
# o one that arrives fragmented is counted and released rather than
# reassembled.
#
# TCP is unaffected in both directions: the segment size is derived from the
# MTU, so no segment this stack sends is ever split, and a peer's segments are
# that peer's MSS. What breaks is a large UDP payload -- an 8 KB NFS read, a
# jumbo reply -- which is why this is off in micro and on everywhere else.
#
# nx_ip_fragment_enable() answers NX_NOT_ENABLED instead of failing when this
# is off, so src/netstack/netstack.c skips the call and its warning rather than
# logging a failure at every stack start.
option(AMINETXDUO_IP_FRAGMENTATION "Fragment and reassemble IPv4 packets" ON)
if(NOT AMINETXDUO_IP_FRAGMENTATION)
add_compile_definitions(NX_DISABLE_FRAGMENTATION)
endif()

option(AMINETXDUO_TESTS "Build the on-Amiga test harnesses" ON)

# ASan and UBSan over the host test tier. The target has no MMU and no
Expand Down Expand Up @@ -1550,7 +1637,20 @@ target_include_directories(threadx PUBLIC
# and nx_tcp_socket_packet_process does exactly that once per received packet.
#
# AMINETXDUO_HOT_O2=OFF puts them back at -Os, which is the comparison to make
# when deciding whether the size is worth it.
# when deciding whether the size is worth it. Measured 2026-09-30, one flag
# apart in the same tree: 3,228 bytes of bsdsocket.library in minimal's shipped
# shape (241,288 -> 238,060) and 3,456 in the full drawer's (368,828 ->
# 365,372).
#
# MICRO AND MINIMAL BOTH TAKE IT OFF; the full drawer keeps it. Off in micro
# since micro existed. Off in minimal since 2026-09-30, where it pays that
# drawer's whole overshoot by itself -- a size decision taken without a
# throughput A/B, which this tree does not have for the option: what the hot
# list buys is the inlining above, and nothing here measures it. A minimal
# build that wants the old behaviour back gets it with
# -DAMINETXDUO_HOT_O2=ON, and one `tools/ci.sh rate` run on an A600 or a 68020
# would turn the trade into a measurement. The full drawer, which is where the
# option was aimed, keeps it either way.
# ---------------------------------------------------------------------------
option(AMINETXDUO_HOT_O2 "Build the profiled hot paths at -O2" ON)

Expand Down
12 changes: 12 additions & 0 deletions CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
"type": "BOOL",
"value": "OFF"
},
"AMINETXDUO_HOT_O2": {
"type": "BOOL",
"value": "OFF"
},
"AMINETXDUO_IPV6": {
"type": "BOOL",
"value": "OFF"
Expand Down Expand Up @@ -88,10 +92,18 @@
"value": "OFF"
},
"AMINETXDUO_CPU": "68000",
"AMINETXDUO_DNS_CACHE": {
"type": "BOOL",
"value": "OFF"
},
"AMINETXDUO_HOT_O2": {
"type": "BOOL",
"value": "OFF"
},
"AMINETXDUO_IP_FRAGMENTATION": {
"type": "BOOL",
"value": "OFF"
},
"AMINETXDUO_IPV6": {
"type": "BOOL",
"value": "OFF"
Expand Down
2 changes: 1 addition & 1 deletion docs/RESEARCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ is in git history, `git show 2b54025b:docs/RESEARCH.md`.
| 55 | `AddDomainNameServer()` as ENOSYS made Roadshow's `AddNetInterface` return rc 20 after doing everything right. A server must land in both the NetX Duo DNS client and `ns_Config.resolver`. | `src/bsdsocket/roadshow.c:165` | current |
| 55 | `IP_HDRINCL` is translated, not passed through: TOS, TTL, protocol and destination are mapped onto `nxd_ip_raw_packet_send()` and the caller's header stripped. | `src/bsdsocket/raw.c:635` | current |
| 55 | ARP entries must be emitted as `RTF_LLINFO` routes or Roadshow's `arp` prints nothing. | `src/bsdsocket/routing.c:464` | see code |
| 57 | `-Os` tree-wide, -24% size at no throughput cost, and it must be set after `project()` or `Compiler/GNU` appends `-O3`. | `CMakeLists.txt:43` | superseded in part, eight profiled NetX Duo files build at `-O2` via `AMINETXDUO_HOT_O2` |
| 57 | `-Os` tree-wide, -24% size at no throughput cost, and it must be set after `project()` or `Compiler/GNU` appends `-O3`. | `CMakeLists.txt:43` | superseded in part, eight profiled NetX Duo files build at `-O2` via `AMINETXDUO_HOT_O2`; that is now the default drawer only, minimal and micro set it OFF (2026-09-30, 3,228 bytes in minimal, 3,456 in the full drawer) |
| 58 | Sixteen ChaCha20 words into 8 data plus 7 address registers and one stack slot, moved with `EXG` at 4.01 cycles against 13.80 for a stack spill. | `src/crypto68k/c68k_chacha20.S` | current |
| 63 | A frame the emulator's host sends to the guest's MAC never returns to that NIC's pcap, so the peer in any bridged test must be a third machine. | `tests/perf/run-fitzbench.sh:118` | see code |
| 63.4 | A crashed WinUAE 6.0.3 on a bridged run is an oversized coalesced receive frame in `gotfunc2()`, not our stack. A dead emulator must report `reason=crash`, not rc 0. | `tools/winuae-run.sh:615` | current |
Expand Down
16 changes: 15 additions & 1 deletion docs/user/AmiNetXDuo.guide
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,13 @@
micro stack also leaves out getaddrinfo/getnameinfo, ancillary data,
out-of-band TCP and less-used administration calls, carries one interface
and no route or address-allocation calls, and holds 243 KB;
it is the one for a 1 MB machine. DHCP, the classic resolver and raw sockets
it is the one for a 1 MB machine. It also keeps no list of the names it
has already resolved, so every lookup asks the name server again, and it
neither splits nor joins an IP packet: a datagram larger than the link's
MTU is refused with EMSGSIZE instead of being fragmented, and one that
arrives fragmented is dropped. TCP is unaffected in both directions,
because a segment's size comes from the MTU and never needs splitting.
DHCP, the classic resolver and raw sockets
are in every stack: a minimal or micro machine still leases its address and
can run ping or traceroute.

Expand Down Expand Up @@ -2811,6 +2817,14 @@
classic resolver and raw sockets; route, address-allocation and
less-used public administration calls are omitted there. Its private
status/control calls remain for the supplied interface tools.
o @{b}The micro stack caches no name and fragments no packet.@{ub}
Every gethostbyname() and gethostbyaddr() goes to the name server, so
a name is looked up again on each call. A datagram larger than the
link MTU is refused with EMSGSIZE rather than split, both from sendto()
and from a raw socket, and an inbound fragment is dropped rather than
reassembled -- so a peer's large UDP reply, an 8 KB NFS-over-UDP read
among them, does not work there. TCP is unaffected in both directions:
its segment size follows the MTU and never needs splitting.
o bind() to a particular local address is enforced for TCP listeners and
UDP delivery. An address this machine does not have is refused with
EADDRNOTAVAIL.
Expand Down
7 changes: 6 additions & 1 deletion port/netxduo-amiga/inc/nx_user.h
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,13 @@ ULONG _nx_amiga_handshake_millis(VOID);
/* -------------------------------------------------------------- resolver, */

/* Inert until nx_dns_cache_initialize() is called; the cache buffer belongs to
the caller (src/netstack/netstack_dns.c). */
the caller (src/netstack/netstack_dns.c). AMINETXDUO_DNS_CACHE=OFF leaves the
macro undefined, which drops the table and its lookup code from nx_dns.c;
netstack_dns.c guards its call on the same macro, so nothing else moves. The
micro drawer takes that, at a name-server round trip per lookup. */
#ifndef AMINETXDUO_DNS_CACHE_OFF
#define NX_DNS_CACHE_ENABLE
#endif

/* wait_option is a PER-QUERY timeout spent this many times over every
configured server with the DNS mutex held. The retransmission ladder lives
Expand Down
27 changes: 27 additions & 0 deletions src/bsdsocket/bsdsocket_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,33 @@ BOOL bsd_udp_accepts_received_packet(const AmiSocket *sock,
const NX_PACKET *packet);
ULONG bsd_udp_available(const AmiSocket *sock);

#ifdef NX_DISABLE_FRAGMENTATION
/* The largest payload this stack can put on the wire with `overhead' bytes of
* headers above it, for a build with no fragmenter: the smallest attached link
* MTU, less those headers. transfer.c owns it; raw.c asks it too, because both
* senders used to hand NetX a datagram it would have split.
*
* Above that size NetX releases the packet without an error --
* nx_ip_driver_packet_send.c takes the else branch of the fragmenter's NULL
* pointer test and returns -- so nx_udp_socket_send() reports success and the
* bytes never leave. A sender has to be refused here or it is lied to.
*
* The egress interface is not this layer's to know: NetX picks it from the
* route for the destination, and asking costs a route lookup and the IP mutex
* on every send. The smallest MTU is a read over at most NX_MAX_PHYSICAL_
* INTERFACES words instead, and it is exact for a stack with one interface --
* the micro drawer, AMINETXDUO_MAX_INTERFACES=1, the only shipping build that
* compiles the fragmenter out. With more than one interface, a machine whose
* links differ in MTU is refused a datagram that would have fit the egress
* one: wrong in the safe direction, and never silent.
*
* -1 means not even the headers fit. A link that has no MTU yet is skipped
* rather than counted, and a stack where none has one keeps the 65,535 cap:
* no link has an address then either, so the send is about to answer
* unreachable and EMSGSIZE would be the wrong answer to give it. */
LONG bsd_nofrag_maxdgram(NX_IP *ip, ULONG overhead);
#endif

#ifdef AMINETXDUO_RX_DIRECT_COMPLETE
/* as_RxDState. IDLE is zero so a MEMF_CLEAR socket starts unarmed. */
#define BSD_RXD_IDLE 0
Expand Down
10 changes: 9 additions & 1 deletion src/bsdsocket/raw.c
Original file line number Diff line number Diff line change
Expand Up @@ -789,12 +789,20 @@ LONG bsd_raw_send_packet(struct AmiSocketBase *base, AmiSocket *sock,
/* 65,535 less the IP header is what BSD lets a raw sender hand over;
the rest is fragmented, here as there (nxd_ip_raw_packet_source_send()
passes NX_FRAGMENT_OKAY). The cap used to be the link MTU, and
`ping -s 1473' was refused, never sent -- see bsd_udp_maxdgram(). */
`ping -s 1473' was refused, never sent -- see bsd_udp_maxdgram().
It is the link MTU again where there is no fragmenter to hand the
datagram to; NetX would drop it without saying so, and `ping -s 2000'
would be told it had been sent. */
{
ULONG overhead = (dest.nxd_ip_version == NX_IP_VERSION_V6)
? 40UL : 20UL;
#ifdef NX_DISABLE_FRAGMENTATION
LONG maxraw = bsd_nofrag_maxdgram(ip, overhead);

if (maxraw < 0 || (LONG)handed->nx_packet_length > maxraw)
#else
if (handed->nx_packet_length + overhead > 65535UL)
#endif
{
nx_packet_release(handed);
return bsd_fail(base, AMI_EMSGSIZE);
Expand Down
46 changes: 44 additions & 2 deletions src/bsdsocket/transfer.c
Original file line number Diff line number Diff line change
Expand Up @@ -875,8 +875,13 @@ static LONG bsd_send_tcp(struct AmiSocketBase *base, AmiSocket *sock,
* the last place that kept a datagram from reaching it. Measured 2026-09-19
* on the A1200 through a 1,400-byte hop: -s 1472 crossed (the router
* fragmented it), -s 1473 and up were refused here, never sent.
*
* A build with no fragmenter (AMINETXDUO_IP_FRAGMENTATION=OFF, the micro
* drawer) has nothing to reach, and NetX would take the datagram apart
* silently instead of refusing it, so the cap goes back to the link MTU:
* bsd_nofrag_maxdgram().
*/
static LONG bsd_udp_maxdgram(const NXD_ADDRESS *addr)
static LONG bsd_udp_maxdgram(NX_IP *ip, const NXD_ADDRESS *addr)
{
ULONG overhead;

Expand All @@ -890,8 +895,45 @@ static LONG bsd_udp_maxdgram(const NXD_ADDRESS *addr)
overhead = (ULONG)NX_IPv4_UDP_PACKET - (ULONG)NX_PHYSICAL_HEADER;
}

#ifdef NX_DISABLE_FRAGMENTATION
return bsd_nofrag_maxdgram(ip, overhead);
#else
(VOID)ip;
return (LONG)(65535UL - overhead);
#endif
}

#ifdef NX_DISABLE_FRAGMENTATION
/* See the declaration in bsdsocket_internal.h for what this answers and why
the smallest MTU is the one it answers with. */
LONG bsd_nofrag_maxdgram(NX_IP *ip, ULONG overhead)
{
ULONG smallest = 0UL;
UINT i;

if (ip == NULL)
return (LONG)(65535UL - overhead);

for (i = 0; i < (UINT)NX_MAX_PHYSICAL_INTERFACES; i++)
{
ULONG mtu = ip->nx_ip_interface[i].nx_interface_ip_mtu_size;

if (ip->nx_ip_interface[i].nx_interface_valid == 0 || mtu == 0UL)
continue;

if (smallest == 0UL || mtu < smallest)
smallest = mtu;
}

if (smallest == 0UL)
return (LONG)(65535UL - overhead);

if (smallest <= overhead)
return -1;

return (LONG)(smallest - overhead);
}
#endif

/*
* The interface a received datagram arrived on.
Expand Down Expand Up @@ -1178,7 +1220,7 @@ static LONG bsd_send_udp(struct AmiSocketBase *base, AmiSocket *sock,
return bsd_fail(base, AMI_ENETUNREACH);
}

maxdgram = bsd_udp_maxdgram(addr);
maxdgram = bsd_udp_maxdgram(ip, addr);
if (len > maxdgram)
return bsd_fail(base, AMI_EMSGSIZE);

Expand Down
Loading
Loading