【GDC2003】Optimizing the Graphics
坚持学习与自我革新不动摇!
本文是GDC2003上NVidia关于图形管线优化相关的分享文章的学习笔记,原文链接在文末给出。
AGP:Accelerated Graphics Port (AGP),这是一个高速点对点数据传输channel,用于实现CPU到显卡的数据传输,以实现3D图形渲染的加速,这个东西最开始是作为PCI类连接器的接替者而开发设计的。
“transform bound” 意味着瓶颈出现在光栅化阶段之前
“fill bound” 则通常意味着瓶颈发生在setup阶段之后。
瓶颈的定位可以通过调整各个stage的workload,之后测试性能来得到。
性能优化在于消除瓶颈,做法是降低瓶颈Stage的workload,或者增加其他非瓶颈Stage的workload。
定位瓶颈步骤
1.修改RT或者DS Buffer的格式,比如说从16bit改到32bit,看看帧率是否有变化,如果帧率变化了,那么就说明当前是Framebuffer写入的带宽上存在瓶颈。
2.如果没有变化,再修改输入采样贴图的分辨率,比如将mipmap强制改到10+或者将点采样改为线性滤波采样,如果改完之后,分辨率有变化,说明当前是贴图读取的带宽存在瓶颈(也就是说,写入跟读取的带宽需要分开考虑)。
3.如果没变化,再修改RT的分辨率,如果帧率发生变化了,再继续修改pixel shader的复杂度,如果帧率变化了,那就说明当前是PS过于复杂了,否则就说明是光栅化阶段存在瓶颈。
4.如果修改RT分辨率没有导致帧率变化,就修改VS的复杂度,如果帧率变化了,就说明是VS存在瓶颈。
5.否则就修改顶点格式,如果帧率变化,就说明当前是AGP上存在瓶颈。
6.如果上述都没有变化,就说明是CPU阶段存在瓶颈。
这里给出一些测试上的小tips
1.在不同的CPU型号跟同一型号的GPU的机器上,如果帧率存在差异,那么就说明是CPU上存在瓶颈。
2.在bios强制设置AGP = 1x,如果帧率发生变化,那就说明AGP上存在瓶颈。
3.如果对GPU进行降频(underlock),即降低core的时钟频率,会导致性能下降(帧率下降)的话,那就说明可能是顶点变换、光栅化阶段或者PS上存在瓶颈;而如果降低memory(内存还是显存?从后文来看,倾向于显存)的时钟频率导致了性能下降的话,那就说明可能是贴图读取的带宽或者Framebuffer写入的带宽存在瓶颈。
优化建议
- 尽可能的避免小批次提交,也就是说,一次性提交十万面片,比十次提交一万面片性能要高
- 降低CPU浪费
- 合批
- 减少不必要的AGP消耗
- 尽量使用indexbuffer参与的绘制方式,避免过多的顶点数据传输
- 调整参与渲染的顶点数据的顺序,提高GPU cache的命中率
-
对物件按照离相机先后顺序进行排序,通过硬件自带的Early-Z降低需要处理的数据量
-
对需要渲染的物件按照渲染状态进行合批,渲染状态即材质包括:
- RenderStates,如BlendMode,FillMode,StencilOps等
- shader
- shader中使用到的参数,如宏,texture以及其他参数
- 使用Occlusion Query减少VS/PS/FS的数据处理量
- 多轮Render
- pass1,查询object有多少像素被绘制
- pass2,剔除掉需要绘制的像素数目比较少的物件
- 优化在于,pass1可以放在上一帧做,不需要进行复杂的shader计算,这一帧直接查询结果,并开始pass2
- 可以用作粗浅的visibility检测
- 对于需要使用lens flare特效的app,可以使用包含太阳的一个四边形进行Occlusion Query,并判断有多少像素可见,用以调整lens flares参数
- 在刚才提到的pass1中,可以直接使用物件的bb进行粗浅的可见性判断
6.避免资源Lock,如CPU读取贴图内容,如果此时这张贴图正被GPU访问,会导致CPU此时处于空闲状态,敲着二郎腿等待着GPU返回结果
CPU瓶颈
1.可能的原因:
-
应用本身导致的CPU受限
-
游戏逻辑
-
游戏AI
-
网络
-
文件IO
-
使用CPU做了除简单裁剪以及排序之外的其他图形学工作
-
驱动问题导致的CPU受限
-
频繁提交小数目图元批次
-
错误的使用API
大多数的图形应用程序都是CPU受限的
2.解决方法:
- 使用CPU Profiler定位问题
- 通过各种方式增加批次内图元数目
- 避免各种通过CPU降低GPU负载的“优化”,尽量让图形的工作归于图形(GPU)
AGP数据传输瓶颈
1.对于AGP 4x来说就不太可能会存在这类的bottleneck,更何况对于当代的AGP 8x?
2.原因主要在于传输了过多的数据:
- 无意义的数据,在GPU中不参与任何计算的,或者参与计算得到的结果对于表现无影响的,或者使用过高精度的数据,如只需要16位即可,却给了32位
- 过多的动态顶点数据:静态顶点数据是一直存在于GPU Mem中,不需要每帧传输?这就是为什么尽量使用GPU Skin而不要使用CPU Skin的原因之一了?
- 动态数据渲染时候调用了错误的API,比如之前所说的尽量使用indexbuffer参与的渲染API?
- video memory过载,使用了过大的framebuffer等导致video memory不够,从而之前传输过的数据,因为年久失修而被排挤出去,导致频繁的数据传输
3.AGP传输的数据格式也会导致AGP受限:
- 顶点数据需要与32字节对齐?
- 为了满足这一条件,可以对顶点数据进行压缩,使之对齐32字节,之后在vs中解压
- 渲染时候顶点流数据无序性太严重,导致Pre-TnL Cache预处理的顶点数据结果完全浪费,渲染结构尽量保证顶点的处理顺序与顶点buffer中的顺序保持一致,比如使用TriangleStrip等
4.优化手段:
-
对于静态物件,创建一个静态只写vertexbuffer,只在初始化的时候写入一次,避免多次传输
-
对于动态物件,创建一个动态vertexbuffer,在刚创建完vb向其中填入数据的时候,使用DISCARD标志;之后使用NOOVERWRITE标志写入,直到Buffer达到最大值,再重新分配Buffer,循环往复
-
DISCARD标志,使用此标志从CPU向GPU传输数据,实际上此时传输的数据是写入到一个新的Buffer中的,而在传输的过程中,GPU还可以使用原来Buffer中之前的数据进行绘制,当传输完成,调用了UnLock之后,原来Buffer中的数据被释放,GPU就使用新的Buffer数据进行渲染
-
NOOVERWRITE标志,使用此标志从CPU向GPU传输数据,通常是采用追加到buffer的形式(此部分数据并未参与到当前的DrawCall中),不会创建新的Buffer,在传输的过程中GPU还是使用之前buffer中的数据进行绘制
-
如果不使用任何标志,如果Lock了GPU正在使用的数据,CPU会等待GPU使用完了之后返回结果才能写入,所以,尽量避免不用任何标志的Lock操作
-
对于半动态物件,将静态部分从动态部分中分离出来,采取上述两种策略的综合->硬件合批
Vertex Transform瓶颈
1.这种瓶颈一般比较少见,除非:
- 每帧需要绘制超过一百万个面片
- 或者超出了vertex shader的最大指令数目,如单个vs指令数目超出128条
2.但是如果出现了这种瓶颈
- 那么就是因为顶点数太多了:
- 降低顶点数:物件LOD,地形LOD等
- 一般这种情况确实比较少,所以实际上在CPU中不需要做过度的LOD,一般2~3级静态LOD就完全足够
- 或者顶点shader太复杂了:
- 使用了过于复杂的light模型:复杂度排序
- 方向光模型<点光模型<聚光灯模型
- 使用了TexGen函数(用作在shader中为顶点数据自动生成对应模式的uv坐标)或者对顶点uv的进行变换的矩阵不是单位矩阵
- 分支与循环过多
- Post-TnL Cache使用不当,与Pre-TnL Cache一样,为了提高命中率,最好保持顶点数据的顺序与处理顺序一致
3.解决方案:
- 调整顶点顺序,增加Cache命中率,降低顶点数据传输量
- 将顶点计算尽量精简为每个物件计算一次
- 降低顶点shader复杂度
- 考虑使用Shader LOD
- 如果瓶颈不在FS,可以考虑将计算移动到FS完成
Triangle Setup瓶颈
-
这种瓶颈基本上不太可能出现
-
受限因素
- 三角面片数目
- 需要插值的顶点属性(一般是有最大值限制的)
3.解决方案
- 降低需要插值的顶点属性数目
- 减少退化面片的数目(当退化面片与有效面片的数目比值超过1的时候,这种处理方法才能起到效果)
光栅化阶段瓶颈 - Raster Bottleneck
受限因素:
- 光栅化的面片的面积越大,速度越慢
- 光栅化的面片的数量越多,速度越慢
PS瓶颈
在固定管线时代,Fragment Shader的设计是能够与其他的Stage匹配得非常良好,之后到了Nvidia 1x时代,虽然Fragment Shader性能因为增加了许多其他的功能而有所下降,但是也不会是瓶颈,Nvdia 3x提高了Fragment Shader的复杂程度,使得DX9的最大PS指令数为512,OpenGL3.0指令数为1024,从而使得复杂的Fragment Shader成为了瓶颈
- 具体原因:
- 像素数目过多
- 使用Early-Z进行剔除处理
- 考虑先做一轮Z-Render,之后再做正常Render:增加了Vertex Shader throughoutput,如果PS不是超级复杂,不要考虑这个方法,另外,这个方法也可以缓解一下Framebuffer的带宽压力
- PS太复杂
- 在采贴图的时候,最好搭配一条组合指令(combiner,,由于是并行处理的,所以这条指令相当于是白送的?
- combiner instructions & texture instructions都应该是奇数的(这是什么神奇的规则?)
- 使用硬件的alphaBlend完成一些神奇的功能:
- 点乘SRCCOLOR*SRCALPHA
- 平方SRCCOLOR*SRCCOLOR
- 使用Shader LOD
- 移动合适的操作到VS
- 尽可能的使用lowp
其他可能的GPU瓶颈原因
1.贴图尺寸过大
- 会导致Texture cache miss
- 需要注意避免dependent texture read
- 在采样的时候规避使用negative LOD bias来锐化采样效果, Texture cache是针对标准的非负LOD Bias设计的
- 可以考虑使用 Anistropic Filtering来替代负LOD Bias的锐化效果
- 注意规避non-power of 2的贴图尺寸,这种也可能会导致贴图缓存性能下降。
- 会导致AGP负担重
优化策略
- 降低贴图尺寸
- 调整贴图格式,比如从32位调整为16位
1.1 Shader中贴图采样次数过多
- 慎用三线性采样,因为需要较多的采样次数,会导致fillrate降低一半
- 如果再开启了anistropic filtering性能会更差
- 只在需要的时候开启各向异性采样,同时在开启的时候也要注意尽量缩减maximum ratio of anistropy
1.2 如何加速贴图上传
- 尽量使用Managed资源而非采用自己的一套scheme?
- 对于动态贴图(临时资源吗?):
- 用d3dusage_dynamic 或者d3dpool_default模式进行创建
- 使用d3d_discard进行lock
- 最好不要对这类动态贴图进行读取
1.3 天空盒大概率会导致贴图读取的瓶颈
2.FrameBuffer问题
- read/write操作过多
- 将Z writes关闭有助于减轻问题
- Only Shut some Channel Off will slower the performance by reading mask first
- 浮点格式的framebuffer需要更多的带宽
- 如果不需要Stencil的话可以考虑使用16bit的Depth
- Cubemap以及Shadow Map的分辨率调低一点,深度采用16bit可能渲染效果也不会太差
- 对于反射需要的话,可以考虑将cubemap用半球map来替代,这样贴图尺寸会更小,且在渲染的时候只需要较少的RT切换。
- 对RT进行重用来避免内存问题
- Z-Cull针对Z-bias、alpha test关闭,并且stencil buffer没有使用的情况做过特殊优化的
- 可以考虑使用dx9的constant color blend功能来实现全屏的染色效果,这样比后处理实现的方式更为节省。
另外需要注意的是,如果PS复杂度较高,那么此时,即使增加了VS的复杂度,也不会有太多额外的代价,但是却可以得到更好的效果。