Skip to main content

astrelis_render/
capability.rs

1//! Render capability system for declaring GPU feature/limit requirements.
2//!
3//! Renderers implement [`RenderCapability`] to declare their GPU requirements.
4//! These are collected via [`crate::GraphicsContextDescriptor::require_capability`] and
5//! [`crate::GraphicsContextDescriptor::request_capability`] to configure device creation.
6//!
7//! # Example
8//!
9//! ```ignore
10//! use astrelis_render::{GraphicsContext, GraphicsContextDescriptor, GpuRequirements, RenderCapability};
11//! use astrelis_render::batched::BestBatchCapability;
12//!
13//! let ctx = pollster::block_on(
14//!     GraphicsContext::new_owned_with_descriptor(
15//!         GraphicsContextDescriptor::new()
16//!             .request_capability::<BestBatchCapability>()
17//!     )
18//! ).unwrap();
19//! ```
20
21use crate::features::GpuFeatures;
22
23/// A trait for renderers to declare their GPU feature and limit requirements.
24///
25/// Implement this on renderer types (or dedicated marker types) so that
26/// [`crate::GraphicsContextDescriptor::require_capability`] and
27/// [`crate::GraphicsContextDescriptor::request_capability`] can automatically
28/// gather the necessary GPU configuration.
29pub trait RenderCapability {
30    /// GPU requirements (features + limits) for this component.
31    fn requirements() -> GpuRequirements;
32
33    /// Human-readable name for diagnostics.
34    fn name() -> &'static str {
35        std::any::type_name::<Self>()
36    }
37}
38
39/// GPU requirements for a render capability.
40///
41/// Contains required features (must be present), requested features (best-effort),
42/// additional raw wgpu features, and minimum device limits.
43#[derive(Debug, Clone)]
44pub struct GpuRequirements {
45    /// Features that must be present (device creation fails if missing).
46    pub required_features: GpuFeatures,
47    /// Features that are desired but not essential (warns if missing).
48    pub requested_features: GpuFeatures,
49    /// Additional raw wgpu features not covered by [`GpuFeatures`].
50    pub additional_wgpu_features: wgpu::Features,
51    /// Minimum device limits. Merged with other requirements via field-wise max.
52    pub min_limits: wgpu::Limits,
53}
54
55impl GpuRequirements {
56    /// Create requirements with no features or elevated limits.
57    pub fn none() -> Self {
58        Self {
59            required_features: GpuFeatures::empty(),
60            requested_features: GpuFeatures::empty(),
61            additional_wgpu_features: wgpu::Features::empty(),
62            min_limits: wgpu::Limits::default(),
63        }
64    }
65
66    /// Create a new requirements builder starting from no requirements.
67    pub fn new() -> Self {
68        Self::none()
69    }
70
71    /// Set required GPU features.
72    pub fn require_features(mut self, features: GpuFeatures) -> Self {
73        self.required_features |= features;
74        self
75    }
76
77    /// Set requested (best-effort) GPU features.
78    pub fn request_features(mut self, features: GpuFeatures) -> Self {
79        self.requested_features |= features;
80        self
81    }
82
83    /// Set additional raw wgpu features.
84    pub fn with_wgpu_features(mut self, features: wgpu::Features) -> Self {
85        self.additional_wgpu_features |= features;
86        self
87    }
88
89    /// Modify minimum limits via a closure.
90    ///
91    /// # Example
92    ///
93    /// ```ignore
94    /// GpuRequirements::new().with_min_limits(|l| {
95    ///     l.max_binding_array_elements_per_shader_stage = 256;
96    /// })
97    /// ```
98    pub fn with_min_limits(mut self, f: impl FnOnce(&mut wgpu::Limits)) -> Self {
99        f(&mut self.min_limits);
100        self
101    }
102
103    /// Merge another set of requirements into this one.
104    ///
105    /// Features are unioned, limits are merged via field-wise max.
106    pub fn merge(&mut self, other: &GpuRequirements) {
107        self.required_features |= other.required_features;
108        self.requested_features |= other.requested_features;
109        self.additional_wgpu_features |= other.additional_wgpu_features;
110        merge_limits_max(&mut self.min_limits, &other.min_limits);
111    }
112}
113
114impl Default for GpuRequirements {
115    fn default() -> Self {
116        Self::none()
117    }
118}
119
120/// Merge two [`wgpu::Limits`] by taking the maximum of each "maximum" field
121/// and the minimum of each "minimum" field (alignment fields).
122///
123/// This ensures the merged limits satisfy both sets of requirements.
124pub fn merge_limits_max(target: &mut wgpu::Limits, other: &wgpu::Limits) {
125    // "Maximum" fields: take the larger value
126    target.max_texture_dimension_1d = target
127        .max_texture_dimension_1d
128        .max(other.max_texture_dimension_1d);
129    target.max_texture_dimension_2d = target
130        .max_texture_dimension_2d
131        .max(other.max_texture_dimension_2d);
132    target.max_texture_dimension_3d = target
133        .max_texture_dimension_3d
134        .max(other.max_texture_dimension_3d);
135    target.max_texture_array_layers = target
136        .max_texture_array_layers
137        .max(other.max_texture_array_layers);
138    target.max_bind_groups = target.max_bind_groups.max(other.max_bind_groups);
139    target.max_bindings_per_bind_group = target
140        .max_bindings_per_bind_group
141        .max(other.max_bindings_per_bind_group);
142    target.max_dynamic_uniform_buffers_per_pipeline_layout = target
143        .max_dynamic_uniform_buffers_per_pipeline_layout
144        .max(other.max_dynamic_uniform_buffers_per_pipeline_layout);
145    target.max_dynamic_storage_buffers_per_pipeline_layout = target
146        .max_dynamic_storage_buffers_per_pipeline_layout
147        .max(other.max_dynamic_storage_buffers_per_pipeline_layout);
148    target.max_sampled_textures_per_shader_stage = target
149        .max_sampled_textures_per_shader_stage
150        .max(other.max_sampled_textures_per_shader_stage);
151    target.max_samplers_per_shader_stage = target
152        .max_samplers_per_shader_stage
153        .max(other.max_samplers_per_shader_stage);
154    target.max_storage_buffers_per_shader_stage = target
155        .max_storage_buffers_per_shader_stage
156        .max(other.max_storage_buffers_per_shader_stage);
157    target.max_storage_textures_per_shader_stage = target
158        .max_storage_textures_per_shader_stage
159        .max(other.max_storage_textures_per_shader_stage);
160    target.max_uniform_buffers_per_shader_stage = target
161        .max_uniform_buffers_per_shader_stage
162        .max(other.max_uniform_buffers_per_shader_stage);
163    target.max_uniform_buffer_binding_size = target
164        .max_uniform_buffer_binding_size
165        .max(other.max_uniform_buffer_binding_size);
166    target.max_storage_buffer_binding_size = target
167        .max_storage_buffer_binding_size
168        .max(other.max_storage_buffer_binding_size);
169    target.max_vertex_buffers = target.max_vertex_buffers.max(other.max_vertex_buffers);
170    target.max_buffer_size = target.max_buffer_size.max(other.max_buffer_size);
171    target.max_vertex_attributes = target
172        .max_vertex_attributes
173        .max(other.max_vertex_attributes);
174    target.max_vertex_buffer_array_stride = target
175        .max_vertex_buffer_array_stride
176        .max(other.max_vertex_buffer_array_stride);
177    target.max_push_constant_size = target
178        .max_push_constant_size
179        .max(other.max_push_constant_size);
180    target.max_compute_workgroup_storage_size = target
181        .max_compute_workgroup_storage_size
182        .max(other.max_compute_workgroup_storage_size);
183    target.max_compute_invocations_per_workgroup = target
184        .max_compute_invocations_per_workgroup
185        .max(other.max_compute_invocations_per_workgroup);
186    target.max_compute_workgroup_size_x = target
187        .max_compute_workgroup_size_x
188        .max(other.max_compute_workgroup_size_x);
189    target.max_compute_workgroup_size_y = target
190        .max_compute_workgroup_size_y
191        .max(other.max_compute_workgroup_size_y);
192    target.max_compute_workgroup_size_z = target
193        .max_compute_workgroup_size_z
194        .max(other.max_compute_workgroup_size_z);
195    target.max_compute_workgroups_per_dimension = target
196        .max_compute_workgroups_per_dimension
197        .max(other.max_compute_workgroups_per_dimension);
198    target.max_binding_array_elements_per_shader_stage = target
199        .max_binding_array_elements_per_shader_stage
200        .max(other.max_binding_array_elements_per_shader_stage);
201
202    // "Minimum" alignment fields: take the smaller value (stricter alignment)
203    target.min_uniform_buffer_offset_alignment = target
204        .min_uniform_buffer_offset_alignment
205        .min(other.min_uniform_buffer_offset_alignment);
206    target.min_storage_buffer_offset_alignment = target
207        .min_storage_buffer_offset_alignment
208        .min(other.min_storage_buffer_offset_alignment);
209}
210
211/// Clamp requested limits to what the adapter actually supports.
212///
213/// For "maximum" fields, takes `min(requested, adapter)` so we never request
214/// more than the adapter can provide. For "minimum" alignment fields, takes
215/// `max(requested, adapter)` since the adapter's alignment is a lower bound.
216pub fn clamp_limits_to_adapter(requested: &wgpu::Limits, adapter: &wgpu::Limits) -> wgpu::Limits {
217    let mut result = requested.clone();
218
219    // "Maximum" fields: don't exceed adapter
220    result.max_texture_dimension_1d = result
221        .max_texture_dimension_1d
222        .min(adapter.max_texture_dimension_1d);
223    result.max_texture_dimension_2d = result
224        .max_texture_dimension_2d
225        .min(adapter.max_texture_dimension_2d);
226    result.max_texture_dimension_3d = result
227        .max_texture_dimension_3d
228        .min(adapter.max_texture_dimension_3d);
229    result.max_texture_array_layers = result
230        .max_texture_array_layers
231        .min(adapter.max_texture_array_layers);
232    result.max_bind_groups = result.max_bind_groups.min(adapter.max_bind_groups);
233    result.max_bindings_per_bind_group = result
234        .max_bindings_per_bind_group
235        .min(adapter.max_bindings_per_bind_group);
236    result.max_dynamic_uniform_buffers_per_pipeline_layout = result
237        .max_dynamic_uniform_buffers_per_pipeline_layout
238        .min(adapter.max_dynamic_uniform_buffers_per_pipeline_layout);
239    result.max_dynamic_storage_buffers_per_pipeline_layout = result
240        .max_dynamic_storage_buffers_per_pipeline_layout
241        .min(adapter.max_dynamic_storage_buffers_per_pipeline_layout);
242    result.max_sampled_textures_per_shader_stage = result
243        .max_sampled_textures_per_shader_stage
244        .min(adapter.max_sampled_textures_per_shader_stage);
245    result.max_samplers_per_shader_stage = result
246        .max_samplers_per_shader_stage
247        .min(adapter.max_samplers_per_shader_stage);
248    result.max_storage_buffers_per_shader_stage = result
249        .max_storage_buffers_per_shader_stage
250        .min(adapter.max_storage_buffers_per_shader_stage);
251    result.max_storage_textures_per_shader_stage = result
252        .max_storage_textures_per_shader_stage
253        .min(adapter.max_storage_textures_per_shader_stage);
254    result.max_uniform_buffers_per_shader_stage = result
255        .max_uniform_buffers_per_shader_stage
256        .min(adapter.max_uniform_buffers_per_shader_stage);
257    result.max_uniform_buffer_binding_size = result
258        .max_uniform_buffer_binding_size
259        .min(adapter.max_uniform_buffer_binding_size);
260    result.max_storage_buffer_binding_size = result
261        .max_storage_buffer_binding_size
262        .min(adapter.max_storage_buffer_binding_size);
263    result.max_vertex_buffers = result.max_vertex_buffers.min(adapter.max_vertex_buffers);
264    result.max_buffer_size = result.max_buffer_size.min(adapter.max_buffer_size);
265    result.max_vertex_attributes = result
266        .max_vertex_attributes
267        .min(adapter.max_vertex_attributes);
268    result.max_vertex_buffer_array_stride = result
269        .max_vertex_buffer_array_stride
270        .min(adapter.max_vertex_buffer_array_stride);
271    result.max_push_constant_size = result
272        .max_push_constant_size
273        .min(adapter.max_push_constant_size);
274    result.max_compute_workgroup_storage_size = result
275        .max_compute_workgroup_storage_size
276        .min(adapter.max_compute_workgroup_storage_size);
277    result.max_compute_invocations_per_workgroup = result
278        .max_compute_invocations_per_workgroup
279        .min(adapter.max_compute_invocations_per_workgroup);
280    result.max_compute_workgroup_size_x = result
281        .max_compute_workgroup_size_x
282        .min(adapter.max_compute_workgroup_size_x);
283    result.max_compute_workgroup_size_y = result
284        .max_compute_workgroup_size_y
285        .min(adapter.max_compute_workgroup_size_y);
286    result.max_compute_workgroup_size_z = result
287        .max_compute_workgroup_size_z
288        .min(adapter.max_compute_workgroup_size_z);
289    result.max_compute_workgroups_per_dimension = result
290        .max_compute_workgroups_per_dimension
291        .min(adapter.max_compute_workgroups_per_dimension);
292    result.max_binding_array_elements_per_shader_stage = result
293        .max_binding_array_elements_per_shader_stage
294        .min(adapter.max_binding_array_elements_per_shader_stage);
295    result.max_color_attachments = result
296        .max_color_attachments
297        .min(adapter.max_color_attachments);
298    result.max_color_attachment_bytes_per_sample = result
299        .max_color_attachment_bytes_per_sample
300        .min(adapter.max_color_attachment_bytes_per_sample);
301    result.max_inter_stage_shader_components = result
302        .max_inter_stage_shader_components
303        .min(adapter.max_inter_stage_shader_components);
304    result.max_non_sampler_bindings = result
305        .max_non_sampler_bindings
306        .min(adapter.max_non_sampler_bindings);
307
308    // "Minimum" alignment fields: adapter's value is the floor
309    result.min_uniform_buffer_offset_alignment = result
310        .min_uniform_buffer_offset_alignment
311        .max(adapter.min_uniform_buffer_offset_alignment);
312    result.min_storage_buffer_offset_alignment = result
313        .min_storage_buffer_offset_alignment
314        .max(adapter.min_storage_buffer_offset_alignment);
315
316    // Subgroup sizes: use adapter values
317    result.min_subgroup_size = adapter.min_subgroup_size;
318    result.max_subgroup_size = adapter.max_subgroup_size;
319
320    result
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_gpu_requirements_none() {
329        let req = GpuRequirements::none();
330        assert!(req.required_features.is_empty());
331        assert!(req.requested_features.is_empty());
332        assert!(req.additional_wgpu_features.is_empty());
333    }
334
335    #[test]
336    fn test_gpu_requirements_builder() {
337        let req = GpuRequirements::new()
338            .require_features(GpuFeatures::INDIRECT_FIRST_INSTANCE)
339            .request_features(GpuFeatures::TIMESTAMP_QUERY)
340            .with_min_limits(|l| {
341                l.max_binding_array_elements_per_shader_stage = 256;
342            });
343
344        assert!(
345            req.required_features
346                .contains(GpuFeatures::INDIRECT_FIRST_INSTANCE)
347        );
348        assert!(
349            req.requested_features
350                .contains(GpuFeatures::TIMESTAMP_QUERY)
351        );
352        assert_eq!(
353            req.min_limits.max_binding_array_elements_per_shader_stage,
354            256
355        );
356    }
357
358    #[test]
359    fn test_gpu_requirements_merge() {
360        let mut a = GpuRequirements::new()
361            .require_features(GpuFeatures::INDIRECT_FIRST_INSTANCE)
362            .with_min_limits(|l| {
363                l.max_binding_array_elements_per_shader_stage = 128;
364            });
365
366        let b = GpuRequirements::new()
367            .require_features(GpuFeatures::TEXTURE_BINDING_ARRAY)
368            .request_features(GpuFeatures::TIMESTAMP_QUERY)
369            .with_min_limits(|l| {
370                l.max_binding_array_elements_per_shader_stage = 256;
371            });
372
373        a.merge(&b);
374
375        assert!(
376            a.required_features
377                .contains(GpuFeatures::INDIRECT_FIRST_INSTANCE)
378        );
379        assert!(
380            a.required_features
381                .contains(GpuFeatures::TEXTURE_BINDING_ARRAY)
382        );
383        assert!(a.requested_features.contains(GpuFeatures::TIMESTAMP_QUERY));
384        assert_eq!(
385            a.min_limits.max_binding_array_elements_per_shader_stage,
386            256
387        );
388    }
389
390    #[test]
391    fn test_merge_limits_max() {
392        let mut a = wgpu::Limits::default();
393        let mut b = wgpu::Limits::default();
394
395        a.max_texture_dimension_2d = 4096;
396        b.max_texture_dimension_2d = 8192;
397        b.max_bind_groups = 8;
398
399        merge_limits_max(&mut a, &b);
400
401        assert_eq!(a.max_texture_dimension_2d, 8192);
402        assert_eq!(a.max_bind_groups, 8);
403    }
404
405    #[test]
406    fn test_clamp_limits_to_adapter() {
407        let mut requested = wgpu::Limits::default();
408        let mut adapter = wgpu::Limits::default();
409
410        // Request more than adapter supports
411        requested.max_binding_array_elements_per_shader_stage = 1024;
412        adapter.max_binding_array_elements_per_shader_stage = 256;
413
414        // Request less than adapter supports
415        requested.max_texture_dimension_2d = 4096;
416        adapter.max_texture_dimension_2d = 8192;
417
418        let clamped = clamp_limits_to_adapter(&requested, &adapter);
419
420        // Should be clamped to adapter max
421        assert_eq!(clamped.max_binding_array_elements_per_shader_stage, 256);
422        // Should keep requested value (it's within adapter range)
423        assert_eq!(clamped.max_texture_dimension_2d, 4096);
424    }
425}