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                apply_limit_buckets: false,
383            })
384            .await;
385
386        adapter.map_err(|_| {
387            let backends = match config.backend {
388                BackendPreference::Auto => "Auto (PRIMARY)".to_string(),
389                BackendPreference::All => "All".to_string(),
390                backend => format!("{backend:?}"),
391            };
392            GpuError::no_adapter(backends)
393        })
394    }
395
396    /// Get default limits based on adapter capabilities.
397    fn default_limits(adapter_limits: &Limits) -> Limits {
398        Limits {
399            max_compute_workgroup_size_x: adapter_limits.max_compute_workgroup_size_x.min(256),
400            max_compute_workgroup_size_y: adapter_limits.max_compute_workgroup_size_y.min(256),
401            max_compute_workgroup_size_z: adapter_limits.max_compute_workgroup_size_z.min(64),
402            max_compute_invocations_per_workgroup: adapter_limits
403                .max_compute_invocations_per_workgroup
404                .min(256),
405            max_compute_workgroups_per_dimension: adapter_limits
406                .max_compute_workgroups_per_dimension,
407            ..Default::default()
408        }
409    }
410
411    /// Validate that requested limits don't exceed adapter capabilities.
412    fn validate_limits(requested: &Limits, adapter: &Limits) -> bool {
413        requested.max_compute_workgroup_size_x <= adapter.max_compute_workgroup_size_x
414            && requested.max_compute_workgroup_size_y <= adapter.max_compute_workgroup_size_y
415            && requested.max_compute_workgroup_size_z <= adapter.max_compute_workgroup_size_z
416            && requested.max_compute_invocations_per_workgroup
417                <= adapter.max_compute_invocations_per_workgroup
418    }
419
420    /// Get the WGPU device.
421    pub fn device(&self) -> &Device {
422        &self.device
423    }
424
425    /// Get the WGPU queue.
426    pub fn queue(&self) -> &Queue {
427        &self.queue
428    }
429
430    /// Get the WGPU adapter.
431    pub fn adapter(&self) -> &Adapter {
432        &self.adapter
433    }
434
435    /// Get the WGPU instance.
436    pub fn instance(&self) -> &Instance {
437        &self.instance
438    }
439
440    /// Get adapter information.
441    pub fn adapter_info(&self) -> &AdapterInfo {
442        &self.adapter_info
443    }
444
445    /// Get device limits.
446    pub fn limits(&self) -> &Limits {
447        &self.limits
448    }
449
450    /// Get the backend being used.
451    pub fn backend(&self) -> Backend {
452        self.adapter_info.backend
453    }
454
455    /// Check if the device supports a specific feature.
456    pub fn supports_feature(&self, feature: Features) -> bool {
457        self.device.features().contains(feature)
458    }
459
460    /// Get maximum workgroup size for compute shaders.
461    pub fn max_workgroup_size(&self) -> (u32, u32, u32) {
462        (
463            self.limits.max_compute_workgroup_size_x,
464            self.limits.max_compute_workgroup_size_y,
465            self.limits.max_compute_workgroup_size_z,
466        )
467    }
468
469    /// Poll the device for completed operations.
470    ///
471    /// This must be called to service pending GPU work — most importantly the
472    /// `map_async` callbacks that back [`crate::GpuBuffer::read`]. Without a
473    /// poll those callbacks are never invoked and a readback future never
474    /// resolves.
475    ///
476    /// When `wait` is `true` this blocks until the device is idle
477    /// ([`wgpu::PollType::Wait`]); when `false` it performs a single
478    /// non-blocking poll ([`wgpu::PollType::Poll`]). Poll errors (e.g. a lost
479    /// device) are intentionally swallowed here — callers observe device loss
480    /// through the operation that submitted the work.
481    pub fn poll(&self, wait: bool) {
482        let poll_type = if wait {
483            wgpu::PollType::wait_indefinitely()
484        } else {
485            wgpu::PollType::Poll
486        };
487        let _ = self.device.poll(poll_type);
488    }
489
490    /// Spawn a background thread that keeps the wgpu device polled.
491    ///
492    /// This is an **advanced helper** intended for callers that run async GPU
493    /// readbacks without a runtime that issues device polls automatically (e.g.
494    /// bare `pollster` or custom executors).
495    ///
496    /// The spawned thread calls `device.poll(wgpu::PollType::Poll)` in a tight
497    /// loop with a 1 ms sleep between iterations.  Call
498    /// [`std::thread::JoinHandle::join`] when the thread is no longer needed
499    /// (the caller is responsible for signalling termination through a shared
500    /// flag or by dropping all `Arc<Device>` clones and letting the thread
501    /// detect the closed handle).
502    ///
503    /// In most tokio/async-std environments the runtime's built-in submission
504    /// loop makes this unnecessary.
505    pub fn spawn_poll_task(&self) -> std::thread::JoinHandle<()> {
506        let device = Arc::clone(&self.device);
507        std::thread::spawn(move || {
508            loop {
509                let poll_result = device.poll(wgpu::PollType::Poll);
510                // If the device queue is empty and all work is done, sleep
511                // more aggressively to avoid spinning.
512                if matches!(poll_result, Ok(wgpu::PollStatus::QueueEmpty)) {
513                    std::thread::sleep(std::time::Duration::from_millis(5));
514                } else {
515                    std::thread::sleep(std::time::Duration::from_millis(1));
516                }
517            }
518        })
519    }
520
521    /// Check if the device is still valid.
522    pub fn is_valid(&self) -> bool {
523        // Try to create a small buffer as a health check
524        self.device.create_buffer(&wgpu::BufferDescriptor {
525            label: Some("health_check"),
526            size: 4,
527            usage: wgpu::BufferUsages::UNIFORM,
528            mapped_at_creation: false,
529        });
530        true
531    }
532
533    /// Override the workgroup tuner (useful in tests or for custom tuning).
534    ///
535    /// Returns `self` so it can be chained in a builder-style pattern:
536    ///
537    /// ```rust,no_run
538    /// # use oxigdal_gpu::{GpuContext, WorkgroupTuner};
539    /// # async fn ex() -> Result<(), Box<dyn std::error::Error>> {
540    /// let gpu = GpuContext::new().await?.with_tuner(WorkgroupTuner::unlimited());
541    /// # Ok(())
542    /// # }
543    /// ```
544    pub fn with_tuner(mut self, tuner: WorkgroupTuner) -> Self {
545        self.tuner = tuner;
546        self
547    }
548
549    // -------------------------------------------------------------------------
550    // Device-lost recovery
551    // -------------------------------------------------------------------------
552
553    /// Returns `true` if the GPU device has been lost and operations will fail.
554    ///
555    /// The flag is set atomically by the wgpu device-lost callback registered
556    /// during [`GpuContext::new`] / [`GpuContext::with_config`].
557    pub fn is_device_lost(&self) -> bool {
558        self.device_lost.load(std::sync::atomic::Ordering::SeqCst)
559    }
560
561    /// Checks whether the device has been lost and returns an error if so.
562    ///
563    /// Call this before every `queue.submit` to short-circuit operations on a
564    /// defunct device instead of generating silent GPU errors.
565    pub fn check_device_lost(&self) -> GpuResult<()> {
566        if self.is_device_lost() {
567            Err(GpuError::device_lost("device was lost"))
568        } else {
569            Ok(())
570        }
571    }
572
573    /// Returns a reference to the shared pipeline cache.
574    ///
575    /// The cache maps `(shader_hash, entry_point, layout_tag)` keys to
576    /// previously compiled [`wgpu::ComputePipeline`]s wrapped in `Arc`.
577    ///
578    /// Kernel implementations can call
579    /// [`PipelineCache::get_or_insert_with`][crate::pipeline_cache::PipelineCache::get_or_insert_with]
580    /// on the inner [`crate::pipeline_cache::PipelineCache`] to avoid
581    /// recompiling the same WGSL shader on every kernel construction.
582    ///
583    /// # Examples
584    ///
585    /// ```rust,no_run
586    /// use oxigdal_gpu::{GpuContext, pipeline_cache::PipelineCacheKey};
587    ///
588    /// # async fn ex(ctx: &GpuContext) {
589    /// let cache = ctx.pipeline_cache();
590    /// if let Ok(mut guard) = cache.lock() {
591    ///     println!("cached pipelines: {}", guard.len());
592    /// }
593    /// # }
594    /// ```
595    pub fn pipeline_cache(&self) -> &SharedPipelineCache {
596        &self.pipeline_cache
597    }
598
599    /// Replaces the internal device-lost flag with an externally-supplied one.
600    ///
601    /// This builder method is intended **for testing only** — it allows a test
602    /// to inject a shared `Arc<AtomicBool>` whose state it controls directly,
603    /// exercising the `check_device_lost` / `is_device_lost` paths without
604    /// requiring a real GPU device.
605    ///
606    /// ```rust
607    /// use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
608    /// // NB: GpuContext::new() requires a GPU; this is conceptual.
609    /// // let ctx = ctx.with_device_lost_flag(Arc::new(AtomicBool::new(true)));
610    /// ```
611    pub fn with_device_lost_flag(mut self, flag: Arc<AtomicBool>) -> Self {
612        self.device_lost = flag;
613        self
614    }
615
616    /// Attempts to recreate the GPU device on the same adapter.
617    ///
618    /// This creates a fresh `GpuContext` via the same wgpu instance, selecting
619    /// an adapter with matching properties to the one this context was built
620    /// on.  The old context **must be dropped** after calling this method;
621    /// continuing to use it will only generate further device-lost errors.
622    ///
623    /// # Errors
624    ///
625    /// Returns an error if adapter enumeration fails or the device cannot be
626    /// re-created (e.g., the GPU has been physically removed).
627    pub async fn reinitialize(&self) -> GpuResult<GpuContext> {
628        // Re-use the existing instance so we stay within the same set of
629        // enabled backends.  We enumerate adapters and pick the first one
630        // whose backend matches the original; fall back to `GpuContext::new`
631        // which will pick the platform default if none match.
632        let original_backend = self.adapter_info.backend;
633        let adapters = self.instance.enumerate_adapters(Backends::all()).await;
634        let matching = adapters
635            .into_iter()
636            .find(|a| a.get_info().backend == original_backend);
637
638        if let Some(adapter) = matching {
639            let adapter_info = adapter.get_info();
640            let adapter_limits = adapter.limits();
641            let tuner = WorkgroupTuner::derive_from_limits(&adapter_limits);
642            let limits = Self::default_limits(&adapter_limits);
643
644            let (device, queue) = adapter
645                .request_device(&DeviceDescriptor {
646                    label: Some("OxiGDAL GPU Context (reinit)"),
647                    required_features: Features::empty(),
648                    required_limits: limits.clone(),
649                    memory_hints: Default::default(),
650                    experimental_features: Default::default(),
651                    trace: Default::default(),
652                })
653                .await
654                .map_err(|e| GpuError::device_request(format!("reinitialize: {e}")))?;
655
656            let device_lost = Arc::new(AtomicBool::new(false));
657            let flag_clone = Arc::clone(&device_lost);
658            device.set_device_lost_callback(move |_reason, message| {
659                warn!("GPU device lost (reinit): {}", message);
660                flag_clone.store(true, std::sync::atomic::Ordering::SeqCst);
661            });
662
663            info!(
664                "GPU device successfully reinitialized on adapter: {}",
665                adapter_info.name
666            );
667
668            // Evict all stale pipelines compiled against the old device before
669            // handing the cache to the new context.
670            let pipeline_cache = new_shared_pipeline_cache();
671
672            Ok(Self {
673                instance: Arc::clone(&self.instance),
674                adapter: Arc::new(adapter),
675                device: Arc::new(device),
676                queue: Arc::new(queue),
677                adapter_info,
678                limits,
679                tuner,
680                device_lost,
681                pipeline_cache,
682            })
683        } else {
684            // No matching adapter found — fall back to a completely fresh init.
685            warn!(
686                "No adapter matching original backend {:?} found during reinitialize, \
687                 falling back to platform default",
688                original_backend
689            );
690            GpuContext::new().await
691        }
692    }
693}
694
695impl std::fmt::Debug for GpuContext {
696    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
697        f.debug_struct("GpuContext")
698            .field("adapter", &self.adapter_info.name)
699            .field("backend", &self.adapter_info.backend)
700            .field("device_type", &self.adapter_info.device_type)
701            .field("limits", &self.limits)
702            .field("tuner", &self.tuner)
703            .finish()
704    }
705}
706
707#[cfg(test)]
708mod tests {
709    use super::*;
710
711    #[tokio::test]
712    async fn test_gpu_context_creation() {
713        // This test will fail if no GPU is available, which is expected
714        match GpuContext::new().await {
715            Ok(ctx) => {
716                println!("GPU Context created: {:?}", ctx);
717                assert!(ctx.is_valid());
718            }
719            Err(e) => {
720                println!("GPU not available (expected in CI): {}", e);
721            }
722        }
723    }
724
725    #[tokio::test]
726    async fn test_backend_preference() {
727        let config = GpuContextConfig::new()
728            .with_backend(BackendPreference::platform_default())
729            .with_power_preference(GpuPowerPreference::HighPerformance);
730
731        match GpuContext::with_config(config).await {
732            Ok(ctx) => {
733                println!("Backend: {:?}", ctx.backend());
734            }
735            Err(e) => {
736                println!("GPU not available: {}", e);
737            }
738        }
739    }
740
741    #[test]
742    fn test_backend_conversion() {
743        assert_eq!(BackendPreference::Vulkan.to_backends(), Backends::VULKAN);
744        assert_eq!(BackendPreference::Metal.to_backends(), Backends::METAL);
745        assert_eq!(BackendPreference::DX12.to_backends(), Backends::DX12);
746    }
747
748    #[test]
749    fn test_platform_default() {
750        let default = BackendPreference::platform_default();
751        println!("Platform default backend: {:?}", default);
752
753        #[cfg(target_os = "macos")]
754        assert_eq!(default, BackendPreference::Metal);
755
756        #[cfg(target_os = "windows")]
757        assert_eq!(default, BackendPreference::DX12);
758
759        #[cfg(target_os = "linux")]
760        assert_eq!(default, BackendPreference::Vulkan);
761    }
762}