November 2022
Posted 2 years ago
During hack week, I wanted to try creating C# shaders. The general idea of it is we take regular C# code and then transpile it to our shader format & compile that. It was a cool experiment but a lot of problems came up with it.

The good

The main reason I wanted to look into C# shaders is the IntelliSense support we'd get out of it. Since everything we transpiled had an equivalent C# type, we would get IntelliSense for free. This meant less time working out documentation and more time writing code that works:
attribute 'contenteditable' not allowedattribute 'blocktype' not allowed
The second great thing about it is that people don't need to worry about learning a different language. All your C# knowledge will transfer*. We'll get back to this later. A lot of people seem to struggle with the idea of HLSL and shaders as a whole. The hope was to allow this to be an entry point and understand the process much easier.

The Bad
C# has A lot of fancy features. Most of these features are not supported by HLSL. This requires us to do some extra heavy lifting to get this working. An example of this is struct initialization. With HLSL you can't easily define a set of default values on a struct. When you create a struct, you need to initialize all the values in this. In C#, we can do selective initialization. For example with a class:
public class Hello
{
public int FieldA { get; set; } = 2;
public int FieldB { get; set; } = 1;
public int FieldC { get; set; }
}

public class World
{
public Hello Create()
{
return new Hello() { FieldB = 0, };
}
}
attribute 'data-code' not allowedattribute 'data-language' not allowedattribute 'contenteditable' not allowedattribute 'blocktype' not allowed
in HLSL, we need to initialize FieldA & FieldB, this means we need to recursively handle default values for all types & create the object in place, as well as keep track of the actual underlying object variable name. This would end up being compiled as:
struct Hello
{
int FieldA;
int FieldB;
int FieldC;
}

Hello Create()
{
Hello temp_var_0;
temp_var_0.FieldA = 2;
temp_var_0.FieldB = 0;
temp_var_0.FieldC = 0;
return temp_var_0;
}attribute 'data-code' not allowedattribute 'data-language' not allowedattribute 'contenteditable' not allowedattribute 'blocktype' not allowed
Since we can't make things 1:1 as in this instance above. We need to also introduce variable state tracking & variable management. We also need to translate everything into HLSL-compatible types. We need to know which variable has which type & which components can be accessed. A simple edge case here for example is dealing with Vector4s. We cannot directly convert a vector4 being initialized with a vector3. This isn't valid HLSL so we need to actually handle cases like this and pass our own extra arguments.
new Vector4( new Vector3( 1, 2, 3 ) ); // Becomes float4(float3(1,2,3));
new Vector4( new Vector3( 1, 2, 3 ), 0 ); // Becomes float4(float3(1,2, 3), 0 );attribute 'data-code' not allowedattribute 'data-language' not allowedattribute 'contenteditable' not allowedattribute 'blocktype' not allowed
Finally, as we're transpiling, a lot of the C# code can be pretty verbose. Our shaders are already verbose, we would need to do a lot of simplification for it to be viable. Currently, it's looking very close to the HLSL side of things. What we can simplify is still TBD.
attribute 'contenteditable' not allowedattribute 'blocktype' not allowed
The Ugly
The generated code could look better but it's quite annoying to read. Shader inputs are also set up to be properties within the class. This introduces the issue of trying to shuffle around Vertex & Pixel inputs around between methods. The solution? Always pass the inputs/parameters to any method call. This can be further optimized by processing the function and only passing what's used, but that'd take a lot longer than a week. Luckily the HLSL compiler optimizes it out and flattens the code either way.

The Result
Now with all the boring stuff out of the way, here's an example of our post-processing pixelation shader recreate in C# as well as the actual generated output of it.

C#
attribute 'contenteditable' not allowedattribute 'blocktype' not allowed
VFX/HLSL
attribute 'contenteditable' not allowedattribute 'blocktype' not allowed
Maybe next hack week I can look at simplifying it & rewriting a large chunk of it. Perhaps I can fix some of the issues I have or perhaps I'll leave it at this. It was a fun experiment and learned a lot through the process!