From 2940ca094430ca04f22882aff05e5400d3990180 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 14 Sep 2026 14:17:11 +0200 Subject: [PATCH] Compare every HLSL mirror function against C# math, bit for bit Only cultmath_snoise was compared, at four inputs, so a flipped cultmath_step, a changed hash constant, and a snoise2 tie change all passed. The mirror is now discovered by reflection: every cultmath_* overload with a math counterpart (26 functions) runs over 104 deterministic cases - each special value in every argument (0, -0, +-1, ties, 1e-7, +-1e4, 1e7, NaN), special pairs, seeded randoms, and vectors with x == y (snoise's x0.x == x0.y) - and must match exactly. Both sides are float32 C# over the same intrinsics, so no tolerance. A cultmath_* function with no counterpart must be named in MirrorOnly. The four-input snoise theory is gone. Mutations: flipped step args fail (349 mismatches); hash constants changed by more than one float32 ulp fail (43758.55: 467, 127.2: 289, 74.8: 97). 43758.5453 -> .5454 is the same float32 and cannot fail. snoise2 x0.x > x0.y -> >= is invisible: over 1,829,415 exact ties the result is bit-identical, because the off-simplex corner's kernel is <= 0 on the shared edge. Co-Authored-By: Claude Opus 5 --- .../HlslSourceCompatibilityTests.cs | 64 ++++++++++++++++--- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs index 3c5aae94..9c57ac70 100644 --- a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs @@ -31,25 +31,69 @@ public sealed class HlslSourceCompatibilityTests private static MethodInfo? ShaderFunction(Type shader, string name, params Type[] parameters) => shader.GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic, null, parameters, null); - [Theory] - [InlineData(0.0f, 0.0f, 0.0f)] - [InlineData(0.25f, -0.5f, 1.75f)] - [InlineData(12.25f, -4.5f, 3.125f)] - [InlineData(-7.3f, 2.9f, 101.4f)] - public void CompiledShaderNoiseMatchesCSharpMirror(float x, float y, float z) + // Mirror internals with no public C# math counterpart. Any other cultmath_* function must match one. + private static readonly string[] MirrorOnly = { "cultmath_snoise_mod289", "cultmath_snoise_permute" }; + + private static readonly float[] Specials = { 0.0f, -0.0f, 1.0f, -1.0f, 0.5f, -2.75f, 3.0f, 1.0e-7f, 1.0e4f, -1.0e4f, 1.0e7f, float.NaN }; + + /// + /// Every mirror function with a C# math counterpart returns the same bits (NaN equal to NaN, -0 equal + /// to 0) over random inputs, every special value in every argument (ties, zeros, negatives, NaN, + /// large values, integer lattice points), special pairs, and vectors whose first two components tie + /// (x0.x == x0.y in snoise). Equality is exact: both sides are float32 C# over the same intrinsics. + /// + [Fact] + public void EveryMirrorFunctionMatchesCSharpMath() { var (assembly, errors) = CompileShaderMirror(); Assert.True(assembly is not null, string.Join(Environment.NewLine, errors)); var shaderType = assembly!.GetType("CultMathHlsl.HlslShader")!; var shader = Activator.CreateInstance(shaderType); - var snoise3 = (float)ShaderFunction(shaderType, "cultmath_snoise", typeof(float3))!.Invoke(shader, new object[] { float3(x, y, z) })!; - var snoise2 = (float)ShaderFunction(shaderType, "cultmath_snoise", typeof(float2))!.Invoke(shader, new object[] { float2(x, y) })!; + var random = new System.Random(0x5EED); + float Next() => random.NextSingle() * 200.0f - 100.0f; + var cases = new List>(); + foreach (var s in Specials) cases.Add(_ => s); + for (var k = 0; k < Specials.Length; k++) { var o = k; cases.Add(i => Specials[(i + o + 1) % Specials.Length]); } + for (var k = 0; k < 64; k++) { var values = Enumerable.Range(0, 32).Select(_ => Next()).ToArray(); cases.Add(i => values[i]); } + for (var k = 0; k < 16; k++) { var values = Enumerable.Range(0, 32).Select(_ => Next()).ToArray(); cases.Add(i => values[i & ~1]); } - Assert.Equal(snoise(float3(x, y, z)), snoise3, precision: 6); - Assert.Equal(snoise(float2(x, y)), snoise2, precision: 6); + var mismatches = new List(); + var compared = new HashSet(); + foreach (var mirror in shaderType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic).Where(m => m.Name.StartsWith("cultmath_"))) + { + var types = mirror.GetParameters().Select(p => p.ParameterType).ToArray(); + var counterpart = typeof(math).GetMethod(mirror.Name["cultmath_".Length..], BindingFlags.Public | BindingFlags.Static, null, types, null); + if (counterpart is null) + { + Assert.Contains(mirror.Name, MirrorOnly); + continue; + } + + compared.Add(mirror.Name); + foreach (var input in cases) + { + var slot = 0; + var args = types.Select(t => t == typeof(float) ? (object)input(slot++) + : Activator.CreateInstance(t, Enumerable.Range(0, Components(t)).Select(_ => (object)input(slot++)).ToArray())!).ToArray(); + var expected = Floats(counterpart.Invoke(null, args)!); + var actual = Floats(mirror.Invoke(shader, args)!); + if (!expected.SequenceEqual(actual)) + { + mismatches.Add($"{mirror.Name}({string.Join(", ", args.Select(a => string.Join(" ", Floats(a))))}): C# {string.Join(" ", expected)}, mirror {string.Join(" ", actual)}"); + } + } + } + + Assert.True(mismatches.Count == 0, $"{mismatches.Count} mismatches:{Environment.NewLine}{string.Join(Environment.NewLine, mismatches.Take(12))}"); + Assert.Equal(26, compared.Count); } + private static int Components(Type t) => t.GetFields().Count(f => f.FieldType == typeof(float) && !f.IsStatic); + + private static float[] Floats(object value) => value is float f ? new[] { f } + : value.GetType().GetFields().Where(field => field.FieldType == typeof(float) && !field.IsStatic).Select(field => (float)field.GetValue(value)!).ToArray(); + /// The documented HLSL-to-C# transformations, and nothing else. internal static string TransformHlslToCSharp(string hlsl) {