From d4c43ed2659c08223308ea843b610a567fe58490 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 14 Sep 2026 14:47:10 +0200 Subject: [PATCH] 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 --- packages/cultmath/docs/design.md | 19 ++++++++++++++++--- .../HlslSourceCompatibilityTests.cs | 4 +++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/cultmath/docs/design.md b/packages/cultmath/docs/design.md index e716bb6f..de09ee26 100644 --- a/packages/cultmath/docs/design.md +++ b/packages/cultmath/docs/design.md @@ -46,8 +46,8 @@ That makes these HLSL rules, not options: 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 `min`, `max`, `clamp`, and - `saturate` follow the DXIL `FMin`/`FMax`/`Saturate` operations dxc emits for + 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 @@ -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 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. @@ -88,7 +98,10 @@ DirectXShaderCompiler, and 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. +`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)`), diff --git a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs index d0ce8bbd..ca4584c7 100644 --- a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs @@ -42,7 +42,9 @@ public sealed class HlslSourceCompatibilityTests /// 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 /// 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. /// [Fact] public void EveryMirrorFunctionMatchesCSharpMath()