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 渲染程序中。
  • 不可变 fragment 树表示布局约束算法的输出(和输入)。
  • 属性树表示网页文档的转换、剪裁、效果和滚动层次结构。这些参数会在整个流水线中使用。
  • 显示列表和绘制分块是光栅化和分层算法的输入。
  • 合成器帧封装了用于使用 GPU 绘图的 Surface、渲染 Surface 和 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 有时可能会选择在与其父级框架不同的呈现进程中呈现跨源框架。

在示例代码中,总共有三个帧:

父级框架 foo.com,其中包含两个 iframe。

启用网站隔离后,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 生成的 compositor 帧直接合成到 foo.com 主帧的 compositor 帧中。例如,外部进程 bar.com 父级帧可能会通过使用 CSS 转换 iframe 或使用其 DOM 中的其他元素遮挡 iframe 的部分来影响 foo.com/other-url iframe 的显示。

视觉媒体资源更新广告瀑布流

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

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

上文中所述流程的示意图。

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

不可变 fragment 树

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

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

每个 fragment 都代表 DOM 元素的一部分。通常,每个元素只有一个 fragment,但如果它在打印时分布在不同的页面上,或者在多列上下文中分布在不同的列中,则可能有多个 fragment。

布局后,每个 fragment 都会变为不可变,并且永远不会再发生更改。重要的是,我们还会施加一些额外的限制。我们不会:

  • 允许在树中使用任何“向上”引用。(子项不能包含指向其父项的指针。)
  • 沿树形结构“向上传递”数据(子项仅读取其子项中的信息,而不会读取其父项中的信息)。

这些限制允许我们将 fragment 用于后续布局。如果没有这些限制,我们就需要经常重新生成整个树,这会很耗费资源。

大多数布局通常是增量更新,例如,Web 应用在用户点击某个元素时更新界面的一小部分。理想情况下,布局应仅执行与屏幕上实际发生的更改成正比的工作。我们可以通过尽可能重复使用上一个树的许多部分来实现这一点。这意味着(通常)我们只需重新构建树的脊柱。

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

内嵌 fragment 项

内嵌内容(主要是样式文本)使用略有不同的表示法。我们使用表示树的扁平列表来表示内嵌内容,而不是使用包含框和指针的树形结构。主要优势在于,内嵌的扁平列表表示法速度快,适用于检查或查询内嵌数据结构,并且内存效率高。这对于 Web 渲染性能至关重要,因为文本渲染非常复杂,除非进行高度优化,否则很容易成为流水线中最慢的部分。

系统会按其内嵌布局子树的深度优先搜索顺序为每个内嵌格式设置上下文创建一个扁平列表。列表中的每个条目都是一个元组(对象、子代数量)。 例如,请考虑以下 DOM:

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

width 属性设置为 0,以便在“Hi”和“there”之间换行。

将此情况的内嵌格式设置上下文表示为树时,其如下所示:

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

扁平列表如下所示:

  • (线条框,2)
  • (Box <span>, 1)
  • (Text "Hi", 0)
  • (线条框,3)
  • (Box <b>, 1)
  • (Text "there", 0)
  • (Text ".", 0)

此数据结构有很多使用方:无障碍功能 API 和几何图形 API,例如 getClientRectscontenteditable。每种设备都有不同的要求。这些组件通过便捷光标访问扁平数据结构。

光标具有 API,例如 MoveToNextMoveToNextLineCursorForChildren。这种光标表示法对文本内容非常有用,原因有很多:

  • 按深度优先搜索顺序迭代速度非常快。由于此操作类似于光标移动,因此使用频率很高。由于它是扁平列表,因此深度优先搜索只会递增数组偏移量,从而实现快速迭代和内存局部性。
  • 它提供广度优先搜索,例如,在绘制线条和内嵌框的背景时,就需要使用广度优先搜索。
  • 知道子孙数量有助于快速移动到下一个同级兄弟(只需将数组偏移量按该数量递增即可)。

属性树

DOM 是元素(以及文本节点)的树,CSS 可以对元素应用各种样式。

这会以四种方式显示:

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

属性树是一种数据结构,用于说明视觉效果和滚动效果如何应用于 DOM 元素。它们提供了用于回答以下问题的方法:给定布局大小和位置,给定 DOM 元素相对于屏幕的位置在哪里?以及:应使用哪个 GPU 操作序列来应用视觉效果和滚动效果?

