【问题标题】:Optimizing performance of a heavy fragment shader优化重片段着色器的性能
【发布时间】:2013-05-06 02:40:58
【问题描述】:

我需要帮助优化以下着色器集:

顶点:

    precision mediump float;

uniform vec2 rubyTextureSize;

attribute vec4 vPosition;
attribute vec2 a_TexCoordinate;

varying vec2 tc;

void main() {
    gl_Position = vPosition;

    tc = a_TexCoordinate;
}

片段:

precision mediump float;

/*
 Uniforms
 - rubyTexture: texture sampler
 - rubyTextureSize: size of the texture before rendering
 */

uniform sampler2D rubyTexture;
uniform vec2 rubyTextureSize;
uniform vec2 rubyTextureFract;

/*
 Varying attributes
 - tc: coordinate of the texel being processed
 - xyp_[]_[]_[]: a packed coordinate for 3 areas within the texture
 */

varying vec2 tc;

/*
 Constants
 */
/*
 Inequation coefficients for interpolation
 Equations are in the form: Ay + Bx = C
 45, 30, and 60 denote the angle from x each line the cooeficient variable set builds
 */
const vec4 Ai = vec4(1.0, -1.0, -1.0, 1.0);
const vec4 B45 = vec4(1.0, 1.0, -1.0, -1.0);
const vec4 C45 = vec4(1.5, 0.5, -0.5, 0.5);
const vec4 B30 = vec4(0.5, 2.0, -0.5, -2.0);
const vec4 C30 = vec4(1.0, 1.0, -0.5, 0.0);
const vec4 B60 = vec4(2.0, 0.5, -2.0, -0.5);
const vec4 C60 = vec4(2.0, 0.0, -1.0, 0.5);

const vec4 M45 = vec4(0.4, 0.4, 0.4, 0.4);
const vec4 M30 = vec4(0.2, 0.4, 0.2, 0.4);
const vec4 M60 = M30.yxwz;
const vec4 Mshift = vec4(0.2);

// Coefficient for weighted edge detection
const float coef = 2.0;
// Threshold for if luminance values are "equal"
const vec4 threshold = vec4(0.32);

// Conversion from RGB to Luminance (from GIMP)
const vec3 lum = vec3(0.21, 0.72, 0.07);

// Performs same logic operation as && for vectors
bvec4 _and_(bvec4 A, bvec4 B) {
    return bvec4(A.x && B.x, A.y && B.y, A.z && B.z, A.w && B.w);
}

// Performs same logic operation as || for vectors
bvec4 _or_(bvec4 A, bvec4 B) {
    return bvec4(A.x || B.x, A.y || B.y, A.z || B.z, A.w || B.w);
}

// Converts 4 3-color vectors into 1 4-value luminance vector
vec4 lum_to(vec3 v0, vec3 v1, vec3 v2, vec3 v3) {
    //    return vec4(dot(lum, v0), dot(lum, v1), dot(lum, v2), dot(lum, v3));

    return mat4(v0.x, v1.x, v2.x, v3.x, v0.y, v1.y, v2.y, v3.y, v0.z, v1.z,
            v2.z, v3.z, 0.0, 0.0, 0.0, 0.0) * vec4(lum, 0.0);
}

// Gets the difference between 2 4-value luminance vectors
vec4 lum_df(vec4 A, vec4 B) {
    return abs(A - B);
}

// Determines if 2 4-value luminance vectors are "equal" based on threshold
bvec4 lum_eq(vec4 A, vec4 B) {
    return lessThan(lum_df(A, B), threshold);
}

vec4 lum_wd(vec4 a, vec4 b, vec4 c, vec4 d, vec4 e, vec4 f, vec4 g, vec4 h) {
    return lum_df(a, b) + lum_df(a, c) + lum_df(d, e) + lum_df(d, f)
            + 4.0 * lum_df(g, h);
}

// Gets the difference between 2 3-value rgb colors
float c_df(vec3 c1, vec3 c2) {
    vec3 df = abs(c1 - c2);
    return df.r + df.g + df.b;
}

