Loading
CamelliaV の BLOG
0%
INITIALIZING
DOC_ID // 3ebca1ONLINE

[临时AI稿][2026.9.30]Waywallen 双屏折腾复盘:两张壁纸、一个音源,与重启后互换的屏幕编号

2026-9-30
UPDATED: 2026-10-1
复盘笔记
READ 13 MIN/COUNT 5103
#开发#复盘#实用
CamelliaV の BLOG
💡
本文由astra撰写
上一次把 Karousel 的双屏串窗问题修到能用之后,我终于能让外接屏继续滚动平铺、笔记本屏独立放窗口。接下来的愿望很自然:两块屏都放动态壁纸,各选各的;声音则只从我指定的一侧来。
结果,窗口分开了,壁纸和音频还没有真正分开。两侧不同的动态壁纸不能稳定保留,两个实例又可能同时播放声音。表面上像是“再加两个设置项”,查下去却涉及恢复顺序、屏幕身份、进程启动和音频控制的共同约束。
这篇承接
🖥️
[临时AI稿][2026.9.30]Karousel 双屏折腾复盘:窗口归属、85 像素与没有生效的热重载
,记录 Sep 30, 2026 这次 Waywallen 修复。最后落地的是一份已部署的本地补丁:两屏各自恢复壁纸,音频只有一个可选来源;后台重启后,即使两屏的连接编号互换,也不会认错。

从 Karousel 接过来的,究竟是什么问题

前一轮 Karousel 解决的是窗口管理边界:主屏的网格不能再把笔记本窗口卷进去,主屏滚出的列也不能拿笔记本当作“屏外”。最终保留的方案是主屏平铺、副屏浮动,并没有给每块屏重写一套 Karousel。
这次需求落在另外一层。显示器能独立放窗口,不代表壁纸程序的状态也是按显示器保存的;有两个画面,也不代表应该同时有两份声音。
我把目标拆成三条可以检查的约束:
  • 只更换笔记本壁纸时,主屏当前壁纸和渲染实例不应被替换。
  • 无论两侧使用相同还是不同壁纸,音频都只来自一个选定的渲染实例。
  • 重启后台、屏幕重新连接之后,壁纸和音频选择仍应跟随原来的屏幕。
Karousel 的代码和持久配置在这一轮没有继续修改。要处理的是 Waywallen 自己的屏幕、壁纸和音源归属。

第一处纠正:底层并不是完全没有分屏能力

最初看到“两边不能用不同壁纸”,很容易直接开始添加分屏接口。但读源码后发现,后台已经有指定目标的能力:应用壁纸时可以传入 display_ids,只重新绑定这些屏幕。详情界面也已有 Apply to 选择。
所以,不能把用户遇到的结果直接翻译成“从零实现多屏渲染”。问题可能出在目标识别、状态恢复,或者某条控制路径绕过了已有的分屏能力。
一开始还检查了 KDE 壁纸端是否把两块屏报成了同一个目标。现场只有主屏启用了 Waywallen,笔记本仍是静态壁纸;备份配置并为笔记本启用 Waywallen 后,后台确实注册出了两个显示目标。后续确认,两块屏有不同的持久实例标识。没有证据支持“它们被识别成同一个屏幕”这个最初的怀疑。
这里还有一个容易混淆的细节:显示名称在初始化时可能先是通用名称,之后才更新成显示器型号;显示别名又允许用户修改。名称适合展示,不能单独承担持久身份。
真正明确的恢复错误,是稍后在另一段逻辑里找到的。

全局最后一张壁纸,盖过了每块屏自己的记忆

