像素是怎样练成的(四)

简介: 像素是怎样练成的(四)

文本绘制

文本的绘制操作包含一个包含每个字形的标识符和偏移量的块。

该步包含在显示项列表中,看上图中,位于最后一个.

image.png


{光栅化|Raster}将部分{显示列表|Display List}转换位{位图|BitMap}

{显示列表|Display List}中的绘制操作通过称为{光栅化|Raster}的过程来执行。

image.png

最后生成的位图中的每个像素单元都包含用于编码单个像素的颜色透明度


图片解码

{光栅化|Raster}还会解码嵌入在页面中的图像资源

image.png

绘制操作引用了压缩数据(JPEG、PNG等),然后Raster调用相应的解码器对其进行解压缩。


GPU 进程中进行光栅化

渲染器进程是受沙盒保护的,因此它无法直接进行系统调用

命令缓冲区

光栅化的绘制操作被封装在GPU命令缓冲区中,以便通过IPC通道发送。

image.png


Skia

光栅化通过一个名为Skia的库调用OpenGL

Skia在硬件周围提供了一层抽象,并且能够理解更复杂的内容,如路径和贝塞尔曲线。

Skia是由Google维护的开源项目。它被集成在Chrome二进制文件中,但存在于一个单独的代码仓库中。

它还被其他产品(如Android操作系统)使用。Skia的GPU加速代码路径会构建自己的绘图操作缓冲区,在光栅化任务结束时进行刷新。

image.png


GPU加速生成位图

光栅化后的位图存储在内存中,通常是由OpenGL引用这些GPU内存。

image.png

GPU还可以执行生成位图的命令("加速光栅化")。

请注意,这些像素尚未显示在屏幕上!

绘制操作被发送到GPU进程进行光栅化。GPU进程可以发出真正的OpenGL调用

image.png


页面状态发生变化

上面所讲的流程从DOM=>style=>layout=>paint=>raster=>gpu是页面内容到内存中像素的全流程。但是,渲染过程不是静态的,而是需要无时无刻的将页面状态变化也要考虑进去。

所以,就又引入了我们下面的思考,页面状态发生变化该如何处理。

image.png


在讲变化前,我们再来介绍几个概念。

几个关于帧的知识点

  • 屏幕刷新频率
  • 一秒内屏幕刷新的次数(一秒内显示了多少帧的图像),单位 Hz(赫兹),如常见的 60 Hz。
  • 刷新频率取决于硬件的固定参数(不会变的)。
  • 逐行扫描:
  • 显示器并不是一次性将画面显示到屏幕上,而是从左到右边,从上到下逐行扫描,顺序显示整屏的一个个像素点,不过这一过程快到人眼无法察觉到变化。
  • 以 60 Hz 刷新率的屏幕为例,这一过程即 1000 / 60 ≈ 16ms
  • 帧率 (Frame Rate):
  • 表示 GPU 在一秒内绘制操作的帧数,单位 fps。
  • 例如在电影界采用 24 帧的速度足够使画面运行的非常流畅。
  • 帧率是动态变化的,例如当画面静止时,GPU 是没有绘制操作的,屏幕刷新的还是buffer中的数据,即GPU最后操作的帧数据。
  • 画面撕裂(tearing):
  • 一个屏幕内的数据来自2个不同的帧,画面会出现撕裂感。

每个帧是内容在特定时间点的完整渲染状态

image.png


图层

{图层|Layer}是页面的一部分,可以独立于其他图层进行变换和光栅化。

image.png


{图层|Layer}

我们通过一个真实的案例,来看一下图层,并且它是如何被处理的。

有一个shake样式,它的作用是将指定的元素设置transform:rotate(xx)。让其可以实现在原本位置处,摆动。而这种情况,就是一个页面状态变化,是不能直接套用我们之前的渲染管道了。(我们之前的渲染管道是针对静态页面的)

