MPL-2.0 is file-level and the Phacelle port sat inside MIT files. phacelle and
CultPhasor now live in math.Phacelle.cs and CultPhasor.cs, and cultmath_phacelle
in CultMath.Phacelle.hlsl (included by CultMath.hlsl, both tracked copies), each
with the MPL header and Johansen / Luke Mitchell attribution. math stays one
partial class, so math.phacelle is called exactly as before. The GameCult.Math
package declares MIT AND MPL-2.0 and packs THIRD-PARTY-NOTICES.md and the new
include; the Unity LICENSE.md points at CultLib; build-unity-package.ps1
requires the new include.
The random sweeps never met a cell whose box bound sits just under the 2.25 cut
while its weight is still nonzero, so cuts of 2.245, 2.24 and 2.2 survived.
Six hard-coded points (found by an offline search) sit next to such cells; the
C# pruned-vs-unpruned test and the HLSL mirror test both use them, with a
witness check that re-derives the property from the hash.
Docs: the prune keeps about 45 of 64 cells (14 is the nonzero-weight count),
the support radius is 1.49998 (d^2 = 2.24995), normalization's range and
saturation, W = 0 only for adversarial hashes, and the support-edge gradient
seam size.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The generic mirror comparison passes about a hundred inputs through the shader and
missed a mirror whose prune cut was 2.0 instead of 2.25, because only a rare cell has
a nonzero weight under that cut. Twenty thousand random arguments compare the shader
text and math.phacelle field by field and kill that mutant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cut 2a-ii-b. phacelle(float3 p, float3 side, float offset, float normalization)
returns CultPhasor { float4 cos; float4 sin }, each float4(gradient.xyz, value.w).
- 4x4x4 cells, pcg3d-hashed jitter in [-0.5, 0.5), weight max(0, exp(-2 d^2) - 0.01111),
normalization as upstream (lpmitchell/AdvancedTerrainErosion PhacelleNoise). The
caller supplies the stripe wave vector; offset is in cycles.
- The gradient is exact: weight derivatives and the normalization are included, not
upstream's grad(cos) ~ -sin * side.
- Cells whose per-axis lower bound is at least 2.25 have weight exactly 0 and are
skipped. An internal overload switches the prune off so the test can pin bit
equality without a second copy of the loop.
- cultmath_phacelle and CultPhasor mirror it in both tracked CultMath.hlsl copies
(the same blob). The mirror test compares the struct field by field; it now covers
34 functions.
- PhacelleTests: value against an independent double-precision reference, quarter-cycle
phase rotation, gradient against central differences on both sides of the
normalization threshold (excluding the threshold and each cell's weight-support
edge, where the exact gradient is discontinuous), stripes along the flow on the unit
sphere (0.215 against the 0.25 bound), step-difference continuity across cell planes,
and pruned == unpruned bit for bit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul: no defect; independent port of stegu 22434e04d7 matches (3D and 2D), goldens
confirmed independently, value jump 2.2e-6 over 20k crossings, gradient FD 9.7e-6,
every hand mutant killed. 2D follows upstream by ruling. Release follow-ups: rebuild
the Unity CultMath.dll, CHANGELOG entry, pin the two HLSL copies equal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Batches of small boxed problems whose passes end with the KKT test failing and
the cost unmoved (dependent free columns, releases the factor drops, weak-direction
noise-floor problems). They must report Converged, and land within the documented
gap of an exact active-set enumeration. Pins the stall comparison and the cost
scale in Objective, which no test reached before.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Seven values change: four snoise(float3) in HlslSemanticsTests and three snoise(float2)
in MathTests (the 2D (0,0) golden is 0 either way). Regenerated from the code after the
continuity probe passed. The tests no longer claim to match Unity.Mathematics, whose
snoise keeps the old constants.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Drops the unreproducible floors and detection-scale claims, and the seam-fluke
consistency filter (the spikes it hid were the snoise seam, now fixed and pinned by the
continuity probe). Tightens the tolerances to at least 5x the harness maximum (ridged
0.5 -> 0.05), corrects the point count in the bit-equality test, and caps the ridged
compounding loop so a mutant fails loudly instead of hanging.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
With b along the smallest left singular vector, |A^T b| is tiny, so the KKT
tolerance (1e-6 of it) sits below the gradient's rounding noise at cond(A) >=
1e6 and can never be met. The solver must stop on the progress rule, in at most
six passes, never at the cap. This is the path the progress rule exists for;
the earlier fixtures reached it only by accident.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two tolerance behaviours had no observer: the scale taken at the box point
nearest the origin (a 6e-5 release on a 1000-scale problem, in either column
position), and the strict comparisons when the scale and the gradient are both
exactly zero (b = 0 at the origin takes no iteration).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The full-step clamp repairs an out-of-box start on a free column, so the
warm-start clamp was invisible to the old test (Stryker: five survivors). The
new table runs one column with b beyond the box and a cap of 0, where the
solver returns the clamped start untouched, across finite, infinite and NaN
starts and finite and infinite bounds.
Workspace junk (including NaN doubles) is now checked on random boxed problems
that release bounds and use the tolerance, with bit-identical x and equal
iteration counts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Stryker left the refinement step (removed, negated, or subtracted) unobserved,
and it is unobservable for a reason: refinement with a same-precision residual
does not improve forward error, and Cholesky is backward stable, so the
residual the KKT test reads is already at rounding level. Measured on n=24
problems at cond(A) 1e3..1e6, the solver with and without the step gives the
same iteration counts and the same x error against a Gram-Schmidt QR
reference (both limited by cond(A^T A) * eps).
The refinement pass, its dp buffer and the scratch vector are deleted;
WorkspaceLength returns to 4n^2 + 15n. The doc states where the normal
equations stop being optimal (cost gap below 1e-6 |b|^2 through cond(A) 3e5,
about 2e-5 at 1e6, independent of the scale of A) and that a ridge row beyond
that is the caller's policy.
status is now initialised: the unassigned local made Stryker discard every
mutant inside Solve as a compile error, so 255 mutants were never measured.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- n=24 fully free problems at cond 1e2/1e3/1e4, and A x 1e6 with unscaled
bounds, converge in at most two iterations (a regression to cap-spinning
fails on the count, not on a clock).
- b = 0 from a far start reaches the optimum instead of the start.
- Warm-start clamp table, free duplicate column gets no step, parallel columns
at absolute scales 1e3 and 1e-3, near-singular free sets confirm once and
stop, near-collinear optimum at A x 1e6, and zero allocation on the invalid,
converged and cap paths (first and second call).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
snoise_grad.w is bit-equal to snoise (stronger than the spec's 1e-6
floor, measured over 20,000 points). fbm_grad/ridged_grad with 1/3
octaves are pinned bit-exact against a closed form built with the same
incremental gain/lacunarity multiply chain the production loop uses,
catching both a swapped-argument mutant and an applied-once mutant.
The shared invariant-8 property harness central-differences all three
functions' own .w, filtering simplex-lattice seam flukes by comparing
the estimate at two step sizes (measured tolerances and detection
floor in the harness comment). A dedicated fixture bisects to a snoise
zero crossing and pins that ridged_grad's two one-sided gradients there
have opposite sign.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds explicit function-presence assertions and bumps the reflective
bit-parity oracle's expected mirror count from 30 to 33 now that the
three new cultmath_ mirrors are covered automatically.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EveryMirrorFunctionMatchesCSharpMath's generic random/special-value cases
never happen to hit a point where a radius-2 cell wins (about 3.5e-5 of
points) or an integer point at or past 2^23, so an HLSL mutant of cellular's
search radius, its lower-bound prune, or the removed F2=0 guard could change
cultmath_cellular's real output and still pass this test.
Adds three targeted point cases, reusing CellularAndSminGradTests' own
fixtures so the mirror test does not carry a second copy of the construction:
- the six points from AxisAlignedRadiusTwoOffsets/FindPointRealizingOffset,
each of which realizes a different axis-aligned radius-2 offset as F1 or
F2 in the independent 7x7x7 oracle;
- four integer points at 2^24 (both signs), where distinct cells' jittered
features can round onto the same float32 value, reaching the F2 = 0 guard;
- one exact F1 = F2 tie point, the midpoint of two adjacent cells' feature
points (bit-exact-equidistant from both by construction, no search
needed).
Verified against four HLSL mutants applied to shaders/CultMath.hlsl on
scratch copies (not committed) and reverted after each check:
- `dz < 2` instead of `dz <= 2`: killed by the axis-slice point at
(-56.671272, -67.01973, -79.001).
- `dx` starting at -1 instead of -2: killed by the axis-slice point at
(62, -77.91331, -79.42451).
- pruning against f1 instead of f2: killed by an axis-slice point and both
of the 2^24 integer points (3 mismatches).
- the F2 = 0 guard removed (`grad2 = (p - c2) / f2`, unconditional): killed
by the (2^24, 2^24, 2^24) integer point (NaN vs the guarded zero).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CellularAndSminGradTests.cs changes for cut 2a-i's pass-2 findings:
- FeaturePoint and the two other independent pcg3d/pcg4d call sites now hash
int3(cell)/int4(int3(cellId), 0) to match production's integer-hashing fix
(previous commit); the oracle would otherwise diverge from cellular() and
every comparison test would fail.
- New JitterAndIdPassAChiSquareUniformityTest: jitter.x/y/z and id each pass
a chi-square uniformity test (16 bins, 15 df, critical value 37.7 at
alpha=0.001) over 200,000 cells. This is the behavioural pin for the
hashing fix; reverting to pcg3d(cell)'s float-bit hash fails it.
- Two false comments corrected to the measured facts: the
NearestFeatureWins test's claim that |component|=2 offsets are "provably
never the true nearest" is false (their lower bound is 1, not sqrt(3); Soul
measured 3 F1 wins in 2,000,000 samples), and SearchReachesEveryAxis...'s
claim of "lower bound is exactly 2" and "zero hits in 3,000,000" is also
false (lower bound is 1; Soul measured about 1 hit in 28,000).
- PrunedSearchIsBitIdentical... renamed to
...BelowTwoToTheTwentyFifth and its comment states that domain explicitly
(design.md, previous commit).
- GradientTolerance split into SminGradientTolerance (2.5e-4f) and
CellularGradientTolerance (1.3e-3f): the two functions have different
curvature (smin_grad is exactly quadratic in a band; cellular's F1/F2 are
real distance fields), so a shared 3e-4 tolerance carried almost no
headroom over either's actual measured noise (re-measured here at 5.3e-4
for smin's old eps, 2.0e-4/2.6e-4 for cellular), not the "~6e-5" the old
comment claimed. smin_grad's eps moves from 0.001f to 0.01f (no truncation
cost there; dilutes float noise 8x) with a matching widened seam-exclusion
band so p +/- eps cannot step across the seam the check at p already
avoided. cellular's eps stays at 0.0005f (a small sweep found it already
near the truncation/noise sweet spot for a real distance field). Both new
tolerances keep >5x headroom over their own measured worst case and both
still die under a 1.01x-scale mutant on the returned gradient — smin down
to 1.0005x, cellular (M9/M16-style, either gradient) down to 1.002x.
- GradientMatchesCentralDifferencesAwayFromTheF1EqualsF2Seam also excludes a
band around F2 = F3 now, not just F1 = F2: the second-nearest feature's
identity swaps there too, a genuine kink the old test could silently hit
(an unfiltered large sample found a 0.78 edge error this way, swamping the
~2e-4 the filtered test actually sees).
- AxisAlignedRadiusTwoOffsets and FindPointRealizingOffset are extracted from
SearchReachesEveryAxisAlignedRadiusTwoSlice as internal fixtures so
HlslSourceCompatibilityTests (next commit) can reuse the same construction
instead of a second copy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Stryker (mtp, --since 29e50ad3) found 261 survivors / 24 timeouts across the
whole repo; triaging the ones in this fix batch's own new code (cellular's
loop, its prune, cellular_unit) surfaced one real gap disguised as
collateral timeout noise from a batched infinite-loop mutant: dz < 2 (and
the dy/dx equivalents), silently dropping the +2 layer on one axis alone,
passed every existing random-sampled test. That is not the sweeps being
weak: the exactness proof's own margin (F1, F2 always < sqrt(3), radius-2
cells' lower bound is exactly 2) makes a genuine win from any single
|offset component| = 2 cell astronomically rare over uniformly random
points (measured zero hits in 3,000,000 samples).
SearchReachesEveryAxisAlignedRadiusTwoSlice constructs a real example for
each of the six axis-aligned radius-2 offsets directly: for a candidate
integer cell, build the query point that is the closest point of that
cell's own unit cube to the target neighbour's actual jittered feature.
That construction finds a hit for every offset within a couple thousand
candidate cells, deterministically. This kills dz < 2 / dy < 2 / dx < 2 (all
manually confirmed).
Also manually confirmed via Stryker's own reported mutants: the dz--/dy--/
dx-- (increment-to-decrement) mutants are genuine infinite loops, correctly
caught as timeouts. The f2 >= 0.0f mutant (disabling the F2 = 0 guard) is
killed by the existing NoNaNAtIntegerPointsNear2To24 test. The remaining
survivors/timeouts in the new code (the lower-bound prune's >>= vs >
comparison, its arithmetic terms, and cellular_unit's >> vs >>> shift on an
already-unsigned operand) are equivalent mutants: since the per-axis lower
bound is always 0 or 1 by construction, and pruning is a proven-safe
optimization that never changes cellular's output, no mutation confined to
"when does the prune trigger" can be observed by any value-based test.
Verified by hand: disabling pruning entirely reproduces the unpruned
reference bit for bit, as it must.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cut 2a-i fix batch 1, findings F1, F3, F4, F5, F6, F7 (Soul falsification of
the first landing):
- cellular now searches a provably exact 5x5x5 neighbourhood (radius 2), not
3x3x3: 3x3x3 missed the true F2 for 9 of 400,000 sampled points. The
comment in math.cs proves 5x5x5 always contains the true F1 and F2 (both
are always < sqrt(3), every excluded cell is always > 2). A cheap
position-only lower-bound prune skips the pcg3d/jitter/distance work for
cells that provably cannot beat the running F2, without changing any
output bit (F6).
- F2 = 0 now gets the same degenerate-gradient guard F1 = 0 already had,
fixing a NaN that was reachable past |p| of about 2^23 (F3).
- id now comes from pcg4d over the winning cell alone, never from the pcg3d
hash that produced that cell's jitter, so id and jitter are no longer
deterministic functions of each other (F4).
- cellular_unit maps the top 24 bits instead of a plain divide-by-2^32, so
the mapping is always inside [0, 1); the old formula rounded
0xFFFFFF80..0xFFFFFFFF up to exactly 1.0f (F7).
- Central-difference tolerances tightened from 0.02 to 3e-4 (measured float
noise at the committed eps is about 6e-5), and cellular's own gradient
test now differentiates cellular's RETURNED .w values instead of a
duplicate FindF1F2 search, so a mutant that corrupts only the returned
gradient can't hide behind an independently-computed value (F1, F5).
- The struct-mirror comparison in HlslSourceCompatibilityTests now checks
field name and declared type in declaration order, not just position, so
a reordered-and-relabelled mirror struct (H2) or a renamed/retyped one
(H3) both fail the walk instead of surviving on values alone (F2).
- FindF1F2 widened to a true independent 7x7x7 brute-force oracle; new tests
pin production's exact agreement with it, the pruned search's bit-for-bit
equivalence to an unpruned 5x5x5 reference, no NaN at 2^24, and id's
correlation with jitter under 0.05.
- Comment/doc fix: smin_grad is Quilez's quadratic-polynomial smin, not
cubic.
Both shaders/CultMath.hlsl and unity/.../Shaders/CultMath.hlsl stay
byte-identical.
Manually verified mutants M9 (grad1 x 1.01), M16 (grad2 x 1.01), and M18 (id
from hash.y) all die against these tests; H2/H3 struct mutants die via the
new comparator tests. Local dotnet test: 191/191 passing. FXC cs_5_0 compile
of a scratch kernel using both cultmath_smin_grad and cultmath_cellular
succeeds.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dotnet test found two failures where a fixed-decimal-place Assert.Equal
snapped two numbers that already agreed to within about 2e-5 to different
rounded digits, because their true difference straddled a rounding boundary
(0.824987829 vs 0.825006127 -> 0.82 vs 0.83). Both were central-difference
checks (smin_grad's own invariant-8 property test, and cellular's gradient-
matches-central-differences test), not a correctness bug in the primitives.
AssertWithinTolerance replaces the precision-based assertion for both central-
difference comparisons with an explicit absolute-difference check, which
states the actual claim (agreement within the finite-difference step's own
truncation error) without the rounding-boundary artifact. Also dropped an
unused local function (F2At) the build flagged.
Found by the third ygg-verify.sh test run on 77c3580.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Inside namespace CultMath.Tests, unqualified `Random` resolves to the
enclosing CultMath namespace's own Random struct (design.md: "Random is
CultMath's own xorshift32") before `using System;` is even considered, so
`new Random(seed)` in the new test file was constructing CultMath.Random,
which has no NextSingle(). HlslSourceCompatibilityTests.cs already works
around this the same way (`new System.Random(...)`); CellularAndSminGradTests
now qualifies every use the same way.
Found by the second ygg-verify.sh build/test run on e613ed6.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CellularAndSminGradTests pins the rules the cut map calls out, each against a
mutant it must kill:
- smin's value never exceeds the ordinary minimum, and matches min with that
input's own gradient once |a.w - b.w| exceeds k on either side.
- smin_grad's gradient matches central differences of its own .w at random
seeded points (the formula is exact, not an approximation, so no exclusion
band is needed away from the |a.w-b.w|=k seam itself).
- A direct case falsifies blending the gradient with a constant 0.5 or with
(1 - h) instead of h.
- cellular's F1 never exceeds F2, matched against an independent brute-force
Worley search that shares only the public pcg3d hash, not cellular's control
flow.
- The true nearest feature wins even when an independent search finds it in
the (1, 1, 1) neighbour offset, the last one cellular's own loop order
visits, ruling out a "first found" or early-exit mutant.
- The gradient at a feature point is finite zero, not NaN.
- Both nearest and edge gradients match central differences away from a band
around the F1 = F2 set, where the nearest and second-nearest identity swap
discontinuously.
- id is constant across small moves that stay in the same feature's region and
differs from a distant cell's id.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
HlslSourceCompatibilityTests previously flattened only bare vector returns to
scalar leaves. Values() now recurses through struct fields of any type, so a
struct return (invariant 8's one shape, e.g. CultCellular) is compared field
by field down to bit-for-bit scalars, exactly like a vector return already
was. Components() stays scoped to scalar-typed fields, since it only ever
builds function arguments and must not recurse.
TransformHlslToCSharp gains transformation 5: struct fields gain `public`,
since HLSL has no field access-modifier concept and a C# struct's fields
default to private, which would hide them from Values()' reflection. The old
wrap-in-a-class transformation is renumbered 6. design.md's transformation
list documents both, matching design.md:92's numbered list.
StructReturnComparisonRejectsAMismatchedField is the extension's own test: two
hand-built structs with the same shape (mirroring a real mirror-vs-C#
comparison) prove the field-by-field, bit-for-bit walk actually rejects a
struct whose leaf field differs, or whose nested vector's own component
differs, not just a length check.
EveryMirrorFunctionMatchesCSharpMath and HlslMirrorTests' required-symbol list
now cover cultmath_smin_grad and cultmath_cellular; the mirrored-function
count moves from 28 to 30.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Stryker.NET becomes CultLib's mutation tool (CultMath config, baseline 2243
mutants, 350 survived). Behavioural tests close the three areas Aetheria
leans on: the BatchMath SIMD body, first_order_intercept's branches, and
equality and comparison across the vector types. 350 -> 264 survivors;
Soul passed the cut after one accumulation test was added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul's pass on 41440cb found BatchMath.cs:115
(AddRadialFalloffAcceleration2D(ReadOnlySpan<float2>, ...)) 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 <noreply@anthropic.com>
The vector-vector self-tie added in the previous commit does not cover
float2/float3/float4's vector-scalar and scalar-vector overloads: their
existing no-tie fixture (a distinct vector against a single scalar) never
had a component equal to that scalar, so their boundary-flip mutants
(< to <=, > to >=) kept surviving. Add a uniform vector (every component
the same value) against a scalar equal to that value, tying every
component of both scalar overloads at once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The no-tie fixture in each *ComparisonOperatorsDifferPerComponent test
correctly kills direction mutations (< swapped for >) but leaves every
strict/non-strict boundary swap (< to <=, > to >=) surviving, since with
no equal component the two behave identically. That default is
appropriate for a float-threshold epsilon check, but these are
general-purpose vector comparison operators (CultMath's own
RectIntersectionTreatsTouchingEdgesAsContact test compares literal
endpoints), and for the int types an exact tie is the routine case, not
the rare one. Add a complementary "compared against itself" call per type
(vector-vector for double2/double3/int2/int3/int4, plus vector-scalar and
scalar-vector for float2/float3/float4) so every component ties and the
boundary swaps are actually exercised.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Precision` was declared `const float`, so `Assert.Equal(a, b, Precision)`
bound to xunit's absolute-tolerance overload (allowing any two values
within 4.0 of each other) instead of the intended 4-decimal-place
overload. Hand-mutating the `t2 > 0 ? min(t1, t2) : t1` ternary to always
return `t1` (math.cs:287) exposed it: the assertion silently accepted a
result almost 0.27 away from the expected value. Declaring it `const int`
selects the correct overload; confirmed the same hand mutation now fails.
Also add the float2-overload euler-integration length-validation test the
mutation report showed missing (BatchMath.cs:208's ValidateEqualLengths
call had no test at all).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul-style self-check against the actual mutation report (not just green
tests) found several fixtures whose values coincidentally produced the
same output on the buggy path as on the correct one:
- BatchMath's scalar-tail fixtures put the one remainder particle/step
either outside the falloff radius or mask-frozen, so the y-axis
arithmetic a mutation would touch was never reached with an observable
effect. Pin the remainder particle in range (radial falloff) and dynamic
(Euler integration) so the scalar tail's own math is exercised, not just
the vector body's.
- The float2-overload of AddRadialFalloffAcceleration2D had no no-op
(radius <= 0 / strength == 0) test at all.
- first_order_intercept_time's negative-determinant test used a
perpendicular relative velocity, making b = 2*dot(vel, pos) exactly
zero; a mutant that drops the "no solution" branch entirely and falls
through to the linear fallback coincidentally computes 0 too. Use a
non-perpendicular fixture so the fallback's nonzero value is
distinguishable from the correct zero.
- No fixture ever reached the `t1 > 0` branch at all (a > 0, both
quadratic roots real and positive), so the boundary/negate checks
guarding it and the `t2 > 0 ? min(t1, t2) : t1` line were never
covered. Add that case.
- bool2.@false/@true, bool3.@true and bool4.@false have no consumer
anywhere in the suite (bool3.@false and bool4.@true are read by
HlslSemanticsTests); pin all six vector constants directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hand-verified that weakening float3.Equals from && to || passes the whole
suite. The same shape recurs with no shared base type across float2/3/4,
double2/3, int2/3/4, bool2/3/4, quaternion, float2x2 and float3x3. Add
data-driven tests, per component position, that a value differing at
exactly one component is unequal under Equals/==/!=, that GetHashCode
stays consistent with Equals, and that </>/<=/>= (including the
vector-scalar and scalar-vector overloads on the float types) each report
a different result per component. float2x2/float3x3 are covered at row
granularity since they have no == operator; quaternion and bool2/3/4 have
no ordering operators and are covered for equality only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
math.first_order_intercept_time had a single closing-target test. Add
coverage for zero relative velocity, the a-approx-zero linear fallback, a
target that outruns the projectile (negative determinant), a receding
target with both quadratic roots non-positive, and a closing case with
off-origin, off-axis shooter and target state. The closing cases assert
that the projectile (inheriting the shooter's velocity) and the target
actually coincide at the returned time; the no-solution branches pin the
value the source's own contract specifies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both existing BatchMath tests used 2-3 particles while LaneCount >= 4, so
only the scalar tail ever ran, and both held y at 0 for every particle, so
every y-axis mutation was invisible. Add fixtures sized LaneCount * 3 + 1
with distinct nonzero x/y per particle, pinned against a scalar reference
computed independently in the test, plus explicit coverage of the early
return, negative deltaTime, length-mismatch, and empty-span guards on all
four BatchMath overloads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-trip test (ErfinvInvertsErf) was the only defense of
erfinv's accuracy, and it is blind by construction: it can only prove
erf and erfinv agree with each other, not that either is correct. A
hand mutation of a single tail coefficient degraded erfinv's worst
error 300-400x (5.0e-7 to 2.0e-4) and every committed test still
passed. Added ErfinvMatchesReferenceValues, pinned against reference
values from bisecting the platform's double-precision erf (Python's
math.erf, glibc-backed) rather than this package's own erf, out to
|y| = 0.9999, at the same 2e-6 absolute bar erf's own reference test
carries. Expected values are computed for the actual float32-rounded
input, not the decimal literal, because erfinv's derivative diverges
near +-1 and that rounding alone moves the true answer by more than
the error bar if ignored.
Verified against three hand mutations, each restored byte-exactly
afterward:
- erfinv tail leading coefficient 2.83297682f -> 2.83317682f: killed
(4 assertions failed).
- erfinv tail second coefficient 1.00167406f -> 1.00187406f: killed
(4 assertions failed).
- erf's `p` coefficient 0.3275911f -> 0.3275930f: survives. Soul
measured this one 2.3x worse but still under the 2e-6 bar the spec
pins; this is not a fixture gap, it is the mutant staying inside the
accuracy envelope the tests are supposed to allow. Recorded here
rather than tightening the bar past what the spec asked for.
Added ErfinvAtDomainEdgesAndBeyond, pinning the domain and edge
behavior from the matching source comment: erfinv(1) = +infinity,
erfinv(-1) = -infinity, and NaN outside [-1, 1] including +-infinity
and NaN input.
Deleted ErfinvIsMonotonicallyIncreasing: it asserted strict
monotonicity, which erfinv does not have. Scanning every representable
float32 in [0, 0.999999] finds 9662 backward steps, first at
y = 0.00022214651, worst drop 5.960464e-8 (one ULP at that scale) --
invisible only because the old fixture stepped by 0.001. erf has the
same shape: 100 backward steps over [0, 4.5], worst drop 1.192093e-7.
Replaced it with two tests: ErfinvIsMonotonicWithinFloatingPointTolerance,
pinning monotonicity to within 1e-6 (comfortably above the measured
noise floor), and ErfCellMassesStayNonNegativeAcrossRealisticGeometries,
the consumer-level property that actually matters for fire control:
differences of adjacent erf values (cell probability mass) stay
non-negative across 16/64/256/1024/4096 cells over +-4 sigma, plus a
tight 1 sigma spread and a very tight 0.25 sigma spread.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CultMath.Tests is on xunit.v3. Stryker's default vstest runner cannot
capture coverage against xunit.v3's test host and hangs retrying test
sessions indefinitely (observed: 2096 mutants queued, zero completed after
~9 minutes of retries). Stryker's Microsoft.Testing.Platform runner
(-t mtp, still preview) works correctly against xunit.v3 and completed the
same 1351-mutant run in ~4.5 minutes. Pin it in config so a plain
`dotnet stryker` works without a CLI flag.
Add docs/mutation-testing.md: how to run it, the --since form for a single
cut, the triage rules, a known coverage-capture caveat in the mtp runner
(confirmed by hand-mutation against static readonly fields), and the
packages still needing their own config.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Scope mutation testing to CultMath.Tests only, matching Aetheria's
stryker-config.json shape: Standard level, progress/html/json reporters,
break threshold 0 (score is not a gate). Mutate only CultMath's own
sources, excluding the generated Swizzles.g.cs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers the pinned accuracy bar against tabulated erf values (0, +-0.5,
+-1, +-2, +-3, within 2e-6), odd symmetry, the +-1 limits, erfinv's
round trip through erf over y in [-0.999, 0.999], and erfinv monotonicity.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CultMath's hash is the shader-style frac(sin(x) * 43758.5453) and
returns float. Unity.Mathematics' hash returned a well-mixed uint, and
seeding from the float hash collapses: a 100x100 position grid gave 1625
distinct values in Aetheria's zone generator.
- math.pcg(uint), pcg3d(int3/float2/float3), and pcg4d(int4/float4)
follow Jarzynski and Olano, "Hash Functions for GPU Rendering"
(JCGT 9(3), 2020). pcg3d and pcg4d are the paper's code from page 32.
pcg is O'Neill's RXS-M-XS 32/32 output over one LCG step, with the
constants checked against pcg-c's pcg_variants.h.
- CultMath has no uint vectors, so pcg3d and pcg4d carry uint bit
patterns in int3/int4. The float overloads hash IEEE-754 bits via the
HLSL intrinsic asuint(float), which is new. pcg3d(float2) holds z at
0, the paper's route for 2 -> N inputs.
- The HLSL mirror gains the same functions, written over scalar uints
so the text compiles both in dxc 1.9.0.5402 (lib_6_3, HV 2021, clean)
and in C#. HlslSourceCompatibilityTests now compares int and uint
arguments and results by value; a wrong shift in the mirror's pcg4d
gives 208 mismatches.
- Known-answer vectors come from an independent Python transcription.
pcg3d separates a 100x100 half-unit grid (the full int3). One
component alone has one birthday collision there, as a 32-bit value
can. The same grid on the integer lattice has none.
- design.md states that the hashes are integer-exact in HLSL and on the
CPU: dxc wraps uint multiply and add modulo 2^32, as C# unchecked does.
- Bump to 0.2.3 and rebuild the tracked Unity DLL and pdb.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dxc 1.9.0.5402 lowers float2/3/4 normalize(x) to x * Rsqrt(Dot(x, x)),
and DXIL.rst defines Rsqrt as 1 / sqrt(src). A zero vector normalizes
to NaN and a NaN component poisons every component. CultMath divided
by max(length, 1e-20), which returned 0 for a zero vector and a huge
finite component beside a NaN one.
- math.normalize(float2/3/4) is now value * (1 / sqrt(dot(value, value))).
- The HLSL mirror drops cultmath_normalize and its epsilon constant: it
now equals the normalize intrinsic, and nothing consumed it.
- HlslSemanticsTests pins zero and NaN normalize, the rsqrt bit pattern,
and zero length/distance. design.md states the rule and notes that
DXIL.rst's Rsqrt special-value table is a copy of Round's.
- GameCult.Geometry's iso-surface gave degenerate triangles (an inside
vertex on the isovalue) a zero normal through the old epsilon. It now
guards them explicitly, with a test.
- quaternion normalize keeps its identity fallback: quaternion is not
an HLSL type.
- Bump to 0.2.2 and rebuild the tracked Unity DLL and pdb.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Aetheria Cut 8a needs int2(float2), clamp(int, int, int), and
frac(double). Fill them in CultMath as dxc compiles them:
- generate-swizzles.ps1 emits same-size conversion constructors and
math functions between float, int, and bool vectors. dxc lowers
float to int as fptosi (toward zero; NaN and out-of-range undefined),
numeric to bool as != 0 (fcmp une, NaN is true), bool to numeric as
0 or 1. floatN(int) splat keeps floatN(0) from going ambiguous
against floatN(intN).
- Int and int-vector clamp are min(max(x, a), b), matching dxc's
IMin(IMax(x, a), b); inverted bounds return b.
- frac(double) keeps double precision. dxc demotes double frac to
float, as it does exp and lerp; CultMath's existing double overloads
already diverge that way, and design.md now says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The mirror compiles under using static CultMath.math, so it proves composition,
not intrinsic rules; HlslSemanticsTests alone pins those. design.md also states
where FMin/FMax/Saturate turn NaN or infinity into numbers and tells callers to
check isnan first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dxc lowers double min/max to dx.op.binary.f64 opcodes 36/35 (FMin/FMax),
the same operations as float, so Math.Min/Math.Max (NaN-propagating) was the
wrong rule. Double min/max now use the float formulas, with NaN and signed-zero
assertions beside the float ones. Tracked Unity DLL rebuilt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>