From 3456cd15633dee540e417092030869d894dfe453 Mon Sep 17 00:00:00 2001 From: Virgile Bello Date: Wed, 15 Jul 2026 01:54:24 +0200 Subject: [PATCH] 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. --- .../EffectCodeWriter.cs | 125 ++++++++++++------ .../Parsing/SDSL/AST/Literals.cs | 7 +- .../ExpressionParsers/UnaryParsers.Prefix.cs | 2 +- .../Parsers/LiteralParsers/NumberParsers.cs | 13 +- .../Spirv/Building/Builder.Expressions.cs | 12 +- .../Spirv/Building/Context.Constants.cs | 4 +- 6 files changed, 104 insertions(+), 59 deletions(-) diff --git a/sources/shaders/Stride.Shaders.Generators/EffectCodeWriter.cs b/sources/shaders/Stride.Shaders.Generators/EffectCodeWriter.cs index 4d6032a76..1bb3ee32b 100644 --- a/sources/shaders/Stride.Shaders.Generators/EffectCodeWriter.cs +++ b/sources/shaders/Stride.Shaders.Generators/EffectCodeWriter.cs @@ -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 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 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) diff --git a/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/Literals.cs b/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/Literals.cs index edb7ec05e..28819eb54 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/Literals.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/AST/Literals.cs @@ -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(suffix, value, info) +public sealed partial class FloatLiteral(Suffix suffix, double value, TextLocation info) : NumberLiteral(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) { diff --git a/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/Parsers/ExpressionParsers/UnaryParsers.Prefix.cs b/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/Parsers/ExpressionParsers/UnaryParsers.Prefix.cs index 6198bc776..8a8f6109c 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/Parsers/ExpressionParsers/UnaryParsers.Prefix.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/Parsers/ExpressionParsers/UnaryParsers.Prefix.cs @@ -67,7 +67,7 @@ public record struct PrefixParser : IParser } 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; } diff --git a/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/Parsers/LiteralParsers/NumberParsers.cs b/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/Parsers/LiteralParsers/NumberParsers.cs index 0a25bf0cc..dbca94701 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/Parsers/LiteralParsers/NumberParsers.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Parsing/SDSL/Parsers/LiteralParsers/NumberParsers.cs @@ -88,24 +88,21 @@ public struct NumberParser : IParser 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(ref TScanner scanner, ParseResult result, [MaybeNullWhen(false)] out Literal parsed, in ParseError? orError = null) diff --git a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.Expressions.cs b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.Expressions.cs index 43c3dcbf6..af1b2d5a3 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.Expressions.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Builder.Expressions.cs @@ -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)), diff --git a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Context.Constants.cs b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Context.Constants.cs index 04f0e413f..0ae986034 100644 --- a/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Context.Constants.cs +++ b/sources/shaders/Stride.Shaders.Parsers/Spirv/Building/Context.Constants.cs @@ -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()}"), }; }