DEV Community

Roman Salnikov
Roman Salnikov

Posted on

Notes on migrating from WGSL to rust-gpu shaders

Preface

You probably ended up here through googling one of the weird error messages mentioned below. I also tried googling them with limited success, so I decided to post this to save time to anyone in the same situation.

I develop a small game on top of the project structure from Learn WGPU guide. I found WGSL shaders tedious to write, though, since text editor support is non-existent, and it's hard to find examples and precise documentation. Once I learned about rust-gpu approach to writing shaders in plain Rust code, I instantly decided to try it out and to migrate my existing WGSL shaders. Below goes several obstacles I faced and learnings I took away.

rust-gpu is in active development, so most things here might change in future versions. For reference, here are the versions I'm using at the moment:

  • rust edition 2021
  • toolchain nightly-2022-01-13
  • spirv-std 0.4.0-alpha.12

Implicit locations

In WGSL, you explicitly specify locations for position, texture coordinates etc. This is how input parameters in my WGSL shader for displaying textured models looked like:

struct VertexInput { [[location(0)]] position: vec3<f32>; [[location(1)]] uv: vec2<f32>; [[location(2)]] normal: vec3<f32>; [[location(3)]] tangent: vec3<f32>; [[location(4)]] bitangent: vec3<f32>; }; struct VertexOutput { [[builtin(position)]] position: vec4<f32>; [[location(0)]] uv: vec2<f32>; [[location(1)]] tangent_position: vec3<f32>; [[location(2)]] tangent_view_position: vec3<f32>; }; ... [[stage(vertex)]] fn main(model: VertexInput) -> VertexOutput { ... } 
Enter fullscreen mode Exit fullscreen mode

It took me time to figure out how to represent a similar set of input/output parameters in rust-gpu. It turns out the order of arguments in the shader function maps to the location index. It is more evident with input parameters, though. Output parameters are all &mut arguments; as far as I understand, their locations are resolved in order of occurrence except for the ones marked explicitly to built-in meanings (like #[spirv(position, invariant)] in the code below).

#[spirv(vertex)] pub fn main_vs( position: Vec3, // implicit input Location 0 uv: Vec2, // implicit input Location 1 normal: Vec3, // implicit input Location 2 etc. tangent: Vec3, bitangent: Vec3, ... #[spirv(position, invariant)] out_position: &mut Vec4, // builtin position out_uv: &mut Vec2, // implicit output Location 0 out_tangent_position: &mut Vec3, // implicit output Location 1 out_tangent_view_position: &mut Vec3 // implicit output Location 2 ) 
Enter fullscreen mode Exit fullscreen mode

From indexes to Glam methods

In the WGSL shader, all vector arguments had built-in type vec3<f32>, and you could access elements by index. Naively, I copied the code into the rust-gpu shader, using glam::Vec3 as an input type.

use spirv_std::glam::Vec3; #[spirv(vertex)] pub fn main_vs( position: Vec3, ... ) { let x = position[0]; } 
Enter fullscreen mode Exit fullscreen mode

This code causes a confusing compiler error:

error: Using pointers with OpPhi requires capability VariablePointers or VariablePointersStorageBuffer %184 = OpPhi %_ptr_Function_float %181 %172 %182 %173 %183 %174 | = note: module `/Users/bardt/Projects/rust/asteroids/target/spirv-builder/spirv-unknown-vulkan1.2/release/deps/model.spv.dir/module` 
Enter fullscreen mode Exit fullscreen mode

The remedy is to be careful while copy-pasting the code between shaders and consider the new data structures used. In Glam, you access elements via named properties:

use spirv_std::glam::Vec3; #[spirv(vertex)] pub fn main_vs( position: Vec3, ... ) { let x = position.x; // use property instead } 
Enter fullscreen mode Exit fullscreen mode

Matrix parameters

One thing I couldn't make work in WGSL and hoped to get in rust-gpu is passing matrix parameters. I hoped this to work:

#[spirv(vertex)] pub fn main_vs( position: Vec3, uv: Vec2, normal: Vec3, tangent: Vec3, bitangent: Vec3, model_matrix: Mat4, // matrix parameter ... ) { ... } 
Enter fullscreen mode Exit fullscreen mode

Unfortunately, magic didn't happen. Instead, I got a very confusing error. I don't fully understand why the location is 6 while I expected it to be 5. Now I know that this roughly translates to "there is something wrong with your arguments".

error: Entry-point has conflicting input location assignment at location 6, component 0 OpEntryPoint Vertex %2 "main_vs" %position %uv %normal %tangent %bitangent %model_matrix ... 
Enter fullscreen mode Exit fullscreen mode

I had to switch back to passing each matrix column as a separate vector and then combining them in the shader body. The same applies to passing arguments between vertex and fragment shader.

use spirv_std::glam::mat4; #[spirv(vertex)] pub fn main_vs( position: Vec3, uv: Vec2, normal: Vec3, tangent: Vec3, bitangent: Vec3, model_matrix_0: Vec4, model_matrix_1: Vec4, model_matrix_2: Vec4, model_matrix_3: Vec4, ... ) { let model_matrix = mat4( model_matrix_0, model_matrix_1, model_matrix_2, model_matrix_3, ); ... } 
Enter fullscreen mode Exit fullscreen mode

for loops

Another weird thing I noticed is for loops are not working. The shader compiles fine and passes validation, but pixels are not drawn on the screen, so I assume the code doesn't reach the final line.

for i in 0..lights_number { // do stuff } *output = result_color; 
Enter fullscreen mode Exit fullscreen mode

After refactoring to a while loop, everything works just fine.

let mut i = 0_usize; while i < lights_number { // do stuff } 
Enter fullscreen mode Exit fullscreen mode

min/max on ints

One more surprise: the min and max methods do not work on integers while working fine on floats.

let lights_number: usize = lights.size.min(MAX_LIGHTS); 
Enter fullscreen mode Exit fullscreen mode

The error message gives a slight hint on the roots of the problem but doesn't help much in solving it:

error: u8 without OpCapability Int8 --> ~/.rustup/toolchains/nightly-2022-01-13-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/cmp.rs:850:5 | 850 | fn partial_cmp(&self, other: &Ordering) -> Option<Ordering> { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: Stack: core::cmp::min_by::<usize, <usize as core::cmp::Ord>::cmp> <usize as core::cmp::Ord>::min model::main_fs main_fs 
Enter fullscreen mode Exit fullscreen mode

I decided not to spend too much time on finding the root causes (here is a related issue), and rewrote that comparison by hand.

fn min_usize(a: usize, b: usize) -> usize { if a <= b { a } else { b } } let lights_number: usize = min_usize(lights.size, MAX_LIGHTS); 
Enter fullscreen mode Exit fullscreen mode

Conclusion

rust-gpu concept and vision has the potential to flip the game in writing testable, maintainable, reusable shader code. Still, it is in alpha, has a lot of minor issues and inconveniences, and you should seriously evaluate if you want to spend time on those in a project with a deadline and requirements for performance and stability. However, all of this is not the case for my pet project game, so I keep living on the edge and look forward to a bright future.

Top comments (0)