5
\$\begingroup\$

I recently found that you can do this:

int AnotherShaderConstant = 0;

float4 PixShader(VSOUT input, uniform bool Direction)
{
    float4 color;
    if (Direction)
        color = GoLeft();
    else
        color = GoRight();

    return color;
}

technique First
{
    PixelShader = compile ps_3_0 PixShader(AnotherShaderConstant == 1 ? true: false);
}

And it will work properly, meaning that you can change a shader constant "AnotherShaderConstant" at CPU level and the pixel shader will change it's behavior. The question is, will such move be equivalent to the following code that clearly produces a branch:

int AnotherShaderConstant = 0;

float4 PixShader(VSOUT input)
{
    float4 color;
    if (AnotherShaderConstant == 1)
        color = GoLeft();
    else
        color = GoRight();

    return color;
}

technique First
{
    PixelShader = compile ps_3_0 PixShader();
}

What I am trying to understand is will I recieve shader optimization similar to those that uniform constants provide, or will my code be bluntly submited to the GPU and executed for each pixel effectively resulting in a branch?

I mean the uber-optimized Microsoft's shaders from XNA's simple effects do the following:

int AnotherShaderConstant = 0;

float4 PixShader1(VSOUT input)
{
    float4 color;
    color = GoLeft();

    return color;
}

float4 PixShader2(VSOUT input)
{
    float4 color;
    color = GoRight();

    return color;
}

PixelShader PixelShaders[2] =
{
    compile ps_3_0 PixShader1();
    compile ps_3_0 PixShader2();
}

technique First
{
    PixelShader = PixelShaders[AnotherShaderConstant];
}

Which means that their shader compiler can react to changes in shader constants, will it react in such situation?

\$\endgroup\$
  • \$\begingroup\$ Do GoLeft and GoRight have constant behavior, or do they vary? If so, I would evaluate the whole if block in the host code and submit "color" to the shader as a constant parameter. \$\endgroup\$ – wrosecrans Feb 15 '14 at 15:19
  • \$\begingroup\$ Naturally the shaders vary, that's the whole point. I am trying to organize my shaders effectively, while maintaining the highest speed of execution. GoLeft and GoRight return color as float4 depending on a number of factors, which vary between them. For instance GoLeft is a lighting shader, while GoRight does lighting AND computes shadow on top of that. I cannot evaluate the if block. \$\endgroup\$ – cubrman Feb 15 '14 at 16:28
3
\$\begingroup\$

I profiled a test app with NSight and the results clearly showed that using "?" operator in the techniques section produces branches.

When using this structure:

technique First
{
    PixelShader = compile ps_3_0 PixShader(true);
}

technique Second
{
    PixelShader = compile ps_3_0 PixShader(false);
}

the shader sent into GPU is optimized at the CPU level and produces NO branches. I had two branches in my shader: one with 4 instructions and the other with 129. The shader that was sent into the GPU (it's disassembly) had 4 instructions when I used First technique and 129 when I used Second.

This approach produced equal results:

PixelShader pixelShaders[2] = 
{
    compile ps_3_0 PixShader(true),
    compile ps_3_0 PixShader(false),
};

technique First
{
    PixelShader = pixelShaders[AnotherShaderConstant];
}

However, this code:

technique First
{
    PixelShader = compile ps_3_0 PixShader(AnotherShaderConstant == 1 ? true: false);
}

Clearly produces a branch, as ANY shader sent into the GPU had 134 instructions regardless of what AnotherShaderConstant value was. The time it took GPU to process the Draw calls also varied:

For a model that used the simple (First) branch via specifying a different technique NAME or using the ARRAY of shaders took 13946 ms.

For the same model that used the simple (First) branch via changing the AnotherShaderConstant parameter and using the "?" operator took 58063 ms. So it was 4,16 times longer.

The number of rasterized primmitives and shaded pixels was the same in both cases.

UPDATE: Here is a quck and dirty sample of custom shader processor for XNA 4.0:

XNA 4.0 Custom Shader Processor

It compiles all the possible premutations of you shader depending on the number of directives you specify. At runtime, you can switch those shaders by simply changing needed variables.

\$\endgroup\$
  • \$\begingroup\$ Nice answer. However, the link to XNA 4.0 Custom Shader Processor redirects to The file you requested has not been found or may no longer be available. \$\endgroup\$ – PolGraphic Dec 14 '14 at 8:59
  • \$\begingroup\$ ty @PolGraphic, I have changed the link for Google drive, things seem to last there longer :) \$\endgroup\$ – cubrman Dec 14 '14 at 17:20
2
\$\begingroup\$

Normally branches dependent on uniforms get handled by the driver. It is possible that a directX layer handles this even before the driver but it is not documented and pure speculation.

I have noted using AMD drivers that changing a uniform to the point of that one branch will start to be taken, the first time this happens I can get a severe lockup of the whole machine during 1 second. I suspect the driver to be re-compiling its shader-cache at this moment to be able to have the optimized version of the second branch. Which suggests that the driver compiles one distinct shader for each branch, caches it, and uses an indexing system (maybe a unique signature using uniform values hashing) and picks the optimized shader for that combination.

Microsoft also provides pre-shaders, which the fxc compiler will handle if not given the option to ban that stage. (because it can by buggy in complex shader codes) The pre-shader is a little stage where the compiler puts all the computation that will definitely be invariant for all pixels (the same result for one raster pass), and make the card (or CPU) execute that one time on one thread before the actual multi-threaded shader executes on the GPU, but stripped of the invariant code, and the result is passed by microsoft magic between the 2, surely using constant buffers.

with effects, you can use a trick which is to use 2 techniques with a uniform bool passed as a parameter of your lighting function. In which case the compiler will generate 2 different hard-compiled functions from the same code but without a branch.

real dynamic branches exists when the condition is dependent on something variant, like a fetching from a texture with a variable UV. (UV is taken from varying, or calculation). This case, the card has 2 hardware implementation depending on generation, older generations (shader 2/3) will execute both branches, one after the other, and the pixels(threads) will select the result from the branch that corresponds to them after all have been executed. Therefore the execution time is the sum of both branches. This is to be mitigated by the fact that thread blocks (often something like a 16*16 pixel zone on screen) will run only one branch if all pixels of this block happen to have the same condition. Therefore you don't loose too much performance on very low frequencies conditions (like a shadow with one hard edge and all the left part of the screen is shadowed and all the right part is lit, in this case only the center blocks will suffer the slow down).

Second generation (shader 4/5) has actual CPU-like dynamic branching, in this case you don't care really.

\$\endgroup\$
  • \$\begingroup\$ Definitely a thumbs up for an in-depth explanation. You see, I already use multiple techniques with different uniforms trick :). It is even worse, each of my techniques has an array of shaders in it (like in my third example code) and, therefore, I can either change the technique name OR pass a different variable into the shader in order to change it's behavior WITHOUT branches (hopefully). The problem is, when I want to add another shader (or just a variation of the existing one) I need to write a tone of variations for every shader array and every technique :). I search for solutions :). \$\endgroup\$ – cubrman Feb 18 '14 at 17:33
  • \$\begingroup\$ this is so true, I call it "shader combinatorial explosion". some people handle it using #define directives and compile a lot of combination. Some people handle it using a dynamic on-demand (in-game-time) compilation, with the optional presence of a manual shader caching system, so that one typical test-run will have a good enough cache for production use. Some people rely on the driver and use dynamic branching, some opt for shader unification with different engine architecture choices... \$\endgroup\$ – v.oddou Feb 19 '14 at 1:28
  • \$\begingroup\$ well I guess I will profile my suggestion and make a decision based on the few results I can collect :). Thanks anyway. \$\endgroup\$ – cubrman Feb 19 '14 at 16:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.