Skip to main content

lava_flow/memory/gpu/
mod.rs

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/// GPU-backed memory buffer metadata and storage.
23#[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    /// Returns the buffer size in bytes.
36    pub fn size(&self) -> usize {
37        self.size
38    }
39
40    /// Returns the Vulkan-required backing allocation size in bytes (>= size()).
41    pub fn allocation_size(&self) -> u64 {
42        self.allocation_size
43    }
44
45    /// Returns the device identifier used for allocation.
46    pub fn device_id(&self) -> u32 {
47        self.context.device_id
48    }
49
50    /// Returns the exportable external handle.
51    #[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        // Order because of vkBindBufferMemory buffer depends on memory:
60        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/// Vulkan GPU allocator.
74#[derive(Debug)]
75pub struct Allocator {
76    // Shared ownership keeps Vulkan device state alive until the last allocated buffer is dropped.
77    context: Arc<DeviceContext>,
78}
79
80impl Allocator {
81    /// Creates a GPU allocator backend for logical device id `0`.
82    pub fn new() -> Result<Self> {
83        Self::new_for_device(DEFAULT_DEVICE_ID)
84    }
85
86    /// Creates a GPU allocator backend bound to one logical device id.
87    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    /// Returns the logical device id this allocator is bound to.
96    pub fn device_id(&self) -> u32 {
97        self.context.device_id
98    }
99
100    /// Allocates a GPU buffer and tags it with an exportable external handle.
101    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        // Vulkan may require allocating more bytes than requested due to alignment/granularity.
114        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        // Keep RAII guards alive until export succeeds, so failure paths cannot leak resources.
130        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        // One queue at priority 1.0 for buffer allocation/mapping operations.
171        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        // Enable platform-specific external-memory extension for exportable handles.
178        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                // Host-visible + coherent keeps writes CPU-accessible without explicit flush.
202                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        // `ExternalMemoryDevice` is only an ash extension wrapper (dispatch helper), not
213        // an owned Vulkan object with its own destroy function. Destroying the VkDevice is
214        // the required explicit teardown here; wrapper fields then drop as plain Rust data.
215        unsafe { self.device.destroy_device(None) };
216    }
217}
218
219struct VulkanRuntime {
220    // Keep the Vulkan loader alive at least as long as the instance.
221    _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            // Export handle metadata is attached via pNext for buffer creation.
312            vk::ExternalMemoryBufferCreateInfo::default().handle_types(EXTERNAL_MEMORY_HANDLE_TYPE);
313        let buffer_create_info = vk::BufferCreateInfo::default()
314            .size(size as u64)
315            // Transfer usage covers staging-like source/destination copies.
316            .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            // Marks memory as exportable through external-handle APIs.
357            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                    &copy_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}