nix:梳理屏幕布局管理行为

一、先看清:有三个主体在动布局

这是「改了没用」「莫名回弹」的根源 —— 不是 KScreen 一个人做主,nix 侧还有两个机制在抢方向盘。

主体 何时动手 做什么 数据落点
KScreen(KDE 自带,内核外) 每次布局被应用之后、登录、场景切换 把当前布局写进「连接显示器集合」对应的配置文件;下次同集合回来时套用它 ~/.local/share/kscreen/<集合哈希>(另有 _lidOpened 变体)
kscreen-layout-guard(本机 nix) 锁屏 / 解锁、熄屏 / 亮屏(ScreenSaver 信号 + RandR 事件 + DPMS 查询,无轮询) 屏黑前抓快照并停 plasma-kscreen;亮后校验,不符则恢复快照并重启 KScreen;基准「只升不降」;观察窗口内一律不干预 ~/.local/state/kscreen-layout-guard/{idle,lock}/(layout.norm.txt、kscreen-dir/、guard.log、mismatch.log)
kscreen-layout-replay(本机 nix) 连接集合变化(插拔线、开合盖、连接器重编号、MST 分支重建);以及同场景下的布局改动(判是否用户意图) 按用户意图 > 规则表摆位;拿不准时先挂 60 秒观察窗口,不动手 规则 /etc/kscreen-layout-rules.toml;用户意图 ~/.local/state/kscreen-layout-replay/intent.json;日志 replay.log

三者的当前协作关系(ASCII 版):

连接集合变化 ──▶ replay:命中有没有目标? ──有──▶ 摆位 ──▶ guard accept(基准 = 新布局)
                        │
                        └──无──▶ 不干预

同场景、布局被改动(不是机制干的)
        └──▶ replay:挂起 60s 观察窗口(写 hold 文件)──▶ settle 到期
                  ├─ 仍在 ⇒ 记为该连接集合的用户意图 + guard accept
                  └─ 已回目标 / 机制噪声 ⇒ 只清窗口,不动手

屏黑 → 亮屏 ────▶ guard:基准 vs 当前 ──不一致──▶ 先看 hold;无 hold 才「纠正前 sleep 3 复核」→ 恢复快照 + 重启 KScreen
                                     └──一致────▶ 不动

KScreen 自己 ───▶ 每次应用后写 <集合哈希> 文件(下次同集合自动套用)

二、nix 配置里的文件清单(按「动不动布局」分三层)

2.1 主动干预布局(运行时改 X 布局 / 管 KScreen)—— 只有 2 个 .nix

文件 启用状态 作用 运行时入口 / 单元
modules/system/service/kscreen-layout-guard.nix(+ 同目录 kscreen-layout-guard.sh) 已启用(configuration.nix 第 125 行) 保证「屏黑再亮之后布局还是原来那个」:锁屏/熄屏前存快照,亮后校验,不符才恢复。三条校正判据:① 基准里被启用的输出已不连接 ⇒ 视为换场景、重建基准;② 布局符合规则表 ⇒ 更新基准、不干预;③ 观察窗口有效 ⇒ 暂不干预(且纠正前会再等 3 秒复核窗口与 conforms) user 单元 kscreen-layout-guard-lock、kscreen-layout-guard-watch;命令 kscreen-layout-guard {status,accept,snapshot,on-lock,on-unlock,watch}
modules/system/service/kscreen-layout-replay.nix(+ kscreen-layout-replay.py + kwin-data/kscreen-layout-rules.toml) 已启用(configuration.nix 第 126 行) 连接集合变化后按「用户意图 > 规则表」摆位;规则按「外屏数量 + 尺寸集合 + 内屏开关」匹配,不写连接器名;RandR 事件驱动、去抖 3 秒;同场景改动先开 60 秒观察窗口 user 单元 kscreen-layout-replay;命令 kscreen-layout-replay {status,apply,watch,conforms,settle,dump};规则落 /etc/kscreen-layout-rules.toml

2.2 影响「屏能不能亮 / 能不能出图」,但不改布局 —— 4 个 .nix

文件 作用 动手时机
modules/hardware/gpu-amd-780m-mst-bpc.nix 780M 侧每个 MST 下游连接器的色深上限(debugfs max_bpc);决定菊花链带宽够不够三屏同时亮 开机 oneshot amdgpu-mst-bpc + udev(热插拔 / 链路变化 / 连接器重建)
modules/hardware/display-resume-wake.nix 唤醒后外接屏黑屏的补救:X 侧 DPMS 关→开一次,让显示器自己醒(实测不动几何) powerManagement.resumeCommands → user 单元 display-refresh
modules/hardware/gpu-nvidia-4060-egpu.nix eGPU 4060 驱动钉版 + PRIME 输出(prime.offload + reverseSync)—— 决定 4060 侧接的屏能否出图 内核 / 驱动层(开机)
modules/hardware/gpu-mode.nix(+ gpu-mode.sh) 显卡「宿主 / 直通」模式切换机制(状态在 /var/lib/gpu-mode/<BDF>.mode,重启生效);直通后宿主少一块卡 ⇒ 间接改变可用输出 开机 gpu-mode-apply(早于 display-manager)

