OpenGL-渲染架构

CPU与GPU

CPU

中央处理器(central processing unit,简称CPU)作为计算机系统的运算和控制核心,是信息处理、程序运行的最终执行单元。擅长处理复杂和具有依赖关系的运算。

GPU

又称显示核心、视觉处理器、显示芯片,是一种专门在个人电脑、工作站、游戏机和一些移动设备(如平板电脑平板电脑、智能手机等)上做图像和图形相关运算工作的微处理器,是连接计算机和显示终端的纽带。GPU使显卡减少了对CPU的依赖,并进行部分原本CPU的工作,尤其是在3D图形处理时GPU所采用的核心技术硬件T&L(几何转换和光照处理)、立方环境材质贴图和顶点混合、纹理压缩和凹凸映射贴图、双重纹理四像素256位渲染引擎等。而硬件T&L技术可以说是GPU的标志。GPU的生产商主要有NVIDIA和ATI。不擅长处理逻辑复杂的运算,擅长处理单一的运算,具有很高的并行运算能力。

计算机显示方式

随机扫描显示

电子光束根据图案进行扫描绘制,比如绘制一个圆,电子光束就在荧屏上转一圈,绘制两个圆,则转两圈。由于光束移动很快和视觉暂留的原因,导致我们看起来,图案是在一瞬间整个显示出来的。显示一个图像的时间,和图像复杂度有关。

在这里插入图片描述

光栅扫描显示

屏幕由一行行的像素点组成,电子枪从上到下一行行逐一扫描每一个像素点,每个像素点根据显示数据呈现出不同状态,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。在光栅扫描方式中,图形是通过控制每个像素点而显示出来的,每一个像素必须被逐次扫描,这样曝光时间几乎与图形无关。

在这里插入图片描述

光栅扫描显示方式的主要组要组成部分

显示器: 主要用来显示内容,从帧缓冲区中读取数据。

视频控制器: 负责控制刷新的部件,控制帧缓冲区和显示器的对应关系。

帧缓冲区: 又叫帧缓冲或者显卡,用来存储处理过或者即将提取的渲染数据。由一个一个的阵列组成,显示器中的每个像素点都会在帧缓冲区中有对应的区域。

计算机渲染原理

最早期,没有独立显存的说法。CPU处理完的显示数据,直接存放在系统主存中,系统总线将数据读出来后传入显示控制器,再通过显示器进行绘制。随着发展,后来直接在系统主存中开辟了一块专门存放帧缓存的空间,叫做帧缓存。

在这里插入图片描述
在这里插入图片描述

随着绘制压力日趋增长,又衍生出了新的绘制结构,也就是目前主流的独立显卡绘制方式。多出来一个独立显卡的硬件,专门用来存储需要绘制的数据,CPU解码出来的数据放在内存中,再从内存中拷贝到显存中的帧缓冲区,GPU直接操作显存中的内容进行绘制。这样就不会受限于系统总线的数据输送,减轻了系统压力,使绘制能力更强。具体过程为,显示控制器从帧缓存区中读取数据(位图信息),然后进行数模转化(数字信号转化为模拟信号),最后从电子光束屏幕左上角开始逐行扫描进行显示。

在这里插入图片描述

早期问题和解决

问题1:屏幕撕裂

由于帧缓冲区中的数据存储和电子光束扫描并不是时刻关联的,导致某些时刻会出现屏幕撕裂的情况。比如帧缓冲区中存储着第一帧图片数据,视频控制器从中读取并通过电子光束进行扫描显示。当扫描到一半的时候,帧缓冲区中的数据更新为第二帧图片数据,这时,视频控制器继续读取的数据跟着变成了第二帧的后半段数据,于是不同帧的数据拼组在一块显示,就出现了屏幕撕裂的现象。

在这里插入图片描述

解决方案

为了解决屏幕撕裂,苹果引入了“垂直同步Vsync + 双缓冲区DoubleBuffering”机制。

垂直同步: 在每次扫描结束后发送一个垂直同步信号Vsync,接收到垂直信号后才会读取新的数据。这样保证了电子光束每一次扫描都是渲染完整的一帧,避免了屏幕撕裂现象。

双缓冲区: 屏幕撕裂的根本原因是有CPU和GPU工作的不同步导致的,所以垂直同步虽然避免了屏幕撕裂现象,但却没有从根本上解决CPU和GPU工作速度不同步的问题。所以引入了双缓冲区机制。将数据交替存入两个缓存区中,读取也是在两个缓冲区中交替读取。

问题2:掉帧

垂直同步+双缓冲区解决了撕裂问题,但引发了新的问题->掉帧。当绘制完A缓冲区中数据后,如果B缓冲区中数据还未处理完,这时候只能继续显示A缓冲区中数据,这种情况就叫做掉帧,其实就是常见的卡顿。

解决方案

三缓冲区: 三个缓存区虽然仍未从根本上解决掉帧,但相比双缓冲区已经极大地降低了掉帧的发生几率。

总结

以掉帧为代价解决屏幕撕裂问题,来获取更好的使用体验。

iOS下的渲染框架

下图所示为 iOS App 的图形渲染技术栈,App 使用 Core GraphicsCore AnimationCore Image 等框架来绘制可视化内容,这些软件框架相互之间也有着依赖关系。这些框架都需要通过 OpenGL 来调用 GPU 进行绘制,最终将内容显示到屏幕之上。

在这里插入图片描述

  1. UIKit

    UIKit 是 iOS 开发者最常用的框架,可以通过设置 UIKit 组件的布局以及相关属性来绘制界面。

    事实上, UIKit 自身并不具备在屏幕成像的能力,其主要负责对用户操作事件的响应(UIView 继承自 UIResponder),事件响应的传递大体是经过逐层的 视图树 遍历实现的。

  2. Core Animation

    Core Animation 源自于 Layer Kit,动画只是 Core Animation 特性的冰山一角。

    Core Animation 是一个复合引擎,其职责是 尽可能快地组合屏幕上不同的可视内容,这些可视内容可被分解成独立的图层(即 CALayer),这些图层会被存储在一个叫做图层树的体系之中。从本质上而言,CALayer 是用户所能在屏幕上看见的一切的基础。

  3. Core Graphics

    Core Graphics 基于 Quartz 高级绘图引擎,主要用于运行时绘制图像。开发者可以使用此框架来处理基于路径的绘图,转换,颜色管理,离屏渲染,图案,渐变和阴影,图像数据管理,图像创建和图像遮罩以及 PDF 文档创建,显示和分析。

    当开发者需要在 运行时创建图像 时,可以使用 Core Graphics 去绘制。与之相对的是 运行前创建图像,例如用 Photoshop 提前做好图片素材直接导入应用。相比之下,我们更需要 Core Graphics 去在运行时实时计算、绘制一系列图像帧来实现动画。

  4. Core Image

    Core ImageCore Graphics 恰恰相反,Core Graphics 用于在 运行时创建图像,而 Core Image 是用来处理 运行前创建的图像 的。Core Image 框架拥有一系列现成的图像过滤器,能对已存在的图像进行高效的处理。

    大部分情况下,Core Image 会在 GPU 中完成工作,但如果 GPU 忙,会使用 CPU 进行处理。

  5. OpenGL ES

    OpenGL ES(OpenGL for Embedded Systems,简称 GLES),是 OpenGL 的子集。在前面的 图形渲染原理综述 一文中提到过 OpenGL 是一套第三方标准,函数的内部实现由对应的 GPU 厂商开发实现。

  6. Metal

    Metal 类似于 OpenGL ES,也是一套第三方标准,具体实现由苹果实现。大多数开发者都没有直接使用过 Metal,但其实所有开发者都在间接地使用 MetalCore AnimationCore ImageSceneKitSpriteKit 等等渲染框架都是构建于 Metal 之上的。

    当在真机上调试 OpenGL 程序时,控制台会打印出启用 Metal 的日志。根据这一点可以猜测,Apple 已经实现了一套机制将 OpenGL 命令无缝桥接到 Metal 上,由 Metal 担任真正于硬件交互的工作。

UIView 与 CALayer 的关系

CALayer 事实上是用户所能在屏幕上看见的一切的基础。为什么 UIKit 中的视图能够呈现可视化内容?就是因为 UIKit 中的每一个 UI 视图控件其实内部都有一个关联的 CALayer,即 backing layer。由于这种一一对应的关系,视图层级拥有 视图树 的树形结构,对应 CALayer 层级也拥有 图层树 的树形结构。
在这里插入图片描述

其中,视图的职责是 创建并管理 图层,以确保当子视图在层级关系中 添加或被移除 时,其关联的图层在图层树中也有相同的操作,即保证视图树和图层树在结构上的一致性。

那么为什么 iOS 要基于 UIView 和 CALayer 提供两个平行的层级关系呢?

其原因在于要做 职责分离,这样也能避免很多重复代码。在 iOS 和 Mac OS X 两个平台上,事件和用户交互有很多地方的不同,基于多点触控的用户界面和基于鼠标键盘的交互有着本质的区别,这就是为什么 iOS 有 UIKitUIView,对应 Mac OS X 有 AppKitNSView 的原因。它们在功能上很相似,但是在实现上有着显著的区别。

实际上,这里并不是两个层级关系,而是四个。每一个都扮演着不同的角色。除了 视图树图层树,还有 呈现树渲染树

CALayer

那么为什么 CALayer 可以呈现可视化内容呢?因为 CALayer 基本等同于一个 纹理。纹理是 GPU 进行图像渲染的重要依据。

纹理本质上就是一张图片,因此 CALayer 也包含一个 contents 属性指向一块缓存区,称为 backing store,可以存放位图(Bitmap)。iOS 中将该缓存区保存的图片称为 寄宿图

在这里插入图片描述

图形渲染流水线支持从顶点开始进行绘制(在流水线中,顶点会被处理生成纹理),也支持直接使用纹理(图片)进行渲染。相应地,在实际开发中,绘制界面也有两种方式:一种是 手动绘制;另一种是 使用图片

