impeller_hal_vulkan/validation.rs
1//! Capturing validation-layer output so tests can assert on it.
2//!
3//! Validation errors that only reach stderr are worth very little: they are
4//! noticed when someone happens to look, and a device created invalid can pass
5//! an entire suite while appearing to work. Routing the messenger into a log
6//! the context owns turns "validation-clean" from something a developer
7//! remembers to check into something a test asserts.
8
9use ash::vk;
10use std::ffi::{c_void, CStr};
11use std::sync::Mutex;
12
13/// The layer name, requested at instance creation when validation is on.
14pub const VALIDATION_LAYER: &str = "VK_LAYER_KHRONOS_validation";
15
16/// One diagnostic from the validation layer.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ValidationMessage {
19 pub severity: ValidationSeverity,
20 /// The VUID or message identifier, where the layer supplies one.
21 pub id: String,
22 pub message: String,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
26pub enum ValidationSeverity {
27 Info,
28 Warning,
29 Error,
30}
31
32/// Messages collected from the layer for the lifetime of a context.
33#[derive(Debug, Default)]
34pub struct ValidationLog {
35 messages: Mutex<Vec<ValidationMessage>>,
36}
37
38impl ValidationLog {
39 pub fn record(&self, message: ValidationMessage) {
40 // A poisoned lock means a previous callback panicked. Dropping the
41 // message is better than panicking again inside a driver callback,
42 // where unwinding across the FFI boundary is undefined.
43 if let Ok(mut guard) = self.messages.lock() {
44 guard.push(message);
45 }
46 }
47
48 pub fn messages(&self) -> Vec<ValidationMessage> {
49 self.messages.lock().map(|g| g.clone()).unwrap_or_default()
50 }
51
52 /// Messages at error severity, which are the ones that fail a test.
53 pub fn errors(&self) -> Vec<ValidationMessage> {
54 self.messages()
55 .into_iter()
56 .filter(|m| m.severity == ValidationSeverity::Error)
57 .collect()
58 }
59
60 pub fn is_clean(&self) -> bool {
61 self.errors().is_empty()
62 }
63}
64
65/// The messenger callback.
66///
67/// # Safety
68///
69/// Invoked by the loader with a valid callback-data pointer, and with
70/// `user_data` set to the [`ValidationLog`] pointer supplied at messenger
71/// creation. The log outlives the messenger because the context destroys the
72/// messenger before releasing the log.
73pub(crate) unsafe extern "system" fn debug_callback(
74 severity: vk::DebugUtilsMessageSeverityFlagsEXT,
75 _types: vk::DebugUtilsMessageTypeFlagsEXT,
76 data: *const vk::DebugUtilsMessengerCallbackDataEXT<'_>,
77 user_data: *mut c_void,
78) -> vk::Bool32 {
79 // Never unwind out of here: this frame is owned by the driver.
80 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
81 if data.is_null() || user_data.is_null() {
82 return;
83 }
84 let data = unsafe { &*data };
85 let log = unsafe { &*(user_data as *const ValidationLog) };
86
87 let severity = if severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::ERROR) {
88 ValidationSeverity::Error
89 } else if severity.contains(vk::DebugUtilsMessageSeverityFlagsEXT::WARNING) {
90 ValidationSeverity::Warning
91 } else {
92 ValidationSeverity::Info
93 };
94
95 let cstr_or_empty = |p: *const std::ffi::c_char| {
96 if p.is_null() {
97 String::new()
98 } else {
99 unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()
100 }
101 };
102
103 log.record(ValidationMessage {
104 severity,
105 id: cstr_or_empty(data.p_message_id_name),
106 message: cstr_or_empty(data.p_message),
107 });
108 }));
109 let _ = result;
110
111 // False means "do not abort the call that triggered this". The layer is a
112 // reporting mechanism here, not a policy one; tests decide what is fatal.
113 vk::FALSE
114}
115
116/// Severities and types worth subscribing to.
117pub(crate) fn messenger_create_info<'a>() -> vk::DebugUtilsMessengerCreateInfoEXT<'a> {
118 vk::DebugUtilsMessengerCreateInfoEXT::default()
119 .message_severity(
120 vk::DebugUtilsMessageSeverityFlagsEXT::ERROR
121 | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING,
122 )
123 .message_type(
124 vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
125 | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
126 | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE,
127 )
128 .pfn_user_callback(Some(debug_callback))
129}
130
131/// A context that asserts the layer reported nothing when it goes out of scope.
132///
133/// The other half of what this module is for. Capturing the messenger makes
134/// validation assertable; this makes it asserted, which is not the same thing:
135/// a test that turns the layer on and never reads what it said is
136/// indistinguishable from one that left it off, and a check written out in
137/// every test is one some test will be missing. That is not hypothetical --
138/// the layer was on for this crate's own tests and off for the whole shared
139/// harness, which is the largest body of rendering in the suite.
140///
141/// Dropping is the one moment that happens exactly once per context however
142/// the test around it is written, so that is where the check goes.
143///
144/// Derefs to the context, so code that takes a [`crate::VulkanContext`] is unchanged.
145pub struct Validated(std::mem::ManuallyDrop<crate::VulkanContext>);
146
147impl Validated {
148 /// A context with the layer requested, or whatever went wrong instead.
149 pub fn new(device: crate::DevicePreference) -> impeller_hal::Result<Self> {
150 crate::VulkanContext::with_config(crate::ContextConfig {
151 device,
152 validation: true,
153 })
154 .map(|ctx| Self(std::mem::ManuallyDrop::new(ctx)))
155 }
156}
157
158impl std::ops::Deref for Validated {
159 type Target = crate::VulkanContext;
160
161 fn deref(&self) -> &crate::VulkanContext {
162 &self.0
163 }
164}
165
166impl std::ops::DerefMut for Validated {
167 fn deref_mut(&mut self) -> &mut crate::VulkanContext {
168 &mut self.0
169 }
170}
171
172impl Drop for Validated {
173 fn drop(&mut self) {
174 // The context is dropped here rather than after this body, which is
175 // what the `ManuallyDrop` is for and the whole reason this is written
176 // the awkward way. A field's drop runs after its owner's, so checking
177 // the log from here in the ordinary arrangement checks everything the
178 // context did *except* its own teardown -- and a Vulkan object
179 // outliving its device is reported at `vkDestroyDevice`, inside that
180 // teardown. That gap is not hypothetical: a descriptor set layout
181 // leaked on every device in this workspace for as long as the material
182 // set has existed, with every validated test green.
183 //
184 // The log is behind an `Arc`, so a handle taken before the context goes
185 // is still readable after it.
186 let panicking = std::thread::panicking();
187 let active = self.0.validation_active();
188 let log = self.0.validation_log();
189 // SAFETY: this is the only place the context is dropped, `Drop::drop`
190 // runs once, and nothing reads the field afterward.
191 unsafe { std::mem::ManuallyDrop::drop(&mut self.0) };
192
193 // A test that is already failing keeps its own message: panicking here
194 // while the first panic unwinds aborts the process and loses it.
195 if panicking {
196 return;
197 }
198 if !active {
199 // Said rather than passed over. The layer being absent means the
200 // work ran with nothing checking its API use, which is a gap in
201 // what the run covered even where every pixel matched.
202 eprintln!("skipping: the validation layer is unavailable, so API use went unchecked");
203 return;
204 }
205 let errors = log.errors();
206 assert!(errors.is_empty(), "validation errors: {errors:?}");
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn a_log_with_only_warnings_is_still_clean() {
216 let log = ValidationLog::default();
217 log.record(ValidationMessage {
218 severity: ValidationSeverity::Warning,
219 id: "test".into(),
220 message: "a warning".into(),
221 });
222 // Warnings are recorded for inspection but do not fail a run: drivers
223 // emit performance advice that is not a correctness problem.
224 assert!(log.is_clean());
225 assert_eq!(log.messages().len(), 1);
226 assert!(log.errors().is_empty());
227 }
228
229 #[test]
230 fn errors_are_separated_from_the_rest() {
231 let log = ValidationLog::default();
232 for severity in [
233 ValidationSeverity::Info,
234 ValidationSeverity::Warning,
235 ValidationSeverity::Error,
236 ] {
237 log.record(ValidationMessage {
238 severity,
239 id: "id".into(),
240 message: format!("{severity:?}"),
241 });
242 }
243 assert!(!log.is_clean());
244 assert_eq!(log.errors().len(), 1);
245 assert_eq!(log.errors()[0].severity, ValidationSeverity::Error);
246 assert_eq!(log.messages().len(), 3);
247 }
248}