# 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