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 }