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 }