查看原文
其他

GPU基本处理流程

烓围玮未 IP与SoC设计 2022-04-29


GPU全称是Graphics Processing Unit,图形处理单元。它的功能最初与名字一致,是专门用于绘制图像和处理图元数据的特定芯片,后来渐渐加入了其它很多功能,比如数学计算、物理模拟、AI运算等功能。


GPU处理大致可以分为两类:并行计算和图型处理。图形处理就是GPU基本功能;并行计算是因为GPU有大量的计算单元,特别适合并行计算需求大的领域,比如AI计算,科学计算等;


这里主要介绍的是图形处理的流程。GPU图形处理和常规的摄像头视频处理是不一样的。摄像头输入,然后通过ISP处理,输出到显示屏幕上,这种传统的视频处理不需要GPU;而GPU的图形处理是没有一个外部的视频输入源,而是“无中生有”,生成一副图片,所以GPU的图形处理更像我们的绘画过程。


GPU的图形处理流程大致可以分为以下几个步骤:



1.几何处理

2.光栅化;

3.像素处理;

4.渲染输出;


1. 几何处理


GPU处理始于CPU产生的顶点信息。在应用程序阶段,通过高级编程语言(C、C++、JAVA)进行开发,与CPU、内存打交道,主要任务是识别出潜在可视的网格实例,并把它们及其材质呈交给图形硬件以供渲染。在该阶段的末端将产生几何体数据,包括顶点坐标、法向量、纹理坐标、纹理等,通过数据总线传送到图形硬件以供渲染(时间瓶颈),进行几何阶段。


几何阶段主要负责顶点坐标变换、光照、裁剪、投影以及屏幕映射,该阶段基于GPU进行计算,该阶段的末尾得到经过变换和投影之后的顶点坐标、颜色、以及纹理坐标。


几何处理以CPU送给GPU的顶点信息为起点,如下图所示:



成千上万个三角形就够成了我们要处理的图像:



三角形可以构成任何图像,所以三角形也被称为基本的图元。为什么把图形分成无数个三角形而不是正方形,或者其他图形呢?


主要有以下原因:


1. 三角形是最简单的图形,其他图形都能再分解;


2. 三角形的三个定点能唯一确定一个平面,而大于3个定点不一定在一个平面上;


将图形分解为很多个三角形后,GPU只需要并行处理这些三角形就可以了。这就需要用顶点着色器vertex shader来处理。



现代GPU的几何单元具备完整的setup、顶点调节、多变性调节、基本光照调整以及相关的材质调节等多种功能。通过几何单元的动作,程序员创造的抽象数学将被还原成实在的可视空间几何体,这些几何体还会进一步的根据程序要求被调整到适当的位置,已达到微调模型外形实现不同效果的目的。


要说真正的的几何处理能力,其实GPU一开始是不具备的。我们都知道,GPU的前身显卡所充当的角色曾经是单纯的画笔,CPU指哪里,显卡就画哪里,CPU让画什么显卡就画什么。第一代GPU革命虽然让CPU丧失了对Tranform和lighting的霸占,但第一代GPU依旧不具备调节完整的几何调节能力。顶点的位置依旧是CPU说了算,所有关于几何形状的计算全部由CPU来完成,而且一旦确定就不能再行。


后来,随着模型精度的一步步提升,顶点的数目也跟着多边形的增长一步步的激增,终于到了CPU无法负载的地步。



于是,第二代GPU通过Vertex Shader彻底拿走了CPU调节几何的能力。现在,CPU就只剩下最原始的生成顶点这一功能了。


Vertex Shader的出现,让GPU第一次有了独立自主的调节模型内顶点的能力,也让GPU内部出现了像素处理和Triangle Setup之外的新鲜事物。


几何处理的具体过程:


首先是“描点”,几何单元会按照我们看得见的位置,也就是屏幕的位置为视线的起点,以此出发来设置一个可视空间。由于CPU发送过来的关于这些点的信息只不过是他们在空间中所在位置的坐标,因此几何过程的第一步动作,也就是把这些点按照坐标要求摆放出来的过程。



顶点输出过程


在顶点被摆放妥当之后,setup单元接下来要做的事情就是连接。按照正确的规则将顶点连接起来之后,物体的外形也就被确定下来了。当你抬头仰望星空时,你看到的永远都只是一颗一颗的星星,只有把它们连起来,星座才会出现,setup输出三角形的目的也在于此。



多边形输出过程


完成多边形的构成之后,我们就来到了整个步骤的灵魂——顶点的操作了。图像中细节的变化,比如表情等只是微小部分的改变并不会改变图像中的绝大部分,为每一帧画面的不同表情乃至不同外形去规定以及生成一套新的模型显然是非常不划算的事情。以上这些要求其实都可以通过对对应位置的顶点进行操作,“扯动”多边形形状来完成。


