硅基俊宁.
← 返回文章

08-Compositor、Viz、Skia 与 GPU:一帧如何真正到达屏幕

俊宁 · 日期未详
文章目录

从源码阅读入口、进程边界与验证条件理解Compositor、Viz、Skia 与 GPU:一帧如何真正到达屏幕。

本文讨论通用机制和设计判断;示意不对应任何具体产品的已交付状态。源码阅读坐标沿用原稿,本次整理不构成新的运行时验收。

Chromium 一帧从 Compositor 到屏幕的渲染旅程封面

阅读坐标沿用原稿:Chromium 153.0.8010.37,上游 commit b75a5a95ea1a1b55bdbfd6d9f42d47be7507fb8b。本次同步未重新进行源码与目标平台运行时验收。

Chromium 一帧从 Renderer 到屏幕的生命周期

一句话结论

Renderer 产出 paint/compositor 输入后,仍要经过 commit、activate、raster、CompositorFrame、Viz aggregation 与平台 present;排查卡顿或旧画面时,应先确认慢在生命周期哪一段,而不是笼统归因于“GPU”。

Renderer 得到 paint/compositing 输入时,仍然不是用户看到的一帧:它只是“这次页面想画什么”。接下来 Chromium 要在合适的 BeginFrame 时机组织 layer tree、生成 CompositorFrame、跨进程交给 Viz,把多个页面/Browser UI 的 surface 合成,再经 Skia 和 GPU 或软件后端提交给操作系统显示系统。

因此,排查“页面没显示”“动画掉帧”时,最容易犯的错误是只盯 Blink paint,或者看到 GPU 进程存在就认定 GPU 已经完成绘制。更可靠的问题序列是:Renderer 有没有产出 frame?Viz 有没有接收和聚合?输出 surface 是否能 submit/present?用户看到的是哪一个 frame?

一帧的最小主线

以下是桌面 Chromium 的通用阅读骨架。它表达 ownership 和边界,不承诺每个 feature、平台或测试模式都采用同一线程/进程形态。

VSync / BeginFrame source
  -> Renderer WebFrameWidget / cc::LayerTreeHost 决定是否需要 main frame
  -> Blink 生命周期更新、paint property、layer tree / raster 输入
  -> Compositor 产出 viz::CompositorFrame
  -> CompositorFrameSink 经 Mojo 提交给 Viz
  -> Viz 维护 Surface 与 frame sink,聚合页面、Browser UI、video 等 frame
  -> Display / SkiaOutputSurface 选择 GPU 或软件输出路径
  -> GPU service、driver 与平台 surface 执行 submit/present
  -> OS compositor / display scanout,用户看见一帧

“frame”在这条线里至少有三种语义:Renderer 提交的 CompositorFrame、Viz 聚合后的 display frame、显示系统已经 present 的图像。性能指标、截图、输入响应和用户肉眼观察分别可能落在不同阶段;写埋点或下结论前必须先说明采用哪一种。

四个角色,各自不替对方背锅

角色主要职责常见误解
Blink / Renderer compositor将 document 的可绘制状态组织为 layer tree,按帧节奏提交内容paint 完成就已经显示
cc提供 compositor 调度、layer tree、tile/raster 协作等通用机制cc 只是一层数据结构,不影响时序与吞吐
Viz管理 frame sink、surface、BeginFrame、聚合与 display 输出Viz 等同于 GPU 进程,或只负责“转发 frame”
Skia / GPU service / platform栅格、resource、图形 API/driver 交互和最终 surface 提交有 GPU 就一定硬件加速且永不掉帧

它们的分层避免 Renderer 直接管理桌面窗口、驱动和其他页面的最终合成。Browser 也不需要把每个 tab 的绘制结果同步拷贝到自己的内存再画一次;它通过 RenderWidgetHost、frame sink 与平台视图参与协调,但页面 content 的生产仍由 Renderer 负责。

BeginFrame:不是“每 16ms 强制重画”

BeginFrame 是一轮合成工作的节奏信号。它带有 frame 时间、序号和 deadline 等信息,使观察者能决定这一轮是否需要更新、是否来得及提交、没有内容时如何 acknowledge。它既可能来自显示刷新,也可能在特定条件下由内部或 synthetic source 驱动;后台、不可见、被 throttle 的页面更不能按“每次 VSync 必画”理解。

