Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Welcome to the Rust-GPU dev guide! This documentation is meant for documenting how to use and develop on Rust-GPU.

Important

This introduction assumes a basic understanding of GPU and graphics programming. I highly recommend following a tutorial until you have least a colorful triangle on screen or ran your first few compute shaders.

Some of my recommendations:

  • learn wgpu teaches you the modern WebGPU API with the wgpu crate, but doesn’t go much beyond the basics.
  • learnopengl.com is the best graphics programming guide to this day, but it teaches the older OpenGL API which is not supported by rust-gpu. Still, it is one of the best sources intermediary rendering techniques, which you should be able to transfer to a more modern graphics API, or use glow for Rust OpenGL bindings.
  • How to Vulkan is a tutorial for modern Vulkan and highly recommended over all older Vulkan tutorials. I would not recommend Vulkan for a beginner, since it’s a quite extensive API, but it has the most features, extensions and is the best documented API. Unfortunately, rust-gpu doesn’t support Buffer Device Address (BDA) yet, so you’ll have to resort to ordinary descriptor sets or arrays of descriptor sets like you’d see for bindless textures.

Generating a project

Here we’ll describe how you can use our cmdline utility to generate a rust-gpu project from a template. On the next page we’ll inspect its structure, so you can replicate it in any existing project.

First, install our cargo gpu cli, if you haven’t already:

cargo install cargo-gpu

Then generate a rust-gpu project with:

cargo gpu generate

(This subcommand is merely a wrapper around cargo-generate invoked on our rust-gpu-template repository. If you don’t want to run that command, you can also download any expanded template from the generated subdirectory.)

It will ask you a few questions on what kind of project you want:

Which sub-template should be expanded?

  • graphics: renders a colorful triangle with vertex and fragment shaders

Feel free to PR new templates!

Project Name:

Type whatever recognizable name you want.

What API?

  • wgpu: WebGPU with a nice rusty interface
  • ash: lightweight wrapper around Vulkan

If you’re unsure, wgpu is generally easier to use.

How to integrate rust-gpu?

  • cargo-gpu (recommended): It’s a rust-gpu installation manager that isolates the specific nightly toolchain that rust-gpu requires, allowing the rest of your project to remain on a stable toolchain (or any other toolchain). Other users don’t need the cli installed, it just offers some extra utilities.
  • spirv-builder is the older setup that requires your entire project to be compiled using the specific nightly rust-gpu toolchain.

You should have a new folder named after the project name. Simply cd into it and cargo run should get you a rotating triangle!

It may take a while for mygraphics(build) to finish when you run it for the first time. We have to download the specific rust toolchain we need and build the rust-gpu codegen backend that functions with it. These preparations are cached externally in ~/.cache/rust-gpu/, so even after a cargo clean the build should still be significantly faster.

Workspace structure

In the previous chapter, you’ve generated a new rust-gpu project. Inspecting the directory structure, you’ll find something like this:

$ tree
.
├── Cargo.lock
├── Cargo.toml
├── mygraphics
│   ├── build.rs
│   ├── Cargo.toml
│   └── src
│       ├── lib.rs
│       ├── main.rs
│       └── wgpu_renderer
│           ├── mod.rs
│           ├── renderer.rs
│           ├── render_pipeline.rs
│           └── swapchain.rs
└── mygraphics-shaders
    ├── Cargo.toml
    └── src
        └── lib.rs

Note

This is written against the graphics template with wgpu API and cargo-gpu, though other templates shouldn’t differ much.

The first thing you’ll notice that it’s not just a single crate, but an entire workspace with multiple crates. Refer to cargo’s documentation on workspaces if you’ve never worked with one. You can also see the two subdirectories mygraphics and mygraphics-shaders, which are the two crates in our workspaces.

mygraphics-shaders crate

By the shaders suffix you can already guess this is the crate the shaders live in.

Opening it’s lib.rs, you’ll notice the many different #[spirv(..)] annotations on functions and function arguments. We’ll discuss how shaders are written in the next chapter, so ignore all of these for now and focus on the declaration in the very first line: #![no_std].

This crate is a no_std crate, meaning you won’t have access to the full standard library, but only a platform-independent subset that’s called core. Meaning you won’t be able to do file IO, send network packages or create threads. On a GPU, you won’t have access to these APIs, as the GPU has no operating system to handle these requests for us! You also won’t be able to access alloc, loosing you the Vec, HashMap and String data structures, since (most) GPU APIs won’t offer you a memory allocator either. (Some APIs do give you a built-in memory allocator, like CUDA or HIP, but we’re not targeting those.) Feel free to read up on rust’s no_std documentation for more details.

The crate also has two dependencies:

  • spirv-std is our standard library for GPU functionality. Here you’ll find the #[spirv(..)] annotation as well as various intrinsics you can call to invoke special GPU functionality. The version of this crate also determines the version of the rust-gpu compiler that will be used to compile your project.
  • glam is a simple and fast vector library, providing you with types such as Vec2, Vec3, Vec4 and many more. It even implements some swizzling with Vec2::xyxx() and Vec2::from((vec2, 0., 1.)).

Note that the dependencies are declared like this:

spirv-std = { version = "0.11" }
glam = { version = "0.34", default-features = false }

Warning

The glam crate must have its default features disabled, since by default, it includes the std feature which doesn’t work on GPUs. Our spirv-std crate will implicitly enable the std feature of glam if you’re on a CPU, and will use the fallback libm feature when you’re on the GPU.

Additionally, the version of glam you use must also be compatible with spirv-std. We do support a number of older glam versions in case you’re using a library that hasn’t updated, but you must enable it as a feature. For example, if you want to use glam v0.32, replace the above dependencies with this:

spirv-std = { version = "0.11", default-features = false, features = ["glam_0_32"] }
glam = { version = "0.32", default-features = false }

As of writing this, we support glam v0.30 to v0.33, and expect that support to grow. Note that we do not support multiple glam versions in parallel, you must only enable one version.

mygraphics crate

The mygraphics crate is the main executable you’ve run when you executed cargo run. Here you’ll have full access to std as you’re used to, and the place where you interact with whatever graphics API you’ve chosen. The example implementation we provide are intentionally kept small, just enough to run the demo. Since this isn’t a tutorial on how to use any particular graphics API, please refer to whatever documentation or tutorials exist for the graphics API you want to use.

Building the shader

The same mygraphics crate has a build script that turns our Rust shaders into artifacts these graphics APIs can understand. A build script is run before the crate is compiled, allowing you to create arbitrary files and/or Rust source code that your crate may need, see cargo’s documentation on build scripts for more detail.

Dependencies of build scripts are separate from your crate’s dependencies, so we need to declare those in the mygraphics crate’s Cargo.toml. cargo-gpu-install is a slimmed down version of cargo-gpu cli specifically for build scripts, with anyhow for error throwing convenience.

[build-dependencies]
cargo-gpu-install = "0.11"
anyhow = "1.0"

You’ll find the script in ./mygraphics/build.rs and is arguably the most complicated part of the setup. If you’re setting up a new project and not using a template, I’d recommend copying it and adjusting it as needed. The most important part to adjust is the path to the shader crate you want to build. Easiest is to locate it relative to the crate’s directory with CARGO_MANIFEST_DIR.

let manifest_dir = env!("CARGO_MANIFEST_DIR");
let crate_path = [manifest_dir, "..", "mygraphics-shaders"]
    .iter()
    .copied()
    .collect::<PathBuf>();

The next step is to install the required toolchain and build the rust-gpu codegen backend. Instead of specifying a version to install, you pass in the path to the shader crate. It’ll resolve the version to install from the spirv-std dependency, since those two versions must always exactly match.

let install = Install::from_shader_crate(crate_path.clone())
    .within_build_script()
    .run()?;

From the returned install object you can create arbitrary many SpirvBuilders and configure them. Note that the target to compile for in this example is spirv-unknown-vulkan1.3, we’ll talk a bit more about targets below. Feel free to read up on SpirvBuilders different configuration properties for what all these toggles actually do.

Warning

Within build scripts you must to enable the build_script.defaults property, so cargo knows when to rerun the build script, otherwise you might end up with outdated shader artifacts.

let mut builder = install.to_spirv_builder(crate_path, "spirv-unknown-vulkan1.3");
builder.build_script.defaults = true;
builder.spirv_metadata = SpirvMetadata::Full;
let compile_result = builder.build()?;

The builder is also where you add any extensions or capabilities you may want to enable:

#![allow(unused)]
fn main() {
builder.extensions.push("SPV_KHR_ray_tracing".to_owned());
builder.capabilities.push(Capability::StorageImageWriteWithoutFormat);
}

With the shader artifacts built, we need a way to give that artifact to our binary crate. There’s many different approaches, from json metadata to rust codegen, but by far the easiest is to pass the path to the artifact as an enviromnent variable.

let spv_path = compile_result.module.unwrap_single();
println!("cargo::rustc-env=SHADER_SPV_PATH={}", spv_path.display());

To include the artifact as a binary blob into your crate, it is as simple as

const SHADER_ARTIFACT: &[u8] = include_bytes!(env!("SHADER_SPV_PATH"));

Integrating common graphics APIs

wgpu

Recommended target: spirv-unknown-naga-wgsl

This target will produce a *.wgsl file containing wgsl source code. Internally, we compile to SPIR-V and then use wgpu’s naga to transpile it to wgsl. Include the wgsl file like this:

const WGSL: &str = include_str!(env!("SHADER_SPV_PATH"));
let module: wgpu::ShaderModule = device.create_shader_module(WGSL);

ash / Vulkan

Recommended target: spirv-unknown-vulkan1.1 or higher

This target will produce a *.spv file containing SPIR-V binary, with the ash crate use it like this:

const SPV_BYTES: &[u8] = include_bytes!(env!("SHADER_SPV_PATH"));
let spv: Vec<u32> = ash::util::read_spv(&mut std::io::Cursor::new(SPV_BYTES))?;
let module: ash::vk::ShaderModule = device.create_shader_module(
    &vk::ShaderModuleCreateInfo::default().code(&spv),
    None,
)?;

Writing shaders

In this chapter we will have a look how to make your own shaders in rust-gpu.

This introduction is written based on our rust-gpu template project, we recommend setting it up locally if you want to follow along.

Your first fragment shader

If you open up /mygraphics-shaders/src/lib.rs, you’ll find a function called main_fs like this:

#[spirv(fragment)]
pub fn main_fs(vtx_color: Vec3, output: &mut Vec4) {
    *output = Vec4::from((vtx_color, 1.));
}

This is an ordinary rust function with a special macro attached to it, marking it as a fragment shader (sometimes also called pixel shader). In this demo, it is evaluated once for each pixel of our triangle and computes the color that this particular pixel should be. So if we wanted our triangle to be all red, we could change the contents of the function to:

#![allow(unused)]
fn main() {
*output = Vec4::new(1., 0., 0., 1.);
}

Now cargo run the project and you should see a red triangle.

Just setting the triangle to a single color is boring, so let’s have a look how to declare inputs and outputs.

Inputs and Outputs

The fragment shader above is a great example for the two basic parameters:

  • vtx_color: Vec3 parameters passed by-value declare an input to the shader. In GLSL: in vec3 vtx_color;.
  • output: &mut Vec4: parameters passed by-mutable-reference declare an output to the shader. In GLSL: out vec4 output;.

Unlike most other shader languages, inputs and outputs can’t just be declared anywhere within our code and includes. Instead, they are passed as parameters to your entry point function, centralizing where they can be declared.

Note

While this design makes it a lot easier to figure out all the things your shader is declaring, we are aware that it makes it difficult to share these declarations across multiple shaders. We hope to rectify these problems in future rust-gpu versions.

Built-ins and Bindings

Inputs and outputs are by far not the only declaration, so let’s have a look at the vertex shader:

#[spirv(vertex)]
pub fn main_vs(
    #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] constants: &ShaderConstants,
    #[spirv(vertex_index)] vert_id: i32,
    #[spirv(position)] vtx_pos: &mut Vec4,
    vtx_color: &mut Vec3,
) {
    let time = constants.time * constants.speed + vert_id as f32 * (2. * PI * 120. / 360.);
    let position = vec2(f32::sin(time), f32::cos(time)) + constants.offset;
    *vtx_pos = Vec4::from((position, 0.0, 1.0));

    *vtx_color = [vec3(1., 0., 0.), vec3(0., 1., 0.), vec3(0., 0., 1.)][vert_id as usize % 3];
}

The vertex shader makes the triangle rotate by computing the vertex positions on the GPU.

It declares two built-ins that are only available vertex shader:

  • #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] constants: &ShaderConstants declares a storage buffer on the “slot” at descriptor set 0 and binding 0 in glsl terminology. This translates to bind group 0 and binding 0 in the wgpu API, we’ll have more detailed look at buffers in the next section. GLSL: layout(set = 0, binding = 0) ShaderConstants constants;
  • #[spirv(vertex_index)] vert_id: i32 is an input built-in that is the index of the vertex. GLSL: gl_VertexID
  • #[spirv(position)] vtx_pos: &mut Vec4 is an output built-in to set the position of the vertex for the rasterizer. GLSL: gl_Position
  • vtx_color: &mut Vec3 is a standard output that is forwarded to our fragment shader. GLSL: out vec3 vtx_color

Note

Unfortunately, the built-ins have magic names. You can look up most of them in this compiletest and in the compiler source code here.

Sharing data structures between CPU and shaders

The vertex shader declares a storage buffer of type ShaderConstants, which is defined in the shader crate like this:

#![allow(unused)]
fn main() {
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct ShaderConstants {
    pub time: f32,
    pub speed: f32,
    pub offset: Vec2,
}
}

It is just an ordinary rust struct that derives bytemuck::Pod. Deriving Pod allows us to use the bytemuck library to convert our struct into a slice of u8s, write those bytes into a buffer on the GPU, and convert them back to our ShaderConstants in our shader. But to implement Pod our struct needs to fulfill a complex set of unsafe invariants, which the Pod derive macro will check for:

  • It must be marked with #[repr(C)] so our struct has a consistent layout between our CPU compile and shader compile.
  • The type must not have any padding.
  • It also needs to derive Zeroable
  • And all of the above must also hold for any members of the struct.

Warning

rust-gpu does not verify that your declarations actually implement Pod

Since the CPU crate mygraphics depends on our GPU crate mygraphics-shaders, we can use our ShaderConstants type in our CPU code. Here is an example constructing our ShaderConstants, converting them to a &[u8] and creating a new wgpu buffer with them:

#![allow(unused)]
fn main() {
let shader_constants = ShaderConstants {
    time: get_time(),
    speed: 0.2,
    offset: Vec2::new(0., 0.),
};
let contents = bytemuck::bytes_of(&shader_constants);
let buffer = device.create_buffer_init(&BufferInitDescriptor {
    label: Some("ShaderConstants"),
    usage: BufferUsages::STORAGE,
    contents,
});
}

You can then create a wgpu bind group / vulkan descriptor set with said buffer and submit some work to the GPU with our shader, not detailed here as we assume you to already be familiar with graphics APIs.

Images

Common image formats can be found in the spirv_std::image module. For example, Image2d is a very commonly used type, corresponding to texture2D in GLSL, and is likely what you want if you want a regular old sampled texture. Here’s an example for sampling a texture, though you’d also find many other functions in the docs on Image:

#[spirv(fragment)]
pub fn main(
    #[spirv(descriptor_set = 0, binding = 0)] image2d: &Image2dI,
    #[spirv(descriptor_set = 1, binding = 1)] image2d_array: &Image2dArray,
    #[spirv(descriptor_set = 2, binding = 2)] cubemap: &Cubemap,
    #[spirv(descriptor_set = 3, binding = 3)] sampler: &Sampler,
) {
    let v2 = Vec2::new(0.0, 1.0);
    let v3 = Vec3::new(0.0, 1.0, 2.0);
    let r1: IVec4 = image2d.sample(*sampler, v2);
    let r2: Vec4 = image2d_array.sample(*sampler, v3);
    let r3: Vec4 = cubemap.sample(*sampler, v3);
}

Use storage images to write to images:

#[spirv(fragment)]
pub fn main(#[spirv(descriptor_set = 0, binding = 0)] storage_image: &StorageImage2d) {
    let pixel = UVec2::new(123, 456);
    let color = Vec4::new(0.3, 0.4, 0.5, 0.6);
    unsafe {
        storage_image.write(pixel, color);
    }
}

There are a huge number of combinations of image types in SPIR-V, which are represented by a const generic type called spirv_std::image::Image. Specifying the generic parameters of this type is incredibly tedious, so we have a wrapper macro spirv_std::Image! to write the type instead. The types you’ve used above are simply type aliases for the output of that macro:

#![allow(unused)]
fn main() {
type Image2d = Image!(2D, type=f32, sampled);
type Image2dArray = crate::Image!(2D, type=f32, sampled, arrayed);
type Cubemap = crate::Image!(cube, type=f32, sampled);
}

See the docs of the Image! macro for it’s syntax.

Compute shaders

Unlike vertex or fragment shaders, compute shaders need to declare the size of their workgroup:

#[spirv(compute(threads(8, 8)))]
pub fn main() {}

This one declares a workgroup of the size (8, 8, 1). Any dimensions not declared default to 1.

Here’s a full example of a compute shader that computes c = a + b:

#[spirv(compute(threads(32)))]
pub fn main(
    #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] a: &[u32],
    #[spirv(descriptor_set = 0, binding = 1, storage_buffer)] b: &[u32],
    #[spirv(descriptor_set = 0, binding = 2, storage_buffer)] c: &mut [u32],
    #[spirv(global_invocation_id)] global_index: UVec3,
) {
    let i = global_index.x as usize;
    c[i] = a[i] + b[i];
}

Caution

I want to direct your attention to the c: &mut [u32] arg. In Rust, mutable references don’t just allow you to mutate the referenced memory, but also gives you exclusive access to it. Since this mutable references is shared between all the threads, it can’t be exclusive.

This is a mistake on rust-gpu’s entry point API design, not sure when I’ll get around to fix it.

Here’s the most interesting built-ins and intrinsics for compute shaders, put together into a rather silly shader that sum up 128 values. The shared memory is also a mutable reference that is not actually exclusive, like the mutable buffers.

#![allow(unused)]
fn main() {
// assumption: subgroup size == 32
#[spirv(compute(threads(128)))]
pub fn sum(
    #[spirv(global_invocation_id)] global_invocation_id: UVec3,
    #[spirv(local_invocation_id)] local_invocation_id: UVec3,
    #[spirv(num_workgroups)] num_workgroups: UVec3,
    #[spirv(workgroup_id)] workgroup_id: UVec3,
    #[spirv(workgroup)] shared_memory: &mut [f32; 4],
    #[spirv(spec_constant(id = 0, default = 42))] spec_constant: u32,
    #[spirv(descriptor_set = 0, binding = 0, storage_buffer)] values: &[f32],
    #[spirv(descriptor_set = 0, binding = 1, storage_buffer)] output: &mut f32,
) {
    let value = values[global_invocation_id.x as usize];
    let sum_per_subgroup = spirv_std::arch::subgroup_f_add(value);

    let local_idx = local_invocation_id.x as usize;
    if local_idx % 32 == 0 {
        shared_memory[local_idx / 32] = sum_per_subgroup;
    }

    spirv_std::arch::workgroup_memory_barrier_with_group_sync();
    if local_idx == 0 {
        let mut accum = 0.;
        for i in 0..4 {
            shared_memory[i] += accum;
        }
        *output = accum;
    }
}
}

Note

You currently can’t declare the workgroup size as a specialization constant, open an issue if you need this.

Testing Rust-GPU

Rust-GPU has a couple of different kinds of tests, most can be ran through cargo test. Rust-GPU also has end-to-end tests for compiling Rust and validating its SPIR-V output, which can ran by running cargo compiletest. Finally, Rust-GPU has differential tests, which runs Rust and WGSL shaders and makes sure they have the same output. These can be run with cargo difftest.

cargo test && cargo compiletest && cargo difftest

Compile Tests

Adding Tests

Rust-GPU’s end-to-end test’s use an external version of the compiletest tool as a testing framework. Be sure to check out the repository and the rustc Dev-Guide for more information about how it works, how to configure it, and add new tests.

Blessing Tests

You will occasionally need to “bless” the output from UI tests to update the normalised output, you can do this by passing a --bless flag to cargo compiletest.

cargo compiletest --bless

Filtering Tests

When working on tests, you may need to run cargo compiletest a few times, while changing only a small number of tests. You can avoid having to run all the other (unrelated) tests, by passing substrings of their paths, to cargo compiletest, for example:

cargo compiletest arch/u image

The above command will only test ui/arch/u_*.rs and ui/image/*.rs, and skip everything else. You can also add --bless to update expected outputs, as well.

Testing Different Environments

You can test against multiple different SPIR-V environments with the --target-env flag. By default it is set to unknown.

cargo compiletest --target-env=vulkan1.1
# You can also provide multiple values to test multiple environments
cargo compiletest --target-env=vulkan1.1,spv.1.3

Tracing

rust-gpu has a lot of debug! (or trace!) calls, which print out logging information at many points. These are very useful to at least narrow down the location of a bug if not to find it entirely, or just to orient yourself as to why the compiler backend is doing a particular thing.

To see the logs, you need to set the RUSTGPU_LOG environment variable to your log filter (note the “GPU” in the name). The full syntax of the log filters can be found in the rustdoc of tracing-subscriber.

Use RUSTGPU_LOG_FORMAT to control log output format ("tree", "flat", or "json") and RUSTGPU_LOG_COLOR to manage color output ("always", "never", or "auto").

To trace non-rust-gpu parts of the compiler, set the standard RUSTC_LOG environment variable.

Replacements for old codegen arguments

Before rust-gpu supported tracing, there were special codegen arguments to aid observability. As of PR#196 the they have been removed and replaced with the following:

  • --specializer-debug → RUSTGPU_LOG=rustc_codegen_spirv::specializer=debug

Minimizing bugs in SPIR-V

When debugging problems with SPIR-V generated by rust-gpu, you occasionally need to reduce the SPIR-V in order to make it easily shareable with others. We’ve created a short guide on how to do that.

Prerequisites

In order to build and validate SPIR-V you’re going to install SPIR-V tools.

SPIR-V Template

SPIR-V has some amount of required boilerplate in order to be considered valid, we’ve created a small template to help to get started. This file creates a single empty vertex entry-point with a single floating-point constant.

; bug.spvasm
OpCapability Shader
OpCapability VulkanMemoryModel
OpMemoryModel Logical Vulkan

; == Entry-points ==
OpEntryPoint Vertex %vert_fn "vert"

; == Types ==
%void = OpTypeVoid
%f32 = OpTypeFloat 32

; Function Types
%void_fn = OpTypeFunction %void

; == Constants ==
%f32_1 = OpConstant %f32 1

; == Functions ==
%vert_fn = OpFunction %void None %void_fn
  %block = OpLabel
    OpReturn
OpFunctionEnd

Steps

  1. Assemble your spirv with spirv-as ./bug.spvasm, this will produce a out.spv file containing the assembled code.

  2. The assembled code also needs to be validated with spirv-val out.spv

  3. Once the code has been validated as having no issues, you can use spirv-cross to compile the code to various outputs.

    • GLSL spirv-cross out.spv
    • HLSL spirv-cross --hlsl out.spv
    • MSL spirv-cross --msl out.spv
    • Vulkan GLSL spirv-cross -V out.spv

“Codegen args” (flags/options) supported by the Rust-GPU codegen backend

Please keep in mind that many of these flags/options are for internal development, and may break output unexpectedly and generally muck things up. Please only use these if you know what you’re doing.

Help is also appreciated keeping this document up to date, “codegen args” flags/options may be added/removed on an ad-hoc basis without much thought, as they’re internal development tools, not a public API - this documentation is only here because these flags/options may be helpful diagnosing problems for others.

It’s recommended that “codegen args” options that take paths to files or directories are set to full paths, as the working directory of the compiler might be something wonky and unexpected, and it’s easier to set the full path.


How to set “codegen args” flags/options

The most convenient method is relying on spirv-builder reading the RUSTGPU_CODEGEN_ARGS environment variable, e.g.:

$ RUSTGPU_CODEGEN_ARGS="--no-spirv-val --dump-post-link=$PWD/postlink" cargo run -p example-runner-wgpu
...
    Finished release [optimized] target(s) in 15.15s

$ file postlink/*
postlink/module: Khronos SPIR-V binary, little-endian, version 0x010300, generator 0x1b0000

Notably, RUSTGPU_CODEGEN_ARGS="--help" can be used to see a “usage” message (which lists all the flags/options, including ones not listed in this document), via e.g. running a Cargo build that relies on spirv-builder.

However, it’s only a convenient alias for for RUSTGPU_RUSTFLAGS=-Cllvm-args="..." (without having to expose the fact that LLVM’s name is still attached to rustc’s interface for this functionality), and if in direct control of rustc, you can still pass such “codegen args” flags/options wrapped in -C llvm-args="...".


Historical note about past “environment variables”

Many of these flags/options were at one point, individual environment variable (e.g. the --dump-pre-link option used to be the environment variable DUMP_PRE_LINK).

However, that approach is prone to various failure modes, because the environment variables would not get registered as a “dependency” (without extra work that never happened), and the idea of “codegen args” fits better with existing practices (e.g. rustc -C llvm-args="..." for the LLVM codegen backend of rustc).

For more context see also PR #959, which made the transition to this system.

Where are all the rest of the flags/options documented?

If you do run a build with RUSTGPU_CODEGEN_ARGS="--help" (or -C llvm-args="--help"), you will notice more flags/options than are listed in this documented.

This is a historical artifact: as mentioned above, these used to be environment variables, and this document only described those, without talking about the older “codegen args” at all.

While most of those flags are usually only exposed through higher-level spirv-builder APIs, it would be nice to have all of them documented in one place (eventually?).


Debugging “codegen args” flags/options

As mentioned above, these form the bulk of “codegen args”, but keep in mind the list is not exhaustive and you will want to check the full list with e.g. RUSTGPU_CODEGEN_ARGS="--help".

--dump-mir DIR

Dumps the MIR of every function rust-gpu encounters, to files in DIR. Yes, rustc does have options to do this by default, but I always forget the syntax, and plumbing through the option to spirv-builder is annoying, so this is handy to just hack an output.

FIXME(@eddyb) this may be irrelevant now given RUSTGPU_RUSTFLAGS

--dump-module-on-panic FILE

If codegen panics, then write the (partially) emitted module to FILE. Note that this only exists for codegen, if the linker panics, this option does nothing, sadly.

Dumps all input modules to the linker, to files in DIR, before the linker touches them at all.

--dump-post-merge DIR

Dumps the merged module, to a file in DIR, immediately after merging, but before the linker has done anything else (including, well, linking the methods - LinkageAttributes will still exist, etc.). This is very similar to --dump-pre-link, except it outputs only a single file, which might make grepping through for stuff easier.

--dump-pre-inline DIR

Dumps the module, to a file in DIR, immediately before the inliner pass runs.

--dump-post-inline DIR

Dumps the module, to a file in DIR, immediately after the inliner pass runs.

--dump-post-split DIR

Dumps the modules, to files in DIR, immediately after multimodule splitting.

--dump-post-link DIR

Takes: path to directory

Dumps all output modules from the linker, to files in DIR. This may be multiple files due to the multimodule/module splitting option, hence it takes a directory instead of a file path. This is the final output binary before spirv-opt is executed, so it may be useful to output this to check if an issue is in Rust-GPU, or in spirv-opt.

--specializer-dump-instances FILE

Dumps to FILE all instances inferred by the specializer.

--no-spirv-val

Disables running spirv-val on the final output. Spooky scary option, can cause invalid modules!

--no-spirv-opt

Forcibly disables running spirv-opt on the final output, even if optimizations are enabled.

--no-compact-ids

Disables compaction of SPIR-V IDs at the end of linking. Causes absolutely ginormous IDs to be emitted. Useful if you’re println debugging IDs in the linker (although spirv-opt will compact them anyway, be careful).

--no-early-report-zombies

Delays reporting zombies (aka “deferred errors”) even further, to allow more legalization. Currently this also replaces the zombie reporting with a SPIR-T-based version (which may become the default in the future).

--no-infer-storage-classes

Disables the old SPIR-V “Storage Class” (i.e. address space) inference pass, to allow testing alternatives to it (such as SPIR-T qptr passes).

Note that this will produce illegal SPIR-V by default, and you need e.g. --spirt-passes=qptr in order to regain legal “Storage Class” assignments
(see SPIR-T qptr PR for more information on qptr in general)

--no-structurize

Disables CFG structurization. Probably results in invalid modules.

--spirt (until 0.6.0)

Note: as of rust-gpu 0.6.0, SPIR-🇹 is enabled by default. Use --no-spirt to disable. Note: as of rust-gpu 0.8.0, SPIR-🇹 is always being used and cannot be disabled (to reduce the cost of maintenance, testing and further feature development).

Enables using the experimental SPIR-🇹 shader IR framework in the linker - more specifically, this:

  • adds a SPIR-V -> SPIR-🇹 -> SPIR-V roundtrip
    (future SPIR-🇹 passes would go in the middle of this, and eventually codegen might not produce SPIR-V at all)
  • replaces the existing structurizer with SPIR-🇹 structurization (which is more robust and can e.g. handle OpPhis)
  • runs some existing SPIR-V legalization/optimization passes (mem2reg) before inlining, instead of only after (as the OpPhis they would produce are no longer an issue for structurization)

For more information, also see the SPIR-🇹 repository.

--no-spirt (0.6.0 and 0.7.0)

Note: as of rust-gpu 0.8.0, SPIR-🇹 is always being used and cannot be disabled (to reduce the cost of maintenance, testing and further feature development).

Note: if you were using --no-spirt to work around naga issue #1977
(valid loops causing The 'break' is used outside of a 'loop' or 'switch' context),
you may be able to cargo update -p naga to update to a fixed naga version
(0.11.1 for wgpu 0.15, 0.12.1 for wgpu 0.16, and any later versions).

Disables the SPIR-🇹 shader IR framework in the linker.

--spirt-passes PASSES

Enable additional SPIR-🇹 passes, as listed in PASSES (comma-separated).
Their implementation can be found in rustc_codegen_spirv::linker::spirt_passes.

Note: passes that are not already enabled by default are considered experimental and likely not ready for production use, this flag exists primarily for testing.

--dump-spirt-passes DIR

Dump the SPIR-🇹 module across passes (i.e. all of the versions before/after each pass), as a combined report, to a pair of files (.spirt and .spirt.html) in DIR.
(the .spirt.html version of the report is the recommended form for viewing, as it uses tabling for versions, syntax-highlighting-like styling, and use->def linking)

Mutually exclusive with --dump-spirt (this takes precedence over that).

--dump-spirt DIR

Dump the SPIR-🇹 module, similar to --dump-spirt-passes, but only the final version.

Mutually exclusive with --dump-spirt-passes (which takes precedence over this).

--spirt-strip-custom-debuginfo-from-dumps

When dumping (pretty-printed) SPIR-🇹 (e.g. with --dump-spirt-passes), strip all the custom (Rust-GPU-specific) debuginfo instructions, by converting them to the standard SPIR-V debuginfo (which SPIR-🇹 understands more directly).

The default (keeping the custom instructions) is more verbose, but also lossless, if you want to see all instructions exactly as e.g. --spirt-passes see them.

--spirt-keep-debug-sources-in-dumps

When dumping (pretty-printed) SPIR-🇹 (e.g. with --dump-spirt-passes), preserve all the “file contents debuginfo” (i.e. from SPIR-V OpSource instructions), which will end up being included, in full, at the start of the dump.

The default (of hiding the file contents) is less verbose, but (arguably) lossier.

--spirt-keep-unstructured-cfg-in-dumps

When dumping (pretty-printed) SPIR-🇹 (e.g. with --dump-spirt-passes), include the initial unstructured state, as well (i.e. just after lowering from SPIR-V).

The default (of only dumping structured SPIR-T) can have far less noisy dataflow, but unstructured SPIR-T may be needed for e.g. debugging the structurizer itself.

Inline Assembly

Rust-GPU has support for inline SPIR-V assembly. In addition the backend provides several conveniences for writing inline assembly that are documented below. For more information on specific instruction behaviour and syntax, please refer to the SPIR-V specification.

Basic syntax & usage.

You can write inline assembly using the new asm! macro available with the asm feature on nightly. Refer to the Rust unstable book for more information on how to use the macro.

Non-ID arguments are written as-is, e.g.

#![allow(unused)]
fn main() {
asm! {
    "OpCapability DerivativeControl"
}
}

ID based arguments are prefixed with % and their name. Result<id>s accessed with a = and a ID on the left hand side of the expression. E.g.

#![allow(unused)]
fn main() {
let vector = spirv_std::glam::Vec2::new(1.0, 0.0);
let mut result = f32::default();

asm! {
    "%vector = OpLoad _ {vector}",
    "%element = OpVectorExtractDynamic _ %vector {index}",
    "OpStore {element} %element",
    vector = in(reg) &vector,
    index = in(reg) index,
    element = in(reg) &mut result
}
}

asm! only accepts integers, floats, SIMD vectors, pointers and function pointers as input variables. However you can have the pointer point to a generic variable, so you can write generic assembly code like so.

#![allow(unused)]
fn main() {
use spirv_std::{scalar::Scalar, vector::Vector};

// This fn is available as `spirv_std::arch::vector_extract_dynamic`
pub unsafe fn vector_extract_dynamic<T: Scalar, V: Vector<T>>(vector: V, index: usize) -> T {
    let mut result = T::default();

    asm! {
        "%vector = OpLoad _ {vector}",
        "%element = OpVectorExtractDynamic _ %vector {index}",
        "OpStore {element} %element",
        vector = in(reg) &vector,
        index = in(reg) index,
        element = in(reg) &mut result
    }

    result
}
}

Additional syntax

SyntaxDescription
%<name>Used to refer to an abstract ID, every unique <name> use generates a new ID.
typeof{<variable>}Returns the type of variable
_ (underscore)Equivalent to typeof{<variable>}, but uses inference to determine the variable

Attribute syntax

rust-gpu introduces a number of SPIR-V related attributes to express behavior specific to SPIR-V not exposed in the base rust language.

Before you’ll able to use these attributes, make sure you import the attribute from the spirv-std crate:

#![allow(unused)]
fn main() {
use spirv_std::spirv;
}

There are a few different categories of attributes:

Entry points

When declaring an entry point to your shader, SPIR-V needs to know what type of function it is. For example, it could be a fragment shader, or vertex shader. Specifying this attribute is also the way rust-gpu knows that you would like to export a function as an entry point, no other functions are exported.

Example:

#[spirv(fragment)]
fn main() { }

Common values are #[spirv(fragment)] and #[spirv(vertex)]. A list of all supported names can be found in spirv_headers - convert the enum name to snake_case for the rust-gpu attribute name.

Compute shader dimensions

The dimensions (local_size_* in openGL, numthreads in DX) of a compute shader must be specified (eg. #[spirv(compute(threads(32, 16, 97)))]). Trailing ones may be elided.

Example:

#![allow(unused)]
fn main() {
// the x dimension is required
// same as threads(32, 1, 1)
#[spirv(compute(threads(32)))]
pub fn compute_1() {}

// same as threads(32, 57, 1)
#[spirv(compute(threads(32, 57)))]
pub fn compute_2() {}
}

Override entry point name

You can override the default OpEntryPoint name for any entry point with the entry_point_name sub-attribute on any of the execution model attributes. (e.g. #[spirv(vertex(entry_point_name="foo"))])

Builtins

When declaring inputs and outputs, sometimes you want to declare it as a “builtin”. This means many things, but one example is gl_Position from glsl - the GPU assigns inherent meaning to the variable and uses it for placing the vertex in clip space. The equivalent in rust-gpu is called position.

Example:

#[spirv(vertex)]
fn main(
    #[spirv(position)] out_pos: &mut Vec4,
) { }

Common values are #[spirv(position)], #[spirv(vertex_id)], and many more. A list of all supported names can be found in spirv_headers - convert the enum name to snake_case for the rust-gpu attribute name.

Descriptor set and binding

A SPIR-V shader must declare where uniform variables are located with explicit indices that match up with CPU-side code. This can be done with the descriptor_set and binding attributes. Note that descriptor_set = 0 is reserved for future use, and cannot be used.

Example:

#[spirv(fragment)]
fn main(
    #[spirv(uniform, descriptor_set = 2, binding = 5)] var: &mut Vec4,
) { }

Both descriptor_set and binding take an integer argument that specifies the uniform’s index.

Flat

The flat attribute corresponds to the flat keyword in glsl - in other words, the data is not interpolated across the triangle when invoking the fragment shader.

Example:

#[spirv(fragment)]
fn main(#[spirv(flat)] obj: u32) { }

Invariant

The invariant attribute corresponds to the invariant keyword in glsl. It can only be applied to output variables.

Example:

#[spirv(vertex)]
fn main(#[spirv(invariant)] var: &mut f32) { }

Workgroup shared memory

The workgroup attribute defines shared memory, which can be accessed by all invocations within the same workgroup. This corresponds to groupshared memory in hlsl or shared memory in glsl.

Example:

#[spirv(compute(threads(32)))]
fn main(#[spirv(workgroup)] var: &mut [Vec4; 4]) { }

Generic storage classes

The SPIR-V storage class of types is inferred for function signatures. The inference logic can be guided by attributes on the interface specification in the entry points. This also means it needs to be clear from the documentation if an API requires a certain storage class (e.g workgroup) for a variable. Storage class attributes are only permitted on entry points.

Specialization constants

Entry point inputs also allow access to SPIR-V “specialization constants”, which are each associated with an user-specified numeric “ID” (SPIR-V SpecId), used to override them later (“specializing” the shader):

If a “specialization constant” is not overridden, it falls back to its default value, which is either user-specified (via default = ...), or 0 otherwise.

While only “specialization constants” of type u32 are currently supported, it’s always possible to manually create values of other types, from one or more u32s.

Example:

#[spirv(vertex)]
fn main(
    // Default is implicitly `0`, if not specified.
    #[spirv(spec_constant(id = 1))] no_default: u32,

    // IDs don't need to be sequential or obey any order.
    #[spirv(spec_constant(id = 9000, default = 123))] default_123: u32,

    // Assembling a larger value out of multiple `u32` is also possible.
    #[spirv(spec_constant(id = 100))] x_u64_lo: u32,
    #[spirv(spec_constant(id = 101))] x_u64_hi: u32,
) {
    let x_u64 = ((x_u64_hi as u64) << 32) | (x_u64_lo as u64);
}

Note: despite the name “constants”, they are runtime values from the perspective of compiled Rust code (or at most similar to “link-time constants”), and as such have no connection to Rust constants, especially not Rust type-level constants and const generics - while specializing some e.g. fn foo<const N: u32> by N long after it was compiled to SPIR-V, or using “specialization constants” as Rust array lengths, Rust would sadly require dependent types to type-check such code (as it would for e.g. expressing C T[n] types with runtime n), and the main benefit over truly dynamic inputs is a (potential) performance boost.