|
| 1 | +# Compute Shaders |
| 2 | + |
| 3 | +The `yup_rhi` module supports GPU compute shaders through `GpuComputePipeline` |
| 4 | +and `GpuComputePass`. Compute shaders run general-purpose GPU work — audio DSP, |
| 5 | +physics simulation, particle systems, image processing — without any window, |
| 6 | +framebuffer, or graphics pipeline. |
| 7 | + |
| 8 | +## Availability |
| 9 | + |
| 10 | +Compute shaders are available on backends that expose |
| 11 | +`GpuDevice::isComputeAvailable() == true`: **Metal**, **Direct3D 11**, |
| 12 | +**WebGPU** (Dawn and Emscripten), and **OpenGL 4.3+** / **OpenGL ES 3.1+**. |
| 13 | + |
| 14 | +Compute is **not** available on the Headless backend. |
| 15 | + |
| 16 | +## Architecture |
| 17 | + |
| 18 | +Compute shaders bypass Rive's ore layer (`rive::ore`) entirely. The ore layer |
| 19 | +does not yet expose compute dispatch, so `GpuComputePipeline` and |
| 20 | +`GpuComputePass` go directly to the backend-native API: |
| 21 | + |
| 22 | +| Backend | Pipeline compilation | Dispatch | |
| 23 | +| ------------ | -------------------------------------- | ---------------------------------- | |
| 24 | +| Metal | `MTLComputePipelineState` | `dispatchThreadgroups:` | |
| 25 | +| Direct3D 11 | `ID3D11ComputeShader` | `ID3D11DeviceContext::Dispatch()` | |
| 26 | +| WebGPU | `wgpu::ComputePipeline` | `DispatchWorkgroups()` | |
| 27 | +| OpenGL | `GL_COMPUTE_SHADER` + program link | `glDispatchCompute()` | |
| 28 | + |
| 29 | +## Compiling a compute pipeline |
| 30 | + |
| 31 | +### From GLSL (online, requires `YUP_ENABLE_SHADER_TRANSPILER`) |
| 32 | + |
| 33 | +```cpp |
| 34 | +auto result = GpuComputePipeline::compileFromGlsl (device, glslSource); |
| 35 | +if (result.wasOk()) |
| 36 | + auto pipeline = result.getValue(); |
| 37 | +``` |
| 38 | + |
| 39 | +The transpiler compiles GLSL → SPIR-V, reflects the workgroup size from |
| 40 | +`layout(local_size_x=...)`, transpiles to the backend-native language (MSL, |
| 41 | +HLSL, WGSL), and compiles the final pipeline. |
| 42 | + |
| 43 | +### From a `.ysl` shader bundle (offline) |
| 44 | + |
| 45 | +```cpp |
| 46 | +auto bundle = ShaderBundle::loadFromFile (File ("audio_effect.ysl")); |
| 47 | +if (bundle.wasOk()) |
| 48 | +{ |
| 49 | + auto result = GpuComputePipeline::compileFromBundle (device, bundle.getReference()); |
| 50 | + if (result.wasOk()) |
| 51 | + auto pipeline = result.getValue(); |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +### From raw native source (any backend) |
| 56 | + |
| 57 | +```cpp |
| 58 | +GpuShaderSource source; |
| 59 | +source.language = GpuShaderLanguage::msl; // or hlsl, wgsl, glsl |
| 60 | +source.code = mslSource; |
| 61 | +source.codeSize = mslLength; |
| 62 | + |
| 63 | +auto result = GpuComputePipeline::compile (device, source, { 256, 1, 1 }); |
| 64 | +``` |
| 65 | +
|
| 66 | +## Dispatching compute work |
| 67 | +
|
| 68 | +```cpp |
| 69 | +auto pass = GpuComputePass::begin (device); |
| 70 | +
|
| 71 | +pass.setPipeline (pipeline); |
| 72 | +pass.setStorageBuffer (0, 0, inputBuffer); // SSBO binding (set=0, binding=0) |
| 73 | +pass.setStorageBuffer (0, 1, outputBuffer); // SSBO binding (set=0, binding=1) |
| 74 | +pass.setUniformBuffer (0, 2, ¶ms, sizeof params); |
| 75 | +
|
| 76 | +uint32_t groupsX = (numElements + 255) / 256; // workgroupSize.x = 256 |
| 77 | +pass.dispatch (groupsX, 1, 1); |
| 78 | +pass.finish(); // commits work to the GPU |
| 79 | +``` |
| 80 | + |
| 81 | +## Storage buffers |
| 82 | + |
| 83 | +Storage buffers (`GpuBufferType::storage`) are read-write GPU buffers for compute |
| 84 | +shaders. Create them with `GpuBuffer::create()`: |
| 85 | + |
| 86 | +```cpp |
| 87 | +std::vector<float> data (numSamples, 0.0f); |
| 88 | +auto buf = GpuBuffer::create (device, |
| 89 | + GpuBufferType::storage, |
| 90 | + data.data(), |
| 91 | + data.size() * sizeof (float)); |
| 92 | +``` |
| 93 | +
|
| 94 | +Unlike vertex/index/uniform buffers that go through ore, storage buffers are |
| 95 | +allocated directly on the native API (Metal `MTLBuffer`, D3D11 structured |
| 96 | +buffer + UAV, WebGPU `Storage` buffer, GL `GL_SHADER_STORAGE_BUFFER`). |
| 97 | +
|
| 98 | +### Updating a storage buffer in place |
| 99 | +
|
| 100 | +`GpuBuffer` is immutable once created — `GpuDevice::createBuffer()` always |
| 101 | +allocates a new native resource. For code that feeds a storage buffer new |
| 102 | +data every frame or audio callback (a compute effect processing a live |
| 103 | +stream, for instance), reallocating on every iteration is expensive and, on |
| 104 | +a real-time thread such as an audio callback, unsafe: buffer allocation has |
| 105 | +unbounded, driver-dependent latency and can cause audible dropouts. |
| 106 | +
|
| 107 | +`GpuDevice::updateBuffer()` writes new data into an *existing* storage buffer |
| 108 | +without reallocating it: |
| 109 | +
|
| 110 | +```cpp |
| 111 | +// Once, outside the hot loop: |
| 112 | +auto buf = GpuBuffer::create (device, GpuBufferType::storage, initialData, byteSize); |
| 113 | +
|
| 114 | +// Every frame / audio callback — no allocation: |
| 115 | +device->updateBuffer (buf, newData, byteSize); |
| 116 | +``` |
| 117 | + |
| 118 | +`byteSize` must not exceed the buffer's original size. Supported on all |
| 119 | +compute-capable backends (Metal, D3D11, WebGPU/Dawn, OpenGL). |
| 120 | + |
| 121 | +### Buffer binding indices on Metal |
| 122 | + |
| 123 | +`GpuComputePass`'s native dispatch binds Metal buffer arguments directly as |
| 124 | +`group*16 + binding` (see `native/yup_GpuComputePass_metal.cpp`) — there is no |
| 125 | +reflection layer translating GLSL `(set, binding)` pairs to the compiled |
| 126 | +function's actual `[[buffer(N)]]` indices, unlike the render pipeline path. |
| 127 | +This requires the transpiled MSL's argument indices to exactly match the |
| 128 | +GLSL/SPIR-V declared `binding=N` values. The transpiler enforces this by |
| 129 | +setting `CompilerMSL::Options::enable_decoration_binding = true`; without it, |
| 130 | +spirv-cross assigns MSL buffer indices via its own auto-incrementing scheme, |
| 131 | +which can silently diverge from the declared bindings for any shader with |
| 132 | +more than one buffer resource, producing a pipeline that compiles and |
| 133 | +dispatches without error but never actually reads/writes the intended data. |
| 134 | + |
| 135 | +## Example: GPU audio effect |
| 136 | + |
| 137 | +The `GpuAudioProcessingDemo` example demonstrates real-time audio processing on |
| 138 | +the GPU: |
| 139 | + |
| 140 | +1. `AudioIODeviceCallback` captures live audio input |
| 141 | +2. Audio samples are written into a preallocated `GpuBuffer` storage buffer via |
| 142 | + `updateBuffer()` — no GPU allocation happens on the audio thread |
| 143 | +3. A compute shader applies gain + soft clipping |
| 144 | +4. Processed samples are read back from the GPU |
| 145 | +5. Results are routed to the audio output |
| 146 | + |
| 147 | +The compute shader runs on the audio I/O thread, using a dedicated `GpuDevice` |
| 148 | +that does not share state with the render thread. The tiny per-block |
| 149 | +parameters (gain, mix) stay a uniform buffer bound via `setUniformBuffer()` — |
| 150 | +`dispatch()` allocates a small temporary buffer for it on every call, but at |
| 151 | +16 bytes that's negligible next to the audio-block-sized input buffer that |
| 152 | +`updateBuffer()` now avoids reallocating. |
| 153 | + |
| 154 | +```glsl |
| 155 | +#version 450 |
| 156 | +layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in; |
| 157 | +
|
| 158 | +layout(std430, set = 0, binding = 0) buffer InputBuf { float inData[]; }; |
| 159 | +layout(std430, set = 0, binding = 1) buffer OutputBuf { float outData[]; }; |
| 160 | +layout(std140, set = 0, binding = 2) uniform Params { float gain; float mix; }; |
| 161 | +
|
| 162 | +void main() { |
| 163 | + uint i = gl_GlobalInvocationID.x; |
| 164 | + float s = inData[i] * gain; |
| 165 | + outData[i] = tanh(s) * mix + inData[i] * (1.0 - mix); |
| 166 | +} |
| 167 | +``` |
0 commit comments