把什亭之匣画到 Windows 登录屏上

9 月 1 日晚上心血来潮,想把 Windows 11 的登录界面换成什亭之匣。

什亭之匣是 Blue Archive 里的一块平板,游戏内可以直接操作。我要的效果是登录屏的背景换成它的场景, 触发登录动作时播放其中的唤醒互动。Windows 的模糊登录界面仍旧由系统自己绘制,Plana 位于它之上。 改动只限于登录屏,进入系统之后不再涉及。

白天场景的登录屏,时钟被人物挡住一部分
一周之后的效果。场景、时钟和人物都由这个程序绘制。

这台机器承担日常工作,不适合随意改动。所以我没有直接安装,先在虚拟机里做, 量出崩溃率和登录延迟这些数字。数字不理想就不装,过程本身也足够写一篇。

成品是下面这段视频。

Shittim Logon 演示

Windows 只给了一层

这四层里,Windows 只开放了最下面那一层。

最先想到的入口是凭据提供程序(credential provider)。它是一个 COM 组件,LogonUI.exe 会把它加载进自己的进程。 但它只能描述一个磁贴,也就是登录界面中间那块区域,里面放头像、用户名和密码框。 磁贴的内容是一份固定的字段表,图片、标签、输入框、按钮。没有帧回调,绘制范围也不会超出这个矩形。

于是四层落到了三种机制上,风险完全不同。我把它们分成三个阶段,每个阶段都能单独撤回:

阶段机制风险
1用注册表指定锁屏背景零。删掉两个键值就恢复原状
2一个凭据提供程序 DLL中等。它活在 LogonUI 进程里,它出错等同于登录屏出错
3在安全桌面上跑一个渲染进程高。这正是安全桌面的隔离要阻止的事

每个阶段先在虚拟机里过稳定性闸门,通过之后再做下一个。第三阶段默认算不合格,除非有数据证明。

第一阶段:先量登录屏对一张图做了什么

第一步是把一张测试卡设成锁屏背景,看 Windows 会怎么处理它。 我故意把这一步排在渲染器前面。渲染器该按什么分辨率、什么裁切方式、什么亮度出图,答案都在这张卡上, 不先量出来只能靠推测。

Windows 11 原版锁屏
原版锁屏
第一阶段,一张静态的场景图
第一阶段,纯注册表,一张静图

结论后来一直在用。图是满帧铺开的,四个角标正好落在画面边缘,中心十字落在 960,540,不裁切也不留黑边, 所以按屏幕原生分辨率出图就是像素对像素。但画面会被压暗,密码界面比锁屏暗得多。 0 到 255 的灰阶在锁屏上到不了白,在密码界面上勉强过中灰。1 px 的棋盘格被平均成灰色, 细于 2 px 的内容不必再画。

锁屏上的场景
锁屏
密码界面上的同一张图,明显更暗
密码界面,同一张图

后来把这套处理拟合成了两组数字:

界面缩放增益
锁屏1.0000.820
密码界面1.0200.281

密码界面那 2% 的放大,后面让我吃了一次亏。

这里还留了一个坑。锁屏图有两条注册表通道可以设。Policies\Microsoft\Windows\Personalization\LockScreenImage 这一条只在企业版和教育版上生效,Pro 版会把它当成不存在,但值写得进去,注册表里也查得到。 真正管用的是 PersonalizationCSP

第一天的测量记录里就写着这件事,后面好几天代码还是往策略键里写,卸载程序里也留着清理它的逻辑。 一直没有暴露,因为那张图从未显示出来,24 ms 之后覆盖层会画一张几乎相同的图盖在上面。 类似的情况后来又出现过几次,结论早就写下来了,代码没有跟着改。

第二阶段:一个不提供登录方式的凭据提供程序

第二阶段的 DLL 注册进系统,被 LogonUI 加载,然后返回零个磁贴。它不持有任何凭据对象,也不参与认证。

这样安排是为了先回答一个更小的问题:加载这个 DLL 本身会不会让 LogonUI 不稳定。 我们自己画的界面有没有影响,那是后面的事。

