testing harness
This commit is contained in:
parent
f7c67f9872
commit
0426e4039b
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ name = "renderer_device"
|
|||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
testing = []
|
||||
|
||||
[dependencies]
|
||||
ash = { workspace = true }
|
||||
ash-window = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -364,10 +364,12 @@ impl DeviceQueueInfos {
|
|||
})
|
||||
.expect("No graphics queue family found");
|
||||
|
||||
if display_handle.is_some() {
|
||||
assert!(
|
||||
graphics.flags.contains(QueueFlags::PRESENT),
|
||||
"Selected graphics queue family does not support presentation"
|
||||
);
|
||||
}
|
||||
|
||||
let compute = queue_families
|
||||
.iter()
|
||||
|
|
|
|||
409
crates/renderer_device/src/testing.rs
Normal file
409
crates/renderer_device/src/testing.rs
Normal file
|
|
@ -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<Mutex<Vec<CapturedMessage>>>,
|
||||
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<Self> {
|
||||
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(|_| "<invalid device name>".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<Self, String> {
|
||||
// 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::<Vec<_>>();
|
||||
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<CapturedMessage> {
|
||||
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::<Vec<_>>()
|
||||
.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() {
|
||||
"<empty validation message>".into()
|
||||
} else {
|
||||
unsafe { CStr::from_ptr(data.p_message) }
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
};
|
||||
let messages = unsafe { &*(user_data as *const Mutex<Vec<CapturedMessage>>) };
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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"] }
|
||||
|
|
|
|||
90
crates/renderer_rendergraph/tests/graph_validation.rs
Normal file
90
crates/renderer_rendergraph/tests/graph_validation.rs
Normal file
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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"] }
|
||||
|
|
|
|||
87
crates/renderer_resources/tests/managed_set_validation.rs
Normal file
87
crates/renderer_resources/tests/managed_set_validation.rs
Normal file
|
|
@ -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();
|
||||
}
|
||||
Loading…
Reference in a new issue