Skip to main content

wgpu_core/
limits.rs

1//! Functionality related to device and adapter limits.
2//!
3//! # Limit Bucketing
4//!
5//! Web browsers make various information about their operating environment
6//! available to content to provide a better experience. For example, content is
7//! able to detect whether the device has a touch-screen, in order to provide an
8//! appropriate user interface.
9//!
10//! [Browser fingerprinting][bfp] employs this information for the purpose of
11//! constructing a unique "fingerprint" value that is unique to a single browser
12//! or shared among a relatively small number of browsers. Fingerprinting can be
13//! used for various purposes, including to identify and track users across
14//! different websites.
15//!
16//! Limit bucketing can reduce the ability to fingerprint users based on GPU
17//! hardware characteristics when using `wgpu` in applications like a web
18//! browser.
19//!
20//! When limit bucketing is enabled, the adapter limits offered by `wgpu` do not
21//! necessarily reflect the exact capabilities of the hardware. Instead, the
22//! hardware capabilities are rounded down to one of several pre-defined buckets.
23//! The goal of doing this is for there to be enough devices assigned to each
24//! bucket that knowledge of which bucket applies is minimally useful for
25//! fingerprinting.
26//!
27//! Limit bucketing may be requested by setting `apply_limit_buckets` in
28//! [`wgt::RequestAdapterOptions`] or by setting `apply_limit_buckets` to
29//! true when calling [`enumerate_adapters`].
30//!
31//! If your application does not expose `wgpu` to untrusted content, limit
32//! bucketing is not necessary.
33//!
34//! [bfp]: https://support.mozilla.org/en-US/kb/firefox-protection-against-fingerprinting
35//! [`enumerate_adapters`]: `crate::instance::Instance::enumerate_adapters`
36
37use core::mem;
38
39use alloc::{borrow::Cow, vec::Vec};
40use thiserror::Error;
41use wgt::error::{ErrorType, WebGpuError};
42use wgt::{AdapterInfo, AdapterLimitBucketInfo, DeviceType, Features, Limits};
43
44use crate::api_log;
45
46#[derive(Clone, Debug, Error)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48#[error("Limit '{name}' value {requested} is better than allowed {allowed}")]
49pub struct FailedLimit {
50    name: Cow<'static, str>,
51    requested: u64,
52    allowed: u64,
53}
54
55impl WebGpuError for FailedLimit {
56    fn webgpu_error_type(&self) -> ErrorType {
57        ErrorType::Validation
58    }
59}
60
61pub(crate) fn check_limits(requested: &Limits, allowed: &Limits) -> Vec<FailedLimit> {
62    let mut failed = Vec::new();
63
64    requested.check_limits_with_fail_fn(allowed, false, |name, requested, allowed| {
65        failed.push(FailedLimit {
66            name: Cow::Borrowed(name),
67            requested,
68            allowed,
69        })
70    });
71
72    failed
73}
74
75/// Fields in [`wgt::AdapterInfo`] relevant to limit bucketing.
76pub(crate) struct BucketedAdapterInfo {
77    // Equivalent to `adapter.info.device_type == wgt::DeviceType::Cpu`
78    is_fallback_adapter: bool,
79
80    subgroup_min_size: u32,
81    subgroup_max_size: u32,
82}
83
84impl BucketedAdapterInfo {
85    const fn defaults() -> Self {
86        Self {
87            is_fallback_adapter: false,
88            subgroup_min_size: 4,
89            subgroup_max_size: 128,
90        }
91    }
92}
93
94impl Default for BucketedAdapterInfo {
95    fn default() -> Self {
96        Self::defaults()
97    }
98}
99
100pub(crate) struct Bucket {
101    name: &'static str,
102    limits: Limits,
103    info: BucketedAdapterInfo,
104    features: Features,
105}
106
107impl Bucket {
108    pub fn name(&self) -> &'static str {
109        self.name
110    }
111
112    /// Returns `true` if the device having `limits`, `info`, and `features` satisfies
113    /// the bucket definition in `self`.
114    pub fn is_compatible(&self, limits: &Limits, info: &AdapterInfo, features: Features) -> bool {
115        // In the context of limit checks, "allowed" or "available" means
116        // what the device supports. If an application requests an
117        // unsupported value, the error message might say "limit of {} exceeds
118        // allowed value {}". For purposes of bucket compatibility, the bucket values
119        // take the place of application-requested values. If the bucket value
120        // is beyond what the device supports, then the device does not qualify
121        // for that bucket.
122        let candidate_is_fallback_adapter = info.device_type == DeviceType::Cpu;
123
124        let failing_limits = check_limits(&self.limits, limits);
125        let limits_ok = failing_limits.is_empty();
126
127        if !limits_ok {
128            log::debug!("Failing limits: {:#?}", failing_limits);
129        }
130
131        let bucket_has_subgroups = self.features.contains(Features::SUBGROUP);
132        let subgroups_ok = !bucket_has_subgroups
133            || info.subgroup_min_size >= self.info.subgroup_min_size
134                && info.subgroup_max_size <= self.info.subgroup_max_size;
135        if !subgroups_ok {
136            log::debug!(
137                "Subgroup min/max {}/{} is not compatible with allowed {}/{}",
138                self.info.subgroup_min_size,
139                self.info.subgroup_max_size,
140                info.subgroup_min_size,
141                info.subgroup_max_size,
142            );
143        }
144
145        let features_ok = features.contains(self.features);
146        if !features_ok {
147            log::debug!("{:?} are not available", self.features - features);
148        }
149
150        limits_ok
151            && candidate_is_fallback_adapter == self.info.is_fallback_adapter
152            && subgroups_ok
153            && features_ok
154    }
155
156    pub fn try_apply_to(&self, adapter: &mut hal::DynExposedAdapter) -> bool {
157        if !self.is_compatible(
158            &adapter.capabilities.limits,
159            &adapter.info,
160            adapter.features,
161        ) {
162            log::debug!("bucket `{}` is not compatible", self.name);
163            return false;
164        }
165
166        let raw_limits = mem::replace(&mut adapter.capabilities.limits, self.limits.clone());
167
168        // Features in EXEMPT_FEATURES are not affected by limit bucketing.
169        let exposed_features = adapter
170            .features
171            .intersection(EXEMPT_FEATURES)
172            .union(self.features);
173        let raw_features = mem::replace(&mut adapter.features, exposed_features);
174
175        let (bucket_subgroup_min_size, bucket_subgroup_max_size) =
176            if self.features.contains(Features::SUBGROUP) {
177                (self.info.subgroup_min_size, self.info.subgroup_max_size)
178            } else {
179                // WebGPU requires that we report these values when subgroups are
180                // not supported
181                (
182                    wgt::MINIMUM_SUBGROUP_MIN_SIZE,
183                    wgt::MAXIMUM_SUBGROUP_MAX_SIZE,
184                )
185            };
186        let raw_subgroup_min_size = mem::replace(
187            &mut adapter.info.subgroup_min_size,
188            bucket_subgroup_min_size,
189        );
190        let raw_subgroup_max_size = mem::replace(
191            &mut adapter.info.subgroup_max_size,
192            bucket_subgroup_max_size,
193        );
194
195        adapter.info.limit_bucket = Some(AdapterLimitBucketInfo {
196            name: Cow::Borrowed(self.name),
197            raw_limits,
198            raw_features,
199            raw_subgroup_min_size,
200            raw_subgroup_max_size,
201        });
202
203        true
204    }
205}
206
207/// Apply [limit bucketing][lt] to the adapter limits and features in `raw`.
208///
209/// Finds a supported bucket and replaces the capabilities with the set defined by
210/// the bucket. If no suitable bucket is found, returns `None`, but this should only
211/// happen with downlevel devices, and attempting to use limit bucketing with
212/// downlevel devices is not recommended.
213///
214/// [lt]: self#Limit-bucketing
215pub fn apply_limit_buckets(mut raw: hal::DynExposedAdapter) -> Option<hal::DynExposedAdapter> {
216    for bucket in buckets() {
217        if bucket.try_apply_to(&mut raw) {
218            let name = bucket.name();
219            api_log!("Applied limit bucket `{name}`");
220            return Some(raw);
221        }
222    }
223    log::warn!(
224        "No suitable limit bucket found for device with {:?}, {:?}, {:?}",
225        raw.capabilities.limits,
226        raw.info,
227        raw.features,
228    );
229    None
230}
231
232/// These features are left alone by limit bucketing. They will be exposed to higher layers
233/// whenever the device supports them, and they are not considered when determining bucket
234/// compatibility.
235///
236/// All four features in the list are related to external textures. (The texture format
237/// features are used internally by Firefox to support external textures.)
238///
239/// Handling them this way is a bit of a kludge, but is expected to be a short-term
240/// situation only until external texture support is universally available.
241///
242/// Note that while NV12 and P010 can be hidden from content by excluding them from WebIDL,
243/// TEXTURE_FORMATS_16BIT_NORM will eventually be replaced with TEXTURE_FORMATS_TIER1, and
244/// at that point neither excluding the tier1 formats from WebIDL entirely nor allowing
245/// content to use them on a device that doesn't have the feature enabled will be
246/// acceptable. See <https://github.com/gfx-rs/wgpu/issues/8122>.
247pub(crate) const EXEMPT_FEATURES: Features = Features::EXTERNAL_TEXTURE
248    .union(Features::TEXTURE_FORMAT_NV12)
249    .union(Features::TEXTURE_FORMAT_P010)
250    .union(Features::TEXTURE_FORMAT_16BIT_NORM);
251
252/// Return the defined adapter feature/limit buckets
253///
254/// Buckets are not always subsets of preceding buckets, but [`enumerate_adapters`]
255/// considers them in the order they are listed here and uses the first bucket satisfied
256/// by the device.
257///
258/// [`enumerate_adapters`]: `crate::instance::Instance::enumerate_adapters`
259pub(crate) fn buckets() -> impl Iterator<Item = &'static Bucket> {
260    [
261        &BUCKET_M1,
262        &BUCKET_A2,
263        &BUCKET_I1,
264        &BUCKET_N1,
265        &BUCKET_A1,
266        &BUCKET_NO_F16,
267        &BUCKET_LLVMPIPE,
268        &BUCKET_WARP,
269        &BUCKET_DEFAULT,
270        &BUCKET_FALLBACK,
271    ]
272    .iter()
273    .copied()
274}
275
276// The following limits could be higher for some hardware, but are capped where they
277// are to avoid introducing platform or backend dependencies.
278//
279// **`max_vertex_attributes`:** While there is broad support for 32, Intel hardware with
280// Vulkan only supports 29; the D3D12 backend is also limited to 30.
281// See <https://gitlab.freedesktop.org/mesa/mesa/-/blob/465c186fc5f72c51bda943ac0e19f6512f8e6262/src/intel/vulkan/anv_private.h#L188>.
282//
283// **`max_dynamic_{storage,uniform}_buffers_per_pipeline_layout`:** These are limited to
284// 4 and 8 by DX12.
285
286// UPLEVEL is not a bucket that is actually applied to devices. It serves as a baseline from
287// which most of the rest of the buckets are derived. (It could be a real bucket if desired,
288// but since UPLEVEL is an intersection across many devices, there is usually a better match
289// for any particular device.)
290const UPLEVEL: Bucket = Bucket {
291    name: "uplevel-defaults",
292    limits: Limits {
293        max_bind_groups: 8,
294        // use default max_bind_groups_plus_vertex_buffers
295        // use default max_bindings_per_bind_group
296        max_buffer_size: 1 << 30, // 1 GB
297        max_color_attachment_bytes_per_sample: 64,
298        // use default max_color_attachments
299        max_compute_invocations_per_workgroup: 1024,
300        max_compute_workgroup_size_x: 1024,
301        max_compute_workgroup_size_y: 1024,
302        // use default max_compute_workgroup_size_z
303        max_compute_workgroup_storage_size: 32 << 10, // 32 kB
304        // use default max_compute_workgroups_per_dimension
305        // use default max_dynamic_storage_buffers_per_pipeline_layout
306        // use default max_dynamic_uniform_buffers_per_pipeline_layout
307        max_inter_stage_shader_variables: 28,
308        // use default max_sampled_textures_per_shader_stage
309        // use default max_samplers_per_shader_stage
310        // use default max_storage_buffer_binding_size
311        // use default max_storage_buffers_per_shader_stage
312        // use default max_storage_buffers_in_vertex_stage
313        // use default max_storage_buffers_in_fragment_stage
314        max_storage_textures_per_shader_stage: 8,
315        max_storage_textures_in_vertex_stage: 8,
316        max_storage_textures_in_fragment_stage: 8,
317        max_texture_array_layers: 2048,
318        max_texture_dimension_1d: 16384,
319        max_texture_dimension_2d: 16384,
320        // use default max_texture_dimension_3d
321        // use default max_uniform_buffer_binding_size
322        // use default max_uniform_buffers_per_shader_stage
323        max_vertex_attributes: 29,
324        // use default max_vertex_buffer_array_stride
325        // use default max_vertex_buffers
326        // use default min_storage_buffer_offset_alignment
327        // use default min_uniform_buffer_offset_alignment
328        ..Limits::defaults()
329    },
330    info: BucketedAdapterInfo {
331        is_fallback_adapter: false,
332        subgroup_min_size: 4,
333        subgroup_max_size: 128,
334    },
335    features: Features::DEPTH_CLIP_CONTROL
336        .union(Features::DEPTH32FLOAT_STENCIL8)
337        // omit TEXTURE_COMPRESSION_ASTC
338        // omit TEXTURE_COMPRESSION_ASTC_SLICED_3D
339        .union(Features::TEXTURE_COMPRESSION_BC)
340        .union(Features::TEXTURE_COMPRESSION_BC_SLICED_3D)
341        // omit TEXTURE_COMPRESSION_ETC2
342        .union(Features::TIMESTAMP_QUERY)
343        .union(Features::INDIRECT_FIRST_INSTANCE)
344        // omit SHADER_F16
345        .union(Features::RG11B10UFLOAT_RENDERABLE)
346        .union(Features::BGRA8UNORM_STORAGE)
347        .union(Features::FLOAT32_FILTERABLE)
348        .union(Features::FLOAT32_BLENDABLE)
349        // CLIP_DISTANCES not implemented in wgpu dx12 backend; https://github.com/gfx-rs/wgpu/issues/6236
350        .union(Features::DUAL_SOURCE_BLENDING)
351        // TIER1/TIER2 not implemented in wgpu; https://github.com/gfx-rs/wgpu/issues/8122
352        .union(Features::PRIMITIVE_INDEX)
353        // TEXTURE_COMPONENT_SWIZZLE not implemented in wgpu; https://github.com/gfx-rs/wgpu/issues/1028
354        .union(Features::SUBGROUP)
355        .union(Features::IMMEDIATES),
356};
357
358// e.g. Apple M Series
359const BUCKET_M1: Bucket = Bucket {
360    name: "m1",
361    limits: Limits {
362        max_dynamic_uniform_buffers_per_pipeline_layout: 12,
363        max_sampled_textures_per_shader_stage: 48,
364        max_storage_buffer_binding_size: 1 << 30, // 1 GB,
365        max_storage_buffers_per_shader_stage: 9,
366        max_storage_buffers_in_vertex_stage: 9,
367        max_storage_buffers_in_fragment_stage: 9,
368        max_vertex_attributes: 31,
369        ..UPLEVEL.limits
370    },
371    info: BucketedAdapterInfo {
372        subgroup_min_size: 4,
373        subgroup_max_size: 64,
374        ..UPLEVEL.info
375    },
376    features: UPLEVEL
377        .features
378        .union(Features::TEXTURE_COMPRESSION_ASTC)
379        .union(Features::TEXTURE_COMPRESSION_ASTC_SLICED_3D)
380        .union(Features::TEXTURE_COMPRESSION_ETC2)
381        .union(Features::SHADER_F16)
382        .union(Features::CLIP_DISTANCES),
383};
384
385// e.g. Radeon Vega
386const BUCKET_A2: Bucket = Bucket {
387    name: "a2",
388    limits: Limits {
389        max_color_attachment_bytes_per_sample: 128,
390        max_compute_workgroup_storage_size: 64 << 10, // 64 kB,
391        max_sampled_textures_per_shader_stage: 48,
392        max_storage_buffer_binding_size: 1 << 30, // 1 GB,
393        max_storage_buffers_per_shader_stage: 16,
394        max_storage_buffers_in_vertex_stage: 16,
395        max_storage_buffers_in_fragment_stage: 16,
396        max_vertex_attributes: 30,
397        ..UPLEVEL.limits
398    },
399    info: BucketedAdapterInfo {
400        subgroup_min_size: 64,
401        subgroup_max_size: 64,
402        ..UPLEVEL.info
403    },
404    features: UPLEVEL.features.union(Features::SHADER_F16),
405};
406
407// e.g. Intel Arc, UHD 600 Series, Iris Xe
408const BUCKET_I1: Bucket = Bucket {
409    name: "i1",
410    limits: Limits {
411        max_color_attachment_bytes_per_sample: 128,
412        max_sampled_textures_per_shader_stage: 48,
413        max_storage_buffer_binding_size: 1 << 29, // 512 MB,
414        max_storage_buffers_per_shader_stage: 16,
415        max_storage_buffers_in_vertex_stage: 16,
416        max_storage_buffers_in_fragment_stage: 16,
417        ..UPLEVEL.limits
418    },
419    info: BucketedAdapterInfo {
420        subgroup_min_size: 8,
421        subgroup_max_size: 32,
422        ..UPLEVEL.info
423    },
424    features: UPLEVEL.features.union(Features::SHADER_F16),
425};
426
427// e.g. GeForce GTX 1650, GeForce RTX 20, 30, 40, 50 Series
428const BUCKET_N1: Bucket = Bucket {
429    name: "n1",
430    limits: Limits {
431        max_color_attachment_bytes_per_sample: 128,
432        max_compute_workgroup_storage_size: 48 << 10, // 48 kB,
433        max_sampled_textures_per_shader_stage: 48,
434        max_storage_buffer_binding_size: 1 << 30, // 1 GB,
435        max_storage_buffers_per_shader_stage: 16,
436        max_storage_buffers_in_vertex_stage: 16,
437        max_storage_buffers_in_fragment_stage: 16,
438        max_vertex_attributes: 30,
439        ..UPLEVEL.limits
440    },
441    info: BucketedAdapterInfo {
442        subgroup_min_size: 32,
443        subgroup_max_size: 32,
444        ..UPLEVEL.info
445    },
446    features: UPLEVEL.features.union(Features::SHADER_F16),
447};
448
449// e.g. Radeon RX 6000, 7000, 9000 Series
450const BUCKET_A1: Bucket = Bucket {
451    name: "a1",
452    limits: Limits {
453        max_color_attachment_bytes_per_sample: 128,
454        max_sampled_textures_per_shader_stage: 48,
455        max_storage_buffer_binding_size: 1 << 30, // 1 GB,
456        max_storage_buffers_per_shader_stage: 16,
457        max_storage_buffers_in_vertex_stage: 16,
458        max_storage_buffers_in_fragment_stage: 16,
459        max_vertex_attributes: 30,
460        ..UPLEVEL.limits
461    },
462    info: BucketedAdapterInfo {
463        subgroup_min_size: 32,
464        subgroup_max_size: 64,
465        ..UPLEVEL.info
466    },
467    features: UPLEVEL.features.union(Features::SHADER_F16),
468};
469
470// e.g. GeForce GTX 1050, Radeon WX 5100
471const BUCKET_NO_F16: Bucket = Bucket {
472    name: "no-f16",
473    limits: Limits {
474        max_color_attachment_bytes_per_sample: 128,
475        max_compute_workgroup_storage_size: 48 << 10, // 48 kB
476        max_sampled_textures_per_shader_stage: 48,
477        max_storage_buffer_binding_size: 1 << 30, // 1 GB
478        max_storage_buffers_per_shader_stage: 16,
479        max_storage_buffers_in_vertex_stage: 16,
480        max_storage_buffers_in_fragment_stage: 16,
481        max_vertex_attributes: 30,
482        ..UPLEVEL.limits
483    },
484    info: BucketedAdapterInfo {
485        subgroup_min_size: 32,
486        subgroup_max_size: 64,
487        ..UPLEVEL.info
488    },
489    features: UPLEVEL.features,
490};
491
492const BUCKET_LLVMPIPE: Bucket = Bucket {
493    name: "llvmpipe",
494    limits: Limits {
495        max_color_attachment_bytes_per_sample: 128,
496        max_sampled_textures_per_shader_stage: 48,
497        max_storage_buffers_per_shader_stage: 16,
498        max_storage_buffers_in_vertex_stage: 16,
499        max_storage_buffers_in_fragment_stage: 16,
500        max_vertex_attributes: 32,
501        ..UPLEVEL.limits
502    },
503    info: BucketedAdapterInfo {
504        is_fallback_adapter: true,
505        subgroup_min_size: 8,
506        subgroup_max_size: 8,
507    },
508    features: UPLEVEL
509        .features
510        .union(Features::SHADER_F16)
511        .union(Features::CLIP_DISTANCES),
512};
513
514// a.k.a. Microsoft Basic Render Driver
515const BUCKET_WARP: Bucket = Bucket {
516    name: "warp",
517    limits: Limits {
518        max_color_attachment_bytes_per_sample: 128,
519        max_sampled_textures_per_shader_stage: 48,
520        max_storage_buffers_per_shader_stage: 16,
521        max_storage_buffers_in_vertex_stage: 16,
522        max_storage_buffers_in_fragment_stage: 16,
523        max_vertex_attributes: 30,
524        ..UPLEVEL.limits
525    },
526    info: BucketedAdapterInfo {
527        is_fallback_adapter: true,
528        subgroup_min_size: 4,
529        subgroup_max_size: 128,
530    },
531    features: UPLEVEL.features.union(Features::SHADER_F16),
532};
533
534// WebGPU default limits, not a fallback adapter
535const BUCKET_DEFAULT: Bucket = Bucket {
536    name: "default",
537    limits: Limits::defaults(),
538    info: BucketedAdapterInfo::defaults(),
539    features: Features::empty(),
540};
541
542// WebGPU default limits, is a fallback adapter
543const BUCKET_FALLBACK: Bucket = Bucket {
544    name: "fallback",
545    limits: Limits::defaults(),
546    info: BucketedAdapterInfo {
547        is_fallback_adapter: true,
548        ..BucketedAdapterInfo::defaults()
549    },
550    features: Features::empty(),
551};
552
553#[cfg(test)]
554mod tests {
555    use super::*;
556    use wgt::Features;
557
558    #[test]
559    fn enumerate_webgpu_features() {
560        let difference = Features::all_webgpu_mask().difference(
561            Features::DEPTH_CLIP_CONTROL
562                .union(Features::DEPTH32FLOAT_STENCIL8)
563                .union(Features::TEXTURE_COMPRESSION_ASTC)
564                .union(Features::TEXTURE_COMPRESSION_ASTC_SLICED_3D)
565                .union(Features::TEXTURE_COMPRESSION_BC)
566                .union(Features::TEXTURE_COMPRESSION_BC_SLICED_3D)
567                .union(Features::TEXTURE_COMPRESSION_ETC2)
568                .union(Features::TIMESTAMP_QUERY)
569                .union(Features::INDIRECT_FIRST_INSTANCE)
570                .union(Features::SHADER_F16)
571                .union(Features::RG11B10UFLOAT_RENDERABLE)
572                .union(Features::BGRA8UNORM_STORAGE)
573                .union(Features::FLOAT32_FILTERABLE)
574                .union(Features::FLOAT32_BLENDABLE)
575                .union(Features::CLIP_DISTANCES)
576                .union(Features::DUAL_SOURCE_BLENDING)
577                .union(Features::SUBGROUP)
578                //.union(Features::TEXTURE_FORMATS_TIER1) not implemented
579                //.union(Features::TEXTURE_FORMATS_TIER2) not implemented
580                .union(Features::PRIMITIVE_INDEX)
581                .union(Features::TEXTURE_COMPONENT_SWIZZLE)
582                .union(Features::IMMEDIATES)
583                .union(Features::DEBUG_PRINTF),
584        );
585        assert!(
586            difference.is_empty(),
587            "New WebGPU features should be assigned to appropriate limit buckets; missing {difference:?}"
588        );
589    }
590
591    #[test]
592    fn relationships() {
593        // Check that each bucket is a superset of UPLEVEL, ignoring the `is_fallback_adapter` flag.
594        for bucket in [
595            &BUCKET_M1,
596            &BUCKET_A2,
597            &BUCKET_I1,
598            &BUCKET_N1,
599            &BUCKET_A1,
600            &BUCKET_NO_F16,
601            &BUCKET_WARP,
602            &BUCKET_LLVMPIPE,
603        ] {
604            let info = AdapterInfo {
605                subgroup_min_size: bucket.info.subgroup_min_size,
606                subgroup_max_size: bucket.info.subgroup_max_size,
607                ..AdapterInfo::new(
608                    DeviceType::DiscreteGpu, // not a fallback adapter
609                    wgt::Backend::Noop,
610                )
611            };
612            assert!(
613                UPLEVEL.is_compatible(&bucket.limits, &info, bucket.features),
614                "Bucket `{}` should be a superset of UPLEVEL",
615                bucket.name(),
616            );
617        }
618    }
619}