Skip to main content

astrelis_render/
context.rs

1//! GPU context management and resource creation.
2//!
3//! This module provides [`GraphicsContext`], the core GPU abstraction that manages
4//! the WGPU device, queue, and adapter. It uses `Arc<GraphicsContext>` for cheap
5//! cloning and shared ownership across windows and rendering subsystems.
6//!
7//! # Lifecycle
8//!
9//! 1. Create with [`GraphicsContext::new_owned_sync()`] (blocking) or [`GraphicsContext::new_owned()`] (async)
10//! 2. Clone the `Arc<GraphicsContext>` to share with windows, renderers, etc.
11//! 3. Use helper methods to create GPU resources (shaders, buffers, pipelines)
12//! 4. Drop when all Arc references are released
13//!
14//! # Example
15//!
16//! ```rust,no_run
17//! use astrelis_render::GraphicsContext;
18//!
19//! let graphics = GraphicsContext::new_owned_sync()
20//!     .expect("Failed to create GPU context");
21//!
22//! // Clone for sharing (cheap Arc clone)
23//! let graphics_clone = graphics.clone();
24//!
25//! // Use for resource creation
26//! let shader = graphics.create_shader_module(/* ... */);
27//! ```
28//!
29//! # Thread Safety
30//!
31//! `GraphicsContext` is `Send + Sync` and can be safely shared across threads
32//! via `Arc<GraphicsContext>`.
33
34use astrelis_core::profiling::{profile_function, profile_scope};
35
36use crate::capability::{clamp_limits_to_adapter, RenderCapability};
37use crate::features::GpuFeatures;
38use astrelis_test_utils::{
39    GpuBindGroup, GpuBindGroupLayout, GpuBuffer, GpuComputePipeline, GpuRenderPipeline,
40    GpuSampler, GpuShaderModule, GpuTexture, RenderContext,
41};
42use std::sync::Arc;
43use wgpu::{
44    BindGroupDescriptor, BindGroupLayoutDescriptor, BufferDescriptor, ComputePipelineDescriptor,
45    RenderPipelineDescriptor, SamplerDescriptor, ShaderModuleDescriptor, TextureDescriptor,
46};
47
48/// Errors that can occur during graphics context creation.
49#[derive(Debug, Clone)]
50pub enum GraphicsError {
51    /// No suitable GPU adapter was found.
52    NoAdapter,
53
54    /// Failed to create a device.
55    DeviceCreationFailed(String),
56
57    /// Required GPU features are not supported by the adapter.
58    MissingRequiredFeatures {
59        missing: GpuFeatures,
60        adapter_name: String,
61        supported: GpuFeatures,
62    },
63
64    /// Failed to create a surface.
65    SurfaceCreationFailed(String),
66
67    /// Failed to get surface configuration.
68    SurfaceConfigurationFailed(String),
69
70    /// Failed to acquire surface texture.
71    SurfaceTextureAcquisitionFailed(String),
72
73    /// Surface is lost and needs to be recreated.
74    /// This is a recoverable condition - call `reconfigure_surface()` and retry.
75    SurfaceLost,
76
77    /// Surface texture is outdated (e.g., window was resized).
78    /// This is a recoverable condition - call `reconfigure_surface()` and retry.
79    SurfaceOutdated,
80
81    /// Not enough memory to acquire surface texture.
82    SurfaceOutOfMemory,
83
84    /// Surface acquisition timed out.
85    SurfaceTimeout,
86}
87
88impl std::fmt::Display for GraphicsError {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        match self {
91            GraphicsError::NoAdapter => {
92                write!(f, "Failed to find a suitable GPU adapter")
93            }
94            GraphicsError::DeviceCreationFailed(msg) => {
95                write!(f, "Failed to create device: {}", msg)
96            }
97            GraphicsError::MissingRequiredFeatures { missing, adapter_name, supported } => {
98                write!(
99                    f,
100                    "Required GPU features not supported by adapter '{}': {:?}\nSupported: {:?}",
101                    adapter_name, missing, supported
102                )
103            }
104            GraphicsError::SurfaceCreationFailed(msg) => {
105                write!(f, "Failed to create surface: {}", msg)
106            }
107            GraphicsError::SurfaceConfigurationFailed(msg) => {
108                write!(f, "Failed to get surface configuration: {}", msg)
109            }
110            GraphicsError::SurfaceTextureAcquisitionFailed(msg) => {
111                write!(f, "Failed to acquire surface texture: {}", msg)
112            }
113            GraphicsError::SurfaceLost => {
114                write!(f, "Surface lost - needs recreation (window minimize, GPU reset, etc.)")
115            }
116            GraphicsError::SurfaceOutdated => {
117                write!(f, "Surface outdated - needs reconfiguration (window resized)")
118            }
119            GraphicsError::SurfaceOutOfMemory => {
120                write!(f, "Out of memory acquiring surface texture")
121            }
122            GraphicsError::SurfaceTimeout => {
123                write!(f, "Timeout acquiring surface texture")
124            }
125        }
126    }
127}
128
129impl std::error::Error for GraphicsError {}
130
131/// A globally shared graphics context.
132///
133/// # Ownership Pattern
134///
135/// This type uses Arc for shared ownership:
136///
137/// ```rust,no_run
138/// use astrelis_render::GraphicsContext;
139/// use std::sync::Arc;
140///
141/// // Synchronous creation (blocks on async internally)
142/// let ctx = GraphicsContext::new_owned_sync()
143///     .expect("Failed to create graphics context"); // Returns Arc<Self>
144/// let ctx2 = ctx.clone(); // Cheap clone (Arc)
145///
146/// // Asynchronous creation (for async contexts)
147/// # async fn example() {
148/// let ctx = GraphicsContext::new_owned().await; // Returns Arc<Self>
149/// # }
150/// ```
151///
152/// Benefits of the Arc pattern:
153/// - No memory leak
154/// - Proper cleanup on drop
155/// - Better for testing (can create/destroy contexts)
156/// - Arc internally makes cloning cheap
157pub struct GraphicsContext {
158    pub(crate) instance: wgpu::Instance,
159    pub(crate) adapter: wgpu::Adapter,
160    pub(crate) device: wgpu::Device,
161    pub(crate) queue: wgpu::Queue,
162    /// The GPU features that were enabled on this context.
163    enabled_features: GpuFeatures,
164}
165
166impl GraphicsContext {
167    /// Creates a new graphics context with owned ownership (recommended).
168    ///
169    /// Returns `Arc<Self>` which can be cheaply cloned and shared.
170    /// This is the preferred method for new code as it doesn't leak memory.
171    ///
172    /// # Example
173    /// ```rust,no_run
174    /// use astrelis_render::GraphicsContext;
175    ///
176    /// # async fn example() {
177    /// let ctx = GraphicsContext::new_owned().await;
178    /// let ctx2 = ctx.clone(); // Cheap clone
179    /// # }
180    /// ```
181    pub async fn new_owned() -> Result<Arc<Self>, GraphicsError> {
182        profile_function!();
183        Self::new_owned_with_descriptor(GraphicsContextDescriptor::default()).await
184    }
185
186    /// Creates a new graphics context synchronously with owned ownership (recommended).
187    ///
188    /// **Warning:** This blocks the current thread until the context is created.
189    /// For async contexts, use [`new_owned()`](Self::new_owned) instead.
190    ///
191    /// # Errors
192    ///
193    /// Returns `GraphicsError` if:
194    /// - No suitable GPU adapter is found
195    /// - Required GPU features are not supported
196    /// - Device creation fails
197    ///
198    /// # Example
199    ///
200    /// ```rust,no_run
201    /// use astrelis_render::GraphicsContext;
202    ///
203    /// // For examples/tests: use .expect() for simplicity
204    /// let ctx = GraphicsContext::new_owned_sync()
205    ///     .expect("Failed to create graphics context");
206    ///
207    /// // For production: handle the error properly
208    /// let ctx = match GraphicsContext::new_owned_sync() {
209    ///     Ok(ctx) => ctx,
210    ///     Err(e) => {
211    ///         eprintln!("GPU initialization failed: {:?}", e);
212    ///         return;
213    ///     }
214    /// };
215    /// ```
216    pub fn new_owned_sync() -> Result<Arc<Self>, GraphicsError> {
217        profile_function!();
218        pollster::block_on(Self::new_owned())
219    }
220
221    /// Creates a new graphics context with custom descriptor (owned).
222    pub async fn new_owned_with_descriptor(descriptor: GraphicsContextDescriptor) -> Result<Arc<Self>, GraphicsError> {
223        let context = Self::create_context_internal(descriptor).await?;
224        Ok(Arc::new(context))
225    }
226
227    /// Internal method to create context without deciding on ownership pattern.
228    async fn create_context_internal(descriptor: GraphicsContextDescriptor) -> Result<Self, GraphicsError> {
229        profile_function!();
230
231        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
232            backends: descriptor.backends,
233            ..Default::default()
234        });
235
236        let adapter = {
237            profile_scope!("request_adapter");
238            instance
239                .request_adapter(&wgpu::RequestAdapterOptions {
240                    power_preference: descriptor.power_preference,
241                    compatible_surface: None,
242                    force_fallback_adapter: descriptor.force_fallback_adapter,
243                })
244                .await
245                .map_err(|_| GraphicsError::NoAdapter)?
246        };
247
248        // Check required features
249        let required_result = descriptor.required_gpu_features.check_support(&adapter);
250        if let Some(missing) = required_result.missing() {
251            return Err(GraphicsError::MissingRequiredFeatures {
252                missing,
253                adapter_name: adapter.get_info().name.clone(),
254                supported: GpuFeatures::from_wgpu(adapter.features()),
255            });
256        }
257
258        // Determine which requested features are available
259        let available_requested = descriptor.requested_gpu_features
260            & GpuFeatures::from_wgpu(adapter.features());
261
262        // Log which requested features were not available
263        let unavailable_requested =
264            descriptor.requested_gpu_features - available_requested;
265        if !unavailable_requested.is_empty() {
266            tracing::warn!(
267                "Some requested GPU features are not available: {:?}",
268                unavailable_requested
269            );
270        }
271
272        // Combine all features to enable
273        let enabled_features = descriptor.required_gpu_features | available_requested;
274        let wgpu_features = enabled_features.to_wgpu() | descriptor.additional_wgpu_features;
275
276        // Clamp requested limits to adapter capabilities to prevent device creation failure
277        let adapter_limits = adapter.limits();
278        let clamped_limits = clamp_limits_to_adapter(&descriptor.limits, &adapter_limits);
279
280        let (device, queue) = {
281            profile_scope!("request_device");
282            adapter
283                .request_device(&wgpu::DeviceDescriptor {
284                    required_features: wgpu_features,
285                    required_limits: clamped_limits,
286                    label: descriptor.label,
287                    ..Default::default()
288                })
289                .await
290                .map_err(|e| GraphicsError::DeviceCreationFailed(e.to_string()))?
291        };
292
293        tracing::info!(
294            "Created graphics context with features: {:?}",
295            enabled_features
296        );
297
298        Ok(Self {
299            instance,
300            adapter,
301            device,
302            queue,
303            enabled_features,
304        })
305    }
306
307    /// Get a reference to the wgpu device.
308    pub fn device(&self) -> &wgpu::Device {
309        &self.device
310    }
311
312    /// Get a reference to the wgpu queue.
313    pub fn queue(&self) -> &wgpu::Queue {
314        &self.queue
315    }
316
317    /// Get a reference to the wgpu adapter.
318    pub fn adapter(&self) -> &wgpu::Adapter {
319        &self.adapter
320    }
321
322    /// Get a reference to the wgpu instance.
323    pub fn instance(&self) -> &wgpu::Instance {
324        &self.instance
325    }
326
327    /// Get device info
328    pub fn info(&self) -> wgpu::AdapterInfo {
329        self.adapter.get_info()
330    }
331
332    /// Get device limits
333    pub fn limits(&self) -> wgpu::Limits {
334        self.device.limits()
335    }
336
337    /// Get raw wgpu device features
338    pub fn wgpu_features(&self) -> wgpu::Features {
339        self.device.features()
340    }
341
342    /// Get the enabled GPU features (high-level wrapper).
343    pub fn gpu_features(&self) -> GpuFeatures {
344        self.enabled_features
345    }
346
347    /// Check if a specific GPU feature is enabled.
348    pub fn has_feature(&self, feature: GpuFeatures) -> bool {
349        self.enabled_features.contains(feature)
350    }
351
352    /// Check if all specified GPU features are enabled.
353    pub fn has_all_features(&self, features: GpuFeatures) -> bool {
354        self.enabled_features.contains(features)
355    }
356
357    /// Assert that a feature is available, panicking with a clear message if not.
358    ///
359    /// Use this before operations that require specific features.
360    pub fn require_feature(&self, feature: GpuFeatures) {
361        if !self.has_feature(feature) {
362            panic!(
363                "GPU feature {:?} is required but not enabled.\n\
364                 Enabled features: {:?}\n\
365                 To use this feature, add it to `required_gpu_features` in GraphicsContextDescriptor.",
366                feature, self.enabled_features
367            );
368        }
369    }
370
371    // =========================================================================
372    // Texture Format Support Queries
373    // =========================================================================
374
375    /// Check if a texture format is supported for the given usages.
376    ///
377    /// # Example
378    ///
379    /// ```ignore
380    /// let supported = ctx.supports_texture_format(
381    ///     wgpu::TextureFormat::Rgba8Unorm,
382    ///     wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING
383    /// );
384    /// ```
385    pub fn supports_texture_format(
386        &self,
387        format: wgpu::TextureFormat,
388        usages: wgpu::TextureUsages,
389    ) -> bool {
390        self.adapter
391            .get_texture_format_features(format)
392            .allowed_usages
393            .contains(usages)
394    }
395
396    /// Get texture format capabilities.
397    ///
398    /// Returns detailed information about what operations are supported
399    /// for a given texture format.
400    pub fn texture_format_capabilities(
401        &self,
402        format: wgpu::TextureFormat,
403    ) -> wgpu::TextureFormatFeatures {
404        self.adapter.get_texture_format_features(format)
405    }
406}
407
408/// Descriptor for configuring graphics context creation.
409pub struct GraphicsContextDescriptor {
410    /// GPU backends to use
411    pub backends: wgpu::Backends,
412    /// Power preference for adapter selection
413    pub power_preference: wgpu::PowerPreference,
414    /// Whether to force fallback adapter
415    pub force_fallback_adapter: bool,
416    /// Required GPU features (panics if not available).
417    ///
418    /// Use this for features that your application cannot function without.
419    pub required_gpu_features: GpuFeatures,
420    /// Requested GPU features (best-effort, logs warning if unavailable).
421    ///
422    /// Use this for features that would be nice to have but are not essential.
423    pub requested_gpu_features: GpuFeatures,
424    /// Additional raw wgpu features to enable (for features not covered by GpuFeatures).
425    pub additional_wgpu_features: wgpu::Features,
426    /// Required device limits
427    pub limits: wgpu::Limits,
428    /// Optional label for debugging
429    pub label: Option<&'static str>,
430}
431
432impl Default for GraphicsContextDescriptor {
433    fn default() -> Self {
434        Self {
435            backends: wgpu::Backends::all(),
436            power_preference: wgpu::PowerPreference::HighPerformance,
437            force_fallback_adapter: false,
438            required_gpu_features: GpuFeatures::empty(),
439            requested_gpu_features: GpuFeatures::empty(),
440            additional_wgpu_features: wgpu::Features::empty(),
441            limits: wgpu::Limits::default(),
442            label: None,
443        }
444    }
445}
446
447impl GraphicsContextDescriptor {
448    /// Create a new descriptor with default settings.
449    pub fn new() -> Self {
450        Self::default()
451    }
452
453    /// Set required GPU features (panics if not available).
454    pub fn require_features(mut self, features: GpuFeatures) -> Self {
455        self.required_gpu_features = features;
456        self
457    }
458
459    /// Set requested GPU features (best-effort, warns if unavailable).
460    pub fn request_features(mut self, features: GpuFeatures) -> Self {
461        self.requested_gpu_features = features;
462        self
463    }
464
465    /// Add additional required features.
466    pub fn with_required_features(mut self, features: GpuFeatures) -> Self {
467        self.required_gpu_features |= features;
468        self
469    }
470
471    /// Add additional requested features.
472    pub fn with_requested_features(mut self, features: GpuFeatures) -> Self {
473        self.requested_gpu_features |= features;
474        self
475    }
476
477    /// Set additional raw wgpu features (for features not covered by GpuFeatures).
478    pub fn with_wgpu_features(mut self, features: wgpu::Features) -> Self {
479        self.additional_wgpu_features = features;
480        self
481    }
482
483    /// Set the power preference.
484    pub fn power_preference(mut self, preference: wgpu::PowerPreference) -> Self {
485        self.power_preference = preference;
486        self
487    }
488
489    /// Set the backends to use.
490    pub fn backends(mut self, backends: wgpu::Backends) -> Self {
491        self.backends = backends;
492        self
493    }
494
495    /// Set the device limits.
496    pub fn limits(mut self, limits: wgpu::Limits) -> Self {
497        self.limits = limits;
498        self
499    }
500
501    /// Set the debug label.
502    pub fn label(mut self, label: &'static str) -> Self {
503        self.label = Some(label);
504        self
505    }
506
507    /// Require a capability — its features become required, limits are merged.
508    ///
509    /// If the adapter doesn't support the capability's required features,
510    /// device creation will fail with [`GraphicsError::MissingRequiredFeatures`].
511    ///
512    /// # Example
513    ///
514    /// ```ignore
515    /// use astrelis_render::{GraphicsContextDescriptor, GpuProfiler};
516    ///
517    /// let desc = GraphicsContextDescriptor::new()
518    ///     .require_capability::<GpuProfiler>();
519    /// ```
520    pub fn require_capability<T: RenderCapability>(mut self) -> Self {
521        let reqs = T::requirements();
522        self.required_gpu_features |= reqs.required_features;
523        self.required_gpu_features |= reqs.requested_features;
524        self.additional_wgpu_features |= reqs.additional_wgpu_features;
525        crate::capability::merge_limits_max(&mut self.limits, &reqs.min_limits);
526        tracing::trace!("Required capability: {}", T::name());
527        self
528    }
529
530    /// Request a capability — required features stay required, requested features
531    /// stay optional, limits are merged.
532    ///
533    /// The capability's required features are added as required, and its
534    /// requested features are added as requested (best-effort). Limits are
535    /// merged and clamped to adapter capabilities during device creation.
536    ///
537    /// # Example
538    ///
539    /// ```ignore
540    /// use astrelis_render::GraphicsContextDescriptor;
541    /// use astrelis_render::batched::BestBatchCapability;
542    ///
543    /// let desc = GraphicsContextDescriptor::new()
544    ///     .request_capability::<BestBatchCapability>();
545    /// ```
546    pub fn request_capability<T: RenderCapability>(mut self) -> Self {
547        let reqs = T::requirements();
548        self.required_gpu_features |= reqs.required_features;
549        self.requested_gpu_features |= reqs.requested_features;
550        self.additional_wgpu_features |= reqs.additional_wgpu_features;
551        crate::capability::merge_limits_max(&mut self.limits, &reqs.min_limits);
552        tracing::trace!("Requested capability: {}", T::name());
553        self
554    }
555}
556
557// ============================================================================
558// RenderContext trait implementation
559// ============================================================================
560
561impl RenderContext for GraphicsContext {
562    fn create_buffer(&self, desc: &BufferDescriptor) -> GpuBuffer {
563        let buffer = self.device().create_buffer(desc);
564        GpuBuffer::from_wgpu(buffer)
565    }
566
567    fn write_buffer(&self, buffer: &GpuBuffer, offset: u64, data: &[u8]) {
568        let wgpu_buffer = buffer.as_wgpu();
569        self.queue().write_buffer(wgpu_buffer, offset, data);
570    }
571
572    fn create_texture(&self, desc: &TextureDescriptor) -> GpuTexture {
573        let texture = self.device().create_texture(desc);
574        GpuTexture::from_wgpu(texture)
575    }
576
577    fn create_shader_module(&self, desc: &ShaderModuleDescriptor) -> GpuShaderModule {
578        let module = self.device().create_shader_module(desc.clone());
579        GpuShaderModule::from_wgpu(module)
580    }
581
582    fn create_render_pipeline(&self, desc: &RenderPipelineDescriptor) -> GpuRenderPipeline {
583        let pipeline = self.device().create_render_pipeline(desc);
584        GpuRenderPipeline::from_wgpu(pipeline)
585    }
586
587    fn create_compute_pipeline(&self, desc: &ComputePipelineDescriptor) -> GpuComputePipeline {
588        let pipeline = self.device().create_compute_pipeline(desc);
589        GpuComputePipeline::from_wgpu(pipeline)
590    }
591
592    fn create_bind_group_layout(&self, desc: &BindGroupLayoutDescriptor) -> GpuBindGroupLayout {
593        let layout = self.device().create_bind_group_layout(desc);
594        GpuBindGroupLayout::from_wgpu(layout)
595    }
596
597    fn create_bind_group(&self, desc: &BindGroupDescriptor) -> GpuBindGroup {
598        let bind_group = self.device().create_bind_group(desc);
599        GpuBindGroup::from_wgpu(bind_group)
600    }
601
602    fn create_sampler(&self, desc: &SamplerDescriptor) -> GpuSampler {
603        let sampler = self.device().create_sampler(desc);
604        GpuSampler::from_wgpu(sampler)
605    }
606}
607
608#[cfg(test)]
609mod tests {
610    #[cfg(feature = "mock")]
611    use super::*;
612    #[cfg(feature = "mock")]
613    use astrelis_test_utils::MockRenderContext;
614
615    #[test]
616    #[cfg(feature = "mock")]
617    fn test_render_context_trait_object() {
618        // Test that we can use both GraphicsContext and MockRenderContext
619        // polymorphically through the RenderContext trait
620
621        let mock_ctx = MockRenderContext::new();
622
623        fn uses_render_context(ctx: &dyn RenderContext) {
624            let buffer = ctx.create_buffer(&BufferDescriptor {
625                label: Some("Test Buffer"),
626                size: 256,
627                usage: wgpu::BufferUsages::UNIFORM,
628                mapped_at_creation: false,
629            });
630
631            ctx.write_buffer(&buffer, 0, &[0u8; 256]);
632        }
633
634        // Should work with mock context
635        uses_render_context(&mock_ctx);
636
637        // Verify the mock recorded the calls
638        let calls = mock_ctx.calls();
639        assert_eq!(calls.len(), 2); // create_buffer + write_buffer
640    }
641}