源码中可从 cc/mojo_embedder/async_layer_tree_frame_sink.cc 进入:OnBeginFrame()、SubmitCompositorFrame()、DidNotProduceFrame() 和 DidReceiveCompositorFrameAck() 将 BeginFrame、提交和 ack 串起。对应的 cc::LayerTreeHost 是 Renderer compositor 的核心阅读入口;Blink 的 WebFrameWidgetImpl::BeginMainFrame() 则把 Web frame/widget 生命周期接到这套 compositor 调度。

这带来两个实际约束:

因此测“掉帧”不能只数 requestAnimationFrame 回调。至少同时观察 BeginFrame、Renderer submit、Viz ack/present;否则无法区分页面没更新、来不及提交、Viz 没使用,还是输出端没有 present。

从 layer tree 到 CompositorFrame:commit、activate、raster 不是一件事

cc::LayerTreeHost 让 main thread 的页面状态与 compositor 消费的状态可以按帧协作。阅读时至少将下面三类工作分开:

阶段关心的问题不能简化成
Commitmain thread 的 layer/property/动画状态何时同步给 compositorDOM 已变就一定立即可画
Activate某个已准备状态何时成为当前可用于 draw 的 active statecommit 完成就已显示
Raster / resourcepicture、tile、image 等怎样变成可供 draw 的资源,是否受 budget/优先级约束GPU raster 一定无 CPU 成本或绝不阻塞

同一个视觉改动可能只要求属性动画合成,也可能要求 main thread 重算、重新 paint、raster 或更大的 surface 更新。DOM/style/layout/paint 的变化,不能直接反推这里的 commit/raster 成本;反过来,一个 GPU 活跃也不说明 Renderer 的 layout 很快。性能问题必须定位到实际跨过的阶段。

CompositorFrameSink 与 Surface:跨边界提交的对象是什么

Renderer 不把“屏幕像素”直接发给 Browser。它提交的是 viz::CompositorFrame,其中包含 render pass、resource、damage、metadata 等合成输入;具体 resource 是否共享、如何同步、是否为软件路径,取决于 context 与平台配置。

这一边界在基线源码中有清晰的三组入口:

位置说明
third_party/blink/renderer/core/frame/web_frame_widget_impl.ccWebFrameWidgetImpl 持有/使用 LayerTreeHost,是 Web widget 通向 compositor 的阅读入口。
cc/mojo_embedder/async_layer_tree_frame_sink.ccAsyncLayerTreeFrameSink::SubmitCompositorFrame() 通过 CompositorFrameSink 消息端点提交 frame,并接收 BeginFrame/ack。
content/browser/renderer_host/render_widget_host_impl.ccRenderWidgetHostImpl 创建并把 frame sink 端点交给 Browser 侧 HostFrameSinkManager;注释也明确 Renderer 可能在 context loss 后请求新的 sink。

FrameSinkId 和 LocalSurfaceId 解决的不是“给一个窗口取名字”,而是标识 frame 的生产者及其 surface 生命周期。尺寸、device scale factor、嵌入关系或 output surface 变化时,旧 surface 不能被无条件复用;错误地复用或错误归属 frame sink,常表现为黑块、旧帧、input 坐标不一致或嵌入内容消失。

Viz:从多个 surface 到一个 display

Viz 不是单页 Renderer 的后处理器。它需要管理多个 frame sink 和 surface,将网页、Browser chrome、iframe/嵌入内容、video overlay 等可组合输入按嵌入关系聚合为 display output。components/viz/service/frame_sinks/frame_sink_manager_impl.* 和 components/viz/service/display/surface_aggregator.* 是理解这件事的两个核心入口:前者管理 sink/surface 的服务侧状态,后者执行 surface aggregation。

这解释了为什么“Renderer 交帧了但窗口还是旧画面”是可能发生的:frame 也许还没有被用于当前 aggregation,输出 surface 可能不可用,damage 可能为空或被覆盖,窗口/视图可能正在隐藏或重建。反之,页面本身不 repaint,Browser UI 也可能提交自己的 surface,导致用户仍看到窗口局部变化。

阅读 Viz 时推荐使用以下表格记录证据:

