Skip to content

Conversation

@inner-daemons
Copy link
Collaborator

@inner-daemons inner-daemons commented Nov 10, 2025

Connections
Depends on #8370
Works towards #7197
Closes #8003

Description
Add new limits and validation for mesh shaders

Testing
Same old, same old

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 and others added 30 commits August 14, 2025 12:53
@inner-daemons inner-daemons mentioned this pull request Nov 10, 2025
36 tasks
@inner-daemons inner-daemons mentioned this pull request Nov 10, 2025
6 tasks
@inner-daemons inner-daemons marked this pull request as ready for review November 10, 2025 23:35
InterpolationMismatch(Option<naga::Interpolation>),
#[error("Input sampling doesn't match provided {0:?}")]
SamplingMismatch(Option<naga::Sampling>),
#[error("Pipeline input has perprimitive: {expected} but shader declares perprimitive: {}", !expected)]
Copy link
Member

Choose a reason for hiding this comment

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

I expect this will be hard to understand as a user - what is a "perprimitive". I would be a bit more verbose here.

Comment on lines +336 to +341
#[error("Mesh shaders are limited to {limit} output vertices, but the shader has a maximum number of {value}")]
TooManyMeshVertices { limit: u32, value: u32 },
#[error("Mesh shaders are limited to {limit} output primitives, but the shader has a maximum number of {value}")]
TooManyMeshPrimitives { limit: u32, value: u32 },
#[error("Mesh or task shaders are limited to {limit} bytes of task payload, but the shader has a task payload of size {value}")]
TaskPayloadTooLarge { limit: u32, value: u32 },
Copy link
Member

Choose a reason for hiding this comment

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

I would explicitly name the limit that this is based on so they can see exactly which value they need to increment.

Comment on lines +347 to +348
#[error("Primitive index can only be used in a fragment shader if the preceding shader was a vertex shader or a mesh shader that writes to primitive index.
If a mesh shader writes to primitive index, it must be read by the fragment shader.")]
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
#[error("Primitive index can only be used in a fragment shader if the preceding shader was a vertex shader or a mesh shader that writes to primitive index.
If a mesh shader writes to primitive index, it must be read by the fragment shader.")]
#[error("Primitive index can only be used in a fragment shader if the preceding shader was a vertex shader or a mesh shader that writes to primitive index. \
If a mesh shader writes to primitive index, it must be read by the fragment shader.")]

Make sure you escape newlines in string literals.

This really needs to be two different errors. this is two separate situations: Fragment uses it, but geometry doesn't provide it; Mesh writes it, fragment doesn't consume it.

Comment on lines 1233 to 1265
let (
max_task_workgroup_total_count,
max_task_workgroups_per_dimension,
max_mesh_multiview_view_count,
max_task_mesh_workgroup_total_count,
max_task_mesh_workgroups_per_dimension,
max_task_invocations_per_workgroup,
max_task_invocations_per_dimension,
max_mesh_invocations_per_workgroup,
max_mesh_invocations_per_dimension,
max_task_payload_size,
max_mesh_output_vertices,
max_mesh_output_primitives,
max_mesh_output_layers,
max_mesh_multiview_view_count,
) = match self.mesh_shader {
Some(m) => (
m.max_task_work_group_total_count,
m.max_task_work_group_count.into_iter().min().unwrap(),
m.max_mesh_multiview_view_count,
m.max_task_work_group_total_count
.min(m.max_mesh_work_group_total_count),
m.max_task_work_group_count
.into_iter()
.chain(m.max_mesh_work_group_count)
.min()
.unwrap(),
m.max_task_work_group_invocations,
m.max_task_work_group_size.into_iter().min().unwrap(),
m.max_mesh_work_group_invocations,
m.max_mesh_work_group_size.into_iter().min().unwrap(),
m.max_task_payload_size,
m.max_mesh_output_vertices,
m.max_mesh_output_primitives,
m.max_mesh_output_layers,
m.max_mesh_multiview_view_count,
),
None => (0, 0, 0, 0),
None => (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
};
Copy link
Member

Choose a reason for hiding this comment

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

Okay this seems like a recipe for limit mixup disaster.

I would declare each variable at the outer scope, with let mut name = 0; then mutate it in the has-mesh-shader case, that way all the assignments have both the destination and source name right next to each other. This relies on two lists being in the same order.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Lmao good thinking


/// The maximum total value of x*y*z for a given `draw_mesh_tasks` command
pub max_task_workgroup_total_count: u32,
/// The maximum total value for a `RenderPass::draw_mesh_tasks(x, y, z)` operation.
Copy link
Member

Choose a reason for hiding this comment

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

Lets use the exact same wording as the compute shader limit.

/// The maximum total value of x*y*z for a given `draw_mesh_tasks` command
pub max_task_workgroup_total_count: u32,
/// The maximum total value for a `RenderPass::draw_mesh_tasks(x, y, z)` operation.
/// Also for task shader outputs. Defaults to 65535. Higher is "better".
Copy link
Member

Choose a reason for hiding this comment

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

What does this mean?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maximum number of mesh shader workgroups, either those dispatched by a draw_mesh_tasks or by a task workgroup.

Copy link
Member

Choose a reason for hiding this comment

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

    /// The maximum total value for a `RenderPass::draw_mesh_tasks(x, y, z)` operation or the `@builtin(mesh_task_size)` returned from a task shader. Defaults to 65535. Higher is "better".

Something like this

Comment on lines +711 to +713
// These are fundamentally different. It is very common for limits on mesh shaders to be much lower,
// so as to properly use the hardware, where task shaders are usually just emulated with compute
// shaders. Therefore, we should have different limits for mesh vs task shaders.
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure this is actually true. I think just explaining here that task shaders need different limits are fine.

max_task_workgroups_per_dimension: 256,
// llvmpipe reports 0 multiview count, which just means no multiview is allowed
max_mesh_multiview_view_count: 0,
max_task_mesh_workgroup_total_count: 65536,
Copy link
Member

Choose a reason for hiding this comment

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

If there's no limit on d3d12 - we should query that vulkan devices support so we can get a more concrete answer for the maximum.

inner-daemons and others added 2 commits November 26, 2025 11:46
Co-authored-by: Connor Fitzgerald <connorwadefitzgerald@gmail.com>
Co-authored-by: Connor Fitzgerald <connorwadefitzgerald@gmail.com>
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.

Limits & validation of mesh and task shader interface in wgpu-core

3 participants