diff --git a/src/ShaderGen.Primitives/ShaderBuiltins.cs b/src/ShaderGen.Primitives/ShaderBuiltins.cs index 7a10fa5..50fc056 100644 --- a/src/ShaderGen.Primitives/ShaderBuiltins.cs +++ b/src/ShaderGen.Primitives/ShaderBuiltins.cs @@ -1,5 +1,6 @@ using System; using System.Numerics; +using System.Runtime.CompilerServices; namespace ShaderGen { @@ -100,6 +101,7 @@ public static float SampleComparisonLevelZero(DepthTexture2DArrayResource textur public static Vector4 Acos(Vector4 value) => new Vector4((float)Math.Acos(value.X), (float)Math.Acos(value.Y), (float)Math.Acos(value.Z), (float)Math.Acos(value.W)); // Acosh + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float Acosh(float value) => (float)Math.Log(value + Math.Sqrt(value * value - 1.0)); public static Vector2 Acosh(Vector2 value) => new Vector2(Acosh(value.X), Acosh(value.Y)); public static Vector3 Acosh(Vector3 value) => new Vector3(Acosh(value.X), Acosh(value.Y), Acosh(value.Z)); @@ -112,6 +114,7 @@ public static float SampleComparisonLevelZero(DepthTexture2DArrayResource textur public static Vector4 Asin(Vector4 value) => new Vector4((float)Math.Asin(value.X), (float)Math.Asin(value.Y), (float)Math.Asin(value.Z), (float)Math.Asin(value.W)); // Asinh + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float Asinh(float value) => (float)Math.Log(value + Math.Sqrt(value * value + 1.0)); public static Vector2 Asinh(Vector2 value) => new Vector2(Asinh(value.X), Asinh(value.Y)); public static Vector3 Asinh(Vector3 value) => new Vector3(Asinh(value.X), Asinh(value.Y), Asinh(value.Z)); @@ -128,6 +131,7 @@ public static float SampleComparisonLevelZero(DepthTexture2DArrayResource textur public static Vector4 Atan(Vector4 y, Vector4 x) => new Vector4((float)Math.Atan2(y.X, x.X), (float)Math.Atan2(y.Y, x.Y), (float)Math.Atan2(y.Z, x.Z), (float)Math.Atan2(y.W, x.W)); // Atanh + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float Atanh(float value) => (float)(Math.Log((1.0f + value) / (1.0f - value)) / 2.0f); public static Vector2 Atanh(Vector2 value) => new Vector2(Atanh(value.X), Atanh(value.Y)); public static Vector3 Atanh(Vector3 value) => new Vector3(Atanh(value.X), Atanh(value.Y), Atanh(value.Z)); @@ -135,7 +139,8 @@ public static float SampleComparisonLevelZero(DepthTexture2DArrayResource textur // Cbrt TODO add Matrix support private const double _third = 1.0 / 3.0; - public static float Cbrt(float value) => (float)Math.Pow(value, _third); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Cbrt(float value) => (float)Math.Pow(Math.Abs(value), _third); public static Vector2 Cbrt(Vector2 value) => new Vector2(Cbrt(value.X), Cbrt(value.Y)); public static Vector3 Cbrt(Vector3 value) => new Vector3(Cbrt(value.X), Cbrt(value.Y), Cbrt(value.Z)); public static Vector4 Cbrt(Vector4 value) => new Vector4(Cbrt(value.X), Cbrt(value.Y), Cbrt(value.Z), Cbrt(value.W)); @@ -147,9 +152,8 @@ public static float SampleComparisonLevelZero(DepthTexture2DArrayResource textur public static Vector4 Ceiling(Vector4 value) => new Vector4((float)Math.Ceiling(value.X), (float)Math.Ceiling(value.Y), (float)Math.Ceiling(value.Z), (float)Math.Ceiling(value.W)); // Clamp TODO add int & uint versions (see https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/clamp.xhtml) - public static float Clamp(float value, float min, float max) => min >= max - ? float.NaN - : Math.Min(Math.Max(value, min), max); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Clamp(float value, float min, float max) => Math.Min(Math.Max(value, min), max); public static Vector2 Clamp(Vector2 value, Vector2 min, Vector2 max) => new Vector2(Clamp(value.X, min.X, max.X), Clamp(value.Y, min.Y, max.Y)); public static Vector2 Clamp(Vector2 value, float min, float max) => new Vector2(Clamp(value.X, min, max), Clamp(value.Y, min, max)); public static Vector3 Clamp(Vector3 value, Vector3 min, Vector3 max) => new Vector3(Clamp(value.X, min.X, max.X), Clamp(value.Y, min.Y, max.Y), Clamp(value.Z, min.Z, max.Z)); @@ -181,14 +185,27 @@ public static float Clamp(float value, float min, float max) => min >= max public static Vector3 Floor(Vector3 value) => new Vector3((float)Math.Floor(value.X), (float)Math.Floor(value.Y), (float)Math.Floor(value.Z)); public static Vector4 Floor(Vector4 value) => new Vector4((float)Math.Floor(value.X), (float)Math.Floor(value.Y), (float)Math.Floor(value.Z), (float)Math.Floor(value.W)); + + // FMod - See https://stackoverflow.com/questions/7610631/glsl-mod-vs-hlsl-fmod + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float FMod(float a, float b) => a % b; + public static Vector2 FMod(Vector2 a, Vector2 b) => new Vector2(FMod(a.X, b.X), FMod(a.Y, b.Y)); + public static Vector2 FMod(Vector2 a, float b) => new Vector2(FMod(a.X, b), FMod(a.Y, b)); + public static Vector3 FMod(Vector3 a, Vector3 b) => new Vector3(FMod(a.X, b.X), FMod(a.Y, b.Y), FMod(a.Z, b.Z)); + public static Vector3 FMod(Vector3 a, float b) => new Vector3(FMod(a.X, b), FMod(a.Y, b), FMod(a.Z, b)); + public static Vector4 FMod(Vector4 a, Vector4 b) => new Vector4(FMod(a.X, b.X), FMod(a.Y, b.Y), FMod(a.Z, b.Z), FMod(a.W, b.W)); + public static Vector4 FMod(Vector4 a, float b) => new Vector4(FMod(a.X, b), FMod(a.Y, b), FMod(a.Z, b), FMod(a.W, b)); + // Frac TODO Check this really is equivalent + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float Frac(float value) => (float)(value - Math.Floor(value)); public static Vector2 Frac(Vector2 value) => new Vector2(Frac(value.X), Frac(value.Y)); public static Vector3 Frac(Vector3 value) => new Vector3(Frac(value.X), Frac(value.Y), Frac(value.Z)); public static Vector4 Frac(Vector4 value) => new Vector4(Frac(value.X), Frac(value.Y), Frac(value.Z), Frac(value.W)); // Lerp - public static float Lerp(float x, float y, float s) => s < 0f || s > 1f ? float.NaN : x * (1f - s) + y * s; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Lerp(float x, float y, float s) => x * (1f - s) + y * s; public static Vector2 Lerp(Vector2 x, Vector2 y, Vector2 s) => new Vector2(Lerp(x.X, y.X, s.X), Lerp(x.Y, y.Y, s.Y)); public static Vector2 Lerp(Vector2 x, Vector2 y, float s) => new Vector2(Lerp(x.X, y.X, s), Lerp(x.Y, y.Y, s)); public static Vector3 Lerp(Vector3 x, Vector3 y, Vector3 s) => new Vector3(Lerp(x.X, y.X, s.X), Lerp(x.Y, y.Y, s.Y), Lerp(x.Z, y.Z, s.Z)); @@ -198,6 +215,7 @@ public static float Clamp(float value, float min, float max) => min >= max // Log public static float Log(float value) => (float)Math.Log(value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float Log(float a, float newBase) => (float)Math.Log(a, newBase); public static Vector2 Log(Vector2 value) => new Vector2((float)Math.Log(value.X), (float)Math.Log(value.Y)); public static Vector2 Log(Vector2 a, Vector2 newBase) => new Vector2(Log(a.X, newBase.X), Log(a.Y, newBase.Y)); @@ -210,6 +228,7 @@ public static float Clamp(float value, float min, float max) => min >= max public static Vector4 Log(Vector4 a, float newBase) => new Vector4(Log(a.X, newBase), Log(a.Y, newBase), Log(a.Z, newBase), Log(a.W, newBase)); // Log2 + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float Log2(float value) => Log(value, 2f); public static Vector2 Log2(Vector2 value) => new Vector2(Log2(value.X), Log2(value.Y)); public static Vector3 Log2(Vector3 value) => new Vector3(Log2(value.X), Log2(value.Y), Log2(value.Z)); @@ -247,8 +266,9 @@ public static float Clamp(float value, float min, float max) => min >= max m.M14 * v.X + m.M24 * v.Y + m.M34 * v.Z + m.M44 * v.W ); - // Mod TODO: See https://stackoverflow.com/questions/7610631/glsl-mod-vs-hlsl-fmod - public static float Mod(float a, float b) => a % b; // CHECK! + // Mod - See https://stackoverflow.com/questions/7610631/glsl-mod-vs-hlsl-fmod + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Mod(float a, float b) => a - b * (float)Math.Floor(a / b); public static Vector2 Mod(Vector2 a, Vector2 b) => new Vector2(Mod(a.X, b.X), Mod(a.Y, b.Y)); public static Vector2 Mod(Vector2 a, float b) => new Vector2(Mod(a.X, b), Mod(a.Y, b)); public static Vector3 Mod(Vector3 a, Vector3 b) => new Vector3(Mod(a.X, b.X), Mod(a.Y, b.Y), Mod(a.Z, b.Z)); @@ -257,10 +277,11 @@ public static float Clamp(float value, float min, float max) => min >= max public static Vector4 Mod(Vector4 a, float b) => new Vector4(Mod(a.X, b), Mod(a.Y, b), Mod(a.Z, b), Mod(a.W, b)); // Pow - public static float Pow(float x, float y) => (float)Math.Pow(x, y); - public static Vector2 Pow(Vector2 y, Vector2 x) => new Vector2((float)Math.Pow(y.X, x.X), (float)Math.Pow(y.Y, x.Y)); - public static Vector3 Pow(Vector3 y, Vector3 x) => new Vector3((float)Math.Pow(y.X, x.X), (float)Math.Pow(y.Y, x.Y), (float)Math.Pow(y.Z, x.Z)); - public static Vector4 Pow(Vector4 y, Vector4 x) => new Vector4((float)Math.Pow(y.X, x.X), (float)Math.Pow(y.Y, x.Y), (float)Math.Pow(y.Z, x.Z), (float)Math.Pow(y.W, x.W)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Pow(float x, float y) => (float)Math.Pow(Math.Abs(x), y); + public static Vector2 Pow(Vector2 y, Vector2 x) => new Vector2((float)Pow(y.X, x.X), Pow(y.Y, x.Y)); + public static Vector3 Pow(Vector3 y, Vector3 x) => new Vector3(Pow(y.X, x.X), Pow(y.Y, x.Y), Pow(y.Z, x.Z)); + public static Vector4 Pow(Vector4 y, Vector4 x) => new Vector4(Pow(y.X, x.X), Pow(y.Y, x.Y), Pow(y.Z, x.Z), Pow(y.W, x.W)); // Round public static float Round(float value) => (float)Math.Round(value); @@ -293,6 +314,7 @@ public static float Clamp(float value, float min, float max) => min >= max public static Vector4 Sqrt(Vector4 value) => new Vector4((float)Math.Sqrt(value.X), (float)Math.Sqrt(value.Y), (float)Math.Sqrt(value.Z), (float)Math.Sqrt(value.W)); // SmoothStep + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float SmoothStep(float min, float max, float x) { // From https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/smoothstep.xhtml @@ -301,11 +323,6 @@ public static float SmoothStep(float min, float max, float x) * return t * t * (3.0 - 2.0 * t); * Results are undefined if min ≥ max. */ - if (min >= max) - { - return float.NaN; - } - float t = Saturate((x - min) / (max - min)); return t * t * (3f - 2f * t); } diff --git a/src/ShaderGen.Tests/AutoGenerated/BuiltinsTests.cs b/src/ShaderGen.Tests/AutoGenerated/BuiltinsTests.cs new file mode 100644 index 0000000..b74369c --- /dev/null +++ b/src/ShaderGen.Tests/AutoGenerated/BuiltinsTests.cs @@ -0,0 +1,405 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using ShaderGen.Tests.Tools; +using TestShaders; +using Veldrid; +using Xunit; +using Xunit.Abstractions; + +namespace ShaderGen.Tests.AutoGenerated +{ + public class BuiltinsTests + { + #region Test Configuration + + /// + /// The skip reason, set to to enable tests in class. + /// + private const string SkipReason = null; + + /// + /// The number of failure examples to output. + /// + private const int FailureExamples = 5; + + /// + /// Controls the minimum mantissa when generating a floating point number (how 'small' it can go) + /// + /// To test all valid floats this should be set to -126. + private static readonly int MinMantissa = -3; + + /// + /// Controls the maximum mantissa when generating a floating point number (how 'big' it can go) + /// + /// To test all valid floats this should be set to 128. + private static readonly int MaxMantissa = 3; + + /// + /// The float epsilon is used to indicate how close two floats need to be to be considered approximately equal. + /// + private float FloatEpsilon = 1f; + + /// + /// The methods to exclude from + /// + /// TODO See #78 to show why this is another reason to split ShaderBuiltins. + private static readonly HashSet _gpuOnly = new HashSet + { + nameof(ShaderBuiltins.Sample), + nameof(ShaderBuiltins.SampleGrad), + nameof(ShaderBuiltins.Load), + nameof(ShaderBuiltins.Store), + nameof(ShaderBuiltins.SampleComparisonLevelZero), + nameof(ShaderBuiltins.Discard), + nameof(ShaderBuiltins.ClipToTextureCoordinates), + nameof(ShaderBuiltins.Ddx), + nameof(ShaderBuiltins.DdxFine), + nameof(ShaderBuiltins.Ddy), + nameof(ShaderBuiltins.DdyFine), + nameof(ShaderBuiltins.InterlockedAdd) + }; + + /// + /// Gets the methods to test. + /// + /// + /// The methods to test. + /// + private IEnumerable MethodsToTest => typeof(ShaderBuiltins) + .GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public) + .Where(m => !_gpuOnly.Contains(m.Name) && !m.IsSpecialName) + .OrderBy(m => m.Name); + + /// + /// The number of test iterations for each backend. + /// + private const int TestLoops = 1000; + + #endregion + + + /// + /// The output stream for tests. + /// + private readonly ITestOutputHelper _output; + + /// + /// Initializes a new instance of the class. + /// + /// The output. + public BuiltinsTests(ITestOutputHelper output) + { + _output = output; + } + + [SkippableFact(typeof(RequiredToolFeatureMissingException), Skip = SkipReason)] + private void TestBuiltins() + { + // Find all backends that can create a headless graphics device on this system. + IReadOnlyList toolChains = ToolChain.Requires(ToolFeatures.HeadlessGraphicsDevice, false); + if (toolChains.Count < 1) + { + throw new RequiredToolFeatureMissingException( + $"At least one tool chain capable of creating headless graphics devices is required for this test!"); + } + + /* + * Auto-generate C# code for testing methods. + */ + IReadOnlyList methods = null; + Mappings mappings; + Compilation compilation; + using (new TestTimer(_output, () => $"Generating C# shader code to test {methods.Count} methods")) + { + // Get all the methods we wish to test + methods = MethodsToTest.ToArray(); + mappings = CreateMethodTestCompilation(methods, out compilation); + + // Note, you could use compilation.Emit(...) at this point to compile the auto-generated code! + // however, for now we'll invoke methods directly rather than executing the C# code that has been + // generated, as loading emitted code into a test is currently much more difficult. + } + + // Allocate enough space to store the result sets for each backend! + TestSets testSets = null; + using (new TestTimer( + _output, + () => + $"Generating random test data ({(mappings.BufferSize * testSets.TestLoops).ToMemorySize()}) for {testSets.TestLoops} iterations of {mappings.Methods} methods") + ) + { + testSets = new TestSets(toolChains, compilation, mappings, TestLoops, MinMantissa, MaxMantissa); + } + + /* + * Transpile shaders + */ + + ShaderGenerationResult generationResult; + + using (new TestTimer( + _output, + t => + $"Generated shader sets for {string.Join(", ", toolChains.Select(tc => tc.Name))} backends in {t * 1000:#.##}ms.") + ) + { + ShaderGenerator sg = new ShaderGenerator( + compilation, + testSets.Select(t => t.Backend).Where(b => b != null).ToArray(), + null, + null, + "ComputeShader.CS"); + + generationResult = sg.GenerateShaders(); + } + + /* + * Loop through each backend to run tests. + */ + bool first = true; + using (new TestTimer(_output, "Executing all tests on all backends")) + { + foreach (TestSet testSet in testSets) + { + _output.WriteLine(string.Empty); + if (first) + { + // This is the first test set, so we use Space1 instead of Spacer 2. + first = false; + _output.WriteLine(TestUtil.Spacer1); + } + else + { + _output.WriteLine(TestUtil.Spacer2); + } + + _output.WriteLine(String.Empty); + + testSet.Execute(generationResult, "CS", _output); + } + + _output.WriteLine(string.Empty); + } + + _output.WriteLine(string.Empty); + _output.WriteLine(TestUtil.Spacer1); + _output.WriteLine(string.Empty); + + Assert.True(testSets.Count(t => t.Executed) > 1, + "At least 2 test sets are required for comparison."); + + /* + * Finally, evaluate differences between results + */ + IReadOnlyList<(MethodMap MethodMap, IReadOnlyList Failures)> failures; + using (new TestTimer(_output, "Analysing results for failures")) + { + failures = testSets.GetFailures(FloatEpsilon) + .Select(kvp => (MethodMap: kvp.Key, Failures: kvp.Value)) + .OrderByDescending(kvp => kvp.Failures.Count) + .ToArray(); + } + + if (!failures.Any()) + { + _output.WriteLine("No failures detected!"); + return; + } + + _output.WriteLine( + $"{failures.Count} methods had failures out of {mappings.Methods} ({100.0 * failures.Count / mappings.Methods:#.##}%)."); + + _output.WriteLine(string.Empty); + + // Get pointer array + string lastMethodName = null; + foreach ((MethodMap methodMap, IReadOnlyList methodFailures) in failures) + { + if (lastMethodName != methodMap.Method.Name) + { + if (lastMethodName != null) + { + // Seperate methods of different names with spacer 1 + _output.WriteLine(string.Empty); + _output.WriteLine(TestUtil.Spacer1); + _output.WriteLine(string.Empty); + } + + lastMethodName = methodMap.Method.Name; + } + else + { + _output.WriteLine(string.Empty); + _output.WriteLine(TestUtil.Spacer2); + _output.WriteLine(string.Empty); + } + + int failureCount = methodFailures.Count; + _output.WriteLine( + $"{TestUtil.GetUnicodePieChart((double)failureCount / testSets.TestLoops)} {methodMap.Signature} failed {failureCount}/{testSets.TestLoops} ({failureCount * 100.0 / testSets.TestLoops:#.##}%)."); + + // Output examples! + int example = 0; + foreach (Failure failure in methodFailures) + { + _output.WriteLine(string.Empty); + if (example++ >= FailureExamples) + { + _output.WriteLine("…"); + break; + } + + _output.WriteLine(failure.ToString()); + } + } + + _output.WriteLine(string.Empty); + _output.WriteLine(TestUtil.Spacer2); + _output.WriteLine(string.Empty); + } + + /// + /// Creates the method test compilation. + /// + /// The methods. + /// The compilation. + /// + private Mappings CreateMethodTestCompilation(IReadOnlyCollection methods, + out Compilation compilation) + { + Assert.NotNull(methods); + Assert.NotEmpty(methods); + + // Create compilation + CSharpCompilationOptions cSharpCompilationOptions = + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true); + compilation = CSharpCompilation.Create( + "TestAssembly", + null, + TestUtil.ProjectReferences, + cSharpCompilationOptions); + + // Temporary structure to hold method maps until after we calculate input structure. + var methodMaps = + new(int Index, MethodInfo Method, IReadOnlyDictionary Parameters, string + ReturnField)[methods.Count]; + + PaddedStructCreator paddedStructCreator = new PaddedStructCreator(compilation); + + StringBuilder codeBuilder = new StringBuilder(); + codeBuilder.Append(SBSP1); + codeBuilder.Append(methods.Count); + codeBuilder.Append(SBSP2); + + StringBuilder argsBuilder = new StringBuilder(); + /* + * Output test cases + */ + int methodNumber = 0; + foreach (MethodInfo method in methods) + { + Assert.True(method.IsStatic); + + ParameterInfo[] parameterInfos = method.GetParameters(); + Dictionary parameterMap = + new Dictionary(parameterInfos.Length); + + foreach (ParameterInfo parameterInfo in parameterInfos) + { + if (argsBuilder.Length > 0) + { + argsBuilder.Append(","); + } + + string fieldName = paddedStructCreator.GetFieldName(parameterInfo.ParameterType); + parameterMap.Add(parameterInfo, fieldName); + argsBuilder.Append(SBSParam.Replace("$$NAME$$", fieldName)); + } + + string returnName = method.ReturnType != typeof(void) + ? paddedStructCreator.GetFieldName(method.ReturnType) + : null; + + string output = returnName != null + ? SBSParam.Replace("$$NAME$$", returnName) + " = " + : string.Empty; + + codeBuilder.Append(SBSCase + .Replace("$$CASE$$", methodNumber.ToString()) + .Replace("$$RESULT$$", output) + .Replace("$$METHOD$$", $"{method.DeclaringType.FullName}.{method.Name}") + .Replace("$$ARGS$$", argsBuilder.ToString())); + + methodMaps[methodNumber] = (methodNumber++, method, parameterMap, returnName); + paddedStructCreator.Reset(); + argsBuilder.Clear(); + } + + codeBuilder.Append(SBSP3); + + /* + * Output test fields + */ + IReadOnlyList fields = paddedStructCreator.GetFields(out int bufferSize); + int size = 0; + foreach (PaddedStructCreator.Field field in fields) + { + codeBuilder.AppendLine( + $" // {size,3}: Alignment = {field.AlignmentInfo.ShaderAlignment} {(field.IsPaddingField ? " [PADDING}" : string.Empty)}"); + codeBuilder.AppendLine( + $" {(field.IsPaddingField ? "private" : "public")} {field.Type.FullName} {field.Name};"); + codeBuilder.AppendLine(string.Empty); + size += field.AlignmentInfo.ShaderSize; + } + + Assert.Equal(size, bufferSize); + + codeBuilder.Append(SBSP4); + + string code = codeBuilder.ToString(); + compilation = compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(code)); + return new Mappings(bufferSize, fields.ToDictionary(f => f.Name), methodMaps); + } + + #region Code Building Strings + private static readonly string SBSP1 = @"public class ComputeShader + { + public const uint Methods = "; + private static readonly string SBSP2 = @"; + + [ShaderGen.ResourceTestSet(0)] public ShaderGen.RWStructuredBuffer InOutParameters; + + [ShaderGen.ComputeShader(1, 1, 1)] + public void CS() + { + int index = (int)ShaderGen.ShaderBuiltins.DispatchThreadID.X; + if (index >= Methods) return; + ComputeShaderParameters parameters = InOutParameters[index]; + switch (index) + { +"; + private static readonly string SBSCase = @" case $$CASE$$: + $$RESULT$$$$METHOD$$($$ARGS$$); + break; +"; + private static readonly string SBSParam = @"parameters.$$NAME$$"; + private static readonly string SBSP3 = @" } + + InOutParameters[index] = parameters; + } + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct ComputeShaderParameters + { +"; + private static readonly string SBSP4 = @"}"; + #endregion + } +} \ No newline at end of file diff --git a/src/ShaderGen.Tests/AutoGenerated/Failure.cs b/src/ShaderGen.Tests/AutoGenerated/Failure.cs new file mode 100644 index 0000000..6c055b1 --- /dev/null +++ b/src/ShaderGen.Tests/AutoGenerated/Failure.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using ShaderGen.Tests.Tools; + +namespace ShaderGen.Tests.AutoGenerated +{ + /// + /// A failure of a tested method occurs when the results are not approximately the same for all + /// backends (and the CPU). + /// + internal class Failure + { + /// + /// The method map + /// + public readonly MethodMap MethodMap; + + /// + /// The parameters + /// + public readonly IReadOnlyList Parameters; + + /// + /// The test sets grouped by result. + /// + public readonly IReadOnlyList<(object Result, IReadOnlyList TestSets)> Results; + + private readonly string _string; + + public Failure(MethodMap methodMap, object[] parameters, + IReadOnlyList<(object Result, IReadOnlyList TestSets)> results) + { + MethodMap = methodMap; + Parameters = parameters; + Results = results; + + MethodInfo method = MethodMap.Method; + StringBuilder builder = new StringBuilder() + .Append(method.Name) + .Append('(') + .Append(string.Join(", ", parameters)) + .Append(')'); + + (string Name, object Value)[] resultGroups = results.Select(r => (Name: string.Join(", ", r.TestSets.Select(t => t.Name)), Value: r.Result)).ToArray(); + int pad = resultGroups.Max(r => r.Name.Length) + 3; + foreach ((string Name, object Value) group in resultGroups) + { + builder.AppendLine(string.Empty) + .AppendFormat(group.Name.PadLeft(pad)) + .Append(" = ") + .Append(group.Value); + } + + _string = builder.ToString(); + } + + /// + /// Checks the results sets and return a new if they are inconsistent. + /// + /// The test sets. + /// The method map. + /// The test. + /// The comparer. + /// a new if results are inconsistent; otherwise . + public static Failure Test( + TestSets testSets, + MethodMap methodMap, + int test, + IEqualityComparer comparer) + { + IReadOnlyList<(object Result, IReadOnlyList TestSets)> results = testSets + .Where(t => t.Results != null) + .Select(t => (TestSet: t, Result: methodMap.GetResult(t.Results, test))) + .GroupBy(r => r.Result, r => r.TestSet, comparer) + .Select(g => (Result: g.Key, TestSets: (IReadOnlyList)g.ToArray())) + .OrderByDescending(g => g.TestSets.Count) + .ToArray(); + + if (results.Count == 1) + { + // Results were all identical! + return null; + } + + return new Failure(methodMap, methodMap.GetParameters(testSets.TestData, test), results); + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() => _string; + } + +} \ No newline at end of file diff --git a/src/ShaderGen.Tests/AutoGenerated/Mappings.cs b/src/ShaderGen.Tests/AutoGenerated/Mappings.cs new file mode 100644 index 0000000..81eb790 --- /dev/null +++ b/src/ShaderGen.Tests/AutoGenerated/Mappings.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using Xunit; + +namespace ShaderGen.Tests.AutoGenerated +{ + /// + /// Holds information about the mappings of tested methods to the buffer. + /// + internal class Mappings + { + /// + /// The size of the input structure. + /// + public readonly int StructSize; + + /// + /// The buffer size is a single struct size * number of methods. + /// As such it is the size of the buffer required to run each method exactly once. + /// + public readonly int BufferSize; + + /// + /// The result set size is the amount of space required to store the results of every method exactly once. + /// + public readonly int ResultSetSize; + + /// + /// The number of methods. + /// + public readonly int Methods; + + /// + /// The buffer fields by name. + /// + public readonly IReadOnlyDictionary BufferFields; + + /// + /// The method maps. + /// + public readonly IReadOnlyList MethodMaps; + + /// + /// Initializes a new instance of the class. + /// + /// Size of the input structure. + /// The buffer fields. + /// The methodMaps. + public Mappings(int structSize, IReadOnlyDictionary bufferFields, IReadOnlyCollection<(int Index, MethodInfo Method, IReadOnlyDictionary Parameters, string ReturnField)> methodMaps) + { + StructSize = structSize; + BufferFields = bufferFields; + BufferSize = structSize * methodMaps.Count; + Methods = methodMaps.Count; + + int resultSetSize = 0; + MethodMap[] mmArray = new MethodMap[Methods]; + foreach ((int Index, MethodInfo Method, IReadOnlyDictionary Parameters, string ReturnField) map in methodMaps) + { + PaddedStructCreator.Field returnField = map.ReturnField != null ? BufferFields[map.ReturnField] : null; + + mmArray[map.Index] = new MethodMap( + this, + map.Index, + map.Method, + map.Parameters.ToDictionary(kvp => kvp.Key, kvp => BufferFields[kvp.Value]), + returnField, + resultSetSize); + + if (returnField != null) + { + resultSetSize += returnField.AlignmentInfo.ShaderSize; + } + } + + MethodMaps = mmArray; + ResultSetSize = resultSetSize; + } + + /// + /// Gets the results from the pointer into a result set. + /// + /// The data. + /// The result set + /// The test number. + public void SetResults(IntPtr data, byte[] results, int test) + { + foreach (MethodMap method in MethodMaps) + { + if (method.ResultField == null) + { + continue; + } + + method.SetResult(data, results, test); + } + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() => $"Mappings for {Methods} methods."; + } +} \ No newline at end of file diff --git a/src/ShaderGen.Tests/AutoGenerated/MethodMap.cs b/src/ShaderGen.Tests/AutoGenerated/MethodMap.cs new file mode 100644 index 0000000..2b9cafe --- /dev/null +++ b/src/ShaderGen.Tests/AutoGenerated/MethodMap.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Xunit; + +namespace ShaderGen.Tests.AutoGenerated +{ + /// + /// Holds information about the mapping of a tested method parameterFields and return to a buffer. + /// + internal class MethodMap + { + /// + /// The owner mappings. + /// + public readonly Mappings Mappings; + + /// + /// The index of the method. + /// + public readonly int Index; + + /// + /// The method info. + /// + public readonly MethodInfo Method; + + /// + /// The parameter to field map. + /// + public readonly IReadOnlyDictionary ParameterFields; + + /// + /// The field that holds the return value if any; otherwise . + /// + public readonly PaddedStructCreator.Field ResultField; + + /// + /// The offset into the result set where the result can be found; or -1 to indicate no result. + /// + public readonly int ResultSetOffset; + + /// + /// Initializes a new instance of the class. + /// + /// The mappings. + /// The index. + /// The method. + /// The parameterFields. + /// The return field. + /// The result set offset. + public MethodMap(Mappings mappings, int index, MethodInfo method, IReadOnlyDictionary parameterFields, PaddedStructCreator.Field resultField, int resultSetOffset) + { + Mappings = mappings; + Index = index; + Method = method; + ParameterFields = parameterFields; + ResultField = resultField; + ResultSetOffset = resultField == null ? -1 : resultSetOffset; + Signature = + $"{method.ReturnType.Name} {method.DeclaringType.FullName}.{method.Name}({String.Join(", ", ParameterFields.Select(p => $"{p.Key.ParameterType.Name} {p.Key.Name}"))})"; + } + + /// + /// Gets the signature. + /// + /// + /// The signature. + /// + public string Signature { get; } + + /// + /// Calculates the offset of a specific field in the test data buffer for this method. + /// + /// The field. + /// The test. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int FieldOffset(PaddedStructCreator.Field field, int test) => + test * Mappings.BufferSize + Index * Mappings.StructSize + field.Position; + + /// + /// Generates test data for this method, executes it and stores the result. + /// + /// The test data. + /// The test. + /// The minimum mantissa. + /// The maximum mantissa. + public void GenerateTestData(byte[] testData, int test, int minMantissa, int maxMantissa) + { + // TODO I suspect this can all be done a lot easier with Span once upgraded to .Net Core 2.1 + // Create random input values + foreach (PaddedStructCreator.Field field in ParameterFields.Values) + { + int floatCount = (int)Math.Ceiling( + (float)Math.Max(field.AlignmentInfo.ShaderSize, field.AlignmentInfo.CSharpSize) / + sizeof(float)); + + // Get random floats to fill parameter structure + float[] floats = TestUtil.GetRandomFloats(floatCount, minMantissa, maxMantissa); + GCHandle handle = GCHandle.Alloc(floats, GCHandleType.Pinned); + try + { + // Fill test data + IntPtr ptr = Marshal.UnsafeAddrOfPinnedArrayElement(testData, FieldOffset(field, test)); + Marshal.Copy(floats, 0, ptr, floats.Length); + } + finally + { + handle.Free(); + } + } + } + + /// + /// Gets the parameters for this method from the test data. + /// + /// The test data. + /// The test. + /// + public object[] GetParameters(byte[] testData, int test) + { + // TODO I suspect this can all be done a lot easier with Span once upgraded to .Net Core 2.1 + + // Grab test data + object[] parameters = new object[ParameterFields.Count]; + GCHandle handle = GCHandle.Alloc(testData, GCHandleType.Pinned); + try + { + foreach (KeyValuePair kvp in ParameterFields) + { + ParameterInfo pInfo = kvp.Key; + PaddedStructCreator.Field field = kvp.Value; + + // Create object of correct type + IntPtr ptr = Marshal.AllocCoTaskMem(field.AlignmentInfo.CSharpSize); + Marshal.Copy( + testData, + FieldOffset(field, test), + ptr, + field.AlignmentInfo.CSharpSize); + + // Assign parameter + parameters[pInfo.Position] = Marshal.PtrToStructure(ptr, field.Type); + } + + return parameters; + } + finally + { + handle.Free(); + } + } + + /// + /// Copies the result from test data to the result set. + /// + /// The test data. + /// + /// The test. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetResult(IntPtr testData, byte[] results, int test) + { + Marshal.Copy( + // GPU test data is always a single test + testData + Index * Mappings.StructSize + ResultField.Position, + results, + test * Mappings.ResultSetSize + ResultSetOffset, + ResultField.AlignmentInfo.ShaderSize); + } + + /// + /// Gets the result from the result set. + /// + /// The results. + /// The test. + /// + public object GetResult(byte[] results, int test) + { + GCHandle handle = GCHandle.Alloc(results, GCHandleType.Pinned); + try + { + // Create object of correct type + IntPtr ptr = Marshal.AllocCoTaskMem(ResultField.AlignmentInfo.CSharpSize); + Marshal.Copy(results, test * Mappings.ResultSetSize + ResultSetOffset, ptr, ResultField.AlignmentInfo.CSharpSize); + + // Return structure + return Marshal.PtrToStructure(ptr, ResultField.Type); + } + finally + { + handle.Free(); + } + } + + /// + /// Generates test data for this method, executes it and stores the result. + /// + /// The test data. + /// The results. + /// The test. + public void ExecuteCPU(byte[] testData, byte[] results, int test) + { + object result = Method.Invoke(null, GetParameters(testData, test)); + + if (ResultField == null) + { + Assert.Null(result); + return; + } + + // Copy result into result set + GCHandle handle = GCHandle.Alloc(result, GCHandleType.Pinned); + try + { + // Copy result data + Marshal.Copy(handle.AddrOfPinnedObject(), results, test * Mappings.ResultSetSize + ResultSetOffset, ResultField.AlignmentInfo.ShaderSize); + } + finally + { + handle.Free(); + } + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() => $"Mappings for {Signature} [{Index}]"; + } +} \ No newline at end of file diff --git a/src/ShaderGen.Tests/AutoGenerated/PaddedStructCreator.cs b/src/ShaderGen.Tests/AutoGenerated/PaddedStructCreator.cs new file mode 100644 index 0000000..cf357c9 --- /dev/null +++ b/src/ShaderGen.Tests/AutoGenerated/PaddedStructCreator.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Xunit; + +namespace ShaderGen.Tests.AutoGenerated +{ + /// + /// This class is used to build a set of fields for use in testing methods. + /// + internal class PaddedStructCreator + { + internal class Field + { + public readonly string Name; + public readonly Type Type; + public readonly int Position; + public readonly AlignmentInfo AlignmentInfo; + public readonly bool IsPaddingField; + + /// + /// Initializes a new instance of the class. + /// + /// The name. + /// The type. + /// The position. + /// The alignment information. + /// if set to true this is a padding field. + public Field(string name, Type type, int position, AlignmentInfo alignmentInfo, bool isPaddingField = false) + { + Name = name; + Position = position; + Type = type; + AlignmentInfo = alignmentInfo; + IsPaddingField = isPaddingField; + } + } + + /// + /// Holds fields of a specific type. + /// + private class Creator : IEnumerable + { + private int _current = 0; + public readonly Type Type; + public readonly AlignmentInfo AlignmentInfo; + private readonly List _names = new List(6); + + public Creator(Type type, AlignmentInfo alignmentInfo) + { + Type = type; + AlignmentInfo = alignmentInfo; + } + + /// + /// Gets a field of this type. + /// + /// + public string GetFieldName() + { + if (_current < _names.Count) + { + return _names[_current++]; + } + + string newName; + + _current++; + _names.Add(newName = $"{Type.Name}_{_current}"); + return newName; + } + + /// + /// Resets the creator for this type. + /// + public void Reset() => _current = 0; + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// An enumerator that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() => _names.GetEnumerator(); + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public override string ToString() => $"{_names.Count} fields of type {Type}"; + } + + /// + /// All the creators by type. + /// + private readonly Dictionary _creators = new Dictionary(); + + private readonly Compilation _compilation; + + public PaddedStructCreator(Compilation compilation) => _compilation = compilation; + + /// + /// Gets a field of the specified type. + /// + /// The type. + /// + public string GetFieldName(Type type) + { + if (!_creators.TryGetValue(type, out Creator creator)) + { + _creators.Add(type, + creator = new Creator(type, + TypeSizeCache.Get(_compilation.GetTypeByMetadataName(type.FullName)))); + } + + return creator.GetFieldName(); + } + + /// + /// Resets the field creator. + /// + public void Reset() + { + foreach (Creator creator in _creators.Values) + { + creator.Reset(); + } + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// An enumerator that can be used to iterate through the collection. + /// + public IReadOnlyList GetFields(out int size) + { + AlignmentInfo floatAlignmentInfo = TypeSizeCache.Get(_compilation.GetTypeByMetadataName(typeof(float).FullName)); + int paddingFields = 0; + size = 0; + + // Create list of fields ordered by largest size first, and get the field names. + List<(Creator creator, List names)> fieldsBySize = _creators.Values + .OrderByDescending(c => c.AlignmentInfo.ShaderAlignment) + .ThenByDescending(c => c.AlignmentInfo.ShaderSize) + .Select names)>(c => (c, c.ToList())) + .Where(t => t.names.Count > 0) + .ToList(); + + // Output list of fields + List fields = new List(); + + // For as long as we have fields to place we loop. + while (fieldsBySize.Count > 0) + { + // Get the top of the list + (Creator creator, List names) = fieldsBySize[0]; + fieldsBySize.RemoveAt(0); + int alignment = creator.AlignmentInfo.ShaderAlignment; + Assert.True(alignment % 4 == 0); + + foreach (string fieldName in names) + { + // Check to see if we are aligned + while (size % alignment != 0) + { + Assert.True(size % 4 == 0); + + // Do we have any fields we can use to pad? + int currentSize = size; + (Creator creator, List names) padFields = + fieldsBySize.FirstOrDefault(t => + t.creator.AlignmentInfo.ShaderSize <= alignment && + currentSize % t.creator.AlignmentInfo.ShaderAlignment == 0); + + if (padFields.creator != null) + { + // Use the last field to pad the struct. + fields.Add(new Field(padFields.names.Last(), padFields.creator.Type, size, + padFields.creator.AlignmentInfo)); + + // Increase the struct size. + size += padFields.creator.AlignmentInfo.ShaderSize; + + // Remove the used field + padFields.names.RemoveAt(padFields.names.Count - 1); + if (padFields.names.Count < 1) + { + fieldsBySize.Remove(padFields); + } + } + else + { + // No padding field of the right size available, use a private float + fields.Add(new Field($"_paddingField_{paddingFields++}", typeof(float), size, floatAlignmentInfo, true)); + size += floatAlignmentInfo.ShaderSize; + } + } + + // Add the next field as we are on the correct alignment boundary + fields.Add(new Field(fieldName, creator.Type, size, + creator.AlignmentInfo)); + + size += creator.AlignmentInfo.ShaderSize; + } + } + + return fields; + } + } +} \ No newline at end of file diff --git a/src/ShaderGen.Tests/AutoGenerated/TestSet.cs b/src/ShaderGen.Tests/AutoGenerated/TestSet.cs new file mode 100644 index 0000000..2d1478a --- /dev/null +++ b/src/ShaderGen.Tests/AutoGenerated/TestSet.cs @@ -0,0 +1,240 @@ +using System; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using ShaderGen.Tests.Tools; +using Veldrid; +using Xunit; +using Xunit.Abstractions; + +namespace ShaderGen.Tests.AutoGenerated +{ + /// + /// An individual test set. + /// + internal class TestSet + { + /// + /// The owner collection. + /// + public readonly TestSets TestSets; + + /// + /// The test name. + /// + public readonly string Name; + + /// + /// The if any; otherwise . + /// + public readonly ToolChain ToolChain; + + /// + /// The if any; otherwise . + /// + public readonly LanguageBackend Backend; + + /// + /// The results data. + /// + public byte[] Results { get; private set; } + + /// + /// Gets a value indicating whether this has been executed yet. + /// + /// + /// true if executed; otherwise, false. + /// + public bool Executed => Results != null; + + /// + /// Initializes a new instance of the class. + /// + /// The test sets. + /// The tool chain (if any). + public TestSet(TestSets testSets, ToolChain toolChain = null) + { + TestSets = testSets; + Name = toolChain?.GraphicsBackend.ToString() ?? "CPU"; + ToolChain = toolChain; + Backend = toolChain?.CreateBackend(testSets.Compilation); + } + + /// + /// Allocates the results buffer. + /// + /// The output. + private void AllocateResults(ITestOutputHelper output) + { + // Create results data structure. + using (new TestTimer(output, + $"Creating result buffer ({(TestSets.Mappings.ResultSetSize * TestSets.TestLoops).ToMemorySize()})")) + { + Results = new byte[TestSets.Mappings.ResultSetSize * TestSets.TestLoops]; + } + } + + /// + /// Executes the specified test. + /// + /// The generation result. + /// Name of the cs function. + /// The output. + public void Execute( + ShaderGenerationResult generationResult, + string csFunctionName, + ITestOutputHelper output) + { + if (Executed) + { + output.WriteLine( + $"The {Name} tests have already been executed!"); + return; + } + + TestSets testSets = TestSets; + Mappings mappings = testSets.Mappings; + + if (ToolChain == null) + { + /* + * Generate the test data and the result set data for the CPU. + */ + AllocateResults(output); + using (new TestTimer(output, + $"Running {testSets.TestLoops} iterations on the {Name} backend")) + { + for (int test = 0; test < testSets.TestLoops; test++) + { + foreach (MethodMap method in mappings.MethodMaps) + { + method.ExecuteCPU(TestSets.TestData, Results, test); + } + } + + return; + } + } + + GeneratedShaderSet set; + CompileResult compilationResult; + // Compile shader for this backend. + using (new TestTimer(output, $"Compiling Compute Shader for {ToolChain.GraphicsBackend}")) + { + set = generationResult.GetOutput(Backend).Single(); + compilationResult = + ToolChain.Compile(set.ComputeShaderCode, Stage.Compute, set.ComputeFunction.Name); + } + + if (compilationResult.HasError) + { + output.WriteLine($"Failed to compile Compute Shader from set \"{set.Name}\"!"); + output.WriteLine(compilationResult.ToString()); + return; + } + + Assert.NotNull(compilationResult.CompiledOutput); + + using (GraphicsDevice graphicsDevice = ToolChain.CreateHeadless()) + { + if (!graphicsDevice.Features.ComputeShader) + { + output.WriteLine( + $"The {ToolChain.GraphicsBackend} backend does not support compute shaders, skipping!"); + return; + } + + ResourceFactory factory = graphicsDevice.ResourceFactory; + using (DeviceBuffer inOutBuffer = factory.CreateBuffer( + new BufferDescription( + (uint)mappings.BufferSize, + BufferUsage.StructuredBufferReadWrite, + (uint)mappings.StructSize))) + + using (Shader computeShader = factory.CreateShader( + new ShaderDescription( + ShaderStages.Compute, + compilationResult.CompiledOutput, + csFunctionName))) + + using (ResourceLayout inOutStorageLayout = factory.CreateResourceLayout( + new ResourceLayoutDescription( + new ResourceLayoutElementDescription("InOutBuffer", ResourceKind.StructuredBufferReadWrite, + ShaderStages.Compute)))) + + using (Pipeline computePipeline = factory.CreateComputePipeline(new ComputePipelineDescription( + computeShader, + new[] { inOutStorageLayout }, + 1, 1, 1))) + + + using (ResourceSet computeResourceSet = factory.CreateResourceSet( + new ResourceSetDescription(inOutStorageLayout, inOutBuffer))) + + using (CommandList commandList = factory.CreateCommandList()) + { + // Ensure the headless graphics device is the backend we expect. + Assert.Equal(ToolChain.GraphicsBackend, graphicsDevice.BackendType); + + output.WriteLine($"Created compute pipeline for {Name} backend."); + + // Allocate the results buffer + AllocateResults(output); + + using (new TestTimer(output, + $"Running {testSets.TestLoops} iterations on the {Name} backend")) + { + // Loop for each test + for (int test = 0; test < testSets.TestLoops; test++) + { + // Update parameter buffer + graphicsDevice.UpdateBuffer( + inOutBuffer, + 0, + // Get the portion of test data for the current test loop + Marshal.UnsafeAddrOfPinnedArrayElement(testSets.TestData, mappings.BufferSize * test), + (uint)mappings.BufferSize); + graphicsDevice.WaitForIdle(); + + // Execute compute shaders + commandList.Begin(); + commandList.SetPipeline(computePipeline); + commandList.SetComputeResourceSet(0, computeResourceSet); + commandList.Dispatch((uint)mappings.Methods, 1, 1); + commandList.End(); + + graphicsDevice.SubmitCommands(commandList); + graphicsDevice.WaitForIdle(); + + // Read back parameters using a staging buffer + using (DeviceBuffer stagingBuffer = + factory.CreateBuffer( + new BufferDescription(inOutBuffer.SizeInBytes, BufferUsage.Staging))) + { + commandList.Begin(); + commandList.CopyBuffer(inOutBuffer, 0, stagingBuffer, 0, stagingBuffer.SizeInBytes); + commandList.End(); + graphicsDevice.SubmitCommands(commandList); + graphicsDevice.WaitForIdle(); + + // Read back test results + MappedResource map = graphicsDevice.Map(stagingBuffer, MapMode.Read); + + mappings.SetResults(map.Data, Results, test); + graphicsDevice.Unmap(stagingBuffer); + } + } + } + } + } + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() => $"{Name} tests{(Executed ? $" [Executed]" : string.Empty)}"; + } +} \ No newline at end of file diff --git a/src/ShaderGen.Tests/AutoGenerated/TestSets.cs b/src/ShaderGen.Tests/AutoGenerated/TestSets.cs new file mode 100644 index 0000000..956b4bf --- /dev/null +++ b/src/ShaderGen.Tests/AutoGenerated/TestSets.cs @@ -0,0 +1,162 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using ShaderGen.Tests.Tools; + +namespace ShaderGen.Tests.AutoGenerated +{ + /// + /// + /// + /// + internal class TestSets : IReadOnlyList + { + /// + /// The compilation. + /// + public readonly Compilation Compilation; + + /// + /// The mappings. + /// + public readonly Mappings Mappings; + + /// + /// The test data used for these test sets. + /// + public readonly byte[] TestData; + + /// + /// The number of test iterations. + /// + public readonly int TestLoops; + + /// + /// The sets. + /// + private IReadOnlyList _sets; + + /// + /// The minimum mantissa of generated floats. + /// + public readonly int MinMantissa; + + /// + /// The maximum mantissa of generated floats. + /// + public readonly int MaxMantissa; + + /// + /// Initializes a new instance of the class. + /// + /// The tool chains. + /// The compilation. + /// The mappings. + /// The test loops. + /// The minimum mantissa. + /// The maximum mantissa. + public TestSets(IEnumerable toolChains, Compilation compilation, Mappings mappings, int testLoops, int minMantissa, int maxMantissa) + { + Compilation = compilation; + Mappings = mappings; + TestLoops = testLoops; + MinMantissa = minMantissa; + MaxMantissa = maxMantissa; + + TestData = new byte[mappings.BufferSize * testLoops]; + + // Create a set for the CPU and then sets for each tool chain. + _sets = new[] + {new TestSet(this)} + .Concat(toolChains.Select(tc => new TestSet(this, tc))) + .ToArray(); + + // Generate the test data and the result set data for the CPU. + TestData = new byte[mappings.BufferSize * testLoops]; + for (int test = 0; test < testLoops; test++) + { + foreach (MethodMap method in mappings.MethodMaps) + { + method.GenerateTestData(TestData, test, minMantissa, maxMantissa); + } + } + } + + /// + /// Gets the failures. + /// + /// + public IReadOnlyDictionary> GetFailures(float epsilon = float.Epsilon) + { + Dictionary> dictionary = new Dictionary>(); + List failures = new List(); + FloatComparer comparer = new FloatComparer(epsilon); + // Get pointer array + foreach (MethodMap method in Mappings.MethodMaps) + { + if (method.ResultField == null) + { + // This method has no results, so just skip it + continue; + } + + for (int test = 0; test < TestLoops; test++) + { + // Calculate more detailed failure analysis + Failure failure = Failure.Test(this, method, test, comparer); + if (failure != null) + { + failures.Add(failure); + } + } + + if (!failures.Any()) + { + continue; + } + + dictionary[method] = failures.ToArray(); + failures.Clear(); + } + + return dictionary; + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// An enumerator that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() => _sets.GetEnumerator(); + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Gets the number of elements in the collection. + /// + public int Count => _sets.Count; + + /// + /// Gets the at the specified index. + /// + /// + /// The . + /// + /// The index. + /// + public TestSet this[int index] => _sets[index]; + + + } +} diff --git a/src/ShaderGen.Tests/FloatComparer.cs b/src/ShaderGen.Tests/FloatComparer.cs new file mode 100644 index 0000000..666c773 --- /dev/null +++ b/src/ShaderGen.Tests/FloatComparer.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace ShaderGen.Tests +{ + /// + /// Compares floats approximately, and any structures that can be broken down into floats. + /// + /// + public class FloatComparer : IEqualityComparer + { + private static readonly ConcurrentDictionary> _childFieldInfos + = new ConcurrentDictionary>(); + + /// + /// The epsilon for comparing floats. + /// + public readonly float Epsilon; + + /// + /// Initializes a new instance of the class. + /// + /// The epsilon. + public FloatComparer(float epsilon) + { + Epsilon = epsilon; + } + + /// + /// Determines whether the specified , is equal to this instance. + /// + /// The to compare with this instance. + /// The b. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + public bool Equals(object a, object b) + { + if (object.Equals(a, b)) + { + return true; + } + + Type currentType = a?.GetType() ?? b.GetType(); + if (currentType == typeof(float)) + { + return ((float)a).ApproximatelyEqual((float)b, Epsilon); + } + + object aValue = a; + object bValue = b; + Stack<(Type currentType, object aValue, object bValue)> stack + = new Stack<(Type currentType, object aValue, object bValue)>(); + stack.Push((currentType, aValue, bValue)); + + while (stack.Count > 0) + { + // Pop top of stack. + (currentType, aValue, bValue) = stack.Pop(); + + // Get fields (cached) + IReadOnlyCollection childFields = _childFieldInfos.GetOrAdd(currentType, t => t.GetFields().Where(f => !f.IsStatic).ToArray()); + + if (childFields.Count < 1) + { + // No child fields, we have an inequality + return false; + } + + foreach (FieldInfo childField in childFields) + { + object aMemberValue = childField.GetValue(aValue); + object bMemberValue = childField.GetValue(bValue); + + currentType = childField.FieldType; + // Short cut equality + if (object.Equals(aMemberValue, bMemberValue) || + currentType == typeof(float) && ((float)aMemberValue).ApproximatelyEqual((float)bMemberValue, Epsilon)) + { + continue; + } + + stack.Push((currentType, aMemberValue, bMemberValue)); + } + } + + return true; + } + + /// + /// Returns a hash code for this instance. + /// + /// The object. + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public int GetHashCode(object obj) + { + // Note we will get loads of collisions and rely instead on equality. + return 0; + } + } +} \ No newline at end of file diff --git a/src/ShaderGen.Tests/ShaderBuiltinsTests.cs b/src/ShaderGen.Tests/ShaderBuiltinsTests.cs deleted file mode 100644 index ad37531..0000000 --- a/src/ShaderGen.Tests/ShaderBuiltinsTests.cs +++ /dev/null @@ -1,419 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Numerics; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices.WindowsRuntime; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis; -using ShaderGen.Tests.Tools; -using TestShaders; -using Veldrid; -using Veldrid.Sdl2; -using Veldrid.StartupUtilities; -using Xunit; -using Xunit.Abstractions; - -namespace ShaderGen.Tests -{ - public class ShaderBuiltinsTests - { - /// - /// The skip reason, set to to enable tests in class. - /// - private const string SkipReason = null; // "Currently skipping automatic tests until closer implementations can be found."; - - /// - /// The test will fail when the GPU & CPU has any methods that fail higher than the ratio. - /// A value of 1.0f will never fail due to inconsistencies. - /// - private const float MaximumFailureRate = 1f; - - /// - /// How close float's need to be, to be considered a match (ratio). - /// - private const float Tolerance = 0.001f; - - /// - /// How close float's need to be, to be considered a match (ratio). - /// - private const float ToleranceRatio = 0.001f; - - /// - /// The number of failure examples to output - /// - private const int FailureExamples = 3; - - /// - /// Controls the minimum mantissa when generating a floating point number (how 'small' it can go) - /// - /// To test all valid floats this should be set to -126. - private static readonly int MinMantissa = -3; - - /// - /// Controls the maximum mantissa when generating a floating point number (how 'big' it can go) - /// - /// To test all valid floats this should be set to 128. - private static readonly int MaxMantissa = 3; - - /// - /// Will ignore failures if either value is . - /// - private const bool IgnoreNan = true; - /// - /// Will ignore failures if either value is or - /// - private const bool IgnoreInfinity = true; - - /// - /// The maximum test duration for each backend. - /// - private static readonly TimeSpan TestDuration = TimeSpan.FromSeconds(3); - - /// - /// The maximum iteration for each backend. - /// - private static readonly int TestLoops = 10000; - - private readonly ITestOutputHelper _output; - - public ShaderBuiltinsTests(ITestOutputHelper output) - { - _output = output; - } - - - [SkippableFact(typeof(RequiredToolFeatureMissingException), Skip = SkipReason)] - public void TestShaderBuiltins_GlslEs300() - => TestShaderBuiltins(ToolChain.GlslEs300); - - [SkippableFact(typeof(RequiredToolFeatureMissingException), Skip = SkipReason)] - public void TestShaderBuiltins_Glsl330() - => TestShaderBuiltins(ToolChain.Glsl330); - - [SkippableFact(typeof(RequiredToolFeatureMissingException), Skip = SkipReason)] - public void TestShaderBuiltins_Glsl450() - => TestShaderBuiltins(ToolChain.Glsl450); - - [SkippableFact(typeof(RequiredToolFeatureMissingException), Skip = SkipReason)] - public void TestShaderBuiltins_Hlsl() - => TestShaderBuiltins(ToolChain.Direct3D11); - - [SkippableFact(typeof(RequiredToolFeatureMissingException), Skip = SkipReason)] - public void TestShaderBuiltins_Metal() - => TestShaderBuiltins(ToolChain.Metal); - - private void TestShaderBuiltins(ToolChain toolChain) - { - if (!toolChain.Features.HasFlag(ToolFeatures.ToHeadless)) - { - throw new RequiredToolFeatureMissingException( - $"The {toolChain} does not support creating a headless graphics device!"); - } - - string csFunctionName = - $"{nameof(TestShaders)}.{nameof(ShaderBuiltinsComputeTest)}.{nameof(ShaderBuiltinsComputeTest.CS)}"; - Compilation compilation = TestUtil.GetCompilation(); - - LanguageBackend backend = toolChain.CreateBackend(compilation); - - /* - * Compile backend - */ - ShaderSetProcessor processor = new ShaderSetProcessor(); - - ShaderGenerator sg = new ShaderGenerator( - compilation, - backend, - null, - null, - csFunctionName, - processor); - - ShaderGenerationResult generationResult = sg.GenerateShaders(); - GeneratedShaderSet set = generationResult.GetOutput(backend).Single(); - _output.WriteLine($"Generated shader set for {toolChain.Name} backend."); - - CompileResult compilationResult = - toolChain.Compile(set.ComputeShaderCode, Stage.Compute, set.ComputeFunction.Name); - if (compilationResult.HasError) - { - _output.WriteLine($"Failed to compile Compute Shader from set \"{set.Name}\"!"); - _output.WriteLine(compilationResult.ToString()); - Assert.True(false); - } - else - { - _output.WriteLine($"Compiled Compute Shader from set \"{set.Name}\"!"); - } - - Assert.NotNull(compilationResult.CompiledOutput); - - int sizeOfParametersStruct = Unsafe.SizeOf(); - - - - // Create failure data structure, first by method #, then by field name. - Dictionary differences)>> failures - = new Dictionary differences)>>(); - - // We need two copies, one for the CPU & one for GPU - ComputeShaderParameters[] cpuParameters = new ComputeShaderParameters[ShaderBuiltinsComputeTest.Methods]; - ComputeShaderParameters[] gpuParameters = new ComputeShaderParameters[ShaderBuiltinsComputeTest.Methods]; - int loops = 0; - long durationTicks; - - // Set start. - long startTicks = Stopwatch.GetTimestamp(); - - ShaderBuiltinsComputeTest cpuTest = new ShaderBuiltinsComputeTest(); - /* - * Run shader on GPU. - */ - using (GraphicsDevice graphicsDevice = toolChain.CreateHeadless()) - { - if (!graphicsDevice.Features.ComputeShader) - { - throw new RequiredToolFeatureMissingException( - $"The {graphicsDevice.BackendType} backend does not support compute shaders!"); - } - - ResourceFactory factory = graphicsDevice.ResourceFactory; - using (DeviceBuffer inOutBuffer = factory.CreateBuffer( - new BufferDescription( - (uint)sizeOfParametersStruct * ShaderBuiltinsComputeTest.Methods, - BufferUsage.StructuredBufferReadWrite, - (uint)sizeOfParametersStruct))) - - using (Shader computeShader = factory.CreateShader( - new ShaderDescription( - ShaderStages.Compute, - compilationResult.CompiledOutput, - nameof(ShaderBuiltinsComputeTest.CS)))) - - using (ResourceLayout inOutStorageLayout = factory.CreateResourceLayout(new ResourceLayoutDescription( - new ResourceLayoutElementDescription("InOutBuffer", ResourceKind.StructuredBufferReadWrite, - ShaderStages.Compute)))) - - using (Pipeline computePipeline = factory.CreateComputePipeline(new ComputePipelineDescription( - computeShader, - new[] { inOutStorageLayout }, - 1, 1, 1))) - - - using (ResourceSet computeResourceSet = factory.CreateResourceSet( - new ResourceSetDescription(inOutStorageLayout, inOutBuffer))) - - using (CommandList commandList = factory.CreateCommandList()) - { - // Ensure the headless graphics device is the backend we expect. - Assert.Equal(toolChain.GraphicsBackend, graphicsDevice.BackendType); - - _output.WriteLine("Created compute pipeline."); - - do - { - /* - * Build test data in parallel - */ - Parallel.For( - 0, - ShaderBuiltinsComputeTest.Methods, - i => cpuParameters[i] = gpuParameters[i] = TestUtil.FillRandomFloats(MinMantissa, MaxMantissa)); - - /* - * Run shader on CPU in parallel - */ - cpuTest.InOutParameters = new RWStructuredBuffer(ref cpuParameters); - Parallel.For(0, ShaderBuiltinsComputeTest.Methods, - i => cpuTest.DoCS(new UInt3 { X = (uint)i, Y = 0, Z = 0 })); - - // Update parameter buffer - graphicsDevice.UpdateBuffer(inOutBuffer, 0, gpuParameters); - graphicsDevice.WaitForIdle(); - - // Execute compute shaders - commandList.Begin(); - commandList.SetPipeline(computePipeline); - commandList.SetComputeResourceSet(0, computeResourceSet); - commandList.Dispatch(ShaderBuiltinsComputeTest.Methods, 1, 1); - commandList.End(); - - graphicsDevice.SubmitCommands(commandList); - graphicsDevice.WaitForIdle(); - - // Read back parameters using a staging buffer - using (DeviceBuffer stagingBuffer = - factory.CreateBuffer(new BufferDescription(inOutBuffer.SizeInBytes, BufferUsage.Staging))) - { - commandList.Begin(); - commandList.CopyBuffer(inOutBuffer, 0, stagingBuffer, 0, stagingBuffer.SizeInBytes); - commandList.End(); - graphicsDevice.SubmitCommands(commandList); - graphicsDevice.WaitForIdle(); - - // Read back parameters - MappedResourceView map = - graphicsDevice.Map(stagingBuffer, MapMode.Read); - for (int i = 0; i < gpuParameters.Length; i++) - { - gpuParameters[i] = map[i]; - } - - graphicsDevice.Unmap(stagingBuffer); - } - - /* - * Compare results - */ - for (int method = 0; method < ShaderBuiltinsComputeTest.Methods; method++) - { - ComputeShaderParameters cpu = cpuParameters[method]; - ComputeShaderParameters gpu = gpuParameters[method]; - - // Filter results based on tolerances. - IReadOnlyCollection<(string fieldName, float cpuValue, float gpuValue)> - deepCompareObjectFields = TestUtil.DeepCompareObjectFields(cpu, gpu) - .Select<(string fieldName, object aValue, object bValue), (string fieldName, - float cpuValue, float gpuValue)>( - t => (t.fieldName, (float)t.aValue, (float)t.bValue)) - .Where(t => - { -#pragma warning disable 162 - // ReSharper disable ConditionIsAlwaysTrueOrFalse - float a = t.cpuValue; - float b = t.gpuValue; - bool comparable = true; - if (float.IsNaN(a) || float.IsNaN(b)) - { - if (IgnoreNan) - { - return false; - } - - comparable = false; - } - - if (float.IsInfinity(a) || float.IsInfinity(b)) - { - if (IgnoreInfinity) - { - return false; - } - - comparable = false; - } - - return !comparable || - Math.Abs(1.0f - a / b) > ToleranceRatio && - Math.Abs(a - b) > Tolerance; -#pragma warning restore 162 - // ReSharper restore ConditionIsAlwaysTrueOrFalse - }) - .ToArray(); - - if (deepCompareObjectFields.Count < 1) - { - continue; - } - - if (!failures.TryGetValue(method, out var methodList)) - { - failures.Add(method, - methodList = - new List<(ComputeShaderParameters cpu, ComputeShaderParameters gp, - IReadOnlyCollection<(string fieldName, float cpuValue, float gpuValue)>)>()); - } - - methodList.Add((cpu, gpu, deepCompareObjectFields)); - } - - // Continue until we have done enough loops, or run out of time. - durationTicks = Stopwatch.GetTimestamp() - startTicks; - } while (loops++ < TestLoops && - durationTicks < TestDuration.Ticks); - } - } - - TimeSpan testDuration = TimeSpan.FromTicks(durationTicks); - _output.WriteLine( - $"Executed compute shader using {toolChain.GraphicsBackend} {loops} times in {testDuration.TotalSeconds}s."); - - int notIdential = 0; - if (failures.Any()) - { - _output.WriteLine($"{failures.Count} methods experienced failures out of {ShaderBuiltinsComputeTest.Methods} ({100f * failures.Count / ShaderBuiltinsComputeTest.Methods:##.##}%). Details follow..."); - - string spacer1 = new string('=', 80); - string spacer2 = new string('-', 80); - - int failed = 0; - - // Output failures - foreach (var method in failures.OrderBy(kvp => kvp.Key)) - // To order by %-age failure use - .OrderByDescending(kvp =>kvp.Value.Count)) - { - notIdential++; - int methodFailureCount = method.Value.Count; - _output.WriteLine(string.Empty); - _output.WriteLine(spacer1); - float failureRate = 100f * methodFailureCount / loops; - if (failureRate > MaximumFailureRate) - { - failed++; - } - - _output.WriteLine( - $"Method {method.Key} failed {methodFailureCount} times ({failureRate:##.##}%)."); - - - foreach (var group in method.Value.SelectMany(t => t.differences).ToLookup(f => f.fieldName).OrderByDescending(g => g.Count())) - { - _output.WriteLine(spacer2); - _output.WriteLine(string.Empty); - - int fieldFailureCount = group.Count(); - _output.WriteLine($" {group.Key} failed {fieldFailureCount} times ({100f * fieldFailureCount / methodFailureCount:##.##}%)"); - - int examples = 0; - foreach (var tuple in group) - { - if (examples++ > FailureExamples) - { - _output.WriteLine($" ... +{fieldFailureCount - FailureExamples} more"); - break; - } - - _output.WriteLine($" {tuple.cpuValue,13} != {tuple.gpuValue}"); - } - } - } - - Assert.False(failed < 1, $"{failed} methods had a failure rate higher than {MaximumFailureRate * 100:##.##}%!"); - } - - _output.WriteLine(string.Empty); - _output.WriteLine(notIdential < 1 - ? "CPU & CPU results were identical for all methods over all iterations!" - : $"CPU & GPU results did not match for {notIdential} methods!"); - } - - - private class ShaderSetProcessor : IShaderSetProcessor - { - public string Result { get; private set; } - - public string UserArgs { get; set; } - - public void ProcessShaderSet(ShaderSetProcessorInput input) - { - Result = string.Join(" ", input.Model.AllResources.Select(rd => rd.Name)); - } - } - } -} \ No newline at end of file diff --git a/src/ShaderGen.Tests/TestAssets/ComputeShaderParameters.cs b/src/ShaderGen.Tests/TestAssets/ComputeShaderParameters.cs deleted file mode 100644 index b333c7a..0000000 --- a/src/ShaderGen.Tests/TestAssets/ComputeShaderParameters.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Numerics; -using System.Runtime.InteropServices; - -namespace TestShaders -{ - [StructLayout(LayoutKind.Sequential)] - public unsafe struct ComputeShaderParameters - { - public Matrix4x4 P1Matrix; - public Matrix4x4 P2Matrix; - - public Vector4 P1Vector4; - public Vector4 P2Vector4; - public Vector4 P3Vector4; - public Vector4 OutVector4; - - public Vector3 P1Vector3; - public float P1Float; - public Vector3 P2Vector3; - public float P2Float; - public Vector3 P3Vector3; - public float P3Float; - public Vector3 OutVector3; - public float OutFloat; - - public Vector2 P1Vector2; - public Vector2 P2Vector2; - public Vector2 P3Vector2; - public Vector2 OutVector2; - } -} \ No newline at end of file diff --git a/src/ShaderGen.Tests/TestAssets/ShaderBuiltinsComputeTest.cs b/src/ShaderGen.Tests/TestAssets/ShaderBuiltinsComputeTest.cs deleted file mode 100644 index 26592e0..0000000 --- a/src/ShaderGen.Tests/TestAssets/ShaderBuiltinsComputeTest.cs +++ /dev/null @@ -1,550 +0,0 @@ -using System.Numerics; -using System.Runtime.InteropServices; -using ShaderGen; -using TestShaders; -using static ShaderGen.ShaderBuiltins; - -namespace TestShaders -{ - /// - /// Shader to test built in methods. - /// - public class ShaderBuiltinsComputeTest - { - /// - /// The number of methods. - /// - public const uint Methods = 147; - - [ResourceSet(0)] public RWStructuredBuffer InOutParameters; - - [ComputeShader(1, 1, 1)] - public void CS() - { - DoCS(DispatchThreadID); - } - - /* - * TODO Issue #67 - WORKAROUND until DispatchThreadID is removed and the parameter style implemented - */ - public void DoCS(UInt3 dispatchThreadID) - { - int index = (int)dispatchThreadID.X; - - // ReSharper disable once RedundantCast - WORKAROUND for #75 - if (index >= Methods) - { - return; - } - - ComputeShaderParameters parameters = InOutParameters[index]; - switch (index) - { - // Abs - case 0: - parameters.OutFloat = Abs(parameters.P1Float); - break; - case 1: - parameters.OutVector2 = Abs(parameters.P1Vector2); - break; - case 2: - parameters.OutVector3 = Abs(parameters.P1Vector3); - break; - case 3: - parameters.OutVector4 = Abs(parameters.P1Vector4); - break; - - // Acos - case 4: - parameters.OutFloat = Acos(parameters.P1Float); - break; - case 5: - parameters.OutVector2 = Acos(parameters.P1Vector2); - break; - case 6: - parameters.OutVector3 = Acos(parameters.P1Vector3); - break; - case 7: - parameters.OutVector4 = Acos(parameters.P1Vector4); - break; - - // Acosh - case 8: - parameters.OutFloat = Acosh(parameters.P1Float); - break; - case 9: - parameters.OutVector2 = Acosh(parameters.P1Vector2); - break; - case 10: - parameters.OutVector3 = Acosh(parameters.P1Vector3); - break; - case 11: - parameters.OutVector4 = Acosh(parameters.P1Vector4); - break; - - // Asin - case 12: - parameters.OutFloat = Asin(parameters.P1Float); - break; - case 13: - parameters.OutVector2 = Asin(parameters.P1Vector2); - break; - case 14: - parameters.OutVector3 = Asin(parameters.P1Vector3); - break; - case 15: - parameters.OutVector4 = Asin(parameters.P1Vector4); - break; - - // Asinh - case 16: - parameters.OutFloat = Asinh(parameters.P1Float); - break; - case 17: - parameters.OutVector2 = Asinh(parameters.P1Vector2); - break; - case 18: - parameters.OutVector3 = Asinh(parameters.P1Vector3); - break; - case 19: - parameters.OutVector4 = Asinh(parameters.P1Vector4); - break; - - // Atan - case 20: - parameters.OutFloat = Atan(parameters.P1Float); - break; - case 21: - parameters.OutFloat = Atan(parameters.P1Float, parameters.P2Float); - break; - case 22: - parameters.OutVector2 = Atan(parameters.P1Vector2); - break; - case 23: - parameters.OutVector2 = Atan(parameters.P1Vector2, parameters.P2Vector2); - break; - case 24: - parameters.OutVector3 = Atan(parameters.P1Vector3); - break; - case 25: - parameters.OutVector3 = Atan(parameters.P1Vector3, parameters.P2Vector3); - break; - case 26: - parameters.OutVector4 = Atan(parameters.P1Vector4); - break; - case 27: - parameters.OutVector4 = Atan(parameters.P1Vector4, parameters.P2Vector4); - break; - - // Atanh - case 28: - parameters.OutFloat = Atanh(parameters.P1Float); - break; - case 29: - parameters.OutVector2 = Atanh(parameters.P1Vector2); - break; - case 30: - parameters.OutVector3 = Atanh(parameters.P1Vector3); - break; - case 31: - parameters.OutVector4 = Atanh(parameters.P1Vector4); - break; - - // Cbrt - case 32: - parameters.OutFloat = Cbrt(parameters.P1Float); - break; - case 33: - parameters.OutVector2 = Cbrt(parameters.P1Vector2); - break; - case 34: - parameters.OutVector3 = Cbrt(parameters.P1Vector3); - break; - case 35: - parameters.OutVector4 = Cbrt(parameters.P1Vector4); - break; - - // Ceiling - case 36: - parameters.OutFloat = Ceiling(parameters.P1Float); - break; - case 37: - parameters.OutVector2 = Ceiling(parameters.P1Vector2); - break; - case 38: - parameters.OutVector3 = Ceiling(parameters.P1Vector3); - break; - case 39: - parameters.OutVector4 = Ceiling(parameters.P1Vector4); - break; - - // Clamp - case 40: - parameters.OutFloat = Clamp(parameters.P1Float, parameters.P2Float, parameters.P3Float); - break; - case 41: - parameters.OutVector2 = Clamp(parameters.P1Vector2, parameters.P2Vector2, parameters.P1Vector3.XY()); - break; - case 42: - parameters.OutVector2 = Clamp(parameters.P1Vector2, parameters.P1Float, parameters.P2Float); - break; - case 43: - parameters.OutVector3 = Clamp(parameters.P1Vector3, parameters.P2Vector3, parameters.P1Vector4.XYZ()); - break; - case 44: - parameters.OutVector3 = Clamp(parameters.P1Vector3, parameters.P1Float, parameters.P2Float); - break; - case 45: - parameters.OutVector4 = Clamp(parameters.P1Vector4, parameters.P2Vector4, new Vector4(parameters.P1Vector3, parameters.P1Float)); - break; - case 46: - parameters.OutVector4 = Clamp(parameters.P1Vector4, parameters.P1Float, parameters.P2Float); - break; - - // Cos - case 47: - parameters.OutFloat = Cos(parameters.P1Float); - break; - case 48: - parameters.OutVector2 = Cos(parameters.P1Vector2); - break; - case 49: - parameters.OutVector3 = Cos(parameters.P1Vector3); - break; - case 50: - parameters.OutVector4 = Cos(parameters.P1Vector4); - break; - - // Coshh - case 51: - parameters.OutFloat = Cosh(parameters.P1Float); - break; - case 52: - parameters.OutVector2 = Cosh(parameters.P1Vector2); - break; - case 53: - parameters.OutVector3 = Cosh(parameters.P1Vector3); - break; - case 54: - parameters.OutVector4 = Cosh(parameters.P1Vector4); - break; - - // Exp - case 55: - parameters.OutFloat = Exp(parameters.P1Float); - break; - case 56: - parameters.OutVector2 = Exp(parameters.P1Vector2); - break; - case 57: - parameters.OutVector3 = Exp(parameters.P1Vector3); - break; - case 58: - parameters.OutVector4 = Exp(parameters.P1Vector4); - break; - - // Floor - case 59: - parameters.OutFloat = Floor(parameters.P1Float); - break; - case 60: - parameters.OutVector2 = Floor(parameters.P1Vector2); - break; - case 61: - parameters.OutVector3 = Floor(parameters.P1Vector3); - break; - case 62: - parameters.OutVector4 = Floor(parameters.P1Vector4); - break; - - // Frac - case 63: - parameters.OutFloat = Frac(parameters.P1Float); - break; - case 64: - parameters.OutVector2 = Frac(parameters.P1Vector2); - break; - case 65: - parameters.OutVector3 = Frac(parameters.P1Vector3); - break; - case 66: - parameters.OutVector4 = Frac(parameters.P1Vector4); - break; - - // Lerp - case 67: - parameters.OutFloat = Lerp(parameters.P1Float, parameters.P2Float, parameters.P3Float); - break; - case 68: - parameters.OutVector2 = Lerp(parameters.P1Vector2, parameters.P2Vector2, parameters.P1Vector3.XY()); - break; - case 69: - parameters.OutVector2 = Lerp(parameters.P1Vector2, parameters.P2Vector2, parameters.P2Float); - break; - case 70: - parameters.OutVector3 = Lerp(parameters.P1Vector3, parameters.P2Vector3, parameters.P1Vector4.XYZ()); - break; - case 71: - parameters.OutVector3 = Lerp(parameters.P1Vector3, parameters.P2Vector3, parameters.P2Float); - break; - case 72: - parameters.OutVector4 = Lerp(parameters.P1Vector4, parameters.P2Vector4, new Vector4(parameters.P1Vector3, parameters.P1Float)); - break; - case 73: - parameters.OutVector4 = Lerp(parameters.P1Vector4, parameters.P2Vector4, parameters.P2Float); - break; - - // Log - case 74: - parameters.OutFloat = Log(parameters.P1Float); - break; - case 75: - parameters.OutFloat = Log(parameters.P1Float, parameters.P2Float); - break; - case 76: - parameters.OutVector2 = Log(parameters.P1Vector2); - break; - case 77: - parameters.OutVector2 = Log(parameters.P1Vector2, parameters.P2Vector2); - break; - case 78: - parameters.OutVector2 = Log(parameters.P1Vector2, parameters.P1Float); - break; - case 79: - parameters.OutVector3 = Log(parameters.P1Vector3); - break; - case 80: - parameters.OutVector3 = Log(parameters.P1Vector3, parameters.P2Vector3); - break; - case 81: - parameters.OutVector3 = Log(parameters.P1Vector3, parameters.P1Float); - break; - case 82: - parameters.OutVector4 = Log(parameters.P1Vector4); - break; - case 83: - parameters.OutVector4 = Log(parameters.P1Vector4, parameters.P2Vector4); - break; - case 84: - parameters.OutVector4 = Log(parameters.P1Vector4, parameters.P1Float); - break; - - // Log2 - case 85: - parameters.OutFloat = Log2(parameters.P1Float); - break; - case 86: - parameters.OutVector2 = Log2(parameters.P1Vector2); - break; - case 87: - parameters.OutVector3 = Log2(parameters.P1Vector3); - break; - case 88: - parameters.OutVector4 = Log2(parameters.P1Vector4); - break; - - // Log10 - case 89: - parameters.OutFloat = Log10(parameters.P1Float); - break; - case 90: - parameters.OutVector2 = Log10(parameters.P1Vector2); - break; - case 91: - parameters.OutVector3 = Log10(parameters.P1Vector3); - break; - case 92: - parameters.OutVector4 = Log10(parameters.P1Vector4); - break; - - // Max: - case 93: - parameters.OutFloat = Max(parameters.P1Float, parameters.P2Float); - break; - case 94: - parameters.OutVector2 = Max(parameters.P1Vector2, parameters.P2Vector2); - break; - case 95: - parameters.OutVector2 = Max(parameters.P1Vector2, parameters.P2Vector2); - break; - case 96: - parameters.OutVector3 = Max(parameters.P1Vector3, parameters.P2Vector3); - break; - case 97: - parameters.OutVector3 = Max(parameters.P1Vector3, parameters.P2Vector3); - break; - case 98: - parameters.OutVector4 = Max(parameters.P1Vector4, parameters.P2Vector4); - break; - case 99: - parameters.OutVector4 = Max(parameters.P1Vector4, parameters.P2Vector4); - break; - - // Mod: - case 100: - parameters.OutFloat = Mod(parameters.P1Float, parameters.P2Float); - break; - case 101: - parameters.OutVector2 = Mod(parameters.P1Vector2, parameters.P2Vector2); - break; - case 102: - parameters.OutVector2 = Mod(parameters.P1Vector2, parameters.P2Vector2); - break; - case 103: - parameters.OutVector3 = Mod(parameters.P1Vector3, parameters.P2Vector3); - break; - case 104: - parameters.OutVector3 = Mod(parameters.P1Vector3, parameters.P2Vector3); - break; - case 105: - parameters.OutVector4 = Mod(parameters.P1Vector4, parameters.P2Vector4); - break; - case 106: - parameters.OutVector4 = Mod(parameters.P1Vector4, parameters.P2Vector4); - break; - - // Mul: - case 107: - parameters.OutVector4 = Mul(parameters.P1Matrix, parameters.P1Vector4); - break; - - // Pow - case 108: - parameters.OutFloat = Pow(parameters.P1Float, parameters.P2Float); - break; - case 109: - parameters.OutVector2 = Pow(parameters.P1Vector2, parameters.P2Vector2); - break; - case 110: - parameters.OutVector3 = Pow(parameters.P1Vector3, parameters.P2Vector3); - break; - case 111: - parameters.OutVector4 = Pow(parameters.P1Vector4, parameters.P2Vector4); - break; - - // Round - case 112: - parameters.OutFloat = Round(parameters.P1Float); - break; - case 113: - parameters.OutVector2 = Round(parameters.P1Vector2); - break; - case 114: - parameters.OutVector3 = Round(parameters.P1Vector3); - break; - case 115: - parameters.OutVector4 = Round(parameters.P1Vector4); - break; - - // Saturate - case 116: - parameters.OutFloat = Saturate(parameters.P1Float); - break; - case 117: - parameters.OutVector2 = Saturate(parameters.P1Vector2); - break; - case 118: - parameters.OutVector3 = Saturate(parameters.P1Vector3); - break; - case 119: - parameters.OutVector4 = Saturate(parameters.P1Vector4); - break; - - // Sinh - case 120: - parameters.OutFloat = Sinh(parameters.P1Float); - break; - case 121: - parameters.OutVector2 = Sinh(parameters.P1Vector2); - break; - case 122: - parameters.OutVector3 = Sinh(parameters.P1Vector3); - break; - case 123: - parameters.OutVector4 = Sinh(parameters.P1Vector4); - break; - - // Sqrt - case 124: - parameters.OutFloat = Sqrt(parameters.P1Float); - break; - case 125: - parameters.OutVector2 = Sqrt(parameters.P1Vector2); - break; - case 126: - parameters.OutVector3 = Sqrt(parameters.P1Vector3); - break; - case 127: - parameters.OutVector4 = Sqrt(parameters.P1Vector4); - break; - - // SmoothStep - case 128: - parameters.OutFloat = SmoothStep(parameters.P1Float, parameters.P2Float, parameters.P3Float); - break; - case 129: - parameters.OutVector2 = SmoothStep(parameters.P1Vector2, parameters.P2Vector2, parameters.P1Vector3.XY()); - break; - case 130: - parameters.OutVector2 = SmoothStep(parameters.P1Float, parameters.P2Float, parameters.P1Vector2); - break; - case 131: - parameters.OutVector3 = SmoothStep(parameters.P1Vector3, parameters.P2Vector3, parameters.P1Vector4.XYZ()); - break; - case 132: - parameters.OutVector3 = SmoothStep(parameters.P1Float, parameters.P2Float, parameters.P1Vector3); - break; - case 133: - parameters.OutVector4 = SmoothStep(parameters.P1Vector4, parameters.P2Vector4, new Vector4(parameters.P1Vector3, parameters.P1Float)); - break; - case 134: - parameters.OutVector4 = SmoothStep(parameters.P1Float, parameters.P2Float, parameters.P1Vector4); - break; - - // Tan - case 135: - parameters.OutFloat = Tan(parameters.P1Float); - break; - case 136: - parameters.OutVector2 = Tan(parameters.P1Vector2); - break; - case 137: - parameters.OutVector3 = Tan(parameters.P1Vector3); - break; - case 138: - parameters.OutVector4 = Tan(parameters.P1Vector4); - break; - - // Tanh - case 139: - parameters.OutFloat = Tanh(parameters.P1Float); - break; - case 140: - parameters.OutVector2 = Tanh(parameters.P1Vector2); - break; - case 141: - parameters.OutVector3 = Tanh(parameters.P1Vector3); - break; - case 142: - parameters.OutVector4 = Tanh(parameters.P1Vector4); - break; - - // Truncate - case 143: - parameters.OutFloat = Truncate(parameters.P1Float); - break; - case 144: - parameters.OutVector2 = Truncate(parameters.P1Vector2); - break; - case 145: - parameters.OutVector3 = Truncate(parameters.P1Vector3); - break; - case 146: - parameters.OutVector4 = Truncate(parameters.P1Vector4); - break; - } - - InOutParameters[index] = parameters; - } - } -} \ No newline at end of file diff --git a/src/ShaderGen.Tests/TestTimer.cs b/src/ShaderGen.Tests/TestTimer.cs new file mode 100644 index 0000000..bfe0450 --- /dev/null +++ b/src/ShaderGen.Tests/TestTimer.cs @@ -0,0 +1,81 @@ +using System; +using System.Diagnostics; +using Xunit.Abstractions; + +namespace ShaderGen.Tests +{ + /// + /// Allows timing of a block of code. + /// + /// + public sealed class TestTimer : IDisposable + { + /// + /// The action to call once the the object is dipsosed. + /// + private readonly Action _action; + + /// + /// The initial time stamp + /// + private readonly long _timeStamp; + + /// + /// Initializes a new instance of the class. + /// + /// The action. + public TestTimer(Action action) + { + _timeStamp = Stopwatch.GetTimestamp(); + _action = action; + } + + /// + /// Initializes a new instance of the class. + /// + /// The output. + /// The message. + public TestTimer(ITestOutputHelper output, string message) + { + _timeStamp = Stopwatch.GetTimestamp(); + _action = t => output.WriteLine($"{message} took {t * 1000:#.##}ms"); + } + + /// + /// Initializes a new instance of the class. + /// + /// The output. + /// The function that will be called to get a message once the operation is complete. + public TestTimer(ITestOutputHelper output, Func getMessage) + { + _timeStamp = Stopwatch.GetTimestamp(); + _action = t => output.WriteLine($"{getMessage()} took {t * 1000:#.##}ms"); + } + /// + /// Initializes a new instance of the class. + /// + /// The output. + /// The function that will be called to get a message once the operation is complete. + public TestTimer(ITestOutputHelper output, Func getMessage) + { + _timeStamp = Stopwatch.GetTimestamp(); + _action = t => output.WriteLine(getMessage(t)); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + double elapsed = ((double)Stopwatch.GetTimestamp() - _timeStamp) / Stopwatch.Frequency; + try + { + _action(elapsed); + } + catch + { + // ignored + } + } + } +} \ No newline at end of file diff --git a/src/ShaderGen.Tests/TestUtil.cs b/src/ShaderGen.Tests/TestUtil.cs index 6ff42dd..d2525dc 100644 --- a/src/ShaderGen.Tests/TestUtil.cs +++ b/src/ShaderGen.Tests/TestUtil.cs @@ -4,20 +4,27 @@ using System.IO; using System.Collections.Generic; using System.Linq; -using System.Numerics; using System.Reflection; using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis.Text; using System.Runtime.InteropServices; using System.Threading; -using ShaderGen.Glsl; -using ShaderGen.Hlsl; using ShaderGen.Tests.Tools; namespace ShaderGen.Tests { - internal class TestUtil + internal static class TestUtil { + /// + /// A string of '═' symbols + /// + public static readonly string Spacer1 = new string('═', 80); + + /// + /// A string of '━' symbols + /// + public static readonly string Spacer2 = new string('━', 80); + private static readonly string ProjectBasePath = Path.Combine(AppContext.BaseDirectory, "TestAssets"); public static Compilation GetCompilation() @@ -147,16 +154,21 @@ public static LanguageBackend[] GetAllBackends(Compilation compilation, ToolFeat => ToolChain.Requires(features, false).Select(t => t.CreateBackend(compilation)) .ToArray(); - public static IReadOnlyCollection<(string fieldName, object aValue, object bValue)> DeepCompareObjectFields(T a, T b) + public static IReadOnlyList<(string fieldName, object aValue, object bValue)> DeepCompareObjectFields(object a, object b) { // Creat failures list List<(string fieldName, object aValue, object bValue)> failures = new List<(string fieldName, object aValue, object bValue)>(); + if (a == b) + { + return failures; + } + // Get dictionary of fields by field name and type Dictionary> childFieldInfos = new Dictionary>(); - Type currentType = typeof(T); + Type currentType = a?.GetType() ?? b.GetType(); object aValue = a; object bValue = b; Stack<(string fieldName, Type fieldType, object aValue, object bValue)> stack @@ -250,5 +262,160 @@ public static unsafe T FillRandomFloats(int minMantissa = -126, int maxMantis return Unsafe.Read(floats); } + + /// + /// Gets a set of random floats. + /// + /// The number of floats. + /// The minimum mantissa. + /// The maximum mantissa. + /// + /// minMantissa + /// or + /// maxMantissa + public static float[] GetRandomFloats(int floatCount, int minMantissa = -126, int maxMantissa = 128) + { + Random random = _randomGenerators.Value; + float[] floats = new float[floatCount]; + for (int i = 0; i < floatCount; i++) + { + floats[i] = (float)((random.NextDouble() * 2.0 - 1.0) * Math.Pow(2.0, random.Next(minMantissa, maxMantissa))); + } + + return floats; + } + + #region ToMemorySize from https://github.com/webappsuk/CoreLibraries/blob/fbbebc99bc5c1f2e8b140c6c387e3ede4f89b40c/Utilities/UtilityExtensions.cs#L2951-L3116 + private static readonly string[] _memoryUnitsLong = + { + " byte", + " kilobyte", + " megabyte", + " gigabyte", + " terabyte", + " petabyte", + " exabyte" + }; + + private static readonly string[] _memoryUnitsShort = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }; + + /// + /// Converts a number of bytes to a friendly memory size. + /// + /// The bytes. + /// if set to use long form unit names instead of symbols. + /// The number of decimal places between 0 and 16 (ignored for bytes). + /// The break point between 0 and 1024 (or 0D to base on decimal points). + /// System.String. + public static string ToMemorySize( + this int bytes, + bool longUnits = false, + uint decimalPlaces = 1, + double breakPoint = 512D) => ToMemorySize((double)bytes, longUnits, decimalPlaces, breakPoint); + + /// + /// Converts a number of bytes to a friendly memory size. + /// + /// The bytes. + /// if set to use long form unit names instead of symbols. + /// The number of decimal places between 0 and 16 (ignored for bytes). + /// The break point between 0 and 1024 (or 0D to base on decimal points). + /// System.String. + public static string ToMemorySize( + this long bytes, + bool longUnits = false, + uint decimalPlaces = 1, + double breakPoint = 512D) => ToMemorySize((double)bytes, longUnits, decimalPlaces, breakPoint); + + /// + /// Converts a number of bytes to a friendly memory size. + /// + /// The bytes. + /// if set to use long form unit names instead of symbols. + /// The number of decimal places between 0 and 16 (ignored for bytes). + /// The break point between 0 and 1024 (or 0D to base on decimal points). + /// System.String. + public static string ToMemorySize( + this double bytes, + bool longUnits = false, + uint decimalPlaces = 1, + double breakPoint = 512D) + { + if (decimalPlaces < 1) + { + decimalPlaces = 0; + } + else if (decimalPlaces > 16) + { + decimalPlaces = 16; + } + + // 921.6 is 0.9*1024, this means that be default the breakpoint will round up the last decimal place. + if (breakPoint < 1) + { + breakPoint = 921.6D * Math.Pow(10, -decimalPlaces); + } + else if (breakPoint > 1023) + { + breakPoint = 1023; + } + + uint maxDecimalPlaces = 0; + uint unit = 0; + double amount = bytes; + while ((Math.Abs(amount) >= breakPoint) && + (unit < 6)) + { + amount /= 1024; + unit++; + maxDecimalPlaces = Math.Min(decimalPlaces, maxDecimalPlaces + 3); + } + + string format = "{0:N" + maxDecimalPlaces + "}{1}"; + return string.Format( + format, + amount, + longUnits + ? _memoryUnitsLong[unit] + : _memoryUnitsShort[unit]); + } + #endregion + + public static bool ApproximatelyEqual(this float a, float b, float epsilon = float.Epsilon) + { + const float floatNormal = (1 << 23) * float.Epsilon; + + if (a == b) + { + // Shortcut, handles infinities + return true; + } + + float diff = Math.Abs(a - b); + if (a == 0.0f || b == 0.0f || diff < floatNormal) + { + // a or b is zero, or both are extremely close to it. + // relative error is less meaningful here + return diff < (epsilon * floatNormal); + } + + float absA = Math.Abs(a); + float absB = Math.Abs(b); + // use relative error + return diff / Math.Min((absA + absB), float.MaxValue) < epsilon; + } + + /// + /// The unicode characters to represent a pie chart. + /// + private static readonly char[] UnicodePieChars = { '○', '◔', '◑', '◕', '●' }; + + /// + /// Gets the unicode symbol to represent the as a pie chart. + /// + /// The ratio. + /// + public static char GetUnicodePieChart(double ratio) => + UnicodePieChars[(int)Math.Round(Math.Max(Math.Min(ratio, 1.0), 0.0) * (UnicodePieChars.Length - 1))]; } } \ No newline at end of file diff --git a/src/ShaderGen.Tests/Tools/ToolChain.cs b/src/ShaderGen.Tests/Tools/ToolChain.cs index e7cea23..af19df0 100644 --- a/src/ShaderGen.Tests/Tools/ToolChain.cs +++ b/src/ShaderGen.Tests/Tools/ToolChain.cs @@ -211,7 +211,7 @@ public static ToolChain Require( /// The graphicsBackends required (leave empty to get all). /// /// - public static IReadOnlyCollection Requires(params GraphicsBackend[] graphicsBackends) + public static IReadOnlyList Requires(params GraphicsBackend[] graphicsBackends) => Requires(ToolFeatures.All, true, (IEnumerable)graphicsBackends); /// @@ -220,7 +220,7 @@ public static IReadOnlyCollection Requires(params GraphicsBackend[] g /// The graphicsBackends required (leave empty to get all). /// /// - public static IReadOnlyCollection Requires(IEnumerable graphicsBackends) + public static IReadOnlyList Requires(IEnumerable graphicsBackends) => Requires(ToolFeatures.All, true, graphicsBackends); /// @@ -230,7 +230,7 @@ public static IReadOnlyCollection Requires(IEnumerableThe graphicsBackends required (leave empty to get all). /// /// - public static IReadOnlyCollection Requires(ToolFeatures requiredFeatures, + public static IReadOnlyList Requires(ToolFeatures requiredFeatures, params GraphicsBackend[] graphicsBackends) => Requires(requiredFeatures, true, (IEnumerable)graphicsBackends); @@ -241,7 +241,7 @@ public static IReadOnlyCollection Requires(ToolFeatures requiredFeatu /// The graphicsBackends required (leave empty to get all). /// /// - public static IReadOnlyCollection Requires(ToolFeatures requiredFeatures, IEnumerable graphicsBackends) + public static IReadOnlyList Requires(ToolFeatures requiredFeatures, IEnumerable graphicsBackends) => Requires(requiredFeatures, true, graphicsBackends); /// @@ -253,7 +253,7 @@ public static IReadOnlyCollection Requires(ToolFeatures requiredFeatu /// The graphicsBackends required (leave empty to get all). /// /// - public static IReadOnlyCollection Requires(ToolFeatures requiredFeatures, bool throwOnFail, + public static IReadOnlyList Requires(ToolFeatures requiredFeatures, bool throwOnFail, params GraphicsBackend[] graphicsBackends) => Requires(requiredFeatures, throwOnFail, (IEnumerable)graphicsBackends); @@ -274,7 +274,7 @@ public static ToolChain Get(ToolFeatures features) => /// The graphics graphicsBackends. /// /// - public static IReadOnlyCollection Requires( + public static IReadOnlyList Requires( ToolFeatures requiredFeatures, bool throwOnFail, IEnumerable graphicsBackends) @@ -509,6 +509,8 @@ private static CompileResult Execute( string outputPath = null, Encoding encoding = default(Encoding)) { + using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false)) + using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false)) using (Process process = new Process()) { process.StartInfo = new ProcessStartInfo @@ -523,89 +525,92 @@ private static CompileResult Execute( StringBuilder output = new StringBuilder(); StringBuilder error = new StringBuilder(); - - using (AutoResetEvent outputWaitHandle = new AutoResetEvent(false)) - using (AutoResetEvent errorWaitHandle = new AutoResetEvent(false)) + // Add handlers to handle data + // ReSharper disable AccessToDisposedClosure + process.OutputDataReceived += (sender, e) => { - // Add handlers to handle data - // ReSharper disable AccessToDisposedClosure - process.OutputDataReceived += (sender, e) => + if (e.Data == null) { - if (e.Data == null) + try { outputWaitHandle.Set(); } - else - { - output.AppendLine(e.Data); - } - }; - process.ErrorDataReceived += (sender, e) => + catch { } + } + else { - if (e.Data == null) - { - errorWaitHandle.Set(); - } - else - { - error.AppendLine(e.Data); - } - }; - // ReSharper restore AccessToDisposedClosure - - process.Start(); - - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - - int exitCode; - if (!process.WaitForExit(DefaultTimeout) || !outputWaitHandle.WaitOne(DefaultTimeout) || - !errorWaitHandle.WaitOne(DefaultTimeout)) + output.AppendLine(e.Data); + } + }; + process.ErrorDataReceived += (sender, e) => + { + if (e.Data == null) { - if (output.Length > 0) + try { - output.AppendLine("TIMED OUT!").AppendLine(); + errorWaitHandle.Set(); } - - error.AppendLine($"Timed out calling: \"{toolPath}\" {process.StartInfo.Arguments}"); - exitCode = int.MinValue; + catch { } } else { - exitCode = process.ExitCode; + error.AppendLine(e.Data); } + }; + // ReSharper restore AccessToDisposedClosure + + process.Start(); - // Get compiled output (if any). - byte[] outputBytes; - if (string.IsNullOrWhiteSpace(outputPath)) + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + int exitCode; + if (!process.WaitForExit(DefaultTimeout) || !outputWaitHandle.WaitOne(DefaultTimeout) || + !errorWaitHandle.WaitOne(DefaultTimeout)) + { + if (output.Length > 0) { - // No output expected, just encode the existing code into bytes. - outputBytes = (encoding ?? Encoding.Default).GetBytes(code); + output.AppendLine("TIMED OUT!").AppendLine(); } - else + + error.AppendLine($"Timed out calling: \"{toolPath}\" {process.StartInfo.Arguments}"); + exitCode = int.MinValue; + } + else + { + exitCode = process.ExitCode; + } + + // Get compiled output (if any). + byte[] outputBytes; + if (string.IsNullOrWhiteSpace(outputPath)) + { + // No output expected, just encode the existing code into bytes. + outputBytes = (encoding ?? Encoding.Default).GetBytes(code); + } + else + { + if (File.Exists(outputPath)) { - if (File.Exists(outputPath)) + try { - try - { - // Attemp to read output file - outputBytes = File.ReadAllBytes(outputPath); - } - catch (Exception e) - { - outputBytes = Array.Empty(); - error.AppendLine($"Failed to read the output file, \"{outputPath}\": {e.Message}"); - } + // Attemp to read output file + outputBytes = File.ReadAllBytes(outputPath); } - else + catch (Exception e) { outputBytes = Array.Empty(); - error.AppendLine($"The output file \"{outputPath}\" was not found!"); + error.AppendLine($"Failed to read the output file, \"{outputPath}\": {e.Message}"); } } - - return new CompileResult(code, exitCode, output.ToString(), error.ToString(), outputBytes); + else + { + outputBytes = Array.Empty(); + error.AppendLine($"The output file \"{outputPath}\" was not found!"); + } } + + return new CompileResult(code, exitCode, output.ToString(), error.ToString(), outputBytes); } } diff --git a/src/ShaderGen/Glsl/Glsl330KnownFunctions.cs b/src/ShaderGen/Glsl/Glsl330KnownFunctions.cs index 5caea68..569976d 100644 --- a/src/ShaderGen/Glsl/Glsl330KnownFunctions.cs +++ b/src/ShaderGen/Glsl/Glsl330KnownFunctions.cs @@ -23,11 +23,11 @@ private static Dictionary GetMappings() { nameof(ShaderBuiltins.Acosh), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Asin), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Asinh), SimpleNameTranslator() }, - { nameof(ShaderBuiltins.Atan), SimpleNameTranslator() },// Note atan supports both (x) and (y,x) + { nameof(ShaderBuiltins.Atan), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Atanh), SimpleNameTranslator() }, - { nameof(ShaderBuiltins.Cbrt), CubeRoot }, // We can calculate the 1/3rd power, which might not give exactly the same result? + { nameof(ShaderBuiltins.Cbrt), CubeRoot }, { nameof(ShaderBuiltins.Ceiling), SimpleNameTranslator("ceil") }, - { nameof(ShaderBuiltins.Clamp), SimpleNameTranslator() }, + { nameof(ShaderBuiltins.Clamp), Clamp }, { nameof(ShaderBuiltins.ClipToTextureCoordinates), ClipToTextureCoordinates }, { nameof(ShaderBuiltins.Cos), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Cosh), SimpleNameTranslator() }, @@ -39,6 +39,7 @@ private static Dictionary GetMappings() { nameof(ShaderBuiltins.DispatchThreadID), DispatchThreadID }, { nameof(ShaderBuiltins.Exp), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Floor), SimpleNameTranslator() }, + { nameof(ShaderBuiltins.FMod), FMod }, { nameof(ShaderBuiltins.Frac), SimpleNameTranslator("fract") }, { nameof(ShaderBuiltins.GroupThreadID), GroupThreadID }, { nameof(ShaderBuiltins.InstanceID), InstanceID }, @@ -51,10 +52,9 @@ private static Dictionary GetMappings() { nameof(ShaderBuiltins.Log10), Log10 }, { nameof(ShaderBuiltins.Max), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Min), SimpleNameTranslator() }, - // Potential BUG: https://stackoverflow.com/questions/7610631/glsl-mod-vs-hlsl-fmod { nameof(ShaderBuiltins.Mod), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Mul), MatrixMul }, - { nameof(ShaderBuiltins.Pow), SimpleNameTranslator() }, + { nameof(ShaderBuiltins.Pow), Pow }, { nameof(ShaderBuiltins.Round), Round }, { nameof(ShaderBuiltins.Sample), Sample }, { nameof(ShaderBuiltins.SampleComparisonLevelZero), SampleComparisonLevelZero }, @@ -205,7 +205,7 @@ private static Dictionary GetMappings() { "Log10", Log10 }, { "Max", SimpleNameTranslator() }, { "Min", SimpleNameTranslator() }, - { "Pow", SimpleNameTranslator() }, + { "Pow", Pow }, { "Round", Round }, { "Sin", SimpleNameTranslator() }, { "Sinh", SimpleNameTranslator() }, @@ -535,19 +535,6 @@ private static void GetVectorTypeInfo(string name, out string shaderType, out in else { throw new ShaderGenerationException("VectorCtor translator was called on an invalid type: " + name); } } - private static string CubeRoot(string typeName, string methodName, InvocationParameterInfo[] parameters) - { - string pType = parameters[0].FullTypeName; - if (pType == "System.Single" || pType == "float") // TODO Why are we getting float? - { - return $"pow({parameters[0].Identifier}, 0.333333333333333)"; - } - - GetVectorTypeInfo(pType, out string shaderType, out int elementCount); - return - $"pow({parameters[0].Identifier}, {shaderType}({string.Join(",", Enumerable.Range(0, elementCount).Select(i => "0.333333333333333"))}))"; - } - private static string Log(string typeName, string methodName, InvocationParameterInfo[] parameters) { if (parameters.Length < 2) @@ -593,5 +580,73 @@ private static string Round(string typeName, string methodName, InvocationParame // Round(Single, MidpointRounding) throw new NotImplementedException(); } + + private static string CubeRoot(string typeName, string methodName, InvocationParameterInfo[] parameters) + { + return AddCheck(parameters[0].FullTypeName, + $"pow(abs({parameters[0].Identifier}`), 0.333333333333333)"); + } + + private static string Pow(string typeName, string methodName, InvocationParameterInfo[] parameters) + { + // OpenGL returns NaN for -ve P0's, whereas Vulkan ignores sign. + return AddCheck(parameters[0].FullTypeName, + $"pow(abs({parameters[0].Identifier}`),{parameters[1].Identifier}`)"); + } + + private static string Clamp(string typeName, string methodName, InvocationParameterInfo[] parameters) + { + // D3D & Vulkan return Max when max < min, but OpenGL returns Min, so we need + // to correct by returning Max when max < min. + bool isFloat = parameters[1].FullTypeName == "System.Single" || parameters[1].FullTypeName == "float"; + string p1 = $"{parameters[1].Identifier}{(isFloat ? string.Empty : "`")}"; + string p2 = $"{parameters[2].Identifier}{(isFloat ? string.Empty : "`")}"; + return AddCheck(parameters[0].FullTypeName, + $"({p1}<{p2}?clamp({parameters[0].Identifier}`,{p1},{p2}):{p2})"); + } + + private static string FMod(string typeName, string methodName, InvocationParameterInfo[] parameters) + { + // D3D & Vulkan return Max when max < min, but OpenGL returns Min, so we need + // to correct by returning Max when max < min. + bool isFloat = parameters[1].FullTypeName == "System.Single" || parameters[1].FullTypeName == "float"; + string p0 = $"{parameters[0].Identifier}`"; + string p1 = $"{parameters[1].Identifier}{(isFloat ? string.Empty : "`")}"; + return AddCheck(parameters[0].FullTypeName, + $"({p0}-{p1}*trunc({p0}/{p1}))"); + } + + private static readonly string[] _vectorAccessors = { "x", "y", "z", "w" }; + + private static readonly HashSet _oneDimensionalTypes = + new HashSet(new[] + { + "System.Single", + "float", + "System.Int32", + "int", + "System.UInt32", + "uint" + }, + StringComparer.InvariantCultureIgnoreCase); + + /// + /// Implements a check for each element of a vector. + /// + /// Name of the type. + /// The check. + /// + private static string AddCheck(string typeName, string check) + { + if (_oneDimensionalTypes.Contains(typeName)) + { + // The check can stay as it is, strip the '`' characters. + return check.Replace("`", string.Empty); + } + + GetVectorTypeInfo(typeName, out string shaderType, out int elementCount); + return + $"{shaderType}({string.Join(",", _vectorAccessors.Take(elementCount).Select(a => check.Replace("`", "." + a)))})"; + } } } diff --git a/src/ShaderGen/Glsl/Glsl450KnownFunctions.cs b/src/ShaderGen/Glsl/Glsl450KnownFunctions.cs index 3b94f00..5f7761a 100644 --- a/src/ShaderGen/Glsl/Glsl450KnownFunctions.cs +++ b/src/ShaderGen/Glsl/Glsl450KnownFunctions.cs @@ -23,9 +23,9 @@ private static Dictionary GetMappings() { nameof(ShaderBuiltins.Acosh), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Asin), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Asinh), SimpleNameTranslator() }, - { nameof(ShaderBuiltins.Atan), SimpleNameTranslator() },// Note atan supports both (x) and (y,x) + { nameof(ShaderBuiltins.Atan), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Atanh), SimpleNameTranslator() }, - { nameof(ShaderBuiltins.Cbrt), CubeRoot }, // We can calculate the 1/3rd power, which might not give exactly the same result? + { nameof(ShaderBuiltins.Cbrt), CubeRoot }, { nameof(ShaderBuiltins.Ceiling), SimpleNameTranslator("ceil") }, { nameof(ShaderBuiltins.Clamp), SimpleNameTranslator() }, { nameof(ShaderBuiltins.ClipToTextureCoordinates), ClipToTextureCoordinates }, @@ -39,6 +39,7 @@ private static Dictionary GetMappings() { nameof(ShaderBuiltins.DispatchThreadID), DispatchThreadID }, { nameof(ShaderBuiltins.Exp), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Floor), SimpleNameTranslator() }, + { nameof(ShaderBuiltins.FMod), FMod }, { nameof(ShaderBuiltins.Frac), SimpleNameTranslator("fract") }, { nameof(ShaderBuiltins.GroupThreadID), GroupThreadID }, { nameof(ShaderBuiltins.InstanceID), InstanceID }, @@ -51,7 +52,6 @@ private static Dictionary GetMappings() { nameof(ShaderBuiltins.Log10), Log10 }, { nameof(ShaderBuiltins.Max), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Min), SimpleNameTranslator() }, - // Potential BUG: https://stackoverflow.com/questions/7610631/glsl-mod-vs-hlsl-fmod { nameof(ShaderBuiltins.Mod), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Mul), MatrixMul }, { nameof(ShaderBuiltins.Pow), SimpleNameTranslator() }, @@ -565,6 +565,7 @@ private static string CubeRoot(string typeName, string methodName, InvocationPar } GetVectorTypeInfo(pType, out string shaderType, out int elementCount); + // TODO All backends but Vulkan return NaN for Cbrt of a -ve number... return $"pow({parameters[0].Identifier}, {shaderType}({string.Join(",", Enumerable.Range(0, elementCount).Select(i => "0.333333333333333"))}))"; } @@ -614,5 +615,49 @@ private static string Round(string typeName, string methodName, InvocationParame // Round(Single, MidpointRounding) throw new NotImplementedException(); } + + private static string FMod(string typeName, string methodName, InvocationParameterInfo[] parameters) + { + // D3D & Vulkan return Max when max < min, but OpenGL returns Min, so we need + // to correct by returning Max when max < min. + bool isFloat = parameters[1].FullTypeName == "System.Single" || parameters[1].FullTypeName == "float"; + string p0 = $"{parameters[0].Identifier}`"; + string p1 = $"{parameters[1].Identifier}{(isFloat ? string.Empty : "`")}"; + return AddCheck(parameters[0].FullTypeName, + $"({p0}-{p1}*trunc({p0}/{p1}))"); + } + + private static readonly string[] _vectorAccessors = { "x", "y", "z", "w" }; + + private static readonly HashSet _oneDimensionalTypes = + new HashSet(new[] + { + "System.Single", + "float", + "System.Int32", + "int", + "System.UInt32", + "uint" + }, + StringComparer.InvariantCultureIgnoreCase); + + /// + /// Implements a check for each element of a vector. + /// + /// Name of the type. + /// The check. + /// + private static string AddCheck(string typeName, string check) + { + if (_oneDimensionalTypes.Contains(typeName)) + { + // The check can stay as it is, strip the '`' characters. + return check.Replace("`", string.Empty); + } + + GetVectorTypeInfo(typeName, out string shaderType, out int elementCount); + return + $"{shaderType}({string.Join(",", _vectorAccessors.Take(elementCount).Select(a => check.Replace("`", "." + a)))})"; + } } } diff --git a/src/ShaderGen/Glsl/GlslEs300KnownFunctions.cs b/src/ShaderGen/Glsl/GlslEs300KnownFunctions.cs index 88ffdeb..8e9a87a 100644 --- a/src/ShaderGen/Glsl/GlslEs300KnownFunctions.cs +++ b/src/ShaderGen/Glsl/GlslEs300KnownFunctions.cs @@ -23,11 +23,11 @@ private static Dictionary GetMappings() { nameof(ShaderBuiltins.Acosh), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Asin), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Asinh), SimpleNameTranslator() }, - { nameof(ShaderBuiltins.Atan), SimpleNameTranslator() },// Note atan supports both (x) and (y,x) + { nameof(ShaderBuiltins.Atan), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Atanh), SimpleNameTranslator() }, - { nameof(ShaderBuiltins.Cbrt), CubeRoot }, // We can calculate the 1/3rd power, which might not give exactly the same result? + { nameof(ShaderBuiltins.Cbrt), CubeRoot }, { nameof(ShaderBuiltins.Ceiling), SimpleNameTranslator("ceil") }, - { nameof(ShaderBuiltins.Clamp), SimpleNameFloatParameterTranslator() }, + { nameof(ShaderBuiltins.Clamp), Clamp }, { nameof(ShaderBuiltins.ClipToTextureCoordinates), ClipToTextureCoordinates }, { nameof(ShaderBuiltins.Cos), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Cosh), SimpleNameTranslator() }, @@ -39,6 +39,7 @@ private static Dictionary GetMappings() { nameof(ShaderBuiltins.DispatchThreadID), DispatchThreadID }, { nameof(ShaderBuiltins.Exp), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Floor), SimpleNameTranslator() }, + { nameof(ShaderBuiltins.FMod), FMod }, { nameof(ShaderBuiltins.Frac), SimpleNameTranslator("fract") }, { nameof(ShaderBuiltins.GroupThreadID), GroupThreadID }, { nameof(ShaderBuiltins.InstanceID), InstanceID }, @@ -51,10 +52,9 @@ private static Dictionary GetMappings() { nameof(ShaderBuiltins.Log10), Log10 }, { nameof(ShaderBuiltins.Max), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Min), SimpleNameTranslator() }, - // Potential BUG: https://stackoverflow.com/questions/7610631/glsl-mod-vs-hlsl-fmod { nameof(ShaderBuiltins.Mod), SimpleNameFloatParameterTranslator() }, { nameof(ShaderBuiltins.Mul), MatrixMul }, - { nameof(ShaderBuiltins.Pow), SimpleNameFloatParameterTranslator() }, + { nameof(ShaderBuiltins.Pow), Pow }, { nameof(ShaderBuiltins.Round), Round }, { nameof(ShaderBuiltins.Sample), Sample }, { nameof(ShaderBuiltins.SampleComparisonLevelZero), SampleComparisonLevelZero }, @@ -205,7 +205,7 @@ private static Dictionary GetMappings() { "Log10", Log10 }, { "Max", SimpleNameTranslator() }, { "Min", SimpleNameTranslator() }, - { "Pow", SimpleNameFloatParameterTranslator() }, + { "Pow", Pow }, { "Round", Round }, { "Sin", SimpleNameTranslator() }, { "Sinh", SimpleNameTranslator() }, @@ -557,19 +557,6 @@ private static void GetVectorTypeInfo(string name, out string shaderType, out in else { throw new ShaderGenerationException("VectorCtor translator was called on an invalid type: " + name); } } - private static string CubeRoot(string typeName, string methodName, InvocationParameterInfo[] parameters) - { - string pType = parameters[0].FullTypeName; - if (pType == "System.Single" || pType == "float") // TODO Why are we getting float? - { - return $"pow({parameters[0].Identifier}, 0.333333333333333)"; - } - - GetVectorTypeInfo(pType, out string shaderType, out int elementCount); - return - $"pow({parameters[0].Identifier}, {shaderType}({string.Join(",", Enumerable.Range(0, elementCount).Select(i => "0.333333333333333"))}))"; - } - private static string Log(string typeName, string methodName, InvocationParameterInfo[] parameters) { if (parameters.Length < 2) @@ -615,5 +602,73 @@ private static string Round(string typeName, string methodName, InvocationParame // Round(Single, MidpointRounding) throw new NotImplementedException(); } + + private static string CubeRoot(string typeName, string methodName, InvocationParameterInfo[] parameters) + { + return AddCheck(parameters[0].FullTypeName, + $"pow(abs(float({parameters[0].Identifier}`)), 0.333333333333333)"); + } + + private static string Pow(string typeName, string methodName, InvocationParameterInfo[] parameters) + { + // OpenGL returns NaN for -ve P0's, whereas Vulkan ignores sign. + return AddCheck(parameters[0].FullTypeName, + $"pow(abs(float({parameters[0].Identifier}`)),float({parameters[1].Identifier}`))"); + } + + private static string Clamp(string typeName, string methodName, InvocationParameterInfo[] parameters) + { + // D3D & Vulkan return Max when max < min, but OpenGL returns Min, so we need + // to correct by returning Max when max < min. + bool isFloat = parameters[1].FullTypeName == "System.Single" || parameters[1].FullTypeName == "float"; + string p1 = $"{parameters[1].Identifier}{(isFloat ? string.Empty : "`")}"; + string p2 = $"{parameters[2].Identifier}{(isFloat ? string.Empty : "`")}"; + return AddCheck(parameters[0].FullTypeName, + $"((float({p1}) _oneDimensionalTypes = + new HashSet(new[] + { + "System.Single", + "float", + "System.Int32", + "int", + "System.UInt32", + "uint" + }, + StringComparer.InvariantCultureIgnoreCase); + + /// + /// Implements a check for each element of a vector. + /// + /// Name of the type. + /// The check. + /// + private static string AddCheck(string typeName, string check) + { + if (_oneDimensionalTypes.Contains(typeName)) + { + // The check can stay as it is, strip the '`' characters. + return check.Replace("`", string.Empty); + } + + GetVectorTypeInfo(typeName, out string shaderType, out int elementCount); + return + $"{shaderType}({string.Join(",", _vectorAccessors.Take(elementCount).Select(a => check.Replace("`", "." + a)))})"; + } } } diff --git a/src/ShaderGen/Hlsl/HlslKnownFunctions.cs b/src/ShaderGen/Hlsl/HlslKnownFunctions.cs index b2661e4..5849f1f 100644 --- a/src/ShaderGen/Hlsl/HlslKnownFunctions.cs +++ b/src/ShaderGen/Hlsl/HlslKnownFunctions.cs @@ -23,9 +23,9 @@ private static Dictionary GetMappings() { nameof(ShaderBuiltins.Acosh), Acosh }, { nameof(ShaderBuiltins.Asin), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Asinh), Asinh }, - { nameof(ShaderBuiltins.Atan), Atan },// Note atan supports both (x) and (y,x) + { nameof(ShaderBuiltins.Atan), Atan }, { nameof(ShaderBuiltins.Atanh), Atanh }, - { nameof(ShaderBuiltins.Cbrt), CubeRoot }, // We can calculate the 1/3rd power, which might not give exactly the same result? + { nameof(ShaderBuiltins.Cbrt), CubeRoot }, { nameof(ShaderBuiltins.Ceiling), SimpleNameTranslator("ceil") }, { nameof(ShaderBuiltins.Clamp), SimpleNameTranslator() }, { nameof(ShaderBuiltins.ClipToTextureCoordinates), ClipToTextureCoordinates }, @@ -39,6 +39,7 @@ private static Dictionary GetMappings() { nameof(ShaderBuiltins.DispatchThreadID), DispatchThreadID }, { nameof(ShaderBuiltins.Exp), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Floor), SimpleNameTranslator() }, + { nameof(ShaderBuiltins.FMod), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Frac), SimpleNameTranslator() }, { nameof(ShaderBuiltins.GroupThreadID), GroupThreadID }, { nameof(ShaderBuiltins.InstanceID), InstanceID }, @@ -51,10 +52,9 @@ private static Dictionary GetMappings() { nameof(ShaderBuiltins.Log10), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Max), SimpleNameTranslator() }, { nameof(ShaderBuiltins.Min), SimpleNameTranslator() }, - // Potential BUG: https://stackoverflow.com/questions/7610631/glsl-mod-vs-hlsl-fmod - { nameof(ShaderBuiltins.Mod), SimpleNameTranslator("fmod") }, + { nameof(ShaderBuiltins.Mod), Mod }, { nameof(ShaderBuiltins.Mul), SimpleNameTranslator() }, - { nameof(ShaderBuiltins.Pow), SimpleNameTranslator() }, + { nameof(ShaderBuiltins.Pow), Pow }, { nameof(ShaderBuiltins.Round), Round }, { nameof(ShaderBuiltins.Sample), Sample }, { nameof(ShaderBuiltins.SampleComparisonLevelZero), SampleComparisonLevelZero }, @@ -205,7 +205,7 @@ private static Dictionary GetMappings() { "Log10", SimpleNameTranslator() }, { "Max", SimpleNameTranslator() }, { "Min", SimpleNameTranslator() }, - { "Pow", SimpleNameTranslator() }, + { "Pow", Pow }, { "Round", Round }, { "Sin", SimpleNameTranslator() }, { "Sinh", SimpleNameTranslator() }, @@ -531,12 +531,6 @@ private static string Cvc(string typeName, float value) return $"{shaderType}({string.Join(",", Enumerable.Range(0, elementCount).Select(i => v))})"; } - private static string CubeRoot(string typeName, string methodName, InvocationParameterInfo[] parameters) - { - InvocationParameterInfo firstParameter = parameters[0]; - return $"pow({firstParameter.Identifier}, {Cvc(firstParameter.FullTypeName, 0.333333333333333f)})"; - } - private static string Log(string typeName, string methodName, InvocationParameterInfo[] parameters) { if (parameters.Length < 2) @@ -603,7 +597,64 @@ private static string Atanh(string typeName, string methodName, InvocationParame string target = firstParameter.Identifier; // Note this is pretty inaccurate on the GPU! return - $"log(({Cvc(firstParameter.FullTypeName, 1f)}+{target})/({Cvc(firstParameter.FullTypeName, 1f)}-{target})/{Cvc(firstParameter.FullTypeName, 2f)})"; + $"log(({Cvc(firstParameter.FullTypeName, 1f)}+{target})/({Cvc(firstParameter.FullTypeName, 1f)}-{target}))/{Cvc(firstParameter.FullTypeName, 2f)}"; + } + + private static string CubeRoot(string typeName, string methodName, InvocationParameterInfo[] parameters) + { + return AddCheck(parameters[0].FullTypeName, + $"pow(abs({parameters[0].Identifier}`), 0.333333333333333)"); + } + + private static string Pow(string typeName, string methodName, InvocationParameterInfo[] parameters) + { + // Direct 3D returns NaN for -ve P0's, whereas Vulkan ignores sign. + return AddCheck(parameters[0].FullTypeName, + $"pow(abs({parameters[0].Identifier}`),{parameters[1].Identifier}`)"); + } + + private static string Mod(string typeName, string methodName, InvocationParameterInfo[] parameters) + { + // D3D & Vulkan return Max when max < min, but OpenGL returns Min, so we need + // to correct by returning Max when max < min. + bool isFloat = parameters[1].FullTypeName == "System.Single" || parameters[1].FullTypeName == "float"; + string p0 = $"{parameters[0].Identifier}`"; + string p1 = $"{parameters[1].Identifier}{(isFloat ? string.Empty : "`")}"; + return AddCheck(parameters[0].FullTypeName, + $"({p0}-{p1}*floor({p0}/{p1}))"); + } + + private static readonly string[] _vectorAccessors = { "x", "y", "z", "w" }; + + private static readonly HashSet _oneDimensionalTypes = + new HashSet(new[] + { + "System.Single", + "float", + "System.Int32", + "int", + "System.UInt32", + "uint" + }, + StringComparer.InvariantCultureIgnoreCase); + + /// + /// Implements a check for each element of a vector. + /// + /// Name of the type. + /// The check. + /// + private static string AddCheck(string typeName, string check) + { + if (_oneDimensionalTypes.Contains(typeName)) + { + // The check can stay as it is, strip the '`' characters. + return check.Replace("`", string.Empty); + } + + GetVectorTypeInfo(typeName, out string shaderType, out int elementCount); + return + $"{shaderType}({string.Join(",", _vectorAccessors.Take(elementCount).Select(a => check.Replace("`", "." + a)))})"; } } diff --git a/src/ShaderGen/Metal/MetalKnownFunctions.cs b/src/ShaderGen/Metal/MetalKnownFunctions.cs index 9c8f252..66b52f7 100644 --- a/src/ShaderGen/Metal/MetalKnownFunctions.cs +++ b/src/ShaderGen/Metal/MetalKnownFunctions.cs @@ -23,9 +23,9 @@ private static Dictionary GetMappings() {nameof(ShaderBuiltins.Acosh), SimpleNameTranslator()}, {nameof(ShaderBuiltins.Asin), SimpleNameTranslator()}, {nameof(ShaderBuiltins.Asinh), SimpleNameTranslator()}, - {nameof(ShaderBuiltins.Atan), Atan}, // Note atan supports both (x) and (y,x) + {nameof(ShaderBuiltins.Atan), Atan}, {nameof(ShaderBuiltins.Atanh), SimpleNameTranslator()}, - {nameof(ShaderBuiltins.Cbrt), CubeRoot}, // We can calculate the 1/3rd power, which might not give exactly the same result? + {nameof(ShaderBuiltins.Cbrt), CubeRoot}, {nameof(ShaderBuiltins.Ceiling), SimpleNameTranslator("ceil")}, {nameof(ShaderBuiltins.Clamp), Clamp}, {nameof(ShaderBuiltins.ClipToTextureCoordinates), ClipToTextureCoordinates}, @@ -39,6 +39,7 @@ private static Dictionary GetMappings() {nameof(ShaderBuiltins.DispatchThreadID), DispatchThreadID}, {nameof(ShaderBuiltins.Exp), SimpleNameTranslator()}, {nameof(ShaderBuiltins.Floor), SimpleNameTranslator()}, + {nameof(ShaderBuiltins.FMod), FMod }, {nameof(ShaderBuiltins.Frac), SimpleNameTranslator("fract")}, {nameof(ShaderBuiltins.GroupThreadID), GroupThreadID}, {nameof(ShaderBuiltins.InstanceID), InstanceID}, @@ -51,8 +52,7 @@ private static Dictionary GetMappings() {nameof(ShaderBuiltins.Log10), Log10}, {nameof(ShaderBuiltins.Max), SimpleNameTranslator()}, {nameof(ShaderBuiltins.Min), SimpleNameTranslator()}, - // Potential BUG: https://stackoverflow.com/questions/7610631/glsl-mod-vs-hlsl-fmod - {nameof(ShaderBuiltins.Mod), SimpleNameTranslator("fmod")}, + {nameof(ShaderBuiltins.Mod), Mod}, {nameof(ShaderBuiltins.Mul), MatrixMul}, {nameof(ShaderBuiltins.Pow), Pow}, {nameof(ShaderBuiltins.Round), Round}, @@ -673,7 +673,7 @@ private static string Atan(string typeName, string methodName, InvocationParamet return parameters[0].FullTypeName.Contains("Vector") ? $"atan2({InvocationParameterInfo.GetInvocationParameterList(parameters)})" - : $"atan2({parameters[0].Identifier}, (float){parameters[1].Identifier})" ; + : $"atan2({parameters[0].Identifier}, (float){parameters[1].Identifier})"; } private static string Pow(string typeName, string methodName, InvocationParameterInfo[] parameters) @@ -687,5 +687,52 @@ private static string Pow(string typeName, string methodName, InvocationParamete ? $"pow({InvocationParameterInfo.GetInvocationParameterList(parameters)})" : $"pow({parameters[0].Identifier}, (float){parameters[1].Identifier})"; } + + private static string FMod(string typeName, string methodName, InvocationParameterInfo[] parameters) + { + // D3D & Vulkan return Max when max < min, but OpenGL returns Min, so we need + // to correct by returning Max when max < min. + bool isFloat = parameters[1].FullTypeName == "System.Single" || parameters[1].FullTypeName == "float"; + string p0 = $"{parameters[0].Identifier}`"; + string p1 = $"{parameters[1].Identifier}{(isFloat ? string.Empty : "`")}"; + return AddCheck(parameters[0].FullTypeName, + $"({p0}-{p1}*trunc({p0}/{p1}))"); + } + + private static string Mod(string typeName, string methodName, InvocationParameterInfo[] parameters) + { + return $"{parameters[0].Identifier}%{parameters[1].Identifier}"; + } + + private static readonly HashSet _oneDimensionalTypes = + new HashSet(new[] + { + "System.Single", + "float", + "System.Int32", + "int", + "System.UInt32", + "uint" + }, + StringComparer.InvariantCultureIgnoreCase); + + /// + /// Implements a check for each element of a vector. + /// + /// Name of the type. + /// The check. + /// + private static string AddCheck(string typeName, string check) + { + if (_oneDimensionalTypes.Contains(typeName)) + { + // The check can stay as it is, strip the '`' characters. + return check.Replace("`", string.Empty); + } + + GetVectorTypeInfo(typeName, out string shaderType, out int elementCount); + return + $"{shaderType}({string.Join(",", Enumerable.Range(0, elementCount).Select(a => check.Replace("`", $"[{a}]")))})"; + } } } diff --git a/src/ShaderGen/ShaderGenerator.cs b/src/ShaderGen/ShaderGenerator.cs index 42ee649..31edfe2 100644 --- a/src/ShaderGen/ShaderGenerator.cs +++ b/src/ShaderGen/ShaderGenerator.cs @@ -8,8 +8,8 @@ namespace ShaderGen public partial class ShaderGenerator { private readonly Compilation _compilation; - private readonly IReadOnlyCollection _shaderSets = new List(); - private readonly IReadOnlyCollection _languages; + private readonly IReadOnlyList _shaderSets = new List(); + private readonly IReadOnlyList _languages; private readonly IShaderSetProcessor[] _processors; public ShaderGenerator( diff --git a/src/ShaderGen/ShaderSyntaxWalker.cs b/src/ShaderGen/ShaderSyntaxWalker.cs index abc854e..fb894e6 100644 --- a/src/ShaderGen/ShaderSyntaxWalker.cs +++ b/src/ShaderGen/ShaderSyntaxWalker.cs @@ -85,7 +85,7 @@ public static bool TryGetStructDefinition(SemanticModel model, StructDeclaration if (typeInfo.Type.Kind == SymbolKind.ArrayType) { ITypeSymbol elementType = ((IArrayTypeSymbol)typeInfo.Type).ElementType; - AlignmentInfo elementSizeAndAlignment = TypeSizeCache.Get(model, elementType); + AlignmentInfo elementSizeAndAlignment = TypeSizeCache.Get(elementType); fieldSizeAndAlignment = new AlignmentInfo( elementSizeAndAlignment.CSharpSize * arrayElementCount, elementSizeAndAlignment.ShaderSize * arrayElementCount, @@ -94,7 +94,7 @@ public static bool TryGetStructDefinition(SemanticModel model, StructDeclaration } else { - fieldSizeAndAlignment = TypeSizeCache.Get(model, typeInfo.Type); + fieldSizeAndAlignment = TypeSizeCache.Get(typeInfo.Type); } structCSharpSize += structCSharpSize % fieldSizeAndAlignment.CSharpAlignment; diff --git a/src/ShaderGen/TypeSizeCache.cs b/src/ShaderGen/TypeSizeCache.cs index 793374e..3b0e7ad 100644 --- a/src/ShaderGen/TypeSizeCache.cs +++ b/src/ShaderGen/TypeSizeCache.cs @@ -39,7 +39,7 @@ public static class TypeSizeCache private static readonly ConcurrentDictionary s_cachedSizes = new ConcurrentDictionary(); - public static AlignmentInfo Get(SemanticModel model, ITypeSymbol symbol) + public static AlignmentInfo Get(ITypeSymbol symbol) { Debug.Assert(symbol.Kind != SymbolKind.ArrayType); return s_cachedSizes.TryGetValue(symbol, out AlignmentInfo alignmentInfo)