要验证的结论静态入口运行时证据
Renderer 是否真的提交新 frameAsyncLayerTreeFrameSink::SubmitCompositorFrame()trace 中的 submit、frame token/sequence
Browser 是否创建/连接对应 frame sinkRenderWidgetHostImpl 的 CreateCompositorFrameSink 路径Mojo/trace 与目标 FrameSinkId
Viz 是否使用该 surfaceFrameSinkManagerImpl、SurfaceAggregatorviz trace、surface/frame 激活记录
是否产生输出和 presentDisplay、SkiaOutputSurface、平台视图GPU/viz trace、swap/presentation feedback

Skia、GPU service 与平台输出:后端不是一个固定选项

components/viz/service/display/skia_output_surface.* 与 display_embedder/skia_output_surface_impl* 是 display 输出的核心阅读入口;components/viz/service/gl/gpu_service_impl.* 则连接 GPU service 的初始化与服务能力。它们共同把 Viz 的合成结果交给图形后端,但“用 Skia”不自动等于“在独立 GPU 进程以某一种 API 完成所有绘制”。

实际后端可能随操作系统、driver/blocklist、命令行、headless、remote desktop、context loss、软件 fallback 和 feature flag 变化。macOS、Windows、Linux 对 native surface、presentation feedback 和窗口嵌入的实现也各不相同。RenderWidgetHostViewMac、Windows/Aura 等平台 view 文件可以作为平台差异的落点,但不应从某个平台类名反推桌面 Chromium 的通用执行时序。

特别要把两个失败面分开:

它们都可能在 UI 上表现为白屏或卡顿,却需要从不同 trace category、进程日志和平台图形诊断开始。

性能与排障:用 frame 生命周期定位,不用目录猜

现象首先判断典型下一跳
JS 已执行、界面没变化DOM/style/paint 是否真的产生可合成更新Renderer lifecycle 与 invalidation,再看 LayerTreeHost 是否请求 frame
动画回调正常但肉眼掉帧BeginFrame、main frame、submit、ack/present 是否缺一环AsyncLayerTreeFrameSink、viz trace、GPU/presentation feedback
新 Renderer frame 后显示旧画面frame sink/surface 是否仍匹配、Viz 是否聚合RenderWidgetHostImpl、FrameSinkManagerImpl、SurfaceAggregator
黑屏或 context loss 后恢复异常sink 是否重建、resource/context 是否重新建立Browser 侧 create sink、GPU service、平台 view
只在某平台或远程环境出现实际图形后端、driver、headless/remote 配置GN args/命令行、GPU 信息、对应平台 output/view 实现

一次 Performance trace 看见 “Paint” 并不足以完成这张表;同样,任务管理器里 GPU usage 升高也不能证明内容已 present。每次结论都应明确所观察的 frame id/token、进程和阶段。

运行时验证路径

从故障反推边界:提交了一帧,为什么用户仍看到旧画面

Renderer submit 只说明一个 CompositorFrame 进入后续链路。Surface dependency 可能尚未满足,Viz aggregation 可能等待其他 surface,raster/upload 可能阻塞,display scheduler 可能错过 deadline,平台 swap/present 也可能延后;后台 Tab、遮挡、功耗策略和 GPU context loss 还会改变调度。

因此截图、录屏、低延迟 overlay 或自动化视觉判断都要写清采样点。抓到 Renderer-side paint artifact、Viz aggregated frame 和操作系统已 present 的画面代表不同事实。若产品要求“动作完成后截图”,还需定义等待条件、超时、navigation/resize 时的失效语义,避免把上一帧误当作当前结果。

Frame pipeline 是生产者与消费者的协作

Renderer 主线程产生 DOM/style/layout/paint 状态,compositor thread 管理 layer tree 和 frame 提交,Viz/GPU 侧聚合 surface 并驱动 display。各阶段存在队列和节奏控制,不是上一阶段完成就立刻同步执行下一阶段。

BeginFrame 提供一次产生新帧的机会,Renderer 可以因为没有 damage、主线程未完成或被节流而不提交;display 也可能因为 backpressure、surface dependency 或平台调度没有立即采用新 frame。分析掉帧时,应判断是生产者没按时产出、frame 在提交/聚合中等待,还是 present 被平台延后。

Commit、Activate 与 Draw 为什么分开

主线程更新的 layer tree 通常需要 commit 到 compositor side,再等待资源和依赖达到可激活状态。active tree 用于真正 draw,pending tree 可以并行准备下一状态。这种双树/流水设计避免主线程更新直接破坏当前可显示帧,却也带来“代码执行了,画面晚一帧甚至多帧”的现象。

