From b042bf19be10de134374f6b17339707cabf9f22a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 20:39:27 +0200 Subject: [PATCH] cultmath: integral_clamp_linear is finite for infinite clamp bounds (integral-infinite-bound-nan) Each held stretch contributes only when its length is positive, so an infinite bound times a zero length never forms: relu (hi = +inf) and its mirrors give the finite integral instead of NaN, in C#, HLSL (Unity copy byte-identical) and the generated GLSL. Finite cases are unchanged. Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/shaders/CultMath.glsl | 4 ++- packages/cultmath/shaders/CultMath.hlsl | 4 ++- packages/cultmath/src/CultMath/math.Span.cs | 8 ++++-- .../CultMath.Tests/SpanPrimitiveTests.cs | 26 +++++++++++++++++++ .../Shaders/CultMath.hlsl | 4 ++- 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/cultmath/shaders/CultMath.glsl b/packages/cultmath/shaders/CultMath.glsl index 3f090c7c..3f6a1dd5 100644 --- a/packages/cultmath/shaders/CultMath.glsl +++ b/packages/cultmath/shaders/CultMath.glsl @@ -315,7 +315,9 @@ float cultmath_integral_clamp_linear(float a, float b, float lo, float hi, float float leave = clamp(max(atLo, atHi), start, stop); float before = b > 0.0 ? lo : hi; float after = b > 0.0 ? hi : lo; - float total = before * (enter - start) + (a + b * (0.5 * (enter + leave))) * (leave - enter) + after * (stop - leave); + float held = enter > start ? before * (enter - start) : 0.0; + float kept = stop > leave ? after * (stop - leave) : 0.0; + float total = held + (a + b * (0.5 * (enter + leave))) * (leave - enter) + kept; return direction * total; } diff --git a/packages/cultmath/shaders/CultMath.hlsl b/packages/cultmath/shaders/CultMath.hlsl index 8379b5f1..988e6a72 100644 --- a/packages/cultmath/shaders/CultMath.hlsl +++ b/packages/cultmath/shaders/CultMath.hlsl @@ -315,7 +315,9 @@ float cultmath_integral_clamp_linear(float a, float b, float lo, float hi, float float leave = clamp(max(atLo, atHi), start, stop); float before = b > 0.0 ? lo : hi; float after = b > 0.0 ? hi : lo; - float total = before * (enter - start) + (a + b * (0.5 * (enter + leave))) * (leave - enter) + after * (stop - leave); + float held = enter > start ? before * (enter - start) : 0.0; + float kept = stop > leave ? after * (stop - leave) : 0.0; + float total = held + (a + b * (0.5 * (enter + leave))) * (leave - enter) + kept; return direction * total; } diff --git a/packages/cultmath/src/CultMath/math.Span.cs b/packages/cultmath/src/CultMath/math.Span.cs index 6be788d0..5b0fbbeb 100644 --- a/packages/cultmath/src/CultMath/math.Span.cs +++ b/packages/cultmath/src/CultMath/math.Span.cs @@ -104,7 +104,9 @@ public static partial class math // line's value at the stretch's midpoint), and the stretch held at the far bound. b = 0 is the // constant clamp(a, lo, hi); t1 < t0 is the negative of the integral over [t1, t0]; lo above hi // clamps to hi everywhere, as clamp does. A crossing that overflows (b tiny) lies past the span and - // cuts nothing. + // cuts nothing. A held stretch contributes only when its length is positive, so an infinite bound + // (relu has hi = +inf) times a zero length never forms: the integral of a ramp clamped on one side + // only is finite. public static float integral_clamp_linear(float a, float b, float lo, float hi, float t0, float t1) { if (b == 0.0f) @@ -120,7 +122,9 @@ public static partial class math var leave = clamp(max(atLo, atHi), start, stop); var before = b > 0.0f ? lo : hi; var after = b > 0.0f ? hi : lo; - var total = before * (enter - start) + (a + b * (0.5f * (enter + leave))) * (leave - enter) + after * (stop - leave); + var held = enter > start ? before * (enter - start) : 0.0f; + var kept = stop > leave ? after * (stop - leave) : 0.0f; + var total = held + (a + b * (0.5f * (enter + leave))) * (leave - enter) + kept; return direction * total; } } diff --git a/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs b/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs index 44814195..738b268f 100644 --- a/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/SpanPrimitiveTests.cs @@ -319,6 +319,32 @@ public sealed class SpanPrimitiveTests Assert.True(zeroSlopes > 10_000 && reversed > 40_000 && both > 2_000 && neither > 2_000, $"the sample meets a branch too rarely: {zeroSlopes} flat, {reversed} reversed, {both} both, {neither} neither"); } + /// + /// An infinite bound never reached by the line contributes nothing, not infinity times zero: relu + /// (hi = +inf) and its mirror (lo = -inf) give the finite integral, whether the line crosses the finite + /// bound inside the span or not. + /// + [Fact] + public void IntegralClampLinearIsFiniteForInfiniteBounds() + { + var inf = float.PositiveInfinity; + // t over [0, 1] clamped below at 0: all ramp, 1/2. + Assert.Equal(0.5f, integral_clamp_linear(0.0f, 1.0f, 0.0f, inf, 0.0f, 1.0f)); + // t - 1/2 over [0, 1] clamped below at 0: 0 until 1/2, then the ramp's 1/8. + Assert.Equal(0.125f, integral_clamp_linear(-0.5f, 1.0f, 0.0f, inf, 0.0f, 1.0f)); + // The falling line 1/2 - t clamped below at 0 is the mirror: 1/8 over [0, 1]. + Assert.Equal(0.125f, integral_clamp_linear(0.5f, -1.0f, 0.0f, inf, 0.0f, 1.0f)); + // lo = -inf: clamp(t, -inf, 1/2) over [0, 1] is t up to 1/2 then 1/2 (1/8 + 1/4), and 1 - t clamped + // above at 1/2 is 1/2 until t = 1/2 and then falls (1/4 + 1/8). + Assert.Equal(0.375f, integral_clamp_linear(0.0f, 1.0f, -inf, 0.5f, 0.0f, 1.0f)); + Assert.Equal(0.375f, integral_clamp_linear(1.0f, -1.0f, -inf, 0.5f, 0.0f, 1.0f)); + // Both infinite: the line itself, the trapezoid, in either direction. + Assert.Equal(7.5f, integral_clamp_linear(0.0f, 1.0f, -inf, inf, 1.0f, 4.0f)); + Assert.Equal(-7.5f, integral_clamp_linear(0.0f, 1.0f, -inf, inf, 4.0f, 1.0f)); + // A line starting on the finite bound and rising into the infinite one: all ramp, the integral of t over [2, 4]. + Assert.Equal(6.0f, integral_clamp_linear(0.0f, 1.0f, 2.0f, inf, 2.0f, 4.0f)); + } + [Fact] public void IntegralClampLinearHasExactValuesOnTheBinaryFloats() { diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl index 8379b5f1..988e6a72 100644 --- a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl +++ b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl @@ -315,7 +315,9 @@ float cultmath_integral_clamp_linear(float a, float b, float lo, float hi, float float leave = clamp(max(atLo, atHi), start, stop); float before = b > 0.0 ? lo : hi; float after = b > 0.0 ? hi : lo; - float total = before * (enter - start) + (a + b * (0.5 * (enter + leave))) * (leave - enter) + after * (stop - leave); + float held = enter > start ? before * (enter - start) : 0.0; + float kept = stop > leave ? after * (stop - leave) : 0.0; + float total = held + (a + b * (0.5 * (enter + leave))) * (leave - enter) + kept; return direction * total; }