* [PATCH 00/30] wifi: mac80211: unify frame queueing
@ 2026-11-02 20:22 Johannes Berg
2026-11-02 20:22 ` [PATCH 01/30] wifi: mac80211: mesh: clear PS_STA flag when peer wakes up Johannes Berg
` (29 more replies)
0 siblings, 30 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:22 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen
Hi,
So ... this is a bit set of changes, but I don't think it's very
useful to send a non-trivial subset, it won't really explain the
direction at all.
This is my fourth attempt of doing this, it's been on my list
for a long while; some discussions with Toke and interest in it
for SMD transition prompted me to try yet another time, and now
I seem to have something workable.
There's more where this came from, but the rest of the series I
have mostly moves the queue stop handling to TXQs instead of
keeping track of all the various layers of queues, which is a
simplification on top of this.
But going back to why I wanted this set and what I'm actually
doing here:
- we mix ethernet and 802.11 frames on TXQs,
- we build A-MSDUs all the wrong way (should be on dequeue,
not enqueue), and
- we have too many queues for powersaving clients, rather
than using the TXQs.
Much of this series is concerned with the last bullet, but
from a feature perspective I really care most about the first,
since it's needed to (sanely) transfer queues from one station
(representing the AP MLD) to another during SMD Transition.
Apart from the queue stop/wake cleanups, I have a large set
of hwsim changes and tests which were useful while working on
this, but I'm not sure I'll post that at all, it basically
lets hwsim emulate all the various other drivers.
johannes
^ permalink raw reply [flat|nested] 33+ messages in thread
* [PATCH 01/30] wifi: mac80211: mesh: clear PS_STA flag when peer wakes up
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
@ 2026-11-02 20:22 ` Johannes Berg
2026-11-02 20:22 ` [PATCH 02/30] wifi: mac80211: disable BHs when waking TXQs on PS wakeup Johannes Berg
` (28 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:22 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
Since my old race fix, ieee80211_sta_ps_deliver_wakeup() doesn't
reset the WLAN_STA_PS_STA flag any more. Evidently, I forgot to
update the mesh code, so mesh peers that go to sleep are never
considered having woken up ever again.
Set WLAN_STA_PS_DELIVER and clear WLAN_STA_PS_STA before calling
ieee80211_sta_ps_deliver_wakeup().
This commit is ancient, so it's tempting to just remove the mesh
powersave implementation entirely, but this was easier.
Fixes: e3685e03b40f ("mac80211: fix station wakeup powersave race")
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/mesh_ps.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/net/mac80211/mesh_ps.c b/net/mac80211/mesh_ps.c
index ebab1f0a0138..0ec207ee845b 100644
--- a/net/mac80211/mesh_ps.c
+++ b/net/mac80211/mesh_ps.c
@@ -272,6 +272,8 @@ void ieee80211_mps_sta_status_update(struct sta_info *sta)
mps_dbg(sta->sdata, "start PS buffering frames towards %pM\n",
sta->sta.addr);
} else {
+ set_sta_flag(sta, WLAN_STA_PS_DELIVER);
+ clear_sta_flag(sta, WLAN_STA_PS_STA);
ieee80211_sta_ps_deliver_wakeup(sta);
}
}
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 02/30] wifi: mac80211: disable BHs when waking TXQs on PS wakeup
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
2026-11-02 20:22 ` [PATCH 01/30] wifi: mac80211: mesh: clear PS_STA flag when peer wakes up Johannes Berg
@ 2026-11-02 20:22 ` Johannes Berg
2026-11-02 20:22 ` [PATCH 03/30] wifi: mt76: correctly set more-data flag on PS release Johannes Berg
` (27 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:22 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
ieee80211_sta_ps_deliver_wakeup() can be called in process
context, e.g. for mesh when the peer link is changed via
nl80211, but drivers can dequeue frames from TXQs when
wake_tx_queue() is called, requiring soft IRQs disabled.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/sta_info.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index 23166fc39729..80341173ae45 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -1865,12 +1865,15 @@ void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta)
if (!ieee80211_hw_check(&local->hw, AP_LINK_PS))
drv_sta_notify(local, sdata, STA_NOTIFY_AWAKE, &sta->sta);
+ /* this can be called in process context, e.g. for mesh */
+ local_bh_disable();
for (i = 0; i < ARRAY_SIZE(sta->sta.txq); i++) {
if (!sta->sta.txq[i] || !txq_has_queue(sta->sta.txq[i]))
continue;
schedule_and_wake_txq(local, to_txq_info(sta->sta.txq[i]));
}
+ local_bh_enable();
skb_queue_head_init(&pending);
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 03/30] wifi: mt76: correctly set more-data flag on PS release
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
2026-11-02 20:22 ` [PATCH 01/30] wifi: mac80211: mesh: clear PS_STA flag when peer wakes up Johannes Berg
2026-11-02 20:22 ` [PATCH 02/30] wifi: mac80211: disable BHs when waking TXQs on PS wakeup Johannes Berg
@ 2026-11-02 20:22 ` Johannes Berg
2026-11-02 20:22 ` [PATCH 04/30] wifi: mac80211: remove dead aggregation TX code Johannes Berg
` (26 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:22 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
When releasing buffered frames, mt76_release_buffered_frames() only
sets the bit on frames that aren't last, but mac80211 might request
it to be set if there's more buffered, and there could also be some
on other TXQs. Handle that correctly.
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
drivers/net/wireless/mediatek/mt76/tx.c | 19 +++++++++++++++----
1 file changed, 15 insertions(+), 4 deletions(-)
diff --git a/drivers/net/wireless/mediatek/mt76/tx.c b/drivers/net/wireless/mediatek/mt76/tx.c
index b022d3843473..89ecf6b5580f 100644
--- a/drivers/net/wireless/mediatek/mt76/tx.c
+++ b/drivers/net/wireless/mediatek/mt76/tx.c
@@ -420,7 +420,7 @@ mt76_txq_dequeue(struct mt76_phy *phy, struct mt76_txq *mtxq)
static void
mt76_queue_ps_skb(struct mt76_phy *phy, struct ieee80211_sta *sta,
- struct sk_buff *skb, bool last,
+ struct sk_buff *skb, bool last, bool more_data,
enum ieee80211_frame_release_type reason)
{
struct mt76_wcid *wcid = (struct mt76_wcid *)sta->drv_priv;
@@ -437,7 +437,7 @@ mt76_queue_ps_skb(struct mt76_phy *phy, struct ieee80211_sta *sta,
*ieee80211_get_qos_ctl(hdr) |= IEEE80211_QOS_CTL_EOSP;
}
- mt76_skb_set_moredata(skb, !last);
+ mt76_skb_set_moredata(skb, more_data);
__mt76_tx_queue_skb(phy, MT_TXQ_PSD, skb, wcid, sta, NULL);
}
@@ -451,6 +451,7 @@ mt76_release_buffered_frames(struct ieee80211_hw *hw, struct ieee80211_sta *sta,
struct mt76_dev *dev = phy->dev;
struct sk_buff *last_skb = NULL;
struct mt76_queue *hwq = phy->q_tx[MT_TXQ_PSD];
+ unsigned long release_tids = tids;
int i;
spin_lock_bh(&hwq->lock);
@@ -470,14 +471,24 @@ mt76_release_buffered_frames(struct ieee80211_hw *hw, struct ieee80211_sta *sta,
nframes--;
if (last_skb)
mt76_queue_ps_skb(phy, sta, last_skb, false,
- reason);
+ true, reason);
last_skb = skb;
} while (nframes);
}
if (last_skb) {
- mt76_queue_ps_skb(phy, sta, last_skb, true, reason);
+ for_each_set_bit(i, &release_tids, IEEE80211_NUM_TIDS) {
+ unsigned long frame_cnt;
+
+ if (more_data)
+ break;
+
+ ieee80211_txq_get_depth(sta->txq[i], &frame_cnt, NULL);
+ more_data = frame_cnt > 0;
+ }
+
+ mt76_queue_ps_skb(phy, sta, last_skb, true, more_data, reason);
dev->queue_ops->kick(dev, hwq);
} else {
ieee80211_sta_eosp(sta);
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 04/30] wifi: mac80211: remove dead aggregation TX code
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (2 preceding siblings ...)
2026-11-02 20:22 ` [PATCH 03/30] wifi: mt76: correctly set more-data flag on PS release Johannes Berg
@ 2026-11-02 20:22 ` Johannes Berg
2026-11-02 20:22 ` [PATCH 05/30] wifi: mac80211: remove tid_tx->pending Johannes Berg
` (25 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:22 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
Since TXQs are paused for TX aggregation start, and we forced
all drivers to use TXQs in mac80211, ieee80211_tx_prep_agg()
can't find that there's no TXQ anymore. Delete the dead code.
TX now also no longer looks at the aggregation state with the
lock held, so update the docs.
Also set IEEE80211_TX_CTL_AMPDU again, we just fixed an issue
with that, and merging trees would otherwise drop that fix.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/sta_info.h | 6 +--
net/mac80211/tx.c | 83 ++---------------------------------------
2 files changed, 5 insertions(+), 84 deletions(-)
diff --git a/net/mac80211/sta_info.h b/net/mac80211/sta_info.h
index 8c6a9fee6b29..8b64c9b12de0 100644
--- a/net/mac80211/sta_info.h
+++ b/net/mac80211/sta_info.h
@@ -177,11 +177,7 @@ struct sta_info;
* This structure's lifetime is managed by RCU, assignments to
* the array holding it must hold the aggregation mutex.
*
- * The TX path can access it under RCU lock-free if, and
- * only if, the state has the flag %HT_AGG_STATE_OPERATIONAL
- * set. Otherwise, the TX path must also acquire the spinlock
- * and re-check the state, see comments in the tx code
- * touching it.
+ * The TX path accesses it under RCU, lock-free.
*/
struct tid_ampdu_tx {
struct rcu_head rcu_head;
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index e9b6a0415d6a..b945bd2fcb46 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -1153,77 +1153,6 @@ ieee80211_tx_h_calculate_duration(struct ieee80211_tx_data *tx)
/* actual transmit path */
-static bool ieee80211_tx_prep_agg(struct ieee80211_tx_data *tx,
- struct sk_buff *skb,
- struct ieee80211_tx_info *info,
- struct tid_ampdu_tx *tid_tx,
- int tid)
-{
- bool queued = false;
- bool reset_agg_timer = false;
- struct sk_buff *purge_skb = NULL;
-
- if (test_bit(HT_AGG_STATE_OPERATIONAL, &tid_tx->state)) {
- reset_agg_timer = true;
- } else if (test_bit(HT_AGG_STATE_WANT_START, &tid_tx->state)) {
- /*
- * nothing -- this aggregation session is being started
- * but that might still fail with the driver
- */
- } else if (!tx->sta->sta.txq[tid]) {
- spin_lock(&tx->sta->lock);
- /*
- * Need to re-check now, because we may get here
- *
- * 1) in the window during which the setup is actually
- * already done, but not marked yet because not all
- * packets are spliced over to the driver pending
- * queue yet -- if this happened we acquire the lock
- * either before or after the splice happens, but
- * need to recheck which of these cases happened.
- *
- * 2) during session teardown, if the OPERATIONAL bit
- * was cleared due to the teardown but the pointer
- * hasn't been assigned NULL yet (or we loaded it
- * before it was assigned) -- in this case it may
- * now be NULL which means we should just let the
- * packet pass through because splicing the frames
- * back is already done.
- */
- tid_tx = rcu_dereference_protected_tid_tx(tx->sta, tid);
-
- if (!tid_tx) {
- /* do nothing, let packet pass through */
- } else if (test_bit(HT_AGG_STATE_OPERATIONAL, &tid_tx->state)) {
- reset_agg_timer = true;
- } else {
- queued = true;
- if (info->flags & IEEE80211_TX_CTL_NO_PS_BUFFER) {
- clear_sta_flag(tx->sta, WLAN_STA_SP);
- ps_dbg(tx->sta->sdata,
- "STA %pM aid %d: SP frame queued, close the SP w/o telling the peer\n",
- tx->sta->sta.addr, tx->sta->sta.aid);
- }
- info->control.vif = &tx->sdata->vif;
- info->control.flags |= IEEE80211_TX_INTCFL_NEED_TXPROCESSING;
- info->flags &= ~IEEE80211_TX_TEMPORARY_FLAGS;
- __skb_queue_tail(&tid_tx->pending, skb);
- if (skb_queue_len(&tid_tx->pending) > STA_MAX_TX_BUFFER)
- purge_skb = __skb_dequeue(&tid_tx->pending);
- }
- spin_unlock(&tx->sta->lock);
-
- if (purge_skb)
- ieee80211_free_txskb(&tx->local->hw, purge_skb);
- }
-
- /* reset session timer */
- if (reset_agg_timer)
- tid_tx->last_tx = jiffies;
-
- return queued;
-}
-
void ieee80211_aggr_check(struct ieee80211_sub_if_data *sdata,
struct sta_info *sta, struct sk_buff *skb)
{
@@ -1308,14 +1237,10 @@ ieee80211_tx_prepare(struct ieee80211_sub_if_data *sdata,
tid_tx = rcu_dereference(tx->sta->ampdu_mlme.tid_tx[tid]);
}
- if (tid_tx) {
- bool queued;
-
- queued = ieee80211_tx_prep_agg(tx, skb, info,
- tid_tx, tid);
-
- if (unlikely(queued))
- return TX_QUEUED;
+ if (tid_tx && test_bit(HT_AGG_STATE_OPERATIONAL, &tid_tx->state)) {
+ info->flags |= IEEE80211_TX_CTL_AMPDU;
+ /* reset session timer */
+ tid_tx->last_tx = jiffies;
}
}
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 05/30] wifi: mac80211: remove tid_tx->pending
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (3 preceding siblings ...)
2026-11-02 20:22 ` [PATCH 04/30] wifi: mac80211: remove dead aggregation TX code Johannes Berg
@ 2026-11-02 20:22 ` Johannes Berg
2026-11-02 20:22 ` [PATCH 06/30] wifi: mac80211: remove aggregation queue stop reason Johannes Berg
` (24 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:22 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
The previous commit removed the last code that put any frames
onto tid_tx->pending, so the remove all associated code now.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/agg-tx.c | 71 ++++----------------------------------
net/mac80211/debugfs_sta.c | 4 +--
net/mac80211/sta_info.c | 7 +---
net/mac80211/sta_info.h | 2 --
4 files changed, 8 insertions(+), 76 deletions(-)
diff --git a/net/mac80211/agg-tx.c b/net/mac80211/agg-tx.c
index f3ecdd8601a0..497c0f3a7c35 100644
--- a/net/mac80211/agg-tx.c
+++ b/net/mac80211/agg-tx.c
@@ -222,40 +222,6 @@ ieee80211_agg_start_txq(struct sta_info *sta, int tid, bool enable)
local_bh_enable();
}
-/*
- * splice packets from the STA's pending to the local pending,
- * requires a call to ieee80211_agg_splice_finish later
- */
-static void __acquires(agg_queue)
-ieee80211_agg_splice_packets(struct ieee80211_sub_if_data *sdata,
- struct tid_ampdu_tx *tid_tx, u16 tid)
-{
- struct ieee80211_local *local = sdata->local;
- int queue = sdata->vif.hw_queue[ieee80211_ac_from_tid(tid)];
- unsigned long flags;
-
- ieee80211_stop_queue_agg(sdata, tid);
-
- if (WARN(!tid_tx,
- "TID %d gone but expected when splicing aggregates from the pending queue\n",
- tid))
- return;
-
- if (!skb_queue_empty(&tid_tx->pending)) {
- spin_lock_irqsave(&local->queue_stop_reason_lock, flags);
- /* copy over remaining packets */
- skb_queue_splice_tail_init(&tid_tx->pending,
- &local->pending[queue]);
- spin_unlock_irqrestore(&local->queue_stop_reason_lock, flags);
- }
-}
-
-static void __releases(agg_queue)
-ieee80211_agg_splice_finish(struct ieee80211_sub_if_data *sdata, u16 tid)
-{
- ieee80211_wake_queue_agg(sdata, tid);
-}
-
static void ieee80211_remove_tid_tx(struct sta_info *sta, int tid)
{
struct tid_ampdu_tx *tid_tx;
@@ -265,22 +231,12 @@ static void ieee80211_remove_tid_tx(struct sta_info *sta, int tid)
tid_tx = rcu_dereference_protected_tid_tx(sta, tid);
- /*
- * When we get here, the TX path will not be lockless any more wrt.
- * aggregation, since the OPERATIONAL bit has long been cleared.
- * Thus it will block on getting the lock, if it occurs. So if we
- * stop the queue now, we will not get any more packets, and any
- * that might be being processed will wait for us here, thereby
- * guaranteeing that no packets go to the tid_tx pending queue any
- * more.
- */
-
- ieee80211_agg_splice_packets(sta->sdata, tid_tx, tid);
+ ieee80211_stop_queue_agg(sta->sdata, tid);
/* future packets must not find the tid_tx struct any more */
ieee80211_assign_tid_tx(sta, tid, NULL);
- ieee80211_agg_splice_finish(sta->sdata, tid);
+ ieee80211_wake_queue_agg(sta->sdata, tid);
kfree_rcu(tid_tx, rcu_head);
}
@@ -363,11 +319,6 @@ int __ieee80211_stop_tx_ba_session(struct sta_info *sta, u16 tid,
timer_delete_sync(&tid_tx->addba_resp_timer);
timer_delete_sync(&tid_tx->session_timer);
- /*
- * After this packets are no longer handed right through
- * to the driver but are put onto tid_tx->pending instead,
- * with locking to ensure proper access.
- */
clear_bit(HT_AGG_STATE_OPERATIONAL, &tid_tx->state);
/*
@@ -538,9 +489,9 @@ void ieee80211_tx_ba_session_handle_start(struct sta_info *sta, int tid)
"BA request denied - HW unavailable for %pM tid %d\n",
sta->sta.addr, tid);
spin_lock_bh(&sta->lock);
- ieee80211_agg_splice_packets(sdata, tid_tx, tid);
+ ieee80211_stop_queue_agg(sdata, tid);
ieee80211_assign_tid_tx(sta, tid, NULL);
- ieee80211_agg_splice_finish(sdata, tid);
+ ieee80211_wake_queue_agg(sdata, tid);
spin_unlock_bh(&sta->lock);
ieee80211_agg_start_txq(sta, tid, false);
@@ -717,7 +668,6 @@ int ieee80211_start_tx_ba_session(struct ieee80211_sta *pubsta, u16 tid,
goto err_unlock_sta;
}
- skb_queue_head_init(&tid_tx->pending);
__set_bit(HT_AGG_STATE_WANT_START, &tid_tx->state);
tid_tx->timeout = timeout;
@@ -773,20 +723,11 @@ static void ieee80211_agg_tx_operational(struct ieee80211_local *local,
drv_ampdu_action(local, sta->sdata, ¶ms);
- /*
- * synchronize with TX path, while splicing the TX path
- * should block so it won't put more packets onto pending.
- */
spin_lock_bh(&sta->lock);
- ieee80211_agg_splice_packets(sta->sdata, tid_tx, tid);
- /*
- * Now mark as operational. This will be visible
- * in the TX path, and lets it go lock-free in
- * the common case.
- */
+ ieee80211_stop_queue_agg(sta->sdata, tid);
set_bit(HT_AGG_STATE_OPERATIONAL, &tid_tx->state);
- ieee80211_agg_splice_finish(sta->sdata, tid);
+ ieee80211_wake_queue_agg(sta->sdata, tid);
spin_unlock_bh(&sta->lock);
diff --git a/net/mac80211/debugfs_sta.c b/net/mac80211/debugfs_sta.c
index 690c186e3bb9..203f608eba67 100644
--- a/net/mac80211/debugfs_sta.c
+++ b/net/mac80211/debugfs_sta.c
@@ -314,7 +314,7 @@ static ssize_t sta_agg_status_do_read(struct wiphy *wiphy, struct file *file,
p += scnprintf(p, bufsz + buf - p, "next dialog_token: %#02x\n",
sta->ampdu_mlme.dialog_token_allocator + 1);
p += scnprintf(p, bufsz + buf - p,
- "TID\t\tRX\tDTKN\tSSN\t\tTX\tDTKN\tpending\n");
+ "TID\t\tRX\tDTKN\tSSN\t\tTX\tDTKN\n");
for (i = 0; i < IEEE80211_NUM_TIDS; i++) {
bool tid_rx_valid;
@@ -335,8 +335,6 @@ static ssize_t sta_agg_status_do_read(struct wiphy *wiphy, struct file *file,
p += scnprintf(p, bufsz + buf - p, "\t\t%x", !!tid_tx);
p += scnprintf(p, bufsz + buf - p, "\t%#.2x",
tid_tx ? tid_tx->dialog_token : 0);
- p += scnprintf(p, bufsz + buf - p, "\t%03d",
- tid_tx ? skb_queue_len(&tid_tx->pending) : 0);
p += scnprintf(p, bufsz + buf - p, "\n");
}
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index 80341173ae45..e9ca1f26a6bd 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -132,7 +132,6 @@ void ieee80211_purge_sta_txqs(struct sta_info *sta)
static void __cleanup_single_sta(struct sta_info *sta)
{
int ac, i;
- struct tid_ampdu_tx *tid_tx;
struct ieee80211_sub_if_data *sdata = sta->sdata;
struct ieee80211_local *local = sdata->local;
struct ps_data *ps;
@@ -176,11 +175,7 @@ static void __cleanup_single_sta(struct sta_info *sta)
*/
for (i = 0; i < IEEE80211_NUM_TIDS; i++) {
kfree(sta->ampdu_mlme.tid_start_tx[i]);
- tid_tx = rcu_dereference_raw(sta->ampdu_mlme.tid_tx[i]);
- if (!tid_tx)
- continue;
- ieee80211_purge_tx_queue(&local->hw, &tid_tx->pending);
- kfree(tid_tx);
+ kfree(rcu_dereference_raw(sta->ampdu_mlme.tid_tx[i]));
}
}
diff --git a/net/mac80211/sta_info.h b/net/mac80211/sta_info.h
index 8b64c9b12de0..ebc70d2ad637 100644
--- a/net/mac80211/sta_info.h
+++ b/net/mac80211/sta_info.h
@@ -158,7 +158,6 @@ struct sta_info;
* @rcu_head: rcu head for freeing structure
* @session_timer: check if we keep Tx-ing on the TID (by timeout value)
* @addba_resp_timer: timer for peer's response to addba request
- * @pending: pending frames queue -- use sta's spinlock to protect
* @sta: station we are attached to
* @dialog_token: dialog token for aggregation session
* @timeout: session timeout value to be filled in ADDBA requests
@@ -183,7 +182,6 @@ struct tid_ampdu_tx {
struct rcu_head rcu_head;
struct timer_list session_timer;
struct timer_list addba_resp_timer;
- struct sk_buff_head pending;
struct sta_info *sta;
unsigned long state;
unsigned long last_tx;
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 06/30] wifi: mac80211: remove aggregation queue stop reason
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (4 preceding siblings ...)
2026-11-02 20:22 ` [PATCH 05/30] wifi: mac80211: remove tid_tx->pending Johannes Berg
@ 2026-11-02 20:22 ` Johannes Berg
2026-11-02 20:22 ` [PATCH 07/30] wifi: mac80211: rename vif->txq_mgmt to vif->txq_nan_dw Johannes Berg
` (23 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:22 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
Similar to the previous cleanup, the aggregation-related queue
stop reason was only used for splicing packets to/from pending,
so it can go away now. The spinlock was only needed for that,
so remove that too.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/agg-tx.c | 51 --------------------------------------
net/mac80211/ieee80211_i.h | 3 ---
net/mac80211/main.c | 4 +--
3 files changed, 1 insertion(+), 57 deletions(-)
diff --git a/net/mac80211/agg-tx.c b/net/mac80211/agg-tx.c
index 497c0f3a7c35..304819e5309c 100644
--- a/net/mac80211/agg-tx.c
+++ b/net/mac80211/agg-tx.c
@@ -139,45 +139,6 @@ void ieee80211_assign_tid_tx(struct sta_info *sta, int tid,
rcu_assign_pointer(sta->ampdu_mlme.tid_tx[tid], tid_tx);
}
-/*
- * When multiple aggregation sessions on multiple stations
- * are being created/destroyed simultaneously, we need to
- * refcount the global queue stop caused by that in order
- * to not get into a situation where one of the aggregation
- * setup or teardown re-enables queues before the other is
- * ready to handle that.
- *
- * These two functions take care of this issue by keeping
- * a global "agg_queue_stop" refcount.
- */
-static void __acquires(agg_queue)
-ieee80211_stop_queue_agg(struct ieee80211_sub_if_data *sdata, int tid)
-{
- int queue = sdata->vif.hw_queue[ieee80211_ac_from_tid(tid)];
-
- /* we do refcounting here, so don't use the queue reason refcounting */
-
- if (atomic_inc_return(&sdata->local->agg_queue_stop[queue]) == 1)
- ieee80211_stop_queue_by_reason(
- &sdata->local->hw, queue,
- IEEE80211_QUEUE_STOP_REASON_AGGREGATION,
- false);
- __acquire(agg_queue);
-}
-
-static void __releases(agg_queue)
-ieee80211_wake_queue_agg(struct ieee80211_sub_if_data *sdata, int tid)
-{
- int queue = sdata->vif.hw_queue[ieee80211_ac_from_tid(tid)];
-
- if (atomic_dec_return(&sdata->local->agg_queue_stop[queue]) == 0)
- ieee80211_wake_queue_by_reason(
- &sdata->local->hw, queue,
- IEEE80211_QUEUE_STOP_REASON_AGGREGATION,
- false);
- __release(agg_queue);
-}
-
static void
ieee80211_agg_stop_txq(struct sta_info *sta, int tid)
{
@@ -231,13 +192,9 @@ static void ieee80211_remove_tid_tx(struct sta_info *sta, int tid)
tid_tx = rcu_dereference_protected_tid_tx(sta, tid);
- ieee80211_stop_queue_agg(sta->sdata, tid);
-
/* future packets must not find the tid_tx struct any more */
ieee80211_assign_tid_tx(sta, tid, NULL);
- ieee80211_wake_queue_agg(sta->sdata, tid);
-
kfree_rcu(tid_tx, rcu_head);
}
@@ -489,9 +446,7 @@ void ieee80211_tx_ba_session_handle_start(struct sta_info *sta, int tid)
"BA request denied - HW unavailable for %pM tid %d\n",
sta->sta.addr, tid);
spin_lock_bh(&sta->lock);
- ieee80211_stop_queue_agg(sdata, tid);
ieee80211_assign_tid_tx(sta, tid, NULL);
- ieee80211_wake_queue_agg(sdata, tid);
spin_unlock_bh(&sta->lock);
ieee80211_agg_start_txq(sta, tid, false);
@@ -723,13 +678,7 @@ static void ieee80211_agg_tx_operational(struct ieee80211_local *local,
drv_ampdu_action(local, sta->sdata, ¶ms);
- spin_lock_bh(&sta->lock);
-
- ieee80211_stop_queue_agg(sta->sdata, tid);
set_bit(HT_AGG_STATE_OPERATIONAL, &tid_tx->state);
- ieee80211_wake_queue_agg(sta->sdata, tid);
-
- spin_unlock_bh(&sta->lock);
ieee80211_agg_start_txq(sta, tid, true);
}
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index 9906a62ee99c..e05eadebbf8f 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -1406,7 +1406,6 @@ enum queue_stop_reason {
IEEE80211_QUEUE_STOP_REASON_DRIVER,
IEEE80211_QUEUE_STOP_REASON_PS,
IEEE80211_QUEUE_STOP_REASON_CSA,
- IEEE80211_QUEUE_STOP_REASON_AGGREGATION,
IEEE80211_QUEUE_STOP_REASON_SUSPEND,
IEEE80211_QUEUE_STOP_REASON_SKB_ADD,
IEEE80211_QUEUE_STOP_REASON_OFFCHANNEL,
@@ -1628,8 +1627,6 @@ struct ieee80211_local {
struct tasklet_struct tx_pending_tasklet;
struct tasklet_struct wake_txqs_tasklet;
- atomic_t agg_queue_stop[IEEE80211_MAX_QUEUES];
-
/* number of interfaces with allmulti RX */
atomic_t iff_allmultis;
diff --git a/net/mac80211/main.c b/net/mac80211/main.c
index 6e31076bcc10..1055a6752e27 100644
--- a/net/mac80211/main.c
+++ b/net/mac80211/main.c
@@ -1030,10 +1030,8 @@ struct ieee80211_hw *ieee80211_alloc_hw_nm(size_t priv_data_len,
spin_lock_init(&local->ack_status_lock);
idr_init(&local->ack_status_frames);
- for (i = 0; i < IEEE80211_MAX_QUEUES; i++) {
+ for (i = 0; i < IEEE80211_MAX_QUEUES; i++)
skb_queue_head_init(&local->pending[i]);
- atomic_set(&local->agg_queue_stop[i], 0);
- }
tasklet_setup(&local->tx_pending_tasklet, ieee80211_tx_pending);
tasklet_setup(&local->wake_txqs_tasklet, ieee80211_wake_txqs);
tasklet_setup(&local->tasklet, ieee80211_tasklet_handler);
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 07/30] wifi: mac80211: rename vif->txq_mgmt to vif->txq_nan_dw
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (5 preceding siblings ...)
2026-11-02 20:22 ` [PATCH 06/30] wifi: mac80211: remove aggregation queue stop reason Johannes Berg
@ 2026-11-02 20:22 ` Johannes Berg
2026-11-02 20:22 ` [PATCH 08/30] wifi: mac80211: use vlan-aware station lookup for pending TX Johannes Berg
` (22 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:22 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
This TXQ only exists for NAN to hold packets for the DW,
and really only for hwsim. Rename it to clarify, we may
have planned to reuse it for something else later, but
that doesn't seem to really work anyway.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
drivers/net/wireless/virtual/mac80211_hwsim_nan.c | 6 +++---
include/net/mac80211.h | 6 ++++--
net/mac80211/iface.c | 8 ++++----
net/mac80211/tx.c | 4 ++--
net/mac80211/util.c | 10 +++++-----
5 files changed, 18 insertions(+), 16 deletions(-)
diff --git a/drivers/net/wireless/virtual/mac80211_hwsim_nan.c b/drivers/net/wireless/virtual/mac80211_hwsim_nan.c
index 6f90856099e8..733806e9e46b 100644
--- a/drivers/net/wireless/virtual/mac80211_hwsim_nan.c
+++ b/drivers/net/wireless/virtual/mac80211_hwsim_nan.c
@@ -1197,7 +1197,7 @@ static void mac80211_hwsim_nan_resume_txqs(struct mac80211_hwsim_data *data)
/* Check if management queue can transmit */
if (mac80211_hwsim_nan_txq_transmitting(data->hw,
- data->nan.device_vif->txq_mgmt))
+ data->nan.device_vif->txq_nan_dw))
goto resume_txqs_timer;
/* Check if any STA queue can transmit */
@@ -1253,9 +1253,9 @@ mac80211_hwsim_nan_resume_txqs_timer(struct hrtimer *timer)
/* Wake TX queue for management frames on the NAN device interface */
if (mac80211_hwsim_nan_txq_transmitting(data->hw,
- data->nan.device_vif->txq_mgmt))
+ data->nan.device_vif->txq_nan_dw))
ieee80211_hwsim_wake_tx_queue(data->hw,
- data->nan.device_vif->txq_mgmt);
+ data->nan.device_vif->txq_nan_dw);
/* Wake TX queues for all stations */
ieee80211_iterate_stations_atomic(data->hw,
diff --git a/include/net/mac80211.h b/include/net/mac80211.h
index 76ff86ff9974..ddaa311f6208 100644
--- a/include/net/mac80211.h
+++ b/include/net/mac80211.h
@@ -2225,7 +2225,9 @@ enum ieee80211_neg_ttlm_res {
* @drv_priv: data area for driver use, will always be aligned to
* sizeof(void \*).
* @txq: the multicast data TX queue
- * @txq_mgmt: the mgmt frame TX queue, currently only exists for NAN devices
+ * @txq_nan_dw: TX queue for NAN frames without a station, which the driver
+ * should hold until the next discovery window (DW) or according to the
+ * NAN schedule; only exists for NAN device interfaces
* @offload_flags: 802.3 -> 802.11 enapsulation offload flags, see
* &enum ieee80211_offload_flags.
*/
@@ -2244,7 +2246,7 @@ struct ieee80211_vif {
u8 hw_queue[IEEE80211_NUM_ACS];
struct ieee80211_txq *txq;
- struct ieee80211_txq *txq_mgmt;
+ struct ieee80211_txq *txq_nan_dw;
netdev_features_t netdev_features;
u32 driver_flags;
diff --git a/net/mac80211/iface.c b/net/mac80211/iface.c
index 8bfd6bc9c9fe..a4f29444ba45 100644
--- a/net/mac80211/iface.c
+++ b/net/mac80211/iface.c
@@ -711,9 +711,9 @@ static void ieee80211_do_stop(struct ieee80211_sub_if_data *sdata, bool going_do
if (sdata->vif.txq)
ieee80211_txq_purge(sdata->local, to_txq_info(sdata->vif.txq));
- if (sdata->vif.txq_mgmt)
+ if (sdata->vif.txq_nan_dw)
ieee80211_txq_purge(sdata->local,
- to_txq_info(sdata->vif.txq_mgmt));
+ to_txq_info(sdata->vif.txq_nan_dw));
sdata->bss = NULL;
@@ -2450,9 +2450,9 @@ void ieee80211_if_remove(struct ieee80211_sub_if_data *sdata)
if (sdata->vif.txq)
ieee80211_txq_purge(sdata->local, to_txq_info(sdata->vif.txq));
- if (sdata->vif.txq_mgmt)
+ if (sdata->vif.txq_nan_dw)
ieee80211_txq_purge(sdata->local,
- to_txq_info(sdata->vif.txq_mgmt));
+ to_txq_info(sdata->vif.txq_nan_dw));
synchronize_rcu();
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index b945bd2fcb46..3eff5e934f7e 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -1305,7 +1305,7 @@ static struct txq_info *ieee80211_get_txq(struct ieee80211_local *local,
} else if ((!ieee80211_is_mgmt(hdr->frame_control) ||
ieee80211_is_bufferable_mmpdu(skb)) &&
!sta) {
- txq = vif->txq_mgmt;
+ txq = vif->txq_nan_dw;
}
} else if (sta) {
u8 tid = skb->priority & IEEE80211_QOS_CTL_TID_MASK;
@@ -1501,7 +1501,7 @@ void ieee80211_txq_init(struct ieee80211_sub_if_data *sdata,
txqi->txq.tid = tid;
if (tid == IEEE80211_NUM_TIDS) {
- sdata->vif.txq_mgmt = &txqi->txq;
+ sdata->vif.txq_nan_dw = &txqi->txq;
txqi->txq.ac = IEEE80211_AC_VO;
} else {
sdata->vif.txq = &txqi->txq;
diff --git a/net/mac80211/util.c b/net/mac80211/util.c
index 44feb80274c6..181e4d787837 100644
--- a/net/mac80211/util.c
+++ b/net/mac80211/util.c
@@ -370,18 +370,18 @@ static void __ieee80211_wake_txqs(struct ieee80211_sub_if_data *sdata, int ac)
if (vif->txq) {
txqi = to_txq_info(vif->txq);
- /* txq and txq_mgmt are mutually exclusive */
- WARN_ON_ONCE(vif->txq_mgmt);
+ /* txq and txq_nan_dw are mutually exclusive */
+ WARN_ON_ONCE(vif->txq_nan_dw);
if (!test_and_clear_bit(IEEE80211_TXQ_DIRTY, &txqi->flags) ||
(ps && atomic_read(&ps->num_sta_ps)) ||
ac != vif->txq->ac)
txqi = NULL;
- } else if (vif->txq_mgmt) {
- txqi = to_txq_info(vif->txq_mgmt);
+ } else if (vif->txq_nan_dw) {
+ txqi = to_txq_info(vif->txq_nan_dw);
if (!test_and_clear_bit(IEEE80211_TXQ_DIRTY, &txqi->flags) ||
- ac != vif->txq_mgmt->ac)
+ ac != vif->txq_nan_dw->ac)
txqi = NULL;
}
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 08/30] wifi: mac80211: use vlan-aware station lookup for pending TX
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (6 preceding siblings ...)
2026-11-02 20:22 ` [PATCH 07/30] wifi: mac80211: rename vif->txq_mgmt to vif->txq_nan_dw Johannes Berg
@ 2026-11-02 20:22 ` Johannes Berg
2026-11-02 20:22 ` [PATCH 09/30] wifi: mac80211: stop tracking TXQ-buffered TIDs for PS STAs Johannes Berg
` (21 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:22 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
Frames on the pending queue that have been processed already
still need a station pointer, and we're looking that up with
sta_info_get(). That's fine now, but ieee80211_tx_dequeue()
switches AP_VLAN to AP interfaces, and for powersave I want
to release frames from TXQs via pending, so the lookup must
be vlan-aware.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/tx.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 3eff5e934f7e..25ab54a89235 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -5011,7 +5011,7 @@ static bool ieee80211_tx_pending_skb(struct ieee80211_local *local,
__skb_queue_tail(&skbs, skb);
hdr = (struct ieee80211_hdr *)skb->data;
- sta = sta_info_get(sdata, hdr->addr1);
+ sta = sta_info_get_bss(sdata, hdr->addr1);
result = __ieee80211_tx(local, &skbs, sta, true);
}
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 09/30] wifi: mac80211: stop tracking TXQ-buffered TIDs for PS STAs
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (7 preceding siblings ...)
2026-11-02 20:22 ` [PATCH 08/30] wifi: mac80211: use vlan-aware station lookup for pending TX Johannes Berg
@ 2026-11-02 20:22 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 10/30] wifi: mac80211: release frames from TXQs to PS stations Johannes Berg
` (20 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:22 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
The txq_buffered_tids bitmap is initialized when the station goes
to sleep, not updated after. Frames are normally not put onto the
TXQs while a station is sleeping, but it might race (and thus get
it wrong), it's not updated when the TXQ is empty (drivers can
release frames from TXQs for PS-Poll etc.), and I want to switch
PS buffering to happen on TXQs.
Remove the bitmap, calculate the information when needed, and
update the TIM when we queue a frame onto a previously empty TXQ
as well as when sending a PS null response (as the TIM bitmap may
have been wrong in that case).
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/rx.c | 2 --
net/mac80211/sta_info.c | 40 +++++++++++++++++++++++++++++++++++-----
net/mac80211/sta_info.h | 2 --
net/mac80211/tx.c | 12 ++++++++++++
4 files changed, 47 insertions(+), 9 deletions(-)
diff --git a/net/mac80211/rx.c b/net/mac80211/rx.c
index ee1f33178d26..fede61d606fb 100644
--- a/net/mac80211/rx.c
+++ b/net/mac80211/rx.c
@@ -1692,8 +1692,6 @@ static void sta_ps_start(struct sta_info *sta)
if (!list_empty(&txqi->schedule_order))
list_del_init(&txqi->schedule_order);
spin_unlock(&local->active_txq_lock[txq->ac]);
-
- assign_bit(tid, &sta->txq_buffered_tids, txq_has_queue(txq));
}
sta_info_recalc_tim(sta);
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index e9ca1f26a6bd..8e4b506b33a0 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -1099,9 +1099,33 @@ static unsigned long ieee80211_tids_for_ac(int ac)
}
}
+static unsigned long ieee80211_sta_txq_buffered_tids(struct sta_info *sta)
+{
+ struct fq *fq = &sta->local->fq;
+ unsigned long tids = 0;
+ int tid;
+
+ if (!test_sta_flag(sta, WLAN_STA_PS_STA) &&
+ !test_sta_flag(sta, WLAN_STA_PS_DRIVER))
+ return 0;
+
+ /* synchronize with enqueue */
+ spin_lock_bh(&fq->lock);
+ for (tid = 0; tid < IEEE80211_NUM_TIDS; tid++) {
+ struct ieee80211_txq *txq = sta->sta.txq[tid];
+
+ if (txq && txq_has_queue(txq))
+ tids |= BIT(tid);
+ }
+ spin_unlock_bh(&fq->lock);
+
+ return tids;
+}
+
static void __sta_info_recalc_tim(struct sta_info *sta, bool ignore_pending)
{
struct ieee80211_local *local = sta->local;
+ unsigned long txq_buffered_tids;
struct ps_data *ps;
bool indicate_tim = false;
u8 ignore_for_tim = sta->sta.uapsd_queues;
@@ -1141,6 +1165,8 @@ static void __sta_info_recalc_tim(struct sta_info *sta, bool ignore_pending)
if (ignore_pending)
ignore_for_tim = BIT(IEEE80211_NUM_ACS) - 1;
+ txq_buffered_tids = ieee80211_sta_txq_buffered_tids(sta);
+
for (ac = 0; ac < IEEE80211_NUM_ACS; ac++) {
unsigned long tids;
@@ -1156,8 +1182,7 @@ static void __sta_info_recalc_tim(struct sta_info *sta, bool ignore_pending)
indicate_tim |=
sta->driver_buffered_tids & tids;
- indicate_tim |=
- sta->txq_buffered_tids & tids;
+ indicate_tim |= txq_buffered_tids & tids;
}
done:
@@ -1855,7 +1880,6 @@ void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta)
BUILD_BUG_ON(BITS_TO_LONGS(IEEE80211_NUM_TIDS) > 1);
sta->driver_buffered_tids = 0;
- sta->txq_buffered_tids = 0;
if (!ieee80211_hw_check(&local->hw, AP_LINK_PS))
drv_sta_notify(local, sdata, STA_NOTIFY_AWAKE, &sta->sta);
@@ -2051,6 +2075,7 @@ static void
ieee80211_sta_ps_get_frames(struct sta_info *sta, int n_frames, u8 ignored_acs,
enum ieee80211_frame_release_type reason,
struct sk_buff_head *frames,
+ unsigned long txq_buffered_tids,
unsigned long *driver_release_tids)
{
struct ieee80211_sub_if_data *sdata = sta->sdata;
@@ -2072,7 +2097,7 @@ ieee80211_sta_ps_get_frames(struct sta_info *sta, int n_frames, u8 ignored_acs,
if (skb_queue_empty(frames)) {
*driver_release_tids |=
sta->driver_buffered_tids & tids;
- *driver_release_tids |= sta->txq_buffered_tids & tids;
+ *driver_release_tids |= txq_buffered_tids & tids;
}
if (!*driver_release_tids) {
@@ -2120,7 +2145,9 @@ ieee80211_sta_ps_deliver_response(struct sta_info *sta,
__skb_queue_head_init(&frames);
ieee80211_sta_ps_get_frames(sta, n_frames, ignored_acs, reason,
- &frames, &driver_release_tids);
+ &frames,
+ ieee80211_sta_txq_buffered_tids(sta),
+ &driver_release_tids);
more_data = ieee80211_sta_ps_more_data(sta, ignored_acs, reason, driver_release_tids);
@@ -2153,6 +2180,9 @@ ieee80211_sta_ps_deliver_response(struct sta_info *sta,
tid = 7 - 2 * ac;
ieee80211_send_null_response(sta, tid, reason, true, false);
+
+ /* the TIM bit may have been stale, e.g. due to a TXQ purge */
+ sta_info_recalc_tim(sta);
} else if (!driver_release_tids) {
struct sk_buff_head pending;
struct sk_buff *skb;
diff --git a/net/mac80211/sta_info.h b/net/mac80211/sta_info.h
index ebc70d2ad637..f3c22806cf6c 100644
--- a/net/mac80211/sta_info.h
+++ b/net/mac80211/sta_info.h
@@ -662,7 +662,6 @@ struct ieee80211_sta_removed_link_stats {
* entered power saving state, these are also delivered to
* the station when it leaves powersave or polls for frames
* @driver_buffered_tids: bitmap of TIDs the driver has data buffered on
- * @txq_buffered_tids: bitmap of TIDs that mac80211 has txq data buffered on
* @assoc_at: clock boottime (in ns) of last association
* @last_connected: time (in seconds) when a station got connected
* @last_seq_ctrl: last received seq/frag number from this STA (per TID
@@ -749,7 +748,6 @@ struct sta_info {
struct sk_buff_head ps_tx_buf[IEEE80211_NUM_ACS];
struct sk_buff_head tx_filtered[IEEE80211_NUM_ACS];
unsigned long driver_buffered_tids;
- unsigned long txq_buffered_tids;
u64 assoc_at;
long last_connected;
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 25ab54a89235..3304a3a9d144 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -1428,6 +1428,7 @@ static void ieee80211_txq_enqueue(struct ieee80211_local *local,
{
struct fq *fq = &local->fq;
struct fq_tin *tin = &txqi->tin;
+ struct sta_info *sta = NULL;
u32 flow_idx;
ieee80211_set_skb_enqueue_time(skb);
@@ -1444,11 +1445,22 @@ static void ieee80211_txq_enqueue(struct ieee80211_local *local,
IEEE80211_TX_INTCFL_NEED_TXPROCESSING;
__skb_queue_tail(&txqi->frags, skb);
} else {
+ /* TXQ becomes non-empty, update TIM if STA is sleeping */
+ if (txqi->txq.sta && !txq_has_queue(&txqi->txq)) {
+ sta = container_of(txqi->txq.sta, struct sta_info, sta);
+ if (!test_sta_flag(sta, WLAN_STA_PS_STA) &&
+ !test_sta_flag(sta, WLAN_STA_PS_DRIVER))
+ sta = NULL;
+ }
+
flow_idx = fq_flow_idx(fq, skb);
fq_tin_enqueue(fq, tin, flow_idx, skb,
fq_skb_free_func);
}
spin_unlock_bh(&fq->lock);
+
+ if (sta)
+ sta_info_recalc_tim(sta);
}
static bool fq_vlan_filter_func(struct fq *fq, struct fq_tin *tin,
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 10/30] wifi: mac80211: release frames from TXQs to PS stations
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (8 preceding siblings ...)
2026-11-02 20:22 ` [PATCH 09/30] wifi: mac80211: stop tracking TXQ-buffered TIDs for PS STAs Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 11/30] wifi: mac80211: mesh: release frames from TXQs sleeping peers Johannes Berg
` (19 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
When a station goes to sleep, frames may be on its TXQs.
These are indicated in the TIM but for most drivers are
never released. Since the goal is to buffer frames on
TXQs start here by releasing them when requested by the
station.
Also consider them for the more-data bit, and document this.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
include/net/mac80211.h | 11 +++-
net/mac80211/ieee80211_i.h | 2 +
net/mac80211/sta_info.c | 116 +++++++++++++++++++++++++++++++------
net/mac80211/tx.c | 29 ++++++++--
4 files changed, 132 insertions(+), 26 deletions(-)
diff --git a/include/net/mac80211.h b/include/net/mac80211.h
index ddaa311f6208..a144f3fd5203 100644
--- a/include/net/mac80211.h
+++ b/include/net/mac80211.h
@@ -3771,7 +3771,8 @@ void ieee80211_purge_tx_queue(struct ieee80211_hw *hw,
* the station sends a PS-Poll or a uAPSD trigger frame, mac80211
* will inform the driver of this with the @allow_buffered_frames
* callback; this callback is optional. mac80211 will then transmit
- * the frames as usual and set the %IEEE80211_TX_CTL_NO_PS_BUFFER
+ * the frames, including frames it took from the station's TXQs,
+ * via the @tx callback and set the %IEEE80211_TX_CTL_NO_PS_BUFFER
* on each frame. The last frame in the service period (or the only
* response to a PS-Poll) also has %IEEE80211_TX_STATUS_EOSP set to
* indicate that it ends the service period; as this frame must have
@@ -3809,6 +3810,12 @@ void ieee80211_purge_tx_queue(struct ieee80211_hw *hw,
* from are ignored since mac80211 doesn't know how many frames the
* buffers for those TIDs contain.
*
+ * If the driver doesn't implement @allow_buffered_frames, the TIDs passed
+ * to @release_buffered_frames also include those with frames on the
+ * station's TXQs, and the driver should release these by dequeuing them
+ * with ieee80211_tx_dequeue(). Otherwise, mac80211 releases the frames
+ * on the TXQs itself, in a separate service period.
+ *
* If the driver also implement GO mode, where absence periods may
* shorten service periods (or abort PS-Poll responses), it must
* filter those response frames except in the case of frames that
@@ -4508,6 +4515,8 @@ struct ieee80211_prep_tx_info {
* more-data bit must always be set.
* The @tids parameter tells the driver which TIDs to release frames
* from, for PS-poll it will always have only a single bit set.
+ * If @allow_buffered_frames isn't implemented, frames for these TIDs
+ * may also be on the station's TXQs and should be released from there.
* In the case this is used for a PS-poll initiated release, the
* @num_frames parameter will always be 1 so code can be shared. In
* this case the driver must also set %IEEE80211_TX_STATUS_EOSP flag
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index e05eadebbf8f..5045144641a4 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -2722,6 +2722,8 @@ void ieee80211_txq_teardown_flows(struct ieee80211_local *local);
void ieee80211_txq_init(struct ieee80211_sub_if_data *sdata,
struct sta_info *sta,
struct txq_info *txq, int tid);
+struct sk_buff *ieee80211_tx_dequeue_ps_release(struct ieee80211_local *local,
+ struct txq_info *txqi);
void ieee80211_txq_purge(struct ieee80211_local *local,
struct txq_info *txqi);
void ieee80211_purge_sta_txqs(struct sta_info *sta);
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index 8e4b506b33a0..78a4c7d7ed1b 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -2048,6 +2048,7 @@ ieee80211_sta_ps_more_data(struct sta_info *sta, u8 ignored_acs,
enum ieee80211_frame_release_type reason,
unsigned long driver_release_tids)
{
+ unsigned long buffered_tids;
int ac;
/* If the driver has data on more than one TID then
@@ -2059,26 +2060,99 @@ ieee80211_sta_ps_more_data(struct sta_info *sta, u8 ignored_acs,
hweight16(driver_release_tids) > 1)
return true;
+ buffered_tids = sta->driver_buffered_tids |
+ ieee80211_sta_txq_buffered_tids(sta);
+ buffered_tids &= ~driver_release_tids;
+
for (ac = 0; ac < IEEE80211_NUM_ACS; ac++) {
if (ignored_acs & ieee80211_ac_to_qos_mask[ac])
continue;
if (!skb_queue_empty(&sta->tx_filtered[ac]) ||
- !skb_queue_empty(&sta->ps_tx_buf[ac]))
+ !skb_queue_empty(&sta->ps_tx_buf[ac]) ||
+ buffered_tids & ieee80211_tids_for_ac(ac))
return true;
}
return false;
}
+static bool ieee80211_sta_ps_from_txqs(const struct sta_info *sta)
+{
+ struct ieee80211_local *local = sta->local;
+
+ if (sta->sdata->vif.offload_flags & IEEE80211_OFFLOAD_ENCAP_ENABLED)
+ return false;
+
+ return !local->ops->release_buffered_frames ||
+ local->ops->allow_buffered_frames;
+}
+
+/* multiple fragments count as a single buffer unit (BU) */
+static bool ieee80211_sta_ps_frame_ends_bu(struct sk_buff *skb)
+{
+ struct ieee80211_hdr *hdr = (void *)skb->data;
+
+ if (IEEE80211_SKB_CB(skb)->flags & IEEE80211_TX_CTL_HW_80211_ENCAP)
+ return true;
+
+ return !ieee80211_has_morefrags(hdr->frame_control);
+}
+
+static int ieee80211_sta_ps_dequeue_txqs(const struct sta_info *sta,
+ unsigned long tids, int n_frames,
+ struct sk_buff_head *frames)
+{
+ struct ieee80211_local *local = sta->local;
+
+ local_bh_disable();
+ rcu_read_lock();
+
+ while (n_frames > 0 && tids) {
+ int tid = find_highest_prio_tid(tids);
+ struct txq_info *txq;
+ struct sk_buff *skb;
+
+ txq = to_txq_info(sta->sta.txq[tid]);
+ skb = ieee80211_tx_dequeue_ps_release(local, txq);
+ if (!skb) {
+ tids &= ~BIT(tid);
+ continue;
+ }
+
+ __skb_queue_tail(frames, skb);
+ if (ieee80211_sta_ps_frame_ends_bu(skb))
+ n_frames--;
+ }
+
+ rcu_read_unlock();
+ local_bh_enable();
+
+ return n_frames;
+}
+
+static int ieee80211_sta_ps_dequeue(struct sk_buff_head *queue, int n_frames,
+ struct sk_buff_head *frames)
+{
+ struct sk_buff *skb;
+
+ while (n_frames > 0 && (skb = skb_dequeue(queue))) {
+ __skb_queue_tail(frames, skb);
+ n_frames--;
+ }
+
+ return n_frames;
+}
+
static void
ieee80211_sta_ps_get_frames(struct sta_info *sta, int n_frames, u8 ignored_acs,
enum ieee80211_frame_release_type reason,
struct sk_buff_head *frames,
- unsigned long txq_buffered_tids,
unsigned long *driver_release_tids)
{
+ unsigned long txq_buffered_tids = ieee80211_sta_txq_buffered_tids(sta);
struct ieee80211_sub_if_data *sdata = sta->sdata;
+ bool from_txqs = ieee80211_sta_ps_from_txqs(sta);
struct ieee80211_local *local = sdata->local;
int ac;
@@ -2097,25 +2171,27 @@ ieee80211_sta_ps_get_frames(struct sta_info *sta, int n_frames, u8 ignored_acs,
if (skb_queue_empty(frames)) {
*driver_release_tids |=
sta->driver_buffered_tids & tids;
- *driver_release_tids |= txq_buffered_tids & tids;
+ if (!from_txqs)
+ *driver_release_tids |= txq_buffered_tids & tids;
}
if (!*driver_release_tids) {
- struct sk_buff *skb;
+ int remaining;
- while (n_frames > 0) {
- skb = skb_dequeue(&sta->tx_filtered[ac]);
- if (!skb) {
- skb = skb_dequeue(
- &sta->ps_tx_buf[ac]);
- if (skb)
- local->total_ps_buffered--;
- }
- if (!skb)
- break;
- n_frames--;
- __skb_queue_tail(frames, skb);
+ n_frames = ieee80211_sta_ps_dequeue(&sta->tx_filtered[ac],
+ n_frames, frames);
+ if (from_txqs) {
+ unsigned long txq_tids = txq_buffered_tids & tids;
+ n_frames = ieee80211_sta_ps_dequeue_txqs(sta,
+ txq_tids,
+ n_frames,
+ frames);
}
+
+ remaining = n_frames;
+ n_frames = ieee80211_sta_ps_dequeue(&sta->ps_tx_buf[ac],
+ n_frames, frames);
+ local->total_ps_buffered -= remaining - n_frames;
}
/* If we have more frames buffered on this AC, then abort the
@@ -2125,6 +2201,10 @@ ieee80211_sta_ps_get_frames(struct sta_info *sta, int n_frames, u8 ignored_acs,
if (!skb_queue_empty(&sta->tx_filtered[ac]) ||
!skb_queue_empty(&sta->ps_tx_buf[ac]))
break;
+
+ if (from_txqs &&
+ ieee80211_sta_txq_buffered_tids(sta) & tids)
+ break;
}
}
@@ -2145,9 +2225,7 @@ ieee80211_sta_ps_deliver_response(struct sta_info *sta,
__skb_queue_head_init(&frames);
ieee80211_sta_ps_get_frames(sta, n_frames, ignored_acs, reason,
- &frames,
- ieee80211_sta_txq_buffered_tids(sta),
- &driver_release_tids);
+ &frames, &driver_release_tids);
more_data = ieee80211_sta_ps_more_data(sta, ignored_acs, reason, driver_release_tids);
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 3304a3a9d144..9c58faf571f4 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -3903,8 +3903,9 @@ static bool ieee80211_drop_unauth_port(struct ieee80211_tx_data *tx)
return true;
}
-struct sk_buff *ieee80211_tx_dequeue(struct ieee80211_hw *hw,
- struct ieee80211_txq *txq)
+static struct sk_buff *__ieee80211_tx_dequeue(struct ieee80211_hw *hw,
+ struct ieee80211_txq *txq,
+ bool ps_release)
{
struct ieee80211_local *local = hw_to_local(hw);
struct txq_info *txqi = container_of(txq, struct txq_info, txq);
@@ -3922,13 +3923,17 @@ struct sk_buff *ieee80211_tx_dequeue(struct ieee80211_hw *hw,
WARN_ON_ONCE(softirq_count() == 0);
- if (!ieee80211_txq_airtime_check(hw, txq))
+ if (!ps_release && !ieee80211_txq_airtime_check(hw, txq))
return NULL;
begin:
- spin_lock_irqsave(&local->queue_stop_reason_lock, flags);
- q_stopped = local->queue_stop_reasons[q];
- spin_unlock_irqrestore(&local->queue_stop_reason_lock, flags);
+ if (ps_release) {
+ q_stopped = false;
+ } else {
+ spin_lock_irqsave(&local->queue_stop_reason_lock, flags);
+ q_stopped = local->queue_stop_reasons[q];
+ spin_unlock_irqrestore(&local->queue_stop_reason_lock, flags);
+ }
if (unlikely(q_stopped)) {
/* mark for waking later */
@@ -4096,8 +4101,20 @@ struct sk_buff *ieee80211_tx_dequeue(struct ieee80211_hw *hw,
return skb;
}
+
+struct sk_buff *ieee80211_tx_dequeue(struct ieee80211_hw *hw,
+ struct ieee80211_txq *txq)
+{
+ return __ieee80211_tx_dequeue(hw, txq, false);
+}
EXPORT_SYMBOL(ieee80211_tx_dequeue);
+struct sk_buff *ieee80211_tx_dequeue_ps_release(struct ieee80211_local *local,
+ struct txq_info *txqi)
+{
+ return __ieee80211_tx_dequeue(&local->hw, &txqi->txq, true);
+}
+
static inline s32 ieee80211_sta_deficit(struct sta_info *sta, u8 ac)
{
struct airtime_info *air_info = &sta->airtime[ac];
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 11/30] wifi: mac80211: mesh: release frames from TXQs sleeping peers
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (9 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 10/30] wifi: mac80211: release frames from TXQs to PS stations Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 12/30] wifi: mac80211: don't TX from TXQs of sleeping STAs Johannes Berg
` (18 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
Frames may be held on the TXQs of a mesh peer in powersave,
e.g. frames that were already queued when the peer entered
powersave. Release them in mesh peer service periods, and
consider them for more-data and service periods.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/mesh_ps.c | 42 +++++++++++++++++++++++++++++------------
net/mac80211/sta_info.c | 9 ++++-----
net/mac80211/sta_info.h | 4 ++++
3 files changed, 38 insertions(+), 17 deletions(-)
diff --git a/net/mac80211/mesh_ps.c b/net/mac80211/mesh_ps.c
index 0ec207ee845b..0916de33e6a9 100644
--- a/net/mac80211/mesh_ps.c
+++ b/net/mac80211/mesh_ps.c
@@ -458,6 +458,7 @@ static void mpsp_qos_null_append(struct sta_info *sta,
static void mps_frame_deliver(struct sta_info *sta, int n_frames)
{
struct ieee80211_local *local = sta->sdata->local;
+ unsigned long txq_buffered_tids;
int ac;
struct sk_buff_head frames;
struct sk_buff *skb;
@@ -465,18 +466,29 @@ static void mps_frame_deliver(struct sta_info *sta, int n_frames)
skb_queue_head_init(&frames);
- /* collect frame(s) from buffers */
+ if (n_frames < 0)
+ n_frames = INT_MAX;
+
+ txq_buffered_tids = ieee80211_sta_txq_buffered_tids(sta);
+
+ /*
+ * collect frame(s) from buffers, frames on the TXQs were queued
+ * before those on the PS buffer
+ */
for (ac = 0; ac < IEEE80211_NUM_ACS; ac++) {
- while (n_frames != 0) {
- skb = skb_dequeue(&sta->tx_filtered[ac]);
- if (!skb) {
- skb = skb_dequeue(
- &sta->ps_tx_buf[ac]);
- if (skb)
- local->total_ps_buffered--;
- }
- if (!skb)
- break;
+ while (n_frames > 0 &&
+ (skb = skb_dequeue(&sta->tx_filtered[ac]))) {
+ n_frames--;
+ __skb_queue_tail(&frames, skb);
+ }
+
+ n_frames = ieee80211_sta_ps_dequeue_txqs(sta,
+ txq_buffered_tids & ieee80211_tids_for_ac(ac),
+ n_frames, &frames);
+
+ while (n_frames > 0 &&
+ (skb = skb_dequeue(&sta->ps_tx_buf[ac]))) {
+ local->total_ps_buffered--;
n_frames--;
__skb_queue_tail(&frames, skb);
}
@@ -486,6 +498,9 @@ static void mps_frame_deliver(struct sta_info *sta, int n_frames)
more_data = true;
}
+ if (ieee80211_sta_txq_buffered_tids(sta))
+ more_data = true;
+
/* nothing to send? -> EOSP */
if (skb_queue_empty(&frames)) {
mpsp_trigger_send(sta, false, true);
@@ -599,10 +614,13 @@ void ieee80211_mps_frame_release(struct sta_info *sta,
(!elems->awake_window || !get_unaligned_le16(elems->awake_window)))
return;
- if (!test_sta_flag(sta, WLAN_STA_MPSP_OWNER))
+ if (!test_sta_flag(sta, WLAN_STA_MPSP_OWNER)) {
for (ac = 0; ac < IEEE80211_NUM_ACS; ac++)
buffer_local += skb_queue_len(&sta->ps_tx_buf[ac]) +
skb_queue_len(&sta->tx_filtered[ac]);
+ if (ieee80211_sta_txq_buffered_tids(sta))
+ buffer_local++;
+ }
if (!has_buffered && !buffer_local)
return;
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index 78a4c7d7ed1b..7175dfa41b01 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -1081,7 +1081,7 @@ static inline bool __bss_tim_get(u8 *tim, u16 id)
return tim[id / 8] & (1 << (id % 8));
}
-static unsigned long ieee80211_tids_for_ac(int ac)
+unsigned long ieee80211_tids_for_ac(int ac)
{
/* If we ever support TIDs > 7, this obviously needs to be adjusted */
switch (ac) {
@@ -1099,7 +1099,7 @@ static unsigned long ieee80211_tids_for_ac(int ac)
}
}
-static unsigned long ieee80211_sta_txq_buffered_tids(struct sta_info *sta)
+unsigned long ieee80211_sta_txq_buffered_tids(struct sta_info *sta)
{
struct fq *fq = &sta->local->fq;
unsigned long tids = 0;
@@ -2099,9 +2099,8 @@ static bool ieee80211_sta_ps_frame_ends_bu(struct sk_buff *skb)
return !ieee80211_has_morefrags(hdr->frame_control);
}
-static int ieee80211_sta_ps_dequeue_txqs(const struct sta_info *sta,
- unsigned long tids, int n_frames,
- struct sk_buff_head *frames)
+int ieee80211_sta_ps_dequeue_txqs(const struct sta_info *sta, unsigned long tids,
+ int n_frames, struct sk_buff_head *frames)
{
struct ieee80211_local *local = sta->local;
diff --git a/net/mac80211/sta_info.h b/net/mac80211/sta_info.h
index f3c22806cf6c..8edb95ccbfab 100644
--- a/net/mac80211/sta_info.h
+++ b/net/mac80211/sta_info.h
@@ -1004,6 +1004,10 @@ int ieee80211_sta_activate_link(struct sta_info *sta, unsigned int link_id);
void ieee80211_sta_remove_link(struct sta_info *sta, unsigned int link_id);
void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta);
+unsigned long ieee80211_tids_for_ac(int ac);
+unsigned long ieee80211_sta_txq_buffered_tids(struct sta_info *sta);
+int ieee80211_sta_ps_dequeue_txqs(const struct sta_info *sta, unsigned long tids,
+ int n_frames, struct sk_buff_head *frames);
void ieee80211_sta_ps_deliver_poll_response(struct sta_info *sta);
void ieee80211_sta_ps_deliver_uapsd(struct sta_info *sta);
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 12/30] wifi: mac80211: don't TX from TXQs of sleeping STAs
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (10 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 11/30] wifi: mac80211: mesh: release frames from TXQs sleeping peers Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 13/30] wifi: mac80211: don't apply CoDel to PS-buffered frames Johannes Berg
` (17 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
While a station is in powersave, frames still on its
TXQs are transmitted if the driver pulls from the TXQ,
but this isn't really right, it should only be done if
the station wakes up or is polling for them.
Disallow pulling from a TXQ when the station is asleep
unless it's a poll response, and document this.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
include/net/mac80211.h | 16 ++++++++++++----
net/mac80211/mesh_ps.c | 1 +
net/mac80211/sta_info.c | 8 ++++++++
net/mac80211/sta_info.h | 3 +++
net/mac80211/tx.c | 31 +++++++++++++++++++++++++++++++
5 files changed, 55 insertions(+), 4 deletions(-)
diff --git a/include/net/mac80211.h b/include/net/mac80211.h
index a144f3fd5203..1e8f799193dc 100644
--- a/include/net/mac80211.h
+++ b/include/net/mac80211.h
@@ -3764,10 +3764,16 @@ void ieee80211_purge_tx_queue(struct ieee80211_hw *hw,
* driver is also notified of powersave transitions by calling its
* @sta_notify callback.
*
+ * While the station is asleep (or blocked, see below), frames that are
+ * already on its TXQs remain there: the TXQs aren't scheduled, and
+ * ieee80211_tx_dequeue() doesn't return frames from them, except when
+ * they're released as described below.
+ *
* When the station is asleep, it has three choices: it can wake up,
* it can PS-Poll, or it can possibly start a uAPSD service period.
* Waking up is implemented by simply transmitting all buffered (and
- * filtered) frames to the station. This is the easiest case. When
+ * filtered) frames to the station, and scheduling its TXQs again.
+ * This is the easiest case. When
* the station sends a PS-Poll or a uAPSD trigger frame, mac80211
* will inform the driver of this with the @allow_buffered_frames
* callback; this callback is optional. mac80211 will then transmit
@@ -3812,8 +3818,9 @@ void ieee80211_purge_tx_queue(struct ieee80211_hw *hw,
*
* If the driver doesn't implement @allow_buffered_frames, the TIDs passed
* to @release_buffered_frames also include those with frames on the
- * station's TXQs, and the driver should release these by dequeuing them
- * with ieee80211_tx_dequeue(). Otherwise, mac80211 releases the frames
+ * station's TXQs, and the driver must release these by dequeuing them
+ * with ieee80211_tx_dequeue() during that call, it can't dequeue them
+ * at any other time. Otherwise, mac80211 releases the frames
* on the TXQs itself, in a separate service period.
*
* If the driver also implement GO mode, where absence periods may
@@ -4516,7 +4523,8 @@ struct ieee80211_prep_tx_info {
* The @tids parameter tells the driver which TIDs to release frames
* from, for PS-poll it will always have only a single bit set.
* If @allow_buffered_frames isn't implemented, frames for these TIDs
- * may also be on the station's TXQs and should be released from there.
+ * may also be on the station's TXQs, and can only be dequeued from
+ * there during this call.
* In the case this is used for a PS-poll initiated release, the
* @num_frames parameter will always be 1 so code can be shared. In
* this case the driver must also set %IEEE80211_TX_STATUS_EOSP flag
diff --git a/net/mac80211/mesh_ps.c b/net/mac80211/mesh_ps.c
index 0916de33e6a9..ddf35cdd558a 100644
--- a/net/mac80211/mesh_ps.c
+++ b/net/mac80211/mesh_ps.c
@@ -525,6 +525,7 @@ static void mps_frame_deliver(struct sta_info *sta, int n_frames)
* exchange.
*/
info->flags |= IEEE80211_TX_CTL_NO_PS_BUFFER;
+ info->control.flags |= IEEE80211_TX_CTRL_PS_RESPONSE;
if (more_data || !skb_queue_is_last(&frames, skb))
hdr->frame_control |=
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index 7175dfa41b01..51504f57128f 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -1877,6 +1877,8 @@ void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta)
return;
clear_sta_flag(sta, WLAN_STA_SP);
+ /* ensure flag clearing is visible to scheduled TXQs */
+ smp_mb__after_atomic();
BUILD_BUG_ON(BITS_TO_LONGS(IEEE80211_NUM_TIDS) > 1);
sta->driver_buffered_tids = 0;
@@ -2366,6 +2368,10 @@ ieee80211_sta_ps_deliver_response(struct sta_info *sta,
} else {
int tid;
+ /* driver handles - allow it to release ... */
+ if (!ieee80211_sta_ps_from_txqs(sta))
+ WRITE_ONCE(sta->ps_release_tids, driver_release_tids);
+
/*
* We need to release a frame that is buffered somewhere in the
* driver ... it'll have to handle that.
@@ -2378,6 +2384,8 @@ ieee80211_sta_ps_deliver_response(struct sta_info *sta,
*/
drv_release_buffered_frames(local, sta, driver_release_tids,
n_frames, reason, more_data);
+ /* ... until here it's not allowed to release any more */
+ WRITE_ONCE(sta->ps_release_tids, 0);
/*
* Note that we don't recalculate the TIM bit here as it would
diff --git a/net/mac80211/sta_info.h b/net/mac80211/sta_info.h
index 8edb95ccbfab..cd3c89a6a25a 100644
--- a/net/mac80211/sta_info.h
+++ b/net/mac80211/sta_info.h
@@ -662,6 +662,8 @@ struct ieee80211_sta_removed_link_stats {
* entered power saving state, these are also delivered to
* the station when it leaves powersave or polls for frames
* @driver_buffered_tids: bitmap of TIDs the driver has data buffered on
+ * @ps_release_tids: TIDs for which the driver can dequeue from TXQs during
+ * during release_buffered_frames(), only non-zero during that
* @assoc_at: clock boottime (in ns) of last association
* @last_connected: time (in seconds) when a station got connected
* @last_seq_ctrl: last received seq/frag number from this STA (per TID
@@ -748,6 +750,7 @@ struct sta_info {
struct sk_buff_head ps_tx_buf[IEEE80211_NUM_ACS];
struct sk_buff_head tx_filtered[IEEE80211_NUM_ACS];
unsigned long driver_buffered_tids;
+ unsigned long ps_release_tids;
u64 assoc_at;
long last_connected;
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 9c58faf571f4..0bd324a4d4cb 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -3903,6 +3903,22 @@ static bool ieee80211_drop_unauth_port(struct ieee80211_tx_data *tx)
return true;
}
+static bool ieee80211_txq_ps_tx_ok(const struct txq_info *txqi)
+{
+ struct sta_info *sta;
+
+ if (!txqi->txq.sta || txqi->txq.tid == IEEE80211_NUM_TIDS)
+ return true;
+
+ sta = container_of(txqi->txq.sta, struct sta_info, sta);
+
+ if (likely(!test_sta_flag(sta, WLAN_STA_PS_STA) &&
+ !test_sta_flag(sta, WLAN_STA_PS_DRIVER)))
+ return true;
+
+ return READ_ONCE(sta->ps_release_tids) & BIT(txqi->txq.tid);
+}
+
static struct sk_buff *__ieee80211_tx_dequeue(struct ieee80211_hw *hw,
struct ieee80211_txq *txq,
bool ps_release)
@@ -3930,6 +3946,9 @@ static struct sk_buff *__ieee80211_tx_dequeue(struct ieee80211_hw *hw,
if (ps_release) {
q_stopped = false;
} else {
+ if (!ieee80211_txq_ps_tx_ok(txqi))
+ return NULL;
+
spin_lock_irqsave(&local->queue_stop_reason_lock, flags);
q_stopped = local->queue_stop_reasons[q];
spin_unlock_irqrestore(&local->queue_stop_reason_lock, flags);
@@ -4169,6 +4188,14 @@ struct ieee80211_txq *ieee80211_next_txq(struct ieee80211_hw *hw, u8 ac)
if (!txqi)
goto out;
+ if (!ieee80211_txq_ps_tx_ok(txqi)) {
+ /* reschedule on station wakeup */
+ list_del_init(&txqi->schedule_order);
+ if (txqi == head)
+ head = NULL;
+ goto begin;
+ }
+
if (txqi == head) {
if (!found_eligible_txq)
goto out;
@@ -4226,6 +4253,7 @@ void __ieee80211_schedule_txq(struct ieee80211_hw *hw,
has_queue = force ||
(!test_bit(IEEE80211_TXQ_STOP, &txqi->flags) &&
+ ieee80211_txq_ps_tx_ok(txqi) &&
txq_has_queue(txq));
if (list_empty(&txqi->schedule_order) &&
(has_queue || ieee80211_txq_keep_active(txqi))) {
@@ -4338,6 +4366,9 @@ bool ieee80211_txq_may_transmit(struct ieee80211_hw *hw,
if (list_empty(&txqi->schedule_order))
goto out;
+ if (!ieee80211_txq_ps_tx_ok(txqi))
+ goto out;
+
if (!ieee80211_txq_schedule_airtime_check(local, ac))
goto out;
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 13/30] wifi: mac80211: don't apply CoDel to PS-buffered frames
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (11 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 12/30] wifi: mac80211: don't TX from TXQs of sleeping STAs Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-06 12:22 ` Toke Høiland-Jørgensen
2026-11-02 20:23 ` [PATCH 14/30] wifi: mac80211: put filtered data frames back on TXQs Johannes Berg
` (16 subsequent siblings)
29 siblings, 1 reply; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
Frames for sleeping stations remain on the TXQs now, applying
CoDel to them would start dropping them due to a long wait on
dequeue, but for PS STAs they will be on the queues for a while.
Bypass CoDel while a STA is sleeping and for a while (10s) or
when the queue drains after it wakes up, and for frames that
were enqueued after the wakeup as well, which really is the
common case.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/ieee80211_i.h | 7 +++++
net/mac80211/sta_info.c | 6 ++++-
net/mac80211/tx.c | 52 ++++++++++++++++++++++++++++++++++++++
3 files changed, 64 insertions(+), 1 deletion(-)
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index 5045144641a4..cb947341fee6 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -978,6 +978,7 @@ enum txq_info_flags {
IEEE80211_TXQ_AMPDU,
IEEE80211_TXQ_NO_AMSDU,
IEEE80211_TXQ_DIRTY,
+ IEEE80211_TXQ_PS_BACKLOG,
};
/**
@@ -990,6 +991,9 @@ enum txq_info_flags {
* @schedule_order: used with ieee80211_local->active_txqs
* @schedule_round: counter to prevent infinite loops on TXQ scheduling
* @flags: TXQ flags from &enum txq_info_flags
+ * @ps_backlog_end: with %IEEE80211_TXQ_PS_BACKLOG, indicates when
+ * the station woke up, i.e. frames before this were just queued
+ * due to powersave
* @txq: the driver visible part
*/
struct txq_info {
@@ -1003,6 +1007,7 @@ struct txq_info {
struct sk_buff_head frags;
unsigned long flags;
+ codel_time_t ps_backlog_end;
/* keep last! */
struct ieee80211_txq txq;
@@ -2724,6 +2729,8 @@ void ieee80211_txq_init(struct ieee80211_sub_if_data *sdata,
struct txq_info *txq, int tid);
struct sk_buff *ieee80211_tx_dequeue_ps_release(struct ieee80211_local *local,
struct txq_info *txqi);
+void ieee80211_txq_ps_backlog_start(struct ieee80211_local *local,
+ struct txq_info *txqi);
void ieee80211_txq_purge(struct ieee80211_local *local,
struct txq_info *txqi);
void ieee80211_purge_sta_txqs(struct sta_info *sta);
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index 51504f57128f..8d79e532b109 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -1889,10 +1889,14 @@ void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta)
/* this can be called in process context, e.g. for mesh */
local_bh_disable();
for (i = 0; i < ARRAY_SIZE(sta->sta.txq); i++) {
+ struct txq_info *txqi;
+
if (!sta->sta.txq[i] || !txq_has_queue(sta->sta.txq[i]))
continue;
- schedule_and_wake_txq(local, to_txq_info(sta->sta.txq[i]));
+ txqi = to_txq_info(sta->sta.txq[i]);
+ ieee80211_txq_ps_backlog_start(local, txqi);
+ schedule_and_wake_txq(local, txqi);
}
local_bh_enable();
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 0bd324a4d4cb..856018cdb617 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -1380,6 +1380,55 @@ static void codel_drop_func(struct sk_buff *skb,
ieee80211_free_txskb(hw, skb);
}
+/* bypass codel since PS can last a long time */
+static bool ieee80211_txq_codel_bypass(struct txq_info *txqi,
+ const struct fq_flow *flow)
+{
+ struct sta_info *sta;
+ struct sk_buff *skb;
+
+ if (!txqi->txq.sta)
+ return false;
+
+ sta = container_of(txqi->txq.sta, struct sta_info, sta);
+ if (test_sta_flag(sta, WLAN_STA_PS_STA) ||
+ test_sta_flag(sta, WLAN_STA_PS_DRIVER) ||
+ test_sta_flag(sta, WLAN_STA_PS_DELIVER))
+ return true;
+
+ if (likely(!test_bit(IEEE80211_TXQ_PS_BACKLOG, &txqi->flags)))
+ return false;
+
+ /*
+ * Stop treating as backlog after it cleared or after 1s
+ * (the latter to avoid time wrap and issues from that).
+ */
+#define IEEE80211_TXQ_MAX_PS_BACKLOG MS2TIME(MSEC_PER_SEC)
+
+ if (!txqi->tin.backlog_packets ||
+ codel_time_after(codel_get_time(),
+ (codel_time_t)(txqi->ps_backlog_end +
+ IEEE80211_TXQ_MAX_PS_BACKLOG))) {
+ clear_bit(IEEE80211_TXQ_PS_BACKLOG, &txqi->flags);
+ return false;
+ }
+
+ skb = skb_peek(&flow->queue);
+ return skb && codel_time_before(codel_skb_time_func(skb),
+ txqi->ps_backlog_end);
+}
+
+void ieee80211_txq_ps_backlog_start(struct ieee80211_local *local,
+ struct txq_info *txqi)
+{
+ spin_lock_bh(&local->fq.lock);
+ if (txqi->tin.backlog_packets) {
+ txqi->ps_backlog_end = codel_get_time();
+ set_bit(IEEE80211_TXQ_PS_BACKLOG, &txqi->flags);
+ }
+ spin_unlock_bh(&local->fq.lock);
+}
+
static struct sk_buff *fq_tin_dequeue_func(struct fq *fq,
struct fq_tin *tin,
struct fq_flow *flow)
@@ -1395,6 +1444,9 @@ static struct sk_buff *fq_tin_dequeue_func(struct fq *fq,
cparams = &local->cparams;
cstats = &txqi->cstats;
+ if (unlikely(ieee80211_txq_codel_bypass(txqi, flow)))
+ return fq_flow_dequeue(fq, flow);
+
if (flow == &tin->default_flow)
cvars = &txqi->def_cvars;
else
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 14/30] wifi: mac80211: put filtered data frames back on TXQs
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (12 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 13/30] wifi: mac80211: don't apply CoDel to PS-buffered frames Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-06 12:23 ` Toke Høiland-Jørgensen
2026-11-02 20:23 ` [PATCH 15/30] wifi: mac80211: handle the mgmt TXQ of sleeping STAs Johannes Berg
` (15 subsequent siblings)
29 siblings, 1 reply; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
When a device filters frames because the station went to
sleep, we put them back on tx_filtered or pending queues.
This can mix up ordering between TXQs and filtered frames
when the station wakes up again later.
Put filtered frames onto the frags queue of the TXQ they
came from, fixing the order and simplifying the queueing
structure in mac80211.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/ht.c | 5 ++++
net/mac80211/ieee80211_i.h | 5 +++-
net/mac80211/sta_info.c | 52 +++++++++++++++++++++++++++++++++++-
net/mac80211/sta_info.h | 4 ++-
net/mac80211/status.c | 54 +++++++++++++++++++++++++++++++++++++-
net/mac80211/tx.c | 37 ++++++++++++++++++++++++++
6 files changed, 153 insertions(+), 4 deletions(-)
diff --git a/net/mac80211/ht.c b/net/mac80211/ht.c
index c5c2113fa4f7..e26d12659942 100644
--- a/net/mac80211/ht.c
+++ b/net/mac80211/ht.c
@@ -372,6 +372,11 @@ void ieee80211_ba_session_work(struct wiphy *wiphy, struct wiphy_work *work)
/* Allow only frags to be dequeued */
set_bit(IEEE80211_TXQ_STOP, &txqi->flags);
+ /*
+ * Fragments and filtered frames (which already have a
+ * sequence number) must be out before aggregation
+ * starts, otherwise they'd be sent as part of it.
+ */
if (!skb_queue_empty(&txqi->frags)) {
/* Fragmented Tx is ongoing, wait for it to
* finish. Reschedule worker to retry later.
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index cb947341fee6..4a9b68022250 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -987,7 +987,8 @@ enum txq_info_flags {
* @tin: contains packets split into multiple flows
* @def_cvars: codel vars for the @tin's default_flow
* @cstats: code statistics for this queue
- * @frags: used to keep fragments created after dequeue
+ * @frags: used to keep fragments created after dequeue, and frames that
+ * were filtered by the device (queued before the fragments)
* @schedule_order: used with ieee80211_local->active_txqs
* @schedule_round: counter to prevent infinite loops on TXQ scheduling
* @flags: TXQ flags from &enum txq_info_flags
@@ -2731,6 +2732,8 @@ struct sk_buff *ieee80211_tx_dequeue_ps_release(struct ieee80211_local *local,
struct txq_info *txqi);
void ieee80211_txq_ps_backlog_start(struct ieee80211_local *local,
struct txq_info *txqi);
+bool ieee80211_txq_queue_filtered(struct ieee80211_local *local,
+ struct txq_info *txqi, struct sk_buff *skb);
void ieee80211_txq_purge(struct ieee80211_local *local,
struct txq_info *txqi);
void ieee80211_purge_sta_txqs(struct sta_info *sta);
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index 8d79e532b109..c536ab759fef 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -1231,11 +1231,60 @@ static bool sta_info_buffer_expired(struct sta_info *sta, struct sk_buff *skb)
}
+static bool sta_info_cleanup_expire_filtered_txqs(struct ieee80211_local *local,
+ struct sta_info *sta, int ac)
+{
+ unsigned long tids = ieee80211_tids_for_ac(ac);
+ struct sk_buff_head expired;
+ struct fq *fq = &local->fq;
+ bool have_filtered = false;
+ int tid;
+
+ __skb_queue_head_init(&expired);
+
+ spin_lock_bh(&fq->lock);
+ for_each_set_bit(tid, &tids, IEEE80211_NUM_TIDS) {
+ struct txq_info *txqi;
+ struct sk_buff *skb;
+
+ if (!sta->sta.txq[tid])
+ continue;
+
+ txqi = to_txq_info(sta->sta.txq[tid]);
+
+ while ((skb = skb_peek(&txqi->frags))) {
+ if (!(IEEE80211_SKB_CB(skb)->flags &
+ IEEE80211_TX_INTFL_RETRANSMISSION))
+ break;
+
+ if (!sta_info_buffer_expired(sta, skb)) {
+ have_filtered = true;
+ break;
+ }
+
+ __skb_unlink(skb, &txqi->frags);
+ __skb_queue_tail(&expired, skb);
+ }
+ }
+ spin_unlock_bh(&fq->lock);
+
+ if (!skb_queue_empty(&expired))
+ ps_dbg(sta->sdata, "%d filtered frames expired (STA %pM)\n",
+ skb_queue_len(&expired), sta->sta.addr);
+
+ ieee80211_purge_tx_queue(&local->hw, &expired);
+
+ return have_filtered;
+}
+
static bool sta_info_cleanup_expire_buffered_ac(struct ieee80211_local *local,
struct sta_info *sta, int ac)
{
unsigned long flags;
struct sk_buff *skb;
+ bool have_filtered;
+
+ have_filtered = sta_info_cleanup_expire_filtered_txqs(local, sta, ac);
/*
* First check for frames that should expire on the filtered
@@ -1305,7 +1354,8 @@ static bool sta_info_cleanup_expire_buffered_ac(struct ieee80211_local *local,
* used to check whether the cleanup timer still needs to run,
* if there are no frames we don't need to rearm the timer.
*/
- return !(skb_queue_empty(&sta->ps_tx_buf[ac]) &&
+ return have_filtered ||
+ !(skb_queue_empty(&sta->ps_tx_buf[ac]) &&
skb_queue_empty(&sta->tx_filtered[ac]));
}
diff --git a/net/mac80211/sta_info.h b/net/mac80211/sta_info.h
index cd3c89a6a25a..ea7ead4b30b5 100644
--- a/net/mac80211/sta_info.h
+++ b/net/mac80211/sta_info.h
@@ -660,7 +660,9 @@ struct ieee80211_sta_removed_link_stats {
* @tx_filtered: buffers (per AC) of frames we already tried to
* transmit but were filtered by hardware due to STA having
* entered power saving state, these are also delivered to
- * the station when it leaves powersave or polls for frames
+ * the station when it leaves powersave or polls for frames.
+ * Only used for frames not on a TID TXQ, filtered data frames
+ * are put back onto the frags queue of their TXQ instead.
* @driver_buffered_tids: bitmap of TIDs the driver has data buffered on
* @ps_release_tids: TIDs for which the driver can dequeue from TXQs during
* during release_buffered_frames(), only non-zero during that
diff --git a/net/mac80211/status.c b/net/mac80211/status.c
index 414979e2a1d9..6134c37e081b 100644
--- a/net/mac80211/status.c
+++ b/net/mac80211/status.c
@@ -47,6 +47,8 @@ static void ieee80211_handle_filtered_frame(struct ieee80211_local *local,
{
struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
struct ieee80211_hdr *hdr = (void *)skb->data;
+ struct txq_info *txqi = NULL;
+ u32 ctrl_flags, flags;
int ac;
if (info->flags & (IEEE80211_TX_CTL_NO_PS_BUFFER |
@@ -56,6 +58,19 @@ static void ieee80211_handle_filtered_frame(struct ieee80211_local *local,
return;
}
+ if (ieee80211_is_data_present(hdr->frame_control) && sta->uploaded) {
+ u8 tid = skb->priority & IEEE80211_QOS_CTL_TID_MASK;
+
+ if (sta->sta.txq[tid])
+ txqi = to_txq_info(sta->sta.txq[tid]);
+ }
+
+ /* keep these flags across another trip through the TXQ */
+ ctrl_flags = info->control.flags & IEEE80211_TX_CTRL_PORT_CTRL_PROTO;
+ flags = info->flags & IEEE80211_TX_CTL_NO_ACK;
+ if (!(hdr->seq_ctrl & cpu_to_le16(IEEE80211_SCTL_FRAG)))
+ flags |= IEEE80211_TX_CTL_FIRST_FRAGMENT;
+
/*
* This skb 'survived' a round-trip through the driver, and
* hopefully the driver didn't mangle it too badly. However,
@@ -71,6 +86,10 @@ static void ieee80211_handle_filtered_frame(struct ieee80211_local *local,
info->control.flags |= IEEE80211_TX_INTCFL_NEED_TXPROCESSING;
info->flags |= IEEE80211_TX_INTFL_RETRANSMISSION;
info->flags &= ~IEEE80211_TX_TEMPORARY_FLAGS;
+ if (txqi) {
+ info->control.flags |= ctrl_flags;
+ info->flags |= flags;
+ }
sta->deflink.status_stats.filtered++;
@@ -142,6 +161,38 @@ static void ieee80211_handle_filtered_frame(struct ieee80211_local *local,
* changes before calling TX status events if ordering can be
* unknown.
*/
+ if (txqi) {
+ if (!test_sta_flag(sta, WLAN_STA_PS_STA)) {
+ if (info->flags & IEEE80211_TX_INTFL_RETRIED)
+ goto drop;
+ info->flags |= IEEE80211_TX_INTFL_RETRIED;
+ }
+
+ if (!ieee80211_txq_queue_filtered(local, txqi, skb))
+ goto drop;
+
+ /*
+ * ieee80211_sta_ps_deliver_wakeup() either sees the frame
+ * and wakes the TXQ, or this code sees it awake and does
+ */
+ smp_mb();
+
+ if (test_sta_flag(sta, WLAN_STA_PS_STA)) {
+ sta_info_recalc_tim(sta);
+
+ if (!timer_pending(&local->sta_cleanup))
+ mod_timer(&local->sta_cleanup,
+ round_jiffies(jiffies +
+ STA_INFO_CLEANUP_INTERVAL));
+ } else {
+ /* avoid recursive driver calls */
+ ieee80211_schedule_txq(&local->hw, &txqi->txq);
+ set_bit(IEEE80211_TXQ_DIRTY, &txqi->flags);
+ tasklet_schedule(&local->wake_txqs_tasklet);
+ }
+ return;
+ }
+
if (test_sta_flag(sta, WLAN_STA_PS_STA) &&
skb_queue_len(&sta->tx_filtered[ac]) < STA_MAX_TX_BUFFER) {
skb_queue_tail(&sta->tx_filtered[ac], skb);
@@ -162,9 +213,10 @@ static void ieee80211_handle_filtered_frame(struct ieee80211_local *local,
return;
}
+drop:
ps_dbg_ratelimited(sta->sdata,
"dropped TX filtered frame, queue_len=%d PS=%d @%lu\n",
- skb_queue_len(&sta->tx_filtered[ac]),
+ txqi ? -1 : skb_queue_len(&sta->tx_filtered[ac]),
!!test_sta_flag(sta, WLAN_STA_PS_STA), jiffies);
ieee80211_free_txskb(&local->hw, skb);
}
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 856018cdb617..cc6996591853 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -1515,6 +1515,33 @@ static void ieee80211_txq_enqueue(struct ieee80211_local *local,
sta_info_recalc_tim(sta);
}
+bool ieee80211_txq_queue_filtered(struct ieee80211_local *local,
+ struct txq_info *txqi, struct sk_buff *skb)
+{
+ struct sk_buff *iter, *pos = (struct sk_buff *)&txqi->frags;
+ unsigned int n_filtered = 0;
+ struct fq *fq = &local->fq;
+ bool ret = false;
+
+ spin_lock_bh(&fq->lock);
+ skb_queue_walk(&txqi->frags, iter) {
+ if (!(IEEE80211_SKB_CB(iter)->flags &
+ IEEE80211_TX_INTFL_RETRANSMISSION)) {
+ pos = iter;
+ break;
+ }
+ n_filtered++;
+ }
+
+ if (n_filtered < STA_MAX_TX_BUFFER) {
+ __skb_queue_before(&txqi->frags, pos, skb);
+ ret = true;
+ }
+ spin_unlock_bh(&fq->lock);
+
+ return ret;
+}
+
static bool fq_vlan_filter_func(struct fq *fq, struct fq_tin *tin,
struct fq_flow *flow, struct sk_buff *skb,
void *data)
@@ -4051,6 +4078,16 @@ static struct sk_buff *__ieee80211_tx_dequeue(struct ieee80211_hw *hw,
ieee80211_free_txskb(&local->hw, skb);
goto begin;
}
+
+ /*
+ * First transmitted frame clears filter. The non-atomic check
+ * is first since it's very unlikely.
+ */
+ if (unlikely(test_sta_flag(tx.sta, WLAN_STA_CLEAR_PS_FILT)) &&
+ test_and_clear_sta_flag(tx.sta, WLAN_STA_CLEAR_PS_FILT)) {
+ info->flags |= IEEE80211_TX_CTL_CLEAR_PS_FILT;
+ ieee80211_check_fast_xmit(tx.sta);
+ }
}
/*
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 15/30] wifi: mac80211: handle the mgmt TXQ of sleeping STAs
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (13 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 14/30] wifi: mac80211: put filtered data frames back on TXQs Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 16/30] wifi: mac80211: hold frames for sleeping STAs on TXQs Johannes Berg
` (14 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
Frames to a sleeping station are only transmitted when
it polls or wakes up, but the management TXQ doesn't
buffer frames right now. Change that, so we can later
get rid of the extra buffering layer.
Some frames aren't buffered, don't put those on a TXQ
at all, send them directly instead.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/rx.c | 8 +++++--
net/mac80211/sta_info.c | 46 ++++++++++++++++++++++-------------------
net/mac80211/tx.c | 22 +++++++++++---------
3 files changed, 43 insertions(+), 33 deletions(-)
diff --git a/net/mac80211/rx.c b/net/mac80211/rx.c
index fede61d606fb..d17e06394142 100644
--- a/net/mac80211/rx.c
+++ b/net/mac80211/rx.c
@@ -1684,10 +1684,14 @@ static void sta_ps_start(struct sta_info *sta)
ieee80211_clear_fast_xmit(sta);
- for (tid = 0; tid < IEEE80211_NUM_TIDS; tid++) {
+ for (tid = 0; tid < ARRAY_SIZE(sta->sta.txq); tid++) {
struct ieee80211_txq *txq = sta->sta.txq[tid];
- struct txq_info *txqi = to_txq_info(txq);
+ struct txq_info *txqi;
+ if (!txq)
+ continue;
+
+ txqi = to_txq_info(txq);
spin_lock(&local->active_txq_lock[txq->ac]);
if (!list_empty(&txqi->schedule_order))
list_del_init(&txqi->schedule_order);
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index c536ab759fef..25cf1929fbfc 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -1086,7 +1086,7 @@ unsigned long ieee80211_tids_for_ac(int ac)
/* If we ever support TIDs > 7, this obviously needs to be adjusted */
switch (ac) {
case IEEE80211_AC_VO:
- return BIT(6) | BIT(7);
+ return BIT(6) | BIT(7) | BIT(IEEE80211_NUM_TIDS);
case IEEE80211_AC_VI:
return BIT(4) | BIT(5);
case IEEE80211_AC_BE:
@@ -1099,6 +1099,13 @@ unsigned long ieee80211_tids_for_ac(int ac)
}
}
+static struct txq_info *sta_txqi(const struct sta_info *sta, int tid)
+{
+ if (!sta->sta.txq[tid])
+ return NULL;
+ return to_txq_info(sta->sta.txq[tid]);
+}
+
unsigned long ieee80211_sta_txq_buffered_tids(struct sta_info *sta)
{
struct fq *fq = &sta->local->fq;
@@ -1111,10 +1118,10 @@ unsigned long ieee80211_sta_txq_buffered_tids(struct sta_info *sta)
/* synchronize with enqueue */
spin_lock_bh(&fq->lock);
- for (tid = 0; tid < IEEE80211_NUM_TIDS; tid++) {
- struct ieee80211_txq *txq = sta->sta.txq[tid];
+ for (tid = 0; tid <= IEEE80211_NUM_TIDS; tid++) {
+ struct txq_info *txqi = sta_txqi(sta, tid);
- if (txq && txq_has_queue(txq))
+ if (txqi && txq_has_queue(&txqi->txq))
tids |= BIT(tid);
}
spin_unlock_bh(&fq->lock);
@@ -1243,15 +1250,13 @@ static bool sta_info_cleanup_expire_filtered_txqs(struct ieee80211_local *local,
__skb_queue_head_init(&expired);
spin_lock_bh(&fq->lock);
- for_each_set_bit(tid, &tids, IEEE80211_NUM_TIDS) {
- struct txq_info *txqi;
+ for_each_set_bit(tid, &tids, IEEE80211_NUM_TIDS + 1) {
+ struct txq_info *txqi = sta_txqi(sta, tid);
struct sk_buff *skb;
- if (!sta->sta.txq[tid])
+ if (!txqi)
continue;
- txqi = to_txq_info(sta->sta.txq[tid]);
-
while ((skb = skb_peek(&txqi->frags))) {
if (!(IEEE80211_SKB_CB(skb)->flags &
IEEE80211_TX_INTFL_RETRANSMISSION))
@@ -2168,7 +2173,7 @@ int ieee80211_sta_ps_dequeue_txqs(const struct sta_info *sta, unsigned long tids
struct txq_info *txq;
struct sk_buff *skb;
- txq = to_txq_info(sta->sta.txq[tid]);
+ txq = sta_txqi(sta, tid);
skb = ieee80211_tx_dequeue_ps_release(local, txq);
if (!skb) {
tids &= ~BIT(tid);
@@ -2213,12 +2218,14 @@ ieee80211_sta_ps_get_frames(struct sta_info *sta, int n_frames, u8 ignored_acs,
/* Get response frame(s) and more data bit for the last one. */
for (ac = 0; ac < IEEE80211_NUM_ACS; ac++) {
- unsigned long tids;
+ unsigned long tids, mac80211_tids;
if (ignored_acs & ieee80211_ac_to_qos_mask[ac])
continue;
tids = ieee80211_tids_for_ac(ac);
+ /* mac80211 releases frames from the management TXQ itself */
+ mac80211_tids = from_txqs ? tids : BIT(IEEE80211_NUM_TIDS);
/* if we already have frames from software, then we can't also
* release from hardware queues
@@ -2227,21 +2234,19 @@ ieee80211_sta_ps_get_frames(struct sta_info *sta, int n_frames, u8 ignored_acs,
*driver_release_tids |=
sta->driver_buffered_tids & tids;
if (!from_txqs)
- *driver_release_tids |= txq_buffered_tids & tids;
+ *driver_release_tids |= txq_buffered_tids & tids &
+ ~BIT(IEEE80211_NUM_TIDS);
}
if (!*driver_release_tids) {
+ unsigned long txq_tids;
int remaining;
n_frames = ieee80211_sta_ps_dequeue(&sta->tx_filtered[ac],
n_frames, frames);
- if (from_txqs) {
- unsigned long txq_tids = txq_buffered_tids & tids;
- n_frames = ieee80211_sta_ps_dequeue_txqs(sta,
- txq_tids,
- n_frames,
- frames);
- }
+ txq_tids = txq_buffered_tids & tids & mac80211_tids;
+ n_frames = ieee80211_sta_ps_dequeue_txqs(sta, txq_tids,
+ n_frames, frames);
remaining = n_frames;
n_frames = ieee80211_sta_ps_dequeue(&sta->ps_tx_buf[ac],
@@ -2257,8 +2262,7 @@ ieee80211_sta_ps_get_frames(struct sta_info *sta, int n_frames, u8 ignored_acs,
!skb_queue_empty(&sta->ps_tx_buf[ac]))
break;
- if (from_txqs &&
- ieee80211_sta_txq_buffered_tids(sta) & tids)
+ if (ieee80211_sta_txq_buffered_tids(sta) & tids & mac80211_tids)
break;
}
}
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index cc6996591853..6bace6ede373 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -1278,7 +1278,9 @@ static struct txq_info *ieee80211_get_txq(struct ieee80211_local *local,
struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
struct ieee80211_txq *txq = NULL;
- if ((info->flags & IEEE80211_TX_CTL_SEND_AFTER_DTIM) ||
+ /* TXQs buffer frames, send unbuffered ones directly */
+ if (info->flags & (IEEE80211_TX_CTL_SEND_AFTER_DTIM |
+ IEEE80211_TX_CTL_NO_PS_BUFFER) ||
(info->control.flags & IEEE80211_TX_CTRL_PS_RESPONSE))
return NULL;
@@ -1486,6 +1488,14 @@ static void ieee80211_txq_enqueue(struct ieee80211_local *local,
ieee80211_set_skb_enqueue_time(skb);
spin_lock_bh(&fq->lock);
+ /* TXQ becomes non-empty, update TIM if STA is sleeping */
+ if (txqi->txq.sta && !txq_has_queue(&txqi->txq)) {
+ sta = container_of(txqi->txq.sta, struct sta_info, sta);
+ if (!test_sta_flag(sta, WLAN_STA_PS_STA) &&
+ !test_sta_flag(sta, WLAN_STA_PS_DRIVER))
+ sta = NULL;
+ }
+
/*
* For management frames, don't really apply codel etc.,
* we don't want to apply any shaping or anything we just
@@ -1497,14 +1507,6 @@ static void ieee80211_txq_enqueue(struct ieee80211_local *local,
IEEE80211_TX_INTCFL_NEED_TXPROCESSING;
__skb_queue_tail(&txqi->frags, skb);
} else {
- /* TXQ becomes non-empty, update TIM if STA is sleeping */
- if (txqi->txq.sta && !txq_has_queue(&txqi->txq)) {
- sta = container_of(txqi->txq.sta, struct sta_info, sta);
- if (!test_sta_flag(sta, WLAN_STA_PS_STA) &&
- !test_sta_flag(sta, WLAN_STA_PS_DRIVER))
- sta = NULL;
- }
-
flow_idx = fq_flow_idx(fq, skb);
fq_tin_enqueue(fq, tin, flow_idx, skb,
fq_skb_free_func);
@@ -3986,7 +3988,7 @@ static bool ieee80211_txq_ps_tx_ok(const struct txq_info *txqi)
{
struct sta_info *sta;
- if (!txqi->txq.sta || txqi->txq.tid == IEEE80211_NUM_TIDS)
+ if (!txqi->txq.sta)
return true;
sta = container_of(txqi->txq.sta, struct sta_info, sta);
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 16/30] wifi: mac80211: hold frames for sleeping STAs on TXQs
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (14 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 15/30] wifi: mac80211: handle the mgmt TXQ of sleeping STAs Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 17/30] wifi: mac80211: move ieee80211_txq_enqueue() up Johannes Berg
` (13 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
We currently use a separate PS buffer for (data) frames,
move to putting the PS buffered frames onto TXQs so they
are released from there by the poll/wakeup handling.
This will let us remove the extra powersave queue step
later.
For now management frames are only there if the driver
opted in to the management TXQ, that changes later.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
include/net/mac80211.h | 8 ++---
net/mac80211/ieee80211_i.h | 2 ++
net/mac80211/rx.c | 3 ++
net/mac80211/sta_info.c | 67 ++++++++++++++++++++++++++++---------
net/mac80211/sta_info.h | 1 +
net/mac80211/tx.c | 68 +++++++++++++++++++++++++++++++++++++-
6 files changed, 128 insertions(+), 21 deletions(-)
diff --git a/include/net/mac80211.h b/include/net/mac80211.h
index 1e8f799193dc..6cad76cc7d20 100644
--- a/include/net/mac80211.h
+++ b/include/net/mac80211.h
@@ -3764,10 +3764,10 @@ void ieee80211_purge_tx_queue(struct ieee80211_hw *hw,
* driver is also notified of powersave transitions by calling its
* @sta_notify callback.
*
- * While the station is asleep (or blocked, see below), frames that are
- * already on its TXQs remain there: the TXQs aren't scheduled, and
- * ieee80211_tx_dequeue() doesn't return frames from them, except when
- * they're released as described below.
+ * While the station is asleep (or blocked, see below), frames for it are
+ * held on its TXQs: the TXQs aren't scheduled, and ieee80211_tx_dequeue()
+ * doesn't return frames from them, except when they're released as
+ * described below.
*
* When the station is asleep, it has three choices: it can wake up,
* it can PS-Poll, or it can possibly start a uAPSD service period.
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index 4a9b68022250..ebfa772a6dd8 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -2732,6 +2732,8 @@ struct sk_buff *ieee80211_tx_dequeue_ps_release(struct ieee80211_local *local,
struct txq_info *txqi);
void ieee80211_txq_ps_backlog_start(struct ieee80211_local *local,
struct txq_info *txqi);
+unsigned int ieee80211_txq_expire(struct ieee80211_local *local,
+ struct txq_info *txqi, unsigned long timeout);
bool ieee80211_txq_queue_filtered(struct ieee80211_local *local,
struct txq_info *txqi, struct sk_buff *skb);
void ieee80211_txq_purge(struct ieee80211_local *local,
diff --git a/net/mac80211/rx.c b/net/mac80211/rx.c
index d17e06394142..5f31326a84b1 100644
--- a/net/mac80211/rx.c
+++ b/net/mac80211/rx.c
@@ -1699,6 +1699,9 @@ static void sta_ps_start(struct sta_info *sta)
}
sta_info_recalc_tim(sta);
+
+ /* expire held frames at a regular interval */
+ sta_info_schedule_cleanup(local);
}
static void sta_ps_end(struct sta_info *sta)
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index 25cf1929fbfc..1ec59cdd4fa2 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -1218,33 +1218,41 @@ void sta_info_recalc_tim(struct sta_info *sta)
__sta_info_recalc_tim(sta, false);
}
-static bool sta_info_buffer_expired(struct sta_info *sta, struct sk_buff *skb)
+static int sta_info_buffer_timeout(struct sta_info *sta)
{
- struct ieee80211_tx_info *info;
int timeout;
- if (!skb)
- return false;
-
- info = IEEE80211_SKB_CB(skb);
-
/* Timeout: (2 * listen_interval * beacon_int * 1024 / 1000000) sec */
timeout = (sta->listen_interval *
sta->sdata->vif.bss_conf.beacon_int *
32 / 15625) * HZ;
if (timeout < STA_TX_BUFFER_EXPIRE)
timeout = STA_TX_BUFFER_EXPIRE;
- return time_after(jiffies, info->control.jiffies + timeout);
+ return timeout;
}
+static bool sta_info_buffer_expired(struct sta_info *sta, struct sk_buff *skb)
+{
+ struct ieee80211_tx_info *info;
-static bool sta_info_cleanup_expire_filtered_txqs(struct ieee80211_local *local,
- struct sta_info *sta, int ac)
+ if (!skb)
+ return false;
+
+ info = IEEE80211_SKB_CB(skb);
+
+ return time_after(jiffies,
+ info->control.jiffies + sta_info_buffer_timeout(sta));
+}
+
+/* returns whether frames are left on the TXQs that we want to expire */
+static bool sta_info_cleanup_expire_txqs(struct ieee80211_local *local,
+ struct sta_info *sta, int ac)
{
unsigned long tids = ieee80211_tids_for_ac(ac);
struct sk_buff_head expired;
struct fq *fq = &local->fq;
- bool have_filtered = false;
+ bool have_buffered = false;
+ unsigned int n_expired = 0;
int tid;
__skb_queue_head_init(&expired);
@@ -1263,7 +1271,7 @@ static bool sta_info_cleanup_expire_filtered_txqs(struct ieee80211_local *local,
break;
if (!sta_info_buffer_expired(sta, skb)) {
- have_filtered = true;
+ have_buffered = true;
break;
}
@@ -1279,7 +1287,27 @@ static bool sta_info_cleanup_expire_filtered_txqs(struct ieee80211_local *local,
ieee80211_purge_tx_queue(&local->hw, &expired);
- return have_filtered;
+ if (!test_sta_flag(sta, WLAN_STA_PS_STA) &&
+ !test_sta_flag(sta, WLAN_STA_PS_DRIVER))
+ return have_buffered;
+
+ for_each_set_bit(tid, &tids, IEEE80211_NUM_TIDS + 1) {
+ struct txq_info *txqi = sta_txqi(sta, tid);
+
+ if (!txqi)
+ continue;
+
+ n_expired += ieee80211_txq_expire(local, txqi,
+ sta_info_buffer_timeout(sta));
+ if (txq_has_queue(&txqi->txq))
+ have_buffered = true;
+ }
+
+ if (n_expired)
+ ps_dbg(sta->sdata, "%u buffered frames expired (STA %pM)\n",
+ n_expired, sta->sta.addr);
+
+ return have_buffered;
}
static bool sta_info_cleanup_expire_buffered_ac(struct ieee80211_local *local,
@@ -1287,9 +1315,9 @@ static bool sta_info_cleanup_expire_buffered_ac(struct ieee80211_local *local,
{
unsigned long flags;
struct sk_buff *skb;
- bool have_filtered;
+ bool have_buffered;
- have_filtered = sta_info_cleanup_expire_filtered_txqs(local, sta, ac);
+ have_buffered = sta_info_cleanup_expire_txqs(local, sta, ac);
/*
* First check for frames that should expire on the filtered
@@ -1359,7 +1387,7 @@ static bool sta_info_cleanup_expire_buffered_ac(struct ieee80211_local *local,
* used to check whether the cleanup timer still needs to run,
* if there are no frames we don't need to rearm the timer.
*/
- return have_filtered ||
+ return have_buffered ||
!(skb_queue_empty(&sta->ps_tx_buf[ac]) &&
skb_queue_empty(&sta->tx_filtered[ac]));
}
@@ -1737,6 +1765,13 @@ int sta_info_destroy_addr_bss(struct ieee80211_sub_if_data *sdata,
return __sta_info_destroy(sta);
}
+void sta_info_schedule_cleanup(struct ieee80211_local *local)
+{
+ if (!timer_pending(&local->sta_cleanup))
+ mod_timer(&local->sta_cleanup,
+ round_jiffies(jiffies + STA_INFO_CLEANUP_INTERVAL));
+}
+
static void sta_info_cleanup(struct timer_list *t)
{
struct ieee80211_local *local = timer_container_of(local, t,
diff --git a/net/mac80211/sta_info.h b/net/mac80211/sta_info.h
index ea7ead4b30b5..307c8b76b80f 100644
--- a/net/mac80211/sta_info.h
+++ b/net/mac80211/sta_info.h
@@ -1008,6 +1008,7 @@ void ieee80211_sta_free_link(struct sta_info *sta, unsigned int link_id);
int ieee80211_sta_activate_link(struct sta_info *sta, unsigned int link_id);
void ieee80211_sta_remove_link(struct sta_info *sta, unsigned int link_id);
+void sta_info_schedule_cleanup(struct ieee80211_local *local);
void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta);
unsigned long ieee80211_tids_for_ac(int ac);
unsigned long ieee80211_sta_txq_buffered_tids(struct sta_info *sta);
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 6bace6ede373..c29dec9d049d 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -462,6 +462,12 @@ ieee80211_tx_h_unicast_ps_buf(struct ieee80211_tx_data *tx)
return TX_CONTINUE;
}
+ /* continue and put the frame on the TXQ */
+ if (sta->uploaded &&
+ (ieee80211_is_data_present(hdr->frame_control) ||
+ sta->sta.txq[IEEE80211_NUM_TIDS]))
+ return TX_CONTINUE;
+
ps_dbg(sta->sdata, "STA %pM aid %d: PS buffer for AC %d\n",
sta->sta.addr, sta->sta.aid, ac);
if (tx->local->total_ps_buffered >= TOTAL_MAX_TX_BUFFER)
@@ -1513,8 +1519,10 @@ static void ieee80211_txq_enqueue(struct ieee80211_local *local,
}
spin_unlock_bh(&fq->lock);
- if (sta)
+ if (sta) {
sta_info_recalc_tim(sta);
+ sta_info_schedule_cleanup(local);
+ }
}
bool ieee80211_txq_queue_filtered(struct ieee80211_local *local,
@@ -1544,6 +1552,64 @@ bool ieee80211_txq_queue_filtered(struct ieee80211_local *local,
return ret;
}
+struct ieee80211_txq_expire_data {
+ codel_time_t now, timeout;
+};
+
+static bool ieee80211_txq_frame_expired(struct sk_buff *skb,
+ struct ieee80211_txq_expire_data *exp)
+{
+ codel_time_t enqueue_time = IEEE80211_SKB_CB(skb)->control.enqueue_time;
+
+ return codel_time_after(exp->now, enqueue_time + exp->timeout);
+}
+
+static bool fq_expire_filter_func(struct fq *fq, struct fq_tin *tin,
+ struct fq_flow *flow, struct sk_buff *skb,
+ void *data)
+{
+ return ieee80211_txq_frame_expired(skb, data);
+}
+
+unsigned int ieee80211_txq_expire(struct ieee80211_local *local,
+ struct txq_info *txqi, unsigned long timeout)
+{
+ struct ieee80211_txq_expire_data exp = {
+ .now = codel_get_time(),
+ /* station controls listen interval, avoid too long timeout */
+ .timeout = min_t(u64, jiffies_to_nsecs(timeout) >> CODEL_SHIFT,
+ S32_MAX / 2),
+ };
+ struct sk_buff_head expired;
+ struct fq *fq = &local->fq;
+ struct sk_buff *skb, *tmp;
+ unsigned int n_expired;
+
+ __skb_queue_head_init(&expired);
+
+ spin_lock_bh(&fq->lock);
+ n_expired = txqi->tin.backlog_packets;
+ fq_tin_filter(fq, &txqi->tin, fq_expire_filter_func, &exp,
+ fq_skb_free_func);
+ n_expired -= txqi->tin.backlog_packets;
+
+ skb_queue_walk_safe(&txqi->frags, skb, tmp) {
+ if (!(IEEE80211_SKB_CB(skb)->control.flags &
+ IEEE80211_TX_INTCFL_NEED_TXPROCESSING) ||
+ !ieee80211_txq_frame_expired(skb, &exp))
+ continue;
+
+ __skb_unlink(skb, &txqi->frags);
+ __skb_queue_tail(&expired, skb);
+ }
+ spin_unlock_bh(&fq->lock);
+
+ n_expired += skb_queue_len(&expired);
+ ieee80211_purge_tx_queue(&local->hw, &expired);
+
+ return n_expired;
+}
+
static bool fq_vlan_filter_func(struct fq *fq, struct fq_tin *tin,
struct fq_flow *flow, struct sk_buff *skb,
void *data)
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 17/30] wifi: mac80211: move ieee80211_txq_enqueue() up
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (15 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 16/30] wifi: mac80211: hold frames for sleeping STAs on TXQs Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 18/30] wifi: mac80211: always use per-STA MMPDU TXQ Johannes Berg
` (12 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
ieee80211_txq_enqueue() will be needed by the powersave buffering
code, so move it and the helpers it uses before that, to avoid a
forward declaration. No functional changes.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/tx.c | 126 +++++++++++++++++++++++-----------------------
1 file changed, 63 insertions(+), 63 deletions(-)
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index c29dec9d049d..aca68ab112e0 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -308,6 +308,69 @@ ieee80211_tx_h_check_assoc(struct ieee80211_tx_data *tx)
return TX_CONTINUE;
}
+static void ieee80211_set_skb_enqueue_time(struct sk_buff *skb)
+{
+ struct sk_buff *next;
+ codel_time_t now = codel_get_time();
+
+ skb_list_walk_safe(skb, skb, next)
+ IEEE80211_SKB_CB(skb)->control.enqueue_time = now;
+}
+
+static void fq_skb_free_func(struct fq *fq,
+ struct fq_tin *tin,
+ struct fq_flow *flow,
+ struct sk_buff *skb)
+{
+ struct ieee80211_local *local;
+
+ local = container_of(fq, struct ieee80211_local, fq);
+ ieee80211_free_txskb(&local->hw, skb);
+}
+
+static void ieee80211_txq_enqueue(struct ieee80211_local *local,
+ struct txq_info *txqi,
+ struct sk_buff *skb)
+{
+ struct fq *fq = &local->fq;
+ struct fq_tin *tin = &txqi->tin;
+ struct sta_info *sta = NULL;
+ u32 flow_idx;
+
+ ieee80211_set_skb_enqueue_time(skb);
+
+ spin_lock_bh(&fq->lock);
+ /* TXQ becomes non-empty, update TIM if STA is sleeping */
+ if (txqi->txq.sta && !txq_has_queue(&txqi->txq)) {
+ sta = container_of(txqi->txq.sta, struct sta_info, sta);
+ if (!test_sta_flag(sta, WLAN_STA_PS_STA) &&
+ !test_sta_flag(sta, WLAN_STA_PS_DRIVER))
+ sta = NULL;
+ }
+
+ /*
+ * For management frames, don't really apply codel etc.,
+ * we don't want to apply any shaping or anything we just
+ * want to simplify the driver API by having them on the
+ * txqi.
+ */
+ if (unlikely(txqi->txq.tid == IEEE80211_NUM_TIDS)) {
+ IEEE80211_SKB_CB(skb)->control.flags |=
+ IEEE80211_TX_INTCFL_NEED_TXPROCESSING;
+ __skb_queue_tail(&txqi->frags, skb);
+ } else {
+ flow_idx = fq_flow_idx(fq, skb);
+ fq_tin_enqueue(fq, tin, flow_idx, skb,
+ fq_skb_free_func);
+ }
+ spin_unlock_bh(&fq->lock);
+
+ if (sta) {
+ sta_info_recalc_tim(sta);
+ sta_info_schedule_cleanup(local);
+ }
+}
+
/* This function is called whenever the AP is about to exceed the maximum limit
* of buffered frames for power saving STAs. This situation should not really
* happen often during normal operation, so dropping the oldest buffered packet
@@ -1332,15 +1395,6 @@ static struct txq_info *ieee80211_get_txq(struct ieee80211_local *local,
return to_txq_info(txq);
}
-static void ieee80211_set_skb_enqueue_time(struct sk_buff *skb)
-{
- struct sk_buff *next;
- codel_time_t now = codel_get_time();
-
- skb_list_walk_safe(skb, skb, next)
- IEEE80211_SKB_CB(skb)->control.enqueue_time = now;
-}
-
static u32 codel_skb_len_func(const struct sk_buff *skb)
{
return skb->len;
@@ -1471,60 +1525,6 @@ static struct sk_buff *fq_tin_dequeue_func(struct fq *fq,
codel_dequeue_func);
}
-static void fq_skb_free_func(struct fq *fq,
- struct fq_tin *tin,
- struct fq_flow *flow,
- struct sk_buff *skb)
-{
- struct ieee80211_local *local;
-
- local = container_of(fq, struct ieee80211_local, fq);
- ieee80211_free_txskb(&local->hw, skb);
-}
-
-static void ieee80211_txq_enqueue(struct ieee80211_local *local,
- struct txq_info *txqi,
- struct sk_buff *skb)
-{
- struct fq *fq = &local->fq;
- struct fq_tin *tin = &txqi->tin;
- struct sta_info *sta = NULL;
- u32 flow_idx;
-
- ieee80211_set_skb_enqueue_time(skb);
-
- spin_lock_bh(&fq->lock);
- /* TXQ becomes non-empty, update TIM if STA is sleeping */
- if (txqi->txq.sta && !txq_has_queue(&txqi->txq)) {
- sta = container_of(txqi->txq.sta, struct sta_info, sta);
- if (!test_sta_flag(sta, WLAN_STA_PS_STA) &&
- !test_sta_flag(sta, WLAN_STA_PS_DRIVER))
- sta = NULL;
- }
-
- /*
- * For management frames, don't really apply codel etc.,
- * we don't want to apply any shaping or anything we just
- * want to simplify the driver API by having them on the
- * txqi.
- */
- if (unlikely(txqi->txq.tid == IEEE80211_NUM_TIDS)) {
- IEEE80211_SKB_CB(skb)->control.flags |=
- IEEE80211_TX_INTCFL_NEED_TXPROCESSING;
- __skb_queue_tail(&txqi->frags, skb);
- } else {
- flow_idx = fq_flow_idx(fq, skb);
- fq_tin_enqueue(fq, tin, flow_idx, skb,
- fq_skb_free_func);
- }
- spin_unlock_bh(&fq->lock);
-
- if (sta) {
- sta_info_recalc_tim(sta);
- sta_info_schedule_cleanup(local);
- }
-}
-
bool ieee80211_txq_queue_filtered(struct ieee80211_local *local,
struct txq_info *txqi, struct sk_buff *skb)
{
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 18/30] wifi: mac80211: always use per-STA MMPDU TXQ
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (16 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 17/30] wifi: mac80211: move ieee80211_txq_enqueue() up Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 19/30] wifi: mac80211: use MMPDU TXQ for filtered mgmt frames Johannes Berg
` (11 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
If the driver uses the MMPDU TXQ we now hold PS frames there,
but if it doesn't we still use extra ps_tx_buf[ac] queues.
Change this to always allocate the MMPDU TXQ even if a given
driver doesn't want it, and then use that to buffer frames.
This gets rid of the ps_tx_buf[ac] queues.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/debugfs_sta.c | 21 +++++--
net/mac80211/ieee80211_i.h | 13 ++++-
net/mac80211/mesh_ps.c | 18 +-----
net/mac80211/sta_info.c | 109 +++++++----------------------------
net/mac80211/sta_info.h | 6 +-
net/mac80211/tx.c | 115 +++++++++++++++----------------------
6 files changed, 101 insertions(+), 181 deletions(-)
diff --git a/net/mac80211/debugfs_sta.c b/net/mac80211/debugfs_sta.c
index 203f608eba67..088fd9d90c81 100644
--- a/net/mac80211/debugfs_sta.c
+++ b/net/mac80211/debugfs_sta.c
@@ -106,13 +106,26 @@ static ssize_t sta_num_ps_buf_frames_read(struct file *file,
size_t count, loff_t *ppos)
{
struct sta_info *sta = file->private_data;
+ struct fq *fq = &sta->local->fq;
char buf[17*IEEE80211_NUM_ACS], *p = buf;
int ac;
- for (ac = 0; ac < IEEE80211_NUM_ACS; ac++)
- p += scnprintf(p, sizeof(buf)+buf-p, "AC%d: %d\n", ac,
- skb_queue_len(&sta->ps_tx_buf[ac]) +
- skb_queue_len(&sta->tx_filtered[ac]));
+ for (ac = 0; ac < IEEE80211_NUM_ACS; ac++) {
+ unsigned long tids = ieee80211_tids_for_ac(ac);
+ unsigned int n = skb_queue_len(&sta->tx_filtered[ac]);
+ int tid;
+
+ spin_lock_bh(&fq->lock);
+ for_each_set_bit(tid, &tids, IEEE80211_NUM_TIDS + 1) {
+ struct txq_info *txqi = sta_txqi(sta, tid);
+
+ n += txqi->tin.backlog_packets +
+ skb_queue_len(&txqi->frags);
+ }
+ spin_unlock_bh(&fq->lock);
+
+ p += scnprintf(p, sizeof(buf) + buf - p, "AC%d: %u\n", ac, n);
+ }
return simple_read_from_buffer(userbuf, count, ppos, buf, p - buf);
}
STA_OPS(num_ps_buf_frames);
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index ebfa772a6dd8..89581935659c 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -45,7 +45,7 @@ struct ieee80211_mesh_fast_tx;
* associated stations are using power saving. */
#define AP_MAX_BC_BUFFER 128
-/* Maximum number of frames buffered to all STAs, including multicast frames.
+/* Maximum number of multicast frames buffered for all interfaces.
* Note: increasing this limit increases the potential memory requirement. Each
* frame can be up to about 2 kB long. */
#define TOTAL_MAX_TX_BUFFER 512
@@ -1717,8 +1717,8 @@ struct ieee80211_local {
#endif /* CONFIG_MAC80211_DEBUG_COUNTERS */
- int total_ps_buffered; /* total number of all buffered unicast and
- * multicast packets for power saving stations
+ int total_ps_buffered; /* total number of all buffered multicast
+ * packets for power saving stations
*/
bool pspolling;
@@ -1987,6 +1987,13 @@ static inline struct txq_info *to_txq_info(struct ieee80211_txq *txq)
return container_of(txq, struct txq_info, txq);
}
+static inline struct txq_info *sta_txqi(const struct sta_info *sta, int tid)
+{
+ if (tid == IEEE80211_NUM_TIDS)
+ return sta->mmpdu_txqi;
+ return to_txq_info(sta->sta.txq[tid]);
+}
+
static inline bool txq_has_queue(struct ieee80211_txq *txq)
{
struct txq_info *txqi = to_txq_info(txq);
diff --git a/net/mac80211/mesh_ps.c b/net/mac80211/mesh_ps.c
index ddf35cdd558a..e3d815fbe31b 100644
--- a/net/mac80211/mesh_ps.c
+++ b/net/mac80211/mesh_ps.c
@@ -471,10 +471,7 @@ static void mps_frame_deliver(struct sta_info *sta, int n_frames)
txq_buffered_tids = ieee80211_sta_txq_buffered_tids(sta);
- /*
- * collect frame(s) from buffers, frames on the TXQs were queued
- * before those on the PS buffer
- */
+ /* collect frame(s) from buffers */
for (ac = 0; ac < IEEE80211_NUM_ACS; ac++) {
while (n_frames > 0 &&
(skb = skb_dequeue(&sta->tx_filtered[ac]))) {
@@ -486,15 +483,7 @@ static void mps_frame_deliver(struct sta_info *sta, int n_frames)
txq_buffered_tids & ieee80211_tids_for_ac(ac),
n_frames, &frames);
- while (n_frames > 0 &&
- (skb = skb_dequeue(&sta->ps_tx_buf[ac]))) {
- local->total_ps_buffered--;
- n_frames--;
- __skb_queue_tail(&frames, skb);
- }
-
- if (!skb_queue_empty(&sta->tx_filtered[ac]) ||
- !skb_queue_empty(&sta->ps_tx_buf[ac]))
+ if (!skb_queue_empty(&sta->tx_filtered[ac]))
more_data = true;
}
@@ -617,8 +606,7 @@ void ieee80211_mps_frame_release(struct sta_info *sta,
if (!test_sta_flag(sta, WLAN_STA_MPSP_OWNER)) {
for (ac = 0; ac < IEEE80211_NUM_ACS; ac++)
- buffer_local += skb_queue_len(&sta->ps_tx_buf[ac]) +
- skb_queue_len(&sta->tx_filtered[ac]);
+ buffer_local += skb_queue_len(&sta->tx_filtered[ac]);
if (ieee80211_sta_txq_buffered_tids(sta))
buffer_local++;
}
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index 1ec59cdd4fa2..3161e00c32a9 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -117,16 +117,8 @@ void ieee80211_purge_sta_txqs(struct sta_info *sta)
struct ieee80211_local *local = sta->sdata->local;
int i;
- for (i = 0; i < ARRAY_SIZE(sta->sta.txq); i++) {
- struct txq_info *txqi;
-
- if (!sta->sta.txq[i])
- continue;
-
- txqi = to_txq_info(sta->sta.txq[i]);
-
- ieee80211_txq_purge(local, txqi);
- }
+ for (i = 0; i <= IEEE80211_NUM_TIDS; i++)
+ ieee80211_txq_purge(local, sta_txqi(sta, i));
}
static void __cleanup_single_sta(struct sta_info *sta)
@@ -158,11 +150,8 @@ static void __cleanup_single_sta(struct sta_info *sta)
ieee80211_purge_sta_txqs(sta);
- for (ac = 0; ac < IEEE80211_NUM_ACS; ac++) {
- local->total_ps_buffered -= skb_queue_len(&sta->ps_tx_buf[ac]);
- ieee80211_purge_tx_queue(&local->hw, &sta->ps_tx_buf[ac]);
+ for (ac = 0; ac < IEEE80211_NUM_ACS; ac++)
ieee80211_purge_tx_queue(&local->hw, &sta->tx_filtered[ac]);
- }
if (ieee80211_vif_is_mesh(&sdata->vif))
mesh_sta_cleanup(sta);
@@ -715,7 +704,6 @@ __sta_info_alloc(struct ieee80211_sub_if_data *sdata,
for (i = 0; i < ARRAY_SIZE(sta->sta.txq); i++) {
struct txq_info *txq = txq_data + i * size;
- /* might not do anything for the (bufferable) MMPDU TXQ */
ieee80211_txq_init(sdata, sta, txq, i);
}
@@ -725,7 +713,6 @@ __sta_info_alloc(struct ieee80211_sub_if_data *sdata,
sta->airtime_weight = IEEE80211_DEFAULT_AIRTIME_WEIGHT;
for (i = 0; i < IEEE80211_NUM_ACS; i++) {
- skb_queue_head_init(&sta->ps_tx_buf[i]);
skb_queue_head_init(&sta->tx_filtered[i]);
sta->airtime[i].deficit = sta->airtime_weight;
atomic_set(&sta->airtime[i].aql_tx_pending, 0);
@@ -1099,13 +1086,6 @@ unsigned long ieee80211_tids_for_ac(int ac)
}
}
-static struct txq_info *sta_txqi(const struct sta_info *sta, int tid)
-{
- if (!sta->sta.txq[tid])
- return NULL;
- return to_txq_info(sta->sta.txq[tid]);
-}
-
unsigned long ieee80211_sta_txq_buffered_tids(struct sta_info *sta)
{
struct fq *fq = &sta->local->fq;
@@ -1180,8 +1160,7 @@ static void __sta_info_recalc_tim(struct sta_info *sta, bool ignore_pending)
if (ignore_for_tim & ieee80211_ac_to_qos_mask[ac])
continue;
- indicate_tim |= !skb_queue_empty(&sta->tx_filtered[ac]) ||
- !skb_queue_empty(&sta->ps_tx_buf[ac]);
+ indicate_tim |= !skb_queue_empty(&sta->tx_filtered[ac]);
if (indicate_tim)
break;
@@ -1320,11 +1299,8 @@ static bool sta_info_cleanup_expire_buffered_ac(struct ieee80211_local *local,
have_buffered = sta_info_cleanup_expire_txqs(local, sta, ac);
/*
- * First check for frames that should expire on the filtered
- * queue. Frames here were rejected by the driver and are on
- * a separate queue to avoid reordering with normal PS-buffered
- * frames. They also aren't accounted for right now in the
- * total_ps_buffered counter.
+ * Also check for frames that should expire on the filtered
+ * queue. Frames here were rejected by the driver.
*/
for (;;) {
spin_lock_irqsave(&sta->tx_filtered[ac].lock, flags);
@@ -1346,35 +1322,6 @@ static bool sta_info_cleanup_expire_buffered_ac(struct ieee80211_local *local,
ieee80211_free_txskb(&local->hw, skb);
}
- /*
- * Now also check the normal PS-buffered queue, this will
- * only find something if the filtered queue was emptied
- * since the filtered frames are all before the normal PS
- * buffered frames.
- */
- for (;;) {
- spin_lock_irqsave(&sta->ps_tx_buf[ac].lock, flags);
- skb = skb_peek(&sta->ps_tx_buf[ac]);
- if (sta_info_buffer_expired(sta, skb))
- skb = __skb_dequeue(&sta->ps_tx_buf[ac]);
- else
- skb = NULL;
- spin_unlock_irqrestore(&sta->ps_tx_buf[ac].lock, flags);
-
- /*
- * frames are queued in order, so if this one
- * hasn't expired yet (or we reached the end of
- * the queue) we can stop testing
- */
- if (!skb)
- break;
-
- local->total_ps_buffered--;
- ps_dbg(sta->sdata, "Buffered frame expired (STA %pM)\n",
- sta->sta.addr);
- ieee80211_free_txskb(&local->hw, skb);
- }
-
/*
* Finally, recalculate the TIM bit for this station -- it might
* now be clear because the station was too slow to retrieve its
@@ -1387,9 +1334,7 @@ static bool sta_info_cleanup_expire_buffered_ac(struct ieee80211_local *local,
* used to check whether the cleanup timer still needs to run,
* if there are no frames we don't need to rearm the timer.
*/
- return have_buffered ||
- !(skb_queue_empty(&sta->ps_tx_buf[ac]) &&
- skb_queue_empty(&sta->tx_filtered[ac]));
+ return have_buffered || !skb_queue_empty(&sta->tx_filtered[ac]);
}
static bool sta_info_cleanup_expire_buffered(struct ieee80211_local *local,
@@ -1950,9 +1895,11 @@ void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta)
{
struct ieee80211_sub_if_data *sdata = sta->sdata;
struct ieee80211_local *local = sdata->local;
+ struct txq_info *mmpdu_txqi = sta->mmpdu_txqi;
struct sk_buff_head pending;
int filtered = 0, buffered = 0, ac, i;
unsigned long flags;
+ struct sk_buff *skb;
struct ps_data *ps;
if (sdata->vif.type == NL80211_IFTYPE_AP_VLAN)
@@ -1996,21 +1943,21 @@ void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta)
spin_lock_bh(&sta->ps_lock);
/* Send all buffered frames to the station */
for (ac = 0; ac < IEEE80211_NUM_ACS; ac++) {
- int count = skb_queue_len(&pending), tmp;
-
spin_lock_irqsave(&sta->tx_filtered[ac].lock, flags);
skb_queue_splice_tail_init(&sta->tx_filtered[ac], &pending);
spin_unlock_irqrestore(&sta->tx_filtered[ac].lock, flags);
- tmp = skb_queue_len(&pending);
- filtered += tmp - count;
- count = tmp;
-
- spin_lock_irqsave(&sta->ps_tx_buf[ac].lock, flags);
- skb_queue_splice_tail_init(&sta->ps_tx_buf[ac], &pending);
- spin_unlock_irqrestore(&sta->ps_tx_buf[ac].lock, flags);
- tmp = skb_queue_len(&pending);
- buffered += tmp - count;
}
+ filtered = skb_queue_len(&pending);
+
+ /* the internal management TXQ isn't serviced by the driver */
+ if (!sta->sta.txq[IEEE80211_NUM_TIDS]) {
+ rcu_read_lock();
+ while ((skb = ieee80211_tx_dequeue_ps_release(local,
+ mmpdu_txqi)))
+ __skb_queue_tail(&pending, skb);
+ rcu_read_unlock();
+ }
+ buffered = skb_queue_len(&pending) - filtered;
ieee80211_add_pending_skbs(local, &pending);
@@ -2026,12 +1973,10 @@ void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta)
atomic_dec(&ps->num_sta_ps);
- local->total_ps_buffered -= buffered;
-
sta_info_recalc_tim(sta);
ps_dbg(sdata,
- "STA %pM aid %d sending %d filtered/%d PS frames since STA woke up\n",
+ "STA %pM aid %d sending %d filtered/%d management frames since STA woke up\n",
sta->sta.addr, sta->sta.aid, filtered, buffered);
ieee80211_check_fast_xmit(sta);
@@ -2165,7 +2110,6 @@ ieee80211_sta_ps_more_data(struct sta_info *sta, u8 ignored_acs,
continue;
if (!skb_queue_empty(&sta->tx_filtered[ac]) ||
- !skb_queue_empty(&sta->ps_tx_buf[ac]) ||
buffered_tids & ieee80211_tids_for_ac(ac))
return true;
}
@@ -2246,9 +2190,7 @@ ieee80211_sta_ps_get_frames(struct sta_info *sta, int n_frames, u8 ignored_acs,
unsigned long *driver_release_tids)
{
unsigned long txq_buffered_tids = ieee80211_sta_txq_buffered_tids(sta);
- struct ieee80211_sub_if_data *sdata = sta->sdata;
bool from_txqs = ieee80211_sta_ps_from_txqs(sta);
- struct ieee80211_local *local = sdata->local;
int ac;
/* Get response frame(s) and more data bit for the last one. */
@@ -2275,26 +2217,19 @@ ieee80211_sta_ps_get_frames(struct sta_info *sta, int n_frames, u8 ignored_acs,
if (!*driver_release_tids) {
unsigned long txq_tids;
- int remaining;
n_frames = ieee80211_sta_ps_dequeue(&sta->tx_filtered[ac],
n_frames, frames);
txq_tids = txq_buffered_tids & tids & mac80211_tids;
n_frames = ieee80211_sta_ps_dequeue_txqs(sta, txq_tids,
n_frames, frames);
-
- remaining = n_frames;
- n_frames = ieee80211_sta_ps_dequeue(&sta->ps_tx_buf[ac],
- n_frames, frames);
- local->total_ps_buffered -= remaining - n_frames;
}
/* If we have more frames buffered on this AC, then abort the
* loop since we can't send more data from other ACs before
* the buffered frames from this.
*/
- if (!skb_queue_empty(&sta->tx_filtered[ac]) ||
- !skb_queue_empty(&sta->ps_tx_buf[ac]))
+ if (!skb_queue_empty(&sta->tx_filtered[ac]))
break;
if (ieee80211_sta_txq_buffered_tids(sta) & tids & mac80211_tids)
diff --git a/net/mac80211/sta_info.h b/net/mac80211/sta_info.h
index 307c8b76b80f..299cc06653ca 100644
--- a/net/mac80211/sta_info.h
+++ b/net/mac80211/sta_info.h
@@ -655,8 +655,8 @@ struct ieee80211_sta_removed_link_stats {
* @listen_interval: listen interval of this station, when we're acting as AP
* @_flags: STA flags, see &enum ieee80211_sta_info_flags, do not use directly
* @ps_lock: used for powersave (when mac80211 is the AP) related locking
- * @ps_tx_buf: buffers (per AC) of frames to transmit to this station
- * when it leaves power saving state or polls
+ * @mmpdu_txqi: the management TXQ, used internally even if the driver
+ * doesn't want to use it
* @tx_filtered: buffers (per AC) of frames we already tried to
* transmit but were filtered by hardware due to STA having
* entered power saving state, these are also delivered to
@@ -749,7 +749,7 @@ struct sta_info {
/* STA powersave lock and frame queues */
spinlock_t ps_lock;
- struct sk_buff_head ps_tx_buf[IEEE80211_NUM_ACS];
+ struct txq_info *mmpdu_txqi;
struct sk_buff_head tx_filtered[IEEE80211_NUM_ACS];
unsigned long driver_buffered_tids;
unsigned long ps_release_tids;
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index aca68ab112e0..32c36a54fb7e 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -335,13 +335,16 @@ static void ieee80211_txq_enqueue(struct ieee80211_local *local,
struct fq *fq = &local->fq;
struct fq_tin *tin = &txqi->tin;
struct sta_info *sta = NULL;
+ struct sk_buff *drop = NULL;
+ bool was_empty;
u32 flow_idx;
ieee80211_set_skb_enqueue_time(skb);
spin_lock_bh(&fq->lock);
/* TXQ becomes non-empty, update TIM if STA is sleeping */
- if (txqi->txq.sta && !txq_has_queue(&txqi->txq)) {
+ was_empty = !txq_has_queue(&txqi->txq);
+ if (txqi->txq.sta) {
sta = container_of(txqi->txq.sta, struct sta_info, sta);
if (!test_sta_flag(sta, WLAN_STA_PS_STA) &&
!test_sta_flag(sta, WLAN_STA_PS_DRIVER))
@@ -353,11 +356,17 @@ static void ieee80211_txq_enqueue(struct ieee80211_local *local,
* we don't want to apply any shaping or anything we just
* want to simplify the driver API by having them on the
* txqi.
+ * They're not subject to the fq limits either, so limit the
+ * number of them held for a station in powersave.
*/
if (unlikely(txqi->txq.tid == IEEE80211_NUM_TIDS)) {
- IEEE80211_SKB_CB(skb)->control.flags |=
- IEEE80211_TX_INTCFL_NEED_TXPROCESSING;
- __skb_queue_tail(&txqi->frags, skb);
+ if (sta && skb_queue_len(&txqi->frags) >= STA_MAX_TX_BUFFER) {
+ drop = skb;
+ } else {
+ IEEE80211_SKB_CB(skb)->control.flags |=
+ IEEE80211_TX_INTCFL_NEED_TXPROCESSING;
+ __skb_queue_tail(&txqi->frags, skb);
+ }
} else {
flow_idx = fq_flow_idx(fq, skb);
fq_tin_enqueue(fq, tin, flow_idx, skb,
@@ -365,22 +374,29 @@ static void ieee80211_txq_enqueue(struct ieee80211_local *local,
}
spin_unlock_bh(&fq->lock);
- if (sta) {
+ if (unlikely(drop)) {
+ ps_dbg(sta->sdata,
+ "STA %pM management TXQ full - dropping frame\n",
+ sta->sta.addr);
+ ieee80211_free_txskb(&local->hw, drop);
+ return;
+ }
+
+ if (sta && was_empty) {
sta_info_recalc_tim(sta);
sta_info_schedule_cleanup(local);
}
}
/* This function is called whenever the AP is about to exceed the maximum limit
- * of buffered frames for power saving STAs. This situation should not really
- * happen often during normal operation, so dropping the oldest buffered packet
- * from each queue should be OK to make some room for new frames. */
+ * of buffered multicast frames for power saving STAs. This situation should not
+ * really happen often during normal operation, so dropping the oldest buffered
+ * packet from each queue should be OK to make some room for new frames. */
static void purge_old_ps_buffers(struct ieee80211_local *local)
{
int total = 0, purged = 0;
struct sk_buff *skb;
struct ieee80211_sub_if_data *sdata;
- struct sta_info *sta;
list_for_each_entry_rcu(sdata, &local->interfaces, list) {
struct ps_data *ps;
@@ -400,24 +416,6 @@ static void purge_old_ps_buffers(struct ieee80211_local *local)
total += skb_queue_len(&ps->bc_buf);
}
- /*
- * Drop one frame from each station from the lowest-priority
- * AC that has frames at all.
- */
- list_for_each_entry_rcu(sta, &local->sta_list, list) {
- int ac;
-
- for (ac = IEEE80211_AC_BK; ac >= IEEE80211_AC_VO; ac--) {
- skb = skb_dequeue(&sta->ps_tx_buf[ac]);
- total += skb_queue_len(&sta->ps_tx_buf[ac]);
- if (skb) {
- purged++;
- ieee80211_free_txskb(&local->hw, skb);
- break;
- }
- }
- }
-
local->total_ps_buffered = total;
ps_dbg_hw(&local->hw, "PS buffers full - purged %d frames\n", purged);
}
@@ -508,7 +506,6 @@ ieee80211_tx_h_unicast_ps_buf(struct ieee80211_tx_data *tx)
struct sta_info *sta = tx->sta;
struct ieee80211_tx_info *info = IEEE80211_SKB_CB(tx->skb);
struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)tx->skb->data;
- struct ieee80211_local *local = tx->local;
if (unlikely(!sta))
return TX_CONTINUE;
@@ -517,31 +514,32 @@ ieee80211_tx_h_unicast_ps_buf(struct ieee80211_tx_data *tx)
test_sta_flag(sta, WLAN_STA_PS_DRIVER) ||
test_sta_flag(sta, WLAN_STA_PS_DELIVER)) &&
!(info->flags & IEEE80211_TX_CTL_NO_PS_BUFFER))) {
- int ac = skb_get_queue_mapping(tx->skb);
-
if (ieee80211_is_mgmt(hdr->frame_control) &&
!ieee80211_is_bufferable_mmpdu(tx->skb)) {
info->flags |= IEEE80211_TX_CTL_NO_PS_BUFFER;
return TX_CONTINUE;
}
+ /* if it's not uploaded we're before assoc, can't be sleeping */
+ if (unlikely(!sta->uploaded)) {
+ info->flags |= IEEE80211_TX_CTL_NO_PS_BUFFER;
+ return TX_CONTINUE;
+ }
+
/* continue and put the frame on the TXQ */
- if (sta->uploaded &&
- (ieee80211_is_data_present(hdr->frame_control) ||
- sta->sta.txq[IEEE80211_NUM_TIDS]))
+ if (ieee80211_is_data_present(hdr->frame_control) ||
+ sta->sta.txq[IEEE80211_NUM_TIDS])
return TX_CONTINUE;
- ps_dbg(sta->sdata, "STA %pM aid %d: PS buffer for AC %d\n",
- sta->sta.addr, sta->sta.aid, ac);
- if (tx->local->total_ps_buffered >= TOTAL_MAX_TX_BUFFER)
- purge_old_ps_buffers(tx->local);
+ ps_dbg(sta->sdata, "STA %pM aid %d: buffer management frame\n",
+ sta->sta.addr, sta->sta.aid);
/* sync with ieee80211_sta_ps_deliver_wakeup */
spin_lock(&sta->ps_lock);
/*
- * STA woke up the meantime and all the frames on ps_tx_buf have
- * been queued to pending queue. No reordering can happen, go
- * ahead and Tx the packet.
+ * STA woke up the meantime and all the frames on the internal
+ * management TXQ have been queued to pending queue. No
+ * reordering can happen, go ahead and Tx the packet.
*/
if (!test_sta_flag(sta, WLAN_STA_PS_STA) &&
!test_sta_flag(sta, WLAN_STA_PS_DRIVER) &&
@@ -550,33 +548,10 @@ ieee80211_tx_h_unicast_ps_buf(struct ieee80211_tx_data *tx)
return TX_CONTINUE;
}
- if (skb_queue_len(&sta->ps_tx_buf[ac]) >= STA_MAX_TX_BUFFER) {
- struct sk_buff *old = skb_dequeue(&sta->ps_tx_buf[ac]);
- ps_dbg(tx->sdata,
- "STA %pM TX buffer for AC %d full - dropping oldest frame\n",
- sta->sta.addr, ac);
- ieee80211_free_txskb(&local->hw, old);
- } else
- tx->local->total_ps_buffered++;
-
- info->control.jiffies = jiffies;
info->control.vif = &tx->sdata->vif;
- info->control.flags |= IEEE80211_TX_INTCFL_NEED_TXPROCESSING;
- info->flags &= ~IEEE80211_TX_TEMPORARY_FLAGS;
- skb_queue_tail(&sta->ps_tx_buf[ac], tx->skb);
+ ieee80211_txq_enqueue(tx->local, sta->mmpdu_txqi, tx->skb);
spin_unlock(&sta->ps_lock);
- if (!timer_pending(&local->sta_cleanup))
- mod_timer(&local->sta_cleanup,
- round_jiffies(jiffies +
- STA_INFO_CLEANUP_INTERVAL));
-
- /*
- * We queued up some frames, so the TIM bit might
- * need to be set, recalculate it.
- */
- sta_info_recalc_tim(sta);
-
return TX_QUEUED;
} else if (unlikely(test_sta_flag(sta, WLAN_STA_PS_STA))) {
ps_dbg(tx->sdata,
@@ -1670,24 +1645,26 @@ void ieee80211_txq_init(struct ieee80211_sub_if_data *sdata,
return;
}
+ txqi->txq.sta = &sta->sta;
+ txqi->txq.tid = tid;
+
if (tid == IEEE80211_NUM_TIDS) {
+ txqi->txq.ac = IEEE80211_AC_VO;
+ sta->mmpdu_txqi = txqi;
+
+ /* driver opt-in, but we always use it internally */
if (sdata->vif.type == NL80211_IFTYPE_STATION) {
- /* Drivers need to opt in to the management MPDU TXQ */
if (!ieee80211_hw_check(&sdata->local->hw,
STA_MMPDU_TXQ))
return;
} else if (!ieee80211_hw_check(&sdata->local->hw,
BUFF_MMPDU_TXQ)) {
- /* Drivers need to opt in to the bufferable MMPDU TXQ */
return;
}
- txqi->txq.ac = IEEE80211_AC_VO;
} else {
txqi->txq.ac = ieee80211_ac_from_tid(tid);
}
- txqi->txq.sta = &sta->sta;
- txqi->txq.tid = tid;
sta->sta.txq[tid] = &txqi->txq;
}
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 19/30] wifi: mac80211: use MMPDU TXQ for filtered mgmt frames
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (17 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 18/30] wifi: mac80211: always use per-STA MMPDU TXQ Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 20/30] wifi: mac80211: avoid reordering PS-release MMPDUs Johannes Berg
` (10 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
After the previous change, filtered data frames lived on the
TXQs in the frags list, but not management frames. Change it
for management frames, now that we always have the MMPDU TXQ,
which enables removing the tx_filtered[] queues entirely.
Note a small change that filtered MMPDUs are now VO, which
was wrong because they don't have a QoS header and then BE
was used as a fallback.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/debugfs_sta.c | 2 +-
net/mac80211/mesh_ps.c | 29 +++-------
net/mac80211/sta_info.c | 83 ++++-------------------------
net/mac80211/sta_info.h | 7 ---
net/mac80211/status.c | 106 ++++++++++++++++++++-----------------
5 files changed, 76 insertions(+), 151 deletions(-)
diff --git a/net/mac80211/debugfs_sta.c b/net/mac80211/debugfs_sta.c
index 088fd9d90c81..e0d797045d0b 100644
--- a/net/mac80211/debugfs_sta.c
+++ b/net/mac80211/debugfs_sta.c
@@ -112,7 +112,7 @@ static ssize_t sta_num_ps_buf_frames_read(struct file *file,
for (ac = 0; ac < IEEE80211_NUM_ACS; ac++) {
unsigned long tids = ieee80211_tids_for_ac(ac);
- unsigned int n = skb_queue_len(&sta->tx_filtered[ac]);
+ unsigned int n = 0;
int tid;
spin_lock_bh(&fq->lock);
diff --git a/net/mac80211/mesh_ps.c b/net/mac80211/mesh_ps.c
index e3d815fbe31b..513f98e94899 100644
--- a/net/mac80211/mesh_ps.c
+++ b/net/mac80211/mesh_ps.c
@@ -462,7 +462,7 @@ static void mps_frame_deliver(struct sta_info *sta, int n_frames)
int ac;
struct sk_buff_head frames;
struct sk_buff *skb;
- bool more_data = false;
+ bool more_data;
skb_queue_head_init(&frames);
@@ -472,23 +472,12 @@ static void mps_frame_deliver(struct sta_info *sta, int n_frames)
txq_buffered_tids = ieee80211_sta_txq_buffered_tids(sta);
/* collect frame(s) from buffers */
- for (ac = 0; ac < IEEE80211_NUM_ACS; ac++) {
- while (n_frames > 0 &&
- (skb = skb_dequeue(&sta->tx_filtered[ac]))) {
- n_frames--;
- __skb_queue_tail(&frames, skb);
- }
-
+ for (ac = 0; ac < IEEE80211_NUM_ACS; ac++)
n_frames = ieee80211_sta_ps_dequeue_txqs(sta,
txq_buffered_tids & ieee80211_tids_for_ac(ac),
n_frames, &frames);
- if (!skb_queue_empty(&sta->tx_filtered[ac]))
- more_data = true;
- }
-
- if (ieee80211_sta_txq_buffered_tids(sta))
- more_data = true;
+ more_data = ieee80211_sta_txq_buffered_tids(sta);
/* nothing to send? -> EOSP */
if (skb_queue_empty(&frames)) {
@@ -588,8 +577,7 @@ void ieee80211_mpsp_trigger_process(u8 *qc, struct sta_info *sta,
void ieee80211_mps_frame_release(struct sta_info *sta,
struct ieee802_11_elems *elems)
{
- int ac, buffer_local = 0;
- bool has_buffered = false;
+ bool has_buffered = false, buffer_local = false;
if (sta->mesh->plink_state == NL80211_PLINK_ESTAB)
has_buffered = ieee80211_check_tim(elems->tim, elems->tim_len,
@@ -604,12 +592,9 @@ void ieee80211_mps_frame_release(struct sta_info *sta,
(!elems->awake_window || !get_unaligned_le16(elems->awake_window)))
return;
- if (!test_sta_flag(sta, WLAN_STA_MPSP_OWNER)) {
- for (ac = 0; ac < IEEE80211_NUM_ACS; ac++)
- buffer_local += skb_queue_len(&sta->tx_filtered[ac]);
- if (ieee80211_sta_txq_buffered_tids(sta))
- buffer_local++;
- }
+ if (!test_sta_flag(sta, WLAN_STA_MPSP_OWNER) &&
+ ieee80211_sta_txq_buffered_tids(sta))
+ buffer_local = true;
if (!has_buffered && !buffer_local)
return;
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index 3161e00c32a9..ad73a20eddb5 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -123,10 +123,9 @@ void ieee80211_purge_sta_txqs(struct sta_info *sta)
static void __cleanup_single_sta(struct sta_info *sta)
{
- int ac, i;
struct ieee80211_sub_if_data *sdata = sta->sdata;
- struct ieee80211_local *local = sdata->local;
struct ps_data *ps;
+ int i;
cancel_work_sync(&sta->drv_deliver_wk);
@@ -150,9 +149,6 @@ static void __cleanup_single_sta(struct sta_info *sta)
ieee80211_purge_sta_txqs(sta);
- for (ac = 0; ac < IEEE80211_NUM_ACS; ac++)
- ieee80211_purge_tx_queue(&local->hw, &sta->tx_filtered[ac]);
-
if (ieee80211_vif_is_mesh(&sdata->vif))
mesh_sta_cleanup(sta);
@@ -713,7 +709,6 @@ __sta_info_alloc(struct ieee80211_sub_if_data *sdata,
sta->airtime_weight = IEEE80211_DEFAULT_AIRTIME_WEIGHT;
for (i = 0; i < IEEE80211_NUM_ACS; i++) {
- skb_queue_head_init(&sta->tx_filtered[i]);
sta->airtime[i].deficit = sta->airtime_weight;
atomic_set(&sta->airtime[i].aql_tx_pending, 0);
sta->airtime[i].aql_limit_low = local->aql_txq_limit_low[i];
@@ -1160,10 +1155,6 @@ static void __sta_info_recalc_tim(struct sta_info *sta, bool ignore_pending)
if (ignore_for_tim & ieee80211_ac_to_qos_mask[ac])
continue;
- indicate_tim |= !skb_queue_empty(&sta->tx_filtered[ac]);
- if (indicate_tim)
- break;
-
tids = ieee80211_tids_for_ac(ac);
indicate_tim |=
@@ -1292,36 +1283,10 @@ static bool sta_info_cleanup_expire_txqs(struct ieee80211_local *local,
static bool sta_info_cleanup_expire_buffered_ac(struct ieee80211_local *local,
struct sta_info *sta, int ac)
{
- unsigned long flags;
- struct sk_buff *skb;
bool have_buffered;
have_buffered = sta_info_cleanup_expire_txqs(local, sta, ac);
- /*
- * Also check for frames that should expire on the filtered
- * queue. Frames here were rejected by the driver.
- */
- for (;;) {
- spin_lock_irqsave(&sta->tx_filtered[ac].lock, flags);
- skb = skb_peek(&sta->tx_filtered[ac]);
- if (sta_info_buffer_expired(sta, skb))
- skb = __skb_dequeue(&sta->tx_filtered[ac]);
- else
- skb = NULL;
- spin_unlock_irqrestore(&sta->tx_filtered[ac].lock, flags);
-
- /*
- * Frames are queued in order, so if this one
- * hasn't expired yet we can stop testing. If
- * we actually reached the end of the queue we
- * also need to stop, of course.
- */
- if (!skb)
- break;
- ieee80211_free_txskb(&local->hw, skb);
- }
-
/*
* Finally, recalculate the TIM bit for this station -- it might
* now be clear because the station was too slow to retrieve its
@@ -1334,7 +1299,7 @@ static bool sta_info_cleanup_expire_buffered_ac(struct ieee80211_local *local,
* used to check whether the cleanup timer still needs to run,
* if there are no frames we don't need to rearm the timer.
*/
- return have_buffered || !skb_queue_empty(&sta->tx_filtered[ac]);
+ return have_buffered;
}
static bool sta_info_cleanup_expire_buffered(struct ieee80211_local *local,
@@ -1897,10 +1862,9 @@ void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta)
struct ieee80211_local *local = sdata->local;
struct txq_info *mmpdu_txqi = sta->mmpdu_txqi;
struct sk_buff_head pending;
- int filtered = 0, buffered = 0, ac, i;
- unsigned long flags;
struct sk_buff *skb;
struct ps_data *ps;
+ int buffered, i;
if (sdata->vif.type == NL80211_IFTYPE_AP_VLAN)
sdata = container_of(sdata->bss, struct ieee80211_sub_if_data,
@@ -1939,17 +1903,11 @@ void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta)
skb_queue_head_init(&pending);
- /* sync with ieee80211_tx_h_unicast_ps_buf */
+ /*
+ * Sync with ieee80211_tx_h_unicast_ps_buf() and the TX status
+ * path, the internal management TXQ isn't serviced by the driver.
+ */
spin_lock_bh(&sta->ps_lock);
- /* Send all buffered frames to the station */
- for (ac = 0; ac < IEEE80211_NUM_ACS; ac++) {
- spin_lock_irqsave(&sta->tx_filtered[ac].lock, flags);
- skb_queue_splice_tail_init(&sta->tx_filtered[ac], &pending);
- spin_unlock_irqrestore(&sta->tx_filtered[ac].lock, flags);
- }
- filtered = skb_queue_len(&pending);
-
- /* the internal management TXQ isn't serviced by the driver */
if (!sta->sta.txq[IEEE80211_NUM_TIDS]) {
rcu_read_lock();
while ((skb = ieee80211_tx_dequeue_ps_release(local,
@@ -1957,7 +1915,7 @@ void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta)
__skb_queue_tail(&pending, skb);
rcu_read_unlock();
}
- buffered = skb_queue_len(&pending) - filtered;
+ buffered = skb_queue_len(&pending);
ieee80211_add_pending_skbs(local, &pending);
@@ -1976,8 +1934,8 @@ void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta)
sta_info_recalc_tim(sta);
ps_dbg(sdata,
- "STA %pM aid %d sending %d filtered/%d management frames since STA woke up\n",
- sta->sta.addr, sta->sta.aid, filtered, buffered);
+ "STA %pM aid %d sending %d management frames since STA woke up\n",
+ sta->sta.addr, sta->sta.aid, buffered);
ieee80211_check_fast_xmit(sta);
}
@@ -2109,8 +2067,7 @@ ieee80211_sta_ps_more_data(struct sta_info *sta, u8 ignored_acs,
if (ignored_acs & ieee80211_ac_to_qos_mask[ac])
continue;
- if (!skb_queue_empty(&sta->tx_filtered[ac]) ||
- buffered_tids & ieee80211_tids_for_ac(ac))
+ if (buffered_tids & ieee80211_tids_for_ac(ac))
return true;
}
@@ -2170,19 +2127,6 @@ int ieee80211_sta_ps_dequeue_txqs(const struct sta_info *sta, unsigned long tids
return n_frames;
}
-static int ieee80211_sta_ps_dequeue(struct sk_buff_head *queue, int n_frames,
- struct sk_buff_head *frames)
-{
- struct sk_buff *skb;
-
- while (n_frames > 0 && (skb = skb_dequeue(queue))) {
- __skb_queue_tail(frames, skb);
- n_frames--;
- }
-
- return n_frames;
-}
-
static void
ieee80211_sta_ps_get_frames(struct sta_info *sta, int n_frames, u8 ignored_acs,
enum ieee80211_frame_release_type reason,
@@ -2218,8 +2162,6 @@ ieee80211_sta_ps_get_frames(struct sta_info *sta, int n_frames, u8 ignored_acs,
if (!*driver_release_tids) {
unsigned long txq_tids;
- n_frames = ieee80211_sta_ps_dequeue(&sta->tx_filtered[ac],
- n_frames, frames);
txq_tids = txq_buffered_tids & tids & mac80211_tids;
n_frames = ieee80211_sta_ps_dequeue_txqs(sta, txq_tids,
n_frames, frames);
@@ -2229,9 +2171,6 @@ ieee80211_sta_ps_get_frames(struct sta_info *sta, int n_frames, u8 ignored_acs,
* loop since we can't send more data from other ACs before
* the buffered frames from this.
*/
- if (!skb_queue_empty(&sta->tx_filtered[ac]))
- break;
-
if (ieee80211_sta_txq_buffered_tids(sta) & tids & mac80211_tids)
break;
}
diff --git a/net/mac80211/sta_info.h b/net/mac80211/sta_info.h
index 299cc06653ca..89be51a010b8 100644
--- a/net/mac80211/sta_info.h
+++ b/net/mac80211/sta_info.h
@@ -657,12 +657,6 @@ struct ieee80211_sta_removed_link_stats {
* @ps_lock: used for powersave (when mac80211 is the AP) related locking
* @mmpdu_txqi: the management TXQ, used internally even if the driver
* doesn't want to use it
- * @tx_filtered: buffers (per AC) of frames we already tried to
- * transmit but were filtered by hardware due to STA having
- * entered power saving state, these are also delivered to
- * the station when it leaves powersave or polls for frames.
- * Only used for frames not on a TID TXQ, filtered data frames
- * are put back onto the frags queue of their TXQ instead.
* @driver_buffered_tids: bitmap of TIDs the driver has data buffered on
* @ps_release_tids: TIDs for which the driver can dequeue from TXQs during
* during release_buffered_frames(), only non-zero during that
@@ -750,7 +744,6 @@ struct sta_info {
/* STA powersave lock and frame queues */
spinlock_t ps_lock;
struct txq_info *mmpdu_txqi;
- struct sk_buff_head tx_filtered[IEEE80211_NUM_ACS];
unsigned long driver_buffered_tids;
unsigned long ps_release_tids;
diff --git a/net/mac80211/status.c b/net/mac80211/status.c
index 6134c37e081b..9b8e0b7e173b 100644
--- a/net/mac80211/status.c
+++ b/net/mac80211/status.c
@@ -49,7 +49,7 @@ static void ieee80211_handle_filtered_frame(struct ieee80211_local *local,
struct ieee80211_hdr *hdr = (void *)skb->data;
struct txq_info *txqi = NULL;
u32 ctrl_flags, flags;
- int ac;
+ bool queued;
if (info->flags & (IEEE80211_TX_CTL_NO_PS_BUFFER |
IEEE80211_TX_CTL_AMPDU |
@@ -58,11 +58,12 @@ static void ieee80211_handle_filtered_frame(struct ieee80211_local *local,
return;
}
- if (ieee80211_is_data_present(hdr->frame_control) && sta->uploaded) {
- u8 tid = skb->priority & IEEE80211_QOS_CTL_TID_MASK;
-
- if (sta->sta.txq[tid])
- txqi = to_txq_info(sta->sta.txq[tid]);
+ if (sta->uploaded) {
+ if (ieee80211_is_data_present(hdr->frame_control))
+ txqi = sta_txqi(sta, skb->priority &
+ IEEE80211_QOS_CTL_TID_MASK);
+ else
+ txqi = sta->mmpdu_txqi;
}
/* keep these flags across another trip through the TXQ */
@@ -86,10 +87,6 @@ static void ieee80211_handle_filtered_frame(struct ieee80211_local *local,
info->control.flags |= IEEE80211_TX_INTCFL_NEED_TXPROCESSING;
info->flags |= IEEE80211_TX_INTFL_RETRANSMISSION;
info->flags &= ~IEEE80211_TX_TEMPORARY_FLAGS;
- if (txqi) {
- info->control.flags |= ctrl_flags;
- info->flags |= flags;
- }
sta->deflink.status_stats.filtered++;
@@ -105,7 +102,6 @@ static void ieee80211_handle_filtered_frame(struct ieee80211_local *local,
if (ieee80211_is_data_qos(hdr->frame_control)) {
u8 *p = ieee80211_get_qos_ctl(hdr);
- int tid = *p & IEEE80211_QOS_CTL_TID_MASK;
/*
* Clear EOSP if set, this could happen e.g.
@@ -114,9 +110,6 @@ static void ieee80211_handle_filtered_frame(struct ieee80211_local *local,
*/
if (*p & IEEE80211_QOS_CTL_EOSP)
*p &= ~IEEE80211_QOS_CTL_EOSP;
- ac = ieee80211_ac_from_tid(tid);
- } else {
- ac = IEEE80211_AC_BE;
}
/*
@@ -161,50 +154,66 @@ static void ieee80211_handle_filtered_frame(struct ieee80211_local *local,
* changes before calling TX status events if ordering can be
* unknown.
*/
- if (txqi) {
- if (!test_sta_flag(sta, WLAN_STA_PS_STA)) {
- if (info->flags & IEEE80211_TX_INTFL_RETRIED)
- goto drop;
- info->flags |= IEEE80211_TX_INTFL_RETRIED;
+ if (!txqi)
+ goto retry;
+
+ if (txqi->txq.tid == IEEE80211_NUM_TIDS &&
+ !sta->sta.txq[IEEE80211_NUM_TIDS]) {
+ /*
+ * The internal management TXQ is only used while the station
+ * is in powersave, see ieee80211_tx_h_unicast_ps_buf().
+ */
+ spin_lock_bh(&sta->ps_lock);
+ if (!test_sta_flag(sta, WLAN_STA_PS_STA) &&
+ !test_sta_flag(sta, WLAN_STA_PS_DRIVER) &&
+ !test_sta_flag(sta, WLAN_STA_PS_DELIVER)) {
+ spin_unlock_bh(&sta->ps_lock);
+ goto retry;
}
- if (!ieee80211_txq_queue_filtered(local, txqi, skb))
+ info->control.flags |= ctrl_flags;
+ info->flags |= flags;
+ queued = ieee80211_txq_queue_filtered(local, txqi, skb);
+ spin_unlock_bh(&sta->ps_lock);
+
+ if (!queued)
goto drop;
- /*
- * ieee80211_sta_ps_deliver_wakeup() either sees the frame
- * and wakes the TXQ, or this code sees it awake and does
- */
- smp_mb();
-
- if (test_sta_flag(sta, WLAN_STA_PS_STA)) {
+ if (test_sta_flag(sta, WLAN_STA_PS_STA))
sta_info_recalc_tim(sta);
-
- if (!timer_pending(&local->sta_cleanup))
- mod_timer(&local->sta_cleanup,
- round_jiffies(jiffies +
- STA_INFO_CLEANUP_INTERVAL));
- } else {
- /* avoid recursive driver calls */
- ieee80211_schedule_txq(&local->hw, &txqi->txq);
- set_bit(IEEE80211_TXQ_DIRTY, &txqi->flags);
- tasklet_schedule(&local->wake_txqs_tasklet);
- }
+ sta_info_schedule_cleanup(local);
return;
}
- if (test_sta_flag(sta, WLAN_STA_PS_STA) &&
- skb_queue_len(&sta->tx_filtered[ac]) < STA_MAX_TX_BUFFER) {
- skb_queue_tail(&sta->tx_filtered[ac], skb);
+ if (!test_sta_flag(sta, WLAN_STA_PS_STA)) {
+ if (info->flags & IEEE80211_TX_INTFL_RETRIED)
+ goto drop;
+ info->flags |= IEEE80211_TX_INTFL_RETRIED;
+ }
+
+ info->control.flags |= ctrl_flags;
+ info->flags |= flags;
+ if (!ieee80211_txq_queue_filtered(local, txqi, skb))
+ goto drop;
+
+ /*
+ * ieee80211_sta_ps_deliver_wakeup() either sees the frame
+ * and wakes the TXQ, or this code sees it awake and does
+ */
+ smp_mb();
+
+ if (test_sta_flag(sta, WLAN_STA_PS_STA)) {
sta_info_recalc_tim(sta);
-
- if (!timer_pending(&local->sta_cleanup))
- mod_timer(&local->sta_cleanup,
- round_jiffies(jiffies +
- STA_INFO_CLEANUP_INTERVAL));
- return;
+ sta_info_schedule_cleanup(local);
+ } else {
+ /* avoid recursive driver calls */
+ ieee80211_schedule_txq(&local->hw, &txqi->txq);
+ set_bit(IEEE80211_TXQ_DIRTY, &txqi->flags);
+ tasklet_schedule(&local->wake_txqs_tasklet);
}
+ return;
+retry:
if (!test_sta_flag(sta, WLAN_STA_PS_STA) &&
!(info->flags & IEEE80211_TX_INTFL_RETRIED)) {
/* Software retry the packet once */
@@ -215,8 +224,7 @@ static void ieee80211_handle_filtered_frame(struct ieee80211_local *local,
drop:
ps_dbg_ratelimited(sta->sdata,
- "dropped TX filtered frame, queue_len=%d PS=%d @%lu\n",
- txqi ? -1 : skb_queue_len(&sta->tx_filtered[ac]),
+ "dropped TX filtered frame PS=%d @%lu\n",
!!test_sta_flag(sta, WLAN_STA_PS_STA), jiffies);
ieee80211_free_txskb(&local->hw, skb);
}
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 20/30] wifi: mac80211: avoid reordering PS-release MMPDUs
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (18 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 19/30] wifi: mac80211: use MMPDU TXQ for filtered mgmt frames Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 21/30] wifi: mac80211: buffer multicast frames on vif TXQ Johannes Berg
` (9 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
MMPDUs released during powersave are transmitted through the
'pending' queues, per AC/HW queue, based on the SKB HW queue
mapping. This can mix up their order because we try to send
e.g. AddBA request for an AC on that AC, and if the station
then goes to sleep they might be released on wakeup together,
and cross on the TXQs.
To avoid this, assign the HW queue based on the TXQ AC when
releasing frames after/during powersave.
This isn't a new issue, but is much easier to fix now.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/tx.c | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 32c36a54fb7e..0b322bef5a40 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -4265,7 +4265,18 @@ EXPORT_SYMBOL(ieee80211_tx_dequeue);
struct sk_buff *ieee80211_tx_dequeue_ps_release(struct ieee80211_local *local,
struct txq_info *txqi)
{
- return __ieee80211_tx_dequeue(&local->hw, &txqi->txq, true);
+ struct ieee80211_txq *txq = &txqi->txq;
+ struct sk_buff *skb;
+
+ skb = __ieee80211_tx_dequeue(&local->hw, txq, true);
+ if (!skb)
+ return NULL;
+
+ /* to avoid buffered MMPDUs for different ACs crossing each other */
+ skb_set_queue_mapping(skb, txq->ac);
+ IEEE80211_SKB_CB(skb)->hw_queue = txq->vif->hw_queue[txq->ac];
+
+ return skb;
}
static inline s32 ieee80211_sta_deficit(struct sta_info *sta, u8 ac)
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 21/30] wifi: mac80211: buffer multicast frames on vif TXQ
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (19 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 20/30] wifi: mac80211: avoid reordering PS-release MMPDUs Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 22/30] wifi: mac80211: always use TXQ for mcast data frames Johannes Berg
` (8 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
For devices with HOST_BROADCAST_PS_BUFFERING, mcast
frames are buffered in aanother bc_buf queue while
any station is sleeping, for after DTIM, so drivers
can retrieve them with ieee80211_get_buffered_bc().
Put them on the interface TXQ instead and block it
until the frames can be released.
Frame processing is now on dequeue, like all other
frames, so IEEE80211_TX_PS_BUFFERED can go away.
Remove the total_ps_buffered debugfs file, we can't
really provide it easily now.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/cfg.c | 3 +-
net/mac80211/debugfs.c | 3 -
net/mac80211/debugfs_netdev.c | 9 +-
net/mac80211/ieee80211_i.h | 15 +--
net/mac80211/iface.c | 24 +---
net/mac80211/mesh.c | 4 +-
net/mac80211/tx.c | 211 ++++++++++++++++++----------------
7 files changed, 130 insertions(+), 139 deletions(-)
diff --git a/net/mac80211/cfg.c b/net/mac80211/cfg.c
index 6e466f8e1653..b8385789d6e9 100644
--- a/net/mac80211/cfg.c
+++ b/net/mac80211/cfg.c
@@ -2369,8 +2369,7 @@ static int ieee80211_stop_ap(struct wiphy *wiphy, struct net_device *dev,
drv_stop_ap(sdata->local, sdata, link_conf);
/* free all potentially still buffered bcast frames */
- local->total_ps_buffered -= skb_queue_len(&sdata->u.ap.ps.bc_buf);
- ieee80211_purge_tx_queue(&local->hw, &sdata->u.ap.ps.bc_buf);
+ ieee80211_txq_purge_mcast_ps(local, sdata);
ieee80211_link_copy_chanctx_to_vlans(link, true);
ieee80211_link_release_channel(link);
diff --git a/net/mac80211/debugfs.c b/net/mac80211/debugfs.c
index 027db8b9f846..e7c1d376b98d 100644
--- a/net/mac80211/debugfs.c
+++ b/net/mac80211/debugfs.c
@@ -64,8 +64,6 @@ DEBUGFS_READONLY_FILE(user_power, "%d",
local->user_power_level);
DEBUGFS_READONLY_FILE(power, "%d",
local->hw.conf.power_level);
-DEBUGFS_READONLY_FILE(total_ps_buffered, "%d",
- local->total_ps_buffered);
DEBUGFS_READONLY_FILE(wep_iv, "%#08x",
local->wep_iv & 0xffffff);
DEBUGFS_READONLY_FILE(rate_ctrl_alg, "%s",
@@ -654,7 +652,6 @@ void debugfs_hw_add(struct ieee80211_local *local)
local->debugfs.keys = debugfs_create_dir("keys", phyd);
- DEBUGFS_ADD(total_ps_buffered);
DEBUGFS_ADD(wep_iv);
DEBUGFS_ADD(rate_ctrl_alg);
DEBUGFS_ADD(queues);
diff --git a/net/mac80211/debugfs_netdev.c b/net/mac80211/debugfs_netdev.c
index 81d18ab83543..616546eee2cf 100644
--- a/net/mac80211/debugfs_netdev.c
+++ b/net/mac80211/debugfs_netdev.c
@@ -608,8 +608,15 @@ IEEE80211_IF_FILE(num_mcast_sta_vlan, u.vlan.num_mcast_sta, ATOMIC);
static ssize_t ieee80211_if_fmt_num_buffered_multicast(
const struct ieee80211_sub_if_data *sdata, char *buf, int buflen)
{
+ struct txq_info *txqi;
+
+ if (!sdata->vif.txq)
+ return scnprintf(buf, buflen, "0\n");
+
+ txqi = to_txq_info(sdata->vif.txq);
return scnprintf(buf, buflen, "%u\n",
- skb_queue_len(&sdata->u.ap.ps.bc_buf));
+ test_bit(IEEE80211_TXQ_MCAST_PS, &txqi->flags) ?
+ txqi->tin.backlog_packets : 0);
}
IEEE80211_IF_FILE_R(num_buffered_multicast);
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index 89581935659c..b498765afa20 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -45,11 +45,6 @@ struct ieee80211_mesh_fast_tx;
* associated stations are using power saving. */
#define AP_MAX_BC_BUFFER 128
-/* Maximum number of multicast frames buffered for all interfaces.
- * Note: increasing this limit increases the potential memory requirement. Each
- * frame can be up to about 2 kB long. */
-#define TOTAL_MAX_TX_BUFFER 512
-
/* Required encryption head and tailroom */
#define IEEE80211_ENCRYPT_HEADROOM 8
#define IEEE80211_ENCRYPT_TAILROOM 26 /* current max: GMAC/CMAC-256 MME */
@@ -188,7 +183,6 @@ typedef unsigned __bitwise ieee80211_tx_result;
#define TX_QUEUED ((__force ieee80211_tx_result) 2u)
#define IEEE80211_TX_UNICAST BIT(1)
-#define IEEE80211_TX_PS_BUFFERED BIT(2)
struct ieee80211_tx_data {
struct sk_buff *skb;
@@ -341,7 +335,6 @@ struct ps_data {
* NB: don't touch this bitmap, use sta_info_{set,clear}_tim_bit */
u8 tim[sizeof(unsigned long) * BITS_TO_LONGS(IEEE80211_MAX_AID + 1)]
__aligned(__alignof__(unsigned long));
- struct sk_buff_head bc_buf;
atomic_t num_sta_ps; /* number of stations in PS mode */
int dtim_count;
bool dtim_bc_mc;
@@ -979,6 +972,8 @@ enum txq_info_flags {
IEEE80211_TXQ_NO_AMSDU,
IEEE80211_TXQ_DIRTY,
IEEE80211_TXQ_PS_BACKLOG,
+ /* on the interface TXQ: frames held until after the DTIM beacon */
+ IEEE80211_TXQ_MCAST_PS,
};
/**
@@ -1717,10 +1712,6 @@ struct ieee80211_local {
#endif /* CONFIG_MAC80211_DEBUG_COUNTERS */
- int total_ps_buffered; /* total number of all buffered multicast
- * packets for power saving stations
- */
-
bool pspolling;
/*
* PS can only be enabled when we have exactly one managed
@@ -2745,6 +2736,8 @@ bool ieee80211_txq_queue_filtered(struct ieee80211_local *local,
struct txq_info *txqi, struct sk_buff *skb);
void ieee80211_txq_purge(struct ieee80211_local *local,
struct txq_info *txqi);
+void ieee80211_txq_purge_mcast_ps(struct ieee80211_local *local,
+ const struct ieee80211_sub_if_data *sdata);
void ieee80211_purge_sta_txqs(struct sta_info *sta);
void ieee80211_txq_remove_vlan(struct ieee80211_local *local,
struct ieee80211_sub_if_data *sdata);
diff --git a/net/mac80211/iface.c b/net/mac80211/iface.c
index a4f29444ba45..b1666ab2d5d0 100644
--- a/net/mac80211/iface.c
+++ b/net/mac80211/iface.c
@@ -485,7 +485,6 @@ static void ieee80211_do_stop(struct ieee80211_sub_if_data *sdata, bool going_do
struct sk_buff *skb, *tmp;
u32 hw_reconf_flags = 0;
int i, flushed;
- struct ps_data *ps;
struct cfg80211_chan_def chandef;
bool cancel_scan;
struct cfg80211_nan_func *func;
@@ -584,28 +583,8 @@ static void ieee80211_do_stop(struct ieee80211_sub_if_data *sdata, bool going_do
GFP_KERNEL, 0);
}
- if (sdata->vif.type == NL80211_IFTYPE_AP) {
+ if (sdata->vif.type == NL80211_IFTYPE_AP)
WARN_ON(!list_empty(&sdata->u.ap.vlans));
- } else if (sdata->vif.type == NL80211_IFTYPE_AP_VLAN) {
- /* remove all packets in parent bc_buf pointing to this dev */
- __skb_queue_head_init(&freeq);
- ps = &sdata->bss->ps;
-
- spin_lock_irqsave(&ps->bc_buf.lock, flags);
- skb_queue_walk_safe(&ps->bc_buf, skb, tmp) {
- if (skb->dev == sdata->dev) {
- __skb_unlink(skb, &ps->bc_buf);
- local->total_ps_buffered--;
- __skb_queue_tail(&freeq, skb);
- }
- }
- spin_unlock_irqrestore(&ps->bc_buf.lock, flags);
-
- skb_queue_walk_safe(&freeq, skb, tmp) {
- __skb_unlink(skb, &freeq);
- ieee80211_free_txskb(&local->hw, skb);
- }
- }
if (going_down)
local->open_count--;
@@ -1948,7 +1927,6 @@ static void ieee80211_setup_sdata(struct ieee80211_sub_if_data *sdata,
sdata->vif.p2p = true;
fallthrough;
case NL80211_IFTYPE_AP:
- skb_queue_head_init(&sdata->u.ap.ps.bc_buf);
INIT_LIST_HEAD(&sdata->u.ap.vlans);
sdata->vif.bss_conf.bssid = sdata->vif.addr;
break;
diff --git a/net/mac80211/mesh.c b/net/mac80211/mesh.c
index 5f523e14cf84..99baf8293196 100644
--- a/net/mac80211/mesh.c
+++ b/net/mac80211/mesh.c
@@ -1243,8 +1243,7 @@ void ieee80211_stop_mesh(struct ieee80211_sub_if_data *sdata)
kfree_rcu(bcn, rcu_head);
/* free all potentially still buffered group-addressed frames */
- local->total_ps_buffered -= skb_queue_len(&ifmsh->ps.bc_buf);
- skb_queue_purge(&ifmsh->ps.bc_buf);
+ ieee80211_txq_purge_mcast_ps(local, sdata);
timer_delete_sync(&sdata->u.mesh.housekeeping_timer);
timer_delete_sync(&sdata->u.mesh.mesh_path_root_timer);
@@ -1789,7 +1788,6 @@ void ieee80211_mesh_init_sdata(struct ieee80211_sub_if_data *sdata)
timer_setup(&ifmsh->mesh_path_root_timer,
ieee80211_mesh_path_root_timer, 0);
INIT_LIST_HEAD(&ifmsh->preq_queue.list);
- skb_queue_head_init(&ifmsh->ps.bc_buf);
spin_lock_init(&ifmsh->mesh_preq_queue_lock);
spin_lock_init(&ifmsh->sync_offset_lock);
RCU_INIT_POINTER(ifmsh->beacon, NULL);
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 0b322bef5a40..0361cdfdbb70 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -279,9 +279,6 @@ ieee80211_tx_h_check_assoc(struct ieee80211_tx_data *tx)
if (tx->sdata->vif.type == NL80211_IFTYPE_OCB)
return TX_CONTINUE;
- if (tx->flags & IEEE80211_TX_PS_BUFFERED)
- return TX_CONTINUE;
-
if (tx->sta)
assoc = test_sta_flag(tx->sta, WLAN_STA_ASSOC);
@@ -328,14 +325,45 @@ static void fq_skb_free_func(struct fq *fq,
ieee80211_free_txskb(&local->hw, skb);
}
+static struct sk_buff *ieee80211_txq_dequeue_oldest(struct fq *fq,
+ struct fq_tin *tin)
+{
+ struct list_head *lists[] = { &tin->new_flows, &tin->old_flows };
+ struct fq_flow *flow, *oldest = NULL;
+ codel_time_t oldest_time = 0;
+
+ lockdep_assert_held(&fq->lock);
+
+ for (int i = 0; i < ARRAY_SIZE(lists); i++) {
+ list_for_each_entry(flow, lists[i], flowchain) {
+ struct sk_buff *skb = skb_peek(&flow->queue);
+ codel_time_t t;
+
+ if (!skb)
+ continue;
+
+ t = IEEE80211_SKB_CB(skb)->control.enqueue_time;
+ if (!oldest || codel_time_before(t, oldest_time)) {
+ oldest = flow;
+ oldest_time = t;
+ }
+ }
+ }
+
+ if (!oldest)
+ return NULL;
+
+ return fq_flow_dequeue(fq, oldest);
+}
+
static void ieee80211_txq_enqueue(struct ieee80211_local *local,
struct txq_info *txqi,
struct sk_buff *skb)
{
+ struct sk_buff *drop = NULL, *drop_oldest = NULL;
struct fq *fq = &local->fq;
struct fq_tin *tin = &txqi->tin;
struct sta_info *sta = NULL;
- struct sk_buff *drop = NULL;
bool was_empty;
u32 flow_idx;
@@ -368,12 +396,27 @@ static void ieee80211_txq_enqueue(struct ieee80211_local *local,
__skb_queue_tail(&txqi->frags, skb);
}
} else {
+ if (unlikely(!txqi->txq.sta &&
+ IEEE80211_SKB_CB(skb)->flags &
+ IEEE80211_TX_CTL_SEND_AFTER_DTIM)) {
+ set_bit(IEEE80211_TXQ_MCAST_PS, &txqi->flags);
+ if (tin->backlog_packets >= AP_MAX_BC_BUFFER)
+ drop_oldest = ieee80211_txq_dequeue_oldest(fq,
+ tin);
+ }
+
flow_idx = fq_flow_idx(fq, skb);
fq_tin_enqueue(fq, tin, flow_idx, skb,
fq_skb_free_func);
}
spin_unlock_bh(&fq->lock);
+ if (unlikely(drop_oldest)) {
+ ps_dbg(vif_to_sdata(txqi->txq.vif),
+ "BC TX buffer full - dropping the oldest frame\n");
+ ieee80211_free_txskb(&local->hw, drop_oldest);
+ }
+
if (unlikely(drop)) {
ps_dbg(sta->sdata,
"STA %pM management TXQ full - dropping frame\n",
@@ -388,43 +431,12 @@ static void ieee80211_txq_enqueue(struct ieee80211_local *local,
}
}
-/* This function is called whenever the AP is about to exceed the maximum limit
- * of buffered multicast frames for power saving STAs. This situation should not
- * really happen often during normal operation, so dropping the oldest buffered
- * packet from each queue should be OK to make some room for new frames. */
-static void purge_old_ps_buffers(struct ieee80211_local *local)
-{
- int total = 0, purged = 0;
- struct sk_buff *skb;
- struct ieee80211_sub_if_data *sdata;
-
- list_for_each_entry_rcu(sdata, &local->interfaces, list) {
- struct ps_data *ps;
-
- if (sdata->vif.type == NL80211_IFTYPE_AP)
- ps = &sdata->u.ap.ps;
- else if (ieee80211_vif_is_mesh(&sdata->vif))
- ps = &sdata->u.mesh.ps;
- else
- continue;
-
- skb = skb_dequeue(&ps->bc_buf);
- if (skb) {
- purged++;
- ieee80211_free_txskb(&local->hw, skb);
- }
- total += skb_queue_len(&ps->bc_buf);
- }
-
- local->total_ps_buffered = total;
- ps_dbg_hw(&local->hw, "PS buffers full - purged %d frames\n", purged);
-}
-
static ieee80211_tx_result
ieee80211_tx_h_multicast_ps_buf(struct ieee80211_tx_data *tx)
{
struct ieee80211_tx_info *info = IEEE80211_SKB_CB(tx->skb);
struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)tx->skb->data;
+ struct ieee80211_sub_if_data *sdata = tx->sdata;
struct ps_data *ps;
/*
@@ -432,7 +444,7 @@ ieee80211_tx_h_multicast_ps_buf(struct ieee80211_tx_data *tx)
*
* If any of the associated/peer stations is in power save mode,
* the frame is buffered to be sent after DTIM beacon frame.
- * This is done either by the hardware or us.
+ * This is done either by the hardware or us (on the interface TXQ).
*/
/* powersaving STAs currently only in AP/VLAN/mesh mode */
@@ -442,6 +454,8 @@ ieee80211_tx_h_multicast_ps_buf(struct ieee80211_tx_data *tx)
return TX_CONTINUE;
ps = &tx->sdata->bss->ps;
+ sdata = container_of(tx->sdata->bss,
+ struct ieee80211_sub_if_data, u.ap);
} else if (ieee80211_vif_is_mesh(&tx->sdata->vif)) {
ps = &tx->sdata->u.mesh.ps;
} else {
@@ -460,29 +474,15 @@ ieee80211_tx_h_multicast_ps_buf(struct ieee80211_tx_data *tx)
info->hw_queue = tx->sdata->vif.cab_queue;
/* no stations in PS mode and no buffered packets */
- if (!atomic_read(&ps->num_sta_ps) && skb_queue_empty(&ps->bc_buf))
+ if (!atomic_read(&ps->num_sta_ps) &&
+ !(sdata->vif.txq &&
+ test_bit(IEEE80211_TXQ_MCAST_PS,
+ &to_txq_info(sdata->vif.txq)->flags)))
return TX_CONTINUE;
info->flags |= IEEE80211_TX_CTL_SEND_AFTER_DTIM;
- /* device releases frame after DTIM beacon */
- if (!ieee80211_hw_check(&tx->local->hw, HOST_BROADCAST_PS_BUFFERING))
- return TX_CONTINUE;
-
- /* buffered in mac80211 */
- if (tx->local->total_ps_buffered >= TOTAL_MAX_TX_BUFFER)
- purge_old_ps_buffers(tx->local);
-
- if (skb_queue_len(&ps->bc_buf) >= AP_MAX_BC_BUFFER) {
- ps_dbg(tx->sdata,
- "BC TX buffer full - dropping the oldest frame\n");
- ieee80211_free_txskb(&tx->local->hw, skb_dequeue(&ps->bc_buf));
- } else
- tx->local->total_ps_buffered++;
-
- skb_queue_tail(&ps->bc_buf, tx->skb);
-
- return TX_QUEUED;
+ return TX_CONTINUE;
}
static int ieee80211_use_mfp(__le16 fc, struct sta_info *sta,
@@ -565,9 +565,6 @@ ieee80211_tx_h_unicast_ps_buf(struct ieee80211_tx_data *tx)
static ieee80211_tx_result debug_noinline
ieee80211_tx_h_ps_buf(struct ieee80211_tx_data *tx)
{
- if (unlikely(tx->flags & IEEE80211_TX_PS_BUFFERED))
- return TX_CONTINUE;
-
if (tx->flags & IEEE80211_TX_UNICAST)
return ieee80211_tx_h_unicast_ps_buf(tx);
else
@@ -1323,11 +1320,18 @@ static struct txq_info *ieee80211_get_txq(struct ieee80211_local *local,
struct ieee80211_txq *txq = NULL;
/* TXQs buffer frames, send unbuffered ones directly */
- if (info->flags & (IEEE80211_TX_CTL_SEND_AFTER_DTIM |
- IEEE80211_TX_CTL_NO_PS_BUFFER) ||
+ if ((info->flags & IEEE80211_TX_CTL_NO_PS_BUFFER) ||
(info->control.flags & IEEE80211_TX_CTRL_PS_RESPONSE))
return NULL;
+ if (info->flags & IEEE80211_TX_CTL_SEND_AFTER_DTIM) {
+ if (!ieee80211_hw_check(&local->hw,
+ HOST_BROADCAST_PS_BUFFERING) ||
+ !vif->txq)
+ return NULL;
+ return to_txq_info(vif->txq);
+ }
+
/*
* While (re)association request/response frames are not considered
* bufferable MMPDUs, use the TXQ abstraction for the transmission of
@@ -1417,7 +1421,7 @@ static void codel_drop_func(struct sk_buff *skb,
ieee80211_free_txskb(hw, skb);
}
-/* bypass codel since PS can last a long time */
+/* bypass codel since PS or DTIM buffering can last a long time */
static bool ieee80211_txq_codel_bypass(struct txq_info *txqi,
const struct fq_flow *flow)
{
@@ -1425,7 +1429,7 @@ static bool ieee80211_txq_codel_bypass(struct txq_info *txqi,
struct sk_buff *skb;
if (!txqi->txq.sta)
- return false;
+ return test_bit(IEEE80211_TXQ_MCAST_PS, &txqi->flags);
sta = container_of(txqi->txq.sta, struct sta_info, sta);
if (test_sta_flag(sta, WLAN_STA_PS_STA) ||
@@ -1677,6 +1681,7 @@ void ieee80211_txq_purge(struct ieee80211_local *local,
spin_lock_bh(&fq->lock);
fq_tin_reset(fq, tin, fq_skb_free_func);
ieee80211_purge_tx_queue(&local->hw, &txqi->frags);
+ clear_bit(IEEE80211_TXQ_MCAST_PS, &txqi->flags);
spin_unlock_bh(&fq->lock);
spin_lock_bh(&local->active_txq_lock[txqi->txq.ac]);
@@ -1684,6 +1689,19 @@ void ieee80211_txq_purge(struct ieee80211_local *local,
spin_unlock_bh(&local->active_txq_lock[txqi->txq.ac]);
}
+void ieee80211_txq_purge_mcast_ps(struct ieee80211_local *local,
+ const struct ieee80211_sub_if_data *sdata)
+{
+ struct txq_info *txqi;
+
+ if (!sdata->vif.txq)
+ return;
+
+ txqi = to_txq_info(sdata->vif.txq);
+ if (test_bit(IEEE80211_TXQ_MCAST_PS, &txqi->flags))
+ ieee80211_txq_purge(local, txqi);
+}
+
void ieee80211_txq_set_params(struct ieee80211_local *local, int radio_idx)
{
if (local->hw.wiphy->txq_limit)
@@ -4032,7 +4050,7 @@ static bool ieee80211_txq_ps_tx_ok(const struct txq_info *txqi)
struct sta_info *sta;
if (!txqi->txq.sta)
- return true;
+ return !test_bit(IEEE80211_TXQ_MCAST_PS, &txqi->flags);
sta = container_of(txqi->txq.sta, struct sta_info, sta);
@@ -5415,8 +5433,13 @@ static void __ieee80211_beacon_add_tim(struct ieee80211_sub_if_data *sdata,
skb_put_u8(skb, ps->dtim_count);
skb_put_u8(skb, link_conf->dtim_period);
- if (ps->dtim_count == 0 && !skb_queue_empty(&ps->bc_buf))
- mcast_traffic = true;
+ if (ps->dtim_count == 0 && sdata->vif.txq) {
+ struct txq_info *txqi = to_txq_info(sdata->vif.txq);
+
+ mcast_traffic = test_bit(IEEE80211_TXQ_MCAST_PS,
+ &txqi->flags) &&
+ txq_has_queue(&txqi->txq);
+ }
ps->dtim_bc_mc = mcast_traffic;
@@ -6505,14 +6528,17 @@ ieee80211_get_buffered_bc(struct ieee80211_hw *hw,
struct ieee80211_vif *vif)
{
struct ieee80211_local *local = hw_to_local(hw);
- struct sk_buff *skb = NULL;
- struct ieee80211_tx_data tx;
- struct ieee80211_sub_if_data *sdata;
- struct ps_data *ps;
- struct ieee80211_tx_info *info;
+ struct ieee80211_sub_if_data *sdata = vif_to_sdata(vif);
struct ieee80211_chanctx_conf *chanctx_conf;
+ struct ieee80211_tx_info *info;
+ struct sk_buff *skb = NULL;
+ struct txq_info *txqi;
+ struct ps_data *ps;
- sdata = vif_to_sdata(vif);
+ if (!vif->txq)
+ return NULL;
+
+ txqi = to_txq_info(vif->txq);
rcu_read_lock();
chanctx_conf = rcu_dereference(sdata->vif.bss_conf.chanctx_conf);
@@ -6537,36 +6563,29 @@ ieee80211_get_buffered_bc(struct ieee80211_hw *hw,
if (ps->dtim_count != 0 || !ps->dtim_bc_mc)
goto out; /* send buffered bc/mc only after DTIM beacon */
- while (1) {
- skb = skb_dequeue(&ps->bc_buf);
- if (!skb)
- goto out;
- local->total_ps_buffered--;
+ local_bh_disable();
+ skb = __ieee80211_tx_dequeue(hw, vif->txq, true);
- if (!skb_queue_empty(&ps->bc_buf) && skb->len >= 2) {
- struct ieee80211_hdr *hdr =
- (struct ieee80211_hdr *) skb->data;
- /* more buffered multicast/broadcast frames ==> set
- * MoreData flag in IEEE 802.11 header to inform PS
- * STAs */
- hdr->frame_control |=
- cpu_to_le16(IEEE80211_FCTL_MOREDATA);
- }
+ spin_lock(&local->fq.lock);
+ if (!txq_has_queue(vif->txq)) {
+ clear_bit(IEEE80211_TXQ_MCAST_PS, &txqi->flags);
+ } else if (skb &&
+ !(IEEE80211_SKB_CB(skb)->flags &
+ IEEE80211_TX_CTL_HW_80211_ENCAP)) {
+ struct ieee80211_hdr *hdr = (void *)skb->data;
- if (sdata->vif.type == NL80211_IFTYPE_AP)
- sdata = IEEE80211_DEV_TO_SUB_IF(skb->dev);
- if (!ieee80211_tx_prepare(sdata, &tx, NULL, skb))
- break;
- ieee80211_free_txskb(hw, skb);
+ /* inform PS stations about more buffered frames */
+ hdr->frame_control |= cpu_to_le16(IEEE80211_FCTL_MOREDATA);
}
+ spin_unlock(&local->fq.lock);
+ local_bh_enable();
+
+ if (!skb)
+ goto out;
info = IEEE80211_SKB_CB(skb);
-
- tx.flags |= IEEE80211_TX_PS_BUFFERED;
+ info->flags |= IEEE80211_TX_CTL_SEND_AFTER_DTIM;
info->band = chanctx_conf->def.chan->band;
-
- if (invoke_tx_handlers(&tx))
- skb = NULL;
out:
rcu_read_unlock();
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 22/30] wifi: mac80211: always use TXQ for mcast data frames
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (20 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 21/30] wifi: mac80211: buffer multicast frames on vif TXQ Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 23/30] wifi: mac80211: refactor fast-xmit header encapsulation Johannes Berg
` (7 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
For devices without HOST_BROADCAST_PS_BUFFERING, we marked
mcast data frames for AFTER_DTIM on enqueue and bypassed
the TXQ for that frame. Change it to always put such frames
onto the TXQ and mark them with AFTER_DTIM on dequeue for
device buffering (though chances are this isn't needed),
and for host buffering pause the TXQ until DTIM if we see
a sleeping station.
The vif TXQ is then used for all the frames, and doesn't
need to be handled specially for wakeup.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/tx.c | 95 +++++++++++++++++++++++++++++++--------------
net/mac80211/util.c | 5 ---
2 files changed, 65 insertions(+), 35 deletions(-)
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 0361cdfdbb70..8026aa1ff136 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -356,6 +356,43 @@ static struct sk_buff *ieee80211_txq_dequeue_oldest(struct fq *fq,
return fq_flow_dequeue(fq, oldest);
}
+static struct ps_data *
+ieee80211_sdata_ps_data(struct ieee80211_sub_if_data *sdata)
+{
+ /* only AP and mesh have PS support now */
+ if (sdata->vif.type == NL80211_IFTYPE_AP ||
+ sdata->vif.type == NL80211_IFTYPE_AP_VLAN) {
+ if (!sdata->bss)
+ return NULL;
+ return &sdata->bss->ps;
+ }
+
+ if (ieee80211_vif_is_mesh(&sdata->vif))
+ return &sdata->u.mesh.ps;
+
+ return NULL;
+}
+
+static bool ieee80211_txq_mcast_buffer(struct ieee80211_sub_if_data *sdata,
+ struct sk_buff *skb)
+{
+ struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)skb->data;
+ struct ps_data *ps = ieee80211_sdata_ps_data(sdata);
+
+ if (!ps || !is_multicast_ether_addr(hdr->addr1) ||
+ ieee80211_has_order(hdr->frame_control))
+ return false;
+
+ if (sdata->vif.type == NL80211_IFTYPE_AP_VLAN)
+ sdata = container_of(sdata->bss, struct ieee80211_sub_if_data,
+ u.ap);
+
+ return atomic_read(&ps->num_sta_ps) ||
+ (sdata->vif.txq &&
+ test_bit(IEEE80211_TXQ_MCAST_PS,
+ &to_txq_info(sdata->vif.txq)->flags));
+}
+
static void ieee80211_txq_enqueue(struct ieee80211_local *local,
struct txq_info *txqi,
struct sk_buff *skb)
@@ -397,8 +434,10 @@ static void ieee80211_txq_enqueue(struct ieee80211_local *local,
}
} else {
if (unlikely(!txqi->txq.sta &&
- IEEE80211_SKB_CB(skb)->flags &
- IEEE80211_TX_CTL_SEND_AFTER_DTIM)) {
+ ieee80211_hw_check(&local->hw,
+ HOST_BROADCAST_PS_BUFFERING) &&
+ ieee80211_txq_mcast_buffer(
+ vif_to_sdata(txqi->txq.vif), skb))) {
set_bit(IEEE80211_TXQ_MCAST_PS, &txqi->flags);
if (tin->backlog_packets >= AP_MAX_BC_BUFFER)
drop_oldest = ieee80211_txq_dequeue_oldest(fq,
@@ -436,35 +475,16 @@ ieee80211_tx_h_multicast_ps_buf(struct ieee80211_tx_data *tx)
{
struct ieee80211_tx_info *info = IEEE80211_SKB_CB(tx->skb);
struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)tx->skb->data;
- struct ieee80211_sub_if_data *sdata = tx->sdata;
- struct ps_data *ps;
/*
* broadcast/multicast frame
*
* If any of the associated/peer stations is in power save mode,
* the frame is buffered to be sent after DTIM beacon frame.
- * This is done either by the hardware or us (on the interface TXQ).
+ * Data frames go on the vif TXQ, other frames are marked
+ * with IEEE80211_TX_CTL_SEND_AFTER_DTIM here if needed.
*/
-
- /* powersaving STAs currently only in AP/VLAN/mesh mode */
- if (tx->sdata->vif.type == NL80211_IFTYPE_AP ||
- tx->sdata->vif.type == NL80211_IFTYPE_AP_VLAN) {
- if (!tx->sdata->bss)
- return TX_CONTINUE;
-
- ps = &tx->sdata->bss->ps;
- sdata = container_of(tx->sdata->bss,
- struct ieee80211_sub_if_data, u.ap);
- } else if (ieee80211_vif_is_mesh(&tx->sdata->vif)) {
- ps = &tx->sdata->u.mesh.ps;
- } else {
- return TX_CONTINUE;
- }
-
-
- /* no buffering for ordered frames */
- if (ieee80211_has_order(hdr->frame_control))
+ if (!ieee80211_sdata_ps_data(tx->sdata))
return TX_CONTINUE;
if (ieee80211_is_probe_req(hdr->frame_control))
@@ -473,14 +493,11 @@ ieee80211_tx_h_multicast_ps_buf(struct ieee80211_tx_data *tx)
if (ieee80211_hw_check(&tx->local->hw, QUEUE_CONTROL))
info->hw_queue = tx->sdata->vif.cab_queue;
- /* no stations in PS mode and no buffered packets */
- if (!atomic_read(&ps->num_sta_ps) &&
- !(sdata->vif.txq &&
- test_bit(IEEE80211_TXQ_MCAST_PS,
- &to_txq_info(sdata->vif.txq)->flags)))
+ if (ieee80211_is_data_present(hdr->frame_control))
return TX_CONTINUE;
- info->flags |= IEEE80211_TX_CTL_SEND_AFTER_DTIM;
+ if (ieee80211_txq_mcast_buffer(tx->sdata, tx->skb))
+ info->flags |= IEEE80211_TX_CTL_SEND_AFTER_DTIM;
return TX_CONTINUE;
}
@@ -4061,6 +4078,21 @@ static bool ieee80211_txq_ps_tx_ok(const struct txq_info *txqi)
return READ_ONCE(sta->ps_release_tids) & BIT(txqi->txq.tid);
}
+/*
+ * For devices buffering group addressed frames themselves, mark them
+ * for that if there are stations in powersave when they're dequeued.
+ */
+static void ieee80211_txq_mcast_ps(struct ieee80211_local *local,
+ struct ieee80211_sub_if_data *sdata,
+ struct sk_buff *skb)
+{
+ if (ieee80211_hw_check(&local->hw, HOST_BROADCAST_PS_BUFFERING))
+ return;
+
+ if (ieee80211_txq_mcast_buffer(sdata, skb))
+ IEEE80211_SKB_CB(skb)->flags |= IEEE80211_TX_CTL_SEND_AFTER_DTIM;
+}
+
static struct sk_buff *__ieee80211_tx_dequeue(struct ieee80211_hw *hw,
struct ieee80211_txq *txq,
bool ps_release)
@@ -4208,6 +4240,9 @@ static struct sk_buff *__ieee80211_tx_dequeue(struct ieee80211_hw *hw,
}
}
+ if (!txq->sta)
+ ieee80211_txq_mcast_ps(local, tx.sdata, skb);
+
if (skb_has_frag_list(skb) &&
!ieee80211_hw_check(&local->hw, TX_FRAG_LIST)) {
if (skb_linearize(skb)) {
diff --git a/net/mac80211/util.c b/net/mac80211/util.c
index 181e4d787837..b46302dfdd7e 100644
--- a/net/mac80211/util.c
+++ b/net/mac80211/util.c
@@ -327,7 +327,6 @@ static void __ieee80211_wake_txqs(struct ieee80211_sub_if_data *sdata, int ac)
struct ieee80211_local *local = sdata->local;
struct ieee80211_vif *vif = &sdata->vif;
struct fq *fq = &local->fq;
- struct ps_data *ps = NULL;
struct txq_info *txqi = NULL;
struct sta_info *sta;
int i;
@@ -338,9 +337,6 @@ static void __ieee80211_wake_txqs(struct ieee80211_sub_if_data *sdata, int ac)
if (!test_bit(SDATA_STATE_RUNNING, &sdata->state))
goto out;
- if (sdata->vif.type == NL80211_IFTYPE_AP)
- ps = &sdata->bss->ps;
-
list_for_each_entry_rcu(sta, &local->sta_list, list) {
if (sdata != sta->sdata)
continue;
@@ -374,7 +370,6 @@ static void __ieee80211_wake_txqs(struct ieee80211_sub_if_data *sdata, int ac)
WARN_ON_ONCE(vif->txq_nan_dw);
if (!test_and_clear_bit(IEEE80211_TXQ_DIRTY, &txqi->flags) ||
- (ps && atomic_read(&ps->num_sta_ps)) ||
ac != vif->txq->ac)
txqi = NULL;
} else if (vif->txq_nan_dw) {
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 23/30] wifi: mac80211: refactor fast-xmit header encapsulation
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (21 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 22/30] wifi: mac80211: always use TXQ for mcast data frames Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 24/30] wifi: mac80211: encapsulate fast-xmit frames at TXQ dequeue Johannes Berg
` (6 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
Move the 802.11 frame encapsulation using the fast-xmit
template to a separate function for future use.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/tx.c | 55 +++++++++++++++++++++++++++++------------------
1 file changed, 34 insertions(+), 21 deletions(-)
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 8026aa1ff136..683e2e219a8f 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -3889,6 +3889,39 @@ ieee80211_tx_skb_fixup(struct sk_buff *skb, netdev_features_t features)
return NULL;
}
+static int ieee80211_fast_tx_encap(struct ieee80211_sub_if_data *sdata,
+ const struct ieee80211_fast_tx *fast_tx,
+ struct sk_buff *skb,
+ const u8 *da, const u8 *sa)
+{
+ int hw_headroom = sdata->local->hw.extra_tx_headroom;
+ int extra_head = fast_tx->hdr_len - (ETH_HLEN - 2);
+ struct ieee80211_hdr *hdr;
+
+ /* will not be crypto-handled beyond what we do here, so use false
+ * as the may-encrypt argument for the resize to not account for
+ * more room than we already have in 'extra_head'
+ */
+ if (unlikely(ieee80211_skb_resize(sdata, skb,
+ max_t(int, extra_head + hw_headroom -
+ skb_headroom(skb), 0),
+ ENCRYPT_NO)))
+ return -ENOMEM;
+
+ hdr = skb_push(skb, extra_head);
+ memcpy(skb->data, fast_tx->hdr, fast_tx->hdr_len);
+ memcpy(skb->data + fast_tx->da_offs, da, ETH_ALEN);
+ memcpy(skb->data + fast_tx->sa_offs, sa, ETH_ALEN);
+
+ if (hdr->frame_control & cpu_to_le16(IEEE80211_STYPE_QOS_DATA)) {
+ u8 tid = skb->priority & IEEE80211_QOS_CTL_TAG1D_MASK;
+
+ *ieee80211_get_qos_ctl(hdr) = tid;
+ }
+
+ return 0;
+}
+
void __ieee80211_xmit_fast(struct ieee80211_sub_if_data *sdata,
struct sta_info *sta,
struct ieee80211_fast_tx *fast_tx,
@@ -3900,8 +3933,6 @@ void __ieee80211_xmit_fast(struct ieee80211_sub_if_data *sdata,
struct ieee80211_tx_info *info;
struct ieee80211_tx_data tx;
ieee80211_tx_result r;
- int hw_headroom = sdata->local->hw.extra_tx_headroom;
- int extra_head = fast_tx->hdr_len - (ETH_HLEN - 2);
skb = skb_share_check(skb, GFP_ATOMIC);
if (unlikely(!skb))
@@ -3911,21 +3942,9 @@ void __ieee80211_xmit_fast(struct ieee80211_sub_if_data *sdata,
ieee80211_amsdu_aggregate(sdata, sta, fast_tx, skb, da, sa))
return;
- /* will not be crypto-handled beyond what we do here, so use false
- * as the may-encrypt argument for the resize to not account for
- * more room than we already have in 'extra_head'
- */
- if (unlikely(ieee80211_skb_resize(sdata, skb,
- max_t(int, extra_head + hw_headroom -
- skb_headroom(skb), 0),
- ENCRYPT_NO)))
+ if (ieee80211_fast_tx_encap(sdata, fast_tx, skb, da, sa))
goto free;
- hdr = skb_push(skb, extra_head);
- memcpy(skb->data, fast_tx->hdr, fast_tx->hdr_len);
- memcpy(skb->data + fast_tx->da_offs, da, ETH_ALEN);
- memcpy(skb->data + fast_tx->sa_offs, sa, ETH_ALEN);
-
info = IEEE80211_SKB_CB(skb);
memset(info, 0, sizeof(*info));
info->band = fast_tx->band;
@@ -3941,12 +3960,6 @@ void __ieee80211_xmit_fast(struct ieee80211_sub_if_data *sdata,
info->flags |= IEEE80211_TX_CTL_REQ_TX_STATUS;
#endif
- if (hdr->frame_control & cpu_to_le16(IEEE80211_STYPE_QOS_DATA)) {
- u8 tid = skb->priority & IEEE80211_QOS_CTL_TAG1D_MASK;
-
- *ieee80211_get_qos_ctl(hdr) = tid;
- }
-
__skb_queue_head_init(&tx.skbs);
tx.flags = IEEE80211_TX_UNICAST;
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 24/30] wifi: mac80211: encapsulate fast-xmit frames at TXQ dequeue
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (22 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 23/30] wifi: mac80211: refactor fast-xmit header encapsulation Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 25/30] net/fq_impl: allow pulling packets from the same flow Johannes Berg
` (5 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
Instead of building the 802.11 header using fast-xmit when a
frame is transmitted, keep it on the TXQ in ethernet format,
and build the header on dequeue. For now only for fast-xmit,
but the target is to handle (almost) all frames that way so
we get better reactivity to changes while the frames are on
the queue, and can (eventually) also transfer the queues of
one AP MLD to another during SMD Transition.
For now, this breaks software A-MSDUs (we can't build those
in ethernet format), the next patch creates them at dequeue
instead of enqueue, also improving reactivity.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/ieee80211_i.h | 7 ++++
net/mac80211/tx.c | 82 +++++++++++++++++++++++++++++++++++---
2 files changed, 83 insertions(+), 6 deletions(-)
diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
index b498765afa20..f450368c1c6e 100644
--- a/net/mac80211/ieee80211_i.h
+++ b/net/mac80211/ieee80211_i.h
@@ -184,6 +184,13 @@ typedef unsigned __bitwise ieee80211_tx_result;
#define IEEE80211_TX_UNICAST BIT(1)
+/*
+ * Internally, use "need encap" since it's the better name, but the
+ * hardware might do it instead of sofwtare, so alias it to the HW
+ * flag - that way don't need to translate.
+ */
+#define IEEE80211_TX_INTFL_NEED_80211_ENCAP IEEE80211_TX_CTL_HW_80211_ENCAP
+
struct ieee80211_tx_data {
struct sk_buff *skb;
struct sk_buff_head skbs;
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 683e2e219a8f..7d1d1754d931 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -3992,6 +3992,31 @@ void __ieee80211_xmit_fast(struct ieee80211_sub_if_data *sdata,
kfree_skb(skb);
}
+static bool ieee80211_xmit_fast_queue(struct ieee80211_sub_if_data *sdata,
+ struct sta_info *sta,
+ const struct ieee80211_fast_tx *fast_tx,
+ struct sk_buff *skb)
+{
+ struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
+
+ memset(info, 0, sizeof(*info));
+ info->band = fast_tx->band;
+ info->control.vif = &sdata->vif;
+ info->flags = IEEE80211_TX_CTL_FIRST_FRAGMENT |
+ IEEE80211_TX_CTL_DONTFRAG |
+ IEEE80211_TX_INTFL_NEED_80211_ENCAP;
+ info->control.flags = IEEE80211_TX_CTRL_FAST_XMIT |
+ u32_encode_bits(IEEE80211_LINK_UNSPECIFIED,
+ IEEE80211_TX_CTRL_MLO_LINK);
+
+#ifdef CONFIG_MAC80211_DEBUGFS
+ if (sdata->local->force_tx_status)
+ info->flags |= IEEE80211_TX_CTL_REQ_TX_STATUS;
+#endif
+
+ return ieee80211_queue_skb(sdata->local, sdata, sta, skb);
+}
+
static bool ieee80211_xmit_fast(struct ieee80211_sub_if_data *sdata,
struct sta_info *sta,
struct ieee80211_fast_tx *fast_tx,
@@ -4001,8 +4026,7 @@ static bool ieee80211_xmit_fast(struct ieee80211_sub_if_data *sdata,
struct ieee80211_hdr *hdr = (void *)fast_tx->hdr;
struct tid_ampdu_tx *tid_tx = NULL;
struct sk_buff *next;
- struct ethhdr eth;
- u8 tid = IEEE80211_NUM_TIDS;
+ u8 tid;
/* control port protocol needs a lot of special handling */
if (cpu_to_be16(ethertype) == sdata->control_port_protocol)
@@ -4027,8 +4051,6 @@ static bool ieee80211_xmit_fast(struct ieee80211_sub_if_data *sdata,
}
}
- memcpy(ð, skb->data, ETH_HLEN - 2);
-
/* after this point (skb is modified) we cannot return false */
skb = ieee80211_tx_skb_fixup(skb, ieee80211_sdata_netdev_features(sdata));
if (!skb)
@@ -4036,8 +4058,14 @@ static bool ieee80211_xmit_fast(struct ieee80211_sub_if_data *sdata,
skb_list_walk_safe(skb, skb, next) {
skb_mark_not_on_list(skb);
- __ieee80211_xmit_fast(sdata, sta, fast_tx, skb, tid_tx,
- eth.h_dest, eth.h_source);
+
+ skb = skb_share_check(skb, GFP_ATOMIC);
+ if (unlikely(!skb))
+ continue;
+
+ /* can only fail if the station is no longer uploaded */
+ if (!ieee80211_xmit_fast_queue(sdata, sta, fast_tx, skb))
+ ieee80211_free_txskb(&sdata->local->hw, skb);
}
return true;
@@ -4106,6 +4134,39 @@ static void ieee80211_txq_mcast_ps(struct ieee80211_local *local,
IEEE80211_SKB_CB(skb)->flags |= IEEE80211_TX_CTL_SEND_AFTER_DTIM;
}
+static int ieee80211_txq_build_hdr(struct ieee80211_tx_data *tx)
+{
+ const struct ieee80211_fast_tx *fast_tx;
+ struct sk_buff *skb = tx->skb;
+ struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
+ struct ethhdr eth;
+
+ info->flags &= ~IEEE80211_TX_INTFL_NEED_80211_ENCAP;
+
+ fast_tx = rcu_dereference(tx->sta->fast_tx);
+ if (fast_tx && fast_tx->key == tx->key) {
+ memcpy(ð, skb->data, ETH_HLEN - 2);
+ if (ieee80211_fast_tx_encap(tx->sdata, fast_tx, skb,
+ eth.h_dest, eth.h_source)) {
+ ieee80211_free_txskb(&tx->local->hw, skb);
+ return -ENOMEM;
+ }
+ return 0;
+ }
+
+ info->control.flags &= ~IEEE80211_TX_CTRL_FAST_XMIT;
+
+ /* use sta->sdata for VLAN, info->control.vif is the AP */
+ skb = ieee80211_build_hdr(tx->sta->sdata, skb, tx->sta);
+ if (IS_ERR(skb))
+ return PTR_ERR(skb);
+
+ ieee80211_set_qos_hdr(tx->sta->sdata, skb);
+ dev_sw_netstats_tx_add(skb->dev, 1, skb->len);
+ tx->skb = skb;
+ return 0;
+}
+
static struct sk_buff *__ieee80211_tx_dequeue(struct ieee80211_hw *hw,
struct ieee80211_txq *txq,
bool ps_release)
@@ -4213,6 +4274,15 @@ static struct sk_buff *__ieee80211_tx_dequeue(struct ieee80211_hw *hw,
info->flags |= (IEEE80211_TX_CTL_AMPDU |
IEEE80211_TX_CTL_DONTFRAG);
+ /* the fast-xmit check here distinguishes from HW offload */
+ if (info->flags & IEEE80211_TX_INTFL_NEED_80211_ENCAP &&
+ info->control.flags & IEEE80211_TX_CTRL_FAST_XMIT) {
+ if (ieee80211_txq_build_hdr(&tx))
+ goto begin;
+ skb = tx.skb;
+ hdr = (struct ieee80211_hdr *)skb->data;
+ }
+
if (info->flags & IEEE80211_TX_CTL_HW_80211_ENCAP) {
if (!ieee80211_hw_check(&local->hw, HAS_RATE_CONTROL)) {
r = ieee80211_tx_h_rate_ctrl(&tx);
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 25/30] net/fq_impl: allow pulling packets from the same flow
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (23 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 24/30] wifi: mac80211: encapsulate fast-xmit frames at TXQ dequeue Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 26/30] wifi: mac80211: build A-MSDUs at dequeue Johannes Berg
` (4 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
If fq_tin_dequeue() returns a packet, also (optionally)
fill a flow pointer and add a function to pull another
packet from the same flow. This will be used for A-MSDU
aggregation in mac80211.
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
include/net/fq_impl.h | 34 +++++++++++++++++++++++++++++++++-
net/mac80211/tx.c | 2 +-
2 files changed, 34 insertions(+), 2 deletions(-)
diff --git a/include/net/fq_impl.h b/include/net/fq_impl.h
index 8aca881937da..eb8802dd2a26 100644
--- a/include/net/fq_impl.h
+++ b/include/net/fq_impl.h
@@ -87,9 +87,14 @@ static int fq_flow_drop(struct fq *fq, struct fq_flow *flow,
return packets;
}
+/*
+ * Dequeue the next packet from the tin; if @flowp isn't %NULL
+ * set it to the flow for use with fq_flow_dequeue_next().
+ */
static struct sk_buff *fq_tin_dequeue(struct fq *fq,
struct fq_tin *tin,
- fq_tin_dequeue_t dequeue_func)
+ fq_tin_dequeue_t dequeue_func,
+ struct fq_flow **flowp)
{
struct fq_flow *flow;
struct list_head *head;
@@ -131,6 +136,33 @@ static struct sk_buff *fq_tin_dequeue(struct fq *fq,
tin->tx_bytes += skb->len;
tin->tx_packets++;
+ if (flowp)
+ *flowp = flow;
+
+ return skb;
+}
+
+/*
+ * Dequeue another packet from the flow that fq_tin_dequeue() returned,
+ * e.g. to aggregate packets.
+ *
+ * Note: If the lock was released between calls, the caller must check
+ * that flow->tin is still the same tin.
+ */
+static struct sk_buff *fq_flow_dequeue_next(struct fq *fq,
+ struct fq_flow *flow)
+{
+ struct fq_tin *tin = flow->tin;
+ struct sk_buff *skb;
+
+ skb = fq_flow_dequeue(fq, flow);
+ if (!skb)
+ return NULL;
+
+ flow->deficit -= skb->len;
+ tin->tx_bytes += skb->len;
+ tin->tx_packets++;
+
return skb;
}
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 7d1d1754d931..fbe77131799d 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -4222,7 +4222,7 @@ static struct sk_buff *__ieee80211_tx_dequeue(struct ieee80211_hw *hw,
if (unlikely(test_bit(IEEE80211_TXQ_STOP, &txqi->flags)))
goto out;
- skb = fq_tin_dequeue(fq, tin, fq_tin_dequeue_func);
+ skb = fq_tin_dequeue(fq, tin, fq_tin_dequeue_func, NULL);
}
if (!skb)
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 26/30] wifi: mac80211: build A-MSDUs at dequeue
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (24 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 25/30] net/fq_impl: allow pulling packets from the same flow Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 27/30] wifi: mac80211: handle 802.11 encap at TXQ dequeue Johannes Berg
` (3 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
Now that fast-xmit frames sit on the TXQ in ethernet
format, we can build A-MSDUs on dequeue. This way we
get better reactivity to link conditions that must be
taken into account for A-MSDU size, etc.
As before, only frames of a single flow are aggregated,
changing this would add additional complexity for how
codel operates.
With this, we might be able to relax the minstrel cap
on A-MSDU sizes, but that needs further investigation.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/tx.c | 291 ++++++++++++++++++++++------------------------
1 file changed, 136 insertions(+), 155 deletions(-)
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index fbe77131799d..57b67774f527 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -3554,7 +3554,7 @@ static bool ieee80211_amsdu_realloc_pad(struct ieee80211_local *local,
}
static bool ieee80211_amsdu_prepare_head(struct ieee80211_sub_if_data *sdata,
- struct ieee80211_fast_tx *fast_tx,
+ const struct ieee80211_fast_tx *fast_tx,
struct sk_buff *skb)
{
struct ieee80211_local *local = sdata->local;
@@ -3620,152 +3620,6 @@ static bool ieee80211_amsdu_prepare_head(struct ieee80211_sub_if_data *sdata,
return true;
}
-static bool ieee80211_amsdu_aggregate(struct ieee80211_sub_if_data *sdata,
- struct sta_info *sta,
- struct ieee80211_fast_tx *fast_tx,
- struct sk_buff *skb,
- const u8 *da, const u8 *sa)
-{
- struct ieee80211_local *local = sdata->local;
- struct fq *fq = &local->fq;
- struct fq_tin *tin;
- struct fq_flow *flow;
- u8 tid = skb->priority & IEEE80211_QOS_CTL_TAG1D_MASK;
- struct ieee80211_txq *txq = sta->sta.txq[tid];
- struct txq_info *txqi;
- struct sk_buff **frag_tail, *head;
- int subframe_len = skb->len - ETH_ALEN;
- u8 max_subframes = sta->sta.max_amsdu_subframes;
- int max_frags = local->hw.max_tx_fragments;
- int max_amsdu_len = sta->sta.cur->max_amsdu_len;
- int orig_truesize;
- u32 flow_idx;
- __be16 len;
- void *data;
- bool ret = false;
- unsigned int orig_len;
- int n = 2, nfrags, pad = 0;
- u16 hdrlen;
-
- if (!ieee80211_hw_check(&local->hw, TX_AMSDU))
- return false;
-
- if (sdata->vif.offload_flags & IEEE80211_OFFLOAD_ENCAP_ENABLED)
- return false;
-
- if (ieee80211_vif_is_mesh(&sdata->vif))
- return false;
-
- if (skb_is_gso(skb))
- return false;
-
- if (!txq)
- return false;
-
- txqi = to_txq_info(txq);
- if (test_bit(IEEE80211_TXQ_NO_AMSDU, &txqi->flags))
- return false;
-
- if (sta->sta.cur->max_rc_amsdu_len)
- max_amsdu_len = min_t(int, max_amsdu_len,
- sta->sta.cur->max_rc_amsdu_len);
-
- if (sta->sta.cur->max_tid_amsdu_len[tid])
- max_amsdu_len = min_t(int, max_amsdu_len,
- sta->sta.cur->max_tid_amsdu_len[tid]);
-
- flow_idx = fq_flow_idx(fq, skb);
-
- spin_lock_bh(&fq->lock);
-
- /* TODO: Ideally aggregation should be done on dequeue to remain
- * responsive to environment changes.
- */
-
- tin = &txqi->tin;
- flow = fq_flow_classify(fq, tin, flow_idx, skb);
- head = skb_peek_tail(&flow->queue);
- if (!head || skb_is_gso(head))
- goto out;
-
- orig_truesize = head->truesize;
- orig_len = head->len;
-
- if (skb->len + head->len > max_amsdu_len)
- goto out;
-
- nfrags = 1 + skb_shinfo(skb)->nr_frags;
- nfrags += 1 + skb_shinfo(head)->nr_frags;
- frag_tail = &skb_shinfo(head)->frag_list;
- while (*frag_tail) {
- nfrags += 1 + skb_shinfo(*frag_tail)->nr_frags;
- frag_tail = &(*frag_tail)->next;
- n++;
- }
-
- if (max_subframes && n > max_subframes)
- goto out;
-
- if (max_frags && nfrags > max_frags)
- goto out;
-
- if (!drv_can_aggregate_in_amsdu(local, head, skb))
- goto out;
-
- if (!ieee80211_amsdu_prepare_head(sdata, fast_tx, head))
- goto out;
-
- /* If n == 2, the "while (*frag_tail)" loop above didn't execute
- * and frag_tail should be &skb_shinfo(head)->frag_list.
- * However, ieee80211_amsdu_prepare_head() can reallocate it.
- * Reload frag_tail to have it pointing to the correct place.
- */
- if (n == 2)
- frag_tail = &skb_shinfo(head)->frag_list;
-
- /*
- * Pad out the previous subframe to a multiple of 4 by adding the
- * padding to the next one, that's being added. Note that head->len
- * is the length of the full A-MSDU, but that works since each time
- * we add a new subframe we pad out the previous one to a multiple
- * of 4 and thus it no longer matters in the next round.
- */
- hdrlen = fast_tx->hdr_len - sizeof(rfc1042_header);
- if ((head->len - hdrlen) & 3)
- pad = 4 - ((head->len - hdrlen) & 3);
-
- if (!ieee80211_amsdu_realloc_pad(local, skb, sizeof(rfc1042_header) +
- 2 + pad))
- goto out_recalc;
-
- ret = true;
- data = skb_push(skb, ETH_ALEN + 2);
- ether_addr_copy(data, da);
- ether_addr_copy(data + ETH_ALEN, sa);
-
- data += 2 * ETH_ALEN;
- len = cpu_to_be16(subframe_len);
- memcpy(data, &len, 2);
- memcpy(data + 2, rfc1042_header, sizeof(rfc1042_header));
-
- memset(skb_push(skb, pad), 0, pad);
-
- head->len += skb->len;
- head->data_len += skb->len;
- *frag_tail = skb;
-
-out_recalc:
- fq->memory_usage += head->truesize - orig_truesize;
- if (head->len != orig_len) {
- flow->backlog += head->len - orig_len;
- tin->backlog_bytes += head->len - orig_len;
- }
-out:
- spin_unlock_bh(&fq->lock);
-
- return ret;
-}
-
/*
* Can be called while the sta lock is held. Anything that can cause packets to
* be generated will cause deadlock!
@@ -3929,7 +3783,6 @@ void __ieee80211_xmit_fast(struct ieee80211_sub_if_data *sdata,
const u8 *da, const u8 *sa)
{
struct ieee80211_local *local = sdata->local;
- struct ieee80211_hdr *hdr = (void *)fast_tx->hdr;
struct ieee80211_tx_info *info;
struct ieee80211_tx_data tx;
ieee80211_tx_result r;
@@ -3938,10 +3791,6 @@ void __ieee80211_xmit_fast(struct ieee80211_sub_if_data *sdata,
if (unlikely(!skb))
return;
- if ((hdr->frame_control & cpu_to_le16(IEEE80211_STYPE_QOS_DATA)) &&
- ieee80211_amsdu_aggregate(sdata, sta, fast_tx, skb, da, sa))
- return;
-
if (ieee80211_fast_tx_encap(sdata, fast_tx, skb, da, sa))
goto free;
@@ -4134,7 +3983,135 @@ static void ieee80211_txq_mcast_ps(struct ieee80211_local *local,
IEEE80211_SKB_CB(skb)->flags |= IEEE80211_TX_CTL_SEND_AFTER_DTIM;
}
-static int ieee80211_txq_build_hdr(struct ieee80211_tx_data *tx)
+static bool ieee80211_amsdu_subframe(struct ieee80211_local *local,
+ struct sk_buff *skb, int prev_len)
+{
+ __be16 len = cpu_to_be16(skb->len - ETH_ALEN);
+ int pad = (4 - (prev_len & 3)) & 3;
+ struct ethhdr eth;
+ void *data;
+
+ if (!ieee80211_amsdu_realloc_pad(local, skb, sizeof(rfc1042_header) +
+ 2 + pad))
+ return false;
+
+ memcpy(ð, skb->data, ETH_HLEN - 2);
+
+ data = skb_push(skb, sizeof(rfc1042_header) + 2);
+ ether_addr_copy(data, eth.h_dest);
+ ether_addr_copy(data + ETH_ALEN, eth.h_source);
+ memcpy(data + 2 * ETH_ALEN, &len, 2);
+ memcpy(data + ETH_HLEN, rfc1042_header, sizeof(rfc1042_header));
+
+ memset(skb_push(skb, pad), 0, pad);
+
+ return true;
+}
+
+static void ieee80211_tx_build_amsdu(const struct ieee80211_tx_data *tx,
+ const struct txq_info *txqi,
+ struct fq_flow *flow,
+ const struct ieee80211_fast_tx *fast_tx)
+{
+ int hdrlen = fast_tx->hdr_len - sizeof(rfc1042_header);
+ struct ieee80211_hdr *hdr = (void *)fast_tx->hdr;
+ struct ieee80211_local *local = tx->local;
+ struct sta_info *sta = tx->sta;
+ struct sk_buff *head = tx->skb;
+ struct fq *fq = &local->fq;
+ struct sk_buff **frag_tail;
+ int max_frags = local->hw.max_tx_fragments;
+ u8 max_subframes = sta->sta.max_amsdu_subframes;
+ int max_amsdu_len = sta->sta.cur->max_amsdu_len;
+ int n = 1, nfrags, len;
+ u8 tid = txqi->txq.tid;
+
+ if (!flow || !ieee80211_hw_check(&local->hw, TX_AMSDU) ||
+ !(hdr->frame_control & cpu_to_le16(IEEE80211_STYPE_QOS_DATA)) ||
+ test_bit(IEEE80211_TXQ_NO_AMSDU, &txqi->flags) ||
+ skb_is_gso(head))
+ return;
+
+ if (sta->sta.cur->max_rc_amsdu_len)
+ max_amsdu_len = min_t(int, max_amsdu_len,
+ sta->sta.cur->max_rc_amsdu_len);
+
+ if (sta->sta.cur->max_tid_amsdu_len[tid])
+ max_amsdu_len = min_t(int, max_amsdu_len,
+ sta->sta.cur->max_tid_amsdu_len[tid]);
+
+ /* the head will get an A-MSDU subframe header */
+ len = head->len + ETH_HLEN;
+ nfrags = 1 + skb_shinfo(head)->nr_frags;
+ frag_tail = &skb_shinfo(head)->frag_list;
+
+ spin_lock_bh(&fq->lock);
+
+ /* the flow may have been reused while the lock was released */
+ if (flow->tin != &txqi->tin)
+ goto out;
+
+ while (*frag_tail) {
+ nfrags += 1 + skb_shinfo(*frag_tail)->nr_frags;
+ frag_tail = &(*frag_tail)->next;
+ n++;
+ }
+
+ while (1) {
+ struct sk_buff *skb = skb_peek(&flow->queue);
+ int pad = (4 - ((len - hdrlen) & 3)) & 3;
+ struct ieee80211_tx_info *info;
+
+ if (!skb)
+ break;
+
+ info = IEEE80211_SKB_CB(skb);
+ if (!(info->flags & IEEE80211_TX_INTFL_NEED_80211_ENCAP) ||
+ !(info->control.flags & IEEE80211_TX_CTRL_FAST_XMIT) ||
+ skb_is_gso(skb))
+ break;
+
+ if (len + pad + skb->len + sizeof(rfc1042_header) + 2 >
+ max_amsdu_len)
+ break;
+
+ if (max_subframes && n + 1 > max_subframes)
+ break;
+
+ if (max_frags &&
+ nfrags + 1 + skb_shinfo(skb)->nr_frags > max_frags)
+ break;
+
+ if (!drv_can_aggregate_in_amsdu(local, head, skb))
+ break;
+
+ if (!ieee80211_amsdu_prepare_head(tx->sdata, fast_tx, head))
+ break;
+
+ skb = fq_flow_dequeue_next(fq, flow);
+
+ if (!ieee80211_amsdu_subframe(local, skb, len - hdrlen)) {
+ ieee80211_free_txskb(&local->hw, skb);
+ break;
+ }
+
+ len += skb->len;
+ nfrags += 1 + skb_shinfo(skb)->nr_frags;
+ n++;
+
+ head->len += skb->len;
+ head->data_len += skb->len;
+ *frag_tail = skb;
+ frag_tail = &skb->next;
+ }
+
+out:
+ spin_unlock_bh(&fq->lock);
+}
+
+static int ieee80211_txq_build_hdr(struct ieee80211_tx_data *tx,
+ const struct txq_info *txqi,
+ struct fq_flow *flow)
{
const struct ieee80211_fast_tx *fast_tx;
struct sk_buff *skb = tx->skb;
@@ -4151,6 +4128,7 @@ static int ieee80211_txq_build_hdr(struct ieee80211_tx_data *tx)
ieee80211_free_txskb(&tx->local->hw, skb);
return -ENOMEM;
}
+ ieee80211_tx_build_amsdu(tx, txqi, flow, fast_tx);
return 0;
}
@@ -4177,6 +4155,7 @@ static struct sk_buff *__ieee80211_tx_dequeue(struct ieee80211_hw *hw,
struct sk_buff *skb = NULL;
struct fq *fq = &local->fq;
struct fq_tin *tin = &txqi->tin;
+ struct fq_flow *flow;
struct ieee80211_tx_info *info;
struct ieee80211_tx_data tx;
ieee80211_tx_result r;
@@ -4210,6 +4189,8 @@ static struct sk_buff *__ieee80211_tx_dequeue(struct ieee80211_hw *hw,
spin_lock_bh(&fq->lock);
+ flow = NULL;
+
/* Make sure fragments stay together. */
skb = __skb_dequeue(&txqi->frags);
if (unlikely(skb)) {
@@ -4222,7 +4203,7 @@ static struct sk_buff *__ieee80211_tx_dequeue(struct ieee80211_hw *hw,
if (unlikely(test_bit(IEEE80211_TXQ_STOP, &txqi->flags)))
goto out;
- skb = fq_tin_dequeue(fq, tin, fq_tin_dequeue_func, NULL);
+ skb = fq_tin_dequeue(fq, tin, fq_tin_dequeue_func, &flow);
}
if (!skb)
@@ -4277,7 +4258,7 @@ static struct sk_buff *__ieee80211_tx_dequeue(struct ieee80211_hw *hw,
/* the fast-xmit check here distinguishes from HW offload */
if (info->flags & IEEE80211_TX_INTFL_NEED_80211_ENCAP &&
info->control.flags & IEEE80211_TX_CTRL_FAST_XMIT) {
- if (ieee80211_txq_build_hdr(&tx))
+ if (ieee80211_txq_build_hdr(&tx, txqi, flow))
goto begin;
skb = tx.skb;
hdr = (struct ieee80211_hdr *)skb->data;
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 27/30] wifi: mac80211: handle 802.11 encap at TXQ dequeue
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (25 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 26/30] wifi: mac80211: build A-MSDUs at dequeue Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 28/30] wifi: mac80211: split some TX code into helpers Johannes Berg
` (2 subsequent siblings)
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
Now that fast-xmit and encapsulation offload frames both go
on the TXQ in ethernet format, it's not as relevant whether
or not the device can do it, defer that decision to dequeue
time.
This is mostly preparation work, the next commits change it
so that all (unicast data) frames go onto the TXQs without
conversion first.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/sta_info.c | 6 --
net/mac80211/tx.c | 140 +++++++++++++++++++++++++++++-----------
2 files changed, 102 insertions(+), 44 deletions(-)
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index ad73a20eddb5..8e044ff5f716 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -2078,9 +2078,6 @@ static bool ieee80211_sta_ps_from_txqs(const struct sta_info *sta)
{
struct ieee80211_local *local = sta->local;
- if (sta->sdata->vif.offload_flags & IEEE80211_OFFLOAD_ENCAP_ENABLED)
- return false;
-
return !local->ops->release_buffered_frames ||
local->ops->allow_buffered_frames;
}
@@ -2090,9 +2087,6 @@ static bool ieee80211_sta_ps_frame_ends_bu(struct sk_buff *skb)
{
struct ieee80211_hdr *hdr = (void *)skb->data;
- if (IEEE80211_SKB_CB(skb)->flags & IEEE80211_TX_CTL_HW_80211_ENCAP)
- return true;
-
return !ieee80211_has_morefrags(hdr->frame_control);
}
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 57b67774f527..5ab061a010a6 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -3854,8 +3854,7 @@ static bool ieee80211_xmit_fast_queue(struct ieee80211_sub_if_data *sdata,
info->flags = IEEE80211_TX_CTL_FIRST_FRAGMENT |
IEEE80211_TX_CTL_DONTFRAG |
IEEE80211_TX_INTFL_NEED_80211_ENCAP;
- info->control.flags = IEEE80211_TX_CTRL_FAST_XMIT |
- u32_encode_bits(IEEE80211_LINK_UNSPECIFIED,
+ info->control.flags = u32_encode_bits(IEEE80211_LINK_UNSPECIFIED,
IEEE80211_TX_CTRL_MLO_LINK);
#ifdef CONFIG_MAC80211_DEBUGFS
@@ -4008,6 +4007,14 @@ static bool ieee80211_amsdu_subframe(struct ieee80211_local *local,
return true;
}
+static bool ieee80211_can_use_fast_xmit(struct sk_buff *skb)
+{
+ u16 ethertype = (skb->data[12] << 8) | skb->data[13];
+
+ /* length "ethertype" doesn't go with RFC 1042 header */
+ return ethertype >= ETH_P_802_3_MIN;
+}
+
static void ieee80211_tx_build_amsdu(const struct ieee80211_tx_data *tx,
const struct txq_info *txqi,
struct fq_flow *flow,
@@ -4032,6 +4039,10 @@ static void ieee80211_tx_build_amsdu(const struct ieee80211_tx_data *tx,
skb_is_gso(head))
return;
+ /* don't aggregate frames with status request */
+ if (IEEE80211_SKB_CB(head)->status_data_idr)
+ return;
+
if (sta->sta.cur->max_rc_amsdu_len)
max_amsdu_len = min_t(int, max_amsdu_len,
sta->sta.cur->max_rc_amsdu_len);
@@ -4067,8 +4078,8 @@ static void ieee80211_tx_build_amsdu(const struct ieee80211_tx_data *tx,
info = IEEE80211_SKB_CB(skb);
if (!(info->flags & IEEE80211_TX_INTFL_NEED_80211_ENCAP) ||
- !(info->control.flags & IEEE80211_TX_CTRL_FAST_XMIT) ||
- skb_is_gso(skb))
+ !ieee80211_can_use_fast_xmit(skb) ||
+ info->status_data_idr || skb_is_gso(skb))
break;
if (len + pad + skb->len + sizeof(rfc1042_header) + 2 >
@@ -4109,37 +4120,105 @@ static void ieee80211_tx_build_amsdu(const struct ieee80211_tx_data *tx,
spin_unlock_bh(&fq->lock);
}
+static bool ieee80211_hw_can_encap(const struct ieee80211_tx_data *tx,
+ bool ps_release)
+{
+ struct ieee80211_sub_if_data *sdata = tx->sta ? tx->sta->sdata :
+ tx->sdata;
+ struct ieee80211_tx_info *info = IEEE80211_SKB_CB(tx->skb);
+ struct ieee80211_sub_if_data *bss = sdata;
+ u32 flags;
+
+ /* PS release has 802.11 header flags we set */
+ if (ps_release ||
+ (tx->sta && (test_sta_flag(tx->sta, WLAN_STA_PS_STA) ||
+ test_sta_flag(tx->sta, WLAN_STA_PS_DRIVER))))
+ return false;
+
+ if (sdata->vif.type == NL80211_IFTYPE_AP_VLAN)
+ bss = container_of(sdata->bss, struct ieee80211_sub_if_data,
+ u.ap);
+ flags = bss->vif.offload_flags;
+
+ if (!(flags & IEEE80211_OFFLOAD_ENCAP_ENABLED) ||
+ (sdata->wdev.use_4addr && !(flags & IEEE80211_OFFLOAD_ENCAP_4ADDR)) ||
+ (!tx->sta && !(flags & IEEE80211_OFFLOAD_ENCAP_MCAST)))
+ return false;
+
+ if (info->control.flags & IEEE80211_TX_CTRL_PORT_CTRL_PROTO)
+ return false;
+
+ return !tx->key ||
+ (tx->key->flags & KEY_FLAG_UPLOADED_TO_HARDWARE &&
+ tx->key->conf.cipher != WLAN_CIPHER_SUITE_TKIP);
+}
+
+static void ieee80211_8023_tx_stats(struct sta_info *sta, struct sk_buff *skb)
+{
+ int ac = skb_get_queue_mapping(skb);
+
+ dev_sw_netstats_tx_add(skb->dev, 1, skb->len);
+ if (sta) {
+ sta->deflink.tx_stats.packets[ac]++;
+ sta->deflink.tx_stats.bytes[ac] += skb->len;
+ }
+}
+
+static void ieee80211_set_hw_encap(struct ieee80211_tx_data *tx,
+ struct ieee80211_txq *txq)
+{
+ struct sk_buff *skb = tx->skb;
+ struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
+
+ info->control.flags |= IEEE80211_TX_CTRL_MLO_LINK_UNSPEC;
+ info->hw_queue = txq->vif->hw_queue[skb_get_queue_mapping(skb)];
+
+ ieee80211_8023_tx_stats(tx->sta, skb);
+}
+
static int ieee80211_txq_build_hdr(struct ieee80211_tx_data *tx,
const struct txq_info *txqi,
struct fq_flow *flow)
{
- const struct ieee80211_fast_tx *fast_tx;
struct sk_buff *skb = tx->skb;
struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
+ struct ieee80211_sub_if_data *sdata = tx->sdata;
+ const struct ieee80211_fast_tx *fast_tx = NULL;
struct ethhdr eth;
info->flags &= ~IEEE80211_TX_INTFL_NEED_80211_ENCAP;
+ info->control.flags = u32_replace_bits(info->control.flags,
+ IEEE80211_LINK_UNSPECIFIED,
+ IEEE80211_TX_CTRL_MLO_LINK);
+ if (WARN_ON_ONCE(!tx->sta)) {
+ ieee80211_free_txskb(&tx->local->hw, skb);
+ return -EINVAL;
+ }
+
+ /* use sta->sdata for VLAN, info->control.vif is the AP */
+ sdata = tx->sta->sdata;
fast_tx = rcu_dereference(tx->sta->fast_tx);
- if (fast_tx && fast_tx->key == tx->key) {
+
+ /* if key changed the header template might have changed */
+ if (fast_tx && fast_tx->key == tx->key &&
+ ieee80211_can_use_fast_xmit(skb)) {
memcpy(ð, skb->data, ETH_HLEN - 2);
if (ieee80211_fast_tx_encap(tx->sdata, fast_tx, skb,
eth.h_dest, eth.h_source)) {
ieee80211_free_txskb(&tx->local->hw, skb);
return -ENOMEM;
}
+ info->control.flags |= IEEE80211_TX_CTRL_FAST_XMIT;
ieee80211_tx_build_amsdu(tx, txqi, flow, fast_tx);
return 0;
}
- info->control.flags &= ~IEEE80211_TX_CTRL_FAST_XMIT;
-
- /* use sta->sdata for VLAN, info->control.vif is the AP */
- skb = ieee80211_build_hdr(tx->sta->sdata, skb, tx->sta);
+ skb = ieee80211_build_hdr(sdata, skb, tx->sta);
if (IS_ERR(skb))
return PTR_ERR(skb);
- ieee80211_set_qos_hdr(tx->sta->sdata, skb);
+ ieee80211_set_qos_hdr(sdata, skb);
dev_sw_netstats_tx_add(skb->dev, 1, skb->len);
tx->skb = skb;
return 0;
@@ -4255,12 +4334,13 @@ static struct sk_buff *__ieee80211_tx_dequeue(struct ieee80211_hw *hw,
info->flags |= (IEEE80211_TX_CTL_AMPDU |
IEEE80211_TX_CTL_DONTFRAG);
- /* the fast-xmit check here distinguishes from HW offload */
- if (info->flags & IEEE80211_TX_INTFL_NEED_80211_ENCAP &&
- info->control.flags & IEEE80211_TX_CTRL_FAST_XMIT) {
- if (ieee80211_txq_build_hdr(&tx, txqi, flow))
+ if (info->flags & IEEE80211_TX_INTFL_NEED_80211_ENCAP) {
+ if (ieee80211_hw_can_encap(&tx, ps_release))
+ ieee80211_set_hw_encap(&tx, txq);
+ else if (ieee80211_txq_build_hdr(&tx, txqi, flow))
goto begin;
skb = tx.skb;
+ info = IEEE80211_SKB_CB(skb);
hdr = (struct ieee80211_hdr *)skb->data;
}
@@ -5029,6 +5109,9 @@ static bool ieee80211_tx_8023(struct ieee80211_sub_if_data *sdata,
skb_list_walk_safe(skb, skb, next) {
skb_mark_not_on_list(skb);
+ /* frames from the pending queue were counted already */
+ if (!txpending)
+ ieee80211_8023_tx_stats(sta, skb);
if (!__ieee80211_tx_8023(sdata, skb, sta, txpending))
ret = false;
}
@@ -5045,7 +5128,7 @@ static void ieee80211_8023_xmit(struct ieee80211_sub_if_data *sdata,
struct ieee80211_chanctx_conf *chanctx_conf;
struct tid_ampdu_tx *tid_tx = NULL;
struct sk_buff *seg, *next;
- unsigned int skbs = 0, len = 0;
+ unsigned int len = 0;
u16 queue;
u8 tid;
@@ -5062,17 +5145,8 @@ static void ieee80211_8023_xmit(struct ieee80211_sub_if_data *sdata,
if (sta)
tid_tx = rcu_dereference(sta->ampdu_mlme.tid_tx[tid]);
- if (tid_tx) {
- if (!test_bit(HT_AGG_STATE_OPERATIONAL, &tid_tx->state)) {
- /* fall back to non-offload slow path */
- __ieee80211_subif_start_xmit(skb, dev, 0,
- IEEE80211_TX_CTRL_MLO_LINK_UNSPEC);
- return;
- }
-
- if (tid_tx->timeout)
- tid_tx->last_tx = jiffies;
- }
+ if (tid_tx && tid_tx->timeout)
+ tid_tx->last_tx = jiffies;
skb = ieee80211_tx_skb_fixup(skb, ieee80211_sdata_netdev_features(sdata));
if (!skb)
@@ -5104,7 +5178,6 @@ static void ieee80211_8023_xmit(struct ieee80211_sub_if_data *sdata,
info->control.hw_key = &key->conf;
skb_list_walk_safe(skb, seg, next) {
- skbs++;
len += seg->len;
if (seg != skb)
memcpy(IEEE80211_SKB_CB(seg), info, sizeof(*info));
@@ -5113,13 +5186,6 @@ static void ieee80211_8023_xmit(struct ieee80211_sub_if_data *sdata,
if (unlikely(sk_requests_wifi_status(skb->sk)))
ieee80211_store_ack_skb(local, skb, 0);
- dev_sw_netstats_tx_add(dev, skbs, len);
-
- if (sta) {
- sta->deflink.tx_stats.packets[queue] += skbs;
- sta->deflink.tx_stats.bytes[queue] += len;
- }
-
ieee80211_tpt_led_trig_tx(local, len);
ieee80211_tx_8023(sdata, skb, sta, false);
@@ -6668,9 +6734,7 @@ ieee80211_get_buffered_bc(struct ieee80211_hw *hw,
spin_lock(&local->fq.lock);
if (!txq_has_queue(vif->txq)) {
clear_bit(IEEE80211_TXQ_MCAST_PS, &txqi->flags);
- } else if (skb &&
- !(IEEE80211_SKB_CB(skb)->flags &
- IEEE80211_TX_CTL_HW_80211_ENCAP)) {
+ } else if (skb) {
struct ieee80211_hdr *hdr = (void *)skb->data;
/* inform PS stations about more buffered frames */
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 28/30] wifi: mac80211: split some TX code into helpers
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (26 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 27/30] wifi: mac80211: handle 802.11 encap at TXQ dequeue Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 29/30] wifi: mac80211: queue unicast data frames as ethernet Johannes Berg
2026-11-02 20:23 ` [PATCH 30/30] wifi: mac80211: keep fast-xmit for sleeping STAs Johannes Berg
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
Split the dynamic powersave, control port protocol flags
handling and frame resizing out of the TX code into a few
helper functions to be used later.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/tx.c | 82 ++++++++++++++++++++++++++++++-----------------
1 file changed, 52 insertions(+), 30 deletions(-)
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 5ab061a010a6..9db16eb96588 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -188,45 +188,44 @@ static __le16 ieee80211_duration(struct ieee80211_tx_data *tx,
return cpu_to_le16(dur);
}
-/* tx handlers */
-static ieee80211_tx_result debug_noinline
-ieee80211_tx_h_dynamic_ps(struct ieee80211_tx_data *tx)
+static void ieee80211_tx_dynamic_ps(struct ieee80211_sub_if_data *sdata,
+ struct sk_buff *skb)
{
- struct ieee80211_local *local = tx->local;
+ struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
+ struct ieee80211_local *local = sdata->local;
struct ieee80211_if_managed *ifmgd;
- struct ieee80211_tx_info *info = IEEE80211_SKB_CB(tx->skb);
/* driver doesn't support power save */
if (!ieee80211_hw_check(&local->hw, SUPPORTS_PS))
- return TX_CONTINUE;
+ return;
/* hardware does dynamic power save */
if (ieee80211_hw_check(&local->hw, SUPPORTS_DYNAMIC_PS))
- return TX_CONTINUE;
+ return;
/* dynamic power save disabled */
if (local->hw.conf.dynamic_ps_timeout <= 0)
- return TX_CONTINUE;
+ return;
/* we are scanning, don't enable power save */
if (local->scanning)
- return TX_CONTINUE;
+ return;
if (!local->ps_sdata)
- return TX_CONTINUE;
+ return;
/* No point if we're going to suspend */
if (local->quiescing)
- return TX_CONTINUE;
+ return;
/* dynamic ps is supported only in managed mode */
- if (tx->sdata->vif.type != NL80211_IFTYPE_STATION)
- return TX_CONTINUE;
+ if (sdata->vif.type != NL80211_IFTYPE_STATION)
+ return;
if (unlikely(info->flags & IEEE80211_TX_INTFL_OFFCHAN_TX_OK))
- return TX_CONTINUE;
+ return;
- ifmgd = &tx->sdata->u.mgd;
+ ifmgd = &sdata->u.mgd;
/*
* Don't wakeup from power save if u-apsd is enabled, voip ac has
@@ -243,8 +242,8 @@ ieee80211_tx_h_dynamic_ps(struct ieee80211_tx_data *tx)
*/
if ((ifmgd->flags & IEEE80211_STA_UAPSD_ENABLED) &&
(ifmgd->uapsd_queues & IEEE80211_WMM_IE_STA_QOSINFO_AC_VO) &&
- skb_get_queue_mapping(tx->skb) == IEEE80211_AC_VO)
- return TX_CONTINUE;
+ skb_get_queue_mapping(skb) == IEEE80211_AC_VO)
+ return;
if (local->hw.conf.flags & IEEE80211_CONF_PS) {
ieee80211_stop_queues_by_reason(&local->hw,
@@ -258,10 +257,17 @@ ieee80211_tx_h_dynamic_ps(struct ieee80211_tx_data *tx)
/* Don't restart the timer if we're not disassociated */
if (!ifmgd->associated)
- return TX_CONTINUE;
+ return;
mod_timer(&local->dynamic_ps_timer, jiffies +
msecs_to_jiffies(local->hw.conf.dynamic_ps_timeout));
+}
+
+/* tx handlers */
+static ieee80211_tx_result debug_noinline
+ieee80211_tx_h_dynamic_ps(struct ieee80211_tx_data *tx)
+{
+ ieee80211_tx_dynamic_ps(tx->sdata, tx->skb);
return TX_CONTINUE;
}
@@ -588,17 +594,24 @@ ieee80211_tx_h_ps_buf(struct ieee80211_tx_data *tx)
return ieee80211_tx_h_multicast_ps_buf(tx);
}
-static ieee80211_tx_result debug_noinline
-ieee80211_tx_h_check_control_port_protocol(struct ieee80211_tx_data *tx)
+static void
+ieee80211_tx_set_ctrl_port_flags(const struct ieee80211_sub_if_data *sdata,
+ struct sk_buff *skb)
{
- struct ieee80211_tx_info *info = IEEE80211_SKB_CB(tx->skb);
+ struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
- if (unlikely(tx->sdata->control_port_protocol == tx->skb->protocol)) {
- if (tx->sdata->control_port_no_encrypt)
+ if (unlikely(sdata->control_port_protocol == skb->protocol)) {
+ if (sdata->control_port_no_encrypt)
info->flags |= IEEE80211_TX_INTFL_DONT_ENCRYPT;
info->control.flags |= IEEE80211_TX_CTRL_PORT_CTRL_PROTO;
info->flags |= IEEE80211_TX_CTL_USE_MINRATE;
}
+}
+
+static ieee80211_tx_result debug_noinline
+ieee80211_tx_h_check_control_port_protocol(struct ieee80211_tx_data *tx)
+{
+ ieee80211_tx_set_ctrl_port_flags(tx->sdata, tx->skb);
return TX_CONTINUE;
}
@@ -2183,14 +2196,13 @@ static int ieee80211_skb_resize(struct ieee80211_sub_if_data *sdata,
return 0;
}
-void ieee80211_xmit(struct ieee80211_sub_if_data *sdata,
- struct sta_info *sta, struct sk_buff *skb)
+static int ieee80211_tx_resize(struct ieee80211_sub_if_data *sdata,
+ struct sk_buff *skb)
{
- struct ieee80211_local *local = sdata->local;
+ struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)skb->data;
struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
- struct ieee80211_hdr *hdr = (struct ieee80211_hdr *) skb->data;
- int headroom;
enum ieee80211_encrypt encrypt;
+ int headroom;
if (info->flags & IEEE80211_TX_INTFL_DONT_ENCRYPT)
encrypt = ENCRYPT_NO;
@@ -2199,13 +2211,23 @@ void ieee80211_xmit(struct ieee80211_sub_if_data *sdata,
else
encrypt = ENCRYPT_DATA;
- headroom = local->tx_headroom;
+ headroom = sdata->local->tx_headroom;
if (encrypt != ENCRYPT_NO)
headroom += IEEE80211_ENCRYPT_HEADROOM;
headroom -= skb_headroom(skb);
headroom = max_t(int, 0, headroom);
- if (ieee80211_skb_resize(sdata, skb, headroom, encrypt)) {
+ return ieee80211_skb_resize(sdata, skb, headroom, encrypt);
+}
+
+void ieee80211_xmit(struct ieee80211_sub_if_data *sdata,
+ struct sta_info *sta, struct sk_buff *skb)
+{
+ struct ieee80211_local *local = sdata->local;
+ struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
+ struct ieee80211_hdr *hdr;
+
+ if (ieee80211_tx_resize(sdata, skb)) {
ieee80211_free_txskb(&local->hw, skb);
return;
}
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 29/30] wifi: mac80211: queue unicast data frames as ethernet
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (27 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 28/30] wifi: mac80211: split some TX code into helpers Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
2026-11-02 20:23 ` [PATCH 30/30] wifi: mac80211: keep fast-xmit for sleeping STAs Johannes Berg
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
After the earlier changes, unicast data frames without
fast-xmit were still first converted before being queued.
Except for mesh (it requires path selection) put these
onto the TXQs in ethernet format as well. Encapsulating
them at dequeue time uses the fast-xmit template if it
can, otherwise the regular way.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/tx.c | 145 ++++++++++++++++++++++++++++++++++++----------
1 file changed, 113 insertions(+), 32 deletions(-)
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 9db16eb96588..8f528ab7ebf3 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -673,7 +673,8 @@ ieee80211_select_key_8023(struct ieee80211_tx_data *tx)
if (!tx->key)
return TX_CONTINUE;
- if (unlikely(tx->key->flags & KEY_FLAG_TAINTED))
+ if (unlikely(tx->key->flags & KEY_FLAG_TAINTED &&
+ !(info->control.flags & IEEE80211_TX_CTRL_PORT_CTRL_PROTO)))
return TX_DROP;
if (tx->key->flags & KEY_FLAG_UPLOADED_TO_HARDWARE)
@@ -3887,16 +3888,76 @@ static bool ieee80211_xmit_fast_queue(struct ieee80211_sub_if_data *sdata,
return ieee80211_queue_skb(sdata->local, sdata, sta, skb);
}
+static void ieee80211_tx_mark_ba_session_used(const struct sta_info *sta,
+ struct sk_buff *skb)
+{
+ u8 tid = skb->priority & IEEE80211_QOS_CTL_TAG1D_MASK;
+ struct tid_ampdu_tx *tid_tx;
+
+ tid_tx = rcu_dereference(sta->ampdu_mlme.tid_tx[tid]);
+ if (tid_tx && tid_tx->timeout)
+ tid_tx->last_tx = jiffies;
+}
+
+/* full handling, for frames that fast-xmit doesn't apply to */
+static void ieee80211_xmit_queue_full(struct ieee80211_sub_if_data *sdata,
+ struct sta_info *sta,
+ struct sk_buff *skb,
+ u32 info_flags, u32 ctrl_flags)
+{
+ struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
+ struct ieee80211_sub_if_data *ap_sdata = sdata;
+ struct ieee80211_local *local = sdata->local;
+ struct ieee80211_chanctx_conf *chanctx_conf;
+
+ memset(info, 0, sizeof(*info));
+ info->control.vif = &sdata->vif;
+ info->flags = info_flags | IEEE80211_TX_CTL_FIRST_FRAGMENT |
+ IEEE80211_TX_INTFL_NEED_80211_ENCAP;
+ info->control.flags = ctrl_flags;
+
+ if (sdata->vif.type == NL80211_IFTYPE_NAN_DATA) {
+ /* NAN operates on multiple bands */
+ info->band = NUM_NL80211_BANDS;
+ } else if (!ieee80211_vif_is_mld(&sdata->vif)) {
+ /* MLD transmissions must not rely on the band */
+ if (sdata->vif.type == NL80211_IFTYPE_AP_VLAN)
+ ap_sdata = container_of(sdata->bss,
+ struct ieee80211_sub_if_data,
+ u.ap);
+ chanctx_conf =
+ rcu_dereference(ap_sdata->vif.bss_conf.chanctx_conf);
+ if (unlikely(!chanctx_conf))
+ goto free;
+ info->band = chanctx_conf->def.chan->band;
+ }
+
+ ieee80211_tx_set_ctrl_port_flags(sdata, skb);
+
+ if (unlikely(sk_requests_wifi_status(skb->sk)))
+ ieee80211_store_ack_skb(local, skb, 0);
+
+#ifdef CONFIG_MAC80211_DEBUGFS
+ if (local->force_tx_status)
+ info->flags |= IEEE80211_TX_CTL_REQ_TX_STATUS;
+#endif
+
+ ieee80211_tx_dynamic_ps(sdata, skb);
+ ieee80211_tx_mark_ba_session_used(sta, skb);
+
+ if (ieee80211_queue_skb(local, sdata, sta, skb))
+ return;
+free:
+ ieee80211_free_txskb(&local->hw, skb);
+}
+
static bool ieee80211_xmit_fast(struct ieee80211_sub_if_data *sdata,
struct sta_info *sta,
struct ieee80211_fast_tx *fast_tx,
struct sk_buff *skb)
{
u16 ethertype = (skb->data[12] << 8) | skb->data[13];
- struct ieee80211_hdr *hdr = (void *)fast_tx->hdr;
- struct tid_ampdu_tx *tid_tx = NULL;
struct sk_buff *next;
- u8 tid;
/* control port protocol needs a lot of special handling */
if (cpu_to_be16(ethertype) == sdata->control_port_protocol)
@@ -3910,16 +3971,7 @@ static bool ieee80211_xmit_fast(struct ieee80211_sub_if_data *sdata,
if (sk_requests_wifi_status(skb->sk))
return false;
- if (hdr->frame_control & cpu_to_le16(IEEE80211_STYPE_QOS_DATA)) {
- tid = skb->priority & IEEE80211_QOS_CTL_TAG1D_MASK;
- tid_tx = rcu_dereference(sta->ampdu_mlme.tid_tx[tid]);
- if (tid_tx) {
- if (!test_bit(HT_AGG_STATE_OPERATIONAL, &tid_tx->state))
- return false;
- if (tid_tx->timeout)
- tid_tx->last_tx = jiffies;
- }
- }
+ ieee80211_tx_mark_ba_session_used(sta, skb);
/* after this point (skb is modified) we cannot return false */
skb = ieee80211_tx_skb_fixup(skb, ieee80211_sdata_netdev_features(sdata));
@@ -4031,10 +4083,23 @@ static bool ieee80211_amsdu_subframe(struct ieee80211_local *local,
static bool ieee80211_can_use_fast_xmit(struct sk_buff *skb)
{
+ struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
u16 ethertype = (skb->data[12] << 8) | skb->data[13];
/* length "ethertype" doesn't go with RFC 1042 header */
- return ethertype >= ETH_P_802_3_MIN;
+ if (ethertype < ETH_P_802_3_MIN)
+ return false;
+
+ return !(info->control.flags & IEEE80211_TX_CTRL_PORT_CTRL_PROTO);
+}
+
+/* don't aggregate frames with status request */
+static bool ieee80211_tx_needs_own_status(struct sk_buff *skb)
+{
+ struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
+
+ return info->status_data_idr ||
+ info->flags & IEEE80211_TX_INTFL_MLME_CONN_TX;
}
static void ieee80211_tx_build_amsdu(const struct ieee80211_tx_data *tx,
@@ -4058,11 +4123,7 @@ static void ieee80211_tx_build_amsdu(const struct ieee80211_tx_data *tx,
if (!flow || !ieee80211_hw_check(&local->hw, TX_AMSDU) ||
!(hdr->frame_control & cpu_to_le16(IEEE80211_STYPE_QOS_DATA)) ||
test_bit(IEEE80211_TXQ_NO_AMSDU, &txqi->flags) ||
- skb_is_gso(head))
- return;
-
- /* don't aggregate frames with status request */
- if (IEEE80211_SKB_CB(head)->status_data_idr)
+ skb_is_gso(head) || ieee80211_tx_needs_own_status(head))
return;
if (sta->sta.cur->max_rc_amsdu_len)
@@ -4101,7 +4162,7 @@ static void ieee80211_tx_build_amsdu(const struct ieee80211_tx_data *tx,
info = IEEE80211_SKB_CB(skb);
if (!(info->flags & IEEE80211_TX_INTFL_NEED_80211_ENCAP) ||
!ieee80211_can_use_fast_xmit(skb) ||
- info->status_data_idr || skb_is_gso(skb))
+ ieee80211_tx_needs_own_status(skb) || skb_is_gso(skb))
break;
if (len + pad + skb->len + sizeof(rfc1042_header) + 2 >
@@ -4186,14 +4247,12 @@ static void ieee80211_8023_tx_stats(struct sta_info *sta, struct sk_buff *skb)
}
}
-static void ieee80211_set_hw_encap(struct ieee80211_tx_data *tx,
- struct ieee80211_txq *txq)
+static void ieee80211_set_hw_encap(const struct ieee80211_tx_data *tx)
{
struct sk_buff *skb = tx->skb;
struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
info->control.flags |= IEEE80211_TX_CTRL_MLO_LINK_UNSPEC;
- info->hw_queue = txq->vif->hw_queue[skb_get_queue_mapping(skb)];
ieee80211_8023_tx_stats(tx->sta, skb);
}
@@ -4233,16 +4292,25 @@ static int ieee80211_txq_build_hdr(struct ieee80211_tx_data *tx,
}
info->control.flags |= IEEE80211_TX_CTRL_FAST_XMIT;
ieee80211_tx_build_amsdu(tx, txqi, flow, fast_tx);
- return 0;
+ } else {
+ skb = ieee80211_build_hdr(sdata, skb, tx->sta);
+ if (IS_ERR(skb))
+ return PTR_ERR(skb);
+
+ if (ieee80211_tx_resize(sdata, skb)) {
+ ieee80211_free_txskb(&tx->local->hw, skb);
+ return -ENOMEM;
+ }
+
+ ieee80211_set_qos_hdr(sdata, skb);
+ dev_sw_netstats_tx_add(skb->dev, 1, skb->len);
+ tx->skb = skb;
}
- skb = ieee80211_build_hdr(sdata, skb, tx->sta);
- if (IS_ERR(skb))
- return PTR_ERR(skb);
+ if (!tx->sta ||
+ skb->len + FCS_LEN <= tx->local->hw.wiphy->frag_threshold)
+ info->flags |= IEEE80211_TX_CTL_DONTFRAG;
- ieee80211_set_qos_hdr(sdata, skb);
- dev_sw_netstats_tx_add(skb->dev, 1, skb->len);
- tx->skb = skb;
return 0;
}
@@ -4357,8 +4425,10 @@ static struct sk_buff *__ieee80211_tx_dequeue(struct ieee80211_hw *hw,
IEEE80211_TX_CTL_DONTFRAG);
if (info->flags & IEEE80211_TX_INTFL_NEED_80211_ENCAP) {
+ info->hw_queue = vif->hw_queue[skb_get_queue_mapping(skb)];
+
if (ieee80211_hw_can_encap(&tx, ps_release))
- ieee80211_set_hw_encap(&tx, txq);
+ ieee80211_set_hw_encap(&tx);
else if (ieee80211_txq_build_hdr(&tx, txqi, flow))
goto begin;
skb = tx.skb;
@@ -4802,6 +4872,7 @@ void __ieee80211_subif_start_xmit(struct sk_buff *skb,
struct sta_info *sta;
struct sk_buff *next;
bool group_addressed;
+ bool queue_ethernet;
int len = skb->len;
if (unlikely(!ieee80211_sdata_running(sdata) || skb->len < ETH_HLEN)) {
@@ -4839,6 +4910,10 @@ void __ieee80211_subif_start_xmit(struct sk_buff *skb,
goto out;
}
+ queue_ethernet = sta && sta->uploaded &&
+ test_sta_flag(sta, WLAN_STA_ASSOC) &&
+ !ieee80211_vif_is_mesh(&sdata->vif);
+
/* the frame could be fragmented, software-encrypted, and other
* things so we cannot really handle checksum or GSO offload.
* fix it up in software before we handle anything else.
@@ -4861,6 +4936,12 @@ void __ieee80211_subif_start_xmit(struct sk_buff *skb,
goto out;
}
+ if (queue_ethernet) {
+ ieee80211_xmit_queue_full(sdata, sta, skb, info_flags,
+ ctrl_flags);
+ continue;
+ }
+
info = IEEE80211_SKB_CB(skb);
memset(info, 0, sizeof(*info));
info->flags = info_flags;
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 30/30] wifi: mac80211: keep fast-xmit for sleeping STAs
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
` (28 preceding siblings ...)
2026-11-02 20:23 ` [PATCH 29/30] wifi: mac80211: queue unicast data frames as ethernet Johannes Berg
@ 2026-11-02 20:23 ` Johannes Berg
29 siblings, 0 replies; 33+ messages in thread
From: Johannes Berg @ 2026-11-02 20:23 UTC (permalink / raw)
To: linux-wireless; +Cc: Toke Høiland-Jørgensen, Johannes Berg
From: Johannes Berg <johannes.berg@intel.com>
Fast-xmit is disabled while a station is sleeping
since we need to handle the header etc., but now
with all the previous work, this isn't needed any
more.
Remove the associated code, which now also enables
sending an A-MSDU as a poll response.
Assisted-by: LLM
Signed-off-by: Johannes Berg <johannes.berg@intel.com>
---
net/mac80211/rx.c | 2 --
net/mac80211/sta_info.c | 4 ----
net/mac80211/status.c | 1 -
net/mac80211/tx.c | 14 ++------------
4 files changed, 2 insertions(+), 19 deletions(-)
diff --git a/net/mac80211/rx.c b/net/mac80211/rx.c
index 5f31326a84b1..7f8bbcfc2c9f 100644
--- a/net/mac80211/rx.c
+++ b/net/mac80211/rx.c
@@ -1682,8 +1682,6 @@ static void sta_ps_start(struct sta_info *sta)
ps_dbg(sdata, "STA %pM aid %d enters power save mode\n",
sta->sta.addr, sta->sta.aid);
- ieee80211_clear_fast_xmit(sta);
-
for (tid = 0; tid < ARRAY_SIZE(sta->sta.txq); tid++) {
struct ieee80211_txq *txq = sta->sta.txq[tid];
struct txq_info *txqi;
diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
index 8e044ff5f716..7a6d428b6a22 100644
--- a/net/mac80211/sta_info.c
+++ b/net/mac80211/sta_info.c
@@ -1936,8 +1936,6 @@ void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta)
ps_dbg(sdata,
"STA %pM aid %d sending %d management frames since STA woke up\n",
sta->sta.addr, sta->sta.aid, buffered);
-
- ieee80211_check_fast_xmit(sta);
}
static void ieee80211_send_null_response(struct sta_info *sta, int tid,
@@ -2429,7 +2427,6 @@ void ieee80211_sta_block_awake(struct ieee80211_hw *hw,
if (block) {
set_sta_flag(sta, WLAN_STA_PS_DRIVER);
- ieee80211_clear_fast_xmit(sta);
return;
}
@@ -2447,7 +2444,6 @@ void ieee80211_sta_block_awake(struct ieee80211_hw *hw,
ieee80211_queue_work(hw, &sta->drv_deliver_wk);
} else {
clear_sta_flag(sta, WLAN_STA_PS_DRIVER);
- ieee80211_check_fast_xmit(sta);
}
}
EXPORT_SYMBOL(ieee80211_sta_block_awake);
diff --git a/net/mac80211/status.c b/net/mac80211/status.c
index 9b8e0b7e173b..94e224038974 100644
--- a/net/mac80211/status.c
+++ b/net/mac80211/status.c
@@ -118,7 +118,6 @@ static void ieee80211_handle_filtered_frame(struct ieee80211_local *local,
* when it wakes up for the next time.
*/
set_sta_flag(sta, WLAN_STA_CLEAR_PS_FILT);
- ieee80211_clear_fast_xmit(sta);
/*
* This code races in the following way:
diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
index 8f528ab7ebf3..820cfe53b29e 100644
--- a/net/mac80211/tx.c
+++ b/net/mac80211/tx.c
@@ -1331,10 +1331,8 @@ ieee80211_tx_prepare(struct ieee80211_sub_if_data *sdata,
if (!tx->sta)
info->flags |= IEEE80211_TX_CTL_CLEAR_PS_FILT;
- else if (test_and_clear_sta_flag(tx->sta, WLAN_STA_CLEAR_PS_FILT)) {
+ else if (test_and_clear_sta_flag(tx->sta, WLAN_STA_CLEAR_PS_FILT))
info->flags |= IEEE80211_TX_CTL_CLEAR_PS_FILT;
- ieee80211_check_fast_xmit(tx->sta);
- }
info->flags |= IEEE80211_TX_CTL_FIRST_FRAGMENT;
@@ -3318,12 +3316,6 @@ void ieee80211_check_fast_xmit(struct sta_info *sta)
if (!test_sta_flag(sta, WLAN_STA_AUTHORIZED) || !sta->uploaded)
goto out;
- if (test_sta_flag(sta, WLAN_STA_PS_STA) ||
- test_sta_flag(sta, WLAN_STA_PS_DRIVER) ||
- test_sta_flag(sta, WLAN_STA_PS_DELIVER) ||
- test_sta_flag(sta, WLAN_STA_CLEAR_PS_FILT))
- goto out;
-
if (sdata->noack_map)
goto out;
@@ -4403,10 +4395,8 @@ static struct sk_buff *__ieee80211_tx_dequeue(struct ieee80211_hw *hw,
* is first since it's very unlikely.
*/
if (unlikely(test_sta_flag(tx.sta, WLAN_STA_CLEAR_PS_FILT)) &&
- test_and_clear_sta_flag(tx.sta, WLAN_STA_CLEAR_PS_FILT)) {
+ test_and_clear_sta_flag(tx.sta, WLAN_STA_CLEAR_PS_FILT))
info->flags |= IEEE80211_TX_CTL_CLEAR_PS_FILT;
- ieee80211_check_fast_xmit(tx.sta);
- }
}
/*
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* Re: [PATCH 13/30] wifi: mac80211: don't apply CoDel to PS-buffered frames
2026-11-02 20:23 ` [PATCH 13/30] wifi: mac80211: don't apply CoDel to PS-buffered frames Johannes Berg
@ 2026-11-06 12:22 ` Toke Høiland-Jørgensen
0 siblings, 0 replies; 33+ messages in thread
From: Toke Høiland-Jørgensen @ 2026-11-06 12:22 UTC (permalink / raw)
To: Johannes Berg, linux-wireless; +Cc: Johannes Berg
Johannes Berg <johannes@sipsolutions.net> writes:
> From: Johannes Berg <johannes.berg@intel.com>
>
> Frames for sleeping stations remain on the TXQs now, applying
> CoDel to them would start dropping them due to a long wait on
> dequeue, but for PS STAs they will be on the queues for a while.
> Bypass CoDel while a STA is sleeping and for a while (10s) or
> when the queue drains after it wakes up, and for frames that
> were enqueued after the wakeup as well, which really is the
> common case.
>
> Assisted-by: LLM
> Signed-off-by: Johannes Berg <johannes.berg@intel.com>
> ---
> net/mac80211/ieee80211_i.h | 7 +++++
> net/mac80211/sta_info.c | 6 ++++-
> net/mac80211/tx.c | 52 ++++++++++++++++++++++++++++++++++++++
> 3 files changed, 64 insertions(+), 1 deletion(-)
>
> diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
> index 5045144641a4..cb947341fee6 100644
> --- a/net/mac80211/ieee80211_i.h
> +++ b/net/mac80211/ieee80211_i.h
> @@ -978,6 +978,7 @@ enum txq_info_flags {
> IEEE80211_TXQ_AMPDU,
> IEEE80211_TXQ_NO_AMSDU,
> IEEE80211_TXQ_DIRTY,
> + IEEE80211_TXQ_PS_BACKLOG,
> };
>
> /**
> @@ -990,6 +991,9 @@ enum txq_info_flags {
> * @schedule_order: used with ieee80211_local->active_txqs
> * @schedule_round: counter to prevent infinite loops on TXQ scheduling
> * @flags: TXQ flags from &enum txq_info_flags
> + * @ps_backlog_end: with %IEEE80211_TXQ_PS_BACKLOG, indicates when
> + * the station woke up, i.e. frames before this were just queued
> + * due to powersave
> * @txq: the driver visible part
> */
> struct txq_info {
> @@ -1003,6 +1007,7 @@ struct txq_info {
> struct sk_buff_head frags;
>
> unsigned long flags;
> + codel_time_t ps_backlog_end;
>
> /* keep last! */
> struct ieee80211_txq txq;
> @@ -2724,6 +2729,8 @@ void ieee80211_txq_init(struct ieee80211_sub_if_data *sdata,
> struct txq_info *txq, int tid);
> struct sk_buff *ieee80211_tx_dequeue_ps_release(struct ieee80211_local *local,
> struct txq_info *txqi);
> +void ieee80211_txq_ps_backlog_start(struct ieee80211_local *local,
> + struct txq_info *txqi);
The variable above is called ps_backlog_end, the function is called
ps_backlog_start. Seems a tad confusing? :)
> void ieee80211_txq_purge(struct ieee80211_local *local,
> struct txq_info *txqi);
> void ieee80211_purge_sta_txqs(struct sta_info *sta);
> diff --git a/net/mac80211/sta_info.c b/net/mac80211/sta_info.c
> index 51504f57128f..8d79e532b109 100644
> --- a/net/mac80211/sta_info.c
> +++ b/net/mac80211/sta_info.c
> @@ -1889,10 +1889,14 @@ void ieee80211_sta_ps_deliver_wakeup(struct sta_info *sta)
> /* this can be called in process context, e.g. for mesh */
> local_bh_disable();
> for (i = 0; i < ARRAY_SIZE(sta->sta.txq); i++) {
> + struct txq_info *txqi;
> +
> if (!sta->sta.txq[i] || !txq_has_queue(sta->sta.txq[i]))
> continue;
>
> - schedule_and_wake_txq(local, to_txq_info(sta->sta.txq[i]));
> + txqi = to_txq_info(sta->sta.txq[i]);
> + ieee80211_txq_ps_backlog_start(local, txqi);
> + schedule_and_wake_txq(local, txqi);
> }
> local_bh_enable();
>
> diff --git a/net/mac80211/tx.c b/net/mac80211/tx.c
> index 0bd324a4d4cb..856018cdb617 100644
> --- a/net/mac80211/tx.c
> +++ b/net/mac80211/tx.c
> @@ -1380,6 +1380,55 @@ static void codel_drop_func(struct sk_buff *skb,
> ieee80211_free_txskb(hw, skb);
> }
>
> +/* bypass codel since PS can last a long time */
> +static bool ieee80211_txq_codel_bypass(struct txq_info *txqi,
> + const struct fq_flow *flow)
> +{
> + struct sta_info *sta;
> + struct sk_buff *skb;
> +
> + if (!txqi->txq.sta)
> + return false;
> +
> + sta = container_of(txqi->txq.sta, struct sta_info, sta);
> + if (test_sta_flag(sta, WLAN_STA_PS_STA) ||
> + test_sta_flag(sta, WLAN_STA_PS_DRIVER) ||
> + test_sta_flag(sta, WLAN_STA_PS_DELIVER))
> + return true;
> +
> + if (likely(!test_bit(IEEE80211_TXQ_PS_BACKLOG, &txqi->flags)))
> + return false;
> +
> + /*
> + * Stop treating as backlog after it cleared or after 1s
> + * (the latter to avoid time wrap and issues from that).
> + */
> +#define IEEE80211_TXQ_MAX_PS_BACKLOG MS2TIME(MSEC_PER_SEC)
> +
> + if (!txqi->tin.backlog_packets ||
> + codel_time_after(codel_get_time(),
> + (codel_time_t)(txqi->ps_backlog_end +
> + IEEE80211_TXQ_MAX_PS_BACKLOG))) {
> + clear_bit(IEEE80211_TXQ_PS_BACKLOG, &txqi->flags);
> + return false;
> + }
> +
> + skb = skb_peek(&flow->queue);
> + return skb && codel_time_before(codel_skb_time_func(skb),
> + txqi->ps_backlog_end);
> +}
> +
> +void ieee80211_txq_ps_backlog_start(struct ieee80211_local *local,
> + struct txq_info *txqi)
> +{
> + spin_lock_bh(&local->fq.lock);
> + if (txqi->tin.backlog_packets) {
> + txqi->ps_backlog_end = codel_get_time();
> + set_bit(IEEE80211_TXQ_PS_BACKLOG, &txqi->flags);
We should probably clear the codel variables here as well? Otherwise, if
CoDel was already in a dropping state when this happens, that can carry
over to after the bypass ends.
-Toke
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH 14/30] wifi: mac80211: put filtered data frames back on TXQs
2026-11-02 20:23 ` [PATCH 14/30] wifi: mac80211: put filtered data frames back on TXQs Johannes Berg
@ 2026-11-06 12:23 ` Toke Høiland-Jørgensen
0 siblings, 0 replies; 33+ messages in thread
From: Toke Høiland-Jørgensen @ 2026-11-06 12:23 UTC (permalink / raw)
To: Johannes Berg, linux-wireless; +Cc: Johannes Berg
Johannes Berg <johannes@sipsolutions.net> writes:
> From: Johannes Berg <johannes.berg@intel.com>
>
> When a device filters frames because the station went to
> sleep, we put them back on tx_filtered or pending queues.
> This can mix up ordering between TXQs and filtered frames
> when the station wakes up again later.
>
> Put filtered frames onto the frags queue of the TXQ they
> came from, fixing the order and simplifying the queueing
> structure in mac80211.
>
> Assisted-by: LLM
> Signed-off-by: Johannes Berg <johannes.berg@intel.com>
> ---
> net/mac80211/ht.c | 5 ++++
> net/mac80211/ieee80211_i.h | 5 +++-
> net/mac80211/sta_info.c | 52 +++++++++++++++++++++++++++++++++++-
> net/mac80211/sta_info.h | 4 ++-
> net/mac80211/status.c | 54 +++++++++++++++++++++++++++++++++++++-
> net/mac80211/tx.c | 37 ++++++++++++++++++++++++++
> 6 files changed, 153 insertions(+), 4 deletions(-)
>
> diff --git a/net/mac80211/ht.c b/net/mac80211/ht.c
> index c5c2113fa4f7..e26d12659942 100644
> --- a/net/mac80211/ht.c
> +++ b/net/mac80211/ht.c
> @@ -372,6 +372,11 @@ void ieee80211_ba_session_work(struct wiphy *wiphy, struct wiphy_work *work)
> /* Allow only frags to be dequeued */
> set_bit(IEEE80211_TXQ_STOP, &txqi->flags);
>
> + /*
> + * Fragments and filtered frames (which already have a
> + * sequence number) must be out before aggregation
> + * starts, otherwise they'd be sent as part of it.
> + */
> if (!skb_queue_empty(&txqi->frags)) {
> /* Fragmented Tx is ongoing, wait for it to
> * finish. Reschedule worker to retry later.
> diff --git a/net/mac80211/ieee80211_i.h b/net/mac80211/ieee80211_i.h
> index cb947341fee6..4a9b68022250 100644
> --- a/net/mac80211/ieee80211_i.h
> +++ b/net/mac80211/ieee80211_i.h
> @@ -987,7 +987,8 @@ enum txq_info_flags {
> * @tin: contains packets split into multiple flows
> * @def_cvars: codel vars for the @tin's default_flow
> * @cstats: code statistics for this queue
> - * @frags: used to keep fragments created after dequeue
> + * @frags: used to keep fragments created after dequeue, and frames that
> + * were filtered by the device (queued before the fragments)
Hmm, so rather than splicing these two together in one queue and having
all that logic to insert into the middle, and to walk the queue to check
flags, isn't it simpler to just have two distinct queues as struct
members and check them in order on dequeue?
-Toke
^ permalink raw reply [flat|nested] 33+ messages in thread
end of thread, other threads:[~2026-11-06 12:23 UTC | newest]
Thread overview: 33+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-02 20:22 [PATCH 00/30] wifi: mac80211: unify frame queueing Johannes Berg
2026-11-02 20:22 ` [PATCH 01/30] wifi: mac80211: mesh: clear PS_STA flag when peer wakes up Johannes Berg
2026-11-02 20:22 ` [PATCH 02/30] wifi: mac80211: disable BHs when waking TXQs on PS wakeup Johannes Berg
2026-11-02 20:22 ` [PATCH 03/30] wifi: mt76: correctly set more-data flag on PS release Johannes Berg
2026-11-02 20:22 ` [PATCH 04/30] wifi: mac80211: remove dead aggregation TX code Johannes Berg
2026-11-02 20:22 ` [PATCH 05/30] wifi: mac80211: remove tid_tx->pending Johannes Berg
2026-11-02 20:22 ` [PATCH 06/30] wifi: mac80211: remove aggregation queue stop reason Johannes Berg
2026-11-02 20:22 ` [PATCH 07/30] wifi: mac80211: rename vif->txq_mgmt to vif->txq_nan_dw Johannes Berg
2026-11-02 20:22 ` [PATCH 08/30] wifi: mac80211: use vlan-aware station lookup for pending TX Johannes Berg
2026-11-02 20:22 ` [PATCH 09/30] wifi: mac80211: stop tracking TXQ-buffered TIDs for PS STAs Johannes Berg
2026-11-02 20:23 ` [PATCH 10/30] wifi: mac80211: release frames from TXQs to PS stations Johannes Berg
2026-11-02 20:23 ` [PATCH 11/30] wifi: mac80211: mesh: release frames from TXQs sleeping peers Johannes Berg
2026-11-02 20:23 ` [PATCH 12/30] wifi: mac80211: don't TX from TXQs of sleeping STAs Johannes Berg
2026-11-02 20:23 ` [PATCH 13/30] wifi: mac80211: don't apply CoDel to PS-buffered frames Johannes Berg
2026-11-06 12:22 ` Toke Høiland-Jørgensen
2026-11-02 20:23 ` [PATCH 14/30] wifi: mac80211: put filtered data frames back on TXQs Johannes Berg
2026-11-06 12:23 ` Toke Høiland-Jørgensen
2026-11-02 20:23 ` [PATCH 15/30] wifi: mac80211: handle the mgmt TXQ of sleeping STAs Johannes Berg
2026-11-02 20:23 ` [PATCH 16/30] wifi: mac80211: hold frames for sleeping STAs on TXQs Johannes Berg
2026-11-02 20:23 ` [PATCH 17/30] wifi: mac80211: move ieee80211_txq_enqueue() up Johannes Berg
2026-11-02 20:23 ` [PATCH 18/30] wifi: mac80211: always use per-STA MMPDU TXQ Johannes Berg
2026-11-02 20:23 ` [PATCH 19/30] wifi: mac80211: use MMPDU TXQ for filtered mgmt frames Johannes Berg
2026-11-02 20:23 ` [PATCH 20/30] wifi: mac80211: avoid reordering PS-release MMPDUs Johannes Berg
2026-11-02 20:23 ` [PATCH 21/30] wifi: mac80211: buffer multicast frames on vif TXQ Johannes Berg
2026-11-02 20:23 ` [PATCH 22/30] wifi: mac80211: always use TXQ for mcast data frames Johannes Berg
2026-11-02 20:23 ` [PATCH 23/30] wifi: mac80211: refactor fast-xmit header encapsulation Johannes Berg
2026-11-02 20:23 ` [PATCH 24/30] wifi: mac80211: encapsulate fast-xmit frames at TXQ dequeue Johannes Berg
2026-11-02 20:23 ` [PATCH 25/30] net/fq_impl: allow pulling packets from the same flow Johannes Berg
2026-11-02 20:23 ` [PATCH 26/30] wifi: mac80211: build A-MSDUs at dequeue Johannes Berg
2026-11-02 20:23 ` [PATCH 27/30] wifi: mac80211: handle 802.11 encap at TXQ dequeue Johannes Berg
2026-11-02 20:23 ` [PATCH 28/30] wifi: mac80211: split some TX code into helpers Johannes Berg
2026-11-02 20:23 ` [PATCH 29/30] wifi: mac80211: queue unicast data frames as ethernet Johannes Berg
2026-11-02 20:23 ` [PATCH 30/30] wifi: mac80211: keep fast-xmit for sleeping STAs Johannes Berg
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox