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