1use std::fmt;
2
3#[derive(Clone, Debug, PartialEq, Eq)]
4pub enum GpuShortfall {
5 NoAdapter,
6 Device(String),
7 Surface(String),
8 Feature(&'static str),
9 Limit {
10 name: &'static str,
11 needed: u64,
12 has: u64,
13 },
14 Format {
15 format: wgpu::TextureFormat,
16 needs: &'static str,
17 },
18}
19
20#[derive(Clone, Debug, PartialEq)]
21pub struct GpuRefusal {
22 pub shortfall: GpuShortfall,
23 pub adapter: Option<Box<wgpu::AdapterInfo>>,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct FormatNeed {
28 pub format: wgpu::TextureFormat,
29 pub usages: wgpu::TextureUsages,
30 pub flags: wgpu::TextureFormatFeatureFlags,
31 pub needs: &'static str,
32}
33
34#[derive(Clone, Debug, PartialEq)]
35pub struct Floor {
36 pub features: wgpu::Features,
37 pub downlevel: wgpu::DownlevelFlags,
38 pub limits: wgpu::Limits,
39 pub formats: Vec<FormatNeed>,
40}
41
42#[derive(Clone, Debug, PartialEq)]
43pub struct Capabilities {
44 pub features: wgpu::Features,
45 pub downlevel: wgpu::DownlevelFlags,
46 pub limits: wgpu::Limits,
47}
48
49pub const OPTIONAL_FEATURES: wgpu::Features = wgpu::Features::TIMESTAMP_QUERY
50 .union(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS)
51 .union(wgpu::Features::FLOAT32_FILTERABLE)
52 .union(wgpu::Features::TEXTURE_COMPRESSION_BC)
53 .union(wgpu::Features::PIPELINE_CACHE)
54 .union(wgpu::Features::EXPERIMENTAL_RAY_QUERY);
55
56pub const SAMPLED_TEXTURES_PER_STAGE: u32 = 18;
57pub const TEXTURE_SIDE: u32 = 16_384;
58pub const TRACE_STORAGE_BUFFERS_PER_STAGE: u32 = 15;
59pub const TRACE_BYTES_PER_PIXEL: u64 = 48;
60
61fn need(
62 format: wgpu::TextureFormat,
63 usages: wgpu::TextureUsages,
64 flags: wgpu::TextureFormatFeatureFlags,
65 needs: &'static str,
66) -> FormatNeed {
67 FormatNeed {
68 format,
69 usages,
70 flags,
71 needs,
72 }
73}
74
75pub fn live_floor() -> Floor {
76 use wgpu::DownlevelFlags as D;
77 use wgpu::TextureFormat as F;
78 use wgpu::TextureFormatFeatureFlags as Flags;
79 use wgpu::TextureUsages as U;
80 let target = U::RENDER_ATTACHMENT | U::TEXTURE_BINDING;
81 let storage = U::STORAGE_BINDING | U::TEXTURE_BINDING;
82 Floor {
83 features: wgpu::Features::empty(),
84 downlevel: D::COMPUTE_SHADERS
85 | D::FRAGMENT_STORAGE
86 | D::VERTEX_STORAGE
87 | D::CUBE_ARRAY_TEXTURES
88 | D::COMPARISON_SAMPLERS
89 | D::DEPTH_TEXTURE_AND_BUFFER_COPIES
90 | D::NON_POWER_OF_TWO_MIPMAPPED_TEXTURES
91 | D::FULL_DRAW_INDEX_UINT32
92 | D::BUFFER_BINDINGS_NOT_16_BYTE_ALIGNED
93 | D::WEBGPU_TEXTURE_FORMAT_SUPPORT
94 | D::SHADER_F16_IN_F32,
95 limits: wgpu::Limits {
96 max_texture_dimension_2d: TEXTURE_SIDE,
97 max_sampled_textures_per_shader_stage: SAMPLED_TEXTURES_PER_STAGE,
98 ..wgpu::Limits::default()
99 },
100 formats: vec![
101 need(
102 F::Rgba16Float,
103 target | storage | U::COPY_SRC | U::COPY_DST,
104 Flags::FILTERABLE | Flags::BLENDABLE | Flags::STORAGE_WRITE_ONLY,
105 "scene colour, history, reflections and the post chain",
106 ),
107 need(
108 F::R32Float,
109 target | storage,
110 Flags::STORAGE_WRITE_ONLY,
111 "linear depth and depth history",
112 ),
113 need(
114 F::R32Uint,
115 target | storage,
116 Flags::STORAGE_WRITE_ONLY,
117 "object ids and id history",
118 ),
119 need(
120 F::Rgba8Snorm,
121 storage,
122 Flags::STORAGE_WRITE_ONLY,
123 "normal history",
124 ),
125 need(
126 F::Rgba8Unorm,
127 storage,
128 Flags::FILTERABLE | Flags::STORAGE_WRITE_ONLY,
129 "the room haze field and material maps",
130 ),
131 need(
132 F::Rgba32Uint,
133 target,
134 Flags::empty(),
135 "the shadow transmission map",
136 ),
137 need(F::Rg16Float, target, Flags::empty(), "velocity"),
138 need(F::R16Float, target, Flags::FILTERABLE, "the contact field"),
139 need(
140 F::R8Unorm,
141 target,
142 Flags::FILTERABLE | Flags::BLENDABLE,
143 "the reactive mask, canopy cookie and text atlas",
144 ),
145 need(
146 F::Rgba8UnormSrgb,
147 target,
148 Flags::FILTERABLE,
149 "base maps, content and the sRGB output",
150 ),
151 need(
152 F::Rgba32Float,
153 U::TEXTURE_BINDING,
154 Flags::empty(),
155 "impostor geometry",
156 ),
157 need(
158 F::Depth32Float,
159 target | U::COPY_SRC | U::COPY_DST,
160 Flags::empty(),
161 "depth, the shadow atlas and light faces",
162 ),
163 ],
164 }
165}
166
167pub fn trace_floor() -> Floor {
168 use wgpu::TextureFormat as F;
169 use wgpu::TextureFormatFeatureFlags as Flags;
170 use wgpu::TextureUsages as U;
171 Floor {
172 features: wgpu::Features::FLOAT32_FILTERABLE,
173 downlevel: wgpu::DownlevelFlags::COMPUTE_SHADERS,
174 limits: wgpu::Limits {
175 max_storage_buffers_per_shader_stage: TRACE_STORAGE_BUFFERS_PER_STAGE,
176 ..wgpu::Limits::default()
177 },
178 formats: vec![
179 need(
180 F::Rgba32Float,
181 U::STORAGE_BINDING | U::COPY_SRC,
182 Flags::STORAGE_WRITE_ONLY,
183 "the traced colour, albedo and normal",
184 ),
185 need(
186 F::Rgba32Float,
187 U::TEXTURE_BINDING | U::COPY_DST,
188 Flags::FILTERABLE,
189 "the tracer's environment, sampled linearly",
190 ),
191 ],
192 }
193}
194
195pub fn timestamps(features: wgpu::Features) -> bool {
196 features.contains(wgpu::Features::TIMESTAMP_QUERY)
197}
198
199pub fn shortfall(
200 floor: &Floor,
201 offered: &Capabilities,
202 format_features: impl Fn(wgpu::TextureFormat) -> wgpu::TextureFormatFeatures,
203) -> Option<GpuShortfall> {
204 if let Some((name, _)) = floor
205 .features
206 .difference(offered.features)
207 .iter_names()
208 .next()
209 {
210 return Some(GpuShortfall::Feature(name));
211 }
212 if let Some((name, _)) = floor
213 .downlevel
214 .difference(offered.downlevel)
215 .iter_names()
216 .next()
217 {
218 return Some(GpuShortfall::Feature(name));
219 }
220 let mut short = None;
221 floor
222 .limits
223 .check_limits_with_fail_fn(&offered.limits, true, |name, needed, has| {
224 short = Some(GpuShortfall::Limit { name, needed, has });
225 });
226 if short.is_some() {
227 return short;
228 }
229 floor.formats.iter().find_map(|need| {
230 let has = format_features(need.format);
231 (!has.allowed_usages.contains(need.usages) || !has.flags.contains(need.flags)).then_some(
232 GpuShortfall::Format {
233 format: need.format,
234 needs: need.needs,
235 },
236 )
237 })
238}
239
240#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
241pub struct StageBindings {
242 pub sampled_textures: u32,
243 pub samplers: u32,
244 pub storage_buffers: u32,
245 pub storage_textures: u32,
246 pub uniform_buffers: u32,
247}
248
249pub fn stage_bindings(
250 groups: &[&[wgpu::BindGroupLayoutEntry]],
251 stage: wgpu::ShaderStages,
252) -> StageBindings {
253 let mut sum = StageBindings::default();
254 for entry in groups.iter().flat_map(|group| group.iter()) {
255 if !entry.visibility.contains(stage) {
256 continue;
257 }
258 let count = entry.count.map_or(1, |count| count.get());
259 let slot = match entry.ty {
260 wgpu::BindingType::Texture { .. } => &mut sum.sampled_textures,
261 wgpu::BindingType::Sampler(_) => &mut sum.samplers,
262 wgpu::BindingType::StorageTexture { .. } => &mut sum.storage_textures,
263 wgpu::BindingType::Buffer {
264 ty: wgpu::BufferBindingType::Uniform,
265 ..
266 } => &mut sum.uniform_buffers,
267 wgpu::BindingType::Buffer { .. } => &mut sum.storage_buffers,
268 _ => continue,
269 };
270 *slot += count;
271 }
272 sum
273}
274
275pub fn layout_shortfall(
276 groups: &[&[wgpu::BindGroupLayoutEntry]],
277 limits: &wgpu::Limits,
278) -> Option<GpuShortfall> {
279 let groups_needed = groups.len() as u32;
280 if groups_needed > limits.max_bind_groups {
281 return Some(GpuShortfall::Limit {
282 name: "max_bind_groups",
283 needed: u64::from(groups_needed),
284 has: u64::from(limits.max_bind_groups),
285 });
286 }
287 [
288 wgpu::ShaderStages::VERTEX,
289 wgpu::ShaderStages::FRAGMENT,
290 wgpu::ShaderStages::COMPUTE,
291 ]
292 .into_iter()
293 .find_map(|stage| {
294 let sum = stage_bindings(groups, stage);
295 [
296 (
297 "max_sampled_textures_per_shader_stage",
298 sum.sampled_textures,
299 limits.max_sampled_textures_per_shader_stage,
300 ),
301 (
302 "max_samplers_per_shader_stage",
303 sum.samplers,
304 limits.max_samplers_per_shader_stage,
305 ),
306 (
307 "max_storage_buffers_per_shader_stage",
308 sum.storage_buffers,
309 limits.max_storage_buffers_per_shader_stage,
310 ),
311 (
312 "max_storage_textures_per_shader_stage",
313 sum.storage_textures,
314 limits.max_storage_textures_per_shader_stage,
315 ),
316 (
317 "max_uniform_buffers_per_shader_stage",
318 sum.uniform_buffers,
319 limits.max_uniform_buffers_per_shader_stage,
320 ),
321 ]
322 .into_iter()
323 .find(|(_, needed, has)| needed > has)
324 .map(|(name, needed, has)| GpuShortfall::Limit {
325 name,
326 needed: u64::from(needed),
327 has: u64::from(has),
328 })
329 })
330}
331
332impl GpuRefusal {
333 pub fn new(shortfall: GpuShortfall, adapter: Option<wgpu::AdapterInfo>) -> Self {
334 Self {
335 shortfall,
336 adapter: adapter.map(Box::new),
337 }
338 }
339}
340
341impl fmt::Display for GpuShortfall {
342 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343 match self {
344 Self::NoAdapter => write!(f, "no usable GPU"),
345 Self::Device(error) => write!(f, "the GPU refused a device: {error}"),
346 Self::Surface(error) => write!(f, "the GPU can't present to the window: {error}"),
347 Self::Feature(name) => write!(f, "the GPU lacks the feature {name}"),
348 Self::Limit { name, needed, has } => {
349 write!(f, "limit {name} of {needed} is beyond the adapter's {has}")
350 }
351 Self::Format { format, needs } => {
352 write!(f, "the GPU can't use {format:?} for {needs}")
353 }
354 }
355 }
356}
357
358pub fn backend_name(backend: wgpu::Backend) -> &'static str {
359 match backend {
360 wgpu::Backend::Noop => "noop",
361 wgpu::Backend::Vulkan => "Vulkan",
362 wgpu::Backend::Metal => "Metal",
363 wgpu::Backend::Dx12 => "DX12",
364 wgpu::Backend::Gl => "OpenGL",
365 wgpu::Backend::BrowserWebGpu => "WebGPU",
366 }
367}
368
369impl fmt::Display for GpuRefusal {
370 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371 match &self.adapter {
372 Some(info) => write!(
373 f,
374 "{} ({} {} {}, {})",
375 self.shortfall,
376 info.name,
377 info.driver,
378 info.driver_info,
379 backend_name(info.backend),
380 ),
381 None => write!(f, "{}", self.shortfall),
382 }
383 }
384}
385
386impl std::error::Error for GpuRefusal {}
387
388impl From<GpuRefusal> for String {
389 fn from(refusal: GpuRefusal) -> Self {
390 refusal.to_string()
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 fn offered(features: wgpu::Features, limits: wgpu::Limits) -> Capabilities {
399 Capabilities {
400 features,
401 downlevel: wgpu::DownlevelFlags::compliant(),
402 limits,
403 }
404 }
405
406 fn guaranteed(format: wgpu::TextureFormat) -> wgpu::TextureFormatFeatures {
407 format.guaranteed_format_features(wgpu::Features::empty())
408 }
409
410 fn radv() -> wgpu::Limits {
411 wgpu::Limits {
412 max_texture_dimension_2d: 32_768,
413 max_texture_array_layers: 8_192,
414 max_sampled_textures_per_shader_stage: 8_388_606,
415 max_storage_buffers_per_shader_stage: 8_388_606,
416 max_storage_textures_per_shader_stage: 8_388_606,
417 max_bind_groups: 8,
418 max_buffer_size: 1 << 40,
419 max_storage_buffer_binding_size: u64::from(u32::MAX),
420 ..wgpu::Limits::default()
421 }
422 }
423
424 #[test]
425 fn an_adapter_at_the_floor_passes() {
426 let floor = live_floor();
427 let at = offered(wgpu::Features::empty(), floor.limits.clone());
428 assert_eq!(shortfall(&floor, &at, guaranteed), None);
429 let above = offered(OPTIONAL_FEATURES, radv());
430 assert_eq!(shortfall(&floor, &above, guaranteed), None);
431 }
432
433 #[test]
434 fn a_limit_below_the_floor_is_named() {
435 let floor = live_floor();
436 let short = offered(
437 wgpu::Features::all_webgpu_mask(),
438 wgpu::Limits {
439 max_sampled_textures_per_shader_stage: 16,
440 ..radv()
441 },
442 );
443 assert_eq!(
444 shortfall(&floor, &short, guaranteed),
445 Some(GpuShortfall::Limit {
446 name: "max_sampled_textures_per_shader_stage",
447 needed: u64::from(SAMPLED_TEXTURES_PER_STAGE),
448 has: 16,
449 })
450 );
451 let small = offered(wgpu::Features::empty(), wgpu::Limits::default());
452 assert_eq!(
453 shortfall(&floor, &small, guaranteed),
454 Some(GpuShortfall::Limit {
455 name: "max_texture_dimension_2d",
456 needed: 16_384,
457 has: 8_192,
458 })
459 );
460 }
461
462 #[test]
463 fn without_timestamps_the_floor_passes_with_profiling_off() {
464 let floor = live_floor();
465 let plain = offered(wgpu::Features::FLOAT32_FILTERABLE, radv());
466 assert_eq!(shortfall(&floor, &plain, guaranteed), None);
467 assert!(!timestamps(plain.features));
468 let timed = offered(OPTIONAL_FEATURES, radv());
469 assert_eq!(shortfall(&floor, &timed, guaranteed), None);
470 assert!(timestamps(timed.features));
471 }
472
473 #[test]
474 fn a_missing_downlevel_capability_is_named() {
475 let floor = live_floor();
476 let mut old = offered(wgpu::Features::empty(), radv());
477 old.downlevel
478 .remove(wgpu::DownlevelFlags::CUBE_ARRAY_TEXTURES);
479 assert_eq!(
480 shortfall(&floor, &old, guaranteed),
481 Some(GpuShortfall::Feature("CUBE_ARRAY_TEXTURES"))
482 );
483 }
484
485 #[test]
486 fn a_format_without_its_usage_is_named() {
487 let floor = live_floor();
488 let at = offered(wgpu::Features::empty(), radv());
489 let no_storage = |format: wgpu::TextureFormat| {
490 let mut features = guaranteed(format);
491 if format == wgpu::TextureFormat::R32Float {
492 features
493 .allowed_usages
494 .remove(wgpu::TextureUsages::STORAGE_BINDING);
495 }
496 features
497 };
498 assert_eq!(
499 shortfall(&floor, &at, no_storage),
500 Some(GpuShortfall::Format {
501 format: wgpu::TextureFormat::R32Float,
502 needs: "linear depth and depth history",
503 })
504 );
505 }
506
507 #[test]
508 fn the_first_shortfall_wins() {
509 let floor = Floor {
510 features: wgpu::Features::FLOAT32_FILTERABLE,
511 ..live_floor()
512 };
513 let short = offered(wgpu::Features::empty(), wgpu::Limits::default());
514 assert_eq!(
515 shortfall(&floor, &short, guaranteed),
516 Some(GpuShortfall::Feature("FLOAT32_FILTERABLE"))
517 );
518 }
519
520 fn filterable(format: wgpu::TextureFormat) -> wgpu::TextureFormatFeatures {
521 format.guaranteed_format_features(wgpu::Features::FLOAT32_FILTERABLE)
522 }
523
524 #[test]
525 fn an_adapter_at_the_trace_floor_passes() {
526 let floor = trace_floor();
527 let at = offered(floor.features, floor.limits.clone());
528 assert_eq!(shortfall(&floor, &at, filterable), None);
529 let above = offered(OPTIONAL_FEATURES, radv());
530 assert_eq!(shortfall(&floor, &above, filterable), None);
531 let defaults = wgpu::Limits::default();
532 assert_eq!(
533 floor.limits.max_storage_buffer_binding_size,
534 defaults.max_storage_buffer_binding_size
535 );
536 assert_eq!(floor.limits.max_buffer_size, defaults.max_buffer_size);
537 }
538
539 #[test]
540 fn the_default_limits_lack_only_the_trace_floors_storage_buffers() {
541 let floor = trace_floor();
542 let plain = offered(wgpu::Features::FLOAT32_FILTERABLE, wgpu::Limits::default());
543 assert_eq!(
544 shortfall(&floor, &plain, filterable),
545 Some(GpuShortfall::Limit {
546 name: "max_storage_buffers_per_shader_stage",
547 needed: u64::from(TRACE_STORAGE_BUFFERS_PER_STAGE),
548 has: 8,
549 })
550 );
551 let enough = offered(
552 wgpu::Features::FLOAT32_FILTERABLE,
553 wgpu::Limits {
554 max_storage_buffers_per_shader_stage: TRACE_STORAGE_BUFFERS_PER_STAGE,
555 ..wgpu::Limits::default()
556 },
557 );
558 assert_eq!(shortfall(&floor, &enough, filterable), None);
559 }
560
561 #[test]
562 fn the_trace_floor_needs_filterable_float32_and_storage_writes() {
563 let floor = trace_floor();
564 let unfiltered = offered(wgpu::Features::empty(), radv());
565 assert_eq!(
566 shortfall(&floor, &unfiltered, guaranteed),
567 Some(GpuShortfall::Feature("FLOAT32_FILTERABLE"))
568 );
569 let at = offered(wgpu::Features::FLOAT32_FILTERABLE, radv());
570 assert_eq!(
571 shortfall(&floor, &at, guaranteed),
572 Some(GpuShortfall::Format {
573 format: wgpu::TextureFormat::Rgba32Float,
574 needs: "the tracer's environment, sampled linearly",
575 })
576 );
577 let no_storage = |format: wgpu::TextureFormat| {
578 let mut features = filterable(format);
579 features
580 .flags
581 .remove(wgpu::TextureFormatFeatureFlags::STORAGE_WRITE_ONLY);
582 features
583 };
584 assert_eq!(
585 shortfall(&floor, &at, no_storage),
586 Some(GpuShortfall::Format {
587 format: wgpu::TextureFormat::Rgba32Float,
588 needs: "the traced colour, albedo and normal",
589 })
590 );
591 let mut old = offered(wgpu::Features::FLOAT32_FILTERABLE, radv());
592 old.downlevel.remove(wgpu::DownlevelFlags::COMPUTE_SHADERS);
593 assert_eq!(
594 shortfall(&floor, &old, filterable),
595 Some(GpuShortfall::Feature("COMPUTE_SHADERS"))
596 );
597 }
598
599 fn texture(binding: u32, visibility: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
600 wgpu::BindGroupLayoutEntry {
601 binding,
602 visibility,
603 ty: wgpu::BindingType::Texture {
604 sample_type: wgpu::TextureSampleType::Float { filterable: true },
605 view_dimension: wgpu::TextureViewDimension::D2,
606 multisampled: false,
607 },
608 count: None,
609 }
610 }
611
612 #[test]
613 fn stage_sums_add_every_group_a_stage_sees() {
614 let fragment = wgpu::ShaderStages::FRAGMENT;
615 let one: Vec<_> = (0..10).map(|binding| texture(binding, fragment)).collect();
616 let mut two: Vec<_> = (0..8).map(|binding| texture(binding, fragment)).collect();
617 two.push(texture(8, wgpu::ShaderStages::VERTEX_FRAGMENT));
618 two.push(wgpu::BindGroupLayoutEntry {
619 count: std::num::NonZeroU32::new(3),
620 ..texture(9, wgpu::ShaderStages::VERTEX)
621 });
622 let groups = [one.as_slice(), two.as_slice()];
623 assert_eq!(stage_bindings(&groups, fragment).sampled_textures, 19);
624 assert_eq!(
625 stage_bindings(&groups, wgpu::ShaderStages::VERTEX).sampled_textures,
626 4
627 );
628 assert_eq!(
629 layout_shortfall(&groups, &wgpu::Limits::default()),
630 Some(GpuShortfall::Limit {
631 name: "max_sampled_textures_per_shader_stage",
632 needed: 19,
633 has: 16,
634 })
635 );
636 assert_eq!(
637 layout_shortfall(&groups[..1], &wgpu::Limits::default()),
638 None
639 );
640 }
641
642 fn adapter(backend: wgpu::Backend) -> wgpu::AdapterInfo {
643 wgpu::AdapterInfo {
644 name: "adapter".into(),
645 vendor: 0,
646 device: 0,
647 device_type: wgpu::DeviceType::DiscreteGpu,
648 driver: "driver".into(),
649 driver_info: "info".into(),
650 ..wgpu::AdapterInfo::new(wgpu::DeviceType::DiscreteGpu, backend)
651 }
652 }
653
654 #[test]
655 fn a_refusal_names_the_adapter_apart_from_the_shortfall() {
656 let refusal = GpuRefusal::new(
657 GpuShortfall::Limit {
658 name: "max_texture_dimension_2d",
659 needed: 16_384,
660 has: 8_192,
661 },
662 Some(wgpu::AdapterInfo {
663 name: "llvmpipe".into(),
664 ..adapter(wgpu::Backend::Vulkan)
665 }),
666 );
667 assert_eq!(
668 refusal.adapter.as_ref().map(|info| info.name.as_str()),
669 Some("llvmpipe")
670 );
671 let text = refusal.to_string();
672 assert!(text.contains("max_texture_dimension_2d"), "{text}");
673 assert!(text.contains("llvmpipe"), "{text}");
674 assert!(text.contains("Vulkan"), "{text}");
675 assert_eq!(GpuShortfall::NoAdapter.to_string(), "no usable GPU");
676 }
677
678 #[test]
679 fn a_refusal_names_vulkan_dx12_and_metal() {
680 for (backend, name) in [
681 (wgpu::Backend::Vulkan, "Vulkan"),
682 (wgpu::Backend::Dx12, "DX12"),
683 (wgpu::Backend::Metal, "Metal"),
684 ] {
685 let text = GpuRefusal::new(
686 GpuShortfall::Feature("FLOAT32_FILTERABLE"),
687 Some(adapter(backend)),
688 )
689 .to_string();
690 assert!(text.contains(name), "{text}");
691 assert_eq!(backend_name(backend), name);
692 }
693 }
694
695 #[test]
696 fn dx12_tier_2_and_3_hold_the_floors_and_tier_1_at_feature_level_11_0_does_not() {
697 let live = live_floor().limits;
698 let trace = trace_floor().limits;
699 let flat = wgpu::Limits::default();
700 let tier2_sampled_textures = 1_000_000u32;
701 let tier2_sampled_with_ray_tracing = tier2_sampled_textures / 2;
702 let tier2_storage_buffers = 16u32;
703 let tier2_storage_textures = 16u32;
704 let texture_side = 16_384u32;
705 assert!(tier2_sampled_with_ray_tracing >= live.max_sampled_textures_per_shader_stage);
706 assert!(texture_side >= live.max_texture_dimension_2d);
707 assert!(tier2_storage_textures >= live.max_storage_textures_per_shader_stage);
708 assert!(tier2_storage_buffers >= trace.max_storage_buffers_per_shader_stage);
709 assert!(tier2_sampled_textures >= flat.max_sampled_textures_per_shader_stage);
710 assert!(tier2_storage_buffers >= flat.max_storage_buffers_per_shader_stage);
711 assert!(tier2_storage_textures >= flat.max_storage_textures_per_shader_stage);
712 assert!(texture_side >= flat.max_texture_dimension_2d);
713 let tier3_descriptors = 1u32 << 20;
714 let tier3_storage = tier3_descriptors / 4;
715 assert!(tier3_descriptors / 2 >= live.max_sampled_textures_per_shader_stage);
716 assert!(tier3_storage >= trace.max_storage_buffers_per_shader_stage);
717 assert!(tier3_storage >= flat.max_storage_buffers_per_shader_stage);
718 let tier1_feature_level_11_0_storage = 2u32;
719 assert!(tier1_feature_level_11_0_storage < flat.max_storage_buffers_per_shader_stage);
720 assert!(tier1_feature_level_11_0_storage < flat.max_storage_textures_per_shader_stage);
721 assert!(tier1_feature_level_11_0_storage < trace.max_storage_buffers_per_shader_stage);
722 assert!(tier1_feature_level_11_0_storage < live.max_storage_textures_per_shader_stage);
723 }
724}