---
title: "HLSL precise and floating-point determinism"
url: https://recursiveai.org/oneshader/articles/hlsl-precise-keyword-determinism/
description: "What HLSL’s precise keyword guarantees (no fused multiply-adds, no reordering), what it doesn’t, and how it reaches Vulkan, Metal and WebGPU."
date: 2026-10-26
author: OneShader team
---

# HLSL precise and floating-point determinism

What HLSL’s precise keyword guarantees (no fused multiply-adds, no reordering), what it doesn’t, and how it reaches Vulkan, Metal and WebGPU.

October 26, 2026 · OneShader team

HLSL’s `precise` keyword makes the arithmetic that produces a variable deterministic in one specific sense: the compiler and the driver may not fuse a multiply and an add into one fused multiply-add (FMA), and may not reorder the operations. Two shaders that compute the same expression from the same inputs then round the same way, which is what keeps tessellated patch edges watertight and depth equal across passes. `precise` doesn’t make math bit-identical across GPUs: `sin`, `exp` and division still carry each vendor’s error bounds.

Below: why contraction changes results, what Microsoft documents `precise` to do, how it reaches the GPU on Vulkan, Metal and WebGPU, and one case where compilers disagree. The shaders were compiled with DXC 1.9.0.5512 (Vulkan SDK 1.4.363.0) and [OneShader 1.2.0](https://www.nuget.org/packages/RecursiveAI.OneShader.Tool/1.2.0), and every output shown is what they printed.

## Why a fused multiply-add changes the result

`a * b + c` written as two operations rounds twice: once after the multiply, once after the add. An FMA computes the exact product and rounds once at the end. Usually the difference is in the last bit. Near cancellation it’s the whole answer. This is C# on the CPU, where nothing fuses unless you ask:

*Program.cs · .NET 10*

```
float a = 0.1f, b = 10f, c = -1f;
 
float separate = a * b + c;                        // the multiply is rounded to float, then the add
float fused = MathF.FusedMultiplyAdd(a, b, c);     // one rounding, at the end
 
Console.WriteLine($"a * b + c = {separate:R}");
Console.WriteLine($"fma(a,b,c) = {fused:R}");
```

*Output*

```
a * b + c = 0
fma(a,b,c) = 1.4901161E-08
```

`0.1f * 10f` rounds to exactly `1f`, so the separate version gives 0. The fused version keeps the product’s tiny excess over 1. GPU compilers fuse freely by default because FMA is faster and usually more accurate. The trouble starts when one shader’s expression gets fused and another’s doesn’t, for example the same edge vertex computed by two tessellated patches, or the same position computed in a depth pre-pass and a shading pass. Then the two results differ, and you get cracks or z-fighting.

## What precise guarantees

Microsoft’s [variable syntax page](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-variable-syntax) (updated 19 August 2021) lists three restrictions on “any calculations used to produce the value assigned to that variable”:

- “Separate operations are kept separate”: a multiply and an add aren’t fused into a `mad`. If you want the fused form, call `mad` yourself.
- “Order of operations are maintained”: no reassociating `(a + b) + c` into `a + (b + c)`.
- “IEEE unsafe operations are restricted”: fast-math shortcuts that ignore NaN and infinity aren’t used.

It propagates backwards only: “Qualifying a variable precise doesn’t make operations that use the variable precise.” Microsoft’s advice is to “mark the shader outputs precise directly where you declare them”, as in `out precise float4 OutPos : SV_Position`, so that everything feeding the output is covered.

## precise on Vulkan: NoContraction

SPIR-V has no keyword. A compiler puts the `NoContraction` decoration on each arithmetic instruction that feeds the precise variable. The SPIR-V specification defines it as “the operation cannot be combined with another instruction to form a single operation”, and adds that such operations “are not allowed to reassociate” ([SPIR-V specification](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html)). Vulkan spells out the default: “If the operation is not decorated NoContraction then the flags AllowContract, AllowReassoc, AllowRecip, and AllowTransform are assumed” ([Vulkan SPIR-V environment](https://docs.vulkan.org/spec/latest/appendices/spirvenv.html)).

Two vertex shaders show it. The first follows Microsoft’s pattern with `mul`. The second computes the same transform with vector arithmetic:

*Seam.hlsl · HLSL*

```
cbuffer Camera : register(b0) { row_major float4x4 WorldViewProjection; };
 
// Microsoft's pattern: mark the output precise where it is declared.
[shader("vertex")]
void VSMul(float3 position : POSITION, out precise float4 clip : SV_Position)
{
    clip = mul(float4(position, 1), WorldViewProjection);
}
 
// The same transform written as vector arithmetic.
[shader("vertex")]
void VSRows(float3 position : POSITION, out precise float4 clip : SV_Position)
{
    clip = position.x * WorldViewProjection[0]
         + position.y * WorldViewProjection[1]
         + position.z * WorldViewProjection[2]
         + WorldViewProjection[3];
}
```

*Terminal · DXC 1.9.0.5512 · OneShader 1.2.0*

```
for e in VSMul VSRows; do
  dxc -spirv -T vs_6_0 -E $e Seam.hlsl -Fo $e.dxc.spv
  oneshader -T vs_6_0 -E $e Seam.hlsl -Fo $e.spv
  echo "$e: DXC $(spirv-dis $e.dxc.spv | grep -c NoContraction), OneShader $(spirv-dis $e.spv | grep -c NoContraction) NoContraction decorations"
done
```

*Output*

```
VSMul: DXC 1, OneShader 0 NoContraction decorations
VSRows: DXC 6, OneShader 6 NoContraction decorations
```

For the vector form both compilers decorate the same six operations, the three multiplies and three adds. OneShader writes them as `OpFMul`, DXC as `OpVectorTimesScalar`:

*Terminal · OneShader 1.2.0*

```
spirv-dis VSRows.spv | grep -E "NoContraction|= OpF(Mul|Add)"
```

*Output*

```
               OpDecorate %26 NoContraction
               OpDecorate %32 NoContraction
               OpDecorate %33 NoContraction
               OpDecorate %39 NoContraction
               OpDecorate %40 NoContraction
               OpDecorate %44 NoContraction
         %26 = OpFMul %v4float %18 %25
         %32 = OpFMul %v4float %28 %31
         %33 = OpFAdd %v4float %26 %32
         %39 = OpFMul %v4float %35 %38
         %40 = OpFAdd %v4float %33 %39
         %44 = OpFAdd %v4float %40 %43
```

For the `mul` form DXC decorates the matrix-vector multiply itself:

*Terminal · DXC 1.9.0.5512*

```
spirv-dis VSMul.dxc.spv | grep -E "NoContraction|OpMatrixTimesVector"
```

*Output*

```
               OpDecorate %6 NoContraction
          %6 = OpMatrixTimesVector %v4float %27 %25
```

OneShader 1.2.0 doesn’t: it covers scalar and vector arithmetic that feeds a precise variable, but not `mul` with a matrix. If the exact rounding of a transform matters and you compile with OneShader, write the precise part as vector arithmetic, as `VSRows` does, and it gets the same decorations as DXC’s output. All four modules pass `spirv-val`.

One more Vulkan detail. Microsoft’s third restriction, about NaN and infinity, has no SPIR-V counterpart in `NoContraction`. Vulkan assumes `NSZ`, `NotInf` and `NotNaN` for most operations unless the entry point declares the `SignedZeroInfNanPreserve` execution mode, which neither DXC nor OneShader adds for `precise` in these modules. Don’t rely on `precise` to keep a NaN or infinity alive on Vulkan. Test for them explicitly.

## precise on Metal: fast math turned off locally

Metal compiles with fast math by default. OneShader starts a function that has precise arithmetic with two Clang pragmas, which turn off fused multiply-add and reassociation for that function whatever the host’s math mode:

*Terminal · OneShader 1.2.0*

```
oneshader -T vs_6_0 -E VSRows --target msl Seam.hlsl -Fo rows.metal
grep -n "pragma" rows.metal
```

*Output*

```
34:    #pragma clang fp reassociate(off)
35:    #pragma clang fp contract(off)
```

A precise division becomes `precise::divide`, because no pragma stops `a / b` from becoming `a * (1 / b)`. The NaN and infinity flags stay as fast math sets them, as on Vulkan ([MSL backend](https://recursiveai.org/oneshader/docs/msl-backend/)). If you go through SPIR-V instead, SPIRV-Cross carries `NoContraction` into MSL in its own way. For this shader it wraps each add in an `[[clang::optnone]]` helper that calls `fma(1, l, r)`.

## precise on WebGPU: not expressible

WGSL has no way to forbid contraction. The specification allows it outright: “An implementation may reassociate operations” and “may fuse operations if the transformed expression is at least as accurate as the original formulation” ([WGSL](https://www.w3.org/TR/WGSL/), Candidate Recommendation Draft of 26 October 2026). OneShader compiles the arithmetic and says so:

*Terminal · OneShader 1.2.0*

```
oneshader -T vs_6_0 -E VSRows --target wgsl Seam.hlsl -Fo rows.wgsl
```

*Output*

```
Seam.hlsl(14,12): warning OSH5205: 'precise' arithmetic is compiled, but WGSL cannot forbid a multiply-add being fused or a sum being reordered, so the result may differ in the last bits from the other targets. Use 'fma' where the exact rounding matters, or leave WGSL out of the targets for this shader.
```

The output passes naga. With `-WX` the warning stops the build. On WebGPU, the way to get one rounding everywhere is to ask for it: write `mad(a, b, c)` in HLSL on every path that has to agree. OneShader compiles it to `fma` in WGSL and MSL and to `Fma` on SPIR-V, as DXC does. Or keep the computation that has to match on the same target.

## What precise doesn’t give you

- **Cross-vendor bit equality.** Vulkan defines each operation’s accuracy as correctly rounded, an error bound in ULP, or inherited, and the bounds for `sin`, `log`, `exp` and division leave room for different answers on different GPUs ([precision table](https://docs.vulkan.org/spec/latest/appendices/spirvenv.html)). `precise` constrains which operations run and in what order, not how accurate each one is.
- **Coverage of what reads the variable.** Only what feeds it. Mark the output, not an intermediate.
- **Free speed.** Every unfused multiply-add is two instructions. Mark the few outputs that must match, not whole shaders.

## Common questions

### Is precise the same as GLSL’s precise?

They have the same intent, and both reach Vulkan the same way: glslang 16.6.0 puts `NoContraction` on the operations that feed a GLSL `precise` variable, as DXC does for HLSL.

### When should I use precise?

When two computations must give bit-identical results on the same GPU: shared tessellation edges, positions computed in several passes, or a value compared for equality later. Not for “more accuracy”: an FMA is often the more accurate of the two.

The full per-target behavior, including the Terrain example with six decorated operations, is in the [precise keyword recipe](https://recursiveai.org/oneshader/cookbook/hlsl-precise-keyword/). To compile your own shaders, install the tool with `dotnet tool install -g RecursiveAI.OneShader.Tool` or start from [getting started](https://recursiveai.org/oneshader/docs/getting-started/).
