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 [`GraphicsContextDescriptor::require_capability`] and
5//! [`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/// [`GraphicsContextDescriptor::require_capability`] and
27/// [`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.max_texture_dimension_1d.max(other.max_texture_dimension_1d);
127    target.max_texture_dimension_2d = target.max_texture_dimension_2d.max(other.max_texture_dimension_2d);
128    target.max_texture_dimension_3d = target.max_texture_dimension_3d.max(other.max_texture_dimension_3d);
129    target.max_texture_array_layers = target.max_texture_array_layers.max(other.max_texture_array_layers);
130    target.max_bind_groups = target.max_bind_groups.max(other.max_bind_groups);
131    target.max_bindings_per_bind_group = target.max_bindings_per_bind_group.max(other.max_bindings_per_bind_group);
132    target.max_dynamic_uniform_buffers_per_pipeline_layout = target.max_dynamic_uniform_buffers_per_pipeline_layout.max(other.max_dynamic_uniform_buffers_per_pipeline_layout);
133    target.max_dynamic_storage_buffers_per_pipeline_layout = target.max_dynamic_storage_buffers_per_pipeline_layout.max(other.max_dynamic_storage_buffers_per_pipeline_layout);
134    target.max_sampled_textures_per_shader_stage = target.max_sampled_textures_per_shader_stage.max(other.max_sampled_textures_per_shader_stage);
135    target.max_samplers_per_shader_stage = target.max_samplers_per_shader_stage.max(other.max_samplers_per_shader_stage);
136    target.max_storage_buffers_per_shader_stage = target.max_storage_buffers_per_shader_stage.max(other.max_storage_buffers_per_shader_stage);
137    target.max_storage_textures_per_shader_stage = target.max_storage_textures_per_shader_stage.max(other.max_storage_textures_per_shader_stage);
138    target.max_uniform_buffers_per_shader_stage = target.max_uniform_buffers_per_shader_stage.max(other.max_uniform_buffers_per_shader_stage);
139    target.max_uniform_buffer_binding_size = target.max_uniform_buffer_binding_size.max(other.max_uniform_buffer_binding_size);
140    target.max_storage_buffer_binding_size = target.max_storage_buffer_binding_size.max(other.max_storage_buffer_binding_size);
141    target.max_vertex_buffers = target.max_vertex_buffers.max(other.max_vertex_buffers);
142    target.max_buffer_size = target.max_buffer_size.max(other.max_buffer_size);
143    target.max_vertex_attributes = target.max_vertex_attributes.max(other.max_vertex_attributes);
144    target.max_vertex_buffer_array_stride = target.max_vertex_buffer_array_stride.max(other.max_vertex_buffer_array_stride);
145    target.max_push_constant_size = target.max_push_constant_size.max(other.max_push_constant_size);
146    target.max_compute_workgroup_storage_size = target.max_compute_workgroup_storage_size.max(other.max_compute_workgroup_storage_size);
147    target.max_compute_invocations_per_workgroup = target.max_compute_invocations_per_workgroup.max(other.max_compute_invocations_per_workgroup);
148    target.max_compute_workgroup_size_x = target.max_compute_workgroup_size_x.max(other.max_compute_workgroup_size_x);
149    target.max_compute_workgroup_size_y = target.max_compute_workgroup_size_y.max(other.max_compute_workgroup_size_y);
150    target.max_compute_workgroup_size_z = target.max_compute_workgroup_size_z.max(other.max_compute_workgroup_size_z);
151    target.max_compute_workgroups_per_dimension = target.max_compute_workgroups_per_dimension.max(other.max_compute_workgroups_per_dimension);
152    target.max_binding_array_elements_per_shader_stage = target.max_binding_array_elements_per_shader_stage.max(other.max_binding_array_elements_per_shader_stage);
153
154    // "Minimum" alignment fields: take the smaller value (stricter alignment)
155    target.min_uniform_buffer_offset_alignment = target.min_uniform_buffer_offset_alignment.min(other.min_uniform_buffer_offset_alignment);
156    target.min_storage_buffer_offset_alignment = target.min_storage_buffer_offset_alignment.min(other.min_storage_buffer_offset_alignment);
157}
158
159/// Clamp requested limits to what the adapter actually supports.
160///
161/// For "maximum" fields, takes `min(requested, adapter)` so we never request
162/// more than the adapter can provide. For "minimum" alignment fields, takes
163/// `max(requested, adapter)` since the adapter's alignment is a lower bound.
164pub fn clamp_limits_to_adapter(requested: &wgpu::Limits, adapter: &wgpu::Limits) -> wgpu::Limits {
165    let mut result = requested.clone();
166
167    // "Maximum" fields: don't exceed adapter
168    result.max_texture_dimension_1d = result.max_texture_dimension_1d.min(adapter.max_texture_dimension_1d);
169    result.max_texture_dimension_2d = result.max_texture_dimension_2d.min(adapter.max_texture_dimension_2d);
170    result.max_texture_dimension_3d = result.max_texture_dimension_3d.min(adapter.max_texture_dimension_3d);
171    result.max_texture_array_layers = result.max_texture_array_layers.min(adapter.max_texture_array_layers);
172    result.max_bind_groups = result.max_bind_groups.min(adapter.max_bind_groups);
173    result.max_bindings_per_bind_group = result.max_bindings_per_bind_group.min(adapter.max_bindings_per_bind_group);
174    result.max_dynamic_uniform_buffers_per_pipeline_layout = result.max_dynamic_uniform_buffers_per_pipeline_layout.min(adapter.max_dynamic_uniform_buffers_per_pipeline_layout);
175    result.max_dynamic_storage_buffers_per_pipeline_layout = result.max_dynamic_storage_buffers_per_pipeline_layout.min(adapter.max_dynamic_storage_buffers_per_pipeline_layout);
176    result.max_sampled_textures_per_shader_stage = result.max_sampled_textures_per_shader_stage.min(adapter.max_sampled_textures_per_shader_stage);
177    result.max_samplers_per_shader_stage = result.max_samplers_per_shader_stage.min(adapter.max_samplers_per_shader_stage);
178    result.max_storage_buffers_per_shader_stage = result.max_storage_buffers_per_shader_stage.min(adapter.max_storage_buffers_per_shader_stage);
179    result.max_storage_textures_per_shader_stage = result.max_storage_textures_per_shader_stage.min(adapter.max_storage_textures_per_shader_stage);
180    result.max_uniform_buffers_per_shader_stage = result.max_uniform_buffers_per_shader_stage.min(adapter.max_uniform_buffers_per_shader_stage);
181    result.max_uniform_buffer_binding_size = result.max_uniform_buffer_binding_size.min(adapter.max_uniform_buffer_binding_size);
182    result.max_storage_buffer_binding_size = result.max_storage_buffer_binding_size.min(adapter.max_storage_buffer_binding_size);
183    result.max_vertex_buffers = result.max_vertex_buffers.min(adapter.max_vertex_buffers);
184    result.max_buffer_size = result.max_buffer_size.min(adapter.max_buffer_size);
185    result.max_vertex_attributes = result.max_vertex_attributes.min(adapter.max_vertex_attributes);
186    result.max_vertex_buffer_array_stride = result.max_vertex_buffer_array_stride.min(adapter.max_vertex_buffer_array_stride);
187    result.max_push_constant_size = result.max_push_constant_size.min(adapter.max_push_constant_size);
188    result.max_compute_workgroup_storage_size = result.max_compute_workgroup_storage_size.min(adapter.max_compute_workgroup_storage_size);
189    result.max_compute_invocations_per_workgroup = result.max_compute_invocations_per_workgroup.min(adapter.max_compute_invocations_per_workgroup);
190    result.max_compute_workgroup_size_x = result.max_compute_workgroup_size_x.min(adapter.max_compute_workgroup_size_x);
191    result.max_compute_workgroup_size_y = result.max_compute_workgroup_size_y.min(adapter.max_compute_workgroup_size_y);
192    result.max_compute_workgroup_size_z = result.max_compute_workgroup_size_z.min(adapter.max_compute_workgroup_size_z);
193    result.max_compute_workgroups_per_dimension = result.max_compute_workgroups_per_dimension.min(adapter.max_compute_workgroups_per_dimension);
194    result.max_binding_array_elements_per_shader_stage = result.max_binding_array_elements_per_shader_stage.min(adapter.max_binding_array_elements_per_shader_stage);
195    result.max_color_attachments = result.max_color_attachments.min(adapter.max_color_attachments);
196    result.max_color_attachment_bytes_per_sample = result.max_color_attachment_bytes_per_sample.min(adapter.max_color_attachment_bytes_per_sample);
197    result.max_inter_stage_shader_components = result.max_inter_stage_shader_components.min(adapter.max_inter_stage_shader_components);
198    result.max_non_sampler_bindings = result.max_non_sampler_bindings.min(adapter.max_non_sampler_bindings);
199
200    // "Minimum" alignment fields: adapter's value is the floor
201    result.min_uniform_buffer_offset_alignment = result.min_uniform_buffer_offset_alignment.max(adapter.min_uniform_buffer_offset_alignment);
202    result.min_storage_buffer_offset_alignment = result.min_storage_buffer_offset_alignment.max(adapter.min_storage_buffer_offset_alignment);
203
204    // Subgroup sizes: use adapter values
205    result.min_subgroup_size = adapter.min_subgroup_size;
206    result.max_subgroup_size = adapter.max_subgroup_size;
207
208    result
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_gpu_requirements_none() {
217        let req = GpuRequirements::none();
218        assert!(req.required_features.is_empty());
219        assert!(req.requested_features.is_empty());
220        assert!(req.additional_wgpu_features.is_empty());
221    }
222
223    #[test]
224    fn test_gpu_requirements_builder() {
225        let req = GpuRequirements::new()
226            .require_features(GpuFeatures::INDIRECT_FIRST_INSTANCE)
227            .request_features(GpuFeatures::TIMESTAMP_QUERY)
228            .with_min_limits(|l| {
229                l.max_binding_array_elements_per_shader_stage = 256;
230            });
231
232        assert!(req.required_features.contains(GpuFeatures::INDIRECT_FIRST_INSTANCE));
233        assert!(req.requested_features.contains(GpuFeatures::TIMESTAMP_QUERY));
234        assert_eq!(req.min_limits.max_binding_array_elements_per_shader_stage, 256);
235    }
236
237    #[test]
238    fn test_gpu_requirements_merge() {
239        let mut a = GpuRequirements::new()
240            .require_features(GpuFeatures::INDIRECT_FIRST_INSTANCE)
241            .with_min_limits(|l| {
242                l.max_binding_array_elements_per_shader_stage = 128;
243            });
244
245        let b = GpuRequirements::new()
246            .require_features(GpuFeatures::TEXTURE_BINDING_ARRAY)
247            .request_features(GpuFeatures::TIMESTAMP_QUERY)
248            .with_min_limits(|l| {
249                l.max_binding_array_elements_per_shader_stage = 256;
250            });
251
252        a.merge(&b);
253
254        assert!(a.required_features.contains(GpuFeatures::INDIRECT_FIRST_INSTANCE));
255        assert!(a.required_features.contains(GpuFeatures::TEXTURE_BINDING_ARRAY));
256        assert!(a.requested_features.contains(GpuFeatures::TIMESTAMP_QUERY));
257        assert_eq!(a.min_limits.max_binding_array_elements_per_shader_stage, 256);
258    }
259
260    #[test]
261    fn test_merge_limits_max() {
262        let mut a = wgpu::Limits::default();
263        let mut b = wgpu::Limits::default();
264
265        a.max_texture_dimension_2d = 4096;
266        b.max_texture_dimension_2d = 8192;
267        b.max_bind_groups = 8;
268
269        merge_limits_max(&mut a, &b);
270
271        assert_eq!(a.max_texture_dimension_2d, 8192);
272        assert_eq!(a.max_bind_groups, 8);
273    }
274
275    #[test]
276    fn test_clamp_limits_to_adapter() {
277        let mut requested = wgpu::Limits::default();
278        let mut adapter = wgpu::Limits::default();
279
280        // Request more than adapter supports
281        requested.max_binding_array_elements_per_shader_stage = 1024;
282        adapter.max_binding_array_elements_per_shader_stage = 256;
283
284        // Request less than adapter supports
285        requested.max_texture_dimension_2d = 4096;
286        adapter.max_texture_dimension_2d = 8192;
287
288        let clamped = clamp_limits_to_adapter(&requested, &adapter);
289
290        // Should be clamped to adapter max
291        assert_eq!(clamped.max_binding_array_elements_per_shader_stage, 256);
292        // Should keep requested value (it's within adapter range)
293        assert_eq!(clamped.max_texture_dimension_2d, 4096);
294    }
295}