From 58a2d50a76e4a54acfb5b73c8eae79b724a14a42 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 16 Oct 2026 06:34:21 +0200 Subject: [PATCH] cultmath: one owner of the snoise read's absolute terms (SNOISE_READ_VALUE, SNOISE_READ_GRADIENT) span_slack and af_snoise each spelled 2^-14 (value) and 2^-12 (gradient) inline, in C# and HLSL. Both now read the constants beside SNOISE_HESSIAN (math.Affine.cs, CultMath.Affine.hlsl, Unity copies), so a device-judged widening moves every reader. Bits unchanged. The test oracles in AffineTests and GlslMirrorTests read the constants; a new test pins af_snoise's allowance to span_slack's terms. span_slack's comment no longer claims it is the only allowance. Does not build green yet: CultMath.glsl is not regenerated in this commit. Co-Authored-By: Claude Sonnet 5.5 --- .../cultmath/shaders/CultMath.Affine.hlsl | 8 +++++- packages/cultmath/shaders/CultMath.hlsl | 4 +-- packages/cultmath/src/CultMath/math.Affine.cs | 9 +++++- packages/cultmath/src/CultMath/math.Span.cs | 12 ++++---- .../tests/CultMath.Tests/AffineTests.cs | 28 ++++++++++++++++++- .../tests/CultMath.Tests/GlslMirrorTests.cs | 2 +- .../Shaders/CultMath.Affine.hlsl | 8 +++++- .../Shaders/CultMath.hlsl | 4 +-- 8 files changed, 61 insertions(+), 14 deletions(-) diff --git a/packages/cultmath/shaders/CultMath.Affine.hlsl b/packages/cultmath/shaders/CultMath.Affine.hlsl index 21360d07..eee7a2de 100644 --- a/packages/cultmath/shaders/CultMath.Affine.hlsl +++ b/packages/cultmath/shaders/CultMath.Affine.hlsl @@ -12,6 +12,12 @@ // math.Affine.cs (SNOISE_HESSIAN). static const float CULTMATH_SNOISE_HESSIAN = 55.642143; +// The absolute rounding of one float32 read of cultmath_snoise_grad, value and gradient; the owner is +// math.Affine.cs (SNOISE_READ_VALUE, SNOISE_READ_GRADIENT), read here by cultmath_af_snoise and in +// CultMath.hlsl by cultmath_span_slack. +static const float CULTMATH_SNOISE_READ_VALUE = 6.103515625e-5; +static const float CULTMATH_SNOISE_READ_GRADIENT = 2.44140625e-4; + // An upper bound on the operator norm of the third-derivative tensor of cultmath_snoise(float3), the // next order after CULTMATH_SNOISE_HESSIAN; provenance in math.Span.cs (SNOISE_THIRD). static const float CULTMATH_SNOISE_THIRD = 535.8009; @@ -87,7 +93,7 @@ float3 cultmath_af_snoise(float3 centre, float3 axis, float radius) float lipschitz = CULTMATH_SNOISE_LIPSCHITZ * reach; float a = dot(gradient, axis); float remainder = length(gradient) * radius + 0.5 * CULTMATH_SNOISE_HESSIAN * reach * reach; - float allowance = 6.103515625e-5 + 2.44140625e-4 * reach + float allowance = CULTMATH_SNOISE_READ_VALUE + CULTMATH_SNOISE_READ_GRADIENT * reach + CULTMATH_SNOISE_LIPSCHITZ * 9.5367431640625e-7 * (abs(centre.x) + abs(centre.y) + abs(centre.z) + reach); bool centred = abs(a) + remainder < lipschitz; float slope = centred ? a : 0.0; diff --git a/packages/cultmath/shaders/CultMath.hlsl b/packages/cultmath/shaders/CultMath.hlsl index 5c876b04..d871bebd 100644 --- a/packages/cultmath/shaders/CultMath.hlsl +++ b/packages/cultmath/shaders/CultMath.hlsl @@ -645,8 +645,8 @@ float cultmath_span_rounding(float scale, float3 mq, SpanStage stage, float3 oct float3 cultmath_span_slack(float rounding) { - return float3(6.103515625e-5 + CULTMATH_SNOISE_LIPSCHITZ * rounding, - 2.44140625e-4 + CULTMATH_SNOISE_HESSIAN * rounding, + return float3(CULTMATH_SNOISE_READ_VALUE + CULTMATH_SNOISE_LIPSCHITZ * rounding, + CULTMATH_SNOISE_READ_GRADIENT + CULTMATH_SNOISE_HESSIAN * rounding, 9.765625e-4 + CULTMATH_SNOISE_THIRD * rounding); } diff --git a/packages/cultmath/src/CultMath/math.Affine.cs b/packages/cultmath/src/CultMath/math.Affine.cs index acb73251..e7b8eb4b 100644 --- a/packages/cultmath/src/CultMath/math.Affine.cs +++ b/packages/cultmath/src/CultMath/math.Affine.cs @@ -35,6 +35,13 @@ public static partial class math // kernel must re-run MeasureHessian and re-pin it. SNOISE_THIRD (math.Span.cs) is the next-order pin. public const float SNOISE_HESSIAN = 55.642143f; + // The absolute rounding of one float32 read of the snoise kernel, apart from the coordinate's own + // rounding (af_snoise's comment derives both): 2^-14 on the value and 2^-12 on the gradient. The single + // owner of the two, read by af_snoise and by span_slack (math.Span.cs); the shader twins are + // CULTMATH_SNOISE_READ_VALUE and CULTMATH_SNOISE_READ_GRADIENT. + internal const float SNOISE_READ_VALUE = 6.103515625e-5f; + internal const float SNOISE_READ_GRADIENT = 2.44140625e-4f; + /// The point form (x, 0, 0): the value x at every eps. Exact. public static float3 af_point(float value) => new(value, 0.0f, 0.0f); @@ -165,7 +172,7 @@ public static partial class math var lipschitz = SNOISE_LIPSCHITZ * reach; var a = dot(gradient, axis); var remainder = length(gradient) * radius + 0.5f * SNOISE_HESSIAN * reach * reach; - var allowance = 6.103515625e-5f + 2.44140625e-4f * reach + var allowance = SNOISE_READ_VALUE + SNOISE_READ_GRADIENT * reach + SNOISE_LIPSCHITZ * 9.5367431640625e-7f * (abs(centre.x) + abs(centre.y) + abs(centre.z) + reach); var centred = abs(a) + remainder < lipschitz; var slope = centred ? a : 0.0f; diff --git a/packages/cultmath/src/CultMath/math.Span.cs b/packages/cultmath/src/CultMath/math.Span.cs index c49c43f3..410b1aa3 100644 --- a/packages/cultmath/src/CultMath/math.Span.cs +++ b/packages/cultmath/src/CultMath/math.Span.cs @@ -159,10 +159,12 @@ public static partial class math // The allowances (e_n, e_g, e_h) of the value, the gradient's length and the Hessian's Frobenius norm at // a rounding of 2^-20 S_o: the absolute term of each plus its derivative's pin times the rounding. The - // single owner of the three; nothing else computes an allowance. + // single owner of the three allowances of a span; af_snoise (math.Affine.cs) forms its own allowance + // from the same SNOISE_READ_VALUE and SNOISE_READ_GRADIENT, the owners of the first two absolute terms, + // so widening either moves every reader of the kernel at once. internal static float3 span_slack(float rounding) => new( - 6.103515625e-5f + SNOISE_LIPSCHITZ * rounding, - 2.44140625e-4f + SNOISE_HESSIAN * rounding, + SNOISE_READ_VALUE + SNOISE_LIPSCHITZ * rounding, + SNOISE_READ_GRADIENT + SNOISE_HESSIAN * rounding, 9.765625e-4f + SNOISE_THIRD * rounding); // The span is SpanPath: q(t), |t| <= halfLength, through midpoint with velocity at t = 0, |q'| <= speed @@ -199,8 +201,8 @@ public static partial class math // the span's reach sqrt(3) nu halfLength (the coordinate of a path point); span_rounding is its owner. The // allowances are the value's e_n = 2^-14 + L 2^-20 S_o, the gradient length's e_g = 2^-12 + H 2^-20 S_o // (H is the gradient's own derivative's pin) and the Hessian's Frobenius norm's e_h = 2^-10 + T 2^-20 S_o - // (T is the Hessian's derivative's pin): af_snoise's absolute terms for the first two, and 2^-10 for the - // Hessian's; span_slack is their owner. They are observed where they are decided: the coordinate-band + // (T is the Hessian's derivative's pin): SNOISE_READ_VALUE and SNOISE_READ_GRADIENT (math.Affine.cs) are the + // absolute terms of the first two, 2^-10 the Hessian's; span_slack is the owner of the three. They are observed where they are decided: the coordinate-band // snoise_grad and snoise_hessian families of glsl-parity.json and SpanAllowanceCoversReadsAlongTheSpan // check every read against them. Those observations are samples, not bounds: a new seed moves them. What // they say is that the allowances enclose the error with a margin of about 15. diff --git a/packages/cultmath/tests/CultMath.Tests/AffineTests.cs b/packages/cultmath/tests/CultMath.Tests/AffineTests.cs index 51bcacc5..e73f34f5 100644 --- a/packages/cultmath/tests/CultMath.Tests/AffineTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/AffineTests.cs @@ -295,9 +295,35 @@ public sealed class AffineTests // af_snoise's float32 allowance, as its doc comment states it, for the width comparison below. private static double Allowance(float3 centre, double reach) => - Math.ScaleB(1.0, -14) + Math.ScaleB(reach, -12) + SNOISE_READ_VALUE + SNOISE_READ_GRADIENT * reach + SNOISE_LIPSCHITZ * Math.ScaleB(Math.Abs(centre.x) + Math.Abs(centre.y) + Math.Abs(centre.z) + reach, -20); + /// + /// af_snoise reads the same absolute rounding terms of the kernel as span_slack: at the origin with no + /// axis, the form's error is span_slack's value allowance at zero rounding plus the Lipschitz form's + /// L R, and each unit of reach adds span_slack's gradient term at zero rounding plus L 2^-20 (the + /// coordinate's own rounding). Widening the kernel's absolute read in one reader and not the other, + /// the device judge's possible outcome, fails here. The expected bits are formed from span_slack, not + /// from a literal. + /// + [Fact] + public void SnoiseFormAndSpanSlackShareTheKernelsAbsoluteTerms() + { + var zero = new float3(0.0f, 0.0f, 0.0f); + var slack = span_slack(0.0f); + var value = snoise_grad(zero).w; + foreach (var reach in new[] { 0.0f, 1.0f, 3.0f }) + { + var lipschitz = SNOISE_LIPSCHITZ * reach; + var allowance = slack.x + slack.y * reach + SNOISE_LIPSCHITZ * 9.5367431640625e-7f * reach; + var e = lipschitz + allowance; + var expected = e + (abs(value) + 0.0f + e) * 9.5367431640625e-7f; + var form = af_snoise(zero, zero, reach); + Assert.True(BitConverter.SingleToInt32Bits(form.z) == BitConverter.SingleToInt32Bits(expected), + $"af_snoise(0, 0, {reach:R}).z is {form.z:R}; the allowances span_slack reads give {expected:R}"); + } + } + private static float3 UnitVector(System.Random random) { float3 v; diff --git a/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs b/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs index 982e49c3..4cf4179a 100644 --- a/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs @@ -336,7 +336,7 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z > // The float32 allowance of one octave of af_snoise: 2^-14 + 2^-12 R + L 2^-20 (|c|_1 + R). private static double SnoiseAllowance(double centre1, double reach) => - Math.ScaleB(1.0, -14) + Math.ScaleB(reach, -12) + math.SNOISE_LIPSCHITZ * Math.ScaleB(centre1 + reach, -20); + math.SNOISE_READ_VALUE + math.SNOISE_READ_GRADIENT * reach + math.SNOISE_LIPSCHITZ * Math.ScaleB(centre1 + reach, -20); // The Lipschitz form's width bound for one octave of the sampled-form check. private static double SnoiseWidthBound(double centre1, double reach) => math.SNOISE_LIPSCHITZ * reach + SnoiseAllowance(centre1, reach); diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.Affine.hlsl b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.Affine.hlsl index 21360d07..eee7a2de 100644 --- a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.Affine.hlsl +++ b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.Affine.hlsl @@ -12,6 +12,12 @@ // math.Affine.cs (SNOISE_HESSIAN). static const float CULTMATH_SNOISE_HESSIAN = 55.642143; +// The absolute rounding of one float32 read of cultmath_snoise_grad, value and gradient; the owner is +// math.Affine.cs (SNOISE_READ_VALUE, SNOISE_READ_GRADIENT), read here by cultmath_af_snoise and in +// CultMath.hlsl by cultmath_span_slack. +static const float CULTMATH_SNOISE_READ_VALUE = 6.103515625e-5; +static const float CULTMATH_SNOISE_READ_GRADIENT = 2.44140625e-4; + // An upper bound on the operator norm of the third-derivative tensor of cultmath_snoise(float3), the // next order after CULTMATH_SNOISE_HESSIAN; provenance in math.Span.cs (SNOISE_THIRD). static const float CULTMATH_SNOISE_THIRD = 535.8009; @@ -87,7 +93,7 @@ float3 cultmath_af_snoise(float3 centre, float3 axis, float radius) float lipschitz = CULTMATH_SNOISE_LIPSCHITZ * reach; float a = dot(gradient, axis); float remainder = length(gradient) * radius + 0.5 * CULTMATH_SNOISE_HESSIAN * reach * reach; - float allowance = 6.103515625e-5 + 2.44140625e-4 * reach + float allowance = CULTMATH_SNOISE_READ_VALUE + CULTMATH_SNOISE_READ_GRADIENT * reach + CULTMATH_SNOISE_LIPSCHITZ * 9.5367431640625e-7 * (abs(centre.x) + abs(centre.y) + abs(centre.z) + reach); bool centred = abs(a) + remainder < lipschitz; float slope = centred ? a : 0.0; diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl index 5c876b04..d871bebd 100644 --- a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl +++ b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl @@ -645,8 +645,8 @@ float cultmath_span_rounding(float scale, float3 mq, SpanStage stage, float3 oct float3 cultmath_span_slack(float rounding) { - return float3(6.103515625e-5 + CULTMATH_SNOISE_LIPSCHITZ * rounding, - 2.44140625e-4 + CULTMATH_SNOISE_HESSIAN * rounding, + return float3(CULTMATH_SNOISE_READ_VALUE + CULTMATH_SNOISE_LIPSCHITZ * rounding, + CULTMATH_SNOISE_READ_GRADIENT + CULTMATH_SNOISE_HESSIAN * rounding, 9.765625e-4 + CULTMATH_SNOISE_THIRD * rounding); }