【GDC2003】Optimizing the Graphics Pipeline

坚持学习与自我革新不动摇!

本文是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写入的带宽存在瓶颈。


优化建议


  1. 尽可能的避免小批次提交,也就是说,一次性提交十万面片,比十次提交一万面片性能要高
  • 降低CPU浪费
  • 合批
  1. 减少不必要的AGP消耗
  • 尽量使用indexbuffer参与的绘制方式,避免过多的顶点数据传输
  • 调整参与渲染的顶点数据的顺序,提高GPU cache的命中率
  1. 对物件按照离相机先后顺序进行排序,通过硬件自带的Early-Z降低需要处理的数据量

  2. 对需要渲染的物件按照渲染状态进行合批,渲染状态即材质包括:

  • RenderStates,如BlendMode,FillMode,StencilOps等
  • shader
  • shader中使用到的参数,如宏,texture以及其他参数
  1. 使用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瓶颈


  1. 这种瓶颈基本上不太可能出现

  2. 受限因素

  • 三角面片数目
  • 需要插值的顶点属性(一般是有最大值限制的)

3.解决方案

  • 降低需要插值的顶点属性数目
  • 减少退化面片的数目(当退化面片与有效面片的数目比值超过1的时候,这种处理方法才能起到效果)

光栅化阶段瓶颈 - Raster Bottleneck


受限因素:

  • 光栅化的面片的面积越大,速度越慢
  • 光栅化的面片的数量越多,速度越慢

PS瓶颈


在固定管线时代,Fragment Shader的设计是能够与其他的Stage匹配得非常良好,之后到了Nvidia 1x时代,虽然Fragment Shader性能因为增加了许多其他的功能而有所下降,但是也不会是瓶颈,Nvdia 3x提高了Fragment Shader的复杂程度,使得DX9的最大PS指令数为512,OpenGL3.0指令数为1024,从而使得复杂的Fragment Shader成为了瓶颈

  1. 具体原因:
  • 像素数目过多
  • 使用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的复杂度,也不会有太多额外的代价,但是却可以得到更好的效果。

参考

[1] Optimizing the Graphics Pipeline

你可能感兴趣的:(【GDC2003】Optimizing the Graphics Pipeline)