From 551471f0b3b6ef1e79d6e002f7bac5d8aaaebf39 Mon Sep 17 00:00:00 2001 From: 28allday Date: Fri, 24 Jul 2026 19:31:03 +0100 Subject: [PATCH] Auto-select the gaming GPU + cross-vendor CPU-power tooling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MangoHud logs one GPU. On a hybrid box (discrete NVIDIA/AMD beside an AMD/Intel iGPU) its default often latches onto the idle iGPU, so every gpu_* column reads ~0 while the discrete card does the rendering. Detect the actual gaming GPU — any vendor, single- or multi-GPU — and pin it via gpu_list. Selection: single GPU -> that one; discrete NVIDIA preferred; otherwise the card with the most VRAM. Override with MANGOHUD_LOGGER_GPU. Verified against MangoHud's own enumeration (gpu_list=0 -> renderD128, RTX 5060 Ti) and 7 synthetic layouts incl. NVIDIA as the second node. Also make CPU telemetry work across AMD and Intel. cpu_power was logging 0 because the RAPL energy counters are root-only since CVE-2020-8694 (same intel-rapl powercap path on both vendors). Add: - doctor: read-only GPU/CPU telemetry readiness report - fix-cpu-power: install a udev rule making RAPL counters user-readable - enable-time warnings pointing at the fix CPU temp already works via hwmon (k10temp/zenpower/coretemp). Default no-arg behavior (toggle on/off) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 83 +++++++++++- mangohud-logger.sh | 317 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 388 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2a66b80..cfb3f30 100755 --- a/README.md +++ b/README.md @@ -21,8 +21,9 @@ won't trample anything you wrote by hand): 1. **`~/.config/MangoHud/MangoHud.conf`** — appends a marker-bracketed block with `output_folder`, `autostart_log=1`, `log_duration=0` (= until you - quit), `log_interval=100`, `toggle_logging=Shift_L+F2`, and a - `no_display=0` override. (Also serves as the on/off state marker.) + quit), `log_interval=100`, `toggle_logging=Shift_L+F2`, a `no_display=0` + override, and a `gpu_list=` pin (see [GPU selection](#gpu-selection)). + (Also serves as the on/off state marker.) 2. **`~/.config/environment.d/95-mangohud-logger.conf`** — contains: - `MANGOHUD=1` so the Vulkan loader auto-injects MangoHud into every game. @@ -39,6 +40,40 @@ won't trample anything you wrote by hand): When you toggle it off, the script removes everything it wrote. +## GPU selection + +MangoHud logs stats for **one** GPU. On a hybrid machine — a discrete +NVIDIA/AMD card alongside an AMD/Intel iGPU (common on modern AMD desktops, +laptops, and handhelds) — MangoHud's default frequently latches onto the *idle* +iGPU, so every `gpu_*` column in the CSV reads ~0 (0 % load, idle clocks, idle +power) even while the discrete card does all the rendering. + +On enable, the script auto-detects the GPU that's actually rendering games and +pins MangoHud to it with `gpu_list=`. It's **vendor-agnostic**: + +- **Single GPU** (any vendor) → that GPU. +- **Discrete NVIDIA** present → the NVIDIA card. +- **Two AMD/Intel GPUs** → the one with the most VRAM (the discrete card; + iGPUs report little or none). + +It works by mapping DRM render nodes (`renderD128`, `renderD129`, …) to +MangoHud's `gpu_list` index order. The chosen GPU is printed on enable: + +``` +:: gpu logged: gpu_list=0 → renderD128 (NVIDIA), 2 GPU(s) detected +``` + +If the guess is wrong, override it: + +```bash +MANGOHUD_LOGGER_GPU=amd ./mangohud-logger.sh # or: nvidia | intel +MANGOHUD_LOGGER_GPU=1 ./mangohud-logger.sh # or an explicit gpu_list index +``` + +> **NVIDIA note:** MangoHud reads NVIDIA load/power/clocks via NVML. If NVML +> (`libnvidia-ml`, shipped with `nvidia-utils`) is missing, the script warns you +> — without it the NVIDIA `gpu_*` columns stay at 0. + ## Session-flavor detection Game Mode is launched differently on different distros, and the fix differs @@ -140,13 +175,30 @@ mangohud-logger ## Usage ```bash -./mangohud-logger.sh +./mangohud-logger.sh # toggle logging on/off (default) +./mangohud-logger.sh doctor # report GPU/CPU telemetry readiness (no changes) +./mangohud-logger.sh fix-cpu-power # make RAPL CPU-power counters readable (sudo) +./mangohud-logger.sh help # usage ``` -No flags, no subcommands. First run enables; second run disables. After the +Run with no argument to toggle: first run enables, second disables. After the first enable, **log out and back in** (Game Mode users: fully restart into Game Mode) so the env vars become effective. You only need to do that once. +Before your first benchmark, run `doctor` — it tells you exactly which columns +will hold real data and what (if anything) needs fixing: + +``` +:: GPUs (DRM render nodes): + renderD128 (NVIDIA) + renderD129 (AMD) +✓ will log: gpu_list=0 → renderD128 (NVIDIA), 2 GPU(s) detected +✓ NVML present → NVIDIA load/power/clocks will log +:: CPU: AMD +✓ temp source: hwmon 'k10temp' → cpu_temp will log +! power (RAPL): root-only → cpu_power = 0. Fix: mangohud-logger.sh fix-cpu-power +``` + In-game keys (set by the script's MangoHud block): | Key | Action | @@ -157,6 +209,29 @@ In-game keys (set by the script's MangoHud block): CSV files appear in `~/Downloads/mango-logs/`. Drop them into for graphs, or open in a spreadsheet. +## CPU power (`cpu_power` column reads 0?) + +MangoHud reads CPU package power from the RAPL powercap energy counters. Since +CVE-2020-8694 those counters are **root-only** (mode `0400`), so user-mode +MangoHud can't read them and `cpu_power` logs as `0`. This is the same on **AMD +and Intel** — the powercap interface is named `intel-rapl` on both (it's the +framework name; on AMD `intel_rapl_msr` feeds it from the Zen RAPL MSRs). + +`fix-cpu-power` installs a udev rule +(`/etc/udev/rules.d/60-mangohud-logger-rapl.rules`) that makes `energy_uj` +user-readable, and applies it immediately: + +```bash +./mangohud-logger.sh fix-cpu-power +``` + +> **Security note:** this re-exposes the low-severity PLATYPUS power +> side-channel. Fine for a personal gaming/benchmark box; skip it on shared or +> multi-user machines. Remove the rule file to revert. + +CPU **temperature** needs no fix — it comes from hwmon (`k10temp`/`zenpower` on +AMD, `coretemp` on Intel) and works out of the box. + ## Compatibility - **SteamOS Game Mode** (`/usr/lib/steamos/gamescope-session`) — logs via diff --git a/mangohud-logger.sh b/mangohud-logger.sh index c79afc0..649d876 100755 --- a/mangohud-logger.sh +++ b/mangohud-logger.sh @@ -10,11 +10,20 @@ # → 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). @@ -75,15 +84,269 @@ info() { printf '\033[36m::\033[0m %s\n' "$*"; } ok() { printf '\033[32m✓\033[0m %s\n' "$*"; } warn() { printf '\033[33m!\033[0m %s\n' "$*"; } -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 +# PCI vendor IDs (as reported by /sys/class/drm/*/device/vendor). +readonly VENDOR_NVIDIA=0x10de +readonly VENDOR_AMD=0x1002 +readonly VENDOR_INTEL=0x8086 -mkdir -p "$CONFIG_DIR" -[[ -f "$CONFIG_FILE" ]] || : > "$CONFIG_FILE" +# 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="" -if grep -qF "$MARKER_BEGIN" "$CONFIG_FILE"; then + 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" ' @@ -121,10 +384,13 @@ 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)\s*=' "$CONFIG_FILE"; then + 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' @@ -138,6 +404,9 @@ else # 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" @@ -147,6 +416,7 @@ else # 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" <)" + 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)" @@ -231,3 +522,13 @@ EOF 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