diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/error.rs | 33 | ||||
| -rw-r--r-- | src/graphics_permanent.rs | 728 | ||||
| -rw-r--r-- | src/main.rs | 759 |
3 files changed, 768 insertions, 752 deletions
diff --git a/src/error.rs b/src/error.rs index f3f1ca9..f034683 100644 --- a/src/error.rs +++ b/src/error.rs @@ -96,3 +96,36 @@ pub fn ignore_errors(mut body: impl FnMut() -> Result<()>) -> () { } } + +pub enum Acceptable<T> { + Accepted(T), + Rejected(String), +} + +impl<T> Acceptable<T> { + #[allow(unused)] + pub fn is_accepted(&self) -> bool { + if let Acceptable::Accepted(_) = self { true } else { false } + } + + #[allow(unused)] + pub fn is_rejected(&self) -> bool { + if let Acceptable::Rejected(_) = self { true } else { false } + } + + #[allow(unused)] + pub fn unwrap(self) -> T { + if let Acceptable::Accepted(result) = self { + result + } else { + panic!("Unwrapped a rejected Acceptable."); + } + } + + pub fn require(self) -> Result<T> { + match self { + Acceptable::Accepted(value) => Ok(value), + Acceptable::Rejected(message) => Err(Error { message }), + } + } +} diff --git a/src/graphics_permanent.rs b/src/graphics_permanent.rs new file mode 100644 index 0000000..a4ece9d --- /dev/null +++ b/src/graphics_permanent.rs @@ -0,0 +1,728 @@ +#![deny(unsafe_code)] +use crate::error::*; + +use std::collections::{ BTreeMap, BTreeSet, HashSet }; +use std::ffi::{ c_void, CStr }; +use vulkanalia::{ Device, Entry, Instance, Version }; +use vulkanalia::loader::{ LibloadingLoader, LIBRARY }; +use vulkanalia::vk::{ self, HasBuilder, + ApplicationInfo, InstanceCreateInfo, + DeviceV1_0, EntryV1_0, InstanceV1_0, + ExtDebugUtilsExtensionInstanceCommands, + KhrSurfaceExtensionInstanceCommands }; +use winit::dpi::LogicalSize; +use winit::event_loop::ActiveEventLoop; +use winit::window::{ Window, WindowAttributes }; + + +const VULKAN_FIRST_PORTABILITY_VERSION: Version = Version::new(1, 3, 216); + + +// The PermanentGraphicsState collects the various windowing-system and +// Vulkan objects which never need to be regenerated once they're created. The +// ones which do need that are collected below, in WindowDressing. +pub struct PermanentGraphicsState { + // 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. + pub window: Window, + + // The Vulkan "entry" is the part of the Vulkan library ecosystem that's + // responsible for finding and loading the other parts. Once we have the + // instance, the entry is never directly used again, but we retain it + // because doing otherwise would segfault. + #[allow(unused)] + entry: Entry, + + // The Vulkan "instance" is the bulk of the Vulkan library, with most of + // the high-level responsibilities around lifecycle management. + pub instance: 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. + // + // Once we've created this, we never actually need to do anything with + // it, but we do need to retain it, so here it is. + debug_messager: Option<vk::DebugUtilsMessengerEXT>, + + // The Vulkan "surface" is the destination that rendering happens into. + // It is connected to the window but distinct from it. Since we always have + // exactly one window, this is permanent state. + pub surface: vk::SurfaceKHR, + + // The Vulkan "device" is the abstraction for a GPU. A physical one is the + // actual GPU, and a logical one is our connection to it. We pick a physical + // device during initialization, but only the logical one is used later, so + // it's all we track. We'll be referencing the logical device a lot, so we + // follow Vulkan's lead and let it have a short variable name. + pub device: Device, + + // Vulkan has a first-class concept of command queues. We have two of + // them, one for graphics drawing commands and one for presentation. + // + // While these are often the same queue, there is no guarantee of that; + // sometimes there's no queue family that supports both operations together. + // For simplicity's sake we treat them as if they're separate, though the + // handles will alias each other when the initialization logic was able to + // find a family that does both. + // + // Yes, this means the compiler has to deal with pointer aliasing + // concerns, which have a tendency to defeat optimizations. Alas. + pub graphics_queue: vk::Queue, + pub presentation_queue: vk::Queue, +} + + +#[derive(Debug)] +pub struct QueueFamilyIndices { + pub graphics: u32, + pub presentation: u32, +} + +// These structs exist for use in function calling, to remove the potential +// for accidentally passing or returning one boolean as if it's another. +struct EnablePortability(bool); +struct EnableValidation(bool); +pub struct EnableSwapchain(pub bool); + + +impl PermanentGraphicsState { + #[allow(unsafe_code)] + pub fn new(event_loop: &ActiveEventLoop) + -> Result<(Self, vk::PhysicalDevice, QueueFamilyIndices, + EnableSwapchain)> + { + let window = Self::init_window(event_loop)?; + + // There are a few Vulkan features (in the informal sense of + // "feature") that we want to be able to run both with and without. + // The enable_* values, here and below, are wrapped booleans that describe + // those choices. + // + // These are only used to communicate between initialization + // phases; we don't keep them around after that. + let (entry, instance, debug_messager, + enable_portability, enable_validation) + = Self::init_vulkan(&window)?; + + // Conveniently, Vulkanalia's "window" feature allows it to get the + // platform-specific stuff directly out of winit for us. This wrapper does + // not correspond 1:1 to a Vulkan function; rather, it picks the Vulkan + // function from the appropriate platform-specific extension. + // + // The reason it takes the window twice is that that first one is + // actually there to reference the display (in the x11 sense of "display" + // meaning the connection to the windowing system). + let surface = unsafe { + vulkanalia::window::create_surface(&instance, &window, &window) + }?; + + let (physical_device, device, + indices, graphics_queue, presentation_queue, + enable_swapchain) + = Self::init_vulkan_device(&instance, &surface, + enable_validation, enable_portability)?; + + Ok((PermanentGraphicsState { + window, entry, instance, debug_messager, surface, device, + graphics_queue, presentation_queue, + }, physical_device, indices, enable_swapchain)) + } + + #[allow(unsafe_code)] + pub fn destroy(self) -> () { + unsafe { self.device.destroy_device(None) }; + + unsafe { self.instance.destroy_surface_khr(self.surface, None) }; + + // Everything but the instance itself should already be destroyed, + // before we destroy the debug messager. The special hook to get debug + // messages while destroying the instance itself only applies to the + // instance and the messager, so if we were to destroy anything we + // shouldn't after this point, we'd miss out on diagnostics. + if let Some(debug_messager) = self.debug_messager { + unsafe { + self.instance.destroy_debug_utils_messenger_ext(debug_messager, None); + } + } + + unsafe { self.instance.destroy_instance(None) }; + } + + fn init_window(event_loop: &ActiveEventLoop) -> Result<Window> { + // Notice that we do this before having a Vulkan instance. The window is + // actually a parameter needed to create the instance; see + // init_vulkan(), below. + let window_attributes = WindowAttributes::default() + .with_title("Love, Curiosity, Justice") + .with_inner_size(LogicalSize::new(1024, 768)); + + Ok(event_loop.create_window(window_attributes)?) + } + + #[allow(unsafe_code)] + fn init_vulkan(window: &Window) + -> Result<(Entry, Instance, Option<vk::DebugUtilsMessengerEXT>, + EnablePortability, EnableValidation)> + { + let enable_validation = cfg!(feature = "vulkan-validation") + || cfg!(debug_assertions); + + // 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 + // know how to load the entry, then it will take care of the rest. At + // least, that's the theory, but also see flake.nix for all the + // FHS-centric assumptions it makes that we have to correct. + // + // 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. + let loader = unsafe { LibloadingLoader::new(LIBRARY) }?; + let entry = unsafe { Entry::new(loader) }?; + + // Since there's a lot of factors going into our instance creation + // request, we'll build up the parameters mutably. + let mut flags = vk::InstanceCreateFlags::empty(); + let mut extensions = Vec::new(); + let mut layers = Vec::new(); + + // Before we go any further, use Vulkan's introspection to list off + // what's available. + let mut available_extensions = HashSet::new(); + for extension in + unsafe { entry.enumerate_instance_extension_properties(None) }? + { + available_extensions.insert(extension.extension_name); + } + let available_extensions = available_extensions; + + let mut available_layers = HashSet::new(); + for layer in unsafe { entry.enumerate_instance_layer_properties() }? { + available_layers.insert(layer.layer_name); + } + let available_layers = available_layers; + + // There are certain extensions which are required by the nature of our + // windowing system. Happily, vulanaklia knows how to deal with that based + // on the type of window we give it. + // + // This is possible because of an integration between Vulkanalia and + // winit, which is enabled by Vulkanalia's "window" feature. + for extension in vulkanalia::window::get_required_instance_extensions( + window) + { + extensions.push(extension.as_ptr()); + } + + // Deal with Vulkan's thing about opting in to non-conforming + // implementations. + let enable_portability = if entry.version()? + >= VULKAN_FIRST_PORTABILITY_VERSION + { + if cfg!(target_os = "macos") { + // Vulkan on the Mac is not fully conforming. + extensions.push( + vk::KHR_GET_PHYSICAL_DEVICE_PROPERTIES2_EXTENSION.name.as_ptr()); + extensions.push( + vk::KHR_PORTABILITY_ENUMERATION_EXTENSION.name.as_ptr()); + flags.insert(vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR); + + EnablePortability(true) + } else { + EnablePortability(false) + } + } else { + EnablePortability(false) + }; + + // Request the LunarG validation layer, when appropriate. + let validation_layer_name = vk::ExtensionName::from_bytes( + b"VK_LAYER_KHRONOS_validation"); + let enable_validation = if enable_validation { + if available_layers.contains(&validation_layer_name) { + layers.push(validation_layer_name.as_ptr()); + + EnableValidation(true) + } else { + eprintln!("Vulkan validation requested at build time, \ + but no validation layer available."); + + EnableValidation(false) + } + } else { + EnableValidation(false) + }; + + // Request the debug extension. This is the first of three bits of code + // that deal with this, and has the resonsibility of making sure the + // extension is in the list we ask for. + let debug_extension_name = vk::EXT_DEBUG_UTILS_EXTENSION.name; + if available_extensions.contains(&debug_extension_name) { + extensions.push(debug_extension_name.as_ptr()); + } else { + eprintln!("Vulkan debug extension not available; \ + this may mean other messages don't show up."); + } + + let application_info = ApplicationInfo::builder() + .application_name(b"Surreality\0") + .application_version(vk::make_version(1, 0, 0)) + .engine_name(b"Surreality\0") + .engine_version(vk::make_version(1, 0, 0)) + .api_version(vk::make_version(1, 0, 0)); + + // Deceptively, this DOES get mutated later, but Vulkanalia doesn't see + // it that way. + let instance_create_info = InstanceCreateInfo::builder() + .application_info(&application_info) + .flags(flags) + .enabled_extension_names(&extensions) + .enabled_layer_names(&layers); + + // Configure the debug extension. This is the middle of three bits of + // code that deal with this, and has the responsibility of making sure + // the callback will be available during instance creation and + // destruction, which is done in a special way that doesn't rely on having + // a messager, since there can't be one for those steps. + let debug_info = if available_extensions.contains(&debug_extension_name) { + let mut debug_info = vk::DebugUtilsMessengerCreateInfoEXT::builder() + .message_severity(vk::DebugUtilsMessageSeverityFlagsEXT::all()) + .message_type(vk::DebugUtilsMessageTypeFlagsEXT::GENERAL + | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION + | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE) + .user_callback(Some(debug_messager_callback)); + + // Please notice that the reference we pass here will escape Rust's + // lifetime checking, since push_next() casts it to a pointer. We don't + // get nearly as strong a safety guarantee as one might hope (and as [1] + // naively reassures us we do). If we did, the thing we're doing would + // actually be forbidden! + // + // [1] https://kylemayes.github.io/vulkanalia/ + instance_create_info.push_next(&mut debug_info); + + Some(debug_info) + } else { None }; + + 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 + // aware. Only you can prevent segfaults. + entry.create_instance(&instance_create_info, None) + }?; + + // Configure the debug extension. This is the last of three bits of code + // that deal with this, and has the responsibility of asking the instance, + // which now exists, to create the debug messager. + let debug_messager = if let Some(debug_info) = debug_info { + #[allow(unsafe_code)] + Some(unsafe { + instance.create_debug_utils_messenger_ext(&debug_info, None) + }?) + } else { + None + }; + + Ok((entry, instance, debug_messager, + enable_portability, enable_validation)) + } + + #[allow(unsafe_code)] + fn init_vulkan_device(instance: &Instance, surface: &vk::SurfaceKHR, + enable_validation: EnableValidation, + enable_portability: EnablePortability) + -> Result<(vk::PhysicalDevice, Device, QueueFamilyIndices, vk::Queue, + vk::Queue, EnableSwapchain)> + { + let (physical_device, indices) + = Self::pick_vulkan_device(instance, surface)?; + + // We enumerate the device extensions here so they can inform + // configuration. We already did this in score_vulkan_device(), but here + // it is again. + let mut available_extensions = HashSet::new(); + for extension in unsafe { + instance.enumerate_device_extension_properties(physical_device, None) + }? { + available_extensions.insert(extension.extension_name); + } + let available_extensions = available_extensions; + + // Old versions of Vulkan want layers to be enabled at the device + // level as well. Newer ones will ignore this and just use the instance + // layers. + let features = vk::PhysicalDeviceFeatures::builder(); + let mut extensions = Vec::new(); + let mut layers = Vec::new(); + + let validation_layer_name = vk::ExtensionName::from_bytes( + b"VK_LAYER_KHRONOS_validation"); + if enable_validation.0 { + // It's not concerning if this isn't supported, because device + // layers are ignored on recent versions, they're purely historical. + if available_extensions.contains(&validation_layer_name) { + layers.push(validation_layer_name.as_ptr()); + } + } + + let portability_extension_name = vk::ExtensionName::from_bytes( + b"VK_KHR_portability_subset"); + if enable_portability.0 { + // This is untested, since the only scenario where it would come up + // is on a Mac, which we don't actually support. Sorry, and good luck. + if available_extensions.contains(&portability_extension_name) { + extensions.push(portability_extension_name.as_ptr()); + } + } + + let swapchain_extension_name = vk::KHR_SWAPCHAIN_EXTENSION.name; + let enable_swapchain = if available_extensions.contains( + &swapchain_extension_name) + { + // It's important that we not call the swapchain extension + // functions until we've verified the extension is supported. To + // emphasize that, we do it on a separate line. + // + // We've done this check once already, in scoring, and now here + // we are discarding its results a second time. We'll do it for the + // third and last time in swapchain creation. + if let Acceptable::Accepted(_) + = PermanentGraphicsState::find_device_swapchain_features( + &instance, &surface, &physical_device)? + { + extensions.push(swapchain_extension_name.as_ptr()); + + EnableSwapchain(true) + } else { + EnableSwapchain(false) + } + } else { + EnableSwapchain(false) + }; + + // We have one or more queue family indices; we don't know a priori + // how many, because it's possible some of them are the same. We only + // want to create one queue per distinct family, so we find the unique + // indices... + let mut unique_queue_family_indices = BTreeSet::new(); + unique_queue_family_indices.insert(indices.graphics); + unique_queue_family_indices.insert(indices.presentation); + + // ... then add a queue create info struct for each. + let mut queues = Vec::new(); + for index in unique_queue_family_indices { + // Passing the priorities vector also implicitly sets the count of + // how many queues we are creating within the family. This nicety is + // one of the fun things Vulkanalia's builders do for us. + queues.push(vk::DeviceQueueCreateInfo::builder() + .queue_family_index(index) + .queue_priorities(&[1.0])); + } + + let device_info = vk::DeviceCreateInfo::builder() + .queue_create_infos(&queues) + .enabled_layer_names(&layers) + .enabled_extension_names(&extensions) + .enabled_features(&features); + + let device = unsafe { + instance.create_device(physical_device, &device_info, None) + }?; + + // So, this is a little confusing. Queues are found in queue families. + // The family has an index within the device, and the queue has an index + // within the family. We computed the family index above, and when we + // created the device we told it to create just a single queue in that + // family. Now we pass both indices to find the actual queue object. + let graphics_queue = unsafe { + device.get_device_queue(indices.graphics, 0) + }; + + let presentation_queue = unsafe { + device.get_device_queue(indices.presentation, 0) + }; + + Ok((physical_device, device, + indices, graphics_queue, presentation_queue, + enable_swapchain)) + } + + // 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(instance: &Instance, surface: &vk::SurfaceKHR) + -> Result<(vk::PhysicalDevice, QueueFamilyIndices)> + { + let mut best_device = None; + let mut best_score = None; + let mut best_indices = None; + let mut rejected = BTreeMap::new(); + + for device in unsafe { instance.enumerate_physical_devices() }? { + match Self::score_vulkan_device(instance, surface, &device)? { + Acceptable::Accepted((new_score, new_indices)) => { + if let Some(old_score) = best_score { + if new_score > old_score { + best_device = Some(device); + best_score = Some(new_score); + best_indices = Some(new_indices); + } + } else { + best_device = Some(device); + best_score = Some(new_score); + best_indices = Some(new_indices); + } + } + Acceptable::Rejected(reason) => { + let properties = unsafe { + instance.get_physical_device_properties(device) + }; + + let name = properties.device_name.to_string_lossy().into_owned(); + + rejected.insert(properties.device_id, (name, reason)); + } + } + } + + if let (Some(device), Some(indices)) = (best_device, best_indices) { + Ok((device, indices)) + } 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. + // + // In the event that we find the device acceptable, we also return the + // queue family indices we'd be using if we ultimately go with it. While + // this is not strictly necessary, it's better to return them from here + // than to recompute them later on the assumption it'll work out the same. + #[allow(unsafe_code)] + fn score_vulkan_device(instance: &Instance, surface: &vk::SurfaceKHR, + physical_device: &vk::PhysicalDevice) + -> Result<Acceptable<(u64, QueueFamilyIndices)>> + { + // Not all devices support graphics, and not all devices support + // presenting to any given surface. We check whether this one is suitable + // by looking up the indices of the queue families we would use. If we + // ultimately use this device, we'll need these, so we make sure to return + // them. + let indices = match Self::find_device_queue_family_indices( + instance, surface, physical_device)? + { + Acceptable::Rejected(rationale) => { + return Ok(Acceptable::Rejected(rationale)); + } + Acceptable::Accepted(indices) => indices + }; + + // 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(*physical_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. + + // Some of our scoring will depend on what extensions the device + // supports, so we enumerate those. + let mut available_extensions = HashSet::new(); + for extension in unsafe { + instance.enumerate_device_extension_properties(*physical_device, None) + }? { + available_extensions.insert(extension.extension_name); + } + let available_extensions = available_extensions; + + if available_extensions.contains(&vk::KHR_SWAPCHAIN_EXTENSION.name) { + // Double buffering is both quite a nice feature to have, and a good + // indicator that this is a "real" graphics card rather than some + // trivial weird thing. + // + // With that said, however, it only counts if we're able to actually + // use it on the surface we have. Let's find out... + if let Acceptable::Accepted(_) + = PermanentGraphicsState::find_device_swapchain_features( + instance, surface, physical_device)? + { + // We don't count it for enough points to override a device type + // bracket, but it's good for a lot within the bracket. + score += 16; + } + + // This isn't disqualifying, so we don't worry about tracking the + // rationale. We'll deal with that later, if the device actually gets + // selected. + } + + Ok(Acceptable::Accepted((score, indices))) + } + + #[allow(unsafe_code)] + fn find_device_queue_family_indices(instance: &Instance, + surface: &vk::SurfaceKHR, + device: &vk::PhysicalDevice) + -> Result<Acceptable<QueueFamilyIndices>> + { + // We need a queue family that supports graphics drawing commands, and a + // queue family that supports presentation commands. These may or may not + // be the same family. + let mut graphics = None; + let mut presentation = None; + + for (index, queue_family) in (unsafe { + instance.get_physical_device_queue_family_properties(*device) + }).into_iter().enumerate() { + if graphics.is_none() + && queue_family.queue_flags.contains(vk::QueueFlags::GRAPHICS) + { + graphics = Some(index as u32); + } + + if presentation.is_none() && unsafe { + instance.get_physical_device_surface_support_khr( + *device, index as u32, *surface) + }? { + presentation = Some(index as u32); + } + } + + if let Some(graphics) = graphics { + if let Some(presentation) = presentation { + Ok(Acceptable::Accepted(QueueFamilyIndices { + graphics, presentation + })) + } else { + Ok(Acceptable::Rejected( + "Doesn't support presenting to our window.".to_string())) + } + } else { + Ok(Acceptable::Rejected("Doesn't support graphics.".to_string())) + } + } + + // We expect our caller to have already verified that the device supports + // the swapchain extension. + #[allow(unsafe_code)] + pub fn find_device_swapchain_features(instance: &Instance, + surface: &vk::SurfaceKHR, + physical_device: &vk::PhysicalDevice) + -> Result<Acceptable<(vk::SurfaceCapabilitiesKHR, + Vec<vk::SurfaceFormatKHR>, + Vec<vk::PresentModeKHR>)>> + { + let capabilities = unsafe { + instance.get_physical_device_surface_capabilities_khr( + *physical_device, *surface) + }?; + let formats = unsafe { + instance.get_physical_device_surface_formats_khr( + *physical_device, *surface) + }?; + let presentation_modes = unsafe { + instance.get_physical_device_surface_present_modes_khr( + *physical_device, *surface) + }?; + + if formats.is_empty() { + Ok(Acceptable::Rejected("No matching surface formats.".to_string())) + } else if presentation_modes.is_empty() { + Ok(Acceptable::Rejected("No matching presentation modes.".to_string())) + } else { + Ok(Acceptable::Accepted((capabilities, formats, presentation_modes))) + } + } +} + + +#[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 +{ + // 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::WARNING + } else { + vk::DebugUtilsMessageSeverityFlagsEXT::ERROR + }; + + 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 + // test so anything it does is fine with us. + vk::FALSE +} diff --git a/src/main.rs b/src/main.rs index 8254424..b073b7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,138 +1,34 @@ #![deny(unsafe_code)] use crate::error::*; +use crate::graphics_permanent::{ PermanentGraphicsState, QueueFamilyIndices }; use std::cell::RefCell; -use std::collections::{ BTreeMap, BTreeSet, HashSet }; -use std::ffi::{ c_void, CStr }; -use vulkanalia::{ Device, Entry, Instance, Version }; +use std::collections::BTreeSet; +use vulkanalia::{ Device, Instance }; use vulkanalia::bytecode::Bytecode; -use vulkanalia::loader::{ LibloadingLoader, LIBRARY }; -use vulkanalia::vk::{ self, Handle, HasBuilder, - ApplicationInfo, InstanceCreateInfo, - DeviceV1_0, EntryV1_0, InstanceV1_0, - ExtDebugUtilsExtensionInstanceCommands, - KhrSurfaceExtensionInstanceCommands, +use vulkanalia::vk::{ self, Handle, HasBuilder, DeviceV1_0, KhrSwapchainExtensionDeviceCommands }; -use winit::dpi::LogicalSize; use winit::application::ApplicationHandler; use winit::event::WindowEvent; use winit::event_loop::{ ActiveEventLoop, EventLoop }; -use winit::window::{ Window, WindowAttributes, WindowId }; +use winit::window::{ Window, WindowId }; mod error; +mod graphics_permanent; -const VULKAN_FIRST_PORTABILITY_VERSION: Version = Version::new(1, 3, 216); // TODO: use VK_KHR_swapchain_maintenance1 to put a fence on the presentation // operation. doing that will remove the requirement that we have more // simultaneous frames than images. const N_SIMULTANEOUS_FRAMES: usize = 5; -enum Acceptable<T> { - Accepted(T), - Rejected(String), -} - -impl<T> Acceptable<T> { - #[allow(unused)] - fn is_accepted(&self) -> bool { - if let Acceptable::Accepted(_) = self { true } else { false } - } - - #[allow(unused)] - fn is_rejected(&self) -> bool { - if let Acceptable::Rejected(_) = self { true } else { false } - } - - #[allow(unused)] - fn unwrap(self) -> T { - if let Acceptable::Accepted(result) = self { - result - } else { - panic!("Unwrapped a rejected Acceptable."); - } - } - - fn require(self) -> Result<T> { - match self { - Acceptable::Accepted(value) => Ok(value), - Acceptable::Rejected(message) => Err(Error { message }), - } - } -} - -#[derive(Debug)] -struct QueueFamilyIndices { - graphics: u32, - presentation: u32, -} - - struct Surreality { permanent: RefCell<Option<PermanentGraphicsState>>, window_dressing: RefCell<Option<WindowDressing>>, frame_index: usize, } -// The PermanentGraphicsState collects the various windowing-system and -// Vulkan objects which never need to be regenerated once they're created. The -// ones which do need that are collected below, in WindowDressing. -struct PermanentGraphicsState { - // 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: Window, - - // The Vulkan "entry" is the part of the Vulkan library ecosystem that's - // responsible for finding and loading the other parts. Once we have the - // instance, the entry is never directly used again, but we retain it - // because doing otherwise would segfault. - #[allow(unused)] - entry: Entry, - - // The Vulkan "instance" is the bulk of the Vulkan library, with most of - // the high-level responsibilities around lifecycle management. - instance: 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. - // - // Once we've created this, we never actually need to do anything with - // it, but we do need to retain it, so here it is. - debug_messager: Option<vk::DebugUtilsMessengerEXT>, - - // The Vulkan "surface" is the destination that rendering happens into. - // It is connected to the window but distinct from it. Since we always have - // exactly one window, this is permanent state. - surface: vk::SurfaceKHR, - - // The Vulkan "device" is the abstraction for a GPU. A physical one is the - // actual GPU, and a logical one is our connection to it. We pick a physical - // device during initialization, but only the logical one is used later, so - // it's all we track. We'll be referencing the logical device a lot, so we - // follow Vulkan's lead and let it have a short variable name. - device: Device, - - // Vulkan has a first-class concept of command queues. We have two of - // them, one for graphics drawing commands and one for presentation. - // - // While these are often the same queue, there is no guarantee of that; - // sometimes there's no queue family that supports both operations together. - // For simplicity's sake we treat them as if they're separate, though the - // handles will alias each other when the initialization logic was able to - // find a family that does both. - // - // Yes, this means the compiler has to deal with pointer aliasing - // concerns, which have a tendency to defeat optimizations. Alas. - graphics_queue: vk::Queue, - presentation_queue: vk::Queue, -} - // The WindowDressing collects the Vulkan graphics objects which need to be // regenerated or modified when the window changes in certain ways, such as // resizing. The ones which don't need that are collected above, in @@ -188,12 +84,6 @@ struct Concurrency { image_fences: Vec<vk::Fence>, } -// These structs exist for use in function calling, to remove the potential -// for accidentally passing or returning one boolean as if it's another. -struct EnablePortability(bool); -struct EnableValidation(bool); -struct EnableSwapchain(bool); - impl Surreality { fn new() -> Self { @@ -312,589 +202,7 @@ impl Drop for Surreality { window_dressing.destroy(&permanent); } - let device = permanent.device; - let instance = permanent.instance; - - unsafe { device.destroy_device(None) }; - - unsafe { instance.destroy_surface_khr(permanent.surface, None) }; - - // Everything but the instance itself should already be destroyed, - // before we destroy the debug messager. The special hook to get debug - // messages while destroying the instance itself only applies to the - // instance and the messager, so if we were to destroy anything we - // shouldn't after this point, we'd miss out on diagnostics. - if let Some(debug_messager) = permanent.debug_messager { - unsafe { - instance.destroy_debug_utils_messenger_ext(debug_messager, None); - } - } - - unsafe { instance.destroy_instance(None) }; - } - } -} - -impl PermanentGraphicsState { - #[allow(unsafe_code)] - fn new(event_loop: &ActiveEventLoop) - -> Result<(Self, vk::PhysicalDevice, QueueFamilyIndices, - EnableSwapchain)> - { - let window = Self::init_window(event_loop)?; - - // There are a few Vulkan features (in the informal sense of - // "feature") that we want to be able to run both with and without. - // The enable_* values, here and below, are wrapped booleans that describe - // those choices. - // - // These are only used to communicate between initialization - // phases; we don't keep them around after that. - let (entry, instance, debug_messager, - enable_portability, enable_validation) - = Self::init_vulkan(&window)?; - - // Conveniently, Vulkanalia's "window" feature allows it to get the - // platform-specific stuff directly out of winit for us. This wrapper does - // not correspond 1:1 to a Vulkan function; rather, it picks the Vulkan - // function from the appropriate platform-specific extension. - // - // The reason it takes the window twice is that that first one is - // actually there to reference the display (in the x11 sense of "display" - // meaning the connection to the windowing system). - let surface = unsafe { - vulkanalia::window::create_surface(&instance, &window, &window) - }?; - - let (physical_device, device, - indices, graphics_queue, presentation_queue, - enable_swapchain) - = Self::init_vulkan_device(&instance, &surface, - enable_validation, enable_portability)?; - - Ok((PermanentGraphicsState { - window, entry, instance, debug_messager, surface, device, - graphics_queue, presentation_queue, - }, physical_device, indices, enable_swapchain)) - } - - fn init_window(event_loop: &ActiveEventLoop) -> Result<Window> { - // Notice that we do this before having a Vulkan instance. The window is - // actually a parameter needed to create the instance; see - // init_vulkan(), below. - let window_attributes = WindowAttributes::default() - .with_title("Love, Curiosity, Justice") - .with_inner_size(LogicalSize::new(1024, 768)); - - Ok(event_loop.create_window(window_attributes)?) - } - - #[allow(unsafe_code)] - fn init_vulkan(window: &Window) - -> Result<(Entry, Instance, Option<vk::DebugUtilsMessengerEXT>, - EnablePortability, EnableValidation)> - { - let enable_validation = cfg!(feature = "vulkan-validation") - || cfg!(debug_assertions); - - // 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 - // know how to load the entry, then it will take care of the rest. At - // least, that's the theory, but also see flake.nix for all the - // FHS-centric assumptions it makes that we have to correct. - // - // 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. - let loader = unsafe { LibloadingLoader::new(LIBRARY) }?; - let entry = unsafe { Entry::new(loader) }?; - - // Since there's a lot of factors going into our instance creation - // request, we'll build up the parameters mutably. - let mut flags = vk::InstanceCreateFlags::empty(); - let mut extensions = Vec::new(); - let mut layers = Vec::new(); - - // Before we go any further, use Vulkan's introspection to list off - // what's available. - let mut available_extensions = HashSet::new(); - for extension in - unsafe { entry.enumerate_instance_extension_properties(None) }? - { - available_extensions.insert(extension.extension_name); - } - let available_extensions = available_extensions; - - let mut available_layers = HashSet::new(); - for layer in unsafe { entry.enumerate_instance_layer_properties() }? { - available_layers.insert(layer.layer_name); - } - let available_layers = available_layers; - - // There are certain extensions which are required by the nature of our - // windowing system. Happily, vulanaklia knows how to deal with that based - // on the type of window we give it. - // - // This is possible because of an integration between Vulkanalia and - // winit, which is enabled by Vulkanalia's "window" feature. - for extension in vulkanalia::window::get_required_instance_extensions( - window) - { - extensions.push(extension.as_ptr()); - } - - // Deal with Vulkan's thing about opting in to non-conforming - // implementations. - let enable_portability = if entry.version()? - >= VULKAN_FIRST_PORTABILITY_VERSION - { - if cfg!(target_os = "macos") { - // Vulkan on the Mac is not fully conforming. - extensions.push( - vk::KHR_GET_PHYSICAL_DEVICE_PROPERTIES2_EXTENSION.name.as_ptr()); - extensions.push( - vk::KHR_PORTABILITY_ENUMERATION_EXTENSION.name.as_ptr()); - flags.insert(vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR); - - EnablePortability(true) - } else { - EnablePortability(false) - } - } else { - EnablePortability(false) - }; - - // Request the LunarG validation layer, when appropriate. - let validation_layer_name = vk::ExtensionName::from_bytes( - b"VK_LAYER_KHRONOS_validation"); - let enable_validation = if enable_validation { - if available_layers.contains(&validation_layer_name) { - layers.push(validation_layer_name.as_ptr()); - - EnableValidation(true) - } else { - eprintln!("Vulkan validation requested at build time, \ - but no validation layer available."); - - EnableValidation(false) - } - } else { - EnableValidation(false) - }; - - // Request the debug extension. This is the first of three bits of code - // that deal with this, and has the resonsibility of making sure the - // extension is in the list we ask for. - let debug_extension_name = vk::EXT_DEBUG_UTILS_EXTENSION.name; - if available_extensions.contains(&debug_extension_name) { - extensions.push(debug_extension_name.as_ptr()); - } else { - eprintln!("Vulkan debug extension not available; \ - this may mean other messages don't show up."); - } - - let application_info = ApplicationInfo::builder() - .application_name(b"Surreality\0") - .application_version(vk::make_version(1, 0, 0)) - .engine_name(b"Surreality\0") - .engine_version(vk::make_version(1, 0, 0)) - .api_version(vk::make_version(1, 0, 0)); - - // Deceptively, this DOES get mutated later, but Vulkanalia doesn't see - // it that way. - let instance_create_info = InstanceCreateInfo::builder() - .application_info(&application_info) - .flags(flags) - .enabled_extension_names(&extensions) - .enabled_layer_names(&layers); - - // Configure the debug extension. This is the middle of three bits of - // code that deal with this, and has the responsibility of making sure - // the callback will be available during instance creation and - // destruction, which is done in a special way that doesn't rely on having - // a messager, since there can't be one for those steps. - let debug_info = if available_extensions.contains(&debug_extension_name) { - let mut debug_info = vk::DebugUtilsMessengerCreateInfoEXT::builder() - .message_severity(vk::DebugUtilsMessageSeverityFlagsEXT::all()) - .message_type(vk::DebugUtilsMessageTypeFlagsEXT::GENERAL - | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION - | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE) - .user_callback(Some(debug_messager_callback)); - - // Please notice that the reference we pass here will escape Rust's - // lifetime checking, since push_next() casts it to a pointer. We don't - // get nearly as strong a safety guarantee as one might hope (and as [1] - // naively reassures us we do). If we did, the thing we're doing would - // actually be forbidden! - // - // [1] https://kylemayes.github.io/vulkanalia/ - instance_create_info.push_next(&mut debug_info); - - Some(debug_info) - } else { None }; - - 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 - // aware. Only you can prevent segfaults. - entry.create_instance(&instance_create_info, None) - }?; - - // Configure the debug extension. This is the last of three bits of code - // that deal with this, and has the responsibility of asking the instance, - // which now exists, to create the debug messager. - let debug_messager = if let Some(debug_info) = debug_info { - #[allow(unsafe_code)] - Some(unsafe { - instance.create_debug_utils_messenger_ext(&debug_info, None) - }?) - } else { - None - }; - - Ok((entry, instance, debug_messager, - enable_portability, enable_validation)) - } - - #[allow(unsafe_code)] - fn init_vulkan_device(instance: &Instance, surface: &vk::SurfaceKHR, - enable_validation: EnableValidation, - enable_portability: EnablePortability) - -> Result<(vk::PhysicalDevice, Device, QueueFamilyIndices, vk::Queue, - vk::Queue, EnableSwapchain)> - { - let (physical_device, indices) - = Self::pick_vulkan_device(instance, surface)?; - - // We enumerate the device extensions here so they can inform - // configuration. We already did this in score_vulkan_device(), but here - // it is again. - let mut available_extensions = HashSet::new(); - for extension in unsafe { - instance.enumerate_device_extension_properties(physical_device, None) - }? { - available_extensions.insert(extension.extension_name); - } - let available_extensions = available_extensions; - - // Old versions of Vulkan want layers to be enabled at the device - // level as well. Newer ones will ignore this and just use the instance - // layers. - let features = vk::PhysicalDeviceFeatures::builder(); - let mut extensions = Vec::new(); - let mut layers = Vec::new(); - - let validation_layer_name = vk::ExtensionName::from_bytes( - b"VK_LAYER_KHRONOS_validation"); - if enable_validation.0 { - // It's not concerning if this isn't supported, because device - // layers are ignored on recent versions, they're purely historical. - if available_extensions.contains(&validation_layer_name) { - layers.push(validation_layer_name.as_ptr()); - } - } - - let portability_extension_name = vk::ExtensionName::from_bytes( - b"VK_KHR_portability_subset"); - if enable_portability.0 { - // This is untested, since the only scenario where it would come up - // is on a Mac, which we don't actually support. Sorry, and good luck. - if available_extensions.contains(&portability_extension_name) { - extensions.push(portability_extension_name.as_ptr()); - } - } - - let swapchain_extension_name = vk::KHR_SWAPCHAIN_EXTENSION.name; - let enable_swapchain = if available_extensions.contains( - &swapchain_extension_name) - { - // It's important that we not call the swapchain extension - // functions until we've verified the extension is supported. To - // emphasize that, we do it on a separate line. - // - // We've done this check once already, in scoring, and now here - // we are discarding its results a second time. We'll do it for the - // third and last time in swapchain creation. - if let Acceptable::Accepted(_) - = PermanentGraphicsState::find_device_swapchain_features( - &instance, &surface, &physical_device)? - { - extensions.push(swapchain_extension_name.as_ptr()); - - EnableSwapchain(true) - } else { - EnableSwapchain(false) - } - } else { - EnableSwapchain(false) - }; - - // We have one or more queue family indices; we don't know a priori - // how many, because it's possible some of them are the same. We only - // want to create one queue per distinct family, so we find the unique - // indices... - let mut unique_queue_family_indices = BTreeSet::new(); - unique_queue_family_indices.insert(indices.graphics); - unique_queue_family_indices.insert(indices.presentation); - - // ... then add a queue create info struct for each. - let mut queues = Vec::new(); - for index in unique_queue_family_indices { - // Passing the priorities vector also implicitly sets the count of - // how many queues we are creating within the family. This nicety is - // one of the fun things Vulkanalia's builders do for us. - queues.push(vk::DeviceQueueCreateInfo::builder() - .queue_family_index(index) - .queue_priorities(&[1.0])); - } - - let device_info = vk::DeviceCreateInfo::builder() - .queue_create_infos(&queues) - .enabled_layer_names(&layers) - .enabled_extension_names(&extensions) - .enabled_features(&features); - - let device = unsafe { - instance.create_device(physical_device, &device_info, None) - }?; - - // So, this is a little confusing. Queues are found in queue families. - // The family has an index within the device, and the queue has an index - // within the family. We computed the family index above, and when we - // created the device we told it to create just a single queue in that - // family. Now we pass both indices to find the actual queue object. - let graphics_queue = unsafe { - device.get_device_queue(indices.graphics, 0) - }; - - let presentation_queue = unsafe { - device.get_device_queue(indices.presentation, 0) - }; - - Ok((physical_device, device, - indices, graphics_queue, presentation_queue, - enable_swapchain)) - } - - // 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(instance: &Instance, surface: &vk::SurfaceKHR) - -> Result<(vk::PhysicalDevice, QueueFamilyIndices)> - { - let mut best_device = None; - let mut best_score = None; - let mut best_indices = None; - let mut rejected = BTreeMap::new(); - - for device in unsafe { instance.enumerate_physical_devices() }? { - match Self::score_vulkan_device(instance, surface, &device)? { - Acceptable::Accepted((new_score, new_indices)) => { - if let Some(old_score) = best_score { - if new_score > old_score { - best_device = Some(device); - best_score = Some(new_score); - best_indices = Some(new_indices); - } - } else { - best_device = Some(device); - best_score = Some(new_score); - best_indices = Some(new_indices); - } - } - Acceptable::Rejected(reason) => { - let properties = unsafe { - instance.get_physical_device_properties(device) - }; - - let name = properties.device_name.to_string_lossy().into_owned(); - - rejected.insert(properties.device_id, (name, reason)); - } - } - } - - if let (Some(device), Some(indices)) = (best_device, best_indices) { - Ok((device, indices)) - } 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. - // - // In the event that we find the device acceptable, we also return the - // queue family indices we'd be using if we ultimately go with it. While - // this is not strictly necessary, it's better to return them from here - // than to recompute them later on the assumption it'll work out the same. - #[allow(unsafe_code)] - fn score_vulkan_device(instance: &Instance, surface: &vk::SurfaceKHR, - physical_device: &vk::PhysicalDevice) - -> Result<Acceptable<(u64, QueueFamilyIndices)>> - { - // Not all devices support graphics, and not all devices support - // presenting to any given surface. We check whether this one is suitable - // by looking up the indices of the queue families we would use. If we - // ultimately use this device, we'll need these, so we make sure to return - // them. - let indices = match Self::find_device_queue_family_indices( - instance, surface, physical_device)? - { - Acceptable::Rejected(rationale) => { - return Ok(Acceptable::Rejected(rationale)); - } - Acceptable::Accepted(indices) => indices - }; - - // 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(*physical_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. - - // Some of our scoring will depend on what extensions the device - // supports, so we enumerate those. - let mut available_extensions = HashSet::new(); - for extension in unsafe { - instance.enumerate_device_extension_properties(*physical_device, None) - }? { - available_extensions.insert(extension.extension_name); - } - let available_extensions = available_extensions; - - if available_extensions.contains(&vk::KHR_SWAPCHAIN_EXTENSION.name) { - // Double buffering is both quite a nice feature to have, and a good - // indicator that this is a "real" graphics card rather than some - // trivial weird thing. - // - // With that said, however, it only counts if we're able to actually - // use it on the surface we have. Let's find out... - if let Acceptable::Accepted(_) - = PermanentGraphicsState::find_device_swapchain_features( - instance, surface, physical_device)? - { - // We don't count it for enough points to override a device type - // bracket, but it's good for a lot within the bracket. - score += 16; - } - - // This isn't disqualifying, so we don't worry about tracking the - // rationale. We'll deal with that later, if the device actually gets - // selected. - } - - Ok(Acceptable::Accepted((score, indices))) - } - - #[allow(unsafe_code)] - fn find_device_queue_family_indices(instance: &Instance, - surface: &vk::SurfaceKHR, - device: &vk::PhysicalDevice) - -> Result<Acceptable<QueueFamilyIndices>> - { - // We need a queue family that supports graphics drawing commands, and a - // queue family that supports presentation commands. These may or may not - // be the same family. - let mut graphics = None; - let mut presentation = None; - - for (index, queue_family) in (unsafe { - instance.get_physical_device_queue_family_properties(*device) - }).into_iter().enumerate() { - if graphics.is_none() - && queue_family.queue_flags.contains(vk::QueueFlags::GRAPHICS) - { - graphics = Some(index as u32); - } - - if presentation.is_none() && unsafe { - instance.get_physical_device_surface_support_khr( - *device, index as u32, *surface) - }? { - presentation = Some(index as u32); - } - } - - if let Some(graphics) = graphics { - if let Some(presentation) = presentation { - Ok(Acceptable::Accepted(QueueFamilyIndices { - graphics, presentation - })) - } else { - Ok(Acceptable::Rejected( - "Doesn't support presenting to our window.".to_string())) - } - } else { - Ok(Acceptable::Rejected("Doesn't support graphics.".to_string())) - } - } - - // We expect our caller to have already verified that the device supports - // the swapchain extension. - #[allow(unsafe_code)] - fn find_device_swapchain_features(instance: &Instance, - surface: &vk::SurfaceKHR, - physical_device: &vk::PhysicalDevice) - -> Result<Acceptable<(vk::SurfaceCapabilitiesKHR, - Vec<vk::SurfaceFormatKHR>, - Vec<vk::PresentModeKHR>)>> - { - let capabilities = unsafe { - instance.get_physical_device_surface_capabilities_khr( - *physical_device, *surface) - }?; - let formats = unsafe { - instance.get_physical_device_surface_formats_khr( - *physical_device, *surface) - }?; - let presentation_modes = unsafe { - instance.get_physical_device_surface_present_modes_khr( - *physical_device, *surface) - }?; - - if formats.is_empty() { - Ok(Acceptable::Rejected("No matching surface formats.".to_string())) - } else if presentation_modes.is_empty() { - Ok(Acceptable::Rejected("No matching presentation modes.".to_string())) - } else { - Ok(Acceptable::Accepted((capabilities, formats, presentation_modes))) + permanent.destroy(); } } } @@ -1477,56 +785,3 @@ 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 -{ - // 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::WARNING - } else { - vk::DebugUtilsMessageSeverityFlagsEXT::ERROR - }; - - 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 - // test so anything it does is fine with us. - vk::FALSE -} - |