有三条规矩是动手之前就定死的。

  • 不实现 ICredentialProviderFilter。这个接口可以隐藏别的提供程序。把微软自己的密码磁贴藏起来, 自己再出一次错,机器就登不进去了。只加磁贴,不减磁贴。
  • 四种使用场景都要处理。同一个 DLL 会在 CPUS_LOGONCPUS_UNLOCK_WORKSTATIONCPUS_CHANGE_PASSWORDCPUS_CREDUI 下被枚举。最后一个覆盖 UAC 相关的凭据提示、runas,以及任何调用 CredUI 的程序。 这里出问题的表现是几天之后 UAC 对话框不能用,跟登录屏看不出关系。
  • 任何错误路径都返回成功但没有磁贴,不抛异常。

一轮枚举在日志里是这样:

DLL_PROCESS_ATTACH
[+0ms]   SetUsageScenario cpus=1 (CPUS_LOGON)
[+16ms]  Advise
[+16ms]  GetCredentialCount -> 0 tiles

结果是 LogonUI 正常运行,零崩溃,零转储,微软自己的密码磁贴完好。 cptest.exe 直接通过 COM 驱动这个对象走完四种场景下的每个方法,48 项检查零失败, 另外跑了 2000 次创建和释放,没有悬挂引用。虚拟机里 250 次锁定解锁循环零失败,每一次 DLL 都被加载。

登录本身不受影响,开启 AutoAdminLogon 之后能一路进到桌面,进去时提供程序是加载着的。 没有用手输密码验证过,因为宿主机没办法往运行中的客户机里打字,这一点后面会讲。

有两处和文档给人的印象不一样。

第一处是 LockWorkStation 锁屏之后拉起的 LogonUI 报的是 CPUS_LOGONCPUS_UNLOCK_WORKSTATION 从头到尾没有出现过。现在的 Windows 里锁屏和登录屏是同一套界面,解锁走的也是登录那条路。 如果有代码只处理 CPUS_UNLOCK_WORKSTATION,它在锁屏时不会被执行。

第二处是凭据对话框由 CredentialUIBroker.exe 承载,而这个进程确实会加载我们的 DLL。这件事试了三次才确认:

做法结果
在计划任务里跑 Get-Credential退出码 1,没有界面,没有日志
runas /user:跑起来了,没有日志
直接调 CredUIPromptForWindowsCredentialsWDLL 出现在 broker 进程里

前两次都不算证据。Get-Credential 在画出任何东西之前就失败了,runas 在控制台里问密码,根本不经过 CredUI。 一个从未弹出来的对话框,不能用来证明它跳过了我们。第三次直接调 API,日志才有了内容:

ui processes : CredentialUIBroker(660), creduiprobe(5912)
our dll in   : CredentialUIBroker(660)
log          : DLL_PROCESS_ATTACH  pid=660

所以 CredUI 是一个真实的风险面。这个 DLL 被映射进了一个负责给整个系统弹凭据提示的进程, DllMain 或者 COM 激活里出一点问题,全系统的凭据对话框都会坏掉, 而且是在改登录屏的好几天之后才发作。

第三阶段:在安全桌面上画画

唤醒动画和最上层的人物需要一个真正的渲染进程,跑在 WinSta0\Winlogon 上,和 LogonUI 合成到一起。 这件事能不能做到,没有文档写过。

做法是一个 SYSTEM 权限的启动器。它找到控制台会话的 winlogon.exe,复制它的令牌, 调用 CreateProcessAsUser 时把 lpDesktop 指向 WinSta0\Winlogon。进程起来之后从内部确认了自己的位置:

started on desktop Winlogon, session 1
input desktop is Winlogon  (we are on Winlogon)

Z 序也赢了。这一条原本被当成一场争斗而不是一个接口,实际上一个普通的 WS_EX_TOPMOST 窗口 就压在 LogonUI 的所有窗口上面。每 500 ms 重新断言一次 HWND_TOPMOST,而第一次断言之前它已经在最上面了。

z[0] ShittimOverlay          1920x1080   <-- 我们
z[1] BioFeedbackUX XAML Host 1920x1080   (LogonUI)
z[3] LogonUI Logon Window    1920x1080   (LogonUI)

不行的是旧的那条路。WS_EX_LAYEREDUpdateLayeredWindow 每一步都返回成功,屏幕上什么都没有。 同一个二进制换到普通的 Default 桌面上就能正常合成,代码和窗口样式都没有改动,唯一的变量是桌面。

DirectComposition 第一次尝试就成功了。LogonUI 自己就用现代合成路径画在这个桌面上,所以这条路显然是通的。 窗口需要 WS_EX_NOREDIRECTIONBITMAP,否则它会拿到一个重定向表面,合成的内容被挡在一张不透明位图后面, 看上去和合成器拒绝渲染完全一样。

真实登录屏上连拍的六帧,白色闪光逐渐转为品红
9 月 3 日,真实登录屏上 40 张连拍里挑出的六帧。虚拟机自己的时钟从 2:16 走到 2:17,画面上方还是 Windows 的锁屏时钟。

两副骨骼加起来 913 根骨头,在安全桌面上加载、摆姿势并播放。每像素透明度是真的, 头发边缘和泛光的衰减都和 Windows 画的背景混合在一起。

有一件事要说清楚。把一个置顶、全屏、带透明通道的窗口放到凭据输入的桌面上, 正是这个桌面的隔离想要阻止的能力,而它从一个计划任务里第一次尝试就成功了, 没有用到任何漏洞,只用了 SYSTEM 和一个复制的令牌。反过来说它需要先有 SYSTEM, 能以 SYSTEM 运行的程序本来就赢了,所以这不是提权。安全桌面防的是别的用户,不是这台机器的管理员本人。 但它确实削弱了那层隔离,任何介绍这个项目的文字都应该把这句写在明面上。

谁在登录

能画之后,下一个问题是什么时候播。

第一版的信号是一个命名事件。提供程序在枚举结束时把它置位,覆盖层等它。两边日志都报成功,动画一次都没播过。 命名内核对象只在有句柄打开时存在,而置位的写法是打开、SetEvent、关闭。覆盖层还没起来, 最后一个句柄一关,内核就连同已置位的状态一起把对象销毁了。开机时量出来的时间是提供程序 12:17:21.625 置位, 覆盖层 12:17:21.922 打开,晚了 297 ms。

判断有人在登录这件事本身错了三次:

判据为什么错
开机后一段宽限期内丢弃信号开机时屏幕出现和信号落在同一个窗口里,序列根本不触发
第二轮枚举才触发前三个样本看着像用户操作。后来量到它在最后一次输入之后 30 秒才到,那段时间没有人碰过机器。那是 LogonUI 自己的定时器
GetLastInputInfo 前进就触发判据成立,但问题问错了。任何人看一眼机器都会触发,鼠标划过虚拟机控制台窗口也算

答案来自测量。把 Winlogon 桌面的整棵窗口树在两种状态下各拍一份指纹,它们只差一个窗口: 一个全屏的 BioFeedbackUX XAML Host,锁屏时在,密码界面时消失。类名有误导性, 它和生物识别无关,是时钟所在的 XAML 宿主。之前的窗口列表只看前五层,所以两种状态看起来完全一样。

这里还有一条线索是我提供的。Agent 花了两轮推理 Z 序,解释为什么盖不住锁屏,改了代码,什么都没有变。 我在虚拟机前告诉它,锁屏是一个独立的 Windows 层,上面有 Spotlight 的广告,必须先和这一层交互才能到达它的层。 量过之后确认,Windows 11 的锁屏由 LockApp 画在 Default 桌面上,只有密码界面在 Winlogon 桌面上。 覆盖层在任何 Z 序下都盖不住锁屏,两者根本不在同一个桌面。反过来,输入桌面切回来的那一刻就是有人按了键, 从按键到触发的时间从 5.5 秒变成 1 ms。

从贴一层到画整个房间

最初的架构是背景交给 Windows 的静图,人物由覆盖层画。三个从屏幕上报出来的缺陷,最后是同一个错误。

现象原因
人物前面的课桌不见了静图和覆盖层各画半个场景,两半没有接上
密码界面上坐着的人物亮了三倍两种状态用了同一个压暗常数
密码框出现时人物从课桌上滑下去Windows 把壁纸放大了 2%,覆盖层的相机没有跟着放大

后两个靠量出 Windows 的处理然后照着做修掉了,也就是前面那组缩放 1.020、增益 0.281。 修完还是不对,我当时的原话是"人物的被覆盖有点生硬,时间上不像是被整体压暗"。数字是对的,时序不对。 覆盖层看不见 Windows 的动画,它只知道自己盯着的那个窗口在某一刻不再是原来的样子, 而那一刻落在动画的什么位置,没有逐帧捕获就定不下来,实验室恰好没有逐帧捕获。

去匹配一个观察不到的动画不是一个能解的问题,于是改成让覆盖层拥有整个场景。

