MaikKlein / rlsl

Rust to SPIR-V compiler
Apache License 2.0
557 stars 14 forks source link

Handling alignment for uniforms and push constants #31

Closed MaikKlein closed 6 years ago

MaikKlein commented 6 years ago

Requirements

Standard Uniform Buffer Layout

Every member of an OpTypeStruct with storage class of Uniform and a decoration of Block (uniform buffers) must be laid out according to the following rules:

reference

These also then follow the rules from std140. Also mentioned in 14.5.4.

Additionally from the SPIR-V spec

Composite objects in the UniformConstant, Uniform, and PushConstant Storage Classes must be explicitly laid out. The following apply to all the aggregate and matrix types describing such an object, recursively through their nested types:

Meaning

RLSL needs to explicit decorate members of a struct with the correct offset. That offset needs to follow the rules defined in std140, and this only applies to PushConstant and Uniform.

Implementation

Idea 1

RLSL can easily generate the correct offset from the rules from std140 but then the Rust and RLSL code would get out of sync.

One idea would be to only check if the struct is conformant with the rules from std140. Then RLSL would just extract the offset that was defined by the user.

Generally annotation of structs doesn't seem reasonable, for example you don't want your Vec3<f32> to be 16 bytes wide in the vertex input. Either accept the performance hit, or create separate types for the all the uniforms.

Idea 2

#[derive(Packed)]
pub struct SomeStruct{
    pub v: Vec4<f32>,
    pub f: f32
}

// generated
fn compute_packed(&self) -> Packed<SomeStruct>{..}

Compute the correct layout inside RLSL and implement Packed in normal Rust as a custom derive.

It would then know how to serialize the specified struct with the rules of std140. And Packed could look like this

struct Packed<T>{
    _m: PhantomData<T>,
    data: Vec<u8>
}

// Hopefully the max size can be computed statically
struct Packed<T: Uniform>{
    _m: PhantomData<T>,
    data: [u8; T::Size]
}

For example if you have

let uniforms: Vec<SomeStruct> = ..;

And if you want to send it to the shader you have to pack them correctly

for packed_struct in uniforms.iter().map(SomeStruct::compute_packed){
    // pseudo api
    some_shader.send(packed_struct);
    //draw
}

And inside RLSL you can just use SomeStruct directly.

I am unsure how exactly I could implement it because custom derive doesn't expose type information but I think there should be a way. I might have to use const_fn. Alternatively everything could be computed in a build script with the reflection api.

How do you currently handle alignment? What would be your preferred API?

MaikKlein commented 6 years ago

Idea 3

#[derive(Packed)]
pub struct SomeStruct{
    pub v: Vec4<f32>,
    pub f: f32
}

Maybe packed should just create a complete new struct under the hood.

// Generated
pub struct SomeStructPacked{
    pub v: Vec4<f32>,
    pub f: f32,
    _p1: f32, 
    _p2: f32,
    _p3: f32, 
}
trait Packing {
    type Packed;
}

struct Packed<T: Packing>{
    _m: PhantomData<T>,
    data: T::Packed
}
// generated
impl Packing for SomeStruct {
    type Packed = SomeStructPacked;
}

And then you would create those structs directly Packed<SomeStruct>::new(..);.

MaikKlein commented 6 years ago

I think a possible implementation of the generated code could look like this:

pub trait Alignment {
    const REST: usize;
}
#[repr(C)]
struct Align<A: Alignemnt> {
    data: A,
    _align: [u8; A::REST]
}

impl Alignment for Vec3<f32> {
    const REST: usize = 4; //
}
impl Alignment for f32 {
    const REST: usize = 0;
}

#[repr(C, align = "16")]
pub SomeStructPacked{
    v: Align<Vec3<f32>>,
    f: Align<f32>
}

#[derive(Packed)]
pub struct SomeStruct{
    pub v: Vec3<f32>,
    pub f: f32
}