Merge hands/cultmath-shader-lattice-loop

CultMath shaders: cultmath_power_border, cultmath_lattice_cell_distance and
cultmath_weighted_lattice_cells over a fixed-capacity site array
(CULTMATH_MAX_SITES, overridable). Spec cultmath-tapes:cut_spec:cut-shader-lattice-loop.r1,
report h1, Soul s1 holds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-17 17:20:23 +02:00
commit 86cd84a315
15 changed files with 1548 additions and 457 deletions

View file

@ -38,8 +38,9 @@ shader semantics.
drifting lattice whose cell weights come from the exact power border of the body span at each cell centre, with a stretched
metric and an exact ring search over the stated range (`LatticeMaxBoost`, `LatticeMaxStretch`, `LatticeMaxSmoothing`). It composes the
public steps `lattice_site`, `lattice_weight`, `lattice_distance` and `lattice_smooth_step`. The shaders carry the four steps as
`cultmath_lattice_site`, `cultmath_lattice_weight`, `cultmath_lattice_distance` and `cultmath_lattice_smooth_step`; the ring and box loops
are the shader caller's, written as the comment above them in `CultMath.hlsl` gives them, and the argument checks become its preconditions.
`cultmath_lattice_site`, `cultmath_lattice_weight`, `cultmath_lattice_distance` and `cultmath_lattice_smooth_step`, and the whole function as `cultmath_weighted_lattice_cells(p, time, bodies,
count, ...)` over a `float4 bodies[CULTMATH_MAX_SITES]` array (128 by default; define the macro before including to change it);
`cultmath_power_border(p, sites, count)` is the matching border. The argument checks become the shader caller's preconditions.
- `shaders/CultMath.hlsl`, a canonical HLSL mirror include for shader-side
parity. HLSL already owns `float2`, `float3`, and `float4`; the include
exposes `cultmath_*` functions for shared semantics HLSL does not provide.

View file

@ -95,7 +95,8 @@ and HLSL's one-based `_11`..`_33` element names.
Roslyn against CultMath. These transformations are the complete list; if a body
needs anything else, the test fails:
1. Include guards (`#ifndef`/`#define`/`#endif`) are dropped.
1. Include guards (`#ifndef`/`#define`/`#endif`) are dropped, except the capacity macro (`#ifndef CULTMATH_MAX_SITES`,
`#define CULTMATH_MAX_SITES 128`, `#endif`), which becomes `const int CULTMATH_MAX_SITES = 128;`.
2. Functions taking `Texture2D`/`SamplerState` are dropped; GPU resources have
no CultMath analog.
3. File-scope `static const` becomes `const`.
@ -103,7 +104,8 @@ needs anything else, the test fails:
5. Struct fields gain `public`. HLSL struct members have no access-modifier
concept; a C# struct's fields default to `private`, which would hide them
from the mirror test's reflection-based field comparison.
6. The file body is wrapped in a class, since C# has no free functions; HLSL
6. An array parameter `T name[CULTMATH_MAX_SITES]` becomes `T[] name`.
7. The file body is wrapped in a class, since C# has no free functions; HLSL
functions become private instance methods.
HLSL constructs that stay outside the target and must not appear in the mirror:
@ -156,6 +158,10 @@ hand. Regenerate them, and the golden fixture
$env:CULTMATH_WRITE_GLSL = "1"; dotnet test packages/cultmath/tests/CultMath.Tests --filter GlslMirrorTests
```
Run it on Yggdrasil's `mcr.microsoft.com/dotnet/sdk:10.0` image (Ubuntu 24.04) only: that image owns the fixture's platform
families and `device-bounds-lavapipe.txt`, and `CULTMATH_WRITE_GLSL=1` fails on any other OS. Re-record the bounds there in
the same revision (`WRITE_BOUNDS=1`, `tools/device/run-lavapipe.sh`).
`GlslMirrorTests.CommittedGlslEqualsLowering` fails on any difference between
either committed file and the lowering of the committed HLSL. These
transformations, in order, are the complete list:
@ -519,11 +525,12 @@ running `float4(unit normal toward the owner, distance)` and the radical plane o
the distance built from the offset `owner - p` and the weight difference `(w1 - w2)(w1 + w2)` so it does not
cancel for close sites seen from far; the cell boundary distance is the least over every site except the owner
(the fold's nearest), and a site coincident with the owner has no plane. The step trusts the caller's owner: a
query outside the owner's cell reports distance 0 with that plane's normal, not an error. `power_border(p, sites)` is that loop.
query outside the owner's cell reports distance 0 with that plane's normal, not an error. `power_border(p, sites)` is that loop;
the shader's `cultmath_power_border(p, sites, count)` is the same loop over a by-value array (see "Sites in a shader" below).
Cell spans refuse a non-finite query, site position or weight, and more than 2^24 sites (indices are floats).
`power_cells` and
`multiplicative_cells` are that loop over a `ReadOnlySpan<float4>` and nothing else; a shader caller writes the
same loop over its own buffer. Every site of an explicit span is visited, so no window and no weight-ratio range
same loop over its own array. Every site of an explicit span is visited, so no window and no weight-ratio range
apply. `CultSiteCells` holds the two score fields and the two site indices (floats, -1 for none).
`gabriel_margin(a, b, other)` is `(|other - (a + b) / 2| - r) / r` with `r = |a - b| / 2`: zero on the diameter
@ -557,8 +564,21 @@ The accepted range is boost 0 to `LatticeMaxBoost` (4, a weight ratio of 10:1),
and smoothing up to `LatticeMaxSmoothing` (1); outside it the function throws `ArgumentException`. `LatticeMaxRing`
(37) is the proven greatest ring at the range edge. Measured over 24,000 points on orbiting bodies (six bodies,
sigma ratio 6:1, the ruled look and the range edge), the greatest ring visited has median 3, 99th percentile 7 and
maximum 8 (49, 225 and 289 cells); `RingsStayWellInsideTheCap` asserts these. The function is C# only; the shader
twins are the next cut.
maximum 8 (49, 225 and 289 cells); `RingsStayWellInsideTheCap` asserts these.
### Sites in a shader
A shader function cannot take a span, so `cultmath_power_border(p, sites, count)`, `cultmath_lattice_cell_distance(...)`
and `cultmath_weighted_lattice_cells(p, time, bodies, count, ...)` take `float4 sites[CULTMATH_MAX_SITES]` by value and the
count in use, with `count` in `[0, CULTMATH_MAX_SITES]`. `CULTMATH_MAX_SITES` is 128 unless the consumer defines it before
including the library (`#ifndef`): 128 covers every Aetheria zone within GLSL ES 3.00's 224 guaranteed fragment uniform
vectors, and loops run to `count`, so the capacity costs uniform space, not iterations. A caller whose sites live in a
buffer copies them into a local array first; the library takes no resource, accessor macro or uniform of its own. The preconditions the C#
functions throw for are the caller's in a shader (finite arguments, `cellSize > 0`, the stated ranges, `p` within 2^23
cells). The ring pass is bounded `k <= 37` and exits as C# does; the box pass recomputes each distance instead of caching
75 x 75 of them, which gives the same values. The C# mirror compiles the array as `T[]`, and
`HlslSourceCompatibilityTests.SiteArrayMirrorsMatchCSharp` compares the three functions through the span twins in
`SiteArrays` (reflection cannot pass a `ReadOnlySpan`); `CULTMATH_MAX_SITES` is a shader-only capacity with no C# twin.
## Intervals

View file