覆盖层自己画整个房间,连同时钟和角落的标记。密码框需要屏幕的时候再交还回去,用一次交叉淡出。 我们的房间淡出时,Windows 在下面怎么压暗和放大都可以,两边从来不需要一致,也就不会不一致。

覆盖层拥有整个屏幕时的夜晚场景
覆盖层拥有整个屏幕时的样子,时钟和右下角的刻度都是自己画的。

淡出之后露出来的是同一个场景。覆盖层通过 PersonalizationCSP 把当前场景烘好的静图交给 Windows 当锁屏图, 所以密码界面的背景就是刚刚淡出去的那一张。在这之前它是 Windows 的出厂壁纸,人物站在一片蓝色的光斑上。

交给 Windows 的静图,画面里没有人物
交给 Windows 的静图,里面没有人物。第一张正确的 GPU 截图里出现了两个她,前景一个,静图里还烘着一个。

这里还有一个和缓存有关的坑。LogonUI 在创建登录屏时就把背景图缓存下来了, 换掉磁盘上的 PNG 不会有任何变化,要等 LogonUI 重启。

代价是待机时 Windows 自己的时钟、日期和角落按钮都被盖住。时钟和日期换成了自己画的, 按钮在场景交还屏幕的那一刻回来。我没有让它画一个假按钮,只在原位置留了一条 1 px 的刻度线和一个电源符号。 在整个系统上撒谎最要命的地方画一个不属于自己的控件,是不能做的事。

密码框永远不由这个项目绘制

不是暂时不做,是永远不做。两个理由,第一个单独就足够。

第一,那等于让这个进程接触密码。Winlogon 桌面被隔离出来只为一件事, 而一个第三方覆盖层画的密码框,只要做得足够精确,就和凭据窃取没有区别,对正在往里打字的人也一样。 这个项目画出任何看起来可以输密码的东西的那一刻,它就从美化变成了安全桌面要防的那种攻击。

第二,它也不会工作。认证走的是凭据提供程序,而这个项目的提供程序返回零个磁贴, 画出来的输入框后面没有任何东西可以提交。

过场特效的常数是量出来的

退场那道闪光在 Spine 导出里不存在。骨骼里的 37 个动画全是待机、触摸反应和表情, 游戏里这段效果是一套 Unity 粒子,只能重建。

常数分了等级。从 2022 年的预制体二进制里读出来的,或者从录像逐帧量出来的,算 A 级,直接用。 目测调出来的算 B 级,在代码里标着目测。

常数等级
闪光存续0.500 sA
起始上升30 fps 下不超过一帧A
退场到进场的间隔0.400 sA,两个独立来源
峰值+216/255A
三角形速度0A
粒子数量、起始尺寸、透明度15、1.60、25%B

两个和直觉相反的结论:粒子不向外飞;闪光的 alpha 恒定为 1,真正下降的是绿色通道。

移植第一版错了四处,全部由我看屏幕发现:

  • 人物退场时闪光下面还是她自己。那是加法白光叠在还在场的人身上。
  • 房间里是两个人。参考实现里主姿势组之外都算布景的规则把另一个角色的 41 个插槽当成了背景烘进壁纸, 唤醒把她带走之后,壁纸里还留着一个。
  • _base 在这套骨骼里是头顶的光环,不是地面板。按参考的规则处理完,空教室上方悬着两个光环。
  • 发光是泛光,不是推颜色通道。预制体里 _Color 从 2.0 一直到 11.98,所有东西不钳位地累加, 泛光链之后才做唯一一次钳位。

开场动画,以及三次时序错误

开场动画移植自这个博客首页的 Plana 彩蛋。输入密码之后播放,然后停在等待页上,直到 Windows 淡入桌面。 三角形在进入等待页时就要清掉,否则等待页消失的一瞬间会露出还在动的三角形。

时序改了三次:

  1. 提交密码就播。错误密码 20 ms 就被拒绝,动画播了大约一帧然后被丢弃,看上去像闪了一下。 日志是 01:26:05.681 开始,01:26:05.701 停止。没有任何动画短到能扛住 20 ms。
  2. 改成验证通过再播。太晚了,等于在赌 Windows 加载用户配置需要多久。
  3. 最后是提交即播,150 ms 内被拒绝就收回,那时还没有画出任何东西。

还有一次看起来像冻结的问题。提交密码的那一刻所有动画都停了,日志里的帧率是:

