AdapterCaps reports chains, bandwidths, bands, per-packet TX power, narrowband and fast-retune — but nothing about how large a frame the adapter will accept or return. IRtlDevice::send_packet (src/IRtlDevice.h:214) declares no length contract either, and TxCaps has no length field. A consumer sizing frames has to guess, and on at least one backend guessing wrong fails silently.
This is a cross-backend question, which is why it is an issue rather than a change: no single backend can answer it, and a wrong shared default would be worse than the current absence.
The limit exists, differs per transport, and is announced nowhere
PCIe already enforces a hard ceiling. TX_BOUNCE_SZ = 32 * 1024 (src/PcieTransport.cpp:85) and if (len < TX_PKT_DESC_SZ || len > r.bounce_len) return false; (:569). Correct, and invisible to a caller.
The Realtek USB backends have no fixed ceiling of their own. RtlJaguarDevice::send_packet builds std::vector<uint8_t> usb_frame(TXDESC_SIZE + (length - rlen), 0) (src/jaguar1/RtlJaguarDevice.cpp:752) — sized to the frame, so whatever the chip and firmware accept is what happens. That is a reasonable design, but it means the real limit is undocumented and, as far as I can tell, unmeasured. I have not measured it and am not claiming a number.
MT7612U (#412) is the one I do have numbers for, and it is the case that makes the argument:
- TX is not the constraint. Against an RTL8812AU witness the part transmitted every size tested up to 7900 bytes, 60/60 at each, zero CRC errors — far past 802.11's 2304-byte non-A-MSDU MPDU ceiling. The backend's own 2048-byte buffer was a constant nobody had checked.
- RX is the constraint, and it is the MAC's.
MT_MAX_LEN_CFG (0x1018) carries the maximum on-air length in its low 12 bits including the 4-byte FCS. At the 0xf00 that driver programs, that is 3840 on air, so 3836 bytes of MPDU. Measured to the byte: 3836 arrives, 3837 does not.
- Above that, the loss is invisible by construction. The MAC discards the frame before it reaches USB: no transfer completes,
rx_err stays 0, and a drop counter added specifically to catch it does not move either. That was verified rather than assumed — TX ceiling temporarily raised, 60 × 6000-byte frames sent MT7612U-to-MT7612U, 0 received, every counter zero.
Caveats on those numbers, since they are one bench: one witness generation (a single RTL8812AU), and the 7900 figure is "at least", not a located ceiling — I stopped at the buffer I had, not at a refusal.
Until recently that backend also had two disagreeing ceilings behind one public API — send_packet refused above 2016 bytes while send_packets bounded only against its 16 KB aggregate buffer, so the same frame was refused by one entry point and aired by the other. That is fixed, but it is the shape of bug the absence of a declared limit invites.
Why a consumer cares
Anything choosing a frame size against a link budget — video payloads, an ARQ block size, A-MSDU or A-MPDU dimensioning — is trading per-frame overhead against loss probability. Today that choice is made blind, and portable code has to assume the smallest limit anyone might have. On MT7612U the penalty for guessing high is not an error return; it is a receiver that appears to work while dropping the large frames.
Proposed shape
Two uint16_t in AdapterCaps, alongside the existing bw_mask:
uint16_t max_mpdu_tx; /* largest MPDU this backend will submit, excl. FCS */
uint16_t max_mpdu_rx; /* largest MPDU it will hand back, excl. FCS */
Separate, because on MT7612U they genuinely differ (4064 vs 3836) and collapsing them to one number would either understate TX or overstate RX.
The trap to avoid: do not give this a plausible default. bw_mask_for_generation() (src/AdapterCaps.h:80) can have a sensible per-generation default because the widths are known for every family; frame limits are not. A default of 2304 or 4095 would be wrong somewhere and would read as authoritative — a gauge with no reference behind it. 0 should mean "not established on this backend", and a consumer should treat 0 as "assume nothing", not as "zero".
That also makes the field honest to land incrementally: MT7612U can fill it now, and each Realtek family fills it when someone measures or finds the register.
What each backend needs
| backend |
TX |
RX |
how |
| MT7612U |
4064 |
3836 |
done — RX read from MT_MAX_LEN_CFG at runtime, TX is the backend's buffer |
| Jaguar1/2/3, Kestrel, RTL8733B |
unknown |
unknown |
is there a MAX_LEN-equivalent register in the vendor trees, or does it need a size sweep against a witness? |
| PCIe transport |
32 KB − descriptor |
n/a |
already a constant, just needs surfacing |
The Realtek row is the real work and the reason this is an issue: it is a register hunt across four vendor trees plus a bench sweep, not something to bolt onto a backend PR.
Suggested order
Land the two fields with 0 = unknown and no behaviour change → fill MT7612U (already measured) and the PCIe constant → sweep one Realtek family against a witness to establish the method → apply it to the rest. A bringup mtu-style size sweep already exists in #412 and generalises to any backend that can inject.
Happy to do any part of this; raising it here first because the field is shared and the default question is the maintainer's call, not a backend's.
AdapterCapsreports chains, bandwidths, bands, per-packet TX power, narrowband and fast-retune — but nothing about how large a frame the adapter will accept or return.IRtlDevice::send_packet(src/IRtlDevice.h:214) declares no length contract either, andTxCapshas no length field. A consumer sizing frames has to guess, and on at least one backend guessing wrong fails silently.This is a cross-backend question, which is why it is an issue rather than a change: no single backend can answer it, and a wrong shared default would be worse than the current absence.
The limit exists, differs per transport, and is announced nowhere
PCIe already enforces a hard ceiling.
TX_BOUNCE_SZ = 32 * 1024(src/PcieTransport.cpp:85) andif (len < TX_PKT_DESC_SZ || len > r.bounce_len) return false;(:569). Correct, and invisible to a caller.The Realtek USB backends have no fixed ceiling of their own.
RtlJaguarDevice::send_packetbuildsstd::vector<uint8_t> usb_frame(TXDESC_SIZE + (length - rlen), 0)(src/jaguar1/RtlJaguarDevice.cpp:752) — sized to the frame, so whatever the chip and firmware accept is what happens. That is a reasonable design, but it means the real limit is undocumented and, as far as I can tell, unmeasured. I have not measured it and am not claiming a number.MT7612U (#412) is the one I do have numbers for, and it is the case that makes the argument:
MT_MAX_LEN_CFG(0x1018) carries the maximum on-air length in its low 12 bits including the 4-byte FCS. At the0xf00that driver programs, that is 3840 on air, so 3836 bytes of MPDU. Measured to the byte: 3836 arrives, 3837 does not.rx_errstays 0, and a drop counter added specifically to catch it does not move either. That was verified rather than assumed — TX ceiling temporarily raised, 60 × 6000-byte frames sent MT7612U-to-MT7612U, 0 received, every counter zero.Caveats on those numbers, since they are one bench: one witness generation (a single RTL8812AU), and the 7900 figure is "at least", not a located ceiling — I stopped at the buffer I had, not at a refusal.
Until recently that backend also had two disagreeing ceilings behind one public API —
send_packetrefused above 2016 bytes whilesend_packetsbounded only against its 16 KB aggregate buffer, so the same frame was refused by one entry point and aired by the other. That is fixed, but it is the shape of bug the absence of a declared limit invites.Why a consumer cares
Anything choosing a frame size against a link budget — video payloads, an ARQ block size, A-MSDU or A-MPDU dimensioning — is trading per-frame overhead against loss probability. Today that choice is made blind, and portable code has to assume the smallest limit anyone might have. On MT7612U the penalty for guessing high is not an error return; it is a receiver that appears to work while dropping the large frames.
Proposed shape
Two
uint16_tinAdapterCaps, alongside the existingbw_mask:Separate, because on MT7612U they genuinely differ (4064 vs 3836) and collapsing them to one number would either understate TX or overstate RX.
The trap to avoid: do not give this a plausible default.
bw_mask_for_generation()(src/AdapterCaps.h:80) can have a sensible per-generation default because the widths are known for every family; frame limits are not. A default of 2304 or 4095 would be wrong somewhere and would read as authoritative — a gauge with no reference behind it.0should mean "not established on this backend", and a consumer should treat 0 as "assume nothing", not as "zero".That also makes the field honest to land incrementally: MT7612U can fill it now, and each Realtek family fills it when someone measures or finds the register.
What each backend needs
MT_MAX_LEN_CFGat runtime, TX is the backend's bufferMAX_LEN-equivalent register in the vendor trees, or does it need a size sweep against a witness?The Realtek row is the real work and the reason this is an issue: it is a register hunt across four vendor trees plus a bench sweep, not something to bolt onto a backend PR.
Suggested order
Land the two fields with
0 = unknownand no behaviour change → fill MT7612U (already measured) and the PCIe constant → sweep one Realtek family against a witness to establish the method → apply it to the rest. Abringup mtu-style size sweep already exists in #412 and generalises to any backend that can inject.Happy to do any part of this; raising it here first because the field is shared and the default question is the maintainer's call, not a backend's.