2.3 只取证 / 只提供能力 / 已停用 —— 4 个 .nix

文件 说明
modules/hardware/gpu-nvidia-4060-boot-probe.nix 纯取证:开机后 T+0 / +20s / +60s 各采一次(两侧 card1-* 状态 + NVIDIA/DFP/MST 计数),写 /var/log/egpu-boot-probe.log,不改任何状态
modules/system/service/kwin.nix KWin 脚本(centerspan:Meta+Shift+X 居中 / Shift+Z 跨屏)+ 窗口规则(按激活窗口路由快捷键)。不碰输出布局
modules/system/service/displayserver.nix 显示管理器 / greeter、默认会话(X11)、X11 键盘布局(xkbLayout = cn)。没有输出布局规则
modules/hardware/display-lenovo-p27h-edid-pin.nix 已停用(configuration.nix 第 28 行注释,2026-10-18):曾给写死的 DP-9 喂固定 EDID 来断「EDID 被反复重读」的环路;该连接器号后来不存在 ⇒ 内核参数空转,故停

三、时机对照:什么时候谁会动手

时机 动手的主体
开机gpu-mode-apply(显卡绑定)→ amdgpu-mst-bpc(色深)→ display-manager;登录后 replay 跑一次 startup apply 并以当时的签名作为「本场景」基准、guard 起两个监听
热插拔 / 链路变化amdgpu-mst-bpc(udev)→ replay(按用户意图或规则表摆位)→ guard(校验、必要时纠正)
合盖 / 开盖KScreen 按「集合 + 开盖变体」套用它自己记的那份;replay 按用户意图/规则表纠正;guard 校验
锁屏 / 解锁guard(lock 路径):锁屏抓快照 + 停 plasma-kscreen;解锁恢复快照 + 起 KScreen + 校验
熄屏 / 亮屏guard(idle 路径):屏关时停 KScreen(不抓快照);屏亮时校验,不符才纠正
睡眠唤醒display-refresh(DPMS 关→开,只唤 sink)→ replay / guard 各自按事件处理
用户手动改布局(显示设置里点应用)replay 开 60 秒观察窗口(期间 guard 也不动)→ 窗口到期仍在 ⇒ 记为该连接集合的用户意图(见第五节)

四、数据落点与排查命令

内容 位置
布局规则(声明式意图)/etc/kscreen-layout-rules.toml(源:modules/system/service/kwin-data/kscreen-layout-rules.toml)
用户意图(按连接集合签名,优先于规则表)~/.local/state/kscreen-layout-replay/intent.json
观察窗口标记(guard 也读它)~/.local/state/kscreen-layout-hold(内容是到期 epoch 秒)
guard 的基准与配置快照~/.local/state/kscreen-layout-guard/{idle,lock}/
guard 日志 / 不一致记录 / 纠错节流戳~/.local/state/kscreen-layout-guard/{guard.log,mismatch.log}、{idle,lock}/.last_fix
replay 日志~/.local/state/kscreen-layout-replay/replay.log
KScreen 的场景记忆~/.local/share/kscreen/<连接集合哈希>(每模式一份,另有 _lidOpened)
eGPU 开机取证/var/log/egpu-boot-probe.log

常用排查命令:

kscreen-layout-replay status                  # 当前输出 + 命中的目标 + 是否已符合(不动任何东西)
kscreen-layout-replay conforms; echo $?       # 0=当前布局正是目标(guard 纠正前问它)
kscreen-layout-guard  status                  # 基准(idle/lock)+ 当前实际 + 最近日志
xrandr --query                                # 第一行 current = 虚拟屏尺寸
kscreen-doctor -o                             # KScreen 视角的输出表(含 mode id)
systemctl --user is-active plasma-kscreen kscreen-layout-guard-watch kscreen-layout-guard-lock kscreen-layout-replay
rm ~/.local/state/kscreen-layout-hold         # 取消正在进行的观察窗口

五、用户手动改布局:从「改了没用」到「记住」(2026-10-19 21:0x 落地)

原现象:在「显示和监视器」里改布局 → 点「应用」→ 临时生效 → 确认倒计时点「接受」→ 变回老样子。(实测:把竖屏挪 100px,2 秒内被摆回。)

根因 证据
① 确认对话框还没走完,布局已被 replay / guard 改回去 ⇒ 点「接受」保存的是被改回的那份挪 100px 后 2 秒回弹;replay.log「同一连接集合但布局与规则不符 ⇒ 重放」+ guard.log「与基准不符 ⇒ 纠正」
② 服务重启后第一条 RandR 事件被当成「换场景」⇒ 立刻按规则盖回cmd_watch 原写 last_sig = None;21:00:13 日志「[randr] 布局与规则…不符 ⇒ 应用」
③ guard 比 replay 先醒(去抖 2 s vs 3 s)⇒ 观察窗口还没落盘,guard 已经动手21:04:42 guard.log「与基准不符但 10 分钟内已纠正过 ⇒ 节流跳过」(这次只是被节流救了)

落地改动(gen263~gen265)

位置 改动
kscreen-layout-replay.py① 用户意图层 intent.json(键=连接集合签名,如 eDP|DisplayPort-10:1440x2560,DisplayPort-7:2560x1440),cmd_apply 与 cmd_conforms 都先查它 ⇒ 有意图时优先于规则表 ② 60 秒观察窗口 kscreen-layout-hold + 派生 settle 定时收尾 ③ 护栏:KScreen 刚重启(<60s)或 guard 刚纠正 ⇒ 判机制噪声,立即按目标重放、不开窗 ④ cmd_watch 启动用 startup 返回的签名做基准
kscreen-layout-guard.sh① verify_and_maybe_fix 开头读 hold 文件,窗口内「暂不干预」(覆盖 lock / idle / watch 三条路径)② 纠正分支前 sleep 3 复核窗口与 conforms,避开与 replay 的时序竞争

真机验证 A —— 机制侧(21:03~21:10,home 组合:eDP + P27H 横屏 + 竖屏)

步骤 结果
挪竖屏 +2560+900 → +2560+1000+3s / +13s / +28s / +43s 全部保持 1000(改前是 2 秒回弹)
replay 反应「同场景下布局被改动(非本机制发起)⇒ 挂起 60s 观察」
guard 反应(先清节流戳,让它真会纠正)「watch-on: 布局观察窗口内(用户可能正在显示设置里改布局)⇒ 暂不干预」
窗口到期「观察窗口结束:布局仍保持 ⇒ 已把连接集合 … 记为用户意图」;intent.json 里 DisplayPort-10 2560,1000;随后 guard accept 抓新基准
收尾(删意图 + 回规则位)conforms 退出码 0,布局回到 +2560+900

真机验证 B —— 用户在系统设置里真改(21:14~21:15,改后:eDP 1440x900+1440+0、DP-7 2560x1440+1440+900 primary、DP-10 1440x2560+0+305 left)

时刻 事件
21:14:13replay.log「同场景下布局被改动(非本机制发起)⇒ 挂起 60s 观察」——用户改动没有被立刻盖回
21:14:17guard.log「布局观察窗口内(用户可能正在显示设置里改布局)⇒ 暂不干预」×2(去抖带来的两次事件)
21:15:14settle:「观察窗口结束:布局仍保持 ⇒ 已把连接集合 … 记为用户意图」;intent.json 三个输出与用户所改一致
之后kscreen-layout-replay conforms → 0;此时起 guard 不再纠正、replay 会按它恢复

运维口径

  • 想让某个连接模式回到规则表:删 intent.json 里对应键(或整个文件)⇒ 下次事件起按规则表。
  • 想取消正在进行的观察窗口:删 ~/.local/state/kscreen-layout-hold。
  • 意图只对「该连接集合」生效,换插法/换屏走别的键,互不干扰。
  • 限制:意图只恢复 enable / position / rotation,不恢复分辨率(mode);改分辨率请进规则表。
  • 系统设置若提示「没有找到 kscreen 后端」:见第七节(是旧窗口缓存,不是没装)。

六、规则表当前 4 条(/etc/kscreen-layout-rules.toml)

规则名 匹配条件 目标布局
office-3x-2560x1440-horizontal外屏 3 块,全是 2560x1440内屏关;外屏 0,0 / 2560,0 / 5120,0 横排
home-p27h-plus-vertical外屏 2 块 = 2560x1440 + 1440x2560内屏 0,0;横屏 0,900;竖屏 2560,900 左转
single-2560x1440-right外屏 1 块 2560x1440内屏 0,0;外屏 1440,0
laptop-only外屏 0 块内屏 on,0,0(虚拟屏随之收回到内屏尺寸)

改规则的唯一入口:改 modules/system/service/kwin-data/kscreen-layout-rules.toml → nixos-rebuild switch;规则表本身不会被任何运行时动作改写。

七、系统设置提示「没有找到 kscreen 后端」

判据先行:不是后端丢了 —— plasma-kscreen active、DBus 上有 org.kde.KScreen、kscreen-doctor -o 能读到全部输出。

项 说明
真因那个 systemsettings kcm_kscreen 窗口打开期间,guard 的纠正路径 restore_dir + systemctl --user restart plasma-kscreen 重启过后端 ⇒ KCM 缓存的 DBus 连接失效,窗口里就永远显示"没有后端"
本次证据窗口 20:14:01 打开(ps -o lstart),后端在 20:25:41、21:01:43 各重启一次 ⇒ 窗口报错;kill 后重开 systemsettings kcm_kscreen 正常
处置关掉该设置窗口重新打开(不需要装任何包、不需要重启会话)
何时会遇到布局被 guard 纠正过(锁屏/熄屏后布局回弹)之后,若设置窗口一直开着