图形管线之旅 Part2

原文:《A trip through the Graphics Pipeline 2011》
翻译:往昔之剑
 
转载请注明出处
 
还没那么快
 
在上一篇,讲述了渲染命令在被GPU处理前,经历的各类阶段。简而言之,比你想像的要复杂。接下来,我将讲述提过的命令处理器(command processor),最终都对command buffer作了哪些事情。啥?哪提过这个了——骗你的- -。这篇文章确实是第一次提到命令处理器,可是记住,全部command buffer都通过内存或系统内存来访问PCIE和本地显存。咱们将按顺序通过管线,所以在咱们到达命令处理器以前,咱们先来聊聊内存。
 
内存系统
 
GPU没有规则的内存系统,这不一样于你常见的通用CPU或其它硬件,由于它被设计成多种用途。在常见的机器上 你能发现有两个本质区别:第一,GPU内存系统带宽很快,至关快。Core i7 2600K勉强能达到19GB/s的带宽。GeForce GTX480 的带宽接近180GB/s——差了一个数量级啊!
第二点,GPU内存系统频率很慢,至关慢。Nehalem(第一代Core i7)主内存的cache miss大约140个时钟周期,这是按照时钟频率除之内存延迟得来的数据(AnandTech给出的数据)。我刚提到的GeForce GTX 480的内存延迟大约400~800时钟周期,比Core i7有4倍多的内存延迟。除此以外,Core i7的时钟频率是2.93GHz,而GTX 480 shader时钟频率表是1.4GHz——就是说,这里还有两倍的差距。哇塞,差出一个数量级了!靠,搞笑呢吧,我有点激动。这必定是一种权衡,继续听下去。
 
没错——GPU在带宽上大量增长,可是它们要付出大量增长内存延迟的代价(事实证实,这至关耗电,不过已经超出本文讨论范围了)。这种模式上—— GPU的吞吐量受限于延迟。不要一味的等待结果,干点什么事情吧!
 