对此,iOS 中也有两种相应的实现方式:

  • 使用图片:contents image
  • 手动绘制:custom drawing

Contents Image

Contents Image 是指通过 CALayercontents 属性来配置图片。然而,contents 属性的类型为 id。在这种情况下,可以给 contents 属性赋予任何值,app 仍可以编译通过。但是在实践中,如果 content 的值不是 CGImage ,得到的图层将是空白的。

既然如此,为什么要将 contents 的属性类型定义为 id 而非 CGImage。这是因为在 Mac OS 系统中,该属性对 CGImageNSImage 类型的值都起作用,而在 iOS 系统中,该属性只对 CGImage 起作用。

本质上,contents 属性指向的一块缓存区域,称为 backing store,可以存放 bitmap 数据。

Custom Drawing

Custom Drawing 是指使用 Core Graphics 直接绘制寄宿图。实际开发中,一般通过继承 UIView 并实现 -drawRect:方法来自定义绘制。

虽然 -drawRect: 是一个 UIView 方法,但事实上都是底层的 CALayer 完成了重绘工作并保存了产生的图片。下图所示为 -drawRect: 绘制定义寄宿图的基本原理。

在这里插入图片描述

  • UIView 有一个关联图层,即 CALayer

  • CALayer 有一个可选的 delegate 属性,实现了 CALayerDelegate 协议。UIView 作为 CALayer 的代理实现了 CALayerDelegae 协议。

  • 当需要重绘时,即调用 -drawRect:CALayer 请求其代理给予一个寄宿图来显示。

  • CALayer 首先会尝试调用 -displayLayer: 方法,此时代理可以直接设置 contents 属性。

    - (void)displayLayer:(CALayer *)layer;
    
  • 如果代理没有实现 -displayLayer: 方法,CALayer 则会尝试调用 -drawLayer:inContext: 方法。在调用该方法前,CALayer 会创建一个空的寄宿图(尺寸由 boundscontentScale 决定)和一个 Core Graphics的绘制上下文,为绘制寄宿图做准备,作为 ctx 参数传入。

    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
    
  • 最后,由 Core Graphics 绘制生成的寄宿图会存入 backing store

Core Animation 流水线

通过前面的介绍,我们知道了 CALayer 的本质,那么它是如何调用 GPU 并显示可视化内容的呢?下面我们就需要介绍一下 Core Animation 流水线的工作原理。

在这里插入图片描述

事实上,app 本身并不负责渲染,渲染则是由一个独立的进程负责,即 Render Server 进程。

App 通过 IPC 将渲染任务及相关数据提交给 Render ServerRender Server 处理完数据后,再传递至 GPU。最后由 GPU 调用 iOS 的图像设备进行显示。

Core Animation 流水线的详细过程如下:

  • 首先,由 app 处理事件(Handle Events),如:用户的点击操作,在此过程中 app 可能需要更新 视图树,相应地,图层树 也会被更新。
  • 其次,app 通过 CPU 完成对显示内容的计算,如:视图的创建、布局计算、图片解码、文本绘制等。在完成对显示内容的计算之后,app 对图层进行打包,并在下一次 RunLoop 时将其发送至 Render Server,即完成了一次 Commit Transaction 操作。
  • Render Server 主要执行 Open GL、Core Graphics 相关程序,并调用 GPU
  • GPU 则在物理层上完成了对图像的渲染。
  • 最终,GPU 通过 Frame Buffer、视频控制器等相关部件,将图像显示在屏幕上。

对上述步骤进行串联,它们执行所消耗的时间远远超过 16.67 ms,因此为了满足对屏幕的 60 FPS 刷新率的支持,需要将这些步骤进行分解,通过流水线的方式进行并行执行,如下图所示。

在这里插入图片描述

Commit Transaction

在 Core Animation 流水线中,app 调用 Render Server 前的最后一步 Commit Transaction 其实可以细分为 4 个步骤:

  • Layout
  • Display
  • Prepare
  • Commit

Layout

Layout 阶段主要进行视图构建,包括:LayoutSubviews 方法的重载,addSubview: 方法填充子视图等。

Display

Display 阶段主要进行视图绘制,这里仅仅是设置最要成像的图元数据。重载视图的 drawRect: 方法可以自定义 UIView 的显示,其原理是在 drawRect: 方法内部绘制寄宿图,该过程使用 CPU 和内存。

Prepare

Prepare 阶段属于附加步骤,一般处理图像的解码和转换等操作。

Commit

Commit 阶段主要将图层进行打包,并将它们发送至 Render Server。该过程会递归执行,因为图层和视图都是以树形结构存在。

动画渲染原理

iOS 动画的渲染也是基于上述 Core Animation 流水线完成的。这里我们重点关注 app 与 Render Server 的执行流程。

日常开发中,如果不是特别复杂的动画,一般使用 UIView Animation 实现,iOS 将其处理过程分为如下三部阶段:

  • Step 1:调用 animationWithDuration:animations: 方法
  • Step 2:在 Animation Block 中进行 LayoutDisplayPrepareCommit 等步骤。
  • Step 3:Render Server 根据 Animation 逐帧进行渲染。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值