1915 字
10 分钟
Linux 在睡眠唤醒后,刷新率掉回了 60Hz
2026-10-11

我的显示器是 SANC 的 DM73u pro,4K 160Hz,用 HDMI 接在 RX 7800 XT 上,系统是 Manjaro KDE(Wayland)。最近发现一个很烦的问题:电脑睡眠唤醒以后,刷新率经常掉回 60Hz,而且显示设置里 4K 最高就只剩 60Hz,160Hz 的选项直接没了。

这次排查是和 Claude 一起完成的。

160Hz 去哪了#

Claude 先查了三样东西。

一是当前的模式列表。kscreen-doctor -o 里,4K 最高只有 60Hz,2560x1440@144、1920x1080@120 这些倒都还在。

二是 KWin 保存的配置。~/.config/kwinoutputconfig.json 里存的还是 3840x2160 @ 160000,EDID 哈希也和当前显示器一致。也就是说配置没丢,显示器也没被认成别的。

三是显示器的 EDID。用 edid-decode 解出来一看,4K@144 和 4K@160 都只写在 DisplayID 扩展块里:

edid-decode /sys/class/drm/card1-HDMI-A-1/edid
Block 2, DisplayID Extension Block:
Video Timing Modes Type 1 - Detailed Timings Data Block:
DTD: 3840x2160 144.000316 Hz 16:9 318.961 kHz 1347.290000 MHz
DTD: 3840x2160 160.000301 Hz 16:9 352.801 kHz 1490.230000 MHz

关键在最后一列的像素时钟:1347 MHz 和 1490 MHz。HDMI 2.0 的 TMDS 最高只到 600 MHz,刚好够 4K@60(594 MHz)。4K 高刷必须走 HDMI 2.1 的 FRL 链路。

所以并不是 KWin 忘了设置,而是显卡驱动这时把链路当成了 HDMI 2.0,超过 600 MHz 的模式全被筛掉了,KWin 想恢复 160Hz 也没得选。

日志里的规律#

再翻 9 月 25 日以来的日志,规律很明显:

  • 每次从睡眠(suspend)唤醒的同一秒,KWin 都会报一句 Applying output configuration failed!,也就是它想恢复 160Hz 但失败了。7 次睡眠里出现了 5 次。
  • 从休眠(hibernate)恢复的话,一次都没有出现过。
  • 睡眠唤醒时,独显会完整复位一次(日志里的 MODE1 reset)。

Claude 推测的过程是这样的:唤醒时 amdgpu 立刻重新检测 HDMI 链路,可显示器这时往往还没从待机里醒透,HDMI 2.1 的能力协商失败,驱动就退回了 TMDS。等显示器醒了,又没有新的热插拔事件,驱动不会再检测一次,于是就一直卡在 60Hz。偶尔显示器醒得够快就没事,所以是「经常」而不是「每次」。

三条路#

Claude 给了三个办法:

  1. 临时恢复:显示器关了再开,或者拔插一下 HDMI 线,制造一次真正的热插拔。
  2. 根治:换 DP 线。amdgpu 的 HDMI 2.1 支持才刚加进来,还不太稳,DP 跑 4K 高刷靠谱得多。
  3. 不换线:写一个 systemd 的睡眠钩子,唤醒后等几秒,让显示器醒透,再用 amdgpu 的 debugfs 模拟一次拔插。

我选了第三条,因为懒得换线。之前刚装上 Manjaro 的时候,我就发现只能开 60Hz,一查吓一跳:Linux 的 amdgpu 驱动当时还不支持 HDMI 2.1。好消息是 Linux 7.2 合入了 amdgpu 的 HDMI 2.1 FRL 支持,于是我等了一个多月,升级之后就好了。不过这个功能默认是关着的,要在内核参数里加上 amdgpu.dcfeaturemask=0x402 才会打开:0x400 是 FRL 这一位,0x2 是原本的默认值,只写 0x400 会把默认值也关掉。

/etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT='quiet splash udev.log_priority=3 amdgpu.dcfeaturemask=0x402'

改完执行 sudo update-grub,重启后 /sys/module/amdgpu/parameters/dcfeaturemask 读出来是 1026 就说明生效了。

今年折腾 Linux 还挺幸运的,每次发现一个 bug,都是陈年老 bug,结果一翻,全都是即将修复的。或许是 AI 提高了大家的生产力吧!【离 Linux 当主力最近的一次】

先手动拔插一次#

amdgpu 在 debugfs 里给每个接口都留了一个 trigger_hotplug。第一次只往里面写了 1,模式列表毫无变化。改成先写 0 模拟拔掉、等 2 秒、再写 1 插回去,4K@160、144、120 全回来了,KWin 也自动切回了 160Hz:

终端
sudo bash -c '
T=/sys/kernel/debug/dri/0000:03:00.0/HDMI-A-1/trigger_hotplug
echo 0 > $T; sleep 2; echo 1 > $T
'

debugfs 只有 root 能写。路径里的 0000:03:00.0 是独显的 PCI 地址,可以用 lspci 查到。

写成睡眠钩子#

systemd 在睡眠前后会调用 /usr/lib/systemd/system-sleep/ 里的可执行文件,第一个参数是 pre 或 post。脚本就放在这里:

