Skip to main content

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}