CultLib/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs

868 lines
53 KiB
C#
Raw Normal View History

using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
2026-09-14 13:29:59 +02:00
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.
2026-09-14 13:29:59 +02:00
/// </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)));
2026-09-29 23:51:02 +02:00
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]);
}
tests: pin cellular's HLSL mirror against radius-2, 2^24, and tie inputs EveryMirrorFunctionMatchesCSharpMath's generic random/special-value cases never happen to hit a point where a radius-2 cell wins (about 3.5e-5 of points) or an integer point at or past 2^23, so an HLSL mutant of cellular's search radius, its lower-bound prune, or the removed F2=0 guard could change cultmath_cellular's real output and still pass this test. Adds three targeted point cases, reusing CellularAndSminGradTests' own fixtures so the mirror test does not carry a second copy of the construction: - the six points from AxisAlignedRadiusTwoOffsets/FindPointRealizingOffset, each of which realizes a different axis-aligned radius-2 offset as F1 or F2 in the independent 7x7x7 oracle; - four integer points at 2^24 (both signs), where distinct cells' jittered features can round onto the same float32 value, reaching the F2 = 0 guard; - one exact F1 = F2 tie point, the midpoint of two adjacent cells' feature points (bit-exact-equidistant from both by construction, no search needed). Verified against four HLSL mutants applied to shaders/CultMath.hlsl on scratch copies (not committed) and reverted after each check: - `dz < 2` instead of `dz <= 2`: killed by the axis-slice point at (-56.671272, -67.01973, -79.001). - `dx` starting at -1 instead of -2: killed by the axis-slice point at (62, -77.91331, -79.42451). - pruning against f1 instead of f2: killed by an axis-slice point and both of the 2^24 integer points (3 mismatches). - the F2 = 0 guard removed (`grad2 = (p - c2) / f2`, unconditional): killed by the (2^24, 2^24, 2^24) integer point (NaN vs the guarded zero). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 23:14:10 +02:00
// 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())!);
Add PCG integer hashes to CultMath and release 0.2.3 CultMath's hash is the shader-style frac(sin(x) * 43758.5453) and returns float. Unity.Mathematics' hash returned a well-mixed uint, and seeding from the float hash collapses: a 100x100 position grid gave 1625 distinct values in Aetheria's zone generator. - math.pcg(uint), pcg3d(int3/float2/float3), and pcg4d(int4/float4) follow Jarzynski and Olano, "Hash Functions for GPU Rendering" (JCGT 9(3), 2020). pcg3d and pcg4d are the paper's code from page 32. pcg is O'Neill's RXS-M-XS 32/32 output over one LCG step, with the constants checked against pcg-c's pcg_variants.h. - CultMath has no uint vectors, so pcg3d and pcg4d carry uint bit patterns in int3/int4. The float overloads hash IEEE-754 bits via the HLSL intrinsic asuint(float), which is new. pcg3d(float2) holds z at 0, the paper's route for 2 -> N inputs. - The HLSL mirror gains the same functions, written over scalar uints so the text compiles both in dxc 1.9.0.5402 (lib_6_3, HV 2021, clean) and in C#. HlslSourceCompatibilityTests now compares int and uint arguments and results by value; a wrong shift in the mirror's pcg4d gives 208 mismatches. - Known-answer vectors come from an independent Python transcription. pcg3d separates a 100x100 half-unit grid (the full int3). One component alone has one birthday collision there, as a 32-bit value can. The same grid on the integer lattice has none. - design.md states that the hashes are integer-exact in HLSL and on the CPU: dxc wraps uint multiply and add modulo 2^32, as C# unchecked does. - Bump to 0.2.3 and rebuild the tracked Unity DLL and pdb. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:53 +02:00
var actual = Values(mirror.Invoke(shader, args)!);
if (!BitwiseEqual(expected, actual))
{
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0 Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of the first landing): - cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not 3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both are always < sqrt(3), every excluded cell is always > 2). A cheap position-only lower-bound prune skips the pcg3d/jitter/distance work for cells that provably cannot beat the running F2, without changing any output bit (F6). - F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had, fixing a NaN that was reachable past |p| of about 2^23 (F3). - id now comes from pcg4d over the winning cell alone, never from the pcg3d hash that produced that cell's jitter, so id and jitter are no longer deterministic functions of each other (F4). - cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so the mapping is always inside [0, 1); the old formula rounded 0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7). - Central-difference tolerances tightened from 0.02 to 3e-4 (measured float noise at the committed eps is about 6e-5), and cellular's own gradient test now differentiates cellular's RETURNED .w values instead of a duplicate FindF1F2 search, so a mutant that corrupts only the returned gradient can't hide behind an independently-computed value (F1, F5). - The struct-mirror comparison in HlslSourceCompatibilityTests now checks field name and declared type in declaration order, not just position, so a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one (H3) both fail the walk instead of surviving on values alone (F2). - FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests pin production's exact agreement with it, the pruned search's bit-for-bit equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's correlation with jitter under 0.05. - Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not cubic. Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay byte-identical. Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id from hash.y) all die against these tests; H2/H3 struct mutants die via the new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile of a scratch kernel using both cultmath_smin_grad and cultmath_cellular succeeds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
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)));
}
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0 Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of the first landing): - cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not 3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both are always < sqrt(3), every excluded cell is always > 2). A cheap position-only lower-bound prune skips the pcg3d/jitter/distance work for cells that provably cannot beat the running F2, without changing any output bit (F6). - F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had, fixing a NaN that was reachable past |p| of about 2^23 (F3). - id now comes from pcg4d over the winning cell alone, never from the pcg3d hash that produced that cell's jitter, so id and jitter are no longer deterministic functions of each other (F4). - cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so the mapping is always inside [0, 1); the old formula rounded 0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7). - Central-difference tolerances tightened from 0.02 to 3e-4 (measured float noise at the committed eps is about 6e-5), and cellular's own gradient test now differentiates cellular's RETURNED .w values instead of a duplicate FindF1F2 search, so a mutant that corrupts only the returned gradient can't hide behind an independently-computed value (F1, F5). - The struct-mirror comparison in HlslSourceCompatibilityTests now checks field name and declared type in declaration order, not just position, so a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one (H3) both fail the walk instead of surviving on values alone (F2). - FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests pin production's exact agreement with it, the pruned search's bit-for-bit equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's correlation with jitter under 0.05. - Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not cubic. Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay byte-identical. Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id from hash.y) all die against these tests; H2/H3 struct mutants die via the new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile of a scratch kernel using both cultmath_smin_grad and cultmath_cellular succeeds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
/// <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)));
}
Add PCG integer hashes to CultMath and release 0.2.3 CultMath's hash is the shader-style frac(sin(x) * 43758.5453) and returns float. Unity.Mathematics' hash returned a well-mixed uint, and seeding from the float hash collapses: a 100x100 position grid gave 1625 distinct values in Aetheria's zone generator. - math.pcg(uint), pcg3d(int3/float2/float3), and pcg4d(int4/float4) follow Jarzynski and Olano, "Hash Functions for GPU Rendering" (JCGT 9(3), 2020). pcg3d and pcg4d are the paper's code from page 32. pcg is O'Neill's RXS-M-XS 32/32 output over one LCG step, with the constants checked against pcg-c's pcg_variants.h. - CultMath has no uint vectors, so pcg3d and pcg4d carry uint bit patterns in int3/int4. The float overloads hash IEEE-754 bits via the HLSL intrinsic asuint(float), which is new. pcg3d(float2) holds z at 0, the paper's route for 2 -> N inputs. - The HLSL mirror gains the same functions, written over scalar uints so the text compiles both in dxc 1.9.0.5402 (lib_6_3, HV 2021, clean) and in C#. HlslSourceCompatibilityTests now compares int and uint arguments and results by value; a wrong shift in the mirror's pcg4d gives 208 mismatches. - Known-answer vectors come from an independent Python transcription. pcg3d separates a 100x100 half-unit grid (the full int3). One component alone has one birthday collision there, as a 32-bit value can. The same grid on the integer lattice has none. - design.md states that the hashes are integer-exact in HLSL and on the CPU: dxc wraps uint multiply and add modulo 2^32, as C# unchecked does. - Bump to 0.2.3 and rebuild the tracked Unity DLL and pdb. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:53 +02:00
// 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) };
Add PCG integer hashes to CultMath and release 0.2.3 CultMath's hash is the shader-style frac(sin(x) * 43758.5453) and returns float. Unity.Mathematics' hash returned a well-mixed uint, and seeding from the float hash collapses: a 100x100 position grid gave 1625 distinct values in Aetheria's zone generator. - math.pcg(uint), pcg3d(int3/float2/float3), and pcg4d(int4/float4) follow Jarzynski and Olano, "Hash Functions for GPU Rendering" (JCGT 9(3), 2020). pcg3d and pcg4d are the paper's code from page 32. pcg is O'Neill's RXS-M-XS 32/32 output over one LCG step, with the constants checked against pcg-c's pcg_variants.h. - CultMath has no uint vectors, so pcg3d and pcg4d carry uint bit patterns in int3/int4. The float overloads hash IEEE-754 bits via the HLSL intrinsic asuint(float), which is new. pcg3d(float2) holds z at 0, the paper's route for 2 -> N inputs. - The HLSL mirror gains the same functions, written over scalar uints so the text compiles both in dxc 1.9.0.5402 (lib_6_3, HV 2021, clean) and in C#. HlslSourceCompatibilityTests now compares int and uint arguments and results by value; a wrong shift in the mirror's pcg4d gives 208 mismatches. - Known-answer vectors come from an independent Python transcription. pcg3d separates a 100x100 half-unit grid (the full int3). One component alone has one birthday collision there, as a 32-bit value can. The same grid on the integer lattice has none. - design.md states that the hashes are integer-exact in HLSL and on the CPU: dxc wraps uint multiply and add modulo 2^32, as C# unchecked does. - Bump to 0.2.3 and rebuild the tracked Unity DLL and pdb. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:53 +02:00
private static object Scalar(Type t, float f) => t == typeof(float) ? f
: t == typeof(bool) ? f > 0.0f
Add PCG integer hashes to CultMath and release 0.2.3 CultMath's hash is the shader-style frac(sin(x) * 43758.5453) and returns float. Unity.Mathematics' hash returned a well-mixed uint, and seeding from the float hash collapses: a 100x100 position grid gave 1625 distinct values in Aetheria's zone generator. - math.pcg(uint), pcg3d(int3/float2/float3), and pcg4d(int4/float4) follow Jarzynski and Olano, "Hash Functions for GPU Rendering" (JCGT 9(3), 2020). pcg3d and pcg4d are the paper's code from page 32. pcg is O'Neill's RXS-M-XS 32/32 output over one LCG step, with the constants checked against pcg-c's pcg_variants.h. - CultMath has no uint vectors, so pcg3d and pcg4d carry uint bit patterns in int3/int4. The float overloads hash IEEE-754 bits via the HLSL intrinsic asuint(float), which is new. pcg3d(float2) holds z at 0, the paper's route for 2 -> N inputs. - The HLSL mirror gains the same functions, written over scalar uints so the text compiles both in dxc 1.9.0.5402 (lib_6_3, HV 2021, clean) and in C#. HlslSourceCompatibilityTests now compares int and uint arguments and results by value; a wrong shift in the mirror's pcg4d gives 208 mismatches. - Known-answer vectors come from an independent Python transcription. pcg3d separates a 100x100 half-unit grid (the full int3). One component alone has one birthday collision there, as a 32-bit value can. The same grid on the integer lattice has none. - design.md states that the hashes are integer-exact in HLSL and on the CPU: dxc wraps uint multiply and add modulo 2^32, as C# unchecked does. - Bump to 0.2.3 and rebuild the tracked Unity DLL and pdb. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:53 +02:00
: 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)!;
Add PCG integer hashes to CultMath and release 0.2.3 CultMath's hash is the shader-style frac(sin(x) * 43758.5453) and returns float. Unity.Mathematics' hash returned a well-mixed uint, and seeding from the float hash collapses: a 100x100 position grid gave 1625 distinct values in Aetheria's zone generator. - math.pcg(uint), pcg3d(int3/float2/float3), and pcg4d(int4/float4) follow Jarzynski and Olano, "Hash Functions for GPU Rendering" (JCGT 9(3), 2020). pcg3d and pcg4d are the paper's code from page 32. pcg is O'Neill's RXS-M-XS 32/32 output over one LCG step, with the constants checked against pcg-c's pcg_variants.h. - CultMath has no uint vectors, so pcg3d and pcg4d carry uint bit patterns in int3/int4. The float overloads hash IEEE-754 bits via the HLSL intrinsic asuint(float), which is new. pcg3d(float2) holds z at 0, the paper's route for 2 -> N inputs. - The HLSL mirror gains the same functions, written over scalar uints so the text compiles both in dxc 1.9.0.5402 (lib_6_3, HV 2021, clean) and in C#. HlslSourceCompatibilityTests now compares int and uint arguments and results by value; a wrong shift in the mirror's pcg4d gives 208 mismatches. - Known-answer vectors come from an independent Python transcription. pcg3d separates a 100x100 half-unit grid (the full int3). One component alone has one birthday collision there, as a 32-bit value can. The same grid on the integer lattice has none. - design.md states that the hashes are integer-exact in HLSL and on the CPU: dxc wraps uint multiply and add modulo 2^32, as C# unchecked does. - Bump to 0.2.3 and rebuild the tracked Unity DLL and pdb. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:53 +02:00
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0 Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of the first landing): - cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not 3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both are always < sqrt(3), every excluded cell is always > 2). A cheap position-only lower-bound prune skips the pcg3d/jitter/distance work for cells that provably cannot beat the running F2, without changing any output bit (F6). - F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had, fixing a NaN that was reachable past |p| of about 2^23 (F3). - id now comes from pcg4d over the winning cell alone, never from the pcg3d hash that produced that cell's jitter, so id and jitter are no longer deterministic functions of each other (F4). - cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so the mapping is always inside [0, 1); the old formula rounded 0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7). - Central-difference tolerances tightened from 0.02 to 3e-4 (measured float noise at the committed eps is about 6e-5), and cellular's own gradient test now differentiates cellular's RETURNED .w values instead of a duplicate FindF1F2 search, so a mutant that corrupts only the returned gradient can't hide behind an independently-computed value (F1, F5). - The struct-mirror comparison in HlslSourceCompatibilityTests now checks field name and declared type in declaration order, not just position, so a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one (H3) both fail the walk instead of surviving on values alone (F2). - FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests pin production's exact agreement with it, the pruned search's bit-for-bit equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's correlation with jitter under 0.05. - Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not cubic. Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay byte-identical. Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id from hash.y) all die against these tests; H2/H3 struct mutants die via the new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile of a scratch kernel using both cultmath_smin_grad and cultmath_cellular succeeds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
/// <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
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0 Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of the first landing): - cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not 3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both are always < sqrt(3), every excluded cell is always > 2). A cheap position-only lower-bound prune skips the pcg3d/jitter/distance work for cells that provably cannot beat the running F2, without changing any output bit (F6). - F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had, fixing a NaN that was reachable past |p| of about 2^23 (F3). - id now comes from pcg4d over the winning cell alone, never from the pcg3d hash that produced that cell's jitter, so id and jitter are no longer deterministic functions of each other (F4). - cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so the mapping is always inside [0, 1); the old formula rounded 0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7). - Central-difference tolerances tightened from 0.02 to 3e-4 (measured float noise at the committed eps is about 6e-5), and cellular's own gradient test now differentiates cellular's RETURNED .w values instead of a duplicate FindF1F2 search, so a mutant that corrupts only the returned gradient can't hide behind an independently-computed value (F1, F5). - The struct-mirror comparison in HlslSourceCompatibilityTests now checks field name and declared type in declaration order, not just position, so a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one (H3) both fail the walk instead of surviving on values alone (F2). - FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests pin production's exact agreement with it, the pruned search's bit-for-bit equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's correlation with jitter under 0.05. - Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not cubic. Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay byte-identical. Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id from hash.y) all die against these tests; H2/H3 struct mutants die via the new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile of a scratch kernel using both cultmath_smin_grad and cultmath_cellular succeeds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
/// like a bare vector return already is. Each leaf keeps the field name (and its ancestors') and
/// declared type it was read from.
/// </summary>
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0 Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of the first landing): - cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not 3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both are always < sqrt(3), every excluded cell is always > 2). A cheap position-only lower-bound prune skips the pcg3d/jitter/distance work for cells that provably cannot beat the running F2, without changing any output bit (F6). - F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had, fixing a NaN that was reachable past |p| of about 2^23 (F3). - id now comes from pcg4d over the winning cell alone, never from the pcg3d hash that produced that cell's jitter, so id and jitter are no longer deterministic functions of each other (F4). - cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so the mapping is always inside [0, 1); the old formula rounded 0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7). - Central-difference tolerances tightened from 0.02 to 3e-4 (measured float noise at the committed eps is about 6e-5), and cellular's own gradient test now differentiates cellular's RETURNED .w values instead of a duplicate FindF1F2 search, so a mutant that corrupts only the returned gradient can't hide behind an independently-computed value (F1, F5). - The struct-mirror comparison in HlslSourceCompatibilityTests now checks field name and declared type in declaration order, not just position, so a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one (H3) both fail the walk instead of surviving on values alone (F2). - FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests pin production's exact agreement with it, the pruned search's bit-for-bit equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's correlation with jitter under 0.05. - Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not cubic. Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay byte-identical. Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id from hash.y) all die against these tests; H2/H3 struct mutants die via the new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile of a scratch kernel using both cultmath_smin_grad and cultmath_cellular succeeds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
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()
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0 Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of the first landing): - cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not 3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both are always < sqrt(3), every excluded cell is always > 2). A cheap position-only lower-bound prune skips the pcg3d/jitter/distance work for cells that provably cannot beat the running F2, without changing any output bit (F6). - F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had, fixing a NaN that was reachable past |p| of about 2^23 (F3). - id now comes from pcg4d over the winning cell alone, never from the pcg3d hash that produced that cell's jitter, so id and jitter are no longer deterministic functions of each other (F4). - cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so the mapping is always inside [0, 1); the old formula rounded 0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7). - Central-difference tolerances tightened from 0.02 to 3e-4 (measured float noise at the committed eps is about 6e-5), and cellular's own gradient test now differentiates cellular's RETURNED .w values instead of a duplicate FindF1F2 search, so a mutant that corrupts only the returned gradient can't hide behind an independently-computed value (F1, F5). - The struct-mirror comparison in HlslSourceCompatibilityTests now checks field name and declared type in declaration order, not just position, so a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one (H3) both fail the walk instead of surviving on values alone (F2). - FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests pin production's exact agreement with it, the pruned search's bit-for-bit equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's correlation with jitter under 0.05. - Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not cubic. Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay byte-identical. Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id from hash.y) all die against these tests; H2/H3 struct mutants die via the new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile of a scratch kernel using both cultmath_smin_grad and cultmath_cellular succeeds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:40:28 +02:00
: 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. ----
2026-09-14 13:29:59 +02:00
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;
2026-09-14 13:29:59 +02:00
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);
2026-09-14 13:29:59 +02:00
if (any(p < 0.0f))
color.rgb *= 0.5f;
2026-09-14 13:29:59 +02:00
if (all(abs(n) <= 1.0f))
color.bgr = color.bgr * float3(1, 1, 1);
2026-09-14 13:29:59 +02:00
color.rgb = select(p > 10.0f, float3(0), color.rgb);
2026-09-14 13:29:59 +02:00
float2 uv = frac(p.xz * 0.25f);
color.rg = lerp(color.rg, uv, 0.1f);
return float4(color.xy, color.zw).xyz;
2026-09-14 13:29:59 +02:00
}
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()
2026-09-14 13:29:59 +02:00
{
var color = shade(float3(1.0f, 0.5f, 2.0f), float3(0.0f, 1.0f, 0.0f), float2(1.0f, 0.0f), 0.0f);
2026-09-14 13:29:59 +02:00
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);
2026-09-14 13:29:59 +02:00
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));
2026-09-14 13:29:59 +02:00
Assert.Equal(viaRotate.x, steered.x, precision: 5);
Assert.Equal(viaRotate.y, steered.y, precision: 5);
}
}