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);
}