#![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::bytecode::Bytecode; 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, // 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, GraphicsStateForReinit, EnableSwapchain)> { let window = 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) = 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) = init_vulkan_device(&instance, &surface, enable_validation, enable_portability)?; Ok((PermanentGraphicsState { window, entry, instance, debug_messager, surface, device, graphics_queue, presentation_queue, }, GraphicsStateForReinit { 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) }; } // 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, Vec)>> { 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)] pub fn load_spirv_shader_module(device: &Device, binary: &[u8]) -> Result { let bytecode = Bytecode::new(binary)?; let module_info = vk::ShaderModuleCreateInfo::builder() .code(bytecode.code()) .code_size(bytecode.code_size()); let module = unsafe { device.create_shader_module(&module_info, None) }?; Ok(module) } } // The GraphicsStateForReinit connects Vulkan objects which are only needed // during the creation of the window-dressing objects. They are used during // initial startup, and again any time the window-dressing needs to be // reinitialized. Most notably, they are not needed when rendering. pub struct GraphicsStateForReinit { pub physical_device: vk::PhysicalDevice, pub indices: QueueFamilyIndices, } fn init_window(event_loop: &ActiveEventLoop) -> Result { // 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, 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) = 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 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> { // 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 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> { // 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())) } } #[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 }