使用github copliot chat的源码学习之Chromium Compositor
github推出了 copilot chat,里面比较方便的是它可以很快速地搜索和获取各开源项目的上下文。除了单纯的Ask,其实还有执行任务的Agent(不过要开copilot pro),似乎目前是限制了每个月50次request。
因此我利用这个来学习源码,让AI回答关于源码的东西。下面的内容是出自copilot的回答(我认为比较好的),只是个人进行了整理。 这个博客的营养价值并不高,最大的价值可能就是告诉你有这个方便的途径去了解和学习源码。可以直接跳到完整示例,前面大抵只是一些铺垫。
Chromium 页面渲染的核心架构
1. Blink 渲染引擎 (核心组件)
根据官方文档,Blink 是 Chromium 的渲染引擎,负责页面内容的最终展示:
## Blink (Rendering Engine)
Blink 实现了所有在浏览器标签页内渲染内容的功能:
* 实现网络平台规范(如HTML标准),包括DOM、CSS和Web IDL
* 嵌入V8引擎并运行JavaScript
* 从底层网络栈请求资源
* **构建DOM树**
* **计算样式和布局**
* **嵌入Chrome Compositor并绘制图形**
2. 多进程渲染管道
Chromium 采用多进程架构:
- Browser Process (浏览器进程):管理用户交互、网络请求、打印等
- Renderer Process (渲染进程):在沙箱中运行,执行 Blink 引擎进行页面渲染
- Compositor Process:负责最终的图形合成和显示
3. 渲染流程的关键步骤
a. 页面加载与DOM构建
// PrintRenderFrameHelper 处理渲染相关的工作
class PrintRenderFrameHelper
: public blink::WebPrintClient,
public content::RenderFrameObserver {
// 为打印准备页面框架
void OnFramePreparedForPrintPages();
void PrintPages();
bool RenderPagesForPrint(blink::WebLocalFrame* frame,
const blink::WebNode& node);
};
b. 计算布局与样式
Blink 引擎在 DOM 树构建后,计算每个元素的样式(CSS)并确定其在页面中的位置和大小。
c. 绘制(Paint)与栅格化(Rasterization)
// 页面内容渲染到画布
void RenderPageContent(blink::WebLocalFrame* frame,
uint32_t page_index,
cc::PaintCanvas* canvas) {
TRACE_EVENT1("print", "RenderPageContent", "page_index", page_index);
frame->PrintPage(page_index, canvas);
}
d. 图形合成(Compositing)
Chrome Compositor 负责合成不同的图层,最终生成帧(Frame)。
4. Compositor 的作用
Compositor 是 Chromium 的图形合成器,用于:
- 合成多个渲染层
- 处理复杂的变换和效果
- 优化渲染性能
- 管理硬件加速渲染(GPU)
5. 渲染周期(Frame Lifecycle)
一个完整的渲染周期包括:
输入事件 → DOM 更新 → 样式计算 → 布局计算 → 绘制操作 → 栅格化 → 合成 → 显示
6. 优化策略
- 增量渲染:只重新渲染受影响的区域(Dirty Regions)
- 分层渲染:使用多个渲染层提高效率
- GPU 加速:利用硬件加速加快渲染速度
- 异步渲染:多个进程并行处理不同的渲染任务
7. 性能监控
Chromium 内置性能监控:
// PDF 渲染的性能测量
if (!first_paint_metric_reported_ && !client_->IsPrintPreview()) {
first_paint_metric_reported_ = true;
base::UmaHistogramMediumTimes("PDF.FirstPaintTime",
begin_time - engine_creation_time_);
}
总结
Chromium 的页面渲染是一个复杂的分阶段过程:
- 解析阶段:解析 HTML/CSS/JavaScript,构建 DOM 和样式树
- 计算阶段:计算布局(Layout)和样式(Style)
- 绘制阶段:生成绘制指令(Paint Commands)
- 栅格化阶段:将绘制指令转换为像素数据
- 合成阶段:合成多个层并送到显示设备
- 显示阶段:在屏幕上展示最终画面
Compositor 的完整分层架构和数据流
根据官方文档 life_of_a_frame.md 和代码分析,Compositor 分为以下主要部分:
1. BeginFrame 协调层(Frame Coordination)
输入:来自 Display Compositor 的 BeginFrame 信号
↓
输出:BeginFrameArgs(包含时间戳、帧号等)
职责:
- 同步主线程和合成线程的帧生成
- 管理帧率和垂直同步
- 分发 BeginFrame 通知
2. 主线程阶段(Main Thread Pipeline)
BeginFrame
↓
[1] BeginMainFrame - 合成线程请求主线程更新
↓
输入:LayerTreeHost 配置、用户输入、动画状态
输出:BeginMainFrameAndCommitState
↓
[2] Animate - 主线程更新动画
↓
输入:当前时间、动画状态
输出:更新后的属性值
↓
[3] UpdateLayers - Blink 执行布局和绘制
↓
输入:DOM 树、样式树
输出:DisplayItemList(绘制操作)
↓
[4] Commit - 推送改变到合成线程
↓
输入:Layer 树、DisplayItemList、属性
输出:CommitState(原子性提交状态)
详细步骤:
1. Animate Phase
输入:LayerAnimationController (cc/animation/*)
输出:变换、不透明度等属性的新值
2. UpdateLayers Phase
输入:Layer 树,由 Blink 通过 LayerTreeHostClient 接口触发
- client_->UpdateLayerTreeHost()
- Blink 执行布局计算
- Blink 执行绘制,产生 DisplayItemList
输出:更新后的 Layer 树和 DisplayItemList
3. Commit Phase
- PushPropertiesTo():每个 Layer 推送属性到 LayerImpl
- 交换 Pending Tree(待定树)和 Active Tree(活动树)
- 同步动画状态、滚动状态等
3. 合成线程阶段(Impl Thread Pipeline)
这是 Compositor 的核心,分为五个关键阶段:
阶段 1:Commit 完成(Finish Commit)
输入:CommitState
- Layer 树结构和属性
- DisplayItemList
- Scroll 状态
- 动画数据
处理:
- FinishCommit():合成线程接收提交的状态
- 更新 LayerImpl 树
- 更新属性树(Transform Tree、Clip Tree 等)
输出:LayerTreeImpl(合成线程的活动树)
阶段 2:Prepare Tiles(准备瓦片)
输入:LayerTreeImpl、显示视口、缩放因子
处理:
- CalculateRasterScales():计算每个层的栅格化缩放
- PrepareTiles():
├─ CalculateLiveRects():计算可见瓦片范围
├─ AssignGpuMemoryToTiles():分配 GPU 内存预算
└─ ScheduleRasterTasks():安排栅格化任务队列
- TileManager 的职责:
├─ 优先级排序(视口内 > 视口外)
├─ 管理软件和 GPU 栅格化
├─ 管理图像解码
└─ 管理蛋糕层(cake layers)
输出:Raster Tasks(栅格化任务队列)
- 每个任务是:(Tile, RasterSource, Priority)
阶段 3:Rasterization(栅格化)
输入:Raster Tasks
- RasterSource(DisplayItemList 的包装)
- 目标��片大小
- 缩放因子
- 栅格化位置
处理过程(在工作线程执行):
1. PaintOpBuffer Playback
输入:DisplayItemList 中的 PaintOps
处理:
- GetOffsetsOfOpsToRaster():使用 R-Tree 查询需要的操作
- 创建 SkCanvas(CPU 或 GPU)
- 遍历相关 PaintOps,调用 Raster()
输出:像素数据或 GPU 命令
2. Software Raster(软件栅格化)
输入:PaintOps,输出大小
处理:
- 在内存中创建位图
- 使用 Skia 的 CPU 后端绘制
- 使用 SIMD 优化
输出:SkBitmap(CPU 内存中的像素)
3. GPU Raster(GPU 栅格化)
输入:PaintOps,GPU 上下文
处理:
- 序列化 PaintOps
- 通过 RasterInterface 发送到 GPU
- GPU 命令缓冲区执行绘制
- Skia 的 GPU 后端(Ganesh)处理
输出:GPU 纹理资源
输出:Rasterized Tiles
- 包含像素数据(软件)或 GPU 纹理(GPU)
关键类和函数:
// RasterSource:DisplayItemList 的包装,提供栅格化接口
class RasterSource {
void PlaybackToCanvas(
SkCanvas* raster_canvas,
const gfx::Rect& canvas_bitmap_rect, // 目标位置
const gfx::AxisTransform2d& raster_transform, // 缩放
const PlaybackSettings& settings);
};
// TileManager:协调栅格化
class TileManager {
void PrepareTiles(const PrepareTilesParams& params);
void ScheduleRasterTasks();
};
// DisplayItemList:包含 PaintOps
class DisplayItemList {
void Raster(SkCanvas* canvas, const PlaybackParams& params);
std::vector<size_t> OffsetsOfOpsToRaster(SkCanvas* canvas);
};
阶段 4:Activation(激活)
输入:Pending Tree(待定树,已栅格化)
处理:
- WaitForAllTilesToRasterize():等待所有关键瓦片栅格化完成
- ActivatePendingTree():
├─ Pending Tree → Active Tree
├─ 更新动画状态
├─ 更新页面缩放
└─ 清空已完成的栅格化队列
输出:Active Tree(已激活的树,可用于绘制)
阶段 5:Draw Frame(绘制帧)
输入:Active Tree、BeginFrameArgs
处理:
1. CalculateDrawProperties()
输入:Active Tree、视口、设备缩放因子
计算:
- 每个层的最终变换矩阵
- 剪裁区域
- 可见范围
输���:DrawProperties(每个 LayerImpl 都有)
2. GenerateCompositorFrame()
输入:Active Tree with DrawProperties
处理:
- 遍历 Layer 树,生成 Quads(四边形)
- 每个 Quad 包含:
├─ 纹理/资源 ID
├─ 变换矩阵
├─ 剪裁区域
├─ 不透明度
└─ 混合模式
- 为每个 Quad 创建 RenderPass
- 设置组合帧元数据
输出:CompositorFrame
3. CompositorFrame 结构
viz::CompositorFrame {
vector<RenderPass>:
- RenderPass[0]:第一个离屏渲染目标
- RenderPass[1]:第二个离屏渲染目标
- ...
- RenderPass[N]:最后一个输出到显示器的渲染通道
vector<TransferableResource>:引用的 GPU 纹理
CompositorFrameMetadata:元数据
- frame_token:帧标识符
- device_scale_factor:设备像素比
- latency_info:性能指标
- presentation_token:展示令牌
}
输出:CompositorFrame(包含所有绘制指令)
4. Frame Sink & Viz(显示合成器)
输入:CompositorFrame
处理:
1. SubmitCompositorFrame()
- 验证资源有效性
- 同步令牌处理
- 帧令牌生成
2. Viz 处理(viz/service/display/)
- AggregateSurfaces():合成多个 Surface
- ApplyFilters():应用滤镜效果
- Rasterize():最后栅格化(如果需要)
- GenerateDamageRect():计算脏区域
3. Display 合成
- 将多个来源的帧合成到最终输出表面
- 应用变换和效果
输出:最终的屏幕显示内容
整体数据流图
┌─────────────────────────────────────────────────────────────────┐
│ MAIN THREAD (主线程) │
│ │
│ Layer Tree BeginMainFrame │
│ │ Animate │
│ │ UpdateLayers (Blink Layout & Paint) │
│ │ → DisplayItemList (PaintOps) │
│ │ │
│ └──→ Commit (PushPropertiesTo) │
│ CommitState ─────┐ │
│ │ │
└────────────────────────────┼────────────────────────────────────┘
│
┌────────▼──────────┐
│ ProxyImpl Bridge │
│ (Main ↔ Impl) │
└────────┬──────────┘
│
┌────────────────────────────▼────────────────────────────────────┐
│ COMPOSITOR THREAD (合成线程) │
│ │
│ 1. FinishCommit() │
│ CommitState → LayerTreeImpl │
│ 更新 LayerImpl 树、属性树 │
│ │
│ 2. PrepareTiles() ◄─ Scheduler 调度 │
│ ├─ 计算栅格化缩放 │
│ ├─ 分配 GPU 内存 │
│ └─ 生成 RasterTasks 队列 │
│ │ │
│ ▼ (Post to Worker Threads) │
│
│ 3. Rasterization (Worker Threads) │
│ ├─ Software: DisplayItemList → SkBitmap │
│ └─ GPU: PaintOps → GPU Textures │
│ │ │
│ ▼ │
│
│ 4. Activation() ◄─ Scheduler 调度 │
│ Pending Tree (Rasterized) → Active Tree │
│ │
│ 5. DrawFrame() / GenerateCompositorFrame() │
│ ├─ CalculateDrawProperties() │
│ ├─ BuildQuads() │
│ ├─ 生成 RenderPass 列表 │
│ └─ 输出 CompositorFrame │
│ │ │
│ ▼ │
│
│ SubmitCompositorFrame(frame) │
│ │ │
└────────────┼──────────────────────────────────────────────────┘
│
┌────────▼───────────┐
│ LayerTreeFrameSink │
│ (GPU Channel) │
└────────┬────────────┘
│
┌────────────▼──────────────────────────────────────────────────┐
│ VIZ DISPLAY COMPOSITOR (显示合成器) │
│ │
│ 1. MaybeSubmitCompositorFrame() │
│ - 验证帧 │
│ - 应用同步令牌 │
│ │
│ 2. AggregateSurfaces() │
│ - 多个 Surface 合成 │
│ - Z-order 排序 │
│ │
│ 3. Display::Draw() │
│ - GPU 驱动程序指令 │
│ - 应用滤镜和效果 │
│ │
│ 4. Swap & Present │
│ - 缓冲交换 (Backbuffer → Frontbuffer) │
│ - VSync 同步 │
│ - 显示器显示 │
│ │
└──────────────────────────────────────────────────────────────┘
关键概念总结表
| 阶段 | 输入 | 处理器 | 输出 | 执行线程 |
|---|---|---|---|---|
| Animate | AnimationState | LayerAnimationController | 属性值 | Main |
| UpdateLayers | DOM 树、样式 | Blink | DisplayItemList | Main |
| Commit | Layer 树 | LayerTreeHost | CommitState | Main→Impl |
| FinishCommit | CommitState | LayerTreeHostImpl | LayerTreeImpl | Impl |
| PrepareTiles | LayerTreeImpl | TileManager | RasterTasks | Impl |
| Rasterization | RasterTasks | RasterWorkerPool | GPU 纹理/位图 | Worker |
| Activation | Pending Tree | LayerTreeHostImpl | Active Tree | Impl |
| DrawFrame | Active Tree | LayerTreeHostImpl | CompositorFrame | Impl |
| Display | CompositorFrame | DisplayCompositor | 屏幕输出 | Display |
ProxyMain/ProxyImpl 的角色
ProxyMain (主线程)
├─ 负责与主线程通信
├─ 接收 BeginMainFrame 信号
├─ 管理 CommitPipelineStage
└─ 回调主线程结果
ProxyImpl (合成线程)
├─ 负责与合成线程通信
├─ 控制 Scheduler
├─ 管理 LayerTreeHostImpl
└─ 提交 CompositorFrame 到 LayerTreeFrameSink
Compositor 完整示例:绘制一个蓝色正方形
让我们假设要在网页上绘制一个蓝色的 200×200 像素正方形,位置在 (100, 100)。
第1步:主线程 - Blink 记录绘制操作
用户在 HTML/CSS 中写了:
<div style="
width: 200px;
height: 200px;
background-color: blue;
position: absolute;
top: 100px;
left: 100px;
"></div>
Blink 布局完成后,知道要绘制:
- 位置:(100, 100)
- 大小:200×200
- 颜色:蓝色 RGB(0, 0, 255)
然后它创建一个 DisplayItemList 来记录这个绘制操作:
// Blink 在主线程上执行
auto display_list = base::MakeRefCounted<cc::DisplayItemList>();
display_list->StartPaint();
// 1. 保存图形状态
display_list->push<cc::SaveOp>();
// 2. 绘制蓝色正方形
cc::PaintFlags blue_flags;
blue_flags.setColor(SK_ColorBLUE); // RGB(0, 0, 255)
blue_flags.setStyle(SkPaint::kFill_Style);
display_list->push<cc::DrawRectOp>(
SkRect::MakeXYWH(100, 100, 200, 200), // x, y, width, height
blue_flags
);
// 3. 恢复图形状态
display_list->push<cc::RestoreOp>();
display_list->EndPaintOfUnpaired(gfx::Rect(100, 100, 200, 200));
// 完成记录
display_list->Finalize();
// 此时 DisplayItemList 内部包含:
//
// paint_op_buffer_ = [
// SaveOp,
// DrawRectOp(x=100, y=100, w=200, h=200, color=blue),
// RestoreOp
// ]
//
// visual_rects_ = [
// {100, 100, 200, 200}, // SaveOp 的可视范围
// {100, 100, 200, 200}, // DrawRectOp 的可视范围
// {100, 100, 200, 200} // RestoreOp 的可视范围
// ]
这个阶段的输出: 一个包含"绘制指令"的对象,说明要在 (100,100) 位置绘制一个 200×200 的蓝色矩形。但这只是指令,还没有真正的像素数据!
第2步:提交到合成线程
主线程把 DisplayItemList 包装在 CommitState 中,发送给合成线程:
// 主线程创建 CommitState
auto commit_state = std::make_unique<CommitState>();
// 把 DisplayItemList 分配给对应的 Layer
// (在实际代码中,DisplayItemList 被存储在 PictureLayerImpl 中)
commit_state->source_frame_number = 120;
commit_state->device_viewport_rect = gfx::Size(1920, 1080);
commit_state->device_scale_factor = 1.0f; // 假设普通屏幕
commit_state->background_color = SK_ColorWHITE;
// 推送到合成线程
layer_tree_host_->WillCommit(...)
// ...
layer_tree_host_->ActivateCommitState() // 原子性推送
这个阶段的输出: CommitState 对象,包含所有需要的信息,包括 DisplayItemList。
第3步:合成线程 - 准备栅格化
合成线程收到 CommitState,开始准备栅格化:
// 合成线程上
LayerTreeHostImpl* host_impl = ...;
// 1. 接收 CommitState,更新 LayerTreeImpl
host_impl->FinishCommit(commit_state);
// 2. 准备瓦片(Tiles)
TileManager* tile_manager = host_impl->tile_manager();
tile_manager->PrepareTiles();
// 这会为 PictureLayerImpl 创建 Tiles
// 因为我们的正方形只有 200×200,可能只需要 1 个 Tile
// 假设 Tile 大小是 256×256
struct Tile {
gfx::Rect rect; // 瓦片在页面上的位置
RasterSource* source; // 指向 DisplayItemList (包装后)
float scale; // 栅格化缩放因子
int x, y; // 瓦片的网格坐标
};
// 创建一个瓦片
Tile tile0{
.rect = gfx::Rect(0, 0, 256, 256), // 页面坐标
.source = raster_source, // 包含我们的 DisplayItemList
.scale = 1.0f,
.x = 0,
.y = 0
};
// TileManager 会创建栅格化任务
struct RasterTask {
Tile* tile;
RasterSource* raster_source;
gfx::Rect target_rect; // 在瓦片中的位置
Priority priority;
};
RasterTask task{
.tile = &tile0,
.raster_source = raster_source,
.target_rect = gfx::Rect(0, 0, 256, 256),
.priority = PRIORITY_NOW // 视口内,需要立即栅格化
};
// 提交栅格化任务给工作线程
tile_manager->ScheduleRasterTasks(&task);
这个阶段的输出: 栅格化任务队列,告诉工作线程要栅格化哪些区域。
第4步:工作线程 - 栅格化(最关键的部分!)
这是把绘制指令转换成实际像素的地方:
// 工作线程上执行栅格化任务
void RasterWorkerPool::RasterizeTask(const RasterTask& task) {
// 创建一个绘制目标(画布)
// 这是一个 256×256 的缓冲区,用来存放栅格化后的像素
// 方案 A:CPU 栅格化(软件)
if (use_software_raster) {
// 创建 CPU 内存中的位图
SkBitmap bitmap;
bitmap.allocN32Pixels(256, 256); // 256×256 的 32 位 RGBA 像素
// 创建 Skia ��布,指向这个位图
SkCanvas canvas(bitmap);
// 现在我们要回放 DisplayItemList 中的绘制指令
RasterSource* source = task.raster_source;
// 关键步骤:回放绘制操作!
source->PlaybackToCanvas(
&canvas, // 目标画布
gfx::Size(1920, 1080), // 内容大小
gfx::Rect(0, 0, 256, 256), // 瓦片在内容中的位置
gfx::Rect(0, 0, 256, 256), // 需要栅格化的区域
gfx::AxisTransform2d(1.0f), // 无缩放
settings
);
// 现在 bitmap 中包含了栅格化后的像素!
// 具体来说:
// - 位置 (100, 100) 到 (300, 300) 的像素被设为蓝色
// - 其他像素是白色(背景色)
// bitmap 的内存布局示意:
//
// 位置 (0,0) ──────────────────────→ (256,0)
// │ FFFFFF FFFFFF FFFFFF ...
// │ FFFFFF FFFFFF FFFFFF ...
// │ ...
// (100,100) 开始
// │ FFFFFF FFFFFF ...
// │ ...
// │ FFFFFF 0000FF 0000FF 0000FF ... ← 蓝色像素!
// │ FFFFFF 0000FF 0000FF 0000FF ...
// │ FFFFFF 0000FF 0000FF 0000FF ...
// │ ...
// (300,300) 结束
// │ FFFFFF FFFFFF FFFFFF ...
// ↓
// (256,256)
// 上传到 GPU
UploadBitmapToGPU(&bitmap, tile);
}
// 方案 B:GPU 栅格化
else {
// 创建 GPU 纹理作为渲染目标
// 尺寸:256×256,格式:RGBA_8888
GLuint texture = gl::CreateTexture(256, 256, GL_RGBA8);
// 绑定为渲染目标
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D, texture, 0);
// 清除背景(白色)
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 创建 Skia 画布(绘制到 GPU)
SkSurface* surface = SkSurface::MakeFromBackendTexture(...);
SkCanvas* canvas = surface->getCanvas();
// 回放 DisplayItemList 的绘制操作
// 这次是发送 GPU 命令
DisplayItemList::Raster(canvas, ...);
// GPU 执行指令,在纹理中画出蓝色正方形
// 纹理内容现在是:位置 (100, 100) 到 (300, 300) 是蓝色像素
}
}
这个阶段发生了什么:
- 创建一个 256×256 的缓冲区(可以是 CPU 内存或 GPU 纹理)
- 清除背景为白色
-
回放 DisplayItemList 中的绘制指令:
- SaveOp:保存状态
- DrawRectOp(x=100, y=100, w=200, h=200, color=blue):在像素 (100,100) 到 (300,300) 之间填充蓝色
- RestoreOp:恢复状态
具体的像素数据示意:
CPU 内存(SkBitmap)或 GPU 纹理的内容
(每个方块代表一个像素,用 16 进制表示 RGBA 颜色值)
y=0 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF ... (全是白色)
y=1 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF ...
...
y=99 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF ...
y=100 FFFFFFFF ... FFFFFFFF 0000FFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFFFF ...
^ ^ (100,100) 蓝色开始 ^ (300,100) 蓝色结束
y=101 FFFFFFFF ... FFFFFFFF 0000FFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFFFF ...
y=102 FFFFFFFF ... FFFFFFFF 0000FFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFFFF ...
...
y=299 FFFFFFFF ... FFFFFFFF 0000FFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFFFF ...
y=300 FFFFFFFF ... FFFFFFFF 0000FFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFFFF ...
^ ^ (100,300) 蓝色最后一行 ^ (300,300) 蓝色结束
y=301 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF ...
...
y=255 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF ...
其中:
- FFFFFFFF = 白色 (RGBA: 255,255,255,255)
- 0000FFFF = 蓝色 (RGBA: 0,0,255,255)
这个阶段的输出: GPU 纹理或位图,包含 256×256 个像素的实际颜色数据。
第5步:合成线程 - 生成 CompositorFrame
现在我们有了栅格化后的纹理,合成线程创建最终的合成帧:
// 合成线程
void LayerTreeHostImpl::GenerateCompositorFrame() {
// 创建 CompositorFrame
viz::CompositorFrame frame;
// 设置元数据
frame.metadata.frame_token = 0x12345;
frame.metadata.device_scale_factor = 1.0f;
frame.metadata.size_in_pixels = gfx::Size(1920, 1080);
frame.metadata.begin_frame_ack = viz::BeginFrameAck(...);
// 创建渲染通道
auto render_pass = viz::CompositorRenderPass::Create();
render_pass->SetNew(
viz::CompositorRenderPassId(1),
gfx::Rect(0, 0, 1920, 1080), // 输出矩形(整个屏幕)
gfx::Rect(100, 100, 200, 200), // 脏区域(只有正方形区域需要重绘)
gfx::Transform() // 无变换
);
// 添加 Quad(纹理四边形)
auto quad = std::make_unique<viz::TextureDrawQuad>();
// 从栅格化任务中获取纹理 ID
ResourceId texture_id = 0x9999; // GPU 纹理的句柄
quad->SetNew(
nullptr, // shared_quad_state (共享状态)
gfx::Rect(0, 0, 256, 256), // Quad 在屏幕上的位置(注意:这是以瓦片的 (0,0) 开始)
gfx::Rect(0, 0, 256, 256), // 可见区域
false, // 不需要混合
texture_id, // 纹理 ID(指向我们栅格化的蓝色正方形纹理)
true, // 预乘 alpha
gfx::PointF(0, 0), // UV 左上角
gfx::PointF(1, 1), // UV 右下角
SK_ColorWHITE, // 背景色
{1.0f, 1.0f, 1.0f, 1.0f} // 混合颜色
);
// 重要!实际位置需要从瓦片坐标转换
// 瓦片 (0,0) 对应屏幕位置 (0,0)
// 但我们的正方形在 DisplayItemList 中是 (100, 100)
// 所以最终 Quad 的 rect 应该是 (100, 100, 200, 200)
// 更正:
quad->rect = gfx::Rect(100, 100, 200, 200); // 正确的屏幕位置
quad->visible_rect = gfx::Rect(100, 100, 200, 200);
quad->opacity = 1.0f;
render_pass->quad_list.push_back(std::move(quad));
// 添加资源
viz::TransferableResource resource;
resource.id = texture_id;
resource.mailbox_holder = gpu::MailboxHolder(mailbox, sync_token, target);
frame.resource_list.push_back(resource);
// 添加渲染通道
frame.render_pass_list.push_back(std::move(render_pass));
return frame;
}
// CompositorFrame 现在包含:
// {
// metadata: {
// frame_token: 0x12345,
// device_scale_factor: 1.0,
// size_in_pixels: (1920, 1080),
// ...
// },
// render_pass_list: [
// {
// output_rect: (0, 0, 1920, 1080),
// damage_rect: (100, 100, 200, 200),
// quad_list: [
// TextureDrawQuad {
// rect: (100, 100, 200, 200),
// texture_id: 0x9999,
// opacity: 1.0,
// ...
// }
// ]
// }
// ],
// resource_list: [
// {
// id: 0x9999,
// mailbox: ... (指向包含栅格化像素的 GPU 纹理)
// }
// ]
// }
这个阶段的输出: CompositorFrame,包含:
- 要绘制的 Quads(四边形)
- Quad 的位置和大小
- 指向纹理的资源 ID
- 纹理中的实际像素数据
第6步:Viz Display Compositor - 最终合成和显示
// Viz Display Compositor(显示合成器)
void Display::DrawFrame(...) {
// 1. 接收 CompositorFrame
viz::CompositorFrame frame = ...;
// 2. 遍历所有 Quads
for (auto& render_pass : frame.render_pass_list) {
for (auto& quad : render_pass->quad_list) {
if (auto texture_quad = quad.As<viz::TextureDrawQuad>()) {
// 3. 获取纹理(包含栅格化的蓝色正方形像素)
GLuint texture = GetTextureFromResourceId(texture_quad->resource_id);
// 4. 在屏幕上绘制这个纹理
// 位置:(100, 100)
// 大小:200×200
// 内容:我们栅格化的蓝色正方形纹理
glBindTexture(GL_TEXTURE_2D, texture);
glUniform2f(position_uniform, 100.0f, 100.0f);
glUniform2f(size_uniform, 200.0f, 200.0f);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // 绘制四边形
}
}
}
// 5. 交换缓冲区
SwapBuffers();
// GPU 把缓冲区中的像素显示到屏幕上
}
这个阶段发生了什么:
- Display Compositor 收到 CompositorFrame
- 读取 Quad 的信息:位置 (100, 100),大小 200×200
- 发送 GPU 指令:在屏幕上的 (100, 100) 位置绘制纹理
- GPU 执行指令,把纹理中的像素(蓝色正方形)显示到屏幕缓冲区
- 交换缓冲区,屏幕显示最终结果
完整的数据变换总结
┌─────────────────────────────────────────────────────────────┐
│ 1. HTML/CSS 描述 │
│ <div style="width:200px; height:200px; │
│ background-color:blue; top:100px; left:100px;">
└─────────────────────────────────────────────────────────────┘
↓ (Blink 布局)
┌─────────────────────────────────────────────────────────────┐
│ 2. DisplayItemList (绘制指令,不是像素) │
│ [SaveOp, ││ DrawRectOp(x=100, y=100, w=200, h=200, color=blue), ││ RestoreOp] │
└─────────────────────────────────────────────────────────────┘
↓ (提交)
┌─────────────────────────────────────────────────────────────┐
│ 3. CommitState │
│ (包含 DisplayItemList 和其他属性) │
└─────────────────────────────────────────────────────────────┘
↓ (栅格化)
┌─────────────────────────────────────────────────────────────┐
│ 4. GPU 纹理(实际像素)256×256 │
│ FFFFFF... FFFFFF... 蓝色(0000FF)... 蓝色... FFFFFF... │
│ (100,100) 到 (300,300) 的像素是蓝色,其他是白色 │
└─────────────────────────────────────────────────────────────┘
↓ (生成帧)
┌─────────────────────────────────────────────────────────────┐
│ 5. CompositorFrame │
│ quad: { │
│ position: (100, 100), │
│ size: (200, 200), │
│ texture_id: 0x9999 → 指向包含蓝色像素的纹理 │
│ } │
└─────────────────────────────────────────────────────────────┘
↓ (GPU 绘制)
┌─────────────────────────────────────────────────────────────┐
│ 6. 屏幕帧缓冲(1920×1080) │
│ (100, 100) 到 (300, 300) 的像素现在是蓝色 │
│ 其他像素是白色 │
│ │
│ ┌──────────────────────────────────┐ │
│ │ 白白白白白... 白白 │ │
│ │ 白白白白白... 白白 │ │
│ │ ... │ │
│ │ 白白蓝蓝蓝... 蓝白 │ ← (100,100) │
│ │ 白白蓝蓝蓝... 蓝白 │ │
│ │ 白白蓝蓝蓝... 蓝白 │ │
│ │ ... │ │
│ │ 白白蓝蓝蓝... 蓝白 │ ← (300,300) │
│ │ 白白白白白... 白白 │ │
│ └──────────────────────────────────┘ │
│ │
│ ✓ 正方形显示在屏幕上! │
└─────────────────────────────────────────────────────────────┘
关键概念总结
- DisplayItemList = 绘制指令的列表(文本格式)
- 栅格化 = 把绘制指令转换成像素的过程
- 纹理 = GPU 内存中的像素缓冲区
- CompositorFrame = 包含 Quads(四边形)的合成帧,每个 Quad 指向一个纹理
- 最终像素 = 由 GPU 把纹理中的像素通过 Quad 信息绘制到屏幕上
整个过程就像:
- 指令 → 像素 → 纹理 → Quads → 屏幕显示
好问题!栅格化是整个过程中最有趣的部分。让我用最直观的方式解释它。太好了!现在我可以用实际的代码来解释栅格化的原理。让我用一个最简单的蓝色正方形例子详细讲解:
栅格化的本质:将指令转换成像素
核心思想:使用 Skia 库
栅格化的秘密就是:Chromium 使用一个叫 Skia 的图形库,它能把绘制指令转换成像素。
想象 Skia 就像一个虚拟画家:
- 你给它一个空白画布(内存缓冲区)
- 你给它绘制指令("画蓝色矩形")
- 它就会一条条指令地执行,最后得到一个画满了颜色的画布
完整的栅格化过程(以蓝色正方形为例)
步骤 1:创建空白画布
// 工作线程上执行
// 假设我们要栅格化一个 256×256 的瓦片
// 方案 A:CPU 栅格化(软件方式)
// ====================================
// 创建一个 256×256 的位图(像素缓冲区)
SkBitmap bitmap;
bitmap.allocN32Pixels(256, 256);
// 现在 bitmap 中有 256*256 = 65536 个像素
// 每个像素 4 字节(RGBA),共 262,144 字节的内存
// 内存布局示意:
// bitmap.getPixels() 返回一个指向这块内存的指针
// 内存中的数据:
// [像素(0,0)的RGBA] [像素(1,0)的RGBA] [像素(2,0)的RGBA] ...
// [像素(0,1)的RGBA] [像素(1,1)的RGBA] [像素(2,1)的RGBA] ...
// ...
// [像素(255,255)的RGBA]
// 创建一个 Skia 画布,指向这个位图
SkCanvas canvas(bitmap);
// 方案 B:GPU 栅格化
// ====================================
// 创建一个 GPU 纹理作为渲染目标
GLuint framebuffer = CreateFramebuffer();
GLuint texture = CreateTexture(256, 256, GL_RGBA8);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D, texture, 0);
// 创建指向这个 GPU 纹理的 Skia 画布
SkSurface* surface = SkSurface::MakeFromBackendTexture(...);
SkCanvas* canvas = surface->getCanvas();
现在我们有了一个空白画布,所有像素都是未初始化的。
步骤 2:清除背景
// 把所有像素设置为白色(背景色)
canvas->clear(SK_ColorWHITE);
// 底层发生了什么:
// Skia 填充整个 256×256 的像素区域
// 对于每一个像素 (x, y),设置它的值为:
// [R=255, G=255, B=255, A=255] // 白色 RGBA
// 清除后的内存示意:
// [FFFFFFFF] [FFFFFFFF] [FFFFFFFF] ... (全是 0xFFFFFFFF = 白色)
// 总共 65536 个这样的 4 字节值
现在所有像素都是白色。
步骤 3:遍历 DisplayItemList 中的绘制指令并执行
这是最关键的部分!
// RasterSource::PlaybackDisplayListToCanvas()
void PlaybackDisplayListToCanvas(SkCanvas* raster_canvas,
const PlaybackSettings& settings) {
// 获取 DisplayItemList
DisplayItemList* display_list = ...; // 包含我们的绘制指令
// 创建回放参数
PlaybackParams params(settings.image_provider, SkM44());
// 关键步骤:回放 DisplayItemList
display_list->Raster(raster_canvas, params);
}
// DisplayItemList::Raster() 的实现
void DisplayItemList::Raster(SkCanvas* canvas,
const PlaybackParams& params) const {
// 1. 获取需要绘制的操作的偏移量
// (使用 R-Tree 优化:只获取与画布相交的操作)
std::vector<size_t> offsets = OffsetsOfOpsToRaster(canvas);
// offsets = [0, 8, 24] // SaveOp, DrawRectOp, RestoreOp 的偏移量
// 2. 遍历 paint_op_buffer_ 中的操作,执行它们
paint_op_buffer_.Playback(canvas, params, true, &offsets);
}
// PaintOpBuffer::Playback() 的实现
void PaintOpBuffer::Playback(SkCanvas* canvas,
const PlaybackParams& params,
bool local_ctm,
const std::vector<size_t>* offsets) const {
// 遍历所有操作
for (const PaintOp& op : PaintOpBuffer::OffsetIterator(*this, offsets)) {
// 对于每一个操作,调用它的 Raster() 函数
op.Raster(canvas, params);
// 这就是魔法发生的地方!
// Raster() 是虚函数,每个 PaintOp 子类有自己的实现
}
}
现在让我们看看具体的操作执行:
// DisplayItemList 中有这些操作(序列化的形式):
// [SaveOp] [DrawRectOp(...)] [RestoreOp]
// ============================================
// 操作 1:SaveOp::Raster()
// ============================================
void SaveOp::Raster(const SaveOp* op,
SkCanvas* canvas,
const PlaybackParams& params) {
canvas->save();
// 这保存了当前的图形状态(颜色、变换、剪裁等)
}
// ============================================
// 操作 2:DrawRectOp::Raster()(最关键!)
// ============================================
void DrawRectOp::RasterWithFlags(const DrawRectOp* op,
const PaintFlags* flags,
SkCanvas* canvas,
const PlaybackParams& params) {
// op->rect = SkRect{x=100, y=100, w=200, h=200}
// flags->color = SK_ColorBLUE = 0x0000FFFF (RGBA)
// 这是最关键的调用!
canvas->drawRect(op->rect, *flags);
// Skia 会在这一刻做什么?
// 它会遍历要填充的矩形中的每一个像素,设置它们的颜色为蓝色
}
// Skia 内部如何执行 drawRect?
// ====================================
// 伪代码,大致思路:
void SkCanvas::drawRect(const SkRect& rect, const SkPaint& paint) {
// 1. 将 SkRect 转换为像素坐标
int x1 = (int)rect.left(); // 100
int y1 = (int)rect.top(); // 100
int x2 = (int)rect.right(); // 300
int y2 = (int)rect.bottom(); // 300
SkColor color = paint.getColor(); // 0x0000FFFF (蓝色)
// 2. 遍历矩形内的所有像素
for (int y = y1; y < y2; y++) {
for (int x = x1; x < x2; x++) {
// 3. 对于每一个像素,写入蓝色颜色值
uint32_t* pixel_ptr = bitmap.getAddr32(x, y);
*pixel_ptr = color; // 设置为 0x0000FFFF
}
}
// 实际上 Skia 做的更复杂(处理反锯齿、混合等),
// 但核心思想就是这样:写入像素值
}
// 栅格化后的内存状态:
// y=0 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF ... (全是白色)
// y=99 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF ...
// y=100 FFFFFF... FFFFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFF... (开始有蓝色!)
// ^ ^ ^
// 0 100 100-300 是蓝色
// y=101 FFFFFF... FFFFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFF...
// ...
// y=299 FFFFFF... FFFFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFF...
// y=300 FFFFFF... FFFFFF 0000FFFF 0000FFFF ... 0000FFFF FFFFFF...
// y=301 FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF ... (又都是白色了)
// ============================================
// 操作 3:RestoreOp::Raster()
// ============================================
void RestoreOp::Raster(const RestoreOp* op,
SkCanvas* canvas,
const PlaybackParams& params) {
canvas->restore();
// 恢复之前保存的图形状态
}
经过这三个操作,我们已经有了一个完整的栅格化结果!
完整的数据变换示意
// 开始栅格化
// 输入 1:DisplayItemList(绘制指令)
DisplayItemList {
paint_op_buffer: [
SaveOp,
DrawRectOp(
rect={x:100, y:100, w:200, h:200},
color=0x0000FFFF // 蓝色
),
RestoreOp
],
visual_rects: [{100,100,200,200}, ...]
}
// 输入 2:空白画布(256×256 的位图)
SkBitmap bitmap {
width: 256,
height: 256,
pixels: [
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF, // 第 0 行(256 个白色像素)
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF, // 第 1 行
...
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF // 第 255 行
]
}
// ============================================================
// 执行回放
// ============================================================
canvas->save();
canvas->drawRect(SkRect{100,100,200,200}, blue_paint);
canvas->restore();
// 栅格化后的输出:位图内容被修改了
SkBitmap bitmap {
width: 256,
height: 256,
pixels: [
// y=0 到 y=99:全是白色
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF, // 第 0 行
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF, // 第 1 行
...
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF, // 第 99 行
// y=100 到 y=299:中间是蓝色,边缘是白色
0xFFFFFFFF, ..., 0xFFFFFFFF, 0x0000FFFF, 0x0000FFFF, ..., 0x0000FFFF, 0xFFFFFFFF, ...,
// |
// x=0 到 99 100 到 299(蓝色区域) 300 到 255
0xFFFFFFFF, ..., 0xFFFFFFFF, 0x0000FFFF, 0x0000FFFF, ..., 0x0000FFFF, 0xFFFFFFFF, ...,
// ... 重复 200 次(y=100 到 y=299)
// y=300 到 y=255:全是白色
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF, // 第 300 行
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF, // 第 301 行
...
0xFFFFFFFF, 0xFFFFFFFF, ..., 0xFFFFFFFF // 第 255 行
]
}
为什么能把指令转换成像素?
关键是 Skia 库的设计:
// Skia 的核心就是提供一个虚拟的 SkCanvas
// 无论你是在 CPU 上还是 GPU 上绘制,接口都是一样的
class SkCanvas {
public:
// 这些方法有多种实现:
// - CPU 实现:直接修改像素缓冲区
// - GPU 实现:生成 GPU 指令
virtual void drawRect(const SkRect& rect, const SkPaint& paint);
virtual void drawPath(const SkPath& path, const SkPaint& paint);
virtual void drawImage(const SkImage* image, ...);
// ... 等等
};
// CPU 版本的 drawRect(简化)
class SkRasterCanvas : public SkCanvas {
void drawRect(const SkRect& rect, const SkPaint& paint) override {
// 直接操作 fBitmap 的像素
uint32_t* pixels = fBitmap.getPixels();
for (int y = rect.top(); y < rect.bottom(); y++) {
for (int x = rect.left(); x < rect.right(); x++) {
int index = y * width + x;
pixels[index] = paint.getColor(); // 设置像素颜色
}
}
}
};
// GPU 版本的 drawRect(简化)
class SkGPUCanvas : public SkCanvas {
void drawRect(const SkRect& rect, const SkPaint& paint) override {
// 生成 GPU 指令
glBindFramebuffer(GL_FRAMEBUFFER, fFramebuffer);
glClearColor(paint.getColor());
glClear(GL_COLOR_BUFFER_BIT);
// ... 等等
}
};
总结:
- DrawRectOp 本质上就是
canvas->drawRect() - drawRect() 遍历矩形内的所有像素
- 对于每个像素,设置它的颜色为指定的颜色
- 结果是位图中的像素被修改了
这就是为什么能把绘制指令转换成像素!
关键代码片段总结
void RasterSource::PlaybackDisplayListToCanvas(
SkCanvas* raster_canvas,
const PlaybackSettings& settings) const {
CHECK(display_list_);
// 创建回放参数
PlaybackParams params(settings.image_provider, SkM44());
params.raster_inducing_scroll_offsets =
settings.raster_inducing_scroll_offsets;
params.destination_hdr_headroom = settings.hdr_headroom;
// 核心:回放 DisplayItemList
// 这会遍历所有 PaintOps 并执行它们
display_list_->Raster(raster_canvas, params);
}
// 这就是一切!
// display_list_->Raster() 会:
// 1. 遍历 DisplayItemList 中的每个 PaintOp
// 2. 对于每个 PaintOp,调用它的 Raster() 方法
// 3. 每个 Raster() 方法使用 Skia canvas 修改像素
// 4. 最后得到一个填满颜色的位图
