use alloc::{
boxed::Box,
string::{String, ToString as _},
sync::Arc,
vec::Vec,
}; use core::fmt;
use arrayvec::ArrayVec; use hashbrown::{hash_map::Entry, HashSet}; use shader_io_deductions::{display_deductions_as_optional_list, MaxVertexShaderOutputDeduction}; use thiserror::Error; use wgt::{
error::{ErrorType, WebGpuError},
BindGroupLayoutEntry, BindingType,
};
// Most shaders will use a standard interface which is very large. // Passthrough shaders have a much smaller interface. No reason to // box the standard interface though. #[expect(clippy::large_enum_variant)] #[derive(Debug)] pubenum ShaderMetaData {
Interface(Interface),
Passthrough(PassthroughInterface),
} impl ShaderMetaData { pubfn interface(&self) -> Option<&Interface> { matchself { Self::Interface(i) => Some(i), Self::Passthrough(_) => None,
}
}
}
#[derive(Clone, Debug, Error)] #[non_exhaustive] pubenum BindingError { #[error("Binding is missing from the pipeline layout")]
Missing, #[error("Visibility flags don't include the shader stage")]
Invisible, #[error( "Type on the shader side ({shader:?}) does not match the pipeline binding ({binding:?})"
)]
WrongType {
binding: BindingTypeName,
shader: BindingTypeName,
}, #[error("Storage class {binding:?} doesn't match the shader {shader:?}")]
WrongAddressSpace {
binding: naga::AddressSpace,
shader: naga::AddressSpace,
}, #[error("Address space {space:?} is not a valid Buffer address space")]
WrongBufferAddressSpace { space: naga::AddressSpace }, #[error("Buffer structure size {buffer_size}, added to one element of an unbound array, if it's the last field, ended up greater than the given `min_binding_size`, which is {min_binding_size}")]
WrongBufferSize {
buffer_size: wgt::BufferSize,
min_binding_size: wgt::BufferSize,
}, #[error("View dimension {dim:?} (is array: {is_array}) doesn't match the binding {binding:?}")]
WrongTextureViewDimension {
dim: naga::ImageDimension,
is_array: bool,
binding: BindingType,
}, #[error("Texture class {binding:?} doesn't match the shader {shader:?}")]
WrongTextureClass {
binding: naga::ImageClass,
shader: naga::ImageClass,
}, #[error("Comparison flag doesn't match the shader")]
WrongSamplerComparison, #[error("Derived bind group layout type is not consistent between stages")]
InconsistentlyDerivedType, #[error("Texture format {0:?} is not supported for storage use")]
BadStorageFormat(wgt::TextureFormat),
}
#[derive(Clone, Debug, Error)] #[non_exhaustive] pubenum FilteringError { #[error("Integer textures can't be sampled with a filtering sampler")]
Integer, #[error("Non-filterable float textures can't be sampled with a filtering sampler")]
Float,
}
#[derive(Clone, Debug, Error)] #[non_exhaustive] pubenum InputError { #[error("Input is not provided by the earlier stage in the pipeline")]
Missing, #[error("Input type is not compatible with the provided {0}")]
WrongType(NumericType), #[error("Input interpolation doesn't match provided {0:?}")]
InterpolationMismatch(Option<naga::Interpolation>), #[error("Input sampling doesn't match provided {0:?}")]
SamplingMismatch(Option<naga::Sampling>), #[error("Pipeline input has per_primitive={pipeline_input}, but shader expects per_primitive={shader}")]
WrongPerPrimitive { pipeline_input: bool, shader: bool },
}
/// Errors produced when validating a programmable stage of a pipeline. #[derive(Clone, Debug, Error)] #[non_exhaustive] pubenum StageError { #[error(transparent)]
InvalidWorkgroupSize(#[from] InvalidWorkgroupSizeError), #[error("Unable to find entry point '{0}'")]
MissingEntryPoint(String), #[error("Shader global {0:?} is not available in the pipeline layout")]
Binding(naga::ResourceBinding, #[source] BindingError), #[error("Unable to filter the texture ({texture:?}) by the sampler ({sampler:?})")]
Filtering {
texture: naga::ResourceBinding,
sampler: naga::ResourceBinding, #[source]
error: FilteringError,
}, #[error("Location[{location}] {var} is not provided by the previous stage outputs")]
Input {
location: wgt::ShaderLocation,
var: InterfaceVar, #[source]
error: InputError,
}, #[error( "Unable to select an entry point: no entry point was found in the provided shader module"
)]
NoEntryPointFound, #[error( "Unable to select an entry point: \
multiple entry points were found in the provided shader module, \
but no entry point was specified"
)]
MultipleEntryPointsFound, #[error(transparent)]
InvalidResource(#[from] InvalidResourceError), #[error( "vertex shader output location Location[{location}] ({var}) exceeds the \
`max_inter_stage_shader_variables` limit ({}, 0-based){}", // NOTE: Remember: the limit is 0-based for indices.
limit - 1,
display_deductions_as_optional_list(deductions, |d| d.for_location())
)]
VertexOutputLocationTooLarge {
location: u32,
var: InterfaceVar,
limit: u32,
deductions: Vec<MaxVertexShaderOutputDeduction>,
}, #[error( "found {num_found} user-defined vertex shader output variables, which exceeds the \
`max_inter_stage_shader_variables` limit ({limit}){}",
display_deductions_as_optional_list(deductions, |d| d.for_variables())
)]
TooManyUserDefinedVertexOutputs {
num_found: u32,
limit: u32,
deductions: Vec<MaxVertexShaderOutputDeduction>,
}, #[error( "fragment shader input location Location[{location}] ({var}) exceeds the \
`max_inter_stage_shader_variables` limit ({}, 0-based){}", // NOTE: Remember: the limit is 0-based for indices.
limit - 1, // NOTE: WebGPU spec. validation for fragment inputs is expressed in terms of variables // (unlike vertex outputs), so we use `MaxFragmentShaderInputDeduction::for_variables` here // (and not a non-existent `for_locations`).
display_deductions_as_optional_list(deductions, |d| d.for_variables())
)]
FragmentInputLocationTooLarge {
location: u32,
var: InterfaceVar,
limit: u32,
deductions: Vec<MaxFragmentShaderInputDeduction>,
}, #[error( "found {num_found} user-defined fragment shader input variables, which exceeds the \
`max_inter_stage_shader_variables` limit ({limit}){}",
display_deductions_as_optional_list(deductions, |d| d.for_variables())
)]
TooManyUserDefinedFragmentInputs {
num_found: u32,
limit: u32,
deductions: Vec<MaxFragmentShaderInputDeduction>,
}, #[error( "Location[{location}] {var}'s index exceeds the `max_color_attachments` limit ({limit})"
)]
ColorAttachmentLocationTooLarge {
location: u32,
var: InterfaceVar,
limit: u32,
}, #[error("Mesh shaders are limited to {limit} output vertices by `Limits::max_mesh_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 by `Limits::max_mesh_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 by `Limits::max_task_payload_size`, but the shader has a task payload of size {value}")]
TaskPayloadTooLarge { limit: u32, value: u32 }, #[error("Mesh shader's task payload has size ({shader:?}), which doesn't match the payload declared in the task stage ({input:?})")]
TaskPayloadMustMatch {
input: Option<u32>,
shader: Option<u32>,
}, #[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.")]
InvalidPrimitiveIndex, #[error("If a mesh shader writes to primitive index, it must be read by the fragment shader.")]
MissingPrimitiveIndex, #[error("DrawId cannot be used in a mesh shader in a pipeline with a task shader")]
DrawIdError, #[error("Pipeline uses dual-source blending, but the shader does not support it")]
InvalidDualSourceBlending, #[error("Fragment shader writes depth, but pipeline does not have a depth attachment")]
MissingFragDepthAttachment, #[error("Per vertex fragment inputs can only be used in triangle primitive pipelines")]
PerVertexNotTriangles, #[error("Mesh shader pipelines must have primitive topology of TriangleList, LineList or PointList, and this must match with what the mesh shader declares.")]
MeshTopologyMismatch,
}
NumericType {
dim, //Note: Shader always sees data as int, uint, or float. // It doesn't know if the original is normalized in a tighter form.
scalar,
}
}
fn from_texture_format(format: wgt::TextureFormat) -> Self { use naga::{Scalar, VectorSize as Vs}; use wgt::TextureFormat as Tf;
NumericType {
dim, //Note: Shader always sees data as int, uint, or float. // It doesn't know if the original is normalized in a tighter form.
scalar,
}
}
/// Return true if the fragment `format` is covered by the provided `output`. pubfn check_texture_format(
format: wgt::TextureFormat,
output: &NumericType,
) -> Result<(), NumericType> { let nt = NumericType::from_texture_format(format); if nt.is_subtype_of(output) {
Ok(())
} else {
Err(nt)
}
}
pubenum BindingLayoutSource { /// The binding layout is derived from the pipeline layout. /// /// This will be filled in by the shader binding validation, as it iterates the shader's interfaces.
Derived(Box<ArrayVec<bgl::EntryMap, { hal::MAX_BIND_GROUPS }>>), /// The binding layout is provided by the user in BGLs. /// /// This will be validated against the shader's interfaces.
Provided(Arc<crate::binding_model::PipelineLayout>),
}
#[derive(Debug, Clone, Default)] pubstruct StageIo { pub varyings: FastHashMap<wgt::ShaderLocation, InterfaceVar>, /// This must match between mesh & task shaders pub task_payload_size: Option<u32>, /// Fragment shaders cannot input primitive index on mesh shaders that don't output it on DX12. /// Therefore, we track between shader stages if primitive index is written (or if vertex shader /// is used). /// /// This is Some if it was a mesh shader. pub primitive_index: Option<bool>,
}
impl Interface { fn populate(
list: &mut Vec<Varying>,
binding: Option<&naga::Binding>,
ty: naga::Handle<naga::Type>,
arena: &naga::UniqueArena<naga::Type>,
) { let numeric_ty = match arena[ty].inner {
naga::TypeInner::Scalar(scalar) => NumericType {
dim: NumericDimension::Scalar,
scalar,
},
naga::TypeInner::Vector { size, scalar } => NumericType {
dim: NumericDimension::Vector(size),
scalar,
},
naga::TypeInner::Matrix {
columns,
rows,
scalar,
} => NumericType {
dim: NumericDimension::Matrix(columns, rows),
scalar,
},
naga::TypeInner::Struct { ref members, .. } => { for member in members { Self::populate(list, member.binding.as_ref(), member.ty, arena);
} return;
}
naga::TypeInner::Array { base, size, stride } if matches!(
binding,
Some(naga::Binding::BuiltIn(naga::BuiltIn::ClipDistances)),
) =>
{ // NOTE: We should already have validated these in `naga`.
debug_assert_eq!(
&arena[base].inner,
&naga::TypeInner::Scalar(naga::Scalar::F32)
);
debug_assert_eq!(stride, 4);
let naga::ArraySize::Constant(array_size) = size else { // NOTE: Based on the // [spec](https://gpuweb.github.io/gpuweb/wgsl/#fixed-footprint-types): // // > The only valid use of a fixed-size array with an element count that is an // > override-expression that is not a const-expression is as a memory view in // > the workgroup address space.
unreachable!("non-constant array size for `clip_distances`")
}; let array_size = array_size.get();
list.push(Varying::BuiltIn(BuiltIn::ClipDistances { array_size })); return;
} ref other => { //Note: technically this should be at least `log::error`, but // the reality is - every shader coming from `glslc` outputs an array // of clip distances and hits this path :( // So we lower it to `log::debug` to be less annoying as // there's nothing the user can do about it.
log::debug!("Unexpected varying type: {other:?}"); return;
}
};
let immediate_size = naga::valid::ImmediateSlots::size_for_module(module);
letmut entry_points = FastHashMap::default();
entry_points.reserve(module.entry_points.len()); for (index, entry_point) in module.entry_points.iter().enumerate() { let info = info.get_entry_point(index); letmut ep = EntryPoint::default(); for arg in entry_point.function.arguments.iter() { Self::populate(&mut ep.inputs, arg.binding.as_ref(), arg.ty, &module.types);
} iflet Some(ref result) = entry_point.function.result { Self::populate(
&mut ep.outputs,
result.binding.as_ref(),
result.ty,
&module.types,
);
}
for (var_handle, var) in module.global_variables.iter() { let usage = info[var_handle]; if !usage.is_empty() && var.binding.is_some() {
ep.resources.push(resource_mapping[&var_handle]);
}
}
for key in info.sampling_set.iter() {
ep.sampling_pairs
.insert((resource_mapping[&key.image], resource_mapping[&key.sampler]));
}
ep.dual_source_blending = info.dual_source_blending;
ep.workgroup_size = entry_point.workgroup_size;
ep.immediate_slots_required = info.immediate_slots_used;
/// Among other things, this implements some validation logic defined by the WebGPU spec. at /// <https://www.w3.org/TR/webgpu/#abstract-opdef-validating-inter-stage-interfaces>. pubfn check_stage(
&self,
layouts: &mut BindingLayoutSource,
shader_binding_sizes: &mut FastHashMap<naga::ResourceBinding, wgt::BufferSize>,
entry_point_name: &str,
shader_stage: ShaderStageForValidation,
inputs: StageIo,
primitive_topology: Option<wgt::PrimitiveTopology>,
) -> Result<StageIo, StageError> { // Since a shader module can have multiple entry points with the same name, // we need to look for one with the right execution model. let pair = (shader_stage.to_naga(), entry_point_name.to_string()); let entry_point = matchself.entry_points.get(&pair) {
Some(some) => some,
None => return Err(StageError::MissingEntryPoint(pair.1)),
}; let (_, entry_point_name) = pair;
let stage_bit = shader_stage.to_wgt_bit();
// check resources visibility for &handle in entry_point.resources.iter() { let res = &self.resources[handle]; let result = 'err: { match layouts {
BindingLayoutSource::Provided(pipeline_layout) => { // update the required binding size for this buffer iflet ResourceType::Buffer { size } = res.ty { match shader_binding_sizes.entry(res.bind) {
Entry::Occupied(e) => {
*e.into_mut() = size.max(*e.get());
}
Entry::Vacant(e) => {
e.insert(size);
}
}
}
let Some(entry) =
pipeline_layout.get_bgl_entry(res.bind.group, res.bind.binding) else { break'err Err(BindingError::Missing);
};
if !entry.visibility.contains(stage_bit) { break'err Err(BindingError::Invisible);
}
res.check_binding_use(entry)
}
BindingLayoutSource::Derived(layouts) => { let Some(map) = layouts.get_mut(res.bind.group as usize) else { break'err Err(BindingError::Missing);
};
let ty = match res.derive_binding_type(
entry_point
.sampling_pairs
.iter()
.any(|&(im, _samp)| im == handle),
) {
Ok(ty) => ty,
Err(error) => break'err Err(error),
};
// Check the compatibility between textures and samplers // // We only need to do this if the binding layout is provided by the user, as derived // layouts will inherently be correctly tagged. iflet BindingLayoutSource::Provided(pipeline_layout) = layouts { for &(texture_handle, sampler_handle) in entry_point.sampling_pairs.iter() { let texture_bind = &self.resources[texture_handle].bind; let sampler_bind = &self.resources[sampler_handle].bind; let texture_layout = pipeline_layout
.get_bgl_entry(texture_bind.group, texture_bind.binding)
.unwrap(); let sampler_layout = pipeline_layout
.get_bgl_entry(sampler_bind.group, sampler_bind.binding)
.unwrap();
assert!(texture_layout.visibility.contains(stage_bit));
assert!(sampler_layout.visibility.contains(stage_bit));
let deductions = point_list_deduction
.into_iter()
.chain(clip_distance_deductions);
for deduction in deductions.clone() { // NOTE: Deductions, in the current version of the spec. we implement, do not // ever exceed the minimum variables available.
max_vertex_shader_output_variables = max_vertex_shader_output_variables
.checked_sub(deduction.for_variables())
.unwrap();
max_vertex_shader_output_location = max_vertex_shader_output_location
.checked_sub(deduction.for_location())
.unwrap();
}
letmut num_user_defined_outputs = 0;
for output in entry_point.outputs.iter() { match *output {
Varying::Local { ref iv, location } => { if location > max_vertex_shader_output_location { return Err(StageError::VertexOutputLocationTooLarge {
location,
var: iv.clone(),
limit: self.limits.max_inter_stage_shader_variables,
deductions: deductions.collect(),
});
}
num_user_defined_outputs += 1;
}
Varying::BuiltIn(_) => {}
};
iflet Some(
cmp @ wgt::CompareFunction::Equal | cmp @ wgt::CompareFunction::NotEqual,
) = compare_function
{ iflet Varying::BuiltIn(BuiltIn::Position { invariant: false }) = *output {
log::warn!(
concat!( "Vertex shader with entry point {} outputs a ", "@builtin(position) without the @invariant attribute and ", "is used in a pipeline with {cmp:?}. On some machines, ", "this can cause bad artifacting as {cmp:?} assumes the ", "values output from the vertex shader exactly match the ", "value in the depth buffer. The @invariant attribute on the ", "@builtin(position) vertex output ensures that the exact ", "same pixel depths are used every render."
),
entry_point_name,
cmp = cmp
);
}
}
}
let deductions = entry_point.inputs.iter().filter_map(|output| match output {
Varying::Local { .. } => None,
Varying::BuiltIn(builtin) => {
MaxFragmentShaderInputDeduction::from_inter_stage_builtin(builtin.to_naga())
.or_else(|| {
unreachable!(
concat!( "unexpected built-in provided; ", "{:?} is not used for fragment stage input",
),
builtin
)
})
}
});
for deduction in deductions.clone() { // NOTE: Deductions, in the current version of the spec. we implement, do not // ever exceed the minimum variables available.
max_fragment_shader_input_variables = max_fragment_shader_input_variables
.checked_sub(deduction.for_variables())
.unwrap();
}
letmut num_user_defined_inputs = 0;
for output in entry_point.inputs.iter() { match *output {
Varying::Local { ref iv, location } => { if location >= self.limits.max_inter_stage_shader_variables { return Err(StageError::FragmentInputLocationTooLarge {
location,
var: iv.clone(),
limit: self.limits.max_inter_stage_shader_variables,
deductions: deductions.collect(),
});
}
num_user_defined_inputs += 1;
}
Varying::BuiltIn(_) => {}
};
}
for output in &entry_point.outputs { let &Varying::Local { location, ref iv } = output else { continue;
}; if location >= self.limits.max_color_attachments { return Err(StageError::ColorAttachmentLocationTooLarge {
location,
var: iv.clone(),
limit: self.limits.max_color_attachments,
});
}
}
// If the pipeline uses dual-source blending, then the shader // must configure appropriate I/O, but it is not an error to // use a shader that defines the I/O in a pipeline that only // uses one blend source. if dual_source_blending && !entry_point.dual_source_blending { return Err(StageError::InvalidDualSourceBlending);
}
if entry_point
.outputs
.contains(&Varying::BuiltIn(BuiltIn::FragDepth))
&& !has_depth_attachment
{ return Err(StageError::MissingFragDepthAttachment);
}
}
ShaderStageForValidation::Mesh => { for output in &entry_point.outputs { if matches!(output, Varying::BuiltIn(BuiltIn::PrimitiveIndex)) {
this_stage_primitive_index = true;
}
}
}
_ => (),
}
/// Validate a list of color attachment formats against `maxColorAttachmentBytesPerSample`. /// /// The color attachments can be from a render pass descriptor or a pipeline descriptor. /// /// Implements <https://gpuweb.github.io/gpuweb/#abstract-opdef-calculating-color-attachment-bytes-per-sample>. pubfn validate_color_attachment_bytes_per_sample(
attachment_formats: impl IntoIterator<Item = wgt::TextureFormat>,
limit: u32,
) -> Result<(), ColorAttachmentError> { letmut total_bytes_per_sample: u32 = 0; for format in attachment_formats { let byte_cost = format.target_pixel_byte_cost().unwrap(); let alignment = format.target_component_alignment().unwrap();
#[derive(Clone, Debug, Error)] pubenum InvalidWorkgroupSizeError { #[error( "Workgroup size {dimensions:?} ({total} total invocations) must be less or equal to \
the per-dimension limit `Limits::{per_dimension_limits_desc}` of {per_dimension_limits:?} \
and the total invocation limit `Limits::{total_limit_desc}` of {total_limit}"
)]
LimitExceeded {
dimensions: [u32; 3],
per_dimension_limits: [u32; 3],
per_dimension_limits_desc: &'static str,
total: u32,
total_limit: u32,
total_limit_desc: &'static str,
}, #[error("Workgroup sizes {dimensions:?} must be positive")]
Zero { dimensions: [u32; 3] },
}
/// Check X/Y/Z workgroup sizes against per-dimension and overall limits. /// /// This function does not check that the sizes are non-zero. In a dispatch, it is legal for /// the size to be zero. In shader or pipeline creation, it is an error for the size to be /// zero, and the caller must check that. pub(crate) fn check_workgroup_sizes(
sizes: &[u32; 3],
per_dimension_limits: &[u32; 3],
per_dimension_limits_desc: &'static str,
total_limit: u32,
total_limit_desc: &'static str,
) -> Result<u32, InvalidWorkgroupSizeError> { let total = sizes
.iter()
.fold(1u32, |total, &dim| total.saturating_mul(dim));
let invalid_total_invocations = total > total_limit;
let dimension_too_large = sizes
.iter()
.zip(per_dimension_limits.iter())
.any(|(dim, limit)| dim > limit);
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.