01:26:05.178  fps: 63.0 over 1000 ms
01:26:08.570  fps: 10.6 over 3391 ms

3.4 秒里只有 36 帧,进程没有被调度。计划任务以优先级 7 运行,也就是 BELOW_NORMAL, 它启动的一切都继承这个优先级,覆盖层从装上那天起就一直在低优先级下运行。 同一个原因还解释了一条差点被记成发现的事件日志延迟 2.86 秒,事件是准时到的,回调线程没有被调度。 在一个被饿死的进程里量出来的延迟,量的是饥饿本身。

真正的事件日志延迟是另一回事。Winlogon 在按键后 13 到 33 ms 内就写下了认证事件, 但通过事件日志服务拿到它的每一条路都要一秒左右,推送订阅、带过滤的查询和不带过滤的查询都一样。 登录屏在验证通过后大约两秒就被拆掉,一秒的延迟不是动画变慢,是动画看不见。 最后改成自己开一个 ETW 会话直接读 provider,每 15 ms 手动 flush 一次, 五次脚本登录量下来,从按键到触发是 31 到 60 ms。

还有一个我给的预算。转场一直不稳定,我说尽早启动,可以留一个常驻的小进程,内存不超过 100 MB。 于是一个覆盖层进程一直待在控制台会话里,窗口建好但不显示,桌面切换时直接上线。 盖住锁屏的时间从 1026 ms 变成 34 ms,常驻占用 27 到 34 MB。

这条线上最糟糕的一次失败也在这里。有一次重启之后机器直接停在全屏的过场画面上,什么都做不了。 日志显示覆盖层启动后 300 ms 就收到了一个认证通过事件,那时窗口还没有显示。 那是 Windows 的自动重启登录写下的一条认证记录,结果为 0,而没有人输过密码, 等待页当时又没有退出路径。五次脚本重启都复现不出来,只有我从自己登录着的会话里真的重启一次才会出现。

鼠标

这是我坚持得最久的一件事,也是 agent 错得最久的一件事。

我从虚拟机里报了好几次,登录界面上鼠标什么都点不了,密码框、确认按钮、右下角的按钮全都没有反应。 Agent 的回答有依据。窗口是 WS_EX_TRANSPARENT 的,WM_NCHITTEST 返回 HTTRANSPARENT, 这是文档里点击穿透的标准写法。它每 5 秒调一次 WindowFromPoint,日志里写着屏幕中心命中的是 LogonUI 的窗口。 它据此写了一条风险条目,论证我看到的是虚拟机控制台指针的问题。

最后是用真实的点击量的。poke.exe 把正确密码打进去然后点提交箭头,判据是能不能登进去。

覆盖层结果
全屏登不进去
关闭1.7 秒登进去
只盖左边 40%登进去

它盖到哪里就吞掉哪里的点击。WindowFromPoint 的读数没有错,但它回答的不是鼠标问的那个问题。 在 DirectComposition 合成的 WS_EX_NOREDIRECTIONBITMAP 窗口上,WS_EX_TRANSPARENT 不产生点击穿透。 另外 HTTRANSPARENT 只能把消息交给同一个线程里的窗口,本来就交不到 LogonUI 手上。 又试了分层窗口的逐像素透明度,在 1248 个 alpha 为 0 的像素上点了四次,一次都没有到达 LogonUI。

能用的只有窗口区域。SetWindowRgn 会同时裁掉合成的画面和输入,挖掉的那块矩形完全归 Windows。 这也是它的极限。在这个桌面上绘制和输入分不开,能画到的地方就会吞掉点击,交出去让人点的地方就画不了。

所以现在的取舍是密码界面上把密码框那一列和角落的按钮挖掉,那里本来也只画三角形粒子,什么都不损失。 待机时房间是活的,挖出来的洞会是一块亮度不对又不动的静图,所以待机时角落按钮点不动, 任何输入都会唤醒场景,三秒多之后按钮就回来。这是我拍的板。

洞的大小又错了一次。它原本是屏幕宽度的 38% 到 62%,我在一块 4:3 的屏幕上看到人物的手臂被裁掉才报出来。 量过之后发现,在 16:9 上人物的右边缘在 41.8%,那个洞在所有人测试的那块屏幕上一直裁着大约 73 px, 没有人看见。现在洞按 DIP 计算,并且永远不越过人物。

