wifi: mt76: mt76u: use a threaded NAPI for the RX path

The USB RX path delivers frames to the stack via mt76_rx_complete() with
a NULL napi pointer, taking the netif_receive_skb_list() path, so it never
benefits from GRO -- unlike the DMA-based mt76 drivers, which pass a real
napi and use napi_gro_receive(). For bulk TCP traffic this is costly, as
every segment traverses the stack individually.

Service the MT_RXQ_MAIN queue from a threaded NAPI, reusing mt76_dev's
existing napi_dev and napi[] rather than adding new fields. The URB
completion handler schedules the napi; its poll drains the URBs, builds
the skbs, resubmits and delivers them through napi_gro_receive(). The MCU
queue stays on the existing RX worker. This enables GRO and moves RX
processing into its own kernel thread, parallelising the datapath.

On mt7921u at HE-MCS 11 (2x2, 80 MHz; fast.com, multiple streams) this
averages ~588 Mbit/s, versus ~424 Mbit/s when the same napi is instead
driven manually from the RX worker, and ~380 Mbit/s for the unmodified
driver.

Suggested-by: Lorenzo Bianconi <lorenzo@kernel.org>
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Filip Bakreski <phial@phiality.com>
Acked-by: Lorenzo Bianconi <lorenzo@kernel.org>
Link: https://patch.msgid.link/20260609105301.196302-1-phial@phiality.com
Signed-off-by: Felix Fietkau <nbd@nbd.name>
This commit is contained in:
Filip Bakreski 2026-06-09 20:53:01 +10:00 committed by Felix Fietkau
parent a92cd5dd79
commit 5323d3e50c

View File

@ -580,7 +580,11 @@ static void mt76u_complete_rx(struct urb *urb)
q->head = (q->head + 1) % q->ndesc;
q->queued++;
mt76_worker_schedule(&dev->usb.rx_worker);
if (q == &dev->q_rx[MT_RXQ_MAIN])
napi_schedule(&dev->napi[MT_RXQ_MAIN]);
else
mt76_worker_schedule(&dev->usb.rx_worker);
out:
spin_unlock_irqrestore(&q->lock, flags);
}
@ -618,11 +622,23 @@ mt76u_process_rx_queue(struct mt76_dev *dev, struct mt76_queue *q)
}
mt76u_submit_rx_buf(dev, qid, urb);
}
if (qid == MT_RXQ_MAIN) {
local_bh_disable();
mt76_rx_poll_complete(dev, MT_RXQ_MAIN, NULL);
local_bh_enable();
}
}
/* Threaded NAPI poll for the MAIN RX queue: drain URBs, build skbs, resubmit,
* then deliver through napi_gro_receive() and let napi_complete() flush GRO.
*/
static int mt76u_napi_poll(struct napi_struct *napi, int budget)
{
struct mt76_dev *dev = mt76_priv(napi->dev);
rcu_read_lock();
mt76u_process_rx_queue(dev, &dev->q_rx[MT_RXQ_MAIN]);
mt76_rx_poll_complete(dev, MT_RXQ_MAIN, napi);
rcu_read_unlock();
napi_complete(napi);
return 0;
}
static void mt76u_rx_worker(struct mt76_worker *w)
@ -632,8 +648,13 @@ static void mt76u_rx_worker(struct mt76_worker *w)
int i;
rcu_read_lock();
mt76_for_each_q_rx(dev, i)
mt76_for_each_q_rx(dev, i) {
/* MT_RXQ_MAIN is serviced by the threaded NAPI poll */
if (i == MT_RXQ_MAIN)
continue;
mt76u_process_rx_queue(dev, &dev->q_rx[i]);
}
rcu_read_unlock();
}
@ -731,6 +752,13 @@ void mt76u_stop_rx(struct mt76_dev *dev)
for (j = 0; j < q->ndesc; j++)
usb_poison_urb(q->entry[j].urb);
}
/* The MAIN queue napi stays enabled for the device lifetime. The URBs
* are now poisoned, so mt76u_complete_rx() can no longer reschedule it;
* just drain any in-flight poll before the caller frees or resets.
*/
if (dev->napi_dev)
napi_synchronize(&dev->napi[MT_RXQ_MAIN]);
}
EXPORT_SYMBOL_GPL(mt76u_stop_rx);
@ -1051,6 +1079,13 @@ void mt76u_queues_deinit(struct mt76_dev *dev)
mt76u_stop_rx(dev);
mt76u_stop_tx(dev);
if (dev->napi_dev) {
napi_disable(&dev->napi[MT_RXQ_MAIN]);
netif_napi_del(&dev->napi[MT_RXQ_MAIN]);
free_netdev(dev->napi_dev);
dev->napi_dev = NULL;
}
mt76u_free_rx(dev);
mt76u_free_tx(dev);
}
@ -1078,6 +1113,7 @@ int __mt76u_init(struct mt76_dev *dev, struct usb_interface *intf,
{
struct usb_device *udev = interface_to_usbdev(intf);
struct mt76_usb *usb = &dev->usb;
struct mt76_dev **priv;
int err;
INIT_WORK(&usb->stat_work, mt76u_tx_status_data);
@ -1115,6 +1151,20 @@ int __mt76u_init(struct mt76_dev *dev, struct usb_interface *intf,
sched_set_fifo_low(usb->rx_worker.task);
sched_set_fifo_low(usb->status_worker.task);
/* threaded NAPI on a dummy netdev (reusing mt76_dev's napi_dev/napi[])
* services the MAIN RX queue and gives the RX path GRO
*/
dev->napi_dev = alloc_netdev_dummy(sizeof(struct mt76_dev *));
if (!dev->napi_dev)
return -ENOMEM;
priv = netdev_priv(dev->napi_dev);
*priv = dev;
strscpy(dev->napi_dev->name, "mt76u-rx", sizeof(dev->napi_dev->name));
dev->napi_dev->threaded = 1;
netif_napi_add(dev->napi_dev, &dev->napi[MT_RXQ_MAIN], mt76u_napi_poll);
napi_enable(&dev->napi[MT_RXQ_MAIN]);
return 0;
}
EXPORT_SYMBOL_GPL(__mt76u_init);