Adreno GPU上的DirectX平台优化 (3)

  • 5.3 Adreno GPU 性能建议
  • 5.3.1 编写高效的着色器代码


5.3 Adreno GPU 性能建议

除了上面提供的基于图块的渲染建议之外,在尝试最佳利用 Adreno GPU 时还有其他重要的性能考虑因素。 本节接下来是针对 Adreno GPU 的 Direct3D11.1 应用程序的一系列性能建议。

5.3.1 编写高效的着色器代码

为了充分利用 Adreno 着色器资源,了解架构的某些部分很重要。

有一些关于如何使着色器性能更好的提示。

着色器编译很昂贵
当应用程序使用着色器时,Qualcomm 的优化编译器会在编译期间权衡 CPU 处理时间以获得更快的执行时间。 着色器应仅在非关键时间创建(例如,应用程序创建、加载屏幕、显示简单菜单)。

使用内部函数
应尽可能使用作为 HLSL 语言一部分的内在函数,因为您无需通过编写自己的函数来重新发明轮子,而且它们很有可能针对特定的着色器配置文件进行了优化。 有关支持的所有函数,请参阅 HLSL 内在函数列表:http://msdn.microsoft.com/en-us/library/windows/desktop/ff471376(v=vs.85).aspx

使用适当的数据类型
通过在代码中使用适当的数据类型,驱动程序中的编译器和优化器可以优化代码并配对着色器指令。 例如,使用 float4 数据类型而不是 float 数据类型会阻止编译器以可以在硬件上共同发布的方式排列输出。 小错误有时会对性能产生很大影响。 例如,以下代码应消耗单个指令槽。

int4 ResultOfA(int4 a)
{
return a + 1;
}

鉴于 1.0.0 的原因,以下代码可能会占用 8 个指令槽。

int4 ResultOfA (int4 a)
{
return a + 1.0;
}

变量 a 将转换为 float4,然后以浮点形式进行加法,最后将结果转换回返回类型 int4。

打包标量常量
将标量常量打包到由四个通道组成的向量中,可以从根本上和整体上提高硬件的获取效率。 例如,在动画系统的情况下,打包会增加可用于蒙皮的骨骼数量。 以下代码显示了实现此目的的简单方法:

float scale, bias;
float4 a = Pos * scale + bias

下面的代码可能会少用一条指令,因为编译器可以将该行优化为一条更高效的指令 (mad)。

float2 scaleNbias;
float4 a = Pos * scaleNbias.x + scaleNbias.y;

2x2 像素处理

扫描转换器将每个三角形“粗略扫描”成对齐的 8x8 块,如图 5-6 所示。

directshow 调用gpu_编译器

在此之后,每个瓦片被分解成对齐的 2x2 四边形。 其中一些四边形可能包含“死”像素,如图 5-6 中的红色所示。 尺寸小于 2x2 像素的非常小的三角形可能会浪费至少 60% 的 GPU 处理能力。 考虑使用仅使用更大三角形的几何 LOD。 因此,例如在实现粒子系统时,使用较小的三角形实现它可能会降低效率。

纹理采样
硬件同时处理 2x2 像素这一事实在像素级别上提出了挑战。 这些四边形中的四个或更多被分组到一个像素向量(又名像素线程)中。 优化网格使像素向量很好地组合在一起可以更有效地利用纹理缓存。

避免纹理停顿的其他策略是:

  • 避免随机访问
  • 避免 3D 体积纹理
  • 避免在一个像素着色器中从 7 个纹理中获取; 更合适的数字是 4
  • 到处使用压缩格式:更好的内存使用,更少的停顿
  • 尽可能使用 mipmap

一般来说,三线性和各向异性滤波比双线性滤波要昂贵得多,而点滤波和双线性滤波没有区别。 纹理过滤会影响纹理采样的速度。 在 32 位格式的 2D 纹理中进行双线性纹理查找需要一个周期。 添加三线性过滤会使成本加倍到两个周期。 不过,Mipmap 钳制可能会将其降低为双线性成本,因此在实际情况下平均成本可能会更低。 添加各向异性过滤乘以各向异性程度。 这意味着 16x 各向异性查找可能比常规各向同性查找慢 16 倍。 因为各向异性过滤是自适应的,所以这个命中只对需要各向异性过滤的像素进行,最终可能只有几个像素。 实际情况的一条经验法则是,各向异性过滤的成本平均不到两倍。

不同的纹理格式对纹理采样性能有很大影响。 32 位纹理需要 1 个周期来获取。 所以所有 32 位和更小的格式(包括所有压缩格式)都是单周期的。 64 位格式需要两个周期,128 位格式需要四个周期来获取。

