9
\$\begingroup\$

I've been working on porting a relatively large opengl ES 1.1 source to ES 2.0.

In OpenGL ES 2.0 (which means, everything uses shaders), I want to draw a teapot three times.

  1. The first one, with a uniform color (ala the old glColor4f).

  2. The second one, with a per-vertex color (the teapot has its array of vertex color as well)

  3. The third one, with per-vertex texture

  4. And maybe a fourth one, with both per-vertex texture and color. And then maybe a 5th one, with normals as well..

There are two choices I have with implementation, as far as I know. The first is to make a shader that supports all of the above, with a uniform that is set to change the behavior (e.g use the singular color uniform, or the per-vertex color uniform).

The second choice is to create a different shader for each situation. With some custom shader preprocessing, it's not that complicated to do, but the concern is the performance cost in switching shaders between drawing objects. I've read that it's not trivially small.

I mean, the best way to go about this is to build both and measure, but it'd be good to hear any inputs.

\$\endgroup\$

2 Answers 2

11
\$\begingroup\$

The performance cost of branching can be not trivially small too. In your case all vertices and fragments being drawn will be taking the same path through your shaders, so on modern desktop hardware it would not be as bad as it could be, but you're using ES2 which implies that you're not using modern desktop hardware.

The worst case with branching will go something like this:

  • both sides of the branch are evaluated.
  • a "mix" or "step" instruction will be generated by the shader compiler and inserted into your code to decide which side to use.

And all of these extra instructions will be run for each vertex or fragment that you draw. That's potentially millions of extra instructions to be weighed against the cost of a shader change.

Apple's "OpenGL ES Programming Guide for iOS" (which can be taken as representative for your target hardware) has this to say about branching:

Avoid Branching

Branches are discouraged in shaders, as they can reduce the ability to execute operations in parallel on 3D graphics processors. If your shaders must use branches, follow these recommendations:

  • Best performance: Branch on a constant known when the shader is compiled.
  • Acceptable: Branch on a uniform variable.
  • Potentially slow: Branching on a value computed inside the shader.

Instead of creating a large shader with many knobs and levers, create smaller shaders specialized for specific rendering tasks. There is a tradeoff between reducing the number of branches in your shaders and increasing the number of shaders you create. Test different options and choose the fastest solution.

Even if you're satisifed that you're in the "Acceptable" slot here, you still need to consider that with 4 or 5 cases to select between, you're going to be raising the instruction counts in your shaders. You should be aware of the instruction count limits on your target hardware and ensure that you don't go above them, quoting again from the Apple link above:

OpenGL ES implementations are not required to implement a software fallback when these limits are exceeded; instead, the shader simply fails to compile or link.

None of this is to say that branching is not the best solution for your need. You correctly identified the fact that you should profile both approaches, so that's the final recommendation. But just do be aware that as shaders become more complex, a branching-based solution may well give much higher overhead than a few shader changes.

\$\endgroup\$
2
  • \$\begingroup\$ Honestly, that "execute both paths in parallel and choose one result" strategy as a worst case doesn't seem that bad to me, at least when the branched part can be kept relatively short. Compared to a CPU where a branch mis-prediction is akin to backtracking by over a dozen instructions in the pipeline \$\endgroup\$ Commented Jun 14 at 2:11
  • \$\begingroup\$ @thomasrutter - depends; if you multiply the overhead by a few million for e.g. pixel shader coverage, then it can very quickly build up to be an unacceptable overhead. But as you say, if the branched part is quite short, this might not be as bad. \$\endgroup\$ Commented Jun 14 at 10:26
4
\$\begingroup\$

The cost of binding shaders may not be trivial, but it's not going to be your bottleneck unless you're rendering thousands of items without batching all the objects that use the same shaders.

Though I'm not sure if this applies to mobile devices, but GPUs aren't horrendously slow with branches if the condition is between a constant and a uniform. Both are valid, both have been used in the past an will continue to be used in the future, pick whichever one you think would be cleaner in your case.

Additionally, there are a few other ways to accomplish this: "Uber-shaders" and a little trickery with the way OpenGL shader programs are linked.

"Uber-shaders" are essentially the first choice, minus the branching, but you'll have multiple shaders. Instead of using if statements, you use the preprocessor - #define, #ifdef, #else, #endif, and compile different versions, including the proper #defines for what you need.

vec4 color;
#ifdef PER_VERTEX_COLOR
color = in_color;
#else
color = obj_color;
#endif

You can also break the shader up into separate functions. Have one shader that defines prototypes for all the functions and calls them, link a bunch of extra shaders that include the proper implementations. I've used this trick for shadow mapping, to make it easy to swap out how filtering is done on all objects without having to modify all the shaders.

//ins, outs, uniforms

float getShadowCoefficient();

void main()
{
    //shading stuff goes here

    gl_FragColor = color * getShadowCoefficient();
}

Then, I could have multiple other shader files that define getShadowCoefficient(), the necessary uniforms, and nothing else. For example, shadow_none.glsl contains:

float getShadowCoefficient()
{
    return 1;
}

And shadow_simple.glsl contains (simplified from my shader that implements CSMs):

in vec4 eye_position;

uniform sampler2DShadow shad_tex;
uniform mat4 shad_mat;

float getShadowCoefficient()
{
    vec4 shad_coord = shad_mat * eye_position;
    return texture(shad_tex, shad_coord).x;
}

And you could simply choose whether you wanted shading or not by linking a different shadow_* shader. This solution may very well have more overhead, but I'd like to think that the GLSL compiler is good enough to optimize away any extra overhead compared to other ways of doing this. I haven't ran any tests on this, but it's is the way I like to do it.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .