diff options
| author | Irene Knapp <ireneista@irenes.space> | 2026-07-04 00:01:54 -0700 |
|---|---|---|
| committer | Irene Knapp <ireneista@irenes.space> | 2026-07-04 00:15:29 -0700 |
| commit | 793e57dfa3bb5e29f622b3c3a311be6bca319b86 (patch) | |
| tree | 272b8bec5da5bf4102e30bd7a46b9dc45ad741e8 /src | |
| parent | 622e7cc99346b2634a162163193ce4561131d1d2 (diff) | |
connect to a GPU ("device")
also refactor Error into its own file, it was unwieldy. it really should have been there from the start. for the first time, this compiles without warnings! yay! alas, this is merely the first hump of the camel Force-Push: yes Change-Id: Ib6ec835448af469ccb259776b78a27bbc157c8f0
Diffstat (limited to 'src')
| -rw-r--r-- | src/error.rs | 84 | ||||
| -rw-r--r-- | src/main.rs | 218 |
2 files changed, 212 insertions, 90 deletions
diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..39d52b8 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,84 @@ +#![forbid(unsafe_code)] + +#[derive(Debug)] +pub struct Error { + pub message: String, +} +pub type Result<T> = std::result::Result<T, Error>; + +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<winit::error::EventLoopError> 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<winit::error::NotSupportedError> 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<winit::error::OsError> for Error { + fn from(e: winit::error::OsError) -> Self { + Error { + message: format!("The OS told winit about an error: {}", e.to_string()) + } + } +} + +impl From<libloading::Error> for Error { + fn from(e: libloading::Error) -> Self { + Error { + message: format!("The dynamic object loader reported an error: {}", + e.to_string()) + } + } +} + +impl From<Box<dyn vulkanalia::loader::LoaderError>> for Error { + fn from(e: Box<dyn vulkanalia::loader::LoaderError>) -> Self { + Error { + message: format!("The Vulkan loader reported an error: {}", + e.to_string()) + } + } +} + +impl From<vulkanalia::vk::ErrorCode> for Error { + fn from(e: vulkanalia::vk::ErrorCode) -> Self { + Error { + message: format!("Vulkan gave an error code: {}", e.to_string()) + } + } +} + +pub fn ignore_errors(mut body: impl FnMut() -> Result<()>) -> () { + if let Err(e) = body() { + eprintln!("Error: {}", e); + } +} + diff --git a/src/main.rs b/src/main.rs index 05c8cd5..a42e8da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,14 @@ #![deny(unsafe_code)] +use crate::error::*; + use std::cell::OnceCell; use std::collections::{ BTreeMap, HashSet }; use std::ffi::{ c_void, CStr }; -use vulkanalia::{ Entry, Instance, Version }; +use vulkanalia::{ Device, Entry, Instance, Version }; use vulkanalia::loader::{ LibloadingLoader, LIBRARY }; use vulkanalia::vk::{ self, HasBuilder, ApplicationInfo, InstanceCreateInfo, - DeviceV1_4, EntryV1_0, InstanceV1_0, + DeviceV1_0, EntryV1_0, InstanceV1_0, ExtDebugUtilsExtensionInstanceCommands }; use winit::dpi::LogicalSize; use winit::application::ApplicationHandler; @@ -14,87 +16,7 @@ use winit::event::WindowEvent; use winit::event_loop::{ ActiveEventLoop, EventLoop }; use winit::window::{ Window, WindowAttributes, WindowId }; -#[derive(Debug)] -struct Error { - message: String, -} -type Result<T> = std::result::Result<T, Error>; - -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<winit::error::EventLoopError> 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<winit::error::NotSupportedError> 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<winit::error::OsError> for Error { - fn from(e: winit::error::OsError) -> Self { - Error { - message: format!("The OS told winit about an error: {}", e.to_string()) - } - } -} - -impl From<libloading::Error> for Error { - fn from(e: libloading::Error) -> Self { - Error { - message: format!("The dynamic object loader reported an error: {}", - e.to_string()) - } - } -} - -impl From<Box<dyn vulkanalia::loader::LoaderError>> for Error { - fn from(e: Box<dyn vulkanalia::loader::LoaderError>) -> Self { - Error { - message: format!("The Vulkan loader reported an error: {}", - e.to_string()) - } - } -} - -impl From<vulkanalia::vk::ErrorCode> 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); - } -} +mod error; const VULKAN_FIRST_PORTABILITY_VERSION: Version = Version::new(1, 3, 216); @@ -113,6 +35,9 @@ struct Surreality { // we could have more, but for now, we don't. window: OnceCell<Window>, + enable_validation: OnceCell<bool>, + enable_portability: OnceCell<bool>, + // The Vulkan "entry" is the part of the Vulkan library ecosystem that's // responsible for finding and loading the other parts. entry: OnceCell<Entry>, @@ -128,18 +53,30 @@ struct Surreality { // over-formality and we don't indulge it. debug_messager: OnceCell<vk::DebugUtilsMessengerEXT>, - // The Vulkan "device" is the abstraction for a GPU. - device: OnceCell<vk::PhysicalDevice>, + // 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 track both. + // We'll be referencing the logical device a lot, so we follow Vulkan's lead + // and let it have a short variable name. + physical_device: OnceCell<vk::PhysicalDevice>, + device: OnceCell<Device>, + + // Vulkan has a first-class concept of command queues, but we only need a + // single one. + graphics_queue: OnceCell<vk::Queue>, } impl Surreality { fn new() -> Self { Surreality { window: OnceCell::new(), + enable_validation: OnceCell::new(), + enable_portability: OnceCell::new(), entry: OnceCell::new(), instance: OnceCell::new(), debug_messager: OnceCell::new(), + physical_device: OnceCell::new(), device: OnceCell::new(), + graphics_queue: OnceCell::new(), } } @@ -248,7 +185,13 @@ impl Surreality { extensions.push( vk::KHR_PORTABILITY_ENUMERATION_EXTENSION.name.as_ptr()); flags.insert(vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR); + + let _ = self.enable_portability.set(true); + } else { + let _ = self.enable_portability.set(false); } + } else { + let _ = self.enable_portability.set(false); } // Request the LunarG validation layer, when appropriate. @@ -257,10 +200,16 @@ impl Surreality { b"VK_LAYER_KHRONOS_validation"); if available_layers.contains(&layer_name) { layers.push(layer_name.as_ptr()); + + let _ = self.enable_validation.set(enable_validation); } else { eprintln!("Vulkan validation requested at build time, \ but no validation layer available."); + + let _ = self.enable_validation.set(false); } + } else { + let _ = self.enable_validation.set(false); } // Request the debug extension. This is the first of three bits of code @@ -281,7 +230,9 @@ impl Surreality { .engine_version(vk::make_version(1, 0, 0)) .api_version(vk::make_version(1, 0, 0)); - let mut instance_create_info = InstanceCreateInfo::builder() + // 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) @@ -338,8 +289,86 @@ impl Surreality { Ok(()) } + #[allow(unsafe_code)] fn init_vulkan_device(&mut self) -> Result<()> { - let physical_device = self.pick_vulkan_device()?; + if self.physical_device.get().is_none() { + let physical_device = self.pick_vulkan_device()?; + + let _ = self.physical_device.set(physical_device); + } + + if self.device.get().is_none() { + let instance = self.instance.get().unwrap(); + let physical_device = self.physical_device.get().unwrap(); + + // Compute the queue family index of the first queue family that + // supports graphics. + let mut graphics_index = None; + for (index, queue) in (unsafe { + instance.get_physical_device_queue_family_properties(*physical_device) + }).into_iter().enumerate() { + if queue.queue_flags.contains(vk::QueueFlags::GRAPHICS) { + graphics_index = Some(index as u32); + break; + } + } + // We already did this check in score_vulkan_device(), so if it fails + // this second time, that's our own bug and we don't need to explain it + // to our users. + let graphics_index = graphics_index.unwrap(); + + // 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(); + + if *self.enable_validation.get().unwrap() { + let layer_name = vk::ExtensionName::from_bytes( + b"VK_LAYER_KHRONOS_validation"); + layers.push(layer_name.as_ptr()); + } + + if *self.enable_portability.get().unwrap() { + // 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. + let layer_name = vk::ExtensionName::from_bytes( + b"VK_KHR_portability_subset"); + extensions.push(layer_name.as_ptr()); + } + + // Passing the priorities vector also implicitly sets the count of how + // many queues we are creating. This nicety is one of the fun things + // Vulkanalia's builders do for us. + let queue_info = vk::DeviceQueueCreateInfo::builder() + .queue_family_index(graphics_index) + .queue_priorities(&[1.0]); + let queues = vec![queue_info]; + + 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(*self.physical_device.get().unwrap(), + &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(graphics_index, 0) + }; + + let _ = self.device.set(device); + let _ = self.graphics_queue.set(graphics_queue); + } Ok(()) } @@ -421,7 +450,7 @@ impl Surreality { } if !has_graphics { return Ok(Acceptable::Rejected( - "Doesn't support graphics.".to_string()));; + "Doesn't support graphics.".to_string())); } // At this point we know the device meets our high-level requirements, @@ -468,6 +497,15 @@ impl Surreality { impl Drop for Surreality { #[allow(unsafe_code)] fn drop(&mut self) { + if let Some(device) = self.device.get() { + unsafe { device.destroy_device(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.get() && let Some(instance) = self.instance.get() { @@ -555,9 +593,9 @@ extern "system" fn debug_messager_callback( let threshold = if cfg!(feature = "vulkan-validation") || cfg!(debug_assertions) { - vk::DebugUtilsMessageSeverityFlagsEXT::empty() - } else { vk::DebugUtilsMessageSeverityFlagsEXT::WARNING + } else { + vk::DebugUtilsMessageSeverityFlagsEXT::ERROR }; if severity >= threshold { |