2026-09-14 13:57:03 +02:00
using System.Reflection ;
using System.Text.RegularExpressions ;
using Microsoft.CodeAnalysis ;
using Microsoft.CodeAnalysis.CSharp ;
using Xunit ;
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
using static CultMath . math ;
namespace CultMath.Tests ;
/// <summary>
2026-09-14 13:57:03 +02:00
/// The real shader mirror, <c>shaders/CultMath.hlsl</c>, compiles as C# against CultMath under
/// <c>using static CultMath.math;</c> after exactly the source transformations listed in
/// docs/design.md (HLSL Target: Source Transformations). <see cref="TransformHlslToCSharp"/> is
/// that list in code; if a body needs anything else, this test fails.
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
/// </summary>
public sealed class HlslSourceCompatibilityTests
{
2026-09-14 13:57:03 +02:00
[Fact]
public void ShaderMirrorBodiesCompileAsCSharp ( )
{
var ( assembly , errors ) = CompileShaderMirror ( ) ;
Assert . True ( assembly is not null , "HLSL mirror did not compile as C#:" + Environment . NewLine + string . Join ( Environment . NewLine , errors ) ) ;
var shader = assembly ! . GetType ( "CultMathHlsl.HlslShader" ) ! ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_snoise" , typeof ( float3 ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_snoise" , typeof ( float2 ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_value_noise_bicubic" , typeof ( float2 ) ) ) ;
Extend the HLSL mirror comparison to one struct return shape
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>
2026-09-25 19:18:21 +02:00
Assert . NotNull ( ShaderFunction ( shader , "cultmath_smin_grad" , typeof ( float4 ) , typeof ( float4 ) , typeof ( float ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_cellular" , typeof ( float3 ) ) ) ;
2026-10-16 18:17:40 +02:00
var siteCells = shader . GetNestedType ( "CultSiteCells" , BindingFlags . Public | BindingFlags . NonPublic ) ! ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_power_score" , typeof ( float3 ) , typeof ( float4 ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_multiplicative_score" , typeof ( float3 ) , typeof ( float4 ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_site_cells_empty" ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_site_cells_insert" , siteCells , typeof ( float4 ) , typeof ( int ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_site_cells_border" , siteCells ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_gabriel_margin" , typeof ( float3 ) , typeof ( float3 ) , typeof ( float3 ) ) ) ;
2026-10-16 19:15:55 +02:00
Assert . NotNull ( ShaderFunction ( shader , "cultmath_power_border_step" , typeof ( float3 ) , typeof ( float4 ) , typeof ( float4 ) , typeof ( float4 ) ) ) ;
2026-10-17 11:31:46 +02:00
Assert . NotNull ( ShaderFunction ( shader , "cultmath_lattice_site" , typeof ( float2 ) , typeof ( float ) , typeof ( float ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_lattice_weight" , typeof ( float ) , typeof ( float ) , typeof ( float ) , typeof ( float ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_lattice_distance" , typeof ( float2 ) , typeof ( float2 ) , typeof ( float ) , typeof ( float2 ) , typeof ( float ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_lattice_smooth_step" , typeof ( float ) , typeof ( float ) , typeof ( float ) , typeof ( float ) ) ) ;
2026-10-16 21:01:16 +02:00
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 ) ) ) ;
2026-10-16 19:15:55 +02:00
Assert . NotNull ( ShaderFunction ( shader , "cultmath_segment_closest" , typeof ( float3 ) , typeof ( float3 ) , typeof ( float3 ) ) ) ;
2026-10-16 18:17:40 +02:00
Assert . NotNull ( ShaderFunction ( shader , "cultmath_distance_to_segment" , typeof ( float3 ) , typeof ( float3 ) , typeof ( float3 ) ) ) ;
2026-09-26 02:54:17 +02:00
Assert . NotNull ( ShaderFunction ( shader , "cultmath_snoise_grad" , typeof ( float3 ) ) ) ;
2026-10-14 06:31:43 +02:00
Assert . NotNull ( ShaderFunction ( shader , "cultmath_snoise_hessian" , typeof ( float3 ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_integral_clamp_linear" , typeof ( float ) , typeof ( float ) , typeof ( float ) , typeof ( float ) , typeof ( float ) , typeof ( float ) ) ) ;
2026-09-26 02:54:17 +02:00
Assert . NotNull ( ShaderFunction ( shader , "cultmath_fbm_grad" , typeof ( float3 ) , typeof ( int ) , typeof ( float ) , typeof ( float ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_ridged_grad" , typeof ( float3 ) , typeof ( int ) , typeof ( float ) , typeof ( float ) ) ) ;
CultMath: phacelle, Johansen's Phacelle noise on 3D cells with an exact gradient
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>
2026-09-29 23:51:02 +02:00
Assert . NotNull ( ShaderFunction ( shader , "cultmath_phacelle" , typeof ( float3 ) , typeof ( float3 ) , typeof ( float ) , typeof ( float ) ) ) ;
CultMath intervals: cultmath_iv_* over float2(lo, hi) in C# and HLSL, with snoise and fbm ball bounds
math.Interval.cs and shaders/CultMath.Interval.hlsl (included from CultMath.hlsl after Phacelle;
both tracked copies) add iv_point, iv_add, iv_sub, iv_neg, iv_mul, iv_scale, iv_abs, iv_min,
iv_max, iv_sqr, iv_sqrt, iv_exp, iv_clamp, iv_saturate, iv_smoothstep, iv_lerp, iv_snoise_ball
and iv_fbm_ball, and SNOISE_LIPSCHITZ = 10.099261 (1.10 x the refined max |snoise_grad| that
NoiseBoundTests.MeasureLipschitz prints). iv_exp and iv_smoothstep widen by one ulp through
asfloat(uint), added beside asuint as the HLSL intrinsic. The mirror test now compares 52
functions; the Unity package and the NuGet content files carry the new include.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 19:52:33 +02:00
Assert . NotNull ( ShaderFunction ( shader , "cultmath_iv_snoise_ball" , typeof ( float3 ) , typeof ( float ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_iv_fbm_ball" , typeof ( float3 ) , typeof ( float ) , typeof ( int ) , typeof ( float ) , typeof ( float ) ) ) ;
2026-10-04 09:47:49 +02:00
Assert . NotNull ( ShaderFunction ( shader , "cultmath_iv_snoise_ball" , typeof ( float2 ) , typeof ( float ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_af_snoise" , typeof ( float3 ) , typeof ( float3 ) , typeof ( float ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_af_fbm" , typeof ( float3 ) , typeof ( float3 ) , typeof ( float ) , typeof ( int ) , typeof ( float ) , typeof ( float ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_af_frustum_ball" , typeof ( float2 ) , typeof ( float ) , typeof ( float ) , typeof ( float ) , typeof ( float ) ) ) ;
2026-10-16 02:58:05 +02:00
var ( spanPath , spanStage ) = SpanTypes ( shader ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_span_fbm" , spanPath , spanStage , typeof ( int ) , typeof ( float ) , typeof ( float ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_span_ridged" , spanPath , spanStage , typeof ( int ) , typeof ( float ) , typeof ( float ) ) ) ;
Assert . NotNull ( ShaderFunction ( shader , "cultmath_span_stage" , typeof ( bool ) , spanPath , spanStage , typeof ( int ) , typeof ( float ) , typeof ( float ) ) ) ;
2026-09-14 13:57:03 +02:00
}
2026-10-16 02:58:05 +02:00
// The mirror's struct types are private nested types of HlslShader (a struct without a modifier).
private static ( Type Path , Type Stage ) SpanTypes ( Type shader ) = >
( shader . GetNestedType ( "SpanPath" , BindingFlags . Public | BindingFlags . NonPublic ) ! , shader . GetNestedType ( "SpanStage" , BindingFlags . Public | BindingFlags . NonPublic ) ! ) ;
2026-09-14 13:57:03 +02:00
// HLSL functions carry no access modifier, so they compile as private instance methods.
private static MethodInfo ? ShaderFunction ( Type shader , string name , params Type [ ] parameters ) = >
shader . GetMethod ( name , BindingFlags . Instance | BindingFlags . NonPublic , null , parameters , null ) ;
2026-10-14 20:26:51 +02:00
// Shader constants with no C# twin of the same name (empty when every constant has one).
2026-10-17 16:09:18 +02:00
private static readonly string [ ] ConstantsWithoutTwin = { "CULTMATH_MAX_SITES" } ;
2026-10-14 20:26:51 +02:00
/// <summary>
2026-10-14 20:37:35 +02:00
/// Every static const of the compiled HLSL mirror (a static literal CULTMATH_* field, whatever its access) is the
2026-10-14 20:26:51 +02:00
/// same float32, bit for bit, as the math constant of the same name without the prefix (private ones
/// included), so a pin edited in one runtime and not the other fails here whichever constant it is.
/// A constant with neither a twin nor a listing in ConstantsWithoutTwin fails too.
/// </summary>
[Fact]
public void EveryShaderConstantEqualsItsCSharpTwin ( )
{
var ( assembly , errors ) = CompileShaderMirror ( ) ;
Assert . True ( assembly is not null , string . Join ( Environment . NewLine , errors ) ) ;
var shader = assembly ! . GetType ( "CultMathHlsl.HlslShader" ) ! ;
2026-10-14 20:37:35 +02:00
var constants = shader . GetFields ( BindingFlags . Public | BindingFlags . NonPublic | BindingFlags . Static )
2026-10-17 16:09:18 +02:00
. Where ( f = > f . IsLiteral & & f . FieldType = = typeof ( float ) & & f . Name . StartsWith ( "CULTMATH_" , StringComparison . Ordinal ) )
2026-10-14 20:26:51 +02:00
. ToArray ( ) ;
Assert . True ( constants . Length > = 10 , $"only {constants.Length} shader constants were found by reflection" ) ;
foreach ( var constant in constants )
{
var name = constant . Name [ "CULTMATH_" . Length . . ] ;
var twin = typeof ( math ) . GetField ( name , BindingFlags . Public | BindingFlags . NonPublic | BindingFlags . Static ) ;
if ( twin is null )
{
Assert . True ( ConstantsWithoutTwin . Contains ( constant . Name ) , $"{constant.Name} has no math.{name} and is not listed in ConstantsWithoutTwin" ) ;
continue ;
}
var shaderValue = ( float ) constant . GetRawConstantValue ( ) ! ;
var mathValue = ( float ) twin . GetValue ( null ) ! ;
Assert . True ( BitConverter . SingleToInt32Bits ( shaderValue ) = = BitConverter . SingleToInt32Bits ( mathValue ) ,
$"{constant.Name} is {shaderValue:R} in the shader and math.{name} is {mathValue:R}" ) ;
}
}
2026-10-16 01:01:54 +02:00
// Special values every mirror argument takes: zeros, ties, negatives, large values, NaN and both infinities.
private static readonly float [ ] Specials = { 0.0f , - 0.0f , 1.0f , - 1.0f , 0.5f , - 2.75f , 3.0f , 1.0e-7f , 1.0e4f , - 1.0e4f , 1.0e7f , float . NaN , float . PositiveInfinity , float . NegativeInfinity } ;
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
/// <summary>
2026-10-16 02:58:05 +02:00
/// Every mirror function matches the math function of the same name, public or private (a function with no
/// twin fails, and nothing is exempt), with a struct parameter mapped by name and fields to the CultMath
/// type of the same name, and returns the same bits (any NaN equals any NaN;
2026-09-14 14:32:43 +02:00
/// -0 differs from 0) over random inputs, every special value in every argument (ties, zeros,
2026-10-16 01:01:54 +02:00
/// negatives, NaN, infinities, large values, integer lattice points), special pairs, and vectors whose first two
2026-09-14 14:32:43 +02:00
/// components tie (x0.x == x0.y in snoise). This proves the text of CultMath.hlsl computes the same
/// float32 results as C# math on the CPU. It does not prove GPU agreement: driver sin precision alone
2026-09-14 14:47:10 +02:00
/// makes cultmath_hash and value noise differ bit for bit on hardware. The mirror's intrinsics are C#
/// math itself (it compiles under using static CultMath.math), so this proves the composition in the
/// text, not the intrinsic rules; HlslSemanticsTests alone pins those.
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
/// </summary>
[Fact]
public void EveryMirrorFunctionMatchesCSharpMath ( )
2026-09-14 13:57:03 +02:00
{
var ( assembly , errors ) = CompileShaderMirror ( ) ;
Assert . True ( assembly is not null , string . Join ( Environment . NewLine , errors ) ) ;
var shaderType = assembly ! . GetType ( "CultMathHlsl.HlslShader" ) ! ;
var shader = Activator . CreateInstance ( shaderType ) ;
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
var random = new System . Random ( 0x5EED ) ;
float Next ( ) = > random . NextSingle ( ) * 200.0f - 100.0f ;
var cases = new List < Func < int , float > > ( ) ;
foreach ( var s in Specials ) cases . Add ( _ = > s ) ;
for ( var k = 0 ; k < Specials . Length ; k + + ) { var o = k ; cases . Add ( i = > Specials [ ( i + o + 1 ) % Specials . Length ] ) ; }
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 ] ) ; }
2026-10-16 21:01:16 +02:00
// 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 ] ) ;
}
tests: pin cellular's HLSL mirror against radius-2, 2^24, and tie inputs
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>
2026-09-25 23:14:10 +02:00
// 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
// integer point at or past 2^23, so an HLSL mutant of the search radius, the prune, or the
// removed F2=0 guard can survive this test even though it changes cellular's real output.
void AddPointCase ( float3 point )
{
var values = new [ ] { point . x , point . y , point . z } ;
cases . Add ( i = > i < values . Length ? values [ i ] : 0.0f ) ;
}
// The six constructed points from CellularAndSminGradTests.SearchReachesEveryAxisAlignedRadiusTwoSlice:
// each one realizes a different axis-aligned radius-2 offset as F1 or F2, which is exactly
// the case an `if (lowerBound >= f2)` prune written as `>= f1`, or a search radius narrowed on
// one axis (`dz < 2`, `dx` starting at -1), changes.
foreach ( var offset in CellularAndSminGradTests . AxisAlignedRadiusTwoOffsets )
AddPointCase ( CellularAndSminGradTests . FindPointRealizingOffset ( offset ) ) ;
// Integer points at 2^24, several, both signs: past |p| ~= 2^23 distinct cells' jittered
// features can round onto the same float32 value (design.md, "Precision domain"), making
// F2 = 0 reachable and exercising the grad2 = f2 > 0 ? ... : zero guard this test would
// otherwise never touch.
const float twoTo24 = 16777216.0f ;
AddPointCase ( new float3 ( twoTo24 , twoTo24 , twoTo24 ) ) ;
AddPointCase ( new float3 ( - twoTo24 , - twoTo24 , - twoTo24 ) ) ;
AddPointCase ( new float3 ( twoTo24 + 3.0f , - twoTo24 + 5.0f , twoTo24 - 7.0f ) ) ;
AddPointCase ( new float3 ( - twoTo24 + 11.0f , twoTo24 - 13.0f , - twoTo24 + 17.0f ) ) ;
// One exact F1 = F2 tie point: the midpoint of two adjacent cells' feature points is
// bit-exact-equidistant from both by construction (negating a vector does not change its
// length, and p - a = -(p - b) here by symmetry), without needing to search for one.
var tieMidpoint = ( CellularAndSminGradTests . FeaturePoint ( new float3 ( 0.0f , 0.0f , 0.0f ) )
+ CellularAndSminGradTests . FeaturePoint ( new float3 ( 1.0f , 0.0f , 0.0f ) ) ) * 0.5f ;
AddPointCase ( tieMidpoint ) ;
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
var mismatches = new List < string > ( ) ;
var compared = new HashSet < string > ( ) ;
2026-10-17 16:09:18 +02:00
foreach ( var mirror in shaderType . GetMethods ( BindingFlags . Instance | BindingFlags . NonPublic ) . Where ( m = > m . Name . StartsWith ( "cultmath_" ) & & ! m . GetParameters ( ) . Any ( p = > p . ParameterType . IsArray ) ) )
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
{
var types = mirror . GetParameters ( ) . Select ( p = > p . ParameterType ) . ToArray ( ) ;
2026-10-16 02:58:05 +02:00
var counterpart = Twin ( shaderType , mirror . Name [ "cultmath_" . Length . . ] , types ) ;
Assert . True ( counterpart is not null , $"{mirror.Name} has no C# math function of the same name and parameter types, public or private" ) ;
2026-09-14 13:57:03 +02:00
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
compared . Add ( mirror . Name ) ;
foreach ( var input in cases )
{
var slot = 0 ;
2026-10-16 02:58:05 +02:00
var args = types . Select ( t = > Build ( t , ( ) = > input ( slot + + ) ) ) . ToArray ( ) ;
var expected = Values ( counterpart ! . Invoke ( null , args . Select ( a = > CopyFields ( a , TwinType ( shaderType , a . GetType ( ) ) ! ) ) . ToArray ( ) ) ! ) ;
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
var actual = Values ( mirror . Invoke ( shader , args ) ! ) ;
Extend the HLSL mirror comparison to one struct return shape
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>
2026-09-25 19:18:21 +02:00
if ( ! BitwiseEqual ( expected , actual ) )
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
{
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
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>
2026-09-25 20:40:28 +02:00
mismatches . Add ( $"{mirror.Name}({string.Join(" , ", args.Select(a => string.Join(" ", Values(a).Select(v => v.Value))))}): C# {string.Join(" ", expected.Select(v => v.Value))}, mirror {string.Join(" ", actual.Select(v => v.Value))}" ) ;
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
}
}
}
Assert . True ( mismatches . Count = = 0 , $"{mismatches.Count} mismatches:{Environment.NewLine}{string.Join(Environment.NewLine, mismatches.Take(12))}" ) ;
2026-10-17 16:09:18 +02:00
// The three site-array functions are compared by SiteArrayMirrorsMatchCSharp, which draws their counts and arrays.
2026-10-17 16:21:45 +02:00
Assert . Equal ( 140 , compared . Count ) ;
2026-10-17 16:09:18 +02:00
}
/// <summary>
/// The shader functions that take a site array and a count are bit-equal to C#'s span functions on the same
/// arguments: at least 64 compared cases each, counts 0 to the capacity with filler after the count (a loop bound that
/// ignores the count reads it), the lattice at rings up to 17. A case C# refuses is skipped and counted.
/// </summary>
[Fact]
public void SiteArrayMirrorsMatchCSharp ( )
{
var ( assembly , errors ) = CompileShaderMirror ( ) ;
Assert . True ( assembly is not null , string . Join ( Environment . NewLine , errors ) ) ;
var shaderType = assembly ! . GetType ( "CultMathHlsl.HlslShader" ) ! ;
var shader = Activator . CreateInstance ( shaderType ) ;
Assert . Equal ( 17 , SiteArrays . FarPoints [ 0 ] . Ring ) ;
Assert . All ( SiteArrays . FarPoints , far = > Assert . InRange ( far . Ring , 8 , LatticeMaxRing ) ) ;
var draws = new ( string Name , Func < System . Random , object [ ] > Draw ) [ ]
{
( "power_border" , SiteArrays . PowerBorderArguments ) ,
( "lattice_cell_distance" , SiteArrays . LatticeCellDistanceArguments ) ,
( "weighted_lattice_cells" , SiteArrays . LatticeArguments ) ,
} ;
foreach ( var ( name , draw ) in draws )
{
var mirror = shaderType . GetMethod ( "cultmath_" + name , BindingFlags . Instance | BindingFlags . NonPublic ) ! ;
var twin = typeof ( SiteArrays ) . GetMethod ( name , BindingFlags . Static | BindingFlags . NonPublic ) ! ;
Assert . Equal ( twin . GetParameters ( ) . Select ( p = > p . ParameterType ) , mirror . GetParameters ( ) . Select ( p = > p . ParameterType ) ) ;
var random = new System . Random ( 0x517E ) ;
int compared = 0 , refused = 0 ;
for ( var i = 0 ; i < 160 ; i + + )
{
var args = draw ( random ) ;
object expected ;
try { expected = twin . Invoke ( null , args ) ! ; }
catch ( TargetInvocationException e ) when ( e . InnerException is ArgumentException ) { refused + + ; continue ; }
var actual = Values ( mirror . Invoke ( shader , args ) ! ) ;
Assert . True ( BitwiseEqual ( Values ( expected ) , actual ) ,
$"{name} case {i}: C# {string.Join(" ", Values(expected).Select(v => v.Value))}, mirror {string.Join(" ", actual.Select(v => v.Value))}" ) ;
compared + + ;
}
Assert . True ( compared > = 64 , $"{name} compared only {compared} cases ({refused} refused)" ) ;
}
}
/// <summary>A consumer defines CULTMATH_MAX_SITES before including the library; the mirror compiles with the capacity changed.</summary>
[Fact]
public void TheSiteCapacityIsOverridable ( )
{
var ( assembly , errors ) = CompileShaderMirror ( defines : new Dictionary < string , int > { [ "CULTMATH_MAX_SITES" ] = 4 } ) ;
Assert . True ( assembly is not null , string . Join ( Environment . NewLine , errors ) ) ;
var shaderType = assembly ! . GetType ( "CultMathHlsl.HlslShader" ) ! ;
Assert . Equal ( 4 , ( int ) shaderType . GetField ( "CULTMATH_MAX_SITES" , BindingFlags . Public | BindingFlags . NonPublic | BindingFlags . Static ) ! . GetRawConstantValue ( ) ! ) ;
var sites = new [ ] { new float4 ( 0 , 0 , 0 , 1 ) , new float4 ( 10 , 0 , 0 , 1 ) , new float4 ( 0 , 10 , 0 , 2 ) , new float4 ( - 10 , 0 , 0 , 1 ) } ;
var border = ( float4 ) shaderType . GetMethod ( "cultmath_power_border" , BindingFlags . Instance | BindingFlags . NonPublic ) ! . Invoke ( Activator . CreateInstance ( shaderType ) , new object [ ] { new float3 ( 1 , 1 , 0 ) , sites , 4 } ) ! ;
Assert . Equal ( power_border ( new float3 ( 1 , 1 , 0 ) , sites ) , border ) ;
Extend the HLSL mirror comparison to one struct return shape
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>
2026-09-25 19:18:21 +02:00
}
2026-10-16 01:01:54 +02:00
/// <summary>
/// The 1500 arguments (a, b, lo, hi, t0, t1) at which integral_clamp_linear's zero-length rule, its lo
/// normalisation and its infinite bounds all bite: a in {-2.5, 0, 1.5}, b in {-1.5, -0, 0, 2}, lo in
/// {-inf, -1, 0.5, 3, +inf}, hi in {-inf, -0.5, 1, 4, +inf}, and (t0, t1) in {(0, 0), (1.25, 1.25), (-1, 2),
/// (2, -1), (0, 1e-30)}. One spelling, read by the mirror grid test and by the GLSL clamp shadow test.
/// </summary>
internal static IReadOnlyList < float [ ] > IntegralClampLinearEdgeGrid { get ; } = BuildIntegralClampLinearEdgeGrid ( ) ;
private static List < float [ ] > BuildIntegralClampLinearEdgeGrid ( )
{
var inf = float . PositiveInfinity ;
var grid = new List < float [ ] > ( ) ;
foreach ( var a in new [ ] { - 2.5f , 0.0f , 1.5f } )
foreach ( var b in new [ ] { - 1.5f , - 0.0f , 0.0f , 2.0f } )
foreach ( var lo in new [ ] { - inf , - 1.0f , 0.5f , 3.0f , inf } )
foreach ( var hi in new [ ] { - inf , - 0.5f , 1.0f , 4.0f , inf } )
foreach ( var ( t0 , t1 ) in new [ ] { ( 0.0f , 0.0f ) , ( 1.25f , 1.25f ) , ( - 1.0f , 2.0f ) , ( 2.0f , - 1.0f ) , ( 0.0f , 1.0e-30f ) } )
grid . Add ( new [ ] { a , b , lo , hi , t0 , t1 } ) ;
return grid ;
}
/// <summary>
2026-10-16 02:58:05 +02:00
/// The edge grid followed by the committed GLSL fixture's cases of the three integral_clamp_linear families
/// (GlslMirrorTests.IntegralClampLinearFixtureCases, decoded): one list, read by the mirror test and by the
/// clamp-shadow test. The grid fixes a in {-2.5, 0, 1.5}, so a guard keyed on a past 1.75 hides from it
/// (Soul s4); the fixture's random a in [-4, 4] closes that.
/// </summary>
internal static IReadOnlyList < float [ ] > IntegralClampLinearCases = > IntegralClampLinearEdgeGrid . Concat ( GlslMirrorTests . IntegralClampLinearFixtureCases ) . ToArray ( ) ;
/// <summary>
/// The compiled mirror's cultmath_integral_clamp_linear returns C#'s bits on every edge case (the grid and
/// the fixture's cases), and C#'s result is never NaN there. The generic case set catches each of the function's guards (the held and
2026-10-16 01:01:54 +02:00
/// kept stretch guards, the flat branch's zero-length guard) only by its rotation: with the kept guard
/// removed it finds 0 mismatches, with the held guard removed 1 once infinities join the specials, and the
/// edge grid finds 246 for either every time.
/// </summary>
[Fact]
2026-10-16 02:58:05 +02:00
public void IntegralClampLinearMirrorMatchesCSharpOnTheEdgeCases ( )
2026-10-16 01:01:54 +02:00
{
var ( assembly , errors ) = CompileShaderMirror ( ) ;
Assert . True ( assembly is not null , string . Join ( Environment . NewLine , errors ) ) ;
var shaderType = assembly ! . GetType ( "CultMathHlsl.HlslShader" ) ! ;
var shader = Activator . CreateInstance ( shaderType ) ;
var mirror = ShaderFunction ( shaderType , "cultmath_integral_clamp_linear" , Enumerable . Repeat ( typeof ( float ) , 6 ) . ToArray ( ) ) ! ;
var mismatches = new List < string > ( ) ;
2026-10-16 02:58:05 +02:00
var cases = IntegralClampLinearCases ;
foreach ( var c in cases )
2026-10-16 01:01:54 +02:00
{
var expected = integral_clamp_linear ( c [ 0 ] , c [ 1 ] , c [ 2 ] , c [ 3 ] , c [ 4 ] , c [ 5 ] ) ;
var actual = ( float ) mirror . Invoke ( shader , c . Cast < object > ( ) . ToArray ( ) ) ! ;
Assert . False ( float . IsNaN ( expected ) , $"C# returns NaN at ({string.Join(" , ", c)})" ) ;
if ( BitConverter . SingleToInt32Bits ( expected ) ! = BitConverter . SingleToInt32Bits ( actual ) )
mismatches . Add ( $"({string.Join(" , ", c)}): C# {expected:R}, mirror {actual:R}" ) ;
}
2026-10-16 02:58:05 +02:00
Assert . True ( mismatches . Count = = 0 , $"{mismatches.Count} of {cases.Count} cases differ:{Environment.NewLine}{string.Join(Environment.NewLine, mismatches.Take(12))}" ) ;
}
/// <summary>
/// At a SpanStage the shader has no identity constant for, so C#'s identity (copied field by field) is the
/// argument: the mirror's cultmath_span_fbm and cultmath_span_ridged return the field of the mirror's own
/// cultmath_fbm_grad and cultmath_ridged_grad, bit for bit, for 0 to 20 octaves over random and simplex-tie
/// points. The shader-side statement of math.SpanStage.Identity (design decision a: no shader identity).
/// </summary>
[Fact]
public void SpanStagesAtIdentityAreFbmAndRidgedInTheMirror ( )
{
var ( assembly , errors ) = CompileShaderMirror ( ) ;
Assert . True ( assembly is not null , string . Join ( Environment . NewLine , errors ) ) ;
var shaderType = assembly ! . GetType ( "CultMathHlsl.HlslShader" ) ! ;
var shader = Activator . CreateInstance ( shaderType ) ;
var ( pathType , stageType ) = SpanTypes ( shaderType ) ;
var spanArguments = new [ ] { pathType , stageType , typeof ( int ) , typeof ( float ) , typeof ( float ) } ;
var pairs = new [ ]
{
( Span : ShaderFunction ( shaderType , "cultmath_span_fbm" , spanArguments ) ! , Plain : ShaderFunction ( shaderType , "cultmath_fbm_grad" , typeof ( float3 ) , typeof ( int ) , typeof ( float ) , typeof ( float ) ) ! ) ,
( Span : ShaderFunction ( shaderType , "cultmath_span_ridged" , spanArguments ) ! , Plain : ShaderFunction ( shaderType , "cultmath_ridged_grad" , typeof ( float3 ) , typeof ( int ) , typeof ( float ) , typeof ( float ) ) ! ) ,
} ;
var identity = CopyFields ( SpanStage . Identity , stageType ) ;
var random = new System . Random ( 0x57A6E2 ) ;
var compared = 0 ;
foreach ( var p in SpanStageTests . Points ( random ) )
{
var octaves = random . Next ( 0 , 21 ) ;
var lacunarity = 1.4f + 1.2f * random . NextSingle ( ) ;
var gain = 0.3f + 0.5f * random . NextSingle ( ) ;
var path = CopyFields ( new SpanPath { midpoint = p , velocity = new float3 ( 0.6f , 0.0f , 0.8f ) , speed = 1.0f , acceleration = 0.5f , halfLength = 0.25f } , pathType ) ;
foreach ( var ( span , plain ) in pairs )
{
var field = FieldOf ( span . Invoke ( shader , new object [ ] { path , identity , octaves , lacunarity , gain } ) ! , "field" ) ;
var expected = plain . Invoke ( shader , new object [ ] { p , octaves , lacunarity , gain } ) ! ;
Assert . True ( BitwiseEqual ( Values ( expected ) , Values ( field ) ) , $"{span.Name} at {p}, {octaves} octaves: {string.Join(" ", Values(field).Select(v => v.Value))} is not {plain.Name} {string.Join(" ", Values(expected).Select(v => v.Value))}" ) ;
compared + + ;
}
}
Assert . True ( compared > = 460 , $"only {compared} identity comparisons ran" ) ;
}
/// <summary>
/// The generic comparison draws every argument from [-100, 100] (and specials), where the octave loop's
/// frequency, stretch and span terms saturate: 0 or 16 octaves, a Hessian bound pinned at its global value.
/// This sweep draws SpanStageTests' realistic stages and spans (4000, one in four with a zero axis, one in
/// four with a point span, halfLength 0), both stages, and the mirror must return C#'s bits for the whole
/// SpanNoise. Each of stretched and unstretched stages, and of positive and zero fold, must be met at least
/// 500 times, or the sweep proves nothing about that branch.
/// </summary>
[Fact]
public void SpanStageMirrorMatchesCSharpBitForBitOnRealisticSpans ( )
{
var ( assembly , errors ) = CompileShaderMirror ( ) ;
Assert . True ( assembly is not null , string . Join ( Environment . NewLine , errors ) ) ;
var shaderType = assembly ! . GetType ( "CultMathHlsl.HlslShader" ) ! ;
var shader = Activator . CreateInstance ( shaderType ) ;
var ( pathType , stageType ) = SpanTypes ( shaderType ) ;
var spanArguments = new [ ] { pathType , stageType , typeof ( int ) , typeof ( float ) , typeof ( float ) } ;
var mirrors = new [ ] { ShaderFunction ( shaderType , "cultmath_span_fbm" , spanArguments ) ! , ShaderFunction ( shaderType , "cultmath_span_ridged" , spanArguments ) ! } ;
var random = new System . Random ( 0x5EED4 ) ;
float Next ( float extent ) = > ( random . NextSingle ( ) * 2.0f - 1.0f ) * extent ;
int stretched = 0 , unstretched = 0 , folded = 0 , flat = 0 ;
for ( var i = 0 ; i < 4000 ; i + + )
{
var drawn = SpanStageTests . Stage . Draw ( random ) ;
var stage = drawn . S ;
if ( i % 4 = = 1 )
stage . axis = new float3 ( 0.0f , 0.0f , 0.0f ) ;
var halfLength = i % 4 = = 2 ? 0.0f : new [ ] { 0.02f , 0.1f , 0.5f , 2.0f } [ random . Next ( 4 ) ] ;
var span = SpanStageTests . Span . Draw ( random , new float3 ( Next ( 30.0f ) , Next ( 30.0f ) , Next ( 30.0f ) ) , halfLength , random . NextSingle ( ) * 2.0f ) ;
var arguments = new object [ ] { CopyFields ( span . Path , pathType ) , CopyFields ( stage , stageType ) , drawn . Octaves , drawn . Lacunarity , drawn . Gain } ;
if ( dot ( stage . axis , stage . axis ) > 0.0f & & stage . stretch ! = 1.0f )
stretched + + ;
else
unstretched + + ;
for ( var m = 0 ; m < 2 ; m + + )
{
var expected = m = = 0 ? span_fbm ( span . Path , stage , drawn . Octaves , drawn . Lacunarity , drawn . Gain ) : span_ridged ( span . Path , stage , drawn . Octaves , drawn . Lacunarity , drawn . Gain ) ;
var actual = mirrors [ m ] . Invoke ( shader , arguments ) ! ;
Assert . True ( BitwiseEqual ( Values ( expected ) , Values ( actual ) ) ,
$"{mirrors[m].Name} differs from C# at midpoint {span.Path.midpoint}, halfLength {halfLength:R}, {drawn.Octaves} octaves: C# {string.Join(" ", Values(expected).Select(v => v.Value))}, mirror {string.Join(" ", Values(actual).Select(v => v.Value))}" ) ;
if ( m = = 1 & & expected . fold > 0.0f )
folded + + ;
else if ( m = = 1 )
flat + + ;
}
}
Assert . True ( stretched > = 500 & & unstretched > = 500 & & folded > = 500 & & flat > = 500 ,
$"the sweep meets a branch too rarely: stretched {stretched}, unstretched {unstretched}, fold > 0 {folded}, fold = 0 {flat} of 4000" ) ;
2026-10-16 01:01:54 +02:00
}
2026-09-29 23:58:43 +02:00
/// <summary>
/// The generic comparison above passes about a hundred inputs through <c>cultmath_phacelle</c>, and
/// its prune only changes an output for the rare cell whose weight is nonzero but whose lower
/// bound sits just under the 2.25 cut (Soul's F6 lesson for cellular): a mirror whose cut is 2.0
/// survives a hundred inputs. A dense sweep of realistic arguments closes that gap.
/// </summary>
[Fact]
public void PhacelleMirrorMatchesCSharpBitForBitAcrossADenseSweep ( )
{
var ( assembly , errors ) = CompileShaderMirror ( ) ;
Assert . True ( assembly is not null , string . Join ( Environment . NewLine , errors ) ) ;
var shaderType = assembly ! . GetType ( "CultMathHlsl.HlslShader" ) ! ;
var shader = Activator . CreateInstance ( shaderType ) ;
var mirror = ShaderFunction ( shaderType , "cultmath_phacelle" , typeof ( float3 ) , typeof ( float3 ) , typeof ( float ) , typeof ( float ) ) ! ;
var random = new System . Random ( 0x5EED2 ) ;
float Next ( float extent ) = > ( random . NextSingle ( ) * 2.0f - 1.0f ) * extent ;
for ( var i = 0 ; i < 20000 ; i + + )
{
var p = new float3 ( Next ( 50.0f ) , Next ( 50.0f ) , Next ( 50.0f ) ) ;
var side = new float3 ( Next ( 9.0f ) , Next ( 9.0f ) , Next ( 9.0f ) ) ;
var offset = random . NextSingle ( ) ;
var normalization = random . NextSingle ( ) ;
var expected = Values ( phacelle ( p , side , offset , normalization ) ) ;
var actual = Values ( mirror . Invoke ( shader , new object [ ] { p , side , offset , normalization } ) ! ) ;
Assert . True ( BitwiseEqual ( expected , actual ) , $"mirror differs from C# at p = {p}, side = {side}" ) ;
}
}
2026-10-04 11:09:34 +02:00
/// <summary>
/// The generic comparison draws centre, axis and radius from [-100, 100], where the reach is far past the
/// 0.15 or so below which <c>cultmath_af_snoise</c>'s centred form is the narrower one, so an HLSL edit
/// of the choice between the centred and the Lipschitz form (a threshold, a reordered sum) survives it.
/// This sweep draws realistic slices (axis and radius log-uniform in 1e-3 to 0.5, the reach of a tile
/// near a camera), which land on both sides of the choice and next to it, for <c>cultmath_af_snoise</c>
/// and <c>cultmath_af_fbm</c>.
/// </summary>
[Fact]
public void AffineSnoiseMirrorMatchesCSharpBitForBitOnBothSidesOfItsBranch ( )
{
var ( assembly , errors ) = CompileShaderMirror ( ) ;
Assert . True ( assembly is not null , string . Join ( Environment . NewLine , errors ) ) ;
var shaderType = assembly ! . GetType ( "CultMathHlsl.HlslShader" ) ! ;
var shader = Activator . CreateInstance ( shaderType ) ;
var snoiseMirror = ShaderFunction ( shaderType , "cultmath_af_snoise" , typeof ( float3 ) , typeof ( float3 ) , typeof ( float ) ) ! ;
var fbmMirror = ShaderFunction ( shaderType , "cultmath_af_fbm" , typeof ( float3 ) , typeof ( float3 ) , typeof ( float ) , typeof ( int ) , typeof ( float ) , typeof ( float ) ) ! ;
var random = new System . Random ( 0x5EED3 ) ;
float Next ( float extent ) = > ( random . NextSingle ( ) * 2.0f - 1.0f ) * extent ;
float Reach ( ) = > 1.0e-3f * MathF . Pow ( 500.0f , random . NextSingle ( ) ) ;
int centred = 0 , lipschitz = 0 ;
for ( var i = 0 ; i < 20000 ; i + + )
{
var centre = new float3 ( Next ( 50.0f ) , Next ( 50.0f ) , Next ( 50.0f ) ) ;
var axis = normalize ( new float3 ( Next ( 1.0f ) , Next ( 1.0f ) , Next ( 1.0f ) ) ) * Reach ( ) ;
var radius = Reach ( ) ;
var expected = af_snoise ( centre , axis , radius ) ;
var actual = Values ( snoiseMirror . Invoke ( shader , new object [ ] { centre , axis , radius } ) ! ) ;
Assert . True ( BitwiseEqual ( Values ( expected ) , actual ) , $"cultmath_af_snoise differs from C# at centre = {centre}, axis = {axis}, radius = {radius}" ) ;
if ( expected . y ! = 0.0f )
centred + + ;
else
lipschitz + + ;
if ( i % 8 = = 0 )
{
var fbmExpected = Values ( af_fbm ( centre , axis , radius , 4 , 2.0f , 0.5f ) ) ;
var fbmActual = Values ( fbmMirror . Invoke ( shader , new object [ ] { centre , axis , radius , 4 , 2.0f , 0.5f } ) ! ) ;
Assert . True ( BitwiseEqual ( fbmExpected , fbmActual ) , $"cultmath_af_fbm differs from C# at centre = {centre}, axis = {axis}, radius = {radius}" ) ;
}
}
Assert . True ( centred > 2000 & & lipschitz > 2000 , $"the sweep meets one branch too rarely: centred {centred}, Lipschitz {lipschitz} of 20000" ) ;
}
2026-10-04 11:09:25 +02:00
/// <summary>
/// The generic comparison stops at 1e7, so an HLSL edit of <c>cultmath_af_range</c>'s NaN test that
/// misreads the finite values near the float maximum (or an overflowed endpoint) survives it. Forms
/// from zero to the float maximum, infinite and NaN components, both signs, in every slot: the mirror
/// returns C#'s bits, and neither endpoint is ever NaN.
/// </summary>
[Fact]
public void AffineRangeMirrorMatchesCSharpBitForBitAtTheFloatExtremes ( )
{
var ( assembly , errors ) = CompileShaderMirror ( ) ;
Assert . True ( assembly is not null , string . Join ( Environment . NewLine , errors ) ) ;
var shaderType = assembly ! . GetType ( "CultMathHlsl.HlslShader" ) ! ;
var shader = Activator . CreateInstance ( shaderType ) ;
var mirror = ShaderFunction ( shaderType , "cultmath_af_range" , typeof ( float3 ) ) ! ;
var magnitudes = new [ ] { 0.0f , 1.0f , 1.0e30f , 1.0e38f , float . MaxValue / 2.0f , float . MaxValue , float . PositiveInfinity , float . NaN } ;
var values = magnitudes . Concat ( magnitudes . Where ( m = > ! float . IsNaN ( m ) ) . Select ( m = > - m ) ) . ToArray ( ) ;
foreach ( var x0 in values )
foreach ( var a in values )
foreach ( var e in values )
{
var form = new float3 ( x0 , a , e ) ;
var expected = af_range ( form ) ;
var actual = Values ( mirror . Invoke ( shader , new object [ ] { form } ) ! ) ;
Assert . True ( BitwiseEqual ( Values ( expected ) , actual ) , $"cultmath_af_range differs from C# at {form}" ) ;
Assert . False ( float . IsNaN ( expected . x ) | | float . IsNaN ( expected . y ) , $"af_range({form}) is {expected}, a NaN endpoint" ) ;
}
}
CultMath: Phacelle port moves into MPL-2.0 files; prune cut pinned by boundary points
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>
2026-09-30 00:34:27 +02:00
/// <summary>
/// The dense sweep almost never meets a cell whose box bound is just under the 2.25 cut and whose
/// weight is still nonzero: a mirror cut of 2.245, 2.24 or 2.2 survives it. These points sit next
/// to such cells (<see cref="PhacelleTests.PruneCutPoints"/>), so the mirror must match C# bit for
/// bit, and must match the unpruned C# sum (the shader has no prune switch, so that is the
/// independent statement that the cut is exact).
/// </summary>
[Fact]
public void PhacelleMirrorMatchesCSharpBitForBitNextToCellsJustInsideTheCut ( )
{
var ( assembly , errors ) = CompileShaderMirror ( ) ;
Assert . True ( assembly is not null , string . Join ( Environment . NewLine , errors ) ) ;
var shaderType = assembly ! . GetType ( "CultMathHlsl.HlslShader" ) ! ;
var shader = Activator . CreateInstance ( shaderType ) ;
var mirror = ShaderFunction ( shaderType , "cultmath_phacelle" , typeof ( float3 ) , typeof ( float3 ) , typeof ( float ) , typeof ( float ) ) ! ;
foreach ( var p in PhacelleTests . PruneCutPoints )
{
Assert . True ( PhacelleTests . HasCellJustInsideTheCut ( p ) , $"no cell just inside the cut at {p}" ) ;
var actual = Values ( mirror . Invoke ( shader , new object [ ] { p , PhacelleTests . PruneCutSide , PhacelleTests . PruneCutOffset , PhacelleTests . PruneCutNormalization } ) ! ) ;
var pruned = Values ( phacelle ( p , PhacelleTests . PruneCutSide , PhacelleTests . PruneCutOffset , PhacelleTests . PruneCutNormalization ) ) ;
var unpruned = Values ( phacelle ( p , PhacelleTests . PruneCutSide , PhacelleTests . PruneCutOffset , PhacelleTests . PruneCutNormalization , false ) ) ;
Assert . True ( BitwiseEqual ( pruned , actual ) , $"mirror differs from C# at p = {p}" ) ;
Assert . True ( BitwiseEqual ( unpruned , actual ) , $"mirror differs from the unpruned C# sum at p = {p}" ) ;
}
}
Extend the HLSL mirror comparison to one struct return shape
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>
2026-09-25 19:18:21 +02:00
/// <summary>
/// A hand-built pair of structs standing in for a mirror struct return and its C# counterpart
/// (invariant 8's one struct return shape, e.g. <see cref="CultCellular"/>): same field shape,
/// different concrete type, exactly like the real mirror comparison. Proves the field-by-field,
/// bit-for-bit walk actually rejects a struct whose field differs; without this, the walk could
/// have been comparing lengths only, or skipping the nested vector's own components.
/// </summary>
private struct StructMirrorProbeExpected { public float4 first ; public float second ; }
private struct StructMirrorProbeActual { public float4 first ; public float second ; }
[Fact]
public void StructReturnComparisonRejectsAMismatchedField ( )
{
var expected = new StructMirrorProbeExpected { first = new float4 ( 1.0f , 2.0f , 3.0f , 4.0f ) , second = 5.0f } ;
var matching = new StructMirrorProbeActual { first = new float4 ( 1.0f , 2.0f , 3.0f , 4.0f ) , second = 5.0f } ;
var mismatchedLeaf = new StructMirrorProbeActual { first = new float4 ( 1.0f , 2.0f , 3.0f , 4.0f ) , second = 5.5f } ;
var mismatchedNestedComponent = new StructMirrorProbeActual { first = new float4 ( 1.0f , 2.0f , - 3.0f , 4.0f ) , second = 5.0f } ;
Assert . True ( BitwiseEqual ( Values ( expected ) , Values ( matching ) ) ) ;
Assert . False ( BitwiseEqual ( Values ( expected ) , Values ( mismatchedLeaf ) ) ) ;
Assert . False ( BitwiseEqual ( Values ( expected ) , Values ( mismatchedNestedComponent ) ) ) ;
2026-09-14 13:57:03 +02:00
}
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
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>
2026-09-25 20:40:28 +02:00
/// <summary>
/// Struct shapes standing in for CultCellular's own field order (<c>nearest; edge; id</c>), used
/// to falsify two named mutants (F2): H2 declares the mirror struct with its first two fields
/// swapped (<c>edge; nearest; id</c>) and swaps the assignments to match, so every leaf VALUE
/// still lands in the same POSITION as the correct struct; only the field NAME at each position
/// differs. H3 renames and retypes every field (id becomes an int carrying the same bits) while
/// keeping the original position order, so only NAME and TYPE differ, never position.
/// </summary>
private struct NearestEdgeIdExpected { public float4 nearest ; public float4 edge ; public float id ; }
private struct EdgeNearestIdSwappedNames { public float4 edge ; public float4 nearest ; public float id ; }
private struct RenamedAndRetypedFields { public float4 a ; public float4 b ; public int c ; }
[Fact]
public void StructReturnComparisonRejectsFieldsSwappedByNameEvenWhenValuesAlign ( )
{
// H2: the mutant struct's field ORDER is (edge, nearest, id), and its assignments are
// swapped to match, so declaration-position values are identical to the correct struct's.
// Only checking names (not just position) can tell these apart.
var expected = new NearestEdgeIdExpected
{
nearest = new float4 ( 1.0f , 2.0f , 3.0f , 4.0f ) ,
edge = new float4 ( 5.0f , 6.0f , 7.0f , 8.0f ) ,
id = 9.0f ,
} ;
var swapped = new EdgeNearestIdSwappedNames
{
edge = new float4 ( 1.0f , 2.0f , 3.0f , 4.0f ) , // holds what should be "nearest"'s value.
nearest = new float4 ( 5.0f , 6.0f , 7.0f , 8.0f ) , // holds what should be "edge"'s value.
id = 9.0f ,
} ;
Assert . False ( BitwiseEqual ( Values ( expected ) , Values ( swapped ) ) ) ;
}
[Fact]
public void StructReturnComparisonRejectsRenamedAndRetypedFields ( )
{
// H3: field order matches the correct struct exactly, but every field is renamed, and id is
// retyped from float to int carrying the same bit pattern the correct id would round to.
var expected = new NearestEdgeIdExpected
{
nearest = new float4 ( 1.0f , 2.0f , 3.0f , 4.0f ) ,
edge = new float4 ( 5.0f , 6.0f , 7.0f , 8.0f ) ,
id = 9.0f ,
} ;
var renamed = new RenamedAndRetypedFields
{
a = new float4 ( 1.0f , 2.0f , 3.0f , 4.0f ) ,
b = new float4 ( 5.0f , 6.0f , 7.0f , 8.0f ) ,
c = BitConverter . SingleToInt32Bits ( 9.0f ) ,
} ;
Assert . False ( BitwiseEqual ( Values ( expected ) , Values ( renamed ) ) ) ;
}
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
// Integer arguments take the bit pattern of the float input, so specials become 0, 0x80000000, NaN bits, ...
2026-10-16 02:58:05 +02:00
// A bool argument is true when its float input is positive.
private static readonly Type [ ] Scalars = { typeof ( float ) , typeof ( int ) , typeof ( uint ) , typeof ( bool ) } ;
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
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
private static object Scalar ( Type t , float f ) = > t = = typeof ( float ) ? f
2026-10-16 02:58:05 +02:00
: t = = typeof ( bool ) ? f > 0.0f
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
: t = = typeof ( int ) ? BitConverter . SingleToInt32Bits ( f ) : ( object ) ( uint ) BitConverter . SingleToInt32Bits ( f ) ;
2026-10-16 02:58:05 +02:00
private static FieldInfo [ ] InstanceFields ( Type t ) = > t . GetFields ( BindingFlags . Public | BindingFlags . Instance ) ;
// One function ARGUMENT from the case's scalars, in field order: a scalar from its float, a vector by the
// constructor taking its scalar fields, a struct field by field, recursively.
private static object Build ( Type t , Func < float > next )
{
if ( Scalars . Contains ( t ) )
return Scalar ( t , next ( ) ) ;
var fields = InstanceFields ( t ) ;
var constructor = t . GetConstructor ( fields . Select ( f = > f . FieldType ) . ToArray ( ) ) ;
if ( constructor is not null )
return constructor . Invoke ( fields . Select ( f = > Build ( f . FieldType , next ) ) . ToArray ( ) ) ;
var value = Activator . CreateInstance ( t ) ! ;
foreach ( var field in fields )
field . SetValue ( value , Build ( field . FieldType , next ) ) ;
return value ;
}
// The CultMath type a mirror parameter type stands for: itself, or for a type nested in HlslShader the
// CultMath type of the same name, only if both have the same instance fields by name, type (mapped
// likewise) and order. Null when there is no such twin.
private static Type ? TwinType ( Type shaderType , Type mirror )
{
if ( mirror . DeclaringType ! = shaderType )
return mirror ;
var twin = typeof ( math ) . Assembly . GetType ( "CultMath." + mirror . Name ) ;
if ( twin is null )
return null ;
var mine = InstanceFields ( mirror ) ;
var theirs = InstanceFields ( twin ) ;
return mine . Length = = theirs . Length & & mine . Zip ( theirs ) . All ( p = > p . First . Name = = p . Second . Name & & TwinType ( shaderType , p . First . FieldType ) = = p . Second . FieldType ) ? twin : null ;
}
// The math function of the mirror function's name, with its parameter types mapped, public or private.
private static MethodInfo ? Twin ( Type shaderType , string name , Type [ ] mirrorTypes )
{
var types = mirrorTypes . Select ( t = > TwinType ( shaderType , t ) ) . ToArray ( ) ;
return types . Any ( t = > t is null ) ? null
: typeof ( math ) . GetMethod ( name , BindingFlags . Public | BindingFlags . NonPublic | BindingFlags . Static , null , types ! , null ) ;
}
// A struct value as another struct type of the same fields by name: itself when the type is the same.
private static object CopyFields ( object source , Type target )
{
if ( source . GetType ( ) = = target )
return source ;
var copy = Activator . CreateInstance ( target ) ! ;
foreach ( var field in InstanceFields ( target ) )
field . SetValue ( copy , CopyFields ( source . GetType ( ) . GetField ( field . Name ) ! . GetValue ( source ) ! , field . FieldType ) ) ;
return copy ;
}
private static object FieldOf ( object value , string name ) = > value . GetType ( ) . GetField ( name ) ! . GetValue ( value ) ! ;
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
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
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>
2026-09-25 20:40:28 +02:00
/// <summary>
/// One flattened scalar leaf of a (possibly nested) return value: its declaration path (field
/// names joined by '.'), its declared type, and its value. Carrying path and type, not just the
/// value, is what lets the struct-return comparison reject a field that was renamed or retyped
/// but landed in the same position (F2): a positional-only comparison cannot tell "edge" holding
/// nearest's value apart from "nearest" holding it.
/// </summary>
private readonly record struct Leaf ( string Path , Type Type , object Value ) ;
Extend the HLSL mirror comparison to one struct return shape
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>
2026-09-25 19:18:21 +02:00
/// <summary>
/// Flattens a return value to its scalar leaves, recursing through struct fields of any type
/// (invariant 8's one struct return shape, e.g. <see cref="CultCellular"/>'s float4/float4/float
/// fields) so a struct return is compared field by field, down to bit-for-bit scalars, exactly
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
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>
2026-09-25 20:40:28 +02:00
/// like a bare vector return already is. Each leaf keeps the field name (and its ancestors') and
/// declared type it was read from.
Extend the HLSL mirror comparison to one struct return shape
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>
2026-09-25 19:18:21 +02:00
/// </summary>
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
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>
2026-09-25 20:40:28 +02:00
private static Leaf [ ] Values ( object value , string path = "" ) = > Scalars . Contains ( value . GetType ( ) )
? new [ ] { new Leaf ( path , value . GetType ( ) , value ) }
2026-10-14 06:31:43 +02:00
: value is float3x3 matrix
? new [ ] { matrix . _m00 , matrix . _m01 , matrix . _m02 , matrix . _m10 , matrix . _m11 , matrix . _m12 , matrix . _m20 , matrix . _m21 , matrix . _m22 }
. Select ( ( entry , k ) = > new Leaf ( ( path . Length = = 0 ? "" : path + "." ) + "_m" + k / 3 + k % 3 , typeof ( float ) , entry ) ) . ToArray ( )
Fix batch 1 (cellular/smin_grad): exact search, decorrelated id, finite F2=0
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>
2026-09-25 20:40:28 +02:00
: value . GetType ( ) . GetFields ( ) . Where ( f = > ! f . IsStatic )
. SelectMany ( f = > Values ( f . GetValue ( value ) ! , path . Length = = 0 ? f . Name : path + "." + f . Name ) )
. ToArray ( ) ;
// Compares leaves by NAME and TYPE as well as value, in declaration order, so a mirror struct
// whose fields are reordered, renamed, or retyped relative to its C# counterpart fails here even
// when the flattened values happen to line up positionally (F2's H2/H3 mutants).
private static bool BitwiseEqual ( Leaf [ ] expected , Leaf [ ] actual ) = >
expected . Length = = actual . Length & & expected . Zip ( actual ) . All ( p = >
p . First . Path = = p . Second . Path & & p . First . Type = = p . Second . Type & &
( p . First . Value is float a & & p . Second . Value is float b
? ( float . IsNaN ( a ) & & float . IsNaN ( b ) ) | | BitConverter . SingleToInt32Bits ( a ) = = BitConverter . SingleToInt32Bits ( b )
: p . First . Value . Equals ( p . Second . Value ) ) ) ;
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
CultMath: Phacelle port moves into MPL-2.0 files; prune cut pinned by boundary points
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>
2026-09-30 00:34:27 +02:00
/// <summary>
/// The text of <c>CultMath.hlsl</c> with each <c>#include "name"</c> replaced in place by the
/// included file from the same directory, as the shader compiler would resolve it. Only the
CultMath GLSL: Phacelle lowered into its own MPL-2.0 file; iv_frustum_ball and iv_exp in the fixture
GlslLowering.SeparateFiles is the one name-keyed rule: CultMath.Phacelle.hlsl
lowers into shaders/CultMath.Phacelle.glsl (MPL-2.0, guard
CULTMATH_PHACELLE_GLSL) and its include line leaves CultMath.glsl, which is
now MIT and says so in its provenance line. ReadShaderSource takes an
include predicate (one resolver); the C# mirror still inlines everything.
GlslMirrorTests pins both files, checks tokens in each, takes the union of
definitions and asserts cultmath_phacelle( and CultPhasor live only in the
Phacelle file. The fixture appends cultmath_iv_frustum_ball (exact) and
cultmath_iv_exp ("ulp-bounded, platform exp", platform named); the
comparison is per family, the platform family within 1 ulp with the
maximum printed. THIRD-PARTY-NOTICES lists CultMath.Phacelle.glsl, not
CultMath.glsl, as MPL-2.0.
Regenerated on Yggdrasil (Ubuntu 24.04.5, .NET 10.0.12); 286/286 green,
iv_exp within 0 ulp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:40:16 +02:00
/// package's own includes exist, so a missing file is an error rather than a skip. An include for
/// which <paramref name="inline"/> returns false is dropped with its line instead (the GLSL
/// lowering's separately licensed files); the C# mirror inlines every one.
CultMath: Phacelle port moves into MPL-2.0 files; prune cut pinned by boundary points
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>
2026-09-30 00:34:27 +02:00
/// </summary>
CultMath GLSL: Phacelle lowered into its own MPL-2.0 file; iv_frustum_ball and iv_exp in the fixture
GlslLowering.SeparateFiles is the one name-keyed rule: CultMath.Phacelle.hlsl
lowers into shaders/CultMath.Phacelle.glsl (MPL-2.0, guard
CULTMATH_PHACELLE_GLSL) and its include line leaves CultMath.glsl, which is
now MIT and says so in its provenance line. ReadShaderSource takes an
include predicate (one resolver); the C# mirror still inlines everything.
GlslMirrorTests pins both files, checks tokens in each, takes the union of
definitions and asserts cultmath_phacelle( and CultPhasor live only in the
Phacelle file. The fixture appends cultmath_iv_frustum_ball (exact) and
cultmath_iv_exp ("ulp-bounded, platform exp", platform named); the
comparison is per family, the platform family within 1 ulp with the
maximum printed. THIRD-PARTY-NOTICES lists CultMath.Phacelle.glsl, not
CultMath.glsl, as MPL-2.0.
Regenerated on Yggdrasil (Ubuntu 24.04.5, .NET 10.0.12); 286/286 green,
iv_exp within 0 ulp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:40:16 +02:00
internal static string ReadShaderSource ( string cultMathRoot , Func < string , bool > ? inline = null )
CultMath: Phacelle port moves into MPL-2.0 files; prune cut pinned by boundary points
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>
2026-09-30 00:34:27 +02:00
{
var shaders = Path . Combine ( cultMathRoot , "shaders" ) ;
CultMath GLSL: Phacelle lowered into its own MPL-2.0 file; iv_frustum_ball and iv_exp in the fixture
GlslLowering.SeparateFiles is the one name-keyed rule: CultMath.Phacelle.hlsl
lowers into shaders/CultMath.Phacelle.glsl (MPL-2.0, guard
CULTMATH_PHACELLE_GLSL) and its include line leaves CultMath.glsl, which is
now MIT and says so in its provenance line. ReadShaderSource takes an
include predicate (one resolver); the C# mirror still inlines everything.
GlslMirrorTests pins both files, checks tokens in each, takes the union of
definitions and asserts cultmath_phacelle( and CultPhasor live only in the
Phacelle file. The fixture appends cultmath_iv_frustum_ball (exact) and
cultmath_iv_exp ("ulp-bounded, platform exp", platform named); the
comparison is per family, the platform family within 1 ulp with the
maximum printed. THIRD-PARTY-NOTICES lists CultMath.Phacelle.glsl, not
CultMath.glsl, as MPL-2.0.
Regenerated on Yggdrasil (Ubuntu 24.04.5, .NET 10.0.12); 286/286 green,
iv_exp within 0 ulp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:40:16 +02:00
return Regex . Replace ( File . ReadAllText ( Path . Combine ( shaders , "CultMath.hlsl" ) ) , @"(?m)^#include ""([^""]+)""[ \t]*\r?$(\n?)" ,
match = > inline is null | | inline ( match . Groups [ 1 ] . Value )
? File . ReadAllText ( Path . Combine ( shaders , match . Groups [ 1 ] . Value ) ) + match . Groups [ 2 ] . Value
: string . Empty ) ;
CultMath: Phacelle port moves into MPL-2.0 files; prune cut pinned by boundary points
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>
2026-09-30 00:34:27 +02:00
}
2026-10-17 16:09:18 +02:00
/// <summary>
/// The text without its include-guard lines. A capacity macro (<c>#ifndef X</c>, <c>#define X n</c>, <c>#endif</c>, a
/// consumer-overridable default) is not a guard: <paramref name="capacity"/> gives the text it becomes, from its name and value.
/// </summary>
internal static string DropGuards ( string source , Func < string , string , string > capacity ) = >
Regex . Replace ( source , @"(?m)^#ifndef (CULTMATH_MAX_\w+)\r?\n#define \1 (\d+)\r?\n#endif[ \t]*$|^[ \t]*#(ifndef|define|endif)\b.*$" ,
match = > match . Groups [ 1 ] . Success ? capacity ( match . Groups [ 1 ] . Value , match . Groups [ 2 ] . Value ) : string . Empty ) ;
2026-10-16 01:01:54 +02:00
/// <summary>
/// The documented HLSL-to-C# transformations, and nothing else. <paramref name="members"/> is C# source a
/// test supplies, placed first in the class body: it is not a transformation of the HLSL.
/// </summary>
2026-10-17 16:09:18 +02:00
internal static string TransformHlslToCSharp ( string hlsl , string members = "" , IReadOnlyDictionary < string , int > ? defines = null )
2026-09-14 13:57:03 +02:00
{
2026-10-17 16:09:18 +02:00
// 1. Include guards (#ifndef/#define/#endif) are dropped; C# has no textual include. A capacity macro
// (#ifndef X, #define X n, #endif) becomes `const int X = n;`, or the value a test defines for X.
var source = DropGuards ( hlsl , ( name , value ) = > $"const int {name} = {(defines is not null && defines.TryGetValue(name, out var defined) ? defined : value)};" ) ;
2026-09-14 13:57:03 +02:00
// 2. Functions taking Texture2D/SamplerState resources are dropped; GPU resources have
// no CultMath analog.
source = Regex . Replace ( source , @"(?ms)^\w+ \w+\([^)]*\b(Texture2D|SamplerState)\b[^)]*\)\s*\{.*?^\}[ \t]*\r?$" , string . Empty ) ;
// 3. File-scope `static const` becomes `const` (C# consts are implicitly static).
source = Regex . Replace ( source , @"(?m)^static const " , "const " ) ;
// 4. Floating literals gain the `f` suffix; an unsuffixed C# literal is a double.
source = Regex . Replace ( source , @"(?<![\w.])(\d+\.\d*(?:[eE][+-]?\d+)?|\d+[eE][+-]?\d+)(?![\w.])" , "$1f" ) ;
Extend the HLSL mirror comparison to one struct return shape
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>
2026-09-25 19:18:21 +02:00
// 5. Struct fields gain `public`; HLSL has no field access-modifier concept, and a C# struct's
// fields default to private, which would hide them from the mirror test's reflection-based
2026-10-16 02:58:05 +02:00
// field comparison (invariant 8's one struct return shape, e.g. CultCellular) and the struct
// arguments' copies (SpanPath, SpanStage), which reflection reads and writes by field name.
Extend the HLSL mirror comparison to one struct return shape
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>
2026-09-25 19:18:21 +02:00
source = Regex . Replace ( source , @"(?ms)(struct\s+\w+\s*\{)(.*?)(\};)" , m = >
m . Groups [ 1 ] . Value + Regex . Replace ( m . Groups [ 2 ] . Value , @"(?m)^(\s*)(\w+\s+\w+;)" , "$1public $2" ) + m . Groups [ 3 ] . Value ) ;
2026-10-17 16:09:18 +02:00
// 6. An array parameter `T name[N]` becomes `T[] name`; the mirror takes the array whole and the count beside it.
source = Regex . Replace ( source , @"\b(\w+) (\w+)\[CULTMATH_MAX_\w+\]" , "$1[] $2" ) ;
// 7. The file body is wrapped in a class; C# has no free functions.
2026-10-16 01:01:54 +02:00
return "using CultMath;\nusing static CultMath.math;\nnamespace CultMathHlsl;\npublic class HlslShader\n{\n" + members + source + "\n}\n" ;
2026-09-14 13:57:03 +02:00
}
2026-10-17 16:30:19 +02:00
internal static ( Assembly ? Assembly , IReadOnlyList < string > Errors ) CompileShaderMirror ( string members = "" , IReadOnlyDictionary < string , int > ? defines = null , Func < string , string > ? mutate = null )
2026-09-14 13:57:03 +02:00
{
CultMath: Phacelle port moves into MPL-2.0 files; prune cut pinned by boundary points
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>
2026-09-30 00:34:27 +02:00
var hlsl = ReadShaderSource ( FindCultMathRoot ( ) ) ;
2026-10-17 16:30:19 +02:00
if ( mutate is not null )
hlsl = mutate ( hlsl ) ;
2026-10-17 16:09:18 +02:00
var tree = CSharpSyntaxTree . ParseText ( TransformHlslToCSharp ( hlsl , members , defines ) ) ;
2026-09-14 13:57:03 +02:00
var references = ( ( string ) AppContext . GetData ( "TRUSTED_PLATFORM_ASSEMBLIES" ) ! )
. Split ( Path . PathSeparator )
. Append ( typeof ( math ) . Assembly . Location )
. Distinct ( StringComparer . OrdinalIgnoreCase )
. Select ( path = > MetadataReference . CreateFromFile ( path ) ) ;
var compilation = CSharpCompilation . Create (
"CultMathHlslMirror" ,
new [ ] { tree } ,
references ,
new CSharpCompilationOptions ( OutputKind . DynamicallyLinkedLibrary ) ) ;
using var stream = new MemoryStream ( ) ;
var result = compilation . Emit ( stream ) ;
var errors = result . Diagnostics
. Where ( diagnostic = > diagnostic . Severity = = DiagnosticSeverity . Error )
. Select ( diagnostic = > diagnostic . ToString ( ) )
. ToList ( ) ;
return ( result . Success ? Assembly . Load ( stream . ToArray ( ) ) : null , errors ) ;
}
CultMath GLSL: shaders/CultMath.glsl generated from the HLSL by one enumerated lowering, pinned
GlslLowering (test project) is the HLSL -> GLSL ES 3.00 transformation as named steps: includes
inlined by the C# mirror's own resolver, guards and texture functions dropped, type names,
static const, casts by a balanced scanner, intrinsic renames, literals passed through, the
CULTMATH_GLSL guard and a provenance line. GlslMirrorTests pins the committed file to the
lowering (CULTMATH_WRITE_GLSL=1 regenerates it), finds no HLSL-only token, finds every public
function but the texture samplers, and pins fixtures/glsl-parity.json (256 seeded cases per
family as float32 bit patterns) to C#.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:09:44 +02:00
internal static string FindCultMathRoot ( )
2026-09-14 13:57:03 +02:00
{
var directory = new DirectoryInfo ( AppContext . BaseDirectory ) ;
while ( directory is not null & & ! File . Exists ( Path . Combine ( directory . FullName , "packages" , "cultmath" , "shaders" , "CultMath.hlsl" ) ) )
{
directory = directory . Parent ;
}
return Path . Combine ( directory ? . FullName ? ? throw new InvalidOperationException ( "CultMath package root not found." ) , "packages" , "cultmath" ) ;
}
// ---- Hand-written HLSL-shaped host code: swizzles, mixed constructors, matrix rows. ----
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
static float3 shade ( float3 position , float3 normal , float2 wind , float time )
{
float3 p = position ;
p . xz = p . xz + wind * time ;
p . y = saturate ( p . y ) ;
float3x3 basis = float3x3 ( float3 ( 1 , 0 , 0 ) , float3 ( 0 , 1 , 0 ) , float3 ( 0 , 0 , 1 ) ) ;
2026-09-14 13:57:03 +02:00
basis [ 1 ] [ 1 ] = 1.0f ;
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
float3 n = normalize ( mul ( basis , normal ) ) ;
float ndotl = saturate ( dot ( n , normalize ( float3 ( 0.3f , 1 , 0.2f ) ) ) ) ;
2026-09-14 13:57:03 +02:00
float4 color = float4 ( lerp ( float3 ( 0.1f , 0.1f , 0.2f ) , float3 ( 1 , 0.9f , 0.7f ) , ndotl ) , 1 ) ;
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
if ( any ( p < 0.0f ) )
2026-09-14 13:57:03 +02:00
color . rgb * = 0.5f ;
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
if ( all ( abs ( n ) < = 1.0f ) )
2026-09-14 13:57:03 +02:00
color . bgr = color . bgr * float3 ( 1 , 1 , 1 ) ;
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
2026-09-14 13:57:03 +02:00
color . rgb = select ( p > 10.0f , float3 ( 0 ) , color . rgb ) ;
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
float2 uv = frac ( p . xz * 0.25f ) ;
2026-09-14 13:57:03 +02:00
color . rg = lerp ( color . rg , uv , 0.1f ) ;
return float4 ( color . xy , color . zw ) . xyz ;
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
}
static float2 steer ( float2 direction , float torque )
{
float2x2 turn = float2x2 ( cos ( torque ) , - sin ( torque ) , sin ( torque ) , cos ( torque ) ) ;
float2 turned = mul ( direction , turn ) ;
return normalize ( turned ) * pow ( length ( turned ) , 1.0f ) ;
}
[Fact]
2026-09-14 13:57:03 +02:00
public void HandWrittenShaderStyleMathEvaluates ( )
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
{
2026-09-14 13:57:03 +02:00
var color = shade ( float3 ( 1.0f , 0.5f , 2.0f ) , float3 ( 0.0f , 1.0f , 0.0f ) , float2 ( 1.0f , 0.0f ) , 0.0f ) ;
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
Assert . True ( color . x > 0.1f & & color . x < = 1.0f ) ;
Assert . True ( float . IsFinite ( color . y ) & & float . IsFinite ( color . z ) ) ;
2026-09-14 13:57:03 +02:00
var far = shade ( float3 ( 20.0f , 20.0f , 20.0f ) , float3 ( 0.0f , 1.0f , 0.0f ) , float2 ( 0.0f ) , 0.0f ) ;
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
Assert . Equal ( 0.0f , far . z ) ;
2026-09-14 13:57:03 +02:00
var steered = steer ( float2 ( 1.0f , 0.0f ) , 0.5f ) ;
var viaRotate = mul ( float2 ( 1.0f , 0.0f ) , CultMath . float2x2 . Rotate ( 0.5f ) ) ;
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
Assert . Equal ( viaRotate . x , steered . x , precision : 5 ) ;
Assert . Equal ( viaRotate . y , steered . y , precision : 5 ) ;
}
}