From: Felix Fietkau <nbd@nbd.name>
Date: Mon, 14 Jul 2025 10:41:27 +0200
Subject: [PATCH] net: ethernet: mtk_eth_soc: add support for sending
 fraglist GSO packets

When primarily forwarding traffic, TCP fraglist GRO can be noticeably more
efficient than regular TCP GRO. In order to avoid the overhead of
unnecessary segmentation on ethernet tx, add support for sending fraglist
GRO packets.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
---

--- a/drivers/net/ethernet/mediatek/mtk_eth_soc.c
+++ b/drivers/net/ethernet/mediatek/mtk_eth_soc.c
@@ -1403,29 +1403,70 @@ static void mtk_tx_set_dma_desc(struct n
 		mtk_tx_set_dma_desc_v1(dev, txd, info);
 }
 
+struct mtk_tx_map_state {
+    struct mtk_tx_dma *txd, *txd_pdma;
+    struct mtk_tx_buf *tx_buf;
+    int nbuf;
+    int ndesc;
+};
+
+static int
+mtk_tx_map_info(struct mtk_eth *eth, struct mtk_tx_ring *ring,
+		struct net_device *dev, struct mtk_tx_map_state *state,
+		struct mtk_tx_dma_desc_info *txd_info)
+{
+	const struct mtk_soc_data *soc = eth->soc;
+	struct mtk_tx_dma *txd_pdma = state->txd_pdma;
+	struct mtk_tx_buf *tx_buf = state->tx_buf;
+	struct mtk_tx_dma *txd = state->txd;
+	struct mtk_mac *mac = netdev_priv(dev);
+
+	if (state->nbuf &&
+	    (MTK_HAS_CAPS(soc->caps, MTK_QDMA) || (state->nbuf & 1) == 0)) {
+		txd = state->txd = mtk_qdma_phys_to_virt(ring, txd->txd2);
+		txd_pdma = state->txd_pdma = qdma_to_pdma(ring, txd);
+		if (state->txd == ring->last_free)
+			return -1;
+
+		tx_buf = mtk_desc_to_tx_buf(ring, state->txd,
+					    soc->tx.desc_shift);
+		state->tx_buf = tx_buf;
+		memset(tx_buf, 0, sizeof(*tx_buf));
+		state->ndesc++;
+	}
+
+	mtk_tx_set_dma_desc(dev, txd, txd_info);
+	tx_buf->data = (void *)MTK_DMA_DUMMY_DESC;
+	tx_buf->mac_id = mac->id;
+
+	setup_tx_buf(eth, tx_buf, txd_pdma, txd_info->addr,
+		     txd_info->size, state->nbuf++);
+	return 0;
+}
+
 static int mtk_tx_map(struct sk_buff *skb, struct net_device *dev,
 		      int tx_num, struct mtk_tx_ring *ring, bool gso)
 {
 	struct mtk_tx_dma_desc_info txd_info = {
-		.size = skb_headlen(skb),
 		.gso = gso,
-		.csum = skb->ip_summed == CHECKSUM_PARTIAL,
+		.csum = skb->ip_summed == CHECKSUM_PARTIAL || gso,
 		.vlan = skb_vlan_tag_present(skb),
-		.qid = skb_get_queue_mapping(skb),
 		.vlan_tci = skb_vlan_tag_get(skb),
 		.first = true,
-		.last = !skb_is_nonlinear(skb),
+	};
+	struct mtk_tx_map_state state = {
+		.ndesc = 1,
 	};
 	struct netdev_queue *txq;
 	struct mtk_mac *mac = netdev_priv(dev);
 	struct mtk_eth *eth = mac->hw;
 	const struct mtk_soc_data *soc = eth->soc;
-	struct mtk_tx_dma *itxd, *txd;
-	struct mtk_tx_dma *itxd_pdma, *txd_pdma;
-	struct mtk_tx_buf *itx_buf, *tx_buf;
-	int i, n_desc = 1;
+	struct mtk_tx_dma *itxd, *itxd_pdma;
+	struct mtk_tx_buf *itx_buf;
+	struct sk_buff *cur_skb, *next_skb;
 	int queue = skb_get_queue_mapping(skb);
-	int k = 0;
+	unsigned int offset = 0;
+	int i, frag_size;
 
 	txq = netdev_get_tx_queue(dev, queue);
 	itxd = ring->next_free;
@@ -1436,86 +1477,81 @@ static int mtk_tx_map(struct sk_buff *sk
 	itx_buf = mtk_desc_to_tx_buf(ring, itxd, soc->tx.desc_shift);
 	memset(itx_buf, 0, sizeof(*itx_buf));
 
-	txd_info.addr = dma_map_single(eth->dma_dev, skb->data, txd_info.size,
-				       DMA_TO_DEVICE);
-	if (unlikely(dma_mapping_error(eth->dma_dev, txd_info.addr)))
-		return -ENOMEM;
-
-	mtk_tx_set_dma_desc(dev, itxd, &txd_info);
-
-	itx_buf->mac_id = mac->id;
-	setup_tx_buf(eth, itx_buf, itxd_pdma, txd_info.addr, txd_info.size,
-		     k++);
-
-	/* TX SG offload */
-	txd = itxd;
-	txd_pdma = qdma_to_pdma(ring, txd);
-
-	for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
-		skb_frag_t *frag = &skb_shinfo(skb)->frags[i];
-		unsigned int offset = 0;
-		int frag_size = skb_frag_size(frag);
-
+	cur_skb = skb;
+	next_skb = skb_shinfo(skb)->frag_list;
+	state.txd = itxd;
+	state.txd_pdma = itxd_pdma;
+	state.tx_buf = itx_buf;
+
+next:
+	txd_info.qid = queue;
+	frag_size = skb_headlen(cur_skb);
+
+	while (frag_size) {
+		txd_info.size = min_t(unsigned int, frag_size,
+				      soc->tx.dma_max_len);
+		txd_info.addr = dma_map_single(eth->dma_dev, cur_skb->data + offset,
+					       txd_info.size, DMA_TO_DEVICE);
+		if (unlikely(dma_mapping_error(eth->dma_dev, txd_info.addr)))
+			goto err_dma;
+
+		frag_size -= txd_info.size;
+		offset += txd_info.size;
+		txd_info.last = !skb_is_nonlinear(cur_skb) && !next_skb &&
+				!frag_size;
+		if (mtk_tx_map_info(eth, ring, dev, &state, &txd_info) < 0)
+			goto err_dma;
+
+		txd_info.first = false;
+	}
+
+	for (i = 0; i < skb_shinfo(cur_skb)->nr_frags; i++) {
+		skb_frag_t *frag = &skb_shinfo(cur_skb)->frags[i];
+
+		frag_size = skb_frag_size(frag);
+		memset(&txd_info, 0, sizeof(struct mtk_tx_dma_desc_info));
+		txd_info.qid = queue;
+		offset = 0;
 		while (frag_size) {
-			bool new_desc = true;
-
-			if (MTK_HAS_CAPS(soc->caps, MTK_QDMA) ||
-			    (i & 0x1)) {
-				txd = mtk_qdma_phys_to_virt(ring, txd->txd2);
-				txd_pdma = qdma_to_pdma(ring, txd);
-				if (txd == ring->last_free)
-					goto err_dma;
-
-				n_desc++;
-			} else {
-				new_desc = false;
-			}
-
-			memset(&txd_info, 0, sizeof(struct mtk_tx_dma_desc_info));
 			txd_info.size = min_t(unsigned int, frag_size,
 					      soc->tx.dma_max_len);
-			txd_info.qid = queue;
-			txd_info.last = i == skb_shinfo(skb)->nr_frags - 1 &&
-					!(frag_size - txd_info.size);
-			txd_info.addr = skb_frag_dma_map(eth->dma_dev, frag,
-							 offset, txd_info.size,
-							 DMA_TO_DEVICE);
+			txd_info.addr = skb_frag_dma_map(eth->dma_dev, frag, offset,
+							 txd_info.size, DMA_TO_DEVICE);
 			if (unlikely(dma_mapping_error(eth->dma_dev, txd_info.addr)))
 				goto err_dma;
 
-			mtk_tx_set_dma_desc(dev, txd, &txd_info);
-
-			tx_buf = mtk_desc_to_tx_buf(ring, txd,
-						    soc->tx.desc_shift);
-			if (new_desc)
-				memset(tx_buf, 0, sizeof(*tx_buf));
-			tx_buf->data = (void *)MTK_DMA_DUMMY_DESC;
-			tx_buf->mac_id = mac->id;
-
-			setup_tx_buf(eth, tx_buf, txd_pdma, txd_info.addr,
-				     txd_info.size, k++);
-
 			frag_size -= txd_info.size;
 			offset += txd_info.size;
+			txd_info.last = i == skb_shinfo(cur_skb)->nr_frags - 1 &&
+					!frag_size && !next_skb;
+			if (mtk_tx_map_info(eth, ring, dev, &state, &txd_info) < 0)
+				goto err_dma;
 		}
 	}
 
+	if (next_skb) {
+		cur_skb = next_skb;
+		next_skb = cur_skb->next;
+		memset(&txd_info, 0, sizeof(txd_info));
+		goto next;
+	}
+
 	/* store skb to cleanup */
 	itx_buf->type = MTK_TYPE_SKB;
 	itx_buf->data = skb;
 
 	if (!MTK_HAS_CAPS(soc->caps, MTK_QDMA)) {
-		if (k & 0x1)
-			txd_pdma->txd2 |= TX_DMA_LS0;
+		if (state.nbuf & 0x1)
+			state.txd_pdma->txd2 |= TX_DMA_LS0;
 		else
-			txd_pdma->txd2 |= TX_DMA_LS1;
+			state.txd_pdma->txd2 |= TX_DMA_LS1;
 	}
 
 	netdev_tx_sent_queue(txq, skb->len);
 	skb_tx_timestamp(skb);
 
-	ring->next_free = mtk_qdma_phys_to_virt(ring, txd->txd2);
-	atomic_sub(n_desc, &ring->free_count);
+	ring->next_free = mtk_qdma_phys_to_virt(ring, state.txd->txd2);
+	atomic_sub(state.ndesc, &ring->free_count);
 
 	/* make sure that all changes to the dma ring are flushed before we
 	 * continue
@@ -1524,11 +1560,11 @@ static int mtk_tx_map(struct sk_buff *sk
 
 	if (MTK_HAS_CAPS(soc->caps, MTK_QDMA)) {
 		if (netif_xmit_stopped(txq) || !netdev_xmit_more())
-			mtk_w32(eth, txd->txd2, soc->reg_map->qdma.ctx_ptr);
+			mtk_w32(eth, state.txd->txd2, soc->reg_map->qdma.ctx_ptr);
 	} else {
 		int next_idx;
 
-		next_idx = NEXT_DESP_IDX(txd_to_idx(ring, txd, soc->tx.desc_shift),
+		next_idx = NEXT_DESP_IDX(txd_to_idx(ring, state.txd, soc->tx.desc_shift),
 					 ring->dma_size);
 		mtk_w32(eth, next_idx, MT7628_TX_CTX_IDX0);
 	}
@@ -1537,10 +1573,10 @@ static int mtk_tx_map(struct sk_buff *sk
 
 err_dma:
 	do {
-		tx_buf = mtk_desc_to_tx_buf(ring, itxd, soc->tx.desc_shift);
+		itx_buf = mtk_desc_to_tx_buf(ring, itxd, soc->tx.desc_shift);
 
 		/* unmap dma */
-		mtk_tx_unmap(eth, tx_buf, NULL, false);
+		mtk_tx_unmap(eth, itx_buf, NULL, false);
 
 		itxd->txd3 = TX_DMA_LS0 | TX_DMA_OWNER_CPU;
 		if (!MTK_HAS_CAPS(soc->caps, MTK_QDMA))
@@ -1548,7 +1584,7 @@ err_dma:
 
 		itxd = mtk_qdma_phys_to_virt(ring, itxd->txd2);
 		itxd_pdma = qdma_to_pdma(ring, itxd);
-	} while (itxd != txd);
+	} while (itxd != state.txd);
 
 	return -ENOMEM;
 }
