#!/bin/sh
# tlp-func-cpu - Processor Functions
#
# Copyright (c) 2022 Thomas Koch <linrunner at gmx.net> and others.
# This software is licensed under the GPL v2 or later.

# Needs: tlp-func-base

# shellcheck disable=SC2086

# ----------------------------------------------------------------------------
# Constants

readonly CPUD=/sys/devices/system/cpu
readonly CPU_BOOST_ALL_CTRL=${CPUD}/cpufreq/boost
readonly INTEL_PSTATED=${CPUD}/intel_pstate
readonly CPU_MIN_PERF_PCT=$INTEL_PSTATED/min_perf_pct
readonly CPU_MAX_PERF_PCT=$INTEL_PSTATED/max_perf_pct
readonly CPU_TURBO_PSTATE=$INTEL_PSTATED/no_turbo
readonly CPU_HWP_DYN_BOOST=$INTEL_PSTATED/hwp_dynamic_boost

readonly CPU_INFO=/proc/cpuinfo
readonly ENERGYPERF=x86_energy_perf_policy

readonly FWACPID=/sys/firmware/acpi

# ----------------------------------------------------------------------------
# Functions

# --- Scaling Governor

check_intel_pstate () {
    # detect intel_pstate driver -- rc: 0=present/1=absent
    # note: intel_pstate requires Linux 3.9 or higher
    [ -d $INTEL_PSTATED ]
}

set_cpu_scaling_governor () {
    # set CPU scaling governor -- $1: 0=ac mode, 1=battery mode
    local available_govs cfg cpu ec gov

    if [ "$1" = "1" ]; then
        gov=${CPU_SCALING_GOVERNOR_ON_BAT:-}
        cfg="CPU_SCALING_GOVERNOR_ON_BAT"
    else
        gov=${CPU_SCALING_GOVERNOR_ON_AC:-}
        cfg="CPU_SCALING_GOVERNOR_ON_AC"
    fi

    if [ -n "$gov" ]; then
        # check if configured governor is available
        available_govs="$(read_sysf "${CPUD}"/cpu0/cpufreq/scaling_available_governors)"
        if [ -n "$available_govs" ] && ! wordinlist "$gov" "$available_govs"; then
            echo_debug "pm" "set_cpu_scaling_governor($1).not_available: $gov; available=$available_govs"
            echo_message "Error in configuration at ${cfg}=\"${gov}\": governor not available. Skipped."
            return 1
        fi

        # apply governor
        ec=0
        for cpu in "${CPUD}"/cpu*/cpufreq/scaling_governor; do
            if [ -f "$cpu" ] && ! write_sysf "$gov" $cpu; then
                echo_debug "pm" "set_cpu_scaling_governor($1).write_error: $cpu $gov; rc=$?"
                ec=$((ec+1))
            fi
        done
        echo_debug "pm" "set_cpu_scaling_governor($1): $gov; ec=$ec"
    else
        echo_debug "pm" "set_cpu_scaling_governor($1).not_configured"
    fi

    return 0
}

set_cpu_scaling_min_max_freq () {
    # set CPU scaling limits -- $1: 0=ac mode, 1=battery mode
    local minfreq maxfreq cpu ec
    local conf=0

    if [ "$1" = "1" ]; then
        minfreq=${CPU_SCALING_MIN_FREQ_ON_BAT:-}
        maxfreq=${CPU_SCALING_MAX_FREQ_ON_BAT:-}
    else
        minfreq=${CPU_SCALING_MIN_FREQ_ON_AC:-}
        maxfreq=${CPU_SCALING_MAX_FREQ_ON_AC:-}
    fi

    if [ -n "$minfreq" ] && [ "$minfreq" != "0" ]; then
        conf=1
        ec=0
        for cpu in "${CPUD}"/cpu*/cpufreq/scaling_min_freq; do
            if [ -f "$cpu" ] && ! write_sysf "$minfreq" $cpu; then
                echo_debug "pm" "set_cpu_scaling_min_max_freq($1).min.write_error: $cpu $minfreq; rc=$?"
                ec=$((ec+1))
            fi
        done
        echo_debug "pm" "set_cpu_scaling_min_max_freq($1).min: $minfreq; ec=$ec"
    fi

    if [ -n "$maxfreq" ] && [ "$maxfreq" != "0" ]; then
        conf=1
        ec=0
        for cpu in "${CPUD}"/cpu*/cpufreq/scaling_max_freq; do
            if [ -f "$cpu" ] && ! write_sysf "$maxfreq" $cpu; then
                echo_debug "pm" "set_cpu_scaling_min_max_freq($1).max.write_error: $cpu $maxfreq; rc=$?"
                ec=$((ec+1))
            fi
            echo_debug "pm" "set_cpu_scaling_min_max_freq($1).max: $maxfreq; ec=$ec"
        done
    fi

    [ $conf -eq 1 ] || echo_debug "pm" "set_cpu_scaling_min_max_freq($1).not_configured"
    return 0
}

