Skip to main content

astrelis_render/
features.rs

1//! GPU feature detection and management.
2//!
3//! This module provides a type-safe wrapper around wgpu features with support for
4//! required vs requested features.
5
6use bitflags::bitflags;
7
8bitflags! {
9    /// GPU features that can be requested or required.
10    ///
11    /// These map to common wgpu features that are useful for rendering applications.
12    /// Use `GpuFeatures::to_wgpu()` to convert to `wgpu::Features`.
13    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14    pub struct GpuFeatures: u32 {
15        /// Allows the use of `first_instance` parameter in indirect draw calls.
16        /// Required for GPU-driven rendering with indirect buffers.
17        const INDIRECT_FIRST_INSTANCE = 1 << 0;
18
19        /// Allows `multi_draw_indirect_count` for GPU-driven draw count.
20        /// In wgpu 27+, `multi_draw_indirect()` requires no feature flag
21        /// (only `DownlevelFlags::INDIRECT_EXECUTION`), but the count variant
22        /// (`multi_draw_indirect_count`) requires this feature.
23        const MULTI_DRAW_INDIRECT_COUNT = 1 << 1;
24
25        /// Allows push constants in shaders for small, frequently updated data.
26        /// More efficient than uniform buffers for small amounts of per-draw data.
27        const PUSH_CONSTANTS = 1 << 2;
28
29        /// BC texture compression (DXT1, DXT3, DXT5).
30        /// Common on desktop platforms, reduces texture memory usage.
31        const TEXTURE_COMPRESSION_BC = 1 << 3;
32
33        /// Allows disabling depth clipping in the rasterizer.
34        /// Useful for certain shadow mapping techniques.
35        const DEPTH_CLIP_CONTROL = 1 << 4;
36
37        /// 16-bit floating point support in shaders.
38        /// Can improve performance on some GPUs for certain workloads.
39        const SHADER_F16 = 1 << 5;
40
41        /// Allows non-zero `first_vertex` and `first_instance` in indirect draw calls.
42        /// Note: This is a subset of INDIRECT_FIRST_INSTANCE on some platforms.
43        const INDIRECT_FIRST_VERTEX = 1 << 6;
44
45        /// Polygon mode: line (wireframe rendering).
46        const POLYGON_MODE_LINE = 1 << 7;
47
48        /// Polygon mode: point.
49        const POLYGON_MODE_POINT = 1 << 8;
50
51        /// Conservative rasterization for better triangle coverage.
52        const CONSERVATIVE_RASTERIZATION = 1 << 9;
53
54        /// Texture binding arrays (bindless textures).
55        /// Enables more flexible texture access patterns in shaders.
56        const TEXTURE_BINDING_ARRAY = 1 << 10;
57
58        /// Sampled texture and storage buffer binding arrays.
59        const BUFFER_BINDING_ARRAY = 1 << 11;
60
61        /// Storage resource binding arrays with dynamic indexing.
62        const STORAGE_RESOURCE_BINDING_ARRAY = 1 << 12;
63
64        /// Partially bound binding arrays.
65        /// Allows leaving some array slots unbound.
66        const PARTIALLY_BOUND_BINDING_ARRAY = 1 << 13;
67
68        /// 32-bit floating point texture filtering.
69        const FLOAT32_FILTERABLE = 1 << 14;
70
71        /// RG11B10 unsigned floating point render target format.
72        const RG11B10UFLOAT_RENDERABLE = 1 << 15;
73
74        /// BGRA8 unorm storage texture support.
75        const BGRA8UNORM_STORAGE = 1 << 16;
76
77        /// Timestamp queries for GPU profiling.
78        /// Allows measuring GPU execution time with high precision.
79        /// Alone, this only allows timestamp writes on pass definition
80        /// (via `timestamp_writes` in render/compute pass descriptors).
81        const TIMESTAMP_QUERY = 1 << 17;
82
83        /// Allows timestamp write commands at arbitrary points within command encoders.
84        /// Implies `TIMESTAMP_QUERY` is supported.
85        /// Required by `wgpu-profiler` for scopes on command encoders.
86        const TIMESTAMP_QUERY_INSIDE_ENCODERS = 1 << 19;
87
88        /// Allows timestamp write commands at arbitrary points within render/compute passes.
89        /// Implies `TIMESTAMP_QUERY` and `TIMESTAMP_QUERY_INSIDE_ENCODERS` are supported.
90        /// Required by `wgpu-profiler` for scopes on render/compute passes.
91        const TIMESTAMP_QUERY_INSIDE_PASSES = 1 << 20;
92
93        /// Non-uniform indexing of sampled texture and storage buffer binding arrays.
94        /// Required for dynamic indexing into `binding_array<texture_2d<f32>>` with
95        /// values that are not uniform across a draw call (e.g., per-instance texture index).
96        const SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING = 1 << 18;
97    }
98}
99
100impl GpuFeatures {
101    /// Convert to wgpu::Features.
102    pub fn to_wgpu(self) -> wgpu::Features {
103        let mut features = wgpu::Features::empty();
104
105        if self.contains(GpuFeatures::INDIRECT_FIRST_INSTANCE) {
106            features |= wgpu::Features::INDIRECT_FIRST_INSTANCE;
107        }
108        if self.contains(GpuFeatures::MULTI_DRAW_INDIRECT_COUNT) {
109            features |= wgpu::Features::MULTI_DRAW_INDIRECT_COUNT;
110        }
111        if self.contains(GpuFeatures::PUSH_CONSTANTS) {
112            features |= wgpu::Features::PUSH_CONSTANTS;
113        }
114        if self.contains(GpuFeatures::TEXTURE_COMPRESSION_BC) {
115            features |= wgpu::Features::TEXTURE_COMPRESSION_BC;
116        }
117        if self.contains(GpuFeatures::DEPTH_CLIP_CONTROL) {
118            features |= wgpu::Features::DEPTH_CLIP_CONTROL;
119        }
120        if self.contains(GpuFeatures::SHADER_F16) {
121            features |= wgpu::Features::SHADER_F16;
122        }
123        if self.contains(GpuFeatures::POLYGON_MODE_LINE) {
124            features |= wgpu::Features::POLYGON_MODE_LINE;
125        }
126        if self.contains(GpuFeatures::POLYGON_MODE_POINT) {
127            features |= wgpu::Features::POLYGON_MODE_POINT;
128        }
129        if self.contains(GpuFeatures::CONSERVATIVE_RASTERIZATION) {
130            features |= wgpu::Features::CONSERVATIVE_RASTERIZATION;
131        }
132        if self.contains(GpuFeatures::TEXTURE_BINDING_ARRAY) {
133            features |= wgpu::Features::TEXTURE_BINDING_ARRAY;
134        }
135        if self.contains(GpuFeatures::BUFFER_BINDING_ARRAY) {
136            features |= wgpu::Features::BUFFER_BINDING_ARRAY;
137        }
138        if self.contains(GpuFeatures::STORAGE_RESOURCE_BINDING_ARRAY) {
139            features |= wgpu::Features::STORAGE_RESOURCE_BINDING_ARRAY;
140        }
141        if self.contains(GpuFeatures::PARTIALLY_BOUND_BINDING_ARRAY) {
142            features |= wgpu::Features::PARTIALLY_BOUND_BINDING_ARRAY;
143        }
144        if self.contains(GpuFeatures::FLOAT32_FILTERABLE) {
145            features |= wgpu::Features::FLOAT32_FILTERABLE;
146        }
147        if self.contains(GpuFeatures::RG11B10UFLOAT_RENDERABLE) {
148            features |= wgpu::Features::RG11B10UFLOAT_RENDERABLE;
149        }
150        if self.contains(GpuFeatures::BGRA8UNORM_STORAGE) {
151            features |= wgpu::Features::BGRA8UNORM_STORAGE;
152        }
153        if self.contains(GpuFeatures::TIMESTAMP_QUERY) {
154            features |= wgpu::Features::TIMESTAMP_QUERY;
155        }
156        if self.contains(GpuFeatures::TIMESTAMP_QUERY_INSIDE_ENCODERS) {
157            features |= wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS;
158        }
159        if self.contains(GpuFeatures::TIMESTAMP_QUERY_INSIDE_PASSES) {
160            features |= wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES;
161        }
162        if self.contains(GpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING)
163        {
164            features |=
165                wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING;
166        }
167
168        features
169    }
170
171    /// Convert from wgpu::Features to GpuFeatures.
172    ///
173    /// Note: Only features that have a corresponding GpuFeatures flag will be included.
174    pub fn from_wgpu(features: wgpu::Features) -> Self {
175        let mut gpu_features = GpuFeatures::empty();
176
177        if features.contains(wgpu::Features::INDIRECT_FIRST_INSTANCE) {
178            gpu_features |= GpuFeatures::INDIRECT_FIRST_INSTANCE;
179        }
180        if features.contains(wgpu::Features::MULTI_DRAW_INDIRECT_COUNT) {
181            gpu_features |= GpuFeatures::MULTI_DRAW_INDIRECT_COUNT;
182        }
183        if features.contains(wgpu::Features::PUSH_CONSTANTS) {
184            gpu_features |= GpuFeatures::PUSH_CONSTANTS;
185        }
186        if features.contains(wgpu::Features::TEXTURE_COMPRESSION_BC) {
187            gpu_features |= GpuFeatures::TEXTURE_COMPRESSION_BC;
188        }
189        if features.contains(wgpu::Features::DEPTH_CLIP_CONTROL) {
190            gpu_features |= GpuFeatures::DEPTH_CLIP_CONTROL;
191        }
192        if features.contains(wgpu::Features::SHADER_F16) {
193            gpu_features |= GpuFeatures::SHADER_F16;
194        }
195        if features.contains(wgpu::Features::POLYGON_MODE_LINE) {
196            gpu_features |= GpuFeatures::POLYGON_MODE_LINE;
197        }
198        if features.contains(wgpu::Features::POLYGON_MODE_POINT) {
199            gpu_features |= GpuFeatures::POLYGON_MODE_POINT;
200        }
201        if features.contains(wgpu::Features::CONSERVATIVE_RASTERIZATION) {
202            gpu_features |= GpuFeatures::CONSERVATIVE_RASTERIZATION;
203        }
204        if features.contains(wgpu::Features::TEXTURE_BINDING_ARRAY) {
205            gpu_features |= GpuFeatures::TEXTURE_BINDING_ARRAY;
206        }
207        if features.contains(wgpu::Features::BUFFER_BINDING_ARRAY) {
208            gpu_features |= GpuFeatures::BUFFER_BINDING_ARRAY;
209        }
210        if features.contains(wgpu::Features::STORAGE_RESOURCE_BINDING_ARRAY) {
211            gpu_features |= GpuFeatures::STORAGE_RESOURCE_BINDING_ARRAY;
212        }
213        if features.contains(wgpu::Features::PARTIALLY_BOUND_BINDING_ARRAY) {
214            gpu_features |= GpuFeatures::PARTIALLY_BOUND_BINDING_ARRAY;
215        }
216        if features.contains(wgpu::Features::FLOAT32_FILTERABLE) {
217            gpu_features |= GpuFeatures::FLOAT32_FILTERABLE;
218        }
219        if features.contains(wgpu::Features::RG11B10UFLOAT_RENDERABLE) {
220            gpu_features |= GpuFeatures::RG11B10UFLOAT_RENDERABLE;
221        }
222        if features.contains(wgpu::Features::BGRA8UNORM_STORAGE) {
223            gpu_features |= GpuFeatures::BGRA8UNORM_STORAGE;
224        }
225        if features.contains(wgpu::Features::TIMESTAMP_QUERY) {
226            gpu_features |= GpuFeatures::TIMESTAMP_QUERY;
227        }
228        if features.contains(wgpu::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS) {
229            gpu_features |= GpuFeatures::TIMESTAMP_QUERY_INSIDE_ENCODERS;
230        }
231        if features.contains(wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES) {
232            gpu_features |= GpuFeatures::TIMESTAMP_QUERY_INSIDE_PASSES;
233        }
234        if features
235            .contains(wgpu::Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING)
236        {
237            gpu_features |=
238                GpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING;
239        }
240
241        gpu_features
242    }
243
244    /// Check if all the specified features are supported by the adapter.
245    pub fn check_support(self, adapter: &wgpu::Adapter) -> FeatureSupportResult {
246        let adapter_features = GpuFeatures::from_wgpu(adapter.features());
247        let missing = self - (self & adapter_features);
248
249        if missing.is_empty() {
250            FeatureSupportResult::Supported
251        } else {
252            FeatureSupportResult::Missing(missing)
253        }
254    }
255}
256
257impl Default for GpuFeatures {
258    fn default() -> Self {
259        GpuFeatures::empty()
260    }
261}
262
263/// Result of checking feature support.
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265pub enum FeatureSupportResult {
266    /// All requested features are supported.
267    Supported,
268    /// Some features are missing.
269    Missing(GpuFeatures),
270}
271
272impl FeatureSupportResult {
273    /// Returns true if all features are supported.
274    pub fn is_supported(&self) -> bool {
275        matches!(self, FeatureSupportResult::Supported)
276    }
277
278    /// Returns the missing features, if any.
279    pub fn missing(&self) -> Option<GpuFeatures> {
280        match self {
281            FeatureSupportResult::Supported => None,
282            FeatureSupportResult::Missing(features) => Some(*features),
283        }
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn test_gpu_features_empty() {
293        let features = GpuFeatures::empty();
294        assert!(features.is_empty());
295        assert_eq!(features.to_wgpu(), wgpu::Features::empty());
296    }
297
298    #[test]
299    fn test_gpu_features_roundtrip() {
300        let features = GpuFeatures::INDIRECT_FIRST_INSTANCE
301            | GpuFeatures::MULTI_DRAW_INDIRECT_COUNT
302            | GpuFeatures::PUSH_CONSTANTS
303            | GpuFeatures::TIMESTAMP_QUERY;
304
305        let wgpu_features = features.to_wgpu();
306        let back = GpuFeatures::from_wgpu(wgpu_features);
307
308        assert_eq!(features, back);
309    }
310
311    #[test]
312    fn test_gpu_features_contains() {
313        let features = GpuFeatures::INDIRECT_FIRST_INSTANCE | GpuFeatures::PUSH_CONSTANTS;
314
315        assert!(features.contains(GpuFeatures::INDIRECT_FIRST_INSTANCE));
316        assert!(features.contains(GpuFeatures::PUSH_CONSTANTS));
317        assert!(!features.contains(GpuFeatures::MULTI_DRAW_INDIRECT_COUNT));
318    }
319}