Skip to content

RFC: entry point redesign ideation #449

Description

@Firestar99

Roughly ordered from easiest to hardest to design / implement:

  • builtins as functions in spirv-std, that any code can just call instead of relying on the callees to patch through the correct variables. Needs validation during linking, since buildins are often valid only in some shader types.
  • wgpu "bind group layout" / vulkan "descriptor set layout" should have some kind of direct representation within rust-gpu directly. I'm thinking of a struct declaring all the bindings of one descriptor set, soft of like wgsl does it:
#[spirv(descriptor_set)]
struct BindGroupForSystemX {
  #[binding(0)] data: TypedBuffer<MyData>,
  #[binding(1)] base_color: Image2D,
  #[binding(2)] sampler: Sampler,
}

It would be amazing if you could pass them around within this struct (you currently can't), since you could do something like this:

#[spirv(fragment)]
fn my_entry(
    #[spirv(descriptor_set = 0)] system_x: &BindGroupForSystemX,
) {
  systemx::process(system_x)
}

Note how the descriptor_set / bind group id is declared at the entry point, so you could move it to whatever id you'd want and thus avoid conflicts if two systems use the same id? Also I pass it in as a reference, so you can't change eg. the buffer that's in systemx.data to another. We may want that as a feature, but assigning it non-uniformly makes it potentially problematic.

  • small extra: Have a non-generic texture that can be unsafely cast to all the generic textures we have right now. For spirv, we need to automatically add a new texture binding of that texture type and the same binding id when casts happen. Super useful for bindless! See rust-gpu zulip, research, slang __DynamicResource
  • "pipeline layout" should probably also some some representation as well. It declares the ids of the descriptor sets and the push constants. I'd move it to a struct similarly to above. This also makes it trivial to ensure a vertex and fragment shader have the same pipeline layout.
#[spirv(pipeline_layout)]
struct MyPipelineLayout {
  #[descriptor_set(0)] system_x: BindGroupForSystemX,
  #[descriptor_set(1)] system_y: BindGroupForSystemY,
  #[push_constant] push: MyPushConstant,
}

#[spirv(vertex)]
fn my_fragment(pipeline: MyPipelineLayout) {...}
#[spirv(fragment)]
fn my_fragment(pipeline: MyPipelineLayout) {...}
  • Offer derive macros to create bind group layouts / descriptor set layouts / pipeline layouts directly from the structs above for wgpu / vulkan. We'd likely want our own struct representation of descriptor sets and pipeline layouts to derive to, which may be used by the application as metadata. We'd likely want a generic & const-generic variant for compile time checked code like below (from my project):
pub fn create_graphics_pipeline<T: BufferStruct>(
    vertex_shader: &impl BindlessShader<ShaderType = VertexShader, ParamConstant = T>,
    fragment_shader: &impl BindlessShader<ShaderType = FragmentShader, ParamConstant = T>,
    ...
);

We'd also want to offer a non-generic variant for dynamic use-cases, like within this function. And from there offer conversion functions from there to the various wgpu & vulkan objects, hidden behind features. It's just so annoying having to keep bindings and descriptor set IDs in sync!

  • The spirv spec requires that an entry point specifies all the in out and built-ins that are used within the shader. Currently, you specify those explicitly in the entry point. But with all of the above, we'd need a fixup pass during linking to assemble the entry points properly. When we already have that, why not add link-time capabilities as well?

Feel free to add suggestions!

Activity

  1. LegNeato commented on Oct 22, 2025

    @LegNeato
    Collaborator

    I have lots of ideas here and a 1/2 written blog post and spec. I think modeling it as a fn is wrong in general.

  2. nazar-pc commented on Oct 22, 2025

    @nazar-pc
    Contributor

    I personally really like explicit arguments that are "normal Rust" rather than magical intrinsics-like functions. So current situation with arguments is fine, deriving something on a struct is fine too as long as that struct is then an explicit argument of the shader entrypoint.

    I did raise a question about const-like things in Discord. It'd be nice to be able to use things like const SUBGROUP_SIZE: u32 in const generics to size various data structures like arrays. It is workable today as is through DCE, but I'd be interested in maybe seeing a proper integration of those things into Rust type system.

  3. marstaik commented on Oct 29, 2025

    @marstaik

    I think in regards to descriptor sets/bindings, the issue is more on being able to consistently use the same sets over and over again.

    In slang, you can declare binds globally, and then by importing the module you have access to it. That has some plus and minuses.

    My current approach that has me very happy, but involves my own custom macro, is as follows:

    // Hardcoded descriptor mappings: (marker, qualifier, descriptor_set, binding)
    // Add or modify entries here to expose new parameter marker attributes.
    const DESCRIPTORS: &[(&str, &str, u32, u32)] = &[
    	("framebuffer_indo", "storage_buffer", 0, 0),
    	("camera", "storage_buffer", 0, 1),
    	("mesh_info", "storage_buffer", 0, 2),
    	("instance_data", "storage_buffer", 0, 3),
    	("material", "storage_buffer", 1, 0),
    	("instance_material", "storage_buffer", 1, 1),
    ];
    
    use proc_macro::TokenStream;
    use proc_macro2::{Delimiter, Group, TokenTree};
    use quote::{TokenStreamExt, format_ident, quote};
    use spirv_std_types::spirv_attr_version::spirv_attr_with_version;
    
    #[proc_macro_attribute]
    pub fn scene_descriptors(_attr: TokenStream, item: TokenStream) -> TokenStream {
    	let spirv_ident = format_ident!("{}", &spirv_attr_with_version());
    
    	// prepend with #[rust_gpu::spirv(..)]
    	// let attr: proc_macro2::TokenStream = attr.into();
    	let mut tokens: proc_macro2::TokenStream = proc_macro2::TokenStream::new();
    
    	let item: proc_macro2::TokenStream = item.into();
    	for tt in item {
    		match tt {
    			TokenTree::Group(group) if group.delimiter() == Delimiter::Parenthesis => {
    				let mut group_tokens = proc_macro2::TokenStream::new();
    				let mut last_token_hashtag = false;
    				for tt in group.stream() {
    					let is_token_hashtag =
    						matches!(&tt, TokenTree::Punct(punct) if punct.as_char() == '#');
    
    					// If this is an attribute group (#[...]) whose marker matches one of our
    					// hardcoded descriptors, replace it with the full spirv descriptor attribute.
    					if let TokenTree::Group(bracket_group) = &tt {
    						if bracket_group.delimiter() == Delimiter::Bracket && last_token_hashtag {
    							if let Some(TokenTree::Ident(marker_ident)) =
    								bracket_group.stream().into_iter().next()
    							{
    								let marker = marker_ident.to_string();
    								if let Some((_, qualifier, set, binding)) =
    									DESCRIPTORS.iter().find(|(name, ..)| *name == marker)
    								{
    									let qualifier_ident = format_ident!("{}", qualifier);
    									group_tokens.extend(quote! {
    										[cfg_attr(target_arch="spirv", rust_gpu::#spirv_ident (#qualifier_ident, descriptor_set = #set, binding = #binding))]
    									});
    									last_token_hashtag = is_token_hashtag;
    									continue;
    								}
    							}
    						}
    					}
    
    					group_tokens.append(tt);
    					last_token_hashtag = is_token_hashtag;
    				}
    				let mut out = Group::new(Delimiter::Parenthesis, group_tokens);
    				out.set_span(group.span());
    				tokens.append(out);
    			}
    			_ => tokens.append(tt),
    		}
    	}
    	tokens.into()
    }

    Then, when writing shader's, it turns out quite clean IMO:

    #[scene_descriptors]
    #[spirv(vertex)]
    pub fn vertex_color_material_vert(
    	#[camera] cameras: &[Camera],
    	#[instance_data] instance_data: &[InstanceData],
    	#[instance_material] instance_materials: &[VertexColorMaterialData],
    	#[spirv(instance_index)] instance_index: u32,
    	vertex: PositionColor,
    	out_data: &mut VertexOutput,
    	#[spirv(position)] out_pos: &mut Vec4,
    ) {
    	let my_instance_data = &instance_data[instance_index as usize];
    	let my_material_data = &instance_materials[instance_index as usize];
    
    	let world_pos = my_instance_data.world_matrix * vertex.position;
    	let clip_pos = cameras[0].projection_view * world_pos;
    
    	*out_pos = clip_pos;
    	out_data.color = my_material_data.color * vertex.color;
    }

    Maybe we can provide helper macros to setup common descriptor sets like this - and even enforce the actual type they check against.

    Mine is just a rough implementation cause I'm the only consumer.

  4. changed the title [-]Design: entry point redesign ideation[/-] [+]RFC: entry point redesign ideation[/+] on Oct 30, 2025
  5. Firestar99 commented on Nov 3, 2025

    @Firestar99
    MemberAuthor

    I made a first prototype for querying read-only builtins as functions within spirv_std: #459

  6. schell commented on Nov 27, 2025

    @schell
    Contributor

    I love this idea. Passing things around as descriptor sets defined on structs sounds nice, and might also allow us to auto generate linkage for runtimes like wgpu.

  7. nazar-pc commented on Nov 27, 2025

    @nazar-pc
    Contributor

    generate linkage for runtimes like wgpu.

    That would be really nice! The amount of boilerplate required right now is way too much.

  8. Firestar99 commented on Nov 28, 2025

    @Firestar99
    MemberAuthor

    From the description:

    Offer derive macros to create bind group layouts / descriptor set layouts / pipeline layouts directly from the structs above for wgpu / vulkan. We'd likely want our own struct representation of descriptor sets and pipeline layouts to derive to, which may be used by the application as metadata. We'd likely want a generic & const-generic variant for compile time checked code like below (from my project): ...

    It would be trivial to emit said metadata, since we already have a derive (or attr) macro on the struct. It would just need to implement a few traits:

    impl PipelineLayoutTrait for MyPipelineLayout {
        const LAYOUT: PipelineLayout<'static> = PipelineLayout {
            descriptor_sets: &[
                DescriptorSet {
                    set: 0,
                    bindings: <BindGroupForSystemX as DescriptorSet>::BINDINGS,
                },
                DescriptorSet {
                    set: 1,
                    bindings: <BindGroupForSystemY as DescriptorSet>::BINDINGS,
                },
            ];
            push_constant: None,
        }
    }
    
    impl DescriptorSetTrait for BindGroupForSystemX {
        const BINDINGS: &[Binding] = &[
            Binding {
                binding: 0,
                type: BindingType::Buffer,
            },
            Binding {
                binding: 1,
                type: BindingType::Image,
            },
            Binding {
                binding: 2,
                type: BindingType::Sampler,
            },
        ];
    }

    Given these traits in spirv-std:

    pub trait PipelineLayoutTrait {
        const LAYOUT: PipelineLayout<'static>;
    }
    
    pub trait DescriptorSetTrait {
        const BINDINGS: &'static [Binding];
    }
    
    pub struct PipelineLayout<'a> {
        pub descriptor_sets: &'a [DescriptorSet],
        pub push_constant: Option<PushConstant>,
    }
    
    pub struct DescriptorSet<'a> {
        pub set: u32,
        pub bindings: &'a [Binding],
    }
    
    pub struct Binding {
        pub binding: u32,
        pub type: BindingType,
    }
    
    pub enum BindingType {
        Buffer,
        StorageImage,
        SampledImage,
        Sampler,
    }

    And then offer some conversion functions directly to wgpu or ash types, hidden behind some CPU-only feature.

  9. marstaik commented on Dec 7, 2025

    @marstaik

    I like the struct approach, but one immediate thing that comes to mind is that it needs to support generics.

    I have pipeline layouts that are near identical except for one-two types depending on material that are re-used everywhere.

    e.g.:

    #[spirv(descriptor_set)]
    struct SceneBindGroup {
      #[binding(0)] data: TypedBuffer<MyData>,
      #[binding(1)] base_color: Image2D,
      #[binding(2)] sampler: Sampler,
    }
    
    #[spirv(descriptor_set)]
    struct MaterialBindGroup<MaterialData> {
      #[binding(0)] data: TypedBuffer<MaterialData>,
    }
    
    
    #[spirv(pipeline_layout)]
    struct SceneLayout<MaterialData> {
      #[descriptor_set(0)] scene_group: SceneBindGroup,
      #[descriptor_set(1)] material_group: MaterialBindGroup<MaterialData>,
      #[push_constant] push: MyPushConstant,
    }
    
    struct MyMaterialData {
    }
    
    #[spirv(vertex)]
    fn my_fragment(pipeline: MyPipelineLayout<MyMatrialData>) {...}
    #[spirv(fragment)]
    fn my_fragment(pipeline: MyPipelineLayout<MyMaterialData>) {...}
  10. FacelessTiger commented on Dec 11, 2025

    @FacelessTiger
    pub struct PipelineLayout<'a> {
       pub descriptor_sets: &'a [DescriptorSet],
       pub push_constant: Option<PushConstant>,
    }

    If the PushConstant struct here correlates to one you'd actually use in the shader and this would be generated as ash::vk::PipelineLayoutCreateInfo CPU side then my main concern is how you'd correlate that to the push constant range.

    In a bindless system it seems people generally prefer doing oversized ranges (128/256 byte) with all shader stages, while for bindful it generally seems to be the smallest size with the lowest amount of shader stages (which there'd be no way to provide the info for here). But, sometimes people also seem to have multiple push constant structs at different offsets for the different entry points (which neither proposal would support really).

    I think for that aspect it'd probably be best to have the user provide the push constant layout manually when generating the vulkan structs CPU side, so it's the most flexible for all those use cases.

  11. Firestar99 commented on Dec 12, 2025

    @Firestar99
    MemberAuthor

    I could see two different approaches to modelling push constants.

    1. Force users to use the one type to define the push constants for all shader stages. That struct can then contain other structs for each individual shader. It would limit you to assigning push constants to all shader stages and always start at offset 0, like the ash code below:
    PushConstantRange {
    	offset: 0,
    	size: size_of::<MyPushConstant>() as u32,
    	stage_flags: ALL,
    }
    1. We could allow some kind of attribute that allows specifying an offset.

    I think the main question is: How do you model that a vertex and fragment shader are compatible, possibly at compile time? This must include the PipelineLayout (which contains PushConstant) but also the input & output interface between the shaders. And how to deal with values passing through the rasterizer, with integers needing to be #[spirv(flat)]. That's still completely absent from this design.

    I feel like we should focus on a simpler design for just compute shaders first, and then worry about graphics in a second pass. Allowing us to ignore Input, Output and interpolation params.

  12. marstaik commented on Dec 13, 2025

    @marstaik

    An approach I've been considering and I think might just work is to have:

    • All variables must have a spirv attibute
      • This means location needs one as well, but perhaps we can have an auto like #[spirv(location =auto(1)]
    • All spirv attributes on structs and members are recursively expanded
    • Start from the basics, and add abstractions around these as needed (PipelineLayouts, Frag Location -> Vertex Location mapping)

    I'm of the opinion of the base principles are correct, we can stack up compile time checks and other things as we go.

    As I've been implementing and iterating a lot of render shaders (not many compute yet), I've been envisioning something like this:

    Example code
    struct Camera {
        view_matrix: Mat4,
        projection_matrix: Mat4,
    }
    
    struct AmbientLight {}
    
    struct DirectionalLight {}
    
    trait MaterialData{}
    
    struct BaisicMaterialData {
        albedo_color: Vec4,
        roughness: f32,
        metallic: f32,
    }
    
    impl MaterialData for BasicMaterialData {}
    
    trait Vertex {}
    
    struct BasicVertex {
        position: Vec4,
        normal: Vec4,
        tangent: Vec4,
        tex_coords: Vec2,
    }
    
    impl Vertex for BasicVertex {}
    
    
    struct InstanceData {
        world_matrix: Mat4,
        material_index: u32,
        mesh_index: u32,
    }
    
    
    struct Material<M: MaterialData> {
        #[spirv(descriptor_set = 3, binding = 0)] instance_data: &[InstanceData],
        #[spirv(descriptor_set = 3, binding = 1)] material_data: &[M],
    }
    
    impl <M: MaterialData> Material<M> {
        fn get_instance_data(&self, index: u32) -> &InstanceData {
            &self.instance_data[index as usize]
        }
    
        fn get_material_data(&self, index: u32) -> &M {
            &self.material_data[index as usize]
        }
    }
    
    struct Scene {
        #[spirv(descriptor_set = 2, binding = 0)] camera: &Camera,
    }
    
    struct Lights {
        #[spirv(descriptor_set = 1, binding = 0)] ambient_lights: &[AmbientLight],
        #[spirv(descriptor_set = 1, binding = 1)] directional_lights: &[DirectionalLight],
    }
    
    
    struct Globals<V: Vertex> {
       	#[spirv(descriptor_set = 0, binding = 0)] textures: &RuntimeArray<
    		Image!(2D, type=f32, sampled),
    	>,
    	#[spirv(descriptor_set = 0, binding = 0)] meshes: &RuntimeArray<
    		&[V]
    	>,
    }
    
    impl <V: Vertex> Globals<V> {
        fn get_mesh(&self, index: u32) -> &[V] {
            &self.meshes[index as usize]
        }
    }
    
    struct SceneMaterialPipeline<V: Vertex, M: MaterialData> {
        globals: &Globals<V>,
        scene: &Scene,
        lights: &Lights,
        material: &Material<M>,
    }
    
    struct VertexOutput1 {
        position: Vec4,
        normal: Vec4,
        tangent: Vec4,
        tex_coords: Vec2,
        #[spirv(flat)]
        flat: VertexOutputFlat,
    }
    
    struct VertexOutputFlat {
        data: u32,
    }
    
    struct VertexOuptut2 {
        #[spirv(location = 1, auto)]
        extra_data: Vec4,
    }
    
    #[spirv(location = 2, auto)]
    struct VertexOutput3 {
        more_data: Vec4,
    }
    
    #[spirv(vertex)]
    pub fn monomorphized_vertex_main(
        pipeline: &SceneMaterialPipeline<BasicVertex, BasicMaterialData>,
        #[spirv(vertex_index)] vertex_index: u32,
        #[spirv(instance_index)] instance_index: u32,
        #[spirv(location=0, auto)] vout1: &mut VertexOutput1,
        vout2: &mut VertexOuptut2,
        vout3: &mut VertexOutput3,
        // ...
    ) -> VertexOutput {
        let instance = pipeline.material.get_instance_data(instance_index);
        let mesh = pipeline.globals.get_mesh(instance.mesh_index);
        
        // ...
    }
    
    #[spirv(fragment)]
    pub fn monomorphized_fragment_main(
        pipeline: &SceneMaterialPipeline<BasicVertex, BasicMaterialData>,
        #[spirv(location=0)] vin1: &VertexOutput1,
        vin2: &VertexOuptut2,
        vin3: &VertexOutput3,
        // ...
    ) {
        // ...
        
        // ...
    }

    I think one of the biggest usability issues is that unlike other dedicated shader languages, we don't have globals for bindings, which is fine because of the model itself - but how can we make it all easier to use?

    I think that lies in being able to attach a bunch of the spirv attributes to a data structure type we can pass around everywhere, break apart and compose how we need to.

  13. Firestar99 commented on Dec 16, 2025

    @Firestar99
    MemberAuthor
    • @marstaik From what I've gathered, you want to be able to compose N layers of systems, not just 2 layers (descriptor_set and binding) like my first suggestion. The main problem I see in your design is two systems declaring the same descriptor set and bindings for something, but with different values (eg. Buffer and Image). These systems would no longer be composable due to the conflict, even worse, if you got those graphics libs from crates.io since you can't "easily" change them.

    Which is why my design allows systems to only assign bindings, not descriptor sets. So that the entry point (or PipelineLayout), the final consumer of the systems, can assign each system a unique descriptor set. And do so at it's declaration, not spread out over the codebase.

    • Generics: At codegen level, all the generics have been monomorphization away and we only see concrete types. So consider generics to be effectively free to support.

    • Rasterization: I'd like to focus on descriptor sets and bindings first, and worry about the graphics pipeline's capabilities later. Such as Input and Output declarations with locations and interpolation. Apart from #[flat] in structs missing, I think they can stay as they are for quite a while longer, until we've got the resources to address them. Especially since I fixed location assignment recently, you can now pass as many structs as inputs & outputs as you want (once this is merged).

    • (Also could you wrap your code examples in this to make them expandable? thx)

    <details><summary>Example code</summary>
    code here
    </details>
    
  14. antonWetzel commented on Apr 6, 2026

    @antonWetzel

    Context

    With some macro abuse magic I managed to get something similar to wgsl-syntax working (I only converted simple shaders without generics).
    The macros generate a new function, which is the real spirv entry point, with everything expanded to be valid for rust-gpu. Additionally definitions for wgpu are generated.

    My use case are many vertex and fragment shaders, where I want to reuse a definition for Vertex, Camera, etc.

    wgsl rust note
    uniform struct struct
    vertex struct struct
    bind group struct contains multiple #[binding = ...]
    bind set function arguments single #[bind_group = ...]
    entry point function
    pipeline macro combine the used bind groups from multiple functions
    Bind groups
    #[derive(Copy, Clone, Pod, Zeroable)]
    #[repr(C)]
    pub struct Settings {
        pub speed: f32,
        pub time: f32,
        pub color_scale: f32,
    }
    
    #[derive(rust_to_wgpu::Arguments)]
    #[rust_to_wgpu(bind_group)]
    pub struct SettingsUniform<'a> {
        #[rust_to_wgpu(binding = 0, uniform)]
        settings: &'a Settings,
    }
    
    #[derive(rust_to_wgpu::Arguments)]
    #[rust_to_wgpu(bind_group)]
    pub struct Texture<'a> {
    	#[rust_to_wgpu(binding = 0)]
    	pub image: &'a Image!(2D, type=f32, sampled),
    	#[rust_to_wgpu(binding = 1)]
    	pub sampler: &'a Sampler,
    }
    
    fn function_stub(
        #[rust_to_wgpu(bind_group = 0)] settings: SettingsUniform<'a>,
        #[rust_to_wgpu(bind_group = 1)] texture: Texture<'a>,
    ) {
        ...
    }
    Vertex Buffer
    #[derive(Debug, Copy, Clone, Pod, Zeroable, rust_to_wgpu::Arguments)]
    #[repr(C)]
    #[rust_to_wgpu(attributes, step_mode = vertex)]
    pub struct Vertex {
    	#[rust_to_wgpu(location = 0)]
    	pub position: Vec2,
    	#[rust_to_wgpu(location = 1)]
    	pub uv: Vec2,
    }
    
    fn function_stub(
        #[rust_to_wgpu(arguments)] vertex: Vertex,
    ) {
        ...
    }
    Full example
    #![cfg_attr(not(feature = "native"), no_std)]
    #![allow(unused_imports)]
    
    use bytemuck::{Pod, Zeroable};
    use glam::{Vec2, Vec3, Vec4};
    // #[cfg(target_arch = "spirv")]
    // use spirv_std::num_traits::Float;
    use spirv_std::{Image, Sampler};
    
    #[derive(Copy, Clone, Pod, Zeroable)]
    #[repr(C)]
    pub struct Settings {
        pub speed: f32,
        pub time: f32,
        pub color_scale: f32,
    }
    
    #[derive(rust_to_wgpu::Arguments)]
    #[rust_to_wgpu(bind_group)]
    pub struct SettingsUniform<'a> {
        #[rust_to_wgpu(binding = 0, uniform)]
        settings: &'a Settings,
    }
    
    #[derive(Debug, Copy, Clone, Pod, Zeroable, rust_to_wgpu::Arguments)]
    #[repr(C)]
    #[rust_to_wgpu(attributes, step_mode = vertex)]
    pub struct Vertex {
        #[rust_to_wgpu(location = 0)]
        pub position: Vec2,
        #[rust_to_wgpu(location = 1)]
        pub uv: Vec2,
    }
    
    #[derive(Debug, Copy, Clone, Pod, Zeroable, rust_to_wgpu::Arguments)]
    #[repr(C)]
    #[rust_to_wgpu(attributes, step_mode = instance)]
    pub struct Instance {
        #[rust_to_wgpu(location = 2)]
        pub color: Vec3,
        #[rust_to_wgpu(location = 3)]
        pub offset: f32,
    }
    
    #[derive(Debug, rust_to_wgpu::Arguments)]
    pub struct VertexOutput {
        #[rust_to_wgpu(output = "position", input = "frag_coord")]
        vtx_pos: Vec4,
        #[rust_to_wgpu(location = 0)]
        vtx_color: Vec3,
        #[rust_to_wgpu(location = 1)]
        uv: Vec2,
    }
    
    #[rust_to_wgpu::entry]
    #[spirv(vertex)]
    pub fn main_vs<'a>(
        #[rust_to_wgpu(bind_group = 0)] settings: SettingsUniform<'a>,
        #[rust_to_wgpu(arguments)] vertex: Vertex,
        #[rust_to_wgpu(arguments)] instance: Instance,
    ) -> VertexOutput {
        let angle = settings.settings.time * settings.settings.speed + instance.offset;
        let position = glam::Mat2::from_angle(angle).mul_vec2(vertex.position);
    
        VertexOutput {
            vtx_pos: Vec4::from((position, 0.0, 1.0)),
            vtx_color: instance.color,
            uv: vertex.uv,
        }
    }
    
    #[derive(rust_to_wgpu::Arguments)]
    #[rust_to_wgpu(bind_group)]
    pub struct Texture<'a> {
        #[rust_to_wgpu(binding = 0)]
        pub image: &'a Image!(2D, type=f32, sampled),
        #[rust_to_wgpu(binding = 1)]
        pub sampler: &'a Sampler,
    }
    
    #[rust_to_wgpu::entry]
    #[spirv(fragment)]
    pub fn main_fs<'a>(
        #[rust_to_wgpu(bind_group = 0)] settings: SettingsUniform<'a>,
        #[rust_to_wgpu(bind_group = 1)] texture: Texture<'a>,
        #[rust_to_wgpu(arguments)] input: VertexOutput,
    ) -> shaders_dep::FragmentOutput {
        let color = texture.image.sample(*texture.sampler, input.uv);
        shaders_dep::FragmentOutput {
            color: Vec4::from((input.vtx_color * settings.settings.color_scale, 1.0)) * color,
        }
    }
    
    rust_to_wgpu::bind_groups!(main_vs, main_fs);

    The Repository is CodeBerg:rust-to-wgpu 1 and the example with wgpu code CodeBerg:rust-to-wgpu/example 2

    My conclusion

    • I think some built-in way to reuse definitions is better (macros are possible, but very brittle)
    • For me normal return arguments are a must, I don't like &mut-out-parameters
    • I did not use a pipeline-struct, because wgpu requires for every bind group what stage (vertex/fragment/compute) uses it
      • The pipeline would be passed in full to all stages, no clue if this changes the generated spirv

    Footnotes

    1. https://codeberg.org/AntonWetzel/rust-to-wgpu ↩

    2. https://codeberg.org/AntonWetzel/rust-to-wgpu/src/branch/main/example ↩

  15. LegNeato commented on Apr 6, 2026

    @LegNeato
    Collaborator

    You can also check out some thoughts in https://www.vectorware.com/blog/threads-on-gpu/. I don't think functions are the right abstraction for entrypoints, though I am not saying main w/ threads is the best either.

    And while on CPU main() is a function, it is handled special by the language and compiler. If a function ends up being the right abstraction, I would expect the function itself would be special (like having const generics for dispatch sizes, special apis to get inputs and outputs not in the signutre a la env vars, etc).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions