Skip to main content

wallr_core/video/
gpu.rs

1//! GPU adapter detection and selection for hybrid GPU systems.
2//!
3//! Provides intelligent GPU selection to avoid forcing rendering on discrete GPUs
4//! when an integrated GPU is available and preferred.
5
6use crate::video::error::{VideoError, VideoResult};
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10/// GPU selection preference.
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
12#[serde(rename_all = "lowercase")]
13pub enum GpuSelection {
14    /// Automatically select the best GPU (prefer compositor GPU).
15    #[default]
16    Auto,
17    /// Prefer integrated GPU (Intel iGPU, AMD APU).
18    Integrated,
19    /// Prefer discrete GPU (NVIDIA, AMD dGPU).
20    Discrete,
21    /// Select a specific adapter by name.
22    Named(String),
23}
24
25impl fmt::Display for GpuSelection {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        match self {
28            Self::Auto => write!(f, "auto"),
29            Self::Integrated => write!(f, "integrated"),
30            Self::Discrete => write!(f, "discrete"),
31            Self::Named(name) => write!(f, "{}", name),
32        }
33    }
34}
35
36/// Information about a detected GPU adapter.
37#[derive(Debug, Clone)]
38pub struct AdapterInfo {
39    pub name: String,
40    pub backend: wgpu::Backend,
41    pub device_type: wgpu::DeviceType,
42    pub driver: String,
43    pub driver_info: String,
44}
45
46impl AdapterInfo {
47    /// Returns true if this adapter is an integrated GPU.
48    pub fn is_integrated(&self) -> bool {
49        self.device_type == wgpu::DeviceType::IntegratedGpu
50    }
51
52    /// Returns true if this adapter is a discrete GPU.
53    pub fn is_discrete(&self) -> bool {
54        self.device_type == wgpu::DeviceType::DiscreteGpu
55    }
56}
57
58impl fmt::Display for AdapterInfo {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        write!(
61            f,
62            "{} ({:?}, {:?}) - {}",
63            self.name, self.device_type, self.backend, self.driver
64        )
65    }
66}
67
68/// Detect all available GPU adapters.
69pub async fn detect_adapters(instance: &wgpu::Instance) -> Vec<AdapterInfo> {
70    let mut adapters = Vec::new();
71
72    for adapter in instance.enumerate_adapters(wgpu::Backends::all()) {
73        let info = adapter.get_info();
74        adapters.push(AdapterInfo {
75            name: info.name.clone(),
76            backend: info.backend,
77            device_type: info.device_type,
78            driver: info.driver.clone(),
79            driver_info: info.driver_info.clone(),
80        });
81    }
82
83    tracing::info!("Detected {} GPU adapter(s)", adapters.len());
84    for (i, adapter) in adapters.iter().enumerate() {
85        tracing::info!("  [{}] {}", i, adapter);
86    }
87
88    adapters
89}
90
91/// Select the best GPU adapter according to the given preference.
92///
93/// Selection strategy:
94/// - `Auto`: Prefer integrated GPU if available, otherwise use discrete
95/// - `Integrated`: Select the first integrated GPU
96/// - `Discrete`: Select the first discrete GPU
97/// - `Named`: Select by exact name match
98///
99/// Falls back gracefully if the requested adapter is not available.
100pub async fn select_adapter(
101    instance: &wgpu::Instance,
102    preference: &GpuSelection,
103) -> VideoResult<wgpu::Adapter> {
104    let adapters = detect_adapters(instance).await;
105
106    if adapters.is_empty() {
107        return Err(VideoError::AdapterNotFound(
108            "No GPU adapters detected".to_string(),
109        ));
110    }
111
112    let selected_info = match preference {
113        GpuSelection::Auto => {
114            // Prefer integrated GPU to avoid unnecessary power consumption
115            adapters
116                .iter()
117                .find(|a| a.is_integrated())
118                .or_else(|| adapters.first())
119        }
120        GpuSelection::Integrated => adapters.iter().find(|a| a.is_integrated()),
121        GpuSelection::Discrete => adapters.iter().find(|a| a.is_discrete()),
122        GpuSelection::Named(name) => adapters.iter().find(|a| a.name.contains(name)),
123    };
124
125    let selected_info = selected_info.ok_or_else(|| {
126        VideoError::AdapterNotFound(format!("No adapter matching preference: {}", preference))
127    })?;
128
129    tracing::info!("Selected GPU adapter: {}", selected_info);
130
131    // Now request the actual adapter from wgpu
132    let adapter = instance
133        .request_adapter(&wgpu::RequestAdapterOptions {
134            power_preference: match preference {
135                GpuSelection::Integrated => wgpu::PowerPreference::LowPower,
136                GpuSelection::Discrete => wgpu::PowerPreference::HighPerformance,
137                _ => wgpu::PowerPreference::LowPower,
138            },
139            compatible_surface: None,
140            force_fallback_adapter: false,
141        })
142        .await
143        .ok_or_else(|| {
144            VideoError::AdapterNotFound(format!("Failed to request adapter for: {}", preference))
145        })?;
146
147    Ok(adapter)
148}
149
150/// Get diagnostic information about the selected adapter for `wallr info`.
151pub fn adapter_diagnostics(adapter: &wgpu::Adapter) -> String {
152    let info = adapter.get_info();
153    format!(
154        "GPU: {} ({:?})\nBackend: {:?}\nDriver: {}\nDriver Info: {}",
155        info.name, info.device_type, info.backend, info.driver, info.driver_info
156    )
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_gpu_selection_display() {
165        assert_eq!(GpuSelection::Auto.to_string(), "auto");
166        assert_eq!(GpuSelection::Integrated.to_string(), "integrated");
167        assert_eq!(GpuSelection::Discrete.to_string(), "discrete");
168        assert_eq!(
169            GpuSelection::Named("NVIDIA".to_string()).to_string(),
170            "NVIDIA"
171        );
172    }
173
174    #[test]
175    fn test_gpu_selection_default() {
176        assert_eq!(GpuSelection::default(), GpuSelection::Auto);
177    }
178}