# 3. Compute Shader Fundamentals
Part of [[Compute Shaders and GPU Instancing]]. Prev: [[2. RenderMeshInstanced Batches]]. Next: [[4. Indirect Draws with GraphicsBuffer]].
## 3.1 The mental model
A compute shader is a function the GPU runs **many times in parallel**. You choose:
- a **kernel** (an entry point marked `#pragma kernel Name`);
- a **thread group size** inside the shader: `[numthreads(64,1,1)]` means 64 threads per group;
- how many **groups** to launch from C#: `Dispatch(kernel, groupsX, groupsY, groupsZ)`.
Each thread gets an ID. `SV_DispatchThreadID` is the global index (group index x group size + thread index), which you usually use as "my element". `SV_GroupIndex` is the flattened index inside the group, which you use for `groupshared` memory (part 5).
**Dispatch maths:** for N elements and a group size G, launch `ceil(N / G)` groups and guard with `if (id.x >= N) return;` because the last group overshoots.
## 3.2 First kernel: a wave grid
`WaveGrid.compute` (Create > Shader > Compute Shader):
```hlsl
#pragma kernel CSMain
// Per-point output: world position.
RWStructuredBuffer<float3> _Positions;
uint _Resolution; // points per side
float _Spacing;
float _SimTime;
float _Amplitude;
[numthreads(8, 8, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
if (id.x >= _Resolution || id.y >= _Resolution) return; // guard the overshoot
float2 xz = (float2(id.xy) - _Resolution * 0.5) * _Spacing;
float r = length(xz);
float y = sin(r * 0.6 - _SimTime * 3.0) * _Amplitude / (1.0 + r * 0.05);
_Positions[id.y * _Resolution + id.x] = float3(xz.x, y, xz.y);
}
```
`WaveGridDriver.cs`:
```csharp
using UnityEngine;
using UnityEngine.Rendering;
/// <summary>Runs WaveGrid.compute every frame and (optionally) reads a few values back. NOT compiled.</summary>
public sealed class WaveGridDriver : MonoBehaviour
{
[SerializeField] ComputeShader shader;
[SerializeField, Range(8, 1024)] int resolution = 256;
[SerializeField] float spacing = 0.25f;
[SerializeField] float amplitude = 1.5f;
[SerializeField] bool debugReadback;
static readonly int PositionsId = Shader.PropertyToID("_Positions");
static readonly int ResolutionId = Shader.PropertyToID("_Resolution");
static readonly int SpacingId = Shader.PropertyToID("_Spacing");
static readonly int TimeId = Shader.PropertyToID("_SimTime");
static readonly int AmplitudeId = Shader.PropertyToID("_Amplitude");
public GraphicsBuffer Positions { get; private set; }
public int Count => resolution * resolution;
int _kernel;
uint _gx, _gy;
bool _readbackInFlight;
void OnEnable()
{
if (!SystemInfo.supportsComputeShaders)
{
Debug.LogError("Compute shaders are not supported on this device/graphics API.");
enabled = false;
return;
}
_kernel = shader.FindKernel("CSMain");
shader.GetKernelThreadGroupSizes(_kernel, out _gx, out _gy, out _);
// Structured buffer of float3: stride 12 bytes.
Positions = new GraphicsBuffer(GraphicsBuffer.Target.Structured, Count, sizeof(float) * 3);
}
void OnDisable()
{
Positions?.Release();
Positions = null;
}
void Update()
{
shader.SetBuffer(_kernel, PositionsId, Positions);
shader.SetInt(ResolutionId, resolution);
shader.SetFloat(SpacingId, spacing);
shader.SetFloat(TimeId, Time.time);
shader.SetFloat(AmplitudeId, amplitude);
int groupsX = Mathf.CeilToInt(resolution / (float)_gx);
int groupsY = Mathf.CeilToInt(resolution / (float)_gy);
shader.Dispatch(_kernel, groupsX, groupsY, 1);
if (debugReadback && !_readbackInFlight)
{
_readbackInFlight = true;
AsyncGPUReadback.Request(Positions, req =>
{
_readbackInFlight = false;
if (req.hasError) { Debug.LogWarning("Readback failed"); return; }
var data = req.GetData<Vector3>();
Debug.Log(
quot;centre height = {data[Count / 2 + resolution / 2].y:0.00}");
});
}
}
}
```
### Step by step
1. **Support check.** `SystemInfo.supportsComputeShaders` is false on some graphics APIs (and historically on WebGL; see [[Web and WebGL Builds]]). Fail loudly.
2. **`FindKernel` + `GetKernelThreadGroupSizes`.** Ask the shader for its group size instead of hard-coding 8 in C#. Change `numthreads` once, and the C# stays correct.
3. **`GraphicsBuffer(Target.Structured, count, stride)`.** `stride` must match the HLSL element size exactly (`float3` = 12 bytes). Mismatched strides give garbage, not errors.
4. **Bind every frame** you change something. `SetBuffer` is per kernel; `SetFloat`/`SetInt` are shader-wide.
5. **`Dispatch`** with `ceil(N/G)` groups. The guard in HLSL handles the overshoot.
6. **`Release()`** in `OnDisable`. Forgetting it leaks GPU memory and Unity logs "GarbageCollector disposing of GraphicsBuffer" warnings on domain reload.
7. **Readback is asynchronous.** `AsyncGPUReadback.Request` returns later (often a few frames). Never use `GraphicsBuffer.GetData` in a game loop: it stalls the CPU until the GPU catches up.
`_SimTime` is our own property, set with `SetFloat`. A compute shader does not receive material built-ins such as `_Time` automatically, and reusing built-in names invites confusion when the same code is pasted into a material shader.
## 3.3 Visualise it quickly
Hand `Positions` to the indirect shader in part 4 (each point drawn as a small cube) or to VFX Graph with `VisualEffect.SetGraphicsBuffer` ([[VFX Graph]] part 5). Either way the data never leaves the GPU.
## 3.4 Threads, groups and limits
- Group sizes of **64 or 256** threads per group are safe defaults on desktop GPUs. The per-group maximum is API-dependent (1024 on D3D11/12 is the common ceiling; check `SystemInfo.maxComputeWorkGroupSize` for the device).
- `numthreads(8,8,1)` for 2D grids, `(256,1,1)` for flat lists.
- Branching is allowed, but threads in the same wave that take different branches serialise. Keep per-thread work uniform where you can.
## 3.5 Gameplay readback
Gameplay often needs *something* from the GPU (did any agent hit the player?). Pattern:
1. Write a tiny result buffer in the kernel (one `uint` counter via `InterlockedAdd`, or a short list).
2. `AsyncGPUReadback.Request` it every frame (only if the last one finished).
3. Accept that the answer is 1-3 frames old, and design for it (Level Up! would call that "fair": the hit already happened visually).
## 3.6 Exercise
Change `numthreads` to `(16,16,1)` and confirm the C# still dispatches correctly. Then add a second kernel `CSRipple` that adds a ripple at a clicked point (pass it with `SetVector`) and dispatch both kernels in order.
## Sources
- https://docs.unity3d.com/6000.5/Documentation/ScriptReference/ComputeShader.Dispatch.html
- https://docs.unity3d.com/6000.5/Documentation/ScriptReference/ComputeShader.FindKernel.html
- https://docs.unity3d.com/6000.5/Documentation/ScriptReference/ComputeShader.GetKernelThreadGroupSizes.html
- https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Rendering.AsyncGPUReadback.Request.html
- https://docs.unity3d.com/6000.5/Documentation/ScriptReference/SystemInfo-supportsComputeShaders.html
- https://docs.unity3d.com/6000.5/Documentation/Manual/class-ComputeShader.html