diff --git a/deno_webgpu/01_webgpu.js b/deno_webgpu/01_webgpu.js index 8c5ce5c5933..682aa549393 100644 --- a/deno_webgpu/01_webgpu.js +++ b/deno_webgpu/01_webgpu.js @@ -273,8 +273,7 @@ ObjectDefineProperty(GPUSupportedLimitsPrototype, privateCustomInspect, { "maxBufferSize", "maxVertexAttributes", "maxVertexBufferArrayStride", - // TODO(@crowlKats): support max_inter_stage_shader_variables - // "maxInterStageShaderVariables", + "maxInterStageShaderVariables", "maxColorAttachments", "maxColorAttachmentBytesPerSample", "maxComputeWorkgroupStorageSize", diff --git a/deno_webgpu/adapter.rs b/deno_webgpu/adapter.rs index 711a7190c7b..707be23844a 100644 --- a/deno_webgpu/adapter.rs +++ b/deno_webgpu/adapter.rs @@ -344,7 +344,10 @@ impl GPUSupportedLimits { self.0.max_vertex_buffer_array_stride } - // TODO(@crowlKats): support max_inter_stage_shader_variables + #[getter] + fn maxInterStageShaderVariables(&self) -> u32 { + self.0.max_inter_stage_shader_variables + } #[getter] fn maxColorAttachments(&self) -> u32 { diff --git a/wgpu-core/src/device/resource.rs b/wgpu-core/src/device/resource.rs index 79a8a97bc68..a2248138149 100644 --- a/wgpu-core/src/device/resource.rs +++ b/wgpu-core/src/device/resource.rs @@ -3734,10 +3734,10 @@ impl Device { let final_entry_point_name; { - let stage = wgt::ShaderStages::COMPUTE; + let stage = validation::ShaderStageForValidation::Compute; final_entry_point_name = shader_module.finalize_entry_point_name( - stage, + stage.to_naga(), desc.stage.entry_point.as_ref().map(|ep| ep.as_ref()), )?; @@ -3748,7 +3748,6 @@ impl Device { &final_entry_point_name, stage, io, - None, )?; } } @@ -4225,17 +4224,23 @@ impl Device { pipeline::RenderPipelineVertexProcessor::Vertex(ref vertex) => { vertex_stage = { let stage_desc = &vertex.stage; - let stage = wgt::ShaderStages::VERTEX; + let stage = validation::ShaderStageForValidation::Vertex { + topology: desc.primitive.topology, + compare_function: desc.depth_stencil.as_ref().map(|d| d.depth_compare), + }; + let stage_bit = stage.to_wgt_bit(); let vertex_shader_module = &stage_desc.module; vertex_shader_module.same_device(self)?; - let stage_err = - |error| pipeline::CreateRenderPipelineError::Stage { stage, error }; + let stage_err = |error| pipeline::CreateRenderPipelineError::Stage { + stage: stage_bit, + error, + }; _vertex_entry_point_name = vertex_shader_module .finalize_entry_point_name( - stage, + stage.to_naga(), stage_desc.entry_point.as_ref().map(|ep| ep.as_ref()), ) .map_err(stage_err)?; @@ -4248,10 +4253,9 @@ impl Device { &_vertex_entry_point_name, stage, io, - desc.depth_stencil.as_ref().map(|d| d.depth_compare), ) .map_err(stage_err)?; - validated_stages |= stage; + validated_stages |= stage_bit; } Some(hal::ProgrammableStage { module: vertex_shader_module.raw(), @@ -4267,16 +4271,19 @@ impl Device { task_stage = if let Some(task) = task { let stage_desc = &task.stage; - let stage = wgt::ShaderStages::TASK; + let stage = validation::ShaderStageForValidation::Task; + let stage_bit = stage.to_wgt_bit(); let task_shader_module = &stage_desc.module; task_shader_module.same_device(self)?; - let stage_err = - |error| pipeline::CreateRenderPipelineError::Stage { stage, error }; + let stage_err = |error| pipeline::CreateRenderPipelineError::Stage { + stage: stage_bit, + error, + }; _task_entry_point_name = task_shader_module .finalize_entry_point_name( - stage, + stage.to_naga(), stage_desc.entry_point.as_ref().map(|ep| ep.as_ref()), ) .map_err(stage_err)?; @@ -4289,10 +4296,9 @@ impl Device { &_task_entry_point_name, stage, io, - desc.depth_stencil.as_ref().map(|d| d.depth_compare), ) .map_err(stage_err)?; - validated_stages |= stage; + validated_stages |= stage_bit; } Some(hal::ProgrammableStage { module: task_shader_module.raw(), @@ -4306,16 +4312,19 @@ impl Device { }; mesh_stage = { let stage_desc = &mesh.stage; - let stage = wgt::ShaderStages::MESH; + let stage = validation::ShaderStageForValidation::Mesh; + let stage_bit = stage.to_wgt_bit(); let mesh_shader_module = &stage_desc.module; mesh_shader_module.same_device(self)?; - let stage_err = - |error| pipeline::CreateRenderPipelineError::Stage { stage, error }; + let stage_err = |error| pipeline::CreateRenderPipelineError::Stage { + stage: stage_bit, + error, + }; _mesh_entry_point_name = mesh_shader_module .finalize_entry_point_name( - stage, + stage.to_naga(), stage_desc.entry_point.as_ref().map(|ep| ep.as_ref()), ) .map_err(stage_err)?; @@ -4328,10 +4337,9 @@ impl Device { &_mesh_entry_point_name, stage, io, - desc.depth_stencil.as_ref().map(|d| d.depth_compare), ) .map_err(stage_err)?; - validated_stages |= stage; + validated_stages |= stage_bit; } Some(hal::ProgrammableStage { module: mesh_shader_module.raw(), @@ -4347,16 +4355,20 @@ impl Device { let fragment_entry_point_name; let fragment_stage = match desc.fragment { Some(ref fragment_state) => { - let stage = wgt::ShaderStages::FRAGMENT; + let stage = validation::ShaderStageForValidation::Fragment; + let stage_bit = stage.to_wgt_bit(); let shader_module = &fragment_state.stage.module; shader_module.same_device(self)?; - let stage_err = |error| pipeline::CreateRenderPipelineError::Stage { stage, error }; + let stage_err = |error| pipeline::CreateRenderPipelineError::Stage { + stage: stage_bit, + error, + }; fragment_entry_point_name = shader_module .finalize_entry_point_name( - stage, + stage.to_naga(), fragment_state .stage .entry_point @@ -4374,10 +4386,9 @@ impl Device { &fragment_entry_point_name, stage, io, - desc.depth_stencil.as_ref().map(|d| d.depth_compare), ) .map_err(stage_err)?; - validated_stages |= stage; + validated_stages |= stage_bit; } } @@ -4385,7 +4396,7 @@ impl Device { shader_expects_dual_source_blending = interface .fragment_uses_dual_source_blending(&fragment_entry_point_name) .map_err(|error| pipeline::CreateRenderPipelineError::Stage { - stage, + stage: stage_bit, error, })?; } diff --git a/wgpu-core/src/pipeline.rs b/wgpu-core/src/pipeline.rs index ee5bcb0798d..00e400c3f7d 100644 --- a/wgpu-core/src/pipeline.rs +++ b/wgpu-core/src/pipeline.rs @@ -89,11 +89,11 @@ impl ShaderModule { pub(crate) fn finalize_entry_point_name( &self, - stage_bit: wgt::ShaderStages, + stage: naga::ShaderStage, entry_point: Option<&str>, ) -> Result { match &self.interface { - Some(interface) => interface.finalize_entry_point_name(stage_bit, entry_point), + Some(interface) => interface.finalize_entry_point_name(stage, entry_point), None => entry_point .map(|ep| ep.to_string()) .ok_or(validation::StageError::NoEntryPointFound), diff --git a/wgpu-core/src/validation.rs b/wgpu-core/src/validation.rs index 1a4ca444da8..a35d1f8a7dc 100644 --- a/wgpu-core/src/validation.rs +++ b/wgpu-core/src/validation.rs @@ -313,6 +313,25 @@ pub enum StageError { MultipleEntryPointsFound, #[error(transparent)] InvalidResource(#[from] InvalidResourceError), + #[error( + "Location[{location}]: {var}'s index exceeds the `max_inter_stage_shader_variables` limit \ + ({limit}); note that some " // TODO + )] + VertexOutputLocationTooLarge { + location: u32, + var: InterfaceVar, + limit: u32, + deductions: Vec, + }, + #[error( + "found {num_found} user-defined vertex shader output variables, which exceeds the \ + `max_inter_stage_shader_variables` limit ({limit}); note that some " // TODO + )] + TooManyUserDefinedVertexOutputs { + num_found: u32, + limit: u32, + deductions: Vec, + }, #[error( "Location[{location}] {var}'s index exceeds the `max_color_attachments` limit ({limit})" )] @@ -343,12 +362,50 @@ impl WebGpuError for StageError { | Self::MissingEntryPoint(..) | Self::NoEntryPointFound | Self::MultipleEntryPointsFound + | Self::VertexOutputLocationTooLarge { .. } + | Self::TooManyUserDefinedVertexOutputs { .. } | Self::ColorAttachmentLocationTooLarge { .. } => return ErrorType::Validation, }; e.webgpu_error_type() } } +#[derive(Clone, Copy)] +pub struct MaxInterStageShaderVariablesDeduction(MaxInterStageShaderVariablesDeductionImpl); + +impl fmt::Debug for MaxInterStageShaderVariablesDeduction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self(inner) = self; + fmt::Debug::fmt(inner, f) + } +} + +#[derive(Clone, Copy, Debug)] +enum MaxInterStageShaderVariablesDeductionImpl { + PointLinePrimitiveTopology, + ClipDistance { count: u32 }, +} + +impl MaxInterStageShaderVariablesDeductionImpl { + fn variables_from_clip_distance_slot(num_slots: u32) -> u32 { + num_slots.div_ceil(4) + } + + pub fn for_variables(self) -> u32 { + match self { + Self::PointLinePrimitiveTopology => 1, + Self::ClipDistance { count } => Self::variables_from_clip_distance_slot(count), + } + } + + pub fn for_location(self) -> u32 { + match self { + Self::PointLinePrimitiveTopology => 0, + Self::ClipDistance { count } => Self::variables_from_clip_distance_slot(count), + } + } +} + pub fn map_storage_format_to_naga(format: wgt::TextureFormat) -> Option { use naga::StorageFormat as Sf; use wgt::TextureFormat as Tf; @@ -1070,10 +1127,9 @@ impl Interface { pub fn finalize_entry_point_name( &self, - stage_bit: wgt::ShaderStages, + stage: naga::ShaderStage, entry_point_name: Option<&str>, ) -> Result { - let stage = Self::shader_stage_from_stage_bit(stage_bit); entry_point_name .map(|ep| ep.to_string()) .map(Ok) @@ -1090,36 +1146,25 @@ impl Interface { }) } - pub(crate) fn shader_stage_from_stage_bit(stage_bit: wgt::ShaderStages) -> naga::ShaderStage { - match stage_bit { - wgt::ShaderStages::VERTEX => naga::ShaderStage::Vertex, - wgt::ShaderStages::FRAGMENT => naga::ShaderStage::Fragment, - wgt::ShaderStages::COMPUTE => naga::ShaderStage::Compute, - wgt::ShaderStages::MESH => naga::ShaderStage::Mesh, - wgt::ShaderStages::TASK => naga::ShaderStage::Task, - _ => unreachable!(), - } - } - pub fn check_stage( &self, layouts: &mut BindingLayoutSource<'_>, shader_binding_sizes: &mut FastHashMap, entry_point_name: &str, - stage_bit: wgt::ShaderStages, + shader_stage: ShaderStageForValidation, inputs: StageIo, - compare_function: Option, ) -> Result { // 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 = Self::shader_stage_from_stage_bit(stage_bit); - let pair = (shader_stage, entry_point_name.to_string()); + let pair = (shader_stage.to_naga(), entry_point_name.to_string()); let entry_point = match self.entry_points.get(&pair) { Some(some) => some, None => return Err(StageError::MissingEntryPoint(pair.1)), }; let (_stage, 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]; @@ -1241,7 +1286,7 @@ impl Interface { } // check workgroup size limits - if shader_stage.compute_like() { + if shader_stage.to_naga().compute_like() { let max_workgroup_size_limits = [ self.limits.max_compute_workgroup_size_x, self.limits.max_compute_workgroup_size_y, @@ -1275,7 +1320,7 @@ impl Interface { .get(&location) .ok_or(InputError::Missing) .and_then(|provided| { - let (compatible, num_components) = match shader_stage { + let (compatible, num_components) = match shader_stage.to_naga() { // For vertex attributes, there are defaults filled out // by the driver if data is not provided. naga::ShaderStage::Vertex => { @@ -1330,12 +1375,61 @@ impl Interface { } match shader_stage { - naga::ShaderStage::Vertex => { + ShaderStageForValidation::Vertex { + topology, + compare_function, + } => { + let mut max_vertex_shader_output_variables = + self.limits.max_inter_stage_shader_variables; + let mut max_vertex_shader_output_location = max_vertex_shader_output_variables - 1; + + let point_list_deduction = if topology == wgt::PrimitiveTopology::PointList { + Some(MaxInterStageShaderVariablesDeductionImpl::PointLinePrimitiveTopology) + } else { + None + }; + + let clip_distance_deductions = entry_point.outputs.iter().filter_map(|output| { + if let Varying::BuiltIn(naga::BuiltIn::ClipDistance) = output { + Some(MaxInterStageShaderVariablesDeductionImpl::ClipDistance { + // NOTE: `clip_distances`' max array size is currently 8, so at most we deduct 2. + count: todo!("get size of `clip_distances` binding"), + }) + } else { + None + } + }); + + let deductions = point_list_deduction + .into_iter() + .chain(clip_distance_deductions); + + for deduction in deductions.clone() { + // NOTE: We assume that these will never get to 0. + max_vertex_shader_output_variables -= deduction.for_variables(); + max_vertex_shader_output_location -= deduction.for_location(); + } + + let mut num_user_defined_outputs = 0; + 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, + match *output { + Varying::Local { ref iv, location } => { + if location > max_vertex_shader_output_location { + // TODO: add diagnostics context for limit deductions + return Err(StageError::VertexOutputLocationTooLarge { + location, + var: iv.clone(), + limit: self.limits.max_inter_stage_shader_variables, + deductions: deductions + .map(MaxInterStageShaderVariablesDeduction) + .collect(), + }); + } + num_user_defined_outputs += 1; + inter_stage_components += iv.ty.dim.num_components() + } + Varying::BuiltIn(_) => {} }; if let Some( @@ -1362,8 +1456,18 @@ impl Interface { } } } + + if num_user_defined_outputs > max_vertex_shader_output_variables { + return Err(StageError::TooManyUserDefinedVertexOutputs { + num_found: num_user_defined_outputs, + limit: self.limits.max_inter_stage_shader_variables - 1, + deductions: deductions + .map(MaxInterStageShaderVariablesDeduction) + .collect(), + }); + } } - naga::ShaderStage::Fragment => { + ShaderStageForValidation::Fragment => { for output in &entry_point.outputs { let &Varying::Local { location, ref iv } = output else { continue; @@ -1435,3 +1539,37 @@ pub fn validate_color_attachment_bytes_per_sample( Ok(()) } + +pub enum ShaderStageForValidation { + Vertex { + topology: wgt::PrimitiveTopology, + compare_function: Option, + }, + Mesh, + Fragment, + Compute, + Task, + // TODO: preserve ordering? +} + +impl ShaderStageForValidation { + pub fn to_naga(&self) -> naga::ShaderStage { + match self { + Self::Vertex { .. } => naga::ShaderStage::Vertex, + Self::Mesh { .. } => naga::ShaderStage::Mesh, + Self::Fragment { .. } => naga::ShaderStage::Fragment, + Self::Compute => naga::ShaderStage::Compute, + Self::Task => naga::ShaderStage::Task, + } + } + + pub fn to_wgt_bit(&self) -> wgt::ShaderStages { + match self { + Self::Vertex { .. } => wgt::ShaderStages::VERTEX, + Self::Mesh { .. } => wgt::ShaderStages::MESH, + Self::Fragment { .. } => wgt::ShaderStages::FRAGMENT, + Self::Compute => wgt::ShaderStages::COMPUTE, + Self::Task => wgt::ShaderStages::TASK, + } + } +} diff --git a/wgpu-hal/src/dx12/adapter.rs b/wgpu-hal/src/dx12/adapter.rs index acd8031af96..324b1af796f 100644 --- a/wgpu-hal/src/dx12/adapter.rs +++ b/wgpu-hal/src/dx12/adapter.rs @@ -717,6 +717,7 @@ impl super::Adapter { // // Source: https://learn.microsoft.com/en-us/windows/win32/direct3d12/root-signature-limits#memory-limits-and-costs max_immediate_size: 128, + max_inter_stage_shader_variables: 16, min_uniform_buffer_offset_alignment: Direct3D12::D3D12_CONSTANT_BUFFER_DATA_PLACEMENT_ALIGNMENT, min_storage_buffer_offset_alignment: 4, diff --git a/wgpu-hal/src/gles/adapter.rs b/wgpu-hal/src/gles/adapter.rs index 83fd9e2b825..f7a36126833 100644 --- a/wgpu-hal/src/gles/adapter.rs +++ b/wgpu-hal/src/gles/adapter.rs @@ -754,6 +754,7 @@ impl super::Adapter { min_subgroup_size: 0, max_subgroup_size: 0, max_immediate_size: super::MAX_IMMEDIATES as u32 * 4, + max_inter_stage_shader_variables: 16, min_uniform_buffer_offset_alignment, min_storage_buffer_offset_alignment, max_inter_stage_shader_components: { diff --git a/wgpu-hal/src/metal/adapter.rs b/wgpu-hal/src/metal/adapter.rs index 6158df52f53..e9809f86818 100644 --- a/wgpu-hal/src/metal/adapter.rs +++ b/wgpu-hal/src/metal/adapter.rs @@ -1103,6 +1103,7 @@ impl super::PrivateCapabilities { min_subgroup_size: 4, max_subgroup_size: 64, max_immediate_size: 0x1000, + max_inter_stage_shader_variables: 16, min_uniform_buffer_offset_alignment: self.buffer_alignment as u32, min_storage_buffer_offset_alignment: self.buffer_alignment as u32, max_inter_stage_shader_components: self.max_varying_components, diff --git a/wgpu-hal/src/noop/mod.rs b/wgpu-hal/src/noop/mod.rs index 2dd4a17597f..f5f3c4875b6 100644 --- a/wgpu-hal/src/noop/mod.rs +++ b/wgpu-hal/src/noop/mod.rs @@ -178,6 +178,7 @@ pub const CAPABILITIES: crate::Capabilities = { max_buffer_size: ALLOC_MAX_U32 as u64, max_vertex_attributes: ALLOC_MAX_U32, max_vertex_buffer_array_stride: ALLOC_MAX_U32, + max_inter_stage_shader_variables: ALLOC_MAX_U32, min_uniform_buffer_offset_alignment: 1, min_storage_buffer_offset_alignment: 1, max_inter_stage_shader_components: ALLOC_MAX_U32, diff --git a/wgpu-hal/src/vulkan/adapter.rs b/wgpu-hal/src/vulkan/adapter.rs index a511db0b911..741728f9e46 100644 --- a/wgpu-hal/src/vulkan/adapter.rs +++ b/wgpu-hal/src/vulkan/adapter.rs @@ -1340,6 +1340,7 @@ impl PhysicalDeviceProperties { .map(|subgroup_size| subgroup_size.max_subgroup_size) .unwrap_or(0), max_immediate_size: limits.max_push_constants_size, + max_inter_stage_shader_variables: 16, min_uniform_buffer_offset_alignment: limits.min_uniform_buffer_offset_alignment as u32, min_storage_buffer_offset_alignment: limits.min_storage_buffer_offset_alignment as u32, max_inter_stage_shader_components: limits diff --git a/wgpu-info/src/human.rs b/wgpu-info/src/human.rs index 08f2c0b3b5a..8542389e5e5 100644 --- a/wgpu-info/src/human.rs +++ b/wgpu-info/src/human.rs @@ -146,6 +146,7 @@ fn print_adapter(output: &mut impl io::Write, report: &AdapterReport, idx: usize max_buffer_size, max_vertex_attributes, max_vertex_buffer_array_stride, + max_inter_stage_shader_variables, min_uniform_buffer_offset_alignment, min_storage_buffer_offset_alignment, max_inter_stage_shader_components, @@ -198,6 +199,7 @@ fn print_adapter(output: &mut impl io::Write, report: &AdapterReport, idx: usize writeln!(output, "\t\t Min Subgroup Size: {min_subgroup_size}")?; writeln!(output, "\t\t Max Subgroup Size: {max_subgroup_size}")?; writeln!(output, "\t\t Max Immediate data Size: {max_immediate_size}")?; + writeln!(output, "\t\t Max Inter-stage Shader Variables: {max_inter_stage_shader_variables}")?; writeln!(output, "\t\t Min Uniform Buffer Offset Alignment: {min_uniform_buffer_offset_alignment}")?; writeln!(output, "\t\t Min Storage Buffer Offset Alignment: {min_storage_buffer_offset_alignment}")?; writeln!(output, "\t\t Max Inter-Stage Shader Component: {max_inter_stage_shader_components}")?; diff --git a/wgpu-types/src/limits.rs b/wgpu-types/src/limits.rs index 3527e391865..c09b7ef430b 100644 --- a/wgpu-types/src/limits.rs +++ b/wgpu-types/src/limits.rs @@ -175,6 +175,11 @@ pub struct Limits { /// Maximum value for `VertexBufferLayout::array_stride` when creating a `RenderPipeline`. /// Defaults to 2048. Higher is "better". pub max_vertex_buffer_array_stride: u32, + /// Maximum value for the number of input or output variables for inter-stage communication + /// (like vertex outputs or fragment inputs) `@location(…)`s (in WGSL parlance) + /// when creating a `RenderPipeline`. + /// Defaults to 16. Higher is "better". + pub max_inter_stage_shader_variables: u32, /// Required `BufferBindingType::Uniform` alignment for `BufferBinding::offset` /// when creating a `BindGroup`, or for `set_bind_group` `dynamicOffsets`. /// Defaults to 256. Lower is "better". @@ -305,6 +310,7 @@ impl Limits { /// max_buffer_size: 256 << 20, // (256 MiB) /// max_vertex_attributes: 16, /// max_vertex_buffer_array_stride: 2048, + /// max_inter_stage_shader_variables: 16, /// min_uniform_buffer_offset_alignment: 256, /// min_storage_buffer_offset_alignment: 256, /// max_inter_stage_shader_components: 60, @@ -358,6 +364,7 @@ impl Limits { max_buffer_size: 256 << 20, // (256 MiB) max_vertex_attributes: 16, max_vertex_buffer_array_stride: 2048, + max_inter_stage_shader_variables: 16, min_uniform_buffer_offset_alignment: 256, min_storage_buffer_offset_alignment: 256, max_inter_stage_shader_components: 60, @@ -417,6 +424,7 @@ impl Limits { /// min_subgroup_size: 0, /// max_subgroup_size: 0, /// max_immediate_size: 0, + /// max_inter_stage_shader_variables: 16, /// min_uniform_buffer_offset_alignment: 256, /// min_storage_buffer_offset_alignment: 256, /// max_inter_stage_shader_components: 60, @@ -494,6 +502,7 @@ impl Limits { /// min_subgroup_size: 0, /// max_subgroup_size: 0, /// max_immediate_size: 0, + /// max_inter_stage_shader_variables: 16, /// min_uniform_buffer_offset_alignment: 256, /// min_storage_buffer_offset_alignment: 256, /// max_inter_stage_shader_components: 31, diff --git a/wgpu/src/backend/webgpu.rs b/wgpu/src/backend/webgpu.rs index 24502f912fb..bb1ed433b3a 100644 --- a/wgpu/src/backend/webgpu.rs +++ b/wgpu/src/backend/webgpu.rs @@ -809,6 +809,7 @@ fn map_wgt_limits(limits: webgpu_sys::GpuSupportedLimits) -> wgt::Limits { max_buffer_size: limits.max_buffer_size() as u64, max_vertex_attributes: limits.max_vertex_attributes(), max_vertex_buffer_array_stride: limits.max_vertex_buffer_array_stride(), + max_inter_stage_shader_variables: limits.max_inter_stage_shader_variables(), min_uniform_buffer_offset_alignment: limits.min_uniform_buffer_offset_alignment(), min_storage_buffer_offset_alignment: limits.min_storage_buffer_offset_alignment(), max_color_attachments: limits.max_color_attachments(),