如果 commit 很快但 activate 等待,应查看 raster/resource readiness;如果 activate 完成但没有 draw,检查 damage、visibility、BeginFrame 和 display 调度;如果 submit 完成但画面没变,继续看 surface aggregation 与 present feedback。把这些阶段合并成一个“composite”指标会掩盖根因。

Surface identity 解决跨组件合成的生命周期

Viz 不只接收一个页面的一张 bitmap。Browser UI、页面、iframe、视频、popup 等内容可能通过不同 CompositorFrameSink 和 surface 组合。SurfaceId/LocalSurfaceId 帮助系统识别某个尺寸、scale 与生命周期下的 frame 序列,parent surface 通过引用组合 child surface。

当 child surface 尚未提交、已被替换或引用关系不完整时,聚合器可能继续使用旧 surface、显示 fallback,或等待依赖。这类问题看上去像“iframe 没刷新”“视频停在旧帧”,却不一定发生在 Blink paint。应同时记录 frame sink、surface id、submit/aggregate/present 时序。

Surface 生命周期还决定资源何时可以回收。过早释放会产生缺帧或 GPU 错误,过晚释放则会堆积纹理和内存。进程崩溃、Tab 隐藏、缩放/旋转和窗口 resize 都可能创建新的 local surface identity,是故障注入的重要场景。

Raster、Skia 与 GPU 后端不是一条固定路线

Paint 产生的绘制记录可以按 tile raster,后端可能使用 GPU、软件或平台特定组合;Skia 提供绘制与图形能力,ANGLE/驱动/系统 compositor 继续参与最终显示。不同设备、driver blocklist、feature flag 和 context 状态会改变实际路径。

因此一次 GPU 问题必须记录 GPU process、backend、driver、feature status、是否 software fallback 和 context loss。只在开发机关闭 GPU 后“恢复正常”,只能定位到图形路径相关,不能直接得出应在产品中全局禁用 GPU。

将卡顿拆成四种类型

类型典型表现首要证据常见根因方向
Main-thread jank输入、脚本、layout 同时推迟Renderer main trace、long taskJS、同步 layout、大范围 style/paint
Compositor miss主线程空闲但帧没按节奏提交compositor scheduler、BeginFramecommit/activate、资源等待、节流
GPU/Viz stallsubmit 后聚合或 draw 慢Viz/GPU trace、surface/resourceraster、纹理、driver、context loss
Presentation delaydraw 完成但 present feedback 晚display/present timing、OS 工具vsync、窗口系统、显示器/平台策略

实际问题可能同时跨多类。例如主线程晚提交导致 GPU 空闲,随后大批 raster 又造成 GPU 峰值。分析应以同一 frame sequence 对齐阶段,而不是分别截取 CPU 和 GPU 的最高点。

一次视觉回归怎样完成验证

以“AI 侧边栏展开时页面闪旧画面”为例,先给侧边栏 resize/visibility 动作和页面 frame 建立 trace marker;检查 Renderer 是否提交包含新 viewport 的 frame、Viz 是否聚合了正确 child surface、present feedback 是否对应新 frame。再分别验证普通页面、视频/iframe 页面、不同 scale factor、窗口 resize 与 GPU fallback。

截图对比只能证明最终像素差异,无法解释旧帧来自 paint、surface 还是 present。将截图与 frame timeline、surface identity 和 GPU 状态结合,才能把修复写成稳定 contract。

截图、录屏与用户所见也可能不是同一帧

DevTools screenshot、tab capture、系统录屏和肉眼观察可能从不同 surface 或 presentation 阶段取样。定位“截图正确但屏幕闪烁”时,要记录采样源与 frame token;否则一张截图会错误地排除 display/present 问题。跨显示器、不同 refresh rate、HiDPI scale 和窗口遮挡也应进入平台验收,而不是只在固定开发屏幕检查。

工程结论

帧生命周期把“页面想画什么”和“用户真的看见什么”隔开。性能优化与视觉能力应尽量沿现有 surface/frame ownership 接入,并用同一 frame identity 对齐 trace,而不是绕过 Viz/GPU 生命周期。

继续阅读

上游源码入口

文中的相对源码路径可从 Chromium 官方上游快照 定位。此入口保留原稿的上游阅读坐标,不包含任何产品定制代码,也不替代运行时验收。