#!/usr/bin/env bash
# Phase B (low risk): steer USB (XR/HAT) IRQs to EG core; net softirq preference to NET core.
# Best-effort — never fails the boot path. Safe to re-run (idempotent preferences).
#
#   sudo bash apply_irq_affinity.sh
#   sudo OHD_IRQ_AFFINITY=0 bash apply_irq_affinity.sh   # no-op off switch

if grep -q $'\r' "${BASH_SOURCE[0]}" 2>/dev/null; then
  sed -i 's/\r$//' "${BASH_SOURCE[0]}"
  exec bash "${BASH_SOURCE[0]}" "$@"
fi

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=cpu_layout.sh
source "${SCRIPT_DIR}/cpu_layout.sh"

LOG_TAG="ohd-irq-affinity"

log() { echo "${LOG_TAG}: $*" >&2; }

if [[ "${EUID}" -ne 0 ]]; then
  log "FATAL: run as root (sudo)"
  exit 1
fi

if [[ "${OHD_IRQ_AFFINITY:-1}" != "1" ]]; then
  log "OHD_IRQ_AFFINITY!=1 — skip"
  exit 0
fi

if ! ohd_cpu_lanes_ok; then
  log "skip (OHD_CPU_ENABLE=${OHD_CPU_ENABLE} nproc=$(ohd_cpu_count) need>=4)"
  exit 0
fi

EG_CORE="${OHD_CPU_EG}"
NET_CORE="${OHD_CPU_NET}"
NET_MASK="$(ohd_cpu_bitmask_hex "${NET_CORE}")"
EG_MASK="$(ohd_cpu_bitmask_hex "${EG_CORE}")"

pin_irq_list() {
  local irq="$1" core="$2"
  local path="/proc/irq/${irq}/smp_affinity_list"
  if [[ ! -w "${path}" ]]; then
    return 0
  fi
  if echo "${core}" >"${path}" 2>/dev/null; then
    log "irq ${irq} → cpu${core}"
  fi
}

# Match /proc/interrupts names for USB host (Pi5 xHCI etc.) → EG core.
# Avoid graphics (v3d/vc4) so DF keeps its GPU path free.
while read -r line; do
  irq="${line%%:*}"
  irq="${irq// /}"
  [[ "${irq}" =~ ^[0-9]+$ ]] || continue
  lower="$(printf '%s' "${line}" | tr '[:upper:]' '[:lower:]')"
  case "${lower}" in
    *xhci*|*dwc3*|*dwc_otg*|*usb*|*uas*)
      # Skip pure graphics USB display paths if named oddly — xhci is HAT.
      pin_irq_list "${irq}" "${EG_CORE}"
      ;;
    *eth0*|*eth1*|*lan78*|*macb*|*stmmac*|*gmac*|*brcmfmac*|*wlan*|*mmc*)
      # Net / wifi / mmc often share — prefer NET core (best-effort).
      pin_irq_list "${irq}" "${NET_CORE}"
      ;;
  esac
done </proc/interrupts

# Device MSI IRQs for eth under /sys (more reliable than name parse alone).
for iface in eth0 eth1; do
  base="/sys/class/net/${iface}/device"
  [[ -d "${base}" ]] || continue
  if [[ -d "${base}/msi_irqs" ]]; then
    for irqf in "${base}"/msi_irqs/*; do
      [[ -e "${irqf}" ]] || continue
      pin_irq_list "$(basename "${irqf}")" "${NET_CORE}"
    done
  fi
done

# RPS: process inbound packets on NET core when driver supports it.
for iface in /sys/class/net/*; do
  name="$(basename "${iface}")"
  case "${name}" in
    eth*|wlan*|gretap*|wg*|br*)
      for q in "${iface}"/queues/rx-*; do
        [[ -d "${q}" ]] || continue
        rps="${q}/rps_cpus"
        [[ -w "${rps}" ]] || continue
        if echo "${NET_MASK}" >"${rps}" 2>/dev/null; then
          log "rps ${name}/$(basename "${q}") mask=0x${NET_MASK} (cpu${NET_CORE})"
        fi
      done
      ;;
  esac
done

# Also expose EG mask for operators reading journal (not a second pin).
log "layout os=${OHD_CPU_OS} net=${NET_CORE}(rps=0x${NET_MASK}) df=${OHD_CPU_DF} eg=${EG_CORE}(usb prefer 0x${EG_MASK})"
exit 0