1280×1024 的 4:3 屏幕上的同一个场景
1280 × 1024。4:3 屏幕上人物的位置和 16:9 差得很远,挖洞的问题是在这块屏幕上暴露的。

在一台不能直接操作的机器上干活

登录屏是这个项目能挑到的最不友好的目标。不能舒服地挂调试器,不能从外面往里打字,截图不可靠, 而画它的进程由别人以 SYSTEM 启动在一个工具看不见的桌面上。

实验室是一台 Hyper-V 第二代虚拟机。唯一始终可用的通道是 PowerShell Direct, 它不需要网络,不需要客户端代理,在登录屏上也能用。这个项目里每一次诊断都以从它读一份日志结束。

不能往客户机里打字。 Msvm_Keyboard 注入的是模拟 PS/2 键盘,而第二代客户机只有 VMBus 合成键盘。 注入返回 ReturnValue=0,什么都没有送到,这一点是往密码框里打字然后数亮起的像素验证的。 客户机内部调 SendInput 也不行,安全桌面拒绝注入的输入。所以凡是需要一次按键的事都需要我本人。 Agent 只能造不需要按键的入口,比如定时触发唤醒,或者不输密码直接播开场。

不能可靠地截图。 Hyper-V 的缩略图通道大部分时候返回的是缓存帧,而且不报错。 有一次客户机时钟是 13:29,它返回的是四天前的画面。这个通道还只在挂着控制台窗口时才刷新, 没有控制台时画面停在 09:23,客户机时钟是 09:35,挂上控制台八秒之后它读到了 09:37。

更麻烦的是有一次 agent 给我发了九张图说是九个场景,我一眼看出全是同一张,而且被异常压暗,人物也没有盖上。 它的采集器为了绕开缓存每次请求不同的宽度,九张图的哈希各不相同,自己写的去重检查就报了九张不同的帧, 实际是同一张死帧的九次不同缩放。一个采集方法自己就能满足的新鲜度检查,不构成新鲜度检查。 现在要看客户机画了什么,只信覆盖层自己回读渲染目标编码出来的图,写文件的进程就是画它的进程。

不能判断性能。 第二代虚拟机没有 GPU,一切都走 WARP,密码界面的亚克力模糊在实验室里根本不出现, 所有帧率数字都只是关于 WARP 的陈述。后来挂上了 GPU 分区,但宿主每更新一次显卡驱动, 客户机里手工拷贝的驱动文件版本就对不上,设备变成 Code 43,D3D 静默回落到 WARP。 9 月 7 日发现它已经坏了好几天,是靠一行为了别的原因加进去的 adapter: 日志看出来的。 那段时间的帧率数字全部作废,具体哪天坏的已经查不出来。

要调试的进程不是自己启动的。 printf 去不了任何地方,所有东西追加写进日志文件再从 PowerShell Direct 读回来。 前面那个优先级 7 的问题就是这么发现的。

这些 bug 都画出了东西

这个项目里几乎每个缺陷都有同一个特点,它们都画了东西出来。没有一个崩溃、返回错误或者在日志里报失败。

现象实际原因
椅子上留着一条黑裙子一个插槽的组前缀写在了插槽上,不在附件上
两个光环悬在半空_base 当成了地面板
课桌不见,人物撕裂两层各画半个房间
序列从不触发命名事件销毁了自己
序列自己触发把 LogonUI 的定时器当成了用户操作
拷贝成功但资产缺失路径分隔符丢了一个,拷贝成功在了别处
登录时全部冻结进程没有被调度

能推广的一条是验证产物,不验证调用。拷完看目的地,画完数像素,拿截图和本该产生它的文件比。

还有一个我很喜欢的例子。我说跟随时间切换场景开着的时候不怎么随机,只有那么两个场景会被抽中。 代码写的是 GetTickCount64() % active.size(),那不是选择,是机器开机时长的毫秒数。 16 次抽样的分布是平的,但顺序是 5 5 1 2 3 4 1 1 3 4 5 1 2 2 3 4,十五步里有十步刚好加一,顺序才是证据。 而且系统计时的粒度是 15 或 16 ms,对 5 取余之后各个余数出现的次数是 80、80、40、80、40, 有两个场景的概率只有其他场景的一半。

