Editor/Prism/Core/ShaderType.cs

Defines shader-related enums and a readonly record struct ShaderType that represents scalar/vector/matrix/object/struct shader types, provides factories (Vec/Mat/Obj/Struct), queries (IsScalar/IsTexture/etc.), string spellings (Hlsl/Slang), parsing from text, and many predefined well-known types.

Reflection
using System.ComponentModel;

namespace Editor.Prism.Core;

/// <summary>The component type of a scalar, vector or matrix.</summary>
public enum ScalarKind
{
	/// <summary>No value.</summary>
	Void,
	/// <summary>32-bit boolean.</summary>
	Bool,
	/// <summary>32-bit signed integer.</summary>
	Int,
	/// <summary>32-bit unsigned integer.</summary>
	UInt,
	/// <summary>16-bit float. Parses everywhere, but s&amp;box compiles at SM 6.0 so it lowers to float.</summary>
	Half,
	/// <summary>32-bit float. The default for everything in a shader graph.</summary>
	Float,
	/// <summary>64-bit float.</summary>
	Double
}

/// <summary>The broad shape of a <see cref="ShaderType"/>.</summary>
public enum TypeClass
{
	/// <summary>No type. This is <c>default(ShaderType)</c>.</summary>
	Void,
	/// <summary>A single component, e.g. <c>float</c>.</summary>
	Scalar,
	/// <summary>1..4 components, e.g. <c>float3</c>.</summary>
	Vector,
	/// <summary>RowsxCols components, e.g. <c>float3x3</c>.</summary>
	Matrix,
	/// <summary>An opaque resource object, e.g. <c>Texture2D</c> or <c>SamplerState</c>.</summary>
	Object,
	/// <summary>A named user struct.</summary>
	Struct
}

/// <summary>An opaque shader resource type. Never implicitly convertible to or from anything numeric.</summary>
public enum ObjectKind
{
	/// <summary>Not an object type.</summary>
	None,
	/// <summary>1D texture.</summary>
	Texture1D,
	/// <summary>1D texture array.</summary>
	Texture1DArray,
	/// <summary>2D texture. The workhorse.</summary>
	Texture2D,
	/// <summary>2D texture array.</summary>
	Texture2DArray,
	/// <summary>Multisampled 2D texture.</summary>
	Texture2DMS,
	/// <summary>3D volume texture.</summary>
	Texture3D,
	/// <summary>Cube map.</summary>
	TextureCube,
	/// <summary>Cube map array.</summary>
	TextureCubeArray,
	/// <summary>Filtering sampler.</summary>
	SamplerState,
	/// <summary>Comparison sampler, used for shadow lookups.</summary>
	SamplerComparisonState,
	/// <summary>Typed read-only buffer.</summary>
	Buffer,
	/// <summary>Untyped read-only buffer.</summary>
	ByteAddressBuffer,
	/// <summary>Structured read-only buffer.</summary>
	StructuredBuffer,
	/// <summary>Writable 2D texture (compute only).</summary>
	RWTexture2D,
	/// <summary>Writable 3D texture (compute only).</summary>
	RWTexture3D,
	/// <summary>Writable typed buffer (compute only).</summary>
	RWBuffer,
	/// <summary>Writable structured buffer (compute only).</summary>
	RWStructuredBuffer,
	/// <summary>Writable untyped buffer (compute only).</summary>
	RWByteAddressBuffer
}

