diff options
| -rw-r--r-- | src/main.rs | 202 |
1 files changed, 181 insertions, 21 deletions
diff --git a/src/main.rs b/src/main.rs index 5bdce3b..05c8cd5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ #![deny(unsafe_code)] use std::cell::OnceCell; -use std::collections::HashSet; +use std::collections::{ BTreeMap, HashSet }; use std::ffi::{ c_void, CStr }; use vulkanalia::{ Entry, Instance, Version }; use vulkanalia::loader::{ LibloadingLoader, LIBRARY }; @@ -100,14 +100,36 @@ fn ignore_errors(mut body: impl FnMut() -> Result<()>) -> () { const VULKAN_FIRST_PORTABILITY_VERSION: Version = Version::new(1, 3, 216); +enum Acceptable<T> { + Accepted(T), + Rejected(String), +} + + struct Surreality { + // The "window" is the usual operating-system concept of a window; it's + // provided by winit, and may be X11, Wayland, or some more curious thing. + // The way we initialize Vulkan requires us to have at least one of these; + // we could have more, but for now, we don't. window: OnceCell<Window>, + + // The Vulkan "entry" is the part of the Vulkan library ecosystem that's + // responsible for finding and loading the other parts. entry: OnceCell<Entry>, + + // The Vulkan "instance" is the bulk of the Vulkan library, with most of + // the high-level responsibilities around lifecycle management. instance: OnceCell<Instance>, + // The debug messager is a Vulkan object representing our callback which + // Vulkan uses to tell us things. + // // Vulkan spells "messager" as "messenger", but this is absurd // over-formality and we don't indulge it. debug_messager: OnceCell<vk::DebugUtilsMessengerEXT>, + + // The Vulkan "device" is the abstraction for a GPU. + device: OnceCell<vk::PhysicalDevice>, } impl Surreality { @@ -117,6 +139,7 @@ impl Surreality { entry: OnceCell::new(), instance: OnceCell::new(), debug_messager: OnceCell::new(), + device: OnceCell::new(), } } @@ -133,6 +156,10 @@ impl Surreality { self.init_vulkan_instance()?; } + if self.device.get().is_none() { + self.init_vulkan_device()?; + } + Ok(()) } @@ -151,6 +178,7 @@ impl Surreality { Ok(()) } + #[allow(unsafe_code)] fn init_vulkan_entry(&mut self) -> Result<()> { // Okay, so, a Vulkan "entry" is a small set of functions which are used // to dynamically load all the rest of Vulkan. It's our responsibility to @@ -161,9 +189,7 @@ impl Surreality { // Anyway, Vulkanalia offers an integration with libloading, which is a // crate that wraps POSIX dlopen(). We use that; it's enabled by // Vulkanalia's "libloading" feature. - #[allow(unsafe_code)] let loader = unsafe { LibloadingLoader::new(LIBRARY) }?; - #[allow(unsafe_code)] let entry = unsafe { Entry::new(loader) }?; let _ = self.entry.set(entry); @@ -171,6 +197,7 @@ impl Surreality { Ok(()) } + #[allow(unsafe_code)] fn init_vulkan_instance(&mut self) -> Result<()> { let entry = self.entry.get().unwrap(); @@ -186,7 +213,6 @@ impl Surreality { // Before we go any further, use Vulkan's introspection to list off // what's available. let mut available_extensions = HashSet::new(); - #[allow(unsafe_code)] for extension in unsafe { entry.enumerate_instance_extension_properties(None) }? { @@ -195,7 +221,6 @@ impl Surreality { let available_extensions = available_extensions; let mut available_layers = HashSet::new(); - #[allow(unsafe_code)] for layer in unsafe { entry.enumerate_instance_layer_properties() }? { available_layers.insert(layer.layer_name); } @@ -287,7 +312,6 @@ impl Surreality { Some(debug_info) } else { None }; - #[allow(unsafe_code)] let instance = unsafe { // We're promising that every struct referenced here is still alive. // Since it's all pointers, that's... not a thing we statically know. Be @@ -314,6 +338,120 @@ impl Surreality { Ok(()) } + fn init_vulkan_device(&mut self) -> Result<()> { + let physical_device = self.pick_vulkan_device()?; + + Ok(()) + } + + // To Vulkan, a "physical" device is the actual GPU, and a "logical" + // device is per-process state that represents a connection to the GPU. + // Before we can create a logical device, we must choose which physical + // device to connect it to. + #[allow(unsafe_code)] + fn pick_vulkan_device(&mut self) -> Result<vk::PhysicalDevice> { + let mut best_device = None; + let mut best_score = None; + let mut rejected = BTreeMap::new(); + + for device in unsafe { self.instance.get().unwrap().enumerate_physical_devices() }? { + match self.score_vulkan_device(&device)? { + Acceptable::Accepted(new_score) => { + if let Some(old_score) = best_score { + if new_score > old_score { + best_device = Some(device); + best_score = Some(new_score); + } + } else { + best_device = Some(device); + best_score = Some(new_score); + } + } + Acceptable::Rejected(reason) => { + let properties = unsafe { + self.instance.get().unwrap().get_physical_device_properties(device) + }; + + let name = properties.device_name.to_string_lossy().to_string(); + + rejected.insert(properties.device_id, (name, reason)); + } + } + } + + if let Some(device) = best_device { + Ok(device) + } else if rejected.is_empty() { + Err(Error { + message: "The system has no GPUs of any kind.".to_string() + }) + } else { + for (_, (name, reason)) in rejected { + eprintln!("Can't run on {} because: {}", name, reason); + } + + Err(Error { + message: "The system has GPUs, but none are acceptable (see above)." + .to_string() + }) + } + } + + // We're doing two tasks: Quantifying how strongly we prefer a device, and + // deciding whether it's acceptable at all. If it's unacceptable, it's + // possible there will be no acceptable devices, and in that case our caller + // will want to print explanations, but otherwise it'll want to be quiet. So + // the outer Result is whether we successfully evaluated the device, and the + // inner Acceptable is whether we approve of it. + #[allow(unsafe_code)] + fn score_vulkan_device(&mut self, device: &vk::PhysicalDevice) + -> Result<Acceptable<u64>> + { + let instance = self.instance.get().unwrap(); + + // Not all devices support graphics. We find out by checking whether any + // of their queue families do. + let mut has_graphics = false; + for queue in unsafe { + instance.get_physical_device_queue_family_properties(*device) + } { + if queue.queue_flags.contains(vk::QueueFlags::GRAPHICS) { + has_graphics = true; + } + } + if !has_graphics { + return Ok(Acceptable::Rejected( + "Doesn't support graphics.".to_string()));; + } + + // At this point we know the device meets our high-level requirements, + // so it's just a question of scoring. + let properties = unsafe { + instance.get_physical_device_properties(*device) + }; + + let mut score = 0; + if properties.device_type == vk::PhysicalDeviceType::DISCRETE_GPU { + // If the user has a fancy GPU, they prefer it. + score += 128; + } else if properties.device_type + == vk::PhysicalDeviceType::INTEGRATED_GPU + { + // It's still hardware rendering. + score += 96; + } else if properties.device_type == vk::PhysicalDeviceType::VIRTUAL_GPU { + // Whatever it is, the user went to some trouble to set it up. + score += 64; + } else if properties.device_type == vk::PhysicalDeviceType::CPU { + // Software rendering is slow, but at least it's a known quantity. + score += 32; + } + // If it's none of those, we don't have enough information to know if + // that's good or bad, so we assume it's bad. + + Ok(Acceptable::Accepted(score)) + } + fn render(&mut self, window_id: WindowId) -> Result<()> { if let Some(window) = self.window.get() && window_id == window.id() @@ -328,18 +466,17 @@ impl Surreality { } impl Drop for Surreality { + #[allow(unsafe_code)] fn drop(&mut self) { if let Some(debug_messager) = self.debug_messager.get() && let Some(instance) = self.instance.get() { - #[allow(unsafe_code)] unsafe { instance.destroy_debug_utils_messenger_ext(*debug_messager, None); } } if let Some(instance) = self.instance.get() { - #[allow(unsafe_code)] unsafe { instance.destroy_instance(None) }; } } @@ -394,28 +531,51 @@ fn main() -> std::process::ExitCode { } +#[allow(unsafe_code)] extern "system" fn debug_messager_callback( severity: vk::DebugUtilsMessageSeverityFlagsEXT, flags: vk::DebugUtilsMessageTypeFlagsEXT, data: *const vk::DebugUtilsMessengerCallbackDataEXT, _context: *mut c_void) -> vk::Bool32 { - #[allow(unsafe_code)] - let data = unsafe { *data }; - #[allow(unsafe_code)] - let text = unsafe { CStr::from_ptr(data.message) }.to_string_lossy(); - - let severity = if severity >= vk::DebugUtilsMessageSeverityFlagsEXT::ERROR { - "error" - } else if severity >= vk::DebugUtilsMessageSeverityFlagsEXT::WARNING { - "warning" - } else if severity >= vk::DebugUtilsMessageSeverityFlagsEXT::INFO { - "informational message" + // Vulkan sends us everything, it's up to us to apply any filtering we + // want. The thing about this is that games need to be debuggable by end + // users, to diagnose compatibility issues and weird configurations, so we + // still want SOMETHING even when we're built in release mode. + // + // For now, we'll see if we can get away without providing runtime config + // stuff for diagnostics. We set the threshold pretty high in release mode, + // on the theory that our own diagnostics should be sufficient. + // + // Making this strategy work does rely on us actually checking error + // conditions and reporting them in useful ways, so that we only need + // Vulkan's messages for things we truly couldn't have anticipated. We do + // not take a more-is-better approach to diagnostics; the ideal would be to + // provide all the crucial information, and nothing else. + let threshold = if cfg!(feature = "vulkan-validation") + || cfg!(debug_assertions) + { + vk::DebugUtilsMessageSeverityFlagsEXT::empty() } else { - "message of unknown, very minor severity" + vk::DebugUtilsMessageSeverityFlagsEXT::WARNING }; - eprintln!("Vulkan {}: {} (flags {:?})", severity, text, flags); + if severity >= threshold { + let data = unsafe { *data }; + let text = unsafe { CStr::from_ptr(data.message) }.to_string_lossy(); + + let label = if severity >= vk::DebugUtilsMessageSeverityFlagsEXT::ERROR { + "error" + } else if severity >= vk::DebugUtilsMessageSeverityFlagsEXT::WARNING { + "warning" + } else if severity >= vk::DebugUtilsMessageSeverityFlagsEXT::INFO { + "informational message" + } else { + "message of unknown, very minor significance" + }; + + eprintln!("Vulkan {}: {} (flags {:?})", label, text, flags); + } // A return value of true would tell the validation layer we're unhappy // with it, for the sake of conformance testing. We're not a conformance |