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;
    float _padding;
};

[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() {
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct Uniforms {
    width: u32,
    height: u32,
    time: f32,
    _padding: f32,
}
impl goldy::StructuredBufferElement for Uniforms {}
}

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

#![allow(unused)]
fn main() {
let mut retained_pool = RetainedPool::new(device.clone());
let uniform_buffer = retained_pool.acquire_buffer_with_data(
    &[Uniforms { width, height, time: 0.0, _padding: 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, submit the main scheme, claim and consume the surface transaction.

#![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,
        _padding: 0.0,
    };

    state.uniform_deposit.write(
        &mut state.upload_scheme,
        0,
        bytemuck::bytes_of(&uniforms),
    )?;
    state.upload_scheme.submit()?;

    let mut submission = state.scheme.submit()?;
    state.present.claim(&mut submission)?.consume()?;
    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_buffer(
    &mut upload_scheme,
    &uniform_buffer,
    std::mem::size_of::<Uniforms>() as u64,
)?;
}

Step by Step

Update uniformsMemoryExchange::bind_deposit_buffer records the upload topology once; each frame call deposit.write on the upload scheme before the main submit.

Record the scheme onceSurfaceExchange::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 presentscheme.submit() records and submits GPU work. present.claim(&mut submission)?.consume() presents the swapchain image. 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