在 Web 上,视觉效果和滚动效果非常复杂。 因此,属性树最重要的作用是将这种复杂性转换为一个精确表示其结构和含义的单一数据结构,同时移除 DOM 和 CSS 的其余复杂性。这样一来,我们就可以更放心地实现合成和滚动算法。具体而言:

  • 可能容易出错的几何图形和其他计算可以集中到一个位置。
  • 构建和更新属性树的复杂性被隔离到一个渲染流水线阶段。
  • 与完整的 DOM 状态相比,将属性树发送到不同的线程和进程要容易得多、快得多,因此可以将其用于许多用例。
  • 用例越多,我们从之上构建的几何图形缓存带来的好处就越多,因为它们可以重复使用彼此的缓存。

RenderingNG 会将属性树用于多种用途,包括:

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

每个网页文档都有四个单独的属性树:转换、剪裁、效果和滚动。(*) 转换树表示 CSS 转换和滚动。(滚动转换表示为二维转换矩阵。) 剪辑树表示溢出剪辑。效果树代表所有其他视觉效果:不透明度、滤镜、遮罩、混合模式以及其他类型的剪辑(例如 clip-path)。滚动树表示滚动相关信息,例如滚动如何链接在一起;需要它才能在混合渲染器线程中执行滚动。属性树中的每个节点都代表 DOM 元素应用的滚动或视觉效果。如果它恰好具有多个效果,则每个树中同一元素的属性树节点可能不止一个。

每个树的拓扑就像 DOM 的稀疏表示。例如,如果有三个具有溢出剪裁的 DOM 元素,则将有三个剪裁树节点,并且剪裁树的结构将遵循溢出剪裁之间的包含块关系。这些树之间还存在关联。这些链接表示节点的相对 DOM 层次结构,以及应用顺序。例如,如果 DOM 元素上的转换位于具有过滤器的另一个 DOM 元素下方,那么转换当然会在过滤器之前应用。

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

示例

来源

<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 会生成以下显示列表,其中每个单元格都是一个显示项:

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

显示屏项列表的排序为从后到前。在上面的示例中,绿色 div 在 DOM 顺序中位于蓝色 div 之前,但 CSS 绘制顺序要求负 z-index 蓝色 div 在绿色 div(第 4.1 步)之前(第 3 步)绘制。显示项大致对应于 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>

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

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

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

在绘制树遍历期间,系统会维护当前的属性树状态,并将显示项列表分组为共享相同属性树状态的显示项“块”。如以下示例所示:

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

<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>

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

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

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

上表的图片,第 1 个分块中的前两个单元格,第 2 个分块中的第三个单元格,第 3 个分块中的最后两个单元格。

绘制分块的有序列表(即一组显示项和一个属性树状态)是渲染流水线的分层步骤的输入。整个绘制分块列表可以合并到单个复合图层中并一起光栅化,但这会导致每次用户滚动时都需要进行昂贵的光栅化。您可以为每个绘制分块创建一个合成图层,并单独进行光栅化,以避免所有重新光栅化,但这会很快耗尽 GPU 显存。分层步骤必须在 GPU 显存和降低费用之间进行权衡。一个很好的常规方法是,默认合并分块,而不合并具有预计会在合成器线程中更改的属性树状态的绘制分块,例如合成器线程滚动或合成器线程转换动画。

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

  • 一个包含绘制命令的 800x600 复合图层:
    1. drawRect,尺寸为 800x600,颜色为白色
    2. 尺寸为 100x100、位于 0,0 且颜色为粉色的 drawRect
  • 一个包含绘制命令的 144x224 复合图层:
    1. 位置为 0,0 且文本为“Hello world”的 drawTextBlob
    2. 翻译 0,18
    3. rotateZ(25deg)
    4. 尺寸为 75x200、位于 0,0 且颜色为橙色的 drawRect
    5. 位置为 0,0 且文本为“I'm falling”的 drawTextBlob

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

示例:在上一节介绍的属性树中,有 6 个绘制分块。它们及其(转换、剪裁、效果、滚动)属性树状态如下:

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

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

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

卡片

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

