git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/vk/core.rs (32.9K)

  1 //! `VkCore`: the device-level half of the Vulkan backend — instance (+
  2 //! validation layers), physical device + graphics queue, gpu-allocator, and
  3 //! the shared command pool.
  4 //!
  5 //! Two ways in: [`VkCore::new_for_surface`] picks a present-capable device
  6 //! for a window — a Wayland surface, or on macOS a `CAMetalLayer` through
  7 //! MoltenVK (what `VkRenderer` uses) — and [`VkCore::new_headless`]
  8 //! builds the same core with no surface at all — for offscreen consumers
  9 //! (thumbnail rendering, previews, the future RT engine) that render into
 10 //! images instead of a swapchain.
 11 
 12 use std::ffi::{c_void, CStr, CString};
 13 
 14 use ash::vk;
 15 use gpu_allocator::vulkan::{Allocator, AllocatorCreateDesc};
 16 
 17 /// Memory blocks start small and double as a client needs more, up to the
 18 /// allocator's old fixed sizes. Those defaults (a 256 MiB device block and a
 19 /// 64 MiB host one, reserved on the first allocation of each) held ~320 MiB
 20 /// for every client, against ~60 MiB of allocations in a typical window — a
 21 /// status-bar module included. On the iGPU that is system RAM. Until
 22 /// gpu-allocator 0.28 the sizes passed here were ignored (0.27.0's
 23 /// `Allocator::new` stored the defaults whatever the descriptor said).
 24 /// An allocation larger than the current block size gets one of its own.
 25 fn allocation_sizes() -> gpu_allocator::AllocationSizes {
 26     const MIB: u64 = 1024 * 1024;
 27     gpu_allocator::AllocationSizes::new(8 * MIB, 4 * MIB)
 28         .with_max_device_memblock_size(256 * MIB)
 29         .with_max_host_memblock_size(64 * MIB)
 30 }
 31 
 32 const VALIDATION_LAYER: &CStr = c"VK_LAYER_KHRONOS_validation";
 33 
 34 unsafe extern "system" fn debug_callback(
 35     severity: vk::DebugUtilsMessageSeverityFlagsEXT,
 36     _types: vk::DebugUtilsMessageTypeFlagsEXT,
 37     data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>,
 38     _user_data: *mut c_void,
 39 ) -> vk::Bool32 {
 40     if data.is_null() {
 41         return vk::FALSE;
 42     }
 43     let message = unsafe {
 44         let p = (*data).p_message;
 45         if p.is_null() {
 46             return vk::FALSE;
 47         }
 48         CStr::from_ptr(p).to_string_lossy()
 49     };
 50     if severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR) {
 51         log::error!("[vulkan] {message}");
 52     } else if severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::WARNING) {
 53         log::warn!("[vulkan] {message}");
 54     } else {
 55         log::debug!("[vulkan] {message}");
 56     }
 57     vk::FALSE
 58 }
 59 
 60 pub struct VkCore {
 61     // Field order is drop order: allocator and command pool go before the
 62     // device. The instance (and the loaded library) is process-shared and
 63     // never destroyed — see `shared_instance()`.
 64     pub(crate) allocator: Option<Allocator>,
 65     pub(crate) command_pool: vk::CommandPool,
 66     pub(crate) queue: vk::Queue,
 67     pub(crate) queue_family: u32,
 68     /// The VK_KHR_acceleration_structure device loader — present exactly when
 69     /// the ray-query stack (accel structs + ray_query + BDA) was enabled at
 70     /// device creation. Its presence IS the tier-2 capability signal.
 71     pub(crate) accel_loader: Option<ash::khr::acceleration_structure::Device>,
 72     pub(crate) device: ash::Device,
 73     pub(crate) physical_device: vk::PhysicalDevice,
 74     pub(crate) surface_loader: ash::khr::surface::Instance,
 75     pub(crate) instance: ash::Instance,
 76     pub(crate) min_uniform_align: vk::DeviceSize,
 77     /// minAccelerationStructureScratchOffsetAlignment; 1 when no ray-query stack.
 78     pub(crate) as_scratch_align: vk::DeviceSize,
 79     /// Widest rasterizable line (device lineWidthRange cap); 1.0 when the
 80     /// wideLines feature is absent or disabled.
 81     pub(crate) max_line_width: f32,
 82     /// The most anisotropy a sampler may ask for (device
 83     /// maxSamplerAnisotropy); 1.0 when the samplerAnisotropy feature is
 84     /// absent, which is a sampler that asks for none.
 85     pub(crate) max_anisotropy: f32,
 86     /// VK_KHR_incremental_present is enabled: a present may name the
 87     /// rectangles that changed, which the Wayland WSI forwards as the
 88     /// surface's buffer damage in place of "everything".
 89     pub(crate) incremental_present: bool,
 90 }
 91 
 92 /// The process-wide Vulkan entry + instance every [`VkCore`] hangs off.
 93 ///
 94 /// Instance creation is the expensive part of bringing up a renderer (ICD
 95 /// enumeration + driver init, ~70ms warm and much worse on a cold cache), and
 96 /// popup-style consumers (the cce-cloud daemon) create a renderer per window —
 97 /// so the instance is created once and intentionally lives for the process.
 98 struct SharedInstance {
 99     entry: ash::Entry,
100     instance: ash::Instance,
101     // Held so the messenger stays alive; never destroyed.
102     _debug: Option<(ash::ext::debug_utils::Instance, vk::DebugUtilsMessengerEXT)>,
103     /// VK_KHR_surface + VK_KHR_wayland_surface were available and enabled.
104     has_wayland_surface: bool,
105     /// VK_KHR_surface + VK_EXT_metal_surface were (MoltenVK, on macOS).
106     has_metal_surface: bool,
107     api_version: u32,
108 }
109 
110 static SHARED_INSTANCE: std::sync::OnceLock<SharedInstance> = std::sync::OnceLock::new();
111 
112 fn shared_instance() -> &'static SharedInstance {
113     SHARED_INSTANCE.get_or_init(|| unsafe {
114         let t = std::time::Instant::now();
115         let entry = ash::Entry::load().expect("Failed to load libvulkan");
116 
117         // Validation when available (debug builds or CCE_VK_VALIDATION=1).
118         let want_validation =
119             cfg!(debug_assertions) || std::env::var_os("CCE_VK_VALIDATION").is_some();
120         let validation_available = want_validation
121             && entry
122                 .enumerate_instance_layer_properties()
123                 .map(|layers| {
124                     layers
125                         .iter()
126                         .any(|l| CStr::from_ptr(l.layer_name.as_ptr()) == VALIDATION_LAYER)
127                 })
128                 .unwrap_or(false);
129         if want_validation && !validation_available {
130             log::warn!(
131                 "Vulkan validation requested but VK_LAYER_KHRONOS_validation is not installed"
132             );
133         }
134 
135         let api_version = match entry.try_enumerate_instance_version().ok().flatten() {
136             Some(v) if v >= vk::API_VERSION_1_2 => vk::API_VERSION_1_2,
137             Some(v) => v,
138             None => vk::API_VERSION_1_0,
139         };
140         let app_name = c"cce-ui";
141         let app_info = vk::ApplicationInfo::default()
142             .application_name(app_name)
143             .engine_name(app_name)
144             .api_version(api_version);
145 
146         // Surface extensions are enabled whenever the loader offers them, so
147         // the one shared instance serves both windowed and headless cores.
148         let ext_props = entry
149             .enumerate_instance_extension_properties(None)
150             .unwrap_or_default();
151         let has_inst_ext = |name: &CStr| {
152             ext_props
153                 .iter()
154                 .any(|e| CStr::from_ptr(e.extension_name.as_ptr()) == name)
155         };
156         let has_surface = has_inst_ext(ash::khr::surface::NAME);
157         let has_wayland_surface = has_surface && has_inst_ext(ash::khr::wayland_surface::NAME);
158         let has_metal_surface = has_surface && has_inst_ext(ash::ext::metal_surface::NAME);
159         // MoltenVK is a PORTABILITY driver: since loader 1.3.216 the loader
160         // lists one only to an instance that asks for portability
161         // enumeration, and a device of one must enable
162         // VK_KHR_portability_subset (below). macOS only, so the instance a
163         // Linux driver sees is the one it always saw.
164         let portability = cfg!(target_os = "macos") && has_inst_ext(ash::khr::portability_enumeration::NAME);
165 
166         let mut extension_names: Vec<*const i8> = Vec::new();
167         if has_wayland_surface || has_metal_surface {
168             extension_names.push(ash::khr::surface::NAME.as_ptr());
169         }
170         if has_wayland_surface {
171             extension_names.push(ash::khr::wayland_surface::NAME.as_ptr());
172         }
173         if has_metal_surface {
174             extension_names.push(ash::ext::metal_surface::NAME.as_ptr());
175         }
176         if portability {
177             extension_names.push(ash::khr::portability_enumeration::NAME.as_ptr());
178         }
179         if validation_available {
180             extension_names.push(ash::ext::debug_utils::NAME.as_ptr());
181         }
182         let layer_names_owned: Vec<CString> = if validation_available {
183             vec![VALIDATION_LAYER.to_owned()]
184         } else {
185             Vec::new()
186         };
187         let layer_names: Vec<*const i8> = layer_names_owned.iter().map(|l| l.as_ptr()).collect();
188 
189         let instance = entry
190             .create_instance(
191                 &vk::InstanceCreateInfo::default()
192                     .flags(if portability {
193                         vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR
194                     } else {
195                         vk::InstanceCreateFlags::empty()
196                     })
197                     .application_info(&app_info)
198                     .enabled_extension_names(&extension_names)
199                     .enabled_layer_names(&layer_names),
200                 None,
201             )
202             .expect("Failed to create Vulkan instance");
203 
204         let debug = if validation_available {
205             let loader = ash::ext::debug_utils::Instance::new(&entry, &instance);
206             let messenger = loader
207                 .create_debug_utils_messenger(
208                     &vk::DebugUtilsMessengerCreateInfoEXT::default()
209                         .message_severity(
210                             vk::DebugUtilsMessageSeverityFlagsEXT::ERROR
211                                 | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING,
212                         )
213                         .message_type(
214                             vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
215                                 | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
216                                 | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE,
217                         )
218                         .pfn_user_callback(Some(debug_callback)),
219                     None,
220                 )
221                 .expect("Failed to create debug messenger");
222             log::info!("Vulkan validation layers enabled");
223             Some((loader, messenger))
224         } else {
225             None
226         };
227 
228         log::debug!("[timing] shared Vulkan instance init: {:?}", t.elapsed());
229         SharedInstance {
230             entry,
231             instance,
232             _debug: debug,
233             has_wayland_surface,
234             has_metal_surface,
235             api_version,
236         }
237     })
238 }
239 
240 /// What a window's `VkSurfaceKHR` is made from: the window system's own
241 /// handles, as raw pointers.
242 #[derive(Debug, Clone, Copy)]
243 pub enum SurfaceTarget {
244     /// A `wl_display` and a `wl_surface` on it.
245     Wayland { display: *mut c_void, surface: *mut c_void },
246     /// A `CAMetalLayer` (macOS, through MoltenVK's VK_EXT_metal_surface).
247     Metal { layer: *const c_void },
248 }
249 
250 impl SurfaceTarget {
251     /// Make the surface on `instance`, or fail as [`SurfaceLost`] (a dead
252     /// display connection). A loader with no extension for this kind of
253     /// window is a broken install, and panics naming it.
254     unsafe fn create(self, instance: &ash::Instance) -> Result<vk::SurfaceKHR, SurfaceLost> {
255         let shared = shared_instance();
256         match self {
257             SurfaceTarget::Wayland { display, surface } => {
258                 if !shared.has_wayland_surface {
259                     panic!("Vulkan loader offers no VK_KHR_wayland_surface but a window was requested");
260                 }
261                 ash::khr::wayland_surface::Instance::new(&shared.entry, instance)
262                     .create_wayland_surface(
263                         &vk::WaylandSurfaceCreateInfoKHR::default().display(display).surface(surface),
264                         None,
265                     )
266                     .map_err(|result| SurfaceLost { call: "vkCreateWaylandSurfaceKHR", result })
267             }
268             SurfaceTarget::Metal { layer } => {
269                 if !shared.has_metal_surface {
270                     panic!("Vulkan loader offers no VK_EXT_metal_surface (is MoltenVK installed?) but a window was requested");
271                 }
272                 ash::ext::metal_surface::Instance::new(&shared.entry, instance)
273                     .create_metal_surface(&vk::MetalSurfaceCreateInfoEXT::default().layer(layer.cast()), None)
274                     .map_err(|result| SurfaceLost { call: "vkCreateMetalSurfaceEXT", result })
275             }
276         }
277     }
278 }
279 
280 /// A Vulkan call on a window surface failed — in practice
281 /// `ERROR_SURFACE_LOST_KHR`: the display connection under the surface is dead,
282 /// because the compositor exited (a logout) or the transport broke. Mesa's
283 /// Wayland WSI answers the surface queries with a roundtrip, so they are the
284 /// first thing to find out.
285 ///
286 /// That is the client's SESSION ending, not a renderer bug, so the window
287 /// constructors and the swapchain path report it instead of panicking, and the
288 /// caller ends the session the way it would for any other lost connection.
289 /// Until 2026-09-25 each of these calls `expect`ed, and a daemon asked for a
290 /// window over a dead connection took the whole process down at logout
291 /// (cce-cloud, `No surface formats: ERROR_SURFACE_LOST_KHR`).
292 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
293 pub struct SurfaceLost {
294     /// The Vulkan entry point that failed.
295     pub call: &'static str,
296     pub result: vk::Result,
297 }
298 
299 impl std::fmt::Display for SurfaceLost {
300     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301         write!(f, "window surface lost ({}: {})", self.call, self.result)
302     }
303 }
304 
305 impl std::error::Error for SurfaceLost {}
306 
307 fn device_type_name(t: vk::PhysicalDeviceType) -> &'static str {
308     match t {
309         vk::PhysicalDeviceType::INTEGRATED_GPU => "integrated",
310         vk::PhysicalDeviceType::DISCRETE_GPU => "discrete",
311         vk::PhysicalDeviceType::VIRTUAL_GPU => "virtual",
312         vk::PhysicalDeviceType::CPU => "cpu",
313         _ => "other",
314     }
315 }
316 
317 /// The warning for a `CCE_VK_DEVICE` preference the chosen device does not
318 /// meet, or `None` when there was no explicit preference or it was met (a
319 /// rank of 0 is a match, whatever the preference). `chosen` and `seen` are
320 /// device descriptions, `seen` every one the loader offered.
321 ///
322 /// A vendor whose driver failed to LOAD is in no list at all, which is the
323 /// case worth naming: in a cce-shadow session the NVIDIA ICD will not load
324 /// without an X display (`DISPLAY` and `XAUTHORITY`), and the loader says why
325 /// only when asked (`VK_LOADER_DEBUG=error`).
326 fn unmet_device_preference(pref: Option<&str>, chosen_rank: i32, chosen: &str, seen: &[String]) -> Option<String> {
327     let pref = pref?;
328     if chosen_rank == 0 {
329         return None;
330     }
331     Some(format!(
332         "cce-ui: CCE_VK_DEVICE={pref} is not met; using {chosen}. Devices the Vulkan loader offered: {}. \
333          A driver that failed to load is not among them (VK_LOADER_DEBUG=error says why; in a \
334          cce-shadow session the NVIDIA driver needs DISPLAY and XAUTHORITY).",
335         seen.join(", ")
336     ))
337 }
338 
339 impl VkCore {
340     /// A core bound to a Wayland surface: the returned `vk::SurfaceKHR` is
341     /// created from the raw pointers and the chosen device supports presenting
342     /// to it. The caller owns the surface handle (destroy it before the core).
343     ///
344     /// # Safety
345     /// `display_ptr` and `surface_ptr` must be live `wl_display` / `wl_surface`
346     /// pointers that outlive the core and everything created from it.
347     ///
348     /// Fails with [`SurfaceLost`] when the surface cannot be created or no
349     /// device can be asked whether it presents to it — a dead display
350     /// connection, not a driver fault.
351     pub unsafe fn new_for_wayland_surface(
352         display_ptr: *mut c_void,
353         surface_ptr: *mut c_void,
354     ) -> Result<(Self, vk::SurfaceKHR), SurfaceLost> {
355         Self::new_for_surface(SurfaceTarget::Wayland { display: display_ptr, surface: surface_ptr })
356     }
357 
358     /// A core bound to a window's surface, whatever the window system: the
359     /// general form of [`new_for_wayland_surface`](Self::new_for_wayland_surface).
360     ///
361     /// # Safety
362     /// The target's pointers must be live and outlive the core and
363     /// everything created from it.
364     pub unsafe fn new_for_surface(target: SurfaceTarget) -> Result<(Self, vk::SurfaceKHR), SurfaceLost> {
365         let (core, surface) = Self::new_inner(Some(target))?;
366         Ok((core, surface.expect("surface requested but not created")))
367     }
368 
369     /// A new `VkSurfaceKHR` on another Wayland surface, from this core's
370     /// instance — for a renderer moving to a fresh `wl_surface` (a menu popup
371     /// re-opened) without a new device. The caller owns the handle.
372     ///
373     /// # Safety
374     /// `display_ptr` and `surface_ptr` must be live `wl_display` / `wl_surface`
375     /// pointers that outlive the returned surface.
376     pub unsafe fn create_wayland_surface(
377         &self,
378         display_ptr: *mut c_void,
379         surface_ptr: *mut c_void,
380     ) -> Result<vk::SurfaceKHR, SurfaceLost> {
381         self.create_surface(SurfaceTarget::Wayland { display: display_ptr, surface: surface_ptr })
382     }
383 
384     /// A new `VkSurfaceKHR` on another window, from this core's instance:
385     /// the general form of [`create_wayland_surface`](Self::create_wayland_surface).
386     ///
387     /// # Safety
388     /// The target's pointers must be live and outlive the returned surface.
389     pub unsafe fn create_surface(&self, target: SurfaceTarget) -> Result<vk::SurfaceKHR, SurfaceLost> {
390         let surface = target.create(&self.instance)?;
391         // The device was chosen for the FIRST surface's present support; a
392         // later surface on the same display is presentable from the same
393         // family on every driver this runs on, but say so if not.
394         if !self
395             .surface_loader
396             .get_physical_device_surface_support(self.physical_device, self.queue_family, surface)
397             .unwrap_or(false)
398         {
399             log::warn!("[vk] queue family {} cannot present to the re-attached surface", self.queue_family);
400         }
401         Ok(surface)
402     }
403 
404     /// A windowless core: no surface extensions, any graphics-capable device.
405     /// For offscreen rendering (thumbnails, previews) and compute.
406     pub fn new_headless() -> Self {
407         // Only a surface can be lost, and there is none here.
408         unsafe { Self::new_inner(None).expect("headless core cannot lose a surface").0 }
409     }
410 
411     unsafe fn new_inner(
412         window: Option<SurfaceTarget>,
413     ) -> Result<(Self, Option<vk::SurfaceKHR>), SurfaceLost> {
414         // CCE_VK_DEVICE: "integrated" (the default), "discrete", or a device
415         // name substring. An explicit request also lifts a session-wide ICD
416         // pin (VK_DRIVER_FILES / VK_ICD_FILENAMES) for THIS process — the
417         // common setup pins Vulkan to the iGPU to keep the dGPU asleep, which
418         // would otherwise make "discrete" unsatisfiable.
419         let device_pref = std::env::var("CCE_VK_DEVICE")
420             .ok()
421             .map(|v| v.to_lowercase())
422             .filter(|v| !v.is_empty());
423         if device_pref.is_some() {
424             std::env::remove_var("VK_DRIVER_FILES");
425             std::env::remove_var("VK_ICD_FILENAMES");
426         }
427 
428         let shared = shared_instance();
429         let entry = &shared.entry;
430         let instance = shared.instance.clone();
431         let api_version = shared.api_version;
432 
433         // Instance-level loader; only usable when VK_KHR_surface was enabled.
434         let surface_loader = ash::khr::surface::Instance::new(entry, &instance);
435 
436         let surface = match window {
437             Some(target) => Some(target.create(&instance)?),
438             None => None,
439         };
440 
441         // Physical device + queue family: graphics, plus present support when
442         // a surface exists. Prefer integrated (the toolkit's LowPower default)
443         // unless CCE_VK_DEVICE says otherwise; an unsatisfiable preference
444         // falls back to the default order rather than failing.
445         let mut candidates: Vec<(vk::PhysicalDevice, u32, i32)> = Vec::new();
446         // A present-support query that FAILED, as opposed to answering no:
447         // on a dead display connection every device fails it, and "no
448         // suitable device" would then misreport a lost surface.
449         let mut support_error: Option<vk::Result> = None;
450         // Every device the loader offered, for the warning below when the
451         // preference cannot be met — including those that cannot draw here.
452         let mut seen: Vec<String> = Vec::new();
453         for pd in instance
454             .enumerate_physical_devices()
455             .expect("No Vulkan physical devices")
456         {
457             let families = instance.get_physical_device_queue_family_properties(pd);
458             let family = families.iter().enumerate().find_map(|(i, f)| {
459                 let graphics = f.queue_flags.contains(vk::QueueFlags::GRAPHICS);
460                 let present = match surface {
461                     Some(surface) => surface_loader
462                         .get_physical_device_surface_support(pd, i as u32, surface)
463                         .unwrap_or_else(|e| {
464                             support_error = Some(e);
465                             false
466                         }),
467                     None => true,
468                 };
469                 (graphics && present).then_some(i as u32)
470             });
471             {
472                 let props = instance.get_physical_device_properties(pd);
473                 let name = CStr::from_ptr(props.device_name.as_ptr()).to_string_lossy();
474                 let usable = if family.is_some() { "" } else { ", cannot draw to this window" };
475                 seen.push(format!("{name} ({}{usable})", device_type_name(props.device_type)));
476             }
477             if let Some(family) = family {
478                 let props = instance.get_physical_device_properties(pd);
479                 let name = CStr::from_ptr(props.device_name.as_ptr())
480                     .to_string_lossy()
481                     .to_lowercase();
482                 let type_rank = match props.device_type {
483                     vk::PhysicalDeviceType::INTEGRATED_GPU => 0,
484                     vk::PhysicalDeviceType::DISCRETE_GPU => 1,
485                     vk::PhysicalDeviceType::VIRTUAL_GPU => 2,
486                     _ => 3,
487                 };
488                 let rank = match device_pref.as_deref() {
489                     Some("discrete") => match props.device_type {
490                         vk::PhysicalDeviceType::DISCRETE_GPU => 0,
491                         other => {
492                             1 + match other {
493                                 vk::PhysicalDeviceType::INTEGRATED_GPU => 0,
494                                 vk::PhysicalDeviceType::VIRTUAL_GPU => 2,
495                                 _ => 3,
496                             }
497                         }
498                     },
499                     Some("integrated") | None => type_rank,
500                     Some(substr) => {
501                         if name.contains(substr) {
502                             0
503                         } else {
504                             1 + type_rank
505                         }
506                     }
507                 };
508                 candidates.push((pd, family, rank));
509             }
510         }
511         candidates.sort_by_key(|&(_, _, rank)| rank);
512         if let (true, Some(surface), Some(result)) = (candidates.is_empty(), surface, support_error) {
513             surface_loader.destroy_surface(surface, None);
514             return Err(SurfaceLost { call: "vkGetPhysicalDeviceSurfaceSupportKHR", result });
515         }
516         let (physical_device, queue_family, chosen_rank) = *candidates
517             .first()
518             .expect("No suitable Vulkan device found");
519         {
520             let props = instance.get_physical_device_properties(physical_device);
521             let name = CStr::from_ptr(props.device_name.as_ptr()).to_string_lossy();
522             log::info!("Vulkan device: {name}");
523             // An explicit preference that was not met falls back SILENTLY
524             // otherwise — and a fallback renders as the device asked for
525             // would, so nothing on screen says it happened (2026-10-02: hours
526             // of "discrete" shadow captures that were all on the Intel GPU).
527             // stderr, not `log`: most clients init no logger.
528             let chosen = format!("{name} ({})", device_type_name(props.device_type));
529             if let Some(msg) = unmet_device_preference(device_pref.as_deref(), chosen_rank, &chosen, &seen) {
530                 static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
531                 if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
532                     eprintln!("{msg}");
533                 }
534             }
535         }
536         let min_uniform_align = instance
537             .get_physical_device_properties(physical_device)
538             .limits
539             .min_uniform_buffer_offset_alignment;
540 
541         let queue_priorities = [1.0f32];
542         let queue_infos = [vk::DeviceQueueCreateInfo::default()
543             .queue_family_index(queue_family)
544             .queue_priorities(&queue_priorities)];
545         let mut device_extensions: Vec<*const i8> = if window.is_some() {
546             vec![ash::khr::swapchain::NAME.as_ptr()]
547         } else {
548             Vec::new()
549         };
550 
551         // The ray-query stack (the RT engine's tier 2): needs the three
552         // extensions plus the BDA / accel-structure / ray-query features and
553         // an API >= 1.2 device (SPIR-V 1.4 shaders). Enabled whenever the
554         // device offers it; consumers check `accel_loader`.
555         let ext_props = instance
556             .enumerate_device_extension_properties(physical_device)
557             .unwrap_or_default();
558         let has_ext = |name: &CStr| {
559             ext_props
560                 .iter()
561                 .any(|e| CStr::from_ptr(e.extension_name.as_ptr()) == name)
562         };
563         let device_api = instance
564             .get_physical_device_properties(physical_device)
565             .api_version
566             .min(api_version);
567         let mut ray_query = device_api >= vk::API_VERSION_1_2
568             && has_ext(ash::khr::acceleration_structure::NAME)
569             && has_ext(ash::khr::ray_query::NAME)
570             && has_ext(ash::khr::deferred_host_operations::NAME);
571         if ray_query {
572             let mut bda = vk::PhysicalDeviceBufferDeviceAddressFeatures::default();
573             let mut asf = vk::PhysicalDeviceAccelerationStructureFeaturesKHR::default();
574             let mut rqf = vk::PhysicalDeviceRayQueryFeaturesKHR::default();
575             let mut features2 = vk::PhysicalDeviceFeatures2::default()
576                 .push_next(&mut bda)
577                 .push_next(&mut asf)
578                 .push_next(&mut rqf);
579             instance.get_physical_device_features2(physical_device, &mut features2);
580             ray_query = bda.buffer_device_address == vk::TRUE
581                 && asf.acceleration_structure == vk::TRUE
582                 && rqf.ray_query == vk::TRUE;
583         }
584 
585         let mut bda_features =
586             vk::PhysicalDeviceBufferDeviceAddressFeatures::default().buffer_device_address(true);
587         let mut as_features = vk::PhysicalDeviceAccelerationStructureFeaturesKHR::default()
588             .acceleration_structure(true);
589         let mut rq_features = vk::PhysicalDeviceRayQueryFeaturesKHR::default().ray_query(true);
590         // wideLines for adjustable wire thickness (the scene stage's dynamic
591         // line width); without it widths clamp to 1.0. (PolygonMode::LINE /
592         // fillModeNonSolid is deliberately NOT used — wires are LINE_LIST
593         // edge meshes; see the scene stage's wireframe pipeline comment.)
594         let supported_features = instance.get_physical_device_features(physical_device);
595         let wide_lines_supported = supported_features.wide_lines == vk::TRUE;
596         let max_line_width = if wide_lines_supported {
597             instance
598                 .get_physical_device_properties(physical_device)
599                 .limits
600                 .line_width_range[1]
601         } else {
602             1.0
603         };
604         // samplerAnisotropy for user images seen at a slant (a picture
605         // standing in the 3D scene): without it a mipmapped image viewed
606         // edge-on is blurred along BOTH axes to the level its short axis
607         // asks for.
608         let anisotropy_supported = supported_features.sampler_anisotropy == vk::TRUE;
609         let max_anisotropy = if anisotropy_supported {
610             instance
611                 .get_physical_device_properties(physical_device)
612                 .limits
613                 .max_sampler_anisotropy
614                 .max(1.0)
615         } else {
616             1.0
617         };
618         let enabled_features = vk::PhysicalDeviceFeatures::default()
619             .wide_lines(wide_lines_supported)
620             .sampler_anisotropy(anisotropy_supported);
621         let mut device_info = vk::DeviceCreateInfo::default()
622             .queue_create_infos(&queue_infos)
623             .enabled_features(&enabled_features);
624         let incremental_present =
625             window.is_some() && has_ext(ash::khr::incremental_present::NAME);
626         // A portability driver's device (MoltenVK) MUST enable the subset
627         // extension it offers; no Linux driver offers it.
628         if has_ext(ash::khr::portability_subset::NAME) {
629             device_extensions.push(ash::khr::portability_subset::NAME.as_ptr());
630         }
631         if incremental_present {
632             device_extensions.push(ash::khr::incremental_present::NAME.as_ptr());
633         }
634         if ray_query {
635             device_extensions.push(ash::khr::acceleration_structure::NAME.as_ptr());
636             device_extensions.push(ash::khr::ray_query::NAME.as_ptr());
637             device_extensions.push(ash::khr::deferred_host_operations::NAME.as_ptr());
638             device_info = device_info
639                 .push_next(&mut bda_features)
640                 .push_next(&mut as_features)
641                 .push_next(&mut rq_features);
642             log::info!("Vulkan ray-query stack enabled (RT tier 2 available)");
643         }
644         let t = std::time::Instant::now();
645         let device = instance
646             .create_device(
647                 physical_device,
648                 &device_info.enabled_extension_names(&device_extensions),
649                 None,
650             )
651             .expect("Failed to create Vulkan device");
652         log::debug!("[timing] vk create_device: {:?}", t.elapsed());
653         let queue = device.get_device_queue(queue_family, 0);
654         let accel_loader =
655             ray_query.then(|| ash::khr::acceleration_structure::Device::new(&instance, &device));
656         let as_scratch_align = if ray_query {
657             let mut as_props = vk::PhysicalDeviceAccelerationStructurePropertiesKHR::default();
658             let mut props2 = vk::PhysicalDeviceProperties2::default().push_next(&mut as_props);
659             instance.get_physical_device_properties2(physical_device, &mut props2);
660             (as_props.min_acceleration_structure_scratch_offset_alignment as vk::DeviceSize).max(1)
661         } else {
662             1
663         };
664 
665         let allocator = Allocator::new(&AllocatorCreateDesc {
666             instance: instance.clone(),
667             device: device.clone(),
668             physical_device,
669             debug_settings: Default::default(),
670             buffer_device_address: ray_query,
671             allocation_sizes: allocation_sizes(),
672         })
673         .expect("Failed to create GPU allocator");
674 
675         let command_pool = device
676             .create_command_pool(
677                 &vk::CommandPoolCreateInfo::default()
678                     .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER)
679                     .queue_family_index(queue_family),
680                 None,
681             )
682             .expect("Failed to create command pool");
683 
684         Ok((
685             VkCore {
686                 allocator: Some(allocator),
687                 command_pool,
688                 queue,
689                 queue_family,
690                 accel_loader,
691                 device,
692                 physical_device,
693                 surface_loader,
694                 instance,
695                 min_uniform_align,
696                 as_scratch_align,
697                 max_line_width,
698                 max_anisotropy,
699                 incremental_present,
700             },
701             surface,
702         ))
703     }
704 }
705 
706 impl Drop for VkCore {
707     fn drop(&mut self) {
708         unsafe {
709             let _ = self.device.device_wait_idle();
710             // The allocator must go before the device it allocates from. The
711             // instance is process-shared and intentionally never destroyed.
712             drop(self.allocator.take());
713             self.device.destroy_command_pool(self.command_pool, None);
714             self.device.destroy_device(None);
715         }
716     }
717 }
718 
719 #[cfg(test)]
720 mod tests {
721     use super::*;
722 
723     /// An explicit preference that was not met says so; one that was met, or
724     /// no preference at all, says nothing.
725     #[test]
726     fn an_unmet_device_preference_is_reported() {
727         let seen = vec!["Intel(R) Iris(R) Xe Graphics (integrated)".to_string()];
728         let msg = unmet_device_preference(Some("discrete"), 1, &seen[0], &seen).expect("unmet");
729         assert!(msg.contains("CCE_VK_DEVICE=discrete is not met"), "{msg}");
730         assert!(msg.contains("using Intel(R) Iris(R) Xe Graphics (integrated)"), "{msg}");
731         assert!(msg.contains("DISPLAY and XAUTHORITY"), "{msg}");
732         assert_eq!(unmet_device_preference(Some("discrete"), 0, &seen[0], &seen), None);
733         assert_eq!(unmet_device_preference(None, 1, &seen[0], &seen), None);
734     }
735 }