2026-05-08 01:49:44 +01:00
|
|
|
# Design
|
|
|
|
|
|
2026-06-03 02:58:56 +01:00
|
|
|
CultMath exists to make renderer math readable in every place it needs to live:
|
|
|
|
|
Rust hot kernels, C# host code, and HLSL shader code.
|
2026-05-08 01:49:44 +01:00
|
|
|
|
2026-09-14 13:30:00 +02:00
|
|
|
## 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:
|
|
|
|
|
|
2026-09-14 13:57:03 +02:00
|
|
|
- Every swizzle exists. `float2/3/4`, `int2/3/4`, and `bool2/3/4` expose every
|
|
|
|
|
read swizzle of length 2 to 4, repeats included (`v.wwxy`), in both `xyzw` and
|
|
|
|
|
`rgba` naming (never mixed), plus single-component `r/g/b/a`. `double2/3` get
|
|
|
|
|
lengths 2 and 3 only, because there is no `double4`. Components and swizzles are
|
|
|
|
|
writable: `v.xz = f2;`, `c.rgb *= 0.5f;`, and `v[1] = s;` work. Setters exist
|
|
|
|
|
only for non-repeating masks, because HLSL rejects `v.xx = ...`.
|
|
|
|
|
- Every HLSL mixed constructor exists, as a struct constructor and as a `math`
|
|
|
|
|
function: `float4(float2, float2)`, `float4(float, float2, float)`,
|
|
|
|
|
`float3(float, float2)`, the copy form `float3(float3)`, and the rest, for the
|
|
|
|
|
float, int, and bool families (double gets constructors only; a `math.double3`
|
|
|
|
|
function would hide the `double3` type name).
|
|
|
|
|
- Swizzles and mixed constructors are generated. `tools/generate-swizzles.ps1`
|
|
|
|
|
writes the checked-in `src/CultMath/Swizzles.g.cs` (partial structs plus a
|
|
|
|
|
partial `math`). Edit the generator, rerun it, and commit both; never edit the
|
|
|
|
|
output by hand.
|
2026-09-14 13:30:00 +02:00
|
|
|
- Comparison operators (`<`, `>`, `<=`, `>=`, `==`, `!=`) are component-wise and
|
|
|
|
|
return `bool2`/`bool3`/`bool4`. Reduce them with `any` and `all`.
|
|
|
|
|
`Equals`/`GetHashCode` stay value-based so vectors still work as dictionary
|
|
|
|
|
keys.
|
|
|
|
|
- `select(condition, whenTrue, whenFalse)` uses HLSL's argument order.
|
2026-09-14 13:57:03 +02:00
|
|
|
- Matrices are row-major: constructors take rows, `m[i]` returns a reference to
|
|
|
|
|
row i (so `m[1][2] = s;` writes one element), `_mRC` is row R column C, and
|
|
|
|
|
products go through `mul` (`mul(m, v)` treats `v` as a column, `mul(v, m)` as a
|
|
|
|
|
row). `*` on matrices would be component-wise in HLSL, so CultMath does not
|
|
|
|
|
define it.
|
Document where matrix row writes are silently lost
The operator kept the [UnscopedRef] ref this[int] row indexer as is. design.md
and the float2x2/float3x3 indexers now say where m[r][c] = v lands: locals,
array elements, class fields, and ref locals keep the write; a readonly field,
in parameter, property (auto-properties too), or static readonly identity
takes a defensive copy, so the write compiles and is lost. Fix: copy to a
local, write, assign back. Verified by compiling and running each case with
Roslyn against CultMath (no diagnostics; writes land 5, lost 0).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:18:00 +02:00
|
|
|
- `m[r][c] = v` writes through locals, array elements, fields of classes, and
|
|
|
|
|
ref locals. Through a readonly field, an `in` parameter, a property
|
|
|
|
|
(auto-properties included), or a static readonly value such as
|
|
|
|
|
`float3x3.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.
|
2026-09-14 13:30:00 +02:00
|
|
|
- Scalar/vector arithmetic and comparisons have explicit overloads on both
|
|
|
|
|
sides rather than leaning on implicit scalar splat.
|
2026-09-14 13:57:03 +02:00
|
|
|
- Intrinsics return HLSL's types and edge cases: `sign` returns `int`/`intN`
|
2026-09-14 14:09:49 +02:00
|
|
|
(NaN gives 0); `step(y, x)` is `x < y ? 0 : 1` (NaN gives 1), which is how dxc
|
2026-09-14 14:47:10 +02:00
|
|
|
lowers it, in C# and in the HLSL mirror; float and double `min` and `max`, and
|
|
|
|
|
float `clamp` and `saturate`, follow the DXIL `FMin`/`FMax`/`Saturate` operations dxc emits for
|
Match float min, max, clamp, and saturate to DXIL FMin/FMax/Saturate
dxc lowers float min/max/clamp to dx.op FMin/FMax (36/35) and saturate to
dx.op Saturate (7). DXIL.rst defines them: FMin(a, b) is a < b ? a : b,
FMax(a, b) is a >= b ? a : b, a NaN operand returns the other, and
Saturate is FMin(1, FMax(0, x)). C# used MathF.Min/Max, which pass NaN
through and order signed zeros, so saturate(NaN) was NaN instead of 0.
Every float overload now follows the spec, normalize uses the same max as
its mirror, and the mirror's saturate spells Saturate's operand order
(which maps -0 to +0, where clamp(x, 0, 1) keeps -0). Non-NaN results
change only in the sign of a zero. design.md records the rules and that
dxc marks float math fast, so GPU NaN results are not guaranteed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:32:19 +02:00
|
|
|
them: `min(a, b)` is `a < b ? a : b`, `max(a, b)` is `a >= b ? a : b`, a NaN
|
|
|
|
|
operand returns the other (so `saturate(NaN)` is 0), `clamp(x, a, b)` is
|
|
|
|
|
`min(max(x, a), b)`, and `saturate(x)` is `min(1, max(0, x))`, which maps -0
|
|
|
|
|
to +0; `any`/`all` accept numeric vectors (a component is true when it is
|
2026-09-14 13:57:03 +02:00
|
|
|
not 0); `min`, `max`, and `abs` on int vectors return int vectors.
|
|
|
|
|
|
Match float min, max, clamp, and saturate to DXIL FMin/FMax/Saturate
dxc lowers float min/max/clamp to dx.op FMin/FMax (36/35) and saturate to
dx.op Saturate (7). DXIL.rst defines them: FMin(a, b) is a < b ? a : b,
FMax(a, b) is a >= b ? a : b, a NaN operand returns the other, and
Saturate is FMin(1, FMax(0, x)). C# used MathF.Min/Max, which pass NaN
through and order signed zeros, so saturate(NaN) was NaN instead of 0.
Every float overload now follows the spec, normalize uses the same max as
its mirror, and the mirror's saturate spells Saturate's operand order
(which maps -0 to +0, where clamp(x, 0, 1) keeps -0). Non-NaN results
change only in the sign of a zero. design.md records the rules and that
dxc marks float math fast, so GPU NaN results are not guaranteed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:32:19 +02:00
|
|
|
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`).
|
|
|
|
|
|
2026-09-14 14:47:10 +02:00
|
|
|
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.
|
|
|
|
|
|
2026-09-14 13:57:03 +02:00
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
1. Include guards (`#ifndef`/`#define`/`#endif`) are dropped.
|
|
|
|
|
2. Functions taking `Texture2D`/`SamplerState` are dropped; GPU resources have
|
|
|
|
|
no CultMath analog.
|
|
|
|
|
3. File-scope `static const` becomes `const`.
|
|
|
|
|
4. Floating literals gain `f` (`0.5` becomes `0.5f`).
|
|
|
|
|
5. 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.
|
2026-09-14 13:30:00 +02:00
|
|
|
|
2026-09-14 14:32:43 +02:00
|
|
|
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
|
2026-09-14 14:47:10 +02:00
|
|
|
`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.
|
2026-09-14 14:32:43 +02:00
|
|
|
|
2026-09-14 13:30:00 +02:00
|
|
|
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(...)`).
|
|
|
|
|
|
2026-05-08 01:49:44 +01:00
|
|
|
## Rules
|
|
|
|
|
|
2026-09-14 13:30:00 +02:00
|
|
|
- Keep type names HLSL-shaped: `float2`, `float3`, `float4`, `float3x3`, and `math`.
|
2026-06-03 02:58:56 +01:00
|
|
|
- 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.
|
2026-06-10 21:22:43 +01:00
|
|
|
- Keep `shaders/CultMath.hlsl` as the canonical shader mirror for shared
|
2026-09-14 13:30:00 +02:00
|
|
|
CultMath functions HLSL does not already provide (HLSL intrinsics such as
|
|
|
|
|
`mul`, `any`, and `log` need no mirror). Project-local shader helpers should
|
|
|
|
|
move here once more than one organ needs them or once CPU/GPU parity matters.
|
2026-05-08 01:49:44 +01:00
|
|
|
- Implement public shader semantics directly; do not copy Unity.Mathematics.
|
2026-09-14 13:30:00 +02:00
|
|
|
- Prefer plain mutable value types with public component fields, as HLSL does.
|
2026-05-08 01:49:44 +01:00
|
|
|
- Prefer component-wise overloads over clever generic machinery until the API
|
|
|
|
|
earns that complexity.
|
2026-09-14 13:30:00 +02:00
|
|
|
- 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.UnityBridge` assembly.
|
2026-05-08 01:49:44 +01:00
|
|
|
|
|
|
|
|
## Non-Goals
|
|
|
|
|
|
|
|
|
|
- Compiling C# to shaders.
|
2026-06-10 21:22:43 +01:00
|
|
|
- Generating HLSL from Rust or Rust from HLSL.
|
2026-05-08 01:49:44 +01:00
|
|
|
- Replacing `System.Numerics` for general .NET work.
|
|
|
|
|
- Recreating every Unity.Mathematics type because a spreadsheet somewhere says
|
|
|
|
|
“coverage.” That way lies decorative obesity.
|