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:
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); }
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:
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
InterlockedCompareExchangeandInterlockedCompareStore(on buffers and ongroupsharedmemory), and forInterlockedMin/Maxon signedintbuffers, 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,XorandExchange, andMin/Maxonuint, produce WGSL that validates.No float atomics.
InterlockedAddon afloatis 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 andgroupsharedmemory, and the device needsInt64Atomics(in the entry’sRequiredFeatures). Metal 2.4 has only unsignedInterlockedMax/InterlockedMinon 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
groupsharedmemory or withWaveActiveSum, 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.