//! Real Vulkan test devices with Khronos validation output captured for assertions. //! //! Run the device smoke tests with `cargo test -p renderer_device --features testing`. use std::{ ffi::{CStr, c_void}, fmt, sync::{Arc, Mutex}, }; use ash::{Entry, ext, khr, vk}; use crate::{ Device, Instance, PhysicalDeviceFeatures, instance::{DebugUtils, InstanceDesc, VALIDATION_LAYER}, }; const TEST_LAYER_SETTINGS: &[vk::LayerSettingEXT<'static>] = &[ vk::LayerSettingEXT { p_layer_name: VALIDATION_LAYER.as_ptr(), p_setting_name: c"validate_sync".as_ptr(), value_count: 1, p_values: &[vk::TRUE] as *const u32 as _, ty: vk::LayerSettingTypeEXT::BOOL32, _marker: core::marker::PhantomData, }, vk::LayerSettingEXT { p_layer_name: VALIDATION_LAYER.as_ptr(), p_setting_name: c"validate_best_practices".as_ptr(), value_count: 1, p_values: &[vk::TRUE] as *const u32 as _, ty: vk::LayerSettingTypeEXT::BOOL32, _marker: core::marker::PhantomData, }, ]; /// One validation callback message captured from a real Vulkan instance. #[derive(Debug, Clone)] pub struct CapturedMessage { pub severity: vk::DebugUtilsMessageSeverityFlagsEXT, pub types: vk::DebugUtilsMessageTypeFlagsEXT, pub message: String, } impl fmt::Display for CapturedMessage { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?} {:?}: {}", self.severity, self.types, self.message) } } /// A Vulkan instance and logical device configured for validation-backed tests. /// /// Instances are independent; tests may acquire their own device without sharing Vulkan state. pub struct TestVulkan { device: Device, // Drop the callback messenger while its instance and user data are still alive. _messenger: DebugUtils, messages: Arc>>, instance: Instance, } impl TestVulkan { /// Attempt to create a Vulkan instance/device, printing a reason and returning `None` when /// Vulkan, a physical device, or the Khronos validation layer is unavailable. pub fn acquire() -> Option { match Self::create() { Ok(test) => { eprintln!( "renderer_device Vulkan test adapter: {}", test.device .properties() .core .device_name_as_c_str() .map(CStr::to_string_lossy) .unwrap_or_else(|_| "".into()) ); Some(test) } Err(reason) => { eprintln!("Skipping Vulkan test: {reason}"); None } } } /// Create a validation-backed Vulkan test device or panic. Useful for CI jobs where Vulkan is /// a required test dependency. pub fn expect() -> Self { Self::acquire().expect("Vulkan test device is required") } fn create() -> Result { // Do not rewrite loader environment variables here. The loader's default search paths and // environment provided by the development shell are both supported. let entry = unsafe { Entry::load() }.map_err(|error| format!("loading Vulkan: {error}"))?; let layers = unsafe { entry.enumerate_instance_layer_properties() } .map_err(|error| format!("enumerating instance layers: {error}"))?; if !layers .iter() .any(|layer| layer.layer_name_as_c_str().ok() == Some(VALIDATION_LAYER)) { return Err("VK_LAYER_KHRONOS_validation is not available".into()); } let instance = Instance::new(&InstanceDesc { app_name: Some("renderer_device validation tests"), instance_extensions: &[crate::make_extension!(khr::surface)], layer_settings: TEST_LAYER_SETTINGS, layers: &[VALIDATION_LAYER], display_handle: None, ..Default::default() }) .map_err(|error| format!("creating Vulkan instance: {error}"))?; let messages = Arc::new(Mutex::new(Vec::new())); let debug_loader = ext::debug_utils::Instance::new(&instance.inner.entry, &instance.inner.raw); let create_info = vk::DebugUtilsMessengerCreateInfoEXT::default() .message_severity( vk::DebugUtilsMessageSeverityFlagsEXT::ERROR | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING, ) .message_type( vk::DebugUtilsMessageTypeFlagsEXT::GENERAL | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE, ) .user_data(Arc::as_ptr(&messages) as *mut c_void) .pfn_user_callback(Some(capture_callback)); let raw_messenger = unsafe { debug_loader.create_debug_utils_messenger(&create_info, None) } .map_err(|error| format!("creating validation message messenger: {error}"))?; let messenger = DebugUtils { instance: debug_loader, messenger: raw_messenger, }; let adapter = instance .choose_adapter_default(None, &[], None) .map_err(|error| format!("selecting physical device: {error}"))?; let supported = &adapter.features; let requested = PhysicalDeviceFeatures { core: vk::PhysicalDeviceFeatures { shader_sampled_image_array_dynamic_indexing: supported .core .shader_sampled_image_array_dynamic_indexing, ..Default::default() }, core12: vk::PhysicalDeviceVulkan12Features { descriptor_indexing: supported.core12.descriptor_indexing, descriptor_binding_partially_bound: supported .core12 .descriptor_binding_partially_bound, descriptor_binding_update_unused_while_pending: supported .core12 .descriptor_binding_update_unused_while_pending, descriptor_binding_sampled_image_update_after_bind: supported .core12 .descriptor_binding_sampled_image_update_after_bind, descriptor_binding_storage_buffer_update_after_bind: supported .core12 .descriptor_binding_storage_buffer_update_after_bind, runtime_descriptor_array: supported.core12.runtime_descriptor_array, timeline_semaphore: supported.core12.timeline_semaphore, ..Default::default() }, core13: vk::PhysicalDeviceVulkan13Features { synchronization2: supported.core13.synchronization2, dynamic_rendering: supported.core13.dynamic_rendering, maintenance4: supported.core13.maintenance4, ..Default::default() }, ..Default::default() }; let device = adapter .create_logical_device(&instance, &[], requested, None) .map_err(|error| format!("creating logical device: {error}"))?; Ok(Self { device, _messenger: messenger, messages, instance, }) } pub fn instance(&self) -> &Instance { &self.instance } pub fn device(&self) -> &Device { &self.device } pub fn physical_device(&self) -> &crate::PhysicalDeviceInfo { self.device.physical_device() } /// Prints all warnings and fails only for validation errors. pub fn assert_no_validation_errors(&self) { let messages = self.take_messages(); for message in &messages { if message .severity .contains(vk::DebugUtilsMessageSeverityFlagsEXT::WARNING) { eprintln!("Vulkan validation warning: {message}"); } } let errors = messages .into_iter() .filter(|message| { message .severity .contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR) }) .collect::>(); assert!( errors.is_empty(), "Vulkan validation errors:\n{}", format_messages(&errors) ); } /// Fail for warnings as well as errors, for tests that explicitly require a clean log. pub fn assert_no_warnings(&self) { let messages = self.take_messages(); assert!( messages.is_empty(), "Vulkan validation messages:\n{}", format_messages(&messages) ); } /// Drain captured messages; useful for tests that intentionally exercise a validation error. pub fn take_messages(&self) -> Vec { std::mem::take( &mut *self .messages .lock() .expect("validation message lock poisoned"), ) } } fn format_messages(messages: &[CapturedMessage]) -> String { messages .iter() .map(ToString::to_string) .collect::>() .join("\n") } unsafe extern "system" fn capture_callback( severity: vk::DebugUtilsMessageSeverityFlagsEXT, types: vk::DebugUtilsMessageTypeFlagsEXT, callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>, user_data: *mut c_void, ) -> vk::Bool32 { if callback_data.is_null() || user_data.is_null() { return vk::FALSE; } let data = unsafe { &*callback_data }; let message = if data.p_message.is_null() { "".into() } else { unsafe { CStr::from_ptr(data.p_message) } .to_string_lossy() .into_owned() }; let messages = unsafe { &*(user_data as *const Mutex>) }; if let Ok(mut messages) = messages.lock() { messages.push(CapturedMessage { severity, types, message, }); } vk::FALSE } #[cfg(test)] mod tests { use super::*; use crate::device::{ Allocation, AllocationDesc, AllocationStrategy, DeviceObject, MemoryLocation, }; #[test] fn instance_and_device_creation() { let Some(test) = TestVulkan::acquire() else { return; }; assert_ne!(test.instance().inner.raw.handle(), vk::Instance::null()); assert_ne!(test.device().dev().handle(), vk::Device::null()); test.device().wait_idle().expect("device wait idle"); test.assert_no_validation_errors(); } #[test] fn buffer_lifecycle_under_validation() { let Some(test) = TestVulkan::acquire() else { return; }; let device = test.device(); let info = vk::BufferCreateInfo::default() .size(256) .usage(vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST) .sharing_mode(vk::SharingMode::EXCLUSIVE); let buffer = unsafe { device.dev().create_buffer(&info, None) }.expect("create buffer"); let buffer = DeviceObject::new(device.clone(), buffer); let allocation = device .allocate_for_buffer( *buffer, AllocationDesc { name: "renderer_device validation smoke buffer", location: MemoryLocation::CpuToGpu, strategy: AllocationStrategy::AllocatorManaged, }, ) .expect("allocate buffer memory"); let allocation = DeviceObject::new(device.clone(), allocation); device .bind_buffer_memory(&allocation, *buffer) .expect("bind buffer memory"); drop(buffer); drop(allocation); test.assert_no_validation_errors(); } #[test] fn aliasable_buffer_allocation_binds_at_zero_under_validation() { let Some(test) = TestVulkan::acquire() else { return; }; let device = test.device(); let create_info = vk::BufferCreateInfo::default() .size(256) .usage(vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST) .sharing_mode(vk::SharingMode::EXCLUSIVE); let buffer = unsafe { device.dev().create_buffer(&create_info, None) } .expect("create aliasable buffer"); let buffer = DeviceObject::new(device.clone(), buffer); let requirements = device .buffer_memory_requirements(*buffer) .expect("query buffer requirements"); let memory = device .allocate_memory(&requirements, vk::MemoryPropertyFlags::empty()) .expect("allocate aliasable buffer memory"); let binding = device .bind_buffer_at_offset(*buffer, &requirements, memory, 0) .expect("bind aliasable buffer memory"); assert!(matches!(binding, Allocation::Shared(_))); drop(buffer); drop(binding); test.assert_no_validation_errors(); } #[test] fn aliasable_image_allocation_binds_at_zero_under_validation() { let Some(test) = TestVulkan::acquire() else { return; }; let device = test.device(); let create_info = vk::ImageCreateInfo::default() .image_type(vk::ImageType::TYPE_2D) .format(vk::Format::R8G8B8A8_UNORM) .extent(vk::Extent3D { width: 16, height: 16, depth: 1, }) .mip_levels(1) .array_layers(1) .samples(vk::SampleCountFlags::TYPE_1) .tiling(vk::ImageTiling::OPTIMAL) .usage(vk::ImageUsageFlags::TRANSFER_DST) .sharing_mode(vk::SharingMode::EXCLUSIVE) .initial_layout(vk::ImageLayout::UNDEFINED); let image = unsafe { device.dev().create_image(&create_info, None) } .expect("create aliasable image"); let image = DeviceObject::new(device.clone(), image); let requirements = device .image_memory_requirements(*image) .expect("query image requirements"); let memory = device .allocate_memory(&requirements, vk::MemoryPropertyFlags::empty()) .expect("allocate aliasable image memory"); let binding = device .bind_image_at_offset(*image, &requirements, memory, 0) .expect("bind aliasable image memory"); assert!(matches!(binding, Allocation::Shared(_))); drop(image); drop(binding); test.assert_no_validation_errors(); } #[test] fn aliasable_binding_rejects_out_of_bounds_and_unaligned_offsets() { let Some(test) = TestVulkan::acquire() else { return; }; let device = test.device(); let create_info = vk::BufferCreateInfo::default() .size(256) .usage(vk::BufferUsageFlags::TRANSFER_DST) .sharing_mode(vk::SharingMode::EXCLUSIVE); let make_buffer = || { let raw = unsafe { device.dev().create_buffer(&create_info, None) } .expect("create offset validation buffer"); DeviceObject::new(device.clone(), raw) }; let buffer = make_buffer(); let requirements = device .buffer_memory_requirements(*buffer) .expect("query offset validation requirements"); let memory = device .allocate_memory(&requirements, vk::MemoryPropertyFlags::empty()) .expect("allocate offset validation memory"); assert!(matches!( device.bind_buffer_at_offset(*buffer, &requirements, memory, requirements.size), Err(crate::Error::InvalidAllocationOffset { .. }) )); drop(buffer); let buffer = make_buffer(); let requirements = device .buffer_memory_requirements(*buffer) .expect("query unaligned validation requirements"); if requirements.alignment > 1 { let memory = device .allocate_memory(&requirements, vk::MemoryPropertyFlags::empty()) .expect("allocate unaligned validation memory"); assert!(matches!( device.bind_buffer_at_offset(*buffer, &requirements, memory, 1), Err(crate::Error::InvalidAllocationOffset { .. }) )); } drop(buffer); test.assert_no_validation_errors(); } #[test] fn validation_capture_works() { let Some(test) = TestVulkan::acquire() else { return; }; let device = test.device(); let buffer_info = vk::BufferCreateInfo::default() .size(16) .usage(vk::BufferUsageFlags::TRANSFER_DST) .sharing_mode(vk::SharingMode::EXCLUSIVE); let buffer = unsafe { device.dev().create_buffer(&buffer_info, None) } .expect("create validation probe buffer"); let buffer = DeviceObject::new(device.clone(), buffer); let allocation = device .allocate_for_buffer( *buffer, AllocationDesc { name: "renderer_device validation probe buffer", location: MemoryLocation::CpuToGpu, strategy: AllocationStrategy::AllocatorManaged, }, ) .expect("allocate validation probe buffer"); let allocation = DeviceObject::new(device.clone(), allocation); device .bind_buffer_memory(&allocation, *buffer) .expect("bind validation probe buffer"); let pool_info = vk::CommandPoolCreateInfo::default() .queue_family_index(device.queues.graphics().family.index.index()); let pool = unsafe { device.dev().create_command_pool(&pool_info, None) } .expect("create command pool"); let allocate_info = vk::CommandBufferAllocateInfo::default() .command_pool(pool) .level(vk::CommandBufferLevel::PRIMARY) .command_buffer_count(1); let command_buffer = unsafe { device.dev().allocate_command_buffers(&allocate_info) } .expect("allocate command buffer")[0]; unsafe { device .dev() .begin_command_buffer(command_buffer, &vk::CommandBufferBeginInfo::default()) .expect("begin command buffer"); // The size must be a multiple of four. The invalid command is never submitted. device .dev() .cmd_fill_buffer(command_buffer, *buffer, 0, 3, 0); device .dev() .end_command_buffer(command_buffer) .expect("end command buffer"); device.dev().destroy_command_pool(pool, None); } drop(buffer); drop(allocation); let messages = test.take_messages(); assert!( messages.iter().any(|message| { message .severity .contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR) && message.message.contains("VUID-vkCmdFillBuffer-size") }), "expected a vkCmdFillBuffer size validation error, got:\n{}", format_messages(&messages) ); } }