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::FLOAT32_BLENDABLE)
349 .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::CLIP_DISTANCES),
380};
381
382const BUCKET_A2: Bucket = Bucket {
384 name: "a2",
385 limits: Limits {
386 max_color_attachment_bytes_per_sample: 128,
387 max_compute_workgroup_storage_size: 64 << 10, max_sampled_textures_per_shader_stage: 48,
389 max_storage_buffer_binding_size: 1 << 30, max_storage_buffers_per_shader_stage: 16,
391 max_vertex_attributes: 30,
392 ..UPLEVEL.limits
393 },
394 info: BucketedAdapterInfo {
395 subgroup_min_size: 64,
396 subgroup_max_size: 64,
397 ..UPLEVEL.info
398 },
399 features: UPLEVEL.features.union(Features::SHADER_F16),
400};
401
402const BUCKET_I1: Bucket = Bucket {
404 name: "i1",
405 limits: Limits {
406 max_color_attachment_bytes_per_sample: 128,
407 max_sampled_textures_per_shader_stage: 48,
408 max_storage_buffer_binding_size: 1 << 29, max_storage_buffers_per_shader_stage: 16,
410 ..UPLEVEL.limits
411 },
412 info: BucketedAdapterInfo {
413 subgroup_min_size: 8,
414 subgroup_max_size: 32,
415 ..UPLEVEL.info
416 },
417 features: UPLEVEL.features.union(Features::SHADER_F16),
418};
419
420const BUCKET_N1: Bucket = Bucket {
422 name: "n1",
423 limits: Limits {
424 max_color_attachment_bytes_per_sample: 128,
425 max_compute_workgroup_storage_size: 48 << 10, max_sampled_textures_per_shader_stage: 48,
427 max_storage_buffer_binding_size: 1 << 30, max_storage_buffers_per_shader_stage: 16,
429 max_vertex_attributes: 30,
430 ..UPLEVEL.limits
431 },
432 info: BucketedAdapterInfo {
433 subgroup_min_size: 32,
434 subgroup_max_size: 32,
435 ..UPLEVEL.info
436 },
437 features: UPLEVEL.features.union(Features::SHADER_F16),
438};
439
440const BUCKET_A1: Bucket = Bucket {
442 name: "a1",
443 limits: Limits {
444 max_color_attachment_bytes_per_sample: 128,
445 max_sampled_textures_per_shader_stage: 48,
446 max_storage_buffer_binding_size: 1 << 30, max_storage_buffers_per_shader_stage: 16,
448 max_vertex_attributes: 30,
449 ..UPLEVEL.limits
450 },
451 info: BucketedAdapterInfo {
452 subgroup_min_size: 32,
453 subgroup_max_size: 64,
454 ..UPLEVEL.info
455 },
456 features: UPLEVEL.features.union(Features::SHADER_F16),
457};
458
459const BUCKET_NO_F16: Bucket = Bucket {
461 name: "no-f16",
462 limits: Limits {
463 max_color_attachment_bytes_per_sample: 128,
464 max_compute_workgroup_storage_size: 48 << 10, max_sampled_textures_per_shader_stage: 48,
466 max_storage_buffer_binding_size: 1 << 30, max_storage_buffers_per_shader_stage: 16,
468 max_vertex_attributes: 30,
469 ..UPLEVEL.limits
470 },
471 info: BucketedAdapterInfo {
472 subgroup_min_size: 32,
473 subgroup_max_size: 64,
474 ..UPLEVEL.info
475 },
476 features: UPLEVEL.features,
477};
478
479const BUCKET_LLVMPIPE: Bucket = Bucket {
480 name: "llvmpipe",
481 limits: Limits {
482 max_color_attachment_bytes_per_sample: 128,
483 max_sampled_textures_per_shader_stage: 48,
484 max_storage_buffers_per_shader_stage: 16,
485 max_vertex_attributes: 32,
486 ..UPLEVEL.limits
487 },
488 info: BucketedAdapterInfo {
489 is_fallback_adapter: true,
490 subgroup_min_size: 8,
491 subgroup_max_size: 8,
492 },
493 features: UPLEVEL
494 .features
495 .union(Features::SHADER_F16)
496 .union(Features::CLIP_DISTANCES),
497};
498
499const BUCKET_WARP: Bucket = Bucket {
501 name: "warp",
502 limits: Limits {
503 max_color_attachment_bytes_per_sample: 128,
504 max_sampled_textures_per_shader_stage: 48,
505 max_storage_buffers_per_shader_stage: 16,
506 max_vertex_attributes: 30,
507 ..UPLEVEL.limits
508 },
509 info: BucketedAdapterInfo {
510 is_fallback_adapter: true,
511 subgroup_min_size: 4,
512 subgroup_max_size: 128,
513 },
514 features: UPLEVEL.features.union(Features::SHADER_F16),
515};
516
517const BUCKET_DEFAULT: Bucket = Bucket {
519 name: "default",
520 limits: Limits::defaults(),
521 info: BucketedAdapterInfo::defaults(),
522 features: Features::empty(),
523};
524
525const BUCKET_FALLBACK: Bucket = Bucket {
527 name: "fallback",
528 limits: Limits::defaults(),
529 info: BucketedAdapterInfo {
530 is_fallback_adapter: true,
531 ..BucketedAdapterInfo::defaults()
532 },
533 features: Features::empty(),
534};
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539 use wgt::Features;
540
541 #[test]
542 fn enumerate_webgpu_features() {
543 let difference = Features::all_webgpu_mask().difference(
544 Features::DEPTH_CLIP_CONTROL
545 .union(Features::DEPTH32FLOAT_STENCIL8)
546 .union(Features::TEXTURE_COMPRESSION_ASTC)
547 .union(Features::TEXTURE_COMPRESSION_ASTC_SLICED_3D)
548 .union(Features::TEXTURE_COMPRESSION_BC)
549 .union(Features::TEXTURE_COMPRESSION_BC_SLICED_3D)
550 .union(Features::TEXTURE_COMPRESSION_ETC2)
551 .union(Features::TIMESTAMP_QUERY)
552 .union(Features::INDIRECT_FIRST_INSTANCE)
553 .union(Features::SHADER_F16)
554 .union(Features::RG11B10UFLOAT_RENDERABLE)
555 .union(Features::BGRA8UNORM_STORAGE)
556 .union(Features::FLOAT32_FILTERABLE)
557 .union(Features::FLOAT32_BLENDABLE)
558 .union(Features::CLIP_DISTANCES)
559 .union(Features::DUAL_SOURCE_BLENDING)
560 .union(Features::SUBGROUP)
561 .union(Features::PRIMITIVE_INDEX)
564 .union(Features::IMMEDIATES),
567 );
568 assert!(
569 difference.is_empty(),
570 "New WebGPU features should be assigned to appropriate limit buckets; missing {difference:?}"
571 );
572 }
573
574 #[test]
575 fn relationships() {
576 for bucket in [
578 &BUCKET_M1,
579 &BUCKET_A2,
580 &BUCKET_I1,
581 &BUCKET_N1,
582 &BUCKET_A1,
583 &BUCKET_NO_F16,
584 &BUCKET_WARP,
585 &BUCKET_LLVMPIPE,
586 ] {
587 let info = AdapterInfo {
588 subgroup_min_size: bucket.info.subgroup_min_size,
589 subgroup_max_size: bucket.info.subgroup_max_size,
590 ..AdapterInfo::new(
591 DeviceType::DiscreteGpu, wgt::Backend::Noop,
593 )
594 };
595 assert!(
596 UPLEVEL.is_compatible(&bucket.limits, &info, bucket.features),
597 "Bucket `{}` should be a superset of UPLEVEL",
598 bucket.name(),
599 );
600 }
601 }
602}