Announcing HLSL 2021

Chris

Today we are excited to announce the release of HLSL 2021! HLSL 2021 brings an assortment of new language features that we think will make shader authoring in HLSL more familiar and productive.
Enabling HLSL 2021 is as easy as passing the `-HV 2021`, and you can immediately start enjoying all the new language features like:
  • Template functions and data types
  • Overloadable operators for user defined data types
  • Bitfield members for data types
We intend to make HLSL 2021 the default language version in a future release after giving time for developers to test adopt and migrate code, and for us to perform more testing and integration. Once it is made the default you can use the `-HV` flag to specify an older language version if needed.
There are also some other changes in HLSL 2021 that may have some more subtle or unexpected impact specifically:
  • Strict casting rules for user defined data types
  • Logical operator short circuiting for scalar types
  • C++ `for` loop scoping rules
Let’s start by talking about some of the more subtle changes in HLSL 2021, then delve into all the fun new features!

Strict Casting of User-defined Data Types

Prior to HLSL 2021, user defined data types with the same member layout would be treated as the same type and freely casted between. This behavior caused certain unintuitive errors and allowed some code to compile that may not have been as intended. For example given the following code:
struct LinearRGB {
  float3 RGB;
};

struct LinearYCoCg {
  float3 YCoCg;
};

float GetLuma4(LinearRGB In) {
  return In.RGB.g * 2.0 + In.RGB.r + In.RGB.b;
}

float GetLuma4(LinearYCoCg In) {
  return In.YCoCg.x;
}
In this example before HLSL 2021, any call to `GetLuma4` would be ambiguous because any object of type `LinearYCoCg` freely behaves as `LinearRGB`. With strict casting in HLSL 2021, these calls are unique and resolvable.
This feature will cause some source code incompatibilities. For example, since the `LinearRGB` structure defined above will no longer implicitly convert to `LinearYCoCg` the following code compiles with previous language versions, but will not under HLSL 2021.
void Modify(inout LinearRGB V) {
  V.RGB.x += 1;
}

[numthreads(64, 1, 1)]
void main() {
  LinearYCoCg V = {{0.0, 0.0, 0.0}};
  Modify(V);
}
You can workaround these failures by explicitly casting to the appropriate type, as in:
[numthreads(64, 1, 1)]
void main() {
  LinearYCoCg V = {{0.0, 0.0, 0.0}};
  Modify((LinearRGB)V);
}

Logical Operator Short Circuiting

HLSL 2021 is introducing logical operator short circuiting behavior matching C. This means that if the first operand of a boolean `&&` is evaluated as `false`, the second operand will not be evaluated. Similarly, if the first operand of a boolean `||` is evaluated as `true` the second operand will not be evaluated. This can introduce behavior changes if the second operand performs a side-effecting operation. Take the following code:
struct Doggo {
  bool isWagging;

  bool wag() {
    isWagging = !isWagging;
    return !isWagging;
  }

  void bark() {
    // woof!
  }
};

[numthreads(1,1,1)]
void main() {
  Doggo Fido = {false};
  for (int i = 0; i < 10; ++i) {
    if (Fido.isWagging && Fido.wag())
      Fido.bark();
  }
}
In HLSL 2021, Fido’s wag and bark methods never get called. In HLSL pre-2021, wag gets called every time through the loop and bark gets called every other time through the loop.
In addition to adding short circuiting behavior, in HLSL 2021 logical operators can only be used with scalar values. Prior to HLSL 2021, logical operators could be used with vector types to provide vector outputs like the code below:
int3 X = {1, 1, 1};
int3 Y = {0, 0, 0};
bool3 Cond = X && Y;
bool3 Cond2 = X || Y;
int3 Z = X ? 1 : 0;
In HLSL 2021, this code will produce errors. If the old behavior is required the new `and()`, `or()`, and `select()` intrinsics can be applied as demonstrated below:
bool3 Cond = and(X, Y);
bool3 Cond2 = or(X, Y);
int3 Z = select(X, 1, 0);
In cases where a single scalar evaluation is appropriate the `any()` intrinsic can be applied or an individual vector element can be used.

C++ `for` Loop Scoping Rules

