1use crate::error::{AllocationReason, LavaFlowError, Result};
2use crate::memory::allocator::InterprocessMemoryHandle;
3use ash::vk;
4use std::sync::{Arc, OnceLock};
5
6#[cfg(unix)]
7mod unix;
8#[cfg(windows)]
9mod windows;
10
11#[cfg(unix)]
12use unix::{EXTERNAL_MEMORY_HANDLE_TYPE, ExternalHandle, ExternalMemoryDevice};
13#[cfg(windows)]
14use windows::{EXTERNAL_MEMORY_HANDLE_TYPE, ExternalHandle, ExternalMemoryDevice};
15
16const ENV_DISABLE_VULKAN: &str = "LAVA_FLOW_DISABLE_VULKAN";
17const DEFAULT_DEVICE_ID: u32 = 0;
18const PKG_VERSION_MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR");
19const PKG_VERSION_MINOR: &str = env!("CARGO_PKG_VERSION_MINOR");
20const PKG_VERSION_PATCH: &str = env!("CARGO_PKG_VERSION_PATCH");
21
22#[derive(Debug)]
24pub struct MemoryBuffer {
25 context: Arc<DeviceContext>,
26 size: usize,
27 allocation_size: u64,
28 buffer: vk::Buffer,
29 memory: vk::DeviceMemory,
30 #[cfg_attr(not(any(test, windows)), allow(dead_code))]
31 external_handle: ExternalHandle,
32}
33
34impl MemoryBuffer {
35 pub fn size(&self) -> usize {
37 self.size
38 }
39
40 pub fn allocation_size(&self) -> u64 {
42 self.allocation_size
43 }
44
45 pub fn device_id(&self) -> u32 {
47 self.context.device_id
48 }
49
50 #[cfg_attr(not(test), allow(dead_code))]
52 pub(crate) fn shared_handle(&self) -> Result<InterprocessMemoryHandle> {
53 self.external_handle.duplicate_for_ipc()
54 }
55}
56
57impl Drop for MemoryBuffer {
58 fn drop(&mut self) {
59 if self.buffer != vk::Buffer::null() {
61 unsafe { self.context.device.destroy_buffer(self.buffer, None) };
62 self.buffer = vk::Buffer::null();
63 }
64 if self.memory != vk::DeviceMemory::null() {
65 unsafe { self.context.device.free_memory(self.memory, None) };
66 self.memory = vk::DeviceMemory::null();
67 }
68 self.size = 0;
69 self.allocation_size = 0;
70 }
71}
72
73#[derive(Debug)]
75pub struct Allocator {
76 context: Arc<DeviceContext>,
78}
79
80impl Allocator {
81 pub fn new() -> Result<Self> {
83 Self::new_for_device(DEFAULT_DEVICE_ID)
84 }
85
86 pub fn new_for_device(device_id: u32) -> Result<Self> {
88 if vulkan_disabled_by_env() {
89 return Err(LavaFlowError::GpuBackendUnavailable);
90 }
91 let context = Arc::new(DeviceContext::new(device_id)?);
92 Ok(Self { context })
93 }
94
95 pub fn device_id(&self) -> u32 {
97 self.context.device_id
98 }
99
100 pub fn allocate(&self, size: usize) -> Result<MemoryBuffer> {
102 let requested_size = size;
103 if requested_size == 0 {
104 return Err(LavaFlowError::InvalidAllocationRequest {
105 size: requested_size,
106 reason: AllocationReason::ZeroSize,
107 });
108 }
109 let context = &self.context;
110 let buffer = OwnedBuffer::create(context, requested_size)?;
111 let memory_requirements =
112 VULKAN_API.buffer_memory_requirements(&context.device, buffer.as_raw());
113 let allocation_size = memory_requirements.size;
115 if allocation_size < requested_size as u64 {
116 return Err(vulkan_operation_error(
117 "get_buffer_memory_requirements",
118 format!(
119 "allocation size {} smaller than requested {}",
120 allocation_size, requested_size
121 ),
122 ));
123 }
124 let memory_type_index =
125 context.resolve_memory_type_index(memory_requirements.memory_type_bits)?;
126 let memory =
127 OwnedMemory::allocate(context, buffer.as_raw(), allocation_size, memory_type_index)?;
128 VULKAN_API.bind_buffer_memory(&context.device, buffer.as_raw(), memory.as_raw())?;
129 let external_handle = VULKAN_API.export_memory_handle(context, memory.as_raw())?;
131
132 Ok(MemoryBuffer {
133 context: Arc::clone(&self.context),
134 size: requested_size,
135 allocation_size,
136 buffer: buffer.into_raw(),
137 memory: memory.into_raw(),
138 external_handle,
139 })
140 }
141}
142
143struct DeviceContext {
144 _runtime: Arc<VulkanRuntime>,
145 #[cfg_attr(not(test), allow(dead_code))]
146 queue_family_index: u32,
147 device_id: u32,
148 device: ash::Device,
149 external_memory_device: ExternalMemoryDevice,
150 memory_properties: vk::PhysicalDeviceMemoryProperties,
151}
152
153impl std::fmt::Debug for DeviceContext {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 f.debug_struct("DeviceContext")
156 .field("device_id", &self.device_id)
157 .field("runtime_strong_count", &Arc::strong_count(&self._runtime))
158 .finish_non_exhaustive()
159 }
160}
161
162impl DeviceContext {
163 fn new(device_id: u32) -> Result<Self> {
164 let runtime = VulkanRuntime::instance()?;
165 let physical_device = runtime.get_physical_device(device_id)?;
166 let instance = &runtime.instance;
167 let queue_family_index = VULKAN_API
168 .find_queue_family_index(instance, physical_device)
169 .ok_or_else(|| vulkan_operation_error("pick_queue_family", "no queue family found"))?;
170 let queue_priorities = [1.0_f32];
172 let queue_create_info = [vk::DeviceQueueCreateInfo::default()
173 .queue_family_index(queue_family_index)
174 .queue_priorities(&queue_priorities)];
175
176 let extension_names = ExternalMemoryDevice::required_extensions();
177 let device_info = vk::DeviceCreateInfo::default()
179 .queue_create_infos(&queue_create_info)
180 .enabled_extension_names(extension_names);
181 let device = VULKAN_API.create_device(instance, physical_device, &device_info)?;
182
183 let external_memory_device = ExternalMemoryDevice::new(instance, &device);
184 let memory_properties =
185 unsafe { instance.get_physical_device_memory_properties(physical_device) };
186 Ok(Self {
187 _runtime: runtime,
188 queue_family_index,
189 device_id,
190 device,
191 external_memory_device,
192 memory_properties,
193 })
194 }
195
196 fn resolve_memory_type_index(&self, type_bits: u32) -> Result<u32> {
197 VULKAN_API
198 .find_memory_type_index(
199 &self.memory_properties,
200 type_bits,
201 vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT,
203 )
204 .ok_or_else(|| {
205 vulkan_operation_error("find_memory_type_index", "no host-visible memory type")
206 })
207 }
208}
209
210impl Drop for DeviceContext {
211 fn drop(&mut self) {
212 unsafe { self.device.destroy_device(None) };
216 }
217}
218
219struct VulkanRuntime {
220 _entry: ash::Entry,
222 instance: ash::Instance,
223}
224
225impl VulkanRuntime {
226 fn instance() -> Result<Arc<Self>> {
227 static SHARED_RUNTIME: OnceLock<std::result::Result<Arc<VulkanRuntime>, String>> =
228 OnceLock::new();
229 SHARED_RUNTIME
230 .get_or_init(|| {
231 let entry = VULKAN_API.load_entry().map_err(|err| err.to_string())?;
232 let instance = Self::create_instance(&entry).map_err(|err| err.to_string())?;
233 Ok(Arc::new(Self {
234 _entry: entry,
235 instance,
236 }))
237 })
238 .as_ref()
239 .map(Arc::clone)
240 .map_err(|err| vulkan_operation_error("init_runtime", err.clone()))
241 }
242
243 fn get_physical_device(&self, requested_device_id: u32) -> Result<vk::PhysicalDevice> {
244 let physical_devices = VULKAN_API.enumerate_physical_devices(&self.instance)?;
245 if physical_devices.is_empty() {
246 return Err(LavaFlowError::GpuBackendUnavailable);
247 }
248
249 physical_devices
250 .get(requested_device_id as usize)
251 .copied()
252 .ok_or(LavaFlowError::GpuDeviceNotFound {
253 device_id: requested_device_id,
254 })
255 }
256
257 fn create_instance(entry: &ash::Entry) -> Result<ash::Instance> {
258 let instance_api = unsafe { entry.try_enumerate_instance_version() }
259 .map_err(|err| vulkan_operation_error("enumerate_instance_version", err.to_string()))?
260 .unwrap_or(vk::API_VERSION_1_0);
261 Self::ensure_min_vulkan_version(instance_api)?;
262
263 let package_version = Self::package_version();
264 let app_info = vk::ApplicationInfo::default()
265 .application_name(c"lava-flow")
266 .application_version(package_version)
267 .engine_name(c"lava-flow")
268 .engine_version(package_version)
269 .api_version(vk::API_VERSION_1_2);
270 let instance_info = vk::InstanceCreateInfo::default().application_info(&app_info);
271 VULKAN_API.create_instance(entry, &instance_info)
272 }
273
274 fn ensure_min_vulkan_version(instance_api: u32) -> Result<()> {
275 if instance_api < vk::API_VERSION_1_2 {
276 return Err(vulkan_operation_error(
277 "check_instance_version",
278 format!(
279 "requires Vulkan 1.2+, found {}.{}.{}",
280 vk::api_version_major(instance_api),
281 vk::api_version_minor(instance_api),
282 vk::api_version_patch(instance_api),
283 ),
284 ));
285 }
286 Ok(())
287 }
288
289 fn package_version() -> u32 {
290 let major = PKG_VERSION_MAJOR.parse::<u32>().unwrap_or(0);
291 let minor = PKG_VERSION_MINOR.parse::<u32>().unwrap_or(0);
292 let patch = PKG_VERSION_PATCH.parse::<u32>().unwrap_or(0);
293 vk::make_api_version(0, major, minor, patch)
294 }
295}
296
297impl Drop for VulkanRuntime {
298 fn drop(&mut self) {
299 unsafe { self.instance.destroy_instance(None) };
300 }
301}
302
303struct OwnedBuffer<'a> {
304 context: &'a DeviceContext,
305 buffer: vk::Buffer,
306}
307
308impl<'a> OwnedBuffer<'a> {
309 fn create(context: &'a DeviceContext, size: usize) -> Result<Self> {
310 let mut external_buffer_info =
311 vk::ExternalMemoryBufferCreateInfo::default().handle_types(EXTERNAL_MEMORY_HANDLE_TYPE);
313 let buffer_create_info = vk::BufferCreateInfo::default()
314 .size(size as u64)
315 .usage(vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST)
317 .sharing_mode(vk::SharingMode::EXCLUSIVE)
318 .push_next(&mut external_buffer_info);
319 let buffer = unsafe { context.device.create_buffer(&buffer_create_info, None) }
320 .map_err(|err| vulkan_operation_error("create_buffer", err.to_string()))?;
321 Ok(Self { context, buffer })
322 }
323
324 fn as_raw(&self) -> vk::Buffer {
325 self.buffer
326 }
327
328 fn into_raw(mut self) -> vk::Buffer {
329 let raw = self.buffer;
330 self.buffer = vk::Buffer::null();
331 raw
332 }
333}
334
335impl Drop for OwnedBuffer<'_> {
336 fn drop(&mut self) {
337 if self.buffer != vk::Buffer::null() {
338 unsafe { self.context.device.destroy_buffer(self.buffer, None) };
339 }
340 }
341}
342
343struct OwnedMemory<'a> {
344 context: &'a DeviceContext,
345 memory: vk::DeviceMemory,
346}
347
348impl<'a> OwnedMemory<'a> {
349 fn allocate(
350 context: &'a DeviceContext,
351 buffer: vk::Buffer,
352 allocation_size: u64,
353 memory_type_index: u32,
354 ) -> Result<Self> {
355 let mut export_memory_info =
356 vk::ExportMemoryAllocateInfo::default().handle_types(EXTERNAL_MEMORY_HANDLE_TYPE);
358 let mut dedicated_info = vk::MemoryDedicatedAllocateInfo::default().buffer(buffer);
359 let alloc_info = vk::MemoryAllocateInfo::default()
360 .allocation_size(allocation_size)
361 .memory_type_index(memory_type_index)
362 .push_next(&mut export_memory_info)
363 .push_next(&mut dedicated_info);
364 let memory = VULKAN_API.allocate_memory(&context.device, &alloc_info)?;
365 Ok(Self { context, memory })
366 }
367
368 fn as_raw(&self) -> vk::DeviceMemory {
369 self.memory
370 }
371
372 fn into_raw(mut self) -> vk::DeviceMemory {
373 let raw = self.memory;
374 self.memory = vk::DeviceMemory::null();
375 raw
376 }
377}
378
379impl Drop for OwnedMemory<'_> {
380 fn drop(&mut self) {
381 if self.memory != vk::DeviceMemory::null() {
382 unsafe { self.context.device.free_memory(self.memory, None) };
383 }
384 }
385}
386
387trait VulkanApi: Sync {
388 fn load_entry(&self) -> Result<ash::Entry>;
389 fn create_instance(
390 &self,
391 entry: &ash::Entry,
392 instance_info: &vk::InstanceCreateInfo<'_>,
393 ) -> Result<ash::Instance>;
394 fn enumerate_physical_devices(
395 &self,
396 instance: &ash::Instance,
397 ) -> Result<Vec<vk::PhysicalDevice>>;
398 fn find_queue_family_index(
399 &self,
400 instance: &ash::Instance,
401 physical_device: vk::PhysicalDevice,
402 ) -> Option<u32>;
403 fn create_device(
404 &self,
405 instance: &ash::Instance,
406 physical_device: vk::PhysicalDevice,
407 device_info: &vk::DeviceCreateInfo<'_>,
408 ) -> Result<ash::Device>;
409 fn find_memory_type_index(
410 &self,
411 properties: &vk::PhysicalDeviceMemoryProperties,
412 type_bits: u32,
413 required_flags: vk::MemoryPropertyFlags,
414 ) -> Option<u32>;
415 fn allocate_memory(
416 &self,
417 device: &ash::Device,
418 alloc_info: &vk::MemoryAllocateInfo<'_>,
419 ) -> Result<vk::DeviceMemory>;
420 fn buffer_memory_requirements(
421 &self,
422 device: &ash::Device,
423 buffer: vk::Buffer,
424 ) -> vk::MemoryRequirements;
425 fn bind_buffer_memory(
426 &self,
427 device: &ash::Device,
428 buffer: vk::Buffer,
429 memory: vk::DeviceMemory,
430 ) -> Result<()>;
431 fn export_memory_handle(
432 &self,
433 context: &DeviceContext,
434 memory: vk::DeviceMemory,
435 ) -> Result<ExternalHandle>;
436}
437
438struct RealVulkanApi;
439
440impl VulkanApi for RealVulkanApi {
441 fn load_entry(&self) -> Result<ash::Entry> {
442 unsafe { ash::Entry::load() }
443 .map_err(|err| vulkan_operation_error("load_entry", err.to_string()))
444 }
445
446 fn create_instance(
447 &self,
448 entry: &ash::Entry,
449 instance_info: &vk::InstanceCreateInfo<'_>,
450 ) -> Result<ash::Instance> {
451 unsafe { entry.create_instance(instance_info, None) }
452 .map_err(|err| vulkan_operation_error("create_instance", err.to_string()))
453 }
454
455 fn enumerate_physical_devices(
456 &self,
457 instance: &ash::Instance,
458 ) -> Result<Vec<vk::PhysicalDevice>> {
459 unsafe { instance.enumerate_physical_devices() }
460 .map_err(|err| vulkan_operation_error("enumerate_physical_devices", err.to_string()))
461 }
462
463 fn find_queue_family_index(
464 &self,
465 instance: &ash::Instance,
466 physical_device: vk::PhysicalDevice,
467 ) -> Option<u32> {
468 let queue_families =
469 unsafe { instance.get_physical_device_queue_family_properties(physical_device) };
470 let queue_family_is_usable = |props: &vk::QueueFamilyProperties| {
471 props.queue_count > 0
472 && props
473 .queue_flags
474 .intersects(vk::QueueFlags::COMPUTE | vk::QueueFlags::GRAPHICS)
475 };
476 queue_families
477 .iter()
478 .position(queue_family_is_usable)
479 .and_then(|index| u32::try_from(index).ok())
480 }
481
482 fn create_device(
483 &self,
484 instance: &ash::Instance,
485 physical_device: vk::PhysicalDevice,
486 device_info: &vk::DeviceCreateInfo<'_>,
487 ) -> Result<ash::Device> {
488 unsafe { instance.create_device(physical_device, device_info, None) }
489 .map_err(|err| vulkan_operation_error("create_device", err.to_string()))
490 }
491
492 fn find_memory_type_index(
493 &self,
494 properties: &vk::PhysicalDeviceMemoryProperties,
495 type_bits: u32,
496 required_flags: vk::MemoryPropertyFlags,
497 ) -> Option<u32> {
498 let count = usize::try_from(properties.memory_type_count).ok()?;
499 properties.memory_types[..count]
500 .iter()
501 .enumerate()
502 .find_map(|(index, memory_type)| {
503 let bit = 1_u32.checked_shl(u32::try_from(index).ok()?)?;
504 let supported = (type_bits & bit) != 0;
505 if supported && memory_type.property_flags.contains(required_flags) {
506 u32::try_from(index).ok()
507 } else {
508 None
509 }
510 })
511 }
512
513 fn allocate_memory(
514 &self,
515 device: &ash::Device,
516 alloc_info: &vk::MemoryAllocateInfo<'_>,
517 ) -> Result<vk::DeviceMemory> {
518 unsafe { device.allocate_memory(alloc_info, None) }
519 .map_err(|err| vulkan_operation_error("allocate_memory", err.to_string()))
520 }
521
522 fn buffer_memory_requirements(
523 &self,
524 device: &ash::Device,
525 buffer: vk::Buffer,
526 ) -> vk::MemoryRequirements {
527 unsafe { device.get_buffer_memory_requirements(buffer) }
528 }
529
530 fn bind_buffer_memory(
531 &self,
532 device: &ash::Device,
533 buffer: vk::Buffer,
534 memory: vk::DeviceMemory,
535 ) -> Result<()> {
536 unsafe { device.bind_buffer_memory(buffer, memory, 0) }
537 .map_err(|err| vulkan_operation_error("bind_buffer_memory", err.to_string()))
538 }
539
540 fn export_memory_handle(
541 &self,
542 context: &DeviceContext,
543 memory: vk::DeviceMemory,
544 ) -> Result<ExternalHandle> {
545 context.export_memory_handle(memory)
546 }
547}
548
549#[cfg(not(test))]
550static VULKAN_API: RealVulkanApi = RealVulkanApi;
551
552#[cfg(test)]
553static VULKAN_API: tests::support::MockVulkanApi = tests::support::MockVulkanApi;
554
555fn vulkan_disabled_by_env() -> bool {
556 std::env::var_os(ENV_DISABLE_VULKAN).is_some()
557}
558
559fn vulkan_operation_error(operation: &'static str, details: impl Into<String>) -> LavaFlowError {
560 LavaFlowError::VulkanOperation {
561 operation,
562 details: details.into(),
563 }
564}
565
566#[cfg(test)]
567mod tests {
568 use self::support::set_fail_point;
569 use super::*;
570 use crate::test_support::env::Guard as EnvGuard;
571 use ash::vk::Handle;
572 use std::sync::Once;
573
574 const BUFFER_SIZE: usize = 64;
575 const UNKNOWN_DEVICE_ID: u32 = 99;
576 static VULKAN_SKIP_NOTICE: Once = Once::new();
577
578 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
579 pub(super) enum FailPoint {
580 NoPhysicalDevice,
581 NoQueueFamily,
582 CreateInstance,
583 EnumeratePhysicalDevicesError,
584 CreateDevice,
585 LoadEntry,
586 FindMemoryType,
587 AllocateMemory,
588 BufferRequirementsTooSmall,
589 BindMemory,
590 ExportHandle,
591 ExportHandleSyscall,
592 }
593
594 pub(super) mod support {
595 use super::*;
596 use std::{cell::Cell, thread_local};
597
598 thread_local! {
599 static FAIL_POINT: Cell<Option<FailPoint>> = const { Cell::new(None) };
600 }
601
602 pub(super) fn set_fail_point(fail_point: FailPoint) {
603 FAIL_POINT.with(|slot| slot.set(Some(fail_point)));
604 }
605
606 fn should_fail(fail_point: FailPoint) -> bool {
607 FAIL_POINT.with(|slot| {
608 if slot.get() == Some(fail_point) {
609 slot.set(None);
610 true
611 } else {
612 false
613 }
614 })
615 }
616
617 pub(in crate::memory::gpu) struct MockVulkanApi;
618
619 impl VulkanApi for MockVulkanApi {
620 fn load_entry(&self) -> Result<ash::Entry> {
621 if should_fail(FailPoint::LoadEntry) {
622 Err(vulkan_operation_error(
623 "load_entry",
624 vk::Result::ERROR_INITIALIZATION_FAILED.to_string(),
625 ))
626 } else {
627 RealVulkanApi.load_entry()
628 }
629 }
630
631 fn create_instance(
632 &self,
633 entry: &ash::Entry,
634 instance_info: &vk::InstanceCreateInfo<'_>,
635 ) -> Result<ash::Instance> {
636 if should_fail(FailPoint::CreateInstance) {
637 Err(vulkan_operation_error(
638 "create_instance",
639 vk::Result::ERROR_INITIALIZATION_FAILED.to_string(),
640 ))
641 } else {
642 RealVulkanApi.create_instance(entry, instance_info)
643 }
644 }
645
646 fn enumerate_physical_devices(
647 &self,
648 instance: &ash::Instance,
649 ) -> Result<Vec<vk::PhysicalDevice>> {
650 if should_fail(FailPoint::EnumeratePhysicalDevicesError) {
651 return Err(vulkan_operation_error(
652 "enumerate_physical_devices",
653 vk::Result::ERROR_INITIALIZATION_FAILED.to_string(),
654 ));
655 }
656 if should_fail(FailPoint::NoPhysicalDevice) {
657 Ok(Vec::new())
658 } else {
659 RealVulkanApi.enumerate_physical_devices(instance)
660 }
661 }
662
663 fn find_queue_family_index(
664 &self,
665 instance: &ash::Instance,
666 physical_device: vk::PhysicalDevice,
667 ) -> Option<u32> {
668 if should_fail(FailPoint::NoQueueFamily) {
669 None
670 } else {
671 RealVulkanApi.find_queue_family_index(instance, physical_device)
672 }
673 }
674
675 fn create_device(
676 &self,
677 instance: &ash::Instance,
678 physical_device: vk::PhysicalDevice,
679 device_info: &vk::DeviceCreateInfo<'_>,
680 ) -> Result<ash::Device> {
681 if should_fail(FailPoint::CreateDevice) {
682 Err(vulkan_operation_error(
683 "create_device",
684 vk::Result::ERROR_INITIALIZATION_FAILED.to_string(),
685 ))
686 } else {
687 RealVulkanApi.create_device(instance, physical_device, device_info)
688 }
689 }
690
691 fn find_memory_type_index(
692 &self,
693 properties: &vk::PhysicalDeviceMemoryProperties,
694 type_bits: u32,
695 required_flags: vk::MemoryPropertyFlags,
696 ) -> Option<u32> {
697 if should_fail(FailPoint::FindMemoryType) {
698 None
699 } else {
700 RealVulkanApi.find_memory_type_index(properties, type_bits, required_flags)
701 }
702 }
703
704 fn allocate_memory(
705 &self,
706 device: &ash::Device,
707 alloc_info: &vk::MemoryAllocateInfo<'_>,
708 ) -> Result<vk::DeviceMemory> {
709 if should_fail(FailPoint::AllocateMemory) {
710 Err(vulkan_operation_error(
711 "allocate_memory",
712 vk::Result::ERROR_OUT_OF_DEVICE_MEMORY.to_string(),
713 ))
714 } else {
715 RealVulkanApi.allocate_memory(device, alloc_info)
716 }
717 }
718
719 fn buffer_memory_requirements(
720 &self,
721 device: &ash::Device,
722 buffer: vk::Buffer,
723 ) -> vk::MemoryRequirements {
724 let mut requirements = RealVulkanApi.buffer_memory_requirements(device, buffer);
725 if should_fail(FailPoint::BufferRequirementsTooSmall) && requirements.size > 0 {
726 requirements.size -= 1;
727 }
728 requirements
729 }
730
731 fn bind_buffer_memory(
732 &self,
733 device: &ash::Device,
734 buffer: vk::Buffer,
735 memory: vk::DeviceMemory,
736 ) -> Result<()> {
737 if should_fail(FailPoint::BindMemory) {
738 Err(vulkan_operation_error(
739 "bind_buffer_memory",
740 vk::Result::ERROR_MEMORY_MAP_FAILED.to_string(),
741 ))
742 } else {
743 RealVulkanApi.bind_buffer_memory(device, buffer, memory)
744 }
745 }
746
747 fn export_memory_handle(
748 &self,
749 context: &DeviceContext,
750 memory: vk::DeviceMemory,
751 ) -> Result<ExternalHandle> {
752 if should_fail(FailPoint::ExportHandle) {
753 return Err(vulkan_operation_error(
754 "export_memory_handle",
755 "forced test failure",
756 ));
757 }
758 if should_fail(FailPoint::ExportHandleSyscall) {
759 #[cfg(unix)]
760 return Err(vulkan_operation_error(
761 "get_memory_fd",
762 vk::Result::ERROR_INVALID_EXTERNAL_HANDLE.to_string(),
763 ));
764 #[cfg(windows)]
765 return Err(vulkan_operation_error(
766 "get_memory_win32_handle",
767 vk::Result::ERROR_INVALID_EXTERNAL_HANDLE.to_string(),
768 ));
769 }
770 RealVulkanApi.export_memory_handle(context, memory)
771 }
772 }
773 }
774
775 fn maybe_allocator() -> Option<Allocator> {
776 match Allocator::new() {
777 Ok(allocator) => Some(allocator),
778 Err(_) => {
779 VULKAN_SKIP_NOTICE.call_once(|| {
780 eprintln!(
781 "Skipping Vulkan-dependent GPU tests: Vulkan backend unavailable (driver/loader/runtime missing or disabled via LAVA_FLOW_DISABLE_VULKAN)."
782 );
783 });
784 None
785 }
786 }
787 }
788
789 fn with_allocator(test: impl FnOnce(&mut Allocator)) {
790 if let Some(mut allocator) = maybe_allocator() {
791 test(&mut allocator);
792 }
793 }
794
795 fn assert_error_matches(
796 err: &LavaFlowError,
797 expected: &str,
798 predicate: impl FnOnce(&LavaFlowError) -> bool,
799 ) {
800 assert!(
801 predicate(err),
802 "expected error matching {expected}, got {err:?}"
803 );
804 }
805
806 fn allocator_instance_handle(allocator: &Allocator) -> u64 {
807 allocator.context._runtime.instance.handle().as_raw()
808 }
809
810 struct OwnedInstance {
811 instance: ash::Instance,
812 }
813
814 impl OwnedInstance {
815 fn new(instance: ash::Instance) -> Self {
816 Self { instance }
817 }
818
819 fn as_ref(&self) -> &ash::Instance {
820 &self.instance
821 }
822 }
823
824 impl Drop for OwnedInstance {
825 fn drop(&mut self) {
826 unsafe { self.instance.destroy_instance(None) };
827 }
828 }
829
830 fn available_device_ids() -> Vec<u32> {
831 if vulkan_disabled_by_env() {
832 return Vec::new();
833 }
834
835 let Ok(entry) = VULKAN_API.load_entry() else {
836 return Vec::new();
837 };
838 let app_info = vk::ApplicationInfo::default()
839 .application_name(c"lava-flow")
840 .application_version(0)
841 .engine_name(c"lava-flow")
842 .engine_version(0)
843 .api_version(vk::API_VERSION_1_2);
844 let instance_info = vk::InstanceCreateInfo::default().application_info(&app_info);
845 let Ok(instance) = VULKAN_API.create_instance(&entry, &instance_info) else {
846 return Vec::new();
847 };
848 let instance = OwnedInstance::new(instance);
849 let Ok(physical_devices) = VULKAN_API.enumerate_physical_devices(instance.as_ref()) else {
850 return Vec::new();
851 };
852 (0..physical_devices.len())
853 .map(|index| u32::try_from(index).expect("enumerated device index must fit into u32"))
854 .collect::<Vec<u32>>()
855 }
856
857 #[test]
858 fn available_device_ids_returns_empty_when_disabled() {
859 let _guard = EnvGuard::set(ENV_DISABLE_VULKAN, "1");
860 assert!(available_device_ids().is_empty());
861 }
862
863 #[test]
864 fn available_device_ids_returns_empty_when_entry_load_fails() {
865 set_fail_point(FailPoint::LoadEntry);
866 assert!(available_device_ids().is_empty());
867 }
868
869 #[test]
870 fn available_device_ids_returns_empty_when_instance_create_fails() {
871 set_fail_point(FailPoint::CreateInstance);
872 assert!(available_device_ids().is_empty());
873 }
874
875 #[test]
876 fn available_device_ids_returns_empty_when_enumerate_fails() {
877 set_fail_point(FailPoint::EnumeratePhysicalDevicesError);
878 assert!(available_device_ids().is_empty());
879 }
880
881 #[test]
882 fn allocate_rejects_zero_size() {
883 with_allocator(|allocator| {
884 let err = allocator
885 .allocate(0)
886 .expect_err("zero-sized allocation must fail");
887 assert!(matches!(
888 err,
889 LavaFlowError::InvalidAllocationRequest {
890 size: 0,
891 reason: AllocationReason::ZeroSize,
892 }
893 ));
894 });
895 }
896
897 #[test]
898 fn new_reports_backend_unavailable_when_disabled() {
899 let _guard = EnvGuard::set(ENV_DISABLE_VULKAN, "1");
900 let err = Allocator::new().expect_err("constructor should fail without backend");
901 assert_error_matches(&err, "LavaFlowError::GpuBackendUnavailable", |err| {
902 matches!(err, LavaFlowError::GpuBackendUnavailable)
903 });
904 }
905
906 #[test]
907 fn allocator_reports_selected_device_id() {
908 with_allocator(|allocator| {
909 let selected_device = allocator.device_id();
910 assert_ne!(selected_device, UNKNOWN_DEVICE_ID);
911 });
912 }
913
914 #[test]
915 fn creates_allocator_per_discovered_device_and_allocates() {
916 with_allocator(|_| {
917 for device_id in available_device_ids() {
918 let per_device =
919 Allocator::new_for_device(device_id).expect("create per-device allocator");
920 assert_eq!(per_device.device_id(), device_id);
921 let buffer = per_device
922 .allocate(BUFFER_SIZE)
923 .expect("allocate on selected device");
924 assert_eq!(buffer.device_id(), device_id);
925 assert_eq!(buffer.size(), BUFFER_SIZE);
926 }
927 });
928 }
929
930 #[test]
931 fn allocators_share_runtime_instance_for_same_device() {
932 with_allocator(|first| {
933 let second = Allocator::new().expect("create second allocator");
934 assert_eq!(
935 allocator_instance_handle(first),
936 allocator_instance_handle(&second)
937 );
938 });
939 }
940
941 #[test]
942 fn allocators_share_runtime_instance_across_devices() {
943 with_allocator(|_| {
944 let ids = available_device_ids();
945 let first = Allocator::new_for_device(ids[0]).expect("create first allocator");
946 let second_device_id = ids.get(1).copied().unwrap_or(ids[0]);
947 let second =
948 Allocator::new_for_device(second_device_id).expect("create second allocator");
949 assert_eq!(
950 allocator_instance_handle(&first),
951 allocator_instance_handle(&second)
952 );
953 });
954 }
955
956 #[test]
957 fn allocate_rejects_unknown_device() {
958 with_allocator(|_| {
959 let err =
960 Allocator::new_for_device(UNKNOWN_DEVICE_ID).expect_err("unknown device must fail");
961 assert_error_matches(
962 &err,
963 "LavaFlowError::GpuDeviceNotFound { device_id: UNKNOWN_DEVICE_ID }",
964 |err| {
965 matches!(
966 err,
967 LavaFlowError::GpuDeviceNotFound {
968 device_id: UNKNOWN_DEVICE_ID,
969 }
970 )
971 },
972 );
973 });
974 }
975
976 #[test]
977 fn allocate_returns_buffer_with_valid_handle() {
978 with_allocator(|allocator| {
979 let buffer = allocator
980 .allocate(BUFFER_SIZE)
981 .expect("allocate gpu buffer");
982 assert!(format!("{buffer:?}").contains("MemoryBuffer"));
983 assert_eq!(buffer.size(), BUFFER_SIZE);
984 assert!(buffer.allocation_size() >= BUFFER_SIZE as u64);
985 assert_eq!(buffer.device_id(), allocator.device_id());
986 let handle = buffer.shared_handle().expect("export handle");
987 #[cfg(unix)]
988 assert!(matches!(handle, InterprocessMemoryHandle::GpuOpaqueFd(_)));
989 #[cfg(windows)]
990 assert!(matches!(
991 handle,
992 InterprocessMemoryHandle::GpuOpaqueWin32Handle(_)
993 ));
994 });
995 }
996
997 #[test]
998 fn command_copy_between_allocated_buffers_succeeds() {
999 with_allocator(|allocator| {
1000 let src = allocator
1001 .allocate(BUFFER_SIZE)
1002 .expect("allocate source gpu buffer");
1003 let dst = allocator
1004 .allocate(BUFFER_SIZE)
1005 .expect("allocate destination gpu buffer");
1006 let context = &allocator.context;
1007 unsafe {
1008 let queue = context
1009 .device
1010 .get_device_queue(context.queue_family_index, 0);
1011
1012 let pool_info = vk::CommandPoolCreateInfo::default()
1013 .queue_family_index(context.queue_family_index);
1014 let command_pool = context
1015 .device
1016 .create_command_pool(&pool_info, None)
1017 .expect("create command pool");
1018
1019 let alloc_info = vk::CommandBufferAllocateInfo::default()
1020 .command_pool(command_pool)
1021 .level(vk::CommandBufferLevel::PRIMARY)
1022 .command_buffer_count(1);
1023 let command_buffer = context
1024 .device
1025 .allocate_command_buffers(&alloc_info)
1026 .expect("allocate command buffer")[0];
1027
1028 let begin_info = vk::CommandBufferBeginInfo::default()
1029 .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT);
1030 context
1031 .device
1032 .begin_command_buffer(command_buffer, &begin_info)
1033 .expect("begin command buffer");
1034
1035 let copy_regions = [vk::BufferCopy::default().size(BUFFER_SIZE as u64)];
1036 context.device.cmd_copy_buffer(
1037 command_buffer,
1038 src.buffer,
1039 dst.buffer,
1040 ©_regions,
1041 );
1042 context
1043 .device
1044 .end_command_buffer(command_buffer)
1045 .expect("end command buffer");
1046
1047 let command_buffers = [command_buffer];
1048 let submit_infos = [vk::SubmitInfo::default().command_buffers(&command_buffers)];
1049 context
1050 .device
1051 .queue_submit(queue, &submit_infos, vk::Fence::null())
1052 .expect("submit copy command");
1053 context
1054 .device
1055 .queue_wait_idle(queue)
1056 .expect("wait queue idle");
1057
1058 context
1059 .device
1060 .free_command_buffers(command_pool, &command_buffers);
1061 context.device.destroy_command_pool(command_pool, None);
1062 }
1063 });
1064 }
1065
1066 #[test]
1067 fn find_memory_type_index_returns_none_when_bits_do_not_match() {
1068 let mut properties = vk::PhysicalDeviceMemoryProperties {
1069 memory_type_count: 1,
1070 ..Default::default()
1071 };
1072 properties.memory_types[0].property_flags =
1073 vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT;
1074 let index = RealVulkanApi.find_memory_type_index(
1075 &properties,
1076 0,
1077 vk::MemoryPropertyFlags::HOST_VISIBLE,
1078 );
1079 assert_eq!(index, None);
1080 }
1081
1082 #[test]
1083 fn find_memory_type_index_returns_matching_slot() {
1084 let mut properties = vk::PhysicalDeviceMemoryProperties {
1085 memory_type_count: 2,
1086 ..Default::default()
1087 };
1088 properties.memory_types[0].property_flags = vk::MemoryPropertyFlags::DEVICE_LOCAL;
1089 properties.memory_types[1].property_flags =
1090 vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT;
1091 let index = RealVulkanApi.find_memory_type_index(
1092 &properties,
1093 0b10,
1094 vk::MemoryPropertyFlags::HOST_VISIBLE,
1095 );
1096 assert_eq!(index, Some(1));
1097 }
1098
1099 #[test]
1100 fn vulkan_operation_error_keeps_operation_name() {
1101 let err = vulkan_operation_error("unit_test", "details");
1102 assert!(matches!(
1103 err,
1104 LavaFlowError::VulkanOperation { operation, .. } if operation == "unit_test"
1105 ));
1106 }
1107
1108 #[test]
1109 fn new_allocator_initializes() {
1110 with_allocator(|allocator| {
1111 assert_eq!(
1112 allocator.device_id(),
1113 Allocator::new()
1114 .expect("vulkan backend must be available")
1115 .device_id()
1116 );
1117 });
1118 }
1119
1120 #[test]
1121 fn ensure_min_vulkan_version_rejects_older_versions() {
1122 let err = VulkanRuntime::ensure_min_vulkan_version(vk::API_VERSION_1_1)
1123 .expect_err("vulkan 1.1 should be rejected");
1124 assert_error_matches(
1125 &err,
1126 "LavaFlowError::VulkanOperation { operation: \"check_instance_version\", .. }",
1127 |err| {
1128 matches!(
1129 err,
1130 LavaFlowError::VulkanOperation {
1131 operation: "check_instance_version",
1132 ..
1133 }
1134 )
1135 },
1136 );
1137 }
1138
1139 #[test]
1140 fn ensure_min_vulkan_version_accepts_1_2() {
1141 VulkanRuntime::ensure_min_vulkan_version(vk::API_VERSION_1_2)
1142 .expect("vulkan 1.2 should be accepted");
1143 }
1144
1145 #[test]
1146 fn select_physical_device_returns_requested_item() {
1147 with_allocator(|allocator| {
1148 let selected = Allocator::new_for_device(allocator.device_id())
1149 .expect("create allocator for discovered id");
1150 assert_eq!(selected.device_id(), allocator.device_id());
1151 });
1152 }
1153
1154 #[test]
1155 fn select_physical_device_rejects_unknown_requested_index() {
1156 let err = Allocator::new_for_device(UNKNOWN_DEVICE_ID).expect_err("unknown index");
1157 assert_error_matches(
1158 &err,
1159 "LavaFlowError::GpuDeviceNotFound { device_id: UNKNOWN_DEVICE_ID }",
1160 |err| {
1161 matches!(
1162 err,
1163 LavaFlowError::GpuDeviceNotFound {
1164 device_id: UNKNOWN_DEVICE_ID
1165 }
1166 )
1167 },
1168 );
1169 }
1170
1171 #[test]
1172 fn allocate_reports_forced_find_memory_type_failure() {
1173 with_allocator(|allocator| {
1174 set_fail_point(FailPoint::FindMemoryType);
1175 let err = allocator
1176 .allocate(BUFFER_SIZE)
1177 .expect_err("forced memory type failure");
1178 assert_error_matches(
1179 &err,
1180 "LavaFlowError::VulkanOperation { operation: \"find_memory_type_index\", .. }",
1181 |err| {
1182 matches!(
1183 err,
1184 LavaFlowError::VulkanOperation {
1185 operation: "find_memory_type_index",
1186 ..
1187 }
1188 )
1189 },
1190 );
1191 });
1192 }
1193
1194 #[test]
1195 fn allocate_reports_forced_allocate_memory_failure() {
1196 with_allocator(|allocator| {
1197 set_fail_point(FailPoint::AllocateMemory);
1198 let err = allocator
1199 .allocate(BUFFER_SIZE)
1200 .expect_err("forced allocate memory failure");
1201 assert_error_matches(
1202 &err,
1203 "LavaFlowError::VulkanOperation { operation: \"allocate_memory\", .. }",
1204 |err| {
1205 matches!(
1206 err,
1207 LavaFlowError::VulkanOperation {
1208 operation: "allocate_memory",
1209 ..
1210 }
1211 )
1212 },
1213 );
1214 });
1215 }
1216
1217 #[test]
1218 fn allocate_reports_buffer_requirements_smaller_than_requested() {
1219 with_allocator(|allocator| {
1220 set_fail_point(FailPoint::BufferRequirementsTooSmall);
1221 let err = allocator
1222 .allocate(BUFFER_SIZE)
1223 .expect_err("forced small requirements failure");
1224 assert_error_matches(
1225 &err,
1226 "LavaFlowError::VulkanOperation { operation: \"get_buffer_memory_requirements\", .. }",
1227 |err| {
1228 matches!(
1229 err,
1230 LavaFlowError::VulkanOperation {
1231 operation: "get_buffer_memory_requirements",
1232 ..
1233 }
1234 )
1235 },
1236 );
1237 });
1238 }
1239
1240 #[test]
1241 fn allocate_reports_forced_bind_memory_failure() {
1242 with_allocator(|allocator| {
1243 set_fail_point(FailPoint::BindMemory);
1244 let err = allocator
1245 .allocate(BUFFER_SIZE)
1246 .expect_err("forced bind memory failure");
1247 assert_error_matches(
1248 &err,
1249 "LavaFlowError::VulkanOperation { operation: \"bind_buffer_memory\", .. }",
1250 |err| {
1251 matches!(
1252 err,
1253 LavaFlowError::VulkanOperation {
1254 operation: "bind_buffer_memory",
1255 ..
1256 }
1257 )
1258 },
1259 );
1260 });
1261 }
1262
1263 #[test]
1264 fn allocate_reports_forced_export_failure() {
1265 with_allocator(|allocator| {
1266 set_fail_point(FailPoint::ExportHandle);
1267 let err = allocator
1268 .allocate(BUFFER_SIZE)
1269 .expect_err("forced export failure");
1270 assert_error_matches(
1271 &err,
1272 "LavaFlowError::VulkanOperation { operation: \"export_memory_handle\", .. }",
1273 |err| {
1274 matches!(
1275 err,
1276 LavaFlowError::VulkanOperation {
1277 operation: "export_memory_handle",
1278 ..
1279 }
1280 )
1281 },
1282 );
1283 });
1284 }
1285
1286 #[test]
1287 fn allocate_reports_forced_export_syscall_failure() {
1288 with_allocator(|allocator| {
1289 set_fail_point(FailPoint::ExportHandleSyscall);
1290 let err = allocator
1291 .allocate(BUFFER_SIZE)
1292 .expect_err("forced export syscall failure");
1293 #[cfg(windows)]
1294 assert_error_matches(
1295 &err,
1296 "LavaFlowError::VulkanOperation { operation: \"get_memory_win32_handle\", .. }",
1297 |err| {
1298 matches!(
1299 err,
1300 LavaFlowError::VulkanOperation {
1301 operation: "get_memory_win32_handle",
1302 ..
1303 }
1304 )
1305 },
1306 );
1307 #[cfg(unix)]
1308 assert_error_matches(
1309 &err,
1310 "LavaFlowError::VulkanOperation { operation: \"get_memory_fd\", .. }",
1311 |err| {
1312 matches!(
1313 err,
1314 LavaFlowError::VulkanOperation {
1315 operation: "get_memory_fd",
1316 ..
1317 }
1318 )
1319 },
1320 );
1321 });
1322 }
1323
1324 #[test]
1325 fn maybe_allocator_logs_skip_when_disabled() {
1326 let _guard = EnvGuard::set(ENV_DISABLE_VULKAN, "1");
1327 assert!(maybe_allocator().is_none());
1328 }
1329
1330 #[test]
1331 fn vulkan_runtime_drop_is_exercised() {
1332 with_allocator(|_| {
1333 let entry = unsafe { ash::Entry::load() }.expect("load entry");
1334 let instance = VulkanRuntime::create_instance(&entry).expect("create instance");
1335 let runtime = VulkanRuntime {
1336 _entry: entry,
1337 instance,
1338 };
1339 drop(runtime);
1340 });
1341 }
1342
1343 #[test]
1344 fn external_memory_device_debug_impl_is_used() {
1345 with_allocator(|allocator| {
1346 let debug_text = format!("{:?}", allocator.context.external_memory_device);
1347 assert!(debug_text.contains("ExternalMemoryDevice"));
1348 });
1349 }
1350
1351 #[test]
1352 fn device_context_new_reports_forced_create_device_failure() {
1353 set_fail_point(FailPoint::CreateDevice);
1354 let result = DeviceContext::new(DEFAULT_DEVICE_ID);
1355 assert!(result.is_err(), "forced create device failure");
1356 let err = result.expect_err("error is present");
1357 assert_error_matches(
1358 &err,
1359 "LavaFlowError::VulkanOperation { operation: \"create_device\", .. }",
1360 |err| {
1361 matches!(
1362 err,
1363 LavaFlowError::VulkanOperation {
1364 operation: "create_device",
1365 ..
1366 }
1367 )
1368 },
1369 );
1370 }
1371
1372 #[test]
1373 fn new_returns_error_when_forcing_create_device_failure() {
1374 set_fail_point(FailPoint::CreateDevice);
1375 assert!(Allocator::new().is_err());
1376 }
1377
1378 #[test]
1379 fn new_returns_error_when_forcing_no_physical_device() {
1380 set_fail_point(FailPoint::NoPhysicalDevice);
1381 assert!(Allocator::new().is_err());
1382 }
1383
1384 #[test]
1385 fn new_returns_error_when_forcing_no_queue_family() {
1386 set_fail_point(FailPoint::NoQueueFamily);
1387 assert!(Allocator::new().is_err());
1388 }
1389
1390 #[test]
1391 fn device_context_new_reports_no_physical_device_failure() {
1392 set_fail_point(FailPoint::NoPhysicalDevice);
1393 let result = DeviceContext::new(DEFAULT_DEVICE_ID);
1394 assert!(result.is_err(), "forced no physical device failure");
1395 let err = result.expect_err("error is present");
1396 assert_error_matches(&err, "LavaFlowError::GpuBackendUnavailable", |err| {
1397 matches!(err, LavaFlowError::GpuBackendUnavailable)
1398 });
1399 }
1400
1401 #[test]
1402 fn device_context_new_reports_no_queue_family_failure() {
1403 set_fail_point(FailPoint::NoQueueFamily);
1404 let result = DeviceContext::new(DEFAULT_DEVICE_ID);
1405 assert!(result.is_err(), "forced no queue family failure");
1406 let err = result.expect_err("error is present");
1407 assert_error_matches(
1408 &err,
1409 "LavaFlowError::VulkanOperation { operation: \"pick_queue_family\", .. }",
1410 |err| {
1411 matches!(
1412 err,
1413 LavaFlowError::VulkanOperation {
1414 operation: "pick_queue_family",
1415 ..
1416 }
1417 )
1418 },
1419 );
1420 }
1421}