优化3D图形流水线
在使用NVIDIA PerfHUD 5 Launcher的时候,明显发现现在的CPU时间和GPU时间不均衡,于是考虑优化。 下面是参考NVIDIA的OGP开始总结。
优化代码通常是找出瓶颈,对瓶颈进行优化,这里暂不考虑CPU内部的优化方法,主要记录CPU->GPU的3D渲染流水线的瓶颈查出方法以及优化手段。 若仅希望进行CPU方面的优化,可使用一些辅助工具,如Inter的Intel® VTune™ Performance Analyzer,Intel® Thread Profiler 3.1,AMD的CodeAnalyst等。 进行优化的步骤如上面所说:1:找出瓶颈,2:对其优化。 最通用也最有效的找出瓶颈的方法当然是找到核心函数,降低它的时钟周期和负荷,看是否对程序性能有大的影响。优化的手段多是拆东补西而已,即,将影响性能的瓶颈中的任务分配给其他较空闲的部分进行处理,来平衡整体所消耗的时间。 那么来看一下图形渲染流水线大致过程。 1:系统CPU从内存中读取几何顶点 -> 输送到GPU显存 -> 输送到GPU高速顶点缓冲区 -> GPU顶点着色 -> GPU建立三角型 -> GPU矩阵变换 -> GPU光栅化 -> 3 2:系统CPU从内存中读取纹理信息 -> 输送到GPU显存 -> 输送到GPU高速纹理缓冲区( DX10.0以后可与顶点缓冲共同,不再强制区分 ) -> 3 3:片段着色光栅化 -> 输出GPU后台缓冲进行渲染。 那么,很简单的有几大模块在其中可能存在着瓶颈的限制。
1:CPU本身逻辑计算能力的限制。
2:CPU到GPU显存AGP传输能力的限制 (1)顶点 (2)纹理 3:GPU显存到高速缓冲区的传输带宽限制 (1)纹理传输带宽限制 (显存->高速缓冲区) (2)光栅化完毕后的桢传输带宽限制 (高速缓冲区->显存) 注:这里不考虑 顶点 传输的带宽限制,因为这个限制极小 4:GPU高速缓冲区内部处理能力的限制。 (1)顶点变换着色处理能力限制。 (2)顶点最大数量支持限制。 (3)三角型建立限制。 (4)光栅化限制。 (5)象素着色限制。 5:内存过小限制。 6:显卡显存过小,以及其他硬件Caps限制。
上述就是常见3D图形渲染流水线中的瓶颈限制,那么我们下一步去一一确定,可能是哪方面的瓶颈。简单的方法是检测FPS。 注意1:许多瓶颈可能由于硬件更变而更变。 注意2:Debug模式和Release模式的瓶颈表现未必相同。 注意3:查看FPS时候一定关闭垂直同步。 1:改变色深,16bit,32bit,这个是直接影响 桢渲染缓冲 的大小的,若修改了此项之后,FPS有较大变化,则是由于3.2 桢传输带宽限制。 注:这里需要改变所有渲染对象的色深。 2:改变纹理大小尺寸,改变纹理过滤方式,若修改了此项之后,FPS有较大变化,则是由于3.1 纹理传输带宽的限制 或 2.2 纹理AGP传输能力限制。 注:纹理过滤方式中,点过滤速度 > 线性过滤速度 > 三角面过滤速度 > 各向异性过滤速度 若改变纹理过滤方式就将FPS提高了,则是3.1 纹理传输带宽的限制。这步是将纹理数据从显存运输到GPU高速纹理缓冲区的过程。 3:改变桌面分辨率,若修改了此项之后,FPS有较大变化,则是由于 4.4 光栅化限制 或是 4.5 象素着色Shader限制。 此时减少 PixelShader指令数量,若修改了此项之后,FPS有较大变化,则是由于 4.5 象素着色Shader限制,若没有较大变化,则是由于 4.4 光栅化限制。 4:减少 VertexShader 指令数量,若修改了此项之后,FPS有较大变化,则是由于 4.1 顶点变换着色处理能力限制。 5:减少顶点数量和AGP传输速率,若修改了此项之后,FPS有较大变化,则是由于 4.2 顶点最大数量支持限制 或 2.1 顶点AGP传输能力限制。 6:若以上都不是,则是 1.0 CPU逻辑计算能力限制。 注:该项也可根据NVIDIA PerfHUD来检测CPU和GPU的空闲时间来判定,若GPU空闲时间过多,则说明是由于CPU计算能力或AGP传输能力导致。 该项也可用简单的更换CPU,而不更换GPU的方式来检测判定。 7:看资源管理器,CPU占用率,内存占用率,可以知道是否是由于1.0 CPU本身逻辑计算能力的限制 或是 5.0内存过小限制。 8:看DX SDK自带的CapsViewer可以知道显卡的支持性,以获得更多更准确的判定。 9:在BIOS中更变APGP为1X模式,若修改了此项之后,FPS有较大变化,则是由于2.1 或 2.2 AGP传输能力限制。 10:降低GPU配置进行检测判定,此时要注意两项,一是降低GPU的运行频率,一是降低GPU显存性能和大小,可以确定GPU方面的问题大致所在。 11:删除一些游戏中涉及的 物理,AI,逻辑 等占用大量CPU效率的代码以获得更强的针对性。 12:对角色,地形,静态模型,阴影 等设置渲染开关,以更明确的确定问题所在。
优化方法: 一:整体优化。 1:减少小批量作业 (1)让一个顶点缓冲中更多顶点。(1024点以上较适合) (2)少Draw。(尽量一次性多渲染些三角形,减少渲染次数) (3)尽量将多个尺寸小的纹理文件合并为一个尺寸大的纹理文件,减少零碎的小纹理文件数量。 (4)使用VertexShader将一些关系紧密的几何体打包在一起。(VS2.0就已经存在256个4D向量常数) 2:逻辑排序优化 (1)尽量在逻辑层将顶点进行一定的排序以减少在GPU高速缓冲区中的重新排布。 (2)尽量将渲染对象在逻辑层按照深度由屏幕->内部排序,减少不必要的深度拣选。 (3)尽量使用索引条带或索引列表 (4)根据渲染状态和渲染对象对纹理进行基本排序 3:减少不必要的渲染(CPU层的基本二分四叉八叉这里不再强调) (1)在多Pass渲染时,在第一个渲染Pass上对每个渲染对象加以咨询,当第一个Pass中该渲染对象渲染象素量达不到指定标准,则后续Pass不再对其进行渲染。 (2)对一些重复渲染(如太阳眩光特效)需要进行计数,达到指定数量即停止渲染或进行分布式渲染。 (3)对一些复杂的模型设置基本的包围盒判定其渲染必要性。 4:减少线程锁定导致的不必要等待 (1)CPU Lock了一个资源,等待GPU进行渲染,此时常见做法有等待GPU渲染,中间期间CPU经常处于Idle空闲状态,建议此时给CPU其他的事情做,如为下一个资源做好基本准备或进行逻辑处理。 5:减少或平均分布CPU压力(实际上,大部分程序是CPU逻辑计算限制的) (1)CPU压力重点在以下方面可能存在: AI,IO,网络,复杂逻辑,这些部分可进行CPU瓶颈测试以确定优化方向。 (2)优化方针:宁可GPU忙碌也要CPU减压。 (3)使用文章开始时我提到的一些工具去查找CPU中不必要的汇编空循环以及不必要的CPU空闲。 二:局部优化。 6:AGP传输瓶颈 (1)当过多数据通过AGP8X从CPU内存传递到GPU显存时,我们可以选择以下方式优化。 [1]减小顶点个数 [2]减少动态顶点个数,使用VertexShader动画替代。 [3]正确使用API,设置正确参数,避免动态顶点和纹理缓冲区的创建管理。 [4]根据硬件配置属性确定适合的 桢缓冲,纹理缓冲,静态顶点缓冲 的大小。 (2)避免使用无序或不规则数据传输。 [1]顶点数量尺寸应当是32的整数倍。(可使用顶点压缩,再在VertexShader中对顶点数据进行解压缩) [2]确保顶点的有序性。(在CPU逻辑层对其进行排序后传输,NVTriStrip这个工具可以帮我们生成优化的高效的有序的Mesh顶点数据) (3)具体到API层面的几何Mesh传输 [1]对于静态几何体,创建 只写的顶点缓冲,且,仅写入一次。 [2]对于动态几何体,在程序初始创建一个动态顶点缓冲,之后每桢初始锁定DISCARD,进行NOOVEWRITE而不要进行DISCARD,DISCARD的耗时不是NOOVEWRITE可比的。 [3]基本原则,少创建缓冲区,多对其进行重复使用,减少锁定次数。 7:顶点变换传输处理瓶颈(由于GPU有强大的顶点处理能力,一般在顶点变换方面不会有瓶颈出现,但假若出现了。。) (1)顶点太多 [1]使用细节Lod,一般起用2-3级Lod就足够了。 (2)顶点处理过于复杂 [1]减少灯光数量,降低灯光复杂度(方向平行光效率 > 点光源效率 > 聚光灯效率 ) [2]减少顶点着色器指令数量,避免128条以上指令,避免大量的分支指令 [3]对顶点进行CPU层逻辑排序 [4]能在CPU中进行计算的在CPU中进行计算,传递常量给GPU [5]减少和避免CG/HLSL之中的 mov 指令。即使使用了,也要重点注意。 8:大部分情况下 4.3 三角形建立限制 以及 4.4 光栅化限制 是不会成为瓶颈的,但,当三角形数量过多或者光栅化时每个三角形顶点数据过于复杂时可能会出现这种瓶颈,此时减少三角形总数,使用VS或减少Z-cull三角都是有效的方法。 9:象素着色器的瓶颈(在DX7之前,全是固定渲染管道,一般来说传输量和着色器之间的计算是均衡的,但是DX8开始可编程流水管道开始,PixelShader的计算量开始增幅,数据传输量通常相对来说比较小了。) (1)需处理的纹理片段过多过大 [1]在CPU层按照 屏幕->向内 Z-Buffer的顺序排序传入,并按照这个顺序进行渲染。 [2]多Pass渲染时,考虑在第一个渲染Pass中关闭特效并让第一个Pass负责Z-buffer的处理。这样的话,后续Pass中可以避免渲染不要的纹理片段。 (2)每个纹理片段的处理过于复杂 [1]大段的长着色器指令将会很大降低效率,尝试减少着色器指令长度 [2]使用向量操作,并行co-issuing来减少指令数量。 [3]混合使用配对的简单的texture和combiner组合指令。 [4]使用Alpha混合器提高性能。 [5]考虑对阴影也进行Lod计算。 [6]在DX10开始,考虑将顶点缓冲移做象素缓冲进行使用。 (3)额外的优化方法 [1]使用fx_12精度 [2]使用fp16指令 [3]使用Pixel_Shader2.0的时候开启ps_2_a描述开关 [4]减少寄存器的临时存取 [5]减少不必要的精度要求 [6]尽量使用低版本的Shader(但避免使用VS1.0,已经被VS3.0抛弃了) 10:纹理贴图导致的瓶颈 (1)优化方法。 [1]纹理过滤时避免使用 三角面性过滤 和 各相异性过滤,特殊需求除外,一般线性过滤已经可以做的很好。 [2]即使使用各相异性过滤,也要降低相异性比率。使用了各相异性过滤的话,则可以尽量减少三角面性过滤。 [3]降低纹理分辨率,避免使用不必要的高分辨率纹理。 [4]降低纹理色深,例如环境纹理,阴影纹理这些,尽量使用16位。 [5]建议进行纹理压缩,例如DXT格式就可以有效压缩纹理,并且GPU对DXT格式支持很好。 [6]避免使用非二次方的纹理资源。 [7]在进行纹理锐化的时候,避免使用负值的Lod进行锐化,会导致远处失真,尽量使用各相异性过滤进行锐化 [8]对于动态纹理,一般建议用 D3DUSAGE_DYNAMIC D3DPOOL_DEAFAULT 进行创建缓冲,使用 D3DLOCK_DISCARD 进行锁定,尽量做到一次锁定多次使用,不要频繁解锁,另外,永远不要读这样的纹理。 11:桢缓冲导致的瓶颈 (1)优化方法 [1]尽量关闭Z-write,一般来说,在一个渲染Pass中就可以进行完整的Z-buffer处理,在后续的Pass中就应当关闭Z-write,不用担心,即使需要Alpha混合的对象也不再需要开启Z-write了。 [2]尽量开始AlphaTest,实际上这个操作会提高效率,而非降低。 [3]避免使用浮点桢缓存。 [4]若没有启用模版深度缓冲的话,使用16位的Zbuffer就可以了。 [5]避免使用RendToTexture,或者可能的去减少Rend的尺寸。 对于现在可编程流水管线来说,这意味着我们有更大的自由度实现更多的特效,但也有了更多的瓶颈和更多的复杂度,我们遇到问题要正确的获取瓶颈所在,开动脑筋进行优化,平衡各环节间的负载。让各环节不过载不空闲。
更多信息希望您查看Nvidia的《GPU_Programming_Guide》,翻译成中文则是《GPU编程精粹》。以上。