Say where intrinsic rules are pinned and that NaN does not propagate

The mirror compiles under using static CultMath.math, so it proves composition,
not intrinsic rules; HlslSemanticsTests alone pins those. design.md also states
where FMin/FMax/Saturate turn NaN or infinity into numbers and tells callers to
check isnan first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-14 14:47:10 +02:00
commit d4c43ed265
2 changed files with 19 additions and 4 deletions

View file

@ -46,8 +46,8 @@ That makes these HLSL rules, not options:
sides rather than leaning on implicit scalar splat. sides rather than leaning on implicit scalar splat.
- Intrinsics return HLSL's types and edge cases: `sign` returns `int`/`intN` - 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 (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 `min`, `max`, `clamp`, and lowers it, in C# and in the HLSL mirror; float and double `min` and `max`, and
`saturate` follow the DXIL `FMin`/`FMax`/`Saturate` operations dxc emits for 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 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 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 `min(max(x, a), b)`, and `saturate(x)` is `min(1, max(0, x))`, which maps -0
@ -59,6 +59,16 @@ dxc marks float arithmetic and compares `fast` (no NaNs) unless a value is
GPUs are not guaranteed. CultMath defines the CPU result by dxc's lowering and GPUs are not guaranteed. CultMath defines the CPU result by dxc's lowering and
the DXIL operation spec (DirectXShaderCompiler `docs/DXIL.rst`). 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`, Deferred until a consumer needs them: `float4x4`, `transpose`, `determinant`,
and HLSL's one-based `_11`..`_33` element names. and HLSL's one-based `_11`..`_33` element names.
@ -88,7 +98,10 @@ DirectXShaderCompiler, and
The mirror test compares bit patterns (any NaN equals any NaN; -0 differs from 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 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 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. `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 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)`), Unity.Mathematics: `normalize` is safe (divides by `max(length, 1e-20)`),

View file

@ -42,7 +42,9 @@ public sealed class HlslSourceCompatibilityTests
/// negatives, NaN, large values, integer lattice points), special pairs, and vectors whose first two /// negatives, NaN, large values, integer lattice points), special pairs, and vectors whose first two
/// components tie (x0.x == x0.y in snoise). This proves the text of CultMath.hlsl computes the same /// 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 /// float32 results as C# math on the CPU. It does not prove GPU agreement: driver sin precision alone
/// makes cultmath_hash and value noise differ bit for bit on hardware. /// 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.
/// </summary> /// </summary>
[Fact] [Fact]
public void EveryMirrorFunctionMatchesCSharpMath() public void EveryMirrorFunctionMatchesCSharpMath()