diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /third_party/rust/wgpu-core/src/validation.rs | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/wgpu-core/src/validation.rs')
-rw-r--r-- | third_party/rust/wgpu-core/src/validation.rs | 1248 |
1 files changed, 1248 insertions, 0 deletions
diff --git a/third_party/rust/wgpu-core/src/validation.rs b/third_party/rust/wgpu-core/src/validation.rs new file mode 100644 index 0000000000..a0947ae83f --- /dev/null +++ b/third_party/rust/wgpu-core/src/validation.rs @@ -0,0 +1,1248 @@ +use crate::{device::bgl, FastHashMap, FastHashSet}; +use arrayvec::ArrayVec; +use std::{collections::hash_map::Entry, fmt}; +use thiserror::Error; +use wgt::{BindGroupLayoutEntry, BindingType}; + +#[derive(Debug)] +enum ResourceType { + Buffer { + size: wgt::BufferSize, + }, + Texture { + dim: naga::ImageDimension, + arrayed: bool, + class: naga::ImageClass, + }, + Sampler { + comparison: bool, + }, +} + +#[derive(Debug)] +struct Resource { + #[allow(unused)] + name: Option<String>, + bind: naga::ResourceBinding, + ty: ResourceType, + class: naga::AddressSpace, +} + +#[derive(Clone, Copy, Debug)] +enum NumericDimension { + Scalar, + Vector(naga::VectorSize), + Matrix(naga::VectorSize, naga::VectorSize), +} + +impl fmt::Display for NumericDimension { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Self::Scalar => write!(f, ""), + Self::Vector(size) => write!(f, "x{}", size as u8), + Self::Matrix(columns, rows) => write!(f, "x{}{}", columns as u8, rows as u8), + } + } +} + +impl NumericDimension { + fn num_components(&self) -> u32 { + match *self { + Self::Scalar => 1, + Self::Vector(size) => size as u32, + Self::Matrix(w, h) => w as u32 * h as u32, + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct NumericType { + dim: NumericDimension, + scalar: naga::Scalar, +} + +impl fmt::Display for NumericType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{:?}{}{}", + self.scalar.kind, + self.scalar.width * 8, + self.dim + ) + } +} + +#[derive(Clone, Debug)] +pub struct InterfaceVar { + pub ty: NumericType, + interpolation: Option<naga::Interpolation>, + sampling: Option<naga::Sampling>, +} + +impl InterfaceVar { + pub fn vertex_attribute(format: wgt::VertexFormat) -> Self { + InterfaceVar { + ty: NumericType::from_vertex_format(format), + interpolation: None, + sampling: None, + } + } +} + +impl fmt::Display for InterfaceVar { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{} interpolated as {:?} with sampling {:?}", + self.ty, self.interpolation, self.sampling + ) + } +} + +#[derive(Debug)] +enum Varying { + Local { location: u32, iv: InterfaceVar }, + BuiltIn(naga::BuiltIn), +} + +#[allow(unused)] +#[derive(Debug)] +struct SpecializationConstant { + id: u32, + ty: NumericType, +} + +#[derive(Debug, Default)] +struct EntryPoint { + inputs: Vec<Varying>, + outputs: Vec<Varying>, + resources: Vec<naga::Handle<Resource>>, + #[allow(unused)] + spec_constants: Vec<SpecializationConstant>, + sampling_pairs: FastHashSet<(naga::Handle<Resource>, naga::Handle<Resource>)>, + workgroup_size: [u32; 3], + dual_source_blending: bool, +} + +#[derive(Debug)] +pub struct Interface { + limits: wgt::Limits, + features: wgt::Features, + resources: naga::Arena<Resource>, + entry_points: FastHashMap<(naga::ShaderStage, String), EntryPoint>, +} + +#[derive(Clone, Debug, Error)] +#[error("Buffer usage is {actual:?} which does not contain required usage {expected:?}")] +pub struct MissingBufferUsageError { + pub(crate) actual: wgt::BufferUsages, + pub(crate) expected: wgt::BufferUsages, +} + +/// Checks that the given buffer usage contains the required buffer usage, +/// returns an error otherwise. +pub fn check_buffer_usage( + actual: wgt::BufferUsages, + expected: wgt::BufferUsages, +) -> Result<(), MissingBufferUsageError> { + if !actual.contains(expected) { + Err(MissingBufferUsageError { actual, expected }) + } else { + Ok(()) + } +} + +#[derive(Clone, Debug, Error)] +#[error("Texture usage is {actual:?} which does not contain required usage {expected:?}")] +pub struct MissingTextureUsageError { + pub(crate) actual: wgt::TextureUsages, + pub(crate) expected: wgt::TextureUsages, +} + +/// Checks that the given texture usage contains the required texture usage, +/// returns an error otherwise. +pub fn check_texture_usage( + actual: wgt::TextureUsages, + expected: wgt::TextureUsages, +) -> Result<(), MissingTextureUsageError> { + if !actual.contains(expected) { + Err(MissingTextureUsageError { actual, expected }) + } else { + Ok(()) + } +} + +#[derive(Clone, Debug, Error)] +#[non_exhaustive] +pub enum 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 does not match the pipeline binding")] + WrongType, + #[error("Storage class {binding:?} doesn't match the shader {shader:?}")] + WrongAddressSpace { + binding: naga::AddressSpace, + shader: naga::AddressSpace, + }, + #[error("Buffer structure size {0}, added to one element of an unbound array, if it's the last field, ended up greater than the given `min_binding_size`")] + WrongBufferSize(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), + #[error( + "Storage texture with access {0:?} doesn't have a matching supported `StorageTextureAccess`" + )] + UnsupportedTextureStorageAccess(naga::StorageAccess), +} + +#[derive(Clone, Debug, Error)] +#[non_exhaustive] +pub enum 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] +pub enum 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>), +} + +/// Errors produced when validating a programmable stage of a pipeline. +#[derive(Clone, Debug, Error)] +#[non_exhaustive] +pub enum StageError { + #[error("Shader module is invalid")] + InvalidModule, + #[error( + "Shader entry point's workgroup size {current:?} ({current_total} total invocations) must be less or equal to the per-dimension limit {limit:?} and the total invocation limit {total}" + )] + InvalidWorkgroupSize { + current: [u32; 3], + current_total: u32, + limit: [u32; 3], + total: u32, + }, + #[error("Shader uses {used} inter-stage components above the limit of {limit}")] + TooManyVaryings { used: u32, limit: u32 }, + #[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("Location[{location}] is provided by the previous stage output but is not consumed as input by this stage.")] + InputNotConsumed { location: wgt::ShaderLocation }, +} + +fn map_storage_format_to_naga(format: wgt::TextureFormat) -> Option<naga::StorageFormat> { + use naga::StorageFormat as Sf; + use wgt::TextureFormat as Tf; + + Some(match format { + Tf::R8Unorm => Sf::R8Unorm, + Tf::R8Snorm => Sf::R8Snorm, + Tf::R8Uint => Sf::R8Uint, + Tf::R8Sint => Sf::R8Sint, + + Tf::R16Uint => Sf::R16Uint, + Tf::R16Sint => Sf::R16Sint, + Tf::R16Float => Sf::R16Float, + Tf::Rg8Unorm => Sf::Rg8Unorm, + Tf::Rg8Snorm => Sf::Rg8Snorm, + Tf::Rg8Uint => Sf::Rg8Uint, + Tf::Rg8Sint => Sf::Rg8Sint, + + Tf::R32Uint => Sf::R32Uint, + Tf::R32Sint => Sf::R32Sint, + Tf::R32Float => Sf::R32Float, + Tf::Rg16Uint => Sf::Rg16Uint, + Tf::Rg16Sint => Sf::Rg16Sint, + Tf::Rg16Float => Sf::Rg16Float, + Tf::Rgba8Unorm => Sf::Rgba8Unorm, + Tf::Rgba8Snorm => Sf::Rgba8Snorm, + Tf::Rgba8Uint => Sf::Rgba8Uint, + Tf::Rgba8Sint => Sf::Rgba8Sint, + Tf::Bgra8Unorm => Sf::Bgra8Unorm, + + Tf::Rgb10a2Uint => Sf::Rgb10a2Uint, + Tf::Rgb10a2Unorm => Sf::Rgb10a2Unorm, + Tf::Rg11b10Float => Sf::Rg11b10Float, + + Tf::Rg32Uint => Sf::Rg32Uint, + Tf::Rg32Sint => Sf::Rg32Sint, + Tf::Rg32Float => Sf::Rg32Float, + Tf::Rgba16Uint => Sf::Rgba16Uint, + Tf::Rgba16Sint => Sf::Rgba16Sint, + Tf::Rgba16Float => Sf::Rgba16Float, + + Tf::Rgba32Uint => Sf::Rgba32Uint, + Tf::Rgba32Sint => Sf::Rgba32Sint, + Tf::Rgba32Float => Sf::Rgba32Float, + + Tf::R16Unorm => Sf::R16Unorm, + Tf::R16Snorm => Sf::R16Snorm, + Tf::Rg16Unorm => Sf::Rg16Unorm, + Tf::Rg16Snorm => Sf::Rg16Snorm, + Tf::Rgba16Unorm => Sf::Rgba16Unorm, + Tf::Rgba16Snorm => Sf::Rgba16Snorm, + + _ => return None, + }) +} + +fn map_storage_format_from_naga(format: naga::StorageFormat) -> wgt::TextureFormat { + use naga::StorageFormat as Sf; + use wgt::TextureFormat as Tf; + + match format { + Sf::R8Unorm => Tf::R8Unorm, + Sf::R8Snorm => Tf::R8Snorm, + Sf::R8Uint => Tf::R8Uint, + Sf::R8Sint => Tf::R8Sint, + + Sf::R16Uint => Tf::R16Uint, + Sf::R16Sint => Tf::R16Sint, + Sf::R16Float => Tf::R16Float, + Sf::Rg8Unorm => Tf::Rg8Unorm, + Sf::Rg8Snorm => Tf::Rg8Snorm, + Sf::Rg8Uint => Tf::Rg8Uint, + Sf::Rg8Sint => Tf::Rg8Sint, + + Sf::R32Uint => Tf::R32Uint, + Sf::R32Sint => Tf::R32Sint, + Sf::R32Float => Tf::R32Float, + Sf::Rg16Uint => Tf::Rg16Uint, + Sf::Rg16Sint => Tf::Rg16Sint, + Sf::Rg16Float => Tf::Rg16Float, + Sf::Rgba8Unorm => Tf::Rgba8Unorm, + Sf::Rgba8Snorm => Tf::Rgba8Snorm, + Sf::Rgba8Uint => Tf::Rgba8Uint, + Sf::Rgba8Sint => Tf::Rgba8Sint, + Sf::Bgra8Unorm => Tf::Bgra8Unorm, + + Sf::Rgb10a2Uint => Tf::Rgb10a2Uint, + Sf::Rgb10a2Unorm => Tf::Rgb10a2Unorm, + Sf::Rg11b10Float => Tf::Rg11b10Float, + + Sf::Rg32Uint => Tf::Rg32Uint, + Sf::Rg32Sint => Tf::Rg32Sint, + Sf::Rg32Float => Tf::Rg32Float, + Sf::Rgba16Uint => Tf::Rgba16Uint, + Sf::Rgba16Sint => Tf::Rgba16Sint, + Sf::Rgba16Float => Tf::Rgba16Float, + + Sf::Rgba32Uint => Tf::Rgba32Uint, + Sf::Rgba32Sint => Tf::Rgba32Sint, + Sf::Rgba32Float => Tf::Rgba32Float, + + Sf::R16Unorm => Tf::R16Unorm, + Sf::R16Snorm => Tf::R16Snorm, + Sf::Rg16Unorm => Tf::Rg16Unorm, + Sf::Rg16Snorm => Tf::Rg16Snorm, + Sf::Rgba16Unorm => Tf::Rgba16Unorm, + Sf::Rgba16Snorm => Tf::Rgba16Snorm, + } +} + +impl Resource { + fn check_binding_use(&self, entry: &BindGroupLayoutEntry) -> Result<(), BindingError> { + match self.ty { + ResourceType::Buffer { size } => { + let min_size = match entry.ty { + BindingType::Buffer { + ty, + has_dynamic_offset: _, + min_binding_size, + } => { + let class = match ty { + wgt::BufferBindingType::Uniform => naga::AddressSpace::Uniform, + wgt::BufferBindingType::Storage { read_only } => { + let mut naga_access = naga::StorageAccess::LOAD; + naga_access.set(naga::StorageAccess::STORE, !read_only); + naga::AddressSpace::Storage { + access: naga_access, + } + } + }; + if self.class != class { + return Err(BindingError::WrongAddressSpace { + binding: class, + shader: self.class, + }); + } + min_binding_size + } + _ => return Err(BindingError::WrongType), + }; + match min_size { + Some(non_zero) if non_zero < size => { + return Err(BindingError::WrongBufferSize(size)) + } + _ => (), + } + } + ResourceType::Sampler { comparison } => match entry.ty { + BindingType::Sampler(ty) => { + if (ty == wgt::SamplerBindingType::Comparison) != comparison { + return Err(BindingError::WrongSamplerComparison); + } + } + _ => return Err(BindingError::WrongType), + }, + ResourceType::Texture { + dim, + arrayed, + class, + } => { + let view_dimension = match entry.ty { + BindingType::Texture { view_dimension, .. } + | BindingType::StorageTexture { view_dimension, .. } => view_dimension, + _ => { + return Err(BindingError::WrongTextureViewDimension { + dim, + is_array: false, + binding: entry.ty, + }) + } + }; + if arrayed { + match (dim, view_dimension) { + (naga::ImageDimension::D2, wgt::TextureViewDimension::D2Array) => (), + (naga::ImageDimension::Cube, wgt::TextureViewDimension::CubeArray) => (), + _ => { + return Err(BindingError::WrongTextureViewDimension { + dim, + is_array: true, + binding: entry.ty, + }) + } + } + } else { + match (dim, view_dimension) { + (naga::ImageDimension::D1, wgt::TextureViewDimension::D1) => (), + (naga::ImageDimension::D2, wgt::TextureViewDimension::D2) => (), + (naga::ImageDimension::D3, wgt::TextureViewDimension::D3) => (), + (naga::ImageDimension::Cube, wgt::TextureViewDimension::Cube) => (), + _ => { + return Err(BindingError::WrongTextureViewDimension { + dim, + is_array: false, + binding: entry.ty, + }) + } + } + } + let expected_class = match entry.ty { + BindingType::Texture { + sample_type, + view_dimension: _, + multisampled: multi, + } => match sample_type { + wgt::TextureSampleType::Float { .. } => naga::ImageClass::Sampled { + kind: naga::ScalarKind::Float, + multi, + }, + wgt::TextureSampleType::Sint => naga::ImageClass::Sampled { + kind: naga::ScalarKind::Sint, + multi, + }, + wgt::TextureSampleType::Uint => naga::ImageClass::Sampled { + kind: naga::ScalarKind::Uint, + multi, + }, + wgt::TextureSampleType::Depth => naga::ImageClass::Depth { multi }, + }, + BindingType::StorageTexture { + access, + format, + view_dimension: _, + } => { + let naga_format = map_storage_format_to_naga(format) + .ok_or(BindingError::BadStorageFormat(format))?; + let naga_access = match access { + wgt::StorageTextureAccess::ReadOnly => naga::StorageAccess::LOAD, + wgt::StorageTextureAccess::WriteOnly => naga::StorageAccess::STORE, + wgt::StorageTextureAccess::ReadWrite => naga::StorageAccess::all(), + }; + naga::ImageClass::Storage { + format: naga_format, + access: naga_access, + } + } + _ => return Err(BindingError::WrongType), + }; + if class != expected_class { + return Err(BindingError::WrongTextureClass { + binding: expected_class, + shader: class, + }); + } + } + }; + + Ok(()) + } + + fn derive_binding_type(&self) -> Result<BindingType, BindingError> { + Ok(match self.ty { + ResourceType::Buffer { size } => BindingType::Buffer { + ty: match self.class { + naga::AddressSpace::Uniform => wgt::BufferBindingType::Uniform, + naga::AddressSpace::Storage { access } => wgt::BufferBindingType::Storage { + read_only: access == naga::StorageAccess::LOAD, + }, + _ => return Err(BindingError::WrongType), + }, + has_dynamic_offset: false, + min_binding_size: Some(size), + }, + ResourceType::Sampler { comparison } => BindingType::Sampler(if comparison { + wgt::SamplerBindingType::Comparison + } else { + wgt::SamplerBindingType::Filtering + }), + ResourceType::Texture { + dim, + arrayed, + class, + } => { + let view_dimension = match dim { + naga::ImageDimension::D1 => wgt::TextureViewDimension::D1, + naga::ImageDimension::D2 if arrayed => wgt::TextureViewDimension::D2Array, + naga::ImageDimension::D2 => wgt::TextureViewDimension::D2, + naga::ImageDimension::D3 => wgt::TextureViewDimension::D3, + naga::ImageDimension::Cube if arrayed => wgt::TextureViewDimension::CubeArray, + naga::ImageDimension::Cube => wgt::TextureViewDimension::Cube, + }; + match class { + naga::ImageClass::Sampled { multi, kind } => BindingType::Texture { + sample_type: match kind { + naga::ScalarKind::Float => { + wgt::TextureSampleType::Float { filterable: true } + } + naga::ScalarKind::Sint => wgt::TextureSampleType::Sint, + naga::ScalarKind::Uint => wgt::TextureSampleType::Uint, + naga::ScalarKind::AbstractInt + | naga::ScalarKind::AbstractFloat + | naga::ScalarKind::Bool => unreachable!(), + }, + view_dimension, + multisampled: multi, + }, + naga::ImageClass::Depth { multi } => BindingType::Texture { + sample_type: wgt::TextureSampleType::Depth, + view_dimension, + multisampled: multi, + }, + naga::ImageClass::Storage { format, access } => BindingType::StorageTexture { + access: { + const LOAD_STORE: naga::StorageAccess = naga::StorageAccess::all(); + match access { + naga::StorageAccess::LOAD => wgt::StorageTextureAccess::ReadOnly, + naga::StorageAccess::STORE => wgt::StorageTextureAccess::WriteOnly, + LOAD_STORE => wgt::StorageTextureAccess::ReadWrite, + _ => unreachable!(), + } + }, + view_dimension, + format: { + let f = map_storage_format_from_naga(format); + let original = map_storage_format_to_naga(f) + .ok_or(BindingError::BadStorageFormat(f))?; + debug_assert_eq!(format, original); + f + }, + }, + } + } + }) + } +} + +impl NumericType { + fn from_vertex_format(format: wgt::VertexFormat) -> Self { + use naga::{Scalar, VectorSize as Vs}; + use wgt::VertexFormat as Vf; + + let (dim, scalar) = match format { + Vf::Uint32 => (NumericDimension::Scalar, Scalar::U32), + Vf::Uint8x2 | Vf::Uint16x2 | Vf::Uint32x2 => { + (NumericDimension::Vector(Vs::Bi), Scalar::U32) + } + Vf::Uint32x3 => (NumericDimension::Vector(Vs::Tri), Scalar::U32), + Vf::Uint8x4 | Vf::Uint16x4 | Vf::Uint32x4 => { + (NumericDimension::Vector(Vs::Quad), Scalar::U32) + } + Vf::Sint32 => (NumericDimension::Scalar, Scalar::I32), + Vf::Sint8x2 | Vf::Sint16x2 | Vf::Sint32x2 => { + (NumericDimension::Vector(Vs::Bi), Scalar::I32) + } + Vf::Sint32x3 => (NumericDimension::Vector(Vs::Tri), Scalar::I32), + Vf::Sint8x4 | Vf::Sint16x4 | Vf::Sint32x4 => { + (NumericDimension::Vector(Vs::Quad), Scalar::I32) + } + Vf::Float32 => (NumericDimension::Scalar, Scalar::F32), + Vf::Unorm8x2 + | Vf::Snorm8x2 + | Vf::Unorm16x2 + | Vf::Snorm16x2 + | Vf::Float16x2 + | Vf::Float32x2 => (NumericDimension::Vector(Vs::Bi), Scalar::F32), + Vf::Float32x3 => (NumericDimension::Vector(Vs::Tri), Scalar::F32), + Vf::Unorm8x4 + | Vf::Snorm8x4 + | Vf::Unorm16x4 + | Vf::Snorm16x4 + | Vf::Float16x4 + | Vf::Float32x4 => (NumericDimension::Vector(Vs::Quad), Scalar::F32), + Vf::Float64 => (NumericDimension::Scalar, Scalar::F64), + Vf::Float64x2 => (NumericDimension::Vector(Vs::Bi), Scalar::F64), + Vf::Float64x3 => (NumericDimension::Vector(Vs::Tri), Scalar::F64), + Vf::Float64x4 => (NumericDimension::Vector(Vs::Quad), Scalar::F64), + }; + + 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; + + let (dim, scalar) = match format { + Tf::R8Unorm | Tf::R8Snorm | Tf::R16Float | Tf::R32Float => { + (NumericDimension::Scalar, Scalar::F32) + } + Tf::R8Uint | Tf::R16Uint | Tf::R32Uint => (NumericDimension::Scalar, Scalar::U32), + Tf::R8Sint | Tf::R16Sint | Tf::R32Sint => (NumericDimension::Scalar, Scalar::I32), + Tf::Rg8Unorm | Tf::Rg8Snorm | Tf::Rg16Float | Tf::Rg32Float => { + (NumericDimension::Vector(Vs::Bi), Scalar::F32) + } + Tf::Rg8Uint | Tf::Rg16Uint | Tf::Rg32Uint => { + (NumericDimension::Vector(Vs::Bi), Scalar::U32) + } + Tf::Rg8Sint | Tf::Rg16Sint | Tf::Rg32Sint => { + (NumericDimension::Vector(Vs::Bi), Scalar::I32) + } + Tf::R16Snorm | Tf::R16Unorm => (NumericDimension::Scalar, Scalar::F32), + Tf::Rg16Snorm | Tf::Rg16Unorm => (NumericDimension::Vector(Vs::Bi), Scalar::F32), + Tf::Rgba16Snorm | Tf::Rgba16Unorm => (NumericDimension::Vector(Vs::Quad), Scalar::F32), + Tf::Rgba8Unorm + | Tf::Rgba8UnormSrgb + | Tf::Rgba8Snorm + | Tf::Bgra8Unorm + | Tf::Bgra8UnormSrgb + | Tf::Rgb10a2Unorm + | Tf::Rgba16Float + | Tf::Rgba32Float => (NumericDimension::Vector(Vs::Quad), Scalar::F32), + Tf::Rgba8Uint | Tf::Rgba16Uint | Tf::Rgba32Uint | Tf::Rgb10a2Uint => { + (NumericDimension::Vector(Vs::Quad), Scalar::U32) + } + Tf::Rgba8Sint | Tf::Rgba16Sint | Tf::Rgba32Sint => { + (NumericDimension::Vector(Vs::Quad), Scalar::I32) + } + Tf::Rg11b10Float => (NumericDimension::Vector(Vs::Tri), Scalar::F32), + Tf::Stencil8 + | Tf::Depth16Unorm + | Tf::Depth32Float + | Tf::Depth32FloatStencil8 + | Tf::Depth24Plus + | Tf::Depth24PlusStencil8 => { + panic!("Unexpected depth format") + } + Tf::NV12 => panic!("Unexpected nv12 format"), + Tf::Rgb9e5Ufloat => (NumericDimension::Vector(Vs::Tri), Scalar::F32), + Tf::Bc1RgbaUnorm + | Tf::Bc1RgbaUnormSrgb + | Tf::Bc2RgbaUnorm + | Tf::Bc2RgbaUnormSrgb + | Tf::Bc3RgbaUnorm + | Tf::Bc3RgbaUnormSrgb + | Tf::Bc7RgbaUnorm + | Tf::Bc7RgbaUnormSrgb + | Tf::Etc2Rgb8A1Unorm + | Tf::Etc2Rgb8A1UnormSrgb + | Tf::Etc2Rgba8Unorm + | Tf::Etc2Rgba8UnormSrgb => (NumericDimension::Vector(Vs::Quad), Scalar::F32), + Tf::Bc4RUnorm | Tf::Bc4RSnorm | Tf::EacR11Unorm | Tf::EacR11Snorm => { + (NumericDimension::Scalar, Scalar::F32) + } + Tf::Bc5RgUnorm | Tf::Bc5RgSnorm | Tf::EacRg11Unorm | Tf::EacRg11Snorm => { + (NumericDimension::Vector(Vs::Bi), Scalar::F32) + } + Tf::Bc6hRgbUfloat | Tf::Bc6hRgbFloat | Tf::Etc2Rgb8Unorm | Tf::Etc2Rgb8UnormSrgb => { + (NumericDimension::Vector(Vs::Tri), Scalar::F32) + } + Tf::Astc { + block: _, + channel: _, + } => (NumericDimension::Vector(Vs::Quad), Scalar::F32), + }; + + 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 is_subtype_of(&self, other: &NumericType) -> bool { + if self.scalar.width > other.scalar.width { + return false; + } + if self.scalar.kind != other.scalar.kind { + return false; + } + match (self.dim, other.dim) { + (NumericDimension::Scalar, NumericDimension::Scalar) => true, + (NumericDimension::Scalar, NumericDimension::Vector(_)) => true, + (NumericDimension::Vector(s0), NumericDimension::Vector(s1)) => s0 <= s1, + (NumericDimension::Matrix(c0, r0), NumericDimension::Matrix(c1, r1)) => { + c0 == c1 && r0 == r1 + } + _ => false, + } + } + + fn is_compatible_with(&self, other: &NumericType) -> bool { + if self.scalar.kind != other.scalar.kind { + return false; + } + match (self.dim, other.dim) { + (NumericDimension::Scalar, NumericDimension::Scalar) => true, + (NumericDimension::Scalar, NumericDimension::Vector(_)) => true, + (NumericDimension::Vector(_), NumericDimension::Vector(_)) => true, + (NumericDimension::Matrix(..), NumericDimension::Matrix(..)) => true, + _ => false, + } + } +} + +/// Return true if the fragment `format` is covered by the provided `output`. +pub fn 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) + } +} + +pub enum BindingLayoutSource<'a> { + /// 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(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(ArrayVec<&'a bgl::EntryMap, { hal::MAX_BIND_GROUPS }>), +} + +impl<'a> BindingLayoutSource<'a> { + pub fn new_derived(limits: &wgt::Limits) -> Self { + let mut array = ArrayVec::new(); + for _ in 0..limits.max_bind_groups { + array.push(Default::default()); + } + BindingLayoutSource::Derived(array) + } +} + +pub type StageIo = FastHashMap<wgt::ShaderLocation, InterfaceVar>; + +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; + } + 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::warn` to be less annoying. + log::warn!("Unexpected varying type: {:?}", other); + return; + } + }; + + let varying = match binding { + Some(&naga::Binding::Location { + location, + interpolation, + sampling, + .. // second_blend_source + }) => Varying::Local { + location, + iv: InterfaceVar { + ty: numeric_ty, + interpolation, + sampling, + }, + }, + Some(&naga::Binding::BuiltIn(built_in)) => Varying::BuiltIn(built_in), + None => { + log::error!("Missing binding for a varying"); + return; + } + }; + list.push(varying); + } + + pub fn new( + module: &naga::Module, + info: &naga::valid::ModuleInfo, + limits: wgt::Limits, + features: wgt::Features, + ) -> Self { + let mut resources = naga::Arena::new(); + let mut resource_mapping = FastHashMap::default(); + for (var_handle, var) in module.global_variables.iter() { + let bind = match var.binding { + Some(ref br) => br.clone(), + _ => continue, + }; + let naga_ty = &module.types[var.ty].inner; + + let inner_ty = match *naga_ty { + naga::TypeInner::BindingArray { base, .. } => &module.types[base].inner, + ref ty => ty, + }; + + let ty = match *inner_ty { + naga::TypeInner::Image { + dim, + arrayed, + class, + } => ResourceType::Texture { + dim, + arrayed, + class, + }, + naga::TypeInner::Sampler { comparison } => ResourceType::Sampler { comparison }, + naga::TypeInner::Array { stride, .. } => ResourceType::Buffer { + size: wgt::BufferSize::new(stride as u64).unwrap(), + }, + ref other => ResourceType::Buffer { + size: wgt::BufferSize::new(other.size(module.to_ctx()) as u64).unwrap(), + }, + }; + let handle = resources.append( + Resource { + name: var.name.clone(), + bind, + ty, + class: var.space, + }, + Default::default(), + ); + resource_mapping.insert(var_handle, handle); + } + + let mut 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); + let mut ep = EntryPoint::default(); + for arg in entry_point.function.arguments.iter() { + Self::populate(&mut ep.inputs, arg.binding.as_ref(), arg.ty, &module.types); + } + if let 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; + + entry_points.insert((entry_point.stage, entry_point.name.clone()), ep); + } + + Self { + limits, + features, + resources, + entry_points, + } + } + + pub fn check_stage( + &self, + layouts: &mut BindingLayoutSource<'_>, + shader_binding_sizes: &mut FastHashMap<naga::ResourceBinding, wgt::BufferSize>, + entry_point_name: &str, + stage_bit: wgt::ShaderStages, + inputs: StageIo, + compare_function: Option<wgt::CompareFunction>, + ) -> 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 shader_stage = match stage_bit { + wgt::ShaderStages::VERTEX => naga::ShaderStage::Vertex, + wgt::ShaderStages::FRAGMENT => naga::ShaderStage::Fragment, + wgt::ShaderStages::COMPUTE => naga::ShaderStage::Compute, + _ => unreachable!(), + }; + let pair = (shader_stage, entry_point_name.to_string()); + let entry_point = self + .entry_points + .get(&pair) + .ok_or(StageError::MissingEntryPoint(pair.1))?; + + // check resources visibility + for &handle in entry_point.resources.iter() { + let res = &self.resources[handle]; + let result = 'err: { + match layouts { + BindingLayoutSource::Provided(layouts) => { + // update the required binding size for this buffer + if let ResourceType::Buffer { size } = res.ty { + match shader_binding_sizes.entry(res.bind.clone()) { + Entry::Occupied(e) => { + *e.into_mut() = size.max(*e.get()); + } + Entry::Vacant(e) => { + e.insert(size); + } + } + } + + let Some(map) = layouts.get(res.bind.group as usize) else { + break 'err Err(BindingError::Missing); + }; + + let Some(entry) = map.get(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() { + Ok(ty) => ty, + Err(error) => break 'err Err(error), + }; + + match map.entry(res.bind.binding) { + indexmap::map::Entry::Occupied(e) if e.get().ty != ty => { + break 'err Err(BindingError::InconsistentlyDerivedType) + } + indexmap::map::Entry::Occupied(e) => { + e.into_mut().visibility |= stage_bit; + } + indexmap::map::Entry::Vacant(e) => { + e.insert(BindGroupLayoutEntry { + binding: res.bind.binding, + ty, + visibility: stage_bit, + count: None, + }); + } + } + Ok(()) + } + } + }; + if let Err(error) = result { + return Err(StageError::Binding(res.bind.clone(), 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. + if let BindingLayoutSource::Provided(layouts) = 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 = layouts[texture_bind.group as usize] + .get(texture_bind.binding) + .unwrap(); + let sampler_layout = layouts[sampler_bind.group as usize] + .get(sampler_bind.binding) + .unwrap(); + assert!(texture_layout.visibility.contains(stage_bit)); + assert!(sampler_layout.visibility.contains(stage_bit)); + + let sampler_filtering = matches!( + sampler_layout.ty, + wgt::BindingType::Sampler(wgt::SamplerBindingType::Filtering) + ); + let texture_sample_type = match texture_layout.ty { + BindingType::Texture { sample_type, .. } => sample_type, + _ => unreachable!(), + }; + + let error = match (sampler_filtering, texture_sample_type) { + (true, wgt::TextureSampleType::Float { filterable: false }) => { + Some(FilteringError::Float) + } + (true, wgt::TextureSampleType::Sint) => Some(FilteringError::Integer), + (true, wgt::TextureSampleType::Uint) => Some(FilteringError::Integer), + _ => None, + }; + + if let Some(error) = error { + return Err(StageError::Filtering { + texture: texture_bind.clone(), + sampler: sampler_bind.clone(), + error, + }); + } + } + } + + // check workgroup size limits + if shader_stage == naga::ShaderStage::Compute { + let max_workgroup_size_limits = [ + self.limits.max_compute_workgroup_size_x, + self.limits.max_compute_workgroup_size_y, + self.limits.max_compute_workgroup_size_z, + ]; + let total_invocations = entry_point.workgroup_size.iter().product::<u32>(); + + if entry_point.workgroup_size.iter().any(|&s| s == 0) + || total_invocations > self.limits.max_compute_invocations_per_workgroup + || entry_point.workgroup_size[0] > max_workgroup_size_limits[0] + || entry_point.workgroup_size[1] > max_workgroup_size_limits[1] + || entry_point.workgroup_size[2] > max_workgroup_size_limits[2] + { + return Err(StageError::InvalidWorkgroupSize { + current: entry_point.workgroup_size, + current_total: total_invocations, + limit: max_workgroup_size_limits, + total: self.limits.max_compute_invocations_per_workgroup, + }); + } + } + + let mut inter_stage_components = 0; + + // check inputs compatibility + for input in entry_point.inputs.iter() { + match *input { + Varying::Local { location, ref iv } => { + let result = + inputs + .get(&location) + .ok_or(InputError::Missing) + .and_then(|provided| { + let (compatible, num_components) = match shader_stage { + // For vertex attributes, there are defaults filled out + // by the driver if data is not provided. + naga::ShaderStage::Vertex => { + // vertex inputs don't count towards inter-stage + (iv.ty.is_compatible_with(&provided.ty), 0) + } + naga::ShaderStage::Fragment => { + if iv.interpolation != provided.interpolation { + return Err(InputError::InterpolationMismatch( + provided.interpolation, + )); + } + if iv.sampling != provided.sampling { + return Err(InputError::SamplingMismatch( + provided.sampling, + )); + } + ( + iv.ty.is_subtype_of(&provided.ty), + iv.ty.dim.num_components(), + ) + } + naga::ShaderStage::Compute => (false, 0), + }; + if compatible { + Ok(num_components) + } else { + Err(InputError::WrongType(provided.ty)) + } + }); + match result { + Ok(num_components) => { + inter_stage_components += num_components; + } + Err(error) => { + return Err(StageError::Input { + location, + var: iv.clone(), + error, + }) + } + } + } + Varying::BuiltIn(_) => {} + } + } + + // Check all vertex outputs and make sure the fragment shader consumes them. + // This requirement is removed if the `SHADER_UNUSED_VERTEX_OUTPUT` feature is enabled. + if shader_stage == naga::ShaderStage::Fragment + && !self + .features + .contains(wgt::Features::SHADER_UNUSED_VERTEX_OUTPUT) + { + for &index in inputs.keys() { + // This is a linear scan, but the count should be low enough + // that this should be fine. + let found = entry_point.inputs.iter().any(|v| match *v { + Varying::Local { location, .. } => location == index, + Varying::BuiltIn(_) => false, + }); + + if !found { + return Err(StageError::InputNotConsumed { location: index }); + } + } + } + + if shader_stage == naga::ShaderStage::Vertex { + for output in entry_point.outputs.iter() { + //TODO: count builtins towards the limit? + inter_stage_components += match *output { + Varying::Local { ref iv, .. } => iv.ty.dim.num_components(), + Varying::BuiltIn(_) => 0, + }; + + if let Some( + cmp @ wgt::CompareFunction::Equal | cmp @ wgt::CompareFunction::NotEqual, + ) = compare_function + { + if let Varying::BuiltIn(naga::BuiltIn::Position { invariant: false }) = *output + { + log::warn!( + "Vertex shader with entry point {entry_point_name} 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." + ); + } + } + } + } + + if inter_stage_components > self.limits.max_inter_stage_shader_components { + return Err(StageError::TooManyVaryings { + used: inter_stage_components, + limit: self.limits.max_inter_stage_shader_components, + }); + } + + let outputs = entry_point + .outputs + .iter() + .filter_map(|output| match *output { + Varying::Local { location, ref iv } => Some((location, iv.clone())), + Varying::BuiltIn(_) => None, + }) + .collect(); + Ok(outputs) + } + + pub fn fragment_uses_dual_source_blending( + &self, + entry_point_name: &str, + ) -> Result<bool, StageError> { + let pair = (naga::ShaderStage::Fragment, entry_point_name.to_string()); + self.entry_points + .get(&pair) + .ok_or(StageError::MissingEntryPoint(pair.1)) + .map(|ep| ep.dual_source_blending) + } +} |