.shake {
  animation: shake 1s infinite;
  transform-origin: center;
  width:100px;
  height:50px;
}
@keyframes shake {
  0% {
    transform: rotate(0);
  }
  50% {
    transform: rotate(-22.5deg);
  }
  100% {
    transform: rotate(22.5deg);
  }
}

示例1

现在有如下的页面结构

<div>  
  AAA  
  <p class="shake"> BBB </p>
</div>
CCC

通过浏览器的处理后,可以发现现在有了一个单独的layer 区域。

image.png

而页面中呈现的效果如下。

image.png


示例2

页面结构如下:

<div class="shake">  
  AAA  
  <p > BBB </p>
</div>
CCC

layer结构:

image.png

页面呈现效果

image.png


{合成|Compositing}

Compositing is the process or technique of combining visual elements from separate sources into single images, often to create the illusion that all those elements are parts of the same scene. -- 来自维基百科

翻译后的大概意思就是: {合成|Compositing}将来自不同来源的视觉元素组合成单一图像的过程或技术,通常是为了创造所有这些元素是同一场景的一部分的错觉。

下面我们直接看看在页面中通过新增不同的动画效果而合成的视觉效果

image.png

image.png

image.png


合成线程接收输入事件

image.png


图层提升(Layer Promotion)

某些样式属性会导致为布局对象创建一个图层。

image.png

只有一些特殊的渲染层才会被提升为合成层,通常来说有这些情况:

  1. transform:3D变换:translate3dtranslateZ
  2. will-change:opacity | transform | filter
  3. opacity | transform | fliter 应用了过渡和动画(transition/animation
  4. video、canvas、iframe

其实,这里还和层叠上下文有牵扯,这里不展开说明了,具体可以参考CSS重点概念精讲


scrolling layers

image.png


合成分配(Compositing Assignments)生成 property trees

构建{图层树|Layer Tree}是主线程上的一个新的生命周期阶段。

目前,在{绘制|Paint}之前进行图层树的构建,并且每个图层单独进行绘制。

image.png


property trees

合成器可以对绘制图层的方式应用各种属性。这些属性存储在它们自己的树中。

image.png



Commit

在绘制完成后,提交(Commit)操作会在合成线程上更新图层列表和属性树的副本,以使其与主线程上的数据结构状态保持一致。

image.png


分割成瓦片(Tiling)

image.png

光栅化是在绘制之后的步骤,它将绘制操作转换为位图。图层可能很大,如果只有一部分可见,那么对整个图层进行光栅化既耗时间又没必要。

因此,合成线程将图层分割为{瓦片|Tiling}

瓦片是光栅化工作的单位。

瓦片使用专用的光栅化线程池进行光栅化。瓦片的优先级基于它们与{视口|Viewport}的距离。


瓦片被绘制为{四边形|Quads}

一旦所有瓦片完成光栅化,合成线程将生成“绘制四边形”(Draw Quads)。

四边形类似于在屏幕上的特定位置绘制一个瓦片的命令,考虑了图层树应用的所有变换。每个四边形引用了内存中瓦片的光栅化输出。四边形被封装在一个合成器帧对象中,并提交给浏览器进程。

image.png


Display(viz)

{合成帧|Compositor Frame}来自多个渲染器和浏览器(浏览器有自己的用于 UI 的合成器)。

{合成帧|Compositor Frame}与一个{表面|surfaces}相关联,表示它们将显示在屏幕上的位置。

{表面|surfaces}可以嵌入其他{表面|surfaces}

浏览器 UI 嵌入一个渲染器。渲染器可以嵌入其他渲染器,用于跨源 iframe(也称为站点隔离、"out of process iframe" 或 OOPIF)。

显示合成器在 GPU 进程的 Viz 合成器线程上运行。

  • 它是 viz 服务(简称为 "visuals")的一部分。

显示合成器接收传入的帧,并理解嵌入{表面|surfaces}之间的依赖关系("surface aggregation")

image.png


Viz 显示四边形

Viz还会发出GL调用来显示{合成帧|Compositor Frame}中的每个四边形。

这些GL调用在viz合成线程上,它们通过命令缓冲区进行序列化和代理,发送到GPU主线程,在那里解码器会发出真正的GL调用。

image.png


双缓存

为什么要设置双缓存?解决画面撕裂!那何为画面撕裂呢?

画面撕裂原因

屏幕刷新频是固定的,比如每16.6ms从buffer取数据显示完一帧,理想情况下帧率和刷新频率保持一致,即每绘制完成一帧,显示器显示一帧。但是CPU/GPU写数据是不可控的,所以会出现buffer里有些数据根本没显示出来就被重写了,即buffer里的数据可能是来自不同的帧的, 当屏幕刷新时,此时它并不知道buffer的状态,因此从buffer抓取的帧并不是完整的一帧画面,即出现画面撕裂。

简单说就是Display在显示的过程中,buffer内数据被CPU/GPU修改,导致画面撕裂。

双缓存

那咋解决画面撕裂呢?答案是使用 双缓存

由于图像绘制屏幕读取使用的是同个buffer,所以屏幕刷新时可能读取到的是不完整的一帧画面。

双缓存,让绘制和显示器拥有各自的buffer:GPU 始终将完成的一帧图像数据写入到 Back Buffer,而显示器使用 Frame/Front Buffer,当屏幕刷新时,Frame Buffer 并不会发生变化,当Back buffer准备就绪后,它们才进行交换。如下图:

image.png

双缓存,CPU/GPU写数据到Back Buffer,显示器从Frame Buffer取数据

VSync(垂直同步信号)

问题又来了:什么时候进行两个buffer的交换呢?

假如是 Back buffer准备完成一帧数据以后就进行,那么如果此时屏幕还没有完整显示上一帧内容的话,肯定是会出问题的。看来只能是等到屏幕处理完一帧数据后,才可以执行这一操作了。

当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI)。那,这个时间点就是我们进行缓冲区交换的最佳时间。因为此时屏幕没有在刷新,也就避免了交换过程中出现 screen tearing的状况。

