Recursive AI

InterlockedAdd in HLSL, and what to do for floats

All oneshader recipes

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);
}
Terminal
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.

  • 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.
  • 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.