服务:闲置超时黑屏,再次点亮桌面布局混乱

现象

  • 闲置超时导致屏幕关闭后,再点亮时桌面布局错乱:
  • 竖屏消失,只剩两块屏在用
  • 剩下的两块被重排去填补空位(位置整体偏移)
  • 不会自愈 —— 亮屏后一直是错的,必须手工把竖屏拉回来

根因(三层,逐层可追)

① 屏黑期间,显示链路的 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本项目自建:两套触发源 + 两份基准 + 只升不降的校验