Unity图形编程常用查询

Tile-based Rendering Architectur

2021-03-25  本文已影响0人  离原春草

在松弛中打开自己,内心阔朗透气,对外界保持开放度,客气不争,但静守原则,慢慢让自己堆积成形,自性光明 —— 黎戈《心的事情》

0. GPU的Rendering Architecture

0.1 SOC硬件架构

在介绍GPU的渲染架构之前,先让我们来看一下现在计算机的SOC(System On Chip)硬件架构。如下图所示:

现在SOC一般都会集成一块CPU与一块GPU。CPU主要是为一些串行的、flow-control运行逻辑的、内存访问低延迟的场景所设计,其性能改进方向针对的主要是分支控制以及内存cache等模块;而GPU则是为一些并行处理的,无分支运行逻辑的场景所设计的,其性能改进方向主要针对的是寄存器以及算术处理单元(ALU)模块。

0.2 Single Instruction Multiple Data

在逻辑执行实现上,CPU主要面向的是具有频繁的分支跳转结构与循环控制结构的应用场景,一般是在单个线程中处理一堆数据,而GPU面向的则是在不同的数据上执行相同代码逻辑的应用场景,一般是对需要处理的数据分割成不同的数据单元,之后分拆到多线程中,进行并行执行:比如对于图形学中的Vertex Shader以及Pixel Shader等,就需要对不同的Vertex/Pixel执行完全相同的代码逻辑。为了提升其执行效率,就需要在多个不同的线程上同时执行相同的计算机指令,这就是所谓的Single Instruction Multiple Data(SIMD)。

相对于传统CPU的串行执行结构来说,SIMD的优势在于,可以使用单条数据的处理时间消耗,完成多条数据处理的效果,这就极大地提升了执行效率。通常来说,相对于CPU的消耗,GPU的执行效率通常会高出多个数量级(即至少成百上千倍的效率提升)。

SIMD的实现方式主要有两种:Vector-Based以及Scalar-Based。这二者的区别在于处理粒度上的不同:Vector-Based指的是将Vector作为最小处理单元,而Scalar-Based指的是将单一的数据元素作为最小的处理单元。

Vector-Based SIMD的优点在于执行非常高效。其主要用来处理那些需要一次性对多个元素(大多数是二、三、四个元素)进行相同指令操作的问题,对于GPU中大量的颜色以及顶点数据处理情景,这种模式就非常合适。其弊端在于,假如需要处理的数据个数不足以填满其基本处理单元一次性能够处理的元素个数,那么就会存在浪费:比如某个处理单元的处理元素个数为4,而传入的参数是Vector3,那么就会有25%的浪费,而如果传入的是Vector2,就会有50%的浪费!这里的浪费指的是电力以及性能的损耗,毕竟处理单元的部分结构处于空转状态。通常解决这种问题的方式为,对程序代码进行优化,尽量保证Vector-Based处理单元的满载状态,但这会带来程序实现上的负担,且对于一些新手或者部分无法满载的情况而言,这种方式也就失效了。

相对于Vector-Based处理方式来说,由于不需要考虑Vector的满载运行,Scalar-Base处理方式就要灵活很多。虽然在Vector类型的数据结构上,Scalar-Based处理方式的效率不如Vector-Based处理方式,但是由于硬件的处理能力实际上是一样的,所以,在非Vector类型的数据结构的处理上,Scalar-Based处理方式的浪费就要少很多,其输出的效率就要高出Vector-Based处理方式不少。

0.3 GPU架构概览

按照使用硬件来划分,GPU可以分成桌面GPU与移动GPU两种,前者主要应用于图形工作站以及个人PC等桌面设备,而后者则应用于手机,平板等可移动设备上:这类GPU芯片尺寸小,且需要照顾到移动设备电池续航能力的限制,所以通常需要牺牲部分带宽(Note:在移动平台上增加GPU计算能力相对增加带宽而言是比较容易的事情,增加多个GPU Core就能够做到,但是经常受限于移动平台有限的带宽,即使有了足够强大的计算能力,其表现也上不去)与性能。由于使用场景与特点的不同,这两者在使用方式如渲染架构上也有所区别。

