现在的位置: 首页 > 综合 > 正文

图形流水线之旅 part1 简介、软件栈

2013年02月01日 ⁄ 综合 ⁄ 共 4254字 ⁄ 字号 评论关闭

       

        我设想要在这儿对2011年的图形硬、软件做一些综述点讲解。你平时可以找到你的计算机上关于图形栈的功能性说明,但通常不是说“how”和“why”的。我来试着填上这个空白但不是对任何硬件都进行具体分析。我会尽可能的谈到在Windows上运行D3D9/10/11并支持DX11的硬件——不是API的细节,毕竟这个是我最熟悉的。在这第一部分,我会将很多GPU上的原生指令。

 

应用程序

       这是你写的代码,也有你的bugs。当然,API运行时和驱动程序也有bugs,但这里的bugs与他们无关。所以先要调好程序。

 

API运行

       你通过调用API来进行资源创建、状态设置、绘制等操作。API运行时对你的APP设定的当前状态进行追踪,确认参数有效,检查其他容错性,管理用户可见资源,链接着色器(在D3D中,OpenGL是在驱动层处理这个),决定着色器代码是否执行等等更多。然后将它们全部传递给图形驱动程序——更精确地说,是用户模式驱动程序。

 

用户模式图形驱动程序(UMD)

       这里是展示CPU能力的地方。如果你的APP因为你调用的某些API而崩溃了,它多半发生在这儿。会提示有关”nvd3dum.dll”(NVidia)或者”atiumd.dll”(AMD)的错误。看名字就知道,这些就是用户模式代码(UMD),它和你的APP还有API运行在同样的环境与地址空间里,没有丝毫的特权。它执行一个D3D调用的底层API(DDI),这个API相当类似你在表层看到的那个,但对内存管理等事情更明确。

       这个模块是着色程序编译的地方。D3D传递一个已验证的着色程序流给UMD,例如,这段代码被验证语意合法、遵守D3D约束条件(使用了正确的类型,没用非法纹理/采样,没有超过可用常量缓冲等等)。它由HLSL代码编译而来,而且通常已经做了大量优化(各种循环优化、死代码清除、常量传播、迭代函数预测等等)——这是非常好的,因为这意味着驱动程序可以从所有执行在编译环节相关优化中获益。然而,它还有一系列的底层优化(比如寄存器分配和循环展开),这些是驱动程序的任务。长话短说,代码通常立即被转化成中间件表示,然后被编译成更多。着色器硬件最接近D3D字节码,编译不需要去创造奇迹来给出美好的结果(已经做了大量高收益高消耗优化的HLSL编译器有显著的帮助),但这儿仍然存在大量D3D不知道也不需要关心的底层细节(比如HW资源限制和时间安排限制),这些都是不重要的处理。

       当然,如果你的APP是一个知名游戏,NV/AMD的编程人员可能已经看过了你的着色程序而且代替硬件来手工优化。那那些着色器程序就会被UMD检测到并替换掉。

       更有趣的是:一些API状态可能实际上在着色程序中最后才编译。给个例子,相对独特的(或者说是很少用到的)特性,比如纹理边缘通常不会在纹理取样里面执行,而是变成额外代码放在着色器程序里(或者根本不支持)。这意味着同样的着色程序会多个版本,用于API状态的不同组合。

       顺便的,这也是你每次使用新的着色程序或者资源时经常看见一些延迟的原因。驱动程序会将大量的创建/编译工作延迟,只在确实需要的时候才执行(你没法想象一些APP会创建出多少无用垃圾)。图形编程人员知道虚假的另一面——如果你想确定某些事务是否真的创建了(还是只是保留了内存),你可以发送一个虚假的绘制调用来让他产生警告。丑陋又麻烦,但自1999年开始使用3D硬件以来,我一直这么做。

       咱们继续。UMD其实也会处理一些有趣的东西,比如所有D3D9“遗产”级别的着色程序和固定功能流水线——就是那些D3D控制的老老实实的流水线操作。Shader3.0其实并不差(事实上相当合理),但2.0就粗糙而复杂,1.X的Shader版本根本就是个笑话——回想下1.3版里面的像素着色器。说到这儿,固定功能顶点流水线使用顶点光照还有其他一些东西,是吧,其实依然存在于D3D中,而且被所有现代图形驱动支持。虽然它们只是用来转化成新的Shader版本。

       接下来就该是一些像内存管理的事了。UMD会收到像纹理创建的指令,需要为它们提供空间。事实上,UMD只是把从内核模式驱动(kernel-mode driver)申请到的大内存块分配一下。真正的内存映射是内核模式的权利,UMD可做不了。

       但UMD还是可以做点像纹理混合(除非GPU可以在硬件上搞定,一般使用2D位块传输单元而不是3D流水线),和按计划在系统内存与(映射的)显存之间转换这一类事情。最重要的,它也会在KMD分配完毕并传递给它的指令缓冲区(也叫直接存取内存区,DMA buffers)里面写入东西。指令缓冲区包含的肯定是一系列指令。你的所有状态改变和绘制操作都会被UMD转化成硬件看得懂的各种指令。很多事情你也不必手动触发,比如向显存加载纹理和着色程序。

       一般来说,驱动程序会试着往UMD里塞到处理极限。UMD是用户模式代码,因此它运行的任何东西都不需要内核模式来转换消耗,它可以自由地分配内存,使用多线程等等——因为它只是一个固定的DLL文件而已。这个对驱动程序开发人员非常有利——UMD崩溃了,APP也因此崩溃了,但整个系统没事;只需要系统运行时换一个UMD(记住它只是一个DLL文件)就好了。而且,它还可以用调试器调试。所以UMD不仅高效而且很方便哦。

       额,忘了提醒一个很重要的东西。

       我说的用户模式驱动程序(UMD),指的是所有用户模式驱动程序

       刚才说过了,UMD只是一个DLL文件。即使它因D3D的关系可以直接与KMD相通,它仍然只是个固定的DLL,运行在他被调用处理时的地址空间上。

       现在我们使用的是多任务操作系统。额,其实我们已经使用很久了。

       我们继续谈谈GPU的事情。GPU其实是个共享资源。驱动你的主显示器(即使是显卡交火)的GPU只有一个。然而,我们有多个APP要去访问它。这个可不是简单的自动化工作,很早以前,采用的解决方案是把3D功能一次只给一个APP,这个APP活动的时候,其他的都不能访问。即使你要让你的窗口系统使用GPU进行渲染,上面的工作也不会被打断。这就是为什么你需要一些能任意访问GPU的组件而且可以分配时间片。

 

