#!/bin/sh
# halogen-fabric-clock hold | release | status: the AMD GPU's fabric clock (FCLK), held at its top level.
#
# On Strix Halo the GPU and the NPU working at the same time can hang the machine, and corrupt the NPU's results,
# while the GPU's fabric clock changes speed; held at its top level, the two run together cleanly. halogen-flash-server
# with HALOGEN_NPU_MODELS refuses to start until the clock is held. This holds it: the performance level `manual` and
# only the top pp_dpm_fclk level, on every amdgpu device that has the control (the GPU and SoC clocks stay automatic).
# Cost: about 1 W at idle. Run as root; halogen-fabric-clock.service runs `hold` at every boot and after a resume.
#
#   hold     hold it (waits up to 60 s for the GPU driver to come up)
#   release  give it back to the driver (the performance level `auto`)
#   status   print each device's state; exit 0 when every device is held
set -u
SYS="${HALOGEN_SYSFS:-/sys}"

devices() {
  for d in "$SYS"/class/drm/card*/device; do
    [ -w "$d/pp_dpm_fclk" ] || [ -r "$d/pp_dpm_fclk" ] || continue
    [ -e "$d/power_dpm_force_performance_level" ] || continue
    echo "$d"
  done | sort -u
}

held() {   # held DEVICE_DIR: the performance level high, or manual with only the top fclk level selected
  perf=$(cat "$1/power_dpm_force_performance_level" 2>/dev/null)
  [ "$perf" = high ] && return 0
  [ "$perf" = manual ] || return 1
  top=$(grep . "$1/pp_dpm_fclk" | tail -n 1)
  [ "$(grep -c '\*' "$1/pp_dpm_fclk")" = 1 ] && [ "${top%\*}" != "$top" ]
}

cmd="${1:-status}"
case "$cmd" in
  hold)
    n=0
    while [ -z "$(devices)" ] && [ "$n" -lt 60 ]; do sleep 1; n=$((n + 1)); done
    [ -n "$(devices)" ] || { echo "halogen-fabric-clock: no amdgpu device with a fabric clock control (pp_dpm_fclk) under $SYS/class/drm" >&2; exit 1; }
    rc=0
    for d in $(devices); do
      lvl=$(grep . "$d/pp_dpm_fclk" | tail -n 1 | cut -d: -f1)
      # the driver applies the level several seconds after the write, so the read-back waits up to 30 s
      ok=0
      if echo manual > "$d/power_dpm_force_performance_level" && echo "$lvl" > "$d/pp_dpm_fclk"; then
        t=0; until held "$d" || [ "$t" -ge 300 ]; do sleep 0.1; t=$((t + 1)); done
        held "$d" && ok=1
      fi
      if [ "$ok" = 1 ]; then
        echo "halogen-fabric-clock: $d: held at level $lvl ($(grep . "$d/pp_dpm_fclk" | tail -n 1 | awk '{print $2}'))"
      else
        echo "halogen-fabric-clock: $d: could not hold it (level $lvl)" >&2; rc=1
      fi
    done
    exit "$rc" ;;
  release)
    for d in $(devices); do echo auto > "$d/power_dpm_force_performance_level" && echo "halogen-fabric-clock: $d: released (auto)"; done ;;
  status)
    [ -n "$(devices)" ] || { echo "halogen-fabric-clock: no amdgpu device with a fabric clock control"; exit 1; }
    rc=0
    for d in $(devices); do
      if held "$d"; then echo "$d: held ($(cat "$d/power_dpm_force_performance_level"))"
      else echo "$d: NOT held ($(cat "$d/power_dpm_force_performance_level"))"; rc=1; fi
    done
    exit "$rc" ;;
  *) echo "usage: halogen-fabric-clock hold | release | status" >&2; exit 2 ;;
esac
