From 0426e4039bf25a4e2c1d350f8ac4bd3188ccc4ac Mon Sep 17 00:00:00 2001 From: janis Date: Mon, 26 Oct 2026 02:50:28 +0100 Subject: [PATCH] testing harness --- crates/renderer_device/AGENTS.md | 6 +- crates/renderer_device/Cargo.toml | 3 + crates/renderer_device/src/instance.rs | 17 +- crates/renderer_device/src/lib.rs | 3 + crates/renderer_device/src/queue.rs | 10 +- crates/renderer_device/src/testing.rs | 409 ++++++++++++++++++ crates/renderer_rendergraph/Cargo.toml | 1 + .../tests/graph_validation.rs | 90 ++++ crates/renderer_resources/Cargo.toml | 3 + .../tests/managed_set_validation.rs | 87 ++++ 10 files changed, 616 insertions(+), 13 deletions(-) create mode 100644 crates/renderer_device/src/testing.rs create mode 100644 crates/renderer_rendergraph/tests/graph_validation.rs create mode 100644 crates/renderer_resources/tests/managed_set_validation.rs diff --git a/crates/renderer_device/AGENTS.md b/crates/renderer_device/AGENTS.md index 083c9da..3b4016b 100644 --- a/crates/renderer_device/AGENTS.md +++ b/crates/renderer_device/AGENTS.md @@ -11,6 +11,7 @@ - `sync.rs`: fence/semaphore wrappers and pools, `FenceFuture`, and background fence waiters. - `pipeline_cache.rs`: keyed Vulkan pipeline cache with `pipeline_cache.bin` load/export. - `debug.rs`: Vulkan debug callback forwarding messages to tracing. +- `testing.rs` (feature `testing`): `TestVulkan` creates real validation-enabled Vulkan devices and captures validation messages for smoke/integration tests. - `renderer_macros/src/lib.rs`: separate workspace proc-macro source; derives `DeviceRef` for a struct field marked `#[device]`. ## Key types and ownership @@ -36,7 +37,7 @@ - `AllocationStrategy` selects allocator-managed blocks or a dedicated allocation. `Allocation` represents `Owned`, `Shared`, or `Unmanaged` GPU allocations. - `get_extensions` returns supported/enabled and unsupported requested extension sets; `get_layers` returns available requested layers or the supported/unsupported split. - `Queue` shares a `QueueInner` through `Arc`; `with_locked` serializes operations on the Vulkan queue. `QueueFamily` records index, count, flags, granularity, and timestamp bits. -- `DeviceQueues` provides graphics, compute, and transfer queues; absent compute/transfer queues alias the graphics queue. Queue selection requires the selected graphics family to support presentation. +- `DeviceQueues` provides graphics, compute, and transfer queues; absent compute/transfer queues alias the graphics queue. Queue selection requires presentation support only when a display handle is provided. - `DeviceQueueInfos` chooses queue families and produces `vk::DeviceQueueCreateInfo` values before `retrieve_queues` obtains the raw Vulkan queues. - `QueueFlags` in `device.rs` describes requested graphics/async-compute/transfer/present families; `queue::QueueFlags` describes discovered Vulkan family capabilities. They are distinct types. - `FenceFuture` waits and resets a fence, and `SyncThreadpool` schedules blocking fence waits for async wakers. `Fence::from_pool` and `Semaphore::from_pool` wrap recycled pool entries. @@ -45,6 +46,7 @@ - Pooled timeline semaphores are created at value zero; `Semaphore::from_pool` signals the requested initial value when it obtains one. - `PipelineCache::new` computes its key from adapter vendor/API/device/driver properties and tries to load compatible data; cache persistence uses a magic and format version. - `DebugName` is the wrapper for Vulkan object debug names. +- `testing::TestVulkan::acquire` returns a real Vulkan test device or skips with a reason; `expect` requires Vulkan. `take_messages`, `assert_no_validation_errors`, and `assert_no_warnings` inspect the captured debug-utils callback output. Run the device smoke tests using `direnv exec . cargo test -p renderer_device --features testing` from the workspace root. - `Extension` is the name/version pair used by extension helpers and adapter creation. `make_extension!`, `get_extensions`, `get_layers`, and the support/intersection functions centralize Vulkan extension discovery. - `SurfaceCapabilities` returns raw `SurfaceCapabilitiesKHR` plus the effective minimum image count, surface formats, and present modes queried for the raw surface. - `DebugUtilsCreateInfo` is the small severity/message-type configuration record used with debug utilities; `DebugUtils` itself owns the instance loader and messenger handle. @@ -77,7 +79,7 @@ - `queue::QueueFlags` and `device::QueueFlags` intentionally serve different purposes. Keep imports qualified when adding queue-family selection logic. - `PipelineCache` persistence uses a single working-directory path and one cache record. Changes to the key or file header should account for compatibility with existing cache files. - Logical-device setup always includes the swapchain extension in its required extension list, regardless of the caller's extra requested extensions. -- Queue-family selection uses the optional display handle to query presentation support and currently asserts that its chosen graphics family is present-capable. +- Queue-family selection uses the optional display handle to query presentation support and requires its chosen graphics family to be present-capable only when a display handle is provided. - Extension selectors and feature checks drive selection; do not infer that every optional extension is enabled just because a loader type exists. - `Instance::query_presentation_support` handles platform-specific display variants and has an unimplemented XCB branch. - `DebugUtils` is stored inside `InstanceInner`; its destructor destroys the messenger, so retain the loader/messenger for the instance lifetime. diff --git a/crates/renderer_device/Cargo.toml b/crates/renderer_device/Cargo.toml index 8cc57b1..b27768e 100644 --- a/crates/renderer_device/Cargo.toml +++ b/crates/renderer_device/Cargo.toml @@ -3,6 +3,9 @@ name = "renderer_device" version = "0.1.0" edition = "2024" +[features] +testing = [] + [dependencies] ash = { workspace = true } ash-window = { workspace = true } diff --git a/crates/renderer_device/src/instance.rs b/crates/renderer_device/src/instance.rs index c1552d3..10c7cae 100644 --- a/crates/renderer_device/src/instance.rs +++ b/crates/renderer_device/src/instance.rs @@ -26,10 +26,12 @@ pub struct InstanceExtensions { } pub struct InstanceInner { + // Messengers must be destroyed while the raw instance is still alive. Keep this first so + // it drops before `raw` during instance teardown. + pub _debug_utils: DebugUtils, pub raw: ash::Instance, pub extensions: InstanceExtensions, pub entry: Entry, - pub _debug_utils: DebugUtils, } #[derive(Clone)] @@ -73,7 +75,7 @@ const DEBUG_LAYER_SETTINGS: &[vk::LayerSettingEXT] = &[ p_layer_name: VALIDATION_LAYER.as_ptr(), p_setting_name: c"validate_best_practices".as_ptr(), value_count: 1, - p_values: &[1u8; 1] as *const u8 as _, + p_values: &[vk::TRUE] as *const u32 as _, ty: vk::LayerSettingTypeEXT::BOOL32, _marker: core::marker::PhantomData, }, @@ -81,7 +83,7 @@ const DEBUG_LAYER_SETTINGS: &[vk::LayerSettingEXT] = &[ p_layer_name: VALIDATION_LAYER.as_ptr(), p_setting_name: c"validate_best_practices_amd".as_ptr(), value_count: 1, - p_values: &[1u8; 1] as *const u8 as _, + p_values: &[vk::TRUE] as *const u32 as _, ty: vk::LayerSettingTypeEXT::BOOL32, _marker: core::marker::PhantomData, }, @@ -89,7 +91,7 @@ const DEBUG_LAYER_SETTINGS: &[vk::LayerSettingEXT] = &[ p_layer_name: VALIDATION_LAYER.as_ptr(), p_setting_name: c"validate_sync".as_ptr(), value_count: 1, - p_values: &[1u8; 1] as *const u8 as _, + p_values: &[vk::TRUE] as *const u32 as _, ty: vk::LayerSettingTypeEXT::BOOL32, _marker: core::marker::PhantomData, }, @@ -150,8 +152,9 @@ impl Instance { let mut requested_extensions = desc.instance_extensions.to_vec(); requested_extensions.push(make_extension!(ext::debug_utils as 1)); - #[cfg(debug_assertions)] - requested_extensions.push(make_extension!(ext::layer_settings)); + if !desc.layer_settings.is_empty() { + requested_extensions.push(make_extension!(ext::layer_settings)); + } // These are wanted for surface capabilities querying, but not strictly // required, and are only supported on newer devices, especially on @@ -223,9 +226,9 @@ impl Instance { }; let instance = Arc::new(InstanceInner { + _debug_utils: debug_utils, raw: instance, extensions, - _debug_utils: debug_utils, entry, }); diff --git a/crates/renderer_device/src/lib.rs b/crates/renderer_device/src/lib.rs index 6a83abe..e611491 100644 --- a/crates/renderer_device/src/lib.rs +++ b/crates/renderer_device/src/lib.rs @@ -16,6 +16,9 @@ pub mod instance; pub mod pipeline_cache; pub mod queue; pub mod sync; +#[cfg(feature = "testing")] +#[doc(hidden)] +pub mod testing; pub use device::{Device, DeviceOwned, DeviceRef, Extension}; pub use instance::{DebugUtils, DebugUtilsCreateInfo, Instance}; diff --git a/crates/renderer_device/src/queue.rs b/crates/renderer_device/src/queue.rs index 8a77974..bef50c5 100644 --- a/crates/renderer_device/src/queue.rs +++ b/crates/renderer_device/src/queue.rs @@ -364,10 +364,12 @@ impl DeviceQueueInfos { }) .expect("No graphics queue family found"); - assert!( - graphics.flags.contains(QueueFlags::PRESENT), - "Selected graphics queue family does not support presentation" - ); + if display_handle.is_some() { + assert!( + graphics.flags.contains(QueueFlags::PRESENT), + "Selected graphics queue family does not support presentation" + ); + } let compute = queue_families .iter() diff --git a/crates/renderer_device/src/testing.rs b/crates/renderer_device/src/testing.rs new file mode 100644 index 0000000..418e345 --- /dev/null +++ b/crates/renderer_device/src/testing.rs @@ -0,0 +1,409 @@ +//! 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::*; + + #[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 requirements = unsafe { device.dev().get_buffer_memory_requirements(buffer) }; + let allocation = device + .alloc2 + .lock() + .allocate(&gpu_allocator::vulkan::AllocationCreateDesc { + name: "renderer_device validation smoke buffer", + requirements, + location: gpu_allocator::MemoryLocation::CpuToGpu, + linear: true, + allocation_scheme: gpu_allocator::vulkan::AllocationScheme::GpuAllocatorManaged, + }) + .expect("allocate buffer memory"); + unsafe { + device + .dev() + .bind_buffer_memory(buffer, allocation.memory(), allocation.offset()) + .expect("bind buffer memory"); + device.dev().destroy_buffer(buffer, None); + } + device + .alloc2 + .lock() + .free(allocation) + .expect("free buffer memory"); + 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 requirements = unsafe { device.dev().get_buffer_memory_requirements(buffer) }; + let allocation = device + .alloc2 + .lock() + .allocate(&gpu_allocator::vulkan::AllocationCreateDesc { + name: "renderer_device validation probe buffer", + requirements, + location: gpu_allocator::MemoryLocation::CpuToGpu, + linear: true, + allocation_scheme: gpu_allocator::vulkan::AllocationScheme::GpuAllocatorManaged, + }) + .expect("allocate validation probe buffer"); + unsafe { + device + .dev() + .bind_buffer_memory(buffer, allocation.memory(), allocation.offset()) + .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); + device.dev().destroy_buffer(buffer, None); + } + device + .alloc2 + .lock() + .free(allocation) + .expect("free probe 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) + ); + } +} diff --git a/crates/renderer_rendergraph/Cargo.toml b/crates/renderer_rendergraph/Cargo.toml index 1a368d7..54dd1b7 100644 --- a/crates/renderer_rendergraph/Cargo.toml +++ b/crates/renderer_rendergraph/Cargo.toml @@ -12,3 +12,4 @@ ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main [dev-dependencies] renderer_resources = { path = "../renderer_resources", features = ["testing"] } +renderer_device = { path = "../renderer_device", features = ["testing"] } diff --git a/crates/renderer_rendergraph/tests/graph_validation.rs b/crates/renderer_rendergraph/tests/graph_validation.rs new file mode 100644 index 0000000..a10294d --- /dev/null +++ b/crates/renderer_rendergraph/tests/graph_validation.rs @@ -0,0 +1,90 @@ +use std::sync::Arc; + +use ash::vk; +use renderer_device::{device::QueueFlags, testing::TestVulkan}; +use renderer_rendergraph::{ + RenderGraph, + commands::CopyBuffer, + recorder::CommandRecorder, + resources::{BufferRange, ResourceState}, +}; +use renderer_resources::buffers::{Buffer, BufferDesc}; + +#[test] +fn imported_buffer_copy_records_under_validation() { + let Some(test) = TestVulkan::acquire() else { + return; + }; + let device = test.device(); + if device.enabled_features().core13.synchronization2 != vk::TRUE { + eprintln!("Skipping graph recording: synchronization2 is unsupported"); + return; + } + let make_buffer = || { + Arc::new( + Buffer::new( + device.clone(), + BufferDesc { + size: 256, + usage: vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST, + queue_families: QueueFlags::empty(), + mem_location: gpu_allocator::MemoryLocation::CpuToGpu, + ..Default::default() + }, + ) + .expect("create graph test buffer"), + ) + }; + let source = make_buffer(); + let destination = make_buffer(); + let mut graph = RenderGraph::new(); + let source_id = graph + .import_buffer(source, ResourceState::default()) + .expect("import source buffer"); + let destination_id = graph + .import_buffer(destination, ResourceState::default()) + .expect("import destination buffer"); + graph.push(CopyBuffer { + src: source_id, + src_range: BufferRange { + offset: 0, + size: 256, + }, + dst: destination_id, + dst_range: BufferRange { + offset: 0, + size: 256, + }, + }); + let mut graph = graph.compile().expect("compile imported buffer graph"); + + 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 graph command pool"); + let allocation_info = vk::CommandBufferAllocateInfo::default() + .command_pool(pool) + .level(vk::CommandBufferLevel::PRIMARY) + .command_buffer_count(1); + let command_buffer = unsafe { device.dev().allocate_command_buffers(&allocation_info) } + .expect("allocate graph command buffer")[0]; + unsafe { + device + .dev() + .begin_command_buffer(command_buffer, &vk::CommandBufferBeginInfo::default()) + .expect("begin graph command buffer"); + } + let mut recorder = CommandRecorder::new(device.dev(), command_buffer); + graph + .record_commands(&mut recorder) + .expect("record imported buffer graph"); + unsafe { + device + .dev() + .end_command_buffer(command_buffer) + .expect("end graph command buffer"); + device.dev().destroy_command_pool(pool, None); + } + device.wait_idle().expect("wait for graph device idle"); + test.assert_no_validation_errors(); +} diff --git a/crates/renderer_resources/Cargo.toml b/crates/renderer_resources/Cargo.toml index 7966eb7..f2d5648 100644 --- a/crates/renderer_resources/Cargo.toml +++ b/crates/renderer_resources/Cargo.toml @@ -14,3 +14,6 @@ parking_lot = { workspace = true } bytemuck = { workspace = true } tracing = { workspace = true } ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" } + +[dev-dependencies] +renderer_device = { path = "../renderer_device", features = ["testing"] } diff --git a/crates/renderer_resources/tests/managed_set_validation.rs b/crates/renderer_resources/tests/managed_set_validation.rs new file mode 100644 index 0000000..ca5b04f --- /dev/null +++ b/crates/renderer_resources/tests/managed_set_validation.rs @@ -0,0 +1,87 @@ +use std::sync::Arc; + +use ash::vk; +use renderer_device::{device::QueueFlags, testing::TestVulkan}; +use renderer_resources::{ + buffers::{Buffer, BufferDesc}, + pipeline::{ + BufferDescriptor, DescriptorPool, DescriptorPoolDesc, DescriptorSetAllocDesc, + DescriptorSetLayout, DescriptorSetLayoutBindingDesc, DescriptorSetLayoutDesc, + }, +}; + +#[test] +fn managed_set_write_and_pool_reset_under_validation() { + let Some(test) = TestVulkan::acquire() else { + return; + }; + let device = test.device().clone(); + let buffer = Arc::new( + Buffer::new( + device.clone(), + BufferDesc { + size: 256, + usage: vk::BufferUsageFlags::UNIFORM_BUFFER, + queue_families: QueueFlags::empty(), + mem_location: gpu_allocator::MemoryLocation::CpuToGpu, + ..Default::default() + }, + ) + .expect("create descriptor buffer"), + ); + let bindings = [DescriptorSetLayoutBindingDesc { + binding: 0, + count: 1, + kind: vk::DescriptorType::UNIFORM_BUFFER, + stage: vk::ShaderStageFlags::VERTEX, + flags: None, + }]; + let layout = DescriptorSetLayout::new( + device.clone(), + DescriptorSetLayoutDesc { + bindings: &bindings, + ..Default::default() + }, + ) + .expect("create descriptor set layout"); + let sizes = [vk::DescriptorPoolSize::default() + .ty(vk::DescriptorType::UNIFORM_BUFFER) + .descriptor_count(1)]; + let pool = DescriptorPool::new( + device, + DescriptorPoolDesc { + sizes: &sizes, + max_sets: 1, + ..Default::default() + }, + ) + .expect("create descriptor pool"); + let mut set = pool + .allocate_managed(DescriptorSetAllocDesc { + name: None, + layout: &layout, + }) + .expect("allocate managed descriptor set"); + let mut writer = set.writer().expect("create descriptor writer"); + writer + .write_buffer( + 0, + 0, + &[BufferDescriptor { + buffer: &buffer, + offset: 0, + range: 256, + }], + ) + .expect("stage buffer descriptor"); + writer.commit().expect("commit buffer descriptor"); + + test.device().wait_idle().expect("wait for device idle"); + drop(set); + // SAFETY: no command uses this set, the device is idle, and this test excludes concurrent use. + unsafe { pool.reset() }.expect("reset descriptor pool"); + drop(pool); + drop(layout); + drop(buffer); + test.assert_no_validation_errors(); +}