现象
- 闲置超时导致屏幕关闭后,再点亮时桌面布局错乱:
- 竖屏消失,只剩两块屏在用
- 剩下的两块被重排去填补空位(位置整体偏移)
- 不会自愈 —— 亮屏后一直是错的,必须手工把竖屏拉回来
根因(三层,逐层可追)
① 屏黑期间,显示链路的 MST 下游分支 flap(连接器报 connected 但拿不到 mode)
↓
② KScreen 全程在跑,收到 RandR 事件就重放布局 ⇒ 把「缺一块」当成事实写进配置
↓
③ 亮屏只恢复 DPMS,不重新枚举 MST 下游 ⇒ 缺的那块屏不会自己回来
↓
布局保持错乱,而且机制以为一切正常关键在③:「亮屏」不等于「重新探测」。所以缺屏是持久状态,不是暂时现象。
为什么锁屏那条路没出问题
因为锁屏有可监听的状态信号,可以在屏黑之前把 KScreen 停掉;而闲置熄屏没有。
| 场景 | 有没有「即将关屏」的信号 | 结果 |
|---|---|---|
| 锁屏 → 黑屏 → 解锁 | 有(会话锁屏状态变化) | 可以事前停掉布局守护进程 ⇒ 屏黑期间无人重排 |
| 闲置 → 黑屏 → 点亮 | 没有(关屏由电源管理直接下发,不产生任何信号) | 只能事后发现并纠正 |
这也是为什么最终方案不是「等信号」,而是两套触发源共用一份机制:锁屏用信号,闲置熄屏用显示配置事件(有事件且零轮询)。
机制设计
核心是自己维护一份「正确布局」的基准,而不是指望显示守护进程自动同步当前状态 —— 因为「当前状态」可能已经是坏的。
| 设计点 | 做法 | 为什么 |
|---|---|---|
| 两条路径各存一份 | 锁屏一份、熄屏一份,物理隔离 | 两者触发时机不同、可信度不同;共用一份会互相污染(熄屏时抓的快照可能已经是坏的,会把锁屏那份好的盖掉) |
| 基准只升不降 | 以「已启用的输出数量」为判据,只有当前启用数更多时才更新基准 | 坏布局一定是「少了屏」,启用数必然更少 ⇒ 它永远赢不了基准 ⇒ 基准不会被病态污染 |
| 屏关瞬间不抓快照 | 屏关时只停守护进程,不写基准 | 那一瞬间 flap 可能已经发生,抓下来的就是病态;把它当标准等于把错误固化 |
| 基准来源 | 锁屏时抓(用户刚在用,最可信);熄屏时靠「上次确认好的」 | 只要有一次是在正常状态下建立的,之后就不会退化 |
布局快照同时备份显示配置目录,恢复时直接把配置换回去、再启动守护进程 —— 让它在启动时读到的就是正确布局,而不是「先启动、再被纠正」。
行为语义
| 情形 | 动作 | 会不会闪 |
|---|---|---|
| 屏关着 | 停掉布局守护进程,让 flap 无人响应(能拦就拦) | 不闪 |
| 屏亮,布局与基准一致 | 完全不动(只做一次只读比较) | 不闪 |
| 屏亮,比基准多一块屏 | 更新基准(视为布局变好了) | 不闪 |
| 屏亮,比基准少屏 / 位置不对 | 恢复基准并重启守护进程(拦不住就纠正) | 闪 1–2 秒 |
不轮询:靠显示配置的变更事件驱动,事件到了才查一次屏幕电源状态。空闲时不产生任何定时唤醒。
纠正有节流:同一次不一致在 10 分钟内只纠正一次,避免和用户的手动调整反复抢。
验证与手工出口
# 看两份基准、当前实际布局、守护进程与屏幕状态、最近的纠正记录
kscreen-layout-guard status
# 如果你故意改成更少的屏(不再要那一块),把当前布局设为新基准
kscreen-layout-guard accept
# 只抓一次快照(不改基准以外的东西)
kscreen-layout-guard snapshot状态与日志落在用户态状态目录下的 lock/ 与 idle/ 两个子目录,各含布局快照与显示配置备份;不一致的现场记录在 mismatch.log。
边界
- 显示配置按「输出组合」分文件。若屏黑期间输出集合本身变了,直接换回配置可能落空;这时靠校验发现,日志会记下「纠正后仍不一致」。
- 它分不清「故障 flap」和「用户真插了设备」 —— 锁屏期间真插扩展坞,解锁后会被回退到锁屏前那份。
- 基准是「历史上见过的最好布局」。换工位后若要长期用新布局,跑一次
accept。 - 这不是驱动层修复:MST 下游在亮屏后不重新枚举的行为仍在,本机制只是保证结果正确。
涉及组件
| 组件 | 角色 |
|---|---|
kscreen-doctor | 命令行读取/设置显示布局;本机制用它抓布局快照 |
| 显示配置守护服务(KScreen) | 把布局落盘并应用;屏黑期间会被本机制停掉 |
libkscreen | 上面两者的实际提供者;是独立组件,不依赖桌面外壳 |
kscreen-layout-guard | 本项目自建:两套触发源 + 两份基准 + 只升不降的校验 |