因此程序员会事先创造出表情,然后通过方程规定好这些表情中特定顶点的位置,而几何单元所要做的,就是在保证其他绝大部分顶点和模型位置不变的前提下,根据方程的规定将这些顶点操作到合适的位置,这不仅可以大幅降低CPU生成顶点的次数和数量,更可以非常灵活自由的实现程序员想要的外形效果。因此顶点操作这一步,可以说是整个几何处理过程最根本的核心。



光照蒙皮纹理


进行完上述诸多操作,图像就有了外形,接下来要做的就是蒙皮和打光,Vertex Texture和Vertex Lighting会按照程序要求提前访问材质库,并为整个模型附上最基本的带光照信息的底层纹理,就好像把框架模型变成一座石膏雕像一样。到这一步,常规的几何过程基本上就算完成了。图形就此从满是方程和符号的抽象的世界里具体化出来了。


2.光栅化阶段


光栅化是一种将几何图元变为二维图像的过程。该过程包含了两部分的工作。第一部分工作:决定窗口坐标中的哪些整型栅格区域被基本图元占用;第二部分工作:分配一个颜色值和一个深度值到各个区域。


光栅化过程产生的是片元。二维图像上每个点都包含了颜色、深度和纹理数据。将该点和相关信息叫做一个片元(fragment)。每个片元对应于帧缓冲区中的一个像素。


为什么要进行光栅化呢?那是因为几何出来得到的点,三角形都是三维信息,而图形最终只能在二维的屏幕上显示,所以必须进行三维到二维的转换。


光栅化的目的,是找出一个几何单元(比如三角形)所覆盖的像素。



光栅化会根据三角形顶点的位置,来确定需要多少个像素点才能构成这个三角形,以及每个像素点都应该得到哪些信息,比如uv坐标该是什么等。这是通过对顶点数据进行插值来完成的。


光栅化是将几何数据经过一系列变换后最终转换为像素,从而呈现在显示设备上的过程,如下图:



光栅化的本质是坐标变换、几何离散化。


在Unity3D中,每一个美术模型由顶点和顶点构成的三角面来确定。将3D模型绘制到屏幕上时,根据每个三角面的三个顶点,将这个三角面所覆盖的每一个像素(栅格)进行填充的过程,就叫做光栅化。


3.像素处理


像素处理就是对每个像素进行处理的过程。光栅化产生的二维图像是由一个个像素点组成的,对像素进行贴图后,实现表面光照,界面间的半反射,漫反射以及折射等等视觉效果都是像素处理的内容。



在可编程shader出现之前,人们对像素的操作实际上仅仅能够称之为染色。当时的我们可不像现在这么幸福,那时候对特效的处理只能通过固定的单元来直接实现,每一代API下所能够实现的特定的特效,都需要通过预先将其固化成固定指令的形式出现在硬件中,而对于像素的处理,也仅能局限于固化指令所能够允许的范围内,一旦像素进入管线,程序员就失去了对他的控制。因此,可编程shader尤其是Pixel Shader的出现,在当时是一件轰动的大事,Pixel Shader的出现,标志着程序员在像素级层面上第一次具备了可以精确而且随心所欲的控制自己想要实现特效的能力。


想要在像素层面上精确控制并不是一件非常复杂的事情。颜色的表现来自构成它的三原色的混合度以及透明度,而三原色以及透明度在计算机的世界里都是可以通过数字来精确度量和表达的。所以对于像素来说,只要能够随心所欲的处理构成它颜色以及透明度的RGBA这四组数字,就可以精确控制一个像素的颜色表现。



既然像素处理过程就是处理像素,那我们为什么还要现在物体表面蒙上一层预先烘焙好的材质作为基础呢?反正这些材质上大部分的颜色都不是正确的,到头来还是需要ALU对其进行运算并完成修改,那为何不直接让像素处理单元直接在正确的位置上生成正确的像素呢?这样既可以避免改错这么一个看上去似乎没有必要的步骤,又可以用原本进行材质操作的单元的晶体管来进一步强化ALU部分,让其拥有更强大的功能,何乐而不为呢。


答案很简单——因为现在的ALU根本没有那个本事面对直接生成像素所带来的运算量。


像素的处理过程从本质上来说并不复杂,其巨大的执行难度并不来自步骤的繁琐,而是来源于对大量像素进行数学关系运算所导致的运算量,这10年来针对shader反复的折腾其实也只是因为人们对执行单元能够更加高效的处理数学关系的渴求。


4.渲染输出


渲染输出单元:Render Output Processer。


ROP单元的功能,就是处理雾化等特定的特效,进行采样及抗锯齿操作,以及将所有图像元素混合成最终画面并予以输出。


渲染输出第一步就是渲染,渲染这个词对不少人来说一直都很神圣,其实不然。渲染过程无非就是将已经准备好的诸多图像元素混合在一起而已。几何处理单元,光栅化,TMU以及Shader单元,他们所做的看似复杂的工作,都只是为渲染过程准备基础和条件。而真正进行渲染的混合过程,跟其他图形处理过程一样,从本质上来讲其实非常简单。



除了进行Z值相关的检查来保障输出像素的准确性之外,混合输出过程几乎不会带来其他更加复杂的操作。这种相对宽松和富裕的工作环境,让ROP单元有了扩展的充分理由和借口。于是随着人们图形需求的增长,ROP单元很快就具备了另一个作用,那就是协助完成全屏抗锯齿(FSAA)工作。



混合,中和,赋予像素新的颜色,Anti-Aliasing的这些核心内容,不正是ROP最传统的工作么。抗锯齿的原理,注定了这项工作必须通过ROP单元来进行。



Anti-Aliasing过程的起点开始于对图像的放大,我们首先要将整个图像(超级采样,SSAA)或者比较精确的物体边缘(多重采样,MSAA)进行放大,然后对颜色反差巨大的物体边缘部分的像素及其周围的像素进行提取和混合,形成比原来更加自然但也更加模糊的颜色过渡,最后再将图像缩小回原来的尺寸以便消除颜色过渡产生的模糊现象。



ROP的典型结构如下:



首先,由TMU拾取的纹理以及由shader处理完成的像素会被传送到对应的z/stencil buffer,接下来ROP单元会首先对这些纹理和像素进行z/stencil检查,尽管经由光栅化处理之后的模型已经不具备实际存在的Z轴了,但其深度信息依旧会被保存下来,对于深度和模板信息的判断能够让ROP做出让那些像素被显示出来的决定,这不仅能够避免完全遮挡的像素被错误的显示在前面,同时也能够减少后续的color output部分的压力。由于存在对深度的判断和剔除操作,再加上Raster Operations Units这一特殊名称的误导,很多人都以为光栅化过程是在ROP单元才完成的,实际上Rasterization和ROP单元本身并没有什么直接联系的。Rasterization所进行的是对模型的3D-2D坐标投影变换,而ROP则是对像素的混合和输出。


当所有像素都完成了深度检查等操作之后,特定范围深度值的像素将被输送到alpha单元进行透明度检查,由透明度及透明混合所导致的效果对于雾化以及体积光等效果有至关重要的意义,因此alpha单元的检查与深度检查几乎可以说同等重要。根据程序的需要,ROP会以Blend单元对特定的像素进行alpha Blending操作。



经过上述步骤之后,剩下的像素将会被填充进2D化模型需要的范围内,也就是我们常见的Pixel Fillrate过程。Pixel Fillrate就好像一口大锅,作为肉丝、青椒、冬笋还有葱姜盐糖豆瓣酱之类原料出现的像素会在这里被正确的混在一起。经过混合,图形元素所包含的原本孤立的信息会像食材之间交互作用产生的香气一样被释放出来,最终形成我们能够接受的图像。


由于像素上的效果已经被shader以数学的形式处理完毕了,因此如果没有AA操作,那么到这里为止图形渲染工作就算彻底完成了,所有效果的混合及填充将会让正确的画面最终得以呈现,这幅完成处理的画面会被送入output buffer等待输出。而如果程序要求进行AA操作,比如MSAA,那么ROP中的AA单元还需要对填充完毕的画面进行若干次多重采样,然后再对采样出来的像素点进行color Blending操作,完成之后的画面才会被送入帧缓存等待输出到屏幕上。



后记


理解GPU基本处理流程是学习GPU的基础,只有熟悉了整个流程,才能了解GPU众多技术的目的。


技术很重要,技术背后的思想更重要!


技术背后的某些思想就是你解决以后问题的钥匙。希望本文对你有一点点帮助。


参考文章:引用了GPU大百科全书第一章:美女 方程与几何 系列内容和图像


本文作者:烓围玮未。

主要从事ISP/MIPI/GPU/SOC/车规芯片设计

首发于知乎专栏:芯片设计进阶之路

同步微信公众号:芯片设计进阶之路(x_chip)


本文内容仅代表作者观点,不代表平台观点。

如有任何异议,欢迎联系我们。



往期精彩回顾




2021年的第一场雪!英特尔2020年Q4财报解读



从仿真器的角度理解Verilog语言


科普:什么是OTP?什么是MTP?


博文速递:FloorPlan


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存