Skip to content

Conversation

inner-daemons
Copy link
Collaborator

@inner-daemons inner-daemons commented Aug 14, 2025

Connections
Mostly split off from #7930
Works towards #7197

Description
This PR adds mesh shading info to naga IR so that parsers and writers have an interface to use.

Testing
No testing yet (coming in later PRs, the code here has been tested in #7930)

Squash or Rebase?

Squash

Checklist

  • Run cargo fmt.
  • Run taplo format.
  • Run cargo clippy --tests. If applicable, add:
    • --target wasm32-unknown-unknown
  • Run cargo xtask test to run tests.
  • If this contains user-facing changes, add a CHANGELOG.md entry.

@inner-daemons inner-daemons requested a review from a team as a code owner August 14, 2025 17:55
@inner-daemons inner-daemons mentioned this pull request Aug 14, 2025
37 tasks
@inner-daemons
Copy link
Collaborator Author

I'm unsure why the MSRV minimal versions thing is failing, it doesn't look related to this PR since it happens in another crate and this PR doesn't touch anything cargo.

@cwfitzgerald
Copy link
Member

cwfitzgerald commented Aug 17, 2025

Should be fixed with #8112, thanks @andyleiserson!

@inner-daemons
Copy link
Collaborator Author

Going to ping you again @jimblandy just to make sure that this doesn't get forgotten about until the next meeting :)

@jimblandy
Copy link
Member

@SupaMaggie70Incorporated I'll try to get to this tonight, but it may be Saturday.

@jimblandy
Copy link
Member

@SupaMaggie70Incorporated Are these comments at the top of mesh_shading.md still accurate?

Currently naga has no support for mesh shaders beyond recognizing the additional shader stages. For this reason, all shaders must be created with Device::create_shader_module_passthrough.

@inner-daemons
Copy link
Collaborator Author

@SupaMaggie70Incorporated Are these comments at the top of mesh_shading.md still accurate?

Currently naga has no support for mesh shaders beyond recognizing the additional shader stages. For this reason, all shaders must be created with Device::create_shader_module_passthrough.

I've updated the top of mesh_shading.md. Until further PRs it remains true that you will need to use passthrough shaders but it now has more "processing" ability so the statement wasn't strictly true.

@jimblandy
Copy link
Member

jimblandy commented Aug 25, 2025

(This is not a review comment on this PR, and not directed at SupaMaggie, just commenting on the pre-existing state of the docs.)

This is not a thorough explanation of mesh shading and how it works. Those wishing to understand mesh shading more broadly should look elsewhere first.

This is just not adequate. If we're going to add syntax to Naga that is not covered by the WGSL specification, we need to document that syntax. Knowing how mesh shading works in Vulkan does not magically explain the syntax for mesh shaders in WGSL, or how to invoke them in wgpu.

@inner-daemons
Copy link
Collaborator Author

(This is not a review comment on this PR, and not directed at SupaMaggie, just commenting on the pre-existing state of the docs.)

This is not a thorough explanation of mesh shading and how it works. Those wishing to understand mesh shading more broadly should look elsewhere first.

This is just not adequate. If we're going to add syntax to Naga that is not covered by the WGSL specification, we need to document that syntax. Knowing how mesh shading works in Vulkan does not magically explain the syntax for mesh shaders in WGSL, or how to invoke them in wgpu.

It wouldn't be part of this PR, but if you think that having a writeup somewhere to describe mesh shaders would be useful, I'm happy to do that separately. However, I have never written a comprehensive GPU API spec before, so I don't exactly think it would be of comparable quality to e.g. the webgpu spec :)

Also, that specific disclaimer was copy-pasted directly from the RT spec.

@jimblandy
Copy link
Member

It wouldn't be part of this PR, but if you think that having a writeup somewhere to describe mesh shaders would be useful, I'm happy to do that separately. However, I have never written a comprehensive GPU API spec before, so I don't exactly think it would be of comparable quality to e.g. the webgpu spec :)

Well, the alternative is just saying "read the code and the examples and figure it out". You want people to actually use your work, right?

I just pushed some docs; do they look correct?

Copy link
Member

@jimblandy jimblandy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a new Capabilities flag for mesh shading, and the validator should reject mesh stages unless it is set. If that's in a subsequent PR, it needs to be pulled forward into this one; we can't have the stuff in the IR unless we forbid it when it's not enabled. I know the backends will just crash; but it's easiest to keep things coherent.

Please remove the TODO comments and file a follow-up issue for each one if appropriate.

Copy link
Member

@jimblandy jimblandy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The entry point validation in Validator::validate_module_handles needs to handle EntryPoint::mesh_info and EntryPoint::task_payload as well.

@cwfitzgerald
Copy link
Member

There should be a new Capabilities flag for mesh shading, and the validator should reject mesh stages unless it is set. If that's in a subsequent PR, it needs to be pulled forward into this one; we can't have the stuff in the IR unless we forbid it when it's not enabled. I know the backends will just crash; but it's easiest to keep things coherent.

@jimblandy we don't currently do this for compute shaders either though - naga isn't the one who is validating the stage is valid, it's wgpu

@jimblandy
Copy link
Member

Compute shaders are a base WebGPU feature, though, whereas mesh shaders are not.

@cwfitzgerald
Copy link
Member

Oh I was thinking about backends once we already know the stages involved, yes the validator needs to be validating out mesh entry points (and compute entry points too; but that doesn't have to be here)

@inner-daemons
Copy link
Collaborator Author

I have changed Capabilities from being a u32 to a u64, since we just reached 30/32 bits used. I can revert that if it doesn't belong in this PR for some reason.

@cwfitzgerald
Copy link
Member

cwfitzgerald commented Oct 17, 2025

We only need to bump it once we actually hit 33 :)

@inner-daemons
Copy link
Collaborator Author

inner-daemons commented Oct 17, 2025

Ok, I'll revert that.

@inner-daemons
Copy link
Collaborator Author

Just put up #8370, which is the WGSL parsing PR. The WGSL parsing and validation passes there, so this PR's validation seems to not be disallowing valid shaders yet.

@inner-daemons
Copy link
Collaborator Author

@jimblandy Me and @cwfitzgerald agreed that mesh shader outputs should always be buffered, so I'm gonna remove references in the spec to having to call setMeshOutputs exactly once before calling setVertex/setPrimitive and stuff like that.

@jimblandy
Copy link
Member

@jimblandy Me and @cwfitzgerald agreed that mesh shader outputs should always be buffered, so I'm gonna remove references in the spec to having to call setMeshOutputs exactly once before calling setVertex/setPrimitive and stuff like that.

Does this mean we could also potentially have a design where the vertex and primitive arrays and counts are returned by the mesh shader? It seemed to me like much of this stuff would work just as well as new @builtin kinds:

struct MeshOutput {
    @builtin(vertex_count) num_vertices: u32,
    @builtin(primitive_count) num_primitives: u32,
    @builtin(vertices) vertices: array<Vertex, 10>,
    @builtin(primitives) primitives: array<Primitive, 15>,
}

@mesh @workgroup_size(15, 1, 1)
fn mesh_shader(
    @builtin(local_invocation_index) invocation: u32,
    @builtin(task_payload) payload: Payload
) -> MeshOutput {
    ...
}

@inner-daemons
Copy link
Collaborator Author

inner-daemons commented Oct 17, 2025

@jimblandy Me and @cwfitzgerald agreed that mesh shader outputs should always be buffered, so I'm gonna remove references in the spec to having to call setMeshOutputs exactly once before calling setVertex/setPrimitive and stuff like that.

Does this mean we could also potentially have a design where the vertex and primitive arrays and counts are returned by the mesh shader? It seemed to me like much of this stuff would work just as well as new @builtin kinds:

struct MeshOutput {

    @builtin(vertex_count) num_vertices: u32,

    @builtin(primitive_count) num_primitives: u32,

    @builtin(vertices) vertices: array<Vertex, 10>,

    @builtin(primitives) primitives: array<Primitive, 15>,

}



@mesh @workgroup_size(15, 1, 1)

fn mesh_shader(

    @builtin(local_invocation_index) invocation: u32,

    @builtin(task_payload) payload: Payload

) -> MeshOutput {

    ...

}

Err, that's actually really clever. I do think that this isn't right though, since the thing output is per workgroup and this would have each thread return a giant structure, that would in theory be the same for all threads anyway. I'm not opposed to something closer to this though, like a global thread group variable that holds the buffered outputs and then the mesh shader can declare that it uses that global variable.

@inner-daemons
Copy link
Collaborator Author

inner-daemons commented Oct 17, 2025

Ok, I've thought about it some more. I think that we can't really avoid buffering for another reason: the output format isn't guaranteed to match whatever the naga struct is. SPIR-V for example has arcane rules on which values must be in different output arrays. HLSL is very similar. I still don't think we should have the task payload/mesh outputs be per-thread, but a small tweak to your proposal would be as follows:

struct MeshOutput {
    @builtin(vertex_count) num_vertices: u32,
    @builtin(primitive_count) num_primitives: u32,
    @builtin(vertices) vertices: array<Vertex, 10>,
    @builtin(primitives) primitives: array<Primitive, 15>,
}

var<mesh_output> output: MeshOutput;
var<task_payload> task_payload: Payload;

@mesh(output) @workgroup_size(15, 1, 1)
fn mesh_shader(
    @builtin(local_invocation_index) invocation: u32,
) {
    ...
}

Then we would just need to allow these @builtin attributes on fields in a mesh_output variable. This variable would then probably function basically the same as task_payload does in task shaders, where only 1 can be used by a task shader and it must match what was declared, and other stages can't access such a global variable.

@jimblandy
Copy link
Member

jimblandy commented Oct 18, 2025

@inner-daemons Have the commits I've pushed been looking okay?

@inner-daemons
Copy link
Collaborator Author

@jimblandy They look great! I very much appreciate them.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants