using CultMath; using static CultMath.math; namespace CultMath.Tests; /// /// The shader's site-array functions as C# twins. A shader function cannot take a span, so /// cultmath_power_border, cultmath_lattice_cell_distance and cultmath_weighted_lattice_cells take /// float4 sites[CULTMATH_MAX_SITES] and a count; reflection cannot pass a , so each /// twin takes the array and the count and calls the span function on sites[..count]. This table is the one /// place that maps them, read by the mirror test and by the GLSL fixture. The draws are the cases both run. /// internal static class SiteArrays { /// The shader's default CULTMATH_MAX_SITES; the device families spell their array with it. internal const int Capacity = 128; internal static float4 power_border(float3 p, float4[] sites, int count) => math.power_border(p, sites.AsSpan(0, count)); internal static float lattice_cell_distance( float2 q, float2 cell, float time, float drift, float4[] bodies, int count, float cellSize, float boost, float falloff, float2 normal, float stretch) => math.lattice_cell_distance(q, cell, time, drift, bodies.AsSpan(0, count), cellSize, boost, falloff, normal, stretch); internal static float4 weighted_lattice_cells( float2 p, float time, float4[] bodies, int count, float cellSize, float drift, float boost, float falloff, float smoothing, float2 stretchNormal, float stretch) => math.weighted_lattice_cells(p, time, bodies.AsSpan(0, count), cellSize, drift, boost, falloff, smoothing, stretchNormal, stretch); // The array the shader receives: the sites, then filler a loop running past the count would read. The filler is // finite and large, so a loop bound that ignores the count changes the result. private static float4[] Padded(IReadOnlyList sites) { var array = new float4[Capacity]; for (var i = 0; i < Capacity; i++) array[i] = i < sites.Count ? sites[i] : new float4(40.0f * (i + 1), -30.0f * (i + 1), 7.0f, 90.0f + i); return array; } private static float Uniform(System.Random r, float lo, float hi) => lo + (hi - lo) * r.NextSingle(); // Counts 0, 1, 6, 16 and the capacity are drawn often; the rest of [0, Capacity] fills between. private static int Count(System.Random r) => r.Next(3) == 0 ? r.Next(Capacity + 1) : new[] { 0, 1, 6, 16, Capacity }[r.Next(5)]; // Bodies on slow orbits (float4(position.xy, 0, sigma)), spread over the plane the lattice cells cover. private static float4[] Orbit(System.Random r, int count, float time) { var bodies = new List(); for (var i = 0; i < count; i++) { var angle = 0.001f * (i + 1) * time + i; var radius = 100.0f + 2400.0f * r.NextSingle(); bodies.Add(new float4(radius * MathF.Cos(angle), radius * MathF.Sin(angle), 0.0f, Uniform(r, 20.0f, 300.0f))); } return Padded(bodies); } internal static object[] PowerBorderArguments(System.Random r) { var count = Count(r); var sites = new List(); for (var i = 0; i < count; i++) sites.Add(new float4(Uniform(r, -1500.0f, 1500.0f), Uniform(r, -1500.0f, 1500.0f), Uniform(r, -200.0f, 200.0f), Uniform(r, 10.0f, 300.0f))); return new object[] { new float3(Uniform(r, -1500.0f, 1500.0f), Uniform(r, -1500.0f, 1500.0f), Uniform(r, -200.0f, 200.0f)), Padded(sites), count }; } internal static object[] LatticeCellDistanceArguments(System.Random r) { var angle = Uniform(r, 0.0f, 6.2831855f); var look = LatticeCellsTests.Looks[r.Next(LatticeCellsTests.Looks.Length)]; var cell = new float2(r.Next(-300, 300), r.Next(-300, 300)); var count = Count(r); return new object[] { new float2(Uniform(r, -1500.0f, 1500.0f), Uniform(r, -1500.0f, 1500.0f)) / LatticeCellsTests.Cell, cell, Uniform(r, 0.0f, 200.0f), LatticeCellsTests.Drift, Orbit(r, count, Uniform(r, 0.0f, 200.0f)), count, LatticeCellsTests.Cell, look.Boost, look.Falloff, new float2(MathF.Cos(angle), MathF.Sin(angle)), look.Stretch, }; } // The unit-less normals the function normalises itself: ordinary, 3-4-5, tiny, huge, and zero (stretch 1 only). private static readonly float2[] Normals = { LatticeCellsTests.Normal, new(3.0f, 4.0f), new(3.0e-30f, -4.0e-30f), new(1.0e30f, 2.0e30f), new(0.0f, 0.0f) }; internal static readonly LatticeCellsTests.Look WidestLook = new(LatticeMaxBoost, LatticeMaxStretch, LatticeMaxSmoothing, 1.0e6f); /// /// Points whose ring search runs to ring 8 or past it under the widest look (boost, stretch and smoothing at their /// maxima, falloff so wide that every cell is boosted), found once by the radius the exit rule visits, and the /// ring-17 point of LatticeCellsTests.TheCapIsReachedAndMatchesTheFullBox first. Each is (p, time). /// internal static readonly IReadOnlyList<(float2 P, float Time, int Ring)> FarPoints = FindFarPoints(); private static List<(float2, float, int)> FindFarPoints() { var found = new List<(float2, float, int)>(); var cap = (new float2(1446.7345f, -680.7637f), 97.69269f); found.Add((cap.Item1, cap.Item2, LatticeCellsTests.Radius(cap.Item1, cap.Item2, LatticeCellsTests.Bodies(cap.Item2), WidestLook, LatticeCellsTests.Rules[0].stop, new Dictionary<(int, int), float>()))); var r = new System.Random(0x1A77); while (found.Count < 12) { var p = LatticeCellsTests.RandomPoint(r); var time = 13.0f * r.Next(20); var ring = LatticeCellsTests.Radius(p, time, LatticeCellsTests.Bodies(time), WidestLook, LatticeCellsTests.Rules[0].stop, new Dictionary<(int, int), float>()); if (ring >= 8) found.Add((p, time, ring)); } return found; } /// /// Points where an exit one ring early (the rule "bound 0.5*k") returns another result than the exact search, found once by /// running both on the six orbiting bodies under each look: the cases that pin the ring exit to the bit. /// internal static readonly IReadOnlyList<(float2 P, float Time, LatticeCellsTests.Look Look)> ExitBoundPoints = FindExitBoundPoints(); private static List<(float2, float, LatticeCellsTests.Look)> FindExitBoundPoints() { var found = new List<(float2, float, LatticeCellsTests.Look)>(); var looks = LatticeCellsTests.Looks.Append(WidestLook).ToArray(); var r = new System.Random(0xE517); while (found.Count < 24) { var p = LatticeCellsTests.RandomPoint(r); var time = 13.0f * r.Next(20); var look = looks[r.Next(looks.Length)]; var bodies = LatticeCellsTests.Bodies(time); var early = LatticeCellsTests.Radius(p, time, bodies, look, LatticeCellsTests.Rules[2].stop, new Dictionary<(int, int), float>()); if (early >= LatticeCellsTests.Radius(p, time, bodies, look, LatticeCellsTests.Rules[0].stop, new Dictionary<(int, int), float>())) continue; if (!LatticeCellsTests.BoxFold(p, time, bodies, look, Math.Max(0, early), new Dictionary<(int, int), float>()).Equals(LatticeCellsTests.Call(p, time, bodies, look))) found.Add((p, time, look)); } return found; } internal static object[] LatticeArguments(System.Random r) { switch (r.Next(4)) { case 0: var far = FarPoints[r.Next(FarPoints.Count)]; return Lattice(far.P, far.Time, Padded(LatticeCellsTests.Bodies(far.Time)), 6, WidestLook, LatticeCellsTests.Normal); case 1: var tight = ExitBoundPoints[r.Next(ExitBoundPoints.Count)]; return Lattice(tight.P, tight.Time, Padded(LatticeCellsTests.Bodies(tight.Time)), 6, tight.Look, LatticeCellsTests.Normal); } var time = 13.0f * r.Next(20); var normal = Normals[r.Next(Normals.Length)]; var look = normal.x == 0.0f ? LatticeCellsTests.Looks[0] : LatticeCellsTests.Looks[r.Next(LatticeCellsTests.Looks.Length)]; var count = Count(r); return Lattice(LatticeCellsTests.RandomPoint(r), time, Orbit(r, count, time), count, look, normal); } private static object[] Lattice(float2 p, float time, float4[] bodies, int count, LatticeCellsTests.Look look, float2 normal) => new object[] { p, time, bodies, count, LatticeCellsTests.Cell, LatticeCellsTests.Drift, look.Boost, look.Falloff, look.Smoothing, normal, look.Stretch }; }