|
| 1 | +// Copyright 2022 System76 <[email protected]> |
| 2 | +// SPDX-License-Identifier: GPL-3.0-only |
| 3 | + |
| 4 | +use std::collections::HashSet; |
| 5 | +use std::hash::{Hash, Hasher}; |
| 6 | +use std::io; |
| 7 | +use std::ops::Deref; |
| 8 | +use std::path::PathBuf; |
| 9 | + |
| 10 | +#[derive(Debug, Default)] |
| 11 | +pub struct Gpus { |
| 12 | + devices: Vec<Dev>, |
| 13 | + default: Option<Dev>, |
| 14 | +} |
| 15 | + |
| 16 | +impl Gpus { |
| 17 | + // Get gpus via udev |
| 18 | + pub fn load() -> Self { |
| 19 | + let drivers = get_gpus(); |
| 20 | + |
| 21 | + let mut gpus = Gpus::default(); |
| 22 | + for dev in drivers.unwrap() { |
| 23 | + if dev.is_default { |
| 24 | + gpus.default = Some(dev) |
| 25 | + } else { |
| 26 | + gpus.devices.push(dev) |
| 27 | + } |
| 28 | + } |
| 29 | + |
| 30 | + gpus |
| 31 | + } |
| 32 | + |
| 33 | + /// `true` if there is at least one non default gpu |
| 34 | + pub fn is_switchable(&self) -> bool { |
| 35 | + self.default.is_some() && !self.devices.is_empty() |
| 36 | + } |
| 37 | + |
| 38 | + /// Return the default gpu |
| 39 | + pub fn get_default(&self) -> Option<&Dev> { |
| 40 | + self.default.as_ref() |
| 41 | + } |
| 42 | + |
| 43 | + /// Get the first non-default gpu, the current `PreferNonDefaultGpu` specification |
| 44 | + /// Does not tell us which one should be used. Anyway most machine out there should have |
| 45 | + /// only one discrete graphic card. |
| 46 | + /// see: https://gitlab.freedesktop.org/xdg/xdg-specs/-/issues/59 |
| 47 | + pub fn non_default(&self) -> Option<&Dev> { |
| 48 | + self.devices.first() |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +#[derive(Debug)] |
| 53 | +pub struct Dev { |
| 54 | + id: usize, |
| 55 | + driver: Driver, |
| 56 | + is_default: bool, |
| 57 | + parent_path: PathBuf, |
| 58 | +} |
| 59 | + |
| 60 | +impl Dev { |
| 61 | + /// Get the environment variable to launch a program with the correct gpu settings |
| 62 | + pub fn launch_options(&self) -> Vec<(String, String)> { |
| 63 | + let dev_num = self.id.to_string(); |
| 64 | + let mut options = vec![]; |
| 65 | + |
| 66 | + match self.driver { |
| 67 | + Driver::Unknown | Driver::Amd(_) | Driver::Intel => { |
| 68 | + options.push(("DRI_PRIME".into(), dev_num)) |
| 69 | + } |
| 70 | + Driver::Nvidia => { |
| 71 | + options.push(("__GLX_VENDOR_LIBRARY_NAME".into(), "nvidia".into())); |
| 72 | + options.push(("__NV_PRIME_RENDER_OFFLOAD".into(), "1".into())); |
| 73 | + options.push((" __VK_LAYER_NV_optimus".into(), "NVIDIA_only".into())); |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + match self.get_vulkan_icd_paths() { |
| 78 | + Ok(vulkan_icd_paths) if !vulkan_icd_paths.is_empty() => { |
| 79 | + options.push(("VK_ICD_FILENAMES".into(), vulkan_icd_paths.join(":"))) |
| 80 | + } |
| 81 | + Err(err) => eprintln!("Failed to open vulkan icd paths: {err}"), |
| 82 | + _ => {} |
| 83 | + } |
| 84 | + |
| 85 | + options |
| 86 | + } |
| 87 | + |
| 88 | + // Lookup vulkan icd files and return the ones matching the driver in use |
| 89 | + fn get_vulkan_icd_paths(&self) -> io::Result<Vec<String>> { |
| 90 | + let vulkan_icd_paths = dirs::data_dir() |
| 91 | + .expect("local data dir does not exists") |
| 92 | + .join("vulkan/icd.d"); |
| 93 | + let vulkan_icd_paths = &[PathBuf::from("/usr/share/vulkan/icd.d"), vulkan_icd_paths]; |
| 94 | + |
| 95 | + let mut icd_paths = vec![]; |
| 96 | + if let Some(driver) = self.driver.as_str() { |
| 97 | + for path in vulkan_icd_paths { |
| 98 | + if path.exists() { |
| 99 | + for entry in path.read_dir()? { |
| 100 | + let entry = entry?; |
| 101 | + let path = entry.path(); |
| 102 | + if path.is_file() { |
| 103 | + let path_str = path.to_string_lossy(); |
| 104 | + if path_str.contains(driver) { |
| 105 | + icd_paths.push(path_str.to_string()) |
| 106 | + } |
| 107 | + } |
| 108 | + } |
| 109 | + } |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + Ok(icd_paths) |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +// Ensure we filter out "render" devices having the same parent as the card |
| 118 | +impl Hash for Dev { |
| 119 | + fn hash<H: Hasher>(&self, state: &mut H) { |
| 120 | + state.write(self.parent_path.to_string_lossy().as_bytes()); |
| 121 | + state.finish(); |
| 122 | + } |
| 123 | +} |
| 124 | + |
| 125 | +impl PartialEq<Dev> for Dev { |
| 126 | + fn eq(&self, other: &Self) -> bool { |
| 127 | + self.parent_path == other.parent_path |
| 128 | + } |
| 129 | +} |
| 130 | + |
| 131 | +impl Eq for Dev {} |
| 132 | + |
| 133 | +#[derive(Debug)] |
| 134 | +enum Driver { |
| 135 | + Intel, |
| 136 | + Amd(String), |
| 137 | + Nvidia, |
| 138 | + Unknown, |
| 139 | +} |
| 140 | + |
| 141 | +impl Driver { |
| 142 | + fn from_udev<S: Deref<Target = str>>(driver: Option<S>) -> Driver { |
| 143 | + match driver.as_deref() { |
| 144 | + // For amd devices we need the name of the driver to get vulkan icd files |
| 145 | + Some("radeon") => Driver::Amd("radeon".to_string()), |
| 146 | + Some("amdgpu") => Driver::Amd("amdgpu".to_string()), |
| 147 | + Some("nvidia") => Driver::Nvidia, |
| 148 | + Some("iris") | Some("i915") | Some("i965") => Driver::Intel, |
| 149 | + _ => Driver::Unknown, |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + fn as_str(&self) -> Option<&str> { |
| 154 | + match self { |
| 155 | + Driver::Intel => Some("intel"), |
| 156 | + Driver::Amd(driver) => Some(driver.as_str()), |
| 157 | + Driver::Nvidia => Some("nvidia"), |
| 158 | + Driver::Unknown => None, |
| 159 | + } |
| 160 | + } |
| 161 | +} |
| 162 | + |
| 163 | +fn get_gpus() -> io::Result<HashSet<Dev>> { |
| 164 | + let mut enumerator = udev::Enumerator::new()?; |
| 165 | + let mut dev_map = HashSet::new(); |
| 166 | + let mut drivers: Vec<Dev> = enumerator |
| 167 | + .scan_devices()? |
| 168 | + .into_iter() |
| 169 | + .filter(|dev| { |
| 170 | + dev.devnode() |
| 171 | + .map(|path| path.starts_with("/dev/dri")) |
| 172 | + .unwrap_or(false) |
| 173 | + }) |
| 174 | + .filter_map(|dev| { |
| 175 | + dev.parent().and_then(|parent| { |
| 176 | + let id = dev.sysnum(); |
| 177 | + let parent_path = parent.syspath().to_path_buf(); |
| 178 | + let driver = parent.driver().map(|d| d.to_string_lossy().to_string()); |
| 179 | + let driver = Driver::from_udev(driver); |
| 180 | + |
| 181 | + let is_default = parent |
| 182 | + .attribute_value("boot_vga") |
| 183 | + .map(|v| v == "1") |
| 184 | + .unwrap_or(false); |
| 185 | + |
| 186 | + id.map(|id| Dev { |
| 187 | + id, |
| 188 | + driver, |
| 189 | + is_default, |
| 190 | + parent_path, |
| 191 | + }) |
| 192 | + }) |
| 193 | + }) |
| 194 | + .collect(); |
| 195 | + |
| 196 | + // Sort the devices by sysnum so we get card0, card1 first and ignore the other 3D devices |
| 197 | + drivers.sort_by(|a, b| a.id.cmp(&b.id)); |
| 198 | + |
| 199 | + for dev in drivers { |
| 200 | + dev_map.insert(dev); |
| 201 | + } |
| 202 | + |
| 203 | + Ok(dev_map) |
| 204 | +} |
0 commit comments