v0.1.8 — fix Gaming Mode 60 Hz launch on NVIDIA + HDMI

Reported by clutchmuffin: TUI saved e.g. CUSTOM_REFRESH_RATES=165 but
Gaming Mode always launched at 60 Hz. Two stacked bugs:

1. Settings TUI wrote ~/.config/environment.d/gamescope-session-plus.conf
   but never reloaded systemd's user env, so the running user manager
   (and therefore gamescope-session-plus@.service) still had the old
   values until next login. Now flush_pending calls
   `systemctl --user import-environment` / `unset-environment` for the
   keys it just touched.

2. CUSTOM_REFRESH_RATES was written as a single scalar. Gamescope's
   --custom-refresh-rates is a list of *switchable* rates, not a
   launch-rate selector — and with no safe 60 Hz fallback in the list,
   some DRM/NVIDIA paths drop to the EDID-preferred 60 Hz on first
   launch. TUI now writes a comma list (e.g. 60,165). show_state and
   confirm_risky_save de-list to the highest member for display /
   validation.

Adds a troubleshooting README entry documenting the Steam BPM
client-side rate persistence as the canonical first-launch workaround.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
28allday
2026-05-17 20:11:19 +01:00
co-authored by Claude Opus 4.7
parent ba86b8c777
commit 4fa77a6238
3 changed files with 59 additions and 3 deletions
+35 -1
View File
@@ -109,6 +109,19 @@ flush_pending() {
echo "${key}=${value}" >> "$CONF"
fi
done
# environment.d/*.conf is parsed by `systemd --user` at user-manager startup,
# so edits made here don't reach gamescope-session-plus@.service until the
# next login unless we nudge systemd. import-environment pulls the keys we
# just wrote into the running user manager scope so the next Gaming Mode
# launch sees them without a re-login.
if [[ ${#PENDING_SET[@]} -gt 0 ]]; then
# shellcheck disable=SC2046
systemctl --user import-environment $(printf '%s ' "${!PENDING_SET[@]}") 2>/dev/null || true
fi
if [[ ${#PENDING_UNSET[@]} -gt 0 ]]; then
systemctl --user unset-environment "${!PENDING_UNSET[@]}" 2>/dev/null || true
fi
}
# ------------------------------------------------------------------------------
@@ -225,6 +238,12 @@ show_state() {
width=$(effective SCREEN_WIDTH)
height=$(effective SCREEN_HEIGHT)
refresh=$(effective CUSTOM_REFRESH_RATES)
# Stored as a comma list (e.g. "60,165") so gamescope has a safe 60 Hz
# fallback. The user only cares about the rate they picked — the highest
# member of the list.
if [[ "$refresh" == *,* ]]; then
refresh=$(tr ',' '\n' <<<"$refresh" | sort -nr | head -1)
fi
vk_adapter=$(effective VULKAN_ADAPTER)
dri_prime=$(effective DRI_PRIME)
prime_offload=$(effective __NV_PRIME_RENDER_OFFLOAD)
@@ -385,7 +404,17 @@ choose_refresh_rate() {
rate=${choice%% *}
fi
[[ -z "$rate" ]] && return 0
pending_set CUSTOM_REFRESH_RATES "$rate"
# gamescope-session-plus passes this env value to gamescope as
# --custom-refresh-rates, which is a list of *switchable* rates the user can
# cycle between in Steam Big Picture. Writing a single value (e.g. "165") can
# leave gamescope without a safe 60 Hz fallback if the monitor's EDID
# preferred mode is 60 Hz and the high-rate mode isn't enumerated on first
# launch — most visibly on NVIDIA HDMI outputs. Always include 60 as the
# floor so Steam can fall back and the user can switch up to their pick.
local rate_list="$rate"
[[ "$rate" != "60" ]] && rate_list="60,${rate}"
pending_set CUSTOM_REFRESH_RATES "$rate_list"
}
# Keys this menu owns. Cleared at the start of every selection so switching
@@ -526,6 +555,11 @@ confirm_risky_save() {
w=$(effective SCREEN_WIDTH)
h=$(effective SCREEN_HEIGHT)
rate=$(effective CUSTOM_REFRESH_RATES)
# Check the user's actual pick (highest of the comma list) against monitor
# support — not the bare list, which won't match a single mode line.
if [[ "$rate" == *,* ]]; then
rate=$(tr ',' '\n' <<<"$rate" | sort -nr | head -1)
fi
local -a warnings=()
if [[ -n "$w" && -n "$h" ]] && ! resolution_supported "$w" "$h"; then