Commit graph CultLib/packages/cultmath
Author SHA1 Message Date
Meta Vens
fd75c2acf9 Test that the float2 radial falloff overload accumulates, not assigns
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>
2026-09-24 20:13:05 +02:00
Meta Vens
41440cb538 Tie the scalar comparison overloads too, not just vector-vector
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>
2026-09-24 19:49:23 +02:00
Meta Vens
eeb67e2a22 Add tied-value comparisons to kill boundary-flip mutants for real
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>
2026-09-24 19:44:58 +02:00
Meta Vens
6d8195811d Fix a tolerance-overload bug that made intercept precision asserts inert
`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>
2026-09-24 19:38:30 +02:00
Meta Vens
446859927a Fix degenerate fixtures that let real mutants survive their own tests
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>
2026-09-24 19:26:48 +02:00
Meta Vens
8c86bc90e3 Give the CultMath package changelog its Unity meta
Soul: a git-URL package lands read-only in Unity's cache, where a file with
no meta is warned about and ignored on every import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:10:25 +02:00
Meta Vens
ab98462c06 Test vector Equals/GetHashCode/comparison operators per component
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>
2026-09-24 19:06:41 +02:00
Meta Vens
71c42b125f Test first_order_intercept's branches beyond the one happy path
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>
2026-09-24 19:06:41 +02:00
Meta Vens
e3d52406db Test BatchMath's vector body, remainder, and guard branches
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>
2026-09-24 19:06:40 +02:00
Meta Vens
20e6da5942 Release CultMath 0.2.4: erf and erfinv reach the Unity package
math.erf/math.erfinv (source, tests, README) already landed on main via
87b95ae, b688cb2, fc9ce97, 4cdc02b, 6e8362b, cc71f17. This is the release
step 0.2.3 skipped needing: bump the package version, rebuild the tracked
Unity DLL/pdb deterministically, and add the CHANGELOG.md this package
never had (docs/semver-policy.md names this exact gap).

- packages/cultmath/src/CultMath/CultMath.csproj: Version 0.2.3 -> 0.2.4.
- packages/cultmath/unity/org.gamecult.cultmath/package.json: 0.2.3 -> 0.2.4.
- packages/cultmath/unity/org.gamecult.cultmath/CHANGELOG.md: new, [0.2.4]
  entry only (Added: erf, erfinv). Earlier versions are not backfilled, per
  the semver policy's own ruling on this package's pre-existing gap.
- packages/cultmath/unity/org.gamecult.cultmath/Runtime/Plugins/CultMath.dll
  and .pdb: rebuilt via packages/cultmath/scripts/build-unity-package.ps1
  (dotnet build, netstandard2.1, Release, --no-incremental, no Source Link).
  Built on Windows; the assembly is platform-neutral IL. The script's own
  determinism check (fresh build hash == tracked hash) and its version-parity
  check (package.json version == assembly FileVersion) both pass.
- No HLSL mirror change: erf/erfinv are C#-only, and
  packages/cultmath/shaders/CultMath.hlsl already matches the copy under
  unity/org.gamecult.cultmath/Shaders/CultMath.hlsl byte for byte.

Verified independent of build-unity-package.ps1's own checks: a scratch
console app loaded the tagged cultmath-unity-v0.2.3 DLL and this build's
fresh DLL into separate AssemblyLoadContexts and diffed their public
surface. Only two members were added (math.erf(float), math.erfinv(float)),
none removed; AssemblyVersion/FileVersion/InformationalVersion match apart
from the 0.2.3 -> 0.2.4 bump; TargetFrameworkAttribute is unchanged
(.NETStandard,Version=v2.1). erf/erfinv round-trip and domain-edge behavior
(erfinv(+-1) = +-Infinity, erfinv(1.5) = NaN) verified by invoking the
loaded methods directly.

dotnet test packages/cultmath/tests/CultMath.Tests -c Release: 102 passed,
0 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:02:40 +02:00
Meta Vens
cc71f17b7b Say what the cell-mass test proves, and what it does not
Soul: a constant erf passes it. It is one necessary property, not correctness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:43:52 +02:00
Meta Vens
6e8362b7dc List erf/erfinv in the README's math vocabulary
They shipped without ever being added to the public surface list.
Scalar-only is noted explicitly: every other scalar in math.cs has
float2/3/4 overloads, erf/erfinv do not, and that stays a separate
scope decision since the only consumer (Aetheria fire control) is
scalar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:30:07 +02:00
Meta Vens
4cdc02b200 Give erf/erfinv real accuracy, domain and monotonicity tests
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>
2026-09-22 21:30:07 +02:00
Meta Vens
fc9ce970b8 Fix erfinv's sign inversion at the domain edges
math.erfinv(1.0f) returned -infinity and erfinv(-1.0f) returned
+infinity: at value = +-1, w = -log((1-value)(1+value)) = -log(0) =
+infinity, the tail branch's leading coefficient is negative, and
`return p * value` delivered the inverted infinity. erf saturates to
exactly 1.0f at 5.543 sigma, so any interval edge beyond that reaches
this bug in ordinary use.

Guard both edges explicitly before the polynomial runs: erfinv(1) is
now +infinity, erfinv(-1) is -infinity, and anything strictly outside
[-1, 1] (including +-infinity) is NaN, which is the correct answer and
was already the fallthrough behavior. Documented the domain and the
edge behavior in a comment, matching the house style `sign` already
uses at math.cs:84. The approximation itself is unchanged.

Also recorded the true measured accuracy beside erf and erfinv: worst
absolute error 6.621e-7 and 5.066e-7 respectively, both inside the
2e-6 bar this package's tests pin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 21:30:07 +02:00
Meta Vens
6a2b42f650 Pin mtp test-runner for CultMath and document mutation testing
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>
2026-09-22 21:24:05 +02:00
Meta Vens
e92413d6eb Add Stryker config for CultMath and ignore its output
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>
2026-09-22 20:50:10 +02:00
Meta Vens
b688cb2091 Cut 12.0: test erf and erfinv behaviour
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>
2026-09-22 20:42:57 +02:00
Meta Vens
87b95aefa5 Cut 12.0: add erf and erfinv to CultMath
Fire control's Gaussian scatter needs erf for a cell's probability mass
and erfinv to draw the lateral offset, both called per shot. Abramowitz-
Stegun 7.1.26 for erf (stated max abs error 1.5e-7), Giles 2010 (GPU
Computing Gems) for erfinv. Neither is an HLSL intrinsic, so there is no
shader-parity obligation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 20:42:57 +02:00
Meta Vens
089859deb4 Write and enforce the semver policy, per operator ruling on 1.0.60
1.0.60 removed public API (CultInspectorAssetPath) under a patch bump; its
own changelog says "Breaking" and the version number lied. Operator ruling:
semver is policy, strictly, for every published package family.

docs/semver-policy.md states what counts as breaking per surface (managed
API, Unity inspector attributes, CultCache wire format/store compatibility,
CultNet protocol, native bridge exports), the 0.y.z pre-1.0 convention, tag
naming, and required changelog shape. Records the 1.0.60 scar as history;
does not renumber it.

scripts/check-changelog-semver.mjs is the mechanical check: refuses a
changelog "### Breaking" section under too small a bump, and refuses a
version that skips, reverses, or repeats the previous published tag. Skips
re-litigating a version that is already tagged (rebuilding immutable
history is not a release action). Wired into publish-packages.yml for the
four npm/PyPI packages, and into build-unity-package.ps1 (cultlib),
packages/cultmath/scripts/build-unity-package.ps1 (cultmath), and a new
scripts/verify-caching-unity-release.ps1 (caching.unity has no build script
of its own to hook into).

18 node:test cases cover the required table (breaking+major/minor/patch,
non-breaking, 0.y.z, skip, downgrade) plus CLI-level git-tag resolution.
Hand-mutated each rule (bump classification, pre-1.0 lane requirement,
breaking-heading detection, breaking early-return, already-published skip)
and confirmed each kill, then restored byte-exact. No mutation harness is
committed; CultLib has none today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 22:03:22 +02:00
Meta Vens
0f2c1f04ef Add PCG integer hashes to CultMath and release 0.2.3
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>
2026-09-14 18:19:53 +02:00
Meta Vens
111b918ec0 Make CultMath normalize follow dxc and release 0.2.2
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>
2026-09-14 18:04:04 +02:00
Meta Vens
f0605367e0 Release CultMath 0.2.1
Bump the package and assembly version and rebuild the tracked Unity
DLL and pdb from a deterministic build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 16:16:32 +02:00
Meta Vens
7821e06d55 Add HLSL vector conversions, int clamp, and double frac to CultMath
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>
2026-09-14 16:16:32 +02:00
Meta Vens
d4c43ed265 Say where intrinsic rules are pinned and that NaN does not propagate
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>
2026-09-14 14:47:10 +02:00
Meta Vens
19e23db211 Make double min/max follow DXIL FMin/FMax
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>
2026-09-14 14:47:10 +02:00
Meta Vens
08e213dd7c Compare mirror results by bit pattern and say what that proves
SequenceEqual on floats treats -0 as 0. The mirror test now compares
SingleToInt32Bits, with any NaN equal to any NaN. Its summary and
design.md now say the test proves CultMath.hlsl's text computes the same
float32 results as C# math on the CPU, not that a GPU agrees: driver sin
precision alone makes cultmath_hash and value noise differ on hardware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:32:43 +02:00
Meta Vens
9407da0461 Match float min, max, clamp, and saturate to DXIL FMin/FMax/Saturate
dxc lowers float min/max/clamp to dx.op FMin/FMax (36/35) and saturate to
dx.op Saturate (7). DXIL.rst defines them: FMin(a, b) is a < b ? a : b,
FMax(a, b) is a >= b ? a : b, a NaN operand returns the other, and
Saturate is FMin(1, FMax(0, x)). C# used MathF.Min/Max, which pass NaN
through and order signed zeros, so saturate(NaN) was NaN instead of 0.

Every float overload now follows the spec, normalize uses the same max as
its mirror, and the mirror's saturate spells Saturate's operand order
(which maps -0 to +0, where clamp(x, 0, 1) keeps -0). Non-NaN results
change only in the sign of a zero. design.md records the rules and that
dxc marks float math fast, so GPU NaN results are not guaranteed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:32:19 +02:00
Meta Vens
d298d666d0 Rebuild the Unity CultMath DLL without Source Link, non-incrementally
The previous commit's DLL and pdb came from an incremental build that did
not recompile after the Source Link switch, so the pdb still carried the
commit URL. The script now builds with --no-incremental, so a stale obj
directory cannot decide what the byte check compares.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:28:44 +02:00
Meta Vens
a9a2ba69d2 Build the Unity CultMath DLL independent of the commit
The tracked DLL is committed with its source, so a build that embeds the
commit can never match it. Two places named the commit: the informational
version (0.2.0+<sha>) and Source Link's commit URL in the pdb, whose content
id the DLL also carries. The csproj drops the revision from the version, and
the Unity build turns Source Link off; NuGet packs keep it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:27:44 +02:00
Meta Vens
6bc6db68d4 Document where matrix row writes are silently lost
The operator kept the [UnscopedRef] ref this[int] row indexer as is. design.md
and the float2x2/float3x3 indexers now say where m[r][c] = v lands: locals,
array elements, class fields, and ref locals keep the write; a readonly field,
in parameter, property (auto-properties too), or static readonly identity
takes a defensive copy, so the write compiles and is lost. Fix: copy to a
local, write, assign back. Verified by compiling and running each case with
Roslyn against CultMath (no diagnostics; writes land 5, lost 0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:18:00 +02:00
Meta Vens
77ee6fe488 Pin dxc to dxc_2026_07_29 and verify its SHA256
get-dxc.ps1 fetched releases/latest with no pin or hash, so the compiler that
defines parity could change under the tests. It now downloads v1.9.2607's
dxc_2026_07_29.zip (dxcompiler 1.9.0.5402) and refuses a zip whose SHA256 is
not A1DFB116...CD241. The hash was taken from the zip downloaded from
Microsoft's GitHub release and matches the digest GitHub publishes for the
asset. Script ran end to end and installed 1.9.0.5402.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:17:11 +02:00
Meta Vens
2940ca0944 Compare every HLSL mirror function against C# math, bit for bit
Only cultmath_snoise was compared, at four inputs, so a flipped cultmath_step,
a changed hash constant, and a snoise2 tie change all passed. The mirror is
now discovered by reflection: every cultmath_* overload with a math
counterpart (26 functions) runs over 104 deterministic cases - each special
value in every argument (0, -0, +-1, ties, 1e-7, +-1e4, 1e7, NaN), special
pairs, seeded randoms, and vectors with x == y (snoise's x0.x == x0.y) - and
must match exactly. Both sides are float32 C# over the same intrinsics, so no
tolerance. A cultmath_* function with no counterpart must be named in
MirrorOnly. The four-input snoise theory is gone.

Mutations: flipped step args fail (349 mismatches); hash constants changed
by more than one float32 ulp fail (43758.55: 467, 127.2: 289, 74.8: 97).
43758.5453 -> .5454 is the same float32 and cannot fail. snoise2 x0.x > x0.y
-> >= is invisible: over 1,829,415 exact ties the result is bit-identical,
because the off-simplex corner's kernel is <= 0 on the shared edge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:17:11 +02:00
Meta Vens
e0a054a7b4 Match step's NaN result to what dxc emits
dxc 1.9.0.5402 (dxc_2026_07_29, lib_6_3, -HV 2021, -Od) lowers step(e, x) to
fcmp olt x, e; select 0 : 1, so NaN on either side gives 1. The previous
x >= e ? 1 : 0 rule came from prose, not the compiler. math.step and
cultmath_step now use x < e ? 0 : 1 for every overload; the mirror's
cultmath_step compiles to the same fcmp/select as the intrinsic. Unity shader
copy synced; DLL rebuilt and byte-checked by build-unity-package.ps1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:09:49 +02:00
Meta Vens
10b2c15fb8 Rebuild the CultMath 0.2.0 Unity DLL with the generated surface
Fresh deterministic netstandard2.1 build from the CRLF working copy a checkout
produces; build-unity-package.ps1 byte check passes. The DLL grows from
66,048 to 297,472 bytes, almost all generated swizzles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:57:03 +02:00
Meta Vens
7a1bd1ebb1 Document the full HLSL target and its source transformations
design.md states the generated swizzle and mixed-constructor surface, writable
matrix rows, HLSL intrinsic types and NaN behavior, the deferred matrix items
(float4x4, transpose, determinant, _11 names), the exact HLSL-to-C#
transformations the compatibility test applies, and the dxc check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:57:03 +02:00
Meta Vens
88eb365c05 Check bridge declarations, not file names; strip manifest BOMs
build-unity-package.ps1 read the bridge's file names to decide whether it
re-declared core CultMath sources. It now parses declarations: core type
names come from src/CultMath (comments stripped), and every bridge source may
declare types only in CultMath.UnityBridge and never a core type name.
Probed by injecting "struct float3" into UnityConversions.cs: the script
throws.

Strips the UTF-8 BOMs this branch added to both Unity package.json files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:57:03 +02:00
Meta Vens
360176a7da Compile the real HLSL mirror as C# and with dxc; fix its errors
HlslSourceCompatibilityTests now reads shaders/CultMath.hlsl, applies only
the documented transformations (drop include guards, drop Texture2D/
SamplerState functions, file-scope static const -> const, f literal
suffixes, wrap in a class), and compiles it with Roslyn against CultMath.
Any compile error fails the test (probed with an undeclared call). The
compiled cultmath_snoise(float3/float2) also matches math.snoise.

dxc -T lib_6_3 -HV 2021 rejected the mirror: cultmath_snoise(float2) called
the undeclared cultmath_simplex_mod289/permute. Renamed to the declared
cultmath_snoise_* helpers. Vector const locals became plain locals (C# const
is primitives only), and cultmath_step uses value >= edge like HLSL step and
the C# mirror. dxc 1.9.0.5402 now compiles the file cleanly. The Unity
package's Shaders copy is synced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:57:03 +02:00
Meta Vens
5d8ef0c3de Generate every HLSL swizzle and mixed constructor; fix intrinsic types
tools/generate-swizzles.ps1 writes the checked-in src/CultMath/Swizzles.g.cs
as partial structs plus a partial math: for float2/3/4, int2/3/4 and bool2/3/4
every swizzle of length 2-4 including repeats, in xyzw and rgba naming, with
setters only on non-repeating masks, plus single-component r/g/b/a. double2/3
get lengths 2-3 (there is no double4). Every HLSL mixed constructor
(float4(float2, float2), float4(float, float2, float), copy forms, ...) exists
as a struct constructor and, for float/int/bool, a math function. The
hand-written swizzles and mixed constructors it supersedes are deleted.

Matrices store private rows and return ref float2/float3 from the row
indexer ([UnscopedRef], polyfilled for netstandard2.1), so m[1][2] = s
compiles as in HLSL; _mRC are get/set properties over the rows. Equality and
hashing stay value-based.

Intrinsics follow HLSL: sign returns int/intN and yields 0 for NaN; step is
x >= y ? 1 : 0 so NaN yields 0; any/all accept float and int vectors;
min/max/abs on int vectors return int vectors, and abs(int) returns int.

Strips the UTF-8 BOMs this branch added to math.cs, CultMath.csproj and
HlslSemanticsTests.cs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:57:02 +02:00
Meta Vens
2ad8dbc9b3 Rebuild the CultMath 0.2.0 DLL from normalized sources
The previous prebuilt DLL/PDB came from LF working-copy files. The PDB embeds
source checksums, so a text=auto checkout (CRLF on Windows) built different
bytes and build-unity-package.ps1 would reject the tracked artifact. Rebuilt
from a clean checkout of the committed sources and re-verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:30:49 +02:00
Meta Vens
d9e469554d State HLSL source compatibility as CultMath's design target
The design doc now names HLSL as the parity target: HLSL-shaped math
compiles as C# under using static CultMath.math, components and swizzles are
writable, comparisons are component-wise, matrices are row-major through mul,
and select uses HLSL's argument order. "Prefer small immutable value types"
becomes plain mutable value types. CultMath keeps its own decisions where
HLSL is silent (safe normalize, float hash, its own Random), and the core
assembly stays engine-free.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:30:00 +02:00
Meta Vens
23cc86c4e9 Ship CultMath 0.2.0 to Unity with a UnityEngine conversion bridge
- Bump GameCult.Math and org.gamecult.cultmath to 0.2.0; the CultCache
  Unity tools package now depends on cultmath 0.2.0.
- Rebuild the tracked netstandard2.1 CultMath.dll/pdb with the builder's
  deterministic command; build-unity-package.ps1 verifies it byte-for-byte.
- Add Runtime/UnityBridge (CultMath.UnityBridge asmdef) with ToCultMath /
  ToUnity / ToColor extension methods for Vector2/3/4, Vector2Int/3Int,
  Quaternion and Color. C# only admits conversion operators on the source
  or target type, and the core DLL stays engine-free.
- The builder admits exactly that one bridge assembly: it must reference
  UnityEngine and CultMath.dll, and no core source may be compiled in the
  package.
- Sync the Unity copy of CultMath.hlsl and the third-party notice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:30:00 +02:00
Meta Vens
d514efebba Make CultMath HLSL-shaped: writable components, bool-vector comparisons
HLSL is now CultMath's semantic authority where it defines one.

- float2/3/4, int2, double2/3, bool2 and quaternion are plain structs with
  public mutable fields instead of record structs. Indexers and swizzles
  (float2.yx; float3 xy xz yx yz zy xyz xzy yzx zyx; float4 xy xyz xyw yzx)
  have setters.
- ==, !=, <, >, <=, >= are component-wise and return bool2/3/4. Equals and
  GetHashCode stay value-based so vectors remain dictionary keys.
  quaternion is not an HLSL type and keeps a bool ==.
- Scalar/vector arithmetic and comparisons get explicit overloads on both
  sides instead of leaning on implicit splat.
- New types: bool3, bool4, int3, int4, float2x2, float3x3 (row-major like
  HLSL, _mRC fields, Rotate / RotateX/Y/Z / Euler with math.RotationOrder).
- New intrinsics: mul (matrix-vector both sides, matrix-matrix), any, all,
  select (HLSL argument order), log, vector pow/sqrt/sin/cos/acos/atan2,
  double min/max, int3/int4/bool3/bool4/float2x2/float3x3 constructor
  functions.
- simplex_noise is renamed snoise and gains the Ashima 3D variant; the HLSL
  mirror follows as cultmath_snoise(float2/float3).
- Random gains NextFloat3() and NextFloat3(float3 max) on the same draw order.

Tests pin snoise(float3), float3x3.Euler and float2x2.Rotate to
Unity.Mathematics reference values, and an HLSL-shaped source block compiles
under using static CultMath.math.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:29:59 +02:00
Meta Vens
63d6094c94 Clarify CultMath and CultGeometry ownership 2026-08-26 17:47:06 +02:00
Meta Vens
ea88cfcd05 Move CultMath into the CultLib monorepo 2026-08-26 17:35:51 +02:00