RenderingNG 中的关键数据结构

Chris Harrelson
Chris Harrelson
Daniel Cheng
Daniel Cheng
Philip Rogers
Philip Rogers
Koji Ishi
Koji Ishi
Ian Kilpatrick
Ian Kilpatrick
Kyle Charbonneau
Kyle Charbonneau

我们来看看关键的数据结构,这些结构是 渲染流水线

这些数据结构包括:

  • 帧树由本地节点和远程节点组成,这些节点代表 哪个文档是哪个渲染进程,哪个 Blink 渲染器是哪个。
  • 不可变片段树表示 布局约束算法。
  • 属性树表示转换、裁剪、效果和滚动层次结构 文档内容。这些内容在整个流水线中用到。
  • 显示列表和绘制区块是光栅和分层算法的输入。
  • 合成器帧封装了 Surface、渲染 Surface 和 GPU 纹理 使用 GPU 进行绘制时使用的功能块

在浏览这些数据结构之前,以下示例基于 一个来自架构审核。本次 本文档通篇使用的都是示例, 结构。

<!-- Example code -->
<html>
  <div style="overflow: hidden; width: 100px; height: 100px;">
    <iframe style="filter: blur(3px);
      transform: rotateZ(1deg);
      width: 100px; height: 300px"
      id="one" src="foo.com/etc"></iframe>
  </div>
  <iframe style="top:200px;
    transform: scale(1.1) translateX(200px)"
    id="two" src="bar.com"></iframe>
</html>

框架树

Chrome 有时可能会选择渲染跨源框架 与父帧不同的渲染进程中呈现的广告素材

示例代码一共有三个帧:

包含两个 iframe 的父框架 foo.com。

启用网站隔离功能后,Chromium 会使用两个呈现进程来呈现此网页。 每个渲染进程都有针对该网页帧树的表示形式:

两个帧树,代表两个渲染进程。

在其他进程中渲染的帧表示为远程帧。 远程帧存储了渲染时充当占位符所需的最少信息, 例如尺寸 否则,远程帧不包含渲染其实际内容所需的任何信息。

而局部帧表示通过标准的 渲染流水线本地帧包含旋转所需的所有信息, 将该帧的数据(如 DOM 树和样式数据)转换成 可呈现和显示的资源

渲染流水线以 本地帧树 fragment。 考虑一个以 foo.com 作为主框架的更复杂的示例:

<iframe src="bar.com"></iframe>

以及以下 bar.com 子框架:

<iframe src="foo.com/etc"></iframe>

虽然仍然只有两个渲染器,但现在有三个本地帧 树 fragment,其中两个位于 foo.com 的渲染进程中,另一个位于 bar.com的呈现进程:

两个渲染程序和三个帧树 fragment 的表示。

要为网页生成一个合成器框架, Viz 同时从每个 三个局部帧树,然后 汇总它们。 另请参阅“合成器框架”部分

foo.com 主框架和 foo.com/other-page 子框架 属于同一帧树,并在同一进程中渲染。 不过,这两个帧仍然 文档生命周期 因为它们是不同本地帧树 fragment 的一部分。 因此,无法在一次更新中同时为二者生成一个合成器帧。 渲染流程包含的信息不足 合成为 foo.com/other-page 生成的合成器帧 直接放入 foo.com 主框架的合成器框架中。 例如,进程外 bar.com 父帧可能会影响 foo.com/other-url iframe 的显示, 来实现。

可视化属性更新广告瀑布流

设备缩放比例和视口大小等视觉属性会影响渲染的输出 并且必须在本地帧树 fragment 之间同步。 每个本地帧树 fragment 的根都有一个与之关联的 widget 对象。 视觉属性更新在传播前会转到主框架的 widget 其余微件

例如,当视口大小发生变化时:

上述文本中说明的流程示意图。