调度器

       这是一个系统组件。我现在在这儿说的是图形调度器,不是CPU或者IO调度器。这正是你想要的那个——它可以在不同APP间通过时间片来任意访问3D流水线。一种环境的转换会引发GPU上状态的转换(这会在指令缓冲区生成指令),可能也会从显存里调进调出一些资源。当然,任意给定时间内都只有一种处理操作,就是提交指令给3D管道。

       你会发现主机程序员经常抱怨PC上的3D API太过高层又没法干涉,会引发性能消耗。事实上,比起主机游戏,PC上的3D API/驱动有更复杂的问题要去解决——比如它们需要追踪当前全部状态,因为随时都可能从更下层拖出莫名其妙的东西。它们(PC 上的3D API)为破烂的APP工作,还要试着在后台修理性能问题。这个讨人厌的活没人乐意做,当然也包括驱动程序作者自己。最后还是商业性观点赢了,人们期望东西能一直运行还要运行的很流畅。毕竟朝着APP大喊“它出错了”然后生闷气可没法交到朋友。

       不管怎样,搞定了流水线,下一站:内核模式

 

内核模式驱动程序(KMD)

       这个部分才是真的和硬件打交道。同一时刻可能会运行多个UMD实例,但永远只有一个KMD,而且如果它崩溃了,OK,你也挂了——以前是蓝屏,现在Windows知道了如何去结束崩溃的驱动然后重启它。它只要发生一点点崩溃就,不是只有一部分内核内存被污染,而是全都没了。

       KMD在这儿一次性解决所有事。多个APP会在这儿争夺那个唯一的GPU。有的需要调用发射器分配物理内存。有的要在启动时初始化GPU,设置显示模式(从显示器获取模式信息),管理硬件鼠标指针,安排HW计时监视器让GPU无反应的时候重置,响应各种中断等等等等。这些就是KMD要做的事。

       有时在视频播放器与GPU之间设置了内容保护,已解码视频像素对任何非法用户模式代码不可见,这会导致一些糟糕的禁止操作,比如将它们转存到硬盘。KMD也会参与进来。

       对我们来说,重要的是KMD管理着真正的指令缓冲区——就是硬件消耗的东西。UMD处理的指令缓冲区不是真的——事实上,那些只是GPU可访问内存的随机片。真正的情况是UMD接收指令并提交给调度器,等到要处理时再通过UMD的指令缓存传递给KMD。KMD接着让主指令缓冲区调用指令缓存,然后看GPU指令处理器能否从主存里读入,它也可能首先直接访问显存主指令缓冲区是一个相当小的环形缓冲区——唯一做的就是获取写入的系统或初始化指令,然后调用实实在在的3D指令缓存。

       但现在这玩意依然只是内存的一个缓存。图形卡(显卡)知道它的位置——主指令缓冲区里通常存在一个指向GPU的读指针,和一个指向已写入缓存的KMD的写指针(或者更精确,连离GPU多远都告诉了GPU)。那些硬件寄存器被内存映射——KMD周期性地更新它们(无论是否提交新的任务)。

 

总线

       这个东西基本不直接进入显卡(除非它是集成到CPU的)。首先通过总线——通常是PCI串行总线。直接内存存取进行转换,通过同一个路线。这个不会花很长时间,但却是我们旅行的另一个阶段。

 

指令处理器

       这是GPU的前端——就是真正读入KMD写的那些指令的地方。我会在下一部分接着这儿讲,毕竟这一篇已经有点长了。

 

小小的离一下题:OpenGL

       OpenGL相当类似我以上所讲,除了一点,OpenGL里面API和UMD层没有那么明显的区别。不像D3D,GLSL着色程序不是通过API编译,而是全部由驱动程序来处理。一个不好的副作用是,这儿有像GLSL前端一样多的3D硬件供应商,他们基本上都实现了相同的规格,却有独特的bugs和风格。很不爽啊。这意味着驱动程序无论何时碰到着色程序都得自己做所有的优化——包括昂贵的优化。D3D字节码格式在这个问题上是个很干净的解决方案——只有一种编译器(因此不同供应商没有丝毫不兼容),而且它允许一些比你平时使用的更昂贵的数据流分析。











抱歉!评论已关闭.