方法上还留下几条:

  • 不要在有第二个观察者的时候测量。事件日志延迟有一次测出 16 ms,其余每次都是 1040 ms 左右, 唯一的区别是那一次旁边有一个 PowerShell 在轮询同一个通道,它强制了一次提交。
  • 读帧率要看窗口不看速率。10.6 over 3391 ms 不是低帧率,是 3.4 秒的停顿。
  • 一条写死的日志行会骗人。gpu: 10 shaders 在加了三个着色器之后还是 10,被当成新着色器没有加载的证据。
  • 注释看起来比文档新。截图脚本头部的注释里写着一条文档已经明确撤回的理论,agent 信了注释。

发布前

有一个问题是我问出来的,烘出来的静图能不能在所有分辨率的显示器上通用。答案是可以。

场景世界是 2880 × 1620,正好 16:9,相机是一个均匀 cover。一张 16:9 的静图 cover 缩放到任何屏幕, 几何上和原生渲染一致,屏幕尺寸只决定重采样有多少。量出来的结果是一张 1920 × 1080 的静图 缩放到 2256 × 1504,和原生 2256 × 1504 渲染对齐之后零平移,平均差异 0.68/255。 笔记本插拔底座和 Surface 旋转这类显示变化,因此变成覆盖层能处理的事。

发布前的清单,能在实验室做的都做了:

项目结果
分辨率矩阵11 种模式,5:4 到 32:9,静图和实时渲染全部零偏移
DPI4K 下 100%、150%、200% 全部正确
ARM64所有组件交叉构建通过,打包前校验每个二进制的 PE 头
稳定性30 次开机到登录屏循环零崩溃,覆盖层工作集 278.1 到 278.7 MB
设备丢失在真的 RTX 5080 上拔掉设备,60 ms 内检测到,315 ms 后新进程在 WARP 上继续合成
应急Ctrl+Alt+F11 撤掉覆盖层并写一个停用标记,重启也不恢复

DPI 那一条之前是坏的。覆盖层从没声明过 DPI 感知,4K 150% 下 GetSystemMetrics 返回 2560 × 1440, 渲染完再被系统拉伸,而安装器用同一个数烘图,两边接缝错位。 PE 头那条检查是因为一条写死的路径把 x64 的静图烘焙器放进了 ARM64 包,唯一的症状是安装很慢。 稳定性原计划跑 300 次,要 11.6 小时,我叫停了。 Ctrl+Alt+F11 在 9 月 8 日之前从来没有被任何东西按过,我在真机上按了,有用, 在微软那个恢复密码的界面上也有用。

时钟和日期跟系统语言和格式走,GetTimeFormatExGetDateFormatEx 不给格式串, 由 Windows 决定顺序、分隔符、星期几以及 12 还是 24 小时。之前两个时钟一个写死 12 小时制且不带 AM/PM, 13:00 显示成 1:00,另一个钉死 en-US 还自己给了格式串。在写它的那台机器上都看不出问题, 换一个非英语用户第一眼就是它。

配置是一个普通用户用记事本就能改的文本文件,不需要管理员权限。前面有加号的场景是激活的, 每次登录从激活的场景里挑一个。拼错的键、读不出的值和不认识的场景名各自在日志里点名然后忽略, 其中任何一个都不会改变默认值。

它的代价

发布清单的开头写着,从 0.9.2 以来记录了十一条风险,其中十条是我看着屏幕发现的,只有一条来自测量。

第三阶段没有稳定性契约。模糊在下面、人物在上面不是接口保证,是碰巧赢了的结果,一次功能更新就能收回。 它削弱了安全桌面的隔离,待机时盖住了角落的按钮。Windows Hello 的指纹和人脸不产生按键, 默认不触发动画,而这些在实验室里全都测不了。笔记本的 GPU 会是大部分问题的来源,Surface 一台都没有测过。

Windows 10 直接声明不支持。不是编译不了,唤醒的主触发建立在 Windows 11 把锁屏画在 Default 桌面上 这个行为上,Windows 10 不这么做,整条序列要重写一遍再重测一遍。说只支持 Windows 11 比说得含糊要便宜。

下一步是把它装进这台写博客的机器。稳定性闸门是为它设的,WinRE 的逃生路径也是为它排练的。 安全模式救不了一个坏掉的凭据提供程序,LogonUI 在安全模式里照样枚举第三方提供程序, 唯一的出路是从恢复环境离线编辑注册表删掉那个 CLSID。这件事要在装之前先练一遍。