1use 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
75pub(crate) struct BucketedAdapterInfo {
77 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 pub fn is_compatible(&self, limits: &Limits, info: &AdapterInfo, features: Features) -> bool {
115 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 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 (
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
207pub 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
232const 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
252pub(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
276const UPLEVEL: Bucket = Bucket {
291 name: "uplevel-defaults",
292 limits: Limits {
293 max_bind_groups: 8,
294 max_buffer_size: 1 << 30, max_color_attachment_bytes_per_sample: 64,
298 max_compute_invocations_per_workgroup: 1024,
300 max_compute_workgroup_size_x: 1024,
301 max_compute_workgroup_size_y: 1024,
302 max_compute_workgroup_storage_size: 32 << 10, max_inter_stage_shader_variables: 28,
308 max_storage_textures_per_shader_stage: 8,
317 max_texture_array_layers: 2048,
318 max_texture_dimension_1d: 16384,
319 max_texture_dimension_2d: 16384,
320 max_vertex_attributes: 29,
324 ..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 .union(Features::TEXTURE_COMPRESSION_BC)
340 .union(Features::TEXTURE_COMPRESSION_BC_SLICED_3D)
341 .union(Features::TIMESTAMP_QUERY)
343 .union(Features::INDIRECT_FIRST_INSTANCE)
344 .union(Features::RG11B10UFLOAT_RENDERABLE)
346 .union(Features::BGRA8UNORM_STORAGE)
347 .union(Features::FLOAT32_FILTERABLE)
348 .union(Features::DUAL_SOURCE_BLENDING)
351 .union(Features::PRIMITIVE_INDEX)
353 .union(Features::SUBGROUP)
355 .union(Features::IMMEDIATES),
356};
357
358const 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, max_vertex_attributes: 31,
366 ..UPLEVEL.limits
367 },
368 info: BucketedAdapterInfo {
369 subgroup_min_size: 4,
370 subgroup_max_size: 64,
371 ..UPLEVEL.info
372 },
373 features: UPLEVEL
374 .features
375 .union(Features::TEXTURE_COMPRESSION_ASTC)
376 .union(Features::TEXTURE_COMPRESSION_ASTC_SLICED_3D)
377 .union(Features::TEXTURE_COMPRESSION_ETC2)
378 .union(Features::SHADER_F16)
379 .union(Features::FLOAT32_BLENDABLE)
380 .union(Features::CLIP_DISTANCES),
381};
382
383const BUCKET_A2: Bucket = Bucket {
385 name: "a2",
386 limits: Limits {
387 max_color_attachment_bytes_per_sample: 128,
388 max_compute_workgroup_storage_size: 64 << 10, max_sampled_textures_per_shader_stage: 48,
390 max_storage_buffer_binding_size: 1 << 30, max_storage_buffers_per_shader_stage: 16,
392 max_vertex_attributes: 32,
393 ..UPLEVEL.limits
394 },
395 info: BucketedAdapterInfo {
396 subgroup_min_size: 64,
397 subgroup_max_size: 64,
398 ..UPLEVEL.info
399 },
400 features: UPLEVEL.features.union(Features::SHADER_F16),
401};
402
403const BUCKET_I1: Bucket = Bucket {
405 name: "i1",
406 limits: Limits {
407 max_color_attachment_bytes_per_sample: 128,
408 max_sampled_textures_per_shader_stage: 48,
409 max_storage_buffer_binding_size: 1 << 29, max_storage_buffers_per_shader_stage: 16,
411 ..UPLEVEL.limits
412 },
413 info: BucketedAdapterInfo {
414 subgroup_min_size: 8,
415 subgroup_max_size: 32,
416 ..UPLEVEL.info
417 },
418 features: UPLEVEL.features.union(Features::SHADER_F16),
419};
420
421const BUCKET_N1: Bucket = Bucket {
423 name: "n1",
424 limits: Limits {
425 max_color_attachment_bytes_per_sample: 128,
426 max_compute_workgroup_storage_size: 48 << 10, max_sampled_textures_per_shader_stage: 48,
428 max_storage_buffer_binding_size: 1 << 30, max_storage_buffers_per_shader_stage: 16,
430 max_vertex_attributes: 32,
431 ..UPLEVEL.limits
432 },
433 info: BucketedAdapterInfo {
434 subgroup_min_size: 32,
435 subgroup_max_size: 32,
436 ..UPLEVEL.info
437 },
438 features: UPLEVEL.features.union(Features::SHADER_F16),
439};
440
441const BUCKET_A1: Bucket = Bucket {
443 name: "a1",
444 limits: Limits {
445 max_color_attachment_bytes_per_sample: 128,
446 max_sampled_textures_per_shader_stage: 48,
447 max_storage_buffer_binding_size: 1 << 30, max_storage_buffers_per_shader_stage: 16,
449 max_vertex_attributes: 32,
450 ..UPLEVEL.limits
451 },
452 info: BucketedAdapterInfo {
453 subgroup_min_size: 32,
454 subgroup_max_size: 64,
455 ..UPLEVEL.info
456 },
457 features: UPLEVEL.features.union(Features::SHADER_F16),
458};
459
460const BUCKET_NO_F16: Bucket = Bucket {
462 name: "no-f16",
463 limits: Limits {
464 max_color_attachment_bytes_per_sample: 128,
465 max_compute_workgroup_storage_size: 48 << 10, max_sampled_textures_per_shader_stage: 48,
467 max_storage_buffer_binding_size: 1 << 30, max_storage_buffers_per_shader_stage: 16,
469 max_vertex_attributes: 32,
470 ..UPLEVEL.limits
471 },
472 info: BucketedAdapterInfo {
473 subgroup_min_size: 32,
474 subgroup_max_size: 64,
475 ..UPLEVEL.info
476 },
477 features: UPLEVEL.features,
478};
479
480const BUCKET_LLVMPIPE: Bucket = Bucket {
481 name: "llvmpipe",
482 limits: Limits {
483 max_color_attachment_bytes_per_sample: 128,
484 max_sampled_textures_per_shader_stage: 48,
485 max_storage_buffers_per_shader_stage: 16,
486 max_vertex_attributes: 32,
487 ..UPLEVEL.limits
488 },
489 info: BucketedAdapterInfo {
490 is_fallback_adapter: true,
491 subgroup_min_size: 8,
492 subgroup_max_size: 8,
493 },
494 features: UPLEVEL
495 .features
496 .union(Features::SHADER_F16)
497 .union(Features::CLIP_DISTANCES),
498};
499
500const BUCKET_WARP: Bucket = Bucket {
502 name: "warp",
503 limits: Limits {
504 max_color_attachment_bytes_per_sample: 128,
505 max_sampled_textures_per_shader_stage: 48,
506 max_storage_buffers_per_shader_stage: 16,
507 max_vertex_attributes: 32,
508 ..UPLEVEL.limits
509 },
510 info: BucketedAdapterInfo {
511 is_fallback_adapter: true,
512 subgroup_min_size: 4,
513 subgroup_max_size: 128,
514 },
515 features: UPLEVEL.features.union(Features::SHADER_F16),
516};
517
518const BUCKET_DEFAULT: Bucket = Bucket {
520 name: "default",
521 limits: Limits::defaults(),
522 info: BucketedAdapterInfo::defaults(),
523 features: Features::empty(),
524};
525
526const BUCKET_FALLBACK: Bucket = Bucket {
528 name: "fallback",
529 limits: Limits::defaults(),
530 info: BucketedAdapterInfo {
531 is_fallback_adapter: true,
532 ..BucketedAdapterInfo::defaults()
533 },
534 features: Features::empty(),
535};
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540 use wgt::Features;
541
542 #[test]
543 fn enumerate_webgpu_features() {
544 let difference = Features::all_webgpu_mask().difference(
545 Features::DEPTH_CLIP_CONTROL
546 .union(Features::DEPTH32FLOAT_STENCIL8)
547 .union(Features::TEXTURE_COMPRESSION_ASTC)
548 .union(Features::TEXTURE_COMPRESSION_ASTC_SLICED_3D)
549 .union(Features::TEXTURE_COMPRESSION_BC)
550 .union(Features::TEXTURE_COMPRESSION_BC_SLICED_3D)
551 .union(Features::TEXTURE_COMPRESSION_ETC2)
552 .union(Features::TIMESTAMP_QUERY)
553 .union(Features::INDIRECT_FIRST_INSTANCE)
554 .union(Features::SHADER_F16)
555 .union(Features::RG11B10UFLOAT_RENDERABLE)
556 .union(Features::BGRA8UNORM_STORAGE)
557 .union(Features::FLOAT32_FILTERABLE)
558 .union(Features::FLOAT32_BLENDABLE)
559 .union(Features::CLIP_DISTANCES)
560 .union(Features::DUAL_SOURCE_BLENDING)
561 .union(Features::SUBGROUP)
562 .union(Features::PRIMITIVE_INDEX)
565 .union(Features::IMMEDIATES),
568 );
569 assert!(
570 difference.is_empty(),
571 "New WebGPU features should be assigned to appropriate limit buckets; missing {difference:?}"
572 );
573 }
574
575 #[test]
576 fn relationships() {
577 for bucket in [
579 &BUCKET_M1,
580 &BUCKET_A2,
581 &BUCKET_I1,
582 &BUCKET_N1,
583 &BUCKET_A1,
584 &BUCKET_NO_F16,
585 &BUCKET_WARP,
586 &BUCKET_LLVMPIPE,
587 ] {
588 let info = AdapterInfo {
589 subgroup_min_size: bucket.info.subgroup_min_size,
590 subgroup_max_size: bucket.info.subgroup_max_size,
591 ..AdapterInfo::new(
592 DeviceType::DiscreteGpu, wgt::Backend::Noop,
594 )
595 };
596 assert!(
597 UPLEVEL.is_compatible(&bucket.limits, &info, bucket.features),
598 "Bucket `{}` should be a superset of UPLEVEL",
599 bucket.name(),
600 );
601 }
602 }
603}