我的笔记本平时接一块墨水屏,用 Sway,kanshi 在接墨水屏时只开墨水屏、关掉内置屏。最近发现睡眠一段时间再唤醒,墨水屏显示无信号,内置屏也是黑的,连背光都没有。切 VT 没用,重载配置也没用,只能 pkill -x sway 重新登录。睡几分钟一般没事,睡得久就稳定复现。
TL;DR
wlroots 在睡眠时关不掉 CRTC,却还记着原来的分配,唤醒后直接拿这份过期记录去配置输出。只要它和内核的实际状态对不上,之后所有的 atomic commit 都会被内核以 EINVAL 拒绝,Sway 就再也点不亮任何一块屏。
我给 wlroots 打了个补丁,会话恢复时从内核重新读一遍每个接口的 CRTC。打补丁后两次长时间睡眠都正常唤醒了,补丁准备提交给上游,相关的 issue 是 wlroots#4024(在新标签页打开)。
如果你也遇到了,先确认 Sway 的日志里是不是有大量 Atomic commit failed: Invalid argument 和 Search for valid config failed。是的话,在官方修好之前只能重启 Sway,或者自己打补丁。
环境
- Lenovo Yoga 14s,Intel 核显(i915),Arch Linux,内核 7.2,睡眠方式是 s2idle。
- Sway 1.12,wlroots 0.20.2。
- Dasung 13.3 寸墨水屏,通过 USB-C 接 DP,3200x2400@37Hz。
分工
这次排查是我和 Claude Code 一起做的。复现、一次次长时间睡眠测试、决定不等上游而是自己修、打包时的取舍,是我做的。读调试日志、对照 wlroots 源码定位根因、写补丁,主要是 Claude Code 做的,补丁的提交里也带了它的 Co-Authored-By。
走过的弯路
中间有一次墨水屏变成全白,所有按键都没反应,只能断电重启,所以一开始怀疑是墨水屏不适应睡眠,于是在 swayidle 的 before-sleep 里先禁用墨水屏再睡。结果还是黑屏。
后来用 sway -d 开了调试日志才看到,禁用 DP-2 的时候 wlroots 反而会给它分配 CRTC,这个脚本只是在给问题添乱,就撤掉了。
定位根因
关键的日志在睡眠和唤醒那几秒。睡眠时 logind 暂停 seat,wlroots 销毁所有输出,尝试关闭它们的 CRTC:
[backend/drm/drm.c:1377] connector DP-2: De-allocating CRTC 171
[backend/drm/drm.c:1385] connector DP-2: Failed to disable CRTC 171
这时 DRM 设备已经被暂停了,关闭必然失败,但 wlroots 内部还留着 DP-2 绑着 CRTC 171 的记录。唤醒后重新扫描接口:
[backend/drm/backend.c:112] DRM FD resumed
[backend/drm/drm.c:1861] 'eDP-1' connected
[backend/drm/drm.c:1632] Current CRTC: -1
[backend/drm/drm.c:1861] 'DP-2' connected
[backend/drm/drm.c:1632] Current CRTC: 171
[backend/drm/atomic.c:82] connector DP-2: Atomic commit failed: Invalid argument
[sway/config/output.c:1037] Search for valid config failed
这里的「Current CRTC」看起来像是从内核读的,其实不是。翻 connect_drm_connector() 的代码可以看到,它打印和使用的都是 wlroots 自己的 conn->crtc,也就是睡眠前那份没清掉的记录。它会用这个记录决定输出初始是否启用、当前是什么模式。
而内核那边的实际情况是,已经被禁用的 eDP-1 还绑着 CRTC 171。wlroots 以为 171 归 DP-2,eDP-1 什么都没有,后面每一次提交都建立在这个错误的前提上,内核当然全部拒绝。切 VT 也没用,因为每次恢复都会复用同一份过期记录。
内核为什么在唤醒后还给一个已禁用的 eDP-1 绑着 CRTC,我没有查清,可能和 i915 的状态恢复有关。不过不管内核怎么想,wlroots 都不应该假设自己的记录一定是对的。
补丁
思路很简单,会话恢复时,在重新扫描接口之前,先从内核读一遍每个接口当前的 CRTC_ID,把 wlroots 的记录对齐。对不上时打一行日志,方便以后确认:
void refresh_drm_connector_crtcs(struct wlr_drm_backend *drm) {
struct wlr_drm_connector *conn;
wl_list_for_each(conn, &drm->connectors, link) {
if (conn->lease) {
continue;
}
drmModeConnector *drm_conn = drmModeGetConnector(drm->fd, conn->id);
if (!drm_conn) {
wlr_drm_conn_log_errno(conn, WLR_ERROR, "Failed to get DRM connector");
continue;
}
struct wlr_drm_crtc *crtc = connector_get_current_crtc(conn, drm_conn);
drmModeFreeConnector(drm_conn);
if (crtc != conn->crtc) {
char old_crtc_str[16], new_crtc_str[16];
format_nullable_crtc(old_crtc_str, sizeof(old_crtc_str), conn->crtc);
format_nullable_crtc(new_crtc_str, sizeof(new_crtc_str), crtc);
wlr_drm_conn_log(conn, WLR_INFO, "Resyncing CRTC with kernel: %s → %s",
old_crtc_str, new_crtc_str);
}
conn->crtc = crtc;
}
}
然后在 backend/drm/backend.c 的 handle_session_active() 里,scan_drm_connectors() 之前调用它。整个改动 32 行,在 0.20.2 和上游 master 上都能直接应用。
验证
先编了一份打补丁的 libwlroots-0.20.so,用 LD_LIBRARY_PATH 加载。这里有个小坑:Arch 的 /usr/bin/sway 带了 cap_sys_nice,动态链接器对带 capability 的程序会忽略 LD_LIBRARY_PATH,所以得复制一份不带 capability 的 sway 来启动,再用 /proc/<pid>/maps 确认加载的确实是新库。
之后隔很久睡眠唤醒了两次,两次都正常恢复,其中一次日志里出现了:
connector eDP-1: Resyncing CRTC with kernel: no CRTC → CRTC 171
正好就是之前导致卡死的那种不一致,说明根因找对了。
本地打包
临时库毕竟不是长久之计。我从 Arch 官方的 wlroots0.20 打包仓库克隆了 PKGBUILD,加上补丁,改了几处:
- 包名改成
wlroots0.20-crtc-resync,同时provides和conflicts官方的wlroots0.20。这样 sway 的依赖照样满足,pacman -Syu也不会悄悄换回没打补丁的官方包。代价是官方升级 wlroots 时不会自动跟上,要自己留意。 - 去掉了源码 tag 的签名验证。这个包只是临时用,PKGBUILD 里从官方抄来的 sha256 已经锁定了源码内容,没必要为它往 keyring 里导入三把公钥。
makepkg -srCi 一条命令编完装上,-r 会顺手把编译时才需要的 vulkan-headers 删掉。
装好后第一次启动 Sway,墨水屏是全黑的,但 Sway 显示 DP-2 已启用,截图也能看到壁纸。打开再关掉内置屏后就好了,之后没再出现,原因不明。如果你也遇到,可以先 swaymsg output eDP-1 enable 试一下。
还没解决的
- 内核为什么在唤醒后还给已禁用的 eDP-1 绑着 CRTC。
- 还有一个次要问题:查询一个正在禁用的输出的光标格式时,
drm_connector_alloc_crtc()会给它分配 CRTC,前面那个禁用脚本添乱就是因为这个。这次没修。 - 补丁要提交给上游,等合并后我就换回官方包。
评论