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>
This commit is contained in:
parent
d298d666d0
commit
9407da0461
7 changed files with 47 additions and 18 deletions
|
|
@ -46,9 +46,19 @@ 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; `any`/`all` accept numeric vectors (a component is true when it is
|
||||
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
|
||||
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`).
|
||||
|
||||
Deferred until a consumer needs them: `float4x4`, `transpose`, `determinant`,
|
||||
and HLSL's one-based `_11`..`_33` element names.
|
||||
|
||||
|
|
|
|||
|
|
@ -26,10 +26,11 @@ float2 cultmath_clamp(float2 value, float2 minimum, float2 maximum) { return min
|
|||
float3 cultmath_clamp(float3 value, float3 minimum, float3 maximum) { return min(max(value, minimum), maximum); }
|
||||
float4 cultmath_clamp(float4 value, float4 minimum, float4 maximum) { return min(max(value, minimum), maximum); }
|
||||
|
||||
float cultmath_saturate(float value) { return cultmath_clamp(value, 0.0, 1.0); }
|
||||
float2 cultmath_saturate(float2 value) { return cultmath_clamp(value, float2(0.0, 0.0), float2(1.0, 1.0)); }
|
||||
float3 cultmath_saturate(float3 value) { return cultmath_clamp(value, float3(0.0, 0.0, 0.0), float3(1.0, 1.0, 1.0)); }
|
||||
float4 cultmath_saturate(float4 value) { return cultmath_clamp(value, float4(0.0, 0.0, 0.0, 0.0), float4(1.0, 1.0, 1.0, 1.0)); }
|
||||
// DXIL Saturate is FMin(1, FMax(0, x)); dxc lowers min/max to the same FMin/FMax, NaN rules included.
|
||||
float cultmath_saturate(float value) { return min(1.0, max(0.0, value)); }
|
||||
float2 cultmath_saturate(float2 value) { return min(float2(1.0, 1.0), max(float2(0.0, 0.0), value)); }
|
||||
float3 cultmath_saturate(float3 value) { return min(float3(1.0, 1.0, 1.0), max(float3(0.0, 0.0, 0.0), value)); }
|
||||
float4 cultmath_saturate(float4 value) { return min(float4(1.0, 1.0, 1.0, 1.0), max(float4(0.0, 0.0, 0.0, 0.0), value)); }
|
||||
|
||||
float cultmath_lerp(float start, float end, float amount) { return start + (end - start) * amount; }
|
||||
float2 cultmath_lerp(float2 start, float2 end, float2 amount) { return start + (end - start) * amount; }
|
||||
|
|
|
|||
|
|
@ -106,7 +106,9 @@ public static partial class math
|
|||
public static float3 frac(float3 value) => value - floor(value);
|
||||
public static float4 frac(float4 value) => value - floor(value);
|
||||
|
||||
public static float min(float left, float right) => MathF.Min(left, right);
|
||||
// dxc lowers float min/max/clamp to DXIL FMin/FMax (saturate to Saturate, defined as FMin(1, FMax(0, x))).
|
||||
// DXIL.rst: FMin(a, b) is a < b ? a : b, FMax(a, b) is a >= b ? a : b, and a NaN operand returns the other.
|
||||
public static float min(float left, float right) => left < right || float.IsNaN(right) ? left : right;
|
||||
public static int min(int left, int right) => Math.Min(left, right);
|
||||
public static float2 min(float2 left, float2 right) => new(min(left.x, right.x), min(left.y, right.y));
|
||||
public static float3 min(float3 left, float3 right) => new(min(left.x, right.x), min(left.y, right.y), min(left.z, right.z));
|
||||
|
|
@ -115,7 +117,7 @@ public static partial class math
|
|||
public static int3 min(int3 left, int3 right) => new(min(left.x, right.x), min(left.y, right.y), min(left.z, right.z));
|
||||
public static int4 min(int4 left, int4 right) => new(min(left.x, right.x), min(left.y, right.y), min(left.z, right.z), min(left.w, right.w));
|
||||
|
||||
public static float max(float left, float right) => MathF.Max(left, right);
|
||||
public static float max(float left, float right) => left >= right || float.IsNaN(right) ? left : right;
|
||||
public static int max(int left, int right) => Math.Max(left, right);
|
||||
public static float2 max(float2 left, float2 right) => new(max(left.x, right.x), max(left.y, right.y));
|
||||
public static float3 max(float3 left, float3 right) => new(max(left.x, right.x), max(left.y, right.y), max(left.z, right.z));
|
||||
|
|
@ -129,10 +131,11 @@ public static partial class math
|
|||
public static float3 clamp(float3 value, float3 minimum, float3 maximum) => min(max(value, minimum), maximum);
|
||||
public static float4 clamp(float4 value, float4 minimum, float4 maximum) => min(max(value, minimum), maximum);
|
||||
|
||||
public static float saturate(float value) => clamp(value, 0.0f, 1.0f);
|
||||
public static float2 saturate(float2 value) => clamp(value, 0.0f, 1.0f);
|
||||
public static float3 saturate(float3 value) => clamp(value, 0.0f, 1.0f);
|
||||
public static float4 saturate(float4 value) => clamp(value, 0.0f, 1.0f);
|
||||
// DXIL Saturate is FMin(1, FMax(0, x)); that operand order, unlike clamp(x, 0, 1), maps -0 to +0.
|
||||
public static float saturate(float value) => min(1.0f, max(0.0f, value));
|
||||
public static float2 saturate(float2 value) => min(1.0f, max(0.0f, value));
|
||||
public static float3 saturate(float3 value) => min(1.0f, max(0.0f, value));
|
||||
public static float4 saturate(float4 value) => min(1.0f, max(0.0f, value));
|
||||
|
||||
public static float lerp(float start, float end, float amount) => start + (end - start) * amount;
|
||||
public static double lerp(double start, double end, double amount) => start + (end - start) * amount;
|
||||
|
|
@ -202,9 +205,9 @@ public static partial class math
|
|||
public static float distance(float3 left, float3 right) => length(left - right);
|
||||
public static float distance(float4 left, float4 right) => length(left - right);
|
||||
|
||||
public static float2 normalize(float2 value) => value / MathF.Max(length(value), 1.0e-20f);
|
||||
public static float3 normalize(float3 value) => value / MathF.Max(length(value), 1.0e-20f);
|
||||
public static float4 normalize(float4 value) => value / MathF.Max(length(value), 1.0e-20f);
|
||||
public static float2 normalize(float2 value) => value / max(length(value), 1.0e-20f);
|
||||
public static float3 normalize(float3 value) => value / max(length(value), 1.0e-20f);
|
||||
public static float4 normalize(float4 value) => value / max(length(value), 1.0e-20f);
|
||||
public static quaternion normalize(quaternion value)
|
||||
{
|
||||
var length = MathF.Sqrt(value.x * value.x + value.y * value.y + value.z * value.z + value.w * value.w);
|
||||
|
|
|
|||
|
|
@ -165,6 +165,20 @@ public sealed class HlslSemanticsTests
|
|||
Assert.Equal(1.0f, math.step(float.NaN, 1.0f));
|
||||
Assert.Equal(1.0f, math.step(1.0f, 1.0f));
|
||||
|
||||
// DXIL FMin/FMax/Saturate: a NaN operand returns the other; max(a, b) is a >= b ? a : b.
|
||||
Assert.Equal(1.0f, math.min(float.NaN, 1.0f));
|
||||
Assert.Equal(1.0f, math.min(1.0f, float.NaN));
|
||||
Assert.Equal(1.0f, math.max(float.NaN, 1.0f));
|
||||
Assert.Equal(1.0f, math.max(1.0f, float.NaN));
|
||||
Assert.True(float.IsNaN(math.max(float.NaN, float.NaN)));
|
||||
Assert.Equal(2.0f, math.clamp(float.NaN, 2.0f, 3.0f));
|
||||
Assert.Equal(new float3(0.0f, 1.0f, 0.0f), math.saturate(new float3(float.NaN, 2.0f, -1.0f)));
|
||||
Assert.Equal(new float2(-1.0f, 4.0f), math.max(new float2(float.NaN, 4.0f), new float2(-1.0f, float.NaN)));
|
||||
static int Bits(float value) => BitConverter.SingleToInt32Bits(value);
|
||||
Assert.Equal(Bits(-0.0f), Bits(math.max(-0.0f, 0.0f)));
|
||||
Assert.Equal(Bits(0.0f), Bits(math.min(-0.0f, 0.0f)));
|
||||
Assert.Equal(Bits(0.0f), Bits(math.saturate(-0.0f)));
|
||||
|
||||
Assert.Equal(0, math.sign(float.NaN));
|
||||
Assert.Equal(-1, math.sign(-0.5f));
|
||||
Assert.Equal(new int3(-1, 0, 1), math.sign(new float3(-2.0f, 0.0f, 4.0f)));
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -26,10 +26,11 @@ float2 cultmath_clamp(float2 value, float2 minimum, float2 maximum) { return min
|
|||
float3 cultmath_clamp(float3 value, float3 minimum, float3 maximum) { return min(max(value, minimum), maximum); }
|
||||
float4 cultmath_clamp(float4 value, float4 minimum, float4 maximum) { return min(max(value, minimum), maximum); }
|
||||
|
||||
float cultmath_saturate(float value) { return cultmath_clamp(value, 0.0, 1.0); }
|
||||
float2 cultmath_saturate(float2 value) { return cultmath_clamp(value, float2(0.0, 0.0), float2(1.0, 1.0)); }
|
||||
float3 cultmath_saturate(float3 value) { return cultmath_clamp(value, float3(0.0, 0.0, 0.0), float3(1.0, 1.0, 1.0)); }
|
||||
float4 cultmath_saturate(float4 value) { return cultmath_clamp(value, float4(0.0, 0.0, 0.0, 0.0), float4(1.0, 1.0, 1.0, 1.0)); }
|
||||
// DXIL Saturate is FMin(1, FMax(0, x)); dxc lowers min/max to the same FMin/FMax, NaN rules included.
|
||||
float cultmath_saturate(float value) { return min(1.0, max(0.0, value)); }
|
||||
float2 cultmath_saturate(float2 value) { return min(float2(1.0, 1.0), max(float2(0.0, 0.0), value)); }
|
||||
float3 cultmath_saturate(float3 value) { return min(float3(1.0, 1.0, 1.0), max(float3(0.0, 0.0, 0.0), value)); }
|
||||
float4 cultmath_saturate(float4 value) { return min(float4(1.0, 1.0, 1.0, 1.0), max(float4(0.0, 0.0, 0.0, 0.0), value)); }
|
||||
|
||||
float cultmath_lerp(float start, float end, float amount) { return start + (end - start) * amount; }
|
||||
float2 cultmath_lerp(float2 start, float2 end, float2 amount) { return start + (end - start) * amount; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue