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>
143 lines
7.9 KiB
Markdown
143 lines
7.9 KiB
Markdown
# 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`, 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.
|
|
- 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.
|
|
- 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.
|
|
- `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.
|
|
- 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: `sign` returns `int`/`intN`
|
|
(NaN gives 0); `step(y, x)` is `x < y ? 0 : 1` (NaN gives 1), which is how dxc
|
|
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
|
|
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
|
|
not 0); `min`, `max`, and `abs` on 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:
|
|
|
|
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.
|
|
|
|
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`, and `math`.
|
|
- 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.hlsl` as the canonical shader mirror for shared
|
|
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.
|
|
- 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.UnityBridge` assembly.
|
|
|
|
## Non-Goals
|
|
|
|
- Compiling C# to shaders.
|
|
- Generating HLSL from Rust or Rust from HLSL.
|
|
- Replacing `System.Numerics` for general .NET work.
|
|
- Recreating every Unity.Mathematics type because a spreadsheet somewhere says
|
|
“coverage.” That way lies decorative obesity.
|