关键函数叫 startup_last_wallpaper。原来的优先级是:
这个默认值在单屏场景里很容易理解:重启后继续播放最后选过的那张。代码注释也明确表达了这种意图,甚至已有单测要求“全局优先”。
但在双屏场景里,最后一次操作通常只是某一块屏的选择。比如主屏已经设为 A,随后给笔记本设为 B。如果启动时每块屏都先读全局的 B,两块屏各自保存的记录就失去了意义。
已有分屏应用接口解决的是“现在把这张图放到哪里”;启动恢复函数却重新把状态压成了“全局最后一张”。这解释了为什么有目标接口,还不能保证两屏的独立设置持续有效。
修改后的规则是:
实现直接复用原本就存在的按屏查询逻辑:
显式对全部已连接屏幕应用同一张壁纸时,应用流程本来就会更新各屏记录。因此无需再靠“启动时全局优先”实现全屏统一。全局值留作没有独立记录的新屏幕的后备,语义更清楚。
对应测试也不能只把旧断言改成新断言。这次把主屏、笔记本的不同记录和音频来源一起写入配置,实际刷新到磁盘,再重新加载,检查它们仍然分别存在。

两个画面为什么会变成两份声音

现场配置启用了 duplicate_renderers_for_same_wallpaper。它允许同一张壁纸在多块屏幕上分别运行渲染器,而不是共享一个实例。两侧原本就用不同壁纸时,也自然需要不同渲染实例。
画面独立之后,每个实例都可能开启自己的音频。如果后台只提供一个全局“允许播放声音”,两个实例都满足条件,就会一起响。
关闭“同壁纸使用独立渲染器”只能改变其中一种情况:两屏恰好播放相同壁纸时可以共享。它无法满足“两侧不同壁纸,但只要一份声音”的需求。
因此这次增加的不是某个视频播放器上的临时静音开关,而是由后台路由器统一决定音频归属:
  • 自动模式:按稳定的屏幕标识顺序,从已绑定非静态图片壁纸的屏幕中选择一个音源。
  • 指定屏幕:只允许这块屏当前绑定的壁纸实例发声。
  • 指定屏幕断开:保持静音,不擅自转移到另一块屏。
  • 同一个渲染器被多屏共享:它仍然只是一份音源。
“自动”没有实现成“跟随当前鼠标”或“跟随焦点”,也不会分析哪段音轨此刻更值得播放。需要确定来自哪侧时,直接指定屏幕即可。该屏壁纸本身被禁用音频、没有音轨或被暂停时,允许的音源也可以不出声。
目标约束准确地说是最多一个音源,不是要求无论什么情况都必须有声音。

只在启动以后静音,还是晚了一步

做出音频来源选择之后,还有一个时序问题:新渲染器启动时,如果已经把音频设备打开并开始播放,后台等它注册完成再发送静音,仍可能在切壁纸时短暂双响。
这次把限制前移到了初始化消息。对于声明了 enable_audio 设置的渲染器,发给新进程的初始值强制为 false,但保留用户原本保存的音频偏好。之后由路由器在屏幕绑定和归属明确时,重新计算是否允许音频。
切换来源的顺序也明确下来:
如果禁用原音源的控制发送失败,本轮不会继续启用新的音源。已发送成功的音频开关才记入缓存,避免把失败当成完成。
另一个入口是普通设置热更新。原来的流程会把变动的渲染设置发给所有匹配实例;如果它直接广播 enable_audio=true,刚建立的单音源规则就可能被绕过。现在这个字段交由路由器统一应用,其他设置继续沿用现有热更新路径。
音量、全局音频开关、手动静音和自动暂停没有被替代。是否属于选定音源,只是决定某个实例能否播放声音的一个额外条件。

屏幕编号只能用于当前连接

后台的 display_id 是连接时分配的编号。它适合当前请求路由,但不适合写进配置后期待下次仍代表同一块屏。
这次新增了两处数据:
  • 控制协议中的 DisplayInfo.settings_key:优先使用壁纸端提供的稳定 instance_id,没有时才回退到显示名称。
  • 配置中的 GlobalSettings.audio_display:保存所选屏幕的持久标识;空字符串表示自动选择。
这不是发明一套新的硬件识别机制,而是把原本用于各屏配置的持久标识提供给界面和音频逻辑,让不同层用同一份身份信息。
界面的壁纸目标选择也随之改成保存持久标识。真正发起应用请求时,再把它转换成当前连接的 display_id。这样,换壁纸、切换标签页或重开 UI,不必反复选择目标;后台重连后也不依赖旧编号。
这里专门处理了一个危险的小边界:原接口把空目标列表解释成“所有屏幕”。如果用户选择的屏幕断开,查不到对应的当前编号,不能把结果悄悄变成空列表再发出去。因此,显式选中的目标不完整时,“应用”按钮禁用。用户选择 All 时,才明确表示应用到全部屏幕。
按钮文字也区分 Apply to all displays 和 Apply to selected displays,让操作范围出现在真正点击的位置。

回归测试也走过一次弯路

这一轮没有重演上次 KWin 长时间执行旧脚本的加载问题,但仍然不是“写完一次全部通过”。
原有库单测先通过;补上双屏音源场景后,新测试曾在“右侧应该静音”的断言失败。检查后发现,是测试搭建顺序不符合路由器现有的资源回收行为:测试提前注册多个渲染器,再重绑第一块屏,尚未绑定的另一个实例被当成孤儿回收了。
于是断言检查的已经不是预期中的双屏状态。修正方式是按实际生命周期搭建测试:先建立左侧绑定,再注册并绑定右侧;需要替换时再创建替换实例。没有为了让测试通过而关闭生产代码里的回收机制。
最后完整结果是:
新增和调整的测试覆盖独立壁纸恢复、持久音频选择、新进程静音初始化、两侧音频归属、共享渲染器、手动静音组合,以及指定屏移除后的行为。保留的跳过项是原有测试,并不是把新失败的用例忽略掉。

真机验证:看音频流,而不只看按钮状态

主屏保留原来的动态视频,笔记本单独切换到《Reverse: 1999 - Vertin》。这一步检查的是主屏渲染器 ID 是否变化:结果没有变化,说明单侧应用确实没有顺手替换另一侧。
音频验证时,临时取消会干扰观察的自动暂停条件,让两侧都有机会持续播放,然后按“主屏 → 笔记本 → 主屏”切换音频来源。每次都同时读取后台实例状态和 PulseAudio/PipeWire 的播放流,用进程 PID 对应到实际渲染器。
所选音源
活动音频流对应的进程
另一侧状态
主屏
主屏视频渲染器,恰好一条
muted
笔记本
笔记本视频渲染器,恰好一条
muted
再次主屏
主屏视频渲染器,恰好一条
muted
活动流的检查排除了已经静音或暂停的流,而不是仅仅数系统里出现过多少条记录。这样能把“界面显示选中了某屏”和“实际由哪个进程向音频系统输出”联系起来。
测试结束后恢复了原来的自动暂停策略,最终保留主屏作为音频来源。否则留下一个为了测试而强行持续播放的配置,也不能算完成日常使用场景的修复。

最有说服力的证据:重启后,两个编号真的换了

最后重启了 systemd 用户后台服务,让 KDE 壁纸端重新连接。恰好这次连接顺序发生变化:
屏幕
重启前编号
重启后编号
恢复结果
主屏 Y27q-30 / HDMI-A-1
1
2
原来的 4K 动态视频,仍是音频来源
笔记本 NE160QDM-NZB / eDP-1
2
1
独立的 1080p 动态视频,保持静音
这比“重启后看起来没变”更有价值。临时编号变化了,正好能检验选择到底跟的是编号,还是持久身份。
后台日志分别记录了给一块屏恢复条目 14063、给另一块屏恢复条目 119。两个渲染进程的参数确实指向不同视频,系统里只剩主屏进程的一条壁纸音频流。配置中的各屏最后壁纸、显示别名和音频来源也都保留下来。
这里重启的是后台服务,没有重启整机,也没有拔插物理显示器。这两者不能被“编号互换的重连测试”自动替代。

部署和验证的边界