渲染架构按照数据的硬件处理单元的不同,可以分成Unified以及Un-Unified两种。Unified Shading Structure指的是不论是Vertex Shader(VS)也好,还是Pixel/Fragment Shader(PS)也好,都是由相同的处理单元进行计算的。而Un-Unified Shading Structure则是对于Vertex-Shader与Pixel/Fragment Shader的数据计算则是各由不同的专门的处理单元负责完成的。

如上图所示,我们可以看到,从时间轴上看,Unified中,VS与PS是交替穿插执行的,谁有需求谁就上。而Dedicated中,则是各人耕各田,无田自休闲。显然,从执行效率的角度来说,Unified框架要高于Dedicated框架。另外,从VS/PS的复杂度来分析,也可以得知,Unified框架能更好的适应具有不同VS/PS负载比的情况,不论是VS Bound(瓶颈在VS的情况,称作VS Bound)的情况还是PS Bound的情况,Unified框架都能够很好的hold住。

此外,根据硬件渲染架构来划分,GPU又可以划分成三类:

1. IMR的渲染流程及其在移动平台上的表现

桌面GPU基本上使用的是IMR,甚至部分移动GPU(如NVIDIA的GeForce ULP和Vivante的GC系列GPU)也是使用IMR。所谓的IMR,说的是GPU完成某个物体或者某个三角面片的渲染之后,就会将渲染结果写入FrameBuffer中,之后就开启下一个物件的渲染流程。其渲染流程可以用下述的伪代码所概括:

for draw in renderPass:

    for primitive in draw:

        for vertex in primitive:

            execute_vertex_shader(vertex)

        for fragment in primitive:

            execute_fragment_shader(fragment)

IMR渲染架构框架如上图。

假设某个物体A完全被物体B所遮挡,但是A早于B渲染,那么在A渲染完成后,再进行B的渲染,就会出现B的渲染结果将A的渲染结果完全覆盖的情况,这种时候,GPU对于物体A的渲染的所有消耗都成为白费,这就是所谓的OverWrite。

开发人员常常通过使用Early-Z的方法来降低OverWrite。所谓的Early-Z,指的是在渲染之前对物体或者三角面片进行排序,使得在深度上更为靠近相机的物体或三角面片先渲染,从而将靠后的物体或者三角面片通过GPU自带的深度Test方式剔除掉来避免后续的计算消耗的一种方法。但是这种方法的效果表现通常都跟各个具体的场景有关:有的表现好,有的表现差。上图中的Early Visibility Test就是此处的Early-Z。

当然,前面所说的A被B完全遮挡住的情况是一种比较极端的假设,在实际情况中可能A并未被B完全遮挡,而且也可以通过软件对需要渲染的物体按照距离相机的深度进行排序来极力避免这种浪费的产生,但是这种做法一方面是会引入排序的消耗,另一方面也无法完全避免这种浪费。

此外,在一些常见的复杂渲染算法中,IMR往往需要处理大量的数据,而这些数据通常是无法完全塞入GPU的Cache的,因此经常会需要与系统内存进行交互读写(如深度buffer,color buffer,stencil等等),而这个过程是比较慢的,且对带宽有着较高的要求,同时还会产生较高的能耗。虽然通常在桌面应用中,都会对这些传输的数据进行压缩处理,但实际上压缩后的数据量依然不容小觑,因此不适合在移动设备这种电量有限且带宽较低的硬件上使用。

由于使用IMR架构对带宽与能耗有一个比较高的要求,而当前移动平台上由于电量的限制,导致GPU与CPU是共享一块内存的(所谓的Unified Memory Infrastructure),导致移动平台上的带宽也是严重受限的,所以目前大部分的的Mobile GPU(ARM公司的Mali GPU,高通的Adreno,Imagination的PowerVR等)都不能支撑IMR的消耗,必须另谋出路。

为了解决上述两个问题,Tile-based Rendering(TBR)的GPU框架应运而生,而当前的移动平台的渲染架构目前基本上都是采用 Tile-based Rendering(TBR)的渲染模式。那么什么是Tile-based Rendering,这种GPU架构怎么解决带宽与能耗的问题,Tile-based Rendering与Tile-based Deferred Rendering之间的Deferred是什么意思,这两者又有何区别呢?

