Repository navigation
RFC: entry point redesign ideation #449
Description
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
LegNeato commentedon Oct 22, 2025
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.
nazar-pc commentedon Oct 22, 2025
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: u32in 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.marstaik commentedon Oct 29, 2025
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:
Then, when writing shader's, it turns out quite clean IMO:
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.
[-]Design: entry point redesign ideation[/-][+]RFC: entry point redesign ideation[/+]Firestar99 commentedon Nov 3, 2025
I made a first prototype for querying read-only builtins as functions within
spirv_std: #459schell commentedon Nov 27, 2025
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.nazar-pc commentedon Nov 27, 2025
That would be really nice! The amount of boilerplate required right now is way too much.
Firestar99 commentedon Nov 28, 2025
From the description:
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:
Given these traits in spirv-std:
And then offer some conversion functions directly to wgpu or ash types, hidden behind some CPU-only feature.
marstaik commentedon Dec 7, 2025
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.:
FacelessTiger commentedon Dec 11, 2025
If the
PushConstantstruct here correlates to one you'd actually use in the shader and this would be generated asash::vk::PipelineLayoutCreateInfoCPU 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.
Firestar99 commentedon Dec 12, 2025
I could see two different approaches to modelling push constants.
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 containsPushConstant) 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.
marstaik commentedon Dec 13, 2025
An approach I've been considering and I think might just work is to have:
locationneeds one as well, but perhaps we can have anautolike#[spirv(location =auto(1)]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
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.
Firestar99 commentedon Dec 16, 2025
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)
antonWetzel commentedon Apr 6, 2026
Context
With some macro
abusemagic I managed to get something similar towgsl-syntax working (I only converted simple shaders without generics).The macros generate a new function, which is the real
spirventry point, with everything expanded to be valid forrust-gpu. Additionally definitions forwgpuare generated.My use case are many vertex and fragment shaders, where I want to reuse a definition for
Vertex,Camera, etc.wgslrust#[binding = ...]#[bind_group = ...]Bind groups
Vertex Buffer
Full example
The Repository is CodeBerg:rust-to-wgpu 1 and the example with wgpu code CodeBerg:rust-to-wgpu/example 2
My conclusion
&mut-out-parameterswgpurequires for every bind group what stage (vertex/fragment/compute) uses itspirvFootnotes
https://codeberg.org/AntonWetzel/rust-to-wgpu ↩
https://codeberg.org/AntonWetzel/rust-to-wgpu/src/branch/main/example ↩
LegNeato commentedon Apr 6, 2026
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).