而是将视口划分为图块。 每个图块由单独的 GPU 纹理图块提供支持,其中包含视口部分的光栅化像素。然后,渲染程序可以更新单个功能块,甚至只更改现有功能块在屏幕上的位置。例如,滚动网站时,现有功能块的位置会向上移动,只有在极少数情况下,才需要针对页面下方的内容光栅化新的功能块。

四个功能块。
此图片描绘了阳光明媚的一天,其中包含四块功能块。 滚动时,第五个功能块开始显示。其中一个功能块恰好只有一种颜色(天蓝色),并且顶部有一个视频和一个 iframe。

四边形和 Surface

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

GPU 纹理图块。

这些光栅化图块封装在渲染传递中,后者是一个四边形列表。渲染传递不包含任何像素信息;而是包含有关在何处以及如何绘制每个四边形以生成所需像素输出的指令。每个 GPU 纹理图块都有一个绘制四边形。显示合成器只需迭代四边形列表,使用指定的视觉效果绘制每个四边形,即可为渲染传递生成所需的像素输出。由于允许的视觉效果是精心挑选的,可直接映射到 GPU 功能,因此可以高效地在 GPU 上为渲染传递合成绘制四边形。

除了光栅化功能块之外,还有其他类型的绘制四边形。例如,有些纯色绘制四边形完全没有纹理支持,有些纹理绘制四边形则适用于视频或画布等非平铺纹理。

合成器帧还可以嵌入其他合成器帧。 例如,浏览器合成器会生成包含浏览器界面的合成器帧,以及用于嵌入渲染合成器内容的空矩形。另一个示例是网站隔离的 iframe。这种嵌入是通过Surface 实现的。

当混合渲染器提交混合渲染器帧时,该帧会附带一个标识符(称为Surface ID),以允许其他混合渲染器帧通过引用嵌入该帧。Viz 会存储使用特定 Surface ID 提交的最新合成器帧。然后,其他合成器帧稍后可以通过 Surface 绘制四边形引用它,因此 Viz 知道要绘制什么。(请注意,Surface 绘制四边形仅包含 Surface ID,不包含纹理。)

中间渲染通道

某些视觉效果(例如许多滤镜或高级混合模式)需要将两个或更多四边形绘制到中间纹理。然后,中间纹理会绘制到 GPU 上的目标缓冲区(或可能另一个中间纹理),同时应用视觉效果。为此,合成器帧实际上包含一系列渲染传递。始终有一个根渲染传递,它是最后绘制的,其目标对应于帧缓冲区,并且可能还有更多。

之所以称为“渲染通过”,是因为可以进行多次渲染。每个传递都必须在 GPU 上以多个“传递”的形式顺序执行,而单个传递可以在单次超并行 GPU 计算中完成。

聚合

多个 compositor 帧会提交给 Viz,并且需要一起绘制到屏幕上。这通过聚合阶段来实现,该阶段会将它们转换为单个汇总的 compositor 帧。聚合会将 Surface 绘制四边形替换为它们指定的合成器帧。这也是优化掉不必要的中间纹理或屏幕外内容的好机会。例如,在许多情况下,网站隔离的 iframe 的 compositor 帧不需要自己的中间纹理,并且可以通过适当的绘制四边形直接绘制到帧缓冲区。汇总阶段会根据各个渲染合成程序无法访问的全局知识,找出此类优化并加以应用。

示例

以下是代表本帖开头示例的 compositor 帧。

  • foo.com/index.html surface: id=0
    • 渲染传递 0:绘制到输出。
      • 渲染通道绘制四边形:使用 3 像素模糊处理进行绘制,并剪裁到渲染通道 0。
        • 渲染传递 1
          • #one iframe 的功能块内容绘制四边形,并为每个四边形指定 x 和 y 坐标。
      • Surface 绘制四边形:ID 为 2,使用缩放和平移转换绘制。
  • 浏览器界面 surface:ID=1
    • 渲染传递 0:绘制到输出。
      • 为浏览器界面绘制四边形(也采用平铺方式)
  • bar.com/index.html Surface:ID=2
    • 渲染传递 0:绘制到输出。
      • #two iframe 的内容绘制四边形,并为每个四边形指定 x 和 y 位置。

插图作者:Una Kravets。