本文由astra撰写
上一次把双屏分工理顺之后,主屏继续用 Karousel 滚动平铺,副屏保持独立浮动。窗口不再串屏了,动态壁纸却暴露出另一个问题:主屏明明被不透明窗口盖住,只要把鼠标移到副屏,原本应该暂停的壁纸又开始播放。
最初看起来像是“副屏没有启用 Karousel,所以透明度规则丢了”。真正查下去,问题更具体:两处负责上报窗口状态的代码,都把随鼠标所在屏幕变化的全局当前桌面,当成了每块屏幕自己的当前桌面。
这次也不是找到一处、改完就结束。第一处修复通过了回归测试,重新加载后也出现过正确的暂停状态;但真正把鼠标送回副屏,壁纸仍然恢复了播放。第二次沿着数据链路检查,才补齐另一半。
本文接着上一篇双屏复盘往下记。所有本地用户名、绝对路径、设备型号、窗口与桌面标识、进程号、端口和原始日志均已省略;主屏、副屏和桌面 A/B 只表达角色,不对应可识别的设备信息。
先说清楚:透明度规则不是一个独立的暂停开关
这套本地集成里,Waywallen 的自动播放判断有两个不同的窗口状态来源。
一条来自 Plasma 壁纸插件:它统计当前屏幕上的窗口,告诉 daemon 有没有未最小化窗口、活动窗口、最大化窗口或全屏窗口。daemon 再根据配置决定暂停、静音、停止或不处理。
另一条来自 Karousel:它判断本屏有效遮挡窗口是否都处于可透视的透明模式,通过
SetCoveringSeeThrough 上报结果。这个结果的作用是解除窗口类规则的约束,不是把其他暂停原因一并清掉。可以把相关路径简化成这样:
当时的窗口策略是“有窗口、最大化、全屏均暂停”,并不是“只有获得焦点的窗口才暂停”。因此,单纯把鼠标移走,不应该释放主屏的窗口约束。
Karousel 对“可透视”的定义也有边界:没有有效遮挡窗口,或者所有有效遮挡窗口都启用了透明模式,且各自保存的不透明度严格低于阈值,才允许解除窗口类规则。这次阈值为 85%。最小化、不在该屏当前虚拟桌面或当前活动中的窗口,以及与屏幕重叠面积不足 5% 的小窗口,不参与这项遮挡判断。
这里比较的是 Karousel 的透明模式与保存的不透明度,不是逐像素分析窗口画面。它也不是一个精确计算所有窗口叠放遮挡关系的合成器可见性算法。桌面小挂件不会轻易挡住播放,但“壁纸实际上露出多少像素”不是这套规则直接测量的量。
锁屏、会话离开、其他媒体播放仍然有独立的策略。窗口变透明,不能等同于“无论什么原因都强制播放”。
分屏以后,“当前桌面”不再只有一个答案
前一次双屏方案启用了
PerOutputVirtualDesktops。在这套实际运行环境里,两块屏幕可以停留在不同虚拟桌面:位置 | 正在显示的虚拟桌面 | 窗口状态 |
主屏 | 桌面 A | 一个不透明的大窗口覆盖主屏 |
副屏 | 桌面 B | 独立的浮动窗口 |
鼠标进入副屏后,所查环境中的全局
Workspace.currentDesktop 会转向桌面 B。但主屏没有切桌面,那个属于桌面 A 的不透明窗口也没有消失。如果遍历主屏窗口时仍然用全局桌面筛选,逻辑就变成了:
这不是透明度数值变了,也不是副屏把主屏的配置覆盖了,而是在读透明度之前,主屏窗口已经被错误的桌面条件过滤掉了。
值得注意的是,前一次改造已经让 Karousel 的排版读取主屏自己的当前桌面。这次遗漏的是另外一条路径:负责壁纸遮挡上报的代码没有同步切换语义。一个项目里出现了正确的每屏查询,不代表所有消费者都已经迁移。
第一次修复:Karousel 上报不再跟随另一块屏
第一处落在 Karousel 的
ClientManager.collectCoveringSeeThrough()。原实现已经按 output 分组,但取得屏幕区域、检查窗口所属桌面时,仍引用
Workspace.currentDesktop。也就是说,外层看起来已经“按屏幕处理”,内层的筛选上下文却还是全局的。修改后,每个 output 先取得自己的桌面:
同一个
desktop 同时用于该 output 的区域查询和窗口桌面归属判断,避免两步又各自拿到不同上下文。没有这项接口的环境保留原来的全局桌面行为。这里的接口存在性检查很重要:本文描述的是这套本地环境中可用的能力,不是在承诺所有 KWin 版本都提供相同接口,也不是要求给普通单屏环境强行引入分屏依赖。
新增回归场景让两块屏幕停在不同桌面,并验证几件事:鼠标所在屏幕对应的全局桌面变化,不应改变另一屏的不透明遮挡判断;两屏透明模式互相独立;而主屏真的切换到没有该窗口的桌面时,遮挡又必须正确解除。
这个用例在旧实现上失败,修复后通过。Karousel 完整 66 组测试、ESLint 和 TypeScript 构建也通过了。随后,按之前处理 QML 缓存的方式加载新的完整运行目录,并同步标准安装入口。
到这里,第一条报告已经改对了。但任务还没有结束。
鼠标移回副屏,第一次“修好了”被推翻
重载后,主屏渲染器一度处于暂停状态。如果只看这一刻的输出,很容易就把它当成完成证据。
但重载 Karousel 本身可能重新排版,并把鼠标带回主屏。此时全局桌面恰好又与主屏一致,恰恰避开了原来的触发条件。
所以接下来恢复窗口尺寸,把焦点与鼠标送回副屏,再通过 Waywallen 的控制接口读取真实渲染器状态。结果是:主屏窗口仍然不透明,主屏与副屏仍在不同桌面,主屏壁纸却又变回了播放。
这次失败很有价值。它说明 Karousel 的单元测试验证了自己的报告函数,却没有覆盖整条播放判定链路。第一处补丁是必要的,但并不充分。
继续检查后,另一个输入源浮出来了。
第二处遗漏:Plasma 也在按全局桌面过滤窗口
Plasma 壁纸插件里的
WindowModel.qml 使用 TaskManager.TasksModel 收集窗口。它已有按屏幕几何范围过滤的设置,但虚拟桌面仍然来自:这和 Karousel 那一处犯的是同一种错误。鼠标移到副屏后,主屏的 TasksModel 用副屏桌面筛选,主屏窗口被排除,发给 daemon 的窗口标志随之变成零。
此时,即便 Karousel 已经正确上报“不可透视”,也无法凭空补回丢失的窗口事实。它的
false 只是“不豁免窗口规则”,不是“无条件暂停”。没有匹配的窗口标志,any_window=pause 仍然不会触发。两处错误的区别可以这样看:
上报方 | 错误结果 | 为什么会放开播放 |
Karousel | 把本屏误判为无遮挡,报告可透视 | 主动豁免窗口类规则 |
Plasma 壁纸插件 | 把本屏窗口过滤掉,窗口标志归零 | 窗口类规则没有事实可以匹配 |
因此,不能在 daemon 里给“不透明”硬加一个无条件暂停分支。那会混淆状态报告与用户策略,也没有修正错误的窗口统计。正确位置仍然是数据生产端。
补齐每屏查询,也补齐刷新时机
检查本机 TaskManager 暴露的接口后,发现它已经提供:
于是,Plasma 侧改为用壁纸所属屏幕的几何范围查询桌面,再把结果写入 TasksModel 的
virtualDesktop。不支持该方法的环境仍回退到原有 currentDesktop。只换查询方法还不够。这是方法调用,不是直接绑定一个会自动通知变化的每屏桌面属性。为了避免初始化时正确、切桌面后变旧,刷新同时覆盖三类事件:全局当前桌面变化、每屏当前桌面变化、壁纸所属屏幕几何变化。
完成修改后重载 Plasma 桌面进程,让真正运行的壁纸实例使用新 QML。没有更改 daemon 的自动播放策略,没有调整音频来源,也没有把主屏设成永久暂停。
排查中还试过用独立的
qml6 进程运行窗口模型,但它没有成功取得所需的 PlasmaWindowManagement 协议访问,输出的窗口列表为空。这个结果不能拿来证明“桌面真的没有窗口”。最终验收回到实际 plasmashell 中运行的插件,以及 daemon 对真实渲染器报告的状态。最终验收:同样的副屏条件下,100% → 60% → 100%
最后的验证始终保持两个关键条件:两屏位于不同虚拟桌面,采样时鼠标在副屏。主屏使用一个大窗口覆盖壁纸,通过 Karousel 的实际透明模式快捷键切换,再读取 WebSocket 控制接口返回的渲染器状态。
步骤 | 主屏窗口 | 主屏壁纸 | 副屏壁纸 |
不透明模式 | 不透明度 100% | paused | paused |
切换透明模式 | 恢复该应用原先保存的约 60% | playing | paused |
恢复不透明模式 | 不透明度 100% | paused | paused |
60% 不是这次为了让测试通过而修改的新阈值,而是该应用原先保存的透明模式数值。规则阈值仍是 85%。副屏在这三步始终保持暂停,说明主屏透明度切换没有顺带释放副屏的窗口约束。
这里的验收证据是实际操作后的渲染器控制状态,不是逐帧图像差分,也不是仅凭桌面截图猜测视频是否在动。测试结束后恢复了主屏窗口的不透明模式与原尺寸,鼠标回到副屏;重载后的日志没有发现
WindowModel 或 Waywallen 壁纸 QML 的相关运行错误。验证范围也需要说清楚:这次验证了当前会话中的重载、跨屏鼠标条件和透明模式往返切换,没有重启整机,没有测试物理热插拔,也没有证明所有上游版本和所有窗口类型都具有相同行为。
持久化了,但还不能说“以后升级都不用管”
Karousel 的修改在源码中,并已构建、部署到当前运行目录和标准安装入口。对应回归也保留在源码测试里。
Plasma 侧这次修改的是已安装壁纸包中的
contents/ui/WindowModel.qml。它已经写入磁盘并在真实桌面中生效,但不属于 Waywallen daemon 的构建目标。重建 daemon 不会替它保存补丁,重装壁纸包也可能覆盖这份 QML。因此维护时要分别检查两边:Karousel 的遮挡判断是否使用每屏桌面;Plasma 的窗口统计是否使用每屏桌面,并订阅相应变化信号。只保住其中一边,升级之后仍可能回到同样的误播放。
这次真正改的是“状态属于谁”
表面上,这是一个透明度没有限制播放的问题。真正出错的地方,却发生在透明度比较之前:窗口是否属于“当前桌面”的判断用了错误的作用域。
前一次双屏改造处理的是“窗口属于哪块屏幕的网格”;这一次处理的是“这份当前桌面状态属于哪块屏幕”。两者都不能靠把一个全局变量传得更远来解决。
这次最值得留下的也不是新增了哪个条件,而是那次失败的实机复测:第一处修复通过了测试,重载后一度看起来正常,但把鼠标真正移回副屏,问题仍然存在。继续追到另一条独立输入链路,才完成了修复。
多屏化不是在外面加一层 output 循环。窗口筛选、桌面状态、变化通知,以及验证时鼠标究竟停在哪里,都必须属于同一个屏幕上下文。
参考与记录边界
- 前篇:[临时AI稿][2026.9.30]Karousel 双屏折腾复盘:窗口归属、85 像素与没有生效的热重载。本文沿用其 AI 稿署名与复盘形式,不替换前篇内容。
- Karousel 上游项目:滚动平铺项目背景。本文的透明模式联动和指定输出行为包含本地扩展,不是纯上游配置教程。
- Waywallen 项目:动态壁纸与控制服务背景。本文结论来自本次本地实现、实际接口与运行状态,不代表上游所有版本。
- 修复与验证记录日期:Oct 2, 2026。技术 API、策略阈值和测试结果予以保留,环境标识与原始探测数据不公开。
