Skip to main content

alpha2_gpu/
capabilities.rs

1/// The capability class exposed to renderer code.
2#[derive(Copy, Clone, Debug, Eq, PartialEq)]
3#[non_exhaustive]
4pub enum CapabilityTier {
5    /// Full WebGPU-capable adapter.
6    TierA,
7    /// Conservative downlevel adapter.
8    TierB,
9}
10
11/// Internal compositing texture format.
12#[derive(Copy, Clone, Debug, Eq, PartialEq)]
13#[non_exhaustive]
14pub enum IntermediateFormat {
15    /// Half-float RGBA storage (the format does not specify a transfer curve).
16    Rgba16Float,
17    /// Normalized RGBA fallback.
18    Rgba8Unorm,
19}
20
21/// Adapter facts relevant to rendering decisions.
22#[derive(Clone, Debug)]
23#[non_exhaustive]
24pub struct Capabilities {
25    /// Compatibility classification.
26    pub tier: CapabilityTier,
27    /// Chosen intermediate format.
28    pub intermediate_format: IntermediateFormat,
29    /// Platform adapter name.
30    pub adapter_name: String,
31    /// Maximum 2D texture dimension.
32    pub max_texture_dimension_2d: u32,
33    /// Whether render-pass GPU timestamp queries were enabled on the device.
34    pub timestamp_queries: bool,
35    /// Backend selected by wgpu.
36    pub backend: String,
37    /// Adapter driver name.
38    pub driver: String,
39    /// Adapter driver detail/version when exposed by the platform.
40    pub driver_info: String,
41    /// Whether the adapter rasterizes on the CPU (lavapipe, llvmpipe, `SwiftShader`, WARP),
42    /// where shaders pay full cost per pixel regardless of skipped branches.
43    pub software_rasterizer: bool,
44    /// Whether `Rgba16Float` render targets support hardware blending.
45    pub half_float_blendable: bool,
46    /// The DX12 shader compiler (`FXC`, `DXC <path>`, ...); empty on other backends.
47    pub shader_compiler: String,
48}
49
50impl Capabilities {
51    /// Creates an explicit capability snapshot for policy simulation and tests.
52    #[must_use]
53    #[allow(clippy::too_many_arguments)]
54    pub fn new(
55        tier: CapabilityTier,
56        intermediate_format: IntermediateFormat,
57        adapter_name: impl Into<String>,
58        max_texture_dimension_2d: u32,
59        timestamp_queries: bool,
60        backend: impl Into<String>,
61        driver: impl Into<String>,
62        driver_info: impl Into<String>,
63        shader_compiler: impl Into<String>,
64    ) -> Self {
65        Self {
66            tier,
67            intermediate_format,
68            adapter_name: adapter_name.into(),
69            max_texture_dimension_2d,
70            timestamp_queries,
71            backend: backend.into(),
72            driver: driver.into(),
73            driver_info: driver_info.into(),
74            software_rasterizer: false,
75            half_float_blendable: intermediate_format == IntermediateFormat::Rgba16Float,
76            shader_compiler: shader_compiler.into(),
77        }
78    }
79
80    /// One line naming the device: `<adapter> (<backend>, driver <driver> <driver_info>,
81    /// <compiler>)`, empty parts left out.
82    #[must_use]
83    pub fn summary(&self) -> String {
84        let driver = [self.driver.as_str(), self.driver_info.as_str()]
85            .into_iter()
86            .filter(|part| !part.is_empty())
87            .collect::<Vec<_>>()
88            .join(" ");
89        let mut parts = vec![self.backend.clone()];
90        if !driver.is_empty() {
91            parts.push(format!("driver {driver}"));
92        }
93        if !self.shader_compiler.is_empty() {
94            parts.push(self.shader_compiler.clone());
95        }
96        format!("{} ({})", self.adapter_name, parts.join(", "))
97    }
98
99    /// Device facts reflect what the device was opened with; format facts come from the adapter.
100    pub(crate) fn from_device(adapter: &wgpu::Adapter, device: &wgpu::Device) -> Self {
101        let info = adapter.get_info();
102        let shader_compiler = if info.backend == wgpu::Backend::Dx12 {
103            crate::dx12_shader_compiler().to_string()
104        } else {
105            String::new()
106        };
107        let half_float = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba16Float);
108        let software_rasterizer = software_adapter(&info);
109        Self {
110            tier: tier_for(&adapter.get_downlevel_capabilities()),
111            intermediate_format: intermediate_format_for(half_float),
112            half_float_blendable: half_float
113                .allowed_usages
114                .contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
115                && half_float
116                    .flags
117                    .contains(wgpu::TextureFormatFeatureFlags::BLENDABLE),
118            adapter_name: info.name,
119            max_texture_dimension_2d: device.limits().max_texture_dimension_2d,
120            timestamp_queries: device.features().contains(wgpu::Features::TIMESTAMP_QUERY),
121            backend: format!("{:?}", info.backend),
122            driver: info.driver,
123            driver_info: info.driver_info,
124            software_rasterizer,
125            shader_compiler,
126        }
127    }
128}
129
130/// Software rasterizer names: wgpu-hal's GL list plus WARP, which browsers reach through ANGLE
131/// as "Microsoft Basic Render Driver" and wgpu-hal does not flag.
132const SOFTWARE_RENDERER_NAMES: [&str; 4] = [
133    "mesa offscreen",
134    "swiftshader",
135    "llvmpipe",
136    "microsoft basic render",
137];
138
139/// Whether `info` is a software adapter: `DeviceType::Cpu` or a name matched by
140/// [`software_renderer_name`].
141#[must_use]
142pub fn software_adapter(info: &wgpu::AdapterInfo) -> bool {
143    info.device_type == wgpu::DeviceType::Cpu || software_renderer_name(&info.name)
144}
145
146/// Whether a renderer or adapter name (including a browser's unmasked WebGL renderer string)
147/// is a software rasterizer's, ignoring case.
148#[must_use]
149pub fn software_renderer_name(name: &str) -> bool {
150    let name = name.to_ascii_lowercase();
151    SOFTWARE_RENDERER_NAMES
152        .iter()
153        .any(|software| name.contains(software))
154}
155
156/// Half float when `Rgba16Float` can be rendered to, filtered and copied, otherwise 8-bit. The
157/// tier is irrelevant: tier B GL adapters with half-float color buffers still get half float.
158fn intermediate_format_for(half_float: wgpu::TextureFormatFeatures) -> IntermediateFormat {
159    let required = wgpu::TextureUsages::RENDER_ATTACHMENT
160        | wgpu::TextureUsages::TEXTURE_BINDING
161        | wgpu::TextureUsages::COPY_SRC;
162    if half_float.allowed_usages.contains(required)
163        && half_float
164            .flags
165            .contains(wgpu::TextureFormatFeatureFlags::FILTERABLE)
166    {
167        IntermediateFormat::Rgba16Float
168    } else {
169        IntermediateFormat::Rgba8Unorm
170    }
171}
172
173/// Tier A for full WebGPU, tier B for downlevel adapters.
174fn tier_for(downlevel: &wgpu::DownlevelCapabilities) -> CapabilityTier {
175    if downlevel.is_webgpu_compliant() {
176        CapabilityTier::TierA
177    } else {
178        CapabilityTier::TierB
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use wgpu::{TextureFormatFeatureFlags as Flags, TextureUsages as Usages};
186
187    fn half_float(allowed_usages: Usages, flags: Flags) -> wgpu::TextureFormatFeatures {
188        wgpu::TextureFormatFeatures {
189            allowed_usages,
190            flags,
191        }
192    }
193
194    #[test]
195    fn half_float_is_the_intermediate_only_where_it_renders_filters_and_copies() {
196        let required = Usages::RENDER_ATTACHMENT | Usages::TEXTURE_BINDING | Usages::COPY_SRC;
197        assert_eq!(
198            intermediate_format_for(half_float(required | Usages::COPY_DST, Flags::FILTERABLE)),
199            IntermediateFormat::Rgba16Float
200        );
201        for missing in [
202            Usages::RENDER_ATTACHMENT,
203            Usages::TEXTURE_BINDING,
204            Usages::COPY_SRC,
205        ] {
206            assert_eq!(
207                intermediate_format_for(half_float(required - missing, Flags::FILTERABLE)),
208                IntermediateFormat::Rgba8Unorm,
209                "without {missing:?}"
210            );
211        }
212        assert_eq!(
213            intermediate_format_for(half_float(required, Flags::empty())),
214            IntermediateFormat::Rgba8Unorm,
215            "without filtering"
216        );
217        // No half float at all, as a WebGL2 context without a float colour buffer.
218        assert_eq!(
219            intermediate_format_for(half_float(Usages::empty(), Flags::empty())),
220            IntermediateFormat::Rgba8Unorm
221        );
222    }
223
224    #[test]
225    fn software_adapters_are_named_by_type_or_renderer() {
226        let info = |name: &str, device_type| wgpu::AdapterInfo {
227            name: name.to_owned(),
228            vendor: 0,
229            device: 0,
230            device_type,
231            device_pci_bus_id: String::new(),
232            driver: String::new(),
233            driver_info: String::new(),
234            backend: wgpu::Backend::Gl,
235            subgroup_min_size: 0,
236            subgroup_max_size: 0,
237            transient_saves_memory: None,
238            limit_bucket: None,
239        };
240        assert!(software_adapter(&info("", wgpu::DeviceType::Cpu)));
241        assert!(software_adapter(&info(
242            "ANGLE (Microsoft, Microsoft Basic Render Driver (0x0000008C) Direct3D11 vs_5_0 \
243             ps_5_0, D3D11)",
244            wgpu::DeviceType::Other
245        )));
246        assert!(software_adapter(&info(
247            "ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), \
248             SwiftShader driver)",
249            wgpu::DeviceType::Other
250        )));
251        assert!(!software_adapter(&info(
252            "ANGLE (AMD, AMD Radeon Graphics (0x0000164E) Direct3D11 vs_5_0 ps_5_0, D3D11)",
253            wgpu::DeviceType::Other
254        )));
255        assert!(!software_adapter(&info(
256            "Intel(R) UHD Graphics",
257            wgpu::DeviceType::IntegratedGpu
258        )));
259    }
260
261    #[test]
262    fn the_tier_follows_webgpu_compliance_alone() {
263        assert_eq!(
264            tier_for(&wgpu::DownlevelCapabilities::default()),
265            CapabilityTier::TierA
266        );
267        let mut downlevel = wgpu::DownlevelCapabilities::default();
268        downlevel
269            .flags
270            .remove(wgpu::DownlevelFlags::COMPUTE_SHADERS);
271        assert_eq!(tier_for(&downlevel), CapabilityTier::TierB);
272        let shader_model = wgpu::DownlevelCapabilities {
273            shader_model: wgpu::ShaderModel::Sm4,
274            ..Default::default()
275        };
276        assert_eq!(tier_for(&shader_model), CapabilityTier::TierB);
277    }
278}