shaders: fix float codegen for parameter keys (fixes #3250)

Fold scientific-notation exponents into the literal value (was dropped,
producing wrong constants in generated keys and SPIR-V). Emit C# defaults
from the typed AST instead of ToString string-surgery, so scalar/vector
suffixes and type mapping (double->Double, half->float, int->Int) are correct.
This commit is contained in:
Virgile Bello
2026-07-15 01:56:23 +02:00
parent 363228f23c
commit 3456cd1563
6 changed files with 104 additions and 59 deletions
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Stride.Shaders.Core;
using Stride.Shaders.Parsing.Analysis;
using Stride.Shaders.Parsing.SDFX.AST;
@@ -119,52 +120,22 @@ public class EffectCodeWriter : ShaderWriter
}
}
var csharpType = variableType ?? ToCSharpType(parameterType);
Write($"public static readonly {parameterKeyType}ParameterKey<");
if (variableType == null)
VisitNode(new TypeName(parameterType.ToString(), default) { Type = parameterType });
else
Write(variableType);
Write(csharpType);
Write("> ");
Write(name);
Write(" = ");
if (variableMap == null)
{
Write($"ParameterKeys.New{parameterKeyType}<");
if (variableType == null)
VisitNode(new TypeName(parameterType.ToString(), default) { Type = parameterType });
else
Write(variableType);
Write(csharpType);
Write(">(");
if (initialValue != null)
{
var initialValueString = initialValue.ToString()!;
if (initialValueString != "null")
{
var initialValueType = type.ToId();
if (type is ArrayType)
initialValueString = initialValueType + initialValueString;
// Rename float2/3/4 to Vector2/3/4
if (initialValueString.StartsWith("float2", StringComparison.Ordinal) ||
initialValueString.StartsWith("float3", StringComparison.Ordinal) ||
initialValueString.StartsWith("float4", StringComparison.Ordinal))
{
initialValueString = initialValueString.Replace("float", "new Vector");
}
else if (type is ArrayType)
{
initialValueString = "new " + initialValueType;
}
if (isProcessingColor)
{
initialValueString = initialValueString.Replace("Vector3", "Color3");
initialValueString = initialValueString.Replace("Vector4", "Color4");
}
}
Write(initialValueString);
}
// Array value keys use slots and can't carry an array default (NewValue<T> takes a single
// struct), so only scalar/vector/matrix defaults are emitted; array initializers are dropped.
if (initialValue != null && type is not ArrayType && initialValue.ToString() != "null")
WriteInitialValue(initialValue, type);
Write(")");
}
else
@@ -176,6 +147,84 @@ public class EffectCodeWriter : ShaderWriter
isProcessingColor = false;
}
// Maps a resolved shader type to the C# type used for its parameter key and default value.
private string ToCSharpType(SymbolType type) => type switch
{
ScalarType s => ScalarCSharpType(s.Type),
// half cbuffer members are unsupported (reflection rejects them); key as float so the generated C# stays valid.
VectorType { BaseType.Type: Scalar.Float or Scalar.Half, Size: var n } => isProcessingColor && n is 3 or 4 ? $"Color{n}" : $"Vector{n}",
VectorType { BaseType.Type: Scalar.Double, Size: var n } => $"Double{n}",
VectorType { BaseType.Type: Scalar.Int or Scalar.UInt, Size: var n } => $"Int{n}",
MatrixType { Rows: 4, Columns: 4, BaseType.Type: Scalar.Float } => "Matrix",
ArrayType a => $"{ToCSharpType(a.BaseType)}[]",
BufferType or StructuredBufferType or AppendStructuredBufferType or ConsumeStructuredBufferType or ByteAddressBufferType => "Buffer",
TextureType => "Texture",
SamplerType => "SamplerState",
_ => type.ToString(),
};
private static string ScalarCSharpType(Scalar scalar) => scalar switch
{
Scalar.Boolean => "bool",
Scalar.Int => "int",
Scalar.UInt => "uint",
Scalar.Int64 => "long",
Scalar.UInt64 => "ulong",
Scalar.Half or Scalar.Float => "float",
Scalar.Double => "double",
_ => scalar.ToString(),
};
// Emits a constant default value as C#, formatting each numeric literal per its target scalar type.
private void WriteInitialValue(Expression value, SymbolType type)
{
switch (value)
{
case BoolLiteral b:
Write(b.Value ? "true" : "false");
break;
case NumberLiteral number when type is ScalarType scalar:
Write(FormatScalarLiteral(number, scalar.Type));
break;
case VectorLiteral vector when type is VectorType { BaseType: var element }:
Write("new ").Write(ToCSharpType(type)).Write("(");
WriteInitialValueList(vector.Values, element);
Write(")");
break;
case MatrixLiteral matrix when type is MatrixType { BaseType: var element }:
Write("new ").Write(ToCSharpType(type)).Write("(");
WriteInitialValueList(matrix.Values, element);
Write(")");
break;
default:
// Best-effort fallback for shapes we don't model (e.g. a named constant reference).
Write(value.ToString()!);
break;
}
}
private void WriteInitialValueList(List<Expression> values, SymbolType elementType)
{
for (var i = 0; i < values.Count; i++)
{
if (i > 0)
Write(", ");
WriteInitialValue(values[i], elementType);
}
}
// Formats a scalar numeric default as a C# literal with the suffix matching the target scalar type.
private static string FormatScalarLiteral(NumberLiteral number, Scalar scalar) => scalar switch
{
Scalar.Half or Scalar.Float => ((float)number.DoubleValue).ToString(CultureInfo.InvariantCulture) + "f",
Scalar.Double => number.DoubleValue.ToString(CultureInfo.InvariantCulture) + "d",
Scalar.Int => number.LongValue.ToString(CultureInfo.InvariantCulture),
Scalar.UInt => number.ULongValue.ToString(CultureInfo.InvariantCulture) + "u",
Scalar.Int64 => number.LongValue.ToString(CultureInfo.InvariantCulture) + "L",
Scalar.UInt64 => number.ULongValue.ToString(CultureInfo.InvariantCulture) + "UL",
_ => number.DoubleValue.ToString(CultureInfo.InvariantCulture),
};
private bool IsParameterDeclaredInContext(string parameter)
{
foreach (var context in contextStack)
@@ -94,17 +94,16 @@ public partial class IntegerLiteral(Suffix suffix, long value, TextLocation info
// If expectedType is float, handle it:
if (Type is ScalarType { Type: Scalar.Float })
{
return compiler.Context.CompileConstantLiteral(new FloatLiteral(new(32, true, true), Value, null, Info));
return compiler.Context.CompileConstantLiteral(new FloatLiteral(new(32, true, true), Value, Info));
}
return compiler.Context.CompileConstantLiteral(this);
}
}
public sealed partial class FloatLiteral(Suffix suffix, double value, int? exponent, TextLocation info) : NumberLiteral<double>(suffix, value, info)
public sealed partial class FloatLiteral(Suffix suffix, double value, TextLocation info) : NumberLiteral<double>(suffix, value, info)
{
public int? Exponent { get; set; } = exponent;
public static implicit operator FloatLiteral(double v) => new(new(), v, null, new());
public static implicit operator FloatLiteral(double v) => new(new(), v, new());
public override void ProcessSymbol(SymbolTable table, SymbolType? expectedType = null)
{
@@ -67,7 +67,7 @@ public record struct PrefixParser : IParser<Expression>
}
if (op == Operator.Minus && lit is FloatLiteral floatLit)
{
parsed = new FloatLiteral(floatLit.Suffix, -floatLit.DoubleValue, floatLit.Exponent, scanner[position..scanner.Position]);
parsed = new FloatLiteral(floatLit.Suffix, -floatLit.DoubleValue, scanner[position..scanner.Position]);
return true;
}
@@ -88,24 +88,21 @@ public struct NumberParser : IParser<Literal>
var value = double.Parse(scanner.Span[position..scanner.Position], CultureInfo.InvariantCulture);
int? exponent = null;
if (Tokens.Char('e', ref scanner, advance: true) || Tokens.Char('E', ref scanner, advance: true))
{
var signed = Tokens.AnyOf(["+", "-"], ref scanner, out var matched, advance: true);
var expStart = scanner.Position;
Tokens.AnyOf(["+", "-"], ref scanner, out _, advance: true);
if (Tokens.Digit(ref scanner, advance: true))
{
while (Tokens.Digit(ref scanner, advance: true)) ;
exponent = int.Parse(scanner.Span[expStart..scanner.Position], CultureInfo.InvariantCulture);
if (signed && matched == "-")
exponent = -exponent;
// Re-parse the whole mantissa+exponent span so the exponent is folded into the value.
value = double.Parse(scanner.Span[position..scanner.Position], CultureInfo.InvariantCulture);
}
else return Parsers.Exit(ref scanner, result, out parsed, position, new(SDSLErrorMessages.SDSL0001, scanner[scanner.Position], scanner.Memory));
}
if (Tokens.FloatSuffix(ref scanner, out var suffix, advance: true) && suffix is not null)
parsed = new FloatLiteral(suffix.Value, value, exponent, scanner[position..scanner.Position]);
parsed = new FloatLiteral(suffix.Value, value, scanner[position..scanner.Position]);
else
parsed = new FloatLiteral(new(32, true, true), value, exponent, scanner[position..scanner.Position]);
parsed = new FloatLiteral(new(32, true, true), value, scanner[position..scanner.Position]);
return true;
}
public static bool Hex<TScanner>(ref TScanner scanner, ParseResult result, [MaybeNullWhen(false)] out Literal parsed, in ParseError? orError = null)
@@ -609,7 +609,7 @@ public partial class SpirvBuilder
(ScalarType { Type: Scalar.Float }, ScalarType { Type: Scalar.Int }) => InsertData(new OpConvertFToS(context.GetOrRegister(castTypeSameSize), context.Bound++, rowValue)),
(ScalarType { Type: Scalar.Float }, ScalarType { Type: Scalar.UInt }) => InsertData(new OpConvertFToU(context.GetOrRegister(castTypeSameSize), context.Bound++, rowValue)),
(ScalarType { Type: Scalar.Float }, ScalarType { Type: Scalar.Boolean }) => InsertData(new OpFOrdNotEqual(context.GetOrRegister(castTypeSameSize), context.Bound++, rowValue, context.CreateConstantCompositeVectorRepeat(new FloatLiteral(new(32, true, true), 0.0, null, new()), elementSize).Id)),
(ScalarType { Type: Scalar.Float }, ScalarType { Type: Scalar.Boolean }) => InsertData(new OpFOrdNotEqual(context.GetOrRegister(castTypeSameSize), context.Bound++, rowValue, context.CreateConstantCompositeVectorRepeat(new FloatLiteral(new(32, true, true), 0.0, new()), elementSize).Id)),
(ScalarType { Type: Scalar.Int }, ScalarType { Type: Scalar.Boolean }) => InsertData(new OpINotEqual(context.GetOrRegister(castTypeSameSize), context.Bound++, rowValue, context.CreateConstantCompositeVectorRepeat(new IntegerLiteral(new(32, false, true), 0, new()), elementSize).Id)),
(ScalarType { Type: Scalar.UInt }, ScalarType { Type: Scalar.Boolean }) => InsertData(new OpINotEqual(context.GetOrRegister(castTypeSameSize), context.Bound++, rowValue, context.CreateConstantCompositeVectorRepeat(new IntegerLiteral(new(32, false, false), 0, new()), elementSize).Id)),
@@ -620,8 +620,8 @@ public partial class SpirvBuilder
context.CreateConstantCompositeVectorRepeat(new IntegerLiteral(new(32, false, true), 1, new()), elementSize).Id,
context.CreateConstantCompositeVectorRepeat(new IntegerLiteral(new(32, false, true), 0, new()), elementSize).Id)),
(ScalarType { Type: Scalar.Boolean }, ScalarType { Type: Scalar.Float }) => InsertData(new OpSelect(context.GetOrRegister(castTypeSameSize), context.Bound++, rowValue,
context.CreateConstantCompositeVectorRepeat(new FloatLiteral(new(32, true, true), 1.0, null, new()), elementSize).Id,
context.CreateConstantCompositeVectorRepeat(new FloatLiteral(new(32, true, true), 0.0, null, new()), elementSize).Id)),
context.CreateConstantCompositeVectorRepeat(new FloatLiteral(new(32, true, true), 1.0, new()), elementSize).Id,
context.CreateConstantCompositeVectorRepeat(new FloatLiteral(new(32, true, true), 0.0, new()), elementSize).Id)),
// Half conversions (OpFConvert for float<->half<->double, standard int conversions)
(ScalarType { Type: Scalar.Half }, ScalarType { Type: Scalar.Float }) => InsertData(new OpFConvert(context.GetOrRegister(castTypeSameSize), context.Bound++, rowValue)),
@@ -632,10 +632,10 @@ public partial class SpirvBuilder
(ScalarType { Type: Scalar.Half }, ScalarType { Type: Scalar.UInt }) => InsertData(new OpConvertFToU(context.GetOrRegister(castTypeSameSize), context.Bound++, rowValue)),
(ScalarType { Type: Scalar.Int }, ScalarType { Type: Scalar.Half }) => InsertData(new OpConvertSToF(context.GetOrRegister(castTypeSameSize), context.Bound++, rowValue)),
(ScalarType { Type: Scalar.UInt }, ScalarType { Type: Scalar.Half }) => InsertData(new OpConvertUToF(context.GetOrRegister(castTypeSameSize), context.Bound++, rowValue)),
(ScalarType { Type: Scalar.Half }, ScalarType { Type: Scalar.Boolean }) => InsertData(new OpFOrdNotEqual(context.GetOrRegister(castTypeSameSize), context.Bound++, rowValue, context.CreateConstantCompositeVectorRepeat(new FloatLiteral(new(16, true, true), 0.0, null, new()), elementSize).Id)),
(ScalarType { Type: Scalar.Half }, ScalarType { Type: Scalar.Boolean }) => InsertData(new OpFOrdNotEqual(context.GetOrRegister(castTypeSameSize), context.Bound++, rowValue, context.CreateConstantCompositeVectorRepeat(new FloatLiteral(new(16, true, true), 0.0, new()), elementSize).Id)),
(ScalarType { Type: Scalar.Boolean }, ScalarType { Type: Scalar.Half }) => InsertData(new OpSelect(context.GetOrRegister(castTypeSameSize), context.Bound++, rowValue,
context.CreateConstantCompositeVectorRepeat(new FloatLiteral(new(16, true, true), 1.0, null, new()), elementSize).Id,
context.CreateConstantCompositeVectorRepeat(new FloatLiteral(new(16, true, true), 0.0, null, new()), elementSize).Id)),
context.CreateConstantCompositeVectorRepeat(new FloatLiteral(new(16, true, true), 1.0, new()), elementSize).Id,
context.CreateConstantCompositeVectorRepeat(new FloatLiteral(new(16, true, true), 0.0, new()), elementSize).Id)),
// Bitcast (int=>uint or uint=>int)
(ScalarType { Type: Scalar.Int }, ScalarType { Type: Scalar.UInt }) => InsertData(new OpBitcast(context.GetOrRegister(castTypeSameSize), context.Bound++, rowValue)),
@@ -280,8 +280,8 @@ public partial class SpirvContext
uint i => new IntegerLiteral(new(32, false, false), i, location),
long i => new IntegerLiteral(new(64, false, true), i, location),
ulong i => new IntegerLiteral(new(64, false, false), (long)i, location),
float i => new FloatLiteral(new(32, true, true), i, null, location),
double i => new FloatLiteral(new(64, true, true), i, null, location),
float i => new FloatLiteral(new(32, true, true), i, location),
double i => new FloatLiteral(new(64, true, true), i, location),
_ => throw new NotSupportedException($"Unsupported literal type: {value.GetType()}"),
};
}