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

src/vk/core.rs (20.1K)

  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_wayland_surface`] picks a present-capable
  6 //! device for a window (what `VkRenderer` uses), and [`VkCore::new_headless`]
  7 //! builds the same core with no surface at all — for offscreen consumers
  8 //! (thumbnail rendering, previews, the future RT engine) that render into
  9 //! images instead of a swapchain.
 10 
 11 use std::ffi::{c_void, CStr, CString};
 12 
 13 use ash::vk;
 14 use gpu_allocator::vulkan::{Allocator, AllocatorCreateDesc};
 15 
 16 const VALIDATION_LAYER: &CStr = c"VK_LAYER_KHRONOS_validation";
 17 
 18 unsafe extern "system" fn debug_callback(
 19     severity: vk::DebugUtilsMessageSeverityFlagsEXT,
 20     _types: vk::DebugUtilsMessageTypeFlagsEXT,
 21     data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>,
 22     _user_data: *mut c_void,
 23 ) -> vk::Bool32 {
 24     if data.is_null() {
 25         return vk::FALSE;
 26     }
 27     let message = unsafe {
 28         let p = (*data).p_message;
 29         if p.is_null() {
 30             return vk::FALSE;
 31         }
 32         CStr::from_ptr(p).to_string_lossy()
 33     };
 34     if severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR) {
 35         log::error!("[vulkan] {message}");
 36     } else if severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::WARNING) {
 37         log::warn!("[vulkan] {message}");
 38     } else {
 39         log::debug!("[vulkan] {message}");
 40     }
 41     vk::FALSE
 42 }
 43 
 44 pub struct VkCore {
 45     // Field order is drop order: allocator and command pool go before the
 46     // device. The instance (and the loaded library) is process-shared and
 47     // never destroyed — see `shared_instance()`.
 48     pub(crate) allocator: Option<Allocator>,
 49     pub(crate) command_pool: vk::CommandPool,
 50     pub(crate) queue: vk::Queue,
 51     #[allow(dead_code)] // RT engine / future consumers select by family
 52     pub(crate) queue_family: u32,
 53     /// The VK_KHR_acceleration_structure device loader — present exactly when
 54     /// the ray-query stack (accel structs + ray_query + BDA) was enabled at
 55     /// device creation. Its presence IS the tier-2 capability signal.
 56     pub(crate) accel_loader: Option<ash::khr::acceleration_structure::Device>,
 57     pub(crate) device: ash::Device,
 58     pub(crate) physical_device: vk::PhysicalDevice,
 59     pub(crate) surface_loader: ash::khr::surface::Instance,
 60     pub(crate) instance: ash::Instance,
 61     pub(crate) min_uniform_align: vk::DeviceSize,
 62     /// minAccelerationStructureScratchOffsetAlignment; 1 when no ray-query stack.
 63     pub(crate) as_scratch_align: vk::DeviceSize,
 64     /// Widest rasterizable line (device lineWidthRange cap); 1.0 when the
 65     /// wideLines feature is absent or disabled.
 66     pub(crate) max_line_width: f32,
 67 }
 68 
 69 /// The process-wide Vulkan entry + instance every [`VkCore`] hangs off.
 70 ///
 71 /// Instance creation is the expensive part of bringing up a renderer (ICD
 72 /// enumeration + driver init, ~70ms warm and much worse on a cold cache), and
 73 /// popup-style consumers (the cce-cloud daemon) create a renderer per window —
 74 /// so the instance is created once and intentionally lives for the process.
 75 struct SharedInstance {
 76     entry: ash::Entry,
 77     instance: ash::Instance,
 78     // Held so the messenger stays alive; never destroyed.
 79     _debug: Option<(ash::ext::debug_utils::Instance, vk::DebugUtilsMessengerEXT)>,
 80     /// VK_KHR_surface + VK_KHR_wayland_surface were available and enabled.
 81     has_wayland_surface: bool,
 82     api_version: u32,
 83 }
 84 
 85 static SHARED_INSTANCE: std::sync::OnceLock<SharedInstance> = std::sync::OnceLock::new();
 86 
 87 fn shared_instance() -> &'static SharedInstance {
 88     SHARED_INSTANCE.get_or_init(|| unsafe {
 89         let t = std::time::Instant::now();
 90         let entry = ash::Entry::load().expect("Failed to load libvulkan");
 91 
 92         // Validation when available (debug builds or CCE_VK_VALIDATION=1).
 93         let want_validation =
 94             cfg!(debug_assertions) || std::env::var_os("CCE_VK_VALIDATION").is_some();
 95         let validation_available = want_validation
 96             && entry
 97                 .enumerate_instance_layer_properties()
 98                 .map(|layers| {
 99                     layers
100                         .iter()
101                         .any(|l| CStr::from_ptr(l.layer_name.as_ptr()) == VALIDATION_LAYER)
102                 })
103                 .unwrap_or(false);
104         if want_validation && !validation_available {
105             log::warn!(
106                 "Vulkan validation requested but VK_LAYER_KHRONOS_validation is not installed"
107             );
108         }
109 
110         let api_version = match entry.try_enumerate_instance_version().ok().flatten() {
111             Some(v) if v >= vk::API_VERSION_1_2 => vk::API_VERSION_1_2,
112             Some(v) => v,
113             None => vk::API_VERSION_1_0,
114         };
115         let app_name = c"cce-ui";
116         let app_info = vk::ApplicationInfo::default()
117             .application_name(app_name)
118             .engine_name(app_name)
119             .api_version(api_version);
120 
121         // Surface extensions are enabled whenever the loader offers them, so
122         // the one shared instance serves both windowed and headless cores.
123         let ext_props = entry
124             .enumerate_instance_extension_properties(None)
125             .unwrap_or_default();
126         let has_inst_ext = |name: &CStr| {
127             ext_props
128                 .iter()
129                 .any(|e| CStr::from_ptr(e.extension_name.as_ptr()) == name)
130         };
131         let has_wayland_surface =
132             has_inst_ext(ash::khr::surface::NAME) && has_inst_ext(ash::khr::wayland_surface::NAME);
133 
134         let mut extension_names: Vec<*const i8> = Vec::new();
135         if has_wayland_surface {
136             extension_names.push(ash::khr::surface::NAME.as_ptr());
137             extension_names.push(ash::khr::wayland_surface::NAME.as_ptr());
138         }
139         if validation_available {
140             extension_names.push(ash::ext::debug_utils::NAME.as_ptr());
141         }
142         let layer_names_owned: Vec<CString> = if validation_available {
143             vec![VALIDATION_LAYER.to_owned()]
144         } else {
145             Vec::new()
146         };
147         let layer_names: Vec<*const i8> = layer_names_owned.iter().map(|l| l.as_ptr()).collect();
148 
149         let instance = entry
150             .create_instance(
151                 &vk::InstanceCreateInfo::default()
152                     .application_info(&app_info)
153                     .enabled_extension_names(&extension_names)
154                     .enabled_layer_names(&layer_names),
155                 None,
156             )
157             .expect("Failed to create Vulkan instance");
158 
159         let debug = if validation_available {
160             let loader = ash::ext::debug_utils::Instance::new(&entry, &instance);
161             let messenger = loader
162                 .create_debug_utils_messenger(
163                     &vk::DebugUtilsMessengerCreateInfoEXT::default()
164                         .message_severity(
165                             vk::DebugUtilsMessageSeverityFlagsEXT::ERROR
166                                 | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING,
167                         )
168                         .message_type(
169                             vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
170                                 | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
171                                 | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE,
172                         )
173                         .pfn_user_callback(Some(debug_callback)),
174                     None,
175                 )
176                 .expect("Failed to create debug messenger");
177             log::info!("Vulkan validation layers enabled");
178             Some((loader, messenger))
179         } else {
180             None
181         };
182 
183         log::debug!("[timing] shared Vulkan instance init: {:?}", t.elapsed());
184         SharedInstance {
185             entry,
186             instance,
187             _debug: debug,
188             has_wayland_surface,
189             api_version,
190         }
191     })
192 }
193 
194 impl VkCore {
195     /// A core bound to a Wayland surface: the returned `vk::SurfaceKHR` is
196     /// created from the raw pointers and the chosen device supports presenting
197     /// to it. The caller owns the surface handle (destroy it before the core).
198     ///
199     /// # Safety
200     /// `display_ptr` and `surface_ptr` must be live `wl_display` / `wl_surface`
201     /// pointers that outlive the core and everything created from it.
202     pub unsafe fn new_for_wayland_surface(
203         display_ptr: *mut c_void,
204         surface_ptr: *mut c_void,
205     ) -> (Self, vk::SurfaceKHR) {
206         let (core, surface) = Self::new_inner(Some((display_ptr, surface_ptr)));
207         (core, surface.expect("surface requested but not created"))
208     }
209 
210     /// A windowless core: no surface extensions, any graphics-capable device.
211     /// For offscreen rendering (thumbnails, previews) and compute.
212     pub fn new_headless() -> Self {
213         unsafe { Self::new_inner(None).0 }
214     }
215 
216     unsafe fn new_inner(
217         wayland: Option<(*mut c_void, *mut c_void)>,
218     ) -> (Self, Option<vk::SurfaceKHR>) {
219         // CCE_VK_DEVICE: "integrated" (the default), "discrete", or a device
220         // name substring. An explicit request also lifts a session-wide ICD
221         // pin (VK_DRIVER_FILES / VK_ICD_FILENAMES) for THIS process — the
222         // common setup pins Vulkan to the iGPU to keep the dGPU asleep, which
223         // would otherwise make "discrete" unsatisfiable.
224         let device_pref = std::env::var("CCE_VK_DEVICE")
225             .ok()
226             .map(|v| v.to_lowercase())
227             .filter(|v| !v.is_empty());
228         if device_pref.is_some() {
229             std::env::remove_var("VK_DRIVER_FILES");
230             std::env::remove_var("VK_ICD_FILENAMES");
231         }
232 
233         let shared = shared_instance();
234         let entry = &shared.entry;
235         let instance = shared.instance.clone();
236         let api_version = shared.api_version;
237         if wayland.is_some() && !shared.has_wayland_surface {
238             panic!("Vulkan loader offers no VK_KHR_wayland_surface but a window was requested");
239         }
240 
241         // Instance-level loader; only usable when VK_KHR_surface was enabled.
242         let surface_loader = ash::khr::surface::Instance::new(entry, &instance);
243 
244         let surface = wayland.map(|(display_ptr, surface_ptr)| {
245             let wayland_loader = ash::khr::wayland_surface::Instance::new(entry, &instance);
246             wayland_loader
247                 .create_wayland_surface(
248                     &vk::WaylandSurfaceCreateInfoKHR::default()
249                         .display(display_ptr)
250                         .surface(surface_ptr),
251                     None,
252                 )
253                 .expect("Failed to create Wayland surface")
254         });
255 
256         // Physical device + queue family: graphics, plus present support when
257         // a surface exists. Prefer integrated (the toolkit's LowPower default)
258         // unless CCE_VK_DEVICE says otherwise; an unsatisfiable preference
259         // falls back to the default order rather than failing.
260         let mut candidates: Vec<(vk::PhysicalDevice, u32, i32)> = Vec::new();
261         for pd in instance
262             .enumerate_physical_devices()
263             .expect("No Vulkan physical devices")
264         {
265             let families = instance.get_physical_device_queue_family_properties(pd);
266             let family = families.iter().enumerate().find_map(|(i, f)| {
267                 let graphics = f.queue_flags.contains(vk::QueueFlags::GRAPHICS);
268                 let present = match surface {
269                     Some(surface) => surface_loader
270                         .get_physical_device_surface_support(pd, i as u32, surface)
271                         .unwrap_or(false),
272                     None => true,
273                 };
274                 (graphics && present).then_some(i as u32)
275             });
276             if let Some(family) = family {
277                 let props = instance.get_physical_device_properties(pd);
278                 let name = CStr::from_ptr(props.device_name.as_ptr())
279                     .to_string_lossy()
280                     .to_lowercase();
281                 let type_rank = match props.device_type {
282                     vk::PhysicalDeviceType::INTEGRATED_GPU => 0,
283                     vk::PhysicalDeviceType::DISCRETE_GPU => 1,
284                     vk::PhysicalDeviceType::VIRTUAL_GPU => 2,
285                     _ => 3,
286                 };
287                 let rank = match device_pref.as_deref() {
288                     Some("discrete") => match props.device_type {
289                         vk::PhysicalDeviceType::DISCRETE_GPU => 0,
290                         other => {
291                             1 + match other {
292                                 vk::PhysicalDeviceType::INTEGRATED_GPU => 0,
293                                 vk::PhysicalDeviceType::VIRTUAL_GPU => 2,
294                                 _ => 3,
295                             }
296                         }
297                     },
298                     Some("integrated") | None => type_rank,
299                     Some(substr) => {
300                         if name.contains(substr) {
301                             0
302                         } else {
303                             1 + type_rank
304                         }
305                     }
306                 };
307                 candidates.push((pd, family, rank));
308             }
309         }
310         candidates.sort_by_key(|&(_, _, rank)| rank);
311         let (physical_device, queue_family, _) = *candidates
312             .first()
313             .expect("No suitable Vulkan device found");
314         {
315             let props = instance.get_physical_device_properties(physical_device);
316             let name = CStr::from_ptr(props.device_name.as_ptr()).to_string_lossy();
317             log::info!("Vulkan device: {name}");
318         }
319         let min_uniform_align = instance
320             .get_physical_device_properties(physical_device)
321             .limits
322             .min_uniform_buffer_offset_alignment;
323 
324         let queue_priorities = [1.0f32];
325         let queue_infos = [vk::DeviceQueueCreateInfo::default()
326             .queue_family_index(queue_family)
327             .queue_priorities(&queue_priorities)];
328         let mut device_extensions: Vec<*const i8> = if wayland.is_some() {
329             vec![ash::khr::swapchain::NAME.as_ptr()]
330         } else {
331             Vec::new()
332         };
333 
334         // The ray-query stack (the RT engine's tier 2): needs the three
335         // extensions plus the BDA / accel-structure / ray-query features and
336         // an API >= 1.2 device (SPIR-V 1.4 shaders). Enabled whenever the
337         // device offers it; consumers check `accel_loader`.
338         let ext_props = instance
339             .enumerate_device_extension_properties(physical_device)
340             .unwrap_or_default();
341         let has_ext = |name: &CStr| {
342             ext_props
343                 .iter()
344                 .any(|e| CStr::from_ptr(e.extension_name.as_ptr()) == name)
345         };
346         let device_api = instance
347             .get_physical_device_properties(physical_device)
348             .api_version
349             .min(api_version);
350         let mut ray_query = device_api >= vk::API_VERSION_1_2
351             && has_ext(ash::khr::acceleration_structure::NAME)
352             && has_ext(ash::khr::ray_query::NAME)
353             && has_ext(ash::khr::deferred_host_operations::NAME);
354         if ray_query {
355             let mut bda = vk::PhysicalDeviceBufferDeviceAddressFeatures::default();
356             let mut asf = vk::PhysicalDeviceAccelerationStructureFeaturesKHR::default();
357             let mut rqf = vk::PhysicalDeviceRayQueryFeaturesKHR::default();
358             let mut features2 = vk::PhysicalDeviceFeatures2::default()
359                 .push_next(&mut bda)
360                 .push_next(&mut asf)
361                 .push_next(&mut rqf);
362             instance.get_physical_device_features2(physical_device, &mut features2);
363             ray_query = bda.buffer_device_address == vk::TRUE
364                 && asf.acceleration_structure == vk::TRUE
365                 && rqf.ray_query == vk::TRUE;
366         }
367 
368         let mut bda_features =
369             vk::PhysicalDeviceBufferDeviceAddressFeatures::default().buffer_device_address(true);
370         let mut as_features = vk::PhysicalDeviceAccelerationStructureFeaturesKHR::default()
371             .acceleration_structure(true);
372         let mut rq_features = vk::PhysicalDeviceRayQueryFeaturesKHR::default().ray_query(true);
373         // wideLines for adjustable wire thickness (the scene stage's dynamic
374         // line width); without it widths clamp to 1.0. (PolygonMode::LINE /
375         // fillModeNonSolid is deliberately NOT used — wires are LINE_LIST
376         // edge meshes; see the scene stage's wireframe pipeline comment.)
377         let supported_features = instance.get_physical_device_features(physical_device);
378         let wide_lines_supported = supported_features.wide_lines == vk::TRUE;
379         let max_line_width = if wide_lines_supported {
380             instance
381                 .get_physical_device_properties(physical_device)
382                 .limits
383                 .line_width_range[1]
384         } else {
385             1.0
386         };
387         let enabled_features =
388             vk::PhysicalDeviceFeatures::default().wide_lines(wide_lines_supported);
389         let mut device_info = vk::DeviceCreateInfo::default()
390             .queue_create_infos(&queue_infos)
391             .enabled_features(&enabled_features);
392         if ray_query {
393             device_extensions.push(ash::khr::acceleration_structure::NAME.as_ptr());
394             device_extensions.push(ash::khr::ray_query::NAME.as_ptr());
395             device_extensions.push(ash::khr::deferred_host_operations::NAME.as_ptr());
396             device_info = device_info
397                 .push_next(&mut bda_features)
398                 .push_next(&mut as_features)
399                 .push_next(&mut rq_features);
400             log::info!("Vulkan ray-query stack enabled (RT tier 2 available)");
401         }
402         let t = std::time::Instant::now();
403         let device = instance
404             .create_device(
405                 physical_device,
406                 &device_info.enabled_extension_names(&device_extensions),
407                 None,
408             )
409             .expect("Failed to create Vulkan device");
410         log::debug!("[timing] vk create_device: {:?}", t.elapsed());
411         let queue = device.get_device_queue(queue_family, 0);
412         let accel_loader =
413             ray_query.then(|| ash::khr::acceleration_structure::Device::new(&instance, &device));
414         let as_scratch_align = if ray_query {
415             let mut as_props = vk::PhysicalDeviceAccelerationStructurePropertiesKHR::default();
416             let mut props2 = vk::PhysicalDeviceProperties2::default().push_next(&mut as_props);
417             instance.get_physical_device_properties2(physical_device, &mut props2);
418             (as_props.min_acceleration_structure_scratch_offset_alignment as vk::DeviceSize).max(1)
419         } else {
420             1
421         };
422 
423         let allocator = Allocator::new(&AllocatorCreateDesc {
424             instance: instance.clone(),
425             device: device.clone(),
426             physical_device,
427             debug_settings: Default::default(),
428             buffer_device_address: ray_query,
429             allocation_sizes: Default::default(),
430         })
431         .expect("Failed to create GPU allocator");
432 
433         let command_pool = device
434             .create_command_pool(
435                 &vk::CommandPoolCreateInfo::default()
436                     .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER)
437                     .queue_family_index(queue_family),
438                 None,
439             )
440             .expect("Failed to create command pool");
441 
442         (
443             VkCore {
444                 allocator: Some(allocator),
445                 command_pool,
446                 queue,
447                 queue_family,
448                 accel_loader,
449                 device,
450                 physical_device,
451                 surface_loader,
452                 instance,
453                 min_uniform_align,
454                 as_scratch_align,
455                 max_line_width,
456             },
457             surface,
458         )
459     }
460 }
461 
462 impl Drop for VkCore {
463     fn drop(&mut self) {
464         unsafe {
465             let _ = self.device.device_wait_idle();
466             // The allocator must go before the device it allocates from. The
467             // instance is process-shared and intentionally never destroyed.
468             drop(self.allocator.take());
469             self.device.destroy_command_pool(self.command_pool, None);
470             self.device.destroy_device(None);
471         }
472     }
473 }