Backend helper class that maps the compiler IR intrinsics, builtins, literals, swizzles and object-transform helpers to HLSL text. It decides spelling, resource-method vs free-function form, stage-based lowers (implicit LOD to SampleLevel), emits calls and literals, provides fallbacks, normalises swizzles and returns helper HLSL snippets for object transforms.
using System.Globalization;
using System.Text;
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
namespace Editor.Prism.Compiler.Backends;
/// <summary>
/// The HLSL spelling of everything the IR can express: intrinsics, builtins, literals and swizzles.
/// <para>
/// This is the only place in the backend that knows what a canonical operation is actually called on
/// the s&box target. Nodes emit <see cref="Intrinsic"/> ids; this table turns them into text,
/// picks the free-function or texture-method form, substitutes stage-legal replacements, and reports
/// the shader model an operation needs so the compiler can say
/// <em>"WaveMatch requires SM 6.5; s&box compiles at SM 6.0 (Vulkan)"</em> instead of letting the
/// user find out from a raw compiler line.
/// </para>
/// </summary>
public static class HlslIntrinsics
{
/// <summary>The shader model the engine actually compiles at.</summary>
public static ShaderModel Target => ShaderModel.Target;
// ---- naming -----------------------------------------------------------
/// <summary>
/// The HLSL spelling of a canonical operation. Falls back to the catalogue name, which is already
/// the HLSL spelling for every operation that has one.
/// </summary>
public static string Spelling( Intrinsic id ) => id switch
{
Intrinsic.AndFn => "and",
Intrinsic.OrFn => "or",
Intrinsic.Select => "select",
Intrinsic.TextureSize => "GetDimensions",
_ => IntrinsicCatalog.Name( id )
};
/// <summary>
/// True when the operation is a member of a texture or buffer object rather than a free function,
/// so it is spelled <c>tex.Sample( s, uv )</c> and the first argument is the object.
/// </summary>
public static bool IsResourceMethod( Intrinsic id ) => id is
Intrinsic.Sample or Intrinsic.SampleLevel or Intrinsic.SampleBias or Intrinsic.SampleGrad or
Intrinsic.SampleCmp or Intrinsic.SampleCmpLevelZero or
Intrinsic.Gather or Intrinsic.GatherRed or Intrinsic.GatherGreen or Intrinsic.GatherBlue or
Intrinsic.GatherAlpha or Intrinsic.GatherCmp or
Intrinsic.Load or
Intrinsic.CalculateLevelOfDetail or Intrinsic.CalculateLevelOfDetailUnclamped;
/// <summary>True when the operation returns nothing and can only appear as a statement.</summary>
public static bool IsVoid( Intrinsic id ) => IntrinsicCatalog.Get( id ).Shape == IntrinsicShape.Void;
// ---- gating -----------------------------------------------------------
/// <summary>
/// True when the operation exists on the s&box compile target. <paramref name="required"/>
/// receives the minimum shader model either way, so the caller can name it in a diagnostic.
/// </summary>
public static bool IsAvailable( Intrinsic id, out ShaderModel required )
{
var info = IntrinsicCatalog.Get( id );
required = info.MinShaderModel;
return required <= Target;
}
/// <summary>
/// Substitute a stage-legal replacement for an operation that cannot run in
/// <paramref name="stage"/>.
/// <para>
/// The only substitutions that preserve meaning are the implicit-LOD samplers: outside the pixel
/// stage there are no screen-space derivatives, so <c>Sample</c> and <c>SampleBias</c> become
/// <c>SampleLevel( ..., 0 )</c>. Derivatives, <c>clip</c> and attribute evaluation have no
/// substitute and must be reported as errors.
/// </para>
/// </summary>
public static bool TryLowerForStage( Intrinsic id, ShaderStage stage, out Intrinsic lowered )
{
lowered = id;
if ( stage.HasDerivatives() ) return false;
if ( IntrinsicCatalog.IsLegalIn( id, stage ) ) return false;
switch ( id )
{
case Intrinsic.Sample:
case Intrinsic.SampleBias:
lowered = Intrinsic.SampleLevel;
return true;
case Intrinsic.SampleCmp:
lowered = Intrinsic.SampleCmpLevelZero;
return true;
default:
return false;
}
}
// ---- call emission ----------------------------------------------------
/// <summary>
/// Render a call to a canonical operation.
/// <para>
/// Handles the free-function and texture-method forms, inserts the explicit mip level when an
/// implicit-LOD sampler has been lowered for a non-pixel stage, and routes
/// <see cref="Intrinsic.TextureSize"/> through the engine's <c>TextureDimensions*</c> helpers
/// because the raw <c>GetDimensions</c> form has out parameters and is not an expression.
/// </para>
/// </summary>
public static string Call( Intrinsic id, IReadOnlyList<string> args, IReadOnlyList<ShaderType> argTypes,
ShaderStage stage, HlslDialect dialect )
{
args ??= Array.Empty<string>();
argTypes ??= Array.Empty<ShaderType>();
if ( id == Intrinsic.TextureSize ) return TextureDimensions( args, argTypes );
var lowered = TryLowerForStage( id, stage, out var replacement );
var effective = lowered ? replacement : id;
var arguments = lowered ? AdjustForLowering( id, effective, args ) : args;
if ( IsResourceMethod( effective ) && arguments.Count >= 1 )
{
var receiver = arguments[0];
var rest = new string[arguments.Count - 1];
for ( int i = 1; i < arguments.Count; i++ ) rest[i - 1] = arguments[i];
return $"{receiver}.{Spelling( effective )}({Arguments( rest )})";
}
// Slang's `and`/`or` accept vectors; strict HLSL 2021 does too, but the operator form reads
// better for scalars and avoids relying on a 2021-only builtin where it is not needed.
if ( dialect == HlslDialect.StrictHlsl2021 && arguments.Count == 2 &&
effective is Intrinsic.AndFn or Intrinsic.OrFn &&
argTypes.Count == 2 && argTypes[0].IsScalar && argTypes[1].IsScalar )
{
var symbol = effective == Intrinsic.AndFn ? "&&" : "||";
return $"( {arguments[0]} {symbol} {arguments[1]} )";
}
return $"{Spelling( effective )}({Arguments( arguments )})";
}
static IReadOnlyList<string> AdjustForLowering( Intrinsic from, Intrinsic to, IReadOnlyList<string> args )
{
// Sample( t, s, uv[, offset] ) -> t.SampleLevel( s, uv, 0.0[, offset] )
// SampleBias( t, s, uv, bias[, o] ) -> t.SampleLevel( s, uv, 0.0[, offset] )
if ( to != Intrinsic.SampleLevel ) return args;
// The texture, the sampler and the coordinate carry over unchanged; the bias, if there was one,
// is dropped because there is no mip chain selection to bias outside the pixel stage.
var result = new List<string>( args.Count + 1 );
for ( int i = 0; i < args.Count && i < 3; i++ ) result.Add( args[i] );
while ( result.Count < 3 ) result.Add( "0.0f" );
result.Add( "0.0f" );
var tail = from == Intrinsic.SampleBias ? 4 : 3;
for ( int i = tail; i < args.Count; i++ ) result.Add( args[i] );
return result;
}
static string TextureDimensions( IReadOnlyList<string> args, IReadOnlyList<ShaderType> argTypes )
{
var texture = args.Count > 0 ? args[0] : "0";
var mip = args.Count > 1 ? args[1] : "0";
var kind = argTypes.Count > 0 ? argTypes[0].Object : ObjectKind.Texture2D;
var helper = kind switch
{
ObjectKind.Texture1D => "TextureDimensions1D",
ObjectKind.Texture3D => "TextureDimensions3D",
ObjectKind.TextureCube => "TextureDimensionsCube",
ObjectKind.TextureCubeArray => "TextureDimensionsCubeArray",
ObjectKind.Texture2DArray => "TextureDimensions2DArray",
_ => "TextureDimensions2D"
};
return $"{helper}( {texture}, {mip} )";
}
static string Arguments( IReadOnlyList<string> args )
{
if ( args is null || args.Count == 0 ) return string.Empty;
var builder = new StringBuilder();
builder.Append( ' ' );
for ( int i = 0; i < args.Count; i++ )
{
if ( i > 0 ) builder.Append( ", " );
builder.Append( args[i] );
}
builder.Append( ' ' );
return builder.ToString();
}
// ---- builtins ---------------------------------------------------------
/// <summary>
/// The expression an environment-provided value lowers to <em>in a given stage</em>.
/// <para>
/// This is the whole reason nodes never see HLSL: world position is <c>i.vPositionWs</c> in the
/// vertex stage but <c>i.vPositionWithOffsetWs + g_vHighPrecisionLightingOffsetWs</c> in the pixel
/// stage, because <c>FinalizeVertex</c> subtracts a camera-relative offset for precision. Getting
/// that wrong is a compile error at best and a silent world-space bug at worst.
/// </para>
/// <para>
/// Returns <c>null</c> when the value has no representation in this stage or domain; the caller
/// reports a diagnostic and substitutes <see cref="Fallback"/>.
/// </para>
/// </summary>
public static string BuiltinExpression( Builtin id, ShaderStage stage, ShaderDomain domain )
{
var vs = stage == ShaderStage.Vertex;
var ps = stage == ShaderStage.Pixel;
var cs = stage == ShaderStage.Compute;
switch ( id )
{
// -- geometry
case Builtin.WorldPosition:
if ( cs ) return null;
return vs
? "i.vPositionWs.xyz"
: "( i.vPositionWithOffsetWs.xyz + g_vHighPrecisionLightingOffsetWs.xyz )";
case Builtin.ObjectPosition:
if ( cs || domain != ShaderDomain.Surface ) return null;
return vs ? "v.vPositionOs.xyz" : "i.vPositionOs.xyz";
case Builtin.ObjectOrigin:
if ( !vs || domain != ShaderDomain.Surface ) return null;
return "mul( GetTransformMatrix( v.nInstanceTransformID, v.nBoneIndex.x ), float4( 0.0, 0.0, 0.0, 1.0 ) )";
// Scale is the length of each column of the object-to-world linear part. That is exact in
// the vertex stage, where the real matrix exists. The pixel stage only ever sees the
// orthonormalised tangent frame, whose scale the vertex program already normalised away, so
// there is nothing honest to return there and the caller reports it.
case Builtin.ObjectScale:
if ( !vs || domain != ShaderDomain.Surface ) return null;
return $"{ObjectScaleFunction}( {SboxShaderTemplates.VertexInputLocal} )";
case Builtin.WorldNormal:
return cs ? null : "i.vNormalWs.xyz";
case Builtin.ObjectNormal:
if ( cs || domain != ShaderDomain.Surface ) return null;
return "i.vNormalOs.xyz";
case Builtin.WorldTangentU:
return cs ? null : "i.vTangentUWs.xyz";
case Builtin.WorldTangentV:
return cs ? null : "i.vTangentVWs.xyz";
case Builtin.ObjectTangentU:
if ( cs || domain != ShaderDomain.Surface ) return null;
return "i.vTangentUOs_flTangentVSign.xyz";
// -- vertex attributes
case Builtin.VertexColor:
if ( cs || domain != ShaderDomain.Surface ) return null;
return "i.vColor";
case Builtin.TintColor:
if ( cs || domain != ShaderDomain.Surface ) return null;
return "i.vTintColor";
case Builtin.TexCoord0:
return cs ? null : "i.vTextureCoords.xy";
case Builtin.TexCoord1:
return cs ? null : "i.vTextureCoords.zw";
// -- screen space
case Builtin.ClipPosition:
if ( cs ) return null;
return vs ? "i.vPositionPs" : "i.vPositionSs";
case Builtin.ScreenUv:
if ( cs ) return null;
return vs
? "CalculateViewportUv( i.vPositionPs.xy )"
: "CalculateViewportUv( i.vPositionSs.xy )";
case Builtin.PixelPosition:
return ps ? "i.vPositionSs.xy" : null;
case Builtin.FragmentDepth:
return ps ? "i.vPositionSs.z" : null;
case Builtin.ViewDirection:
if ( cs ) return null;
return vs
? "CalculatePositionToCameraDirWs( i.vPositionWs.xyz )"
: "CalculatePositionToCameraDirWs( i.vPositionWithOffsetWs.xyz + g_vHighPrecisionLightingOffsetWs.xyz )";
// -- view constants, available everywhere
case Builtin.CameraPosition: return "g_vCameraPositionWs";
case Builtin.CameraForward: return "g_vCameraDirWs";
case Builtin.CameraNear: return "g_flNearPlane";
case Builtin.CameraFar: return "g_flFarPlane";
case Builtin.ViewportSize: return "g_vViewportSize";
case Builtin.ViewportInvSize: return "g_vInvViewportSize";
case Builtin.ViewportOffset: return "g_vViewportOffset";
case Builtin.Time: return "g_flTime";
case Builtin.SunDirection: return "g_DirectionalLightDirection.xyz";
case Builtin.SunColor: return "g_DirectionalLightColor.rgb";
case Builtin.ViewMatrix: return "g_matWorldToView";
case Builtin.ProjectionMatrix: return "g_matViewToProjection";
case Builtin.ViewProjectionMatrix: return "g_matWorldToProjection";
// TODO(WP-5): the engine exposes no per-frame delta or frame counter in
// PerViewConstantBuffer_t; a graph that wants them should declare a render attribute.
case Builtin.DeltaTime:
case Builtin.FrameCount:
return null;
// The engine builds the object transform from the instancing table while the vertex is
// being transformed and never publishes it as a global, so there is one expression per
// stage: the real matrix in the vertex program, and a reconstruction from the interpolated
// tangent frame in the pixel program. See ObjectTransformVertexHelpers.
case Builtin.ObjectToWorld:
if ( cs || domain != ShaderDomain.Surface ) return null;
return vs
? $"{ObjectToWorldFunction}( {SboxShaderTemplates.VertexInputLocal} )"
: $"{ObjectToWorldFunction}( {SboxShaderTemplates.PixelInputLocal} )";
case Builtin.WorldToObject:
if ( cs || domain != ShaderDomain.Surface ) return null;
return vs
? $"{WorldToObjectFunction}( {SboxShaderTemplates.VertexInputLocal} )"
: $"{WorldToObjectFunction}( {SboxShaderTemplates.PixelInputLocal} )";
case Builtin.IsFrontFace:
if ( domain != ShaderDomain.Surface ) return null;
return ps ? "i.vFrontFacing" : "true";
case Builtin.InstanceId:
if ( !vs || domain != ShaderDomain.Surface ) return null;
return "v.nInstanceTransformID";
// common/vertexinput.hlsl declares no vertex-id stream, so the s&box writer takes it as an
// extra SV_VertexID parameter on MainVs — but only for a graph that actually reads it, which
// is why the pixel stage still has to route it through a varying.
case Builtin.VertexId:
return vs ? SboxShaderTemplates.VertexIdParameter : null;
// -- compute
case Builtin.DispatchThreadId: return cs ? "vThreadId" : null;
case Builtin.GroupThreadId: return cs ? "vGroupThreadId" : null;
case Builtin.GroupId: return cs ? "vGroupId" : null;
default:
return null;
}
}
/// <summary>
/// A neutral, well-typed value to substitute when something cannot be expressed. Never a silent
/// wrong answer — the caller always attaches a diagnostic alongside it.
/// </summary>
public static string Fallback( ShaderType type )
{
if ( type.IsVoid ) return "0";
if ( type.IsMatrix ) return type.Rows == type.Cols ? Identity( type ) : $"( {type.Hlsl} )0";
if ( type.IsObject ) return "0";
return Literal( type, ConstValue.Zero );
}
static string Identity( ShaderType type )
{
var builder = new StringBuilder();
builder.Append( type.Hlsl ).Append( "( " );
for ( int r = 0; r < type.Rows; r++ )
{
for ( int c = 0; c < type.Cols; c++ )
{
if ( r > 0 || c > 0 ) builder.Append( ", " );
builder.Append( r == c ? "1.0f" : "0.0f" );
}
}
builder.Append( " )" );
return builder.ToString();
}
// ---- object transform -------------------------------------------------
/// <summary>Name of the generated object-to-world helper.</summary>
public const string ObjectToWorldFunction = "PrismObjectToWorld";
/// <summary>Name of the generated world-to-object helper.</summary>
public const string WorldToObjectFunction = "PrismWorldToObject";
/// <summary>Name of the generated object-scale helper.</summary>
public const string ObjectScaleFunction = "PrismObjectScale";
/// <summary>
/// The vertex-stage object transform helpers.
/// <para>
/// s&box publishes no object-to-world global. The transform lives in the instancing table and
/// <c>GetTransformMatrix</c> is the only way to it, which is why every shipped shader that needs it
/// (<c>bark</c>, <c>foliage</c>, <c>gizmo_line</c>, <c>terrain_brush</c>) reads it in the vertex
/// program from <c>v.nInstanceTransformID</c>. These three helpers wrap that, widen the affine
/// <c>float3x4</c> to the <c>float4x4</c> the IR's type says a transform matrix is, and derive the
/// inverse and the scale from it. All three are exact.
/// </para>
/// </summary>
public const string ObjectTransformVertexHelpers =
"""
// The engine keeps the object transform in the instancing table rather than a constant, so the
// vertex program is the only place it exists. GetTransformMatrix returns the affine 3x4; the
// fourth row of the equivalent 4x4 is the implicit ( 0, 0, 0, 1 ).
float4x4 PrismObjectToWorld( VertexInput v )
{
float3x4 mAffine = GetTransformMatrix( v.nInstanceTransformID );
return float4x4( mAffine[0], mAffine[1], mAffine[2], float4( 0.0, 0.0, 0.0, 1.0 ) );
}
// The transform is affine, so the cofactor inverse of its linear part is exact and far cheaper
// than a general 4x4 inverse. The three cross products are the columns of the adjugate, which is
// why the assembled matrix is transposed.
float4x4 PrismWorldToObject( VertexInput v )
{
float4x4 mObjectToWorld = PrismObjectToWorld( v );
float3x3 mLinear = float3x3( mObjectToWorld[0].xyz, mObjectToWorld[1].xyz, mObjectToWorld[2].xyz );
float3 vTranslation = float3( mObjectToWorld[0].w, mObjectToWorld[1].w, mObjectToWorld[2].w );
float3 vCofactorX = cross( mLinear[1], mLinear[2] );
float3 vCofactorY = cross( mLinear[2], mLinear[0] );
float3 vCofactorZ = cross( mLinear[0], mLinear[1] );
float flDeterminant = dot( mLinear[0], vCofactorX );
float flRcpDeterminant = abs( flDeterminant ) > 1.0e-12 ? 1.0 / flDeterminant : 0.0;
float3x3 mInverse = transpose( float3x3( vCofactorX, vCofactorY, vCofactorZ ) * flRcpDeterminant );
float3 vInverseTranslation = -mul( mInverse, vTranslation );
return float4x4(
float4( mInverse[0], vInverseTranslation.x ),
float4( mInverse[1], vInverseTranslation.y ),
float4( mInverse[2], vInverseTranslation.z ),
float4( 0.0, 0.0, 0.0, 1.0 ) );
}
// Scale is the length of each column of the linear part, which survives any rotation.
float3 PrismObjectScale( VertexInput v )
{
float4x4 mObjectToWorld = PrismObjectToWorld( v );
float3x3 mColumns = transpose( float3x3( mObjectToWorld[0].xyz, mObjectToWorld[1].xyz, mObjectToWorld[2].xyz ) );
return float3( length( mColumns[0] ), length( mColumns[1] ), length( mColumns[2] ) );
}
""";
/// <summary>
/// The pixel-stage object transform helpers.
/// <para>
/// Nothing interpolates a matrix to the pixel program and there is no room to: a
/// <c>float4x4</c> would cost four of the twelve interpolator slots a graph has. What the pixel
/// program does have is the same tangent frame expressed twice — in object space through
/// <c>vNormalOs</c> / <c>vTangentUOs_flTangentVSign</c>, and in world space through
/// <c>vNormalWs</c> / <c>vTangentUWs</c> / <c>vTangentVWs</c> — plus the same position expressed
/// twice. Two orthonormal frames and a point determine a rigid transform exactly, so that is what
/// is rebuilt.
/// </para>
/// <para>
/// <b>The limit is scale.</b> The vertex program normalises the world-space frame, so the
/// reconstruction recovers rotation and translation exactly and always reports unit scale. That is
/// why <see cref="Builtin.ObjectScale"/> deliberately has no pixel-stage expression: an
/// interpolated frame simply does not carry the answer, and returning 1 would be a silent lie.
/// </para>
/// </summary>
public const string ObjectTransformPixelHelpers =
"""
// No matrix is interpolated, so the transform is rebuilt from the object-space and world-space
// tangent frames the vertex program already passes through. Exact for rotation and translation;
// scale was normalised away in the vertex program, so this always reports unit scale.
float4x4 PrismObjectToWorld( PixelInput i )
{
float3 vTangentOs = i.vTangentUOs_flTangentVSign.xyz;
float3 vBitangentOs = cross( i.vNormalOs.xyz, vTangentOs ) * i.vTangentUOs_flTangentVSign.w;
float3x3 mObjectFrame = float3x3( vTangentOs, vBitangentOs, i.vNormalOs.xyz );
float3x3 mWorldFrame = float3x3( i.vTangentUWs.xyz, i.vTangentVWs.xyz, i.vNormalWs.xyz );
float3x3 mRotation = mul( transpose( mWorldFrame ), mObjectFrame );
float3 vPositionWs = i.vPositionWithOffsetWs.xyz + g_vHighPrecisionLightingOffsetWs.xyz;
float3 vTranslation = vPositionWs - mul( mRotation, i.vPositionOs.xyz );
return float4x4(
float4( mRotation[0], vTranslation.x ),
float4( mRotation[1], vTranslation.y ),
float4( mRotation[2], vTranslation.z ),
float4( 0.0, 0.0, 0.0, 1.0 ) );
}
// The reconstructed rotation is orthonormal, so its inverse is its transpose.
float4x4 PrismWorldToObject( PixelInput i )
{
float4x4 mObjectToWorld = PrismObjectToWorld( i );
float3x3 mRotation = float3x3( mObjectToWorld[0].xyz, mObjectToWorld[1].xyz, mObjectToWorld[2].xyz );
float3 vTranslation = float3( mObjectToWorld[0].w, mObjectToWorld[1].w, mObjectToWorld[2].w );
float3x3 mInverse = transpose( mRotation );
float3 vInverseTranslation = -mul( mInverse, vTranslation );
return float4x4(
float4( mInverse[0], vInverseTranslation.x ),
float4( mInverse[1], vInverseTranslation.y ),
float4( mInverse[2], vInverseTranslation.z ),
float4( 0.0, 0.0, 0.0, 1.0 ) );
}
""";
/// <summary>
/// The object transform helper bodies a stage needs, or null when that stage has none. Only the
/// surface domain has an object space at all; a post-process or compute program has no mesh.
/// </summary>
public static string ObjectTransformHelpers( ShaderStage stage, ShaderDomain domain )
{
if ( domain != ShaderDomain.Surface ) return null;
return stage switch
{
ShaderStage.Vertex => ObjectTransformVertexHelpers,
ShaderStage.Pixel => ObjectTransformPixelHelpers,
_ => null
};
}
// ---- literals ---------------------------------------------------------
/// <summary>
/// Spell a literal. Formatting is round-trip exact and culture invariant, because
/// byte-identical regeneration of an unchanged graph is what makes the "text changed?"
/// short-circuit before recompiling reliable.
/// </summary>
public static string Literal( ShaderType type, ConstValue value )
{
if ( type.IsMatrix )
{
// ConstValue holds four components; a real matrix literal cannot round-trip through it.
return $"( {type.Hlsl} ){Number( value.X, type.Scalar )}";
}
var components = Math.Clamp( type.Components, 1, 4 );
if ( components == 1 ) return Number( value[0], type.Scalar );
var builder = new StringBuilder();
builder.Append( type.Hlsl ).Append( "( " );
for ( int i = 0; i < components; i++ )
{
if ( i > 0 ) builder.Append( ", " );
builder.Append( Number( value[i], type.Scalar ) );
}
builder.Append( " )" );
return builder.ToString();
}
/// <summary>Spell one scalar component of a literal.</summary>
public static string Number( double value, ScalarKind kind )
{
switch ( kind )
{
case ScalarKind.Bool:
return value != 0 ? "true" : "false";
case ScalarKind.Int:
return ( (long)Math.Round( ClampFinite( value, int.MinValue, int.MaxValue ) ) )
.ToString( CultureInfo.InvariantCulture );
case ScalarKind.UInt:
return ( (ulong)Math.Round( ClampFinite( value, 0, uint.MaxValue ) ) )
.ToString( CultureInfo.InvariantCulture ) + "u";
case ScalarKind.Double:
return Decimalise( ClampFinite( value, double.MinValue, double.MaxValue )
.ToString( "R", CultureInfo.InvariantCulture ) );
default:
var single = (float)ClampFinite( value, float.MinValue, float.MaxValue );
return Decimalise( single.ToString( "R", CultureInfo.InvariantCulture ) ) + "f";
}
}
static double ClampFinite( double value, double min, double max )
{
if ( double.IsNaN( value ) ) return 0;
if ( double.IsPositiveInfinity( value ) ) return max;
if ( double.IsNegativeInfinity( value ) ) return min;
return Math.Clamp( value, min, max );
}
static string Decimalise( string text )
{
if ( text.IndexOf( '.' ) >= 0 || text.IndexOf( 'E' ) >= 0 || text.IndexOf( 'e' ) >= 0 ) return text;
return text + ".0";
}
// ---- swizzles ---------------------------------------------------------
/// <summary>
/// Normalise a swizzle mask to the <c>xyzw</c> set. HLSL rejects a mask that mixes
/// <c>xyzw</c> with <c>rgba</c>, and a node that built one from two sources would otherwise fail
/// in the compiler rather than here.
/// </summary>
public static string NormalizeSwizzle( string mask )
{
if ( string.IsNullOrEmpty( mask ) ) return string.Empty;
var builder = new StringBuilder( mask.Length );
foreach ( var c in mask )
{
builder.Append( char.ToLowerInvariant( c ) switch
{
'x' or 'r' or 's' => 'x',
'y' or 'g' or 't' => 'y',
'z' or 'b' or 'p' => 'z',
'w' or 'a' or 'q' => 'w',
_ => 'x'
} );
}
return builder.ToString();
}
/// <summary>The leading <c>.xyzw</c> mask for a component count, e.g. 3 gives <c>xyz</c>.</summary>
public static string LeadingMask( int components ) => Math.Clamp( components, 1, 4 ) switch
{
1 => "x",
2 => "xy",
3 => "xyz",
_ => "xyzw"
};
// ---- declarations -----------------------------------------------------
/// <summary>
/// The naming prefix the engine's own shaders and the built-in generator use for a type. Offered
/// so parameter lowering can mint conventional names; the backend never renames a declaration it
/// is handed, because <see cref="IrGlobalRef"/> already refers to it.
/// </summary>
public static string SuggestedPrefix( ShaderType type ) => GraphCompiler.SymbolPrefix( type );
/// <summary>
/// The HLSL type a module-level declaration is written with.
/// <para>
/// Read-only textures default their element type to <c>float4</c>, but writable textures and every
/// buffer flavour <em>require</em> the template argument, so it is supplied here rather than left
/// to produce a bare <c>RWTexture2D</c> the compiler rejects.
/// </para>
/// </summary>
public static string DeclarationType( GlobalDecl decl )
{
if ( decl is null ) return "float";
var type = decl.Type;
if ( type.IsObject )
{
return RequiresElementType( type.Object ) ? $"{type.Hlsl}<float4>" : type.Hlsl;
}
return decl.Kind switch
{
GlobalKind.Buffer => $"StructuredBuffer<{type.Hlsl}>",
GlobalKind.RwBuffer => $"RWStructuredBuffer<{type.Hlsl}>",
GlobalKind.RwTexture => $"RWTexture2D<{type.Hlsl}>",
_ => type.Hlsl
};
}
/// <summary>True when an opaque resource type cannot be spelled without a template argument.</summary>
public static bool RequiresElementType( ObjectKind kind ) => kind is
ObjectKind.Buffer or ObjectKind.StructuredBuffer or ObjectKind.Texture2DMS or
ObjectKind.RWBuffer or ObjectKind.RWStructuredBuffer or
ObjectKind.RWTexture2D or ObjectKind.RWTexture3D;
}