9
\$\begingroup\$

If you have multiple shaders, could you instead of switching between shaders all the time just have one shader with a switch and each case will send you along to another shader function which you require for that specific object? ( And you would send an enumerated value through your constant buffer for that specific object)

I guess the real question is. What would take up the most performance, the switch or the switching between shaders?

Example:

BigShader(){
   switch(a){
      case 0 :
        { return transPSShader();}
      case 1:
        { return nonTransPSShader();}
ect.
}
\$\endgroup\$
6
  • 4
    \$\begingroup\$ Well, think about this: the switch() has to be evaluated every time the shader runs, i.e. for every pixel drawn. If you keep your shaders separate, there is no extra work per pixel. But don't take my word for it...why not code up both versions and measure the performance? \$\endgroup\$ Commented Jun 12, 2013 at 16:36
  • \$\begingroup\$ hmm you got a point there, didnt think about that it has to do it every pixel. But lets forget about the pixel shader then, how about the vertex shader (and i belive there is a geometry shader? srry im kinda new). Are they called on a per pixel or a per object basis? \$\endgroup\$ Commented Jun 12, 2013 at 16:40
  • 2
    \$\begingroup\$ Vertex shader is run per-vertex. Take an object with 1,000 vertexes - that means your switch needs to be called 1,000 times - versus just one shader change. \$\endgroup\$ Commented Jun 12, 2013 at 16:49
  • \$\begingroup\$ That's not exactly true. GPU is not CPU. GPU is working in parallel. A lot of threads (I think 16-32) are working with the same instruction in the same time. And if it evaluates the same value, it also continues the same way (in you switch). But if you have a lot of "case" branches, it will still be slow - a lot of conditions have to be evaluated. \$\endgroup\$
    – zacharmarz
    Commented Jun 12, 2013 at 17:03
  • \$\begingroup\$ The technique of making one shader with many behaviors is commonly referred to as an "uber shader," which might help you find additional information via Google. \$\endgroup\$ Commented Jun 12, 2013 at 17:11

1 Answer 1

9
\$\begingroup\$

Typically, branching of any kind (switches, if-statements, loops with non-constant iterations) are best avoided. This is mildly true on the PC (not enough to be worth worrying about at all outside of very tight inner loops), especially true on some general-purpose CPUs like the 360's Xenon (common hardware that makes indirect references on branches tolerable on modern super-scalar out-of-order deep-pipeline CPUs was left off the Xenon to save on cost), and especially true on GPUs.

GPUs are very special beasts. They do not work like a general-purpose CPU. They run potentially thousands of copies of a shader at a time, and there are limitations imposed on the hardware to make this possible. One of these limitations is that a number of execution cores share resources. For sake of example, let's say 4 cores are tied together like this on our hypothetical GPU.

So at any time, four "core" are running a shader in lock-step. They share an instruction pointer. They sort-of share a register file. The SIMD behavior of your shaders is not like SIMD behavior on the CPU which is typically used in games; each shader isn't doing four-way vector operations at once, but rather, all four cores work on a single component from the four different data streams at once. These four cores are tightly tied together.

The shared instruction pointer is key. If two of your shaders in this group execute switch case 1 and the other two run switch case 2, all four cores must run both switch cases! Predicated instructions are used to ensure that the results of the instructions in the "off" case are ignored for a particular core, but it still takes the time to execute the instruction and do any memory/register/texture accesses (which is why you should only do texture lookups in uniform code paths), etc.

Hence, branches are "slow" in the fact that your hardware ends up really under-utilized. A potentially very large portion of it ends up spending time evaluating instructions that have no effect. This is different than the CPU case where branches hurt because of pipeline stalls and mispredictions; the GPU often has very restricted branching capabilities anyway.

Is this "slower" than swapping out shaders? That depends. If you batch up your draw commands such that you do all draw operations using a particular shader in a row (so you don't switch from shader A to shader B and then back to shader A, but rather do all drawing with shader A and only then do drawing needing shader B)... it still depends, but it will probably be faster with this batching. As will all things performance-related, you need to test and find out specifically for your application and target hardware. If your switch statements are simple enough you might find that it is indeed quicker to use them.

It is often better anyway to batch up object with identical material properties (same shaders, textures, material constant buffers, etc.) just to avoid changing active resources even when using an uber shader. Breaking up your shaders to play well with this is often not hard at the low-end of the graphics scale. It can get to be a bit more difficult with multiple material types in a deferred shading context, and here a semi-uber shader approach is typically taken (often just for BRDF calculations and the like).

Note that engines like CryTek have taken the uber shader approach (unsure if the most recent incarnations still do), so it's certainly usable for very high-end games in the real world.

\$\endgroup\$
7
  • \$\begingroup\$ awsome anwser :) With uber shader you are refering to what i call Big shader? \$\endgroup\$ Commented Jun 12, 2013 at 18:41
  • \$\begingroup\$ Current GPUs have more like 32-wide or 64-wide SIMD, not just 4-wide. :) \$\endgroup\$ Commented Jun 12, 2013 at 18:45
  • 1
    \$\begingroup\$ @FredrikBostonWestman: yes. You'll be better off using the more common terminology, if for no reason other than that Google searches will be more fruitful. \$\endgroup\$ Commented Jun 12, 2013 at 19:06
  • 1
    \$\begingroup\$ What about a switch on a uniform that is always the same value? Like a fog mode parameter that would only change very rarely. \$\endgroup\$
    – jjxtra
    Commented May 9, 2018 at 5:21
  • \$\begingroup\$ @jjxtra, I had the same question, and just ran a test with uniform float test;, if(test > 1.0) { <complex nested for-loop> }. I only see performance drop when uniform test is actually set above 1.0. Not sure if this is standard, nor would I call this universal coverage, but it does appear to be optimized sometimes. (Tested using WebGL / OpenGL ES 2.0) \$\endgroup\$
    – Codesmith
    Commented Jun 7, 2022 at 17:48

You must log in to answer this question.

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