@ -600,14 +600,12 @@ CultCellular cultmath_cellular(vec3 p)
return result;
}
// Sites: a site is vec4(position.xyz, weight). A span is a loop over a per-site step, because a shader
// function cannot take a span in the mirror (design.md, "Sites"). The consumer's loop, over its own buffer:
// Sites: a site is vec4(position.xyz, weight). A shader function cannot take a span (design.md, "Sites"), so a span is a
// fixed-capacity array and a count (cultmath_power_border, below), or a loop over a per-site step:
// CultSiteCells cells = cultmath_site_cells_empty();
// for (int i = 0; i < count; i++)
// cells = cultmath_site_cells_insert(cells, cultmath_power_score(p, sites[i]), i);
// The exact power cell border is a second loop over the buffer, skipping the owner cells.nearestSite:
// vec4 border = vec4(0.0, 0.0, 0.0, 1.0e30); int owner = int(cells.nearestSite);
// for (int k = 0; k < count; k++) if (k != owner) border = cultmath_power_border_step(p, sites[owner], sites[k], border);
// The exact power cell border is cultmath_power_border, the second loop over the sites that skips the owner cells.nearestSite.
// The step trusts the owner: a p outside the owner's cell reports distance 0 with that plane's normal, not an error.
// Statement for statement with math.Sites.cs (power_score, multiplicative_score, site_cells_*, gabriel_margin).
struct CultSiteCells
@ -688,21 +686,8 @@ float cultmath_gabriel_margin(vec3 a, vec3 b, vec3 other)
return (cultmath_distance(other, (a + b) * 0.5) - r) / r;
}
// Weighted lattice cells: the four per-cell steps of math.Lattice.cs, statement for statement. A shader function cannot take a
// span (design.md, "Sites"), so weighted_lattice_cells is the consumer's loop over its own body buffer, in two passes:
// vec2 q = p / cellSize; ivec2 c0 = (ivec2)floor(q); float f1 = 1.0e30, f2 = 1.0e30; int reach = 0;
// for (int k = 0; ; k++) { // ring pass: Chebyshev rings 0, 1, 2, ... around c0, exact exit
// if (k > 1 && 0.5 * (k - 1) >= max(f2, f1 + 2.0 * smoothing)) break; // k never reaches 38 in the accepted range
// for each cell on ring k: d = cultmath_lattice_distance(q, cultmath_lattice_site(cell, time, drift).xy, w, normal, stretch),
// w = cultmath_lattice_weight(site.z, border, boost, falloff), border = the .w of cultmath_power_border_step
// folded over the bodies (owner = the nearest body of the cell centre (cell + 0.5) * cellSize, z = 0);
// if (d < f1) { f2 = f1; f1 = d; } else if (d < f2) f2 = d; store d for the box pass
// reach = k; }
// float m = f1 + smoothing; // box pass: every stored cell, rows dz = -reach..reach, then dx = -reach..reach
// for each (dz, dx): m = cultmath_lattice_smooth_step(m, d, f1, smoothing); // strict < for the winning id (earlier cell wins ties)
// The preconditions the C# function throws for are the caller's in a shader: finite arguments, cellSize > 0, boost in [0, 4],
// falloff > 0, smoothing in (0, 1], stretch in [1, 2], p within 2^23 cells. The normal is normalised as C# does: divide by its
// largest component, then by its length (a zero normal means (1, 0) and needs stretch == 1). The ring bound is 37.
// Weighted lattice cells: the four per-cell steps of math.Lattice.cs, statement for statement; cultmath_weighted_lattice_cells,
// after them, is the whole function.
vec3 cultmath_lattice_site(vec2 cell, float time, float drift)
{
ivec3 h = cultmath_pcg3d(ivec3(int(cell.x), int(cell.y), 0));
@ -734,6 +719,90 @@ float cultmath_lattice_smooth_step(float m, float d, float f1, float smoothing)
return min(next, f1 + smoothing);
}
// Sites by value: a shader function cannot take a span, so these take vec4 sites[CULTMATH_MAX_SITES] and the count in use, with
// count in [0, CULTMATH_MAX_SITES]. A consumer defines CULTMATH_MAX_SITES before including this file to change the capacity; loops
// run to count, so the capacity costs uniform space, not iterations. A caller whose sites live in a buffer copies them into a local array first.
// The preconditions the C# functions throw for are the caller's in a shader: finite arguments, cellSize > 0, boost in [0, 4],
// falloff > 0, smoothing in (0, 1], stretch in [1, 2], p within 2^23 cells. A zero stretchNormal means (1, 0) and needs stretch == 1.
// cultmath_weighted_lattice_cells is math.weighted_lattice_cells: the ring pass is bounded by k <= 37 and exits as C# does, and the
// box pass recomputes each distance instead of caching it (a 75 x 75 private array is not a shader option), with the same values.
#ifndef CULTMATH_MAX_SITES
#define CULTMATH_MAX_SITES 128
#endif
vec4 cultmath_power_border(vec3 p, vec4 sites[CULTMATH_MAX_SITES], int count)
{
CultSiteCells cells = cultmath_site_cells_empty();
for (int i = 0; i < count; i++)
cells = cultmath_site_cells_insert(cells, cultmath_power_score(p, sites[i]), i);
vec4 border = vec4(0.0, 0.0, 0.0, 1.0e30);
int owner = int(cells.nearestSite);
if (owner < 0)
return border;
for (int k = 0; k < count; k++)
if (k != owner)
border = cultmath_power_border_step(p, sites[owner], sites[k], border);
return border;
}
float cultmath_lattice_cell_distance(vec2 q, vec2 cell, float time, float drift, vec4 bodies[CULTMATH_MAX_SITES], int count,
float cellSize, float boost, float falloff, vec2 normal, float stretch)
{
vec3 site = cultmath_lattice_site(cell, time, drift);
float border = cultmath_power_border(vec3((cell + 0.5) * cellSize, 0.0), bodies, count).w;
return cultmath_lattice_distance(q, site.xy, cultmath_lattice_weight(site.z, border, boost, falloff), normal, stretch);
}
vec4 cultmath_weighted_lattice_cells(vec2 p, float time, vec4 bodies[CULTMATH_MAX_SITES], int count, float cellSize, float drift,
float boost, float falloff, float smoothing, vec2 stretchNormal, float stretch)
{
float scale = max(abs(stretchNormal.x), abs(stretchNormal.y));
vec2 normal = vec2(1.0, 0.0);
if (scale > 0.0)
{
normal = stretchNormal / scale;
normal = normal / length(normal);
}
vec2 q = p / cellSize;
vec2 origin = floor(q);
int cx0 = int(origin.x);
int cz0 = int(origin.y);
float f1 = 1.0e30;
float f2 = 1.0e30;
int reach = 0;
for (int k = 0; k <= 37; k++)
{
if (k > 1 && 0.5 * float(k - 1) >= max(f2, f1 + 2.0 * smoothing))
break;
for (int dz = -k; dz <= k; dz++)
{
int step = dz == -k || dz == k || k == 0 ? 1 : 2 * k;
for (int dx = -k; dx <= k; dx += step)
{
float d = cultmath_lattice_cell_distance(q, vec2(cx0 + dx, cz0 + dz), time, drift, bodies, count, cellSize, boost, falloff, normal, stretch);
if (d < f1) { f2 = f1; f1 = d; }
else if (d < f2) f2 = d;
}
}
reach = k;
}
float m = f1 + smoothing;
float best = 1.0e30;
float second = 1.0e30;
int winDx = 0;
int winDz = 0;
for (int dz = -reach; dz <= reach; dz++)
for (int dx = -reach; dx <= reach; dx++)
{
float d = cultmath_lattice_cell_distance(q, vec2(cx0 + dx, cz0 + dz), time, drift, bodies, count, cellSize, boost, falloff, normal, stretch);
if (d < best) { second = best; best = d; winDx = dx; winDz = dz; }
else if (d < second) second = d;
m = cultmath_lattice_smooth_step(m, d, f1, smoothing);
}
ivec4 id = cultmath_pcg4d(ivec4(cx0 + winDx, cz0 + winDz, 0, 0));
return vec4(best, second, m, float(uint(id.w) >> 8) * (1.0 / 16777216.0));
}
vec2 cultmath_segment_closest(vec2 point_value, vec2 start, vec2 end)
{
vec2 segment = end - start;

View file

@ -600,14 +600,12 @@ CultCellular cultmath_cellular(float3 p)
return result;
}
// Sites: a site is float4(position.xyz, weight). A span is a loop over a per-site step, because a shader
// function cannot take a span in the mirror (design.md, "Sites"). The consumer's loop, over its own buffer:
// Sites: a site is float4(position.xyz, weight). A shader function cannot take a span (design.md, "Sites"), so a span is a
// fixed-capacity array and a count (cultmath_power_border, below), or a loop over a per-site step:
// CultSiteCells cells = cultmath_site_cells_empty();
// for (int i = 0; i < count; i++)
// cells = cultmath_site_cells_insert(cells, cultmath_power_score(p, sites[i]), i);
// The exact power cell border is a second loop over the buffer, skipping the owner cells.nearestSite:
// float4 border = float4(0.0, 0.0, 0.0, 1.0e30); int owner = (int)cells.nearestSite;
// for (int k = 0; k < count; k++) if (k != owner) border = cultmath_power_border_step(p, sites[owner], sites[k], border);
// The exact power cell border is cultmath_power_border, the second loop over the sites that skips the owner cells.nearestSite.
// The step trusts the owner: a p outside the owner's cell reports distance 0 with that plane's normal, not an error.
// Statement for statement with math.Sites.cs (power_score, multiplicative_score, site_cells_*, gabriel_margin).
struct CultSiteCells
@ -688,21 +686,8 @@ float cultmath_gabriel_margin(float3 a, float3 b, float3 other)
return (cultmath_distance(other, (a + b) * 0.5) - r) / r;
}
// Weighted lattice cells: the four per-cell steps of math.Lattice.cs, statement for statement. A shader function cannot take a
// span (design.md, "Sites"), so weighted_lattice_cells is the consumer's loop over its own body buffer, in two passes:
// float2 q = p / cellSize; int2 c0 = (int2)floor(q); float f1 = 1.0e30, f2 = 1.0e30; int reach = 0;
// for (int k = 0; ; k++) { // ring pass: Chebyshev rings 0, 1, 2, ... around c0, exact exit
// if (k > 1 && 0.5 * (k - 1) >= max(f2, f1 + 2.0 * smoothing)) break; // k never reaches 38 in the accepted range
// for each cell on ring k: d = cultmath_lattice_distance(q, cultmath_lattice_site(cell, time, drift).xy, w, normal, stretch),
// w = cultmath_lattice_weight(site.z, border, boost, falloff), border = the .w of cultmath_power_border_step
// folded over the bodies (owner = the nearest body of the cell centre (cell + 0.5) * cellSize, z = 0);
// if (d < f1) { f2 = f1; f1 = d; } else if (d < f2) f2 = d; store d for the box pass
// reach = k; }
// float m = f1 + smoothing; // box pass: every stored cell, rows dz = -reach..reach, then dx = -reach..reach
// for each (dz, dx): m = cultmath_lattice_smooth_step(m, d, f1, smoothing); // strict < for the winning id (earlier cell wins ties)
// The preconditions the C# function throws for are the caller's in a shader: finite arguments, cellSize > 0, boost in [0, 4],
// falloff > 0, smoothing in (0, 1], stretch in [1, 2], p within 2^23 cells. The normal is normalised as C# does: divide by its
// largest component, then by its length (a zero normal means (1, 0) and needs stretch == 1). The ring bound is 37.
// Weighted lattice cells: the four per-cell steps of math.Lattice.cs, statement for statement; cultmath_weighted_lattice_cells,
// after them, is the whole function.
float3 cultmath_lattice_site(float2 cell, float time, float drift)
{
int3 h = cultmath_pcg3d(int3((int)cell.x, (int)cell.y, 0));
@ -734,6 +719,90 @@ float cultmath_lattice_smooth_step(float m, float d, float f1, float smoothing)
return min(next, f1 + smoothing);
}
// Sites by value: a shader function cannot take a span, so these take float4 sites[CULTMATH_MAX_SITES] and the count in use, with
// count in [0, CULTMATH_MAX_SITES]. A consumer defines CULTMATH_MAX_SITES before including this file to change the capacity; loops
// run to count, so the capacity costs uniform space, not iterations. A caller whose sites live in a buffer copies them into a local array first.
// The preconditions the C# functions throw for are the caller's in a shader: finite arguments, cellSize > 0, boost in [0, 4],
// falloff > 0, smoothing in (0, 1], stretch in [1, 2], p within 2^23 cells. A zero stretchNormal means (1, 0) and needs stretch == 1.
// cultmath_weighted_lattice_cells is math.weighted_lattice_cells: the ring pass is bounded by k <= 37 and exits as C# does, and the
// box pass recomputes each distance instead of caching it (a 75 x 75 private array is not a shader option), with the same values.
#ifndef CULTMATH_MAX_SITES
#define CULTMATH_MAX_SITES 128
#endif
float4 cultmath_power_border(float3 p, float4 sites[CULTMATH_MAX_SITES], int count)
{
CultSiteCells cells = cultmath_site_cells_empty();
for (int i = 0; i < count; i++)
cells = cultmath_site_cells_insert(cells, cultmath_power_score(p, sites[i]), i);
float4 border = float4(0.0, 0.0, 0.0, 1.0e30);
int owner = (int)cells.nearestSite;
if (owner < 0)
return border;
for (int k = 0; k < count; k++)
if (k != owner)
border = cultmath_power_border_step(p, sites[owner], sites[k], border);
return border;
}
float cultmath_lattice_cell_distance(float2 q, float2 cell, float time, float drift, float4 bodies[CULTMATH_MAX_SITES], int count,
float cellSize, float boost, float falloff, float2 normal, float stretch)
{
float3 site = cultmath_lattice_site(cell, time, drift);
float border = cultmath_power_border(float3((cell + 0.5) * cellSize, 0.0), bodies, count).w;
return cultmath_lattice_distance(q, site.xy, cultmath_lattice_weight(site.z, border, boost, falloff), normal, stretch);
}
float4 cultmath_weighted_lattice_cells(float2 p, float time, float4 bodies[CULTMATH_MAX_SITES], int count, float cellSize, float drift,
float boost, float falloff, float smoothing, float2 stretchNormal, float stretch)
{
float scale = max(abs(stretchNormal.x), abs(stretchNormal.y));
float2 normal = float2(1.0, 0.0);
if (scale > 0.0)
{
normal = stretchNormal / scale;
normal = normal / length(normal);
}
float2 q = p / cellSize;
float2 origin = floor(q);
int cx0 = (int)origin.x;
int cz0 = (int)origin.y;
float f1 = 1.0e30;
float f2 = 1.0e30;
int reach = 0;
for (int k = 0; k <= 37; k++)
{
if (k > 1 && 0.5 * (float)(k - 1) >= max(f2, f1 + 2.0 * smoothing))
break;
for (int dz = -k; dz <= k; dz++)
{
int step = dz == -k || dz == k || k == 0 ? 1 : 2 * k;
for (int dx = -k; dx <= k; dx += step)
{
float d = cultmath_lattice_cell_distance(q, float2(cx0 + dx, cz0 + dz), time, drift, bodies, count, cellSize, boost, falloff, normal, stretch);
if (d < f1) { f2 = f1; f1 = d; }
else if (d < f2) f2 = d;
}
}
reach = k;
}
float m = f1 + smoothing;
float best = 1.0e30;
float second = 1.0e30;
int winDx = 0;
int winDz = 0;
for (int dz = -reach; dz <= reach; dz++)
for (int dx = -reach; dx <= reach; dx++)
{
float d = cultmath_lattice_cell_distance(q, float2(cx0 + dx, cz0 + dz), time, drift, bodies, count, cellSize, boost, falloff, normal, stretch);
if (d < best) { second = best; best = d; winDx = dx; winDz = dz; }
else if (d < second) second = d;
m = cultmath_lattice_smooth_step(m, d, f1, smoothing);
}
int4 id = cultmath_pcg4d(int4(cx0 + winDx, cz0 + winDz, 0, 0));
return float4(best, second, m, (float)((uint)id.w >> 8) * (1.0 / 16777216.0));
}
float2 cultmath_segment_closest(float2 point_value, float2 start, float2 end)
{
float2 segment = end - start;

View file

@ -200,7 +200,7 @@ public static partial class math
return new float4(best, second, m, cellular_unit(id.w));
}
private static float lattice_cell_distance(
internal static float lattice_cell_distance(
float2 q, float2 cell, float time, float drift, ReadOnlySpan<float4> bodies, float cellSize, float boost, float falloff,
float2 normal, float stretch)
{

View file

@ -70,8 +70,9 @@ internal static class GlslLowering
var source = hlsl.ReplaceLineEndings("\n");
// 2. Include guards are dropped, and so are the functions taking Texture2D/SamplerState, exactly
// as the C# mirror drops them; the host shader samples its own textures.
source = Regex.Replace(source, @"(?m)^[ \t]*#(ifndef|define|endif)\b.*$", string.Empty);
// as the C# mirror drops them; the host shader samples its own textures. The capacity macro
// (#ifndef CULTMATH_MAX_SITES ... #endif) stays: GLSL has the preprocessor, and a consumer overrides it.
source = HlslSourceCompatibilityTests.DropGuards(source, (name, value) => $"#ifndef {name}\n#define {name} {value}\n#endif");
source = Regex.Replace(source, @"(?ms)^\w+ \w+\([^)]*\b(Texture2D|SamplerState)\b[^)]*\)\s*\{.*?^\}[ \t]*$", string.Empty);
// 3. Type names: floatN, intN, uintN and boolN become vecN, ivecN, uvecN and bvecN, and float3x3

View file

@ -0,0 +1,60 @@
using System.Reflection;
using System.Text.RegularExpressions;
using Xunit;
namespace CultMath.Tests;
public sealed partial class GlslMirrorTests
{
private const string Weighted = "cultmath_weighted_lattice_cells(";
private const string SmoothStep = "cultmath_lattice_smooth_step(";
// Edits of the shader text that each break one rule of the lattice loop. M7 (the exit one ring late) is absent: by the
// window-independence proof a later exit visits more cells and changes nothing, so no case can kill it.
private static readonly (string Name, string Function, string Old, string New, int Kills)[] LatticeMutants =
{
("M1 exit one ring early", Weighted, "0.5 * (float)(k - 1) >= max(f2", "0.5 * (float)k >= max(f2", 3),
("M2 exit ignores the smoothing band", Weighted, ">= max(f2, f1 + 2.0 * smoothing))", ">= f2)", 1),
("M3 no pre-normalisation of the normal", Weighted, "normal = stretchNormal / scale;", "normal = stretchNormal;", 1),
("M4 no unit length for the normal", Weighted, "normal = normal / length(normal);", "normal = normal * 1.0;", 1),
("M5 smooth step without its clamp", SmoothStep, "return min(next, f1 + smoothing);", "return next;", 1),
("M6 box pass skips its first row", Weighted, "for (int dz = -reach; dz <= reach; dz++)", "for (int dz = -reach + 1; dz <= reach; dz++)", 1),
};
/// <summary>
/// Each rule of the lattice loop is pinned by the committed fixture's own cases: the shader text with one rule broken (a
/// mutant mirror, the text a device runs) returns another result than the fixture on at least the stated number of
/// cases of the family that exercises it, the exit one ring early on at least three. The device judge holds the same
/// cases to their recorded bound, so a mutant these cases separate fails it too.
/// </summary>
[Fact]
public void TheFixtureCasesSeparateEveryLatticeMutantFromTheShader()
{
var families = FixtureFamilies();
foreach (var (name, function, old, replacement, kills) in LatticeMutants)
{
var (assembly, errors) = HlslSourceCompatibilityTests.CompileShaderMirror(mutate: text =>
{
Assert.True(text.Split(old).Length == 2, $"{name}: the shader text no longer holds exactly one \"{old}\"");
return text.Replace(old, replacement);
});
Assert.True(assembly is not null, name + ": " + string.Join(Environment.NewLine, errors));
var shaderType = assembly!.GetType("CultMathHlsl.HlslShader")!;
var shader = Activator.CreateInstance(shaderType);
var mirror = shaderType.GetMethod(function[..^1], BindingFlags.Instance | BindingFlags.NonPublic)!;
var f = Array.FindIndex(Families, family => family.Glsl.Contains(function));
var method = Resolve(Families[f].Glsl);
var killed = 0;
foreach (var entry in families[f]!["cases"]!.AsArray())
{
var sides = Sides(entry);
if (Hex(new[] { mirror.Invoke(shader, Arguments(method, sides[0]))! }) != sides[1])
killed++;
}
output.WriteLine($"{name}: separated on {killed} of {Points} cases of {Families[f].Glsl}");
Assert.True(killed >= kills, $"{name}: the {Families[f].Glsl} fixture cases separate it on {killed} of {Points}, fewer than {kills}");
}
}
}

View file

@ -22,7 +22,11 @@ public sealed partial class GlslMirrorTests
public GlslMirrorTests(ITestOutputHelper output) => this.output = output;
private static bool Write => Environment.GetEnvironmentVariable("CULTMATH_WRITE_GLSL") == "1";
// The fixture is written by Yggdrasil's mcr.microsoft.com/dotnet/sdk:10.0 image (Ubuntu 24.04) only: its platform families record
// that platform's libm, so a write from another OS would move them.
private static bool Write => Environment.GetEnvironmentVariable("CULTMATH_WRITE_GLSL") == "1"
? OperatingSystem.IsLinux() ? true : throw new InvalidOperationException("CULTMATH_WRITE_GLSL=1 writes glsl-parity.json, whose platform families belong to Yggdrasil's Ubuntu image (mcr.microsoft.com/dotnet/sdk:10.0): regenerate it there.")
: false;
private static string Root => HlslSourceCompatibilityTests.FindCultMathRoot();
@ -497,6 +501,14 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z >
return (int)scalars.Dequeue();
if (type == typeof(uint))
return scalars.Dequeue();
if (type.IsArray)
{
var array = Array.CreateInstance(type.GetElementType()!, SiteArrays.Capacity);
for (var i = 0; i < array.Length; i++)
array.SetValue(FromScalars(type.GetElementType()!, scalars), i);
return array;
}
var value = Activator.CreateInstance(type)!;
foreach (var field in type.GetFields().Where(f => !f.IsStatic))
field.SetValue(value, FromScalars(field.FieldType, scalars));
@ -872,14 +884,26 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z >
}),
// The smooth step: d exactly at f1 + 2 smoothing one in four (the equal branch), near it one in four, the
// smoothing near 0 and at 1, m anywhere up to f1 + smoothing.
// One draw in four is the cap point: m = f1 + smoothing exactly and d = f1 + smoothing (1 + U(0.3, 0.9)) with smoothing in
// [0.5, 1], where next exceeds f1 + smoothing and the min clamps; a step that returns next reads another function there.
new Family("float cultmath_lattice_smooth_step(float, float, float, float)", "ulp-bounded", r =>
{
if (r.Next(4) == 0)
{
var cappedSmoothing = Uniform(r, 0.5f, 1.0f);
var cappedF1 = Uniform(r, 0.0f, 10.0f);
return new object[] { cappedF1 + cappedSmoothing, cappedF1 + cappedSmoothing * (1.0f + Uniform(r, 0.3f, 0.9f)), cappedF1, cappedSmoothing };
}
var smoothing = r.Next(5) switch { 0 => 1.0f, 1 => Uniform(r, 1.0e-3f, 1.0e-2f), _ => Uniform(r, 0.01f, 1.0f) };
var f1 = Uniform(r, 0.0f, 10.0f);
var edge = f1 + 2.0f * smoothing;
var d = r.Next(4) switch { 0 => edge, 1 => edge * Uniform(r, 0.99f, 1.01f), _ => Uniform(r, f1, edge + 1.0f) };
return new object[] { Uniform(r, f1, f1 + smoothing), d, f1, smoothing };
}),
// The site-array functions: a fixed-capacity array and a count, killed on the device (the lattice at rings up to 17).
new Family($"vec4 cultmath_power_border(vec3, vec4[{SiteArrays.Capacity}], int)", "ulp-bounded", SiteArrays.PowerBorderArguments),
new Family($"vec4 cultmath_weighted_lattice_cells(vec2, float, vec4[{SiteArrays.Capacity}], int, float, float, float, float, float, vec2, float)", "ulp-bounded", SiteArrays.LatticeArguments),
}).ToArray();
// A point of magnitude m, m log-uniform in [lo, hi]: one component at +-m, the other two uniform in [-m, m],
@ -988,6 +1012,7 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z >
["vec4"] = typeof(float4), ["ivec3"] = typeof(int3), ["ivec4"] = typeof(int4),
["SpanPath"] = typeof(SpanPath), ["SpanStage"] = typeof(SpanStage),
["CultSiteCells"] = typeof(CultSiteCells),
[$"vec4[{SiteArrays.Capacity}]"] = typeof(float4[]),
};
// The C# math method a GLSL signature names: cultmath_ dropped, parameter types mapped.
@ -996,6 +1021,7 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z >
var match = Regex.Match(glsl, @"^\w+ cultmath_(\w+)\(([^)]*)\)$");
var types = match.Groups[2].Value.Split(", ").Select(t => GlslTypes[t]).ToArray();
return typeof(math).GetMethod(match.Groups[1].Value, BindingFlags.Public | BindingFlags.Static, null, types, null)
?? typeof(SiteArrays).GetMethod(match.Groups[1].Value, BindingFlags.NonPublic | BindingFlags.Static, null, types, null)
?? throw new InvalidOperationException("no C# math counterpart for " + glsl);
}
@ -1011,6 +1037,8 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z >
// the same read as GLSL's columns.
private static IEnumerable<object> Scalars(object value) => value is float or int or uint
? new[] { value }
: value is Array array
? array.Cast<object>().SelectMany(Scalars)
: value is float3x3 matrix
? new object[] { matrix._m00, matrix._m01, matrix._m02, matrix._m10, matrix._m11, matrix._m12, matrix._m20, matrix._m21, matrix._m22 }
: value.GetType().GetFields().Where(f => !f.IsStatic).SelectMany(f => Scalars(f.GetValue(value)!));

View file

@ -70,7 +70,7 @@ public sealed class HlslSourceCompatibilityTests
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 = Array.Empty<string>();
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
@ -85,7 +85,7 @@ public sealed class HlslSourceCompatibilityTests
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.Name.StartsWith("CULTMATH_", StringComparison.Ordinal))
.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)
@ -183,7 +183,7 @@ public sealed class HlslSourceCompatibilityTests
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_")))
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);
@ -204,9 +204,67 @@ public sealed class HlslSourceCompatibilityTests
}
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
@ -682,14 +740,23 @@ public sealed class HlslSourceCompatibilityTests
: 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 = "")
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.
var source = Regex.Replace(hlsl, @"(?m)^\s*#(ifndef|define|endif)\b.*$", string.Empty);
// 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.
@ -708,14 +775,19 @@ public sealed class HlslSourceCompatibilityTests
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. The file body is wrapped in a class; C# has no free functions.
// 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 = "")
internal static (Assembly? Assembly, IReadOnlyList<string> Errors) CompileShaderMirror(string members = "", IReadOnlyDictionary<string, int>? defines = null, Func<string, string>? mutate = null)
{
var hlsl = ReadShaderSource(FindCultMathRoot());
var tree = CSharpSyntaxTree.ParseText(TransformHlslToCSharp(hlsl, members));
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)

View file

@ -9,19 +9,19 @@ namespace CultMath.Tests;
/// </summary>
public sealed class LatticeCellsTests
{
private const float Cell = 260.0f;
private const float Falloff = 120.0f;
private const float Drift = 0.7f;
private static readonly float2 Normal = new(0.6f, 0.8f);
internal const float Cell = 260.0f;
internal const float Falloff = 120.0f;
internal const float Drift = 0.7f;
internal static readonly float2 Normal = new(0.6f, 0.8f);
private sealed record Look(float Boost, float Stretch, float Smoothing, float Falloff = LatticeCellsTests.Falloff);
internal sealed record Look(float Boost, float Stretch, float Smoothing, float Falloff = LatticeCellsTests.Falloff);
private static readonly Look[] Looks =
internal static readonly Look[] Looks =
{
new(1.6f, 1.0f, 0.35f), new(1.6f, 2.0f, 0.35f), new(LatticeMaxBoost, LatticeMaxStretch, LatticeMaxSmoothing), new(0.0f, 1.0f, 0.05f),
};
private static float4[] Bodies(float t)
internal static float4[] Bodies(float t)
{
var bodies = new float4[6];
for (var i = 0; i < bodies.Length; i++)
@ -41,7 +41,7 @@ public sealed class LatticeCellsTests
}
// Folds F1, F2, id and smooth over the box of the given radius in absolute row-major order.
private static float4 BoxFold(float2 p, float time, float4[] bodies, Look look, int radius,
internal static float4 BoxFold(float2 p, float time, float4[] bodies, Look look, int radius,
Dictionary<(int, int), float> weights, float2? unitNormal = null)
{
var q = p / Cell;
@ -71,10 +71,10 @@ public sealed class LatticeCellsTests
return new float4(f1, f2, m, cellular_unit(id.w));
}
private static float4 Call(float2 p, float time, float4[] bodies, Look look, float2? normal = null) =>
internal static float4 Call(float2 p, float time, float4[] bodies, Look look, float2? normal = null) =>
weighted_lattice_cells(p, time, bodies, Cell, Drift, look.Boost, look.Falloff, look.Smoothing, normal ?? Normal, look.Stretch);
private static float2 RandomPoint(System.Random r) =>
internal static float2 RandomPoint(System.Random r) =>
new((r.NextSingle() * 2.0f - 1.0f) * 1500.0f, (r.NextSingle() * 2.0f - 1.0f) * 1500.0f);
[Fact]
@ -97,9 +97,9 @@ public sealed class LatticeCellsTests
}
}
private delegate bool Stop(int k, float f1, float f2, Look look);
internal delegate bool Stop(int k, float f1, float f2, Look look);
private static readonly (string name, Stop stop)[] Rules =
internal static readonly (string name, Stop stop)[] Rules =
{
("correct", (k, f1, f2, l) => k > 1 && 0.5f * (k - 1) >= max(f2, f1 + 2.0f * l.Smoothing)),
("cutoff F1 + k", (k, f1, f2, l) => k > 1 && 0.5f * (k - 1) >= max(f2, f1 + l.Smoothing)),
@ -109,7 +109,7 @@ public sealed class LatticeCellsTests
};
// The radius of the box a search with the given exit rule visits (rings 0 to k - 1 when it stops before ring k).
private static int Radius(float2 p, float time, float4[] bodies, Look look, Stop stop,
internal static int Radius(float2 p, float time, float4[] bodies, Look look, Stop stop,
Dictionary<(int, int), float> weights)
{
var q = p / Cell;

View file

@ -0,0 +1,160 @@
using CultMath;
using static CultMath.math;
namespace CultMath.Tests;
/// <summary>
/// The shader's site-array functions as C# twins. A shader function cannot take a span, so
/// <c>cultmath_power_border</c>, <c>cultmath_lattice_cell_distance</c> and <c>cultmath_weighted_lattice_cells</c> take
/// <c>float4 sites[CULTMATH_MAX_SITES]</c> and a count; reflection cannot pass a <see cref="ReadOnlySpan{T}"/>, so each
/// twin takes the array and the count and calls the span function on <c>sites[..count]</c>. 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.
/// </summary>
internal static class SiteArrays
{
/// <summary>The shader's default CULTMATH_MAX_SITES; the device families spell their array with it.</summary>
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<float4> 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<float4>();
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<float4>();
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);
/// <summary>
/// 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).
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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 };
}

View file

@ -20,14 +20,14 @@ lavapipe-glsl 15 vec2 cultmath_iv_min(vec2, vec2) ulp-bounded 256 0 0 a324b35cb4
lavapipe-glsl 16 vec2 cultmath_iv_max(vec2, vec2) ulp-bounded 256 0 0 d68412262ca234c1da130fc5217ba6856da3e92e435c28c1fbdbf3692a89e1fa
lavapipe-glsl 17 vec2 cultmath_iv_sqr(vec2) ulp-bounded 256 0 0 dbbb0e0df015456393452377d689732428a9a8e24d8e2b407ee9620593b4c46b
lavapipe-glsl 18 vec2 cultmath_iv_sqrt(vec2) ulp-bounded 256 0 0 95bd15d1b283713fc632ced350f5da1b89f8f73e6618329773db1b52acd29991
lavapipe-glsl 19 vec2 cultmath_iv_clamp(vec2, float, float) ulp-bounded 256 0 0 268d61fd6d122e2b85e211c96daf410fb7e2d8ff83ec2082709f640504c852a7
lavapipe-glsl 19 vec2 cultmath_iv_clamp(vec2, float, float) ulp-bounded 256 0 0 abe9daf24930a5e3a49ec994c4cae3a0c27b9d019d6d93ec1963ec3daad664a0
lavapipe-glsl 20 vec2 cultmath_iv_saturate(vec2) ulp-bounded 256 0 0 0f844ef8c4d9a9f748c80677bbbfc36b5f6e682eb87c2d46e7e20635d5f55c26
lavapipe-glsl 21 vec2 cultmath_iv_smoothstep(float, float, vec2) ulp-bounded 256 0 0 3d5944a4c43feb91d6786924e0f5439c05285d28ff1f500b5582893758dd4ce5
lavapipe-glsl 22 vec2 cultmath_iv_lerp(vec2, vec2, float) ulp-bounded 256 0 0 96b09e90a3fbbfe81379a29a23b676e490bc7dc6bb731450197707e6cf4c573f
lavapipe-glsl 23 vec2 cultmath_iv_snoise_ball(vec3, float) ulp-bounded 242 54 0 570eb031260c8f4aab42edc57a5fa1d0da70e8a4e6f0b4bf68208c15506f7cb5
lavapipe-glsl 24 vec2 cultmath_iv_fbm_ball(vec3, float, int, float, float) ulp-bounded 250 56 0 9e7017426314537f84c58dcdd1f85188cbe8f4dd9ababed887f60776cf861ebf
lavapipe-glsl 25 vec4 cultmath_iv_frustum_ball(vec2, float, float, float, float) ulp-bounded 198 1.6457063576593662 0 327c306aa4d06aecbb2681d8808c6dbf0567e87b54a6067c68001e36b3e52169
lavapipe-glsl 26 vec2 cultmath_iv_exp(vec2) ulp-bounded, platform exp 7 7.526057804152758 0 1a46b0fa116f55e5abd70ec9f2a66de0befe8b3577fe472f9eaa98932aae8040
lavapipe-glsl 26 vec2 cultmath_iv_exp(vec2) ulp-bounded, platform exp 7 7.526057804152758 0 b4af8232dc3dd782cc6d790e928ede0f6ba6dfdffd6c71dd9494fb58973e485c
lavapipe-glsl 27 CultPhasor cultmath_phacelle(vec3, vec3, float, float) ulp-bounded, platform exp, sin, cos; across platforms |diff| <= 2^-20 max(|v|, 1); on a device |diff| <= 2^-18 max(|v|, 1) 0 18.5 0 705b4c18027a0838218645e9368c3749ae4b5bf75e0485985725e8e72b73ab88
lavapipe-glsl 28 float cultmath_snoise(vec3) ulp-bounded, on the simplex tie set 127 112 0 6929326e4f27726d7bbdbac3a7823209e2ecfcd301018ef164527c4c1a9f7ae8
lavapipe-glsl 29 vec4 cultmath_snoise_grad(vec3) ulp-bounded, on the simplex tie set 102 546.5 0 6173a812fd4059fa19bce84d34858c6035cdda98aa21a2b210e611cbd58c29d6
@ -36,7 +36,7 @@ lavapipe-glsl 31 vec3 cultmath_af_symbol(float, float) ulp-bounded 256 0 0 04147
lavapipe-glsl 32 vec3 cultmath_af_from_iv(vec2) ulp-bounded 256 0 0 306fe58a3268199fc8fd845a32d956f349a790422fa5c87d5a0824636cb55424
lavapipe-glsl 33 vec2 cultmath_af_range(vec3) ulp-bounded 256 0 0 6245eeba19473d69d0d8155e20e2e6089a6843140723d23ed5d501064023a44a
lavapipe-glsl 34 vec3 cultmath_af_add(vec3, vec3) ulp-bounded 256 0 0 cb1cb3fc69224837342de767525d332687b9d3e17b3de255c28e2f0f65f32640
lavapipe-glsl 35 vec3 cultmath_af_sub(vec3, vec3) ulp-bounded 256 0 0 df44c501a8be20dc46a54faac9583da13f4367adc501a33fe14624f10e54b558
lavapipe-glsl 35 vec3 cultmath_af_sub(vec3, vec3) ulp-bounded 256 0 0 a8fad8ea86cf61841430293fdd9840a54008160250114040380c9634c63bb17e
lavapipe-glsl 36 vec3 cultmath_af_neg(vec3) ulp-bounded 256 0 0 e485acf8ee21c6d880621cc6216b3b2a78318f56f5864b33c5509be27e22a694
lavapipe-glsl 37 vec3 cultmath_af_scale(vec3, float) ulp-bounded 256 0 0 8b0c8a540fea82fe058efc42ffa38dbd69850e579cefe5fdbc4c164b715ec15d
lavapipe-glsl 38 vec3 cultmath_af_add_iv(vec3, vec2) ulp-bounded 256 0 0 620f747e6ba53014c9714a8dbfaa3ff93f281c80938bd035ef64657e3bdc1a16
@ -50,7 +50,7 @@ lavapipe-glsl 45 vec3 cultmath_af_snoise(vec3, vec3, float) ulp-bounded, reach 1
lavapipe-glsl 46 vec3 cultmath_af_fbm(vec3, vec3, float, int, float, float) ulp-bounded, reach 1e-6 to 1e-3 27 102.5 0 42246db4e19a1da20433517cdc7016dda375ea70dc90d1a777679bfd76fb59e8
lavapipe-glsl 47 float cultmath_cycles_divide(float, float) ulp-bounded 256 0 0 87f143c57806ad52c3295cde67e97f31e39c8572b8c0a4b8c577e6fcd8ecbebf
lavapipe-glsl 48 float cultmath_cycles_power(float, float) ulp-bounded, platform pow 136 4.918926113980705 0 0e62db5ea32fdb9fdd3a9b135a914ddadd3231bdee0ff15c709102b26bb89186
lavapipe-glsl 49 float cultmath_cycles_logarithm(float, float) ulp-bounded, platform log 147 4.94622778071426 0 5b2bf1411512c7e5a3f4b335a16fc3ba00ba0784bb87acb14b7d1b9dd551b2f3
lavapipe-glsl 49 float cultmath_cycles_logarithm(float, float) ulp-bounded, platform log 146 4.94622778071426 0 5b2bf1411512c7e5a3f4b335a16fc3ba00ba0784bb87acb14b7d1b9dd551b2f3
lavapipe-glsl 50 float cultmath_cycles_sqrt(float) ulp-bounded 256 0 0 6d4beb34e93ae304b3d9d30f203fb6c2ba222affc0efc35bb5441528e9062a9a
lavapipe-glsl 51 float cultmath_cycles_inverse_sqrt(float) ulp-bounded 256 0 0 71aadd5bd384b9ca2eb89827de53af810a1350b02177de1987e9405e4a6e9ad8
lavapipe-glsl 52 float cultmath_cycles_modulo(float, float) ulp-bounded 256 0 0 cf055cafcb5b2d4789a0a2d5cfbc8bc79f52df646b60f5e47db3f994680949e9
@ -96,16 +96,16 @@ lavapipe-glsl 91 vec3 cultmath_cycles_mix_val(float, vec3, vec3) ulp-bounded 256
lavapipe-glsl 92 vec3 cultmath_cycles_mix_color(float, vec3, vec3) ulp-bounded 256 0 0 00067b1567bcd6c07a81fa5002f145dfca22437a3f3df5389b2665334d14909b
lavapipe-glsl 93 vec3 cultmath_cycles_mix_soft(float, vec3, vec3) ulp-bounded 157 1.9750388068880254 0 ed7c5508ca0543cc20a5f0c5624e75e3ca2a2d6044d481816eeda2edc50c0592
lavapipe-glsl 94 vec3 cultmath_cycles_mix_linear(float, vec3, vec3) ulp-bounded 256 0 0 672435cc30bcbbb253cce7316ee0712b1a18f06924bc89cea2d3388dd164a15d
lavapipe-glsl 95 float cultmath_cycles_arcsine(float) ulp-bounded, platform asin 155 2734.286809678319 0 f28f3ed0a53ffeebdfa84865d2d3d41590e6b46fc1578d40e1215e2a9e1af585
lavapipe-glsl 95 float cultmath_cycles_arcsine(float) ulp-bounded, platform asin 154 2734.286809678319 0 f28f3ed0a53ffeebdfa84865d2d3d41590e6b46fc1578d40e1215e2a9e1af585
lavapipe-glsl 96 float cultmath_cycles_arccosine(float) ulp-bounded, platform acos 79 1046.8674409774287 0 22a2c38ab5a232cdbb33cf0c133362581a398f64a4d0d5d6dd89dc5af06bd890
lavapipe-glsl 97 float cultmath_cycles_arctan2(float, float) ulp-bounded, platform atan 7 25.5 0 2c60447d80186f6dc9726f19c09fa96ec3f66d8f5688968da533d9195ea463c4
lavapipe-glsl 97 float cultmath_cycles_arctan2(float, float) ulp-bounded, platform atan 6 25.5 0 2c60447d80186f6dc9726f19c09fa96ec3f66d8f5688968da533d9195ea463c4
lavapipe-glsl 98 mat3 cultmath_snoise_hessian(vec3) ulp-bounded 30 1358.4375 0 62bbafb8420ed5aaa0ce11d1470ca98945e690b186c33a1e7cf4d71478396465
lavapipe-glsl 99 mat3 cultmath_snoise_hessian(vec3) ulp-bounded, on the simplex tie set 110 1317.4754224467295 0 0efd9b3a637b46033ed0351f64de274db4d9ce05bb83dd1ce57afb3ceaea26bc
lavapipe-glsl 100 float cultmath_integral_clamp_linear(float, float, float, float, float, float) ulp-bounded 256 0 0 84bf9c9a2126b3114a692507cc54f58d313afc33de8a1cac83969cc508433cbe
lavapipe-glsl 101 float cultmath_integral_clamp_linear(float, float, float, float, float, float) ulp-bounded, lo above hi and infinite bounds 256 0 0 ff999094d7794aa525e0f9f9a067aa21408a59559aa546cace3b1bb4a8c7f96d
lavapipe-glsl 102 float cultmath_integral_clamp_linear(float, float, float, float, float, float) ulp-bounded, zero-length spans and flat infinite clamps 256 0 0 c3cc48f4edcac6801b9223f12d289934916431bdfaea8f68af33a1b833ab9860
lavapipe-glsl 103 SpanNoise cultmath_span_fbm(SpanPath, SpanStage, int, float, float) ulp-bounded 7 1012477.3459086476 0 97c531c3a690e056d95e949e243ae74111d4a93a39fd22c0358a103b3bb14bc1
lavapipe-glsl 104 SpanNoise cultmath_span_ridged(SpanPath, SpanStage, int, float, float) ulp-bounded 8 948534.9108393334 0 c5d55a1d05403f6a1a7b7946c3a4c9556277f217fc660d338b3894efbb333e43
lavapipe-glsl 104 SpanNoise cultmath_span_ridged(SpanPath, SpanStage, int, float, float) ulp-bounded 8 948534.9108393334 0 54fd229d69439ff3318690b7802143f8410dc7ece30ad0d56ccc4b68bf39bec2
lavapipe-glsl 105 vec4 cultmath_snoise_grad(vec3) ulp-bounded, coordinate bands 1e-4 to 1e2 116 249.5 0 fff9658a8ec055766c9327ee9b680cf7fb83736c472ae90a11e50d6d2a053fdc
lavapipe-glsl 106 vec4 cultmath_snoise_grad(vec3) ulp-bounded, coordinate bands 1e2 to 4e6 70 11866336.625 0 833484714ebcea59d86e0fbc2eaa4ae247c29541703763989e6758547dbf6db8
lavapipe-glsl 107 mat3 cultmath_snoise_hessian(vec3) ulp-bounded, coordinate bands 1e-4 to 1e2 120 980.7417469278579 0 3ea43dabe0091ea8e994c1dfc65c7274ed38b9343d4d1d06f7caa09c806c91df
@ -117,13 +117,15 @@ lavapipe-glsl 112 vec3 cultmath_segment_closest(vec3, vec3, vec3) ulp-bounded 21
lavapipe-glsl 113 vec4 cultmath_power_border_step(vec3, vec4, vec4, vec4) ulp-bounded 214 43 0 fdfdff73434a146489b002af5e80663658b2dd7b86105a7e65bdab755ab54a5e
lavapipe-glsl 114 vec4 cultmath_power_score(vec3, vec4) ulp-bounded 207 1.4619722193127032 0 dc1aa639ae1dc44003a0f5d8c7c99b2e3d7f51b158c96b3c3bd7a9f7df6d927e
lavapipe-glsl 115 vec4 cultmath_multiplicative_score(vec3, vec4) ulp-bounded 234 1.5529504197086745 0 ee1af26ac82489e8d363e7a6b6b59da7dacfb397d4bc9a723dd68607a69da41a
lavapipe-glsl 116 CultSiteCells cultmath_site_cells_insert(CultSiteCells, vec4, int) ulp-bounded 256 0 0 b8687b139aaa865e9f28fbc84fe37dd4a1d4873d5ff8e2074922c07f56551576
lavapipe-glsl 116 CultSiteCells cultmath_site_cells_insert(CultSiteCells, vec4, int) ulp-bounded 256 0 0 bfad8b369011cfbf14644fec68ce65620694fcbf3cd3804f54953d55587313fc
lavapipe-glsl 117 vec4 cultmath_site_cells_border(CultSiteCells) ulp-bounded 228 1.069496760346883 0 d7b478bef7e6f80902d0b5b255cb4fca30757ff6b9d25dac2030b037a8d342ce
lavapipe-glsl 118 float cultmath_gabriel_margin(vec3, vec3, vec3) ulp-bounded 201 2 0 6a80fbef486de496a08b7c8e7f9f65e2cc0f5b9f93abe50c43e84ad88d1ed034
lavapipe-glsl 119 vec3 cultmath_lattice_site(vec2, float, float) ulp-bounded 256 0 0 84df739c336796638dcf0a99c328a4b8c150c25d80e02248d4b6d023dc13161f
lavapipe-glsl 120 float cultmath_lattice_weight(float, float, float, float) ulp-bounded 231 2.0671110200639147 0 434cdf5603abc2942aa624b9d2e98d5f1231706c86180a80439bc8b72bdd066e
lavapipe-glsl 121 float cultmath_lattice_distance(vec2, vec2, float, vec2, float) ulp-bounded 256 0 0 622d0e1ea0fcf0e2700558dbdaea92b8ce02e44048dbf35944c23b5088c3e40c
lavapipe-glsl 122 float cultmath_lattice_smooth_step(float, float, float, float) ulp-bounded 256 0 0 e6c9aeb52c5e63ebf3c8525dd39b5b88d5f890fd4f23f4894a92232807a3d94e
lavapipe-glsl 121 float cultmath_lattice_distance(vec2, vec2, float, vec2, float) ulp-bounded 256 0 0 ba3fa3bdb4f5f5b2478a4ddfb2e08f2f425e997778a716884e43b13d6ed050b5
lavapipe-glsl 122 float cultmath_lattice_smooth_step(float, float, float, float) ulp-bounded 256 0 0 6cbeddde536b12e83873f2e5ac60d16bef22049d2a7dea61af0ff1e0d5b84740
lavapipe-glsl 123 vec4 cultmath_power_border(vec3, vec4[128], int) ulp-bounded 188 65.72121683891783 0 8427c2b99109a4f59ff745fdb87f261deb1cbae34ce99bc1ae05ebb7f9963b4d
lavapipe-glsl 124 vec4 cultmath_weighted_lattice_cells(vec2, float, vec4[128], int, float, float, float, float, float, vec2, float) ulp-bounded 179 3 0 fd3b58c4e2ba8a7bf801a1e6cdff82468abcb368442bc98b2583fda9d2e6f823
lavapipe-hlsl 0 float cultmath_snoise(vec3) ulp-bounded 94 122.6875 0 5d41c14241a13124f2e1b49f230f5df0c48d0d69fb1be9ef2f33dc1702bf0b0a
lavapipe-hlsl 1 float cultmath_snoise(vec2) ulp-bounded 204 1 0 81d9c62726afdccbbe4ffef173b5c92731eb6bf5927b3979d653e50dbd5d4064
lavapipe-hlsl 2 vec4 cultmath_snoise_grad(vec3) ulp-bounded 71 617.5 0 f7e9ed1707cdb015efaba0e0150193309da0d4c125349518f8b7c60a17b25790
@ -143,14 +145,14 @@ lavapipe-hlsl 15 vec2 cultmath_iv_min(vec2, vec2) ulp-bounded 256 0 0 a324b35cb4
lavapipe-hlsl 16 vec2 cultmath_iv_max(vec2, vec2) ulp-bounded 256 0 0 d68412262ca234c1da130fc5217ba6856da3e92e435c28c1fbdbf3692a89e1fa
lavapipe-hlsl 17 vec2 cultmath_iv_sqr(vec2) ulp-bounded 256 0 0 dbbb0e0df015456393452377d689732428a9a8e24d8e2b407ee9620593b4c46b
lavapipe-hlsl 18 vec2 cultmath_iv_sqrt(vec2) ulp-bounded 256 0 0 95bd15d1b283713fc632ced350f5da1b89f8f73e6618329773db1b52acd29991
lavapipe-hlsl 19 vec2 cultmath_iv_clamp(vec2, float, float) ulp-bounded 256 0 0 268d61fd6d122e2b85e211c96daf410fb7e2d8ff83ec2082709f640504c852a7
lavapipe-hlsl 19 vec2 cultmath_iv_clamp(vec2, float, float) ulp-bounded 256 0 0 abe9daf24930a5e3a49ec994c4cae3a0c27b9d019d6d93ec1963ec3daad664a0
lavapipe-hlsl 20 vec2 cultmath_iv_saturate(vec2) ulp-bounded 256 0 0 0f844ef8c4d9a9f748c80677bbbfc36b5f6e682eb87c2d46e7e20635d5f55c26
lavapipe-hlsl 21 vec2 cultmath_iv_smoothstep(float, float, vec2) ulp-bounded 256 0 0 3d5944a4c43feb91d6786924e0f5439c05285d28ff1f500b5582893758dd4ce5
lavapipe-hlsl 22 vec2 cultmath_iv_lerp(vec2, vec2, float) ulp-bounded 256 0 0 96b09e90a3fbbfe81379a29a23b676e490bc7dc6bb731450197707e6cf4c573f
lavapipe-hlsl 23 vec2 cultmath_iv_snoise_ball(vec3, float) ulp-bounded 244 54 0 f36530a4cd8c9fdc1c73e7ddb6782cd11cbf8493e2ee7b0656e818812504267e
lavapipe-hlsl 24 vec2 cultmath_iv_fbm_ball(vec3, float, int, float, float) ulp-bounded 252 75 0 a1677ea42ce91a5d2b6f83fa7c44879b7a184ed4631108d7a1c6e710bf26ff02
lavapipe-hlsl 25 vec4 cultmath_iv_frustum_ball(vec2, float, float, float, float) ulp-bounded 198 1.6457063576593662 0 327c306aa4d06aecbb2681d8808c6dbf0567e87b54a6067c68001e36b3e52169
lavapipe-hlsl 26 vec2 cultmath_iv_exp(vec2) ulp-bounded, platform exp 7 7.526057804152758 0 1a46b0fa116f55e5abd70ec9f2a66de0befe8b3577fe472f9eaa98932aae8040
lavapipe-hlsl 26 vec2 cultmath_iv_exp(vec2) ulp-bounded, platform exp 7 7.526057804152758 0 b4af8232dc3dd782cc6d790e928ede0f6ba6dfdffd6c71dd9494fb58973e485c
lavapipe-hlsl 27 CultPhasor cultmath_phacelle(vec3, vec3, float, float) ulp-bounded, platform exp, sin, cos; across platforms |diff| <= 2^-20 max(|v|, 1); on a device |diff| <= 2^-18 max(|v|, 1) 0 18.5 0 705b4c18027a0838218645e9368c3749ae4b5bf75e0485985725e8e72b73ab88
lavapipe-hlsl 28 float cultmath_snoise(vec3) ulp-bounded, on the simplex tie set 144 99 0 be76512b18eb678194dad0c4726c0aef9ba34012b60b79d24c2df27989707145
lavapipe-hlsl 29 vec4 cultmath_snoise_grad(vec3) ulp-bounded, on the simplex tie set 111 546.5 0 124d4be07cbb66edab644e649c081cb7c8be5d99b6c1d84fec5d7ef65c76ed0f
@ -159,7 +161,7 @@ lavapipe-hlsl 31 vec3 cultmath_af_symbol(float, float) ulp-bounded 256 0 0 04147
lavapipe-hlsl 32 vec3 cultmath_af_from_iv(vec2) ulp-bounded 256 0 0 306fe58a3268199fc8fd845a32d956f349a790422fa5c87d5a0824636cb55424
lavapipe-hlsl 33 vec2 cultmath_af_range(vec3) ulp-bounded 256 0 0 6245eeba19473d69d0d8155e20e2e6089a6843140723d23ed5d501064023a44a
lavapipe-hlsl 34 vec3 cultmath_af_add(vec3, vec3) ulp-bounded 256 0 0 cb1cb3fc69224837342de767525d332687b9d3e17b3de255c28e2f0f65f32640
lavapipe-hlsl 35 vec3 cultmath_af_sub(vec3, vec3) ulp-bounded 256 0 0 df44c501a8be20dc46a54faac9583da13f4367adc501a33fe14624f10e54b558
lavapipe-hlsl 35 vec3 cultmath_af_sub(vec3, vec3) ulp-bounded 256 0 0 a8fad8ea86cf61841430293fdd9840a54008160250114040380c9634c63bb17e
lavapipe-hlsl 36 vec3 cultmath_af_neg(vec3) ulp-bounded 256 0 0 e485acf8ee21c6d880621cc6216b3b2a78318f56f5864b33c5509be27e22a694
lavapipe-hlsl 37 vec3 cultmath_af_scale(vec3, float) ulp-bounded 256 0 0 8b0c8a540fea82fe058efc42ffa38dbd69850e579cefe5fdbc4c164b715ec15d
lavapipe-hlsl 38 vec3 cultmath_af_add_iv(vec3, vec2) ulp-bounded 256 0 0 620f747e6ba53014c9714a8dbfaa3ff93f281c80938bd035ef64657e3bdc1a16
@ -173,7 +175,7 @@ lavapipe-hlsl 45 vec3 cultmath_af_snoise(vec3, vec3, float) ulp-bounded, reach 1
lavapipe-hlsl 46 vec3 cultmath_af_fbm(vec3, vec3, float, int, float, float) ulp-bounded, reach 1e-6 to 1e-3 24 122.75 0 7f941b1f0c76d9f2121027a555ae7f2a741cc164402f0df75237198ac97f850b
lavapipe-hlsl 47 float cultmath_cycles_divide(float, float) ulp-bounded 256 0 0 87f143c57806ad52c3295cde67e97f31e39c8572b8c0a4b8c577e6fcd8ecbebf
lavapipe-hlsl 48 float cultmath_cycles_power(float, float) ulp-bounded, platform pow 136 4.918926113980705 0 0e62db5ea32fdb9fdd3a9b135a914ddadd3231bdee0ff15c709102b26bb89186
lavapipe-hlsl 49 float cultmath_cycles_logarithm(float, float) ulp-bounded, platform log 147 4.94622778071426 0 5b2bf1411512c7e5a3f4b335a16fc3ba00ba0784bb87acb14b7d1b9dd551b2f3
lavapipe-hlsl 49 float cultmath_cycles_logarithm(float, float) ulp-bounded, platform log 146 4.94622778071426 0 5b2bf1411512c7e5a3f4b335a16fc3ba00ba0784bb87acb14b7d1b9dd551b2f3
lavapipe-hlsl 50 float cultmath_cycles_sqrt(float) ulp-bounded 256 0 0 6d4beb34e93ae304b3d9d30f203fb6c2ba222affc0efc35bb5441528e9062a9a
lavapipe-hlsl 51 float cultmath_cycles_inverse_sqrt(float) ulp-bounded 256 0 0 71aadd5bd384b9ca2eb89827de53af810a1350b02177de1987e9405e4a6e9ad8
lavapipe-hlsl 52 float cultmath_cycles_modulo(float, float) ulp-bounded 256 0 0 cf055cafcb5b2d4789a0a2d5cfbc8bc79f52df646b60f5e47db3f994680949e9
@ -219,16 +221,16 @@ lavapipe-hlsl 91 vec3 cultmath_cycles_mix_val(float, vec3, vec3) ulp-bounded 256
lavapipe-hlsl 92 vec3 cultmath_cycles_mix_color(float, vec3, vec3) ulp-bounded 256 0 0 00067b1567bcd6c07a81fa5002f145dfca22437a3f3df5389b2665334d14909b
lavapipe-hlsl 93 vec3 cultmath_cycles_mix_soft(float, vec3, vec3) ulp-bounded 157 1.9750388068880254 0 ed7c5508ca0543cc20a5f0c5624e75e3ca2a2d6044d481816eeda2edc50c0592
lavapipe-hlsl 94 vec3 cultmath_cycles_mix_linear(float, vec3, vec3) ulp-bounded 256 0 0 672435cc30bcbbb253cce7316ee0712b1a18f06924bc89cea2d3388dd164a15d
lavapipe-hlsl 95 float cultmath_cycles_arcsine(float) ulp-bounded, platform asin 155 2734.286809678319 0 f28f3ed0a53ffeebdfa84865d2d3d41590e6b46fc1578d40e1215e2a9e1af585
lavapipe-hlsl 95 float cultmath_cycles_arcsine(float) ulp-bounded, platform asin 154 2734.286809678319 0 f28f3ed0a53ffeebdfa84865d2d3d41590e6b46fc1578d40e1215e2a9e1af585
lavapipe-hlsl 96 float cultmath_cycles_arccosine(float) ulp-bounded, platform acos 79 1046.8674409774287 0 22a2c38ab5a232cdbb33cf0c133362581a398f64a4d0d5d6dd89dc5af06bd890
lavapipe-hlsl 97 float cultmath_cycles_arctan2(float, float) ulp-bounded, platform atan 7 25.5 0 2c60447d80186f6dc9726f19c09fa96ec3f66d8f5688968da533d9195ea463c4
lavapipe-hlsl 97 float cultmath_cycles_arctan2(float, float) ulp-bounded, platform atan 6 25.5 0 2c60447d80186f6dc9726f19c09fa96ec3f66d8f5688968da533d9195ea463c4
lavapipe-hlsl 98 mat3 cultmath_snoise_hessian(vec3) ulp-bounded 33 2377.8799856935966 0 a64853839f394cc7c367ba5e107caa8bb396f07cf202263aa917095dd1c939f7
lavapipe-hlsl 99 mat3 cultmath_snoise_hessian(vec3) ulp-bounded, on the simplex tie set 132 1317.4754224467295 0 9066a49e50731881583ffca16c949171bb61578a1dfd497c54dafe773ab49488
lavapipe-hlsl 100 float cultmath_integral_clamp_linear(float, float, float, float, float, float) ulp-bounded 256 0 0 84bf9c9a2126b3114a692507cc54f58d313afc33de8a1cac83969cc508433cbe
lavapipe-hlsl 101 float cultmath_integral_clamp_linear(float, float, float, float, float, float) ulp-bounded, lo above hi and infinite bounds 256 0 0 ff999094d7794aa525e0f9f9a067aa21408a59559aa546cace3b1bb4a8c7f96d
lavapipe-hlsl 102 float cultmath_integral_clamp_linear(float, float, float, float, float, float) ulp-bounded, zero-length spans and flat infinite clamps 256 0 0 c3cc48f4edcac6801b9223f12d289934916431bdfaea8f68af33a1b833ab9860
lavapipe-hlsl 103 SpanNoise cultmath_span_fbm(SpanPath, SpanStage, int, float, float) ulp-bounded 3 1024946.5723514018 0 f6395b9f2609d914894c35b442719be3cc3e11d07c5f0bae5a8719651e60cd4e
lavapipe-hlsl 104 SpanNoise cultmath_span_ridged(SpanPath, SpanStage, int, float, float) ulp-bounded 7 948534.9108393334 0 bfda3536007373bdbef75a5c7bb63a7192f1c90b082984588f23e5b4eb767867
lavapipe-hlsl 104 SpanNoise cultmath_span_ridged(SpanPath, SpanStage, int, float, float) ulp-bounded 7 948534.9108393334 0 2adfdc4e6c81ee0bc7bb7869c55207f5033b4176104bc36dce1528ae0a5fe39b
lavapipe-hlsl 105 vec4 cultmath_snoise_grad(vec3) ulp-bounded, coordinate bands 1e-4 to 1e2 119 423.1430718408491 0 a94c38d1baa651a6faa510920cbe9d1f9528c0adb863ba6e35d835588ad00a58
lavapipe-hlsl 106 vec4 cultmath_snoise_grad(vec3) ulp-bounded, coordinate bands 1e2 to 4e6 73 13116827.1875 0 9ad0fd577b26aa717534c0693df97ee41bcded0768f442739b98f505e4a2a430
lavapipe-hlsl 107 mat3 cultmath_snoise_hessian(vec3) ulp-bounded, coordinate bands 1e-4 to 1e2 122 980.7417469278579 0 0977fa6f446e4f916a9b938ce24ea32df6aa912dc991c182014d309dc0efb1cb
@ -240,10 +242,12 @@ lavapipe-hlsl 112 vec3 cultmath_segment_closest(vec3, vec3, vec3) ulp-bounded 21
lavapipe-hlsl 113 vec4 cultmath_power_border_step(vec3, vec4, vec4, vec4) ulp-bounded 214 43 0 fdfdff73434a146489b002af5e80663658b2dd7b86105a7e65bdab755ab54a5e
lavapipe-hlsl 114 vec4 cultmath_power_score(vec3, vec4) ulp-bounded 207 1.4619722193127032 0 dc1aa639ae1dc44003a0f5d8c7c99b2e3d7f51b158c96b3c3bd7a9f7df6d927e
lavapipe-hlsl 115 vec4 cultmath_multiplicative_score(vec3, vec4) ulp-bounded 234 1.5529504197086745 0 ee1af26ac82489e8d363e7a6b6b59da7dacfb397d4bc9a723dd68607a69da41a
lavapipe-hlsl 116 CultSiteCells cultmath_site_cells_insert(CultSiteCells, vec4, int) ulp-bounded 256 0 0 b8687b139aaa865e9f28fbc84fe37dd4a1d4873d5ff8e2074922c07f56551576
lavapipe-hlsl 116 CultSiteCells cultmath_site_cells_insert(CultSiteCells, vec4, int) ulp-bounded 256 0 0 bfad8b369011cfbf14644fec68ce65620694fcbf3cd3804f54953d55587313fc
lavapipe-hlsl 117 vec4 cultmath_site_cells_border(CultSiteCells) ulp-bounded 228 1.069496760346883 0 d7b478bef7e6f80902d0b5b255cb4fca30757ff6b9d25dac2030b037a8d342ce
lavapipe-hlsl 118 float cultmath_gabriel_margin(vec3, vec3, vec3) ulp-bounded 201 2 0 6a80fbef486de496a08b7c8e7f9f65e2cc0f5b9f93abe50c43e84ad88d1ed034
lavapipe-hlsl 119 vec3 cultmath_lattice_site(vec2, float, float) ulp-bounded 256 0 0 84df739c336796638dcf0a99c328a4b8c150c25d80e02248d4b6d023dc13161f
lavapipe-hlsl 120 float cultmath_lattice_weight(float, float, float, float) ulp-bounded 231 2.0671110200639147 0 434cdf5603abc2942aa624b9d2e98d5f1231706c86180a80439bc8b72bdd066e
lavapipe-hlsl 121 float cultmath_lattice_distance(vec2, vec2, float, vec2, float) ulp-bounded 256 0 0 622d0e1ea0fcf0e2700558dbdaea92b8ce02e44048dbf35944c23b5088c3e40c
lavapipe-hlsl 122 float cultmath_lattice_smooth_step(float, float, float, float) ulp-bounded 256 0 0 e6c9aeb52c5e63ebf3c8525dd39b5b88d5f890fd4f23f4894a92232807a3d94e
lavapipe-hlsl 121 float cultmath_lattice_distance(vec2, vec2, float, vec2, float) ulp-bounded 256 0 0 ba3fa3bdb4f5f5b2478a4ddfb2e08f2f425e997778a716884e43b13d6ed050b5
lavapipe-hlsl 122 float cultmath_lattice_smooth_step(float, float, float, float) ulp-bounded 256 0 0 6cbeddde536b12e83873f2e5ac60d16bef22049d2a7dea61af0ff1e0d5b84740
lavapipe-hlsl 123 vec4 cultmath_power_border(vec3, vec4[128], int) ulp-bounded 188 65.72121683891783 0 8427c2b99109a4f59ff745fdb87f261deb1cbae34ce99bc1ae05ebb7f9963b4d
lavapipe-hlsl 124 vec4 cultmath_weighted_lattice_cells(vec2, float, vec4[128], int, float, float, float, float, float, vec2, float) ulp-bounded 179 3 0 fd3b58c4e2ba8a7bf801a1e6cdff82468abcb368442bc98b2583fda9d2e6f823

