| 1 | use crate::Error; |
| 2 | use ash::vk; |
| 3 | use gpu_allocator::{AllocationSizes, AllocatorDebugSettings}; |
| 4 | use gpu_allocator::vulkan::{Allocator, AllocatorCreateDesc}; |
| 5 | use std::mem::ManuallyDrop; |
| 6 | use std::sync::Mutex; |
| 7 | |
| 8 | /// Everything that must outlive every Vulkan object the renderer hands out. |
| 9 | /// Fields drop in declaration order, so the allocator frees its memory before |
| 10 | /// the device is destroyed. |
| 11 | pub(crate) struct Shared { |
| 12 | pub allocator: ManuallyDrop<Mutex<Allocator>>, |
| 13 | pub device: Device, |
| 14 | } |
| 15 | |
| 16 | impl Drop for Shared { |
| 17 | fn drop(&mut self) { |
| 18 | if !crate::error::device_lost() { |
| 19 | unsafe { ManuallyDrop::drop(&mut self.allocator) }; |
| 20 | } |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | /// Loaders for the extensions that dmabuf import, export and fence export |
| 25 | /// need. Absent when the device lacks any of them. |
| 26 | pub(crate) struct DmabufExt { |
| 27 | pub memory_fd: ash::khr::external_memory_fd::Device, |
| 28 | pub fence_fd: ash::khr::external_fence_fd::Device, |
| 29 | pub modifier: ash::ext::image_drm_format_modifier::Device, |
| 30 | } |
| 31 | |
| 32 | const DMABUF_EXTENSIONS: [&std::ffi::CStr; 5] = [ |
| 33 | ash::khr::external_memory_fd::NAME, |
| 34 | ash::ext::external_memory_dma_buf::NAME, |
| 35 | ash::ext::image_drm_format_modifier::NAME, |
| 36 | ash::ext::queue_family_foreign::NAME, |
| 37 | ash::khr::external_fence_fd::NAME, |
| 38 | ]; |
| 39 | |
| 40 | pub(crate) struct Device { |
| 41 | pub _entry: ash::Entry, |
| 42 | pub instance: ash::Instance, |
| 43 | pub physical: vk::PhysicalDevice, |
| 44 | pub device: ash::Device, |
| 45 | pub queue: Mutex<vk::Queue>, |
| 46 | pub queue_family: u32, |
| 47 | pub push_descriptor: ash::khr::push_descriptor::Device, |
| 48 | pub dmabuf: Option<DmabufExt>, |
| 49 | pub name: String, |
| 50 | pub drm_node: Option<u64>, |
| 51 | /// Signalled fences kept for reuse, see `FenceInner`. |
| 52 | pub spare_fences: Mutex<Vec<vk::Fence>>, |
| 53 | } |
| 54 | |
| 55 | /// A lost or hung device is leaked. Waiting on it or destroying it can block |
| 56 | /// forever in the driver, and the process is about to exit anyway. |
| 57 | impl Drop for Device { |
| 58 | fn drop(&mut self) { |
| 59 | if crate::error::device_lost() { |
| 60 | return; |
| 61 | } |
| 62 | unsafe { |
| 63 | let _ = self.device.device_wait_idle(); |
| 64 | for f in self |
| 65 | .spare_fences |
| 66 | .get_mut() |
| 67 | .unwrap_or_else(|e| e.into_inner()) |
| 68 | .drain(..) |
| 69 | { |
| 70 | self.device.destroy_fence(f, None); |
| 71 | } |
| 72 | self.device.destroy_device(None); |
| 73 | self.instance.destroy_instance(None); |
| 74 | } |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | impl Shared { |
| 79 | /// Opens the first suitable device, or the one whose name contains |
| 80 | /// `MONOLITH_VK_DEVICE` when that is set. With `node`, only the device |
| 81 | /// behind that DRM device number qualifies. |
| 82 | pub fn new(node: Option<u64>) -> Result<Self, Error> { |
| 83 | let entry = unsafe { ash::Entry::load()? }; |
| 84 | let app = vk::ApplicationInfo::default() |
| 85 | .application_name(c"monolith") |
| 86 | .engine_name(c"monolith") |
| 87 | .api_version(vk::API_VERSION_1_3); |
| 88 | let wanted = std::env::var("MONOLITH_VK_DEVICE").ok(); |
| 89 | let mut env = node.map(driver_env).unwrap_or_default(); |
| 90 | let (instance, physical, queue_family, name) = loop { |
| 91 | for (var, value) in &env { |
| 92 | // SAFETY: the compositor creates its renderer before starting any |
| 93 | // other thread, so nothing reads the environment concurrently. |
| 94 | unsafe { std::env::set_var(var, value) }; |
| 95 | } |
| 96 | let instance = unsafe { |
| 97 | entry.create_instance( |
| 98 | &vk::InstanceCreateInfo::default().application_info(&app), |
| 99 | None, |
| 100 | ) |
| 101 | }; |
| 102 | for (var, _) in &env { |
| 103 | // SAFETY: as above. Removing them keeps clients from inheriting them. |
| 104 | unsafe { std::env::remove_var(var) }; |
| 105 | } |
| 106 | let instance = match instance { |
| 107 | Ok(i) => i, |
| 108 | Err(_) if !std::mem::take(&mut env).is_empty() => continue, |
| 109 | Err(e) => return Err(e.into()), |
| 110 | }; |
| 111 | match unsafe { pick_physical(&instance, wanted.as_deref(), node) } { |
| 112 | Some((p, f, n)) => break (instance, p, f, n), |
| 113 | None => { |
| 114 | unsafe { instance.destroy_instance(None) }; |
| 115 | if std::mem::take(&mut env).is_empty() { |
| 116 | return Err(Error::NoSuitableDevice); |
| 117 | } |
| 118 | } |
| 119 | } |
| 120 | }; |
| 121 | let priorities = [1.0]; |
| 122 | let available: Vec<_> = unsafe { instance.enumerate_device_extension_properties(physical) } |
| 123 | .unwrap_or_default() |
| 124 | .iter() |
| 125 | .filter_map(|e| e.extension_name_as_c_str().ok().map(ToOwned::to_owned)) |
| 126 | .collect(); |
| 127 | let drm_node = available |
| 128 | .iter() |
| 129 | .any(|a| a.as_c_str() == ash::ext::physical_device_drm::NAME) |
| 130 | .then(|| unsafe { drm_node(&instance, physical) }) |
| 131 | .flatten(); |
| 132 | let has_dmabuf = DMABUF_EXTENSIONS |
| 133 | .iter() |
| 134 | .all(|n| available.iter().any(|a| a.as_c_str() == *n)); |
| 135 | let mut extensions = vec![ash::khr::push_descriptor::NAME.as_ptr()]; |
| 136 | if has_dmabuf { |
| 137 | extensions.extend(DMABUF_EXTENSIONS.iter().map(|n| n.as_ptr())); |
| 138 | } |
| 139 | let global_priority = [ |
| 140 | ash::khr::global_priority::NAME, |
| 141 | ash::ext::global_priority::NAME, |
| 142 | ] |
| 143 | .into_iter() |
| 144 | .find(|n| available.iter().any(|a| a.as_c_str() == *n)); |
| 145 | let mut priorities_to_try = vec![None]; |
| 146 | if let Some(name) = global_priority { |
| 147 | extensions.push(name.as_ptr()); |
| 148 | priorities_to_try = vec![ |
| 149 | Some(vk::QueueGlobalPriorityKHR::REALTIME), |
| 150 | Some(vk::QueueGlobalPriorityKHR::HIGH), |
| 151 | None, |
| 152 | ]; |
| 153 | } |
| 154 | let mut created = Err(vk::Result::ERROR_INITIALIZATION_FAILED); |
| 155 | for priority in priorities_to_try { |
| 156 | let mut features13 = vk::PhysicalDeviceVulkan13Features::default() |
| 157 | .dynamic_rendering(true) |
| 158 | .synchronization2(true); |
| 159 | let mut global = vk::DeviceQueueGlobalPriorityCreateInfoKHR::default() |
| 160 | .global_priority(priority.unwrap_or(vk::QueueGlobalPriorityKHR::MEDIUM)); |
| 161 | let mut queue = vk::DeviceQueueCreateInfo::default() |
| 162 | .queue_family_index(queue_family) |
| 163 | .queue_priorities(&priorities); |
| 164 | if priority.is_some() { |
| 165 | queue = queue.push_next(&mut global); |
| 166 | } |
| 167 | let queues = [queue]; |
| 168 | let info = vk::DeviceCreateInfo::default() |
| 169 | .queue_create_infos(&queues) |
| 170 | .enabled_extension_names(&extensions) |
| 171 | .push_next(&mut features13); |
| 172 | created = unsafe { instance.create_device(physical, &info, None) }; |
| 173 | match &created { |
| 174 | Ok(_) => { |
| 175 | tracing::info!(?priority, "queue global priority"); |
| 176 | break; |
| 177 | } |
| 178 | Err(e) => tracing::debug!(?priority, "queue global priority refused: {e}"), |
| 179 | } |
| 180 | } |
| 181 | let device = match created { |
| 182 | Ok(d) => d, |
| 183 | Err(e) => { |
| 184 | unsafe { instance.destroy_instance(None) }; |
| 185 | return Err(e.into()); |
| 186 | } |
| 187 | }; |
| 188 | let queue = unsafe { device.get_device_queue(queue_family, 0) }; |
| 189 | let push_descriptor = ash::khr::push_descriptor::Device::new(&instance, &device); |
| 190 | let dmabuf = has_dmabuf.then(|| DmabufExt { |
| 191 | memory_fd: ash::khr::external_memory_fd::Device::new(&instance, &device), |
| 192 | fence_fd: ash::khr::external_fence_fd::Device::new(&instance, &device), |
| 193 | modifier: ash::ext::image_drm_format_modifier::Device::new(&instance, &device), |
| 194 | }); |
| 195 | let allocator = Allocator::new(&AllocatorCreateDesc { |
| 196 | instance: instance.clone(), |
| 197 | device: device.clone(), |
| 198 | physical_device: physical, |
| 199 | // The live compositor is non-dumpable, so with |
| 200 | // RUST_LOG=gpu_allocator=debug its log is the only way to see |
| 201 | // what fills the memory it maps. |
| 202 | debug_settings: AllocatorDebugSettings { |
| 203 | log_memory_information: true, |
| 204 | log_allocations: true, |
| 205 | log_frees: true, |
| 206 | ..Default::default() |
| 207 | }, |
| 208 | buffer_device_address: false, |
| 209 | // Host blocks are pinned system RAM that stays resident while any |
| 210 | // allocation lives in them, and the default 64 MB block is mostly idle. |
| 211 | allocation_sizes: AllocationSizes::new(64 << 20, 4 << 20), |
| 212 | }); |
| 213 | let device = Device { |
| 214 | _entry: entry, |
| 215 | instance, |
| 216 | physical, |
| 217 | device, |
| 218 | queue: Mutex::new(queue), |
| 219 | queue_family, |
| 220 | push_descriptor, |
| 221 | dmabuf, |
| 222 | name, |
| 223 | drm_node, |
| 224 | spare_fences: Mutex::new(Vec::new()), |
| 225 | }; |
| 226 | Ok(Self { |
| 227 | allocator: ManuallyDrop::new(Mutex::new(allocator?)), |
| 228 | device, |
| 229 | }) |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | /// Whether `physical` is the device behind DRM device number `node`, as its |
| 234 | /// primary or render node. |
| 235 | unsafe fn serves_node(instance: &ash::Instance, physical: vk::PhysicalDevice, node: u64) -> bool { |
| 236 | let mut drm = vk::PhysicalDeviceDrmPropertiesEXT::default(); |
| 237 | let mut props = vk::PhysicalDeviceProperties2::default().push_next(&mut drm); |
| 238 | unsafe { instance.get_physical_device_properties2(physical, &mut props) }; |
| 239 | let (major, minor) = ( |
| 240 | rustix::fs::major(node) as i64, |
| 241 | rustix::fs::minor(node) as i64, |
| 242 | ); |
| 243 | (drm.has_primary == vk::TRUE && (drm.primary_major, drm.primary_minor) == (major, minor)) |
| 244 | || (drm.has_render == vk::TRUE && (drm.render_major, drm.render_minor) == (major, minor)) |
| 245 | } |
| 246 | |
| 247 | /// Variables set only while the instance is created, unless the user already |
| 248 | /// picks drivers themselves. The loader keeps every ICD it finds loaded for the |
| 249 | /// instance's lifetime, on NixOS including lavapipe and 26 MB of resident |
| 250 | /// libLLVM. Without an X server NVIDIA's ICD also initialises through glvnd's |
| 251 | /// libEGL, which loads every EGL vendor, and Mesa's pulls in LLVM again. |
| 252 | fn driver_env(node: u64) -> Vec<(&'static str, std::ffi::OsString)> { |
| 253 | let set = |vars: &[&str]| vars.iter().any(|v| std::env::var_os(v).is_some()); |
| 254 | let (major, minor) = (rustix::fs::major(node), rustix::fs::minor(node)); |
| 255 | let Ok(driver) = std::fs::read_link(format!("/sys/dev/char/{major}:{minor}/device/driver")) |
| 256 | else { |
| 257 | return Vec::new(); |
| 258 | }; |
| 259 | let driver = driver |
| 260 | .file_name() |
| 261 | .and_then(|d| d.to_str()) |
| 262 | .unwrap_or_default(); |
| 263 | let mut env = Vec::new(); |
| 264 | let glob = match driver { |
| 265 | "nvidia" => Some("*nvidia*"), |
| 266 | "amdgpu" | "radeon" => Some("*radeon*"), |
| 267 | "i915" | "xe" => Some("*intel*"), |
| 268 | "nouveau" => Some("*nouveau*"), |
| 269 | _ => None, |
| 270 | }; |
| 271 | let loader = [ |
| 272 | "VK_LOADER_DRIVERS_SELECT", |
| 273 | "VK_LOADER_DRIVERS_DISABLE", |
| 274 | "VK_DRIVER_FILES", |
| 275 | "VK_ICD_FILENAMES", |
| 276 | "VK_ADD_DRIVER_FILES", |
| 277 | ]; |
| 278 | if let Some(glob) = glob |
| 279 | && !set(&loader) |
| 280 | { |
| 281 | env.push(("VK_LOADER_DRIVERS_SELECT", glob.into())); |
| 282 | } |
| 283 | let glvnd = [ |
| 284 | "__EGL_VENDOR_LIBRARY_FILENAMES", |
| 285 | "__EGL_VENDOR_LIBRARY_DIRS", |
| 286 | ]; |
| 287 | if driver == "nvidia" |
| 288 | && !set(&glvnd) |
| 289 | && let Some(vendor) = nvidia_egl_vendor() |
| 290 | { |
| 291 | env.push(("__EGL_VENDOR_LIBRARY_FILENAMES", vendor.into())); |
| 292 | } |
| 293 | env |
| 294 | } |
| 295 | |
| 296 | /// NVIDIA's glvnd EGL vendor file, from glvnd's default search path. |
| 297 | fn nvidia_egl_vendor() -> Option<std::path::PathBuf> { |
| 298 | [ |
| 299 | "/run/opengl-driver/share/glvnd/egl_vendor.d", |
| 300 | "/etc/glvnd/egl_vendor.d", |
| 301 | "/usr/share/glvnd/egl_vendor.d", |
| 302 | ] |
| 303 | .into_iter() |
| 304 | .filter_map(|dir| std::fs::read_dir(dir).ok()) |
| 305 | .flatten() |
| 306 | .filter_map(Result::ok) |
| 307 | .map(|e| e.path()) |
| 308 | .find(|p| { |
| 309 | p.extension().is_some_and(|e| e == "json") |
| 310 | && p.file_name() |
| 311 | .and_then(|n| n.to_str()) |
| 312 | .is_some_and(|n| n.contains("nvidia")) |
| 313 | }) |
| 314 | } |
| 315 | |
| 316 | /// The render node of `physical`, or its primary node when it has none. |
| 317 | unsafe fn drm_node(instance: &ash::Instance, physical: vk::PhysicalDevice) -> Option<u64> { |
| 318 | let mut drm = vk::PhysicalDeviceDrmPropertiesEXT::default(); |
| 319 | let mut props = vk::PhysicalDeviceProperties2::default().push_next(&mut drm); |
| 320 | unsafe { instance.get_physical_device_properties2(physical, &mut props) }; |
| 321 | let node = |major: i64, minor: i64| rustix::fs::makedev(major as u32, minor as u32); |
| 322 | if drm.has_render == vk::TRUE { |
| 323 | Some(node(drm.render_major, drm.render_minor)) |
| 324 | } else if drm.has_primary == vk::TRUE { |
| 325 | Some(node(drm.primary_major, drm.primary_minor)) |
| 326 | } else { |
| 327 | None |
| 328 | } |
| 329 | } |
| 330 | |
| 331 | unsafe fn pick_physical( |
| 332 | instance: &ash::Instance, |
| 333 | wanted: Option<&str>, |
| 334 | node: Option<u64>, |
| 335 | ) -> Option<(vk::PhysicalDevice, u32, String)> { |
| 336 | let devices = unsafe { instance.enumerate_physical_devices() }.ok()?; |
| 337 | let mut candidates = Vec::new(); |
| 338 | for physical in devices { |
| 339 | let props = unsafe { instance.get_physical_device_properties(physical) }; |
| 340 | if props.api_version < vk::API_VERSION_1_3 { |
| 341 | continue; |
| 342 | } |
| 343 | let name = props |
| 344 | .device_name_as_c_str() |
| 345 | .unwrap_or(c"unknown") |
| 346 | .to_string_lossy() |
| 347 | .into_owned(); |
| 348 | if wanted.is_some_and(|w| !name.contains(w)) { |
| 349 | continue; |
| 350 | } |
| 351 | let mut f13 = vk::PhysicalDeviceVulkan13Features::default(); |
| 352 | let mut f2 = vk::PhysicalDeviceFeatures2::default().push_next(&mut f13); |
| 353 | unsafe { instance.get_physical_device_features2(physical, &mut f2) }; |
| 354 | if f13.dynamic_rendering == vk::FALSE || f13.synchronization2 == vk::FALSE { |
| 355 | continue; |
| 356 | } |
| 357 | let extensions = |
| 358 | unsafe { instance.enumerate_device_extension_properties(physical) }.unwrap_or_default(); |
| 359 | let has = |n: &std::ffi::CStr| { |
| 360 | extensions |
| 361 | .iter() |
| 362 | .any(|e| e.extension_name_as_c_str().ok() == Some(n)) |
| 363 | }; |
| 364 | if !has(ash::khr::push_descriptor::NAME) { |
| 365 | continue; |
| 366 | } |
| 367 | if let Some(node) = node |
| 368 | && !(has(ash::ext::physical_device_drm::NAME) |
| 369 | && unsafe { serves_node(instance, physical, node) }) |
| 370 | { |
| 371 | continue; |
| 372 | } |
| 373 | let families = unsafe { instance.get_physical_device_queue_family_properties(physical) }; |
| 374 | let Some(family) = families |
| 375 | .iter() |
| 376 | .position(|f| f.queue_flags.contains(vk::QueueFlags::GRAPHICS)) |
| 377 | else { |
| 378 | continue; |
| 379 | }; |
| 380 | let rank = match props.device_type { |
| 381 | vk::PhysicalDeviceType::DISCRETE_GPU => 0, |
| 382 | vk::PhysicalDeviceType::INTEGRATED_GPU => 1, |
| 383 | vk::PhysicalDeviceType::VIRTUAL_GPU => 2, |
| 384 | _ => 3, |
| 385 | }; |
| 386 | candidates.push((rank, physical, family as u32, name)); |
| 387 | } |
| 388 | candidates.sort_by_key(|c| c.0); |
| 389 | candidates.into_iter().next().map(|(_, p, f, n)| (p, f, n)) |
| 390 | } |