Utility class that lowers implicit type conversions into IR values for the Prism shader compiler in the Editor. It classifies conversions (identity, splat, widen, pad, truncate, int-to-float) and emits appropriate IR builder operations (Cast, Swizzle) or returns Invalid when not convertible.
using Editor.Prism.Compiler.Ir;
using Editor.Prism.Core;
namespace Editor.Prism.Compiler;
/// <summary>
/// The one lowering of an implicit conversion into IR.
/// <para>
/// Two callers need it and used to spell it differently. <c>NodeEmitContext.Coerce</c> lowered a
/// narrowing as a swizzle plus an optional convert; <c>GraphCompiler.Fit</c> lowered the identical
/// conversion as a single <c>CastKind.Truncate</c>, which the HLSL backend only renders as a mask when
/// the scalar kinds already agree and otherwise falls back to a C-style <c>( float3 )v</c>. Both are
/// legal, but two structurally different expressions for one conversion never hash-cons against each
/// other, so CSE missed and the generated text differed between two paths for no reason.
/// </para>
/// <para>
/// This class holds no diagnostics on purpose: reporting a lossy or illegal conversion is the caller's
/// job, because only the caller knows which port to attach it to. An unclassifiable conversion comes
/// back as <see cref="IrValue.Invalid"/>.
/// </para>
/// </summary>
public static class IrConversions
{
/// <summary>
/// Emit the conversion of <paramref name="value"/> to <paramref name="target"/>, or
/// <see cref="IrValue.Invalid"/> when the two types cannot be converted at all.
/// </summary>
/// <param name="builder">The builder to emit into.</param>
/// <param name="value">The value being converted.</param>
/// <param name="target">The type wanted.</param>
/// <param name="fill">What to pad a widening with, or null for <c>TypeRules.DefaultFill</c>.</param>
public static IrValue Emit( IrBuilder builder, IrValue value, ShaderType target, float? fill = null )
{
if ( builder is null || !value.IsValid ) return IrValue.Invalid;
if ( target.IsVoid || value.Type == target ) return value;
var from = value.Type;
switch ( TypeRules.Classify( from, target ) )
{
case ConversionKind.Identity:
return value;
case ConversionKind.Splat:
return builder.Cast( target, value, CastKind.Splat );
case ConversionKind.Widen:
case ConversionKind.IntToFloat:
return builder.Cast( target, value, CastKind.Convert );
case ConversionKind.Pad:
{
var actual = fill ?? TypeRules.DefaultFill( from, target, target.Components - 1 );
// Convert the components before widening, so the pad literal and the existing lanes are
// already the same scalar kind by the time the constructor is printed.
var widened = from.Scalar == target.Scalar
? value
: builder.Cast( from.WithScalar( target.Scalar ), value, CastKind.Convert );
return builder.Cast( target, widened, CastKind.Pad, actual );
}
case ConversionKind.Truncate:
{
var narrowed = value;
if ( from.IsScalarOrVector && target.IsScalarOrVector && from.Components > target.Components )
{
var mask = "xyzw"[..Math.Clamp( target.Components, 1, 4 )];
narrowed = builder.Swizzle( ShaderType.Vec( from.Scalar, target.Components ), value, mask );
}
if ( narrowed.Type == target ) return narrowed;
return builder.Cast( target, narrowed, CastKind.Convert );
}
default:
return IrValue.Invalid;
}
}
}