5 HLSL 2021
Greg Roth редактировал(а) эту страницу 2021-10-13 13:54:36 -07:00

HLSL 2021 introduces new language features to improve and facilitate the shader authoring experience. Many of them are very similar or identical to C++ counterparts. Where details are missing, C++ functionality can be assumed.

It is available in the latest compiler code, but is not the default. To enable HLSL 2021, add the flag -HV 2021.

Template functions and structs

Functions can be defined with generic type arguments provided those types support all the methods and operators used in the function. The effect is to define a function that is instantiated with type variants for each of the calls that use that type. The syntax involves specifying the templated types using the template keyword with parameters indicating the types within <> braces after it:

template <class Position, class Color>
Color BlendedSample(Position pos, Color col) {
  return g_tex.Sample(g_samp, pos)*col;
}

In similar fashion, structures can be created with generic types. Given HLSL's lack of constructors, instantiation type must be determined by explicit template parameters.

template<typename C, typename W>
struct Blender {
  C srcColor;
  C dstColor;
  W srcWeight;
  W dstWeight;
  C blend() { return srcColor*srcWeight + dstColor*dstWeight; }
};

float3 main(float4 col1 : COLOR0, float4 col2: COLOR2) : SV_Target {
  struct Blender<float3, float> B;
  B.srcColor = col1.rgb;
  B.dstColor = col2.rgb;
  B.srcWeight = col1.a;
  B.dstWeight = 1.0 - col1.a;
  return B.blend();
}

Bitfields on struct elements

Bitfields increase the granularity of type sizes available to structure elements by allowing the inclusion of a numerical annotation after the field name:

struct BitfieldStruct {
  int x: 8;
  int : 8;
  int y : 16;
};

The effect of this is a struct that has three integer members, but only takes up 32 bits. Note that the size specified in the bitfield cannot exceed the size of the main type of the member. Anonymous elements can exclude the name for padding purposes:

struct FlagStruct {
  int flags: 24;
  int : 8;
};

Operator overloading

To allow a broader use of templates and allow creation of types that might mimic the behavior of more familiar programming environments, operators can be defined for user-defined structs that allow those structs to be applied to existing operators just as native types can:

struct MyArray {
  float4 A[MAX_SIZE];
  
  void splat(float f) {
    for (int i = 0; i < MAX_SIZE; i++)
      A[i] = f;
  };

  float4 operator[](int ix) {
    if (ix >= MAX_SIZE)
      return 0.0;
    return A[ix];
  };

  MyArray operator+(MyArray RHS) {
    MyArray OutArray;
    for (int i = 0; i < MAX_SIZE; i++)
      OutArray.A[i] = A[i] + RHS.A[i];
    return OutArray;
  };

  void operator=(MyArray RHS) {
    for (int i = 0; i < MAX_SIZE; i++)
      A[i] = A[i] + RHS.A[i];
  };
};


float4 main(float4 col1: COLOR0, float4 col2: COLOR2, int ix : I) : SV_Target {
  MyArray A, B;
  A.splat(col1);
  B.splat(col2);
  A = A + B;
  return A[ix];
}

C++ style function overloading

HLSL 2021 makes a backward incompatible change to implicit casting that results in function overloading that hews closer to C++ behavior.

Previously, a user defined struct that contained members of the same type and order as another struct would silently be cast into that other struct where relevant. This included when a struct was passed into a function. As a result, if you wanted to have two structs with identical layouts and a different function overload for each, the compiler considered the two overloaded functions to be ambiguous and would produce an error.

With HLSL 2021, such implicit casts are disallowed and will produce an error. As a result, overloads that take structs with the same layout will be considered distinct. If the previously implicit casting behavior is desired, the code can easily be updated with an explicit cast that will work just fine.

Examples of code that will produce an error with HLSL 2021:

struct Color {
  float4 col;
};

struct Position {
  float4 pos;
};

SamplerState g_samp : register (s0);
Texture2D<float4> g_tex : register (t0);

float4 GetColor(Position P) {
  return g_tex.Sample(g_samp, P.pos.xy);
}

float4 main(float4 pos : SV_Position, floa4 col : COLOR0) : SV_Target {
  Position P = {pos};
  Color C = {col};
#ifdef DEBUG
  // display postiion as color
  C = P; // error: cannot implicitly convert from 'Position' to 'Color'
#endif
  return GetColor(C); // error: cannot implicitly convert from 'Color' to 'Position'
                      //  note: candidate function not viable: no known conversion from 'Color' to 'Position' for 1st argument
}

What is newly possible is tailoring function overloads to the specific struct even if it matches another in layout:

float4 Process(Color col) { col.rgb *= col.a; return col;}
float4 Process(Position pos) { pos.xyz *= 1.0/pos.w; return pos;}

Logical operation short-circuiting for scalars

Another potentially backward incompatible change is to the behavior of binary logic operators. Previously, HLSL did not support short circuiting behavior for vector types because each component of the vector might have a different result. Sometimes short circuiting behavior was expected and performance impacts were the result of executing a potentially unnecessary function call.

In HLSL 2021, the usual binary logic operators can only be used with scalars. These scalar operations will exhibit expected short circuiting behavior. If the first operand of && is false, the second will not be evaluated. If the first operand of || is true, the second will not be evaluated. The evaluation of the second and third operands of ?: will be determined by the value of the first.

If vector operands are used with binary logical operators, the compiler will produce an error. To maintain the previous behavior where it is wanted, new and(), or(), and select() intrinsics are introduced corresponding to &&, ||, and ?: operators.

Where existing shaders make use of binary logic operators on vector operands, the author may choose to replace the conditionals with scalar values to leverage the short circuit improvements, or switch to the new intrinsics to maintain the previous behavior.

For example this code will need to be updated:

  doit = vec && (ShouldIDoIt(vec) || !ShouldINotDoIt(vec));

If the boolean vector is needed, short circuiting may not be appropriate as every channel needs to be evaluated and assigned so the new and() and or() intrinsics should be used:

  doit = and(vec, or(ShouldIDoIt(vec), !ShouldINotDoIt(vec)));

If a scalar evaluation is sufficient, then use of any() or possibly a single element of a vector is appropriate:

  doit = vec.x && (any(ShouldIDoIt(vec)) || any(!ShouldINotDoIt(vec)));

Issues

HLSL 2021 is still new and should be considered beta. Despite our best efforts, there are likely still some bugs lurking. If you encounter any not listed in this query, please file a new issue with "[HLSL 2021]" in the title.