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)