ScanMountGoat / wgsl_to_wgpu

Generate typesafe Rust bindings from WGSL shaders to wgpu
MIT License
42 stars 12 forks source link
wgpu wgsl

wgsl_to_wgpu

Latest Version docs.rs
A library for generating typesafe Rust bindings from WGSL shaders to wgpu.

wgsl_to_wgpu is designed to be incorporated into the compilation process using a build script. The WGSL shaders are parsed using naga to generate a corresponding Rust module. The generated Rust module contains the type definitions and boilerplate code needed to work with the WGSL shader module. Using the generated code can also reduce many instances of invalid API usage.

wgsl_to_wgpu facilitates a shader focused workflow where edits to WGSL code are automatically reflected in the corresponding Rust file. For example, changing the type of a uniform in WGSL will raise a compile error in Rust code using the generated struct to initialize the buffer.

Features

Usage

Add the following lines to the Cargo.toml and fill in the appropriate versions for wgsl_to_wgpu. When enabling derives for crates like bytemuck, serde, or encase, these dependencies should also be added to the Cargo.toml with the appropriate derive features. See the provided example project for basic usage.

[build-dependencies]
wgsl_to_wgpu = "..."

See the example crate for how to use the generated code. Run the example with cargo run.

Memory Layout

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_to_wgpu 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_to_wgpu 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. It's strongly recommended to use types like vec4 or mat4 instead of vec3 or mat3 with bytemuck to avoid alignment mismatches. Structs used only as vertex input structs have their layout manually specified using std::mem::offset_of and do not generate layout validation assertions.

Bind Groups

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_to_wgpu 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 bind_groups::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 bindgroups::BindGroup0 may only need to be stored once while bindgroups::BindGroup3 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.

Limitations

Publishing Crates

Rust expects build scripts to not modify files outside of OUT_DIR. The 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 wgsl_to_wgpu::{create_shader_module_embedded, WriteOptions};

// src/build.rs
fn main() {
    println!("cargo:rerun-if-changed=src/model.wgsl");

    // Generate the Rust bindings and write to a file.
    let text = create_shader_module_embedded(wgsl_source, WriteOptions::default()).unwrap();
    let out_dir = std::env::var("OUT_DIR").unwrap();
    std::fs::write(format!("{out_dir}/model.rs"), text.as_bytes()).unwrap();
}

The generated code will need to be included in one of the normal source files. This includes adding any nested modules as needed.

// src/shader.rs
pub mod model {
    include!(concat!(env!("OUT_DIR"), "/model.rs"));
}