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:
parent
98f4c44170
commit
b2852304b1
11 changed files with 192 additions and 40 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
39
packages/cultmath/tools/compile-hlsl.ps1
Normal file
39
packages/cultmath/tools/compile-hlsl.ps1
Normal 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)"
|
||||
}
|
||||
7
packages/cultmath/tools/compile-hlsl.ps1.meta
Normal file
7
packages/cultmath/tools/compile-hlsl.ps1.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3f6c1e7a9b2d4c58a0e4d7b1c95f2a86
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue