root/drivers/net/wireless/intel/iwlwifi/mld/low_latency.c
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/*
 * Copyright (C) 2024-2026 Intel Corporation
 */
#include "mld.h"
#include "iface.h"
#include "low_latency.h"
#include "hcmd.h"
#include "power.h"
#include "mlo.h"

#define MLD_LL_WK_INTERVAL_MSEC 500
#define MLD_LL_PERIOD (HZ * MLD_LL_WK_INTERVAL_MSEC / 1000)
#define MLD_LL_ACTIVE_WK_PERIOD (HZ * 10)

/* packets/MLD_LL_WK_PERIOD seconds */
#define MLD_LL_ENABLE_THRESH 100

static bool iwl_mld_calc_low_latency(struct iwl_mld *mld,
                                     unsigned long timestamp)
{
        struct iwl_mld_low_latency *ll = &mld->low_latency;
        bool global_low_latency = false;
        u8 num_rx_q = mld->trans->info.num_rxqs;

        for (int mac_id = 0; mac_id < NUM_MAC_INDEX_DRIVER; mac_id++) {
                u32 total_vo_vi_pkts = 0;
                bool ll_period_expired;

                /* If it's not initialized yet, it means we have not yet
                 * received/transmitted any vo/vi packet on this MAC.
                 */
                if (!ll->window_start[mac_id])
                        continue;

                ll_period_expired =
                        time_after(timestamp, ll->window_start[mac_id] +
                                   MLD_LL_ACTIVE_WK_PERIOD);

                if (ll_period_expired)
                        ll->window_start[mac_id] = timestamp;

                for (int q = 0; q < num_rx_q; q++) {
                        struct iwl_mld_low_latency_packets_counters *counters =
                                &mld->low_latency.pkts_counters[q];

                        spin_lock_bh(&counters->lock);

                        total_vo_vi_pkts += counters->vo_vi[mac_id];

                        if (ll_period_expired)
                                counters->vo_vi[mac_id] = 0;

                        spin_unlock_bh(&counters->lock);
                }

                /* enable immediately with enough packets but defer
                 * disabling only if the low-latency period expired and
                 * below threshold.
                 */
                if (total_vo_vi_pkts > MLD_LL_ENABLE_THRESH)
                        mld->low_latency.result[mac_id] = true;
                else if (ll_period_expired)
                        mld->low_latency.result[mac_id] = false;

                global_low_latency |= mld->low_latency.result[mac_id];
        }

        return global_low_latency;
}

static void iwl_mld_low_latency_iter(void *_data, u8 *mac,
                                     struct ieee80211_vif *vif)
{
        struct iwl_mld *mld = _data;
        struct iwl_mld_vif *mld_vif = iwl_mld_vif_from_mac80211(vif);
        bool prev = mld_vif->low_latency_causes & LOW_LATENCY_TRAFFIC;
        bool low_latency;

        if (!iwl_mld_vif_fw_id_valid(mld_vif))
                return;

        BUILD_BUG_ON(ARRAY_SIZE(mld->fw_id_to_vif) !=
                     ARRAY_SIZE(mld->low_latency.result));

        low_latency = mld->low_latency.result[mld_vif->fw_id];

        if (prev != low_latency)
                iwl_mld_vif_update_low_latency(mld, vif, low_latency,
                                               LOW_LATENCY_TRAFFIC);
}

static void iwl_mld_low_latency_wk(struct wiphy *wiphy, struct wiphy_work *wk)
{
        struct iwl_mld *mld = container_of(wk, struct iwl_mld,
                                           low_latency.work.work);
        unsigned long timestamp = jiffies;
        bool low_latency_active;

        if (mld->fw_status.in_hw_restart)
                return;

        /* It is assumed that the work was scheduled only after checking
         * at least MLD_LL_PERIOD has passed since the last update.
         */

        low_latency_active = iwl_mld_calc_low_latency(mld, timestamp);

        /* Update the timestamp now after the low-latency calculation */
        mld->low_latency.timestamp = timestamp;

        /* If low-latency is active we need to force re-evaluation after
         * 10 seconds, so that we can disable low-latency when
         * the low-latency traffic ends.
         *
         * Otherwise, we don't need to run the work because there is nothing to
         * disable.
         *
         * Note that this has no impact on the regular scheduling of the
         * updates triggered by traffic - those happen whenever the
         * MLD_LL_PERIOD timeout expire.
         */
        if (low_latency_active)
                wiphy_delayed_work_queue(mld->wiphy, &mld->low_latency.work,
                                         MLD_LL_ACTIVE_WK_PERIOD);

        ieee80211_iterate_active_interfaces_mtx(mld->hw,
                                                IEEE80211_IFACE_ITER_NORMAL,
                                                iwl_mld_low_latency_iter, mld);
}

