testing harness

This commit is contained in:
janis 2026-10-26 02:50:28 +01:00
parent f7c67f9872
commit 0426e4039b
Signed by: janis
SSH key fingerprint: SHA256:bB1qbbqmDXZNT0KKD5c2Dfjg53JGhj7B3CFcLIzSqq8
10 changed files with 616 additions and 13 deletions

View file

@ -11,6 +11,7 @@
- `sync.rs`: fence/semaphore wrappers and pools, `FenceFuture`, and background fence waiters. - `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. - `pipeline_cache.rs`: keyed Vulkan pipeline cache with `pipeline_cache.bin` load/export.
- `debug.rs`: Vulkan debug callback forwarding messages to tracing. - `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]`. - `renderer_macros/src/lib.rs`: separate workspace proc-macro source; derives `DeviceRef` for a struct field marked `#[device]`.
## Key types and ownership ## 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. - `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. - `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. - `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. - `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. - `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. - `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. - 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. - `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. - `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. - `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. - `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. - `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. - `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. - `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. - 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. - 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. - `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. - `DebugUtils` is stored inside `InstanceInner`; its destructor destroys the messenger, so retain the loader/messenger for the instance lifetime.

View file

@ -3,6 +3,9 @@ name = "renderer_device"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[features]
testing = []
[dependencies] [dependencies]
ash = { workspace = true } ash = { workspace = true }
ash-window = { workspace = true } ash-window = { workspace = true }

View file

@ -26,10 +26,12 @@ pub struct InstanceExtensions {
} }
pub struct InstanceInner { 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 raw: ash::Instance,
pub extensions: InstanceExtensions, pub extensions: InstanceExtensions,
pub entry: Entry, pub entry: Entry,
pub _debug_utils: DebugUtils,
} }
#[derive(Clone)] #[derive(Clone)]
@ -73,7 +75,7 @@ const DEBUG_LAYER_SETTINGS: &[vk::LayerSettingEXT] = &[
p_layer_name: VALIDATION_LAYER.as_ptr(), p_layer_name: VALIDATION_LAYER.as_ptr(),
p_setting_name: c"validate_best_practices".as_ptr(), p_setting_name: c"validate_best_practices".as_ptr(),
value_count: 1, value_count: 1,
p_values: &[1u8; 1] as *const u8 as _, p_values: &[vk::TRUE] as *const u32 as _,
ty: vk::LayerSettingTypeEXT::BOOL32, ty: vk::LayerSettingTypeEXT::BOOL32,
_marker: core::marker::PhantomData, _marker: core::marker::PhantomData,
}, },
@ -81,7 +83,7 @@ const DEBUG_LAYER_SETTINGS: &[vk::LayerSettingEXT] = &[
p_layer_name: VALIDATION_LAYER.as_ptr(), p_layer_name: VALIDATION_LAYER.as_ptr(),
p_setting_name: c"validate_best_practices_amd".as_ptr(), p_setting_name: c"validate_best_practices_amd".as_ptr(),
value_count: 1, value_count: 1,
p_values: &[1u8; 1] as *const u8 as _, p_values: &[vk::TRUE] as *const u32 as _,
ty: vk::LayerSettingTypeEXT::BOOL32, ty: vk::LayerSettingTypeEXT::BOOL32,
_marker: core::marker::PhantomData, _marker: core::marker::PhantomData,
}, },
@ -89,7 +91,7 @@ const DEBUG_LAYER_SETTINGS: &[vk::LayerSettingEXT] = &[
p_layer_name: VALIDATION_LAYER.as_ptr(), p_layer_name: VALIDATION_LAYER.as_ptr(),
p_setting_name: c"validate_sync".as_ptr(), p_setting_name: c"validate_sync".as_ptr(),
value_count: 1, value_count: 1,
p_values: &[1u8; 1] as *const u8 as _, p_values: &[vk::TRUE] as *const u32 as _,
ty: vk::LayerSettingTypeEXT::BOOL32, ty: vk::LayerSettingTypeEXT::BOOL32,
_marker: core::marker::PhantomData, _marker: core::marker::PhantomData,
}, },
@ -150,8 +152,9 @@ impl Instance {
let mut requested_extensions = desc.instance_extensions.to_vec(); let mut requested_extensions = desc.instance_extensions.to_vec();
requested_extensions.push(make_extension!(ext::debug_utils as 1)); 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)); requested_extensions.push(make_extension!(ext::layer_settings));
}
// These are wanted for surface capabilities querying, but not strictly // These are wanted for surface capabilities querying, but not strictly
// required, and are only supported on newer devices, especially on // required, and are only supported on newer devices, especially on
@ -223,9 +226,9 @@ impl Instance {
}; };
let instance = Arc::new(InstanceInner { let instance = Arc::new(InstanceInner {
_debug_utils: debug_utils,
raw: instance, raw: instance,
extensions, extensions,
_debug_utils: debug_utils,
entry, entry,
}); });

View file

@ -16,6 +16,9 @@ pub mod instance;
pub mod pipeline_cache; pub mod pipeline_cache;
pub mod queue; pub mod queue;
pub mod sync; pub mod sync;
#[cfg(feature = "testing")]
#[doc(hidden)]
pub mod testing;
pub use device::{Device, DeviceOwned, DeviceRef, Extension}; pub use device::{Device, DeviceOwned, DeviceRef, Extension};
pub use instance::{DebugUtils, DebugUtilsCreateInfo, Instance}; pub use instance::{DebugUtils, DebugUtilsCreateInfo, Instance};

View file

@ -364,10 +364,12 @@ impl DeviceQueueInfos {
}) })
.expect("No graphics queue family found"); .expect("No graphics queue family found");
if display_handle.is_some() {
assert!( assert!(
graphics.flags.contains(QueueFlags::PRESENT), graphics.flags.contains(QueueFlags::PRESENT),
"Selected graphics queue family does not support presentation" "Selected graphics queue family does not support presentation"
); );
}
let compute = queue_families let compute = queue_families
.iter() .iter()

View 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)
);
}
}

View file

@ -12,3 +12,4 @@ ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main
[dev-dependencies] [dev-dependencies]
renderer_resources = { path = "../renderer_resources", features = ["testing"] } renderer_resources = { path = "../renderer_resources", features = ["testing"] }
renderer_device = { path = "../renderer_device", features = ["testing"] }

View 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();
}

View file

@ -14,3 +14,6 @@ parking_lot = { workspace = true }
bytemuck = { workspace = true } bytemuck = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" } ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }
[dev-dependencies]
renderer_device = { path = "../renderer_device", features = ["testing"] }

View 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();
}