VSync(垂直同步)是VerticalSynchronization的简写,它利用VBI时期出现的vertical sync pulse(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。另外,交换是指各自的内存地址,可以认为该操作是瞬间完成。

image.png


一图胜千言

image.png


后记

分享是一种态度

参考资料:

  1. Life of a Pixel
  2. 页面是如何生成的(宏观角度)
  3. RenderingNG中关键数据结构及其角色
  4. chromium结构
  5. CSS重点概念精讲
  6. 维基百科

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
相关文章
|
存储 vr&ar 图形学
法线贴图的视线原理
使用法线贴图可以大大提高渲染效果,使低多边形数的模型看起来具有高多边形数模型的细节和真实感。在游戏开发、电影制作和虚拟现实等领域,法线贴图被广泛应用于增强场景和物体的视觉效果。
160 2
|
4月前
|
编解码 定位技术
航摄比例尺、成图比例尺、地面分辨率与航摄设计用图比例尺
航摄比例尺、成图比例尺、地面分辨率与航摄设计用图比例尺
330 0
|
6月前
|
算法 Java 计算机视觉
基于像素的皮肤检测技术
基于像素的皮肤检测技术
38 1
|
6月前
|
算法 Shell
图像放缩之临近点插值
图像放缩之临近点插值
34 0
|
Web App开发 XML JavaScript
像素是怎样练成的(二)
像素是怎样练成的(二)
|
Web App开发 存储 前端开发
像素是怎样练成的(一)
像素是怎样练成的(一)
|
缓存 JavaScript 前端开发
像素是怎样练成的(三)
像素是怎样练成的(三)
杭电OJ变形 骨牌铺满方格 2501
杭电OJ变形 骨牌铺满方格 2501
106 0
|
算法
坚持写算法题的第四周(一)
坚持写算法题的第四周(一)
120 0
坚持写算法题的第四周(一)
|
算法
坚持写算法题的第四周(三)
坚持写算法题的第四周(三)
111 0
坚持写算法题的第四周(三)