int iwl_mld_low_latency_init(struct iwl_mld *mld)
{
        struct iwl_mld_low_latency *ll = &mld->low_latency;
        unsigned long ts = jiffies;

        ll->pkts_counters = kzalloc_objs(*ll->pkts_counters,
                                         mld->trans->info.num_rxqs);
        if (!ll->pkts_counters)
                return -ENOMEM;

        for (int q = 0; q < mld->trans->info.num_rxqs; q++)
                spin_lock_init(&ll->pkts_counters[q].lock);

        wiphy_delayed_work_init(&ll->work, iwl_mld_low_latency_wk);

        ll->timestamp = ts;

        /* The low-latency window_start will be initialized per-MAC on
         * the first vo/vi packet received/transmitted.
         */

        return 0;
}

void iwl_mld_low_latency_free(struct iwl_mld *mld)
{
        struct iwl_mld_low_latency *ll = &mld->low_latency;

        kfree(ll->pkts_counters);
        ll->pkts_counters = NULL;
}

void iwl_mld_low_latency_restart_cleanup(struct iwl_mld *mld)
{
        struct iwl_mld_low_latency *ll = &mld->low_latency;

        ll->timestamp = jiffies;

        memset(ll->window_start, 0, sizeof(ll->window_start));
        memset(ll->result, 0, sizeof(ll->result));

        for (int q = 0; q < mld->trans->info.num_rxqs; q++)
                memset(ll->pkts_counters[q].vo_vi, 0,
                       sizeof(ll->pkts_counters[q].vo_vi));
}

static int iwl_mld_send_low_latency_cmd(struct iwl_mld *mld, bool low_latency,
                                        u16 mac_id)
{
        struct iwl_mac_low_latency_cmd cmd = {
                .mac_id = cpu_to_le32(mac_id)
        };
        u16 cmd_id = WIDE_ID(MAC_CONF_GROUP, LOW_LATENCY_CMD);
        int ret;

        if (low_latency) {
                /* Currently we don't care about the direction */
                cmd.low_latency_rx = 1;
                cmd.low_latency_tx = 1;
        }

        ret = iwl_mld_send_cmd_pdu(mld, cmd_id, &cmd);
        if (ret)
                IWL_ERR(mld, "Failed to send low latency command\n");

        return ret;
}

static void iwl_mld_vif_set_low_latency(struct iwl_mld_vif *mld_vif, bool set,
                                        enum iwl_mld_low_latency_cause cause)
{
        if (set)
                mld_vif->low_latency_causes |= cause;
        else
                mld_vif->low_latency_causes &= ~cause;
}

void iwl_mld_vif_update_low_latency(struct iwl_mld *mld,
                                    struct ieee80211_vif *vif,
                                    bool low_latency,
                                    enum iwl_mld_low_latency_cause cause)
{
        struct iwl_mld_vif *mld_vif = iwl_mld_vif_from_mac80211(vif);
        bool prev;

        prev = iwl_mld_vif_low_latency(mld_vif);
        iwl_mld_vif_set_low_latency(mld_vif, low_latency, cause);

        low_latency = iwl_mld_vif_low_latency(mld_vif);
        if (low_latency == prev)
                return;

        if (iwl_mld_send_low_latency_cmd(mld, low_latency, mld_vif->fw_id)) {
                /* revert to previous low-latency state */
                iwl_mld_vif_set_low_latency(mld_vif, prev, cause);
                return;
        }

