Emulsion Desktop 窗口闪屏与实时缩放修复:从关闭 DirectComposition 到 BufferQueue

4690 字
23 分钟
Emulsion Desktop 窗口闪屏与实时缩放修复:从关闭 DirectComposition 到 BufferQueue

窗口不闪了,为什么拖大时又出现一片空白?#

Emulsion Desktop 的一次窗口渲染排查,先后出现了三个症状:

  1. 双击标题栏最大化,再还原;鼠标第一次滑过页面时,整个窗口闪一帧浅灰白。
  2. 将窗口材质改成深色后,白闪变成黑闪,侧栏的磨砂也没有原来好看。
  3. 关闭 WebView2 的 DirectComposition 后,闪屏消失,但拖拽窗口变大时,页面内容追不上窗口边界,新扩展区域出现明显空白。

最终保留了原来的透明窗口、Acrylic 和 GPU 渲染,将主画面的呈现策略改为 BufferQueue。在本机的经典外观下,它同时消除了缩放后的首次悬停闪帧,以及拖大时露出的大片材质空白。

这次排查也纠正了一个过早的结论:“缩放结束后不闪”不足以说明窗口渲染修好了,还必须检查持续拖拽中的画面。

复现环境与判断范围#

项目本次环境
应用Emulsion Desktop,0.8.8 开发阶段
框架Wails v3 v3.0.0-beta.28
前端React 19、Vite 6
系统Windows 11,原生 Acrylic 可用
WebView2153.0.4234.32
显示缩放175%
GPUAMD 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,不通过修改材质颜色掩盖问题。实验的目标是同时满足:

  1. 尺寸变化后首次悬停不闪。
  2. 持续拖大时内容跟随边界。
  3. Acrylic 仍能透出背景。
  4. 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 采样物理屏幕:

  1. 根据窗口标题和进程 ID找到测试窗口,避免同时运行多个 Emulsion 时选错实例。
  2. 先还原窗口,再读取屏幕坐标。
  3. 使用 BitBlt 与 GetDIBits 获取屏幕像素。
  4. 对静止的不透明内容小区域进行高频采样,保存异常最大的帧和 JSON 数据。
  5. 对拖拽使用水平窄条采样,追踪固定紫色边缘标记,以及窗口边缘内侧的不透明内容。

闪屏测试采样约 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 仓库内完成:

Terminal window
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 分支可能存在差异。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Emulsion Desktop 窗口闪屏与实时缩放修复:从关闭 DirectComposition 到 BufferQueue
https://shaio.top/posts/03mogallery/desktop/261013webview2窗口闪屏与实时缩放修复/
作者
沙海
发布于
2026-10-13
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
沙海
Hello, I'm Shai.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
84
分类
15
标签
48
总字数
67,035
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
GitHub Actions
博客版本
Firefly v6.12.2
文章许可
CC BY-NC-SA 4.0

文章目录