The mirror compiles under using static CultMath.math, so it proves composition, not intrinsic rules; HlslSemanticsTests alone pins those. design.md also states where FMin/FMax/Saturate turn NaN or infinity into numbers and tells callers to check isnan first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.9 KiB
Design
CultMath exists to make renderer math readable in every place it needs to live: Rust hot kernels, C# host code, and HLSL shader code.
Parity Target: HLSL
HLSL is the semantic authority for every type and intrinsic HLSL defines. The
goal is source compatibility: HLSL-shaped math compiles as C# under
using static CultMath.math;. C# forces float literal suffixes (0.5f); the
rest of the spelling should survive the move.
That makes these HLSL rules, not options:
- Every swizzle exists.
float2/3/4,int2/3/4, andbool2/3/4expose every read swizzle of length 2 to 4, repeats included (v.wwxy), in bothxyzwandrgbanaming (never mixed), plus single-componentr/g/b/a.double2/3get lengths 2 and 3 only, because there is nodouble4. Components and swizzles are writable:v.xz = f2;,c.rgb *= 0.5f;, andv[1] = s;work. Setters exist only for non-repeating masks, because HLSL rejectsv.xx = .... - Every HLSL mixed constructor exists, as a struct constructor and as a
mathfunction:float4(float2, float2),float4(float, float2, float),float3(float, float2), the copy formfloat3(float3), and the rest, for the float, int, and bool families (double gets constructors only; amath.double3function would hide thedouble3type name). - Swizzles and mixed constructors are generated.
tools/generate-swizzles.ps1writes the checked-insrc/CultMath/Swizzles.g.cs(partial structs plus a partialmath). Edit the generator, rerun it, and commit both; never edit the output by hand. - Comparison operators (
<,>,<=,>=,==,!=) are component-wise and returnbool2/bool3/bool4. Reduce them withanyandall.Equals/GetHashCodestay value-based so vectors still work as dictionary keys. select(condition, whenTrue, whenFalse)uses HLSL's argument order.- Matrices are row-major: constructors take rows,
m[i]returns a reference to row i (som[1][2] = s;writes one element),_mRCis row R column C, and products go throughmul(mul(m, v)treatsvas a column,mul(v, m)as a row).*on matrices would be component-wise in HLSL, so CultMath does not define it. m[r][c] = vwrites through locals, array elements, fields of classes, and ref locals. Through a readonly field, aninparameter, a property (auto-properties included), or a static readonly value such asfloat3x3.identity, C# takes a defensive copy: the write compiles and is silently lost. Copy the matrix to a local, write to it, and assign it back.- Scalar/vector arithmetic and comparisons have explicit overloads on both sides rather than leaning on implicit scalar splat.
- Intrinsics return HLSL's types and edge cases:
signreturnsint/intN(NaN gives 0);step(y, x)isx < y ? 0 : 1(NaN gives 1), which is how dxc lowers it, in C# and in the HLSL mirror; float and doubleminandmax, and floatclampandsaturate, follow the DXILFMin/FMax/Saturateoperations dxc emits for them:min(a, b)isa < b ? a : b,max(a, b)isa >= b ? a : b, a NaN operand returns the other (sosaturate(NaN)is 0),clamp(x, a, b)ismin(max(x, a), b), andsaturate(x)ismin(1, max(0, x)), which maps -0 to +0;any/allaccept numeric vectors (a component is true when it is not 0);min,max, andabson int vectors return int vectors.
dxc marks float arithmetic and compares fast (no NaNs) unless a value is
precise, so drivers may optimize NaN handling away and NaN results on real
GPUs are not guaranteed. CultMath defines the CPU result by dxc's lowering and
the DXIL operation spec (DirectXShaderCompiler docs/DXIL.rst).
NaN does not reliably propagate through CultMath, just as it does not on the
GPU. Because min, max, and saturate return the non-NaN operand, some
functions turn NaN or infinity into numbers: smoothstep(a, a, a) is 0;
infinite inputs to smoothstep and the intercept helpers give numbers; the
bezier, smoothstep01, and smootherstep functions return finite values for
NaN input; and normalize((NaN, 2.41)) is (NaN, 2.41e20), because the NaN
length is clamped to the epsilon and the other component is divided by it.
Callers that must detect bad data should check isnan before calling these
functions.
Deferred until a consumer needs them: float4x4, transpose, determinant,
and HLSL's one-based _11.._33 element names.
HLSL Target: Source Transformations
HlslSourceCompatibilityTests compiles the real shaders/CultMath.hlsl with
Roslyn against CultMath. These transformations are the complete list; if a body
needs anything else, the test fails:
- Include guards (
#ifndef/#define/#endif) are dropped. - Functions taking
Texture2D/SamplerStateare dropped; GPU resources have no CultMath analog. - File-scope
static constbecomesconst. - Floating literals gain
f(0.5becomes0.5f). - The file body is wrapped in a class, since C# has no free functions; HLSL functions become private instance methods.
HLSL constructs that stay outside the target and must not appear in the mirror:
const locals of vector type (C# const is primitives only; write a plain
local) and writes through a swizzle component (v.xy.x = s, which C# rejects as
modifying a temporary).
The HLSL side is checked too: tools/get-dxc.ps1 fetches Microsoft's
DirectXShaderCompiler, and
dxc -T lib_6_3 -HV 2021 shaders/CultMath.hlsl must compile cleanly.
The mirror test compares bit patterns (any NaN equals any NaN; -0 differs from
0). It proves the text of CultMath.hlsl computes the same float32 results as
C# math on the CPU, not that a GPU agrees: driver sin precision alone makes
cultmath_hash and value noise differ bit for bit on hardware. The mirror is
compiled under using static CultMath.math;, so its intrinsics are C# math
itself: the test proves the composition in the text, not the intrinsic rules,
which only HlslSemanticsTests pins.
Where HLSL is silent, CultMath keeps its own decisions and does not defer to
Unity.Mathematics: normalize is safe (divides by max(length, 1e-20)),
hash returns float, and Random is CultMath's own xorshift32. Engine-shaped
helpers that HLSL lacks (float2x2.Rotate, float3x3.Euler, quaternion,
snoise) enter only when a consumer needs them, with their semantics stated in
code.
Known C# friction: under using static CultMath.math; the constructor
functions (float3(...), float3x3(...)) hide the type names in member access,
so static members need a qualified type (CultMath.float3x3.Euler(...)).
Rules
- Keep type names HLSL-shaped:
float2,float3,float4,float3x3, andmath. - Keep Rust as the owner of native hot-path kernels and parity fixtures.
- Keep C#, shader, and scripting surfaces as runtime wrappers, not competing math authorities.
- Keep
shaders/CultMath.hlslas the canonical shader mirror for shared CultMath functions HLSL does not already provide (HLSL intrinsics such asmul,any, andlogneed no mirror). Project-local shader helpers should move here once more than one organ needs them or once CPU/GPU parity matters. - Implement public shader semantics directly; do not copy Unity.Mathematics.
- Prefer plain mutable value types with public component fields, as HLSL does.
- Prefer component-wise overloads over clever generic machinery until the API earns that complexity.
- Keep the surface boring and exact. Fill what a consumer uses, guided by HLSL semantics; noise primitives, matrices, quaternions, and packing helpers need an owner before they enter.
- Keep the core assembly engine-free. Unity conversions live in the Unity
package's
CultMath.UnityBridgeassembly.
Non-Goals
- Compiling C# to shaders.
- Generating HLSL from Rust or Rust from HLSL.
- Replacing
System.Numericsfor general .NET work. - Recreating every Unity.Mathematics type because a spreadsheet somewhere says “coverage.” That way lies decorative obesity.