        if (ieee80211_vif_type_p2p(vif) != NL80211_IFTYPE_P2P_CLIENT)
                return;

        iwl_mld_update_mac_power(mld, vif, false);

        if (low_latency)
                iwl_mld_retry_emlsr(mld, vif);
}

static bool iwl_mld_is_vo_vi_pkt(struct ieee80211_hdr *hdr)
{
        u8 tid;
        static const u8 tid_to_mac80211_ac[] = {
                IEEE80211_AC_BE,
                IEEE80211_AC_BK,
                IEEE80211_AC_BK,
                IEEE80211_AC_BE,
                IEEE80211_AC_VI,
                IEEE80211_AC_VI,
                IEEE80211_AC_VO,
                IEEE80211_AC_VO,
        };

        if (!hdr || !ieee80211_is_data_qos(hdr->frame_control))
                return false;

        tid = ieee80211_get_tid(hdr);
        if (tid >= IWL_MAX_TID_COUNT)
                return false;

        return tid_to_mac80211_ac[tid] < IEEE80211_AC_VI;
}

void iwl_mld_low_latency_update_counters(struct iwl_mld *mld,
                                         struct ieee80211_hdr *hdr,
                                         struct ieee80211_sta *sta,
                                         u8 queue)
{
        struct iwl_mld_sta *mld_sta = iwl_mld_sta_from_mac80211(sta);
        struct iwl_mld_vif *mld_vif = iwl_mld_vif_from_mac80211(mld_sta->vif);
        struct iwl_mld_low_latency_packets_counters *counters;
        unsigned long ts = jiffies ? jiffies : 1;
        u8 fw_id = mld_vif->fw_id;

        /* we should have failed op mode init if NULL */
        if (WARN_ON_ONCE(!mld->low_latency.pkts_counters))
                return;

        if (!iwl_mld_vif_fw_id_valid(mld_vif))
                return;

        if (WARN_ON_ONCE(queue >= mld->trans->info.num_rxqs))
                return;

        if (mld->low_latency.stopped)
                return;

        if (!iwl_mld_is_vo_vi_pkt(hdr))
                return;

        counters = &mld->low_latency.pkts_counters[queue];

        spin_lock_bh(&counters->lock);
        counters->vo_vi[fw_id]++;
        spin_unlock_bh(&counters->lock);

        /* Initialize the window_start on the first vo/vi packet */
        if (!mld->low_latency.window_start[fw_id])
                mld->low_latency.window_start[fw_id] = ts;

        if (time_is_before_jiffies(mld->low_latency.timestamp + MLD_LL_PERIOD))
                wiphy_delayed_work_queue(mld->wiphy, &mld->low_latency.work,
                                         0);
}

void iwl_mld_low_latency_stop(struct iwl_mld *mld)
{
        lockdep_assert_wiphy(mld->wiphy);

        mld->low_latency.stopped = true;

        wiphy_delayed_work_cancel(mld->wiphy, &mld->low_latency.work);
}

void iwl_mld_low_latency_restart(struct iwl_mld *mld)
{
        struct iwl_mld_low_latency *ll = &mld->low_latency;
        bool low_latency = false;
        unsigned long ts = jiffies;

        lockdep_assert_wiphy(mld->wiphy);

        ll->timestamp = ts;
        mld->low_latency.stopped = false;

        for (int mac = 0; mac < NUM_MAC_INDEX_DRIVER; mac++) {
                ll->window_start[mac] = 0;
                low_latency |= ll->result[mac];

                for (int q = 0; q < mld->trans->info.num_rxqs; q++) {
                        spin_lock_bh(&ll->pkts_counters[q].lock);
                        ll->pkts_counters[q].vo_vi[mac] = 0;
                        spin_unlock_bh(&ll->pkts_counters[q].lock);
                }
        }

        /* if low latency is active, force re-evaluation to cover the case of
         * no traffic.
         */
        if (low_latency)
                wiphy_delayed_work_queue(mld->wiphy, &ll->work, MLD_LL_PERIOD);
}