SPIRV layout checking for Rust
2020-01-05 — Less error-prone CPU <-> GPU data transfer.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 thatvec3
followed by afloat
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.