# --- Performance Policies

supports_intel_cpu_epb () {
    # rc: 0=CPU supports EPB/1=false
    grep -E -q -m 1 '^flags.+epb' $CPU_INFO
}

supports_intel_cpu_epp () {
    # rc: 0=CPU supports HWP.EPP/1=false
    grep -E -q -m 1 '^flags.+hwp_epp' $CPU_INFO
}

set_intel_cpu_perf_policy () {
    # set Intel CPU energy vs. performance policies
    # $1: 0=ac mode, 1=battery mode
    #
    # depending on the CPU model the values
    #   performance, balance_performance, default, balance_power, power
    # in CPU_ENERGY_PERF_POLICY_ON_AC/BAT are applied to:
    # (1) energy-performance preference (HWP.EPP) in MSR_IA32_HWP_REQUEST
    # (2) energy-performance bias (EPB) in MSR_IA32_ENERGY_PERF_BIAS
    # when HWP.EPP is available, EPB is not set (unless forced)
    #
    # References:
    # - https://www.kernel.org/doc/html/latest/admin-guide/pm/intel_pstate.html#energy-vs-performance-hints
    # - https://www.kernel.org/doc/html/latest/admin-guide/pm/intel_epb.html
    # - https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4beec1d7519691b4b6c6b764e75b4e694a09c5f7
    # - http://manpages.ubuntu.com/manpages/disco/man8/x86_energy_perf_policy.8.html

    local cnt cpuf ec perf pnum

    if [ "$1" = "1" ]; then
        perf=${CPU_ENERGY_PERF_POLICY_ON_BAT:-}
    else
        perf=${CPU_ENERGY_PERF_POLICY_ON_AC:-}
    fi
    # translate alphanumeric values from EPB to HWP.EPP syntax
    case "$perf" in
        'balance-performance')  perf='balance_performance' ;;
        'normal')               perf='default' ;;
        'balance-power')        perf='balance_power' ;;
        'powersave')            perf='power' ;;
    esac

    if [ -z "$perf" ]; then
        echo_debug "pm" "set_intel_cpu_perf_policy($1).not_configured"
        return 0
    fi

    if check_intel_pstate && supports_intel_cpu_epp; then
        # validate HWP.EPP setting
        case "$perf" in
            performance|balance_performance|default|balance_power|power) # OK --> continue
                ;;

            [0-9]|[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]) # HWP.EPP accepts 0 to 255 --> continue
                ;;

            *) # invalid setting
                echo_debug "pm" "set_intel_cpu_perf_policy($1).hwp_epp.invalid: perf=$perf"
                return 0
                ;;
        esac

        # apply HWP.EPP setting
        cnt=0
        ec=0
        for cpuf in "${CPUD}"/cpu*/cpufreq/energy_performance_preference; do
            if [ -f $cpuf ]; then
                cnt=$((cnt+1))
                if  ! write_sysf "$perf" $cpuf; then
                    echo_debug "pm" "set_intel_cpu_perf_policy($1).hwp_epp.write_error: $perf $cpuf; rc=$?"
                    ec=$((ec+1))
                fi
            fi
        done
        if [ $cnt -gt 0 ]; then
            echo_debug "pm" "set_intel_cpu_perf_policy($1).hwp_epp: $perf; ec=$ec"
            # HWP.EPP active and applied, quit unless EPB is forced
            [ "$X_FORCE_EPB" = "1" ] || return 0
        else
            echo_debug "pm" "set_intel_cpu_perf_policy($1).hwp_epp.not_available"
        fi
    else
        echo_debug "pm" "set_intel_cpu_perf_policy($1).hwp_epp.unsupported_cpu"
    fi

    if supports_intel_cpu_epb; then
        # translate HWP.EPP alphanumeric to EPB numeric values for native kernel support
        # and x86_energy_perf_policy backwards compatibility; validate numeric values
        case "$perf" in
            performance)         pnum=0 ;;
            balance_performance) pnum=4 ;;
            default)             pnum=6 ;;
            balance_power)       pnum=8 ;;
            power)               pnum=15 ;;

            0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15) pnum="$perf" ;;

            *) # invalid setting
                echo_debug "pm" "set_intel_cpu_perf_policy($1).epb.invalid: perf=$perf"
                return 0
                ;;
        esac

        # apply EPB setting
        # try native kernel API first (5.2 and later)
        cnt=0
        ec=0
        for cpuf in "${CPUD}"/cpu*/power/energy_perf_bias; do
            if [ -f $cpuf ]; then
                cnt=$((cnt + 1))
                if  ! write_sysf "$pnum" $cpuf; then
                    echo_debug "pm" "set_intel_cpu_perf_policy($1).epb_kernel.write_error: $perf($pnum) $cpuf; rc=$?"
                    ec=$((ec+1))
                fi
            fi
        done
        if [ $cnt -gt 0 ]; then
            # native kernel API actually detected
            echo_debug "pm" "set_intel_cpu_perf_policy($1).epb_kernel: $perf($pnum); ec=$ec"
            return 0
        fi

        # no native kernel support, try x86_energy_perf_policy
        if ! cmd_exists $ENERGYPERF; then
            # x86_energy_perf_policy not installed
            echo_debug "pm" "set_intel_cpu_perf_policy($1).epb_x: $perf($pnum); x86_energy_perf_policy not installed"
        else
            # x86_energy_perf_policy needs kernel module 'msr'
            load_modules $MOD_MSR
            $ENERGYPERF $pnum > /dev/null 2>&1
            rc=$?
            case $rc in
                0) echo_debug "pm" "set_intel_cpu_perf_policy($1).epb_x: $perf($pnum); rc=$?" ;;
                1) echo_debug "pm" "set_intel_cpu_perf_policy($1).epb_x: $perf($pnum); rc=$? (unsupported cpu)" ;;
                2) echo_debug "pm" "set_intel_cpu_perf_policy($1).epb_x: $perf($pnum); rc=$? (kernel specific x86_energy_perf_policy missing)" ;;
                *) echo_debug "pm" "set_intel_cpu_perf_policy($1).epb_x: $perf($pnum); rc=$? (unknown)" ;;
            esac
            return $rc
        fi
    else
        echo_debug "pm" "set_intel_cpu_perf_policy($1).epb.unsupported_cpu"
    fi

    return 0
}

