CultLib/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs
Meta Vens 70914d9a95 CultMath: the fixture cases separate each lattice-loop mutant from the shader text
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 16:30:19 +02:00

868 lines
53 KiB
C#

using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
using static CultMath.math;
namespace CultMath.Tests;
/// <summary>
/// The real shader mirror, <c>shaders/CultMath.hlsl</c>, compiles as C# against CultMath under
/// <c>using static CultMath.math;</c> after exactly the source transformations listed in
/// docs/design.md (HLSL Target: Source Transformations). <see cref="TransformHlslToCSharp"/> is
/// that list in code; if a body needs anything else, this test fails.
/// </summary>
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" };
/// <summary>
/// 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.
/// </summary>
[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 };
/// <summary>
/// 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.
/// </summary>
[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<Func<int, float>>();
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<string>();
var compared = new HashSet<string>();
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);
}
/// <summary>
/// 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.
/// </summary>
[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<System.Random, object[]> 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)");
}
}
/// <summary>A consumer defines CULTMATH_MAX_SITES before including the library; the mirror compiles with the capacity changed.</summary>
[Fact]
public void TheSiteCapacityIsOverridable()
{
var (assembly, errors) = CompileShaderMirror(defines: new Dictionary<string, int> { ["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);
}
/// <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 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.
/// </summary>
internal static IReadOnlyList<float[]> IntegralClampLinearCases => IntegralClampLinearEdgeGrid.Concat(GlslMirrorTests.IntegralClampLinearFixtureCases).ToArray();
/// <summary>
/// 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.
/// </summary>
[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<string>();
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<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 {cases.Count} cases differ:{Environment.NewLine}{string.Join(Environment.NewLine, mismatches.Take(12))}");
}
/// <summary>
/// At a SpanStage the shader has no identity constant for, so C#'s identity (copied field by field) is the
/// argument: the mirror's cultmath_span_fbm and cultmath_span_ridged return the field of the mirror's own
/// cultmath_fbm_grad and cultmath_ridged_grad, bit for bit, for 0 to 20 octaves over random and simplex-tie
/// points. The shader-side statement of math.SpanStage.Identity (design decision a: no shader identity).
/// </summary>
[Fact]
public void SpanStagesAtIdentityAreFbmAndRidgedInTheMirror()
{
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 (pathType, stageType) = SpanTypes(shaderType);
var spanArguments = new[] { pathType, stageType, typeof(int), typeof(float), typeof(float) };
var pairs = new[]
{
(Span: ShaderFunction(shaderType, "cultmath_span_fbm", spanArguments)!, Plain: ShaderFunction(shaderType, "cultmath_fbm_grad", typeof(float3), typeof(int), typeof(float), typeof(float))!),
(Span: ShaderFunction(shaderType, "cultmath_span_ridged", spanArguments)!, Plain: ShaderFunction(shaderType, "cultmath_ridged_grad", typeof(float3), typeof(int), typeof(float), typeof(float))!),
};
var identity = CopyFields(SpanStage.Identity, stageType);
var random = new System.Random(0x57A6E2);
var compared = 0;
foreach (var p in SpanStageTests.Points(random))
{
var octaves = random.Next(0, 21);
var lacunarity = 1.4f + 1.2f * random.NextSingle();
var gain = 0.3f + 0.5f * random.NextSingle();
var path = CopyFields(new SpanPath { midpoint = p, velocity = new float3(0.6f, 0.0f, 0.8f), speed = 1.0f, acceleration = 0.5f, halfLength = 0.25f }, pathType);
foreach (var (span, plain) in pairs)
{
var field = FieldOf(span.Invoke(shader, new object[] { path, identity, octaves, lacunarity, gain })!, "field");
var expected = plain.Invoke(shader, new object[] { p, octaves, lacunarity, gain })!;
Assert.True(BitwiseEqual(Values(expected), Values(field)), $"{span.Name} at {p}, {octaves} octaves: {string.Join(" ", Values(field).Select(v => v.Value))} is not {plain.Name} {string.Join(" ", Values(expected).Select(v => v.Value))}");
compared++;
}
}
Assert.True(compared >= 460, $"only {compared} identity comparisons ran");
}
/// <summary>
/// The generic comparison draws every argument from [-100, 100] (and specials), where the octave loop's
/// frequency, stretch and span terms saturate: 0 or 16 octaves, a Hessian bound pinned at its global value.
/// This sweep draws SpanStageTests' realistic stages and spans (4000, one in four with a zero axis, one in
/// four with a point span, halfLength 0), both stages, and the mirror must return C#'s bits for the whole
/// SpanNoise. Each of stretched and unstretched stages, and of positive and zero fold, must be met at least
/// 500 times, or the sweep proves nothing about that branch.
/// </summary>
[Fact]
public void SpanStageMirrorMatchesCSharpBitForBitOnRealisticSpans()
{
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 (pathType, stageType) = SpanTypes(shaderType);
var spanArguments = new[] { pathType, stageType, typeof(int), typeof(float), typeof(float) };
var mirrors = new[] { ShaderFunction(shaderType, "cultmath_span_fbm", spanArguments)!, ShaderFunction(shaderType, "cultmath_span_ridged", spanArguments)! };
var random = new System.Random(0x5EED4);
float Next(float extent) => (random.NextSingle() * 2.0f - 1.0f) * extent;
int stretched = 0, unstretched = 0, folded = 0, flat = 0;
for (var i = 0; i < 4000; i++)
{
var drawn = SpanStageTests.Stage.Draw(random);
var stage = drawn.S;
if (i % 4 == 1)
stage.axis = new float3(0.0f, 0.0f, 0.0f);
var halfLength = i % 4 == 2 ? 0.0f : new[] { 0.02f, 0.1f, 0.5f, 2.0f }[random.Next(4)];
var span = SpanStageTests.Span.Draw(random, new float3(Next(30.0f), Next(30.0f), Next(30.0f)), halfLength, random.NextSingle() * 2.0f);
var arguments = new object[] { CopyFields(span.Path, pathType), CopyFields(stage, stageType), drawn.Octaves, drawn.Lacunarity, drawn.Gain };
if (dot(stage.axis, stage.axis) > 0.0f && stage.stretch != 1.0f)
stretched++;
else
unstretched++;
for (var m = 0; m < 2; m++)
{
var expected = m == 0 ? span_fbm(span.Path, stage, drawn.Octaves, drawn.Lacunarity, drawn.Gain) : span_ridged(span.Path, stage, drawn.Octaves, drawn.Lacunarity, drawn.Gain);
var actual = mirrors[m].Invoke(shader, arguments)!;
Assert.True(BitwiseEqual(Values(expected), Values(actual)),
$"{mirrors[m].Name} differs from C# at midpoint {span.Path.midpoint}, halfLength {halfLength:R}, {drawn.Octaves} octaves: C# {string.Join(" ", Values(expected).Select(v => v.Value))}, mirror {string.Join(" ", Values(actual).Select(v => v.Value))}");
if (m == 1 && expected.fold > 0.0f)
folded++;
else if (m == 1)
flat++;
}
}
Assert.True(stretched >= 500 && unstretched >= 500 && folded >= 500 && flat >= 500,
$"the sweep meets a branch too rarely: stretched {stretched}, unstretched {unstretched}, fold > 0 {folded}, fold = 0 {flat} of 4000");
}
/// <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
/// bound sits just under the 2.25 cut (Soul's F6 lesson for cellular): a mirror whose cut is 2.0
/// survives a hundred inputs. A dense sweep of realistic arguments closes that gap.
/// </summary>
[Fact]
public void PhacelleMirrorMatchesCSharpBitForBitAcrossADenseSweep()
{
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_phacelle", typeof(float3), typeof(float3), typeof(float), typeof(float))!;
var random = new System.Random(0x5EED2);
float Next(float extent) => (random.NextSingle() * 2.0f - 1.0f) * extent;
for (var i = 0; i < 20000; i++)
{
var p = new float3(Next(50.0f), Next(50.0f), Next(50.0f));
var side = new float3(Next(9.0f), Next(9.0f), Next(9.0f));
var offset = random.NextSingle();
var normalization = random.NextSingle();
var expected = Values(phacelle(p, side, offset, normalization));
var actual = Values(mirror.Invoke(shader, new object[] { p, side, offset, normalization })!);
Assert.True(BitwiseEqual(expected, actual), $"mirror differs from C# at p = {p}, side = {side}");
}
}
/// <summary>
/// The generic comparison draws centre, axis and radius from [-100, 100], where the reach is far past the
/// 0.15 or so below which <c>cultmath_af_snoise</c>'s centred form is the narrower one, so an HLSL edit
/// of the choice between the centred and the Lipschitz form (a threshold, a reordered sum) survives it.
/// This sweep draws realistic slices (axis and radius log-uniform in 1e-3 to 0.5, the reach of a tile
/// near a camera), which land on both sides of the choice and next to it, for <c>cultmath_af_snoise</c>
/// and <c>cultmath_af_fbm</c>.
/// </summary>
[Fact]
public void AffineSnoiseMirrorMatchesCSharpBitForBitOnBothSidesOfItsBranch()
{
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 snoiseMirror = ShaderFunction(shaderType, "cultmath_af_snoise", typeof(float3), typeof(float3), typeof(float))!;
var fbmMirror = ShaderFunction(shaderType, "cultmath_af_fbm", typeof(float3), typeof(float3), typeof(float), typeof(int), typeof(float), typeof(float))!;
var random = new System.Random(0x5EED3);
float Next(float extent) => (random.NextSingle() * 2.0f - 1.0f) * extent;
float Reach() => 1.0e-3f * MathF.Pow(500.0f, random.NextSingle());
int centred = 0, lipschitz = 0;
for (var i = 0; i < 20000; i++)
{
var centre = new float3(Next(50.0f), Next(50.0f), Next(50.0f));
var axis = normalize(new float3(Next(1.0f), Next(1.0f), Next(1.0f))) * Reach();
var radius = Reach();
var expected = af_snoise(centre, axis, radius);
var actual = Values(snoiseMirror.Invoke(shader, new object[] { centre, axis, radius })!);
Assert.True(BitwiseEqual(Values(expected), actual), $"cultmath_af_snoise differs from C# at centre = {centre}, axis = {axis}, radius = {radius}");
if (expected.y != 0.0f)
centred++;
else
lipschitz++;
if (i % 8 == 0)
{
var fbmExpected = Values(af_fbm(centre, axis, radius, 4, 2.0f, 0.5f));
var fbmActual = Values(fbmMirror.Invoke(shader, new object[] { centre, axis, radius, 4, 2.0f, 0.5f })!);
Assert.True(BitwiseEqual(fbmExpected, fbmActual), $"cultmath_af_fbm differs from C# at centre = {centre}, axis = {axis}, radius = {radius}");
}
}
Assert.True(centred > 2000 && lipschitz > 2000, $"the sweep meets one branch too rarely: centred {centred}, Lipschitz {lipschitz} of 20000");
}
/// <summary>
/// The generic comparison stops at 1e7, so an HLSL edit of <c>cultmath_af_range</c>'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.
/// </summary>
[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");
}
}
/// <summary>
/// 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
/// to such cells (<see cref="PhacelleTests.PruneCutPoints"/>), so the mirror must match C# bit for
/// bit, and must match the unpruned C# sum (the shader has no prune switch, so that is the
/// independent statement that the cut is exact).
/// </summary>
[Fact]
public void PhacelleMirrorMatchesCSharpBitForBitNextToCellsJustInsideTheCut()
{
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_phacelle", typeof(float3), typeof(float3), typeof(float), typeof(float))!;
foreach (var p in PhacelleTests.PruneCutPoints)
{
Assert.True(PhacelleTests.HasCellJustInsideTheCut(p), $"no cell just inside the cut at {p}");
var actual = Values(mirror.Invoke(shader, new object[] { p, PhacelleTests.PruneCutSide, PhacelleTests.PruneCutOffset, PhacelleTests.PruneCutNormalization })!);
var pruned = Values(phacelle(p, PhacelleTests.PruneCutSide, PhacelleTests.PruneCutOffset, PhacelleTests.PruneCutNormalization));
var unpruned = Values(phacelle(p, PhacelleTests.PruneCutSide, PhacelleTests.PruneCutOffset, PhacelleTests.PruneCutNormalization, false));
Assert.True(BitwiseEqual(pruned, actual), $"mirror differs from C# at p = {p}");
Assert.True(BitwiseEqual(unpruned, actual), $"mirror differs from the unpruned C# sum at p = {p}");
}
}
/// <summary>
/// A hand-built pair of structs standing in for a mirror struct return and its C# counterpart
/// (invariant 8's one struct return shape, e.g. <see cref="CultCellular"/>): same field shape,
/// different concrete type, exactly like the real mirror comparison. Proves the field-by-field,
/// bit-for-bit walk actually rejects a struct whose field differs; without this, the walk could
/// have been comparing lengths only, or skipping the nested vector's own components.
/// </summary>
private struct StructMirrorProbeExpected { public float4 first; public float second; }
private struct StructMirrorProbeActual { public float4 first; public float second; }
[Fact]
public void StructReturnComparisonRejectsAMismatchedField()
{
var expected = new StructMirrorProbeExpected { first = new float4(1.0f, 2.0f, 3.0f, 4.0f), second = 5.0f };
var matching = new StructMirrorProbeActual { first = new float4(1.0f, 2.0f, 3.0f, 4.0f), second = 5.0f };
var mismatchedLeaf = new StructMirrorProbeActual { first = new float4(1.0f, 2.0f, 3.0f, 4.0f), second = 5.5f };
var mismatchedNestedComponent = new StructMirrorProbeActual { first = new float4(1.0f, 2.0f, -3.0f, 4.0f), second = 5.0f };
Assert.True(BitwiseEqual(Values(expected), Values(matching)));
Assert.False(BitwiseEqual(Values(expected), Values(mismatchedLeaf)));
Assert.False(BitwiseEqual(Values(expected), Values(mismatchedNestedComponent)));
}
/// <summary>
/// Struct shapes standing in for CultCellular's own field order (<c>nearest; edge; id</c>), used
/// to falsify two named mutants (F2): H2 declares the mirror struct with its first two fields
/// swapped (<c>edge; nearest; id</c>) and swaps the assignments to match, so every leaf VALUE
/// still lands in the same POSITION as the correct struct; only the field NAME at each position
/// differs. H3 renames and retypes every field (id becomes an int carrying the same bits) while
/// keeping the original position order, so only NAME and TYPE differ, never position.
/// </summary>
private struct NearestEdgeIdExpected { public float4 nearest; public float4 edge; public float id; }
private struct EdgeNearestIdSwappedNames { public float4 edge; public float4 nearest; public float id; }
private struct RenamedAndRetypedFields { public float4 a; public float4 b; public int c; }
[Fact]
public void StructReturnComparisonRejectsFieldsSwappedByNameEvenWhenValuesAlign()
{
// H2: the mutant struct's field ORDER is (edge, nearest, id), and its assignments are
// swapped to match, so declaration-position values are identical to the correct struct's.
// Only checking names (not just position) can tell these apart.
var expected = new NearestEdgeIdExpected
{
nearest = new float4(1.0f, 2.0f, 3.0f, 4.0f),
edge = new float4(5.0f, 6.0f, 7.0f, 8.0f),
id = 9.0f,
};
var swapped = new EdgeNearestIdSwappedNames
{
edge = new float4(1.0f, 2.0f, 3.0f, 4.0f), // holds what should be "nearest"'s value.
nearest = new float4(5.0f, 6.0f, 7.0f, 8.0f), // holds what should be "edge"'s value.
id = 9.0f,
};
Assert.False(BitwiseEqual(Values(expected), Values(swapped)));
}
[Fact]
public void StructReturnComparisonRejectsRenamedAndRetypedFields()
{
// H3: field order matches the correct struct exactly, but every field is renamed, and id is
// retyped from float to int carrying the same bit pattern the correct id would round to.
var expected = new NearestEdgeIdExpected
{
nearest = new float4(1.0f, 2.0f, 3.0f, 4.0f),
edge = new float4(5.0f, 6.0f, 7.0f, 8.0f),
id = 9.0f,
};
var renamed = new RenamedAndRetypedFields
{
a = new float4(1.0f, 2.0f, 3.0f, 4.0f),
b = new float4(5.0f, 6.0f, 7.0f, 8.0f),
c = BitConverter.SingleToInt32Bits(9.0f),
};
Assert.False(BitwiseEqual(Values(expected), Values(renamed)));
}
// Integer arguments take the bit pattern of the float input, so specials become 0, 0x80000000, NaN bits, ...
// A bool argument is true when its float input is positive.
private static readonly Type[] Scalars = { typeof(float), typeof(int), typeof(uint), typeof(bool) };
private static object Scalar(Type t, float f) => t == typeof(float) ? f
: t == typeof(bool) ? f > 0.0f
: t == typeof(int) ? BitConverter.SingleToInt32Bits(f) : (object)(uint)BitConverter.SingleToInt32Bits(f);
private static FieldInfo[] InstanceFields(Type t) => t.GetFields(BindingFlags.Public | BindingFlags.Instance);
// One function ARGUMENT from the case's scalars, in field order: a scalar from its float, a vector by the
// constructor taking its scalar fields, a struct field by field, recursively.
private static object Build(Type t, Func<float> next)
{
if (Scalars.Contains(t))
return Scalar(t, next());
var fields = InstanceFields(t);
var constructor = t.GetConstructor(fields.Select(f => f.FieldType).ToArray());
if (constructor is not null)
return constructor.Invoke(fields.Select(f => Build(f.FieldType, next)).ToArray());
var value = Activator.CreateInstance(t)!;
foreach (var field in fields)
field.SetValue(value, Build(field.FieldType, next));
return value;
}
// The CultMath type a mirror parameter type stands for: itself, or for a type nested in HlslShader the
// CultMath type of the same name, only if both have the same instance fields by name, type (mapped
// likewise) and order. Null when there is no such twin.
private static Type? TwinType(Type shaderType, Type mirror)
{
if (mirror.DeclaringType != shaderType)
return mirror;
var twin = typeof(math).Assembly.GetType("CultMath." + mirror.Name);
if (twin is null)
return null;
var mine = InstanceFields(mirror);
var theirs = InstanceFields(twin);
return mine.Length == theirs.Length && mine.Zip(theirs).All(p => p.First.Name == p.Second.Name && TwinType(shaderType, p.First.FieldType) == p.Second.FieldType) ? twin : null;
}
// The math function of the mirror function's name, with its parameter types mapped, public or private.
private static MethodInfo? Twin(Type shaderType, string name, Type[] mirrorTypes)
{
var types = mirrorTypes.Select(t => TwinType(shaderType, t)).ToArray();
return types.Any(t => t is null) ? null
: typeof(math).GetMethod(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, null, types!, null);
}
// A struct value as another struct type of the same fields by name: itself when the type is the same.
private static object CopyFields(object source, Type target)
{
if (source.GetType() == target)
return source;
var copy = Activator.CreateInstance(target)!;
foreach (var field in InstanceFields(target))
field.SetValue(copy, CopyFields(source.GetType().GetField(field.Name)!.GetValue(source)!, field.FieldType));
return copy;
}
private static object FieldOf(object value, string name) => value.GetType().GetField(name)!.GetValue(value)!;
/// <summary>
/// One flattened scalar leaf of a (possibly nested) return value: its declaration path (field
/// names joined by '.'), its declared type, and its value. Carrying path and type, not just the
/// value, is what lets the struct-return comparison reject a field that was renamed or retyped
/// but landed in the same position (F2): a positional-only comparison cannot tell "edge" holding
/// nearest's value apart from "nearest" holding it.
/// </summary>
private readonly record struct Leaf(string Path, Type Type, object Value);
/// <summary>
/// Flattens a return value to its scalar leaves, recursing through struct fields of any type
/// (invariant 8's one struct return shape, e.g. <see cref="CultCellular"/>'s float4/float4/float
/// fields) so a struct return is compared field by field, down to bit-for-bit scalars, exactly
/// like a bare vector return already is. Each leaf keeps the field name (and its ancestors') and
/// declared type it was read from.
/// </summary>
private static Leaf[] Values(object value, string path = "") => Scalars.Contains(value.GetType())
? new[] { new Leaf(path, value.GetType(), value) }
: value is float3x3 matrix
? new[] { matrix._m00, matrix._m01, matrix._m02, matrix._m10, matrix._m11, matrix._m12, matrix._m20, matrix._m21, matrix._m22 }
.Select((entry, k) => new Leaf((path.Length == 0 ? "" : path + ".") + "_m" + k / 3 + k % 3, typeof(float), entry)).ToArray()
: value.GetType().GetFields().Where(f => !f.IsStatic)
.SelectMany(f => Values(f.GetValue(value)!, path.Length == 0 ? f.Name : path + "." + f.Name))
.ToArray();
// Compares leaves by NAME and TYPE as well as value, in declaration order, so a mirror struct
// whose fields are reordered, renamed, or retyped relative to its C# counterpart fails here even
// when the flattened values happen to line up positionally (F2's H2/H3 mutants).
private static bool BitwiseEqual(Leaf[] expected, Leaf[] actual) =>
expected.Length == actual.Length && expected.Zip(actual).All(p =>
p.First.Path == p.Second.Path && p.First.Type == p.Second.Type &&
(p.First.Value is float a && p.Second.Value is float b
? (float.IsNaN(a) && float.IsNaN(b)) || BitConverter.SingleToInt32Bits(a) == BitConverter.SingleToInt32Bits(b)
: p.First.Value.Equals(p.Second.Value)));
/// <summary>
/// The text of <c>CultMath.hlsl</c> with each <c>#include "name"</c> replaced in place by the
/// included file from the same directory, as the shader compiler would resolve it. Only the
/// package's own includes exist, so a missing file is an error rather than a skip. An include for
/// which <paramref name="inline"/> returns false is dropped with its line instead (the GLSL
/// lowering's separately licensed files); the C# mirror inlines every one.
/// </summary>
internal static string ReadShaderSource(string cultMathRoot, Func<string, bool>? inline = null)
{
var shaders = Path.Combine(cultMathRoot, "shaders");
return Regex.Replace(File.ReadAllText(Path.Combine(shaders, "CultMath.hlsl")), @"(?m)^#include ""([^""]+)""[ \t]*\r?$(\n?)",
match => inline is null || inline(match.Groups[1].Value)
? File.ReadAllText(Path.Combine(shaders, match.Groups[1].Value)) + match.Groups[2].Value
: string.Empty);
}
/// <summary>
/// The text without its include-guard lines. A capacity macro (<c>#ifndef X</c>, <c>#define X n</c>, <c>#endif</c>, a
/// consumer-overridable default) is not a guard: <paramref name="capacity"/> gives the text it becomes, from its name and value.
/// </summary>
internal static string DropGuards(string source, Func<string, string, string> capacity) =>
Regex.Replace(source, @"(?m)^#ifndef (CULTMATH_MAX_\w+)\r?\n#define \1 (\d+)\r?\n#endif[ \t]*$|^[ \t]*#(ifndef|define|endif)\b.*$",
match => match.Groups[1].Success ? capacity(match.Groups[1].Value, match.Groups[2].Value) : string.Empty);
/// <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 = "", IReadOnlyDictionary<string, int>? defines = null)
{
// 1. Include guards (#ifndef/#define/#endif) are dropped; C# has no textual include. A capacity macro
// (#ifndef X, #define X n, #endif) becomes `const int X = n;`, or the value a test defines for X.
var source = DropGuards(hlsl, (name, value) => $"const int {name} = {(defines is not null && defines.TryGetValue(name, out var defined) ? defined : value)};");
// 2. Functions taking Texture2D/SamplerState resources are dropped; GPU resources have
// no CultMath analog.
source = Regex.Replace(source, @"(?ms)^\w+ \w+\([^)]*\b(Texture2D|SamplerState)\b[^)]*\)\s*\{.*?^\}[ \t]*\r?$", string.Empty);
// 3. File-scope `static const` becomes `const` (C# consts are implicitly static).
source = Regex.Replace(source, @"(?m)^static const ", "const ");
// 4. Floating literals gain the `f` suffix; an unsuffixed C# literal is a double.
source = Regex.Replace(source, @"(?<![\w.])(\d+\.\d*(?:[eE][+-]?\d+)?|\d+[eE][+-]?\d+)(?![\w.])", "$1f");
// 5. Struct fields gain `public`; HLSL has no field access-modifier concept, and a C# struct's
// fields default to private, which would hide them from the mirror test's reflection-based
// field comparison (invariant 8's one struct return shape, e.g. CultCellular) and the struct
// arguments' copies (SpanPath, SpanStage), which reflection reads and writes by field name.
source = Regex.Replace(source, @"(?ms)(struct\s+\w+\s*\{)(.*?)(\};)", m =>
m.Groups[1].Value + Regex.Replace(m.Groups[2].Value, @"(?m)^(\s*)(\w+\s+\w+;)", "$1public $2") + m.Groups[3].Value);
// 6. An array parameter `T name[N]` becomes `T[] name`; the mirror takes the array whole and the count beside it.
source = Regex.Replace(source, @"\b(\w+) (\w+)\[CULTMATH_MAX_\w+\]", "$1[] $2");
// 7. 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" + members + source + "\n}\n";
}
internal static (Assembly? Assembly, IReadOnlyList<string> Errors) CompileShaderMirror(string members = "", IReadOnlyDictionary<string, int>? defines = null, Func<string, string>? mutate = null)
{
var hlsl = ReadShaderSource(FindCultMathRoot());
if (mutate is not null)
hlsl = mutate(hlsl);
var tree = CSharpSyntaxTree.ParseText(TransformHlslToCSharp(hlsl, members, defines));
var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)
.Split(Path.PathSeparator)
.Append(typeof(math).Assembly.Location)
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(path => MetadataReference.CreateFromFile(path));
var compilation = CSharpCompilation.Create(
"CultMathHlslMirror",
new[] { tree },
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
using var stream = new MemoryStream();
var result = compilation.Emit(stream);
var errors = result.Diagnostics
.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.Select(diagnostic => diagnostic.ToString())
.ToList();
return (result.Success ? Assembly.Load(stream.ToArray()) : null, errors);
}
internal static string FindCultMathRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "packages", "cultmath", "shaders", "CultMath.hlsl")))
{
directory = directory.Parent;
}
return Path.Combine(directory?.FullName ?? throw new InvalidOperationException("CultMath package root not found."), "packages", "cultmath");
}
// ---- Hand-written HLSL-shaped host code: swizzles, mixed constructors, matrix rows. ----
static float3 shade(float3 position, float3 normal, float2 wind, float time)
{
float3 p = position;
p.xz = p.xz + wind * time;
p.y = saturate(p.y);
float3x3 basis = float3x3(float3(1, 0, 0), float3(0, 1, 0), float3(0, 0, 1));
basis[1][1] = 1.0f;
float3 n = normalize(mul(basis, normal));
float ndotl = saturate(dot(n, normalize(float3(0.3f, 1, 0.2f))));
float4 color = float4(lerp(float3(0.1f, 0.1f, 0.2f), float3(1, 0.9f, 0.7f), ndotl), 1);
if (any(p < 0.0f))
color.rgb *= 0.5f;
if (all(abs(n) <= 1.0f))
color.bgr = color.bgr * float3(1, 1, 1);
color.rgb = select(p > 10.0f, float3(0), color.rgb);
float2 uv = frac(p.xz * 0.25f);
color.rg = lerp(color.rg, uv, 0.1f);
return float4(color.xy, color.zw).xyz;
}
static float2 steer(float2 direction, float torque)
{
float2x2 turn = float2x2(cos(torque), -sin(torque), sin(torque), cos(torque));
float2 turned = mul(direction, turn);
return normalize(turned) * pow(length(turned), 1.0f);
}
[Fact]
public void HandWrittenShaderStyleMathEvaluates()
{
var color = shade(float3(1.0f, 0.5f, 2.0f), float3(0.0f, 1.0f, 0.0f), float2(1.0f, 0.0f), 0.0f);
Assert.True(color.x > 0.1f && color.x <= 1.0f);
Assert.True(float.IsFinite(color.y) && float.IsFinite(color.z));
var far = shade(float3(20.0f, 20.0f, 20.0f), float3(0.0f, 1.0f, 0.0f), float2(0.0f), 0.0f);
Assert.Equal(0.0f, far.z);
var steered = steer(float2(1.0f, 0.0f), 0.5f);
var viaRotate = mul(float2(1.0f, 0.0f), CultMath.float2x2.Rotate(0.5f));
Assert.Equal(viaRotate.x, steered.x, precision: 5);
Assert.Equal(viaRotate.y, steered.y, precision: 5);
}
}