图形管线之旅 Part 1

原文:《A trip through the Graphics Pipeline 2011》
翻译:往昔之剑
 
转载请注明出处
 
你可以找到很多PC图形栈的功能描述,但是通常却不明所以然。我会尽量避开硬件部分的种种细节,来填补这些空白知识点。我打算讲述
一下在Windows上运行d3d9/10/11的dx11接口的硬件,因为发生在PC上的堆栈细节我再熟悉不过了,而不是API之类的细节。这第一部分,会讲很多我们实际在GPU上执行的本地指令。
 
应用程序
这是你的代码部分,还包括各种bug,没错,运行时API和驱动程序都有bug,但不是指的这两者,现在准备好修复掉他们。
 
运行时API
即通过API的资源创建/状态设置/draw call等,运行时API跟踪应用程序设置的状态,验证参数,处理错误,一致性检测,管理用户的可见资源,或者检验shader代码和link shader(在d3d中是这样,OpenGL是在驱动层做的),还可能分批处理更多的任务,然后在图形驱动中处理所有事情——更准确的说,是在用户模式驱动中(user-mode driver)
 
用户模式驱动(UMD)
这是CPU端隐藏的最为神秘的部分,如果你的应用程序因调用了某些API崩溃了,那通常都是这里造成的:)。它可能是“nbd3dum.dll”(NVidia)或者“atiumd*.dll”(AMD)。 如命名所示,这是用户模式代码。它和你的应用程序一样,都运行在相同的上下文和地址空间里,并没有什么特殊的。它实现的底层API(DDI)即D3D。这个API和你表面上看到的很类似,但是在内存管理上是有一点区别的。
 
这个模块诸如发生在Shader编译期间。D3D传递一个预校验好的Shader记号给UMD,即代码已经检查过了,是语法正确,符合D3D约束的(指使用正确的类型,没超出可用的纹理/采样器,没超出可用的常量缓冲,等)。这是从HLSL代码编译的,通常有相当多的高级优化(各种循环优化,消除没用的代码,常量传递,预测分支等)——这是很好的,在编译阶段进行了大量的优化是很有帮助的。然而,还有大量的底层优化(比如寄存器分配和循环展开)都是由驱动来完成的。长话短说,通常都先转化为中间表示(Intermediate Representation,IR),然后再编译。Shader硬件指令很接近于D3D字节码(HLSL编译器已经帮助做了高度的优化工作),但是仍有一些底层细节(比如硬件资源限制和调度约束),D3D不知道也并不关心,这不是重要过程。
 
当然,如果你的应用程序是知名游戏,NV/AMD的程序员可能会查看你的shader,并给出针对于硬件优化过的shader。这些Shader能被UMD检测到并替换,是很友好的。
 
更有趣的是:某些API状态可能实际上最终被编译进Shader里——举个例子,相对一些不常使用的特性,比如纹理边框(texture borders)可能没实现进纹理采样器里,但是通过额外的shader代码来模拟实现(或者完全不支持)。这意味着,有时相同的Shader有多个版本,对应不同的API状态。
 
顺便说一下,这就是为什么你经常看到第一次使用一个新的shader时候会发生延迟。大量的创建/编译工作被驱动延迟执行,并且只在它实际用到的时候才执行(你根本不会相信一些应用程序创建了多少没用的资源)。图形程序员都知道的一个事情——如果你想要确保某些东西被实际创建了(而不是仅仅的保存在内存里),你需要执行一个虚拟的draw call来让它被“唤醒”(原文:warm it up)。这很烦人,但是这自从我1999年第一次开始使用3D硬件就一直这样,这是个不争的事实,这点上要去适应它:)
 
继续话题。UMD还能处理一些有趣的东西,比如D3D9遗留的Shader版本和固定管线——没错,所有的D3D功能都支持。3.0 shader profile并不糟糕(实际上还相当合理),但是2.0有点混乱,各种1.x的shader版本那就更乱套了——还记得1.3 pixel shader吗?还有 带顶点光照的固定顶点管线?是的,现代显卡支持所有的D3D功能,尽管他们目前只是翻译到新的Shader版本(这样做已经很长时间了)。
 
还有内存管理。UMD要获取到纹理创建指令的内容,并且需要为他们分配空间。实际上UMD只是进一步从KMD(内核模式驱动,Kernel-Mode Driver)分配一些大的内存块,实际的映射和非映射页(管理UMD可见的显存,反之,GPU可以访问的系统内存)是KMD的特权,UMD不能做。
 
但是,UMD可以做,像重组纹理(swizzling textures)(除了GPU可以在硬件里执行,通常使用2D块传输单元而不是真正的3D管线)和系统内存与映射显存之间的传输调度。最重要的是,一旦KMD已经分配和处理好,UMD还可以写command buffer(或叫”DMA Buffer“——我将交替使用这个名称)。Command buffer,包含了各种指令。所有的状态改变和绘制操作将通过UMD转化成硬件可识别指令。很多事情不需要手动触发——比如上传纹理和Shader到显存。
 
一般来说,驱动会尝试尽可能的在UMD中实际处理,UMD是用户模式代码,所以运行在这里的部分不需要昂贵内核模式转换,它可以*分配内存,分派出多个线程工作,等等——这只是一个常规的DLL(尽管API不是应用程序直接提供的)。这也有利于驱动开发——假如UMD崩溃了,应用程序也会崩溃,但是不是整个系统都崩溃,当系统运行时,它可以被替换(它仅仅是个DLL),可以被常规的调试器调试,等等。所以,这不仅很有效,还很方便。
 
还有一个重点我没有提到。
 
我说的是“User-Mode Driver”吗?我说的是“User-Mode Drivers”。
如之前所说,UMD只是个DLL。好吧,借助D3D的帮助直接通向KMD,但它仍旧是个普通的DLL,运行在调用进程的地址空间中。
但是,我们如今在使用的多任务系统已经有很长时间了。
我一直在谈论关于GPU的什么?是共享资源。你的主显示只有一个(即使你使用SLI/交火)。然而,我们有多个应用程序尝试访问它(假设只有他们这样做)。这不会自动工作。在过去,解决办法是 当应用程序激活时,一次只给出一个3D应用程序,所有其他的不能访问。但是如果你尝试让你的窗口系统使用GPU渲染,这就不行了。这个原因就是为什么你需要一些组件 来仲裁访问GPU,并分配时间片等。
 
进入调度器
这是一个系统组件。我这里说的图形调度器,不是CPU或IO调度器。确切的说,由它来决定在不同应用程序之间何时访问想要使用的3D管线。一些GPU状态切换(生成额外的commands到command buffer)会发生上下文切换,还可能交换一些资源在显存内外。当然,在指定的时间只有一个进程可以实际提交commands到3D管线。
 
你会经常发现终端程序员抱怨PC上的3D API太高层,不可控,并且耗性能。但是,比起终端游戏,PC上的3D API/驱动真的有太多复杂问题要解决——他们需要跟踪全部的当前状态,因为随时可能从底层发现问题!他们还要管不好使的应用程序,尝试修复背后的性能问题。这是很烦人的事,没人喜欢,当然也包括驱动作者他们自己,但实际上站在商业角度,人们想要应用能继续运行(并且顺利运行)。只对着应用程序喊“出错啦!”,然后生着闷气,慢慢检查,你是赚不到朋友的。
 
管线之旅的下一站:内核模式!
 
内核模式(KMD)
 
这部分由硬件实际处理。可能有多个UMD实例运行在同一时间,但永远只有一个KMD,如果KMD崩溃了,你的程序也就玩完了——过去是蓝屏,但现在的Windows知道如何杀死崩溃驱动的进程并且重新载入它。尽管它只是崩溃,而非内核内存被污染,但一旦发生,所有的东西就都没了。 
 
KMD一次性处理所有事物,尽管多个应用程序都争夺使用它,但只有一份GPU内存。某些程序实际调用的是物理内存。同样,某些程序在启动时必须初始化GPU,设置显示模式(从显示设备获取信息),管理硬件鼠标指针,制定硬件监视器,如果一定时间内无响应,就重置GPU等等。这就是KMD做的事情。  
 
还有视频播放器DRM格式的内容保护,以及GPU解码像素对非法用户不可见,可能会导致一些糟糕的事情,比如转存储到磁盘。KMD也会参与这些事情。
 
对我们来说最重要的是,KMD管理实际的command buffer。要知道,这才是硬件的实际消耗。UMD处理的command buffer并不是真实的——它们只是能访问到的GPU随机内存片段。实际上,由UMD完成处理,将它们提交到调度器,然后等到该处理了,再传递UMD的command buffer到KMD。然后,KMD向主command buffer写入调用指令,再跟据GPU指令处理器能否读取主内存,可能会先执行DMA访问显存。主command buffer通常是一个(相当小的)环形缓冲结构——只能获取写入的系统/初始化指令和调用实际的3D command buffer。
 
但这还只是一个内存缓冲,显卡还得知道它的位置——通常在主command buffer里有一个GPU端的读指针和一个标记KMD已写入缓冲位置的写指针(或更准确的说,到目前为止告诉GPU写了多少)。KMD会周期性更新这些被内存映射的寄存器(通常是每次提交新工作块的时候)……
 
总线
 
不直接访问显卡(除非是集成在CPU上的),因为它需要先通过PCI Express总线,DMA传输也是走的这个路线。这部分不会讲述很长时间,但也是我们的图形管线之旅的一站地。
 
指令处理器
 
这是GPU的前端——实际读取KMD写入指令的地方。我将继续在下一篇讲述,这篇文章写得已经有点长了:)
 
小旁白:OpenGL
 
OpenGL和我上面讲述很类似,除了API和UMD层的区别。不像D3D,GLSL shader编译不能通过API操作,只能在驱动中完成。这点有个不好的地方,有很多GLSL平台,像3D硬件厂商,他们只实现相同的规范,但有各自的bug和问题。这意味着驱动不得不自己做好所有的优化。D3D字节码格式在这点上解决方案很简洁——只有一个编译器(因此不同厂商很少有不兼容的情况),并且可以进行数据流分析。
 
 
上一篇:Android平台下OpenGL图形编程


下一篇:图形管线之旅 Part6