Different shader kernels depending on active shader features?

Is it possible for a shader to conditionally use different stage kernels depending on which keywords are defined, by wrapping the #pragma directives within an #if statement?

For example, could you do something like this and expect Unity’s shader compiler to handle it?

#pragma shader_feature MY_TESSELLATION
#pragma vertex vert
#pragma fragment frag
#if (MY_TESSELLATION)
    #pragma hull my_hull
    #pragma domain my_domain
#endif
#if (MY_TESSELLATION)
    hullInput vert() ...
#else
    fragInput vert() ...
#endif

I’ve also tried this way, figuring it’s a little more explicit for the compiler, but still no dice.

#pragma shader_feature MY_TESSELLATION

#if (MY_TESSELLATION)
    #pragma vertex tess_vert
    #pragma hull my_hull
    #pragma domain my_domain
    #pragma fragment frag
#else
    #pragma vertex vert
    #pragma fragment frag
#endif

fragInput vert() ...
float4 frag() ...

#if (MY_TESSELLATION)
    hullInput tess_vert() ...
    hullInput my_hull() ...
    fragInput my_domain() ...
#endif

Is it straight up just not possible to toggle kernels with shader features like this? The use case here is that we want the game to use different shader configurations depending on the desired platform/feature set.

Tried making a minimal repro test case. Getting the following compiler complaints that I don’t really get, given the shader code.

Error: Did not find shader kernel ‘my_domain’ to compile [my confusion - it can’t find my_domain since it’s defined within an #if (USE_MY_TESSELLATION) block, but the my_domain kernel is only requested within an #if (USE_MY_TESSELLATION) block as well…]
Error: Did not find shader kernel ‘my_hull’ to compile
Error: Did not find shader kernel ‘my_vert_tess’ to compile
Warning: Duplicate #pragma vertex found, ignoring: #pragma vertex my_vert (line 24) [my confusion - how can there be a duplicate #pragma vertex when they’re declared within mutually exclusive #if blocks?]
Warning: Duplicate #pragma fragment found, ignoring: #pragma fragment my_frag (line 25)

Shader "Custom/ConditionalTessFail" {

    Properties
    {
        _TessFactor("Tessellation Factor", Float) = 5
        [Toggle(USE_MY_TESSELLATION)]_UseTess("Use Tessellation", Float) = 0
    }

    SubShader
    {
        Pass
        {
            CGPROGRAM

            #pragma target 4.6
            #pragma shader_feature USE_MY_TESSELLATION

            #if (USE_MY_TESSELLATION)
                #pragma vertex my_vert_tess
                #pragma hull my_hull
                #pragma domain my_domain
                #pragma fragment my_frag
            #else
                #pragma vertex my_vert
                #pragma fragment my_frag
            #endif

            struct vert_input
            {
                float4 vertex : POSITION;
            };
  
            struct frag_input
            {
                float4 vertex : SV_POSITION;
            };

            frag_input my_vert(vert_input i)
            {
                frag_input o = (frag_input)0;
                o.vertex = UnityObjectToClipPos(i.vertex);

                return o;
            }

            float4 my_frag(frag_input i) : SV_Target0
            {
                return 1;
            }

            #if (USE_MY_TESSELLATION)
                float _TessFactor;
                struct hull_input
                {
                    float4 vertex : INTERNALTESSPOS;
                };

                hull_input my_vert_tess(vert_input v)
                {
                    hull_input o;
                    o.vertex = v.vertex;
                    return o;
                }

                struct TessellationFactors
                {
                    float edge[3] : SV_TessFactor;
                    float inside : SV_InsideTessFactor;
                };

                TessellationFactors GetTessellationFactors(InputPatch<hull_input, 3> patch)
                {
                    TessellationFactors f;
                    f.edge[0] = _TessFactor;
                    f.edge[1] = _TessFactor;
                    f.edge[2] = _TessFactor;
                    f.inside = _TessFactor;
                    return f;
                }

                [UNITY_domain("tri")]
                [UNITY_outputcontrolpoints(3)]
                [UNITY_outputtopology("triangle_cw")]
                [UNITY_partitioning("fractional_odd")]
                [UNITY_patchconstantfunc("GetTessellationFactors")]
                hull_input my_hull(InputPatch<hull_input, 3> patch, uint id : SV_OutputControlPointID)
                {
                    return patch[id];
                }

                [UNITY_domain("tri")]
                frag_input my_domain(TessellationFactors factors, OutputPatch<hull_input, 3> patch, float3 domLoc : SV_DomainLocation)
                {
                    vert_input o;
                    o.vertex = patch[0].vertex * domLoc.x + patch[1].vertex * domLoc.y + patch[2].vertex * domLoc.z;
                    return my_vert(o);
                }
            #endif
            ENDCG
        }
    }
}

In Unity’s shader compiling system, #pragma lines are processed before #if lines, so you can’t create variants with different shader stages. This makes sense a bit since they use some #pragma lines to determine what variants to compile.

Thanks for the response. So is it just not possible to conditionally enable tessellation on a shader? It seems like the compiler should be able to support something along those lines, since some platforms support the feature and some don’t.

Correct.

This is possible. You need a shader with multiple SubShaders, each with different #pragma target and/or #pragma requires, with the highest level or most requirements first.
https://docs.unity3d.com/Manual/SL-ShaderCompileTargets.html

Shader "SubShaderExample"
{
  Properties {
    // stuff
  }

  // SubShader with all passes requiring Shader Level 3.0
  SubShader {
    Pass {
      CGPROGRAM
      #pragma target 3.0
      // stuff
      ENDCG
    }
  }

  // SubShader with all passes requiring Shader Level 2.0, used only if the first SubShader can't be
  SubShader {
    Pass {
      CGPROGRAM
      #pragma target 2.0
      // stuff
      ENDCG
    }
  }
}

I see. What about the use case where a platform supports the feature but our app doesn’t have available performance cycles to enable the feature for that platform (ie, we manually want to disable/enable the feature and not rely on hardware support level).

The LOD levels are arbitrary, and mostly unused by Unity at this point, so the documentation only represents how it was used for legacy shaders. The Standard shader has two SubShaders, one using LOD 300 w/ #pragma target 3.0, the other using LOD 150 w/ #pragma target 2.0. That doesn’t mesh with that documentation at all, but again it doesn’t really matter. Just set your LOD to >300 for tesellation and set the max LOD to less than that when you want to skip that subshader.

1 Like

Awesome, I knew there had to be some use for the shader LOD system - I’ll look into it! Thanks so much for your help bgolus.

This sounds like a way to effectively workaround the wanted-for-many-years feature of conditionally compiling shader Pass’s - create two subshaders, one with the pass, one without, move all their code into a separate cginc file to avoid maintaining two copies, and use Shader.maximumLOD to choose between variants at runtime?

Another workaround would be to use Unity - Scripting API: Material.SetShaderPassEnabled