Skip to main content

max / everycycle

7.5 KB · 206 lines History Blame Raw
1 //! Shared Vulkan setup for probes: instance, physical device,
2 //! logical device, compute queue, command pool.
3 //!
4 //! A `VulkanContext` is created once per appraisal run, against one
5 //! specific physical device identified by its PCI bus address. Probes
6 //! borrow it to record command buffers and submit work; resource
7 //! cleanup happens when the context is dropped.
8
9 use std::ffi::{CStr, c_char};
10
11 use ash::{Entry, Instance, vk};
12
13 const APP_NAME: &CStr = c"everycycle-appraise";
14
15 #[derive(Debug)]
16 pub enum VulkanContextError {
17 Load(ash::LoadingError),
18 Vulkan(vk::Result),
19 NoMatchingDevice(String),
20 NoComputeQueueFamily,
21 }
22
23 impl core::fmt::Display for VulkanContextError {
24 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
25 match self {
26 Self::Load(e) => write!(f, "could not load Vulkan loader: {e:?}"),
27 Self::Vulkan(r) => write!(f, "Vulkan call failed: {r:?}"),
28 Self::NoMatchingDevice(addr) => {
29 write!(f, "no physical device at bus address {addr}")
30 }
31 Self::NoComputeQueueFamily => {
32 write!(f, "no queue family on the selected device supports COMPUTE")
33 }
34 }
35 }
36 }
37
38 impl std::error::Error for VulkanContextError {}
39
40 pub struct VulkanContext {
41 pub queue_family_index: u32,
42 pub queue: vk::Queue,
43 pub command_pool: vk::CommandPool,
44 pub physical_device: vk::PhysicalDevice,
45 pub memory_properties: vk::PhysicalDeviceMemoryProperties,
46
47 /// Nanoseconds per timestamp-query tick on this device.
48 pub timestamp_period_ns: f32,
49 /// Meaningful low-order bits in a timestamp query on the chosen
50 /// queue family. Zero means the queue cannot write timestamps, and
51 /// probes must fall back to host timing.
52 pub timestamp_valid_bits: u32,
53
54 // Held for cleanup. Dropped in reverse declaration order.
55 pub device: ash::Device,
56 pub instance: Instance,
57 _entry: Entry,
58 }
59
60 impl VulkanContext {
61 /// Pick the physical device whose PCI bus address matches `target`
62 /// and bring up the logical device, compute queue, and command pool
63 /// against it.
64 pub fn new(target_bus_address: &str) -> Result<Self, VulkanContextError> {
65 let entry = unsafe { Entry::load() }.map_err(VulkanContextError::Load)?;
66 let instance = create_instance(&entry)?;
67
68 let physical = pick_device(&instance, target_bus_address)?;
69 let memory_properties = unsafe { instance.get_physical_device_memory_properties(physical) };
70
71 let queue_family_index = pick_compute_queue_family(&instance, physical)?;
72
73 let timestamp_period_ns = unsafe { instance.get_physical_device_properties(physical) }
74 .limits
75 .timestamp_period;
76 let timestamp_valid_bits =
77 unsafe { instance.get_physical_device_queue_family_properties(physical) }
78 .get(queue_family_index as usize)
79 .map_or(0, |fam| fam.timestamp_valid_bits);
80
81 let priorities = [1.0_f32];
82 let queue_info = vk::DeviceQueueCreateInfo::default()
83 .queue_family_index(queue_family_index)
84 .queue_priorities(&priorities);
85 let device_info =
86 vk::DeviceCreateInfo::default().queue_create_infos(std::slice::from_ref(&queue_info));
87 let device = unsafe { instance.create_device(physical, &device_info, None) }
88 .map_err(VulkanContextError::Vulkan)?;
89
90 let queue = unsafe { device.get_device_queue(queue_family_index, 0) };
91
92 let pool_info = vk::CommandPoolCreateInfo::default()
93 .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER)
94 .queue_family_index(queue_family_index);
95 let command_pool = unsafe { device.create_command_pool(&pool_info, None) }
96 .map_err(VulkanContextError::Vulkan)?;
97
98 Ok(Self {
99 queue_family_index,
100 queue,
101 command_pool,
102 physical_device: physical,
103 memory_properties,
104 timestamp_period_ns,
105 timestamp_valid_bits,
106 device,
107 instance,
108 _entry: entry,
109 })
110 }
111
112 /// Find a memory type index matching `required_flags` from the
113 /// device's memory-properties table and a bitmask of acceptable
114 /// types (`type_filter`, as returned by `MemoryRequirements`).
115 #[must_use]
116 pub fn find_memory_type(
117 &self,
118 type_filter: u32,
119 required_flags: vk::MemoryPropertyFlags,
120 ) -> Option<u32> {
121 for i in 0..self.memory_properties.memory_type_count {
122 let supported = (type_filter & (1 << i)) != 0;
123 let mt = self.memory_properties.memory_types[i as usize];
124 if supported && mt.property_flags.contains(required_flags) {
125 return Some(i);
126 }
127 }
128 None
129 }
130 }
131
132 impl Drop for VulkanContext {
133 fn drop(&mut self) {
134 unsafe {
135 // Wait for any outstanding work before tearing things down.
136 // Errors at drop time can't be returned; the destructor
137 // calls themselves are infallible.
138 let _ = self.device.device_wait_idle();
139 self.device.destroy_command_pool(self.command_pool, None);
140 self.device.destroy_device(None);
141 self.instance.destroy_instance(None);
142 }
143 }
144 }
145
146 fn create_instance(entry: &Entry) -> Result<Instance, VulkanContextError> {
147 let app_info = vk::ApplicationInfo::default()
148 .application_name(APP_NAME)
149 .engine_name(APP_NAME)
150 .api_version(vk::API_VERSION_1_3);
151 let create_info = vk::InstanceCreateInfo::default().application_info(&app_info);
152 unsafe { entry.create_instance(&create_info, None) }.map_err(VulkanContextError::Vulkan)
153 }
154
155 fn pick_device(
156 instance: &Instance,
157 target: &str,
158 ) -> Result<vk::PhysicalDevice, VulkanContextError> {
159 let devices =
160 unsafe { instance.enumerate_physical_devices() }.map_err(VulkanContextError::Vulkan)?;
161 for dev in devices {
162 if device_bus_address(instance, dev).as_deref() == Some(target) {
163 return Ok(dev);
164 }
165 }
166 Err(VulkanContextError::NoMatchingDevice(target.to_string()))
167 }
168
169 fn device_bus_address(instance: &Instance, dev: vk::PhysicalDevice) -> Option<String> {
170 if !device_supports_extension(instance, dev, ash::ext::pci_bus_info::NAME) {
171 return None;
172 }
173 let mut pci = vk::PhysicalDevicePCIBusInfoPropertiesEXT::default();
174 let mut props2 = vk::PhysicalDeviceProperties2::default().push_next(&mut pci);
175 unsafe { instance.get_physical_device_properties2(dev, &mut props2) };
176 Some(format!(
177 "{:04x}:{:02x}:{:02x}.{:x}",
178 pci.pci_domain, pci.pci_bus, pci.pci_device, pci.pci_function
179 ))
180 }
181
182 fn device_supports_extension(instance: &Instance, dev: vk::PhysicalDevice, want: &CStr) -> bool {
183 let Ok(exts) = (unsafe { instance.enumerate_device_extension_properties(dev) }) else {
184 return false;
185 };
186 exts.iter().any(|ext| {
187 let raw = ext.extension_name.as_ptr().cast::<c_char>();
188 let name = unsafe { CStr::from_ptr(raw) };
189 name == want
190 })
191 }
192
193 fn pick_compute_queue_family(
194 instance: &Instance,
195 physical: vk::PhysicalDevice,
196 ) -> Result<u32, VulkanContextError> {
197 let families = unsafe { instance.get_physical_device_queue_family_properties(physical) };
198 for (i, fam) in families.iter().enumerate() {
199 if fam.queue_flags.contains(vk::QueueFlags::COMPUTE) {
200 #[allow(clippy::cast_possible_truncation)]
201 return Ok(i as u32);
202 }
203 }
204 Err(VulkanContextError::NoComputeQueueFamily)
205 }
206