#!/usr/bin/env bash # # mangohud-logger — toggle MangoHud CSV logging on/off # # Just run it: if logging is off it gets switched on, if it's already on # it gets switched off. # # What it manages on enable: # 1. ~/.config/MangoHud/MangoHud.conf # → appends a marker block with output_folder, autostart_log, etc. # → keeps your existing keys; overrides `no_display` while logging # (autostart_log rides on the render hook that no_display disables). # → pins gpu_list to the GPU actually rendering games (see below), so the # gpu_* columns log real data instead of an idle second GPU. # 2. ~/.config/environment.d/95-mangohud-logger.conf # → contains MANGOHUD=1 so the Vulkan layer auto-loads into every # game (gamescope sessions, Steam, native Vulkan apps). # Picked up by systemd-user at next session login. # # GPU selection (vendor-agnostic): # MangoHud logs stats for ONE GPU. On a hybrid box (an AMD/Intel iGPU beside a # discrete NVIDIA/AMD card) its default often picks the idle iGPU, so every # gpu_* column reads ~0. We auto-detect the render node that's the actual # gaming GPU — any vendor — and pin `gpu_list` to its index. Override with # MANGOHUD_LOGGER_GPU= or =nvidia|amd|intel if the guess is wrong. # # Logs land in the user's Downloads folder under "mango-logs" # (honours XDG_DOWNLOAD_DIR from ~/.config/user-dirs.dirs, else ~/Downloads). set -euo pipefail readonly MARKER_BEGIN="# >>> mangohud-logger BEGIN >>>" readonly MARKER_END="# <<< mangohud-logger END <<<" CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/MangoHud" CONFIG_FILE="$CONFIG_DIR/MangoHud.conf" ENV_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/environment.d" ENV_FILE="$ENV_DIR/95-mangohud-logger.conf" GS_SESSION_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/gamescope-session-plus/sessions.d" GS_SESSION_FILE="$GS_SESSION_DIR/steam" GS_OWNED_MARKER="# Written by mangohud-logger." # Detect which gamescope (Game Mode) session manager is in use — this decides # whether our env override is actually sourced at session start: # plus → gamescope-session-plus sources ~/.config/gamescope-session-plus/ # sessions.d/* after its own steam script, so our override works. # steamos → /usr/lib/steamos/gamescope-session hard-exports # STEAM_USE_MANGOAPP=1 and MANGOHUD_CONFIGFILE=/no_display, and # sources NO user file. A global override is therefore impossible; # logging requires per-game Steam launch options instead. # none → no gamescope session found (desktop-only machine). detect_gs_flavor() { if [[ -e /usr/share/gamescope-session-plus/sessions.d/steam ]]; then printf 'plus\n' elif [[ -e /usr/lib/steamos/gamescope-session ]]; then printf 'steamos\n' else printf 'none\n' fi } GS_FLAVOR="$(detect_gs_flavor)" # The per-game launch-option string that makes logging work on the steamos # flavor (repoints MANGOHUD_CONFIGFILE back at our config so autostart_log fires). LAUNCH_OPT="MANGOHUD_CONFIGFILE=$CONFIG_FILE mangohud %command%" # Resolve Downloads via xdg-user-dir if available, else the user-dirs.dirs file, # else fall back to ~/Downloads. This makes the script work on non-English locales. if command -v xdg-user-dir >/dev/null 2>&1; then DOWNLOADS_DIR="$(xdg-user-dir DOWNLOAD)" elif [[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs" ]]; then # shellcheck disable=SC1090,SC1091 source "${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs" DOWNLOADS_DIR="${XDG_DOWNLOAD_DIR:-$HOME/Downloads}" else DOWNLOADS_DIR="$HOME/Downloads" fi LOG_DIR="$DOWNLOADS_DIR/mango-logs" err() { printf '\033[31merror:\033[0m %s\n' "$*" >&2; } info() { printf '\033[36m::\033[0m %s\n' "$*"; } ok() { printf '\033[32m✓\033[0m %s\n' "$*"; } warn() { printf '\033[33m!\033[0m %s\n' "$*"; } # PCI vendor IDs (as reported by /sys/class/drm/*/device/vendor). readonly VENDOR_NVIDIA=0x10de readonly VENDOR_AMD=0x1002 readonly VENDOR_INTEL=0x8086 # select_gpu — figure out which GPU MangoHud should log, vendor-agnostically. # # MangoHud enumerates DRM render nodes (renderD128, renderD129, …) in ascending # order and `gpu_list=` selects the n-th of them (verified: gpu_list=0 maps to # renderD128). We pick the node that's the real gaming GPU and return its index. # # Selection order: # 1. MANGOHUD_LOGGER_GPU override — a numeric index, or nvidia|amd|intel. # 2. Single GPU → index 0 (covers every single-card machine, any vendor). # 3. Multi-GPU → prefer a discrete NVIDIA; else the card with the most VRAM # (discrete AMD/Intel report mem_info_vram_total; iGPUs report far less/none). # # Sets globals: GPU_INDEX (may be ""), GPU_VENDOR, GPU_DESC. select_gpu() { GPU_INDEX=""; GPU_VENDOR=""; GPU_DESC="" local nodes=() mapfile -t nodes < <( for n in /sys/class/drm/renderD*; do [[ -e "$n/device/vendor" ]] && basename "$n" done | sort # ascending == MangoHud's gpu_list index order ) local count=${#nodes[@]} if (( count == 0 )); then GPU_DESC="no DRM render node found (software renderer?) — gpu_list left unset" return fi # Index-aligned vendor + VRAM for each node. local vendors=() vrams=() i for i in "${!nodes[@]}"; do vendors[i]=$(cat "/sys/class/drm/${nodes[i]}/device/vendor" 2>/dev/null || echo unknown) vrams[i]=$(cat "/sys/class/drm/${nodes[i]}/device/mem_info_vram_total" 2>/dev/null || echo 0) done if [[ -n "${MANGOHUD_LOGGER_GPU:-}" ]]; then local ov="${MANGOHUD_LOGGER_GPU,,}" if [[ "$ov" =~ ^[0-9]+$ ]]; then GPU_INDEX="$ov" else local want="" case "$ov" in nvidia) want=$VENDOR_NVIDIA ;; amd) want=$VENDOR_AMD ;; intel) want=$VENDOR_INTEL ;; esac for i in "${!nodes[@]}"; do [[ "${vendors[i]}" == "$want" ]] && { GPU_INDEX="$i"; break; } done [[ -z "$GPU_INDEX" ]] && GPU_INDEX=0 # requested vendor absent → first GPU fi elif (( count == 1 )); then GPU_INDEX=0 else # Prefer discrete NVIDIA (NVIDIA never populates mem_info_vram_total via DRM # sysfs, so the VRAM check below can't see it — hence this explicit pass). for i in "${!nodes[@]}"; do [[ "${vendors[i]}" == "$VENDOR_NVIDIA" ]] && { GPU_INDEX="$i"; break; } done if [[ -z "$GPU_INDEX" ]]; then local best=0 bestvram=-1 for i in "${!nodes[@]}"; do if (( ${vrams[i]:-0} > bestvram )); then bestvram=${vrams[i]:-0}; best=$i; fi done GPU_INDEX="$best" fi fi # Describe the choice (guard against an out-of-range manual override). local node="${nodes[$GPU_INDEX]:-?}" vsel="${vendors[$GPU_INDEX]:-unknown}" name case "$vsel" in "$VENDOR_NVIDIA") name=NVIDIA ;; "$VENDOR_AMD") name=AMD ;; "$VENDOR_INTEL") name=Intel ;; *) name="vendor $vsel" ;; esac GPU_VENDOR="$vsel" GPU_DESC="gpu_list=$GPU_INDEX → $node ($name), $count GPU(s) detected" } # libnvidia-ml (NVML) is what lets MangoHud read NVIDIA load/power/clocks. have_nvml() { ldconfig -p 2>/dev/null | grep -q 'libnvidia-ml' \ || [[ -e /usr/lib/libnvidia-ml.so.1 || -e /usr/lib64/libnvidia-ml.so.1 ]] } # ── CPU telemetry (vendor-neutral: works on AMD *and* Intel) ──────────────── # # MangoHud reads CPU temperature from hwmon and CPU package power from the RAPL # powercap energy counters. Both paths are the same regardless of CPU vendor: # • temp → hwmon driver: k10temp/zenpower (AMD), coretemp (Intel) # • power → /sys/class/powercap/intel-rapl:* — despite the "intel" name this is # the powercap *framework*; on AMD it's fed by intel_rapl_msr from the Zen # RAPL MSRs. Since CVE-2020-8694 the energy_uj files are root-only (0400), # so user-mode MangoHud can't read them and cpu_power logs as 0. readonly RAPL_UDEV_FILE=/etc/udev/rules.d/60-mangohud-logger-rapl.rules # cpu_vendor → AMD | Intel | cpu_vendor() { local v; v=$(awk -F': ' '/^vendor_id/{print $2; exit}' /proc/cpuinfo 2>/dev/null) case "$v" in AuthenticAMD) echo AMD ;; GenuineIntel) echo Intel ;; *) echo "${v:-unknown}" ;; esac } # First RAPL package energy counter (enough to gate on), else empty. rapl_energy_file() { compgen -G '/sys/class/powercap/*rapl*/energy_uj' 2>/dev/null | head -1; } # rapl_state → readable | denied | absent rapl_state() { local f; f=$(rapl_energy_file) [[ -z "$f" ]] && { echo absent; return; } if cat "$f" >/dev/null 2>&1; then echo readable; else echo denied; fi } # cpu_temp_source → hwmon 'name' providing CPU temp (any vendor), else empty. cpu_temp_source() { local h n for h in /sys/class/hwmon/hwmon*; do n=$(cat "$h/name" 2>/dev/null) || continue case "$n" in k10temp|zenpower|zenpower3|coretemp|k8temp) echo "$n"; return ;; esac done echo "" } # do_fix_cpu_power — install a udev rule making RAPL energy counters readable so # MangoHud can log cpu_power. Vendor-neutral (same RAPL path on AMD and Intel). do_fix_cpu_power() { local vend; vend=$(cpu_vendor) info "CPU power ($vend) comes from RAPL energy counters, root-only since CVE-2020-8694." info "This installs a udev rule so they're user-readable and MangoHud can log cpu_power." warn "Security note: re-exposes the low-severity PLATYPUS power side-channel." warn "Fine for a personal gaming/benchmark box; skip on shared/multi-user machines." echo case "$(rapl_state)" in absent) err "no RAPL powercap interface found." info "try: sudo modprobe intel_rapl_msr (then re-run)" return 1 ;; readable) ok "RAPL energy counters are already user-readable — nothing to do." return 0 ;; esac local chmod_bin; chmod_bin=$(command -v chmod || echo /usr/bin/chmod) # KERNEL match covers the package (intel-rapl:0) and its subzones (intel-rapl:0:0). # %S%p resolves to the node's /sys path; we chmod its energy_uj attribute. local rule="SUBSYSTEM==\"powercap\", KERNEL==\"intel-rapl:*\", RUN+=\"$chmod_bin 0444 %S%p/energy_uj\"" info "writing $RAPL_UDEV_FILE (sudo)…" if ! printf '%s\n' \ "# Installed by mangohud-logger: make RAPL energy_uj user-readable for CPU-power logging." \ "# Remove this file to revert. See: mangohud-logger help" \ "$rule" | sudo tee "$RAPL_UDEV_FILE" >/dev/null; then err "failed to write $RAPL_UDEV_FILE"; return 1 fi sudo udevadm control --reload-rules 2>/dev/null || true sudo udevadm trigger --subsystem-match=powercap 2>/dev/null || true # Apply to already-present nodes now (udev 'add' won't re-fire for existing ones). sudo "$chmod_bin" -f 0444 /sys/class/powercap/*rapl*/energy_uj 2>/dev/null || true if [[ "$(rapl_state)" == readable ]]; then ok "done — cpu_power will now be logged (persists across reboots)." else warn "rule installed but counters still not readable; a reboot should apply it." fi } # do_doctor — report cross-vendor telemetry readiness; makes no changes. do_doctor() { info "mangohud-logger doctor — telemetry readiness (read-only)" echo if command -v mangohud >/dev/null 2>&1; then ok "mangohud: $(mangohud --version 2>/dev/null | head -1)" else err "mangohud: NOT installed" fi info "session flavor: $GS_FLAVOR" echo # GPUs — list every render node, then the one that will be logged. info "GPUs (DRM render nodes):" local n vend name for n in $(compgen -G '/sys/class/drm/renderD*' 2>/dev/null | sort); do [[ -e "$n/device/vendor" ]] || continue vend=$(cat "$n/device/vendor" 2>/dev/null) case "$vend" in "$VENDOR_NVIDIA") name=NVIDIA ;; "$VENDOR_AMD") name=AMD ;; "$VENDOR_INTEL") name=Intel ;; *) name="vendor $vend" ;; esac printf ' %s (%s)\n' "$(basename "$n")" "$name" done select_gpu if [[ -n "$GPU_INDEX" ]]; then ok "will log: $GPU_DESC" else warn "will log: $GPU_DESC"; fi if [[ "$GPU_VENDOR" == "$VENDOR_NVIDIA" ]]; then if have_nvml; then ok " NVML present → NVIDIA load/power/clocks will log" else err " NVML missing → NVIDIA gpu_* columns will be 0 (install nvidia-utils)"; fi fi echo # CPU — temp + power, vendor-neutral. info "CPU: $(cpu_vendor)" local ts; ts=$(cpu_temp_source) if [[ -n "$ts" ]]; then ok " temp source: hwmon '$ts' → cpu_temp will log" else warn " temp: no known sensor (k10temp/zenpower/coretemp) → cpu_temp may be 0"; fi case "$(rapl_state)" in readable) ok " power (RAPL): readable → cpu_power will log" ;; denied) warn " power (RAPL): root-only → cpu_power = 0. Fix: $(basename "$0") fix-cpu-power" ;; absent) warn " power (RAPL): no powercap interface → cpu_power = 0 (modprobe intel_rapl_msr)" ;; esac echo if [[ -f "$CONFIG_FILE" ]] && grep -qF "$MARKER_BEGIN" "$CONFIG_FILE"; then ok "logging is currently ENABLED (log folder: $LOG_DIR)" else info "logging is currently disabled — run '$(basename "$0")' to enable" fi } usage() { cat < Force which GPU MangoHud logs (default: auto — prefers the discrete gaming GPU, any vendor) Works across AMD/Intel CPUs and NVIDIA/AMD/Intel GPUs, single- or multi-GPU. EOF } # do_toggle — the original enable/disable behavior (default action). do_toggle() { if ! command -v mangohud >/dev/null 2>&1; then err "mangohud is not installed — install it first (e.g. 'sudo pacman -S mangohud lib32-mangohud')." exit 1 fi mkdir -p "$CONFIG_DIR" [[ -f "$CONFIG_FILE" ]] || : > "$CONFIG_FILE" if grep -qF "$MARKER_BEGIN" "$CONFIG_FILE"; then # Currently enabled → strip the block and remove the env file. tmp=$(mktemp) awk -v b="$MARKER_BEGIN" -v e="$MARKER_END" ' $0 == b { skip = 1; next } $0 == e { skip = 0; next } !skip { print } ' "$CONFIG_FILE" > "$tmp" # Trim trailing blank lines the block may have left behind. sed -i -e :a -e '/^\s*$/{$d;N;ba' -e '}' "$tmp" mv "$tmp" "$CONFIG_FILE" env_removed=0 if [[ -f "$ENV_FILE" ]]; then rm -f "$ENV_FILE" env_removed=1 fi # Only remove the gamescope session override if WE wrote it. gs_removed=0 if [[ -f "$GS_SESSION_FILE" ]] && head -1 "$GS_SESSION_FILE" | grep -qF "$GS_OWNED_MARKER"; then rm -f "$GS_SESSION_FILE" gs_removed=1 fi ok "MangoHud logging disabled" info "existing logs kept in: $LOG_DIR" if (( env_removed )); then info "removed: $ENV_FILE" info "MANGOHUD env var stays set in the *current* session until logout" fi if (( gs_removed )); then info "removed: $GS_SESSION_FILE" fi else # Currently disabled → append the config block + write the env file. # Check for pre-existing manual log keys *before* we write, so the warning is accurate. manual_log_keys=0 if grep -qE '^\s*(output_folder|autostart_log|log_duration|log_interval|toggle_logging|gpu_list)\s*=' "$CONFIG_FILE"; then manual_log_keys=1 fi # Work out which GPU to log (any vendor, single- or multi-GPU) before writing. select_gpu mkdir -p "$LOG_DIR" { [[ -s "$CONFIG_FILE" ]] && printf '\n' printf '%s\n' "$MARKER_BEGIN" printf '# Added by mangohud-logger — remove this block to disable logging.\n' printf 'output_folder=%s\n' "$LOG_DIR" printf 'autostart_log=1\n' printf 'log_duration=0\n' printf 'log_interval=100\n' printf 'toggle_logging=Shift_L+F2\n' # Override any earlier `no_display` — MangoHud's autostart_log rides on the # render hook, which `no_display` disables. Without this, logs never start. printf 'no_display=0\n' # Pin the GPU MangoHud logs to the actual gaming GPU (see select_gpu). Omitted # only when no render node was found, so MangoHud keeps its own default. [[ -n "$GPU_INDEX" ]] && printf 'gpu_list=%s\n' "$GPU_INDEX" printf '%s\n' "$MARKER_END" } >> "$CONFIG_FILE" # Inline config string mirroring the MangoHud.conf block. MANGOHUD_CONFIG takes # precedence over MANGOHUD_CONFIGFILE, so this is what makes logging work GLOBALLY # in SteamOS Game Mode — that session forces MANGOHUD_CONFIGFILE=/no_display, # but it never sets MANGOHUD_CONFIG, so ours (from environment.d) wins for every game. # NOTE: relies on output_folder having no commas/spaces (true for the default path). MH_INLINE="output_folder=$LOG_DIR,autostart_log=1,log_duration=0,log_interval=100,toggle_logging=Shift_L+F2,no_display=0" [[ -n "$GPU_INDEX" ]] && MH_INLINE+=",gpu_list=$GPU_INDEX" mkdir -p "$ENV_DIR" cat > "$ENV_FILE" </no_display, which would otherwise suppress our config and # stop logging. This inline config wins over it, so autostart_log/output_folder take # effect for every game with NO per-game launch options. Verified: MANGOHUD_CONFIG # beats a no_display configfile (vkcube CSV log produced). MANGOHUD_CONFIG=$MH_INLINE EOF # Game Mode handling depends on which session manager is installed (see # detect_gs_flavor). Only gamescope-session-plus sources our override file; # the steamos flavor needs per-game launch options instead. gs_action="" if [[ "$GS_FLAVOR" == "plus" ]]; then # gamescope-session-plus override — only write if file is absent or already # ours, so we never trample a user-written override. if [[ ! -f "$GS_SESSION_FILE" ]] || head -1 "$GS_SESSION_FILE" | grep -qF "$GS_OWNED_MARKER"; then mkdir -p "$GS_SESSION_DIR" cat > "$GS_SESSION_FILE" </no_display and STEAM_USE_MANGOAPP=1 with no user # hook — so a global override is impossible. Remove any stale override we # wrote on a previous (plus-flavor) run so it doesn't mislead. if [[ -f "$GS_SESSION_FILE" ]] && head -1 "$GS_SESSION_FILE" | grep -qF "$GS_OWNED_MARKER"; then rm -f "$GS_SESSION_FILE" fi gs_action="n/a — using MANGOHUD_CONFIG (global, no override file needed)" else gs_action="no gamescope session detected (desktop only)" fi if (( manual_log_keys )); then warn "pre-existing log-related keys found in config — they may override ours." fi # NVIDIA GPU stats come from NVML; without it every gpu_* column logs as 0. if [[ "$GPU_VENDOR" == "$VENDOR_NVIDIA" ]] && ! have_nvml; then warn "selected an NVIDIA GPU but libnvidia-ml (NVML) not found — GPU load/power" warn "won't be logged. Install the NVIDIA userspace (e.g. 'nvidia-utils')." fi # CPU power/temp readiness (same checks on AMD and Intel). rapl=$(rapl_state) if [[ "$rapl" == denied ]]; then warn "cpu_power won't log — RAPL counters are root-only. Enable it with:" warn " $(basename "$0") fix-cpu-power" elif [[ "$rapl" == absent ]]; then warn "cpu_power won't log — no RAPL powercap interface (try: sudo modprobe intel_rapl_msr)." fi [[ -z "$(cpu_temp_source)" ]] && warn "no known CPU temp sensor found — cpu_temp may read 0." ok "MangoHud logging enabled" info "log folder: $LOG_DIR" info "toggle key: Shift+F2 (during a game)" info "config file: $CONFIG_FILE" info "env file: $ENV_FILE (MANGOHUD=1)" info "gpu logged: ${GPU_DESC:-MangoHud default}" if [[ -n "$GPU_INDEX" ]]; then info " (override with MANGOHUD_LOGGER_GPU=)" fi info "cpu ($( cpu_vendor )): temp=$( [[ -n "$(cpu_temp_source)" ]] && echo ok || echo none ) power(RAPL)=$rapl" info "session type: $GS_FLAVOR (gamescope override: $gs_action)" info "note: while logging is on, the HUD becomes visible during games" info " (autostart_log needs the render hook that no_display disables)" if [[ "$GS_FLAVOR" == "steamos" ]]; then # SteamOS Game Mode forces MANGOHUD_CONFIGFILE=/no_display, but our # MANGOHUD_CONFIG (set above in environment.d) overrides it for every game — # so logging IS global here, no per-game launch options needed. info "→ logging is GLOBAL via MANGOHUD_CONFIG — no per-game launch options needed." warn "log out and back into Game Mode (or reboot) so environment.d takes effect." info "while logging, the in-game MangoHud overlay will be visible (autostart_log" info "needs the render hook that no_display disables) — that's expected." info "fallback for a single game (e.g. if you don't want to restart the session):" printf ' \033[1m%s\033[0m\n' "$LAUNCH_OPT" else warn "log out and back in (or restart your gamescope session) so the env var takes effect." fi fi } # ── Command dispatch ──────────────────────────────────────────────────────── case "${1:-toggle}" in toggle|"") do_toggle ;; doctor|check|--check|-c) do_doctor ;; fix-cpu-power|fix|--fix-cpu-power) do_fix_cpu_power ;; -h|--help|help) usage ;; *) err "unknown command: $1"; echo; usage; exit 1 ;; esac