三角形在显卡中的渲染流程

0.前言

我们都知道渲染的流水线,顶点着色器、像素着色器、光栅化等软件层面的概念,在GPU层面,我们知道sm、sp、寄存器、L1、L2等硬件概念,但是软件和硬件的层面是怎么对应的,渲染一个三角形的流程在gpu上到底是怎么进行的,这篇文章就来探讨一下这个问题

1.GPU渲染架构图

NV给出的这个图几乎涵盖了渲染的关键过程

2.物理架构


从Fermi开始Nvdia使用类似的原理架构,使用一个Giga Thread Engine来管理所有正在进行的工作,GPU被划分成多个GPCs(Graphics Processing Cluster),每个GPC拥有多个SM和一个光栅化引擎(Raster Engine),他们其中有很多的连接,最显著的是Crossbar,他可以连接GPCs和其他功能性模块例如ROP或者其他子系统。

着色器程序的执行都是在SM上完成的,sm包含了许多可以进行数学运行的线程核心。一个线程可以被一个vertex或者pixel-shader调用,这些核心或者其他单元被Warp Schedulers管理驱动,Warp调度器管理32线程为一组的Warp,然后送出指令到Dispatch Units。

3.逻辑管线

  1. 程序通过图形API(DX\GL\WEBGL)发出drawcall指令,指令会被推送到驱动程序,驱动会检查指令的合法性,然后会把指令放到GPU可以读取的Pushbuffer中
  2. 经过一段时间或者显示调用flush指令后,驱动程序把Pushbuffer的内容发送给GPU,GPU通过主机接口(Host Interface)接受这些命令,并通过Front End处理这些命令
  3. 在图元分配器(Primitive Distributor)中开始工作分配,处理indexbuffer中的顶点产生三角形工作批次(batches),然后发送给多个PGCs
  4. 在GPC中,每个SM中的Poly Morph Engine负责通过三角形索引(triangle indices)取出三角形的数据(vertex data)[图中的Vertex Fetch模块]
  5. 在获取数据之后,在sm中以32个线程为一组的线程束(warp)来调度,来开始处理顶点数据
  6. SM的warp调度器会按照顺序分发指令给整个warp,单个warp中的线程会锁步(lock-step)执行各自的指令,如果线程碰到不激活执行的情况也会被遮掩(be masked out),被遮掩的原因有很多,例如当前的指令是if(true)的分支,但是当前线程的数据的条件是false,或者比如一个循环被终止了但是别的还在走,因此在shader中的分支会显著增加时间消耗,在一个warp中的分支除非32个线程都走到if或者else里面,否则相当于所有的分支都走了一遍,线程不能独立执行指令而是以warp为单位,warp相互之间是独立的
  7. warp中的指令可以被一次完成,也可能经过多次调度,例如sm中的加载存储单元明显少于数学操作
  8. 由于某些指令比其他指令需要更长的时间才能完成,特别是内存加载,warp调度器可能会简单地切换到另一个没有内存等待的warp,这是gpu如何克服内存读取延迟的关键,只是简单地切换活动线程组。为了使这种切换非常快,调度器管理的所有warp在寄存器文件中都有自己的寄存器。这里就会有个矛盾产生,shader需要越多的寄存器,就会给warp留下越少的空间,就会产生越少的warp,这时候在碰到内存延迟的时候就会只是等待,而没有可以运行的warp可以切换
  9. 一旦warp完成了vertex-shader的所有指令,运算结果会被Viewport Transform模块处理,三角形会被裁剪然后准备栅格化,GPU会使用L1和L2缓存来进行vertex-shader和pixel-shader的数据通信
  10. 接下来这些三角形将被分割,再分配给多个GPC,三角形的范围决定着它将被分配到哪个光栅引擎(raster engines),每个raster engines覆盖了多个屏幕上的tile,这等于把三角形的渲染分配到多个tile上面
  11. sm上的Attribute Setup保证了从vertex-shader来的数据经过插值后是pixel-shade是可读的
  12. GPC上的光栅引擎(raster engines)在它接收到的三角形上工作,来负责这些这些三角形的像素信息的生成(同时会处理背面剔除和early z剔除)
  13. 32个像素线程将被分成一组,或者说8个2X2的像素块,这是在像素着色器上面的最小工作单元,在这个像素线程内,如果没有被三角形覆盖就会被遮掩,sm中的warp调度器会管理像素着色器的任务
  14. 接下来的阶段就和vertex-shader中的逻辑步骤完全一样,但是变成了在像素着色器线程中执行。 由于不耗费任何性能可以获取一个像素内的值,导致锁步执行非常便利,所有的线程可以保证所有的指令可以在同一点
  15. 最后一步,现在像素着色器已经完成了颜色的计算还有深度值的计算,在这个点上,我们必须考虑三角形的原始api顺序,然后才将数据移交给ROP(render output unit),一个ROP内部有很多ROP单元,在ROP单元中处理深度测试,和framebuffer的混合,深度和颜色的设置必须是原子操作,否则的话两个不同的三角形在同一个像素点就会有冲突和错误。

3.物理单元

3.1 LD/ST

16个LD/ST辅助模块,可满足一个Warp从Share Memory或Video Memory加载(Load)或存储(Store)数据。

3.2 SFU

Fermi有4个SFU,负责计算特殊的ALU运算,如SIN、COS,平方根等等,另外SFU还负责为每个像素插值。

3.2 Texture Unit、Texture Cache

Fermi有4个texture units,每个texture unit在一个cycle最多可取4个sample,这时刚好可以送给一个Warp(的16个车道),每个texture uint有16K的texture cache,并且在往下有L2的支持。

3.3 Texture Unit、Texture Cache