@@ -1568,6 +1604,9 @@ static int mtk_cal_txd_req(struct mtk_et
 		nfrags += skb_shinfo(skb)->nr_frags;
 	}
 
+	for (skb = skb_shinfo(skb)->frag_list; skb; skb = skb->next)
+		nfrags += mtk_cal_txd_req(eth, skb);
+
 	return nfrags;
 }
 
@@ -1608,6 +1647,10 @@ static bool mtk_skb_has_small_frag(struc
 		if (skb_frag_size(&skb_shinfo(skb)->frags[i]) < min_size)
 			return true;
 
+	for (skb = skb_shinfo(skb)->frag_list; skb; skb = skb->next)
+		if (mtk_skb_has_small_frag(skb))
+			return true;
+
 	return false;
 }
 
--- a/drivers/net/ethernet/mediatek/mtk_eth_soc.h
+++ b/drivers/net/ethernet/mediatek/mtk_eth_soc.h
@@ -51,6 +51,8 @@
 				 NETIF_F_HW_VLAN_CTAG_TX | \
 				 NETIF_F_SG | NETIF_F_TSO | \
 				 NETIF_F_TSO6 | \
+				 NETIF_F_FRAGLIST | \
+				 NETIF_F_GSO_FRAGLIST | \
 				 NETIF_F_IPV6_CSUM |\
 				 NETIF_F_HW_TC)
 #define MTK_HW_FEATURES_MT7628	(NETIF_F_SG | NETIF_F_RXCSUM)
