From fd75c2acf95e0ed8285a0ac4d0943cb9a48cc4e2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 24 Sep 2026 20:13:05 +0200 Subject: [PATCH] Test that the float2 radial falloff overload accumulates, not assigns Soul's pass on 41440cb found BatchMath.cs:115 (AddRadialFalloffAcceleration2D(ReadOnlySpan, ...)) accumulates onto the caller's acceleration buffer, but no test ever seeded that buffer with nonzero values: mutating += to = passed all 133 tests. The float-array overload already had this guard (RadialFalloffAccumulatesOntoExistingAcceleration); add the float2 equivalent, with a nonzero, per-element-distinct starting buffer, mixing in-range and out-of-range particles so both the accumulated and unchanged-seed cases are checked. Checked every other += site in BatchMath.cs for the same gap: the array-overload's scalar tail and vector body, and both IntegrateSemiImplicitEuler2D overloads, all already seed their accumulator buffers (vx/vy/px/py, velocity/position) with nonzero, per-element values in their existing tests. Only this one site was bare. Verified by hand-mutating BatchMath.cs:115 (+= to =): the new test fails (expected 6.78, actual 0.48); reverted the source byte-exactly after. Co-Authored-By: Claude Opus 5.5 --- .../tests/CultMath.Tests/BatchMathTests.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/cultmath/tests/CultMath.Tests/BatchMathTests.cs b/packages/cultmath/tests/CultMath.Tests/BatchMathTests.cs index 3db3259c..62280c74 100644 --- a/packages/cultmath/tests/CultMath.Tests/BatchMathTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/BatchMathTests.cs @@ -299,6 +299,56 @@ public sealed class BatchMathTests Assert.True(outsideCount > 0, "fixture must include particles outside the radius"); } + [Fact] + public void Float2RadialFalloffAccumulatesOntoExistingAcceleration() + { + // Guards against a mutation that assigns instead of accumulating (BatchMath.cs:115): + // seed a nonzero, per-element-distinct acceleration and confirm the falloff + // contribution is added to it, not overwritten. An out-of-range particle must be left + // exactly at its seed value, not reset to zero. + var count = 5; + var positions = new float2[count]; + var seedAcceleration = new float2[count]; + for (var i = 0; i < count; i++) + { + positions[i] = new float2(i * 1.1f - 2.0f, i * -0.8f + 1.0f); + seedAcceleration[i] = new float2(i * 2.3f + 4.0f, i * -1.7f - 6.0f); + } + + var center = new float2(1.5f, -0.5f); + const float strength = 3.0f; + const float radius = 3.0f; + var acceleration = (float2[])seedAcceleration.Clone(); + + BatchMath.AddRadialFalloffAcceleration2D(positions, center, strength, radius, acceleration); + + var insideCount = 0; + var outsideCount = 0; + for (var i = 0; i < count; i++) + { + var delta = center - positions[i]; + var distanceSquared = Math.Max(delta.x * delta.x + delta.y * delta.y, 1.0e-8f); + var distance = MathF.Sqrt(distanceSquared); + + if (distance > radius) + { + outsideCount++; + Assert.Equal(seedAcceleration[i].x, acceleration[i].x, Tolerance); + Assert.Equal(seedAcceleration[i].y, acceleration[i].y, Tolerance); + continue; + } + + insideCount++; + var falloff = Math.Max(0.0f, 1.0f - distance / radius); + var contribution = delta / distance * (strength * falloff); + Assert.Equal(seedAcceleration[i].x + contribution.x, acceleration[i].x, Tolerance); + Assert.Equal(seedAcceleration[i].y + contribution.y, acceleration[i].y, Tolerance); + } + + Assert.True(insideCount > 0, "fixture must include particles inside the radius"); + Assert.True(outsideCount > 0, "fixture must include particles outside the radius"); + } + [Fact] public void Float2EulerIntegrationMatchesScalarReferenceWithNonzeroYAndMask() {