- The user must be able to write regular Rust code.
No translation of Rust code will occur in the
#[wgsl]macro. The macro is strictly additive in that it adds more Rust code to the user's written code. Specifically, it will add the transpiled WGSL source code and imports. - The Rust type system should catch as many type errors as possible, so WGSL validation doesn't have to.
Stated another way,
wgsl-rsshould never produce Rust code that compiles, but produces invalid WGSL. You can be sure that if your Rust compiles, your shader will too. - Macros are ok, as Rust folks are used to using macros.
Eg.
uniform!(binding(0), group(0), BRIGHTNESS: f32);is fine, and in fact we must use macros for all WGSL that can't be represented with Rust syntax. - WGSL builtins can be variadic and can be called with different types, and may return different types.
This presents two problems to solve:
- Parameter types: When the type of a parameter differs between WGSL
builtin "flavors", the support strategy in
wgsl-rsis to use a trait that allows the parameter types to be dependent on a type parameter. Either in the type implementing the trait, an associated type or to have the parameterimpl AnotherTrait. So far this has been flexible enough. - Variadic functions: When a WGSL builtin can be called with varying parameter counts,
the strategy in
wgsl-rsis to create multiple functions - one for each variation. Each Rust function is then mapped to the same WGSL builtin using theBUILTIN_NAME_CASE_MAP. For example, (Rust => WGSL):texture_sample=>textureSampletexture_sample_array=>textureSampletexture_sample_array_offset=>textureSample
- Parameter types: When the type of a parameter differs between WGSL
builtin "flavors", the support strategy in
With wgsl-rs you can import other modules. While this is a boon for development it has some significant
restrictions. Specifically you can only glob-import other WGSL modules written with wgsl-rs, or the
wgsl_rs::std::*. If you try to import arbitrary modules you'll get a compiler error about not having
path::to::module::WGSL_MODULE in scope.
Swizzles are tricky because in Rust they could be accomplished with traits like glam's
Vec4Swizzle trait (and friends), but in WGSL they look like field accessors.
Because of design decisions the Rust must be un-altered so we use functions similar to the
Vec4Swizzle strategy. So to swizzle xyz in WGSL you would just call .xyz() in Rust.
There's a lot to implement here. So far I've been pretty successful (3 functions done) using this AI prompt:
Please add the
{fn}function using the module-level documentation table as a guide, following the implementation ofabsandacos, which used theNumericBuiltinAbsandNumericBuiltinAcostraits, respectively.
You should replace {fn} with whatever function you want to implement.
Keep in mind that the VecN<T> types have glam types as their inner fields, so you can use that for many of these.
Keep in mind that glam vectors are not iterators, you can't call zip on them. Instead, you can call to_array on
each vector and write into one of them, finally calling .into() on the array to convert it back to the vector.
See the implementations of NumericBuiltinPow and NumericBuiltinStep for an example of this.
I've gone with a "one-trait-per-function" strategy because each function has little differences, and I anticipate having to use generic associated types for some functions.
Shaders may return user defined types (structs) that have their fields annotated with the #[builtin(...)]
and #[location(...)] macros, which corresponds to the description in
the spec:
// Mixed builtins and user-defined inputs.
struct MyInputs {
@location(0) x: vec4<f32>,
@builtin(front_facing) y: bool,
@location(1) @interpolate(flat) z: u32
}
struct MyOutputs {
@builtin(frag_depth) x: f32,
@location(0) y: vec4<f32>
}
@fragment
fn fragShader(in1: MyInputs) -> MyOutputs {
// ...
}And here would be the corresponding Rust:
// Mixed builtins and user-defined inputs.
pub struct MyInputs {
#[location(0)]
pub x: Vec4<f32>,
#[builtin(front_facing)]
pub y: bool,
#[location(1)]
#[interpolate(flat)]
pub z: u32
}
pub struct MyOutputs {
#[builtin(frag_depth)]
pub x: f32,
#[location(0)]
pub y: vec4<f32>
}
#[fragment]
pub fn fragShader(in1: MyInputs) -> MyOutputs {
// ...
}But in Rust we can't put annotations on return types, so we'll have to specify that in the shader
stage proc-attribute macros (vertex, fragment and compute).
But if the return type a vertex or fragment shader is Vec4f, and the macro didn't specify a
return type location or builtin, wgsl-rs will automatically insert an appropriate annotation.
I think most people who need to specify a return value other than the default @builtin(position) for
vertex shaders or @location(0) for fragment shaders will use a struct, so this is fine.
When the linkage-wgpu feature is enabled, the #[wgsl] macro generates additional wgpu-specific
code to simplify integration with wgpu applications:
Buffer descriptors and creation functions - For each uniform! and storage! declaration:
- A
{NAME}_BUFFER_DESCRIPTOR: wgpu::BufferDescriptor<'static>constant - A
create_{name}_buffer(device: &wgpu::Device) -> wgpu::Bufferfunction
Bind group modules - For each bind group, a linkage::bind_group_{N} module containing:
LAYOUT_ENTRIESandLAYOUT_DESCRIPTORconstantslayout(device)- creates the bind group layoutcreate(device, layout, ...)- type-safe bind group creation with named parameterscreate_dynamic(device, layout, entries)- dynamic bind group creation with a slice
Shader entry point modules - For vertex, fragment, and compute entry points:
- Entry point name constants
- Helper functions for creating pipeline states
This feature adds wgpu as a dependency to wgsl-rs.
Rust match statements transpile to WGSL switch statements:
-
match x { 0 => {...}, 1 => {...}, _ => {...} }→switch x { case 0 {...} case 1 {...} default {...} } -
Or-patterns
1 | 2 | 3 => {...}→case 1, 2, 3 {...} -
Missing
_arm auto-generatesdefault {} -
Non-literal patterns (constants, identifiers) emit a warning suppressed with
#[wgsl_allow(non_literal_match_statement_patterns)] -
Match expressions (in let bindings) are unsupported (WGSL switch is a statement)
-
Guard clauses, range patterns, struct/tuple patterns are unsupported
-
For future work regarding type checking, we may be able to get away with a trick. We could alter the Rust code to result in the pattern matched, then use that result in an empty function that takes an integer. This would cause Rust to do the type checking for us, before WGSL validation. That would keep us from having to emit a warning.
Example input Rust:
match my_expr { MyEnum::Variant1 => { do_stuff(); } }
Output Rust:
let __match_result = match my_expr { input @ MyEnum::Variant1 => { do_stuff(); input } }; __ensure_integer(__match_result);
Output WGSL:
switch my_expr { case MyEnum_Variant1: { do_stuff(); } default: {} }
Maybe we should also look into what we can do with for-loop bounds and the
non_literal_loop_boundswarning in this manner.
for i in 0..n transpiles to for (var i = 0; i < n; i++) and for i in 0..=n transpiles to for (var i = 0; i <= n; i++).
Nested loops work correctly.
Only bounded ranges are supported (WGSL requires explicit bounds).
For-loops with non-literal bounds (where the bounds cannot be verified at compile-time to be ascending)
emit a compile error on stable, since warnings can't be emitted (there's a hack to emit them as deprecations, but it's hacky).
On nightly it emits a warning.
Use #[wgsl_allow(non_literal_loop_bounds)] on the for-loop to suppress these errors/warnings.
Added ptr!(address_space, T) macro for WGSL pointer types in function parameters.
- Supports
functionandprivateaddress spaces (the only ones passable to functions without extensions) - Expands to
&mut Tin Rust for CPU execution - Transpiles to
ptr<function, T>orptr<private, T>in WGSL - Added dereference operator (
*) support for pointer indirection - Both
&xand&mut xtranspile to&xin WGSL (mutability is determined by access mode)
Example:
pub fn increment(p: ptr!(function, i32)) {
*p += 1;
}
fn main() {
let mut x: i32 = 5;
increment(&mut x); // x is now 6
}Added Atomic<T> type for thread-safe atomic operations (WGSL atomic<T> where T is i32 or u32).
Added workgroup! macro for declaring workgroup-scoped variables shared across compute shader invocations.
Atomic<T>on CPU is backed bystd::sync::atomic::{AtomicI32, AtomicU32}with full atomic operationsworkgroup!(NAME: TYPE)transpiles tovar<workgroup> NAME: TYPE;in WGSL- Workgroup variables on CPU are backed by
LazyLock<RwLock<T>>for thread-safe shared state - Added
AddressSpace::Workgroupvariant for future pointer support - Atomic builtin functions (atomicLoad, atomicStore, etc.) will be added in a future update
Added storageBarrier(), workgroupBarrier(), textureBarrier(), and workgroupUniformLoad() synchronization builtins for compute shader workgroup coordination.
- Barrier functions are no-ops on the CPU side (no parallel dispatch runtime yet)
workgroup_uniform_loaduses aWorkgroupUniformLoadtrait with impls forWorkgroup<T: Clone>andWorkgroup<Atomic<{u32,i32}>>- Extended
ptr!macro and parser to supportworkgroupaddress space - Added name mappings in
BUILTIN_CASE_NAME_MAPfor all four sync builtins
Added a standalone fbm-example crate that renders an animated fractal brownian motion
shader in a winit window using wgpu. Port of the classic FBM shader by Patricio
Gonzalez Vivo from GLSL.
Lessons learned during the port:
- Typed literal suffixes like
0.0_f32are emitted verbatim into WGSL, causing parse errors. Use plain0.0instead. The transpiler does not strip Rust literal suffixes. #[fragment]does not strip#[builtin(...)]from function parameters (unlike#[vertex]and#[compute]which do). Workaround: use an#[input]struct with#[builtin(position)]on the field instead of a direct parameter attribute. See #84.- Accessing uniforms in expressions:
get!(U_TIME)returns aModuleVarReadGuard<T>on the Rust side. To use the value in arithmetic, wrap with the identity type constructor:f32(get!(U_TIME))for scalars, orvec2f(get!(U_RESOLUTION).x(), get!(U_RESOLUTION).y())for vectors. These become no-op constructors in WGSL. This is a bit messy and should be improved long-term.
Added discard!() macro for the WGSL discard statement (fragment shaders only).
discard!()transpiles todiscard;in WGSL- On the CPU side, sets a thread-local flag checked by
dispatch_fragmentsto suppress the fragment output - Execution continues after
discard!()(matching WGSL semantics where helper invocations continue running for derivative computation), but the output is discarded - Can be called from any function reachable from a fragment entry point, not just the entry point itself
- Storage/workgroup writes are not yet suppressed on the CPU side for discarded invocations (future work)
Migrated PipelineLayoutDescriptor::bind_group_layouts to &[Option<&BindGroupLayout>], switched InstanceDescriptor::default() to new_with_display_handle / new_without_display_handle, and updated Surface::get_current_texture() callers to match the new CurrentSurfaceTexture enum. Examples now reconfigure the surface on Outdated and skip the frame on Timeout / Occluded / Lost / Validation instead of panicking.
Extended roundtrip-tests with 9 new sub-tests covering bit manipulation (clz,
popcount, ctz, reverse_bits, first_leading/trailing_bit, extract/insert_bits),
bitcast (scalar and vec4 roundtrips), and pack/unpack (4x8/2x16 snorm/unorm/float).
Discovered naga/Metal backend bug with firstLeadingBit(0xFFFFFFFF_u).
Added roundtrip-tests crate — a standalone binary that validates GPU vs CPU coherence
for core numeric builtins (trig, exponential, rounding, clamping, geometric). Found and
fixed fract (was using self - trunc(self) instead of WGSL's self - floor(self))
and round (was rounding half away from zero instead of WGSL's half to even).
Added RuntimeArray<T> type for runtime-sized arrays (WGSL array<T> without size parameter).
These are used in storage buffers, typically as the last field of a struct.
On CPU, RuntimeArray<T> is backed by Vec<T> with full indexing support.
Transpiles to array<T> in WGSL.