View file

@ -16,6 +16,7 @@ import sys
HLSL_VEC = re.compile(r'(float|int|uint)([234])$')
GLSL_VEC = re.compile(r'(i|u|)vec([234])$')
KIND = {'float': 'f', 'int': 'i', 'uint': 'u'}
ARRAY = re.compile(r'(\w+)\[(\d+)\]$') # an array leaf, vec4[128]: N elements of one leaf type, N x its words
def structs(root):
@ -79,6 +80,19 @@ def body(sig, n, win, wout, defs, glsl):
ret, name, params = m.group(1), m.group(2), [p.strip() for p in m.group(3).split(',') if p.strip()]
lines, offset, args = [], 0, []
for j, p in enumerate(params):
array = ARRAY.match(p)
if array:
elem, size = array.group(1), int(array.group(2))
words = leaves(elem, defs)
lines.append(' %s a%d[%d];' % (elem if glsl else hlsl_type(elem), j, size))
lines.append(' for (uint e = 0u; e < %du; e++) {' % size)
for k, leaf in enumerate(words):
word = ('In.a[b + %du + e * %du]' if glsl else 'In[b + %du + e * %du]') % (offset + k, len(words))
lines.append(' a%d[e]%s = %s;' % (j, access(leaf, glsl), read(leaf[1], glsl, word)))
lines.append(' }')
offset += size * len(words)
args.append('a%d' % j)
continue
ptype = p if glsl else hlsl_type(p)
lines.append(' %s a%d;' % (ptype, j))
for leaf in leaves(p, defs):