For compatibility with older versions of HLSL variables declared in the `for` statement of a `for` loop are attributed to the scope containing the `for` loop. This results in unexpected variable shadowing behavior. For example, in the code below the declaration of `I` in the `for` loop replaces the declaration of `I` outside the loop:
int I = 2;
for (int I = 0; I < 10; ++I) {
  // make pretty colors
}
// I == 10!!!
This behavior is a carry over from ANSI C, and was changed in C++ and C99.
This change may cause code incompatibilities in your codebase. Code that declares variables in the `for` statement and expects the variables to be live after the end of the loop will no longer compile. Code that was relying on the old behavior can be fixed by hoisting the declaration out of the `for` statement.

Template Functions and Data Types

HLSL 2021 adds support for C++-like templates for structs and functions. HLSL 2021 templates support full and partial specializations, and template parameter inference when possible.
Just as in C++, a template is declared using the `template` keyword and a template parameter list:
template<typename T>
void increment(inout T X) {
  X += 1;
}
An explicit specialization can be declared using the `template` keyword with an empty parameter list:
template<>
void increment(inout float3 X) {
  X.x += 1.0;
}
Partial template specializations are not supported at this time, but may be in a future release.
Template functions are called using the normal syntax for function calls wherever the argument types allow for inferring the template parameters:
[numthreads(1,1,1)]
void main() {
  int X = 0;
  int3 Y = {0,0,0};
  float3 Z = {0.0,0.0,0.0};
  increment(X);
  increment(Y);
  increment(Z);
}
If a template parameter cannot be inferred by the input, HLSL templates follow C++ template rules and require parameters be specified at the call site:
template<typename V, typename T>
V cast(T X) {
  return (V)X;
}

[numthreads(1,1,1)]
void main() {
  int X = 1;
  uint Y = cast<uint>(X);
}

Member Operator Overloading

HLSL 2021 is also extending programmers ability to create expressive custom data structures by enabling operator overloading on user defined data types. With HLSL 2021 you can override the arithmetic, bitwise, boolean, comparison, call, array subscript, and casting operators similar to how you would in C++.
There are a few key differences from C++ that stem from HLSL not supporting reference and pointer types. Since operators cannot return references, some C++ operators do not make sense to override (address of `&`, dereference `*` and pointer member `->` operators). This also means that operators that perform assignment (i.e. `=` or `+=`), and operators that generally return references to `this` (i.e. `<<` and `>>`) are not supported in HLSL 2021.
Additionally since HLSL does not support dynamic memory allocation the `new` and `delete` operators cannot be overridden.
The following code example demonstrates some of the ways that operators can be overloaded and used in HLSL 2021:
struct Pupper {
  int Fur;

  Pupper operator +(int MoarFur) {
    Pupper Val = {Fur + MoarFur};
    return Val;
  }

  bool operator <=(int y){
    return Fur <= y;
  }

  operator bool() {
    return Fur > 50;
  }
};

[numthreads(1, 1, 1)]
void main(uint tidx : SV_DispatchThreadId) {
  Pupper y = {0};
  for (Pupper x = y; x <= 100; x = x + 1) {
    if ((bool)x)
      y = y + 1;
  }
}
The full list of operators that can be overridden in HLSL 2021 are listed below.
The arithmetic operators:  `+`, ``, `*`, `/`, `%`
The bitwise operators:  `&`, `|`, `^`
Boolean and comparison operators:  `&&`, `||`, `!=`, `==`, `<=`, `>=`, `<`, `>`
Additional operators:  call `()`, subscript `[]`, casts `<type>`.

 

Bitfield Members in Data Types

In order to make it easier to utilize data structures from CPU code and provide more flexible integer sizes, HLSL 2021 has added bit field support for struct members. This allows specifying an arbitrary number of bits to use for an integer value inside a struct. Bit fields must be of an underlying integer type, but can be any number of bits less than the size of the specified type. With this syntax a 32-bit packed color could be represented as:
struct ColorRGBA {
  uint R : 8;
  uint G : 8;
  uint B : 8;
  uint A : 8;
};

Get Going!

We are very excited about all of these new features and we can’t wait to see what amazing things you do with them.
All of these new features are available in the latest DirectX Shader Compiler Release, and should be backwards compatible to any DirectX 12 runtime.
Special thanks to our partners at Google and AMD for their contributions to the HLSL 2021 release, and to our many users and contributors on GitHub.
Please file any bugs you encounter with HLSL 2021 or HLSL generally against our GitHub project.

 

1 comment

Leave a comment