void main() {

    /*
     Mask for algorhithm
     +-----+-----+-----+-----+-----+
     |     |  1  |  2  |  3  |     |
     +-----+-----+-----+-----+-----+
     |  5  |  6  |  7  |  8  |  9  |
     +-----+-----+-----+-----+-----+
     | 10  | 11  | 12  | 13  | 14  |
     +-----+-----+-----+-----+-----+
     | 15  | 16  | 17  | 18  | 19  |
     +-----+-----+-----+-----+-----+
     |     | 21  | 22  | 23  |     |
     +-----+-----+-----+-----+-----+
     */

    float x = rubyTextureFract.x;
    float y = rubyTextureFract.y;

    vec4 xyp_1_2_3 = tc.xxxy + vec4(-x, 0.0, x, -2.0 * y);
    vec4 xyp_6_7_8 = tc.xxxy + vec4(-x, 0.0, x, -y);
    vec4 xyp_11_12_13 = tc.xxxy + vec4(-x, 0.0, x, 0.0);
    vec4 xyp_16_17_18 = tc.xxxy + vec4(-x, 0.0, x, y);
    vec4 xyp_21_22_23 = tc.xxxy + vec4(-x, 0.0, x, 2.0 * y);
    vec4 xyp_5_10_15 = tc.xyyy + vec4(-2.0 * x, -y, 0.0, y);
    vec4 xyp_9_14_9 = tc.xyyy + vec4(2.0 * x, -y, 0.0, y);

    // Get mask values by performing texture lookup with the uniform sampler
    vec3 P1 = texture2D(rubyTexture, xyp_1_2_3.xw).rgb;
    vec3 P2 = texture2D(rubyTexture, xyp_1_2_3.yw).rgb;
    vec3 P3 = texture2D(rubyTexture, xyp_1_2_3.zw).rgb;

    vec3 P6 = texture2D(rubyTexture, xyp_6_7_8.xw).rgb;
    vec3 P7 = texture2D(rubyTexture, xyp_6_7_8.yw).rgb;
    vec3 P8 = texture2D(rubyTexture, xyp_6_7_8.zw).rgb;

    vec3 P11 = texture2D(rubyTexture, xyp_11_12_13.xw).rgb;
    vec3 P12 = texture2D(rubyTexture, xyp_11_12_13.yw).rgb;
    vec3 P13 = texture2D(rubyTexture, xyp_11_12_13.zw).rgb;

    vec3 P16 = texture2D(rubyTexture, xyp_16_17_18.xw).rgb;
    vec3 P17 = texture2D(rubyTexture, xyp_16_17_18.yw).rgb;
    vec3 P18 = texture2D(rubyTexture, xyp_16_17_18.zw).rgb;

    vec3 P21 = texture2D(rubyTexture, xyp_21_22_23.xw).rgb;
    vec3 P22 = texture2D(rubyTexture, xyp_21_22_23.yw).rgb;
    vec3 P23 = texture2D(rubyTexture, xyp_21_22_23.zw).rgb;

    vec3 P5 = texture2D(rubyTexture, xyp_5_10_15.xy).rgb;
    vec3 P10 = texture2D(rubyTexture, xyp_5_10_15.xz).rgb;
    vec3 P15 = texture2D(rubyTexture, xyp_5_10_15.xw).rgb;

    vec3 P9 = texture2D(rubyTexture, xyp_9_14_9.xy).rgb;
    vec3 P14 = texture2D(rubyTexture, xyp_9_14_9.xz).rgb;
    vec3 P19 = texture2D(rubyTexture, xyp_9_14_9.xw).rgb;

    // Store luminance values of each point in groups of 4
    // so that we may operate on all four corners at once
    vec4 p7 = lum_to(P7, P11, P17, P13);
    vec4 p8 = lum_to(P8, P6, P16, P18);
    vec4 p11 = p7.yzwx; // P11, P17, P13, P7
    vec4 p12 = lum_to(P12, P12, P12, P12);
    vec4 p13 = p7.wxyz; // P13, P7,  P11, P17
    vec4 p14 = lum_to(P14, P2, P10, P22);
    vec4 p16 = p8.zwxy; // P16, P18, P8,  P6
    vec4 p17 = p7.zwxy; // P17, P13, P7,  P11
    vec4 p18 = p8.wxyz; // P18, P8,  P6,  P16
    vec4 p19 = lum_to(P19, P3, P5, P21);
    vec4 p22 = p14.wxyz; // P22, P14, P2,  P10
    vec4 p23 = lum_to(P23, P9, P1, P15);

    // Scale current texel coordinate to [0..1]
    vec2 fp = fract(tc * rubyTextureSize);

    // Determine amount of "smoothing" or mixing that could be done on texel corners
    vec4 AiMulFpy = Ai * fp.y;
    vec4 B45MulFpx = B45 * fp.x;
    vec4 ma45 = smoothstep(C45 - M45, C45 + M45, AiMulFpy + B45MulFpx);
    vec4 ma30 = smoothstep(C30 - M30, C30 + M30, AiMulFpy + B30 * fp.x);
    vec4 ma60 = smoothstep(C60 - M60, C60 + M60, AiMulFpy + B60 * fp.x);
    vec4 marn = smoothstep(C45 - M45 + Mshift, C45 + M45 + Mshift,
            AiMulFpy + B45MulFpx);

    // Perform edge weight calculations
    vec4 e45 = lum_wd(p12, p8, p16, p18, p22, p14, p17, p13);
    vec4 econt = lum_wd(p17, p11, p23, p13, p7, p19, p12, p18);
    vec4 e30 = lum_df(p13, p16);
    vec4 e60 = lum_df(p8, p17);

    // Calculate rule results for interpolation
    bvec4 r45_1 = _and_(notEqual(p12, p13), notEqual(p12, p17));
    bvec4 r45_2 = _and_(not (lum_eq(p13, p7)), not (lum_eq(p13, p8)));
    bvec4 r45_3 = _and_(not (lum_eq(p17, p11)), not (lum_eq(p17, p16)));
    bvec4 r45_4_1 = _and_(not (lum_eq(p13, p14)), not (lum_eq(p13, p19)));
    bvec4 r45_4_2 = _and_(not (lum_eq(p17, p22)), not (lum_eq(p17, p23)));
    bvec4 r45_4 = _and_(lum_eq(p12, p18), _or_(r45_4_1, r45_4_2));
    bvec4 r45_5 = _or_(lum_eq(p12, p16), lum_eq(p12, p8));
    bvec4 r45 = _and_(r45_1, _or_(_or_(_or_(r45_2, r45_3), r45_4), r45_5));
    bvec4 r30 = _and_(notEqual(p12, p16), notEqual(p11, p16));
    bvec4 r60 = _and_(notEqual(p12, p8), notEqual(p7, p8));

    // Combine rules with edge weights
    bvec4 edr45 = _and_(lessThan(e45, econt), r45);
    bvec4 edrrn = lessThanEqual(e45, econt);
    bvec4 edr30 = _and_(lessThanEqual(coef * e30, e60), r30);
    bvec4 edr60 = _and_(lessThanEqual(coef * e60, e30), r60);

    // Finalize interpolation rules and cast to float (0.0 for false, 1.0 for true)
    vec4 final45 = vec4(_and_(_and_(not (edr30), not (edr60)), edr45));
    vec4 final30 = vec4(_and_(_and_(edr45, not (edr60)), edr30));
    vec4 final60 = vec4(_and_(_and_(edr45, not (edr30)), edr60));
    vec4 final36 = vec4(_and_(_and_(edr60, edr30), edr45));
    vec4 finalrn = vec4(_and_(not (edr45), edrrn));

    // Determine the color to mix with for each corner
    vec4 px = step(lum_df(p12, p17), lum_df(p12, p13));

    // Determine the mix amounts by combining the final rule result and corresponding
    // mix amount for the rule in each corner
    vec4 mac = final36 * max(ma30, ma60) + final30 * ma30 + final60 * ma60
            + final45 * ma45 + finalrn * marn;

    /*
     Calculate the resulting color by traversing clockwise and counter-clockwise around
     the corners of the texel

     Finally choose the result that has the largest difference from the texel's original
     color
     */
    vec3 res1 = P12;
    res1 = mix(res1, mix(P13, P17, px.x), mac.x);
    res1 = mix(res1, mix(P7, P13, px.y), mac.y);
    res1 = mix(res1, mix(P11, P7, px.z), mac.z);
    res1 = mix(res1, mix(P17, P11, px.w), mac.w);

    vec3 res2 = P12;
    res2 = mix(res2, mix(P17, P11, px.w), mac.w);
    res2 = mix(res2, mix(P11, P7, px.z), mac.z);
    res2 = mix(res2, mix(P7, P13, px.y), mac.y);
    res2 = mix(res2, mix(P13, P17, px.x), mac.x);

    gl_FragColor = vec4(mix(res1, res2, step(c_df(P12, res1), c_df(P12, res2))),
            1.0);
}

