Search Unity

Question Can I expose a shader value without affecting shader compiler optimizations?

Discussion in 'Shaders' started by bobbaluba, Sep 7, 2020.

  1. bobbaluba

    bobbaluba

    Joined:
    Feb 27, 2013
    Posts:
    81
    So let's say I do something like this in a shader:

    float ConstantTestFragment() : SV_TARGET {
    float foo = 2;
    return sqrt(foo);
    }


    So the shader compiler seems to be smart enough to precompute the result of sqrt(2). If I hit "Compile and show code", I see this in the output

    0: mov o0.x, l(1.414214)


    However, let's say I wanted "foo" to be configurable per material, so I add a property for it

    Properties {
    _Foo("Foo", Range(0, 10)) = 2
    }
    ...
    uniform float _Foo;
    float ConstantTestFragment() : SV_TARGET {
    return sqrt(_Foo);
    }


    Now I end up with this code:

    0: sqrt o0.x, cb0[14].x


    I.e. the optimization is gone, since the shader code is generic for all materials and can't make assumptions about the value of foo.

    Now, I'm wondering, is there a way I can have each material compile its own version of the shader with optimizations?

    So I looked into shader features, and it sort of looks like I can at least get multiple variants to compile i.e.

    #pragma multi_compile SIMPLE_SHADING BETTER_SHADING GOOD_SHADING BEST_SHADING


    and then set foo according to the define. However, what do I do if I want to support arbitrary values of foo?

    So I strongly suspect that what I'm wishing for doesn't exist, which I guess would be some kind of constant property that would be compiled for each specific material with it's specific constants (not uniforms). I wouldn't expect to be able to change it run-time, I just want it to be compiled for the material values I have set in the inspector for the material assets in the project.

    Now I know I could just do sqrt(foo) on the cpu before sending it, I just chose that as an example because it's easier to see whether the compiler can optimize or not. There are more complicated examples that would greatly benefit from the compiler knowing constant values (branching, loop unrolling or several different uses of the same constant).

    If per-material constants are not possible, is there a way I could inject a project-wide #define into a shader? (I'm using a custom scriptable render pipeline if that matters).
     
  2. neoshaman

    neoshaman

    Joined:
    Feb 11, 2011
    Posts:
    6,493
    I don't if there is an option, I myself plan to just write code that write code to replace constant and create a shader file, in editor. If the values change in the build, then it's not constant, the value can't be optimized ahead. It used to be that unity had a the possibility to create shader on teh fly in builds, it's been depreciated, I don't know if there is an equivalent.
     
  3. bobbaluba

    bobbaluba

    Joined:
    Feb 27, 2013
    Posts:
    81
    I think with OpenGL, you fed the driver shader code anyway, you didn't need to ship a shader compiler with your game in order to generate shaders at runtime. With Vulkan etc. (not sure about DX11) you have to give the shader byte code, so I think that's why they need to know all shader variants at build time.

    Sound like generating a shader constants file is the way to go.
     
  4. neoshaman

    neoshaman

    Joined:
    Feb 11, 2011
    Posts:
    6,493
    Question is, can i do this within unity?
     
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    All the official ways to generate and load shaders at runtime have been removed. It is not something Unity supports at all.

    However OpenGL always takes an uncompiled shader as the input that get compiled on first startup, though I believe Unity automatically caches the compiled versions on the device. But it's not valid to run compiled OpenGL shaders from a different device. Unity's OpenGL shader files are transpiled from compiled DX9 bytecode, so they're not really human readable unless you've hand written them in OpenGL to start with. It's entirely plausible that you could generate a GLSL shader file, add it to an asset bundle, and load that on an Android device. But this is well outside the officially supported route for this. And it would not work on anything not running OpenGL since Unity will not load uncompiled shaders on non OpenGL devices.

    Doesn't really solve the problem since you need to have all the variants listed in the shader file, and you can't put
    #pragma multi_compile
    lines in anything but the base .shader file. You could add a unique variant keyword per material (preferably all on one long
    multi_compile
    line), or have a unique shader per material. But this can lead to a massive increase in shader variants, which also massively increases the memory usage and load time.

    For the simple case you described, the answer is absolutely to use a material property that's already the square root value you want. For the more complex case of unrolling a loop, the usual solution is to have some small number of predefined options in a
    #pragma shader_feature
    keyword list that covers the common cases.

    Code (csharp):
    1. // properties
    2. // Can't just be the numbers in the KeywordEnum, must have some non numeric character at the start of each entry so it is evaluated as a string. Quotes aren't allowed unfortunately.
    3. [KeywordEnum(Num 0, Num 4, Num 8, Num 16)] _LoopCount ("Loop Count", Float) = 0
    4.  
    5. // shader setup
    6. // property name + keyword enum, all caps, with spaces replaced with underscore
    7. #pragma shader_feature _ _LOOPCOUNT_NUM_4 _LOOPCOUNT_NUM_8 _LOOPCOUNT_NUM_16
    8.  
    9. // map keywords to #define values
    10. #if defined(_LOOPCOUNT_NUM_16)
    11. #define LOOP_COUNT 16
    12. #elif defined(_LOOPCOUNT_NUM_8)
    13. #define LOOP_COUNT 8
    14. #elif defined(_LOOPCOUNT_NUM_4)
    15. #define LOOP_COUNT 4
    16. #else
    17. #define LOOP_COUNT 0
    18. #endif
    19.  
    20. // in function
    21. // use now hard coded loop
    22. for (int iter=0; iter<LOOP_COUNT; iter++)
    23. {
    24.   // do stuff
    25. }
    If you want finer granularity, have the keyword set a max iterations (used by the for loop so it can unroll), and have a separate integer property that you compare against in the for loop.
    Code (csharp):
    1. for (int iter=0; iter<LOOP_COUNT; iter++)
    2. {
    3.   if (iter >= _FineLoopCount)
    4.     break;
    5.   // do stuff
    6. }
    That might look ugly, but thats how shaders will unroll "dynamic" loops anyway.
     
    neoshaman likes this.