这台机器的 /usr/local/bin/waywallen 是补充 Qt/QML 搜索路径的 wrapper,真正的 daemon 是 waywallen.bin。直接用裸 ELF 覆盖 wrapper,可能使 daemon 能启动、拉起的 UI 却找不到 QML 模块。这是本机已有的部署约束,不是这次新发现的双屏根因。
部署前备份了旧程序、wrapper 和配置,随后只替换 daemon ELF 与 UI,并核对安装后的 daemon 与构建产物哈希一致。当前会话用于观察窗口的临时 KWin 脚本已经卸载,临时打开的 UI 也已关闭。
这次确认到的范围如下:
层次
证据
源码
库单测 394 项通过,1 项原有跳过;daemon 和 Qt UI 构建通过。
双屏视频
单侧换图不替换另一侧渲染器;两侧使用不同视频。
音频
三次来源切换均检查实际活动音频流及 PID;后台重启后仍只有主屏音频流。
持久化
配置重新加载测试通过;真机后台重启、编号互换后仍正确恢复。
界面
新版 UI 成功运行,壁纸详情目标区域完成截图检查;设置页选择项编译通过,其后台热更新已实测。
尚未覆盖
整机重启、物理热插拔、长期运行;未对每一种第三方渲染插件分别做双屏音频实测,也未完成设置页全部交互的视觉验收。
不同插件是否完整遵守音频控制协议,仍值得后续检查。本次真实音频链路验证用的是视频渲染器,不能把这一结果扩大成所有 scene、web 插件的完整兼容性认证。
补丁已经部署,但源码尚未提交为 commit,也没有提交上游。普通后台重启会保留当前配置;以后重新安装上游程序,仍可能覆盖本地二进制,需要从保留的源码重建。

现在怎么用

两块屏的 KDE 壁纸类型都设为 Waywallen。打开壁纸详情,在底部 Apply to 选择主屏或笔记本,再点应用。想让两侧同步换图时,显式选择 All。
在 Settings → Wallpaper audio display 中选择声音来自哪块屏;中文标签是“壁纸音频来源屏幕”。当前配置指定主屏,所以笔记本可以播放另一张动态壁纸,同时不抢声音。指定主屏断开时保持静音;若希望只在当前连接的屏幕里选一个,可以改成自动。
最后留下的使用状态很具体:主屏原壁纸继续播放,笔记本有自己的壁纸,音频来源主屏,原来的自动暂停规则照常生效。

从这两轮双屏修复里留下什么

Karousel 那一轮,最重要的转变是把“看起来在副屏的窗口”变成真正不受主屏网格管理的窗口。Waywallen 这一轮,则是把“现在能对某屏应用壁纸”延伸到启动恢复、屏幕重连和音频控制的全过程。
两轮都暴露出同一个问题:一个局部接口正确,不代表整个生命周期遵守同一条规则。Karousel 可以在退出平铺时又把窗口拉回主屏;Waywallen 可以在应用时区分目标,却在重启时优先读全局记录,把区分抹掉。
这次相对扎实的部分,是把每条要求都找到了对应证据:单侧换图看另一侧进程是否变化,单音源看实际播放流,持久屏幕选择看编号变化后是否仍然正确。编译通过、测试通过和真实行为通过分别回答不同的问题。
下一次再遇到“多屏支持已经有了,但用起来还是不独立”,先沿着状态的保存、恢复和控制入口查一遍,往往比立刻再加一层分屏接口更接近根因。

记录与参考

  • 本地修复记录:~/code/waywallen/docs/multiscreen.md。
  • 本地备份与验证材料:~/.local/state/waywallen/backups/20260930-multiscreen/,包含旧 daemon、UI、wrapper、修改前配置、测试日志、后台重启后的状态和目标选择截图。
  • 主要实现位置:src/settings.rs、src/routing/router.rs、src/renderer_manager.rs、src/ws_server.rs、proto/control.proto,以及 UI 的设置页、壁纸详情页和显示模型。
  • 本文由执行修复的 AI 助手依据会话、代码和留存验证结果整理。它描述的是本机这份补丁与本次测试范围,不是上游所有版本的现成功能说明。
NAVIGATION // Related Articles
Loading...
© 2024-2026 CamelliaV