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}