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
232pub(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
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,
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 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_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
385const 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, max_sampled_textures_per_shader_stage: 48,
392 max_storage_buffer_binding_size: 1 << 30, 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
407const 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, 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
427const 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, max_sampled_textures_per_shader_stage: 48,
434 max_storage_buffer_binding_size: 1 << 30, 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
449const 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, 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
470const 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, max_sampled_textures_per_shader_stage: 48,
477 max_storage_buffer_binding_size: 1 << 30, 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
514const 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
534const BUCKET_DEFAULT: Bucket = Bucket {
536 name: "default",
537 limits: Limits::defaults(),
538 info: BucketedAdapterInfo::defaults(),
539 features: Features::empty(),
540};
541
542const 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::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 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, 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}