把什亭之匣画到 Windows 登录屏上
一次把 Windows 11 登录屏换成实时渲染场景的记录:三个阶段、一台不能直接操作的虚拟机,以及几次被屏幕推翻的结论
9 月 1 日晚上心血来潮,想把 Windows 11 的登录界面换成什亭之匣。
什亭之匣是 Blue Archive 里的一块平板,游戏内可以直接操作。我要的效果是登录屏的背景换成它的场景,触发登录动作时播放其中的唤醒互动。Windows 的模糊登录界面仍旧由系统自己绘制,Plana 位于它之上。改动只限于登录屏,进入系统之后不再涉及。

这台机器承担日常工作,不适合随意改动。所以我没有直接安装,先在虚拟机里做,量出崩溃率和登录延迟这些数字。数字不理想就不装,过程本身也足够写一篇。
成品是下面这段视频。
Windows 只给了一层
这四层里,Windows 只开放了最下面那一层。
最先想到的入口是凭据提供程序(credential provider)。它是一个 COM 组件,LogonUI.exe 会把它加载进自己的进程。但它只能描述一个磁贴,也就是登录界面中间那块区域,里面放头像、用户名和密码框。磁贴的内容是一份固定的字段表,图片、标签、输入框、按钮。没有帧回调,绘制范围也不会超出这个矩形。
于是四层落到了三种机制上,风险完全不同。我把它们分成三个阶段,每个阶段都能单独撤回:
| 阶段 | 机制 | 风险 |
|---|---|---|
| 1 | 用注册表指定锁屏背景 | 零。删掉两个键值就恢复原状 |
| 2 | 一个凭据提供程序 DLL | 中等。它活在 LogonUI 进程里,它出错等同于登录屏出错 |
| 3 | 在安全桌面上跑一个渲染进程 | 高。这正是安全桌面的隔离要阻止的事 |
每个阶段先在虚拟机里过稳定性闸门,通过之后再做下一个。第三阶段默认算不合格,除非有数据证明。
第一阶段:先量登录屏对一张图做了什么
第一步是把一张测试卡设成锁屏背景,看 Windows 会怎么处理它。我故意把这一步排在渲染器前面。渲染器该按什么分辨率、什么裁切方式、什么亮度出图,答案都在这张卡上,不先量出来只能靠推测。


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


