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}