set_intel_cpu_perf_pct () {
    # set Intel P-state performance limits
    # $1: 0=ac mode, 1=battery mode
    local min max


    if ! check_intel_pstate; then
        echo_debug "pm" "set_intel_cpu_perf_pct($1).no_intel_pstate"
        return 0
    fi

    if [ "$1" = "1" ]; then
        min=${CPU_MIN_PERF_ON_BAT:-}
        max=${CPU_MAX_PERF_ON_BAT:-}
    else
        min=${CPU_MIN_PERF_ON_AC:-}
        max=${CPU_MAX_PERF_ON_AC:-}
    fi

    if [ ! -f $CPU_MIN_PERF_PCT ]; then
        echo_debug "pm" "set_intel_cpu_perf_pct($1).min.not_supported"
    elif [ -n "$min" ]; then
        write_sysf "$min" $CPU_MIN_PERF_PCT
        echo_debug "pm" "set_intel_cpu_perf_pct($1).min: $min; rc=$?"
    else
        echo_debug "pm" "set_intel_cpu_perf_pct($1).min.not_configured"
    fi

    if [ ! -f $CPU_MAX_PERF_PCT ]; then
        echo_debug "pm" "set_intel_cpu_perf_pct($1).max.not_supported"
    elif [ -n "$max" ]; then
        write_sysf "$max" $CPU_MAX_PERF_PCT
        echo_debug "pm" "set_intel_cpu_perf_pct($1).max: $max; rc=$?"
    else
        echo_debug "pm" "set_intel_cpu_perf_pct($1).max.not_configured"
    fi

    return 0
}

set_cpu_boost_all () { # $1: 0=ac mode, 1=battery mode
    # global cpu boost behavior control based on the current power mode
    #
    # Relevant config option(s): CPU_BOOST_ON_{AC,BAT} with values {'',0,1}
    #
    # Note:
    #  * needs commit #615b7300717b9ad5c23d1f391843484fe30f6c12
    #     (linux-2.6 tree), "Add support for disabling dynamic overclocking",
    #    => requires Linux 3.7 or later

    local val

    if [ "$1" = "1" ]; then
        val=${CPU_BOOST_ON_BAT:-}
    else
        val=${CPU_BOOST_ON_AC:-}
    fi

    if [ -z "$val" ]; then
        # do nothing if unconfigured
        echo_debug "pm" "set_cpu_boost_all($1).not_configured"
        return 0
    fi

    if check_intel_pstate; then
        # use intel_pstate sysfiles, invert value
        if write_sysf "$((val ^ 1))" $CPU_TURBO_PSTATE; then
            echo_debug "pm" "set_cpu_boost_all($1).intel_pstate: $val"
        else
            echo_debug "pm" "set_cpu_boost_all($1).intel_pstate.cpu_not_supported"
        fi
    elif [ -f $CPU_BOOST_ALL_CTRL ]; then
        # use acpi_cpufreq sysfiles
        # simple test for attribute "w" doesn't work, so actually write
        if write_sysf "$val" $CPU_BOOST_ALL_CTRL; then
            echo_debug "pm" "set_cpu_boost_all($1).acpi_cpufreq: $val"
        else
            echo_debug "pm" "set_cpu_boost_all($1).acpi_cpufreq.cpu_not_supported"
        fi
    else
        echo_debug "pm" "set_cpu_boost_all($1).not_available"
    fi

    return 0
}

