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,
42    /// `SwiftShader`, WARP), where a shader's cost is paid for every pixel it
43    /// is run on whatever branches it skips.
44    pub software_rasterizer: bool,
45    /// Whether `Rgba16Float` render targets blend in hardware, so pictures
46    /// can be composited in linear light by blending alone.
47    pub half_float_blendable: bool,
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    ) -> Self {
64        Self {
65            tier,
66            intermediate_format,
67            adapter_name: adapter_name.into(),
68            max_texture_dimension_2d,
69            timestamp_queries,
70            backend: backend.into(),
71            driver: driver.into(),
72            driver_info: driver_info.into(),
73            software_rasterizer: false,
74            half_float_blendable: intermediate_format == IntermediateFormat::Rgba16Float,
75        }
76    }
77
78    pub(crate) fn from_adapter(adapter: &wgpu::Adapter) -> Self {
79        let info = adapter.get_info();
80        let half_float = adapter.get_texture_format_features(wgpu::TextureFormat::Rgba16Float);
81        Self {
82            tier: tier_for(&adapter.get_downlevel_capabilities()),
83            intermediate_format: intermediate_format_for(half_float),
84            half_float_blendable: half_float
85                .allowed_usages
86                .contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
87                && half_float
88                    .flags
89                    .contains(wgpu::TextureFormatFeatureFlags::BLENDABLE),
90            adapter_name: info.name,
91            max_texture_dimension_2d: adapter.limits().max_texture_dimension_2d,
92            timestamp_queries: adapter.features().contains(wgpu::Features::TIMESTAMP_QUERY),
93            backend: format!("{:?}", info.backend),
94            driver: info.driver,
95            driver_info: info.driver_info,
96            software_rasterizer: info.device_type == wgpu::DeviceType::Cpu,
97        }
98    }
99}
100
101/// The intermediate format for an adapter whose `Rgba16Float` support is
102/// `half_float`: half float where pictures can be rendered to it, sampled
103/// with filtering, and copied out; 8-bit everywhere else.
104///
105/// The tier does not enter into it: a WebGL2 or GL adapter with a float or
106/// half-float colour buffer is tier B and still gets half float.
107fn intermediate_format_for(half_float: wgpu::TextureFormatFeatures) -> IntermediateFormat {
108    let required = wgpu::TextureUsages::RENDER_ATTACHMENT
109        | wgpu::TextureUsages::TEXTURE_BINDING
110        | wgpu::TextureUsages::COPY_SRC;
111    if half_float.allowed_usages.contains(required)
112        && half_float
113            .flags
114            .contains(wgpu::TextureFormatFeatureFlags::FILTERABLE)
115    {
116        IntermediateFormat::Rgba16Float
117    } else {
118        IntermediateFormat::Rgba8Unorm
119    }
120}
121
122/// Tier A for an adapter that meets all of WebGPU, tier B for a downlevel
123/// one.
124fn tier_for(downlevel: &wgpu::DownlevelCapabilities) -> CapabilityTier {
125    if downlevel.is_webgpu_compliant() {
126        CapabilityTier::TierA
127    } else {
128        CapabilityTier::TierB
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use wgpu::{TextureFormatFeatureFlags as Flags, TextureUsages as Usages};
136
137    fn half_float(allowed_usages: Usages, flags: Flags) -> wgpu::TextureFormatFeatures {
138        wgpu::TextureFormatFeatures {
139            allowed_usages,
140            flags,
141        }
142    }
143
144    #[test]
145    fn half_float_is_the_intermediate_only_where_it_renders_filters_and_copies() {
146        let required = Usages::RENDER_ATTACHMENT | Usages::TEXTURE_BINDING | Usages::COPY_SRC;
147        assert_eq!(
148            intermediate_format_for(half_float(required | Usages::COPY_DST, Flags::FILTERABLE)),
149            IntermediateFormat::Rgba16Float
150        );
151        for missing in [
152            Usages::RENDER_ATTACHMENT,
153            Usages::TEXTURE_BINDING,
154            Usages::COPY_SRC,
155        ] {
156            assert_eq!(
157                intermediate_format_for(half_float(required - missing, Flags::FILTERABLE)),
158                IntermediateFormat::Rgba8Unorm,
159                "without {missing:?}"
160            );
161        }
162        assert_eq!(
163            intermediate_format_for(half_float(required, Flags::empty())),
164            IntermediateFormat::Rgba8Unorm,
165            "without filtering"
166        );
167        // No half float at all, as a WebGL2 context without a float colour buffer.
168        assert_eq!(
169            intermediate_format_for(half_float(Usages::empty(), Flags::empty())),
170            IntermediateFormat::Rgba8Unorm
171        );
172    }
173
174    #[test]
175    fn the_tier_follows_webgpu_compliance_alone() {
176        assert_eq!(
177            tier_for(&wgpu::DownlevelCapabilities::default()),
178            CapabilityTier::TierA
179        );
180        let mut downlevel = wgpu::DownlevelCapabilities::default();
181        downlevel
182            .flags
183            .remove(wgpu::DownlevelFlags::COMPUTE_SHADERS);
184        assert_eq!(tier_for(&downlevel), CapabilityTier::TierB);
185        let shader_model = wgpu::DownlevelCapabilities {
186            shader_model: wgpu::ShaderModel::Sm4,
187            ..Default::default()
188        };
189        assert_eq!(tier_for(&shader_model), CapabilityTier::TierB);
190    }
191}