后来把这套处理拟合成了两组数字:
| 界面 | 缩放 | 增益 |
|---|---|---|
| 锁屏 | 1.000 | 0.820 |
| 密码界面 | 1.020 | 0.281 |
密码界面那 2% 的放大,后面让我吃了一次亏。
这里还留了一个坑。锁屏图有两条注册表通道可以设。Policies\Microsoft\Windows\Personalization\LockScreenImage
这一条只在企业版和教育版上生效,Pro 版会把它当成不存在,但值写得进去,注册表里也查得到。真正管用的是 PersonalizationCSP。
第一天的测量记录里就写着这件事,后面好几天代码还是往策略键里写,卸载程序里也留着清理它的逻辑。一直没有暴露,因为那张图从未显示出来,24 ms 之后覆盖层会画一张几乎相同的图盖在上面。类似的情况后来又出现过几次,结论早就写下来了,代码没有跟着改。
第二阶段:一个不提供登录方式的凭据提供程序
第二阶段的 DLL 注册进系统,被 LogonUI 加载,然后返回零个磁贴。它不持有任何凭据对象,也不参与认证。
这样安排是为了先回答一个更小的问题:加载这个 DLL 本身会不会让 LogonUI 不稳定。我们自己画的界面有没有影响,那是后面的事。
有三条规矩是动手之前就定死的。
- 不实现
ICredentialProviderFilter。这个接口可以隐藏别的提供程序。把微软自己的密码磁贴藏起来,自己再出一次错,机器就登不进去了。只加磁贴,不减磁贴。 - 四种使用场景都要处理。同一个 DLL 会在
CPUS_LOGON、CPUS_UNLOCK_WORKSTATION、CPUS_CHANGE_PASSWORD和CPUS_CREDUI下被枚举。最后一个覆盖 UAC 相关的凭据提示、runas,以及任何调用 CredUI 的程序。这里出问题的表现是几天之后 UAC 对话框不能用,跟登录屏看不出关系。 - 任何错误路径都返回成功但没有磁贴,不抛异常。
一轮枚举在日志里是这样:
1DLL_PROCESS_ATTACH
2[+0ms] SetUsageScenario cpus=1 (CPUS_LOGON)
3[+16ms] Advise
4[+16ms] GetCredentialCount -> 0 tiles结果是 LogonUI 正常运行,零崩溃,零转储,微软自己的密码磁贴完好。
cptest.exe 直接通过 COM 驱动这个对象走完四种场景下的每个方法,48 项检查零失败,另外跑了 2000 次创建和释放,没有悬挂引用。虚拟机里 250 次锁定解锁循环零失败,每一次 DLL 都被加载。
登录本身不受影响,开启 AutoAdminLogon 之后能一路进到桌面,进去时提供程序是加载着的。没有用手输密码验证过,因为宿主机没办法往运行中的客户机里打字,这一点后面会讲。
有两处和文档给人的印象不一样。
第一处是 LockWorkStation 锁屏之后拉起的 LogonUI 报的是 CPUS_LOGON,CPUS_UNLOCK_WORKSTATION
从头到尾没有出现过。现在的 Windows 里锁屏和登录屏是同一套界面,解锁走的也是登录那条路。如果有代码只处理 CPUS_UNLOCK_WORKSTATION,它在锁屏时不会被执行。
第二处是凭据对话框由 CredentialUIBroker.exe 承载,而这个进程确实会加载我们的 DLL。这件事试了三次才确认:
| 做法 | 结果 |
|---|---|
在计划任务里跑 Get-Credential | 退出码 1,没有界面,没有日志 |
runas /user: | 跑起来了,没有日志 |
直接调 CredUIPromptForWindowsCredentialsW | DLL 出现在 broker 进程里 |
前两次都不算证据。Get-Credential 在画出任何东西之前就失败了,runas 在控制台里问密码,根本不经过 CredUI。一个从未弹出来的对话框,不能用来证明它跳过了我们。第三次直接调 API,日志才有了内容:
1ui processes : CredentialUIBroker(660), creduiprobe(5912)
2our dll in : CredentialUIBroker(660)
3log : DLL_PROCESS_ATTACH pid=660所以 CredUI 是一个真实的风险面。这个 DLL 被映射进了一个负责给整个系统弹凭据提示的进程,
DllMain 或者 COM 激活里出一点问题,全系统的凭据对话框都会坏掉,而且是在改登录屏的好几天之后才发作。
第三阶段:在安全桌面上画画
唤醒动画和最上层的人物需要一个真正的渲染进程,跑在 WinSta0\Winlogon 上,和 LogonUI 合成到一起。这件事能不能做到,没有文档写过。
做法是一个 SYSTEM 权限的启动器。它找到控制台会话的 winlogon.exe,复制它的令牌,调用 CreateProcessAsUser 时把 lpDesktop 指向 WinSta0\Winlogon。进程起来之后从内部确认了自己的位置:
1started on desktop Winlogon, session 1
2input desktop is Winlogon (we are on Winlogon)Z 序也赢了。这一条原本被当成一场争斗而不是一个接口,实际上一个普通的 WS_EX_TOPMOST 窗口就压在 LogonUI 的所有窗口上面。每 500 ms 重新断言一次 HWND_TOPMOST,而第一次断言之前它已经在最上面了。
1z[0] ShittimOverlay 1920x1080 <-- 我们
2z[1] BioFeedbackUX XAML Host 1920x1080 (LogonUI)
3z[3] LogonUI Logon Window 1920x1080 (LogonUI)不行的是旧的那条路。WS_EX_LAYERED 加 UpdateLayeredWindow 每一步都返回成功,屏幕上什么都没有。同一个二进制换到普通的 Default 桌面上就能正常合成,代码和窗口样式都没有改动,唯一的变量是桌面。
DirectComposition 第一次尝试就成功了。LogonUI 自己就用现代合成路径画在这个桌面上,所以这条路显然是通的。窗口需要 WS_EX_NOREDIRECTIONBITMAP,否则它会拿到一个重定向表面,合成的内容被挡在一张不透明位图后面,看上去和合成器拒绝渲染完全一样。

两副骨骼加起来 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 的出厂壁纸,人物站在一片蓝色的光斑上。

