cultmath: flat integral_clamp_linear returns +0 for a zero-length span; HLSL guard, lo-normalise and pin tests behavioural; owned dxc compile script

The b == 0 branch now guards t1 != t0 like the sloped branch (C# and both HLSL
copies), so an infinite bound times a zero length no longer forms NaN.
Edge-grid mirror test and a counting-clamp shadow test replace the text-order
check; the four pin tests assert equality with their measured expression;
tools/compile-hlsl.ps1 is the one dxc compile verdict for the HLSL copies.

Builds. CultMath.glsl and glsl-parity.json are not yet regenerated, so
CommittedGlslEqualsLowering and GoldenFixtureMatchesCSharp are red at this
commit; the next commit regenerates them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-16 01:01:54 +02:00
commit b2852304b1
11 changed files with 192 additions and 40 deletions

View file

@ -167,6 +167,7 @@ CultMath keeps portable DXC outside git under `packages/cultmath/.tools/`:
```powershell
.\packages\cultmath\tools\get-dxc.ps1
.\packages\cultmath\tools\compile-hlsl.ps1
.\packages\cultmath\tools\get-spirv-cross.ps1
.\packages\cultmath\tools\compile-hlsl-spirv.ps1 `
-ShaderPath E:\Projects\Odin\crates\muninn-move-tracker\shaders\MoveSphereCandidate.comp.hlsl `
@ -177,6 +178,8 @@ CultMath keeps portable DXC outside git under `packages/cultmath/.tools/`:
-OutputPath E:\Projects\Odin\crates\muninn-move-tracker\artifacts\shader\MoveSphereCandidate.comp.metal
```
`compile-hlsl.ps1` compiles both `CultMath.hlsl` copies as a `lib_6_3` library with the x64 DXC.
DXC emits Vulkan SPIR-V with `-spirv`. `-TargetEnv vulkan1.2` is exposed by the
script, but the current Windows DXC release compiled this shader successfully
with DXC's default Vulkan target while rejecting the dotted target-env value.

View file

@ -112,8 +112,9 @@ local) and writes through a swizzle component (`v.xy.x = s`, which C# rejects as
modifying a temporary).
The HLSL side is checked too: `tools/get-dxc.ps1` fetches Microsoft's
DirectXShaderCompiler, and
`dxc -T lib_6_3 -HV 2021 shaders/CultMath.hlsl` must compile cleanly.
DirectXShaderCompiler (x64), and `tools/compile-hlsl.ps1` compiles both copies of
`CultMath.hlsl` with it (`dxc -T lib_6_3 -HV 2021`); the script throws when dxc cannot
launch or exits non-zero, so a stale exit code never reads as a pass.
The mirror test compares bit patterns (any NaN equals any NaN; -0 differs from
0). It proves the text of `CultMath.hlsl` computes the same float32 results as

View file

