[deno] Fix some problems in the handling of device limits (#8085)

* Correct error type when a requested limit is too high
* Clamp requested limits to the WebGPU defaults

Fixes #8084
This commit is contained in:
Andy Leiserson 2025-08-20 14:58:56 -07:00 committed by GitHub
parent fe86710af7
commit e7a99be058
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 126 additions and 57 deletions

View File

@ -98,6 +98,7 @@ This allows using precompiled shaders without manually checking which backend's
- The function you pass to `Device::on_uncaptured_error()` must now implement `Sync` in addition to `Send`, and be wrapped in `Arc` instead of `Box`. - The function you pass to `Device::on_uncaptured_error()` must now implement `Sync` in addition to `Send`, and be wrapped in `Arc` instead of `Box`.
In exchange for this, it is no longer possible for calling `wgpu` functions while in that callback to cause a deadlock (not that we encourage you to actually do that). In exchange for this, it is no longer possible for calling `wgpu` functions while in that callback to cause a deadlock (not that we encourage you to actually do that).
By @kpreid in [#8011](https://github.com/gfx-rs/wgpu/pull/8011). By @kpreid in [#8011](https://github.com/gfx-rs/wgpu/pull/8011).
- The limits requested for a device must now satisfy `min_subgroup_size <= max_subgroup_size`. By @andyleiserson in [#8085](https://github.com/gfx-rs/wgpu/pull/8085).
#### Naga #### Naga

View File

@ -19,6 +19,7 @@ webgpu:api,operation,render_pass,storeOp:render_pass_store_op,color_attachment_w
webgpu:api,operation,render_pass,storeOp:render_pass_store_op,color_attachment_only:* webgpu:api,operation,render_pass,storeOp:render_pass_store_op,color_attachment_only:*
webgpu:api,operation,render_pass,storeOp:render_pass_store_op,multiple_color_attachments:* webgpu:api,operation,render_pass,storeOp:render_pass_store_op,multiple_color_attachments:*
webgpu:api,operation,render_pass,storeOp:render_pass_store_op,depth_stencil_attachment_only:* webgpu:api,operation,render_pass,storeOp:render_pass_store_op,depth_stencil_attachment_only:*
fails-if(dx12) webgpu:api,validation,capability_checks,limits,maxBindGroups:setBindGroup,*
webgpu:api,validation,createBindGroup:buffer,effective_buffer_binding_size:* webgpu:api,validation,createBindGroup:buffer,effective_buffer_binding_size:*
webgpu:api,validation,encoding,beginComputePass:* webgpu:api,validation,encoding,beginComputePass:*
webgpu:api,validation,encoding,beginRenderPass:* webgpu:api,validation,encoding,beginRenderPass:*

View File

@ -125,8 +125,16 @@ impl GPUAdapter {
return Err(CreateDeviceError::RequiredFeaturesNotASubset); return Err(CreateDeviceError::RequiredFeaturesNotASubset);
} }
let required_limits = // When support for compatibility mode is added, this will need to look
serde_json::from_value(serde_json::to_value(descriptor.required_limits)?)?; // at whether the adapter is "compatibility-defaulting" or
// "core-defaulting", and choose the appropriate set of defaults.
//
// Support for compatibility mode is tracked in
// https://github.com/gfx-rs/wgpu/issues/8124.
let required_limits = serde_json::from_value::<wgpu_types::Limits>(serde_json::to_value(
descriptor.required_limits,
)?)?
.or_better_values_from(&wgpu_types::Limits::default());
let trace = std::env::var_os("DENO_WEBGPU_TRACE") let trace = std::env::var_os("DENO_WEBGPU_TRACE")
.map(|path| wgpu_types::Trace::Directory(std::path::PathBuf::from(path))) .map(|path| wgpu_types::Trace::Directory(std::path::PathBuf::from(path)))
@ -196,7 +204,7 @@ pub enum CreateDeviceError {
#[class(inherit)] #[class(inherit)]
#[error(transparent)] #[error(transparent)]
Serde(#[from] serde_json::Error), Serde(#[from] serde_json::Error),
#[class(type)] #[class("DOMExceptionOperationError")]
#[error(transparent)] #[error(transparent)]
Device(#[from] wgpu_core::instance::RequestDeviceError), Device(#[from] wgpu_core::instance::RequestDeviceError),
} }

View File

@ -16,6 +16,7 @@ extern crate alloc;
use alloc::borrow::Cow; use alloc::borrow::Cow;
use alloc::{string::String, vec, vec::Vec}; use alloc::{string::String, vec, vec::Vec};
use core::cmp::Ordering;
use core::{ use core::{
fmt, fmt,
hash::{Hash, Hasher}, hash::{Hash, Hasher},
@ -459,6 +460,71 @@ impl fmt::Display for RequestAdapterError {
} }
} }
/// Invoke a macro for each of the limits.
///
/// The supplied macro should take two arguments. The first is a limit name, as
/// an identifier, typically used to access a member of `struct Limits`. The
/// second is `Ordering::Less` if valid values are less than the limit (the
/// common case), or `Ordering::Greater` if valid values are more than the limit
/// (for limits like alignments, which are minima instead of maxima).
macro_rules! with_limits {
($macro_name:ident) => {
$macro_name!(max_texture_dimension_1d, Ordering::Less);
$macro_name!(max_texture_dimension_1d, Ordering::Less);
$macro_name!(max_texture_dimension_2d, Ordering::Less);
$macro_name!(max_texture_dimension_3d, Ordering::Less);
$macro_name!(max_texture_array_layers, Ordering::Less);
$macro_name!(max_bind_groups, Ordering::Less);
$macro_name!(max_bindings_per_bind_group, Ordering::Less);
$macro_name!(
max_dynamic_uniform_buffers_per_pipeline_layout,
Ordering::Less
);
$macro_name!(
max_dynamic_storage_buffers_per_pipeline_layout,
Ordering::Less
);
$macro_name!(max_sampled_textures_per_shader_stage, Ordering::Less);
$macro_name!(max_samplers_per_shader_stage, Ordering::Less);
$macro_name!(max_storage_buffers_per_shader_stage, Ordering::Less);
$macro_name!(max_storage_textures_per_shader_stage, Ordering::Less);
$macro_name!(max_uniform_buffers_per_shader_stage, Ordering::Less);
$macro_name!(max_binding_array_elements_per_shader_stage, Ordering::Less);
$macro_name!(max_uniform_buffer_binding_size, Ordering::Less);
$macro_name!(max_storage_buffer_binding_size, Ordering::Less);
$macro_name!(max_vertex_buffers, Ordering::Less);
$macro_name!(max_buffer_size, Ordering::Less);
$macro_name!(max_vertex_attributes, Ordering::Less);
$macro_name!(max_vertex_buffer_array_stride, Ordering::Less);
$macro_name!(min_uniform_buffer_offset_alignment, Ordering::Greater);
$macro_name!(min_storage_buffer_offset_alignment, Ordering::Greater);
$macro_name!(max_inter_stage_shader_components, Ordering::Less);
$macro_name!(max_color_attachments, Ordering::Less);
$macro_name!(max_color_attachment_bytes_per_sample, Ordering::Less);
$macro_name!(max_compute_workgroup_storage_size, Ordering::Less);
$macro_name!(max_compute_invocations_per_workgroup, Ordering::Less);
$macro_name!(max_compute_workgroup_size_x, Ordering::Less);
$macro_name!(max_compute_workgroup_size_y, Ordering::Less);
$macro_name!(max_compute_workgroup_size_z, Ordering::Less);
$macro_name!(max_compute_workgroups_per_dimension, Ordering::Less);
$macro_name!(min_subgroup_size, Ordering::Greater);
$macro_name!(max_subgroup_size, Ordering::Less);
$macro_name!(max_push_constant_size, Ordering::Less);
$macro_name!(max_non_sampler_bindings, Ordering::Less);
$macro_name!(max_task_workgroup_total_count, Ordering::Less);
$macro_name!(max_task_workgroups_per_dimension, Ordering::Less);
$macro_name!(max_mesh_multiview_count, Ordering::Less);
$macro_name!(max_mesh_output_layers, Ordering::Less);
$macro_name!(max_blas_primitive_count, Ordering::Less);
$macro_name!(max_blas_geometry_count, Ordering::Less);
$macro_name!(max_tlas_instance_count, Ordering::Less);
};
}
/// Represents the sets of limits an adapter/device supports. /// Represents the sets of limits an adapter/device supports.
/// ///
/// We provide three different defaults. /// We provide three different defaults.
@ -1015,68 +1081,61 @@ impl Limits {
fatal: bool, fatal: bool,
mut fail_fn: impl FnMut(&'static str, u64, u64), mut fail_fn: impl FnMut(&'static str, u64, u64),
) { ) {
use core::cmp::Ordering; macro_rules! check_with_fail_fn {
($name:ident, $ordering:expr) => {
macro_rules! compare { let invalid_ord = $ordering.reverse();
($name:ident, $ordering:ident) => { // In the case of `min_subgroup_size`, requesting a value of
match self.$name.cmp(&allowed.$name) { // zero means "I'm not going to use subgroups", so we have to
Ordering::$ordering | Ordering::Equal => (), // special case that. If any of our minimum limits could
_ => { // meaningfully go all the way to zero, that would conflict with
fail_fn(stringify!($name), self.$name as u64, allowed.$name as u64); // this.
if fatal { if self.$name != 0 && self.$name.cmp(&allowed.$name) == invalid_ord {
return; fail_fn(stringify!($name), self.$name as u64, allowed.$name as u64);
} if fatal {
return;
} }
} }
}; };
} }
compare!(max_texture_dimension_1d, Less); if self.min_subgroup_size > self.max_subgroup_size {
compare!(max_texture_dimension_2d, Less); fail_fn(
compare!(max_texture_dimension_3d, Less); "max_subgroup_size",
compare!(max_texture_array_layers, Less); self.min_subgroup_size as u64,
compare!(max_bind_groups, Less); allowed.min_subgroup_size as u64,
compare!(max_bindings_per_bind_group, Less); );
compare!(max_dynamic_uniform_buffers_per_pipeline_layout, Less);
compare!(max_dynamic_storage_buffers_per_pipeline_layout, Less);
compare!(max_sampled_textures_per_shader_stage, Less);
compare!(max_samplers_per_shader_stage, Less);
compare!(max_storage_buffers_per_shader_stage, Less);
compare!(max_storage_textures_per_shader_stage, Less);
compare!(max_uniform_buffers_per_shader_stage, Less);
compare!(max_binding_array_elements_per_shader_stage, Less);
compare!(max_uniform_buffer_binding_size, Less);
compare!(max_storage_buffer_binding_size, Less);
compare!(max_vertex_buffers, Less);
compare!(max_buffer_size, Less);
compare!(max_vertex_attributes, Less);
compare!(max_vertex_buffer_array_stride, Less);
compare!(min_uniform_buffer_offset_alignment, Greater);
compare!(min_storage_buffer_offset_alignment, Greater);
compare!(max_inter_stage_shader_components, Less);
compare!(max_color_attachments, Less);
compare!(max_color_attachment_bytes_per_sample, Less);
compare!(max_compute_workgroup_storage_size, Less);
compare!(max_compute_invocations_per_workgroup, Less);
compare!(max_compute_workgroup_size_x, Less);
compare!(max_compute_workgroup_size_y, Less);
compare!(max_compute_workgroup_size_z, Less);
compare!(max_compute_workgroups_per_dimension, Less);
if self.min_subgroup_size > 0 && self.max_subgroup_size > 0 {
compare!(min_subgroup_size, Greater);
compare!(max_subgroup_size, Less);
} }
compare!(max_push_constant_size, Less); with_limits!(check_with_fail_fn);
compare!(max_non_sampler_bindings, Less); }
compare!(max_task_workgroup_total_count, Less); /// For each limit in `other` that is better than the value in `self`,
compare!(max_task_workgroups_per_dimension, Less); /// replace the value in `self` with the value from `other`.
compare!(max_mesh_multiview_count, Less); ///
compare!(max_mesh_output_layers, Less); /// A request for a limit value less than the WebGPU-specified default must
/// be ignored. This function is used to clamp such requests to the default
/// value.
///
/// This function is not for clamping requests for values beyond the
/// supported limits. For that purpose the desired function would be
/// `or_worse_values_from` (which doesn't exist, but could be added if
/// needed).
#[must_use]
pub fn or_better_values_from(mut self, other: &Self) -> Self {
macro_rules! or_better_value_from {
($name:ident, $ordering:expr) => {
match $ordering {
// Limits that are maximum values (most of them)
Ordering::Less => self.$name = self.$name.max(other.$name),
// Limits that are minimum values
Ordering::Greater => self.$name = self.$name.min(other.$name),
Ordering::Equal => unreachable!(),
}
};
}
compare!(max_blas_primitive_count, Less); with_limits!(or_better_value_from);
compare!(max_blas_geometry_count, Less);
compare!(max_tlas_instance_count, Less); self
} }
} }