shader-slang / slang

Making it easier to work with shaders
MIT License
2.07k stars 177 forks source link

Preprocessor macro for targets #4307

Closed chaoticbob closed 3 months ago

chaoticbob commented 3 months ago

In some cases, it would be useful to have a preprocessor macro for targets to allow custom handling for the target's platform.

For example, when compiling to SPIRV, it would be useful to have __spirv__ to add attribute decorations to resource declarations:

struct ParamDesc {
    int Value;
};

#if defined(__spirv__)
[[vk::push_constant]]
#endif
ConstantBuffer<ParamDesc> Params : register(b1);

float4 main(float4 P : SV_POSITION) : SV_TARGET
{
    return float4(Params.Value, 0, 0, 1);
}

For the above case, not all compilers can handle attribute decorations and ignore unknown ones. FXC fails with a syntax error if the surrounding #if defined is removed:

C:\local\Temp\4e959031-9817-4f20-9573-ef6712bed9c6.hlsl(6,2): error X3000: syntax error: unexpected token '['

Another example is when using mesh shaders, there's some functionality differences between Apple GPUs and PC GPUs that users may wish to custom handle:

struct Payload {
    uint MeshletIndices[32];
};

struct MeshOutput {
    float4               PositionCS : SV_POSITION;
#if defined(__metal__)
    // Metal doesn't support support barycentric interpolation using
    // vertex id from mesh shader to pixel data, so pass attribute
    /// data.
    float3               PositionWS : POSITIONWS;
    float3               Normal     : NORMAL;
    float2               TexCoord   : TEXCOORD;    
#else
    // Use barycentric coords in pixel shader to interpolate attributes
    nointerpolation uint VertexIndex : VERTEX_INDEX;
#endif    
};

[outputtopology("triangle")]
[numthreads(128, 1, 1)]
[shader("mesh")]
void main(
                 uint       gtid : SV_GroupThreadID, 
                 uint       gid  : SV_GroupID, 
     in payload  Payload    payload, 
    out indices  uint3      triangles[128], 
    out vertices MeshOutput vertices[64]) 
{
    SetMeshOutputCounts(4, 16);

    vertices[gtid].PositionCS = float4(1, 0, 0, 1);

#if defined(__metal__)
    vertices[gtid].PositionWS = float3(0, 0, 0);
    vertices[gtid].Normal     = float3(0, 1, 0);
    vertices[gtid].TexCoord   = float2(0, 0);
#else
    vertices[gtid].VertexIndex = gtid;    
#endif
}

Besides having __spirv__ to maintain compatibility with DXC, I don't have recommendations on what the preprocessor macros should be for the targets.

csyonghe commented 3 months ago

The way Slang compiler is architected is that we will always compile user code to platform-neutral Slang IR modules in-dependent of the final code generation target, and then specialize the IR to each target after linking.

This means that our front-end has no assumption or knowledge of the code gen target, and it cannot evaluate anything at parsing/type checking time that is target-dependent.

In function bodies, we already have __target_switch that can deal with target-dependent code.

What is being asked is what should we do for the global scope.

I think this should be handled by the capability system. We can allow the user to do:

struct MyType
{
     [target_specific(metal)] int metalOnlyField;
}

And the compiler can implement the behavior that such fields declared with require is only available and defined for metal, and attempting to use metalOnlyField from a function intended for other targets is a type error.

csyonghe commented 3 months ago

In the meanwhile, the user can always choose to define those macros themselves if they know what target they are compiling for. It is just that we cannot provide a builtin __spirv__ macro due to the compiler design.

chaoticbob commented 3 months ago

Got it! Thanks for the explanation, Yong.

Just looked at __target_switch and it makes sense for any shader that's authored as Slang source. The syntax style is new to me and will take a bit to get used to, but from first reading it seems able to achieve similar things to the platform macro blocks. I do think the documentation could use some additional wording for anyone coming to look for a solution on how to customize behavior in function bodies for different targets.

For the case statements, do the values just correspond to the arg values passed to -target or is there a different mappings that users should be aware of?

Since the compiler can't support the macros due to architecture, we can address the issue through guidance per your suggestion of user defined macros.

jkwak-work commented 3 months ago

For the very first example, you can use the command line argument to control the macro.

struct ParamDesc {
    int Value;
};

#if defined(__spirv__)
[[vk::push_constant]]
#endif
ConstantBuffer<ParamDesc> Params : register(b1);

float4 main(float4 P : SV_POSITION) : SV_TARGET
{
    return float4(Params.Value, 0, 0, 1);
}

In the command-line argument, you can use -D__spirv__ in your example above.

slangc.exe source.slang -target spirv -D__spirv__ -entry main -stage fragment
ArielG-NV commented 3 months ago

For the case statements, do the values just correspond to the arg values passed to -target or is there a different mappings that users should be aware of?

For the case statements you may pass almost any capabilities from the Slang capabilities system to target specific feature sets

For most users, case glsl: case spirv: case hlsl: default: will be enough.

For examples, hlsl.meta.slang and glsl.meta.slang is a good reference for how to use __target_switch.

chaoticbob commented 3 months ago

@ArielG-NV Just for completeness, will there also be a Metal target as well? Something like metal?

csyonghe commented 3 months ago

@chaoticbob Yes, the metal capability is already defined.

ArielG-NV commented 3 months ago

there already is a metal target

the targets: cpp, metal, glsl, hlsl, spirv, cuda

chaoticbob commented 3 months ago

Thanks. Closing this issue, will follow with @bmillsNV and @swoods-nv about documentation.