Scope: current community-owned Rust GPU mainline (rust-gpu), compiled to SPIR-V.
Audience: people writing real shaders and build pipelines, not just toy examples.
Goal: one clear reference for the parts you actually need: setup, crate structure, build flow, attributes, data layout, resources, images, advanced stages, debugging, testing, and the gotchas that matter on the current community version.
Read this first
Rust GPU is still a moving target. Treat it like a pinned toolchain snapshot, not a casually upgradable library stack.
- Keep all Rust-GPU crates on the exact same version/revision.
- Use the exact nightly expected by the current Rust-GPU mainline.
- Prefer the current
mainbranch / pinned git revision when following “latest community version” guidance.- When docs and source disagree, trust the current repo source, examples, and tests over older book text.
Rust GPU is a Rust compiler backend plus standard-library-like support crates for writing GPU shaders in Rust and compiling them to SPIR-V.
Core pieces:
rustc_codegen_spirv— the SPIR-V codegen backend.spirv-std— shader-side APIs, attributes, intrinsics, images, ray tracing types, etc.spirv-builder— the recommended way to build shader crates from a host crate /build.rs.
Mental model:
- Your host app is ordinary Rust.
- Your shader crate is
no_std-style Rust compiled for a SPIR-V target. - A shared crate can hold math helpers and ABI-stable structs used by both CPU and GPU.
For the current community version, the important facts are:
- The project is community-owned under the Rust-GPU organization.
- The current workspace version is
0.9.0. - The workspace edition is Rust 2024.
- The repo pins
nightly-2026-04-02withrust-src,rustc-dev, andllvm-tools. - The project is technically usable but not yet production-ready.
- The project explicitly warns that it does not offer backward-compatibility guarantees and that the supported path is building from the latest
main.
A. “Latest community version” / most accurate
- Pin the Rust-GPU repo by git revision.
- Copy the repo’s
rust-toolchain.toml. - Keep your shader-side Rust-GPU crates on that same revision.
B. Release-based
- Use the exact same published version for all Rust-GPU crates.
- Do not mix
spirv-builderfrom one release withspirv-stdfrom another.
Do not think of it as “just another crate dependency”. The compiler backend, shader stdlib, and builder must line up.
A good workspace layout looks like this:
my-project/
├─ Cargo.toml
├─ rust-toolchain.toml
├─ build.rs # host-side shader build
├─ crates/
│ ├─ app/ # host app
│ ├─ shaders/my_shader/ # shader crate
│ └─ shared/ # CPU/GPU shared code and ABI types
Host crate
- owns the windowing / renderer / pipelines / descriptors / bind groups
- invokes
spirv-builderinbuild.rs - loads
.spvproduced by the builder
Shader crate
- contains entry points and shader-only resource bindings
- uses
#![cfg_attr(target_arch = "spirv", no_std)] - imports
spirv_std::spirv
Shared crate
- contains:
#[repr(C)]structs shared with the host- POD-safe constant blocks / push constants / per-dispatch structs
- pure math / utility functions that should run on CPU and GPU
- should avoid host-only APIs
Start by copying the Rust-GPU repo’s rust-toolchain.toml into your workspace.
This is the single most important setup rule.
Why:
rustc_codegen_spirvis tightly coupled to Rust internals.- A “nearby” nightly is often not good enough.
- If the version mismatches, builds can fail or behave incorrectly.
The recommended path for real applications is a host-side build.rs using spirv-builder.
use spirv_builder::{SpirvBuilder, SpirvMetadata};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut builder =
SpirvBuilder::new("crates/shaders/my_shader", "spirv-unknown-vulkan1.1")
.deny_warnings(true)
.spirv_metadata(SpirvMetadata::NameVariables)
.preserve_bindings(true);
// Recommended build-script behavior.
builder.build_script.defaults = true;
// Emit an env var like `my_shader.spv` that points to the built module.
builder.build_script.env_shader_spv_path = Some(true);
// Optional: avoid incremental target-dir clashes if you have multiple builders.
// builder.target_dir_path = Some("my-shader-builder".into());
builder.build()?;
Ok(())
}spirv-builderhandles the nested SPIR-V build correctly.build_script.defaults = trueturns on the useful build-script defaults.env_shader_spv_path = truemakes the.spveasy to include from the host.SpirvMetadata::NameVariablesis usually the best reflection/debugging compromise.preserve_bindings(true)helps when tooling or reflection depends on bindings staying visible.
const SHADER: &[u8] = include_bytes!(env!("my_shader.spv"));For wgpu, you may instead use its SPIR-V inclusion helpers.
This is a major practical best practice.
rustc_codegen_spirv is a build dependency. If your build dependencies are left unoptimized in dev mode, shader builds can become painfully slow.
Put this in your workspace Cargo.toml:
[profile.release.build-override]
opt-level = 3
codegen-units = 16
[profile.dev.build-override]
opt-level = 3If you mirror Rust-GPU’s own workspace more closely, you can also use stronger release build-override settings. The key idea is the same: optimize build dependencies.
If you build:
- multiple shader crates,
- multiple target variants,
- host + Android/browser variants,
- or several builders in one workspace,
give each builder a unique target_dir_path to prevent incremental artifact clashes.
builder.target_dir_path = Some("my-shader-target".into());By default, Rust GPU prefers bundling entry points into a single SPIR-V module.
Use:
.multimodule(true)only when downstream tooling insists on one module per entry point or behaves badly with multi-entry modules.
multimodule = true and build_script.env_shader_spv_path = true do not work together.
You can build shader crates manually via .cargo/config.toml, but this is the lower-level path.
Use it only when you specifically need:
- direct manual control,
- standalone shader-crate builds,
- or custom integration not centered on a host
build.rs.
That route typically requires:
- building
rustc_codegen_spirv, - pointing Cargo at the dynamic backend,
- registering the
rust_gputool via-Zcrate-attr, - and using
build-std=["core"].
For most applications, spirv-builder is cleaner and safer.
A strong default shader crate starts like this:
#![cfg_attr(target_arch = "spirv", no_std)]
#![deny(warnings)]
use spirv_std::spirv;
use spirv_std::glam::{vec4, Vec4};
#[spirv(fragment)]
pub fn main_fs(output: &mut Vec4) {
*output = vec4(1.0, 0.0, 0.0, 1.0);
}cfg_attr(target_arch = "spirv", no_std)lets the same crate or shared code remain usable on CPU builds when practical.#![deny(warnings)]is helpful because shader-build warnings can otherwise be easy to miss.spirv_std::spirvis the standard attribute entry point.spirv_std::glamkeeps math types aligned with what Rust GPU expects.
Use:
use spirv_std::spirv;
use spirv_std::glam;or:
use spirv_std::glam::{Vec2, Vec3, Vec4, UVec3};This avoids version drift and keeps shader-side math consistent.
One of Rust GPU’s biggest advantages is sharing Rust code between host-side logic and shader logic.
- Keep pure math, color transforms, helper functions, and common constants in a shared crate.
- Use
#[cfg(target_arch = "spirv")]only for the parts that truly need shader-only APIs. - Prefer
spirv_std::glamtypes for vectors/matrices in shared code. - Keep shared types ABI-stable with
#[repr(C)]. - Derive
bytemuck::Podandbytemuck::Zeroablefor host-uploaded structs when appropriate.
#![cfg_attr(target_arch = "spirv", no_std)]
use bytemuck::{Pod, Zeroable};
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct ShaderConstants {
pub width: u32,
pub height: u32,
pub time: f32,
pub exposure: f32,
}For host/GPU shared structs:
- use
#[repr(C)] - use only fields whose layout you understand precisely
- do not assume Rust’s default layout is stable
- be careful with booleans and exotic enums in externally visible ABI
- prefer plain scalars, vectors, arrays, and explicit structs
Rust GPU uses #[spirv(...)] attributes for SPIR-V-specific behavior.
There are four big buckets you use most:
- execution model / entry point
- builtins
- resource binding / storage class
- interpolation / layout / specialization
Common ones:
#[spirv(vertex)]
#[spirv(fragment)]
#[spirv(compute(threads(64)))]The current codebase supports these execution model names (snake_case of SPIR-V names):
vertextessellation_controltessellation_evaluationgeometryfragmentcomputetask_nvmesh_nvtask_extmesh_extray_generationintersectionany_hitclosest_hitmisscallable
Use the SPIR-V execution model name in snake_case.
Compute entry points must specify their local workgroup dimensions:
#[spirv(compute(threads(64)))]
pub fn main_cs(
#[spirv(global_invocation_id)] id: glam::UVec3,
#[spirv(storage_buffer, descriptor_set = 0, binding = 0)] data: &mut [u32],
) {
let i = id.x as usize;
data[i] *= 2;
}threads(x)means(x, 1, 1)threads(x, y)means(x, y, 1)- you can specify 1 to 3 dimensions
- the x dimension is required
By default, the Rust function name becomes the SPIR-V entry-point name.
Override it like this:
#[spirv(vertex(entry_point_name = "vs_main"))]
pub fn vertex_main() { /* ... */ }Use this when your engine, host API, or cross-language pipeline expects a specific entry-point name.
Builtins are special inputs/outputs with fixed GPU meaning.
Examples:
#[spirv(vertex)]
pub fn vs(
#[spirv(vertex_index)] vertex_index: i32,
#[spirv(position, invariant)] out_pos: &mut glam::Vec4,
) {
// ...
}#[spirv(fragment)]
pub fn fs(
#[spirv(frag_coord)] frag_coord: &glam::Vec4,
out_color: &mut glam::Vec4,
) {
// ...
}#[spirv(compute(threads(64)))]
pub fn cs(
#[spirv(global_invocation_id)] gid: glam::UVec3,
#[spirv(local_invocation_id)] lid: glam::UVec3,
#[spirv(workgroup_id)] wid: glam::UVec3,
) {
// ...
}Graphics
positionvertex_idvertex_indexinstance_idinstance_indexfrag_coordfrag_depthfront_facingprimitive_idlayerviewport_indexsample_idsample_mask
Compute
num_workgroupsworkgroup_idlocal_invocation_idglobal_invocation_idlocal_invocation_index
Subgroup
subgroup_sizenum_subgroupssubgroup_idsubgroup_local_invocation_id
Ray tracing
launch_idlaunch_sizeworld_ray_originworld_ray_directionobject_ray_originobject_ray_directionray_tminray_tmaxobject_to_worldworld_to_objecthit_kind
For builtins, use the SPIR-V BuiltIn name in snake_case.
Resources visible to the host API need explicit binding metadata.
#[spirv(fragment)]
pub fn main(
#[spirv(uniform, descriptor_set = 0, binding = 0)] camera: &Camera,
#[spirv(storage_buffer, descriptor_set = 0, binding = 1)] vertices: &[Vertex],
) {
// ...
}Older book text says descriptor_set = 0 is reserved.
That is stale for the current community version.
Current repo examples, macros, and compile tests actively use:
descriptor_set = 0So in the current mainline, set 0 is usable and is the normal default in examples.
These are the important entry-point parameter storage-class-style attributes:
uniform_constantuniformstorage_bufferpush_constantworkgroupimagephysical_storage_buffer
Ray tracing / callable / task-payload related:
callable_dataincoming_callable_dataray_payloadincoming_ray_payloadhit_attributeshader_record_buffertask_payload_workgroup_ext
Uniform / constant data
#[spirv(uniform, descriptor_set = 0, binding = 0)] camera: &CameraRead-only storage buffer
#[spirv(storage_buffer, descriptor_set = 0, binding = 1)] input: &[u32]Writable storage buffer
#[spirv(storage_buffer, descriptor_set = 0, binding = 2)] output: &mut [u32]Push constants
#[spirv(push_constant)] constants: &ShaderConstantsWorkgroup shared memory
#[spirv(workgroup)] scratch: &mut [u32; 256]flat means “do not interpolate”.
#[spirv(fragment)]
pub fn main(
#[spirv(flat)] material_id: u32,
out_color: &mut glam::Vec4,
) {
// ...
}Use #[spirv(flat)] for fragment inputs that are discrete / non-interpolated, especially integers and IDs.
Current compile tests show that integer fragment inputs are expected to be flat.
If an input should not be interpolated, say so explicitly.
Use invariant on outputs when you need invariant behavior analogous to GLSL invariant.
#[spirv(vertex)]
pub fn main(
#[spirv(position, invariant)] out_pos: &mut glam::Vec4,
) {
// ...
}Use it only where you actually need invariance guarantees.
This marks shared memory visible to invocations in the same workgroup.
#[spirv(compute(threads(256)))]
pub fn reduce(
#[spirv(local_invocation_id)] lid: glam::UVec3,
#[spirv(workgroup)] scratch: &mut [u32; 256],
) {
let local = lid.x as usize;
scratch[local] = local as u32;
}- pair shared-memory use with the right barriers/synchronization
- keep workgroup arrays fixed-size and simple
- do not write before the data is logically initialized
- assume race bugs until proven otherwise
Use specialization constants for values you want to override at pipeline creation time.
#[spirv(fragment)]
pub fn main(
#[spirv(spec_constant(id = 1, default = 64))] block_size: u32,
out: &mut glam::Vec4,
) {
let x = block_size as f32;
*out = glam::vec4(x, 0.0, 0.0, 1.0);
}- direct support is for
u32 - larger values can be assembled from multiple
u32s - these are runtime specialization values, not Rust
constgenerics
Use specialization constants for:
- pipeline tuning
- algorithm variants
- fixed-at-pipeline-creation thresholds
Do not mentally model them as compile-time Rust type parameters.
Good shader-visible types:
u32,i32,f32- fixed arrays
glamvectors / matrices that Rust GPU supports#[repr(C)]structs made of the above
Avoid vague / hosty / layout-ambiguous public interfaces.
Use the re-exported glam from spirv_std where practical:
use spirv_std::glam::{Vec2, Vec3, Vec4, UVec3, Mat4};Why:
- less version drift
- shader-side compatibility is already curated around it
Storage buffers frequently use slices:
#[spirv(storage_buffer, descriptor_set = 0, binding = 0)] input: &[u32]
#[spirv(storage_buffer, descriptor_set = 0, binding = 1)] output: &mut [u32]This is one of the nicest ergonomic wins in Rust GPU.
- use slices when the host-side buffer length is dynamic
- use fixed arrays when the size is truly fixed and ABI-known
- keep element types POD-like and layout-simple
TypedBuffer<T> is an explicit typed buffer handle for descriptor indexing use cases.
Patterns:
&[u32]— implicit one buffer&TypedBuffer<[u32]>— explicit one buffer&RuntimeArray<TypedBuffer<[u32]>>— many indexed buffers
Use this when you need descriptor-indexing style access instead of a single directly-bound slice.
RuntimeArray<T> models SPIR-V runtime arrays, but it does not know its length at the Rust type level.
Treat it as an advanced low-level feature:
- host-side bounds discipline matters
- indexing APIs are unsafe for a reason
- keep layouts and indexing contracts very clear
ByteAddressableBuffer gives you untyped word-addressed loads/stores.
Use it when you really need HLSL-style arbitrary byte/word access.
- everything must be aligned to 4-byte multiples
byte_indexmust be a multiple of 4- loads/stores are effectively transmute-like and can bypass normal safety expectations
Prefer typed buffers and ordinary structured data first.
Use ByteAddressableBuffer only for:
- packed formats
- interoperability with external layouts
- explicit low-level buffer protocols
Rust GPU supports a large set of SPIR-V image types.
You can write the verbose generic image type directly, but you almost always want one of these:
- the
Image!macro - the common type aliases in
spirv_std::image
Examples:
Image2dImage2dUImage2dIStorageImage2dStorageImage2dUStorageImage2dICubemap
Typical sampled 2D texture:
use spirv_std::image::Image2d;
use spirv_std::Sampler;The macro form is the most expressive:
spirv_std::Image!(2D, type=f32, sampled)
spirv_std::Image!(2D, type=u32, sampled)
spirv_std::Image!(2D, format=rgba32f, sampled=false)
spirv_std::Image!(cube, type=f32, sampled)General shape:
Image!(
<dimensionality>,
<type=... | format=...>,
[sampled],
[multisampled],
[arrayed],
[depth],
)Supported dimensionalities include:
1D2D3Drectcubesubpass
Use the type aliases for common cases.
Use Image! when you need a more exact image form.
Sampled image:
#[spirv(descriptor_set = 0, binding = 0)] image: &spirv_std::image::Image2d,
#[spirv(descriptor_set = 0, binding = 1)] sampler: &spirv_std::Sampler,Storage image:
#[spirv(descriptor_set = 0, binding = 0)] image: &spirv_std::image::StorageImage2d,- use sampled images for texture sampling
- use storage images for image load/store style access
The image API includes methods such as:
fetchfetch_with_lodgathersamplesample_biassample_by_lodsample_by_gradient- depth-reference sampling variants
- projected-coordinate sampling variants
- lower-level
*_with(...)forms
Start with the highest-level method that matches your use case.
Drop to the more explicit forms only when you actually need the extra operands.
The current compiler enforces that SubpassData image types use #[spirv(input_attachment_index = ...)].
If you work with input attachments:
- use the correct subpass image type
- provide the
input_attachment_index - keep the host-side render pass / subpass contract aligned
This is the low-level instruction-adjacent API surface.
Use it for:
- barriers
- subgroup operations
- derivatives
- atomics
- helper invocation / demote
- clocks
- mesh shading intrinsics
- ray tracing instruction wrappers
- miscellaneous SPIR-V ops not covered by higher-level helpers
Use spirv_std::arch when:
- there is no higher-level safe wrapper,
- or you intentionally need instruction-level control.
Do not start there for everything.
This exposes memory-model types like:
-
ScopeCrossDeviceDeviceWorkgroupSubgroupInvocationQueueFamily
-
SemanticsACQUIRERELEASEACQUIRE_RELEASEUNIFORM_MEMORYWORKGROUP_MEMORYIMAGE_MEMORYMAKE_AVAILABLEMAKE_VISIBLEVOLATILE- and others
Use these when you need explicit memory ordering semantics in advanced synchronization code.
This is the shader-side ray tracing API surface.
Key types/features include:
AccelerationStructureRayFlagsRayQueryray_query!trace_ray- query/commit/confirm helpers
- callable / payload-related storage-class interactions
Rust GPU re-exports debug_printf helpers for runtime debugging, and the builder also supports panic strategies that use debug printf.
Use these only when debugging; leave them out of production shader paths.
The current codebase includes subgroup and barrier support in spirv_std::arch.
Examples of useful primitives:
- workgroup memory barriers
- barriers with group synchronization
- subgroup operations
- atomics
A very common pattern is:
- write workgroup-local shared memory
- synchronize
- read/reduce/reuse
Example style:
unsafe {
spirv_std::arch::workgroup_memory_barrier_with_group_sync();
}- keep barrier placement obvious
- prefer one clearly-correct synchronization strategy over a “clever” one
- isolate subgroup/workgroup algorithms into small tested helpers
- assume host-visible corruption can come from a race before blaming layout
The current community version has mesh/task shader support in the source tree and compile tests.
Execution models:
task_extmesh_ext- also
task_nv/mesh_nv
use spirv_std::arch::set_mesh_outputs_ext;
use spirv_std::glam::{UVec3, Vec4};
use spirv_std::spirv;
#[spirv(mesh_ext(
threads(1),
output_vertices = 3,
output_primitives_ext = 1,
output_triangles_ext
))]
pub fn main(
#[spirv(position)] positions: &mut [Vec4; 3],
#[spirv(primitive_triangle_indices_ext)] indices: &mut [UVec3; 1],
) {
unsafe { set_mesh_outputs_ext(3, 1); }
positions[0] = Vec4::new(-0.5, 0.5, 0.0, 1.0);
positions[1] = Vec4::new( 0.5, 0.5, 0.0, 1.0);
positions[2] = Vec4::new( 0.0, -0.5, 0.0, 1.0);
indices[0] = UVec3::new(0, 1, 2);
}use spirv_std::arch::emit_mesh_tasks_ext;
use spirv_std::spirv;
#[spirv(task_ext(threads(1)))]
pub fn main() {
unsafe { emit_mesh_tasks_ext(1, 1, 1); }
}- enable the required capability / extension for the target
- keep thread counts explicit
- obey the rules around uniform control flow
- for mesh shaders, call
set_mesh_outputs_extcorrectly - for task shaders,
emit_mesh_tasks_ext*must be used correctly and at the right control-flow point
The current codebase includes ray-tracing execution models, payload storage classes, acceleration structures, and ray queries.
use spirv_std::spirv;
use spirv_std::ray_tracing::{AccelerationStructure, RayFlags};
#[spirv(ray_generation)]
pub fn main(
#[spirv(descriptor_set = 0, binding = 0)] accel: &AccelerationStructure,
#[spirv(ray_payload)] payload: &mut glam::Vec3,
) {
unsafe {
accel.trace_ray(
RayFlags::NONE,
0, 0, 0, 0,
glam::vec3(0.0, 0.0, 0.0),
0.001,
glam::vec3(0.0, 0.0, -1.0),
10_000.0,
payload,
);
}
}ray_generationintersectionany_hitclosest_hitmisscallable
ray_payloadincoming_ray_payloadcallable_dataincoming_callable_datahit_attributeshader_record_buffer
- gate ray-tracing code behind the right target features/capabilities
- keep payload structs simple and explicit
- isolate ray-query logic in helpers
- match your host pipeline / SBT layout exactly
Rust GPU supports inline SPIR-V assembly via nightly asm!.
Use it only when:
- no higher-level API exists,
- you need a precise instruction sequence,
- or you are prototyping backend gaps.
- literal non-ID instructions are written directly
- SPIR-V IDs use
%name - result IDs use
%result = ... - it accepts integers, floats, SIMD vectors, pointers, and function pointers as inputs
Use inline asm as a last resort.
When a spirv_std::arch helper exists, prefer that.
SpirvBuilder exposes several useful controls.
.spirv_metadata(SpirvMetadata::None)
.spirv_metadata(SpirvMetadata::NameVariables)
.spirv_metadata(SpirvMetadata::Full)Recommendation
Nonefor smallest binariesNameVariablesfor most real projectsFullonly when debugging hard problems
.deny_warnings(true)Recommended for disciplined shader crates.
.multimodule(true)Only when downstream tooling truly needs one module per entry point.
.capability(...)
.extension("SPV_EXT_mesh_shader")The builder docs note that you can check these in code with:
#[cfg(target_feature = "RayTracingKHR")]
#[cfg(target_feature = "ext:SPV_EXT_mesh_shader")].shader_panic_strategy(...)See the next section.
relax_struct_storerelax_logical_pointerrelax_block_layoutuniform_buffer_standard_layoutscalar_block_layoutskip_block_layout
Only change these when you understand the API/layout consequences.
.preserve_bindings(true)Useful when reflection or downstream tools need bindings preserved even if unused.
.extra_arg(...)Use sparingly. Many codegen args are internal/unstable.
shader_crate_default_features(bool)shader_crate_features(...)
Useful for keeping shader dependency surfaces minimal and intentional.
.check().clippy().build()
These are useful for CI/linting workflows around shader crates.
Current builder panic strategies include:
SilentExit(default)DebugPrintfThenExit { ... }
Default, low-noise, and the right production default.
Great for debugging, but requires runtime support:
- Vulkan validation/debug-printf setup
- or
wgpuSPIR-V passthrough setup
For wgpu, the builder docs note you need:
wgpu::Features::SPIRV_SHADER_PASSTHROUGHcreate_shader_module_passthrough
- use
SilentExitin normal builds - temporarily switch to debug-printf-based panic handling when chasing shader crashes
Useful environment variables:
RUSTGPU_LOGRUSTGPU_LOG_FORMATRUSTGPU_LOG_COLORRUSTC_LOG(for rustc-side internals)
Use these when debugging build/backend behavior.
Rust GPU exposes:
RUSTGPU_CODEGEN_ARGS
This is useful for experimentation and diagnostics, but many codegen arguments are internal and unstable. Prefer the structured builder API where possible.
The current docs mention these workflows:
cargo testcargo compiletestcargo difftest
To update expected outputs for compile tests:
cargo compiletest --bless
Use three layers:
1. CPU/shared-code tests
- test shared math and pure functions on CPU
- this is fast and catches a lot
2. Shader compile tests
- build shaders in CI
- fail on warnings
- lock toolchain and target
3. Runtime tests / sample runners
- render a known image or run a compute shader and validate output
Push as much logic as possible into CPU-testable shared code, and keep stage/resource glue thin.
Rust GPU’s official platform-support page is older and high-level, but it still documents the target families available in the project.
spirv-unknown-spv1.0spirv-unknown-spv1.1spirv-unknown-spv1.2spirv-unknown-spv1.3spirv-unknown-spv1.4spirv-unknown-spv1.5spirv-unknown-spv1.6
spirv-unknown-vulkan1.0spirv-unknown-vulkan1.1spirv-unknown-vulkan1.1spv1.4spirv-unknown-vulkan1.2spirv-unknown-vulkan1.3spirv-unknown-vulkan1.4
spirv-unknown-webgpu0
spirv-unknown-opengl4.0spirv-unknown-opengl4.1spirv-unknown-opengl4.2spirv-unknown-opengl4.3spirv-unknown-opengl4.5
spirv-unknown-opencl1.2spirv-unknown-opencl1.2embeddedspirv-unknown-opencl2.0spirv-unknown-opencl2.0embeddedspirv-unknown-opencl2.1spirv-unknown-opencl2.1embeddedspirv-unknown-opencl2.2spirv-unknown-opencl2.2embedded
For most current real-world use:
- prefer Vulkan targets directly,
- or use Rust GPU with a
wgpuhost where that fits your portability goals.
The official ecosystem and blog pages make it clear that Rust GPU sits inside a broader Rust-on-GPU ecosystem.
For a host-side graphics/compute stack today, the most relevant pairings are:
wgpu— broad portable host API, especially good when portability mattersash— lower-level Vulkan host API- Naga — relevant in the broader shader translation ecosystem
- CubeCL, krnl, cudarc — adjacent Rust GPU compute ecosystem projects, not Rust GPU itself
Choose your host API and shader compiler path separately in your head:
- Rust GPU = how your Rust shader code becomes SPIR-V
wgpu/ash= how your host app creates pipelines and binds resources
- Pin the exact toolchain expected by Rust GPU.
- Keep all Rust-GPU crate versions/revisions matched exactly.
- Use
spirv-builderinbuild.rsfor normal app integration. - Turn on optimized build-dependencies.
- Put CPU/GPU shared math and ABI structs in a shared crate.
- Use
#![cfg_attr(target_arch = "spirv", no_std)]. - Prefer
spirv_std::glam. - Use
#[repr(C)]+bytemuck::{Pod, Zeroable}for host-visible data structs. - Prefer simple buffer/image interfaces and explicit bindings.
- Use
SpirvMetadata::NameVariablesas a good default. - Use
preserve_bindings(true)if reflection/tooling needs it. - Use
flatfor discrete/non-interpolated fragment inputs. - Keep synchronization obvious and conservative.
- Use
spirv_std::archonly when higher-level APIs are insufficient. - Test shared logic on CPU and keep stage glue thin.
- Do not mix arbitrary Rust GPU versions.
- Do not upgrade the nightly casually.
- Do not trust older docs over current source/examples/tests when they conflict.
- Do not start with
multimoduleunless a tool forces you there. - Do not lean on unstable
extra_arg/codegen hacks first. - Do not expose layout-ambiguous Rust types across the CPU/GPU ABI.
- Do not reach for
ByteAddressableBufferunless you actually need it. - Do not assume “shader constants” behave like Rust
constgenerics. - Do not forget that debug-printf-based panic reporting needs runtime support.
Likely causes:
- nightly mismatch
- updated Rust GPU revision
- mixed crate versions
- stale incremental build outputs
Usually:
- build dependencies are being compiled in debug mode
rustc_codegen_spirvis being rebuilt too often- target dirs are colliding between builders
Check:
- descriptor set / binding annotations
- preserved vs optimized-away bindings
- host-side layout agreement
- reflection expectations
Because the current docs contain a stale statement. Current source/examples/tests use set 0 normally.
Add #[spirv(flat)].
You likely need SPIR-V passthrough plus the right validation/debug-printf configuration.
Use normal Rust style, yes. Use normal host APIs / std / OS services inside shader code, no.
Write shaders as no_std Rust with spirv-std and explicit GPU interfaces.
| Purpose | Form |
|---|---|
| Vertex shader | #[spirv(vertex)] |
| Fragment shader | #[spirv(fragment)] |
| Compute shader | #[spirv(compute(threads(x[, y[, z]])))] |
| Task shader | #[spirv(task_ext(threads(...)))] |
| Mesh shader | #[spirv(mesh_ext(...))] |
| Ray generation | #[spirv(ray_generation)] |
| Closest hit | #[spirv(closest_hit)] |
| Miss | #[spirv(miss)] |
| Attribute | Meaning |
|---|---|
position |
clip-space position builtin |
frag_coord |
fragment coordinates builtin |
vertex_index / vertex_id |
vertex builtin input |
global_invocation_id |
compute global ID |
local_invocation_id |
compute local ID |
workgroup_id |
compute workgroup ID |
descriptor_set = N |
descriptor set |
binding = M |
binding number |
push_constant |
push constant block |
storage_buffer |
storage buffer |
uniform |
uniform buffer / uniform data |
workgroup |
shared local memory |
flat |
no interpolation |
invariant |
invariant output |
spec_constant(id=..., default=...) |
specialization constant |
input_attachment_index = N |
subpass/input attachment index |
| Setting | Recommendation |
|---|---|
| Toolchain | exact Rust-GPU-pinned nightly |
| Integration | spirv-builder in build.rs |
build_script.defaults |
true |
env_shader_spv_path |
true for single-module builds |
spirv_metadata |
NameVariables |
deny_warnings |
true |
preserve_bindings |
true if reflection matters |
multimodule |
false unless required |
| Panic strategy | SilentExit normally |
This cheat sheet is intentionally based on both the official docs and the current community repo source tree.
That matters because the current mainline has some documented behavior that is fresher in source/examples/tests than in older book text. The clearest example is the outdated note about descriptor_set = 0.
- current Rust-GPU repo source / examples / tests
- official book + official site
- official blog + ecosystem pages
Official project pages:
- https://rust-gpu.github.io/
- https://rust-gpu.github.io/rust-gpu/book/
- https://rust-gpu.github.io/rust-gpu/book/writing-shader-crates.html
- https://rust-gpu.github.io/rust-gpu/book/building-rust-gpu.html
- https://rust-gpu.github.io/blog/
- https://rust-gpu.github.io/ecosystem/
Current repo / current-source references:
- https://github.com/Rust-GPU/rust-gpu
- https://github.com/Rust-GPU/rust-gpu/blob/main/README.md
- https://github.com/Rust-GPU/rust-gpu/blob/main/Cargo.toml
- https://github.com/Rust-GPU/rust-gpu/blob/main/rust-toolchain.toml
- https://github.com/Rust-GPU/rust-gpu/blob/main/docs/src/attributes.md
- https://github.com/Rust-GPU/rust-gpu/blob/main/docs/src/platform-support.md
- https://github.com/Rust-GPU/rust-gpu/blob/main/docs/src/testing.md
- https://github.com/Rust-GPU/rust-gpu/blob/main/docs/src/tracing.md
- https://github.com/Rust-GPU/rust-gpu/blob/main/docs/src/codegen-args.md
- https://github.com/Rust-GPU/rust-gpu/tree/main/examples
- https://github.com/Rust-GPU/rust-gpu/tree/main/crates/spirv-builder
- https://github.com/Rust-GPU/rust-gpu/tree/main/crates/spirv-std