HLSL wave intrinsics such as WaveActiveSum, WavePrefixCountBits and WaveReadLaneFirst let the threads of one wave (a warp on NVIDIA, a wavefront on AMD, a subgroup in Vulkan and WebGPU, a SIMD-group in Metal) share values without memory or barriers. Each one has a direct equivalent on every modern API: a GroupNonUniform instruction in SPIR-V, a simd_ or quad_ function in Metal, and a subgroup builtin in WGSL. The table below lists all of them, with the device feature each one needs.
The table comes from compiling one small shader per intrinsic with OneShader 1.2.0 to all three targets, and with DXC 1.9.0.5512 (Vulkan SDK 1.4.363.0) to SPIR-V. For every row, the two compilers emit the same SPIR-V instruction and group operation, and every module passes spirv-val for Vulkan 1.2.
Wave terms in one paragraph
Microsoft’s Shader Model 6.0 page (updated 28 August 2026) defines the vocabulary. A wave is “a set of lanes (threads) executed simultaneously”. An active lane is one currently executing, which “in pixel shaders […] may include any helper pixel lanes”. A quad is “a set of 4 adjacent lanes corresponding to pixels arranged in a 2x2 square”. Reductions such as WaveActiveSum combine the active lanes and give every one of them the result. Prefix functions such as WavePrefixSum combine only the lanes with smaller indices, so the first lane gets the identity. The wave size is the hardware’s choice. Read it with WaveGetLaneCount() instead of assuming 32 or 64. WebGPU reports the range as subgroupMinSize and subgroupMaxSize on the adapter (WebGPU).
HLSL wave intrinsics mapping table
SPIR-V instructions that take a type prefix use the one for the operand: F for float, I for integer add and multiply, S or U for min and max. The Vulkan feature is the VkSubgroupFeatureFlagBits bit that subgroupSupportedOperations has to include.
SPIR-V and the Vulkan feature
| HLSL | SPIR-V (DXC and OneShader) | Vulkan feature |
|---|---|---|
WaveGetLaneCount | SubgroupSize built-in | Basic |
WaveGetLaneIndex | SubgroupLocalInvocationId built-in | Basic |
WaveIsFirstLane | OpGroupNonUniformElect | Basic |
WaveActiveAnyTrue | OpGroupNonUniformAny | Vote |
WaveActiveAllTrue | OpGroupNonUniformAll | Vote |
WaveActiveAllEqual | OpGroupNonUniformAllEqual | Vote |
WaveActiveBallot | OpGroupNonUniformBallot | Ballot |
WaveActiveCountBits | OpGroupNonUniformBallotBitCount Reduce of a ballot | Ballot |
WaveActiveSum | OpGroupNonUniform*Add Reduce | Arithmetic |
WaveActiveProduct | OpGroupNonUniform*Mul Reduce | Arithmetic |
WaveActiveMin | OpGroupNonUniform*Min Reduce | Arithmetic |
WaveActiveMax | OpGroupNonUniform*Max Reduce | Arithmetic |
WaveActiveBitAnd | OpGroupNonUniformBitwiseAnd Reduce | Arithmetic |
WaveActiveBitOr | OpGroupNonUniformBitwiseOr Reduce | Arithmetic |
WaveActiveBitXor | OpGroupNonUniformBitwiseXor Reduce | Arithmetic |
WavePrefixSum | OpGroupNonUniform*Add ExclusiveScan | Arithmetic |
WavePrefixProduct | OpGroupNonUniform*Mul ExclusiveScan | Arithmetic |
WavePrefixCountBits | OpGroupNonUniformBallotBitCount ExclusiveScan of a ballot | Ballot |
WaveReadLaneAt | OpGroupNonUniformShuffle | Shuffle |
WaveReadLaneFirst | OpGroupNonUniformBroadcastFirst | Ballot |
QuadReadAcrossX | OpGroupNonUniformQuadSwap (direction 0) | Quad |
QuadReadAcrossY | OpGroupNonUniformQuadSwap (direction 1) | Quad |
QuadReadAcrossDiagonal | OpGroupNonUniformQuadSwap (direction 2) | Quad |
QuadReadLaneAt | OpGroupNonUniformQuadBroadcast | Quad |
Metal and WGSL
| HLSL | Metal (MSL) | WGSL |
|---|---|---|
WaveGetLaneCount | [[threads_per_simdgroup]] | subgroup_size built-in |
WaveGetLaneIndex | [[thread_index_in_simdgroup]] | subgroup_invocation_id built-in |
WaveIsFirstLane | simd_is_first | subgroupElect |
WaveActiveAnyTrue | simd_any | subgroupAny |
WaveActiveAllTrue | simd_all | subgroupAll |
WaveActiveAllEqual | simd_all of a comparison with simd_broadcast_first | subgroupAll of a comparison with subgroupBroadcastFirst |
WaveActiveBallot | simd_ballot | subgroupBallot |
WaveActiveCountBits | simd_sum of uint(condition) | subgroupAdd of u32(condition) |
WaveActiveSum | simd_sum | subgroupAdd |
WaveActiveProduct | simd_product | subgroupMul |
WaveActiveMin | simd_min | subgroupMin |
WaveActiveMax | simd_max | subgroupMax |
WaveActiveBitAnd | simd_and | subgroupAnd |
WaveActiveBitOr | simd_or | subgroupOr |
WaveActiveBitXor | simd_xor | subgroupXor |
WavePrefixSum | simd_prefix_exclusive_sum | subgroupExclusiveAdd |
WavePrefixProduct | simd_prefix_exclusive_product | subgroupExclusiveMul |
WavePrefixCountBits | simd_prefix_exclusive_sum of uint(condition) | subgroupExclusiveAdd of u32(condition) |
WaveReadLaneAt | simd_shuffle | subgroupShuffle |
WaveReadLaneFirst | simd_broadcast_first | subgroupBroadcastFirst |
QuadReadAcrossX | quad_shuffle_xor | quadSwapX |
QuadReadAcrossY | quad_shuffle_xor | quadSwapY |
QuadReadAcrossDiagonal | quad_shuffle_xor | quadSwapDiagonal |
QuadReadLaneAt | quad_shuffle | quadBroadcast |
DXC’s own chart in its SPIR-V mapping manual gives the same instructions. Two things in the table aren’t obvious. WaveReadLaneFirst needs the Ballot feature, not Basic, because SPIR-V puts OpGroupNonUniformBroadcastFirst under the Ballot capability. And the prefix functions are exclusive everywhere: HLSL has no inclusive scan, so add the lane’s own value if you need one.
One shader, three targets
A compute shader that sums a buffer with one atomic per wave instead of one per thread, and a pixel shader that reads its quad neighbors to get a slope:
StructuredBuffer<uint> Values : register(t0); RWByteAddressBuffer Total : register(u1); // Sum a buffer with one atomic per wave instead of one per thread. [shader("compute")] [numthreads(64, 1, 1)] void CSSum(uint3 id : SV_DispatchThreadID) { uint waveTotal = WaveActiveSum(Values[id.x]); if (WaveIsFirstLane()) Total.InterlockedAdd(0, waveTotal); } Texture2D<float> Height : register(t2); SamplerState Linear : register(s3); // A screen-space slope from the neighboring pixel in the 2x2 quad. [shader("pixel")] float4 PSSlope(float4 pos : SV_Position, float2 uv : TEXCOORD0) : SV_Target { float h = Height.Sample(Linear, uv); float dx = QuadReadAcrossX(h) - h; float dy = QuadReadAcrossY(h) - h; return float4(abs(dx), abs(dy), h, 1); }
Compiled from C#, with the subgroup operations each entry point needs read from reflection:
using OneShader; using OneShader.Reflection; CompileResult result = ShaderCompiler.CompileFile("Shaders/Waves.hlsl", new CompileOptions { Targets = [CompileTarget.SpirV, CompileTarget.Msl, CompileTarget.Wgsl], }); foreach (var d in result.Diagnostics) Console.WriteLine(d); if (!result.Success) return 1; foreach (EntryPointInfo entry in result.Reflection!.EntryPoints) { // The values are Vulkan's VkSubgroupFeatureFlagBits: compare with subgroupSupportedOperations. Console.WriteLine($"{entry.Name} ({entry.Stage}): {entry.SubgroupOperations} = 0x{(uint)entry.SubgroupOperations:x}"); File.WriteAllBytes($"{entry.Name}.spv", result.Find(entry.Name, CompileTarget.SpirV)!.ToArray()); File.WriteAllText($"{entry.Name}.metal", result.Find(entry.Name, CompileTarget.Msl)!.AsText()); File.WriteAllText($"{entry.Name}.wgsl", result.Find(entry.Name, CompileTarget.Wgsl)!.AsText()); } return 0;
CSSum (Compute): Basic, Arithmetic = 0x5 PSSlope (Pixel): Quad = 0x80
The values are Vulkan’s VkSubgroupFeatureFlagBits, so a host can test (supported & needed) == needed against VkPhysicalDeviceVulkan11Properties::subgroupSupportedOperations before it creates the pipeline. In CSSum, the wave sum is OpGroupNonUniformIAdd with Reduce on Vulkan, simd_sum on Metal and subgroupAdd on WebGPU. The SPIR-V declares only the capabilities it uses, here GroupNonUniform and GroupNonUniformArithmetic, and both modules pass spirv-val.
What each API needs before you dispatch
Vulkan
Wave intrinsics need SPIR-V 1.3, so Vulkan 1.1 or newer. With DXC, pass -fspv-target-env=vulkan1.1 or later, because its default environment is Vulkan 1.0 and it stops with “Vulkan 1.1 is required for Wave Operation” (details). OneShader always targets Vulkan 1.2. Then check subgroupSupportedOperations, and subgroupSupportedStages for the stage you use, in VkPhysicalDeviceVulkan11Properties.
Direct3D 12
Microsoft says to call ID3D12Device::CheckFeatureSupport with D3D12_FEATURE_DATA_D3D12_OPTIONS1 (SM 6.0 page). OneShader doesn’t write DXIL. For Direct3D 12, compile the same source with DXC.
WebGPU
Wave intrinsics need the optional "subgroups" feature, requested when the device is created (WebGPU). OneShader’s WGSL starts with enable subgroups; and diagnostic(off, subgroup_uniformity);, since HLSL doesn’t require wave operations to sit in uniform control flow, and PipelineLayout.TryToWebGpu lists subgroups in RequiredFeatures. WebGPU’s subgroup functions exist only in compute and fragment shaders, so a vertex shader that uses one compiles for SPIR-V and Metal but is error OSH5202 for WGSL. naga 30 can’t check these files yet:
naga CSSum.wgslCould not parse WGSL: error: the `subgroups` enable-extension is not yet supported ┌─ CSSum.wgsl:1:8 │ 1 │ enable subgroups; │ ^^^^^^^^^ this enable-extension specifies standard functionality which is not yet implemented in Naga │ = note: Let Naga maintainers know that you ran into this at <https://github.com/gfx-rs/wgpu/issues/5555>, so they can prioritize it!
The OneShader repository validates its wave shaders in a browser on a real WebGPU adapter instead (WebGPU execution parity).
Metal
SIMD-group functions need Metal 2.1 or later, which OneShader’s MSL 2.4 output already requires (MSL backend). The lane index and count aren’t available to Metal vertex functions.
What isn’t portable
WaveMatchandWaveMultiPrefix*(Shader Model 6.5) need partitioned subgroup operations. SPIR-V has them only as a vendor extension (DXC’s manual listsSPV_NV_shader_subgroup_partitioned), and Metal and WGSL have none, so OneShader reports error OSH0350 with a suggestion: build the groups withWaveActiveBallotandWaveReadLaneFirstin a loop.- Quad operations in compute shaders. In Shader Model 6.0 through 6.5 “the mapping of threads to quads is implementation-dependent” in compute shaders, per Microsoft. Use them in pixel shaders.
- Helper lanes. In pixel shaders the active lanes can include helper pixels, so a wave reduction may count lanes that write nothing.
- Wave size. A reduction is per wave, not per thread group. Combine the per-wave results with one atomic or a
groupsharedarray, asCSSumdoes.
Common questions
Are HLSL wave intrinsics the same as Vulkan subgroups?
Yes, the same hardware feature under different names. Each HLSL wave function compiles to a SPIR-V GroupNonUniform instruction with Subgroup scope, which is what GLSL’s subgroup* functions compile to as well (glslang turns subgroupAdd into the same OpGroupNonUniformIAdd with Reduce).
What does WaveActiveCountBits return?
The number of active lanes for which the condition is true, the same value in every lane. On SPIR-V it is a ballot followed by OpGroupNonUniformBallotBitCount with Reduce. countbits, by contrast, counts the set bits of one integer in one lane.
For a step-by-step example with a stream-compaction shader, see the wave intrinsics recipe. The instruction-level rules per target are in the SPIR-V, MSL and WGSL backend pages.