此过程并不是即时完成的, 因此复制的视觉属性也包含同步令牌。 Viz 合成器使用此同步令牌等待所有本地帧树 fragment 以提交包含当前同步令牌的合成器帧。 此过程可避免将具有不同视觉属性的合成器帧混合在一起。

不可变 fragment 树

不可变 fragment 树是渲染的布局阶段的输出 流水线。它表示页面上所有元素的位置和大小 (不应用转换)。

每个树中的 fragment 的表示,其中有一个 fragment 标记为需要布局。

每个片段都代表一个 DOM 元素的一部分。 通常,每个元素只有一个 fragment, 但如果在打印时拆分成不同的页面 多列环境中的各列。

布局完成后,每个 fragment 都将变得不可变,并且不会再更改。 重要的是,我们还设定了一些其他限制。我们不会:

  • 允许任何“向上”操作树中的引用 (子级不能拥有指向其父级的指针。)
  • “bubble”树向下移动数据 (子级只从其子级读取信息,而非从父级读取信息)。

这些限制使我们能够将 fragment 重复用于后续布局。 如果没有这些限制,我们就需要经常重新生成整个树,这会成本高昂。

大多数布局通常是增量更新,例如, 当用户点击某个元素时,Web 应用会更新界面的一小部分内容。 理想情况下,布局的作用应该与屏幕上实际更改成比例。 为了实现这一点,我们尽可能多地重复使用之前树中的部分。 这意味着(通常)我们只需要重建树干。

将来,这种不可变的设计可以让我们做一些有趣的事情 例如根据需要跨线程边界传递不可变 Fragment 树 (在其他线程上执行后续阶段), 生成多个树以实现流畅的布局动画, 或执行并行推测性布局。 它还赋予我们多线程布局的潜力。

内嵌 fragment 项

内嵌内容(主要是带样式的文本)使用的呈现方式略有不同。 与包含方框和指针的树结构不同, 我们会在表示树的扁平列表中表示内嵌内容。 主要优势在于,对于内嵌的扁平列表表示法可以快速执行, 有助于检查或查询内嵌数据结构, 且内存效率高 这对于网页呈现性能极其重要 因为文本渲染非常复杂, 除非对其进行高度优化,否则很容易成为流水线中最慢的部分。

系统会为各行之间的 内嵌格式设置上下文 按其内嵌布局子树的深度优先搜索的顺序排列。 列表中的每个条目都是 (object, number of descendants) 的元组。 例如,假设存在以下 DOM:

<div style="width: 0;">
  <span style="color: blue; position: relative;">Hi</span> <b>there</b>.
</div>

width 属性设置为 0,该行会在“Hi”之间换行和“那里”

当针对此情形的内嵌格式设置上下文以树表示时, 如下所示:

{
  "Line box": {
    "Box <span>": {
      "Text": "Hi"
    }
  },
  "Line box": {
    "Box <b>": {
      "Text": "There"
    }
  },
  {
    "Text": "."
  }
}

扁平列表如下所示:

  • (折线框,2)
  • (第 <span> 个框,第 1 个)
  • (发送短信“你好”,0)
  • (线框,3)
  • (第 <b> 个框,1)
  • (文本“there”,0)
  • (文本“.”,0)

这种数据结构有许多使用者:无障碍功能 API、 以及几何图形 API, getClientRects、 和 contenteditable。 每种方式都有不同的要求。 这些组件通过便捷的游标访问平面数据结构。

游标 具有 API,例如 MoveToNextMoveToNextLineCursorForChildren。 这种游标表示形式对于文本内容而言非常强大,原因有多种:

  • 按深度优先搜索顺序的迭代非常快。 这种情况经常被使用,因为它与脱字符号移动类似。 由于它是扁平列表,因此深度优先搜索只会增加数组偏移量, 提供快速迭代和内存局部性。
  • 它提供广度优先搜索,这在一些情况下很有必要,例如 绘制线条和内联框的背景。
  • 了解后代的数量后,可快速移到下一个同级 (只需将数组偏移量增加该数字即可)。

属性树

DOM 是元素树(加上文本节点),CSS 可以应用各种 为元素添加样式

这通过以下四种方式呈现:

  • 布局:布局约束算法的输入。
  • 绘制:如何绘制和光栅元素 (但不包括其后代)。
  • 可视化:应用于 DOM 子树的光栅/绘制效果, 例如转换、过滤器和裁剪。
  • 滚动:轴对齐和圆角 裁剪和滚动所含子树。

属性树是一种数据结构,用于说明视觉和滚动效果如何应用于 DOM 元素。 它们提供了一些方法来回答诸多问题,例如:在哪里, 是给定 DOM 元素, 布局尺寸和位置呢? 以及:应使用什么序列的 GPU 操作来应用视觉和滚动效果?

网络上的视觉和滚动效果极为复杂。 属性树最重要的就是 精确地表示其结构和含义的单个数据结构, 同时消除了 DOM 和 CSS 其余的复杂性。 这使我们能够更自信地实现合成和滚动算法。具体而言:

  • 容易出错的几何图形和其他计算 可以集中在一处
  • 构建和更新属性树的复杂性 被隔离到一个渲染流水线阶段中。
  • 与完整的 DOM 状态相比,将属性树发送到不同的线程和进程要简单得多,也更快。 因此可以在许多用例中使用它们。
  • 使用场景越多 通过基于其构建的几何图形缓存取得的胜利越多, 因为他们可以重复使用彼此的缓存。

RenderingNG 使用属性树有多种用途,包括:

  • 将合成与绘制分离,以及从主线程合成。
  • 确定最佳合成 / 绘制策略。
  • 正在测量 IntersectionObserver 几何图形。
  • 避免为屏幕外元素和 GPU 纹理图块工作。
  • 高效准确地将绘制和光栅失效。
  • 正在测量 布局偏移和 Core Web Vitals 中的 Largest Contentful Paint

每个网络文档都有四个独立的属性树:转换、裁剪、效果和滚动。(*) 转换树表示 CSS 转换和滚动。 (滚动转换表示为 2D 转换矩阵。) 剪贴树表示 溢出片段。 效果树代表所有其他视觉效果:不透明度、滤镜、遮罩 混合模式和其他类型的剪辑(例如剪辑路径)。 滚动树表示有关滚动的信息 例如 链接在一起; 需要在合成器线程上执行滚动操作。 属性树中的每个节点都表示 DOM 元素应用的滚动或视觉效果。 如果碰巧有多个效果 对于相同元素,每个树中可能有多个属性树节点。

每个树的拓扑都像是 DOM 的稀疏表示法。 例如,如果有三个带溢出剪辑的 DOM 元素, 就会有三个剪辑树节点 剪贴树的结构将遵循 溢出片段之间的包含块关系。 树之间还有联系。 这些链接表示相对的 DOM 层次结构, 以及节点的应用顺序 例如,如果 DOM 元素上的转换在另一个带有过滤器的 DOM 元素下面, 那么当然,转换在过滤器之前应用。

每个 DOM 元素都有一个属性树状态, 是一个 4 元组(转换、裁剪、效果、滚动) 表示最近的祖先剪辑, 转换,并影响对该元素生效的树节点。 这非常方便,因为有了这些信息,我们就能准确知道剪辑的列表, 可以应用该元素的转换和效应, 以及顺序 它能告诉我们它在屏幕上的位置以及如何绘制它。

示例

来源

<html>
  <div style="overflow: scroll; width: 100px; height: 100px;">
    <iframe style="filter: blur(3px);
      transform: rotateZ(1deg);
      width: 100px; height: 300px"
  id="one" srcdoc="iframe one"></iframe>
  </div>
  <iframe style="top:200px;
      transform: scale(1.1) translateX(200px)" id=two
      srcdoc="iframe two"></iframe>
</html>

对于前面的示例(与简介中的示例略有不同), 以下是生成的属性树的关键元素:

属性树中各种元素的示例。

显示列表和绘制区块

显示项包含低级绘制命令(请参阅 此处) 可以使用 Skia. 显示项通常比较简单,只需几个绘制命令即可,如绘制边框或背景。绘制树遍历按照 CSS 绘制顺序遍历布局树和关联的 fragment。 来生成显示项列表。

例如:

带有“Hello World”字样的蓝色框放置在绿色矩形内。

<div id="green" style="background:green; width:80px;">
    Hello world
</div>
<div id="blue" style="width:100px;
  height:100px; background:blue;
  position:absolute;
  top:0; left:0; z-index:-1;">
</div>

此 HTML 和 CSS 会生成以下显示列表: 其中,每个单元格都是一个显示项:

视图背景 #blue背景 #green背景 #green内嵌文本
drawRect,尺寸为 800x600,颜色为白色。 drawRect,尺寸为 100x100,位于位置 0,0,颜色为蓝色。 drawRect,尺寸为 80x18,位于位置 8,8,颜色为绿色。 drawTextBlob,位置为 8,8,文本为“Hello world”。

显示项列表从后到前排序。 在上面的示例中,按照 DOM 顺序,绿色 div 位于蓝色 div 之前, 但 CSS 绘制顺序要求负的 Z-index 蓝色 div 绘制 在绿色 div 之前(第 3 步) (第 4.1 步)。 显示项大致对应于 CSS 绘制顺序规范的原子步骤。 单个 DOM 元素可能会产生多个显示项, 例如, #green 有一个用于显示背景的显示项,又为内嵌文本设置了另一个显示项。 这种粒度对于表示 CSS 绘制顺序规范的完整复杂性非常重要, 例如由负外边距创建的交错:

一个绿色矩形,其中局部叠加了一个灰色框,上面显示“Hello world”字样。

<div id="green" style="background:green; width:80px;">
    Hello world
</div>
<div id="gray" style="width:35px; height:20px;
  background:gray;margin-top:-10px;"></div>

这将生成以下显示列表,其中每个单元格都是一个显示项:

视图背景 #green背景 #gray背景 #green内嵌文本
drawRect,尺寸为 800x600,颜色为白色。 drawRect,尺寸为 80x18,位于位置 8,8,颜色为绿色。 drawRect,尺寸为 35x20,位于位置 8,16,颜色为灰色。 drawTextBlob,位置为 8,8,文本为“Hello world”。

系统会存储显示项列表,供后续更新重复使用。 如果布局对象在绘制树遍历期间未发生变化, 其显示项会从前一个列表中复制。 另一种优化依赖于 CSS 绘制顺序规范的一个属性: 堆叠上下文以原子方式绘制。 如果布局对象在堆栈上下文中没有任何变化, Paint 树遍历会跳过堆叠上下文 并复制上一个列表中的显示项的整个序列。

当前的属性树状态会在绘制树遍历期间保持不变 并且显示项列表被分组为“区块”具有相同属性树状态的展示项。 以下示例对此进行了演示:

一个粉色盒子和一个橙色倾斜的盒子。

<div id="scroll" style="background:pink; width:100px;
   height:100px; overflow:scroll;
   position:absolute; top:0; left:0;">
    Hello world
    <div id="orange" style="width:75px; height:200px;
      background:orange; transform:rotateZ(25deg);">
        I'm falling
    </div>
</div>

这将生成以下显示列表,其中每个单元格都是一个显示项:

视图背景 #scroll背景 #scroll内嵌文本 #orange背景 #orange内嵌文本
drawRect,尺寸为 800x600,颜色为白色。 drawRect,尺寸为 100x100,位于位置 0,0,颜色为粉色。 drawTextBlob,位置为 0,0,文本为“Hello world”。 drawRect,尺寸为 75x200,位于位置 0,0,颜色为橙色。 位置为 0,0 且带有文本“I'm Falling”的 drawTextBlob

然后,转换属性树和绘制区块将如下所示(为简洁起见):

上表的图片,即数据块 1 中的前两个单元格、数据块 2 中的第三个单元格以及数据块 3 中的后两个单元格。

绘制块的有序列表, 即显示项组和属性树状态 是渲染流水线中分层步骤的输入。 系统会将整个绘制块列表合并成一个合成图层,然后光栅化在一起。 但每次用户滚动网页时都需要进行高昂的光栅化处理。 可以为每个绘制区块创建一个合成层 并单独进行光栅化,以避免重新进行光栅化,但这样会很快耗尽 GPU 内存。 “分层”步骤必须在 GPU 内存与情况发生变化时降低费用之间进行权衡取舍。 一种比较好的做法是默认合并分块, 不合并具有属性树状态预计会在合成器线程上发生变化的绘制区块, 例如使用 compositor-thread 滚动或 compositor-thread 转换动画。

理想情况下,上面的示例应生成两个合成层:

  • 一个包含绘图命令的 800x600 合成图层: <ph type="x-smartling-placeholder">
      </ph>
    1. drawRect,尺寸为 800x600,颜色为白色
    2. drawRect,尺寸为 100x100,位于位置 0,0,粉色
  • 一个包含绘图命令的 144x224 合成层: <ph type="x-smartling-placeholder">
      </ph>
    1. 位置为 0,0 且文本为“Hello world”的 drawTextBlob
    2. 翻译 0,18
    3. rotateZ(25deg)
    4. drawRect,尺寸为 75x200,位于位置 0,0,颜色为橙色
    5. drawTextBlob”的位置为 0,0,文本为“我倒了”

如果用户滚动 #scroll, 移动第二个合成图层,但不需要光栅化。

就本示例而言: 前面介绍属性树的部分后, 一共有六个绘制块。 连同它们的(转换、裁剪、效果、滚动)属性树状态,它们是:

  • 文档背景:文档滚动、文档剪辑、根、文档滚动。
  • div 的水平、垂直和滚动角(三个独立的绘制块): 文档滚动、文档剪辑、#one模糊处理、文档滚动。
  • iframe #one#one旋转、溢出滚动片段、#one模糊处理、div 滚动。
  • iframe #two#two 缩放、文档剪辑、根、文档滚动。

合成器帧:Surface、渲染 Surface 和 GPU 纹理图块

浏览器和渲染进程管理内容的光栅化, 然后将合成器帧提交到 Viz 进程,以便呈现在屏幕上。 合成器帧表示如何将光栅化内容拼接在一起 并使用 GPU 高效绘制图像

卡片

从理论上讲, 渲染进程或浏览器进程合成器可以将像素光栅化 转换为与渲染器视口完整尺寸相同的单个纹理,并将该纹理提交给 Viz。 要显示它,显示合成器只需复制像素, 从该单一纹理移动到帧缓冲区中的适当位置 (例如屏幕)。但是,如果该合成器想要更新 则需对整个视口重新光栅化,并向 Viz 提交新的纹理。

取而代之的是,视口会分成多个图块。 单独的 GPU 纹理图块会为每个图块背后部分视口的光栅化像素。 然后,渲染程序可以更新各个图块,甚至 只需更改现有图块在屏幕上的位置即可 例如,在滚动网站时 现有图块的位置会发生变化,且只会偶尔 则对于页面更靠下位置的内容,需要对新图块进行光栅化。

<ph type="x-smartling-placeholder">
</ph> 四个板块。
此图片描绘的是晴天的图片,有四个图块。 当发生滚动操作时,第五个图块便会开始显示。 其中一个板块恰好只有一种颜色(天蓝色), 顶部有一个视频和 iframe。

四边形和曲面

GPU 纹理图块是一种特殊的四边形, 这只是一类或另一类纹理的花哨名称。 四边形用于标识输入纹理,并指明如何对输入纹理进行转换和应用视觉效果。 例如,常规内容图块有一个转换指示其在图块网格中的 x、y 位置。

