#![deny(unsafe_code)] use std::cell::OnceCell; use std::collections::{ BTreeMap, HashSet }; use std::ffi::{ c_void, CStr }; use vulkanalia::{ Entry, Instance, Version }; use vulkanalia::loader::{ LibloadingLoader, LIBRARY }; use vulkanalia::vk::{ self, HasBuilder, ApplicationInfo, InstanceCreateInfo, DeviceV1_4, EntryV1_0, InstanceV1_0, ExtDebugUtilsExtensionInstanceCommands }; use winit::dpi::LogicalSize; use winit::application::ApplicationHandler; use winit::event::WindowEvent; use winit::event_loop::{ ActiveEventLoop, EventLoop }; use winit::window::{ Window, WindowAttributes, WindowId }; #[derive(Debug)] struct Error { message: String, } type Result = std::result::Result; impl std::fmt::Display for Error { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { fmt.write_str(&self.message) } } impl From for Error { fn from(e: winit::error::EventLoopError) -> Self { match e { winit::error::EventLoopError::NotSupported(e) => Self::from(e), winit::error::EventLoopError::Os(e) => Self::from(e), winit::error::EventLoopError::RecreationAttempt => Error { message: "There may only ever be a single winit event loop.".to_string() }, winit::error::EventLoopError::ExitFailure(code) => Error { message: format!("Clean unhappy exit with code {} via winit event loop", code) } } } } impl From for Error { fn from(e: winit::error::NotSupportedError) -> Self { Error { message: format!("The winit backend does not support an operation: {}", e.to_string()) } } } impl From for Error { fn from(e: winit::error::OsError) -> Self { Error { message: format!("The OS told winit about an error: {}", e.to_string()) } } } impl From for Error { fn from(e: libloading::Error) -> Self { Error { message: format!("The dynamic object loader reported an error: {}", e.to_string()) } } } impl From> for Error { fn from(e: Box) -> Self { Error { message: format!("The Vulkan loader reported an error: {}", e.to_string()) } } } impl From for Error { fn from(e: vulkanalia::vk::ErrorCode) -> Self { Error { message: format!("Vulkan gave an error code: {}", e.to_string()) } } } fn ignore_errors(mut body: impl FnMut() -> Result<()>) -> () { if let Err(e) = body() { eprintln!("Error: {}", e); } } const VULKAN_FIRST_PORTABILITY_VERSION: Version = Version::new(1, 3, 216); enum Acceptable { 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, // The Vulkan "entry" is the part of the Vulkan library ecosystem that's // responsible for finding and loading the other parts. entry: OnceCell, // The Vulkan "instance" is the bulk of the Vulkan library, with most of // the high-level responsibilities around lifecycle management. instance: OnceCell, // 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, // The Vulkan "device" is the abstraction for a GPU. device: OnceCell, } impl Surreality { fn new() -> Self { Surreality { window: OnceCell::new(), entry: OnceCell::new(), instance: OnceCell::new(), debug_messager: OnceCell::new(), device: OnceCell::new(), } } fn init(&mut self, event_loop: &ActiveEventLoop) -> Result<()> { if self.window.get().is_none() { self.init_window(event_loop)?; } if self.entry.get().is_none() { self.init_vulkan_entry()?; } if self.instance.get().is_none() { self.init_vulkan_instance()?; } if self.device.get().is_none() { self.init_vulkan_device()?; } Ok(()) } fn init_window(&mut self, 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_instance(), below. let window_attributes = WindowAttributes::default() .with_title("Love, Curiosity, Justice") .with_inner_size(LogicalSize::new(1024, 768)); let window: Window = event_loop.create_window(window_attributes)?; let _ = self.window.set(window); 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 // 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) }?; let _ = self.entry.set(entry); Ok(()) } #[allow(unsafe_code)] fn init_vulkan_instance(&mut self) -> Result<()> { let entry = self.entry.get().unwrap(); let enable_validation = cfg!(feature = "vulkan-validation") || cfg!(debug_assertions); // 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( self.window.get().unwrap()) { extensions.push(extension.as_ptr()); } // Deal with Vulkan's thing about opting in to non-conforming // implementations. 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); } } // Request the LunarG validation layer, when appropriate. if enable_validation { let layer_name = vk::ExtensionName::from_bytes( b"VK_LAYER_KHRONOS_validation"); if available_layers.contains(&layer_name) { layers.push(layer_name.as_ptr()); } else { eprintln!("Vulkan validation requested at build time, \ but no validation layer available."); } } // 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)); let mut 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. if let Some(debug_info) = debug_info && self.debug_messager.get().is_none() { #[allow(unsafe_code)] let debug_messager = unsafe { instance.create_debug_utils_messenger_ext(&debug_info, None) }?; let _ = self.debug_messager.set(debug_messager); } let _ = self.instance.set(instance); 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 { 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> { 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() { println!("render the window"); } else { println!("render something unknown"); } Ok(()) } } 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() { unsafe { instance.destroy_debug_utils_messenger_ext(*debug_messager, None); } } if let Some(instance) = self.instance.get() { unsafe { instance.destroy_instance(None) }; } } } impl ApplicationHandler for Surreality { fn resumed(&mut self, event_loop: &ActiveEventLoop) { ignore_errors(move || { self.init(event_loop)?; Ok(()) }); } fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) { println!("window event {:?}", event); match event { WindowEvent::RedrawRequested => { if !event_loop.exiting() { if let Err(e) = self.render(window_id) { eprintln!("Error: {}", e); } } } WindowEvent::CloseRequested => { event_loop.exit(); } _ => { } } } } fn main() -> std::process::ExitCode { let body: fn() -> Result<()> = || { let event_loop = EventLoop::new()?; let mut surreality = Surreality::new(); event_loop.run_app(&mut surreality)?; Ok(()) }; match body() { Ok(()) => std::process::ExitCode::SUCCESS, Err(e) => { eprintln!("Error: {}", e); std::process::ExitCode::from(1) } } } #[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::empty() } else { vk::DebugUtilsMessageSeverityFlagsEXT::WARNING }; 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 }