@ -304,7 +304,7 @@ float cultmath_integral_clamp_linear(float a, float b, float lo, float hi, float
{
lo = min(lo, hi);
if (b == 0.0)
return clamp(a, lo, hi) * (t1 - t0);
return t1 != t0 ? clamp(a, lo, hi) * (t1 - t0) : 0.0;
float direction = t1 < t0 ? -1.0 : 1.0;
float start = min(t0, t1);

View file

@ -112,7 +112,7 @@ public static partial class math
{
lo = min(lo, hi);
if (b == 0.0f)
return clamp(a, lo, hi) * (t1 - t0);
return t1 != t0 ? clamp(a, lo, hi) * (t1 - t0) : 0.0f;
var direction = t1 < t0 ? -1.0f : 1.0f;
var start = min(t0, t1);

View file

@ -45,24 +45,50 @@ public sealed class GlslMirrorTests
}
}
// Counting stand-ins for the mirror's clamp, one per overload, declared in the mirror's class so they
// hide math.clamp for every function in it. Each counts a call whose minimum exceeds its maximum in any
// component and forwards to math.clamp. (The int vector overloads only forward: no mirror function
// clamps integer vectors with swapped bounds, and counting them would be unreachable.)
private const string CountingClampShadows = @"
public static int ClampMinAboveMax;
static float clamp(float x, float a, float b) { if (a > b) ClampMinAboveMax++; return math.clamp(x, a, b); }
static float2 clamp(float2 x, float2 a, float2 b) { if (a.x > b.x || a.y > b.y) ClampMinAboveMax++; return math.clamp(x, a, b); }
static float3 clamp(float3 x, float3 a, float3 b) { if (a.x > b.x || a.y > b.y || a.z > b.z) ClampMinAboveMax++; return math.clamp(x, a, b); }
static float4 clamp(float4 x, float4 a, float4 b) { if (a.x > b.x || a.y > b.y || a.z > b.z || a.w > b.w) ClampMinAboveMax++; return math.clamp(x, a, b); }
static int clamp(int x, int a, int b) { if (a > b) ClampMinAboveMax++; return math.clamp(x, a, b); }
static int2 clamp(int2 x, int2 a, int2 b) { if (a.x > b.x || a.y > b.y) ClampMinAboveMax++; return math.clamp(x, a, b); }
static int3 clamp(int3 x, int3 a, int3 b) { if (a.x > b.x || a.y > b.y || a.z > b.z) ClampMinAboveMax++; return math.clamp(x, a, b); }
static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z > b.z || a.w > b.w) ClampMinAboveMax++; return math.clamp(x, a, b); }
";
/// <summary>
/// The lowering keeps HLSL's statement order, and GLSL leaves clamp(x, minVal, maxVal) undefined for
/// minVal above maxVal, so integral_clamp_linear must normalise lo = min(lo, hi) before any clamp in
/// its body can see it: in the committed HLSL and the committed GLSL alike, the normalisation precedes
/// the first clamp.
/// The lowering keeps HLSL's statement order (CommittedGlslEqualsLowering pins the GLSL as that
/// lowering), and GLSL leaves clamp(x, minVal, maxVal) undefined for minVal above maxVal, so no clamp
/// reached by cultmath_integral_clamp_linear may see lo above hi, on any path and for any spelling of the
/// normalisation. The mirror is compiled with counting clamps and the function is run over the edge grid
/// (lo above hi, infinite bounds, zero-length spans, b = 0 and b != 0): the count of such clamp calls
/// stays 0 after every call.
/// </summary>
[Fact]
public void IntegralClampLinearNormalisesLoBeforeAnyClamp()
public void IntegralClampLinearNeverClampsWithMinAboveMax()
{
foreach (var (file, text) in Lowered.Where(l => l.File == "CultMath.glsl").Concat(new[] { ("CultMath.hlsl", Hlsl) }))
var (assembly, errors) = HlslSourceCompatibilityTests.CompileShaderMirror(CountingClampShadows);
Assert.True(assembly is not null, string.Join(Environment.NewLine, errors));
var shaderType = assembly!.GetType("CultMathHlsl.HlslShader")!;
var shader = Activator.CreateInstance(shaderType);
var count = shaderType.GetField("ClampMinAboveMax")!;
var function = shaderType.GetMethod("cultmath_integral_clamp_linear", BindingFlags.Instance | BindingFlags.NonPublic)!;
var bad = new List<string>();
foreach (var c in HlslSourceCompatibilityTests.IntegralClampLinearEdgeGrid)
{
var start = text.IndexOf("cultmath_integral_clamp_linear(float a", StringComparison.Ordinal);
Assert.True(start >= 0, $"{file} has no cultmath_integral_clamp_linear");
var body = text[start..text.IndexOf("}", start, StringComparison.Ordinal)];
var normalise = body.IndexOf("lo = min(lo, hi);", StringComparison.Ordinal);
var firstClamp = body.IndexOf("clamp(", StringComparison.Ordinal);
Assert.True(normalise >= 0 && normalise < firstClamp, $"{file}: lo = min(lo, hi) must come before the first clamp in cultmath_integral_clamp_linear");
count.SetValue(null, 0);
function.Invoke(shader, c.Cast<object>().ToArray());
if ((int)count.GetValue(null)! > 0)
bad.Add($"({string.Join(", ", c)})");
}
Assert.True(bad.Count == 0, $"{bad.Count} cases clamp with the minimum above the maximum:{Environment.NewLine}{string.Join(Environment.NewLine, bad.Take(12))}");
}
// Identifier boundaries: cultmath_lerp is a CultMath function, lerp( is HLSL.
@ -675,6 +701,19 @@ public sealed class GlslMirrorTests
_ => new object[] { a, slope, float.NegativeInfinity, float.PositiveInfinity, t0, t1 },
};
}),
// Zero-length spans (t1 = t0 three times in four) with a flat or sloped line, and infinite clamp bounds
// on either side or both: an infinite bound times a zero length must give +0, and a flat line clamped
// to an infinite bound over a nonzero span is infinite, never NaN.
new Family("float cultmath_integral_clamp_linear(float, float, float, float, float, float)", "ulp-bounded, zero-length spans and flat infinite clamps",
r =>
{
float Line() => r.Next(3) switch { 0 => 0.0f, 1 => -0.0f, _ => Uniform(r, -4.0f, 4.0f) };
float Bound() => r.Next(3) switch { 0 => float.NegativeInfinity, 1 => float.PositiveInfinity, _ => Uniform(r, -2.0f, 2.0f) };
var (a, b, lo, hi) = (Line(), Line(), Bound(), Bound());
var t0 = Uniform(r, -3.0f, 3.0f);
var t1 = r.Next(4) != 0 ? t0 : Uniform(r, -3.0f, 3.0f);
return new object[] { a, b, lo, hi, t0, t1 };
}),
}).ToArray();
// The fixture text, each case's arguments given by family and point index.

View file

@ -84,12 +84,13 @@ public sealed class HlslSourceCompatibilityTests
}
}
private static readonly float[] Specials = { 0.0f, -0.0f, 1.0f, -1.0f, 0.5f, -2.75f, 3.0f, 1.0e-7f, 1.0e4f, -1.0e4f, 1.0e7f, float.NaN };
// Special values every mirror argument takes: zeros, ties, negatives, large values, NaN and both infinities.
private static readonly float[] Specials = { 0.0f, -0.0f, 1.0f, -1.0f, 0.5f, -2.75f, 3.0f, 1.0e-7f, 1.0e4f, -1.0e4f, 1.0e7f, float.NaN, float.PositiveInfinity, float.NegativeInfinity };
/// <summary>
/// Every mirror function with a C# math counterpart returns the same bits (any NaN equals any NaN;
/// -0 differs from 0) over random inputs, every special value in every argument (ties, zeros,
/// negatives, NaN, large values, integer lattice points), special pairs, and vectors whose first two
/// negatives, NaN, infinities, large values, integer lattice points), special pairs, and vectors whose first two
/// components tie (x0.x == x0.y in snoise). This proves the text of CultMath.hlsl computes the same
/// float32 results as C# math on the CPU. It does not prove GPU agreement: driver sin precision alone
/// makes cultmath_hash and value noise differ bit for bit on hardware. The mirror's intrinsics are C#
@ -178,6 +179,56 @@ public sealed class HlslSourceCompatibilityTests
Assert.Equal(120, compared.Count);
}
/// <summary>
/// The 1500 arguments (a, b, lo, hi, t0, t1) at which integral_clamp_linear's zero-length rule, its lo
/// normalisation and its infinite bounds all bite: a in {-2.5, 0, 1.5}, b in {-1.5, -0, 0, 2}, lo in
/// {-inf, -1, 0.5, 3, +inf}, hi in {-inf, -0.5, 1, 4, +inf}, and (t0, t1) in {(0, 0), (1.25, 1.25), (-1, 2),
/// (2, -1), (0, 1e-30)}. One spelling, read by the mirror grid test and by the GLSL clamp shadow test.
/// </summary>
internal static IReadOnlyList<float[]> IntegralClampLinearEdgeGrid { get; } = BuildIntegralClampLinearEdgeGrid();
private static List<float[]> BuildIntegralClampLinearEdgeGrid()
{
var inf = float.PositiveInfinity;
var grid = new List<float[]>();
foreach (var a in new[] { -2.5f, 0.0f, 1.5f })
foreach (var b in new[] { -1.5f, -0.0f, 0.0f, 2.0f })
foreach (var lo in new[] { -inf, -1.0f, 0.5f, 3.0f, inf })
foreach (var hi in new[] { -inf, -0.5f, 1.0f, 4.0f, inf })
foreach (var (t0, t1) in new[] { (0.0f, 0.0f), (1.25f, 1.25f), (-1.0f, 2.0f), (2.0f, -1.0f), (0.0f, 1.0e-30f) })
grid.Add(new[] { a, b, lo, hi, t0, t1 });
return grid;
}
/// <summary>
/// The compiled mirror's cultmath_integral_clamp_linear returns C#'s bits on every edge-grid case, and C#'s
/// result is never NaN there. The generic case set catches each of the function's guards (the held and
/// kept stretch guards, the flat branch's zero-length guard) only by its rotation: with the kept guard
/// removed it finds 0 mismatches, with the held guard removed 1 once infinities join the specials, and the
/// edge grid finds 246 for either every time.
/// </summary>
[Fact]
public void IntegralClampLinearMirrorMatchesCSharpOnTheEdgeGrid()
{
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_integral_clamp_linear", Enumerable.Repeat(typeof(float), 6).ToArray())!;
var mismatches = new List<string>();
foreach (var c in IntegralClampLinearEdgeGrid)
{
var expected = integral_clamp_linear(c[0], c[1], c[2], c[3], c[4], c[5]);
var actual = (float)mirror.Invoke(shader, c.Cast<object>().ToArray())!;
Assert.False(float.IsNaN(expected), $"C# returns NaN at ({string.Join(", ", c)})");
if (BitConverter.SingleToInt32Bits(expected) != BitConverter.SingleToInt32Bits(actual))
mismatches.Add($"({string.Join(", ", c)}): C# {expected:R}, mirror {actual:R}");
}
Assert.True(mismatches.Count == 0, $"{mismatches.Count} of {IntegralClampLinearEdgeGrid.Count} cases differ:{Environment.NewLine}{string.Join(Environment.NewLine, mismatches.Take(12))}");
}
/// <summary>
/// The generic comparison above passes about a hundred inputs through <c>cultmath_phacelle</c>, and
/// its prune only changes an output for the rare cell whose weight is nonzero but whose lower
@ -448,8 +499,11 @@ public sealed class HlslSourceCompatibilityTests
: string.Empty);
}
/// <summary>The documented HLSL-to-C# transformations, and nothing else.</summary>
internal static string TransformHlslToCSharp(string hlsl)
/// <summary>
/// The documented HLSL-to-C# transformations, and nothing else. <paramref name="members"/> is C# source a
/// test supplies, placed first in the class body: it is not a transformation of the HLSL.
/// </summary>
internal static string TransformHlslToCSharp(string hlsl, string members = "")
{
// 1. Include guards (#ifndef/#define/#endif) are dropped; C# has no textual include.
var source = Regex.Replace(hlsl, @"(?m)^\s*#(ifndef|define|endif)\b.*$", string.Empty);
@ -471,13 +525,13 @@ public sealed class HlslSourceCompatibilityTests
m.Groups[1].Value + Regex.Replace(m.Groups[2].Value, @"(?m)^(\s*)(\w+\s+\w+;)", "$1public $2") + m.Groups[3].Value);
// 6. The file body is wrapped in a class; C# has no free functions.
return "using CultMath;\nusing static CultMath.math;\nnamespace CultMathHlsl;\npublic class HlslShader\n{\n" + source + "\n}\n";
return "using CultMath;\nusing static CultMath.math;\nnamespace CultMathHlsl;\npublic class HlslShader\n{\n" + members + source + "\n}\n";
}
internal static (Assembly? Assembly, IReadOnlyList<string> Errors) CompileShaderMirror()
internal static (Assembly? Assembly, IReadOnlyList<string> Errors) CompileShaderMirror(string members = "")
{
var hlsl = ReadShaderSource(FindCultMathRoot());
var tree = CSharpSyntaxTree.ParseText(TransformHlslToCSharp(hlsl));
var tree = CSharpSyntaxTree.ParseText(TransformHlslToCSharp(hlsl, members));
var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)
.Split(Path.PathSeparator)
.Append(typeof(math).Assembly.Location)

View file

@ -469,8 +469,8 @@ public sealed class NoiseBoundTests
/// <summary>
/// Pins SNOISE_LIPSCHITZ: no |snoise_grad| over 1e5 seeded points exceeds it, and the ascent from
/// MeasureLipschitz's witness reaches a maximum that, times 1.10, is the constant to within half a
/// percent, so a constant lowered (or raised) by 1% fails here.
/// MeasureLipschitz's witness reaches a maximum that, times 1.10, equals the constant bit for bit
/// (the expression MeasureLipschitz prints), so any moved constant fails here.
/// </summary>
[Fact]
public void LipschitzConstantPinsSampledGradients()
@ -479,8 +479,8 @@ public sealed class NoiseBoundTests
Assert.True(sampled <= SNOISE_LIPSCHITZ, $"|snoise_grad| reaches {sampled:R}, above SNOISE_LIPSCHITZ {SNOISE_LIPSCHITZ:R}");
var refined = AscendGradientNorm(LipschitzWitness);
Assert.True(refined <= SNOISE_LIPSCHITZ, $"|snoise_grad| reaches {refined:R}, above SNOISE_LIPSCHITZ {SNOISE_LIPSCHITZ:R}");
var ratio = SNOISE_LIPSCHITZ / (1.10f * refined);
Assert.True(MathF.Abs(ratio - 1.0f) <= 0.005f, $"SNOISE_LIPSCHITZ is {ratio:R} times 1.10 x the re-measured {refined:R}; re-run MeasureLipschitz and re-pin");
var measured = 1.10f * refined;
Assert.True(SNOISE_LIPSCHITZ == measured, $"SNOISE_LIPSCHITZ is {SNOISE_LIPSCHITZ:R}, the re-measured 1.10 x {refined:R} is {measured:R}; re-run MeasureLipschitz and re-pin");
}
// ---- The curvature and 2D Lipschitz constants (math.Affine.cs, math.Interval.cs) ----
@ -609,8 +609,8 @@ public sealed class NoiseBoundTests
/// <summary>
/// Pins SNOISE_HESSIAN: no Hessian norm over 1e5 seeded points exceeds it, and the ascent from
/// MeasureHessian's witness reaches a maximum that, times 1.10, is the constant to within half a
/// percent, so a constant moved by 1% fails here.
/// MeasureHessian's witness reaches a maximum that, times 1.10, equals the constant bit for bit
/// (the expression MeasureHessian prints), so any moved constant fails here.
/// </summary>
[Fact]
public void HessianConstantPinsSampledCurvature()
@ -619,8 +619,8 @@ public sealed class NoiseBoundTests
Assert.True(sampled <= SNOISE_HESSIAN, $"the Hessian norm reaches {sampled:R}, above SNOISE_HESSIAN {SNOISE_HESSIAN:R}");
var refined = Ascend(HessianNorm, HessianWitness);
Assert.True(refined <= SNOISE_HESSIAN, $"the Hessian norm reaches {refined:R}, above SNOISE_HESSIAN {SNOISE_HESSIAN:R}");
var ratio = SNOISE_HESSIAN / (1.10 * refined);
Assert.True(Math.Abs(ratio - 1.0) <= 0.005, $"SNOISE_HESSIAN is {ratio:R} times 1.10 x the re-measured {refined:R}; re-run MeasureHessian and re-pin");
var measured = (float)(refined * 1.10);
Assert.True(SNOISE_HESSIAN == measured, $"SNOISE_HESSIAN is {SNOISE_HESSIAN:R}, the re-measured 1.10 x {refined:R} is {measured:R}; re-run MeasureHessian and re-pin");
}
// ---- The third-derivative constant (math.Span.cs) ----
@ -947,8 +947,8 @@ public sealed class NoiseBoundTests
/// <summary>
/// Pins SNOISE_THIRD: no third-derivative norm over 1e5 seeded points exceeds it, and the ascent from
/// MeasureThird's witness reaches a maximum that, times 1.10, is the constant to within half a
/// percent, so a constant moved by 1% fails here.
/// MeasureThird's witness reaches a maximum that, times 1.10, equals the constant bit for bit
/// (the expression MeasureThird prints), so any moved constant fails here.
/// </summary>
[Fact]
public void ThirdConstantPinsSampled()
@ -957,8 +957,8 @@ public sealed class NoiseBoundTests
Assert.True(sampled <= SNOISE_THIRD, $"the third-derivative norm reaches {sampled:R}, above SNOISE_THIRD {SNOISE_THIRD:R}");
var refined = Ascend(ThirdNorm, ThirdWitness);
Assert.True(refined <= SNOISE_THIRD, $"the third-derivative norm reaches {refined:R}, above SNOISE_THIRD {SNOISE_THIRD:R}");
var ratio = SNOISE_THIRD / (1.10 * refined);
Assert.True(Math.Abs(ratio - 1.0) <= 0.005, $"SNOISE_THIRD is {ratio:R} times 1.10 x the re-measured {refined:R}; re-run MeasureThird and re-pin");
var measured = (float)(refined * 1.10);
Assert.True(SNOISE_THIRD == measured, $"SNOISE_THIRD is {SNOISE_THIRD:R}, the re-measured 1.10 x {refined:R} is {measured:R}; re-run MeasureThird and re-pin");
}
/// <summary>
@ -1019,7 +1019,7 @@ public sealed class NoiseBoundTests
/// <summary>
/// Pins SNOISE2_LIPSCHITZ as LipschitzConstantPinsSampledGradients pins SNOISE_LIPSCHITZ: 1e5 seeded
/// points under it, and the ascent from MeasureLipschitz2's witness within half a percent of it / 1.10.
/// points under it, and the ascent from MeasureLipschitz2's witness, times 1.10, equals it bit for bit (the expression MeasureLipschitz2 prints).
/// </summary>
[Fact]
public void Lipschitz2ConstantPins()
@ -1028,8 +1028,8 @@ public sealed class NoiseBoundTests
Assert.True(sampled <= SNOISE2_LIPSCHITZ, $"|grad snoise2| reaches {sampled:R}, above SNOISE2_LIPSCHITZ {SNOISE2_LIPSCHITZ:R}");
var refined = Ascend2(Gradient2Norm, Lipschitz2Witness);
Assert.True(refined <= SNOISE2_LIPSCHITZ, $"|grad snoise2| reaches {refined:R}, above SNOISE2_LIPSCHITZ {SNOISE2_LIPSCHITZ:R}");
var ratio = SNOISE2_LIPSCHITZ / (1.10 * refined);
Assert.True(Math.Abs(ratio - 1.0) <= 0.005, $"SNOISE2_LIPSCHITZ is {ratio:R} times 1.10 x the re-measured {refined:R}; re-run MeasureLipschitz2 and re-pin");
var measured = (float)(refined * 1.10);
Assert.True(SNOISE2_LIPSCHITZ == measured, $"SNOISE2_LIPSCHITZ is {SNOISE2_LIPSCHITZ:R}, the re-measured 1.10 x {refined:R} is {measured:R}; re-run MeasureLipschitz2 and re-pin");
}
/// <summary>

