我在两台 Windows 电脑上都用 Parsec 和 Moonlight。远程的时候,我想要一块专门给远程用的虚拟屏:分辨率、刷新率和缩放按远程的习惯设好(我用的是 4K、60Hz、200% 缩放),不用迁就主机上接着的那台物理显示器。
建一块虚拟屏不难,难的是决定它什么时候出现、什么时候消失。这篇讲的就是这个时机问题。帧率和画质不在这篇的范围里,我关心的是连接和桌面用起来顺不顺。
一开始的规则错在哪
最早我给自己定的规则很直觉:Parsec 连上,就切到虚拟屏;Parsec 断开,就恢复原样。
后来我把这条规则改掉了。断开往往不代表收工,可能只是网络抖了一下,也可能是我换了个客户端接着连。如果每次断开都恢复物理屏,窗口就会被挤回原来的显示器;重新连上再切回虚拟屏,窗口又被重排一遍。远端的工作明明还在,桌面却被来回折腾。
我真正想要的是:远端的工作还在,只是人暂时离开。 于是规则变成了:
- Parsec 断开、退出,或者主机上的 Parsec 进程重启:保留虚拟屏,下次连上直接复用。
- Moonlight 只是断开了串流:同样保留,因为串流里的应用很可能还在跑。
- Moonlight 的会话和应用都真正结束了:这才算收工。恢复原来的物理屏布局,关掉桥接自己创建的那块屏。
换句话说,“断开”和“结束”是两件事。把这两件事分清,后面的设计基本就定下来了。
怎么搭起来
中间是我在本机写的一个小桥接,它要做三件事:
- 判断有人连进来。 Parsec 有没有入站连接,是从主机日志里判断的。
- 建屏。 Parsec 第一次入站时,桥接先把当前的物理屏布局存下来,再通过 Foundation Sunshine 的本地控制接口创建一块 Zako 虚拟屏。Moonlight 这一路的主机端本来就是 Foundation Sunshine,两边用的是同一块屏。
- 收尾。 按上面的规则决定是保留还是恢复。
桥接跑在已经登录的交互桌面里,因为它要改的就是这个桌面的显示布局。
虚拟屏的生命周期。“断开”进入保留状态,“结束”才回到物理布局。
凌晨发现的坑:屏幕在,桌面不一定在上面
初版判断“虚拟屏是否就绪”,只看虚拟设备在不在。30 日凌晨我发现这样不够:驱动列表里那块屏好好地挂着,Windows 却可能早就把这一路输出停用了。设备还在,桌面已经不在它上面了。
修正的思路是,不再问“设备在不在”,改为问“Windows 现在实际在往哪些输出上画”:
- 虚拟屏不在活动输出里,就先把它加入扩展桌面;
- 然后切到“仅虚拟屏”;
- 如果布局本来就已经是目标状态,就什么都不做。
第三步看起来最不起眼,其实很重要。重新应用一遍同样的布局也会闪屏,窗口也可能跳位置。检查状态时要看它有没有真正生效,而不是看它存不存在;状态已经对了,就别再去动它。
修好之后,断开时保留这一块已经正常了;主动结束应用后,日志里也能看到恢复流程按预期走完。
只收拾自己建的东西
桥接会动显示布局,所以它必须知道哪块屏是自己的。我定了几条规矩:
- 不是桥接创建的虚拟屏,一律不接管;
- Moonlight 还在忙的时候,不回收;
- 说不清归属的时候,宁可留着,并且报告出来;
- 如果屏幕被外部关掉,桥接会补做一次布局恢复;但只要 Parsec 还连着,就不会马上再建一块。
这里的取舍很简单。错留一块屏,代价不过是多占一路显示输出;错删一块正在用的屏,打断的可能是另一个正在进行的会话。两者的代价差得很远,所以拿不准的时候就选保留。
几点体会
- 需求本身值得重新定义。“断开就恢复”听起来很干净,实际用起来很折腾。把“断开”和“结束”分开以后,很多分支自然就消失了。
- 检查要看生效,不要看存在。 驱动里有,不等于系统在用。这个道理在显示器上成立,在别的系统状态上大概也成立。
- 已经正确的状态就别再碰。 对显示布局来说,重复操作不是没有代价的。
用到的组件
- Foundation Sunshine:https://github.com/AlkaidLab/foundation-sunshine,入门文档见 https://www.alkaidlab.cn/docs/en/getting-started/
- Parsec:https://parsec.app/downloads
- Moonlight:https://moonlight-stream.org/
这篇讲的是思路和规则,没有附桥接本身。如果你也用这两个客户端,上面的规则可以直接拿去,按自己的环境实现。
Claude Opus 5.5 辅助书写。