立方体贴图和投影纹理查找不会产生任何额外成本,而特定于着色器的渐变——基于 ddx() / ddy()——需要一个额外的周期。 这意味着通常需要一个周期的常规双线性查找需要两个具有特定于着色器的渐变。 请注意,这些特定于着色器的渐变不能跨查找存储,因此如果您在同一个采样器中再次使用相同的渐变进行纹理查找,则会再次消耗单周期命中。

3D 纹理
使用 3D 纹理会对内存和缓存使用产生严重影响。 有效利用纹理内存已经是实时 3D 内容的一项主要任务。 向纹理贴图添加额外维度会显着增加其内存占用。

  • 保持纹理小(每个方向 < 32 纹素)
  • 在适当的地方重复和镜像。 体积细节纹理可以重复以达到很好的效果。 将镜像地址 (D3D11_TEXTURE_ADDRESS_MIRROR) 模式用于对称纹理,例如体积光照贴图。 镜像一次 (D3D11_TEXTURE_ADDRESS_MIRROR_ONCE) 寻址模式对于 3D 光照贴图特别有用。
  • 保持纹理位深度尽可能低。 体积细节纹理和光照贴图通常是灰度的。 对于这些,使用单通道纹理。
  • 使用压缩。 开发人员应该为他们的应用程序做出适当的大小/质量权衡。

分枝
静态分支表现良好,但存在额外 GPR 的风险。 在着色器中使用动态分支具有特定的、非常量的开销,这取决于确切的着色器代码。 因此,使用动态分支并不总是能赢得性能。 由于多个像素被作为一个处理,如果某些像素采用分支而其他像素不采用分支,则 GPU 必须执行两条路径(所有指令确实执行,但有一个控制输出的屏蔽位,以便只有适当的像素 做作的)。 在这种情况下,性能会比单独处理每个像素更差。 如果所有像素都走相同的路径,则 GPU 能够真正走分支,这对性能有好处。

包着色器插值器
着色器内插值(在顶点和像素着色器之间传递的值)需要 GPR 来保存输入像素着色器的数据。 因此,您应该尽量减少它们的使用。 在值一致的情况下使用常量。 所有插值器都有四个组件,无论您是否使用它们,因此将值打包在一起。 将两个 float2 纹理坐标放入单个 float4 值是一种常见做法,但其他策略采用更具创造性的打包方式。

细节层次着色器
为了提高着色器的效率,类似于几何细节层次,可以实现着色器细节层次。 根据与相机的距离,交换着色器的质量、成本和能源效率。 物体离相机越远,成本和质量就越便宜,同时提高能源效率。

LOD 系统的粒度可以基于每帧或每对象。 LOD 值算法将遵循与相机的距离。 对于照明系统,LOD 级别可以是:

  1. 法线贴图逐像素光照,附加详细贴图
  2. 法线贴图逐像素光照,无详细贴图
  3. 顶点只用颜色纹理点亮

最小化着色器 GPR
最小化 GPR 可能是最重要的性能优化方法。 向编译器输入更简单的着色器有助于保证最佳结果。 有时,修改 HLSL 以保存一条指令甚至可以保存 GPR。 不展开循环也可以保存 GPR,但这取决于着色器编译器。 (相反,展开的循环倾向于天真地将纹理提取集中到着色器的顶部,导致需要更多的 GPR 来同时保存多个纹理坐标和提取的结果。)

避免对着色器常量进行数学运算
自从着色器问世以来,几乎所有已发布的游戏都使用指令对着色器常量执行不必要的数学运算。 在您自己的着色器中识别这些指令并将这些计算移至 CPU。 在编译后的微代码中识别着色器常量的数学可能要容易得多。

避免优步着色器
优步着色器将多个着色器组合成一个使用静态分支的着色器。 如果您试图减少状态更改和批量绘制调用,使用它们很有意义。 然而,这通常以增加 GPR 计数为代价,这会对性能产生影响。

避免在像素着色器中杀死像素(剪辑)
一些开发人员认为,在像素着色器中手动杀死像素(HLSL 中的 clip())可以提高性能。 规则并不简单,原因有二:

  • 如果线程中的某些像素被杀死,而其他像素没有被杀死,着色器仍会执行。
  • 如果使用剪辑,则驱动程序必须禁用 Early-Z。 这样做的原因是因为如果启用 Early-Z 并且像素着色器中的像素被杀死,则该像素的深度缓冲区值将被错误地更新。 因此,在着色器使用 clip() 函数的情况下,驱动程序必须禁用 Early-Z。

建议只在像素着色器中使用 clip() 指令来实现像 alpha 测试这样的效果是绝对必要的。 否则,不要使用 clip() 作为性能优化,因为它可能会导致性能下降。