以上是你须要了解的关于GPU内存的知识,除了DRAM的趣闻外,后面讲的也都很重要:DRAM芯片被组织成2D网格——不管逻辑上仍是物理上。有(水平)行线和(垂直)列线,这些线的每一个交叉点有一个晶体管和一个电容器。若是你想知道如何用这些材料制做内存,能够访问( https://tokyo.zxproxy.com/browse.php?u=V%2FmbvKGmGdz9QE4KTynGv27LALUJtfhzT4wC%2FQA%3D&b=14#Operation_principle)。总之,重点是DRAM的地址是被行地址和列地址分开的,DRAM的一次内部读/写老是访问给出行的全部列。访问内存一行上的全部列比访问相同数量的多行内存的开销要小得多。这仅是DRAM的一个小知识,但对后续来讲很是重要。注意:之后再看一下这里。把这里,包括以前的内容联系起来,只读几个内存字节,并不能达到内存带宽的最大值,若是想要内存带宽饱和,应该一次读满一整行DRAM。
 
PCIe 主机接口
 
按照图形程序员的观点,这部分硬件没什么意思。实际上,这也是GPU的硬件架构。你也得关心它,由于它太慢的话也是瓶颈。因此得找靠谱的人把它弄好,确保没问题。除此以外,它还能让GPU读/写显存和大量的寄存器,让GPU访问(一部分)主内存。让人烦恼的是,这些传输的延迟比内存延迟更糟糕,由于得从芯片发出信号,到插槽里,通过主板,而后到CPU上要很长时间。带宽虽然合适——在16-lane PCIe 2.0接口上 可达8GB/s峰值,大部分是GPU使用的, 而CPU只占1/3~1/2的带宽。这个比例是能够的。不像早期的AGP,是对称的点对点连接——带宽是双向的。AGP有一个快速通道,从CPU到GPU上,可是反向是不行的。
 
最后一部分的内存小知识
 
老实说,咱们如今已经很是很是接近实际看到的3D指令了!你都快闻到它了。但还有一件事情咱们须要解决。由于如今咱们有两种内存——(本地的)显存和映射的系统内存。一个是往北走一天的路程,另外一个是往南走沿着PCIe高速公路一周的旅程,选哪一个?简单的解决方案:只增长一条额外的地址线,来告诉你走哪条路。这就简单了,颇有效并已经使用很长时间了。或者你多是在统一的内存架构上,好比某些游戏主机(不包括PC)。那种状况的话,就不用选了,只有内存是你要去的地方。若是你想更好一点,你就添加一个MMU(memory management unit内存管理单元),它提供给你彻底虚拟化的地址空间,并容许你搞一些很好的tricks,好比频繁地访问显存中的纹理(这很快),其余部分在系统内存里,以及大部分彻底没映射的——就跟凭空变出来同样,一般,读一个磁盘花50年的话, 这绝不夸张,访问内存就比如是一天,这就是一个硬件的读取花费时间,这至关的快。操蛋的磁盘!我跑题了……
 
当你显存不够用的时候,MMU还能整理显存地址空间上的碎片,而不用实际拷贝。好东西啊,它让多个进程共享同一GPU更简单了。使用一个MMU确定是能够的,但我不肯定是否须要它,尽管它至关好用(谁来帮帮我? 若是我搞懂了,我会更新这篇文章,可是如今我压根不懂)。总之,MMU/虚拟内存并非你实际添加上去的(不像在架构里的缓存和一致性存储器),并不针对于某个特别的阶段——我会在别的地方提到它,先把它放着。
 
还有个DMA引擎,能够拷贝内存而不牵扯到咱们重要的3D硬件/shader内核。一般,它至少能够在系统内存和显存之间拷贝(双向的)。它经常进行显存复制(若是你须要整理显存IPIan的话,这就颇有用)。它一般不能进行系统内存的拷贝,由于这是GPU,而不是内存拷贝单元——在CPU上执行系统内存拷贝,不用双向的通过PCIe。
 
我画了个图,展现更多的细节——如今,你的GPU有多个内存控制器,每一个控制多个内存条,它们都得争取到带宽:)
 
好,来列个清单。CPU端有一个预置的command buffer,有了PCIE主机接口,CPU能够通知咱们和写寄存器。咱们得把逻辑转变成地址载入,而后返回数据——若是它是从系统内存通过PCIe的,假如咱们想要获取显存中的command buffer。KMD会设置一个DMA传输,不管是CPU仍是GPU上的shader内核都不用管它。而后经过内存系统能够拿到显存中的拷贝数据,这就是咱们设置的全部过程,最后来看一下command buffer。
 
终于到了命令处理器
 
在开始讨论命令处理器以前,已经作了不少准备工做,用一个词归纳它,那就是“缓冲”。
 
如上所述,内存通道是高带宽而且高延迟的。对于大多数GPU管线后续位而言,解决办法是运行多个独立的线程。但若是这样作的话,咱们只有一个命令处理器,得考虑一下command buffer的顺序(由于command buffer包含了状态改变和执行渲染须要的正确队列)。因此咱们接下来应该作的事情是:添加一个足够大的缓冲区向前预取来避免间断。
 
在该缓冲区中,命令处理器能到达实际的命令处理前端——基本上就是个知道如何解析指令的状态机(按照硬件规范格式)。一些指令处理2D渲染操做——除非把命令处理器单独分为2D的,3D前端才不用管它。无论怎样,如今的GPU仍藏有检测2D的硬件功能,就像是淘汰掉的VGA芯片的某个地方依然支持文本模式,4-bit/像素的位平面模式,平滑滚动之类的同样。没用显微镜就能发现这些淘汰掉的东西说明运气不错。总之,这些东西还存在,可是之后我就再也不讲它们了:)而后是实际处理3D/shader管线里一些图元(primitive)的指令了。我会在接下来的部分讲它们。还有一些指令在3D/shader管线里因为各类缘由(和各类管线设置)不参与渲染,后面都会讲。
 
