Compute Shader Backend Differences

May 15, 2026 · View on GitHub

This document describes compute shader support across wgpu's HAL backends.

Support Matrix

FeatureVulkanDX12MetalGLESSoftwareBrowserNoop
Compute shadersYesYesYesPartialYes (interpreted)YesNo
Storage buffersYesYesYesYesYes (interpreted)YesNo
Timestamp queriesYesYesStubStubNoNoNo
Indirect dispatchYesYesYesYesNoYesNo
Buffer mapping (GPU->CPU)YesYesYesYesYesYesNo
Max workgroup size X1024+1024102410241024BrowserN/A
Max workgroup invocations1024+1024102410241024BrowserN/A

Vulkan

Status: Full compute support.

Vulkan provides the most complete compute shader implementation:

  • Shader compilation: WGSL -> SPIR-V (via gogpu/naga).
  • Timestamp queries: Fully implemented using vkCmdWriteTimestamp at the beginning and end of compute passes. Results are copied to a buffer via vkCmdCopyQueryPoolResults.
  • Memory barriers: Automatic global memory barrier inserted at compute pass end (End()) to ensure shader writes are visible to subsequent commands.
  • Workgroup size limits: Depends on the physical device. Typical minimum guaranteed by Vulkan spec: 128 invocations per workgroup. Most desktop GPUs support 1024.
  • Synchronization: Use Fence + Device.Wait() to synchronize CPU with GPU completion.
  • Timestamp period: Queue.GetTimestampPeriod() returns the nanoseconds-per-tick value for the GPU's timestamp counter.

Vulkan-Specific Notes

  • Query pools are reset automatically before each timestamp write.
  • The ResolveQuerySet method inserts a pipeline barrier before copying results to ensure timestamps are finalized.
  • VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT flags ensure results are complete 64-bit values.

DirectX 12

Status: Compute shaders work. Timestamp queries supported.

  • Shader compilation: WGSL -> HLSL -> DXBC via FXC (default), or WGSL -> DXIL direct via gogpu/naga/dxil (GOGPU_DX12_DXIL=1, SM 6.0+, no external dependencies).
  • Timestamp queries: Fully implemented using ID3D12Device::CreateQueryHeap with D3D12_QUERY_TYPE_TIMESTAMP and ID3D12GraphicsCommandList::EndQuery + ResolveQueryData. Begin/end-of-pass timestamps written automatically.
  • Workgroup size limits: Maximum 1024 invocations per workgroup (D3D12 spec).
  • Indirect dispatch: DispatchIndirect via ExecuteIndirect with pre-created ID3D12CommandSignature (dispatch args: 3 × uint32 = 12 bytes). Also supports DrawIndirect (16B) and DrawIndexedIndirect (20B).
  • Root signature: Compute pipelines use a separate root signature from graphics pipelines. Descriptor heaps are bound lazily on first SetBindGroup call.

DX12-Specific Notes

  • Descriptor heaps must be bound before setting descriptor tables. The ComputePassEncoder tracks this with descriptorHeapsSet.
  • No explicit compute pass begin/end is needed at the D3D12 API level.

Metal

Status: Compute shaders work. Timestamp queries are stubbed.

  • Shader compilation: WGSL -> MSL (via gogpu/naga).
  • Timestamp queries: CreateQuerySet currently returns ErrTimestampsNotSupported. Metal supports counter sample buffers (MTLCounterSampleBuffer) for GPU timestamps on Apple Silicon. Implementation is planned.
  • Workgroup size limits: Maximum 1024 invocations per workgroup on Apple Silicon. Older Intel Macs may have lower limits.
  • Compute encoder: Metal uses MTLComputeCommandEncoder (created from MTLCommandBuffer). The encoder is Retain-ed to survive autorelease pool drains.

Metal-Specific Notes

  • GetTimestampPeriod() returns 1.0 (Metal timestamps are already in nanoseconds).
  • Bind group resources are bound directly via setBuffer:offset:atIndex: on the compute encoder.

OpenGL ES (GLES)

Status: Partial compute support. Requires OpenGL ES 3.1+.

  • Shader compilation: WGSL -> GLSL (via gogpu/naga).
  • Timestamp queries: CreateQuerySet currently returns ErrTimestampsNotSupported. GLES supports timer queries via GL_EXT_disjoint_timer_query extension (not universally available). Implementation is planned.
  • Compute support flag: Check DownlevelFlagsComputeShaders in adapter capabilities.
  • GLES 3.0: No compute shader support (only GLES 3.1+).
  • GLES 3.1+: Compute shaders supported via glDispatchCompute.

GLES-Specific Notes

  • Compute commands are deferred: BeginComputePass records commands into a list that is replayed on Submit.
  • The glMemoryBarrier call is needed after dispatch to ensure writes are visible.
  • Some drivers (especially Mesa llvmpipe on CI) may have limited compute support.

Software Backend

Status: Compute shaders supported via SPIR-V interpreter (v0.27.0+).

The software backend executes compute shaders on CPU through a built-in SPIR-V interpreter. Designed for shader debugging, CI/CD testing, and GPU-less environments — not for production workloads (interpreted, ~100× slower than JIT software renderers).

  • Shader compilation: WGSL → SPIR-V (via gogpu/naga), then interpreted instruction-by-instruction.
  • Storage buffers: Full read/write support. Buffer writes from compute shader are immediately visible.
  • Atomics: OpAtomicIAdd, OpAtomicISub, OpAtomicExchange, OpAtomicCompareExchange, OpAtomicSMin/SMax/UMin/UMax.
  • Workgroup shared memory: Per-workgroup allocation, zeroed at dispatch start.
  • Barriers: OpControlBarrier accepted (sequential execution, no-op for synchronization).
  • Shader debugger: DebugContext with breakpoints, JSON trace, watch variables. Zero overhead when disabled.
  • Workgroup size: Read from OpExecutionMode LocalSize in SPIR-V.
  • CreateQuerySet returns an error (timestamps not supported).
  • DispatchIndirect logs a warning (not yet implemented).

Verified

wgpu/examples/software-test/ — 256-element scaled-copy compute shader, all values match. gogpu/examples/particles/ — 4096-particle orbital simulation (compute + instanced render).

Browser WebGPU Backend

Status: Full compute support via browser WebGPU API.

The browser backend delegates compute operations directly to the browser's WebGPU implementation via syscall/js. No shader compilation in Go — WGSL is passed as a string to createShaderModule().

  • Shader compilation: Browser handles WGSL compilation internally.
  • Storage buffers: Full support via browser GPUBuffer.
  • Timestamp queries: Not yet wired (browser support varies).
  • Indirect dispatch: dispatchWorkgroupsIndirect via syscall/js.
  • Buffer mapping: mapAsync + getMappedRangejs.CopyBytesToGo.
  • Workgroup limits: Determined by browser/GPU combination.

Browser-Specific Notes

  • All compute passes are recorded and submitted like native — beginComputePass, setPipeline, setBindGroup, dispatchWorkgroups, end, queue.submit.
  • Data transfer uses js.CopyBytesToJS/js.CopyBytesToGo for crossing the JS/WASM boundary.
  • Build: GOOS=js GOARCH=wasm go build.

Noop Backend

Status: Compute shaders are not supported.

The noop backend is a testing backend that accepts all API calls without executing anything. All compute pass operations are no-ops.

  • CreateQuerySet returns ErrTimestampsNotSupported.
  • Useful for unit testing command encoder state machines without requiring a GPU.

Workgroup Size Recommendations

Data SizeRecommended @workgroup_sizeNotes
< 1K elements64Small batches, low overhead
1K - 100K64 or 128Good balance for most GPUs
100K - 1M256Better occupancy on discrete GPUs
> 1M256Consider multiple dispatches

The optimal workgroup size depends on the GPU architecture, register usage, and shared memory requirements. When in doubt, use 64 -- it works well across all backends.

Memory Considerations

Buffer Alignment

  • Uniform buffers: 256-byte alignment required on most backends.
  • Storage buffers: No strict alignment, but 16-byte alignment is recommended.
  • Copy operations: Source/destination offsets may need alignment (check Alignments.BufferCopyOffset).

GPU Memory Limits

Large compute workloads may exhaust GPU memory. Monitor for ErrDeviceOutOfMemory errors from buffer and pipeline creation.

Staging Buffers

To read compute results back to the CPU:

  1. Create the output buffer with BufferUsageStorage | BufferUsageCopySrc.
  2. Create a staging (readback) buffer with BufferUsageMapRead | BufferUsageCopyDst.
  3. Use CopyBufferToBuffer to copy from output to staging inside a command encoder.
  4. Submit the command buffer.
  5. Use Buffer.Map(ctx, MapModeRead, 0, size) to map the staging buffer (blocks until GPU completes, or returns ctx.Err() on cancellation).
  6. Use Buffer.MappedRange(offset, size) to access the bytes.
  7. Call Buffer.Unmap() when done.

This two-step process is required because GPU-optimal memory is typically not directly accessible by the CPU.

For non-blocking readback (game loops, frame-budgeted compute), use Buffer.MapAsync + Device.Poll(PollPoll) instead. See COMPUTE-SHADERS.md for full examples, and dev/research/ADR-BUFFER-MAPPING-API.md for design rationale.