/// <summary>
/// The Prism type lattice. Unlike the built-in ShaderGraph — whose entire type model is an
/// <c>int Components</c> in 0..4 — this is a real type: scalar kind, shape, dimensions and object kind.
/// <para>
/// Conventions: a vector stores its width in <see cref="Rows"/> with <see cref="Cols"/> = 1;
/// a 1-wide vector canonicalises to <see cref="TypeClass.Scalar"/> so <c>float</c> and <c>float1</c>
/// are the same value. <c>default(ShaderType)</c> is <see cref="Void"/>.
/// </para>
/// </summary>
public readonly record struct ShaderType(
	TypeClass Class, ScalarKind Scalar, byte Rows, byte Cols, ObjectKind Object, string StructName )
{
	// ---- well-known types -------------------------------------------------

	/// <summary>The empty type.</summary>
	public static readonly ShaderType Void = default;

	/// <summary><c>bool</c></summary>
	public static readonly ShaderType Bool = Vec( ScalarKind.Bool, 1 );
	/// <summary><c>bool2</c></summary>
	public static readonly ShaderType Bool2 = Vec( ScalarKind.Bool, 2 );
	/// <summary><c>bool3</c></summary>
	public static readonly ShaderType Bool3 = Vec( ScalarKind.Bool, 3 );
	/// <summary><c>bool4</c></summary>
	public static readonly ShaderType Bool4 = Vec( ScalarKind.Bool, 4 );

	/// <summary><c>int</c></summary>
	public static readonly ShaderType Int = Vec( ScalarKind.Int, 1 );
	/// <summary><c>int2</c></summary>
	public static readonly ShaderType Int2 = Vec( ScalarKind.Int, 2 );
	/// <summary><c>int3</c></summary>
	public static readonly ShaderType Int3 = Vec( ScalarKind.Int, 3 );
	/// <summary><c>int4</c></summary>
	public static readonly ShaderType Int4 = Vec( ScalarKind.Int, 4 );

	/// <summary><c>uint</c></summary>
	public static readonly ShaderType UInt = Vec( ScalarKind.UInt, 1 );
	/// <summary><c>uint2</c></summary>
	public static readonly ShaderType UInt2 = Vec( ScalarKind.UInt, 2 );
	/// <summary><c>uint3</c></summary>
	public static readonly ShaderType UInt3 = Vec( ScalarKind.UInt, 3 );
	/// <summary><c>uint4</c></summary>
	public static readonly ShaderType UInt4 = Vec( ScalarKind.UInt, 4 );

	/// <summary><c>half</c></summary>
	public static readonly ShaderType Half = Vec( ScalarKind.Half, 1 );
	/// <summary><c>half2</c></summary>
	public static readonly ShaderType Half2 = Vec( ScalarKind.Half, 2 );
	/// <summary><c>half3</c></summary>
	public static readonly ShaderType Half3 = Vec( ScalarKind.Half, 3 );
	/// <summary><c>half4</c></summary>
	public static readonly ShaderType Half4 = Vec( ScalarKind.Half, 4 );

	/// <summary><c>float</c></summary>
	public static readonly ShaderType Float = Vec( ScalarKind.Float, 1 );
	/// <summary><c>float2</c></summary>
	public static readonly ShaderType Float2 = Vec( ScalarKind.Float, 2 );
	/// <summary><c>float3</c></summary>
	public static readonly ShaderType Float3 = Vec( ScalarKind.Float, 3 );
	/// <summary><c>float4</c></summary>
	public static readonly ShaderType Float4 = Vec( ScalarKind.Float, 4 );

	/// <summary><c>double</c></summary>
	public static readonly ShaderType Double = Vec( ScalarKind.Double, 1 );

	/// <summary><c>float2x2</c></summary>
	public static readonly ShaderType Float2x2 = Mat( ScalarKind.Float, 2, 2 );
	/// <summary><c>float3x3</c></summary>
	public static readonly ShaderType Float3x3 = Mat( ScalarKind.Float, 3, 3 );
	/// <summary><c>float4x3</c></summary>
	public static readonly ShaderType Float4x3 = Mat( ScalarKind.Float, 4, 3 );
	/// <summary><c>float3x4</c></summary>
	public static readonly ShaderType Float3x4 = Mat( ScalarKind.Float, 3, 4 );
	/// <summary><c>float4x4</c></summary>
	public static readonly ShaderType Float4x4 = Mat( ScalarKind.Float, 4, 4 );

	/// <summary><c>Texture2D</c></summary>
	public static readonly ShaderType Texture2D = Obj( ObjectKind.Texture2D );
	/// <summary><c>Texture2DArray</c></summary>
	public static readonly ShaderType Texture2DArray = Obj( ObjectKind.Texture2DArray );
	/// <summary><c>Texture3D</c></summary>
	public static readonly ShaderType Texture3D = Obj( ObjectKind.Texture3D );
	/// <summary><c>TextureCube</c></summary>
	public static readonly ShaderType TextureCube = Obj( ObjectKind.TextureCube );
	/// <summary><c>TextureCubeArray</c></summary>
	public static readonly ShaderType TextureCubeArray = Obj( ObjectKind.TextureCubeArray );
	/// <summary><c>SamplerState</c></summary>
	public static readonly ShaderType Sampler = Obj( ObjectKind.SamplerState );
	/// <summary><c>SamplerComparisonState</c></summary>
	public static readonly ShaderType SamplerComparison = Obj( ObjectKind.SamplerComparisonState );

	// ---- factories --------------------------------------------------------

	/// <summary>Build a scalar or vector type. <paramref name="components"/> is clamped to 1..4.</summary>
	public static ShaderType Vec( ScalarKind scalar, int components )
	{
		if ( scalar == ScalarKind.Void ) return Void;

		var n = (byte)Math.Clamp( components, 1, 4 );
		return new ShaderType( n == 1 ? TypeClass.Scalar : TypeClass.Vector, scalar, n, 1, ObjectKind.None, null );
	}

	/// <summary>Build a matrix type. Dimensions are clamped to 1..4.</summary>
	public static ShaderType Mat( ScalarKind scalar, int rows, int cols )
	{
		if ( scalar == ScalarKind.Void ) return Void;

		return new ShaderType( TypeClass.Matrix, scalar,
			(byte)Math.Clamp( rows, 1, 4 ), (byte)Math.Clamp( cols, 1, 4 ), ObjectKind.None, null );
	}

	/// <summary>Build an opaque resource type.</summary>
	public static ShaderType Obj( ObjectKind kind )
	{
		if ( kind == ObjectKind.None ) return Void;

		return new ShaderType( TypeClass.Object, ScalarKind.Void, 0, 0, kind, null );
	}

	/// <summary>Build a named user struct type.</summary>
	public static ShaderType Struct( string name )
	{
		if ( string.IsNullOrEmpty( name ) ) return Void;

		return new ShaderType( TypeClass.Struct, ScalarKind.Void, 0, 0, ObjectKind.None, name );
	}

	// ---- queries ----------------------------------------------------------

	/// <summary>Total component count: 1 for a scalar, N for a vector, RxC for a matrix, 0 otherwise.</summary>
	public int Components => Class switch
	{
		TypeClass.Scalar => 1,
		TypeClass.Vector => Rows,
		TypeClass.Matrix => Rows * Cols,
		_ => 0
	};

	/// <summary>True for the empty type.</summary>
	public bool IsVoid => Class == TypeClass.Void;

	/// <summary>True for a single-component numeric type.</summary>
	public bool IsScalar => Class == TypeClass.Scalar;

	/// <summary>True for a 2..4 component numeric type.</summary>
	public bool IsVector => Class == TypeClass.Vector;

	/// <summary>True for a scalar or a vector.</summary>
	public bool IsScalarOrVector => Class is TypeClass.Scalar or TypeClass.Vector;

	/// <summary>True for a matrix type.</summary>
	public bool IsMatrix => Class == TypeClass.Matrix;

	/// <summary>True for an opaque resource type.</summary>
	public bool IsObject => Class == TypeClass.Object;

	/// <summary>True for a named user struct.</summary>
	public bool IsStruct => Class == TypeClass.Struct;

	/// <summary>True when the type has numeric components (scalar, vector or matrix).</summary>
	public bool IsNumeric => Class is TypeClass.Scalar or TypeClass.Vector or TypeClass.Matrix;

	/// <summary>True when the component type is a float, half or double.</summary>
	public bool IsFloatingPoint => Scalar is ScalarKind.Half or ScalarKind.Float or ScalarKind.Double;

	/// <summary>True when the component type is an int or uint.</summary>
	public bool IsIntegral => Scalar is ScalarKind.Int or ScalarKind.UInt;

	/// <summary>True when the component type is a bool.</summary>
	public bool IsBoolean => Scalar == ScalarKind.Bool;

	/// <summary>True for any texture object.</summary>
	public bool IsTexture => Class == TypeClass.Object && Object is
		ObjectKind.Texture1D or ObjectKind.Texture1DArray or ObjectKind.Texture2D or ObjectKind.Texture2DArray or
		ObjectKind.Texture2DMS or ObjectKind.Texture3D or ObjectKind.TextureCube or ObjectKind.TextureCubeArray or
		ObjectKind.RWTexture2D or ObjectKind.RWTexture3D;

	/// <summary>True for either sampler kind.</summary>
	public bool IsSampler => Class == TypeClass.Object &&
		Object is ObjectKind.SamplerState or ObjectKind.SamplerComparisonState;

	/// <summary>True for a buffer object of any kind.</summary>
	public bool IsBuffer => Class == TypeClass.Object && Object is
		ObjectKind.Buffer or ObjectKind.ByteAddressBuffer or ObjectKind.StructuredBuffer or
		ObjectKind.RWBuffer or ObjectKind.RWStructuredBuffer or ObjectKind.RWByteAddressBuffer;

	/// <summary>True for a writable (UAV) resource. These require the Uav capability and a compute stage.</summary>
	public bool IsWritable => Class == TypeClass.Object && Object is
		ObjectKind.RWTexture2D or ObjectKind.RWTexture3D or ObjectKind.RWBuffer or
		ObjectKind.RWStructuredBuffer or ObjectKind.RWByteAddressBuffer;

	/// <summary>The single-component type behind this one, e.g. <c>float3</c> gives <c>float</c>.</summary>
	/// <remarks>
	/// Hidden from reflection-driven UI. This property returns its own declaring type, and
	/// <see cref="ShaderType"/> is a plain struct with no <c>ControlWidget</c> registered, so a
	/// <c>ControlSheet</c> that meets one — via <c>ConstantNode.ValueType</c> or a blackboard
	/// parameter's type — expands it, then expands the result, forever. That recursion happens inside
	/// engine code and ends as an uncatchable <c>StackOverflowException</c> that kills all of s&amp;box.
	/// A scalar's <c>ScalarType</c> is itself, so the chain never even changes value.
	/// </remarks>
	[Hide, Browsable( false ), JsonIgnore]
	public ShaderType ScalarType => IsNumeric ? Vec( Scalar, 1 ) : Void;

	/// <summary>The same shape with a different component kind.</summary>
	public ShaderType WithScalar( ScalarKind scalar ) => Class switch
	{
		TypeClass.Scalar or TypeClass.Vector => Vec( scalar, Rows ),
		TypeClass.Matrix => Mat( scalar, Rows, Cols ),
		_ => this
	};

	/// <summary>The same component kind widened or narrowed to <paramref name="components"/> (scalars/vectors only).</summary>
	public ShaderType WithComponents( int components ) =>
		IsScalarOrVector ? Vec( Scalar, components ) : this;

	// ---- spelling ---------------------------------------------------------

	/// <summary>How this type is spelled in HLSL.</summary>
	public string Hlsl => Class switch
	{
		TypeClass.Void => "void",
		TypeClass.Scalar => ScalarName( Scalar ),
		TypeClass.Vector => $"{ScalarName( Scalar )}{Rows}",
		TypeClass.Matrix => $"{ScalarName( Scalar )}{Rows}x{Cols}",
		TypeClass.Object => ObjectName( Object ),
		TypeClass.Struct => StructName ?? "void",
		_ => "void"
	};

	/// <summary>
	/// How this type is spelled in Slang. Slang is an HLSL superset, so the spellings match today —
	/// this stays a separate member so the Slang backend can diverge without touching call sites.
	/// </summary>
	public string Slang => Class switch
	{
		TypeClass.Void => "void",
		TypeClass.Scalar => ScalarName( Scalar ),
		TypeClass.Vector => $"{ScalarName( Scalar )}{Rows}",
		TypeClass.Matrix => $"{ScalarName( Scalar )}{Rows}x{Cols}",
		TypeClass.Object => ObjectName( Object ),
		TypeClass.Struct => StructName ?? "void",
		_ => "void"
	};

	/// <inheritdoc/>
	public override string ToString() => Hlsl;

	/// <summary>HLSL keyword for a scalar kind.</summary>
	public static string ScalarName( ScalarKind kind ) => kind switch
	{
		ScalarKind.Bool => "bool",
		ScalarKind.Int => "int",
		ScalarKind.UInt => "uint",
		ScalarKind.Half => "half",
		ScalarKind.Float => "float",
		ScalarKind.Double => "double",
		_ => "void"
	};

	/// <summary>HLSL keyword for an object kind.</summary>
	public static string ObjectName( ObjectKind kind ) => kind switch
	{
		ObjectKind.Texture1D => "Texture1D",
		ObjectKind.Texture1DArray => "Texture1DArray",
		ObjectKind.Texture2D => "Texture2D",
		ObjectKind.Texture2DArray => "Texture2DArray",
		ObjectKind.Texture2DMS => "Texture2DMS",
		ObjectKind.Texture3D => "Texture3D",
		ObjectKind.TextureCube => "TextureCube",
		ObjectKind.TextureCubeArray => "TextureCubeArray",
		ObjectKind.SamplerState => "SamplerState",
		ObjectKind.SamplerComparisonState => "SamplerComparisonState",
		ObjectKind.Buffer => "Buffer",
		ObjectKind.ByteAddressBuffer => "ByteAddressBuffer",
		ObjectKind.StructuredBuffer => "StructuredBuffer",
		ObjectKind.RWTexture2D => "RWTexture2D",
		ObjectKind.RWTexture3D => "RWTexture3D",
		ObjectKind.RWBuffer => "RWBuffer",
		ObjectKind.RWStructuredBuffer => "RWStructuredBuffer",
		ObjectKind.RWByteAddressBuffer => "RWByteAddressBuffer",
		_ => "void"
	};

	// ---- parsing ----------------------------------------------------------

	/// <summary>
	/// Parse a concrete type spelling such as <c>float3</c>, <c>float3x3</c>, <c>bool</c> or <c>Texture2D</c>.
	/// Type variables (<c>T</c>, <c>vecN</c>, <c>any</c>, <c>float{N}</c>) are NOT accepted here — see
	/// <see cref="TypeRules.IsTypeVariable"/>.
	/// </summary>
	public static bool TryParse( string text, out ShaderType type )
	{
		type = Void;
		if ( string.IsNullOrWhiteSpace( text ) ) return false;

		var s = text.Trim();

		// Object types are matched case-insensitively against the canonical spellings.
		foreach ( var kind in s_objectKinds )
		{
			if ( string.Equals( s, ObjectName( kind ), StringComparison.OrdinalIgnoreCase ) )
			{
				type = Obj( kind );
				return true;
			}
		}

		if ( string.Equals( s, "void", StringComparison.OrdinalIgnoreCase ) )
		{
			type = Void;
			return true;
		}

		if ( string.Equals( s, "sampler", StringComparison.OrdinalIgnoreCase ) )
		{
			type = Sampler;
			return true;
		}

		foreach ( var kind in s_scalarKinds )
		{
			var name = ScalarName( kind );
			if ( !s.StartsWith( name, StringComparison.OrdinalIgnoreCase ) ) continue;

			var tail = s[name.Length..];

			if ( tail.Length == 0 )
			{
				type = Vec( kind, 1 );
				return true;
			}

			if ( tail.Length == 1 && tail[0] >= '1' && tail[0] <= '4' )
			{
				type = Vec( kind, tail[0] - '0' );
				return true;
			}

			if ( tail.Length == 3 && tail[1] == 'x' &&
				tail[0] >= '1' && tail[0] <= '4' && tail[2] >= '1' && tail[2] <= '4' )
			{
				type = Mat( kind, tail[0] - '0', tail[2] - '0' );
				return true;
			}
		}

		return false;
	}

	/// <summary>Parse a concrete type spelling, returning <see cref="Void"/> when unrecognised.</summary>
	public static ShaderType Parse( string text ) => TryParse( text, out var t ) ? t : Void;

	static readonly ScalarKind[] s_scalarKinds =
	[
		ScalarKind.Double, ScalarKind.Float, ScalarKind.Half, ScalarKind.UInt, ScalarKind.Int, ScalarKind.Bool
	];

	static readonly ObjectKind[] s_objectKinds =
	[
		ObjectKind.Texture1DArray, ObjectKind.Texture1D, ObjectKind.Texture2DArray, ObjectKind.Texture2DMS,
		ObjectKind.Texture2D, ObjectKind.Texture3D, ObjectKind.TextureCubeArray, ObjectKind.TextureCube,
		ObjectKind.SamplerComparisonState, ObjectKind.SamplerState, ObjectKind.ByteAddressBuffer,
		ObjectKind.StructuredBuffer, ObjectKind.Buffer, ObjectKind.RWTexture2D, ObjectKind.RWTexture3D,
		ObjectKind.RWStructuredBuffer, ObjectKind.RWByteAddressBuffer, ObjectKind.RWBuffer
	];
}