接下来是改变状态的指令。做为一个程序员,你能够认为它们只是改变了一些变量。可是GPU是一个巨大的并行计算器,在并行系统里你不能只改变一个全局变量并想让它正确工做——若是你不能保证全部东西都是一成不变的,最后就会出bug。有几种常见的办法,基本上全部的芯片都针对不一样类型的状态使用不一样的方法:
  • 当改变一个状态的时候,你得让全部涉及到的工做都结束掉(即flush部分管线)。在过去,显卡芯片都是这么处理状态改变的——这很简单,而且批次少,三角面少,管线简短的时候开销不大。随着批次和三角面数的增长,这种开销也增长了。这种办法限用于改变频率不高的(只刷新整个管线的一部分影响不大)或者只实现开销大/难度大的特殊需求。
  • 你可让硬件单元彻底的无状态。只传递状态改变指令给指定的阶段,而后周期性的把这个阶段追加到当前状态。这些状态不会保存在哪里——但老是存在,若是管线的其它阶段想要知道这些状态位是能够的,由于已经当参数传进来了(而后传递给下一阶段)。若是你的状态只改变少数位,那就不划算了。要是改变所有的纹理采样状态设置,那还行。
  • 有时只存一份状态的拷贝,每次阶段上都要改变一大堆的东西,都得刷新它。但要是存两份拷贝(或四份)那就好多了,这样前端的状态设置就能够提早了。要是你有足够的寄存器(插槽)来位每一个状态存储两个副本,一些激活的工做用插槽0,你能够安全的修改插槽1而不用中止或干扰到工做的运行。如今你不要发送整个状态到管线了——只有一个指令,选择使用插槽0仍是1。固然,若是插槽0和1正在使用,又赶上一个状态要改变的时候,你仍是得等,可是你能够提早操做一步。这个技术不止用两个插槽。
  • 对于采样器或者纹理资源视图(Shader Resource View)的状态,你能够在同一时间大量的设置,不过你也不会这么作。你不会仅仅由于你要跟踪两条凭空的状态集,就为2*128的纹理保留状态空间。对于这种状况,你可使用一种寄存器重命名方案——拥有128个实际纹理描述的内存池。若是在一个shader里真的须要128个纹理,那改变状态将很是的慢。但大多状况下,一个应用程序用不到20个纹理,你有至关多的空间来保障多个版本。
这些并不全面——但重点是在你的应用程序里改变一个变量看似简单(甚至UMD/KMD和command buffer也是)实际上可能须要背后大量的硬件支持来保证性能。
 
同步
 
指令的最后一部分是处理CPU/GPU和GPU/GPU同步。一般,这些形式都是“若是事件X发生,则执行Y”。我将先讲“执行Y”的部分,它多是GPU告诉CPU如今该作什么的推送通知(“CPU啊,我正要进入显示设备0的垂直空白间隙VBI,因此你要是不想无效的翻缓冲,如今就赶忙干活吧!”),或者也多是GPU只记录发生了什么,CPU能够之后来询问它(“说吧,GPU,你最近处理了哪一个command buffer片断?”—“等我查一下啊,序列号是303。”)前者经过中断来实现,只用在频率不高的,优先级高的事件,由于中断开销很大。在这以后,须要每次触发事件时,从command buffer将值写入到CPU可见的GPU寄存器。
 
好比你有16个寄存器,将寄存器0赋值为currentCommandBufferSeqId。给每次要提交到GPU的command buffer分配一个序列号(这步在KMD中完成),而后在每一个command buffer的开始部分,添加标记“若是到达这里,就写入register 0”。瞧,如今GPU知道正在处理哪一个command buffer了,咱们知道命令处理器会按序列严格执行完全部的指令,因此若是第一个指令command303被执行,那就是直到序列号为302的command buffer都已经完成了,它们如今能够被KMD从新利用、释放、更改,或者想怎么处理均可以。
 
