diff --git a/packages/cultmath/docs/design.md b/packages/cultmath/docs/design.md
index 32cc03f0..081265d0 100644
--- a/packages/cultmath/docs/design.md
+++ b/packages/cultmath/docs/design.md
@@ -743,7 +743,8 @@ add `2^-126`, the most an underflow to a subnormal can lose (`TinyOperandsEnclos
`af_from_iv` halves its endpoints before it sums and differences them, and `af_range`
scales its terms before it sums them, so endpoints near the float maximum do not overflow
in the middle of the op; a form that overflows anyway gives `af_range` infinite endpoints,
-never NaN (`FromIntervalAndRangeHoldAtTheExtremes`). `AbsorptionsArePinned` and
+never NaN (`FromIntervalAndRangeHoldAtTheExtremes`). `af_range` finds a NaN endpoint by its bit pattern, because
+`lo == lo` is a compare dxc marks `fast` and deletes under its default flags. `AbsorptionsArePinned` and
`SnoiseFormCarriesItsDocumentedWidth` pin each absorption and `af_snoise`'s float32 allowance both
ways: the enclosure tests alone cannot see them on a CPU, where float32 is exact.
diff --git a/packages/cultmath/shaders/CultMath.Affine.hlsl b/packages/cultmath/shaders/CultMath.Affine.hlsl
index 2c91a00d..f165148b 100644
--- a/packages/cultmath/shaders/CultMath.Affine.hlsl
+++ b/packages/cultmath/shaders/CultMath.Affine.hlsl
@@ -22,14 +22,16 @@ float3 cultmath_af_from_iv(float2 interval)
return float3(x0, 0.0, e + (abs(x0) + e) * 9.5367431640625e-7 + 1.17549435e-38);
}
-// An endpoint that would be NaN is the infinity on its side.
+// An endpoint that would be NaN is the infinity on its side. NaN is found by its bit pattern: dxc marks
+// compares fast, so a test that needs NaN to make `lo == lo` false is deleted under its default flags.
float2 cultmath_af_range(float3 x)
{
float r = abs(x.y) + x.z;
float w = abs(x.x) * 9.5367431640625e-7 + r * 9.5367431640625e-7;
float lo = x.x - r - w;
float hi = x.x + r + w;
- return float2(lo == lo ? lo : asfloat(0xff800000u), hi == hi ? hi : asfloat(0x7f800000u));
+ return float2((asuint(lo) & 0x7fffffffu) > 0x7f800000u ? asfloat(0xff800000u) : lo,
+ (asuint(hi) & 0x7fffffffu) > 0x7f800000u ? asfloat(0x7f800000u) : hi);
}
float3 cultmath_af_add(float3 x, float3 y)
diff --git a/packages/cultmath/shaders/CultMath.glsl b/packages/cultmath/shaders/CultMath.glsl
index 99dbd4b8..d89ff8a3 100644
--- a/packages/cultmath/shaders/CultMath.glsl
+++ b/packages/cultmath/shaders/CultMath.glsl
@@ -665,14 +665,16 @@ vec3 cultmath_af_from_iv(vec2 interval)
return vec3(x0, 0.0, e + (abs(x0) + e) * 9.5367431640625e-7 + 1.17549435e-38);
}
-// An endpoint that would be NaN is the infinity on its side.
+// An endpoint that would be NaN is the infinity on its side. NaN is found by its bit pattern: dxc marks
+// compares fast, so a test that needs NaN to make `lo == lo` false is deleted under its default flags.
vec2 cultmath_af_range(vec3 x)
{
float r = abs(x.y) + x.z;
float w = abs(x.x) * 9.5367431640625e-7 + r * 9.5367431640625e-7;
float lo = x.x - r - w;
float hi = x.x + r + w;
- return vec2(lo == lo ? lo : uintBitsToFloat(0xff800000u), hi == hi ? hi : uintBitsToFloat(0x7f800000u));
+ return vec2((floatBitsToUint(lo) & 0x7fffffffu) > 0x7f800000u ? uintBitsToFloat(0xff800000u) : lo,
+ (floatBitsToUint(hi) & 0x7fffffffu) > 0x7f800000u ? uintBitsToFloat(0x7f800000u) : hi);
}
vec3 cultmath_af_add(vec3 x, vec3 y)
diff --git a/packages/cultmath/src/CultMath/math.Affine.cs b/packages/cultmath/src/CultMath/math.Affine.cs
index 9bfbeb10..3c00e397 100644
--- a/packages/cultmath/src/CultMath/math.Affine.cs
+++ b/packages/cultmath/src/CultMath/math.Affine.cs
@@ -66,7 +66,8 @@ public static partial class math
/// (|x0| + |a| + e) 2^-20, formed term by term so that the sum never passes the float maximum before it
/// is scaled. A form with an infinite component (an overflowed op) gives infinite
/// endpoints, and an endpoint that would be NaN (infinity minus infinity, or a NaN component) is
- /// the infinity on its side, so the result is never NaN and still encloses.
+ /// the infinity on its side, so the result is never NaN and still encloses. NaN is found by its bit
+ /// pattern, not by lo == lo, which a compiler that assumes no NaNs (dxc's default) deletes.
///
public static float2 af_range(float3 x)
{
@@ -74,7 +75,8 @@ public static partial class math
var w = abs(x.x) * 9.5367431640625e-7f + r * 9.5367431640625e-7f;
var lo = x.x - r - w;
var hi = x.x + r + w;
- return new float2(lo == lo ? lo : float.NegativeInfinity, hi == hi ? hi : float.PositiveInfinity);
+ return new float2((asuint(lo) & 0x7fffffffu) > 0x7f800000u ? float.NegativeInfinity : lo,
+ (asuint(hi) & 0x7fffffffu) > 0x7f800000u ? float.PositiveInfinity : hi);
}
///
diff --git a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs
index b1e3d8f8..1decfb96 100644
--- a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs
+++ b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs
@@ -169,6 +169,36 @@ public sealed class HlslSourceCompatibilityTests
}
}
+
+ ///
+ /// The generic comparison stops at 1e7, so an HLSL edit of cultmath_af_range's NaN test that
+ /// misreads the finite values near the float maximum (or an overflowed endpoint) survives it. Forms
+ /// from zero to the float maximum, infinite and NaN components, both signs, in every slot: the mirror
+ /// returns C#'s bits, and neither endpoint is ever NaN.
+ ///
+ [Fact]
+ public void AffineRangeMirrorMatchesCSharpBitForBitAtTheFloatExtremes()
+ {
+ 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 mirror = ShaderFunction(shaderType, "cultmath_af_range", typeof(float3))!;
+
+ var magnitudes = new[] { 0.0f, 1.0f, 1.0e30f, 1.0e38f, float.MaxValue / 2.0f, float.MaxValue, float.PositiveInfinity, float.NaN };
+ var values = magnitudes.Concat(magnitudes.Where(m => !float.IsNaN(m)).Select(m => -m)).ToArray();
+ foreach (var x0 in values)
+ foreach (var a in values)
+ foreach (var e in values)
+ {
+ var form = new float3(x0, a, e);
+ var expected = af_range(form);
+ var actual = Values(mirror.Invoke(shader, new object[] { form })!);
+ Assert.True(BitwiseEqual(Values(expected), actual), $"cultmath_af_range differs from C# at {form}");
+ Assert.False(float.IsNaN(expected.x) || float.IsNaN(expected.y), $"af_range({form}) is {expected}, a NaN endpoint");
+ }
+ }
+
///
/// The dense sweep almost never meets a cell whose box bound is just under the 2.25 cut and whose
/// weight is still nonzero: a mirror cut of 2.245, 2.24 or 2.2 survives it. These points sit next
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 2c91a00d..f165148b 100644
--- a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.Affine.hlsl
+++ b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.Affine.hlsl
@@ -22,14 +22,16 @@ float3 cultmath_af_from_iv(float2 interval)
return float3(x0, 0.0, e + (abs(x0) + e) * 9.5367431640625e-7 + 1.17549435e-38);
}
-// An endpoint that would be NaN is the infinity on its side.
+// An endpoint that would be NaN is the infinity on its side. NaN is found by its bit pattern: dxc marks
+// compares fast, so a test that needs NaN to make `lo == lo` false is deleted under its default flags.
float2 cultmath_af_range(float3 x)
{
float r = abs(x.y) + x.z;
float w = abs(x.x) * 9.5367431640625e-7 + r * 9.5367431640625e-7;
float lo = x.x - r - w;
float hi = x.x + r + w;
- return float2(lo == lo ? lo : asfloat(0xff800000u), hi == hi ? hi : asfloat(0x7f800000u));
+ return float2((asuint(lo) & 0x7fffffffu) > 0x7f800000u ? asfloat(0xff800000u) : lo,
+ (asuint(hi) & 0x7fffffffu) > 0x7f800000u ? asfloat(0x7f800000u) : hi);
}
float3 cultmath_af_add(float3 x, float3 y)