---
title: "InterlockedAdd in HLSL, and what to do for floats"
url: https://recursiveai.org/oneshader/cookbook/hlsl-interlockedadd/
description: "Count with InterlockedAdd in an HLSL compute shader on Vulkan, Metal and WebGPU, and add floats atomically with a compare-exchange loop, since there is no…"
version: "1.2.0"
---

# InterlockedAdd in HLSL, and what to do for floats

Count with `InterlockedAdd` in an HLSL compute shader on Vulkan, Metal and WebGPU, and add floats atomically with a compare-exchange loop, since there is no float `InterlockedAdd`. Includes what OneShader 1.2.0 doesn’t compile.

`InterlockedAdd`, `Min`, `Max`, `And`, `Or`, `Xor`, `Exchange` and `CompareExchange` work on 32-bit `int` and `uint`, in RW structured and byte-address buffers and in `groupshared` memory. Since 1.2.0 they also work on `int64_t` and `uint64_t` on SPIR-V (see below). A histogram shows both the plain counter and the float sum, `Histogram.hlsl`:

```hlsl
Texture2D<float4> Image : register(t0);
RWStructuredBuffer<uint> Bins : register(u1);     // 256 bins
RWStructuredBuffer<uint> Total : register(u2);    // one float, stored as its bits

// There is no float InterlockedAdd: add with a compare-exchange loop on the bits.
void AtomicAddFloat(uint index, float value)
{
    uint expected = Total[index];
    for (;;)
    {
        uint desired = asuint(asfloat(expected) + value);
        uint original;
        InterlockedCompareExchange(Total[index], expected, desired, original);
        if (original == expected)
            break;
        expected = original;
    }
}

[shader("compute")]
[numthreads(16, 16, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
    float3 rgb = Image[id.xy].rgb;
    float luma = dot(rgb, float3(0.2126, 0.7152, 0.0722));
    uint bin = min(uint(luma * 256.0), 255u);

    InterlockedAdd(Bins[bin], 1u);   // 32-bit int or uint
    AtomicAddFloat(0, luma);
}
```

```sh
oneshader --target spirv --target msl --target wgsl Histogram.hlsl -Fo out/histogram
spirv-val --target-env vulkan1.2 out/histogram.spv
naga out/histogram.wgsl
```

The compile succeeds for all three targets and writes `histogram.spv`, `histogram.metal` and `histogram.wgsl`. The SPIR-V passes `spirv-val`. The Metal output uses `atomic_fetch_add_explicit` and `atomic_compare_exchange_weak_explicit`, and the WGSL output uses `atomicAdd` on `array<atomic<u32>>`. naga, however, rejects the WGSL:

```text
error: Function [0] 'osh_atomic_cas_storage_u32' is invalid
   ┌─ histogram.wgsl:7:1
   │  
 7 │ ╭ fn osh_atomic_cas_storage_u32(p: ptr<storage, atomic<u32>, read_write>, compare: u32, value: u32) -> u32 {
...
   = Argument 'p' at index 0 is a pointer of space Storage { access: StorageAccess(LOAD | STORE) }, which can't be passed into functions.
```

## Limits in 1.2.0

- **Compare-exchange on WGSL.** For `InterlockedCompareExchange` and `InterlockedCompareStore` (on buffers and on `groupshared` memory), and for `InterlockedMin`/`Max` on signed `int` buffers, the WGSL that 1.2.0 writes passes a pointer to a helper function, and naga rejects that. So the float add above works on Vulkan and Metal, but not on WebGPU yet. `InterlockedAdd`, `And`, `Or`, `Xor` and `Exchange`, and `Min`/`Max` on `uint`, produce WGSL that validates.
- **No float atomics.** `InterlockedAdd` on a `float` is an error that says what to do:

  ```text
  floatadd.hlsl(3,62): error OSH0314: 'InterlockedAdd' works on int, uint, int64_t and uint64_t, but the first argument is 'float'. Float atomics are not supported; reinterpret the data as uint with asuint()/asfloat().
  ```

- **64-bit atomics are mostly SPIR-V.** On SPIR-V, 64-bit `Interlocked*` work on structured buffers and `groupshared` memory, and the device needs `Int64Atomics` (in the entry’s `RequiredFeatures`). Metal 2.4 has only unsigned `InterlockedMax`/`InterlockedMin` on a buffer whose result isn’t used, and WGSL has no 64-bit integers: other forms are errors (OSH5102, OSH5202) when those targets are selected. See [16-bit and 64-bit types](https://recursiveai.org/oneshader/cookbook/hlsl-16-bit-64-bit-types/).
- **No atomics on textures** for Metal and WGSL (Metal needs 3.1). Use a buffer.

## Good to know

- The compare-exchange loop retries until no other thread changed the value between the read and the write. Under heavy contention it’s slow, so reduce first: sum within the thread group in `groupshared` memory or with `WaveActiveSum`, and do one atomic per group or per wave. See [Wave intrinsics](https://recursiveai.org/oneshader/cookbook/hlsl-wave-intrinsics/).
- Floating-point addition isn’t associative, so a sum built from atomics differs in the last bits from run to run, in any compiler.
- The two-argument form `InterlockedAdd(dest, value)` discards the old value. The three-argument form returns it.

For the dispatch and layouts around a compute shader, see [Write a compute shader for Vulkan and WebGPU](https://recursiveai.org/oneshader/cookbook/hlsl-compute-shader/).
