using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
using static CultMath.math;
namespace CultMath.Tests;
///
/// The real shader mirror, shaders/CultMath.hlsl, compiles as C# against CultMath under
/// using static CultMath.math; after exactly the source transformations listed in
/// docs/design.md (HLSL Target: Source Transformations). is
/// that list in code; if a body needs anything else, this test fails.
///
public sealed class HlslSourceCompatibilityTests
{
[Fact]
public void ShaderMirrorBodiesCompileAsCSharp()
{
var (assembly, errors) = CompileShaderMirror();
Assert.True(assembly is not null, "HLSL mirror did not compile as C#:" + Environment.NewLine + string.Join(Environment.NewLine, errors));
var shader = assembly!.GetType("CultMathHlsl.HlslShader")!;
Assert.NotNull(ShaderFunction(shader, "cultmath_snoise", typeof(float3)));
Assert.NotNull(ShaderFunction(shader, "cultmath_snoise", typeof(float2)));
Assert.NotNull(ShaderFunction(shader, "cultmath_value_noise_bicubic", typeof(float2)));
Assert.NotNull(ShaderFunction(shader, "cultmath_smin_grad", typeof(float4), typeof(float4), typeof(float)));
Assert.NotNull(ShaderFunction(shader, "cultmath_cellular", typeof(float3)));
var siteCells = shader.GetNestedType("CultSiteCells", BindingFlags.Public | BindingFlags.NonPublic)!;
Assert.NotNull(ShaderFunction(shader, "cultmath_power_score", typeof(float3), typeof(float4)));
Assert.NotNull(ShaderFunction(shader, "cultmath_multiplicative_score", typeof(float3), typeof(float4)));
Assert.NotNull(ShaderFunction(shader, "cultmath_site_cells_empty"));
Assert.NotNull(ShaderFunction(shader, "cultmath_site_cells_insert", siteCells, typeof(float4), typeof(int)));
Assert.NotNull(ShaderFunction(shader, "cultmath_site_cells_border", siteCells));
Assert.NotNull(ShaderFunction(shader, "cultmath_gabriel_margin", typeof(float3), typeof(float3), typeof(float3)));
Assert.NotNull(ShaderFunction(shader, "cultmath_power_border_step", typeof(float3), typeof(float4), typeof(float4), typeof(float4)));
Assert.NotNull(ShaderFunction(shader, "cultmath_lattice_site", typeof(float2), typeof(float), typeof(float)));
Assert.NotNull(ShaderFunction(shader, "cultmath_lattice_weight", typeof(float), typeof(float), typeof(float), typeof(float)));
Assert.NotNull(ShaderFunction(shader, "cultmath_lattice_distance", typeof(float2), typeof(float2), typeof(float), typeof(float2), typeof(float)));
Assert.NotNull(ShaderFunction(shader, "cultmath_lattice_smooth_step", typeof(float), typeof(float), typeof(float), typeof(float)));
Assert.NotNull(ShaderFunction(shader, "cultmath_segment_closest", typeof(float2), typeof(float2), typeof(float2)));
Assert.NotNull(ShaderFunction(shader, "cultmath_distance_to_segment", typeof(float2), typeof(float2), typeof(float2)));
Assert.NotNull(ShaderFunction(shader, "cultmath_segment_closest", typeof(float3), typeof(float3), typeof(float3)));
Assert.NotNull(ShaderFunction(shader, "cultmath_distance_to_segment", typeof(float3), typeof(float3), typeof(float3)));
Assert.NotNull(ShaderFunction(shader, "cultmath_snoise_grad", typeof(float3)));
Assert.NotNull(ShaderFunction(shader, "cultmath_snoise_hessian", typeof(float3)));
Assert.NotNull(ShaderFunction(shader, "cultmath_integral_clamp_linear", typeof(float), typeof(float), typeof(float), typeof(float), typeof(float), typeof(float)));
Assert.NotNull(ShaderFunction(shader, "cultmath_fbm_grad", typeof(float3), typeof(int), typeof(float), typeof(float)));
Assert.NotNull(ShaderFunction(shader, "cultmath_ridged_grad", typeof(float3), typeof(int), typeof(float), typeof(float)));
Assert.NotNull(ShaderFunction(shader, "cultmath_phacelle", typeof(float3), typeof(float3), typeof(float), typeof(float)));
Assert.NotNull(ShaderFunction(shader, "cultmath_iv_snoise_ball", typeof(float3), typeof(float)));
Assert.NotNull(ShaderFunction(shader, "cultmath_iv_fbm_ball", typeof(float3), typeof(float), typeof(int), typeof(float), typeof(float)));
Assert.NotNull(ShaderFunction(shader, "cultmath_iv_snoise_ball", typeof(float2), typeof(float)));
Assert.NotNull(ShaderFunction(shader, "cultmath_af_snoise", typeof(float3), typeof(float3), typeof(float)));
Assert.NotNull(ShaderFunction(shader, "cultmath_af_fbm", typeof(float3), typeof(float3), typeof(float), typeof(int), typeof(float), typeof(float)));
Assert.NotNull(ShaderFunction(shader, "cultmath_af_frustum_ball", typeof(float2), typeof(float), typeof(float), typeof(float), typeof(float)));
var (spanPath, spanStage) = SpanTypes(shader);
Assert.NotNull(ShaderFunction(shader, "cultmath_span_fbm", spanPath, spanStage, typeof(int), typeof(float), typeof(float)));
Assert.NotNull(ShaderFunction(shader, "cultmath_span_ridged", spanPath, spanStage, typeof(int), typeof(float), typeof(float)));
Assert.NotNull(ShaderFunction(shader, "cultmath_span_stage", typeof(bool), spanPath, spanStage, typeof(int), typeof(float), typeof(float)));
}
// The mirror's struct types are private nested types of HlslShader (a struct without a modifier).
private static (Type Path, Type Stage) SpanTypes(Type shader) =>
(shader.GetNestedType("SpanPath", BindingFlags.Public | BindingFlags.NonPublic)!, shader.GetNestedType("SpanStage", BindingFlags.Public | BindingFlags.NonPublic)!);
// HLSL functions carry no access modifier, so they compile as private instance methods.
private static MethodInfo? ShaderFunction(Type shader, string name, params Type[] parameters) =>
shader.GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic, null, parameters, null);
// Shader constants with no C# twin of the same name (empty when every constant has one).
private static readonly string[] ConstantsWithoutTwin = { "CULTMATH_MAX_SITES" };
///
/// Every static const of the compiled HLSL mirror (a static literal CULTMATH_* field, whatever its access) is the
/// same float32, bit for bit, as the math constant of the same name without the prefix (private ones
/// included), so a pin edited in one runtime and not the other fails here whichever constant it is.
/// A constant with neither a twin nor a listing in ConstantsWithoutTwin fails too.
///
[Fact]
public void EveryShaderConstantEqualsItsCSharpTwin()
{
var (assembly, errors) = CompileShaderMirror();
Assert.True(assembly is not null, string.Join(Environment.NewLine, errors));
var shader = assembly!.GetType("CultMathHlsl.HlslShader")!;
var constants = shader.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
.Where(f => f.IsLiteral && f.FieldType == typeof(float) && f.Name.StartsWith("CULTMATH_", StringComparison.Ordinal))
.ToArray();
Assert.True(constants.Length >= 10, $"only {constants.Length} shader constants were found by reflection");
foreach (var constant in constants)
{
var name = constant.Name["CULTMATH_".Length..];
var twin = typeof(math).GetField(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
if (twin is null)
{
Assert.True(ConstantsWithoutTwin.Contains(constant.Name), $"{constant.Name} has no math.{name} and is not listed in ConstantsWithoutTwin");
continue;
}
var shaderValue = (float)constant.GetRawConstantValue()!;
var mathValue = (float)twin.GetValue(null)!;
Assert.True(BitConverter.SingleToInt32Bits(shaderValue) == BitConverter.SingleToInt32Bits(mathValue),
$"{constant.Name} is {shaderValue:R} in the shader and math.{name} is {mathValue:R}");
}
}
// 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 };
///
/// Every mirror function matches the math function of the same name, public or private (a function with no
/// twin fails, and nothing is exempt), with a struct parameter mapped by name and fields to the CultMath
/// type of the same name, and 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, 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#
/// math itself (it compiles under using static CultMath.math), so this proves the composition in the
/// text, not the intrinsic rules; HlslSemanticsTests alone pins those.
///
[Fact]
public void EveryMirrorFunctionMatchesCSharpMath()
{
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 random = new System.Random(0x5EED);
float Next() => random.NextSingle() * 200.0f - 100.0f;
var cases = new List>();
foreach (var s in Specials) cases.Add(_ => s);
for (var k = 0; k < Specials.Length; k++) { var o = k; cases.Add(i => Specials[(i + o + 1) % Specials.Length]); }
for (var k = 0; k < 64; k++) { var values = Enumerable.Range(0, 32).Select(_ => Next()).ToArray(); cases.Add(i => values[i]); }
for (var k = 0; k < 16; k++) { var values = Enumerable.Range(0, 32).Select(_ => Next()).ToArray(); cases.Add(i => values[i & ~1]); }
// Distinct sites closer than 1e-2 (slots 3-6 are the owner and 7-10 the other site of
// cultmath_power_border_step): a length guard wider than "coincident" changes the result for these only.
for (var k = 0; k < 16; k++)
{
var values = Enumerable.Range(0, 32).Select(_ => Next()).ToArray();
for (var c = 0; c < 3; c++) values[7 + c] = values[3 + c] + (random.NextSingle() * 2.0f - 1.0f) * 1.0e-3f;
values[7] += 2.0e-4f;
cases.Add(i => values[i]);
}
// Points aimed squarely at cellular's HLSL mirror (F6/F3 Soul findings): the generic random
// and special-value cases above pass ~92 inputs through cultmath_cellular and never happen to
// hit a point where a radius-2 cell wins (about 3.5e-5 of points, Soul measured) or an
// integer point at or past 2^23, so an HLSL mutant of the search radius, the prune, or the
// removed F2=0 guard can survive this test even though it changes cellular's real output.
void AddPointCase(float3 point)
{
var values = new[] { point.x, point.y, point.z };
cases.Add(i => i < values.Length ? values[i] : 0.0f);
}
// The six constructed points from CellularAndSminGradTests.SearchReachesEveryAxisAlignedRadiusTwoSlice:
// each one realizes a different axis-aligned radius-2 offset as F1 or F2, which is exactly
// the case an `if (lowerBound >= f2)` prune written as `>= f1`, or a search radius narrowed on
// one axis (`dz < 2`, `dx` starting at -1), changes.
foreach (var offset in CellularAndSminGradTests.AxisAlignedRadiusTwoOffsets)
AddPointCase(CellularAndSminGradTests.FindPointRealizingOffset(offset));
// Integer points at 2^24, several, both signs: past |p| ~= 2^23 distinct cells' jittered
// features can round onto the same float32 value (design.md, "Precision domain"), making
// F2 = 0 reachable and exercising the grad2 = f2 > 0 ? ... : zero guard this test would
// otherwise never touch.
const float twoTo24 = 16777216.0f;
AddPointCase(new float3(twoTo24, twoTo24, twoTo24));
AddPointCase(new float3(-twoTo24, -twoTo24, -twoTo24));
AddPointCase(new float3(twoTo24 + 3.0f, -twoTo24 + 5.0f, twoTo24 - 7.0f));
AddPointCase(new float3(-twoTo24 + 11.0f, twoTo24 - 13.0f, -twoTo24 + 17.0f));
// One exact F1 = F2 tie point: the midpoint of two adjacent cells' feature points is
// bit-exact-equidistant from both by construction (negating a vector does not change its
// length, and p - a = -(p - b) here by symmetry), without needing to search for one.
var tieMidpoint = (CellularAndSminGradTests.FeaturePoint(new float3(0.0f, 0.0f, 0.0f))
+ CellularAndSminGradTests.FeaturePoint(new float3(1.0f, 0.0f, 0.0f))) * 0.5f;
AddPointCase(tieMidpoint);
var mismatches = new List();
var compared = new HashSet();
foreach (var mirror in shaderType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic).Where(m => m.Name.StartsWith("cultmath_") && !m.GetParameters().Any(p => p.ParameterType.IsArray)))
{
var types = mirror.GetParameters().Select(p => p.ParameterType).ToArray();
var counterpart = Twin(shaderType, mirror.Name["cultmath_".Length..], types);
Assert.True(counterpart is not null, $"{mirror.Name} has no C# math function of the same name and parameter types, public or private");
compared.Add(mirror.Name);
foreach (var input in cases)
{
var slot = 0;
var args = types.Select(t => Build(t, () => input(slot++))).ToArray();
var expected = Values(counterpart!.Invoke(null, args.Select(a => CopyFields(a, TwinType(shaderType, a.GetType())!)).ToArray())!);
var actual = Values(mirror.Invoke(shader, args)!);
if (!BitwiseEqual(expected, actual))
{
mismatches.Add($"{mirror.Name}({string.Join(", ", args.Select(a => string.Join(" ", Values(a).Select(v => v.Value))))}): C# {string.Join(" ", expected.Select(v => v.Value))}, mirror {string.Join(" ", actual.Select(v => v.Value))}");
}
}
}
Assert.True(mismatches.Count == 0, $"{mismatches.Count} mismatches:{Environment.NewLine}{string.Join(Environment.NewLine, mismatches.Take(12))}");
// The three site-array functions are compared by SiteArrayMirrorsMatchCSharp, which draws their counts and arrays.
Assert.Equal(140, compared.Count);
}
///
/// The shader functions that take a site array and a count are bit-equal to C#'s span functions on the same
/// arguments: at least 64 compared cases each, counts 0 to the capacity with filler after the count (a loop bound that
/// ignores the count reads it), the lattice at rings up to 17. A case C# refuses is skipped and counted.
///
[Fact]
public void SiteArrayMirrorsMatchCSharp()
{
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);
Assert.Equal(17, SiteArrays.FarPoints[0].Ring);
Assert.All(SiteArrays.FarPoints, far => Assert.InRange(far.Ring, 8, LatticeMaxRing));
var draws = new (string Name, Func Draw)[]
{
("power_border", SiteArrays.PowerBorderArguments),
("lattice_cell_distance", SiteArrays.LatticeCellDistanceArguments),
("weighted_lattice_cells", SiteArrays.LatticeArguments),
};
foreach (var (name, draw) in draws)
{
var mirror = shaderType.GetMethod("cultmath_" + name, BindingFlags.Instance | BindingFlags.NonPublic)!;
var twin = typeof(SiteArrays).GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic)!;
Assert.Equal(twin.GetParameters().Select(p => p.ParameterType), mirror.GetParameters().Select(p => p.ParameterType));
var random = new System.Random(0x517E);
int compared = 0, refused = 0;
for (var i = 0; i < 160; i++)
{
var args = draw(random);
object expected;
try { expected = twin.Invoke(null, args)!; }
catch (TargetInvocationException e) when (e.InnerException is ArgumentException) { refused++; continue; }
var actual = Values(mirror.Invoke(shader, args)!);
Assert.True(BitwiseEqual(Values(expected), actual),
$"{name} case {i}: C# {string.Join(" ", Values(expected).Select(v => v.Value))}, mirror {string.Join(" ", actual.Select(v => v.Value))}");
compared++;
}
Assert.True(compared >= 64, $"{name} compared only {compared} cases ({refused} refused)");
}
}
/// A consumer defines CULTMATH_MAX_SITES before including the library; the mirror compiles with the capacity changed.
[Fact]
public void TheSiteCapacityIsOverridable()
{
var (assembly, errors) = CompileShaderMirror(defines: new Dictionary { ["CULTMATH_MAX_SITES"] = 4 });
Assert.True(assembly is not null, string.Join(Environment.NewLine, errors));
var shaderType = assembly!.GetType("CultMathHlsl.HlslShader")!;
Assert.Equal(4, (int)shaderType.GetField("CULTMATH_MAX_SITES", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!.GetRawConstantValue()!);
var sites = new[] { new float4(0, 0, 0, 1), new float4(10, 0, 0, 1), new float4(0, 10, 0, 2), new float4(-10, 0, 0, 1) };
var border = (float4)shaderType.GetMethod("cultmath_power_border", BindingFlags.Instance | BindingFlags.NonPublic)!.Invoke(Activator.CreateInstance(shaderType), new object[] { new float3(1, 1, 0), sites, 4 })!;
Assert.Equal(power_border(new float3(1, 1, 0), sites), border);
}
///
/// 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.
///
internal static IReadOnlyList IntegralClampLinearEdgeGrid { get; } = BuildIntegralClampLinearEdgeGrid();
private static List BuildIntegralClampLinearEdgeGrid()
{
var inf = float.PositiveInfinity;
var grid = new List();
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;
}
///
/// The edge grid followed by the committed GLSL fixture's cases of the three integral_clamp_linear families
/// (GlslMirrorTests.IntegralClampLinearFixtureCases, decoded): one list, read by the mirror test and by the
/// clamp-shadow test. The grid fixes a in {-2.5, 0, 1.5}, so a guard keyed on a past 1.75 hides from it
/// (Soul s4); the fixture's random a in [-4, 4] closes that.
///
internal static IReadOnlyList IntegralClampLinearCases => IntegralClampLinearEdgeGrid.Concat(GlslMirrorTests.IntegralClampLinearFixtureCases).ToArray();
///
/// The compiled mirror's cultmath_integral_clamp_linear returns C#'s bits on every edge case (the grid and
/// the fixture's cases), 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.
///
[Fact]
public void IntegralClampLinearMirrorMatchesCSharpOnTheEdgeCases()
{
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();
var cases = IntegralClampLinearCases;
foreach (var c in cases)
{
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