Crates.io | wgsl_bindgen |
lib.rs | wgsl_bindgen |
version | 0.21.1 |
created_at | 2024-02-03 17:27:18.844972+00 |
updated_at | 2025-08-10 10:19:24.155421+00 |
description | Type safe Rust bindings workflow for wgsl shaders in wgpu |
homepage | |
repository | https://github.com/Swoorup/wgsl_bindgen |
max_upload_size | |
id | 1125551 |
size | 1,317,986 |
🚀 Generate typesafe Rust bindings from WGSL shaders for wgpu
wgsl_bindgen transforms your WGSL shader development workflow by automatically generating Rust types, constants, and boilerplate code that perfectly match your shaders. Powered by naga-oil, it integrates seamlessly into your build process to catch shader-related errors at compile time rather than runtime.
Before: Manual, error-prone shader bindings
// ❌ Easy to make mistakes - no compile-time verification
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
entries: &[
wgpu::BindGroupEntry {
binding: 0, // Is this the right binding index?
resource: texture_view.as_binding(), // Is this the right type?
},
wgpu::BindGroupEntry {
binding: 1, // What if you change the shader?
resource: sampler.as_binding(),
},
],
// ... more boilerplate
});
After: Typesafe, auto-generated bindings
// ✅ Compile-time safety - generated from your actual shaders
let bind_group = my_shader::WgpuBindGroup0::from_bindings(
device,
my_shader::WgpuBindGroup0Entries::new(my_shader::WgpuBindGroup0EntriesParams {
my_texture: &texture_view, // Type-checked parameter names
my_sampler: &sampler, // Matches your WGSL exactly
})
);
bind_group.set(&mut render_pass); // Simple, safe usage
Supports import syntax and many more features from naga oil flavour.
Add shader defines dynamically when using either WgslShaderSourceType::EmbedWithNagaOilComposer
or WgslShaderSourceType::ComposerWithRelativePath
source output type.
The WgslShaderSourceType::ComposerWithRelativePath
provides full control over file I/O without requiring nightly Rust, making it ideal for integration with custom asset systems and hot reloading.
File Visitor Pattern: The visit_shader_files
function allows custom processing of all shader files in a dependency tree. This enables advanced use cases like:
// Example: Hot reloading with file watching
use shader_bindings::visit_shader_files;
visit_shader_files(
"shaders",
ShaderEntry::MyShader,
|path| std::fs::read_to_string(path),
|file_path, file_content| {
println!("Processing shader: {}", file_path);
// Add to file watcher, cache, etc.
}
)?;
Shader registry utility to dynamically call create_shader
variants depending on the variant. This is useful when trying to keep cache of entry to shader modules. Also remember to add shader defines to accomodate for different permutation of the shader modules.
Ability to add additional scan directories for shader imports when defining the workflow.
Cargo.toml
[build-dependencies]
wgsl_bindgen = "0.19"
[dependencies]
wgpu = "25"
bytemuck = { version = "1.0", features = ["derive"] }
# Optional: for additional features
# encase = "0.8"
# serde = { version = "1.0", features = ["derive"] }
# Note: When using ComposerWithRelativePath, enable naga-ir feature for optimal performance:
# wgpu = { version = "25", features = ["naga-ir"] }
shaders/my_shader.wgsl
)struct Uniforms {
transform: mat4x4<f32>,
time: f32,
}
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>,
}
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var my_texture: texture_2d<f32>;
@group(0) @binding(2) var my_sampler: sampler;
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
output.clip_position = uniforms.transform * vec4<f32>(input.position, 1.0);
output.uv = input.uv;
return output;
}
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(my_texture, my_sampler, input.uv);
}
build.rs
)use wgsl_bindgen::{WgslBindgenOptionBuilder, WgslTypeSerializeStrategy, GlamWgslTypeMap};
fn main() -> Result<(), Box<dyn std::error::Error>> {
WgslBindgenOptionBuilder::default()
.workspace_root("shaders")
.add_entry_point("shaders/my_shader.wgsl")
.serialization_strategy(WgslTypeSerializeStrategy::Bytemuck)
.type_map(GlamWgslTypeMap) // Use glam for math types
.output("src/shader_bindings.rs")
.build()?
.generate()?;
Ok(())
}
// Include the generated bindings
mod shader_bindings;
use shader_bindings::my_shader;
fn setup_render_pipeline(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> wgpu::RenderPipeline {
// Create shader module from generated code
let shader = my_shader::create_shader_module_embed_source(device);
// Use generated pipeline layout
let pipeline_layout = my_shader::create_pipeline_layout(device);
// Use generated vertex entry with proper buffer layout
let vertex_entry = my_shader::vs_main_entry(wgpu::VertexStepMode::Vertex);
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
layout: Some(&pipeline_layout),
vertex: my_shader::vertex_state(&shader, &vertex_entry),
fragment: Some(my_shader::fragment_state(&shader, &my_shader::fs_main_entry([
Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})
]))),
// ... other pipeline state
})
}
fn setup_bind_group(device: &wgpu::Device, texture_view: &wgpu::TextureView, sampler: &wgpu::Sampler) -> my_shader::WgpuBindGroup0 {
// Create uniform buffer with generated struct
let uniforms = my_shader::Uniforms::new(
glam::Mat4::IDENTITY, // transform
0.0, // time
);
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
contents: bytemuck::cast_slice(&[uniforms]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
// Create bind group using generated types - fully type-safe!
my_shader::WgpuBindGroup0::from_bindings(
device,
my_shader::WgpuBindGroup0Entries::new(my_shader::WgpuBindGroup0EntriesParams {
uniforms: wgpu::BufferBinding {
buffer: &uniform_buffer,
offset: 0,
size: None,
},
my_texture: texture_view,
my_sampler: sampler,
})
)
}
🎉 That's it! Your shader bindings are now fully type-safe and will automatically update when you modify your WGSL files.
📚 See the example project for a complete working demo with multiple shaders, including advanced features like texture arrays and overlay rendering.
Choose how your WGSL types are serialized to Rust:
// For zero-copy, compile-time verified layouts (recommended)
.serialization_strategy(WgslTypeSerializeStrategy::Bytemuck)
// For runtime padding/alignment handling
.serialization_strategy(WgslTypeSerializeStrategy::Encase)
Use your preferred math library:
// glam (recommended for games)
.type_map(GlamWgslTypeMap)
// nalgebra (recommended for scientific computing)
.type_map(NalgebraWgslTypeMap)
// Use built-in Rust arrays (no external dependencies)
.type_map(RustWgslTypeMap)
Override specific types or structs:
.override_struct_field_type([
("MyStruct", "my_field", quote!(MyCustomType))
])
.add_override_struct_mapping(("MyWgslStruct", quote!(my_crate::MyRustStruct)))
Control how shaders are embedded:
// Embed shader source directly (recommended for most cases)
.shader_source_type(WgslShaderSourceType::EmbedSource)
// Use file paths for hot-reloading during development
.shader_source_type(WgslShaderSourceType::HardCodedFilePath)
// Use naga-oil composer for advanced import features
.shader_source_type(WgslShaderSourceType::EmbedWithNagaOilComposer)
// Use relative paths with custom file loading (no nightly Rust required)
// Requires wgpu "naga-ir" feature for optimal performance
.shader_source_type(WgslShaderSourceType::ComposerWithRelativePath)
The ComposerWithRelativePath
option allows you to provide your own file loading logic, which is perfect for integrating with custom asset systems.
Performance Note: This mode uses wgpu's naga-ir
feature to pass Naga IR modules directly to the GPU instead of converting back to WGSL source. This provides better performance by avoiding the round-trip conversion process. Make sure to enable the feature in your dependencies:
[dependencies]
wgpu = { version = "25", features = ["naga-ir"] }
// In your build.rs
.shader_source_type(WgslShaderSourceType::ComposerWithRelativePath)
// In your application code
let module = main::load_naga_module_from_path(
"assets/shaders", // Base directory
ShaderEntry::Main, // Entry point enum variant
&mut composer,
shader_defs,
|path| std::fs::read_to_string(path), // Your custom file loader
)?;
// Or use your own asset system
let module = main::load_naga_module_from_path(
"shaders",
ShaderEntry::Main,
&mut composer,
shader_defs,
|path| asset_manager.load_text_file(path), // Custom asset manager
)?;
wgsl_bindgen uses a specific strategy to resolve the import paths in your WGSL source code. This process is handled by the ModulePathResolver::generate_possible_paths function.
Consider the following directory structure:
/my_project
├── src
│ ├── shaders
│ │ ├── main.wgsl
│ │ ├── utils
│ │ │ ├── math.wgsl
│ ├── main.rs
├── Cargo.toml
And the following import statement in main.wgsl:
import utils::math;
Here's how wgsl_bindgen resolves the import path:
utils::math
) starts with the module prefix. If a module prefix is set and matches, it removes the prefix and treats the rest of the import module name as a relative path from the entry source directory converting the double semicolor ::
to forward slash /
from the directory of the current source file (src/shaders
).utils/math.wgsl
in the same directory as main.wgsl
.src/shaders/utils/math.wgsl
.src/shaders/utils.wgsl
treating math
as an item within utils.wgsl
had it existed.This strategy allows wgsl_bindgen
to handle a variety of import statement formats and directory structures, providing flexibility in how you organize your WGSL source files.
WGSL structs have different memory layout requirements than Rust structs or standard layout algorithms like repr(C)
or repr(packed)
. Matching the expected layout to share data between the CPU and GPU can be tedious and error prone. wgsl_bindgen offers options to add derives for encase to handle padding and alignment at runtime or bytemuck for enforcing padding and alignment at compile time.
When deriving bytemuck, wgsl_bindgen will use naga's layout calculations to add const assertions to ensure that all fields of host-shareable types (structs for uniform and storage buffers) have the correct offset, size, and alignment expected by WGSL.
wgpu uses resource bindings organized into bind groups to define global shader resources like textures and buffers. Shaders can have many resource bindings organized into up to 4 bind groups. wgsl_bindgen will generate types and functions for initializing and setting these bind groups in a more typesafe way. Adding, removing, or changing bind groups in the WGSl shader will typically result in a compile error instead of a runtime error when compiling the code without updating the code for creating or using these bind groups.
While bind groups can easily be set all at once using the set_bind_groups
function, it's recommended to organize bindings into bindgroups based on their update frequency. Bind group 0 will change the least frequently like per frame resources with bind group 3 changing most frequently like per draw resources. Bind groups can be set individually using their set(render_pass)
method. This can provide a small performance improvement for scenes with many draw calls. See descriptor table frequency (DX12) and descriptor set frequency (Vulkan) for details.
Organizing bind groups in this way can also help to better organize rendering resources in application code instead of redundantly storing all resources with each object. The BindGroup0
may only need to be stored once while WgpuBindGroup3
may be stored for each mesh in the scene. Note that bind groups store references to their underlying resource bindings, so it is not necessary to recreate a bind group if the only the uniform or storage buffer contents change. Avoid creating new bind groups during rendering if possible for best performance.
Organize bind groups by update frequency:
// Bind group 0: Per-frame data (transforms, time)
// Bind group 1: Per-material data (textures, material properties)
// Bind group 2: Per-object data (model matrices, instance data)
Use RenderBundles for static geometry:
let render_bundle = device.create_render_bundle_encoder(&descriptor);
bind_group.set(&mut render_bundle);
render_bundle.draw(0..vertex_count, 0..1);
let bundle = render_bundle.finish(&descriptor);
Prefer bytemuck for zero-copy performance:
.serialization_strategy(WgslTypeSerializeStrategy::Bytemuck)
// Generated structs work seamlessly with wgpu
let vertices = vec![
my_shader::VertexInput::new(glam::Vec3::ZERO, glam::Vec2::ZERO),
my_shader::VertexInput::new(glam::Vec3::X, glam::Vec2::X),
my_shader::VertexInput::new(glam::Vec3::Y, glam::Vec2::Y),
];
// Update uniforms safely with type checking
let uniforms = my_shader::Uniforms::new(
camera.view_projection_matrix(),
time.elapsed_secs(),
);
queue.write_buffer(&uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES
)We welcome contributions! Please see our contribution guidelines for details on:
This project is licensed under the MIT License - see the LICENSE file for details.
quote
libraryThe provided example project outputs the generated bindings to the src/
directory for documentation purposes.
This approach is also fine for applications. Published crates should follow the recommendations for build scripts in the Cargo Book.
use miette::{IntoDiagnostic, Result};
use wgsl_bindgen::{WgslTypeSerializeStrategy, WgslBindgenOptionBuilder, GlamWgslTypeMap};
// src/build.rs
fn main() -> Result<()> {
WgslBindgenOptionBuilder::default()
.workspace_root("src/shader")
.add_entry_point("src/shader/testbed.wgsl")
.add_entry_point("src/shader/triangle.wgsl")
.serialization_strategy(WgslTypeSerializeStrategy::Bytemuck)
.type_map(GlamWgslTypeMap)
.derive_serde(false)
.output("src/shader.rs")
.build()?
.generate()
.into_diagnostic()
}
The generated code will need to be included in one of the normal source files. This includes adding any nested modules as needed.
// src/lib.rs
mod shader;