48个Warp是以SIMT(单指令多线程)的方式同时运行在16个core上的,每个Warp在Register File都有自己的一份。在每个cycle,Dispatch Unit负责决定何个Warp的何条指令来使用这16个core,执行一条指令。

3.3 olyMorph Engine

除了Shading相关的资源,图中还包括了5个Fixed Function模块——Vertex Fetch, Tessellator,Viewport Transform(Clipping应该也包括在里头),Attribute Setup,Stream Output也含在其中,合称PolyMorph Engine(多形体引擎)。Fermi把这些Fixed Function模块放到一个Pool来Share。

4.内存结构


GPU的内存分为好几个类型,不同类型的速度不一样

5.实验

接下来用我自己的显卡,这个显卡是GeForce GT 755M,只有2个sm,每个sm最大64个warp,每个warp中最多32个thread,配合nv的opengl扩展,通过gl_ThreadInWarpNV,gl_WarpIDNV,gl_SMIDNV这几个指令我们可以验证一下上面的流程,同时可以确定一些上面没有提到的东西

5.1 顶点处理


这个图是设置不同的参数,把顶点处理器的参数输出,这个GPU有两个两个,把另外一个sm都输出黑色,thread输出绿色,warp输出红色

  • a)图是按threadID(color.g=gl_ThreadInWarpNV/gl_WarpSizeNV)来输出的,三角网格是100*100,可以看到一个线程对应一个顶点,一个warp对应一组顶点
  • b)图是按warpID(color.r=gl_WarpIDNV/gl_WarpsPerSMNV)来输出的,三角网格是100*100,网格倾斜了15度。黑色的是另外一个sm的
  • c)图是warpID,三角网格是15*15的。统计后的warp数是8。d)图可以看到对应的网格线
  • e)图是warpID,三角网格是40*40的。统计后的warp数是25。f)图可以看到对应的网格线
  • e)图是warpID,三角网格是100*100的。统计后的warp数是23。顶点着色器是比较简单的
  • h)图是warpID,三角网格是100*100的。统计后的warp数是58。作为e)的对比图,顶点着色器直接做了一个100万次的循环,目的是拉长顶点着色器的完成时间

通过这些图的对比我们可以得出结论

  1. 对比a)和b),顶点着色器是以一个顶点为一个线程来处理的,32线程为一个warp
  2. 对比c和d其中的warp数,可以看到顶点越多warp数就越多
  3. 对比g和h其中的warp数,系统调用的warp会根据顶点数和顶点着色器的任务来分配,越少的顶点warp越少,而且每个着色器上的warp数不会按照最大数来分配,毕竟warp数是软件概念,当单个warp时间过长时,系统为了隐藏延迟会调用更多的warp来参与计算
  4. 顶点着色器最终的运行效率会取决于顶点数,和复杂程度。但是在真实的环境中顶点数不会真正的瓶颈,20个顶点和200个顶点差别不会很大
5.2 像素处理


这个图和顶点的处理的类似,只是换成像素阶段,另外另外一个sm换成蓝色方便观察

  • a)图直接显示出来warp和thread,thread是绿色的块,thread是warp等于0的时候才显示
  • b)图是a)图的局部放大,一个线程块是4*8个像素
  • c)图是两个三角的warp,蓝色的是另外一个sm的,可以看到按块来着色,其中一个块就是一个光栅化引擎
  • d) e)图都是c)图的放大
  • f)图是5*5的网格g)图是f图的放大细节

根据上面的我们可以得出一些结论

  1. 观察a),可以看到这个三角形是倾斜的,可以到里面的分块并没有倾斜而是始终平行于屏幕,所以光栅化会无视原来三角的位置,只会处理三角覆盖屏幕的位置,这个和顶点程序是完全不一样的
  2. 观察b),可以看到一个像素就是一个thread,像素着色依旧是按照warp来调度的
  3. 观察d),每个光栅化引擎(raster engines)都是16*16的像素块,也就是每个包含4*4一个warp
  4. 对比d)e)g),,在三角分割出,可以看到如果一个光栅化引擎恰好覆盖两个三角,那么两个三角会有可能被两个sm覆盖,这里可以确定这个光栅化引擎包含两个sm,一个光栅化引擎在处理覆盖的像素的时候,如果覆盖的区域包含多个三角,每个三角都有可能被不同的sm处理,但是不同的三角分配的warp肯定是不同的,同时在g图也可以看到即使一个光栅化引擎只包含一个三角,可有可能分配给不同的sm里面的不同warp处理
  5. 根据另外一个文章的结果,还可以知道这些光栅化块是按照顺序一个一个在进行

6.理解

通过上述步骤,可以大致理解三角形在gpu中如何渲染的,也可以解释自己一直有的几个问题。

  1. 顶点着色器和像素着色都是在同一个单元中执行的(在原来的架构中vs和ps的确是分开的,后来nv把这个统一了)
  2. vs和ps中的数据是通过L1和L2缓存传递的
  3. warp和thread都是逻辑上的概念,sm和sp都是物理上的概念
  4. 上述第12步里面z-cull是early z optimization而不是常说的z-test,z-test是要在ROP的时候发生,但是这个z-cull要在alpha test, user clip,multi-sampling,texkill, color key都是关闭的情况下才能生效
  5. 基本可以理解三角形的多少,对于顶点着色器和像素着色器的影响

7.参考

  1. Life of a triangle – NVIDIA’s logical pipeline
  2. early z optimization
  3. GPU画像素的顺序是什么
  4. NV_shader_thread_group
  5. (Demo) Visualizing NVIDIA gl_ThreadInWarpNV, gl_WarpIDNV and gl_SMIDNV

Leave a Comment