着色器接收 2D 纹理,旨在将其在高分辨率 2D 表面(设备屏幕)上完美缩放。 这是对 SABR 缩放算法的优化,以防万一。

它已经可以工作,并且在非常高端的设备(如 LG Nexus 4)上运行良好,但在性能较弱的设备上确实很慢。

对我来说真正重要的设备是配备 Mali 400MP GPU 的三星 Galaxy S 2 \ 3 - 使用此着色器时性能非常糟糕。

到目前为止我已经尝试过:

  1. 消除变化(来自 ARM 的 Mali 指南的建议)- 做了微小的改进。
  2. 用我自己的覆盖 mix() 函数 - 没有用。
  3. 将浮点精度降低到 lowp - 没有改变任何东西。

我通过计算渲染时间(eglSwapBuffers 之前和之后)来衡量性能 - 这给了我一个非常线性和一致的性能衡量标准。

除此之外,我真的不知道在哪里看或这里可以优化什么......

我知道这是一个繁重的算法,我并不是在寻求关于使用哪些替代缩放方法的建议 - 我已经尝试了很多,这个算法给出了最好的视觉效果。我希望以优化的方式使用完全相同的算法。

更新

  1. 我发现,如果我使用常量向量而不是依赖向量进行所有纹理提取,我会获得重大的性能提升,所以这显然是一个很大的瓶颈 - 可能是因为缓存。 但是,我仍然需要进行这些提取。我玩了至少一些使用 vec2 变化的提取(没有任何混合),但它并没有改善任何东西。我想知道什么是有效轮询 21 个纹素的好方法。

  2. 我发现计算的主要部分是使用完全相同的一组纹素进行多次 - 因为输出至少按 x2 缩放,并且我使用 GL_NEAREST 进行轮询。至少有 4 个片段落在完全相同的纹素上。如果在高分辨率设备上缩放为 x4,则有 16 个片段落在相同的纹素上——这是一种很大的浪费。 有没有办法执行额外的着色器通道来计算在多个片段之间不会改变的所有值?我考虑过渲染到一个额外的屏幕外纹理,但我需要为每个纹素存储多个值,而不仅仅是一个。