View file

@ -322,7 +322,7 @@ public sealed class SpanPrimitiveTests
/// <summary>
/// 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.
/// bound inside the span or not. The flat branch (b = 0) obeys the same zero-length rule.
/// </summary>
[Fact]
public void IntegralClampLinearIsFiniteForInfiniteBounds()
@ -343,6 +343,15 @@ public sealed class SpanPrimitiveTests
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));
// The flat branch obeys the same zero-length rule: a stretch of zero length contributes +0 however
// infinite the bound it would be clamped to, and a nonzero length of an infinite constant is infinite.
// The bit patterns are compared because Assert.Equal treats -0 as 0.
Assert.Equal(0, BitConverter.SingleToInt32Bits(integral_clamp_linear(-2.3244874f, -0.0f, 0.0f, -inf, 1.429815f, 1.429815f)));
Assert.Equal(0, BitConverter.SingleToInt32Bits(integral_clamp_linear(1.5f, 0.0f, inf, inf, 2.0f, 2.0f)));
Assert.Equal(0, BitConverter.SingleToInt32Bits(integral_clamp_linear(1.5f, -0.0f, -inf, -inf, 0.0f, 0.0f)));
Assert.Equal(inf, integral_clamp_linear(1.5f, 0.0f, inf, inf, 0.0f, 2.0f));
Assert.Equal(inf, integral_clamp_linear(1.5f, 0.0f, -inf, -inf, 2.0f, 0.0f));
}
/// <summary>