/usr/lib/systemd/system-sleep/amdgpu-hdmi-redetect
#!/bin/sh
# 挂起唤醒后 HDMI 刷新率掉回 60Hz 的补丁。
#
# 原因:唤醒时 amdgpu 立即重新检测 HDMI 链路,HDMI 2.1 显示器往往还没醒透,
# 协商不到 FRL,驱动就退回 TMDS 并筛掉 4K@120/144/160,KWin 只能用 60Hz。
# 做法:唤醒 6 秒后检查各 HDMI 口的当前刷新率,<=60Hz 就通过 amdgpu debugfs 模拟
# 一次拔插,驱动重新检测后高刷模式回来,KWin 自动套回保存的设置(期间黑屏 2~3 秒)。
#
# 日志:journalctl -u amdgpu-hdmi-redetect
# 手动检查(只看不动):pkexec /usr/lib/systemd/system-sleep/amdgpu-hdmi-redetect check
# 撤销:删除本文件即可。
DELAY=6s
scan() {
act=$1
for conn in /sys/class/drm/card*-HDMI-A-*; do
[ "$(cat "$conn/status" 2>/dev/null)" = connected ] || continue
card=${conn##*/}
name=${card#*-}
card=${card%%-*}
pci=$(basename "$(readlink -f "/sys/class/drm/$card/device")")
dbg=/sys/kernel/debug/dri/$pci
[ -w "$dbg/$name/trigger_hotplug" ] || continue
# 从 atomic state 里找出该接口所在 crtc 的当前刷新率
hz=$(awk -v conn="$name" '
/^[^\t]/ { c = ""; k = "" }
/^crtc\[/ { c = $2 }
/^connector\[/ { k = $2 }
c != "" && /^\tmode: / { s = $0; sub(/^\tmode: "[^"]*": */, "", s); split(s, f, " "); hz[c] = f[1] }
k == conn && /^\tcrtc=/ { s = $0; sub(/^\tcrtc=/, "", s); print hz[s] + 0; exit }
' "$dbg/state")
hz=${hz:-0}
if [ "$hz" -lt 1 ]; then
echo "$name ($pci): 读不到当前刷新率,跳过"
elif [ "$hz" -gt 60 ]; then
echo "$name ($pci): 当前 ${hz}Hz,正常"
elif [ "$act" = 1 ]; then
echo "$name ($pci): 当前 ${hz}Hz,模拟拔插重新检测"
echo 0 > "$dbg/$name/trigger_hotplug"
sleep 2
echo 1 > "$dbg/$name/trigger_hotplug"
else
echo "$name ($pci): 当前 ${hz}Hz,需要重新检测"
fi
done
}
case "$1" in
post)
# 交给临时 timer 延后执行,不拖住 systemd-sleep(否则网络、锁屏等都要跟着等)
systemd-run --quiet --no-block --collect \
--unit=amdgpu-hdmi-redetect \
--timer-property=RemainAfterElapse=no \
--timer-property=AccuracySec=1s \
--on-active="$DELAY" \
"$(readlink -f "$0")" redetect
;;
redetect) scan 1 ;;
check) scan 0 ;;
esac
exit 0

几个设计上的考虑:

  • 不在钩子里直接 sleep。钩子跑完之前唤醒流程不会往下走,网络、锁屏都得跟着等,所以交给 systemd-run 建一个临时定时器,6 秒后再执行。
  • 看的是实际刷新率。从 debugfs 的 atomic state 里读当前模式,高于 60Hz 就什么都不做,不会白白黑屏一次。
  • 不写死 card0、card1。这台机器有核显和独显两块卡,编号重启后可能互换,脚本按 PCI 地址去找 debugfs 目录。
  • 带一个只看不动的 check 模式,方便确认它读得对不对。

定时器慢了十几秒#

装好以后模拟了一次唤醒调用,6 秒过去了,日志里却迟迟没有它执行的记录,过了十几秒才跑起来。原来 systemd 定时器默认的精度 AccuracySec 是 1 分钟,为了省电,它会把触发时间往后挪一点。加上 --timer-property=AccuracySec=1s 之后,6 秒的延时就准了。

实测#

装好后我马上睡眠了一次。唤醒后过了几秒,屏幕黑了 2~3 秒,然后回到了 160Hz。日志也对得上:

journalctl
10月 11 13:52:54 yuzi-arch systemd-sleep[1375495]: System returned from sleep operation 'suspend'.
10月 11 13:53:01 yuzi-arch amdgpu-hdmi-redetect[1375739]: HDMI-A-1 (0000:03:00.0): 当前 60Hz,模拟拔插重新检测

有意思的是,这次 KWin 并没有报 Applying output configuration failed!,但还是掉到了 60Hz。幸好脚本判断的是实际刷新率,而不是 KWin 的日志,不然这次就漏掉了。

以后唤醒时屏幕黑一下,就是它在把 160Hz 找回来。想看它每次做了什么:

终端
journalctl -b -u amdgpu-hdmi-redetect

不想要了,删掉这个脚本就行。

最近 AI 反编译老游戏挺火的。照这个趋势,在 Linux 上玩各种游戏、用 Adobe 级别的软件干活,感觉快要成了啊!

参考资料#

Linux 在睡眠唤醒后,刷新率掉回了 60Hz
作者
Yuzi
发布于
2026-10-11
许可协议
CC BY-NC-SA 4.0