View file

@ -600,14 +600,12 @@ CultCellular cultmath_cellular(float3 p)
return result;
}
// Sites: a site is float4(position.xyz, weight). A span is a loop over a per-site step, because a shader
// function cannot take a span in the mirror (design.md, "Sites"). The consumer's loop, over its own buffer:
// Sites: a site is float4(position.xyz, weight). A shader function cannot take a span (design.md, "Sites"), so a span is a
// fixed-capacity array and a count (cultmath_power_border, below), or a loop over a per-site step:
// CultSiteCells cells = cultmath_site_cells_empty();
// for (int i = 0; i < count; i++)
// cells = cultmath_site_cells_insert(cells, cultmath_power_score(p, sites[i]), i);
// The exact power cell border is a second loop over the buffer, skipping the owner cells.nearestSite:
// float4 border = float4(0.0, 0.0, 0.0, 1.0e30); int owner = (int)cells.nearestSite;
// for (int k = 0; k < count; k++) if (k != owner) border = cultmath_power_border_step(p, sites[owner], sites[k], border);
// The exact power cell border is cultmath_power_border, the second loop over the sites that skips the owner cells.nearestSite.
// The step trusts the owner: a p outside the owner's cell reports distance 0 with that plane's normal, not an error.
// Statement for statement with math.Sites.cs (power_score, multiplicative_score, site_cells_*, gabriel_margin).
struct CultSiteCells
@ -688,21 +686,8 @@ float cultmath_gabriel_margin(float3 a, float3 b, float3 other)
return (cultmath_distance(other, (a + b) * 0.5) - r) / r;
}
// Weighted lattice cells: the four per-cell steps of math.Lattice.cs, statement for statement. A shader function cannot take a
// span (design.md, "Sites"), so weighted_lattice_cells is the consumer's loop over its own body buffer, in two passes:
// float2 q = p / cellSize; int2 c0 = (int2)floor(q); float f1 = 1.0e30, f2 = 1.0e30; int reach = 0;
// for (int k = 0; ; k++) { // ring pass: Chebyshev rings 0, 1, 2, ... around c0, exact exit
// if (k > 1 && 0.5 * (k - 1) >= max(f2, f1 + 2.0 * smoothing)) break; // k never reaches 38 in the accepted range
// for each cell on ring k: d = cultmath_lattice_distance(q, cultmath_lattice_site(cell, time, drift).xy, w, normal, stretch),
// w = cultmath_lattice_weight(site.z, border, boost, falloff), border = the .w of cultmath_power_border_step
// folded over the bodies (owner = the nearest body of the cell centre (cell + 0.5) * cellSize, z = 0);
// if (d < f1) { f2 = f1; f1 = d; } else if (d < f2) f2 = d; store d for the box pass
// reach = k; }
// float m = f1 + smoothing; // box pass: every stored cell, rows dz = -reach..reach, then dx = -reach..reach
// for each (dz, dx): m = cultmath_lattice_smooth_step(m, d, f1, smoothing); // strict < for the winning id (earlier cell wins ties)
// The preconditions the C# function throws for are the caller's in a shader: finite arguments, cellSize > 0, boost in [0, 4],
// falloff > 0, smoothing in (0, 1], stretch in [1, 2], p within 2^23 cells. The normal is normalised as C# does: divide by its
// largest component, then by its length (a zero normal means (1, 0) and needs stretch == 1). The ring bound is 37.
// Weighted lattice cells: the four per-cell steps of math.Lattice.cs, statement for statement; cultmath_weighted_lattice_cells,
// after them, is the whole function.
float3 cultmath_lattice_site(float2 cell, float time, float drift)
{
int3 h = cultmath_pcg3d(int3((int)cell.x, (int)cell.y, 0));
@ -734,6 +719,90 @@ float cultmath_lattice_smooth_step(float m, float d, float f1, float smoothing)
return min(next, f1 + smoothing);
}
// Sites by value: a shader function cannot take a span, so these take float4 sites[CULTMATH_MAX_SITES] and the count in use, with
// count in [0, CULTMATH_MAX_SITES]. A consumer defines CULTMATH_MAX_SITES before including this file to change the capacity; loops
// run to count, so the capacity costs uniform space, not iterations. A caller whose sites live in a buffer copies them into a local array first.
// The preconditions the C# functions throw for are the caller's in a shader: finite arguments, cellSize > 0, boost in [0, 4],
// falloff > 0, smoothing in (0, 1], stretch in [1, 2], p within 2^23 cells. A zero stretchNormal means (1, 0) and needs stretch == 1.
// cultmath_weighted_lattice_cells is math.weighted_lattice_cells: the ring pass is bounded by k <= 37 and exits as C# does, and the
// box pass recomputes each distance instead of caching it (a 75 x 75 private array is not a shader option), with the same values.
#ifndef CULTMATH_MAX_SITES
#define CULTMATH_MAX_SITES 128
#endif
float4 cultmath_power_border(float3 p, float4 sites[CULTMATH_MAX_SITES], int count)
{
CultSiteCells cells = cultmath_site_cells_empty();
for (int i = 0; i < count; i++)
cells = cultmath_site_cells_insert(cells, cultmath_power_score(p, sites[i]), i);
float4 border = float4(0.0, 0.0, 0.0, 1.0e30);
int owner = (int)cells.nearestSite;
if (owner < 0)
return border;
for (int k = 0; k < count; k++)
if (k != owner)
border = cultmath_power_border_step(p, sites[owner], sites[k], border);
return border;
}
float cultmath_lattice_cell_distance(float2 q, float2 cell, float time, float drift, float4 bodies[CULTMATH_MAX_SITES], int count,
float cellSize, float boost, float falloff, float2 normal, float stretch)
{
float3 site = cultmath_lattice_site(cell, time, drift);
float border = cultmath_power_border(float3((cell + 0.5) * cellSize, 0.0), bodies, count).w;
return cultmath_lattice_distance(q, site.xy, cultmath_lattice_weight(site.z, border, boost, falloff), normal, stretch);
}
float4 cultmath_weighted_lattice_cells(float2 p, float time, float4 bodies[CULTMATH_MAX_SITES], int count, float cellSize, float drift,
float boost, float falloff, float smoothing, float2 stretchNormal, float stretch)
{
float scale = max(abs(stretchNormal.x), abs(stretchNormal.y));
float2 normal = float2(1.0, 0.0);
if (scale > 0.0)
{
normal = stretchNormal / scale;
normal = normal / length(normal);
}
float2 q = p / cellSize;
float2 origin = floor(q);
int cx0 = (int)origin.x;
int cz0 = (int)origin.y;
float f1 = 1.0e30;
float f2 = 1.0e30;
int reach = 0;
for (int k = 0; k <= 37; k++)
{
if (k > 1 && 0.5 * (float)(k - 1) >= max(f2, f1 + 2.0 * smoothing))
break;
for (int dz = -k; dz <= k; dz++)
{
int step = dz == -k || dz == k || k == 0 ? 1 : 2 * k;
for (int dx = -k; dx <= k; dx += step)
{
float d = cultmath_lattice_cell_distance(q, float2(cx0 + dx, cz0 + dz), time, drift, bodies, count, cellSize, boost, falloff, normal, stretch);
if (d < f1) { f2 = f1; f1 = d; }
else if (d < f2) f2 = d;
}
}
reach = k;
}
float m = f1 + smoothing;
float best = 1.0e30;
float second = 1.0e30;
int winDx = 0;
int winDz = 0;
for (int dz = -reach; dz <= reach; dz++)
for (int dx = -reach; dx <= reach; dx++)
{
float d = cultmath_lattice_cell_distance(q, float2(cx0 + dx, cz0 + dz), time, drift, bodies, count, cellSize, boost, falloff, normal, stretch);
if (d < best) { second = best; best = d; winDx = dx; winDz = dz; }
else if (d < second) second = d;
m = cultmath_lattice_smooth_step(m, d, f1, smoothing);
}
int4 id = cultmath_pcg4d(int4(cx0 + winDx, cz0 + winDz, 0, 0));
return float4(best, second, m, (float)((uint)id.w >> 8) * (1.0 / 16777216.0));
}
float2 cultmath_segment_closest(float2 point_value, float2 start, float2 end)
{
float2 segment = end - start;