SPIRV layout checking for Rust

Less error-prone CPU <-> GPU data transfer. Posted in:

Isn't it just great when the GLSL compiler adds unexpected padding between fields in a buffer, and the only way you can tell is that your rendering is broken in weird ways? Having lost a couple of hours to one such bug, I decided I needed a way to quickly catch that sort of error.

Enter the spirv-struct-layout crate. It's pretty straightforward - you define a rust struct, #[derive(SpirvLayout)] on it, and then later on you can invoke the check_spirv_layout(...) function with the SPIRV bytecode of your shader.

Let's say we start with the following buffer structure in GLSL:

layout(std430, binding = 0) buffer Uniforms {
  mat4 model_view;
  vec3 light_dir;
  vec4 position;
} buf;

If you spend a lot of time writing shaders, you may already have caught the problem we want to address here: the spec says vec4 must be aligned to 16 bytes, so the compiler is going to add 4 bytes of padding after light_dir to ensure that position is correctly aligned.

Let's go ahead and define a rust struct to match this GLSL type:

use spirv_struct_layout::{CheckSpirvStruct, SpirvLayout};

#[repr(C)]
#[derive(SpirvLayout)]
struct Uniforms {
    model_view: [f32; 16],
    light_dir: [f32; 3],
    position: [f32; 4],
}

And finally we can run the SPIRV layout check:

fn main() {
    let spirv = cast_clice_u8_to_u32!(include_bytes!("simple.frag.spv"));

    Uniforms::check_spirv_layout("buf", spirv);
}

And if all goes well, the program will exit with an error:

thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `80`,
 right: `76`: field position should have an offset of 80 bytes, but was 76 bytes', spirv_struct_layout/examples/simple/main.rs:20:5

Now that the disaster has been averted, you have a few ways to address the underlying issue.

  • The most straightforward approach is to just never use vec3 in uniform buffers - the alignment rules in GLSL just don't work out nicely when interchanging with host types, and various older GLSL compilers implement them incorrectly regardless.
  • You could also insert 4 bytes of padding on the rust side (i.e. insert a blank f32 member between), although unused struct members are somewhat unergonomic to deal with in rust.
  • For this very simple example, you could also just switch the order of the last two entries in the struct - since the vec4 neatly fills up the entire 16 bytes required by the alignment.
  • And lastly, you could build a Vec3 type in Rust and use #[repr(align(16))] to force the same alignment as GLSL uses (but note Rust will also expand the size of the struct to 16 bytes in this case, meaning that vec3 followed by a float will still end up with 4 bytes of padding between).

This crate is still a work in progress, so when you run into rough edges, please report them over on the github repo.