From 5d8e032d7f208d2137b4ef27002035a9c8dbe9be Mon Sep 17 00:00:00 2001 From: Libby Date: Wed, 4 Nov 2026 17:22:13 +0000 Subject: [PATCH] cultmath: glsl-parity header names the recording image and lavapipe/Mesa; asin/acos/atan2 oracle tolerance capped at 1 ulp The glsl-parity.json header gains recorded_on (generator writes it; goldens untouched). CyclesOracleTests allows cycles_arcsine, cycles_arccosine and cycles_arctan2 (glsl-parity families 95-97) 1 ulp from Blender, with the measured cases named; a test pins the cap at 1 ulp, the three names, and that a 2 ulp miss still fails. Co-Authored-By: Claude Sonnet 5.5 --- .../tests/CultMath.Tests/CyclesOracleTests.cs | 38 +++++++++++++++++-- .../tests/CultMath.Tests/GlslMirrorTests.cs | 1 + .../CultMath.Tests/fixtures/glsl-parity.json | 1 + 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/cultmath/tests/CultMath.Tests/CyclesOracleTests.cs b/packages/cultmath/tests/CultMath.Tests/CyclesOracleTests.cs index e4afbc74..86ccbfc8 100644 --- a/packages/cultmath/tests/CultMath.Tests/CyclesOracleTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/CyclesOracleTests.cs @@ -19,11 +19,23 @@ namespace CultMath.Tests; /// public sealed class CyclesOracleTests { - // The ulp distance a function is allowed from the reference. Empty: the fixtures are the SSE4.2 kernel's results - // and every function equals them, power and logarithm included (glibc's powf and logf, 0 ulp from MathF on Linux). + // The ulp distance a function is allowed from the reference; a function not named is held to 0. The fixtures are + // the SSE4.2 kernel's results and almost every function equals them, power and logarithm included (glibc's powf and + // logf, 0 ulp from MathF on Linux). asin, acos and atan2 are the exception: the fixtures' glibc and the libm these + // tests run on round them one ulp apart (glsl-parity.json families 95-97 are the same three, platform families + // there too), so each is allowed 1 ulp, never more (TolerancesAreCappedAtOneUlpAndStayOnTheDriftingFunctions). // An entry needs a measured case on the reference named in it (a failing case's bits and ulp); other builds' // tolerances belong to the class table in tools/cycles-oracle/README.md, not here. - private static readonly Dictionary Tolerances = new(); + private const int MaxToleratedUlp = 1; + + private static readonly Dictionary Tolerances = new() + { + ["cycles_arcsine"] = (1, "case 3f4dd9af -> 3f6f256c, C# 3f6f256d"), + ["cycles_arccosine"] = (1, "case 3f027192 -> 3f84a03c, C# 3f84a03b"), + ["cycles_arctan2"] = (1, "case 3f000000 bdcccccd -> 3fe2541c, C# 3fe2541d"), + }; + + private static int Allowed(string function) => Tolerances.TryGetValue(function, out var tolerance) ? tolerance.Ulp : 0; private const string CSharp = "C#"; private const string Mirror = "HLSL mirror"; @@ -120,7 +132,7 @@ public sealed class CyclesOracleTests var name = (string)entry!["function"]!; var method = Method(name); var clamped = (string?)entry["then"] is not null; - var allowed = Tolerances.TryGetValue(name, out var tolerance) ? tolerance.Ulp : 0; + var allowed = Allowed(name); var worst = 0L; string? worstCase = null; var cases = entry["cases"]!.AsArray(); @@ -158,6 +170,24 @@ public sealed class CyclesOracleTests Assert.True(failures.Count == 0, string.Join(Environment.NewLine, failures)); } + [Fact] + public void TolerancesAreCappedAtOneUlpAndStayOnTheDriftingFunctions() + { + Assert.Equal(new[] { "cycles_arccosine", "cycles_arcsine", "cycles_arctan2" }, Tolerances.Keys.Order().ToArray()); + Assert.All(Tolerances.Values, t => Assert.InRange(t.Ulp, 1, MaxToleratedUlp)); + Assert.All(Tolerances.Values, t => Assert.NotEmpty(t.Measured)); + + // A result two ulps from the reference is past every allowance, and one ulp is within the drifting functions' only. + foreach (var function in Tolerances.Keys.Append("cycles_power")) + { + var want = 0.7f; + var twoAway = MathF.BitIncrement(MathF.BitIncrement(want)); + Assert.Equal(2, Distance(want, twoAway)); + Assert.True(Distance(want, twoAway) > Allowed(function), $"{function}: a 2 ulp miss would pass"); + Assert.Equal(function != "cycles_power", Distance(want, MathF.BitIncrement(want)) <= Allowed(function)); + } + } + private static JsonNode RampEntry(string interpolation) => Load("ramp")["functions"]!.AsArray().Single(e => e!["node"]!.ToString() == "ShaderNodeValToRGB " + interpolation)!; diff --git a/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs b/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs index 10e08380..874ace67 100644 --- a/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs @@ -975,6 +975,7 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z > var json = new StringBuilder(); json.Append("{\n"); json.Append(" \"generator\": \"GlslMirrorTests.GoldenFixtureMatchesCSharp (packages/cultmath/tests/CultMath.Tests); regenerate with CULTMATH_WRITE_GLSL=1\",\n"); + json.Append(" \"recorded_on\": \"platform families: Yggdrasil's mcr.microsoft.com/dotnet/sdk:10.0 image (Debian GNU/Linux 13 trixie, .NET 10.0.14); lavapipe device bounds: fixtures/device-bounds-lavapipe.txt (llvmpipe LLVM 20.1.2, mesa-vulkan-drivers 25.2.8-0ubuntu0.24.04.4, ubuntu:24.04 probe image)\",\n"); json.Append($" \"seed\": {Seed},\n"); json.Append($" \"points\": {Points},\n"); json.Append(" \"encoding\": \"each case is 'arguments -> result', every scalar a float32 or 32-bit integer bit pattern in hex, vectors and structs flattened in field order\",\n"); diff --git a/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json b/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json index 244d068c..a3e68668 100644 --- a/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json +++ b/packages/cultmath/tests/CultMath.Tests/fixtures/glsl-parity.json @@ -1,5 +1,6 @@ { "generator": "GlslMirrorTests.GoldenFixtureMatchesCSharp (packages/cultmath/tests/CultMath.Tests); regenerate with CULTMATH_WRITE_GLSL=1", + "recorded_on": "platform families: Yggdrasil's mcr.microsoft.com/dotnet/sdk:10.0 image (Debian GNU/Linux 13 trixie, .NET 10.0.14); lavapipe device bounds: fixtures/device-bounds-lavapipe.txt (llvmpipe LLVM 20.1.2, mesa-vulkan-drivers 25.2.8-0ubuntu0.24.04.4, ubuntu:24.04 probe image)", "seed": 20261002, "points": 256, "encoding": "each case is 'arguments -> result', every scalar a float32 or 32-bit integer bit pattern in hex, vectors and structs flattened in field order",