qualcommax: qca_edma: add NETIF_F_FRAGLIST support

A GRO aggregate that outgrows MAX_SKB_FRAGS keeps its further segments
on a frag_list, and without NETIF_F_FRAGLIST the stack segments it
before it reaches the conduit. Write the heads and fragments of the
frag_list skbs to descriptors like any other fragment. An aggregate
that needs more than 32 descriptors is segmented in software by
ndo_features_check rather than linearised.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Julius Bairaktaris <julius@bairaktaris.de>
Link: https://github.com/openwrt/openwrt/pull/25700
Signed-off-by: Robert Marko <robimarko@gmail.com>
This commit is contained in:
Julius Bairaktaris authored and Robert Marko committed 2026-10-18 12:50:08 +02:00
1 parent 812890612e
commit 89ac01dc9b
1 file changed
+95 -24
@@ -773,6 +773,41 @@ static u32 edma_tx_tso(struct sk_buff *skb, struct edma_tx_preheader *txph)
return EDMA_TXDESC_TSO_EN;
}
/* A frame is described by its head, its fragments, then the head and
* fragments of every skb on its frag_list.
*/
static u16 edma_tx_ndesc(const struct sk_buff *skb)
{
const struct sk_buff *seg;
u16 n = skb_shinfo(skb)->nr_frags + 1;
skb_walk_frags(skb, seg)
n += !!skb_headlen(seg) + skb_shinfo(seg)->nr_frags;
return n;
}
static bool edma_tx_map(struct device *dev, struct edma_ring *ring,
struct sk_buff *skb, u32 idx, u16 *n, u16 ndesc,
struct page *page, u32 off, u32 len, u32 tso)
{
u32 fidx = (idx + *n) & (ring->count - 1);
struct edma_txdesc *txdesc;
dma_addr_t dma;
dma = dma_map_page(dev, page, off, len, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma))
return false;
ring->skb_store[fidx] = skb;
txdesc = EDMA_TXDESC_DESC(ring, fidx);
txdesc->buffer_addr = cpu_to_le32(dma);
txdesc->word1 = tso | (++*n < ndesc ? EDMA_TXDESC_MORE : 0) |
(len & EDMA_TXDESC_DATA_LENGTH_MASK);
return true;
}
static netdev_tx_t edma_ring_xmit(struct edma_queue *q, struct net_device *netdev,
struct sk_buff *skb)
{
@@ -784,10 +819,11 @@ static netdev_tx_t edma_ring_xmit(struct edma_queue *q, struct net_device *netde
u16 mask = txdesc_ring->count - 1;
struct edma_tx_preheader *txph;
struct dsa_oob_tag_info *tag_info;
u16 ndesc = shinfo->nr_frags + 1;
u16 ndesc = edma_tx_ndesc(skb);
struct edma_txdesc *txdesc;
u16 prod, cons, dst_info;
u16 prod, cons, dst_info, n;
u32 val, idx, i, len, bytes, tso;
struct sk_buff *seg;
dma_addr_t head_dma;
bool taken = false;
__be16 proto;
@@ -860,23 +896,29 @@ static netdev_tx_t edma_ring_xmit(struct edma_queue *q, struct net_device *netde
<< EDMA_TXDESC_DATA_OFFSET_SHIFT) |
(len & EDMA_TXDESC_DATA_LENGTH_MASK);
for (i = 0; i < shinfo->nr_frags; i++) {
const skb_frag_t *frag = &shinfo->frags[i];
u32 fidx = (idx + 1 + i) & mask;
dma_addr_t dma;
n = 1;
seg = skb;
do {
const struct skb_shared_info *si = skb_shinfo(seg);
len = skb_frag_size(frag);
dma = skb_frag_dma_map(dev, frag, 0, len, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma))
if (seg != skb && skb_headlen(seg) &&
!edma_tx_map(dev, txdesc_ring, skb, idx, &n, ndesc,
virt_to_page(seg->data),
offset_in_page(seg->data), skb_headlen(seg),
tso))
goto unmap;
txdesc_ring->skb_store[fidx] = skb;
txdesc = EDMA_TXDESC_DESC(txdesc_ring, fidx);
txdesc->buffer_addr = cpu_to_le32(dma);
txdesc->word1 = tso | (i + 1 < shinfo->nr_frags ?
EDMA_TXDESC_MORE : 0) |
(len & EDMA_TXDESC_DATA_LENGTH_MASK);
}
for (i = 0; i < si->nr_frags; i++) {
const skb_frag_t *frag = &si->frags[i];
if (!edma_tx_map(dev, txdesc_ring, skb, idx, &n, ndesc,
skb_frag_page(frag), skb_frag_off(frag),
skb_frag_size(frag), tso))
goto unmap;
}
seg = seg == skb ? shinfo->frag_list : seg->next;
} while (seg);
prod = (prod + ndesc) & mask;
@@ -904,8 +946,8 @@ static netdev_tx_t edma_ring_xmit(struct edma_queue *q, struct net_device *netde
return NETDEV_TX_OK;
unmap:
while (i--) {
u32 fidx = (idx + 1 + i) & mask;
while (--n) {
u32 fidx = (idx + n) & mask;
txdesc = EDMA_TXDESC_DESC(txdesc_ring, fidx);
dma_unmap_page(dev, le32_to_cpu(txdesc->buffer_addr),
@@ -1698,19 +1740,47 @@ static int edma_ndo_change_mtu(struct net_device *netdev, int new_mtu);
*/
static bool edma_tx_needs_linearize(const struct sk_buff *skb)
{
const struct skb_shared_info *shinfo = skb_shinfo(skb);
const struct sk_buff *seg = skb;
bool short_prev = false;
int i;
if (shinfo->nr_frags + 1 > EDMA_TX_MAX_SEGS)
if (edma_tx_ndesc(skb) > EDMA_TX_MAX_SEGS)
return true;
for (i = 0; i + 1 < shinfo->nr_frags; i++)
if (skb_frag_size(&shinfo->frags[i]) < EDMA_TX_MIN_SEG)
return true;
do {
const struct skb_shared_info *si = skb_shinfo(seg);
if (seg != skb && skb_headlen(seg)) {
if (short_prev)
return true;
short_prev = skb_headlen(seg) < EDMA_TX_MIN_SEG;
}
for (i = 0; i < si->nr_frags; i++) {
if (short_prev)
return true;
short_prev = skb_frag_size(&si->frags[i]) < EDMA_TX_MIN_SEG;
}
seg = seg == skb ? si->frag_list : seg->next;
} while (seg);
return false;
}
/* A GRO aggregate on its frag_list can need more descriptors than a frame
* may take; it is segmented in software rather than made linear.
*/
static netdev_features_t edma_ndo_features_check(struct sk_buff *skb,
struct net_device *netdev,
netdev_features_t features)
{
if (skb_is_gso(skb) && edma_tx_needs_linearize(skb))
features &= ~NETIF_F_GSO_MASK;
return vlan_features_check(skb, features);
}
static netdev_tx_t edma_ndo_xmit(struct sk_buff *skb, struct net_device *netdev)
{
struct edma_priv *priv = netdev_priv(netdev);
@@ -1752,6 +1822,7 @@ static const struct net_device_ops edma_netdev_ops = {
.ndo_open = edma_ndo_open,
.ndo_stop = edma_ndo_stop,
.ndo_start_xmit = edma_ndo_xmit,
.ndo_features_check = edma_ndo_features_check,
.ndo_change_mtu = edma_ndo_change_mtu,
.ndo_set_mac_address = eth_mac_addr,
.ndo_validate_addr = eth_validate_addr,
@@ -2157,7 +2228,7 @@ static int edma_probe(struct platform_device *pdev)
netdev->netdev_ops = &edma_netdev_ops;
netdev->hw_features = NETIF_F_RXCSUM | NETIF_F_IP_CSUM |
NETIF_F_IPV6_CSUM | NETIF_F_SG | NETIF_F_TSO |
NETIF_F_TSO6 | NETIF_F_RXHASH;
NETIF_F_TSO6 | NETIF_F_RXHASH | NETIF_F_FRAGLIST;
netdev->features = netdev->hw_features;
/* A DSA user port takes its features from the conduit's vlan_features. */
netdev->vlan_features = netdev->hw_features;