这里是原文链接。
这里介绍的是GPU在2011年的软硬件相关内容。在网上大家能找到PC中的graphics stack在做什么的描述,但是却很少能找到为什么要这么做以及究竟是怎么做的的文章介绍,这里会尝试来填补这个空白,当然这个介绍不会太过深入硬件的底层实现。由于作者对D3D比较熟悉,因此这里就以D3D为例进行介绍,但是其实大部分知识都是相同的,OpenGL的基本框架与D3D基本大同小异,因此也可以互为映证。
下面会对GPU管线进行分层介绍。
The Application
这里对应的是用户代码,用户代码包括资源创建,状态设置,渲染指令调用等。
The API Runtime
用户代码通过一些特定的API调用实现对GPU的操控,这些API调用最终都会汇总到API Runtime,这个部分负责对用户所设定的状态进行追踪,对参数进行激活,对错误与一致性进行检测,对用户可见的资源如贴图等进行管理,对shader代码进行激活与反激活,对shader代码进行链接处理(至少D3D是这样做的,OpenGL是在GPU驱动层完成这一项的)以及其他的一批工作,之后将这些工作交付给显卡驱动,更准确的说,是user-mode驱动。
The user-mode graphics driver (or UMD)
如果因为部分图形API调用而导致应用崩溃,十有八九就是发生在这里。这个部分被称之为 “nvd3dum.dll” (NVidia) 或者 “atiumd*.dll” (AMD)。如名字所暗示的,这个对应的是user-mode代码,这段代码运行的上下文环境跟地址空间都跟主应用没什么两样(哦,跟API Runtime也是相同的),并没有什么特权。这个部分实际上实现了一套比较底层的API,这个API跟平时在表面上看到的图形API差不多,不过更多的是关注内存管理相关的内容。
这个部分会完成shader编译相关的功能。D3D会将一个经过检查(语法检查,API调用合法性检查,资源有效性检查等)的shader代码传递给UMD,HLSL Shader代码在编译过程中会经过一系列的高层次优化(循环优化策略,无效代码移除,分支预测等),这些优化对于后续的GPU驱动都有很大帮助,此外可能还有一部分的低层次优化(比如循环展开unroll,寄存器分配等),而这些优化通常驱动会比较希望交由驱动等底层来进行。经过编译后shader代码就转换为中间语言Intermediate Representation(IR)。
另外,如果各位开发的是一款爆款游戏的话,那么NV/AMD厂商可能还会对游戏中的shader代码进行分析并针对他们的硬件进行手动优化,当然为了避免被人唾骂这里需要保证最终的渲染结果是相同的。这些优化也是通过UMD对shader进行替换实现的(NV/AMD:不客气~~)。
更有意思的是,部分API的状态可能会对shader编译的结果产生影响,举个例子,部分奇特的特性(或者说很少使用的一些特性)比如texture borders实际上根本不是在texture sampler中实现的,而是通过额外的shader代码来模拟(或者根本就不支持)。这也就意味着,同样的shader代码,使用不同的API状态的组合,其最终的结果也有所不同?(表示不是很能理解,可能是少见多怪了。。)
在实际开发与游戏中,经常会遇到由于新资源加载或者新shader使用时的卡顿,实际上这是因为很多shader编译、创建过程都被驱动推迟了,在项目打开的时候实际上只是将资源加载到内存中,并没有真正编译,只有等到真正需要使用的时候才会进行创建与编译(有很多游戏会在开发过程中积累了很多无用的资源,从这个角度来看,这种做法还是很有好处的)。这种黑魔法的使用历史很早,至少从1999年就已经存在了。
UMD还会负责处理D3D的一些早期shader版本与固定管线的支持工作,比如D3D至今还依然支持shader 1.3等早期版本(现在都是通过将之转换为更新版本的shader代码来实现支持)。
UMD还会负责一些内存管理相关的工作,比如有时候会处理一些类似于贴图创建之类的指令,这时候就需要为之分配相应的空间。实际上,UMD是将从KMD(kernel-mode driver)分配过来的大块内存进行二次分配而已。而page之间的mapping跟unmapping逻辑,指定UMD可见的显存区域,以及反过来,指定GPU可以访问的系统内存区域等工作则是KMD的特权,UMD是做不了的。
不过UMD可以做到类似于swizzling textures的工作(除非GPU硬件可以做到这个工作,通常是使用2D的位块传送,而非使用真3D管线来完成),以及负责对数据在显存与系统内存之间的传送进行规划scheduling等工作;更重要的是,一旦KMD创建Command buffer完成并将之移交给UMD,UMD可以对command buffer(或者说DMA buffer,在这里两者是等价的)进行写操作。所有的状态设置以及渲染等操作,最终都会被UMD转换为硬件可识别的Command并塞入到Command buffer中。此外还有一些非人为指定的重要工作,比如将贴图以及shader代码等上传到显卡中等。
总的来说,驱动会将尽可能多的工作塞入到UMD中完成,因为这是user-mode代码,在这个上面运行不需要考虑高消耗的user-mode转换处理,且可以快速分配内存,并使用多线程对工作进行调配加速。实际上,这就是一个常规的DLL,不同的是它是由API而非用户APP加载,这种做法对于驱动开发来说,有很多好处,比如一旦UMD崩溃了,顶多只会导致应用崩溃,对于底层的驱动是没有任何影响的;并且可以在运行时对UMD进行替换,可以使用普通的调试器进行调试等等;非常高效且方便。
这里还有一个非常重要的内容没有提到。
Did I say “user-mode driver”? I meant “user-mode drivers”.
前面说到,UMD实际上是一个DLL,而这个DLL是运行在调用这个DLL的进程的地址空间的,不过,现在的操作系统大多是多任务处理操作系统,那会有什么问题呢?
这里所讨论的GPU实际上是一个共享资源,即使电脑使用的是SLI/Crossfire,也只会有唯一的一个GPU来驱动主要的显示器。而在窗口化操作系统中,可能会同时有多个应用需要对其进行访问。一些早期的解决方案是将3D权限一次只赋给一个应用,不过对于窗口化操作系统,这种做法显然是不合理的,因此这里需要一个单独的组件来对将时间划片并对GPU访问权限进行仲裁。
Enter the scheduler
这是一个系统组件,实际上这个scheduler指的是Graphics Scheduler而非CPU/IO Scheduler。这个Scheduler负责通过划分时间片的方式来对3D管线访问权限进行仲裁。当发生context切换的时候,这里至少需要对GPU进行状态切换(会需要塞入额外的指令到command buffer中),并可能会需要对显存进行资源的置换(resource swapping in/out)。当然,这里需要保证在任何时候都仅有唯一的一个应用可以向3D管线提交指令。
你会发现,主机开发人员经常会吐槽PC 3D API的高度固定化(无法手操)以及因此导致的高性能消耗。不过事实上,PC上的API复杂程度要远远高于主机。由于多应用多任务处理的考虑,PC需要对所有应用的当前状态进行追踪与管理,而这个过程即使是驱动开发者都没眼看,不过谁让用户需要呢。
The kernel-mode driver (KMD)
这个就是直接与硬件打交道的部分了,同一时刻可能会有多个UMD在运行,但是KMD则有且只有一个。如果KMD崩了,那么恭喜你,系统挂了,通常这种情况对应的就是蓝屏问题,不过现在Windows进步了,如果只是驱动崩了,系统会将之kill掉并重载,之后还可以继续愉快的玩耍,不过如果是内存挂了,那么不好意思,没戏了。
KMD处理的任务或者说事情都是独一无二的:比如多个应用都需要争夺的GPU显存,物理内存的分配,GPU的初始化与Display Mode设置,管理硬件鼠标指针(鼠标指针是独一无二的,有一个专门的硬件负责对其进行管理),对硬件watchdog计时器进行程控,从而在GPU无响应的时候对其进行重置,对中断进行响应等等。
KMD还会参与到一些系统保护或DRM相关的一些任务中去,避免一些禁止触发的操作的发生。
对我们而言,KMD最重要的功能是对事实上的command buffer(即真正用于交由硬件读取与使用的,下面用main buffer代替)进行管理,前面介绍的UMD创建的command buffer并不是一个真实的buffer(实际上是一个GPU可访问的内存空间的随机切片)。这个过程是这样的,UMD创建了GPU可以通过地址访问到的Command Buffer,之后会将之提交到Scheduler,Scheduler会等到对应的进程拿到权限后,将之传输给KMD;之后KMD会向main buffer发出一个对command buffer的调用指令,根据GPU command processor是否能拿到主存(系统内存)的访问权限,这里可能还要判定是否需要直接从显存中通过DMA进行获取。main buffer实际上是一个尺寸非常小的ring buffer(可以参考这篇文章中的GPU Resource Management),写入其中的都是对3D command buffer的系统指令以及初始化指令。
说到底Ring buffer也不过是显卡所能访问到的一块内存空间,其中包括了一个读指针,指向的是当前GPU在main buffer中的位置,还有一个写指针,对应的是KMD已经写入的指令的长度,这些都是放在硬件寄存器中的,与内存有着一种映射关系,KMD会定期进行更新(通常是在一批新的任务提交的时候触发)。
The bus
略
The command processor!
Command Processor会从KMD输出的Command Buffer中读取Command,并开始执行,这个组件可以看成是GPU的开端(frontend),因为这篇文章已经够长了,因此这部分内容将放在后面的blog中来讲述。
Small aside: OpenGL
OpenGL的相关实现跟上面所描述的D3D实现部分差不多,在API跟UMD上有着不是特别显著的区别;此外,跟D3D不同,GLSL的编译不是通过API完成,而是完全由驱动完成,因此由于厂商的不同,相同的GLSL最终编译输出的结果可能就会不一样,因此表现可能就会千差万别;此外,这也就意味着shader的优化全都是交由驱动来实现,相对而言D3D的字节码形式的输出结果就显得干净的多——只有一个编译器,因此不同的GPU也会得到相同的输出结果。
Omissions and simplifcations
这里给出一个概述,实际上本文所介绍的内容忽略了很多的细节,比如说scheduler就不止一种实现方式,驱动可以从多种实现方案中选择一种;此外,CPU跟GPU之间的同步实现细节这里也没有介绍等等。
网友评论