这里还有一个和缓存有关的坑。LogonUI 在创建登录屏时就把背景图缓存下来了,换掉磁盘上的 PNG 不会有任何变化,要等 LogonUI 重启。
代价是待机时 Windows 自己的时钟、日期和角落按钮都被盖住。时钟和日期换成了自己画的,按钮在场景交还屏幕的那一刻回来。我没有让它画一个假按钮,只在原位置留了一条 1 px 的刻度线和一个电源符号。在整个系统上撒谎最要命的地方画一个不属于自己的控件,是不能做的事。
密码框永远不由这个项目绘制
不是暂时不做,是永远不做。两个理由,第一个单独就足够。
第一,那等于让这个进程接触密码。Winlogon 桌面被隔离出来只为一件事,而一个第三方覆盖层画的密码框,只要做得足够精确,就和凭据窃取没有区别,对正在往里打字的人也一样。这个项目画出任何看起来可以输密码的东西的那一刻,它就从美化变成了安全桌面要防的那种攻击。
第二,它也不会工作。认证走的是凭据提供程序,而这个项目的提供程序返回零个磁贴,画出来的输入框后面没有任何东西可以提交。
过场特效的常数是量出来的
退场那道闪光在 Spine 导出里不存在。骨骼里的 37 个动画全是待机、触摸反应和表情,游戏里这段效果是一套 Unity 粒子,只能重建。
常数分了等级。从 2022 年的预制体二进制里读出来的,或者从录像逐帧量出来的,算 A 级,直接用。目测调出来的算 B 级,在代码里标着目测。
| 常数 | 值 | 等级 |
|---|---|---|
| 闪光存续 | 0.500 s | A |
| 起始上升 | 30 fps 下不超过一帧 | A |
| 退场到进场的间隔 | 0.400 s | A,两个独立来源 |
| 峰值 | +216/255 | A |
| 三角形速度 | 0 | A |
| 粒子数量、起始尺寸、透明度 | 15、1.60、25% | B |
两个和直觉相反的结论:粒子不向外飞;闪光的 alpha 恒定为 1,真正下降的是绿色通道。
移植第一版错了四处,全部由我看屏幕发现:
- 人物退场时闪光下面还是她自己。那是加法白光叠在还在场的人身上。
- 房间里是两个人。参考实现里主姿势组之外都算布景的规则把另一个角色的 41 个插槽当成了背景烘进壁纸,唤醒把她带走之后,壁纸里还留着一个。
_base在这套骨骼里是头顶的光环,不是地面板。按参考的规则处理完,空教室上方悬着两个光环。- 发光是泛光,不是推颜色通道。预制体里
_Color从 2.0 一直到 11.98,所有东西不钳位地累加,泛光链之后才做唯一一次钳位。
开场动画,以及三次时序错误
开场动画移植自这个博客首页的 Plana 彩蛋。输入密码之后播放,然后停在等待页上,直到 Windows 淡入桌面。三角形在进入等待页时就要清掉,否则等待页消失的一瞬间会露出还在动的三角形。
时序改了三次:
- 提交密码就播。错误密码 20 ms 就被拒绝,动画播了大约一帧然后被丢弃,看上去像闪了一下。日志是 01:26:05.681 开始,01:26:05.701 停止。没有任何动画短到能扛住 20 ms。
- 改成验证通过再播。太晚了,等于在赌 Windows 加载用户配置需要多久。
- 最后是提交即播,150 ms 内被拒绝就收回,那时还没有画出任何东西。
还有一次看起来像冻结的问题。提交密码的那一刻所有动画都停了,日志里的帧率是:
101:26:05.178 fps: 63.0 over 1000 ms
201:26:08.570 fps: 10.6 over 3391 ms3.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 计算,并且永远不越过人物。

在一台不能直接操作的机器上干活
登录屏是这个项目能挑到的最不友好的目标。不能舒服地挂调试器,不能从外面往里打字,截图不可靠,而画它的进程由别人以 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,静图和实时渲染全部零偏移 |
| DPI | 4K 下 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 日之前从来没有被任何东西按过,我在真机上按了,有用,在微软那个恢复密码的界面上也有用。
时钟和日期跟系统语言和格式走,GetTimeFormatEx 和 GetDateFormatEx 不给格式串,由 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。这件事要在装之前先练一遍。