astrelis_render/
context.rs

1use crate::features::GpuFeatures;
2use std::sync::Arc;
3
4/// Errors that can occur during graphics context creation.
5#[derive(Debug, Clone)]
6pub enum GraphicsError {
7    /// No suitable GPU adapter was found.
8    NoAdapter,
9
10    /// Failed to create a device.
11    DeviceCreationFailed(String),
12
13    /// Required GPU features are not supported by the adapter.
14    MissingRequiredFeatures {
15        missing: GpuFeatures,
16        adapter_name: String,
17        supported: GpuFeatures,
18    },
19
20    /// Failed to create a surface.
21    SurfaceCreationFailed(String),
22
23    /// Failed to get surface configuration.
24    SurfaceConfigurationFailed(String),
25
26    /// Failed to acquire surface texture.
27    SurfaceTextureAcquisitionFailed(String),
28}
29
30impl std::fmt::Display for GraphicsError {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        match self {
33            GraphicsError::NoAdapter => {
34                write!(f, "Failed to find a suitable GPU adapter")
35            }
36            GraphicsError::DeviceCreationFailed(msg) => {
37                write!(f, "Failed to create device: {}", msg)
38            }
39            GraphicsError::MissingRequiredFeatures { missing, adapter_name, supported } => {
40                write!(
41                    f,
42                    "Required GPU features not supported by adapter '{}': {:?}\nSupported: {:?}",
43                    adapter_name, missing, supported
44                )
45            }
46            GraphicsError::SurfaceCreationFailed(msg) => {
47                write!(f, "Failed to create surface: {}", msg)
48            }
49            GraphicsError::SurfaceConfigurationFailed(msg) => {
50                write!(f, "Failed to get surface configuration: {}", msg)
51            }
52            GraphicsError::SurfaceTextureAcquisitionFailed(msg) => {
53                write!(f, "Failed to acquire surface texture: {}", msg)
54            }
55        }
56    }
57}
58
59impl std::error::Error for GraphicsError {}
60
61/// A globally shared graphics context.
62///
63/// # Ownership Pattern
64///
65/// This type uses Arc for shared ownership:
66///
67/// ```rust,no_run
68/// use astrelis_render::GraphicsContext;
69/// use std::sync::Arc;
70///
71/// // Synchronous creation (blocks on async internally)
72/// let ctx = GraphicsContext::new_owned_sync_or_panic(); // Returns Arc<Self>
73/// let ctx2 = ctx.clone(); // Cheap clone (Arc)
74///
75/// // Asynchronous creation (for async contexts)
76/// # async fn example() {
77/// let ctx = GraphicsContext::new_owned().await; // Returns Arc<Self>
78/// # }
79/// ```
80///
81/// Benefits of the Arc pattern:
82/// - No memory leak
83/// - Proper cleanup on drop
84/// - Better for testing (can create/destroy contexts)
85/// - Arc internally makes cloning cheap
86pub struct GraphicsContext {
87    pub instance: wgpu::Instance,
88    pub adapter: wgpu::Adapter,
89    pub device: wgpu::Device,
90    pub queue: wgpu::Queue,
91    /// The GPU features that were enabled on this context.
92    enabled_features: GpuFeatures,
93}
94
95impl GraphicsContext {
96    /// Creates a new graphics context with owned ownership (recommended).
97    ///
98    /// Returns `Arc<Self>` which can be cheaply cloned and shared.
99    /// This is the preferred method for new code as it doesn't leak memory.
100    ///
101    /// # Example
102    /// ```rust,no_run
103    /// use astrelis_render::GraphicsContext;
104    ///
105    /// # async fn example() {
106    /// let ctx = GraphicsContext::new_owned().await;
107    /// let ctx2 = ctx.clone(); // Cheap clone
108    /// # }
109    /// ```
110    pub async fn new_owned() -> Result<Arc<Self>, GraphicsError> {
111        Self::new_owned_with_descriptor(GraphicsContextDescriptor::default()).await
112    }
113
114    /// Creates a new graphics context synchronously with owned ownership (recommended).
115    ///
116    /// This blocks the current thread until the context is created.
117    ///
118    /// # Errors
119    ///
120    /// Returns `GraphicsError` if:
121    /// - No suitable GPU adapter is found
122    /// - Required GPU features are not supported
123    /// - Device creation fails
124    pub fn new_owned_sync() -> Result<Arc<Self>, GraphicsError> {
125        pollster::block_on(Self::new_owned())
126    }
127
128    /// Creates a new graphics context synchronously, panicking on error.
129    ///
130    /// This is a convenience method for tests and examples where error handling
131    /// is not needed. For production code, prefer `new_owned_sync()` which returns
132    /// a `Result`.
133    ///
134    /// # Panics
135    ///
136    /// Panics if graphics context creation fails.
137    pub fn new_owned_sync_or_panic() -> Arc<Self> {
138        Self::new_owned_sync().expect("Failed to create graphics context")
139    }
140
141    /// Creates a new graphics context with custom descriptor (owned).
142    pub async fn new_owned_with_descriptor(descriptor: GraphicsContextDescriptor) -> Result<Arc<Self>, GraphicsError> {
143        let context = Self::create_context_internal(descriptor).await?;
144        Ok(Arc::new(context))
145    }
146
147    /// Internal method to create context without deciding on ownership pattern.
148    async fn create_context_internal(descriptor: GraphicsContextDescriptor) -> Result<Self, GraphicsError> {
149        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
150            backends: descriptor.backends,
151            ..Default::default()
152        });
153
154        let adapter = instance
155            .request_adapter(&wgpu::RequestAdapterOptions {
156                power_preference: descriptor.power_preference,
157                compatible_surface: None,
158                force_fallback_adapter: descriptor.force_fallback_adapter,
159            })
160            .await
161            .map_err(|_| GraphicsError::NoAdapter)?;
162
163        // Check required features
164        let required_result = descriptor.required_gpu_features.check_support(&adapter);
165        if let Some(missing) = required_result.missing() {
166            return Err(GraphicsError::MissingRequiredFeatures {
167                missing,
168                adapter_name: adapter.get_info().name.clone(),
169                supported: GpuFeatures::from_wgpu(adapter.features()),
170            });
171        }
172
173        // Determine which requested features are available
174        let available_requested = descriptor.requested_gpu_features
175            & GpuFeatures::from_wgpu(adapter.features());
176
177        // Log which requested features were not available
178        let unavailable_requested =
179            descriptor.requested_gpu_features - available_requested;
180        if !unavailable_requested.is_empty() {
181            tracing::warn!(
182                "Some requested GPU features are not available: {:?}",
183                unavailable_requested
184            );
185        }
186
187        // Combine all features to enable
188        let enabled_features = descriptor.required_gpu_features | available_requested;
189        let wgpu_features = enabled_features.to_wgpu() | descriptor.additional_wgpu_features;
190
191        let (device, queue) = adapter
192            .request_device(&wgpu::DeviceDescriptor {
193                required_features: wgpu_features,
194                required_limits: descriptor.limits.clone(),
195                label: descriptor.label,
196                ..Default::default()
197            })
198            .await
199            .map_err(|e| GraphicsError::DeviceCreationFailed(e.to_string()))?;
200
201        tracing::info!(
202            "Created graphics context with features: {:?}",
203            enabled_features
204        );
205
206        Ok(Self {
207            instance,
208            adapter,
209            device,
210            queue,
211            enabled_features,
212        })
213    }
214
215    /// Get device info
216    pub fn info(&self) -> wgpu::AdapterInfo {
217        self.adapter.get_info()
218    }
219
220    /// Get device limits
221    pub fn limits(&self) -> wgpu::Limits {
222        self.device.limits()
223    }
224
225    /// Get raw wgpu device features
226    pub fn wgpu_features(&self) -> wgpu::Features {
227        self.device.features()
228    }
229
230    /// Get the enabled GPU features (high-level wrapper).
231    pub fn gpu_features(&self) -> GpuFeatures {
232        self.enabled_features
233    }
234
235    /// Check if a specific GPU feature is enabled.
236    pub fn has_feature(&self, feature: GpuFeatures) -> bool {
237        self.enabled_features.contains(feature)
238    }
239
240    /// Check if all specified GPU features are enabled.
241    pub fn has_all_features(&self, features: GpuFeatures) -> bool {
242        self.enabled_features.contains(features)
243    }
244
245    /// Assert that a feature is available, panicking with a clear message if not.
246    ///
247    /// Use this before operations that require specific features.
248    pub fn require_feature(&self, feature: GpuFeatures) {
249        if !self.has_feature(feature) {
250            panic!(
251                "GPU feature {:?} is required but not enabled.\n\
252                 Enabled features: {:?}\n\
253                 To use this feature, add it to `required_gpu_features` in GraphicsContextDescriptor.",
254                feature, self.enabled_features
255            );
256        }
257    }
258
259    // =========================================================================
260    // Texture Format Support Queries
261    // =========================================================================
262
263    /// Check if a texture format is supported for the given usages.
264    ///
265    /// # Example
266    ///
267    /// ```ignore
268    /// let supported = ctx.supports_texture_format(
269    ///     wgpu::TextureFormat::Rgba8Unorm,
270    ///     wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING
271    /// );
272    /// ```
273    pub fn supports_texture_format(
274        &self,
275        format: wgpu::TextureFormat,
276        usages: wgpu::TextureUsages,
277    ) -> bool {
278        let capabilities = self.adapter.get_texture_format_features(format);
279
280        // Check if all requested usages are supported
281        if usages.contains(wgpu::TextureUsages::TEXTURE_BINDING)
282            && !capabilities
283                .allowed_usages
284                .contains(wgpu::TextureUsages::TEXTURE_BINDING)
285        {
286            return false;
287        }
288        if usages.contains(wgpu::TextureUsages::STORAGE_BINDING)
289            && !capabilities
290                .allowed_usages
291                .contains(wgpu::TextureUsages::STORAGE_BINDING)
292        {
293            return false;
294        }
295        if usages.contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
296            && !capabilities
297                .allowed_usages
298                .contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
299        {
300            return false;
301        }
302        if usages.contains(wgpu::TextureUsages::COPY_SRC)
303            && !capabilities
304                .allowed_usages
305                .contains(wgpu::TextureUsages::COPY_SRC)
306        {
307            return false;
308        }
309        if usages.contains(wgpu::TextureUsages::COPY_DST)
310            && !capabilities
311                .allowed_usages
312                .contains(wgpu::TextureUsages::COPY_DST)
313        {
314            return false;
315        }
316
317        true
318    }
319
320    /// Get texture format capabilities.
321    ///
322    /// Returns detailed information about what operations are supported
323    /// for a given texture format.
324    pub fn texture_format_capabilities(
325        &self,
326        format: wgpu::TextureFormat,
327    ) -> wgpu::TextureFormatFeatures {
328        self.adapter.get_texture_format_features(format)
329    }
330
331    // =========================================================================
332    // Limit Queries (Convenience Methods)
333    // =========================================================================
334
335    /// Get the maximum 2D texture dimension.
336    ///
337    /// This is the maximum width and height for 2D textures.
338    #[inline]
339    pub fn max_texture_dimension_2d(&self) -> u32 {
340        self.device.limits().max_texture_dimension_2d
341    }
342
343    /// Get the maximum buffer size in bytes.
344    ///
345    /// This is the maximum size for any buffer.
346    #[inline]
347    pub fn max_buffer_size(&self) -> u64 {
348        self.device.limits().max_buffer_size
349    }
350
351    /// Get the minimum uniform buffer offset alignment.
352    ///
353    /// When using dynamic uniform buffers, offsets must be aligned to this value.
354    #[inline]
355    pub fn min_uniform_buffer_offset_alignment(&self) -> u32 {
356        self.device.limits().min_uniform_buffer_offset_alignment
357    }
358
359    /// Get the minimum storage buffer offset alignment.
360    ///
361    /// When using dynamic storage buffers, offsets must be aligned to this value.
362    #[inline]
363    pub fn min_storage_buffer_offset_alignment(&self) -> u32 {
364        self.device.limits().min_storage_buffer_offset_alignment
365    }
366
367    /// Get the maximum push constant size in bytes.
368    ///
369    /// Push constants require the `PUSH_CONSTANTS` feature.
370    /// Returns 0 if push constants are not supported.
371    #[inline]
372    pub fn max_push_constant_size(&self) -> u32 {
373        self.device.limits().max_push_constant_size
374    }
375
376    /// Get the maximum 1D texture dimension.
377    #[inline]
378    pub fn max_texture_dimension_1d(&self) -> u32 {
379        self.device.limits().max_texture_dimension_1d
380    }
381
382    /// Get the maximum 3D texture dimension.
383    #[inline]
384    pub fn max_texture_dimension_3d(&self) -> u32 {
385        self.device.limits().max_texture_dimension_3d
386    }
387
388    /// Get the maximum texture array layers.
389    #[inline]
390    pub fn max_texture_array_layers(&self) -> u32 {
391        self.device.limits().max_texture_array_layers
392    }
393
394    /// Get the maximum bind groups.
395    #[inline]
396    pub fn max_bind_groups(&self) -> u32 {
397        self.device.limits().max_bind_groups
398    }
399
400    /// Get the maximum bindings per bind group.
401    #[inline]
402    pub fn max_bindings_per_bind_group(&self) -> u32 {
403        self.device.limits().max_bindings_per_bind_group
404    }
405
406    /// Get the maximum dynamic uniform buffers per pipeline layout.
407    #[inline]
408    pub fn max_dynamic_uniform_buffers_per_pipeline_layout(&self) -> u32 {
409        self.device
410            .limits()
411            .max_dynamic_uniform_buffers_per_pipeline_layout
412    }
413
414    /// Get the maximum dynamic storage buffers per pipeline layout.
415    #[inline]
416    pub fn max_dynamic_storage_buffers_per_pipeline_layout(&self) -> u32 {
417        self.device
418            .limits()
419            .max_dynamic_storage_buffers_per_pipeline_layout
420    }
421
422    /// Get the maximum sampled textures per shader stage.
423    #[inline]
424    pub fn max_sampled_textures_per_shader_stage(&self) -> u32 {
425        self.device.limits().max_sampled_textures_per_shader_stage
426    }
427
428    /// Get the maximum samplers per shader stage.
429    #[inline]
430    pub fn max_samplers_per_shader_stage(&self) -> u32 {
431        self.device.limits().max_samplers_per_shader_stage
432    }
433
434    /// Get the maximum storage buffers per shader stage.
435    #[inline]
436    pub fn max_storage_buffers_per_shader_stage(&self) -> u32 {
437        self.device.limits().max_storage_buffers_per_shader_stage
438    }
439
440    /// Get the maximum storage textures per shader stage.
441    #[inline]
442    pub fn max_storage_textures_per_shader_stage(&self) -> u32 {
443        self.device.limits().max_storage_textures_per_shader_stage
444    }
445
446    /// Get the maximum uniform buffers per shader stage.
447    #[inline]
448    pub fn max_uniform_buffers_per_shader_stage(&self) -> u32 {
449        self.device.limits().max_uniform_buffers_per_shader_stage
450    }
451
452    /// Get the maximum uniform buffer binding size.
453    #[inline]
454    pub fn max_uniform_buffer_binding_size(&self) -> u32 {
455        self.device.limits().max_uniform_buffer_binding_size
456    }
457
458    /// Get the maximum storage buffer binding size.
459    #[inline]
460    pub fn max_storage_buffer_binding_size(&self) -> u32 {
461        self.device.limits().max_storage_buffer_binding_size
462    }
463
464    /// Get the maximum vertex buffers.
465    #[inline]
466    pub fn max_vertex_buffers(&self) -> u32 {
467        self.device.limits().max_vertex_buffers
468    }
469
470    /// Get the maximum vertex attributes.
471    #[inline]
472    pub fn max_vertex_attributes(&self) -> u32 {
473        self.device.limits().max_vertex_attributes
474    }
475
476    /// Get the maximum vertex buffer array stride.
477    #[inline]
478    pub fn max_vertex_buffer_array_stride(&self) -> u32 {
479        self.device.limits().max_vertex_buffer_array_stride
480    }
481
482    /// Get the maximum compute workgroup storage size.
483    #[inline]
484    pub fn max_compute_workgroup_storage_size(&self) -> u32 {
485        self.device.limits().max_compute_workgroup_storage_size
486    }
487
488    /// Get the maximum compute invocations per workgroup.
489    #[inline]
490    pub fn max_compute_invocations_per_workgroup(&self) -> u32 {
491        self.device.limits().max_compute_invocations_per_workgroup
492    }
493
494    /// Get the maximum compute workgroup size X.
495    #[inline]
496    pub fn max_compute_workgroup_size_x(&self) -> u32 {
497        self.device.limits().max_compute_workgroup_size_x
498    }
499
500    /// Get the maximum compute workgroup size Y.
501    #[inline]
502    pub fn max_compute_workgroup_size_y(&self) -> u32 {
503        self.device.limits().max_compute_workgroup_size_y
504    }
505
506    /// Get the maximum compute workgroup size Z.
507    #[inline]
508    pub fn max_compute_workgroup_size_z(&self) -> u32 {
509        self.device.limits().max_compute_workgroup_size_z
510    }
511
512    /// Get the maximum compute workgroups per dimension.
513    #[inline]
514    pub fn max_compute_workgroups_per_dimension(&self) -> u32 {
515        self.device.limits().max_compute_workgroups_per_dimension
516    }
517}
518
519/// Descriptor for configuring graphics context creation.
520pub struct GraphicsContextDescriptor {
521    /// GPU backends to use
522    pub backends: wgpu::Backends,
523    /// Power preference for adapter selection
524    pub power_preference: wgpu::PowerPreference,
525    /// Whether to force fallback adapter
526    pub force_fallback_adapter: bool,
527    /// Required GPU features (panics if not available).
528    ///
529    /// Use this for features that your application cannot function without.
530    pub required_gpu_features: GpuFeatures,
531    /// Requested GPU features (best-effort, logs warning if unavailable).
532    ///
533    /// Use this for features that would be nice to have but are not essential.
534    pub requested_gpu_features: GpuFeatures,
535    /// Additional raw wgpu features to enable (for features not covered by GpuFeatures).
536    pub additional_wgpu_features: wgpu::Features,
537    /// Required device limits
538    pub limits: wgpu::Limits,
539    /// Optional label for debugging
540    pub label: Option<&'static str>,
541}
542
543impl Default for GraphicsContextDescriptor {
544    fn default() -> Self {
545        Self {
546            backends: wgpu::Backends::all(),
547            power_preference: wgpu::PowerPreference::HighPerformance,
548            force_fallback_adapter: false,
549            required_gpu_features: GpuFeatures::empty(),
550            requested_gpu_features: GpuFeatures::empty(),
551            additional_wgpu_features: wgpu::Features::empty(),
552            limits: wgpu::Limits::default(),
553            label: None,
554        }
555    }
556}
557
558impl GraphicsContextDescriptor {
559    /// Create a new descriptor with default settings.
560    pub fn new() -> Self {
561        Self::default()
562    }
563
564    /// Set required GPU features (panics if not available).
565    pub fn require_features(mut self, features: GpuFeatures) -> Self {
566        self.required_gpu_features = features;
567        self
568    }
569
570    /// Set requested GPU features (best-effort, warns if unavailable).
571    pub fn request_features(mut self, features: GpuFeatures) -> Self {
572        self.requested_gpu_features = features;
573        self
574    }
575
576    /// Add additional required features.
577    pub fn with_required_features(mut self, features: GpuFeatures) -> Self {
578        self.required_gpu_features |= features;
579        self
580    }
581
582    /// Add additional requested features.
583    pub fn with_requested_features(mut self, features: GpuFeatures) -> Self {
584        self.requested_gpu_features |= features;
585        self
586    }
587
588    /// Set additional raw wgpu features (for features not covered by GpuFeatures).
589    pub fn with_wgpu_features(mut self, features: wgpu::Features) -> Self {
590        self.additional_wgpu_features = features;
591        self
592    }
593
594    /// Set the power preference.
595    pub fn power_preference(mut self, preference: wgpu::PowerPreference) -> Self {
596        self.power_preference = preference;
597        self
598    }
599
600    /// Set the backends to use.
601    pub fn backends(mut self, backends: wgpu::Backends) -> Self {
602        self.backends = backends;
603        self
604    }
605
606    /// Set the device limits.
607    pub fn limits(mut self, limits: wgpu::Limits) -> Self {
608        self.limits = limits;
609        self
610    }
611
612    /// Set the debug label.
613    pub fn label(mut self, label: &'static str) -> Self {
614        self.label = Some(label);
615        self
616    }
617}