Skip to main content

oxigdal_gpu/
context.rs

1//! GPU context management for OxiGDAL.
2//!
3//! This module handles WGPU device initialization, adapter selection,
4//! and resource management for GPU-accelerated operations.
5
6use crate::error::{GpuError, GpuResult};
7use crate::pipeline_cache::{SharedPipelineCache, new_shared_pipeline_cache};
8use crate::workgroup_tuner::WorkgroupTuner;
9use std::sync::Arc;
10use std::sync::atomic::AtomicBool;
11use tracing::{debug, info, warn};
12use wgpu::{
13    Adapter, AdapterInfo, Backend, Backends, Device, DeviceDescriptor, Features, Instance,
14    InstanceDescriptor, Limits, PowerPreference, Queue, RequestAdapterOptions,
15};
16
17/// GPU backend preference for adapter selection.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum BackendPreference {
20    /// Prefer Vulkan backend (cross-platform, best performance on Linux/Windows).
21    Vulkan,
22    /// Prefer Metal backend (best performance on macOS/iOS).
23    Metal,
24    /// Prefer DX12 backend (best performance on Windows).
25    DX12,
26    /// Prefer WebGPU backend (for browser environments).
27    WebGPU,
28    /// Auto-select the best available backend for the platform.
29    Auto,
30    /// Try all available backends in order of preference.
31    All,
32}
33
34impl BackendPreference {
35    /// Convert to WGPU backends flags.
36    pub fn to_backends(&self) -> Backends {
37        match self {
38            Self::Vulkan => Backends::VULKAN,
39            Self::Metal => Backends::METAL,
40            Self::DX12 => Backends::DX12,
41            Self::WebGPU => Backends::BROWSER_WEBGPU,
42            Self::Auto => Backends::PRIMARY,
43            Self::All => Backends::all(),
44        }
45    }
46
47    /// Get platform-specific default backend.
48    pub fn platform_default() -> Self {
49        #[cfg(target_os = "macos")]
50        return Self::Metal;
51
52        #[cfg(target_os = "windows")]
53        return Self::DX12;
54
55        #[cfg(target_os = "linux")]
56        return Self::Vulkan;
57
58        #[cfg(target_arch = "wasm32")]
59        return Self::WebGPU;
60
61        #[cfg(not(any(
62            target_os = "macos",
63            target_os = "windows",
64            target_os = "linux",
65            target_arch = "wasm32"
66        )))]
67        return Self::Auto;
68    }
69}
70
71/// Power preference for GPU selection.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum GpuPowerPreference {
74    /// Prefer low power consumption (integrated GPU).
75    LowPower,
76    /// Prefer high performance (discrete GPU).
77    HighPerformance,
78    /// No preference, use system default.
79    Default,
80}
81
82impl From<GpuPowerPreference> for PowerPreference {
83    fn from(pref: GpuPowerPreference) -> Self {
84        match pref {
85            GpuPowerPreference::LowPower => PowerPreference::LowPower,
86            GpuPowerPreference::HighPerformance => PowerPreference::HighPerformance,
87            GpuPowerPreference::Default => PowerPreference::None,
88        }
89    }
90}
91
92/// Configuration for GPU context initialization.
93#[derive(Debug, Clone)]
94pub struct GpuContextConfig {
95    /// Backend preference for adapter selection.
96    pub backend: BackendPreference,
97    /// Power preference for GPU selection.
98    pub power_preference: GpuPowerPreference,
99    /// Required GPU features.
100    pub required_features: Features,
101    /// Required GPU limits.
102    pub required_limits: Option<Limits>,
103    /// Enable debug mode (validation layers).
104    pub debug: bool,
105    /// Label for the device (for debugging).
106    pub label: Option<String>,
107}
108
109impl Default for GpuContextConfig {
110    fn default() -> Self {
111        Self {
112            backend: BackendPreference::platform_default(),
113            power_preference: GpuPowerPreference::HighPerformance,
114            required_features: Features::empty(),
115            required_limits: None,
116            debug: cfg!(debug_assertions),
117            label: Some("OxiGDAL GPU Context".to_string()),
118        }
119    }
120}
121
122impl GpuContextConfig {
123    /// Create a new GPU context configuration.
124    pub fn new() -> Self {
125        Self::default()
126    }
127
128    /// Set backend preference.
129    pub fn with_backend(mut self, backend: BackendPreference) -> Self {
130        self.backend = backend;
131        self
132    }
133
134    /// Set power preference.
135    pub fn with_power_preference(mut self, power: GpuPowerPreference) -> Self {
136        self.power_preference = power;
137        self
138    }
139
140    /// Set required features.
141    pub fn with_features(mut self, features: Features) -> Self {
142        self.required_features = features;
143        self
144    }
145
146    /// Set required limits.
147    pub fn with_limits(mut self, limits: Limits) -> Self {
148        self.required_limits = Some(limits);
149        self
150    }
151
152    /// Enable debug mode.
153    pub fn with_debug(mut self, debug: bool) -> Self {
154        self.debug = debug;
155        self
156    }
157
158    /// Set device label.
159    pub fn with_label(mut self, label: impl Into<String>) -> Self {
160        self.label = Some(label.into());
161        self
162    }
163
164    /// Request `SHADER_F16` GPU feature support.
165    ///
166    /// When this flag is set the context will require the adapter to expose
167    /// `wgpu::Features::SHADER_F16`, which enables the `enable f16;`
168    /// directive in WGSL shaders and allows native half-precision arithmetic
169    /// on the GPU.
170    ///
171    /// # Availability
172    ///
173    /// Not all adapters support this feature.  Check with
174    /// [`GpuContext::supports_feature`] after context creation.  If the
175    /// adapter lacks support, context creation will fail — consider first
176    /// checking via [`wgpu::Adapter::features`] or falling back to the
177    /// widening path provided by [`crate::buffer::from_f16_slice_widening`].
178    pub fn with_f16_support(mut self) -> Self {
179        self.required_features |= Features::SHADER_F16;
180        self
181    }
182
183    /// Request `IMMEDIATES` GPU feature support (push constants).
184    ///
185    /// When this flag is set the context will require the adapter to expose
186    /// [`wgpu::Features::IMMEDIATES`], which enables:
187    /// - `var<immediate>` variables in WGSL shaders.
188    /// - [`wgpu::PipelineLayoutDescriptor::immediate_size`] > 0.
189    /// - [`wgpu::ComputePass::set_immediates`] / [`wgpu::RenderPass::set_immediates`].
190    ///
191    /// This corresponds to Vulkan push constants and is available on Vulkan,
192    /// Metal, DX12, and WebGPU (as a proposal).
193    ///
194    /// # Availability
195    ///
196    /// Check adapter support before requesting this feature.  If unsupported,
197    /// use a uniform buffer to pass small per-dispatch constants instead.
198    pub fn with_push_constants(mut self) -> Self {
199        self.required_features |= Features::IMMEDIATES;
200        self
201    }
202
203    /// Request cooperative-matrix and subgroup GPU features.
204    ///
205    /// When this flag is set the context will attempt to request:
206    /// - [`wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX`] — hardware tile
207    ///   MMA (WMMA / tensor-core) support.
208    /// - [`wgpu::Features::SUBGROUP`] — subgroup intrinsics in compute and
209    ///   fragment shaders (required by the cooperative-matrix extension).
210    ///
211    /// Both flags are added to [`GpuContextConfig::required_features`].
212    /// If the adapter does not expose these features, context creation will
213    /// fail.  Use [`crate::cooperative_matrix::supports_cooperative_matrix`]
214    /// after creating a context **without** this method to check availability
215    /// before upgrading.
216    ///
217    /// # Fallback
218    ///
219    /// The workgroup-tiled GEMM path in
220    /// [`crate::cooperative_matrix::build_cooperative_matrix_gemm_pipeline`]
221    /// is correct on **all** adapters regardless of this flag.  Requesting
222    /// cooperative-matrix features is only necessary when the shader code
223    /// explicitly uses `subgroupMatrix*` builtins.
224    pub fn with_cooperative_matrix(mut self) -> Self {
225        self.required_features |= Features::SUBGROUP;
226        self.required_features |= Features::EXPERIMENTAL_COOPERATIVE_MATRIX;
227        self
228    }
229
230    /// Build a [`GpuContext`] from this configuration.
231    ///
232    /// Convenience async builder that calls [`GpuContext::with_config`].
233    ///
234    /// # Errors
235    ///
236    /// Returns an error if no suitable GPU adapter is found or device
237    /// request fails (e.g., a required feature is unsupported).
238    pub async fn build(self) -> crate::error::GpuResult<GpuContext> {
239        GpuContext::with_config(self).await
240    }
241}
242
243/// GPU context holding device and queue.
244///
245/// This is the main entry point for GPU operations. It manages the WGPU
246/// device and queue, and provides methods for creating buffers, pipelines,
247/// and executing compute shaders.
248#[derive(Clone)]
249pub struct GpuContext {
250    /// WGPU instance.
251    instance: Arc<Instance>,
252    /// WGPU adapter.
253    adapter: Arc<Adapter>,
254    /// WGPU device.
255    device: Arc<Device>,
256    /// WGPU queue.
257    queue: Arc<Queue>,
258    /// Adapter information.
259    adapter_info: AdapterInfo,
260    /// Device limits.
261    limits: Limits,
262    /// Workgroup sizes tuned to this adapter's limits.
263    pub tuner: WorkgroupTuner,
264    /// Atomic flag set to `true` when the GPU device is lost.
265    ///
266    /// Written from the wgpu device-lost callback (a background thread);
267    /// read on the submission path before every `queue.submit`.
268    device_lost: Arc<AtomicBool>,
269    /// Shared cache of compiled compute pipelines.
270    ///
271    /// Keyed by `(shader_hash, entry_point, layout_tag)`.  Must be cleared
272    /// whenever the underlying `wgpu::Device` is replaced (device-lost
273    /// recovery), because compiled pipelines are bound to a specific device.
274    pipeline_cache: SharedPipelineCache,
275}
276
277impl GpuContext {
278    /// Create a new GPU context with default configuration.
279    ///
280    /// # Errors
281    ///
282    /// Returns an error if no suitable GPU adapter is found or device
283    /// request fails.
284    pub async fn new() -> GpuResult<Self> {
285        Self::with_config(GpuContextConfig::default()).await
286    }
287
288    /// Create a new GPU context with custom configuration.
289    ///
290    /// # Errors
291    ///
292    /// Returns an error if no suitable GPU adapter is found or device
293    /// request fails.
294    pub async fn with_config(config: GpuContextConfig) -> GpuResult<Self> {
295        info!(
296            "Initializing GPU context with backend: {:?}",
297            config.backend
298        );
299
300        // Create WGPU instance
301        let instance = Instance::new(InstanceDescriptor {
302            backends: config.backend.to_backends(),
303            ..InstanceDescriptor::new_without_display_handle()
304        });
305
306        // Request adapter
307        let adapter = Self::request_adapter(&instance, &config).await?;
308        let adapter_info = adapter.get_info();
309
310        info!(
311            "Selected GPU adapter: {} ({:?})",
312            adapter_info.name, adapter_info.backend
313        );
314        debug!("Adapter info: {:?}", adapter_info);
315
316        // Get adapter limits
317        let adapter_limits = adapter.limits();
318
319        // Derive workgroup sizes from the adapter's actual capabilities before
320        // we potentially clamp/override them via `required_limits`.
321        let tuner = WorkgroupTuner::derive_from_limits(&adapter_limits);
322
323        let limits = config
324            .required_limits
325            .unwrap_or_else(|| Self::default_limits(&adapter_limits));
326
327        // Validate limits
328        if !Self::validate_limits(&limits, &adapter_limits) {
329            return Err(GpuError::device_request(format!(
330                "Requested limits exceed adapter capabilities: \
331                 max_compute_workgroup_size_x: {} (adapter: {})",
332                limits.max_compute_workgroup_size_x, adapter_limits.max_compute_workgroup_size_x
333            )));
334        }
335
336        // Request device
337        let (device, queue) = adapter
338            .request_device(&DeviceDescriptor {
339                label: config.label.as_deref(),
340                required_features: config.required_features,
341                required_limits: limits.clone(),
342                memory_hints: Default::default(),
343                experimental_features: Default::default(),
344                trace: Default::default(),
345            })
346            .await
347            .map_err(|e| GpuError::device_request(e.to_string()))?;
348
349        // Install device-lost callback: atomically flags that the device has
350        // been lost so that subsequent `queue.submit` calls can short-circuit.
351        let device_lost = Arc::new(AtomicBool::new(false));
352        let flag_clone = Arc::clone(&device_lost);
353        device.set_device_lost_callback(move |_reason, message| {
354            warn!("GPU device lost: {}", message);
355            flag_clone.store(true, std::sync::atomic::Ordering::SeqCst);
356        });
357
358        info!("GPU device created successfully");
359        debug!("Device limits: {:?}", limits);
360        debug!("Workgroup tuner: {:?}", tuner);
361
362        Ok(Self {
363            instance: Arc::new(instance),
364            adapter: Arc::new(adapter),
365            device: Arc::new(device),
366            queue: Arc::new(queue),
367            adapter_info,
368            limits,
369            tuner,
370            device_lost,
371            pipeline_cache: new_shared_pipeline_cache(),
372        })
373    }
374
375    /// Request a suitable GPU adapter.
376    async fn request_adapter(instance: &Instance, config: &GpuContextConfig) -> GpuResult<Adapter> {
377        let adapter = instance
378            .request_adapter(&RequestAdapterOptions {
379                power_preference: config.power_preference.into(),
380                force_fallback_adapter: false,
381                compatible_surface: None,
382            })
383            .await;
384
385        adapter.map_err(|_| {
386            let backends = match config.backend {
387                BackendPreference::Auto => "Auto (PRIMARY)".to_string(),
388                BackendPreference::All => "All".to_string(),
389                backend => format!("{backend:?}"),
390            };
391            GpuError::no_adapter(backends)
392        })
393    }
394
395    /// Get default limits based on adapter capabilities.
396    fn default_limits(adapter_limits: &Limits) -> Limits {
397        Limits {
398            max_compute_workgroup_size_x: adapter_limits.max_compute_workgroup_size_x.min(256),
399            max_compute_workgroup_size_y: adapter_limits.max_compute_workgroup_size_y.min(256),
400            max_compute_workgroup_size_z: adapter_limits.max_compute_workgroup_size_z.min(64),
401            max_compute_invocations_per_workgroup: adapter_limits
402                .max_compute_invocations_per_workgroup
403                .min(256),
404            max_compute_workgroups_per_dimension: adapter_limits
405                .max_compute_workgroups_per_dimension,
406            ..Default::default()
407        }
408    }
409
410    /// Validate that requested limits don't exceed adapter capabilities.
411    fn validate_limits(requested: &Limits, adapter: &Limits) -> bool {
412        requested.max_compute_workgroup_size_x <= adapter.max_compute_workgroup_size_x
413            && requested.max_compute_workgroup_size_y <= adapter.max_compute_workgroup_size_y
414            && requested.max_compute_workgroup_size_z <= adapter.max_compute_workgroup_size_z
415            && requested.max_compute_invocations_per_workgroup
416                <= adapter.max_compute_invocations_per_workgroup
417    }
418
419    /// Get the WGPU device.
420    pub fn device(&self) -> &Device {
421        &self.device
422    }
423
424    /// Get the WGPU queue.
425    pub fn queue(&self) -> &Queue {
426        &self.queue
427    }
428
429    /// Get the WGPU adapter.
430    pub fn adapter(&self) -> &Adapter {
431        &self.adapter
432    }
433
434    /// Get the WGPU instance.
435    pub fn instance(&self) -> &Instance {
436        &self.instance
437    }
438
439    /// Get adapter information.
440    pub fn adapter_info(&self) -> &AdapterInfo {
441        &self.adapter_info
442    }
443
444    /// Get device limits.
445    pub fn limits(&self) -> &Limits {
446        &self.limits
447    }
448
449    /// Get the backend being used.
450    pub fn backend(&self) -> Backend {
451        self.adapter_info.backend
452    }
453
454    /// Check if the device supports a specific feature.
455    pub fn supports_feature(&self, feature: Features) -> bool {
456        self.device.features().contains(feature)
457    }
458
459    /// Get maximum workgroup size for compute shaders.
460    pub fn max_workgroup_size(&self) -> (u32, u32, u32) {
461        (
462            self.limits.max_compute_workgroup_size_x,
463            self.limits.max_compute_workgroup_size_y,
464            self.limits.max_compute_workgroup_size_z,
465        )
466    }
467
468    /// Poll the device for completed operations.
469    ///
470    /// This should be called periodically to process GPU operations.
471    pub fn poll(&self, _wait: bool) {
472        // wgpu 28 doesn't have explicit poll control, device polls automatically
473        // This method is kept for API compatibility
474    }
475
476    /// Spawn a background thread that keeps the wgpu device polled.
477    ///
478    /// This is an **advanced helper** intended for callers that run async GPU
479    /// readbacks without a runtime that issues device polls automatically (e.g.
480    /// bare `pollster` or custom executors).
481    ///
482    /// The spawned thread calls `device.poll(wgpu::PollType::Poll)` in a tight
483    /// loop with a 1 ms sleep between iterations.  Call
484    /// [`std::thread::JoinHandle::join`] when the thread is no longer needed
485    /// (the caller is responsible for signalling termination through a shared
486    /// flag or by dropping all `Arc<Device>` clones and letting the thread
487    /// detect the closed handle).
488    ///
489    /// In most tokio/async-std environments the runtime's built-in submission
490    /// loop makes this unnecessary.
491    pub fn spawn_poll_task(&self) -> std::thread::JoinHandle<()> {
492        let device = Arc::clone(&self.device);
493        std::thread::spawn(move || {
494            loop {
495                let poll_result = device.poll(wgpu::PollType::Poll);
496                // If the device queue is empty and all work is done, sleep
497                // more aggressively to avoid spinning.
498                if matches!(poll_result, Ok(wgpu::PollStatus::QueueEmpty)) {
499                    std::thread::sleep(std::time::Duration::from_millis(5));
500                } else {
501                    std::thread::sleep(std::time::Duration::from_millis(1));
502                }
503            }
504        })
505    }
506
507    /// Check if the device is still valid.
508    pub fn is_valid(&self) -> bool {
509        // Try to create a small buffer as a health check
510        self.device.create_buffer(&wgpu::BufferDescriptor {
511            label: Some("health_check"),
512            size: 4,
513            usage: wgpu::BufferUsages::UNIFORM,
514            mapped_at_creation: false,
515        });
516        true
517    }
518
519    /// Override the workgroup tuner (useful in tests or for custom tuning).
520    ///
521    /// Returns `self` so it can be chained in a builder-style pattern:
522    ///
523    /// ```rust,no_run
524    /// # use oxigdal_gpu::{GpuContext, WorkgroupTuner};
525    /// # async fn ex() -> Result<(), Box<dyn std::error::Error>> {
526    /// let gpu = GpuContext::new().await?.with_tuner(WorkgroupTuner::unlimited());
527    /// # Ok(())
528    /// # }
529    /// ```
530    pub fn with_tuner(mut self, tuner: WorkgroupTuner) -> Self {
531        self.tuner = tuner;
532        self
533    }
534
535    // -------------------------------------------------------------------------
536    // Device-lost recovery
537    // -------------------------------------------------------------------------
538
539    /// Returns `true` if the GPU device has been lost and operations will fail.
540    ///
541    /// The flag is set atomically by the wgpu device-lost callback registered
542    /// during [`GpuContext::new`] / [`GpuContext::with_config`].
543    pub fn is_device_lost(&self) -> bool {
544        self.device_lost.load(std::sync::atomic::Ordering::SeqCst)
545    }
546
547    /// Checks whether the device has been lost and returns an error if so.
548    ///
549    /// Call this before every `queue.submit` to short-circuit operations on a
550    /// defunct device instead of generating silent GPU errors.
551    pub fn check_device_lost(&self) -> GpuResult<()> {
552        if self.is_device_lost() {
553            Err(GpuError::device_lost("device was lost"))
554        } else {
555            Ok(())
556        }
557    }
558
559    /// Returns a reference to the shared pipeline cache.
560    ///
561    /// The cache maps `(shader_hash, entry_point, layout_tag)` keys to
562    /// previously compiled [`wgpu::ComputePipeline`]s wrapped in `Arc`.
563    ///
564    /// Kernel implementations can call
565    /// [`PipelineCache::get_or_insert_with`][crate::pipeline_cache::PipelineCache::get_or_insert_with]
566    /// on the inner [`crate::pipeline_cache::PipelineCache`] to avoid
567    /// recompiling the same WGSL shader on every kernel construction.
568    ///
569    /// # Examples
570    ///
571    /// ```rust,no_run
572    /// use oxigdal_gpu::{GpuContext, pipeline_cache::PipelineCacheKey};
573    ///
574    /// # async fn ex(ctx: &GpuContext) {
575    /// let cache = ctx.pipeline_cache();
576    /// if let Ok(mut guard) = cache.lock() {
577    ///     println!("cached pipelines: {}", guard.len());
578    /// }
579    /// # }
580    /// ```
581    pub fn pipeline_cache(&self) -> &SharedPipelineCache {
582        &self.pipeline_cache
583    }
584
585    /// Replaces the internal device-lost flag with an externally-supplied one.
586    ///
587    /// This builder method is intended **for testing only** — it allows a test
588    /// to inject a shared `Arc<AtomicBool>` whose state it controls directly,
589    /// exercising the `check_device_lost` / `is_device_lost` paths without
590    /// requiring a real GPU device.
591    ///
592    /// ```rust
593    /// use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
594    /// // NB: GpuContext::new() requires a GPU; this is conceptual.
595    /// // let ctx = ctx.with_device_lost_flag(Arc::new(AtomicBool::new(true)));
596    /// ```
597    pub fn with_device_lost_flag(mut self, flag: Arc<AtomicBool>) -> Self {
598        self.device_lost = flag;
599        self
600    }
601
602    /// Attempts to recreate the GPU device on the same adapter.
603    ///
604    /// This creates a fresh `GpuContext` via the same wgpu instance, selecting
605    /// an adapter with matching properties to the one this context was built
606    /// on.  The old context **must be dropped** after calling this method;
607    /// continuing to use it will only generate further device-lost errors.
608    ///
609    /// # Errors
610    ///
611    /// Returns an error if adapter enumeration fails or the device cannot be
612    /// re-created (e.g., the GPU has been physically removed).
613    pub async fn reinitialize(&self) -> GpuResult<GpuContext> {
614        // Re-use the existing instance so we stay within the same set of
615        // enabled backends.  We enumerate adapters and pick the first one
616        // whose backend matches the original; fall back to `GpuContext::new`
617        // which will pick the platform default if none match.
618        let original_backend = self.adapter_info.backend;
619        let adapters = self.instance.enumerate_adapters(Backends::all()).await;
620        let matching = adapters
621            .into_iter()
622            .find(|a| a.get_info().backend == original_backend);
623
624        if let Some(adapter) = matching {
625            let adapter_info = adapter.get_info();
626            let adapter_limits = adapter.limits();
627            let tuner = WorkgroupTuner::derive_from_limits(&adapter_limits);
628            let limits = Self::default_limits(&adapter_limits);
629
630            let (device, queue) = adapter
631                .request_device(&DeviceDescriptor {
632                    label: Some("OxiGDAL GPU Context (reinit)"),
633                    required_features: Features::empty(),
634                    required_limits: limits.clone(),
635                    memory_hints: Default::default(),
636                    experimental_features: Default::default(),
637                    trace: Default::default(),
638                })
639                .await
640                .map_err(|e| GpuError::device_request(format!("reinitialize: {e}")))?;
641
642            let device_lost = Arc::new(AtomicBool::new(false));
643            let flag_clone = Arc::clone(&device_lost);
644            device.set_device_lost_callback(move |_reason, message| {
645                warn!("GPU device lost (reinit): {}", message);
646                flag_clone.store(true, std::sync::atomic::Ordering::SeqCst);
647            });
648
649            info!(
650                "GPU device successfully reinitialized on adapter: {}",
651                adapter_info.name
652            );
653
654            // Evict all stale pipelines compiled against the old device before
655            // handing the cache to the new context.
656            let pipeline_cache = new_shared_pipeline_cache();
657
658            Ok(Self {
659                instance: Arc::clone(&self.instance),
660                adapter: Arc::new(adapter),
661                device: Arc::new(device),
662                queue: Arc::new(queue),
663                adapter_info,
664                limits,
665                tuner,
666                device_lost,
667                pipeline_cache,
668            })
669        } else {
670            // No matching adapter found — fall back to a completely fresh init.
671            warn!(
672                "No adapter matching original backend {:?} found during reinitialize, \
673                 falling back to platform default",
674                original_backend
675            );
676            GpuContext::new().await
677        }
678    }
679}
680
681impl std::fmt::Debug for GpuContext {
682    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
683        f.debug_struct("GpuContext")
684            .field("adapter", &self.adapter_info.name)
685            .field("backend", &self.adapter_info.backend)
686            .field("device_type", &self.adapter_info.device_type)
687            .field("limits", &self.limits)
688            .field("tuner", &self.tuner)
689            .finish()
690    }
691}
692
693#[cfg(test)]
694mod tests {
695    use super::*;
696
697    #[tokio::test]
698    async fn test_gpu_context_creation() {
699        // This test will fail if no GPU is available, which is expected
700        match GpuContext::new().await {
701            Ok(ctx) => {
702                println!("GPU Context created: {:?}", ctx);
703                assert!(ctx.is_valid());
704            }
705            Err(e) => {
706                println!("GPU not available (expected in CI): {}", e);
707            }
708        }
709    }
710
711    #[tokio::test]
712    async fn test_backend_preference() {
713        let config = GpuContextConfig::new()
714            .with_backend(BackendPreference::platform_default())
715            .with_power_preference(GpuPowerPreference::HighPerformance);
716
717        match GpuContext::with_config(config).await {
718            Ok(ctx) => {
719                println!("Backend: {:?}", ctx.backend());
720            }
721            Err(e) => {
722                println!("GPU not available: {}", e);
723            }
724        }
725    }
726
727    #[test]
728    fn test_backend_conversion() {
729        assert_eq!(BackendPreference::Vulkan.to_backends(), Backends::VULKAN);
730        assert_eq!(BackendPreference::Metal.to_backends(), Backends::METAL);
731        assert_eq!(BackendPreference::DX12.to_backends(), Backends::DX12);
732    }
733
734    #[test]
735    fn test_platform_default() {
736        let default = BackendPreference::platform_default();
737        println!("Platform default backend: {:?}", default);
738
739        #[cfg(target_os = "macos")]
740        assert_eq!(default, BackendPreference::Metal);
741
742        #[cfg(target_os = "windows")]
743        assert_eq!(default, BackendPreference::DX12);
744
745        #[cfg(target_os = "linux")]
746        assert_eq!(default, BackendPreference::Vulkan);
747    }
748}