更新

  1. 尝试使用已知的布尔规则简化布尔表达式 - 节省了我一些操作,但对性能没有任何影响。

更新

  1. 想到了一种将计算传递给顶点着色器的方法——只需要一个“几何”来创建我的全屏,但在缩放之前有很多顶点对应于每个原始像素。例如,如果我的原始纹理是 320x200,而我的目标屏幕是 1280x800,则将有 320x200 个顶点均匀分布。然后,在这些顶点中进行大部分计算。问题是 - 我的目标设备 (S2 \ S3) 不支持顶点纹理采样。

更新

  1. 在 LG Nexus 4 与三星 Galaxy S3 上测量的性能表明 Nexus 4 的运行速度快了 10 倍以上。怎么会这样?这些是同一代、相同分辨率等的 2 台设备...... Mali 400MP 在某些情况下真的很糟糕吗?我敢肯定,与 Nexus 4 相比,有一些非常具体的东西使它运行得如此缓慢(但还没有找到)。

【问题讨论】:

  • 我现在没有时间阅读整个程序,但你认为你可以做几遍并在遍之间混合吗?我不认为它可以加速任何东西,但至少它可以释放一些着色器核心......删除变化的问题是更难预测纹理查找(尽管它让你加快了速度)
  • 你认为你可以分离你的过滤器吗?看起来您正在那里进行卷积,如果内核可以在垂直和水平通道中分离,您不需要进行 21 次查找,而是 10 次。
  • 嗨,Trax,您能否详细说明如何分离过滤器?您将如何通过 10 次查找对所需的 21 个纹素进行采样?
  • 关于几次pass——我想过(在更新中),但是在最终颜色输出之前有多个中间值——这里怎么能做多次pass呢?
  • 使用可分离过滤器我的意思是,如果您可以在首先计算水平邻居->输出中将其打破并将其用作垂直通道的输入。但正如您所指出的,有许多相互依赖的产品。

标签: android opengl-es opengl-es-2.0 shader mali


【解决方案1】:

根据我的经验,移动 GPU 性能与texture2D 调用的数量大致成正比。你有 21 个,这确实很多。通常,内存查找比计算慢数百倍,因此您可以进行大量计算,但仍然会在纹理查找上遇到瓶颈。 (这也意味着优化其余代码可能效果不大,因为这意味着它在等待纹理查找时不会很忙,而是在等待纹理查找时处于空闲状态。)所以你需要减少您调用的纹理 2D 数量。

很难说如何做到这一点,因为我不太了解您的着色器,但有一些想法:

  • 将其分成水平通道,然后是垂直通道。这仅适用于某些着色器,例如模糊,但它可以严重减少纹理查找的次数。例如,一个 5x5 的高斯模糊会天真地进行 25 次纹理查找;如果水平完成然后垂直完成,它只使用 10。
  • 使用线性过滤来“作弊”。如果您在启用线性过滤的情况下准确地在 4 个像素之间而不是 1 个像素的中间进行采样,您将免费获得所有 4 个像素的平均值。但是我不知道它如何影响你的着色器。再次在模糊示例中,使用线性过滤在中间像素的任一侧同时采样两个像素,您可以通过 3 个纹理 2D 调用对 5 个像素进行采样,从而将 5x5 模糊减少到水平和垂直的 6 个样本。
  • 只需使用较小的内核(这样您就不会采集太多样本)。这会影响质量,因此您可能需要某种方法来检测设备性能,并在设备看起来很慢时切换到质量较低的着色器。

【讨论】:

  • 谢谢 - 我理解这些建议,但我不知道如何将其应用于特定的着色器。使用 2 次通过或线性过滤似乎不会产生此处所需的中间值。我已经有一个低质量的解决方案作为后备。
  • 我想到了一种非正统的方法来消除我的 texture2D 调用——绘制一个顶点网格而不是一个四边形,并将包含所有需要的纹素的属性传递给每个顶点。这将消除所有纹理采样,但每帧都会创建一个大的绘图缓冲区。在我尝试实施这个冗长的解决方案之前,有什么建议吗?
  • 绝对避免顶点网格。您将用 CPU at_the_same_frequency 上的工作替换在 GPU 上的固定功能硬件中完成的工作......这是一个重大损失。需要明确的是,一般来说,在 CPU 上的少量工作可以避免 GPU 上的大量工作的情况下,将工作从管道上拉到 CPU 可能是一个胜利,但这并不适用于此。
  • @ahcox - 事实如何:1. 在此执行期间我的 CPU 为 0%,GPU 是一个很大的瓶颈,以及 2. 这个片段着色器所做的一切都将完成 4x-16x比现在少几倍(取决于比例因子)。
  • @SirKnigget 这开始看起来像一个完全不同的算法。目前,这对每个输出像素进行收集。听起来好像您打算对来自顶点着色器的每个输入进行散射。我什至没有看到可以用顶点和片段程序来表达问题的有效算法。
【解决方案2】:

您可能需要注意几个 Mali-400 的异常情况:

  • 您确实应该在不进行纹理查找的情况下使用变量(即在顶点着色器中计算“xyp_1_2_3.xw”等,并为每个纹理查找使用一个变量,而不是对它们进行混合)。
  • 在某些特定数量的指令(不幸的是,保密协议阻止我透露这个数字),性能下降得非常严重。您可以从离线编译器获取指令计数。为了解决这个问题,您可以将着色器拆分为多个较小的着色器,并使用未记录的 GL_ARM_framebuffer_read-extension 来读取前一个着色器的结果。 (Google 可以告诉你如何使用它,似乎。在离线着色器编译器的二进制文件中搜索一下也可能会有所帮助)

【讨论】:

  • 我尝试使用多个 vec2 变量进行查找(尽可能多的变量)并以正常方式进行其余查找。它只是变慢了......
  • vec2 的东西更多的是关于精度而不是性能,真的。否则,变量必须通过一个寄存器,即 fp16。
  • 你建议如何应用你所说的?我检查了该 GPU 上的最大变量,使用所有变量(作为 vec2)传递纹理坐标并对其进行采样,并使用正常偏移计算对其余纹理像素进行采样。这没有帮助,但也许我做错了什么。
  • 我的两点中的后者可能与您的问题更相关。第一点(正如我所说)是关于精度,而不是关于性能。在某些情况下,它也有助于提高性能,但这取决于您的顶点数等。
  • 不,我不认为这是有记录的,这就是我一直说“无证”的原因。我说你可以通过谷歌了解如何使用它,我指的是上面的链接确实显示了一个使用它的着色器。
【解决方案3】:

片段着色器性能的上限(执行时间的下限)由 21 次纹理加载和一次写入帧缓冲区 (gl_FragColor =) 设置。构建一个片段着色器是值得的,它只执行 21 次加载,将每次加载的结果累积到单个 vec4 中,然后将其写出。如果您在麻烦的目标硬件上运行此着色器,您将知道更复杂的着色器所在位置与其在那些特定 GPU/驱动程序/平台修订版上的最大潜在性能之间的差异。您的真实着色器只能比这慢,所以如果这个简单的测试着色器本身太慢,您将不得不在更远的地方寻找解决方案。

一旦建立了基线,我只有模糊的建议来改进你真正感兴趣的着色器,但也许我的推理是感兴趣的。我看到您的代码在顶部将所有纹理负载聚集在一起。在硬件层面,纹理加载具有极长的延迟,但 GPU 着色器处理器能够在运行时执行其他操作,包括在同一工作块中运行其他线程。这广泛地意味着,在负载之间分布有大量算术工作的最终着色器二进制文件将在负载的阴影下免费执行算术工作,并且使用少量寄存器的着色器程序将允许在同时,每个线程可能在执行其算术工作,而其他线程被阻止加载纹素。希望任何着色器编译器都能移动您的代码以实现所需的交错。但是,帮它一把也无妨,因此:

  • 尝试在不中断的情况下将文件中的每个算术语句(按词法)向上移动到尽可能高的位置。如果编译器错过了一个技巧,这可以帮助分散你的负载。
  • 尝试尽快使用所有中间结果,以便编译器能够识别它们的变量已失效,从而释放寄存器。这可能会减少寄存器的使用,从而增加程序的占用率。实现此效果的一个想法是,如果您有一堆在最后求和的部分结果,则可以将保存部分结果的许多变量的最终总和转换为在生成每个部分结果时累积成单个变量。李>

一如既往,YMMV

【讨论】:

  • 关于移动算术语句 - 大多数算术依赖于获取的纹素(少数除外)。你会建议均匀分布吗? (如:读取纹素 7、11、17、133,计算,读取纹素 8、6、16、18,计算等)
猜你喜欢
  • 1970-01-01
  • 2019-03-04
  • 2011-09-19
  • 1970-01-01
  • 2011-08-31
  • 1970-01-01
  • 1970-01-01
  • 2011-05-24
  • 1970-01-01
相关资源
最近更新 更多