From 08e213dd7c5d7eb6ccbfec4d6d064d1178aaf149 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 14 Sep 2026 14:32:43 +0200 Subject: [PATCH] Compare mirror results by bit pattern and say what that proves SequenceEqual on floats treats -0 as 0. The mirror test now compares SingleToInt32Bits, with any NaN equal to any NaN. Its summary and design.md now say the test proves CultMath.hlsl's text computes the same float32 results as C# math on the CPU, not that a GPU agrees: driver sin precision alone makes cultmath_hash and value noise differ on hardware. Co-Authored-By: Claude Opus 5 --- packages/cultmath/docs/design.md | 5 +++++ .../CultMath.Tests/HlslSourceCompatibilityTests.cs | 13 ++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/cultmath/docs/design.md b/packages/cultmath/docs/design.md index 9153b63d..e716bb6f 100644 --- a/packages/cultmath/docs/design.md +++ b/packages/cultmath/docs/design.md @@ -85,6 +85,11 @@ The HLSL side is checked too: `tools/get-dxc.ps1` fetches Microsoft's DirectXShaderCompiler, and `dxc -T lib_6_3 -HV 2021 shaders/CultMath.hlsl` must compile cleanly. +The mirror test compares bit patterns (any NaN equals any NaN; -0 differs from +0). It proves the text of `CultMath.hlsl` computes the same float32 results as +C# `math` on the CPU, not that a GPU agrees: driver `sin` precision alone makes +`cultmath_hash` and value noise differ bit for bit on hardware. + Where HLSL is silent, CultMath keeps its own decisions and does not defer to Unity.Mathematics: `normalize` is safe (divides by `max(length, 1e-20)`), `hash` returns float, and `Random` is CultMath's own xorshift32. Engine-shaped diff --git a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs index 9c57ac70..d0ce8bbd 100644 --- a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs @@ -37,10 +37,12 @@ public sealed class HlslSourceCompatibilityTests 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. + /// Every mirror function with a C# math counterpart returns the same bits (any NaN equals any NaN; + /// -0 differs from 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). This proves the text of CultMath.hlsl computes the same + /// float32 results as C# math on the CPU. It does not prove GPU agreement: driver sin precision alone + /// makes cultmath_hash and value noise differ bit for bit on hardware. /// [Fact] public void EveryMirrorFunctionMatchesCSharpMath() @@ -78,7 +80,8 @@ public sealed class HlslSourceCompatibilityTests : 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)) + if (expected.Length != actual.Length || expected.Zip(actual).Any(p => !(float.IsNaN(p.First) && float.IsNaN(p.Second)) + && BitConverter.SingleToInt32Bits(p.First) != BitConverter.SingleToInt32Bits(p.Second))) { mismatches.Add($"{mirror.Name}({string.Join(", ", args.Select(a => string.Join(" ", Floats(a))))}): C# {string.Join(" ", expected)}, mirror {string.Join(" ", actual)}"); }