use alloc::{ borrow::{Cow, ToOwned}, boxed::Box, string::String, sync::{Arc, Weak}, vec::Vec, }; use core::{fmt, mem::ManuallyDrop, ops::Range}; use arrayvec::ArrayVec; use thiserror::Error; #[cfg(feature = "serde")] use serde::Deserialize; #[cfg(feature = "serde")] use serde::Serialize; use wgt::error::{ErrorType, WebGpuError}; use crate::{ device::{ bgl, Device, DeviceError, MissingDownlevelFlags, MissingFeatures, SHADER_STAGE_COUNT, }, id::{BindGroupLayoutId, BufferId, ExternalTextureId, SamplerId, TextureViewId, TlasId}, init_tracker::{BufferInitTrackerAction, TextureInitTrackerAction}, pipeline::{ComputePipeline, RenderPipeline}, resource::{ Buffer, DestroyedResourceError, ExternalTexture, InvalidResourceError, Labeled, MissingBufferUsageError, MissingTextureUsageError, RawResourceAccess, ResourceErrorIdent, Sampler, TextureView, Tlas, TrackingData, }, resource_log, snatch::{SnatchGuard, Snatchable}, track::{BindGroupStates, ResourceUsageCompatibilityError}, Label, }; #[derive(Clone, Debug, Error)] #[non_exhaustive] pub enum BindGroupLayoutEntryError { #[error("Cube dimension is not expected for texture storage")] StorageTextureCube, #[error("Atomic storage textures are not allowed by baseline webgpu, they require the native only feature TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES")] StorageTextureAtomic, #[error("Arrays of bindings unsupported for this type of binding")] ArrayUnsupported, #[error("Multisampled binding with sample type `TextureSampleType::Float` must have filterable set to false.")] SampleTypeFloatFilterableBindingMultisampled, #[error("Multisampled texture binding view dimension must be 2d, got {0:?}")] Non2DMultisampled(wgt::TextureViewDimension), #[error(transparent)] MissingFeatures(#[from] MissingFeatures), #[error(transparent)] MissingDownlevelFlags(#[from] MissingDownlevelFlags), } #[derive(Clone, Debug, Error)] #[non_exhaustive] pub enum CreateBindGroupLayoutError { #[error(transparent)] Device(#[from] DeviceError), #[error("Conflicting binding at index {0}")] ConflictBinding(u32), #[error("Binding {binding} entry is invalid")] Entry { binding: u32, #[source] error: BindGroupLayoutEntryError, }, #[error(transparent)] TooManyBindings(BindingTypeMaxCountError), #[error("Bind groups may not contain both a binding array and a dynamically offset buffer")] ContainsBothBindingArrayAndDynamicOffsetArray, #[error("Bind groups may not contain both a binding array and a uniform buffer")] ContainsBothBindingArrayAndUniformBuffer, #[error("Binding index {binding} is greater than the maximum number {maximum}")] InvalidBindingIndex { binding: u32, maximum: u32 }, #[error("Invalid visibility {0:?}")] InvalidVisibility(wgt::ShaderStages), } impl WebGpuError for CreateBindGroupLayoutError { fn webgpu_error_type(&self) -> ErrorType { match self { Self::Device(e) => e.webgpu_error_type(), Self::ConflictBinding(_) | Self::Entry { .. } | Self::TooManyBindings(_) | Self::InvalidBindingIndex { .. } | Self::InvalidVisibility(_) | Self::ContainsBothBindingArrayAndDynamicOffsetArray | Self::ContainsBothBindingArrayAndUniformBuffer => ErrorType::Validation, } } } #[derive(Clone, Debug, Error)] #[non_exhaustive] pub enum BindingError { #[error(transparent)] DestroyedResource(#[from] DestroyedResourceError), #[error("Buffer {buffer}: Binding with size {binding_size} at offset {offset} would overflow buffer size of {buffer_size}")] BindingRangeTooLarge { buffer: ResourceErrorIdent, offset: wgt::BufferAddress, binding_size: u64, buffer_size: u64, }, #[error("Buffer {buffer}: Binding offset {offset} is greater than buffer size {buffer_size}")] BindingOffsetTooLarge { buffer: ResourceErrorIdent, offset: wgt::BufferAddress, buffer_size: u64, }, } impl WebGpuError for BindingError { fn webgpu_error_type(&self) -> ErrorType { match self { Self::DestroyedResource(e) => e.webgpu_error_type(), Self::BindingRangeTooLarge { .. } | Self::BindingOffsetTooLarge { .. } => { ErrorType::Validation } } } } // TODO: there may be additional variants here that can be extracted into // `BindingError`. #[derive(Clone, Debug, Error)] #[non_exhaustive] pub enum CreateBindGroupError { #[error(transparent)] Device(#[from] DeviceError), #[error(transparent)] DestroyedResource(#[from] DestroyedResourceError), #[error(transparent)] BindingError(#[from] BindingError), #[error( "Binding count declared with at most {expected} items, but {actual} items were provided" )] BindingArrayPartialLengthMismatch { actual: usize, expected: usize }, #[error( "Binding count declared with exactly {expected} items, but {actual} items were provided" )] BindingArrayLengthMismatch { actual: usize, expected: usize }, #[error("Array binding provided zero elements")] BindingArrayZeroLength, #[error("Binding size {actual} of {buffer} is less than minimum {min}")] BindingSizeTooSmall { buffer: ResourceErrorIdent, actual: u64, min: u64, }, #[error("{0} binding size is zero")] BindingZeroSize(ResourceErrorIdent), #[error("Number of bindings in bind group descriptor ({actual}) does not match the number of bindings defined in the bind group layout ({expected})")] BindingsNumMismatch { actual: usize, expected: usize }, #[error("Binding {0} is used at least twice in the descriptor")] DuplicateBinding(u32), #[error("Unable to find a corresponding declaration for the given binding {0}")] MissingBindingDeclaration(u32), #[error(transparent)] MissingBufferUsage(#[from] MissingBufferUsageError), #[error(transparent)] MissingTextureUsage(#[from] MissingTextureUsageError), #[error("Binding declared as a single item, but bind group is using it as an array")] SingleBindingExpected, #[error("Effective buffer binding size {size} for storage buffers is expected to align to {alignment}, but size is {size}")] UnalignedEffectiveBufferBindingSizeForStorage { alignment: u8, size: u64 }, #[error("Buffer offset {0} does not respect device's requested `{1}` limit {2}")] UnalignedBufferOffset(wgt::BufferAddress, &'static str, u32), #[error( "Buffer binding {binding} range {given} exceeds `max_*_buffer_binding_size` limit {limit}" )] BufferRangeTooLarge { binding: u32, given: u32, limit: u32, }, #[error("Binding {binding} has a different type ({actual:?}) than the one in the layout ({expected:?})")] WrongBindingType { // Index of the binding binding: u32, // The type given to the function actual: wgt::BindingType, // Human-readable description of expected types expected: &'static str, }, #[error("Texture binding {binding} expects multisampled = {layout_multisampled}, but given a view with samples = {view_samples}")] InvalidTextureMultisample { binding: u32, layout_multisampled: bool, view_samples: u32, }, #[error( "Texture binding {} expects sample type {:?}, but was given a view with format {:?} (sample type {:?})", binding, layout_sample_type, view_format, view_sample_type )] InvalidTextureSampleType { binding: u32, layout_sample_type: wgt::TextureSampleType, view_format: wgt::TextureFormat, view_sample_type: wgt::TextureSampleType, }, #[error("Texture binding {binding} expects dimension = {layout_dimension:?}, but given a view with dimension = {view_dimension:?}")] InvalidTextureDimension { binding: u32, layout_dimension: wgt::TextureViewDimension, view_dimension: wgt::TextureViewDimension, }, #[error("Storage texture binding {binding} expects format = {layout_format:?}, but given a view with format = {view_format:?}")] InvalidStorageTextureFormat { binding: u32, layout_format: wgt::TextureFormat, view_format: wgt::TextureFormat, }, #[error("Storage texture bindings must have a single mip level, but given a view with mip_level_count = {mip_level_count:?} at binding {binding}")] InvalidStorageTextureMipLevelCount { binding: u32, mip_level_count: u32 }, #[error("External texture bindings must have a single mip level, but given a view with mip_level_count = {mip_level_count:?} at binding {binding}")] InvalidExternalTextureMipLevelCount { binding: u32, mip_level_count: u32 }, #[error("External texture bindings must have a format of `rgba8unorm`, `bgra8unorm`, or `rgba16float, but given a view with format = {format:?} at binding {binding}")] InvalidExternalTextureFormat { binding: u32, format: wgt::TextureFormat, }, #[error("Sampler binding {binding} expects comparison = {layout_cmp}, but given a sampler with comparison = {sampler_cmp}")] WrongSamplerComparison { binding: u32, layout_cmp: bool, sampler_cmp: bool, }, #[error("Sampler binding {binding} expects filtering = {layout_flt}, but given a sampler with filtering = {sampler_flt}")] WrongSamplerFiltering { binding: u32, layout_flt: bool, sampler_flt: bool, }, #[error("TLAS binding {binding} is required to support vertex returns but is missing flag AccelerationStructureFlags::ALLOW_RAY_HIT_VERTEX_RETURN")] MissingTLASVertexReturn { binding: u32 }, #[error("Bound texture views can not have both depth and stencil aspects enabled")] DepthStencilAspect, #[error("The adapter does not support read access for storage textures of format {0:?}")] StorageReadNotSupported(wgt::TextureFormat), #[error("The adapter does not support atomics for storage textures of format {0:?}")] StorageAtomicNotSupported(wgt::TextureFormat), #[error("The adapter does not support write access for storage textures of format {0:?}")] StorageWriteNotSupported(wgt::TextureFormat), #[error("The adapter does not support read-write access for storage textures of format {0:?}")] StorageReadWriteNotSupported(wgt::TextureFormat), #[error(transparent)] ResourceUsageCompatibility(#[from] ResourceUsageCompatibilityError), #[error(transparent)] InvalidResource(#[from] InvalidResourceError), } impl WebGpuError for CreateBindGroupError { fn webgpu_error_type(&self) -> ErrorType { let e: &dyn WebGpuError = match self { Self::Device(e) => e, Self::DestroyedResource(e) => e, Self::BindingError(e) => e, Self::MissingBufferUsage(e) => e, Self::MissingTextureUsage(e) => e, Self::ResourceUsageCompatibility(e) => e, Self::InvalidResource(e) => e, Self::BindingArrayPartialLengthMismatch { .. } | Self::BindingArrayLengthMismatch { .. } | Self::BindingArrayZeroLength | Self::BindingSizeTooSmall { .. } | Self::BindingsNumMismatch { .. } | Self::BindingZeroSize(_) | Self::DuplicateBinding(_) | Self::MissingBindingDeclaration(_) | Self::SingleBindingExpected | Self::UnalignedEffectiveBufferBindingSizeForStorage { .. } | Self::UnalignedBufferOffset(_, _, _) | Self::BufferRangeTooLarge { .. } | Self::WrongBindingType { .. } | Self::InvalidTextureMultisample { .. } | Self::InvalidTextureSampleType { .. } | Self::InvalidTextureDimension { .. } | Self::InvalidStorageTextureFormat { .. } | Self::InvalidStorageTextureMipLevelCount { .. } | Self::WrongSamplerComparison { .. } | Self::WrongSamplerFiltering { .. } | Self::DepthStencilAspect | Self::StorageReadNotSupported(_) | Self::StorageWriteNotSupported(_) | Self::StorageReadWriteNotSupported(_) | Self::StorageAtomicNotSupported(_) | Self::MissingTLASVertexReturn { .. } | Self::InvalidExternalTextureMipLevelCount { .. } | Self::InvalidExternalTextureFormat { .. } => return ErrorType::Validation, }; e.webgpu_error_type() } } #[derive(Clone, Debug, Error)] pub enum BindingZone { #[error("Stage {0:?}")] Stage(wgt::ShaderStages), #[error("Whole pipeline")] Pipeline, } #[derive(Clone, Debug, Error)] #[error("Too many bindings of type {kind:?} in {zone}, limit is {limit}, count was {count}. Check the limit `{}` passed to `Adapter::request_device`", .kind.to_config_str())] pub struct BindingTypeMaxCountError { pub kind: BindingTypeMaxCountErrorKind, pub zone: BindingZone, pub limit: u32, pub count: u32, } impl WebGpuError for BindingTypeMaxCountError { fn webgpu_error_type(&self) -> ErrorType { ErrorType::Validation } } #[derive(Clone, Debug)] pub enum BindingTypeMaxCountErrorKind { DynamicUniformBuffers, DynamicStorageBuffers, SampledTextures, Samplers, StorageBuffers, StorageTextures, UniformBuffers, BindingArrayElements, BindingArraySamplerElements, AccelerationStructures, } impl BindingTypeMaxCountErrorKind { fn to_config_str(&self) -> &'static str { match self { BindingTypeMaxCountErrorKind::DynamicUniformBuffers => { "max_dynamic_uniform_buffers_per_pipeline_layout" } BindingTypeMaxCountErrorKind::DynamicStorageBuffers => { "max_dynamic_storage_buffers_per_pipeline_layout" } BindingTypeMaxCountErrorKind::SampledTextures => { "max_sampled_textures_per_shader_stage" } BindingTypeMaxCountErrorKind::Samplers => "max_samplers_per_shader_stage", BindingTypeMaxCountErrorKind::StorageBuffers => "max_storage_buffers_per_shader_stage", BindingTypeMaxCountErrorKind::StorageTextures => { "max_storage_textures_per_shader_stage" } BindingTypeMaxCountErrorKind::UniformBuffers => "max_uniform_buffers_per_shader_stage", BindingTypeMaxCountErrorKind::BindingArrayElements => { "max_binding_array_elements_per_shader_stage" } BindingTypeMaxCountErrorKind::BindingArraySamplerElements => { "max_binding_array_sampler_elements_per_shader_stage" } BindingTypeMaxCountErrorKind::AccelerationStructures => { "max_acceleration_structures_per_shader_stage" } } } } #[derive(Debug, Default)] pub(crate) struct PerStageBindingTypeCounter { vertex: u32, fragment: u32, compute: u32, } impl PerStageBindingTypeCounter { pub(crate) fn add(&mut self, stage: wgt::ShaderStages, count: u32) { if stage.contains(wgt::ShaderStages::VERTEX) { self.vertex += count; } if stage.contains(wgt::ShaderStages::FRAGMENT) { self.fragment += count; } if stage.contains(wgt::ShaderStages::COMPUTE) { self.compute += count; } } pub(crate) fn max(&self) -> (BindingZone, u32) { let max_value = self.vertex.max(self.fragment.max(self.compute)); let mut stage = wgt::ShaderStages::NONE; if max_value == self.vertex { stage |= wgt::ShaderStages::VERTEX } if max_value == self.fragment { stage |= wgt::ShaderStages::FRAGMENT } if max_value == self.compute { stage |= wgt::ShaderStages::COMPUTE } (BindingZone::Stage(stage), max_value) } pub(crate) fn merge(&mut self, other: &Self) { self.vertex = self.vertex.max(other.vertex); self.fragment = self.fragment.max(other.fragment); self.compute = self.compute.max(other.compute); } pub(crate) fn validate( &self, limit: u32, kind: BindingTypeMaxCountErrorKind, ) -> Result<(), BindingTypeMaxCountError> { let (zone, count) = self.max(); if limit < count { Err(BindingTypeMaxCountError { kind, zone, limit, count, }) } else { Ok(()) } } } #[derive(Debug, Default)] pub(crate) struct BindingTypeMaxCountValidator { dynamic_uniform_buffers: u32, dynamic_storage_buffers: u32, sampled_textures: PerStageBindingTypeCounter, samplers: PerStageBindingTypeCounter, storage_buffers: PerStageBindingTypeCounter, storage_textures: PerStageBindingTypeCounter, uniform_buffers: PerStageBindingTypeCounter, acceleration_structures: PerStageBindingTypeCounter, binding_array_elements: PerStageBindingTypeCounter, binding_array_sampler_elements: PerStageBindingTypeCounter, has_bindless_array: bool, } impl BindingTypeMaxCountValidator { pub(crate) fn add_binding(&mut self, binding: &wgt::BindGroupLayoutEntry) { let count = binding.count.map_or(1, |count| count.get()); if binding.count.is_some() { self.binding_array_elements.add(binding.visibility, count); self.has_bindless_array = true; if let wgt::BindingType::Sampler(_) = binding.ty { self.binding_array_sampler_elements .add(binding.visibility, count); } } else { match binding.ty { wgt::BindingType::Buffer { ty: wgt::BufferBindingType::Uniform, has_dynamic_offset, .. } => { self.uniform_buffers.add(binding.visibility, count); if has_dynamic_offset { self.dynamic_uniform_buffers += count; } } wgt::BindingType::Buffer { ty: wgt::BufferBindingType::Storage { .. }, has_dynamic_offset, .. } => { self.storage_buffers.add(binding.visibility, count); if has_dynamic_offset { self.dynamic_storage_buffers += count; } } wgt::BindingType::Sampler { .. } => { self.samplers.add(binding.visibility, count); } wgt::BindingType::Texture { .. } => { self.sampled_textures.add(binding.visibility, count); } wgt::BindingType::StorageTexture { .. } => { self.storage_textures.add(binding.visibility, count); } wgt::BindingType::AccelerationStructure { .. } => { self.acceleration_structures.add(binding.visibility, count); } wgt::BindingType::ExternalTexture => { // https://www.w3.org/TR/webgpu/#gpuexternaltexture // In order to account for many possible representations, // the binding conservatively uses the following, for each // external texture: // * Three sampled textures for up to 3 planes // * One additional sampled texture for a 3D LUT // * One sampler to sample the LUT // * One uniform buffer for metadata self.sampled_textures.add(binding.visibility, count * 4); self.samplers.add(binding.visibility, count); self.uniform_buffers.add(binding.visibility, count); } } } } pub(crate) fn merge(&mut self, other: &Self) { self.dynamic_uniform_buffers += other.dynamic_uniform_buffers; self.dynamic_storage_buffers += other.dynamic_storage_buffers; self.sampled_textures.merge(&other.sampled_textures); self.samplers.merge(&other.samplers); self.storage_buffers.merge(&other.storage_buffers); self.storage_textures.merge(&other.storage_textures); self.uniform_buffers.merge(&other.uniform_buffers); self.acceleration_structures .merge(&other.acceleration_structures); self.binding_array_elements .merge(&other.binding_array_elements); self.binding_array_sampler_elements .merge(&other.binding_array_sampler_elements); } pub(crate) fn validate(&self, limits: &wgt::Limits) -> Result<(), BindingTypeMaxCountError> { if limits.max_dynamic_uniform_buffers_per_pipeline_layout < self.dynamic_uniform_buffers { return Err(BindingTypeMaxCountError { kind: BindingTypeMaxCountErrorKind::DynamicUniformBuffers, zone: BindingZone::Pipeline, limit: limits.max_dynamic_uniform_buffers_per_pipeline_layout, count: self.dynamic_uniform_buffers, }); } if limits.max_dynamic_storage_buffers_per_pipeline_layout < self.dynamic_storage_buffers { return Err(BindingTypeMaxCountError { kind: BindingTypeMaxCountErrorKind::DynamicStorageBuffers, zone: BindingZone::Pipeline, limit: limits.max_dynamic_storage_buffers_per_pipeline_layout, count: self.dynamic_storage_buffers, }); } self.sampled_textures.validate( limits.max_sampled_textures_per_shader_stage, BindingTypeMaxCountErrorKind::SampledTextures, )?; self.samplers.validate( limits.max_samplers_per_shader_stage, BindingTypeMaxCountErrorKind::Samplers, )?; self.storage_buffers.validate( limits.max_storage_buffers_per_shader_stage, BindingTypeMaxCountErrorKind::StorageBuffers, )?; self.storage_textures.validate( limits.max_storage_textures_per_shader_stage, BindingTypeMaxCountErrorKind::StorageTextures, )?; self.uniform_buffers.validate( limits.max_uniform_buffers_per_shader_stage, BindingTypeMaxCountErrorKind::UniformBuffers, )?; self.binding_array_elements.validate( limits.max_binding_array_elements_per_shader_stage, BindingTypeMaxCountErrorKind::BindingArrayElements, )?; self.binding_array_sampler_elements.validate( limits.max_binding_array_sampler_elements_per_shader_stage, BindingTypeMaxCountErrorKind::BindingArraySamplerElements, )?; self.acceleration_structures.validate( limits.max_acceleration_structures_per_shader_stage, BindingTypeMaxCountErrorKind::AccelerationStructures, )?; Ok(()) } /// Validate that the bind group layout does not contain both a binding array and a dynamic offset array. /// /// This allows us to use `UPDATE_AFTER_BIND` on vulkan for bindless arrays. Vulkan does not allow /// `UPDATE_AFTER_BIND` on dynamic offset arrays. See pub(crate) fn validate_binding_arrays(&self) -> Result<(), CreateBindGroupLayoutError> { let has_dynamic_offset_array = self.dynamic_uniform_buffers > 0 || self.dynamic_storage_buffers > 0; let has_uniform_buffer = self.uniform_buffers.max().1 > 0; if self.has_bindless_array && has_dynamic_offset_array { return Err(CreateBindGroupLayoutError::ContainsBothBindingArrayAndDynamicOffsetArray); } if self.has_bindless_array && has_uniform_buffer { return Err(CreateBindGroupLayoutError::ContainsBothBindingArrayAndUniformBuffer); } Ok(()) } } /// Bindable resource and the slot to bind it to. /// cbindgen:ignore #[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct BindGroupEntry< 'a, B = BufferId, S = SamplerId, TV = TextureViewId, TLAS = TlasId, ET = ExternalTextureId, > where [BufferBinding]: ToOwned, [S]: ToOwned, [TV]: ToOwned, <[BufferBinding] as ToOwned>::Owned: fmt::Debug, <[S] as ToOwned>::Owned: fmt::Debug, <[TV] as ToOwned>::Owned: fmt::Debug, { /// Slot for which binding provides resource. Corresponds to an entry of the same /// binding index in the [`BindGroupLayoutDescriptor`]. pub binding: u32, #[cfg_attr( feature = "serde", serde(bound(deserialize = "BindingResource<'a, B, S, TV, TLAS, ET>: Deserialize<'de>")) )] /// Resource to attach to the binding pub resource: BindingResource<'a, B, S, TV, TLAS, ET>, } /// cbindgen:ignore pub type ResolvedBindGroupEntry<'a> = BindGroupEntry< 'a, Arc, Arc, Arc, Arc, Arc, >; /// Describes a group of bindings and the resources to be bound. #[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct BindGroupDescriptor< 'a, BGL = BindGroupLayoutId, B = BufferId, S = SamplerId, TV = TextureViewId, TLAS = TlasId, ET = ExternalTextureId, > where [BufferBinding]: ToOwned, [S]: ToOwned, [TV]: ToOwned, <[BufferBinding] as ToOwned>::Owned: fmt::Debug, <[S] as ToOwned>::Owned: fmt::Debug, <[TV] as ToOwned>::Owned: fmt::Debug, [BindGroupEntry<'a, B, S, TV, TLAS, ET>]: ToOwned, <[BindGroupEntry<'a, B, S, TV, TLAS, ET>] as ToOwned>::Owned: fmt::Debug, { /// Debug label of the bind group. /// /// This will show up in graphics debuggers for easy identification. pub label: Label<'a>, /// The [`BindGroupLayout`] that corresponds to this bind group. pub layout: BGL, #[cfg_attr( feature = "serde", serde(bound( deserialize = "<[BindGroupEntry<'a, B, S, TV, TLAS, ET>] as ToOwned>::Owned: Deserialize<'de>" )) )] /// The resources to bind to this bind group. #[allow(clippy::type_complexity)] pub entries: Cow<'a, [BindGroupEntry<'a, B, S, TV, TLAS, ET>]>, } /// cbindgen:ignore pub type ResolvedBindGroupDescriptor<'a> = BindGroupDescriptor< 'a, Arc, Arc, Arc, Arc, Arc, Arc, >; /// Describes a [`BindGroupLayout`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct BindGroupLayoutDescriptor<'a> { /// Debug label of the bind group layout. /// /// This will show up in graphics debuggers for easy identification. pub label: Label<'a>, /// Array of entries in this BindGroupLayout pub entries: Cow<'a, [wgt::BindGroupLayoutEntry]>, } /// Used by [`BindGroupLayout`]. It indicates whether the BGL must be /// used with a specific pipeline. This constraint only happens when /// the BGLs have been derived from a pipeline without a layout. #[derive(Debug)] pub(crate) enum ExclusivePipeline { None, Render(Weak), Compute(Weak), } impl fmt::Display for ExclusivePipeline { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ExclusivePipeline::None => f.write_str("None"), ExclusivePipeline::Render(p) => { if let Some(p) = p.upgrade() { p.error_ident().fmt(f) } else { f.write_str("RenderPipeline") } } ExclusivePipeline::Compute(p) => { if let Some(p) = p.upgrade() { p.error_ident().fmt(f) } else { f.write_str("ComputePipeline") } } } } } /// Bind group layout. #[derive(Debug)] pub struct BindGroupLayout { pub(crate) raw: ManuallyDrop>, pub(crate) device: Arc, pub(crate) entries: bgl::EntryMap, /// It is very important that we know if the bind group comes from the BGL pool. /// /// If it does, then we need to remove it from the pool when we drop it. /// /// We cannot unconditionally remove from the pool, as BGLs that don't come from the pool /// (derived BGLs) must not be removed. pub(crate) origin: bgl::Origin, pub(crate) exclusive_pipeline: crate::OnceCellOrLock, #[allow(unused)] pub(crate) binding_count_validator: BindingTypeMaxCountValidator, /// The `label` from the descriptor used to create the resource. pub(crate) label: String, } impl Drop for BindGroupLayout { fn drop(&mut self) { resource_log!("Destroy raw {}", self.error_ident()); if matches!(self.origin, bgl::Origin::Pool) { self.device.bgl_pool.remove(&self.entries); } // SAFETY: We are in the Drop impl and we don't use self.raw anymore after this point. let raw = unsafe { ManuallyDrop::take(&mut self.raw) }; unsafe { self.device.raw().destroy_bind_group_layout(raw); } } } crate::impl_resource_type!(BindGroupLayout); crate::impl_labeled!(BindGroupLayout); crate::impl_parent_device!(BindGroupLayout); crate::impl_storage_item!(BindGroupLayout); impl BindGroupLayout { pub(crate) fn raw(&self) -> &dyn hal::DynBindGroupLayout { self.raw.as_ref() } } #[derive(Clone, Debug, Error)] #[non_exhaustive] pub enum CreatePipelineLayoutError { #[error(transparent)] Device(#[from] DeviceError), #[error( "Immediate data at index {index} has range bound {bound} not aligned to {}", wgt::IMMEDIATES_ALIGNMENT )] MisalignedImmediateRange { index: usize, bound: u32 }, #[error(transparent)] MissingFeatures(#[from] MissingFeatures), #[error("Immediate data range (index {index}) provides for stage(s) {provided:?} but there exists another range that provides stage(s) {intersected:?}. Each stage may only be provided by one range")] MoreThanOneImmediateRangePerStage { index: usize, provided: wgt::ShaderStages, intersected: wgt::ShaderStages, }, #[error("Immediate data at index {index} has range {}..{} which exceeds device immediate data size limit 0..{max}", range.start, range.end)] ImmediateRangeTooLarge { index: usize, range: Range, max: u32, }, #[error(transparent)] TooManyBindings(BindingTypeMaxCountError), #[error("Bind group layout count {actual} exceeds device bind group limit {max}")] TooManyGroups { actual: usize, max: usize }, #[error(transparent)] InvalidResource(#[from] InvalidResourceError), } impl WebGpuError for CreatePipelineLayoutError { fn webgpu_error_type(&self) -> ErrorType { let e: &dyn WebGpuError = match self { Self::Device(e) => e, Self::MissingFeatures(e) => e, Self::InvalidResource(e) => e, Self::TooManyBindings(e) => e, Self::MisalignedImmediateRange { .. } | Self::MoreThanOneImmediateRangePerStage { .. } | Self::ImmediateRangeTooLarge { .. } | Self::TooManyGroups { .. } => return ErrorType::Validation, }; e.webgpu_error_type() } } #[derive(Clone, Debug, Error)] #[non_exhaustive] pub enum ImmediateUploadError { #[error("Provided immediate data with indices {offset}..{end_offset} overruns matching immediate data range at index {idx}, with stage(s) {:?} and indices {:?}", range.stages, range.range)] TooLarge { offset: u32, end_offset: u32, idx: usize, range: wgt::ImmediateRange, }, #[error("Provided immediate data is for stage(s) {actual:?}, stage with a partial match found at index {idx} with stage(s) {matched:?}, however immediates must be complete matches")] PartialRangeMatch { actual: wgt::ShaderStages, idx: usize, matched: wgt::ShaderStages, }, #[error("Provided immediate data is for stage(s) {actual:?}, but intersects a immediate data range (at index {idx}) with stage(s) {missing:?}. Immediates must provide the stages for all ranges they intersect")] MissingStages { actual: wgt::ShaderStages, idx: usize, missing: wgt::ShaderStages, }, #[error("Provided immediate data is for stage(s) {actual:?}, however the pipeline layout has no immediate data range for the stage(s) {unmatched:?}")] UnmatchedStages { actual: wgt::ShaderStages, unmatched: wgt::ShaderStages, }, #[error("Provided immediate data offset {0} does not respect `IMMEDIATES_ALIGNMENT`")] Unaligned(u32), } impl WebGpuError for ImmediateUploadError { fn webgpu_error_type(&self) -> ErrorType { ErrorType::Validation } } /// Describes a pipeline layout. /// /// A `PipelineLayoutDescriptor` can be used to create a pipeline layout. #[derive(Clone, Debug, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(bound = "BGL: Serialize"))] pub struct PipelineLayoutDescriptor<'a, BGL = BindGroupLayoutId> where [BGL]: ToOwned, <[BGL] as ToOwned>::Owned: fmt::Debug, { /// Debug label of the pipeline layout. /// /// This will show up in graphics debuggers for easy identification. pub label: Label<'a>, /// Bind groups that this pipeline uses. The first entry will provide all the bindings for /// "set = 0", second entry will provide all the bindings for "set = 1" etc. #[cfg_attr( feature = "serde", serde(bound(deserialize = "<[BGL] as ToOwned>::Owned: Deserialize<'de>")) )] pub bind_group_layouts: Cow<'a, [BGL]>, /// Set of immediate data ranges this pipeline uses. Each shader stage that /// uses immediates must define the range in immediate data memory that /// corresponds to its single `layout(immediates)` uniform block. /// /// If this array is non-empty, the /// [`Features::IMMEDIATES`](wgt::Features::IMMEDIATES) feature must /// be enabled. pub immediates_ranges: Cow<'a, [wgt::ImmediateRange]>, } /// cbindgen:ignore pub type ResolvedPipelineLayoutDescriptor<'a, BGL = Arc> = PipelineLayoutDescriptor<'a, BGL>; #[derive(Debug)] pub struct PipelineLayout { pub(crate) raw: ManuallyDrop>, pub(crate) device: Arc, /// The `label` from the descriptor used to create the resource. pub(crate) label: String, pub(crate) bind_group_layouts: ArrayVec, { hal::MAX_BIND_GROUPS }>, pub(crate) immediates_ranges: ArrayVec, } impl Drop for PipelineLayout { fn drop(&mut self) { resource_log!("Destroy raw {}", self.error_ident()); // SAFETY: We are in the Drop impl and we don't use self.raw anymore after this point. let raw = unsafe { ManuallyDrop::take(&mut self.raw) }; unsafe { self.device.raw().destroy_pipeline_layout(raw); } } } impl PipelineLayout { pub(crate) fn raw(&self) -> &dyn hal::DynPipelineLayout { self.raw.as_ref() } pub(crate) fn get_binding_maps(&self) -> ArrayVec<&bgl::EntryMap, { hal::MAX_BIND_GROUPS }> { self.bind_group_layouts .iter() .map(|bgl| &bgl.entries) .collect() } /// Validate immediates match up with expected ranges. pub(crate) fn validate_immediates_ranges( &self, stages: wgt::ShaderStages, offset: u32, end_offset: u32, ) -> Result<(), ImmediateUploadError> { // Don't need to validate size against the immediate data size limit here, // as immediate data ranges are already validated to be within bounds, // and we validate that they are within the ranges. if offset % wgt::IMMEDIATES_ALIGNMENT != 0 { return Err(ImmediateUploadError::Unaligned(offset)); } // Immediate data validation looks very complicated on the surface, but // the problem can be range-reduced pretty well. // // Immediates require (summarized from the vulkan spec): // 1. For each byte in the range and for each shader stage in stageFlags, // there must be a immediate data range in the layout that includes that // byte and that stage. // 2. For each byte in the range and for each immediate data range that overlaps that byte, // `stage` must include all stages in that immediate data range’s `stage`. // // However there are some additional constraints that help us: // 3. All immediate data ranges are the only range that can access that stage. // i.e. if one range has VERTEX, no other range has VERTEX // // Therefore we can simplify the checks in the following ways: // - Because 3 guarantees that the immediate data range has a unique stage, // when we check for 1, we can simply check that our entire updated range // is within a immediate data range. i.e. our range for a specific stage cannot // intersect more than one immediate data range. let mut used_stages = wgt::ShaderStages::NONE; for (idx, range) in self.immediates_ranges.iter().enumerate() { // contains not intersects due to 2 if stages.contains(range.stages) { if !(range.range.start <= offset && end_offset <= range.range.end) { return Err(ImmediateUploadError::TooLarge { offset, end_offset, idx, range: range.clone(), }); } used_stages |= range.stages; } else if stages.intersects(range.stages) { // Will be caught by used stages check below, but we can do this because of 1 // and is more helpful to the user. return Err(ImmediateUploadError::PartialRangeMatch { actual: stages, idx, matched: range.stages, }); } // The immediate data range intersects range we are uploading if offset < range.range.end && range.range.start < end_offset { // But requires stages we don't provide if !stages.contains(range.stages) { return Err(ImmediateUploadError::MissingStages { actual: stages, idx, missing: stages, }); } } } if used_stages != stages { return Err(ImmediateUploadError::UnmatchedStages { actual: stages, unmatched: stages - used_stages, }); } Ok(()) } } crate::impl_resource_type!(PipelineLayout); crate::impl_labeled!(PipelineLayout); crate::impl_parent_device!(PipelineLayout); crate::impl_storage_item!(PipelineLayout); #[repr(C)] #[derive(Clone, Debug, Hash, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct BufferBinding { pub buffer: B, pub offset: wgt::BufferAddress, pub size: Option, } pub type ResolvedBufferBinding = BufferBinding>; // Note: Duplicated in `wgpu-rs` as `BindingResource` // They're different enough that it doesn't make sense to share a common type #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum BindingResource< 'a, B = BufferId, S = SamplerId, TV = TextureViewId, TLAS = TlasId, ET = ExternalTextureId, > where [BufferBinding]: ToOwned, [S]: ToOwned, [TV]: ToOwned, <[BufferBinding] as ToOwned>::Owned: fmt::Debug, <[S] as ToOwned>::Owned: fmt::Debug, <[TV] as ToOwned>::Owned: fmt::Debug, { Buffer(BufferBinding), #[cfg_attr( feature = "serde", serde(bound(deserialize = "<[BufferBinding] as ToOwned>::Owned: Deserialize<'de>")) )] BufferArray(Cow<'a, [BufferBinding]>), Sampler(S), #[cfg_attr( feature = "serde", serde(bound(deserialize = "<[S] as ToOwned>::Owned: Deserialize<'de>")) )] SamplerArray(Cow<'a, [S]>), TextureView(TV), #[cfg_attr( feature = "serde", serde(bound(deserialize = "<[TV] as ToOwned>::Owned: Deserialize<'de>")) )] TextureViewArray(Cow<'a, [TV]>), AccelerationStructure(TLAS), ExternalTexture(ET), } pub type ResolvedBindingResource<'a> = BindingResource< 'a, Arc, Arc, Arc, Arc, Arc, >; #[derive(Clone, Debug, Error)] #[non_exhaustive] pub enum BindError { #[error( "{bind_group} {group} expects {expected} dynamic offset{s0}. However {actual} dynamic offset{s1} were provided.", s0 = if *.expected >= 2 { "s" } else { "" }, s1 = if *.actual >= 2 { "s" } else { "" }, )] MismatchedDynamicOffsetCount { bind_group: ResourceErrorIdent, group: u32, actual: usize, expected: usize, }, #[error( "Dynamic binding index {idx} (targeting {bind_group} {group}, binding {binding}) with value {offset}, does not respect device's requested `{limit_name}` limit: {alignment}" )] UnalignedDynamicBinding { bind_group: ResourceErrorIdent, idx: usize, group: u32, binding: u32, offset: u32, alignment: u32, limit_name: &'static str, }, #[error( "Dynamic binding offset index {idx} with offset {offset} would overrun the buffer bound to {bind_group} {group} -> binding {binding}. \ Buffer size is {buffer_size} bytes, the binding binds bytes {binding_range:?}, meaning the maximum the binding can be offset is {maximum_dynamic_offset} bytes", )] DynamicBindingOutOfBounds { bind_group: ResourceErrorIdent, idx: usize, group: u32, binding: u32, offset: u32, buffer_size: wgt::BufferAddress, binding_range: Range, maximum_dynamic_offset: wgt::BufferAddress, }, } impl WebGpuError for BindError { fn webgpu_error_type(&self) -> ErrorType { ErrorType::Validation } } #[derive(Debug)] pub struct BindGroupDynamicBindingData { /// The index of the binding. /// /// Used for more descriptive errors. pub(crate) binding_idx: u32, /// The size of the buffer. /// /// Used for more descriptive errors. pub(crate) buffer_size: wgt::BufferAddress, /// The range that the binding covers. /// /// Used for more descriptive errors. pub(crate) binding_range: Range, /// The maximum value the dynamic offset can have before running off the end of the buffer. pub(crate) maximum_dynamic_offset: wgt::BufferAddress, /// The binding type. pub(crate) binding_type: wgt::BufferBindingType, } pub(crate) fn buffer_binding_type_alignment( limits: &wgt::Limits, binding_type: wgt::BufferBindingType, ) -> (u32, &'static str) { match binding_type { wgt::BufferBindingType::Uniform => ( limits.min_uniform_buffer_offset_alignment, "min_uniform_buffer_offset_alignment", ), wgt::BufferBindingType::Storage { .. } => ( limits.min_storage_buffer_offset_alignment, "min_storage_buffer_offset_alignment", ), } } pub(crate) fn buffer_binding_type_bounds_check_alignment( alignments: &hal::Alignments, binding_type: wgt::BufferBindingType, ) -> wgt::BufferAddress { match binding_type { wgt::BufferBindingType::Uniform => alignments.uniform_bounds_check_alignment.get(), wgt::BufferBindingType::Storage { .. } => wgt::COPY_BUFFER_ALIGNMENT, } } #[derive(Debug)] pub(crate) struct BindGroupLateBufferBindingInfo { /// The normal binding index in the bind group. pub binding_index: u32, /// The size that exists at bind time. pub size: wgt::BufferSize, } #[derive(Debug)] pub struct BindGroup { pub(crate) raw: Snatchable>, pub(crate) device: Arc, pub(crate) layout: Arc, /// The `label` from the descriptor used to create the resource. pub(crate) label: String, pub(crate) tracking_data: TrackingData, pub(crate) used: BindGroupStates, pub(crate) used_buffer_ranges: Vec, pub(crate) used_texture_ranges: Vec, pub(crate) dynamic_binding_info: Vec, /// Actual binding sizes for buffers that don't have `min_binding_size` /// specified in BGL. Listed in the order of iteration of `BGL.entries`. pub(crate) late_buffer_binding_infos: Vec, } impl Drop for BindGroup { fn drop(&mut self) { if let Some(raw) = self.raw.take() { resource_log!("Destroy raw {}", self.error_ident()); unsafe { self.device.raw().destroy_bind_group(raw); } } } } impl BindGroup { pub(crate) fn try_raw<'a>( &'a self, guard: &'a SnatchGuard, ) -> Result<&'a dyn hal::DynBindGroup, DestroyedResourceError> { // Clippy insist on writing it this way. The idea is to return None // if any of the raw buffer is not valid anymore. for buffer in &self.used_buffer_ranges { buffer.buffer.try_raw(guard)?; } for texture in &self.used_texture_ranges { texture.texture.try_raw(guard)?; } self.raw .get(guard) .map(|raw| raw.as_ref()) .ok_or_else(|| DestroyedResourceError(self.error_ident())) } pub(crate) fn validate_dynamic_bindings( &self, bind_group_index: u32, offsets: &[wgt::DynamicOffset], ) -> Result<(), BindError> { if self.dynamic_binding_info.len() != offsets.len() { return Err(BindError::MismatchedDynamicOffsetCount { bind_group: self.error_ident(), group: bind_group_index, expected: self.dynamic_binding_info.len(), actual: offsets.len(), }); } for (idx, (info, &offset)) in self .dynamic_binding_info .iter() .zip(offsets.iter()) .enumerate() { let (alignment, limit_name) = buffer_binding_type_alignment(&self.device.limits, info.binding_type); if offset as wgt::BufferAddress % alignment as u64 != 0 { return Err(BindError::UnalignedDynamicBinding { bind_group: self.error_ident(), group: bind_group_index, binding: info.binding_idx, idx, offset, alignment, limit_name, }); } if offset as wgt::BufferAddress > info.maximum_dynamic_offset { return Err(BindError::DynamicBindingOutOfBounds { bind_group: self.error_ident(), group: bind_group_index, binding: info.binding_idx, idx, offset, buffer_size: info.buffer_size, binding_range: info.binding_range.clone(), maximum_dynamic_offset: info.maximum_dynamic_offset, }); } } Ok(()) } } crate::impl_resource_type!(BindGroup); crate::impl_labeled!(BindGroup); crate::impl_parent_device!(BindGroup); crate::impl_storage_item!(BindGroup); crate::impl_trackable!(BindGroup); #[derive(Clone, Debug, Error)] #[non_exhaustive] pub enum GetBindGroupLayoutError { #[error("Invalid group index {0}")] InvalidGroupIndex(u32), #[error(transparent)] InvalidResource(#[from] InvalidResourceError), } impl WebGpuError for GetBindGroupLayoutError { fn webgpu_error_type(&self) -> ErrorType { match self { Self::InvalidGroupIndex(_) => ErrorType::Validation, Self::InvalidResource(e) => e.webgpu_error_type(), } } } #[derive(Clone, Debug, Error, Eq, PartialEq)] #[error( "In bind group index {group_index}, the buffer bound at binding index {binding_index} \ is bound with size {bound_size} where the shader expects {shader_size}." )] pub struct LateMinBufferBindingSizeMismatch { pub group_index: u32, pub binding_index: u32, pub shader_size: wgt::BufferAddress, pub bound_size: wgt::BufferAddress, }