关于“若是事件X发生”中的“事件X”是什么,咱们来举一个例子,好比“若是你到达这里了”——大概就是这个意思。再好比,“若是在command buffer里,shader读取完全部渲染批次的贴图以前”(这时候就代表回收再利用texture/render target的内存是安全的),“若是全部激活的render target/UAV已经处理完了”(这表示实际上能够将它们做为纹理安全使用了),“若是到目前为止全部的操做都已经完成”,之类的等等。
 
这些操做一般被叫作“fences”,顺表说一下,有不少种方法从状态寄存器中取出写入的值,但我以为最靠谱的方法是使用一个顺序计数器(可能借用了其它知识)。没错,这里有些概念我没讲,由于我以为你应该都懂。我之后可能会详细说明:)
 
已经讲了一半了——如今能够从GPU返回状态到CPU了,容许咱们在驱动程序里作适当的内存管理(如今能够知道,在何时能够实际安全的复用vertex buffer,command buffer, texture和其它资源了)。但这还没完——还漏了一个难点。要是咱们须要纯粹的在GPU端同步呢?咱们回到刚才render target的例子上,在实际的渲染完成以前,不能使用它做为纹理(而且在其余步骤发生的时候——曾经用过的纹理单元也有不少细节)。解决办法是“等待”指令:“一直等到寄存器M有值N”。这能够是等于,小于,或者更复杂的比较操做——简单起见,只讨论等于的状况。在提交渲染批次以前,“等待”能够容许咱们同步render target。还能够容许咱们构建一个flush GPU的操做:“若是挂起的工做完成了,设置寄存器0为++seqId”/“一直等到寄存器0有值seqId”。GPU/GPU同步就所有搞定了——在DX11的compute shader指令里,有一种更细粒度的同步,这是GPU端惟一的同步机制。关于 规则渲染,你不须要了解太多。
 
顺便说一下,若是你能够写CPU端的寄存器,你还能够用另外一种方法——提交一个局部comand buffer,包含上一个的特殊值,而后让 CPU端替代GPU端改变寄存器。这种方法能够用来实现D3D11风格的多线程渲染,你能够提交一个包含vertex/index buffer引用的渲染批次,CPU仍然要加锁(有可能会正被另外一个线程写入)。你仅须要在实际渲染调用以前发送一个等待指令,随后一旦vertex/index buffer解锁,CPU就能够改变寄存器内容了。若是GPU没收到这个指令,那么等待指令就是一个空操做;若是收到了,就花费一些(命令处理器)时间处理。干的漂亮吧?实际上, 若是你在提交指令以后更改command buffer,即便没有CPU可写的状态寄存器,也能够实现这种方法,只要有一个command buffer“跳转”指令。细节留给读者思考:)
 
固然,你没必要须要这种设置寄存器/等待寄存器模型;对于GPU/GPU同步,你仅须要一个“render target barrier”指令,来确保render target能够安全使用,还须要一个“flush全部东东”的指令。可是我更喜欢这种设置寄存器风格的模型,由于能够一石二鸟(反馈给CPU正在使用的资源,和GPU自同步)。
 
这里,我画了一张图。有点复杂,我说一下细节。基本想法是这样:命令处理器开始部分有一个先入先出队列(FIFO),跟着是指令解码逻辑,由2D单元、3D前端(常规3D渲染)或者shader单元(compute shader)等多种块来直接执行操做,还有一个块处理同步/等待指令(包含我说过得公开可见寄存器),和一个处理command buffer跳转/调用指令的单元(改变了当前的预取地址,转向FIFO)。全部分派工做的单元都须要发送给咱们完成事件,因此咱们知道什么时候纹理再也不被使用了,以及能够再利用它们的内存。
 
结束语
 
下一步,才正真接触渲染工做。最后还剩3个部分关于GPU,咱们开始看一下顶点数据!(三角形还没被光栅化呢。还须要一些时间)
 
事实上,在这个阶段,管线已经出现了分支;若是咱们运行compute shader,下一步将是compute shader阶段。可是咱们先不讲它,由于compute shader是后面的部分!先讲常规渲染。
相关文章
相关标签/搜索