Skip to content

Commit 62dbe94

Browse files
committed
Compute shaders
1 parent 7b06b78 commit 62dbe94

36 files changed

Lines changed: 3727 additions & 62 deletions

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
4040
- **Backends**: `GpuDevice` has native implementations for all platforms (Metal, OpenGL, Direct3D 11, Dawn, WebGPU/Emscripten, Headless). OpenGL backend probes `GL_VERSION` at runtime to detect compute shader support (GL ≥4.3 / GLES ≥3.1).
4141
- **`::Ptr` safety**: all RHI types that own resources (`GpuPipeline`, `GpuBuffer`, `GpuTexture`, `GpuTarget`, `GpuCanvas`) are reference-counted with `::Ptr`. Factory methods take `GpuDevice::Ptr` to keep the device alive for the resource's lifetime. `GpuFrame` is move-only stack RAII and takes `GpuDevice&` (no ownership).
4242

43+
#### Compute Shaders & GPU Audio
44+
45+
- New `GpuComputePipeline` class (`rhi/yup_GpuComputePipeline.h`): an immutable compiled compute pipeline that bypasses ore to go directly to the backend-native API (Metal `MTLComputePipelineState`, D3D11 `ID3D11ComputeShader`, WebGPU/Dawn `wgpu::ComputePipeline`, OpenGL `GL_COMPUTE_SHADER`). `compile(ctx, source, GpuWorkgroupSize)`, `compileFromBundle(ctx, ShaderBundle)`, and `compileFromGlsl(ctx, glsl)` (when `YUP_ENABLE_SHADER_TRANSPILER = 1`) all return `ResultValue<GpuComputePipeline::Ptr>`.
46+
- New `GpuComputePass` class (`rhi/yup_GpuComputePass.h`): move-only RAII compute dispatch encoder (`GpuComputePass::begin(device)`). Binds a `GpuComputePipeline`, storage buffers (`setStorageBuffer`), uniform buffers (`setUniformBuffer`), and textures (`setTexture`), then dispatches workgroups via `dispatch(gx, gy, gz)`.
47+
- `GpuBuffer` extended with `GpuBufferType::storage`: native storage buffer creation for each backend (Metal `MTLBuffer`, D3D11 structured buffer + UAV, WebGPU `Storage` buffer, OpenGL `GL_SHADER_STORAGE_BUFFER`). Storage buffers are bound to `GpuComputePass::setStorageBuffer()`.
48+
- `GpuDevice` backends expose native compute handles: `getDevice()`/`getCommandQueue()` (Metal), `getD3DDevice()`/`getD3DDeviceContext()` (D3D11), `getWgpuDevice()`/`getWgpuQueue()` (WebGPU/Emscripten), `getBackendDevice()`/`getDevice()`/`getQueue()` (Dawn).
49+
- `GpuAudioProcessingDemo` example: real-time GPU-accelerated audio effect (gain + soft clipper) using compute shaders. Captures live audio via `AudioIODeviceCallback`, uploads to GPU storage buffers, dispatches a compute shader, and reads back processed audio — all on the audio I/O thread.
50+
- New `GpuDevice::updateBuffer()`: writes new data into an existing storage buffer without reallocating it (Metal `contents` memcpy, D3D11 `UpdateSubresource`, WebGPU/Dawn `WriteBuffer`, GL `glBufferSubData`). Fixes `GpuAudioProcessingDemo` reallocating its input storage buffer every audio callback, which caused audible stutter. The gain/mix parameters remain a uniform buffer (as before) — that path is unaffected and its small per-dispatch allocation is negligible next to the audio-block-sized buffer this fix removes.
51+
- Fixed `ShaderTranspiler`'s MSL backend assigning storage/uniform buffer indices via spirv-cross's own auto-incrementing scheme instead of the shader's declared `layout(binding=N)`: added `CompilerMSL::Options::enable_decoration_binding = true` so the compiled `[[buffer(N)]]` index always matches the declared binding, matching what `GpuComputePass`'s native dispatch (which binds slots as `group*16+binding` with no reflection indirection) requires. This was silently producing zero output from any Metal compute shader with more than one storage/uniform buffer, including `GpuAudioProcessingDemo`.
52+
- Metal `GpuDevice`/`GpuComputePass` calls now wrap their Objective-C work in `@autoreleasepool` blocks — without one, real-time callers (e.g. an audio thread with no ambient pool) accumulated command buffers/encoders indefinitely.
53+
4354
#### Image Formats
4455

4556
- Added TIFF read/write support (`TiffImageFormat`) via libtiff: RGB, RGBA, Grayscale at 8/16-bit; multi-page reading; DPI and EXIF/ICC/XMP metadata extraction.
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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, &params, 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+
```

docs/graphics/rhi/index.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ and textures while remaining portable across Metal, Direct3D, OpenGL / OpenGL ES
77
WebGL2, WebGPU, and Vulkan (in progress).
88

99
Use the RHI when you need custom GPU work that the 2D `Graphics` API does not
10-
express - 3D geometry, post-process effects, compute-style fullscreen passes, or
11-
offscreen render-to-texture pipelines. For GPU compute without any window or
10+
express - 3D geometry, post-process effects, compute passes for DSP or simulation,
11+
or offscreen render-to-texture pipelines. For GPU compute without any window or
1212
graphics (e.g. audio DSP on the GPU), use `GpuDevice` directly — no
1313
`GraphicsContext` or `yup_graphics` dependency needed.
1414

@@ -19,8 +19,9 @@ graphics (e.g. audio DSP on the GPU), use `GpuDevice` directly — no
1919
| Draw 2D vector content (paths, text, images) | `Graphics` (not the RHI) |
2020
| Render custom geometry with your own shaders | `GpuPipeline` + `GpuRenderPass` |
2121
| Apply a fullscreen post-process effect | `GpuPipeline` (fullscreen) |
22+
| Run GPU compute (DSP, simulation) | `GpuComputePipeline` + `GpuComputePass` |
2223
| Render offscreen and sample the result as a texture | `GpuTarget` or `GpuCanvas` |
23-
| Mix 2D drawing *and* custom passes on one surface | `GpuCanvas` |
24+
| Mix 2D drawing *and* custom passes on one surface | `GpuCanvas` |
2425

2526
## Classes at a glance
2627

@@ -30,10 +31,14 @@ graphics (e.g. audio DSP on the GPU), use `GpuDevice` directly — no
3031
submit.
3132
- **`GpuRenderPass`** - records draw commands (pipeline, bindings, draws) into a
3233
render target within a frame.
34+
- **`GpuComputePass`** - records compute dispatch commands (pipeline, storage
35+
buffers, uniforms) directly against the backend-native API.
3336
- **`GpuPipeline`** - an immutable, compiled vertex + fragment pipeline plus
3437
fixed state.
38+
- **`GpuComputePipeline`** - an immutable, compiled compute pipeline (single
39+
compute stage, native backend API, no ore dependency).
3540
- **`GpuPipelineCache`** - thread-safe compile-or-fetch cache for pipelines.
36-
- **`GpuBuffer`** - an immutable vertex, index, or uniform buffer.
41+
- **`GpuBuffer`** - an immutable vertex, index, uniform, or storage buffer.
3742
- **`GpuTexture`** - an opaque GPU texture, the currency between passes,
3843
`Image`, and `Graphics::drawTexture`.
3944
- **`GpuTarget`** - a minimal offscreen render surface for render-pass-only work.

0 commit comments

Comments
 (0)