2. TBR

在GPU中有一块超高速的On-Chip芯片,其作用类似于常说的Cache,此芯片的容量较小,最小可以去到16*16像素块大小。而所谓的Tile-Based Rendering,就是将以往IMR与系统内存进行频繁交互的过程迁移到这块高速On-Chip Buffer中完成,避开上面的那些阻碍性的慢速操作,从而实现渲染加速的一种Rendering架构。

由于On-Chip芯片容量较小,无法将整屏的FrameBuffer数据全部塞进去,所以为了充分利用On-Chip芯片的高速优势,就需要将IMR中的FrameBuffer分割成一个个的互不重叠的小方块Tile,每次只渲染一个Tile,这样在渲染每个Tile的时候,所有需要的数据如Color、Depth等都能够从On-Chip芯片中取得,从而避免与系统内存进行缓慢的交互处理。在Tile渲染完成之后,就会将其结果输出到FrameBuffer中对应的区域。当所有Tile都渲染完成的时候,FrameBuffer上的数据就是需要显示的整个画面的数据,如下图所示:

注意,实际上Tile的尺寸并不一定需要是方块,实际上,PowerVR SGX的Tile的长宽是不相同的。Tile-Based Rendering的渲染架构图如下所示。

参考上面的TBR的渲染流程图,可以文字简述如下:

如果用伪代码来表示,TBR的渲染流程可以分成两个Pass,在第一个pass的时候,GPU将提交过来的所有DrawCall都收集起来,经过Vertex(Shader)+Tiling处理后存储内存中的Frame Data结构中;之后在第二个pass的时候,逐Tile逐Tile的将Frame Data数据从内存中读取出来,之后完成Rasterization + Fragment Shader相关的渲染操作:

# Pass one
for draw in renderPass:
    for primitive in draw:
        for vertex in primitive:
            execute_vertex_shader(vertex)
        append_tile_list(primitive)
# Pass two
for tile in renderPass:
    for primitive in tile:
        for fragment in primitive:
            execute_fragment_shader(fragment)

按照这种描述,对于那些跨越多个Tile的三角面片来说,就需要被渲染多次,按理来说,应该比只渲染一次的IMR要更慢才对,到底是如何实现其渲染的加速的呢?实际上,其能做到加速主要是由于以下几个因素:

TBR也有着自身的一些不足:

TBR跟IR的渲染逻辑流程如下图所示,其中上半部分是TBR,下半部分是IR:


3. TBDR

在TBR的渲染流程中,由于On-Chip 芯片的介入,使得对于各个面片的处理过程避免了与内存的频繁交互,使得写入FrameBuffer的数据都是最终会显示到屏幕上的数据,避免了IMR中的OverWrite,但是却没有避免OverProcess,即重叠的面片还是需要经历一道处理工序,虽然其结果并没有输出到FrameBuffer中。

PowerVR在TBR的基础上,增加了一种叫做Hidden Surface Removal(HSR,Mali上类似的技术名字叫Forward Pixel Killing)的硬件(如下图所示),可以对每个Tile中三角形列表进行检测,提前将被遮挡的三角形剔除出去,从而避免这些无效数据对pixel shader造成的消耗,进一步提升性能,这就是所谓的TBDR。

之所以PowerVR有勇气称他们的技术为Truly TBDR,是因为他们对Defer的解释是将所有像素相关的处理过程都Delay或者说Defer到其Visibility属性都已经完全清楚知道之后再进行,如此就能完全避免无谓的OverWrite了。而在IMR或者TBR中,对于被遮挡面片或者物体的处理,是通过软件算法在CPU中完成的,而PowerVR通过增加硬件的方式大大加速了这个过程,对OverWrite的侦测与剔除流程也更为完善:保证了只有那些在屏幕中可见的像素才会被传递到Fragment Shader阶段进行着色处理。

3.1 Tiler

