CultMath shaders: restore the float2 segment overloads, sample close distinct sites, document the border step's owner contract

Shader overloads stay native (ruling shader-overloads-stay-native): cultmath_segment_closest and
cultmath_distance_to_segment have float2 and float3 forms again, with mirror asserts and fixture families.
The rejected "avoid overloads" comment and design text are gone. The border step's outside-cell behaviour
(distance 0 with that plane's normal) is documented in C# and the HLSL header and pinned by a test; the mirror
and fixture gain distinct sites closer than 1e-2. GLSL and fixture regenerate in the next commit; this one does
not pass CommittedGlslEqualsLowering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-16 21:01:16 +02:00
commit 16657fa20f
7 changed files with 81 additions and 8 deletions

View file

@ -510,7 +510,8 @@ general). The exact power cell border is a second pass: `power_border_step(p, ow
running `float4(unit normal toward the owner, distance)` and the radical plane of `owner` against `other`, with
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. `power_border(p, sites)` is that loop.
(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.
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
@ -521,8 +522,7 @@ apply. `CultSiteCells` holds the two score fields and the two site indices (floa
circle, negative inside it, -1 when `a` and `b` coincide. `soft_gabriel_gate(sites, first, second, softness)`
is `smoothstep(-softness, softness, least margin over every other site)` on positions alone, 1 when no other site
exists; it is C# only. `distance_to_segment(point, start, end)` has out-free 2D and 3D forms beside the `out`
form, all over one public `segment_closest` per dimension. The shader twins keep one name each (3D only, a 2D
caller uses z = 0), so none joins the overload rename list.
form, all over one public `segment_closest` per dimension, each with its 2D and 3D shader overload.
## Intervals

View file

@ -608,7 +608,7 @@ CultCellular cultmath_cellular(float3 p)
// 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);
// Shader names are unique (one 3D form of segment distance; a 2D caller uses z = 0), so none joins the overload rename list.
// 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,6 +688,17 @@ float cultmath_gabriel_margin(float3 a, float3 b, float3 other)
return (cultmath_distance(other, (a + b) * 0.5) - r) / r;
}
float2 cultmath_segment_closest(float2 point_value, float2 start, float2 end)
{
float2 segment = end - start;
float segmentLengthSq = cultmath_lengthsq(segment);
if (segmentLengthSq <= 0.0)
return start;
float t = cultmath_saturate(dot(point_value - start, segment) / segmentLengthSq);
return start + segment * t;
}
float3 cultmath_segment_closest(float3 point_value, float3 start, float3 end)
{
float3 segment = end - start;
@ -699,6 +710,11 @@ float3 cultmath_segment_closest(float3 point_value, float3 start, float3 end)
return start + segment * t;
}
float cultmath_distance_to_segment(float2 point_value, float2 start, float2 end)
{
return cultmath_distance(point_value, cultmath_segment_closest(point_value, start, end));
}
float cultmath_distance_to_segment(float3 point_value, float3 start, float3 end)
{
return cultmath_distance(point_value, cultmath_segment_closest(point_value, start, end));

View file

@ -69,7 +69,9 @@ public static partial class math
// difference (w1 - w2)(w1 + w2), never from a difference of two large squared distances, so it does not
// cancel for a far query or close sites. border is the running (unit normal toward the owner, distance),
// starting at (0, 0, 0, 1.0e30): the cell's boundary distance is the least plane distance over every other
// site. A site coincident with the owner has no plane and leaves border unchanged.
// site. A site coincident with the owner has no plane and leaves border unchanged. The step trusts the caller's
// owner: a p outside the owner's cell has a negative signed distance to some plane, which is clamped to 0, so the
// step reports distance 0 with that plane's normal, a plausible on-the-border answer, not an error.
public static float4 power_border_step(float3 p, float4 owner, float4 other, float4 border)
{
var d = other.xyz - owner.xyz;

View file

@ -522,6 +522,8 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z >
private sealed record Family(string Glsl, string Tolerance, Func<System.Random, object[]> Arguments, string? Check = null, Platform Platform = Platform.None, Func<JsonArray, string>? Verify = null);
private static float Uniform(System.Random r, float lo, float hi) => lo + (hi - lo) * r.NextSingle();
private static float2 Point2(System.Random r) => new(Uniform(r, -50.0f, 50.0f), Uniform(r, -50.0f, 50.0f));
private static float3 Point3(System.Random r) => new(Uniform(r, -50.0f, 50.0f), Uniform(r, -50.0f, 50.0f), Uniform(r, -50.0f, 50.0f));
// A point on the simplex tie set, where snoise's x0 has equal components (NoiseGradTests.SnoiseIsContinuousOnTheSimplexDiagonal):
@ -797,6 +799,16 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z >
new Family("mat3 cultmath_snoise_hessian(vec3)", "ulp-bounded, coordinate bands 1e2 to 4e6", r => new object[] { BandPoint3(r, 1.0e2, 4.0e6) }, SpanAllowanceRead, Verify: cases => AssertSpanAllowance(cases, hessian: true, small: false)),
// Segment distance in 2D and 3D (one segment in eight degenerate), then the site cells: scores, fold step
// over states built by inserting 0-3 random scores (one in four with an exactly tied score), border, margin.
new Family("float cultmath_distance_to_segment(vec2, vec2, vec2)", "ulp-bounded", r =>
{
var start = Point2(r);
return new object[] { Point2(r), start, r.Next(8) == 0 ? start : Point2(r) };
}),
new Family("vec2 cultmath_segment_closest(vec2, vec2, vec2)", "ulp-bounded", r =>
{
var start = Point2(r);
return new object[] { Point2(r), start, r.Next(8) == 0 ? start : Point2(r) };
}),
new Family("float cultmath_distance_to_segment(vec3, vec3, vec3)", "ulp-bounded", r =>
{
var start = Point3(r);
@ -808,12 +820,15 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z >
return new object[] { Point3(r), start, r.Next(8) == 0 ? start : Point3(r) };
}),
// The border step: owner and other random (one in six coincident), a running border either empty or a
// random (normal, distance).
// random (normal, distance); one in six other is a distinct site within 1e-3 of the owner (inside the 1e-2 guard a
// changed length test would trip), one in six coincident.
new Family("vec4 cultmath_power_border_step(vec3, vec4, vec4, vec4)", "ulp-bounded", r =>
{
var owner = Site(r, 0.1f, 20.0f);
var border = r.Next(2) == 0 ? new float4(0.0f, 0.0f, 0.0f, 1.0e30f) : new float4(Point3(r), Uniform(r, 0.0f, 50.0f));
return new object[] { Point3(r), owner, r.Next(6) == 0 ? owner : Site(r, 0.1f, 20.0f), border };
var pick = r.Next(6);
var near = new float4(owner.xyz + new float3(Uniform(r, 1.0e-4f, 1.0e-3f), Uniform(r, -1.0e-3f, 1.0e-3f), Uniform(r, -1.0e-3f, 1.0e-3f)), Uniform(r, 0.1f, 20.0f));
return new object[] { Point3(r), owner, pick == 0 ? owner : pick == 1 ? near : Site(r, 0.1f, 20.0f), border };
}),
new Family("vec4 cultmath_power_score(vec3, vec4)", "ulp-bounded", r => new object[] { Point3(r), Site(r, 0.1f, 20.0f) }),
new Family("vec4 cultmath_multiplicative_score(vec3, vec4)", "ulp-bounded", r => new object[] { Point3(r), Site(r, 0.1f, 100.0f, logWeight: true) }),

View file

@ -35,6 +35,8 @@ public sealed class HlslSourceCompatibilityTests
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_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)));
@ -130,6 +132,16 @@ public sealed class HlslSourceCompatibilityTests
for (var k = 0; k < 64; k++) { var values = Enumerable.Range(0, 32).Select(_ => Next()).ToArray(); cases.Add(i => values[i]); }
for (var k = 0; k < 16; k++) { var values = Enumerable.Range(0, 32).Select(_ => Next()).ToArray(); cases.Add(i => values[i & ~1]); }
// Distinct sites closer than 1e-2 (slots 3-6 are the owner and 7-10 the other site of
// cultmath_power_border_step): a length guard wider than "coincident" changes the result for these only.
for (var k = 0; k < 16; k++)
{
var values = Enumerable.Range(0, 32).Select(_ => Next()).ToArray();
for (var c = 0; c < 3; c++) values[7 + c] = values[3 + c] + (random.NextSingle() * 2.0f - 1.0f) * 1.0e-3f;
values[7] += 2.0e-4f;
cases.Add(i => values[i]);
}
// Points aimed squarely at cellular's HLSL mirror (F6/F3 Soul findings): the generic random
// and special-value cases above pass ~92 inputs through cultmath_cellular and never happen to
// hit a point where a radius-2 cell wins (about 3.5e-5 of points, Soul measured) or an

View file

@ -488,6 +488,18 @@ public sealed class SiteCellsTests
Assert.Equal(new float2(4.0f, 0.0f), segment_closest(new float2(9.0f, 3.0f), new float2(0.0f, 0.0f), new float2(4.0f, 0.0f)));
}
[Fact]
public void PowerBorderStepOutsideTheOwnersCellReportsDistanceZeroWithThatPlanesNormal()
{
var left = new float4(0.0f, 0.0f, 0.0f, 1.0f);
var right = new float4(4.0f, 0.0f, 0.0f, 1.0f);
var empty = new float4(0.0f, 0.0f, 0.0f, 1.0e30f);
var inside = power_border_step(new float3(1.0f, 0.0f, 0.0f), left, right, empty);
Assert.Equal(new float4(-1.0f, 0.0f, 0.0f, 1.0f), inside);
var outside = power_border_step(new float3(3.5f, 0.0f, 0.0f), left, right, empty);
Assert.Equal(new float4(-1.0f, 0.0f, 0.0f, 0.0f), outside);
}
[Fact]
public void PowerBorderStepLeavesTheBorderAloneForASiteCoincidentWithTheOwner()
{

View file

@ -608,7 +608,7 @@ CultCellular cultmath_cellular(float3 p)
// 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);
// Shader names are unique (one 3D form of segment distance; a 2D caller uses z = 0), so none joins the overload rename list.
// 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,6 +688,17 @@ float cultmath_gabriel_margin(float3 a, float3 b, float3 other)
return (cultmath_distance(other, (a + b) * 0.5) - r) / r;
}
float2 cultmath_segment_closest(float2 point_value, float2 start, float2 end)
{
float2 segment = end - start;
float segmentLengthSq = cultmath_lengthsq(segment);
if (segmentLengthSq <= 0.0)
return start;
float t = cultmath_saturate(dot(point_value - start, segment) / segmentLengthSq);
return start + segment * t;
}
float3 cultmath_segment_closest(float3 point_value, float3 start, float3 end)
{
float3 segment = end - start;
@ -699,6 +710,11 @@ float3 cultmath_segment_closest(float3 point_value, float3 start, float3 end)
return start + segment * t;
}
float cultmath_distance_to_segment(float2 point_value, float2 start, float2 end)
{
return cultmath_distance(point_value, cultmath_segment_closest(point_value, start, end));
}
float cultmath_distance_to_segment(float3 point_value, float3 start, float3 end)
{
return cultmath_distance(point_value, cultmath_segment_closest(point_value, start, end));