Emulsion Desktop 窗口闪屏与实时缩放修复:从关闭 DirectComposition 到 BufferQueue
窗口不闪了,为什么拖大时又出现一片空白?
Emulsion Desktop 的一次窗口渲染排查,先后出现了三个症状:
- 双击标题栏最大化,再还原;鼠标第一次滑过页面时,整个窗口闪一帧浅灰白。
- 将窗口材质改成深色后,白闪变成黑闪,侧栏的磨砂也没有原来好看。
- 关闭 WebView2 的 DirectComposition 后,闪屏消失,但拖拽窗口变大时,页面内容追不上窗口边界,新扩展区域出现明显空白。
最终保留了原来的透明窗口、Acrylic 和 GPU 渲染,将主画面的呈现策略改为 BufferQueue。在本机的经典外观下,它同时消除了缩放后的首次悬停闪帧,以及拖大时露出的大片材质空白。
这次排查也纠正了一个过早的结论:“缩放结束后不闪”不足以说明窗口渲染修好了,还必须检查持续拖拽中的画面。
复现环境与判断范围
| 项目 | 本次环境 |
|---|---|
| 应用 | Emulsion Desktop,0.8.8 开发阶段 |
| 框架 | Wails v3 v3.0.0-beta.28 |
| 前端 | React 19、Vite 6 |
| 系统 | Windows 11,原生 Acrylic 可用 |
| WebView2 | 153.0.4234.32 |
| 显示缩放 | 175% |
| GPU | AMD Radeon 780M |
| WebGL 渲染器 | ANGLE / Direct3D11 |
后文的数字均来自这台机器的对照实验。它们说明本机问题和修复效果,没有覆盖所有显卡、驱动以及混合 DPI 多屏场景。
先理解窗口中的三层背景
页面里的玻璃效果涉及三层,配置容易互相混淆。
原生窗口与 DWM 材质
Windows 的 DWM 负责桌面合成。Acrylic 作用在原生窗口背景上,能让窗口透出被系统处理过的桌面或其他窗口。
Wails 主窗口的相关配置为:
BackgroundType: application.BackgroundTypeTranslucent,Windows: application.WindowsWindow{ BackdropType: application.Acrylic, Theme: application.SystemDefault,},在这个版本的 Wails 中,非 Solid 背景会让 WebView 的默认背景透明。对于可交互的透明窗口,Windows 实现还会设置 WS_EX_NOREDIRECTIONBITMAP。
这个标志值得检查,但后续对照证明:仅移除这个标志不能消除闪屏,不能把它当成充分的根因解释。
WebView2 / Chromium 的页面呈现
HTML、布局、绘制和 GPU 合成发生在 WebView2 内。窗口尺寸通知到达浏览器,并不等于新尺寸的画面已经呈现在屏幕上。
Wails 的 WM_SIZE 处理会调用 ResizeWithBounds,最终更新 WebView2 的 PutBounds。这里的 InvokeSync 同步的是宿主调用,并没有承诺“浏览器已完成布局并提交这一帧”。
因此,至少要区分:
原生窗口边界变化 ↓WebView2 控制器收到新的 Bounds ↓页面布局与绘制 ↓GPU 内容呈现、DComp 树更新 ↓DWM 合成到屏幕中间某个步骤暂时没有覆盖到窗口区域时,原生窗口背景就可能露出来。
CSS 的透明面和 backdrop-filter
Emulsion 的侧栏与内容区是并列布局。主内容不会从侧栏背后滚过,所以仅在一个平整、不透明的底色上写 backdrop-filter,很难获得真正的桌面磨砂。
经典外观的透明链路是:
body和窗口外壳让出底色。- 主内容区铺不透明背景,保证正文不会整屏透出桌面。
- 侧栏和标题栏使用半透明面,让原生 Acrylic 透上来。
- CSS 模糊和色调处理放在伪元素上,避免改变祖先对
fixed弹窗的包含块。
这也是为什么将窗口背景改成 Solid 后,侧栏会退化:即使 CSS 还写着模糊,原生透明链路也已经断开。
第一轮:把白闪改成黑闪,只改变了症状
最初的修复将材质从 Acrylic 改成固定深色 MicaAlt:
Windows: application.WindowsWindow{ BackdropType: application.Tabbed, Theme: application.Dark,},浅色的闪帧因此变暗,在深色内容区中不那么显眼。但它没有证明那一帧被消除,只是改变了窗口底层露出来时的颜色。
随后又将 BackgroundType 改成 BackgroundTypeSolid,同时仍保留 MicaAlt 配置。配置和说明开始矛盾:代码关闭了原生透明材质,注释却仍描述“整窗透明、Acrylic 磨砂”。
从用户观感看,就是白色闪屏减少了,黑色闪屏还在,侧栏也更像一块实色面。
这类问题不适合只看“闪的颜色”。应该直接测量:窗口内容是否丢了一帧,以及材质透明链路是否仍然成立。
第二轮:关闭 DirectComposition,有效,但验收漏了一项
创建一个隔离的 Wails 测试窗口:透明 HTML、半透明侧栏、深色不透明主区,再加几个会触发重绘的悬停区域。页面内容和 Acrylic 配置保持一致,仅改变浏览器呈现参数。
基线测试中,尺寸变化之后第一次鼠标进入页面,固定深色区域的平均 RGB 亮度从约 25 瞬间升到 213。这与整窗露出亮色原生材质的现象相符。
加入下面的配置后,这个闪帧消失:
Windows: application.WindowsOptions{ AdditionalBrowserArgs: []string{"--disable-direct-composition"},},这是浏览器呈现路径的变化,并没有关闭全部 GPU 渲染。本机 WebGL2 仍然可用,仍显示 AMD Radeon 780M / ANGLE D3D11。
当时完成了缩放结束后的悬停、真实标题栏双击、Acrylic 传透和编译检查,但没有测持续拖拽。随后用户反馈:拖大窗口时,边界先变大,页面后补上,中间露出空白。
补测发现,这项参数确实引入了回归。
连续拖拽的对照结果
给页面添加一个临时固定边缘标记,再用原生拖拽路径连续改变窗口大小。标记仅用于测试,不进入应用代码。
| 测试页面 | 默认 DirectComposition | 关闭 DirectComposition |
|---|---|---|
| 简化 Wails 页面 | 稳定轮次通常只落后约 1–5 px,但仍可能闪帧 | 平均落后约 24–49 px,峰值 96 px |
| 真实经典页面 | 需要替代方案同时验证闪屏与拖拽 | 拖大平均落后约 136 px,峰值约 200–221 px,出现明显 Acrylic 空白带 |
这些是固定拖拽轨迹下的屏幕像素测量,不是帧率测试,也不能直接换算成所有场景下的延迟毫秒数。
原来的“页面内容延迟加载”描述,实际更接近新尺寸的画面提交落后。无需网络请求的简化页面也能出现同类现象,所以不能仅靠页面加载速度解释。
第三轮:从源码寻找更窄的呈现方案
继续保留 Acrylic 和透明 WebView,不通过修改材质颜色掩盖问题。实验的目标是同时满足:
- 尺寸变化后首次悬停不闪。
- 持续拖大时内容跟随边界。
- Acrylic 仍能透出背景。
- GPU 渲染保持正常。
哪些实验没有解决问题
| 实验 | 本次结论 |
|---|---|
单独清除 WS_EX_NOREDIRECTIONBITMAP | 仍会闪帧,窗口样式不是充分解释 |
Wails UseVisualHosting | 拖拽较快,但首次悬停闪帧仍出现 |
在关闭 DComp 的同时加 --disable-gpu-vsync | 仍有几十像素的拖拽落后 |
关闭 DirectCompositionResizeFixes | 拖拽较快,但闪帧仍出现 |
关闭 RemoveRedirectionBitmap | 没有消除本机闪帧 |
| 拖拽期间临时切换 DWM 材质 | 未形成有效替代修复,未采用 |
还有一些早期实验用了名字看起来相关的开关,例如 DirectCompositionUseDCompSurface、DirectCompositionSwapChainIndependentFlip。没有确认它们对当前 Chromium 153 主画面路径的实际作用,不能将“传了参数但没变化”写成已排除机制。
源码研究还发现视频覆盖层禁用开关采用下划线形式:
--disable_direct_composition_video_overlays它不能与随手猜出的连字符写法等同;而且视频覆盖层本来就不是这个无视频复现页面的优先方向。整个排查过程中,参数能出现在命令行上,与它真正改变了目标路径,是两件事。
主画面内容与 DComp Commit 并非天然同步
Chromium 153 的 DirectCompositionResizeFixes 默认开启。相关源码解释了一个重要时序:
presenting a frame and changing the DComp layer clip rect can happen at different times
也就是:呈现画面内容,与改变 DComp 图层的裁剪区域,可能发生在不同时间。
为处理缩放中的边界,代码会清理视口之外的区域,使其透明。对于透明宿主,这类透明区域可能露出原生窗口背景。
这个源码路径与观察到的现象相符,但还不足以证明 Microsoft 的 WebView2 分支内部具体在哪一步发生了异常。因此,更准确的判断是:对照实验把问题定位到了主画面呈现与 DComp 更新的配合;源码提供了同步策略的解释。
关键的另一段注释是:
With delegated compositing or a buffer queue, Present and Commit are synchronized and the clear is not needed.
这指向了 BufferQueue。
BufferQueue 改变的是主画面的缓冲策略
Chromium 的功能定义描述:Windows 上启用 BufferQueue 后,主画面使用缓冲队列,替代 DXGI swap chain 或 DComp surface。
它保留 DirectComposition,不会把整个浏览器切回此前较慢的呈现路径。源码中的能力门槛为:
return support_level >= OutputSurface::DCSupportLevel::kDCompDynamicTexture && base::FeatureList::IsEnabled(features::kBufferQueue);因此,只开启功能名还不够。运行环境必须支持动态 DComp 纹理;不支持的系统可能继续使用普通路径。
最终实现:保留 DComp,启用 BufferQueue
最终改动位于 emulsion-desktop-v3/main.go 的应用级 Windows 配置:
wailsApp := application.New(application.Options{ // Services、Assets、SingleInstance 等配置保持原有实现。 Windows: application.WindowsOptions{ // 同步主画面缓冲与 DComp 提交。 // 保留 DirectComposition、GPU 渲染与原生 Acrylic。 EnabledFeatures: []string{"BufferQueue"}, },})同时移除旧的 --disable-direct-composition,窗口配置保持:
window := wailsApp.Window.NewWithOptions(application.WebviewWindowOptions{ // 尺寸、路由及其他窗口选项省略。 BackgroundType: application.BackgroundTypeTranslucent, Windows: application.WindowsWindow{ BackdropType: application.Acrylic, Theme: application.SystemDefault, },})浏览器参数在应用级设置,主窗口、菜单独立窗口、存储源表单共用。前端玻璃样式不需要为了这一轮窗口呈现修复改变,工具窗口也不必各复制一份策略。
实际运行参数确认包含:
--enable-features=BufferQueue并确认不再包含:
--disable-direct-composition没有修改 Wails 模块缓存,没有增加定时强制重绘,也没有关闭硬件加速。
如何验证:必须看 Windows 实际合成后的屏幕
浏览器截图可能漏掉原生闪帧
CDP 或 Playwright 截图适合检查 DOM 和布局,但不能完整证明 DWM 没有在某一帧露出宿主背景。
这次使用 Win32 GDI 采样物理屏幕:
- 根据窗口标题和进程 ID找到测试窗口,避免同时运行多个 Emulsion 时选错实例。
- 先还原窗口,再读取屏幕坐标。
- 使用
BitBlt与GetDIBits获取屏幕像素。 - 对静止的不透明内容小区域进行高频采样,保存异常最大的帧和 JSON 数据。
- 对拖拽使用水平窄条采样,追踪固定紫色边缘标记,以及窗口边缘内侧的不透明内容。
闪屏测试采样约 100 次/秒以上,选择小区域才能获得足够时序覆盖。最初约 75ms 一次的大区域截屏就没有捕获到这类短暂异常,不能据此认定没有闪屏。
这仍然是采样验证,不是逐刷新周期录制所有帧;“未捕获异常”需要和可稳定复现的基线对照一起判断。
先保证测量本身有效
排查中踩到了几个会制造“假通过”的坑:
IsWindowVisible返回真不代表窗口未最小化。最小化窗口可能位于-32000坐标,采样屏幕外会得到全零像素。- ctypes 调 Win32 API 必须声明指针宽度正确的参数和返回类型,否则句柄可能截断,甚至只在资源释放时才报错。
- 窗口拖小后,固定的右边缘标记可能还没更新、被窗口裁掉。找不到标记不能直接解释为出现空白。
- 紫色边框会提高 RGB 平均值。检测 Acrylic 空白时必须排除边框颜色,检查另一块本应不透明的内容区域。
- 页面刷新后可能跳回连接页面。每轮测量前应确认路由、外观、主题以及侧栏确实存在。
原生拖拽测试复用了 Wails 启动缩放的消息路径:WM_NCLBUTTONDOWN 加对应边缘命中值。真实标题栏双击则另行发送鼠标点击,并检查 IsZoomed 在最大化和还原后分别成立、取消。
验证动作要覆盖两个时间阶段
缩放之后的测试:
- 最大化,再还原。
- 普通尺寸变化。
- 分别等待 0.7、1.2、2、4 秒,再将鼠标移入会触发重绘的区域。
- 单独验证真实标题栏双击。
缩放过程中的测试:
- 连续拖大和拖小。
- 测页面边缘相对原生边界的落后像素。
- 检查新扩展区域是否露出亮色 Acrylic。
- 区分首轮与后续轮次,不能只选最好看的一组数据。
验证玻璃与 GPU 没被破坏
DWM 属性 DWMWA_SYSTEMBACKDROP_TYPE 的属性编号是 38,读取结果 3 对应 Acrylic。
此外,在验证窗口后面放置临时红色、蓝色背景:
| 采样区域 | 红蓝背景切换后的平均像素差 |
|---|---|
| 侧栏 | 约 33.4/255 |
| 不透明主内容区 | 0 |
这证明侧栏确实还能透出原生背景,内容区也没有跟着变透明。检查仅使用临时背景窗口,没有修改桌面壁纸。
WebGL2 返回的渲染器仍是:
ANGLE (AMD, AMD Radeon 780M Graphics, Direct3D11)因此,改善没有以关闭 GPU 或改用软件渲染为代价。
最终对照结果
简化 Wails 页面
| 指标 | 关闭整个 DirectComposition | 启用 BufferQueue |
|---|---|---|
| 缩放后首次悬停闪帧 | 未捕获 | 8 轮未捕获 |
| 连续拖大平均落后 | 约 24–49 px | 约 0.66–0.86 px |
| 拖大落后峰值 | 96 px | 约 5–9 px |
| BufferQueue 组的 95% 采样 | — | 不超过 4 px |
真实 Desktop 经典外观
| 指标 | 旧的重定向呈现方案 | BufferQueue |
|---|---|---|
| 3 轮连续拖大平均落后 | 约 136 px | 约 7–11 px |
| 亮色 Acrylic 空白带 | 3 轮拖大均出现,参与统计的采样全部命中 | 对照的 3 轮拖大未采样到 |
| 连续缩小 | 内容边缘仍明显落后,标记容易被裁剪 | 不透明内容区域未采样到 Acrylic 空白;标记裁剪不计作空白 |
| 深色主题双击最大化/还原与普通尺寸变化 | — | 合计 8 轮后首次悬停未捕获闪帧 |
| 原生 Acrylic 与 GPU | 保持可用 | 保持可用 |
真实页面比简化页面更重,首轮还可能出现更大的延迟。这次验证不能支持“所有元素零延迟”,能够支持的是:经典外观中旧方案引入的大片材质空白已消除,连续拖拽跟随显著改善,且没有重新引入原先的整窗闪帧。
构建与维护检查
代码变化仅涉及 Windows 浏览器呈现配置。CHANGELOG.md 同步更正此前的修复说明,并将原生回归方法记录到项目规范。
在 Desktop 仓库内完成:
go build -tags 'production,nodynamic' -o '<临时目录>\emulsion-verified.exe' .go test -tags nodynamic .go vet -tags nodynamic .git diff --check上述构建、主包测试、静态检查和差异检查均通过。这里只运行了主包测试,没有声称执行全部 Go 包或完整前端测试套件。
真实页面测试使用隔离的 APPDATA、LOCALAPPDATA 和浏览器缓存,调试端口及测试会话通过临时 Go 构建覆盖文件提供,没有写进正式代码。测试窗口结束后清理,正常开发客户端已确认加载 BufferQueue 配置。
仍需保留的边界
液态玻璃不等于经典 Acrylic
液态玻璃外观还包含 CSS 滤镜、SVG 折射、尺寸监听和材质重算。本次实测中,即使使用 BufferQueue,连续缩放仍有几十像素级的额外落后;首次悬停闪屏测试保持干净。
临时关闭滤镜后延迟也没有完全消失,因此只能判断它包含额外的页面渲染成本,不能将全部延迟简单归因于某一个滤镜或 ResizeObserver。
这一项尚未完成独立的前端性能优化,不能把经典外观的结果推广为两套外观都已同样流畅。
BufferQueue 有能力门槛,也不是永久稳定的 WebView2 API
Chromium 的源码能力判断说明,不支持动态 DComp 纹理的系统可能保留普通呈现路径。命令行里看到功能名,只能证明参数传入,不能单独证明该路径已经激活。
本机通过行为对照得到有效结果;其他 GPU、较旧系统、不同 WebView2 版本、混合 DPI 多屏需要复测。Microsoft 也明确指出,浏览器功能标志可能变化,不应把它们当成长期稳定的公共 API。
因此,升级 WebView2 或 Wails 后,应该重跑以下三组验证:连续拖拽、缩放后首次悬停、Acrylic 与硬件渲染。不能只保留这行配置而忘记它依赖的运行时条件。
这次排查留下的工程经验
第一个教训是,材料颜色变化会掩盖丢帧。白闪变黑闪、暗色内容不再刺眼,都不等于呈现恢复正常。
第二个教训是,宿主 Bounds 更新、页面布局完成和 Windows 屏幕上的一帧,是不同的事件。应根据症状选择观测层,而不是用 DOM 截图证明原生合成没有问题。
第三个教训来自用户反馈:修复闪屏的验收最初只关注操作完成后的状态,漏掉持续拖拽。补上这一项之后,才发现关闭整个 DirectComposition 的代价,并找到更合适的主画面缓冲策略。
最后,保留对照和失败结果比保留一个“有效参数”更有价值。下一次运行时升级之后,可以重新判断:问题是否仍在、BufferQueue 是否仍生效,以及是否还有更合适的框架或浏览器原生修复。
参考源码与文档
下面的 Chromium 链接固定到 milestone 153 分支点提交 86cee6df69e0463a839c0cc8435c9d4c259434d3,用于解释本次排查。它与 Microsoft 私有 WebView2 分支可能存在差异。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!