我们之前说到,对于一帧中提交到GPU的所有三角面片的Tile处理,在PowerVR的结构中是通过Tiler硬件完成的。下面我们来看下Tiler的主要的工作流程。

在每一帧中,提交到GPU的所有面片都会按照用户定义好的逻辑如Vertex Shader进行变换处理,最终我们得到的实际上是屏幕空间中的数据。PowerVR中的Tile Accelerator就会根据此时的位置,判定当前的三角面片都落在了哪些Tile上面。并将这个三角面片索引添加到对应这些Tile的Triangle List中。而各个的Triangle List以及变换后的三角面片的数据都存储在一个临时存储结构中,这个结构在PowerVR中叫做Parameter Buffer,也就是我们刚才所说的Frame Data。

跟Frame Data一样,Parameter Buffer同样也是存储在系统内存中的,且包含了渲染一个Tile所需的全部数据。

3.2 HSR(Hidden Surface Removal)

在传统的IMR渲染框架中,Override带来的消耗主要有两方面:不可见像素的渲染成本以及将这些无效像素数据写入FrameBuffer的带宽消耗。如前所属,TBR通过On-Chip芯片解决了后者的消耗问题,但是前者无效像素的Shading消耗还依然存在,因此对三角面片进行排序,按照深度顺序进行渲染,借用early-Z的优势,依然能对帧率的提升产生作用。

PowerVR的HSR技术可以避免上述不可见像素的shading消耗,具体来说,在进行fragment shader之前,HSR就会对三角面片进行处理,确保只有会显示在最终屏幕中的像素才会参与到后续的计算当中。具体的执行过程类似于Z Test,如下图所示,对于不透明物件,只保留最近的像素,对于透明的物件而言,其对应的像素数据都会被保留。

HSR原理

Mali家的Forward Pixel Killing(FPK)技术在实现上更加接近Depth Test的逻辑,如下图所示,对于每个处于Tile覆盖之下的像素(更准确来说是quad),都会将之传入FPK Logic单元,在这个过程中会与已经存入到FPK Buffer对应位置的最近depth数值进行比对,只有当不远于当前已存储的数据,才会进入后续的shading过程,并同时更新FPK Buffer中的Depth数据。

FPK

关于FPK更进一步的细节请移步参考文献[10]:

  • 如果Raster新产生的quad pass test,并且quad的4个pixel被fully covered,那就把与该quad具有相同位置的,更早的(意味着更远)那些thread全终止(它们可能还在FPK Buffer或已经在近Fragment Shader了)。
  • 另外,当quad被两个较近triangle组合起来cover到时,较远的triangle对应该位置的quad也不需要做shading。因此为进一步优化,Mali保存了整个tile所有Quad最近一次的coverage,如果FPK新近的quad不是full covered,但与该quad最近的一次coverage相或后是full coverage,则类似1),要把更早的thread全终止,即发出kill信号。
    -- ~ -- ~ -- ~ -- ~ -- ~ -- ~ -- ~ -- ~ -- ~ -- ~
    作者:xiaocai
    链接:https://www.zhihu.com/question/49141824/answer/136096531
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
FPK示例

经过HSR之后,需要shading的像素数目就大大减少了。因为HSR是将最终需要显示到屏幕中的像素进行输出,那么会影响到这一判断的一些操作如discard,sample masking,alpha testing,alpha-to-coverage以及alpha blending都会对结果产生影响,使得HSR的优化失效。所以为了最大化HSR的性能提升效果,就需要竭力避免上述操作,即使因此需要增加state change也是值得的。即使真的不能避免,最好也要将这些物件的渲染顺序进行限定,从前往后依次是:Opaque->Alpha Test-> Alpha Blend

在参考文献所举的例子中,对于一个画面平局OverWrite数为4.7的场景,经过PowerVR的HSR处理后,可以将OverWrite降低到1.2,看得出来,HSR的效果还是很强劲的。

对于那些并非PowerVR的硬件来说,也可以通过增加一个depth pass的方式来模拟HSR(实际上,HSR的实现流程也是相仿的,只是结果更为精确):