View file

@ -0,0 +1,39 @@
param(
# The HLSL files to compile, each as a lib_6_3 library with its own directory as the include path.
# Default: both copies of CultMath.hlsl (the canonical one and the Unity package's byte copy).
# tools\compile-hlsl.ps1 -ShaderPath <a file holding 'float f( { return 0; }'> is the negative control.
[string[]]$ShaderPath = @(),
# The dxc to run. Default: the x64 install get-dxc.ps1 makes; it is never searched for, so a dxc
# that cannot launch (the arm64 one on x64) is an error, not a skipped check.
[string]$DxcPath = ""
)
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
if (-not $DxcPath) {
$DxcPath = Join-Path $repoRoot ".tools\dxc\current\bin\x64\dxc.exe"
}
if (-not $ShaderPath) {
$ShaderPath = @(
(Join-Path $repoRoot "shaders\CultMath.hlsl"),
(Join-Path $repoRoot "unity\org.gamecult.cultmath\Shaders\CultMath.hlsl"))
}
if (-not (Test-Path -LiteralPath $DxcPath)) {
throw "dxc not found (parameter DxcPath). Run tools\get-dxc.ps1 first or pass -DxcPath."
}
$outputDirectory = Join-Path $repoRoot ".tools\hlsl"
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
$index = 0
foreach ($shader in $ShaderPath) {
$index++
$shaderFile = Get-Item -LiteralPath $shader
$output = Join-Path $outputDirectory "$index-$($shaderFile.BaseName).dxil"
& $DxcPath -T lib_6_3 -HV 2021 -I $shaderFile.DirectoryName $shaderFile.FullName -Fo $output
if ($LASTEXITCODE -ne 0) {
throw "dxc failed with exit code $LASTEXITCODE (parameter ShaderPath, entry $index)."
}
Write-Host "compiled shader $index of $(@($ShaderPath).Count) (lib_6_3, -HV 2021)"
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3f6c1e7a9b2d4c58a0e4d7b1c95f2a86
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -304,7 +304,7 @@ float cultmath_integral_clamp_linear(float a, float b, float lo, float hi, float
{
lo = min(lo, hi);
if (b == 0.0)
return clamp(a, lo, hi) * (t1 - t0);
return t1 != t0 ? clamp(a, lo, hi) * (t1 - t0) : 0.0;
float direction = t1 < t0 ? -1.0 : 1.0;
float start = min(t0, t1);