EmbarkStudios / rust-gpu

🐉 Making Rust a first-class language and ecosystem for GPU shaders 🚧
https://shader.rs
Apache License 2.0
7.33k stars 247 forks source link

Provide a way to default to descriptor_set = 0 and auto generate bindings. #828

Closed charles-r-earp closed 1 year ago

charles-r-earp commented 2 years ago

shaderc has a compiler option set_autobind_uniforms which allows bindings to be omitted.

This would look like this:

#[spirv(compute(threads(64)))]
pub fn main_cs(
    #[spirv(global_invocation_id)] id: UVec3,
    #[spirv(storage_buffer)] prime_indices: &mut [u32],
) {
    let index = id.x as usize;
    prime_indices[index] = collatz(prime_indices[index]).unwrap_or(u32::MAX);
}

Descriptor set would be 0. Bindings would be generated in order, starting with 0. Handling of mixing the two could be implemented, or the macro could error if either the set or binding is specified.

Potentially, this could be opted into either via an option to SpirvBuilder, or the spirv macro itself. Like: `#[spirv(compute(threads(64)), descriptor_set=0, autobind_uniforms)]"

Saving the explicit descriptor set and bindings is mostly for clarity and ease of use, since at the moment it is fairly verbose.

However, particular motivation for automatically generating bindings is for use in macros / generated definitions. For example, optional arguments:

macro_rules! impl_gemm {
    ($($func:ident<$(@splitk=$splitk:tt,)? $T:ty, $TC:ty, $TS:tt, $TSA:tt, $TSB:tt, $UNR:tt, $MICA:tt, $MICB:tt>($($bias:tt=true)?)),* $(,)?) => (
        $(
            #[allow(unused_attributes)]
            #[spirv(compute(threads($TS)))]
            pub fn $func(
                #[spirv(workgroup_id)]
                group_id: UVec3,
                #[spirv(local_invocation_id)]
                local_id: UVec3,
                #[spirv(storage_buffer, descriptor_set=0, binding=0)]
                a: &[$T],
                #[spirv(workgroup)]
                a_tile: &mut [[$T; $TSA * $MICA + 1]; $UNR],
                #[spirv(storage_buffer, descriptor_set=0, binding=1)]
                b: &[$T],
                #[spirv(workgroup)]
                b_tile: &mut [[$T; $TSB * $MICB + 1]; $UNR],
                $(
                    #[spirv(storage_buffer, descriptor_set=0, binding=2)]
                    $bias: &[$T],
                    #[spirv(storage_buffer, descriptor_set=0, binding=3)] // Because we have to add specify bindings, c must be redeclared
                    c: &mut [$TC],
                    #[cfg(feature="false")]  // ugly hack to emulate c style #if false
                )?
                #[spirv(storage_buffer, descriptor_set=0, binding=2)]
                c: &mut [$TC],
                #[spirv(push_constant)]
                push_consts: &GemmPushConsts<$T>,
            ) { 
               /* impl */ 
           }
        )+
     )
}

impl_gemm!{
    gemm_f32_tsa16_tsb16_unr16_mica1_micb1<f32, f32, 256, 16, 16, 16, 1, 1>(),
    gemm_bias_f32_tsa16_tsb16_unr16_mica1_micb1<f32, f32, 256, 16, 16, 16, 1, 1>(bias=true), // Using a macro makes it much easier and less verbose to define several different shaders with different types, a bias, and different generic parameters. 
}

The full gemm impl can be found here Essentially, bias is an optional argument, and the macro allows for this argument to be added to the generated entry definition. But this requires some gymnastics to redefine the binding of c from 2 to 3, requiring it to be redeclared. And regardless, it's extremely noisy and tedious to write #[spirv(descriptor_set = 0, binding = 0)] for every argument, when this can be inferred from argument order.

I understand there is some effort towards automatic generation of resource bindings, which may somehow conflict with defaulting to descriptor set = 0 and binding = 0.. when not specified, but this can be mitigated by explicitly opting in.

I can probably implement a workaround for my use case but it does seem like it would be very nice to remove the noise and improve the ergonomics for this. I would like to generate shader code via macros, both for simplicity and potentially for inline shader code, and that is made a lot easier if fewer things have to be specified, especially when working with a list of arguments, adding additional arguments, etc.

I'm happy to work on implementing this if it is a possibility. Otherwise I plan to implement this via a wrapper proc macro, but it would be nicer to standardize this natively.

Thoughts?

hrydgard commented 2 years ago

While it's easy to see some utility in this, with the way we are using rust-gpu so far, the explicit-ness of the current syntax is not a problem for us, and with limited resources, that's our focus right now.

We do have some early thoughts in a similar direction but it's still pretty far off and the design is likely to be different to this, so for the time being you're probably best off with a wrapper proc macro, unfortunately.

expenses commented 2 years ago

I just wanna chip in here and say that what I would love love love is a descriptor-sets-as-structs system, e.g. something like:

struct DescriptorSet<'a> {
    #[spirv(binding = 0, storage_buffer)] prime_indices: &'a mut [u32]
}

#[spirv(compute(threads(64)))]
pub fn main_cs(
    #[spirv(global_invocation_id)] id: UVec3,
    #[spirv(descriptor_set = 0)] descriptor_set: DescriptorSet,
) {
    let index = id.x as usize;
    prime_indices[index] = collatz(descriptor_set.prime_indices[index]).unwrap_or(u32::MAX);
}

I'm not especially opinionated as to whether binding IDs should be allowed to be inferred or not. With this above system it should be possible to have a macro tag that you put on structs to autogenerate the binding IDs anyway.

expenses commented 2 years ago

I just wanna chip in here and say that what I would love love love is a descriptor-sets-as-structs system

This is mentioned here: https://embarkstudios.github.io/rust-gpu/book/rfcs/001-resource-binding-syntax.html but I'm not sure if there's an associated issue.

oisyn commented 1 year ago

I'm closing this as a won't fix for now, we have other priorities.