通过第一次pass拿到的Depth Buffer,可以有效避免第二次pass中无效像素在复杂Fragment Shader中的消耗。

这种技术在IMR中经常使用,同样,在TBR中也有很好的效果,只是在这两种模式中的表现有所不同:IMR中,Depth-Only Pass会需要写Depth Buffer,这就会有带宽消耗;而TBR中,Depth数据实际上是存在Frame Data中的,所以没有这部分消耗;不过因为需要走两次Pass,所以在TBR中,可能需要额外存储一份Frame Data。

4. TBR的渲染流程

从之前的TBR、TBDR的渲染架构图,我们可以看到实际上渲染的三个步骤:CPU,Vertex Shader,Fragment Shader三者其实是没有太大耦合的,出于性能考虑,可以考虑按照流水线的生产方式将之错开放置在三帧中完成,渲染流程如下图所示:

这张图中绘制的三种操作在每帧中的耗时看起来是相同的,实际上情况可能比这个复杂一点:三种操作号是各不相同,且具体耗时情况随着渲染内容的不同而有所不同。

这种流水线型的渲染结构会有以下一些问题:

5. TBR/TBDR使用建议

TBR与传统的IMR的实现方式的不同,决定了其使用方式也存在差异。因此在使用TBR的时候,有一些注意事项,总结归纳列举如下:

与IMR渲染的中间结果是放置在FrameBuffer中不同,TBR处理的所有中间数据都是frame data,frame data的尺寸与提交到GPU的三角面片数成正相关,三角面片数越多,frame data的尺寸越大,在每帧正常终止的时候,会清空frame data,所以在使用TBR的时候,必须要保证每一帧都会被正确的终止,否则可能出现frame data持续增加而导致性能快速下降的表现。实际上,当三角面片书过多,就会导致驱动对frame data进行一次强制的flush刷新操作,将数据写入到内存中,等到进入fragment shader阶段的时候,再从内存中读取出来。这种情况下消耗的带宽至少会占用16倍的正常flush所用带宽大小。从这一点看来,三角面片的数目与渲染消耗的时间并不是一种线性关系

在一些应用中,常常因为上一帧的数据内容可以重用,而选择保留上一帧的内容,但是这种操作可能会导致一些额外消耗:渲染开始的时候,Tile Buffer会需要从之前保留的FrameBuffer中读取颜色数据,就产生了带宽消耗以及时间消耗。当然,如果绘制保留内容的时间远远大于拷贝之前Buffer数据的时间,那么保留上一帧的数据内容还是很划算的。高通为了处理这种情况,特意增加了一个扩展接口,EXT_discard_frame_buffer,通过调用这个接口,可以指定哪些区域的数据需要更新,具体可以参考这篇文章

比如进行一些会导致frame data被flush到FrameBuffer的操作如eglSwapBuffer, glFlush, glFinish, glReadPixels, glCopyTexImage, glBlitFrameBuffer, query occlusion in current frame, render-to-texture(有些驱动中,glBindFrameBuffer也可能会导致对FrameBuffer的Shading操作,因此每帧中最好只调用一次bind操作)等,总结起来,这些操作可以概括为

此外需要注意的是,即使这些操作中的部分操作看起来像是全程在GPU中完成,比如从pixel pack buffer中调用glReadPixel操作,也是会有很大损耗,为啥呢,因为每个draw-then-access操作,都会触发fragment shading,从而造成消耗。

6.参考文章

[1] 移动GPU三种主流架构优缺点浅析

[2] Tile-Based Rendering

[3] Understanding PowerVR Series5XT: PowerVR, TBDR and architecture efficiency

[4] POWERVR GRAPHICS ARCHITECTURES

[5] A look at the PowerVR graphics architecture: Tile-based rendering

[6] OpenGL Insights

[7] PowerVR Hardware Architecture Overview for Developers

[8] A look at the PowerVR graphics architecture: Deferred rendering

[9] 针对移动端TBDR架构GPU特性的渲染优化

[10] Tile-based 和 Full-screen 方式的 Rasterization 相比有什么优劣? - xiaocai的回答 - 知乎

上一篇下一篇

猜你喜欢

热点阅读