set_intel_cpu_hwp_dyn_boost () {
    # set the Intel CPU dynamic boost feature (available in HWP active mode)
    # $1: 0=ac mode, 1=battery mode
    local val

    if ! check_intel_pstate; then
        echo_debug "pm" "set_intel_cpu_hwp_dyn_boost($1).no_intel_pstate"
        return 0
    fi

    if [ "$1" = "1" ]; then
        val=${CPU_HWP_DYN_BOOST_ON_BAT:-}
    else
        val=${CPU_HWP_DYN_BOOST_ON_AC:-}
    fi

    if [ ! -f $CPU_HWP_DYN_BOOST ]; then
        echo_debug "pm" "set_intel_cpu_hwp_dyn_boost($1).not_supported"
    elif [ -n "$val" ]; then
        write_sysf "$val" $CPU_HWP_DYN_BOOST
        echo_debug "pm" "set_intel_cpu_hwp_dyn_boost($1): $val; rc=$?"
    else
        echo_debug "pm" "set_intel_cpu_hwp_dyn_boost($1).not_configured"
    fi

    return 0
}
set_sched_powersave () { # set multi-core/-thread powersave policy
    # $1: 0=ac mode, 1=battery mode

    local pwr pool sdev
    local avail=0

    if [ "$1" = "1" ]; then
        pwr=${SCHED_POWERSAVE_ON_BAT:-}
    else
        pwr=${SCHED_POWERSAVE_ON_AC:-}
    fi

    if [ -z "$pwr" ]; then
        # do nothing if unconfigured
        echo_debug "pm" "set_sched_powersave($1).not_configured"
        return 0
    fi

    for pool in mc smp smt; do
        sdev="${CPUD}/sched_${pool}_power_savings"
        if [ -f "$sdev" ]; then
            write_sysf "$pwr" $sdev
            echo_debug "pm" "set_sched_powersave($1): ${sdev##/*/} $pwr; rc=$?"
            avail=1
        fi
    done

    [ "$avail" = "1" ] || echo_debug "pm" "set_sched_powersave($1).not_available"

    return 0
}

# --- Misc

set_nmi_watchdog () { # enable/disable nmi watchdog
    local nmiwd=${NMI_WATCHDOG:-}

    if [ -z "$nmiwd" ]; then
        # do nothing if unconfigured
        echo_debug "pm" "set_nmi_watchdog.not_configured"
        return 0
    fi

    if [ -f /proc/sys/kernel/nmi_watchdog ]; then
        if write_sysf "$nmiwd" /proc/sys/kernel/nmi_watchdog; then
            echo_debug "pm" "set_nmi_watchdog: $nmiwd; rc=$?"
        else
            echo_debug "pm" "set_nmi_watchdog.disabled_by_kernel: $nmiwd"
        fi
    else
        echo_debug "pm" "set_nmi_watchdog.not_available"
    fi

    return 0
}

# --- Platform

set_platform_profile () { # set platform profile
    # $1: 0=ac mode, 1=battery mode

    local pwr

    if [ "$1" = "1" ]; then
        pwr=${PLATFORM_PROFILE_ON_BAT:-}
    else
        pwr=${PLATFORM_PROFILE_ON_AC:-}
    fi

    if [ -z "$pwr" ]; then
        # do nothing if unconfigured
        echo_debug "pm" "set_platform_profile($1).not_configured"
        return 0
    fi

    if [ -f $FWACPID/platform_profile ]; then
        if write_sysf "$pwr" $FWACPID/platform_profile; then
            echo_debug "pm" "set_platform_profile($1): $pwr"
        else
            echo_debug "pm" "set_platform_profile($1).write_error"
        fi
    else
        echo_debug "pm" "set_platform_profile($1).not_available"
    fi

    return 0
}
