From 16657fa20fa65b41f9d5c331b493022c02fc7946 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 16 Oct 2026 21:01:16 +0200 Subject: [PATCH] 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 --- packages/cultmath/docs/design.md | 6 +++--- packages/cultmath/shaders/CultMath.hlsl | 18 +++++++++++++++++- packages/cultmath/src/CultMath/math.Sites.cs | 4 +++- .../tests/CultMath.Tests/GlslMirrorTests.cs | 19 +++++++++++++++++-- .../HlslSourceCompatibilityTests.cs | 12 ++++++++++++ .../tests/CultMath.Tests/SiteCellsTests.cs | 12 ++++++++++++ .../Shaders/CultMath.hlsl | 18 +++++++++++++++++- 7 files changed, 81 insertions(+), 8 deletions(-) diff --git a/packages/cultmath/docs/design.md b/packages/cultmath/docs/design.md index c688baa1..4a085dae 100644 --- a/packages/cultmath/docs/design.md +++ b/packages/cultmath/docs/design.md @@ -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` 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 diff --git a/packages/cultmath/shaders/CultMath.hlsl b/packages/cultmath/shaders/CultMath.hlsl index 3005f5e0..ed1d4247 100644 --- a/packages/cultmath/shaders/CultMath.hlsl +++ b/packages/cultmath/shaders/CultMath.hlsl @@ -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)); diff --git a/packages/cultmath/src/CultMath/math.Sites.cs b/packages/cultmath/src/CultMath/math.Sites.cs index 7c3cb813..feab825c 100644 --- a/packages/cultmath/src/CultMath/math.Sites.cs +++ b/packages/cultmath/src/CultMath/math.Sites.cs @@ -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; diff --git a/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs b/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs index 94bcb931..523d7d1a 100644 --- a/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/GlslMirrorTests.cs @@ -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 Arguments, string? Check = null, Platform Platform = Platform.None, Func? 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) }), diff --git a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs index ba898076..2f6a49a0 100644 --- a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs @@ -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 diff --git a/packages/cultmath/tests/CultMath.Tests/SiteCellsTests.cs b/packages/cultmath/tests/CultMath.Tests/SiteCellsTests.cs index f1eec1ff..432d669f 100644 --- a/packages/cultmath/tests/CultMath.Tests/SiteCellsTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/SiteCellsTests.cs @@ -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() { diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl index 3005f5e0..ed1d4247 100644 --- a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl +++ b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl @@ -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));