Your First Compute Shader

This tutorial renders an animated plasma effect by dispatching a compute shader directly to a swapchain drawable — no graphics pipeline, no vertex buffers, no render passes.

See examples/compute_to_surface.rs for the full source.

The Shader

The compute shader uses goldy_exp virtual entry points. It reads uniforms via BufRO<Uniforms> and writes pixels via DirectSpatial<float4>:

import goldy_exp;

struct Uniforms {
    uint width;
    uint height;
    float time;
};

[goldy_compute]
[numthreads(8, 8, 1)]
void cs_main(BufRO<Uniforms> uniforms_buf, DirectSpatial<float4> output, ThreadId tid) {
    Uniforms u = uniforms_buf[0];

    if (tid.x >= u.width || tid.y >= u.height)
        return;

    float2 uv = float2(float(tid.x) / float(u.width),
                       float(tid.y) / float(u.height));
    float2 p = uv * 2.0 - 1.0;
    p.x *= float(u.width) / float(u.height);

    float t = u.time;
    float v = 0.0;
    v += sin(p.x * 6.0 + t);
    v += sin(p.y * 6.0 + t * 1.3);
    v += sin((p.x + p.y) * 4.0 + t * 0.7);
    v += sin(length(p) * 8.0 - t * 2.0);
    v *= 0.25;

    float3 col = float3(0.5 + 0.5 * sin(v * 3.14159 + 0.0),
                        0.5 + 0.5 * sin(v * 3.14159 + 2.094),
                        0.5 + 0.5 * sin(v * 3.14159 + 4.188));
    output[tid.xy] = float4(col, 1.0);
}

Key points:

  • BufRO<Uniforms> is a read-only structured buffer. Index with [0] to load the single element.
  • DirectSpatial<float4> is an RWTexture2D<float4> — write to it with output[tid.xy].
  • ThreadId maps to SV_DispatchThreadID. Each thread handles one pixel.
  • The [goldy_compute] attribute tells the Goldy compiler to wire up bindless slots automatically.

Rust Side

Uniform Buffer

Define the uniform struct on the Rust side with matching layout:

#![allow(unused)]
fn main() {
#[goldy::gpu]
struct Uniforms {
    width: u32,
    height: u32,
    time: f32,
}
}

Create the buffer with BufferKind::Scattered so it gets a bindless descriptor:

#![allow(unused)]
fn main() {
let uniform_buffer = device.acquire_buffer_with_data(
    &[Uniforms { width, height, time: 0.0 }],
    BufferKind::Scattered,
)?;
}

Compute Pipeline and Scheme

Compile the Slang source, create a ComputePipeline, and record a retained scheme once via SurfaceExchange::bind_destination:

#![allow(unused)]
fn main() {
let shader = ShaderModule::from_slang(&device, COMPUTE_SHADER)?;
let compute_pipeline = ComputePipeline::new(&device, &shader)?;

let surface = SurfaceExchange::new_with_depth(&ctx, window.as_ref(), 3, SurfaceConfig::default())?;

let mut scheme = Scheme::new(&ctx);
let wg_x = width.div_ceil(8);
let wg_y = height.div_ceil(8);
let (lease, present) = surface.bind_destination(&mut scheme)?;
scheme
    .node("compute", &compute_pipeline)
    .with_parcel(&uniform_buffer, NodeAccess::Read)
    .with_present(&lease)
    .dispatch(wg_x, wg_y, 1);
}

Rendering a Frame

Each frame: upload new uniform values via a small upload scheme with a bound deposit ((&deposit << &uniforms)?), submit the main scheme, then present with (&mut submission >> &present).take()?.

#![allow(unused)]
fn main() {
fn render_frame(state: &mut RenderState) -> Result<()> {
    let (width, height) = state.surface.size();
    let elapsed = state.start_time.elapsed().as_secs_f32();

    let uniforms = Uniforms {
        width,
        height,
        time: elapsed,
    };

    (&state.uniform_deposit << &uniforms)?;
    state.upload_scheme.submit()?;

    let mut submission = state.scheme.submit()?;
    (&mut submission >> &state.present).take()?;
    Ok(())
}
}

At init, bind the deposit once on a retained upload scheme:

#![allow(unused)]
fn main() {
let mut upload_scheme = Scheme::new(&ctx);
let uniform_deposit = MemoryExchange::new(&ctx).bind_deposit(
    &mut upload_scheme,
    goldy::DepositTarget::buffer_elements::<Uniforms>(&uniform_buffer, 1),
)?;
}

Step by Step

Update uniforms — MemoryExchange::bind_deposit records the upload topology once; each frame tender (&deposit << &uniforms)? before the main submit. write remains for offsets and partial fills.

Record the scheme once — SurfaceExchange::bind_destination registers the present exchange and returns a PresentLease plus a Transaction. scheme.node() creates a compute node bound to a pipeline. with_parcel() declares the uniform buffer dependency. with_present() binds the drawable lease. dispatch() sets the workgroup count.

Submit and present — scheme.submit() records and submits GPU work. (&mut submission >> &present).take()? presents the swapchain image. The explicit &mut borrow is required by operator semantics and leaves other claims on the submission untouched. The compute shader already wrote the pixels — there is no blit or copy step.

Run It

cargo run --example compute_to_surface

You should see an animated plasma pattern filling the window, rendered entirely from compute.

Next Steps