Skip to main content

pfx_gpu/
floor.rs

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}