GPU 纹理图块。

这些光栅化图块封装在一个渲染通道(即四边形列表)中。 渲染通道不包含任何像素信息; 而是提供了有关在何处以及如何绘制每个四边形以产生所需像素输出的说明。 每个 GPU 纹理图块都有一个“绘制四边形”。 显示合成器只需要遍历四边形列表, 使用指定的视觉效果绘制每个图片, 为渲染通道生成所需的像素输出。 为渲染通道合成绘制四边形可在 GPU 上高效完成, 因为我们精心挑选了允许的视觉效果,使其能够直接映射到 GPU 功能。

除了光栅化图块之外,还有其他类型的绘制四边形。 例如,存在完全没有纹理的纯色绘制四边形, 或纹理绘制四边形,适用于视频或画布等非图块纹理。

一个合成器帧也有可能嵌入另一个合成器帧。 例如,浏览器合成器使用浏览器界面生成一个合成器框架, 以及将嵌入渲染合成器内容的空矩形。 另一个例子是网站隔离 iframe。这种嵌入是通过表面完成的。

当合成器提交合成器帧时,会附带一个标识符, 称为 Surface ID,以允许其他合成器框架以引用方式嵌入它。 使用特定 surface ID 提交的最新合成器帧由 Viz 存储。 然后,另一个合成器帧稍后可以通过 Surface 绘制四边形引用它, 这样 Viz 就知道该画什么了。 (请注意,表面绘制四边形仅包含表面 ID,而不包含纹理。)

中间渲染通道

一些视觉效果,例如多个滤镜或高级混合模式 要求将两个或多个四边形绘制到一个中间纹理。 然后,中间纹理会绘制到 GPU 上的目标缓冲区(或者可能是另一个中间纹理)。 同时应用视觉效果 为此,合成器帧实际上包含一个渲染通道列表。 总有一个根渲染通道 最后绘制的并且其目的地与帧缓冲区对应, 或许还会提供更多信息

名称的可能性有多个渲染通道 “渲染通道”每次传递都必须在 GPU 上按顺序执行,也就是“多次”, 而单次遍历可以在单个大规模并行 GPU 计算中完成。

聚合

向 Viz 提交多个合成器帧, 并且需要一起绘制到屏幕上 这是通过聚合阶段实现的,该阶段可将它们转换为单个 聚合的合成器框架。 聚合会使用它们指定的合成器帧来取代表面绘制四边形。 这也是一个优化机会,以消除不必要的中间纹理或屏幕外内容。 例如,在许多情况下,针对网站隔离 iframe 的合成器框架 它不需要自己的中间纹理, 并且可通过相应的绘制四边形直接绘制到帧缓冲区中。 汇总阶段会确定此类优化 并基于单个渲染合成器无法获取的全局知识来应用它们。

示例

以下是代表示例的合成器帧 这篇帖子。

  • foo.com/index.html surface:id=0 <ph type="x-smartling-placeholder">
      </ph>
    • 渲染通道 0:绘制至输出。
      • 渲染通道绘制四边形:使用 3 像素模糊效果进行绘制,然后裁剪至渲染通道 0。
        • 渲染通道 1: <ph type="x-smartling-placeholder">
            </ph>
          • #one iframe 的图块内容绘制四边形,每个四边形分别位于 x 和 y 位置。
      • 表面绘制四边形:ID 为 2,使用缩放和平移转换进行绘制。
  • 浏览器界面 Surface:ID=1 <ph type="x-smartling-placeholder">
      </ph>
    • 渲染通道 0:绘制至输出。
      • 绘制浏览器界面的四边形(同样平铺)
  • bar.com/index.html 平台:ID=2 <ph type="x-smartling-placeholder">
      </ph>
    • 渲染通道 0:绘制至输出。
      • #two iframe 的内容绘制四边形,每个四边形分别位于 x 和 y 位置。

插图作者:Una Kravets。