我的显示器是 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 扩展块里:
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 给了三个办法:
- 临时恢复:显示器关了再开,或者拔插一下 HDMI 线,制造一次真正的热插拔。
- 根治:换 DP 线。amdgpu 的 HDMI 2.1 支持才刚加进来,还不太稳,DP 跑 4K 高刷靠谱得多。
- 不换线:写一个 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 会把默认值也关掉。
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_hotplugecho 0 > $T; sleep 2; echo 1 > $T'debugfs 只有 root 能写。路径里的 0000:03:00.0 是独显的 PCI 地址,可以用 lspci 查到。
写成睡眠钩子
systemd 在睡眠前后会调用 /usr/lib/systemd/system-sleep/ 里的可执行文件,第一个参数是 pre 或 post。脚本就放在这里:
#!/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 ;;esacexit 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。日志也对得上:
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 级别的软件干活,感觉快要成了啊!
参考资料
- Initial AMDGPU HDMI 2.1 FRL Support Successfully Merged For Linux 7.2(Phoronix)
- HDMI 2.1 FRL is disabled by default in 7.2.3, 4K120 silently falls back to YCbCr 4:2:0(CachyOS 论坛,
0x402的由来) - Vibe-Coded Browser Ports Of Games Work Perfectly, Unfortunately(Kotaku)
man 8 systemd-suspend.service:system-sleep钩子的调用方式和参数man 5 systemd.timer:AccuracySec的默认值
