diff options
| author | Irene Knapp <ireneista@irenes.space> | 2026-07-04 03:07:45 -0700 |
|---|---|---|
| committer | Irene Knapp <ireneista@irenes.space> | 2026-07-04 13:16:39 -0700 |
| commit | b3a7ce316f51314a4851eba34d42ed612a6da163 (patch) | |
| tree | daceea55860f0c93a736021e9ffb61084e169430 | |
| parent | cfa6a8e42affa661728dcd7d69534555d3c20605 (diff) | |
create a swapchain (when supported)
lots of decisions about modes in here, which will have far-reaching effects Force-Push: yes Change-Id: Ia73ccbd0bce8933970d7379acc09d5865dc4c2b5
| -rw-r--r-- | src/main.rs | 310 |
1 files changed, 288 insertions, 22 deletions
diff --git a/src/main.rs b/src/main.rs index 9056e98..e864d27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,11 +6,12 @@ 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, +use vulkanalia::vk::{ self, Handle, HasBuilder, ApplicationInfo, InstanceCreateInfo, DeviceV1_0, EntryV1_0, InstanceV1_0, ExtDebugUtilsExtensionInstanceCommands, - KhrSurfaceExtensionInstanceCommands }; + KhrSurfaceExtensionInstanceCommands, + KhrSwapchainExtensionDeviceCommands }; use winit::dpi::LogicalSize; use winit::application::ApplicationHandler; use winit::event::WindowEvent; @@ -48,11 +49,19 @@ impl<T> Acceptable<T> { } } +#[derive(Debug)] struct QueueFamilyIndices { graphics: u32, presentation: u32, } +#[derive(Debug)] +struct SwapchainFeatures { + capabilities: vk::SurfaceCapabilitiesKHR, + formats: Vec<vk::SurfaceFormatKHR>, + presentation_modes: Vec<vk::PresentModeKHR>, +} + struct Surreality { // The "window" is the usual operating-system concept of a window; it's @@ -61,8 +70,18 @@ struct Surreality { // we could have more, but for now, we don't. window: OnceCell<Window>, + // There are a few Vulkan features (in the informal sense of "feature") + // that we want to be able to run both with and without. Here, we have + // booleans describing which we're doing. These are computed during instance + // and device initialization. + // + // In the cases of validation and portability, the booleans are only used + // to communicate between initialization phases, and aren't actually needed + // during rendering. We keep them around anyway though because they're small + // and it's more convenient to have them all in one place. enable_validation: OnceCell<bool>, enable_portability: OnceCell<bool>, + enable_swapchain: OnceCell<bool>, // The Vulkan "entry" is the part of the Vulkan library ecosystem that's // responsible for finding and loading the other parts. @@ -103,6 +122,12 @@ struct Surreality { // concerns, which have a tendency to defeat optimizations. graphics_queue: OnceCell<vk::Queue>, presentation_queue: OnceCell<vk::Queue>, + + // A swapchain is the generalized facility that is used to implement + // double buffering, triple buffering, rendering passes that feed into each + // other, and other things of that nature. It's a first-class thing but for + // now, we use at most one of it. We also support running without one. + swapchain: OnceCell<vk::SwapchainKHR>, } impl Surreality { @@ -111,6 +136,7 @@ impl Surreality { window: OnceCell::new(), enable_validation: OnceCell::new(), enable_portability: OnceCell::new(), + enable_swapchain: OnceCell::new(), entry: OnceCell::new(), instance: OnceCell::new(), debug_messager: OnceCell::new(), @@ -119,6 +145,7 @@ impl Surreality { device: OnceCell::new(), graphics_queue: OnceCell::new(), presentation_queue: OnceCell::new(), + swapchain: OnceCell::new(), } } @@ -143,6 +170,12 @@ impl Surreality { self.init_vulkan_device()?; } + if self.swapchain.get().is_none() + && *self.enable_swapchain.get().unwrap() + { + self.init_vulkan_swapchain()?; + } + Ok(()) } @@ -156,7 +189,7 @@ impl Surreality { let window: Window = event_loop.create_window(window_attributes)?; - let _ = self.window.set(window); + self.window.set(window).unwrap(); Ok(()) } @@ -175,7 +208,7 @@ impl Surreality { let loader = unsafe { LibloadingLoader::new(LIBRARY) }?; let entry = unsafe { Entry::new(loader) }?; - let _ = self.entry.set(entry); + self.entry.set(entry).unwrap(); Ok(()) } @@ -232,12 +265,12 @@ impl Surreality { vk::KHR_PORTABILITY_ENUMERATION_EXTENSION.name.as_ptr()); flags.insert(vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR); - let _ = self.enable_portability.set(true); + self.enable_portability.set(true).unwrap(); } else { - let _ = self.enable_portability.set(false); + self.enable_portability.set(false).unwrap(); } } else { - let _ = self.enable_portability.set(false); + self.enable_portability.set(false).unwrap(); } // Request the LunarG validation layer, when appropriate. @@ -247,15 +280,15 @@ impl Surreality { if available_layers.contains(&layer_name) { layers.push(layer_name.as_ptr()); - let _ = self.enable_validation.set(enable_validation); + self.enable_validation.set(enable_validation).unwrap(); } else { eprintln!("Vulkan validation requested at build time, \ but no validation layer available."); - let _ = self.enable_validation.set(false); + self.enable_validation.set(false).unwrap(); } } else { - let _ = self.enable_validation.set(false); + self.enable_validation.set(false).unwrap(); } // Request the debug extension. This is the first of three bits of code @@ -327,10 +360,10 @@ impl Surreality { instance.create_debug_utils_messenger_ext(&debug_info, None) }?; - let _ = self.debug_messager.set(debug_messager); + self.debug_messager.set(debug_messager).unwrap(); } - let _ = self.instance.set(instance); + self.instance.set(instance).unwrap(); Ok(()) } @@ -352,7 +385,7 @@ impl Surreality { vulkanalia::window::create_surface(&instance, &window, &window) }?; - let _ = self.surface.set(surface); + self.surface.set(surface).unwrap(); Ok(()) } @@ -362,7 +395,7 @@ impl Surreality { if self.physical_device.get().is_none() { let physical_device = self.pick_vulkan_device()?; - let _ = self.physical_device.set(physical_device); + self.physical_device.set(physical_device).unwrap(); } if self.device.get().is_none() { @@ -379,6 +412,17 @@ impl Surreality { let indices = self.find_device_queue_family_indices(&physical_device)? .unwrap(); + // 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. @@ -389,7 +433,11 @@ impl Surreality { if *self.enable_validation.get().unwrap() { let layer_name = vk::ExtensionName::from_bytes( b"VK_LAYER_KHRONOS_validation"); - layers.push(layer_name.as_ptr()); + // 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(&layer_name) { + layers.push(layer_name.as_ptr()); + } } if *self.enable_portability.get().unwrap() { @@ -397,7 +445,33 @@ impl Surreality { // 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()); + if available_extensions.contains(&layer_name) { + extensions.push(layer_name.as_ptr()); + } + } + + { + let layer_name = vk::KHR_SWAPCHAIN_EXTENSION.name; + if available_extensions.contains(&layer_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(_) + = self.find_device_swapchain_features(&physical_device)? + { + extensions.push(layer_name.as_ptr()); + + self.enable_swapchain.set(true).unwrap(); + } else { + self.enable_swapchain.set(false).unwrap(); + } + } else { + self.enable_swapchain.set(false).unwrap(); + } } // We have one or more queue family indices; we don't know a priori @@ -443,9 +517,84 @@ impl Surreality { device.get_device_queue(indices.presentation, 0) }; - let _ = self.device.set(device); - let _ = self.graphics_queue.set(graphics_queue); - let _ = self.presentation_queue.set(presentation_queue); + self.device.set(device).unwrap(); + self.graphics_queue.set(graphics_queue).unwrap(); + self.presentation_queue.set(presentation_queue).unwrap(); + } + + Ok(()) + } + + #[allow(unsafe_code)] + fn init_vulkan_swapchain(&mut self) -> Result<()> { + // Here, too, we need to clone the smart pointer to simplify ownership. + let physical_device = self.physical_device.get().unwrap().clone(); + + if let Acceptable::Accepted(features) + = self.find_device_swapchain_features(&physical_device)? + { + let physical_device = self.physical_device.get().unwrap().clone(); + let device = self.device.get().unwrap().clone(); + let surface = self.surface.get().unwrap().clone(); + + println!("features {:?}", features); + + let format = self.pick_surface_format(&features.formats)?; + + let presentation_mode + = self.pick_presentation_mode(&features.presentation_modes)?; + let extent = self.pick_image_extent(features.capabilities)?; + + let mut image_count = features.capabilities.min_image_count + 1; + if features.capabilities.max_image_count != 0 { + image_count + = image_count.clamp(0, features.capabilities.max_image_count); + } + + // We call this one last time. It needs to return the same thing here + // that it did before; fortunately it's simple enough that that's a safe + // assumption. + let indices = self.find_device_queue_family_indices(&physical_device)? + .unwrap(); + + let mut unique_queue_family_indices = BTreeSet::new(); + unique_queue_family_indices.insert(indices.graphics); + unique_queue_family_indices.insert(indices.presentation); + + // If there's only one queue, we use exclusive sharing mode, which + // will allow things to work without locks. Otherwise we use concurrent + // mode. + let (ordered_indices, sharing_mode) + = if unique_queue_family_indices.len() < 2 + { + (vec![indices.graphics], vk::SharingMode::EXCLUSIVE) + } else { + (vec![indices.graphics, indices.presentation], + vk::SharingMode::CONCURRENT) + }; + + let swapchain_info = vk::SwapchainCreateInfoKHR::builder() + .surface(surface) + .min_image_count(image_count) + .image_format(format.format) + .image_color_space(format.color_space) + .image_extent(extent) + .image_array_layers(1) + .image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT) + .image_sharing_mode(sharing_mode) + .queue_family_indices(&ordered_indices) + .pre_transform(features.capabilities.current_transform) + .composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE) + .present_mode(presentation_mode) + .clipped(true) + .old_swapchain(vk::SwapchainKHR::null()); + + let swapchain = unsafe { + device.create_swapchain_khr(&swapchain_info, None) + }?; + println!("swapchain {:?}", swapchain); + + self.swapchain.set(swapchain).unwrap(); } Ok(()) @@ -461,7 +610,9 @@ impl Surreality { let mut best_score = None; let mut rejected = BTreeMap::new(); - for device in unsafe { self.instance.get().unwrap().enumerate_physical_devices() }? { + 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 { @@ -476,10 +627,11 @@ impl Surreality { } Acceptable::Rejected(reason) => { let properties = unsafe { - self.instance.get().unwrap().get_physical_device_properties(device) + self.instance.get().unwrap() + .get_physical_device_properties(device) }; - let name = properties.device_name.to_string_lossy().to_string(); + let name = properties.device_name.to_string_lossy().into_owned(); rejected.insert(properties.device_id, (name, reason)); } @@ -551,6 +703,36 @@ impl Surreality { // 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(*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(_) + = self.find_device_swapchain_features(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)) } @@ -598,6 +780,84 @@ impl Surreality { } } + // We expect our caller to have already verified that the device supports + // the swapchain extension. + #[allow(unsafe_code)] + fn find_device_swapchain_features(&mut self, device: &vk::PhysicalDevice) + -> Result<Acceptable<SwapchainFeatures>> + { + let instance = self.instance.get().unwrap(); + let surface = self.surface.get().unwrap(); + + let capabilities = unsafe { + instance.get_physical_device_surface_capabilities_khr( + *device, *surface) + }?; + let formats = unsafe { + instance.get_physical_device_surface_formats_khr( + *device, *surface) + }?; + let presentation_modes = unsafe { + instance.get_physical_device_surface_present_modes_khr( + *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(SwapchainFeatures { + capabilities, formats, presentation_modes + })) + } + } + + fn pick_surface_format(&mut self, + available_formats: &Vec<vk::SurfaceFormatKHR>) + -> Result<vk::SurfaceFormatKHR> + { + for format in available_formats { + if format.format == vk::Format::B8G8R8A8_SRGB + && format.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR + { + return Ok(format.clone()); + } + } + + return Ok(available_formats[0].clone()); + } + + fn pick_presentation_mode(&mut self, + _available_modes: &Vec<vk::PresentModeKHR>) + -> Result<vk::PresentModeKHR> + { + // It's guaranteed to have this one. + return Ok(vk::PresentModeKHR::FIFO); + } + + fn pick_image_extent(&mut self, capabilities: vk::SurfaceCapabilitiesKHR) + -> Result<vk::Extent2D> + { + if capabilities.current_extent.width != u32::MAX + && capabilities.current_extent.height != u32::MAX + { + Ok(capabilities.current_extent) + } else { + let window = self.window.get().unwrap(); + let window_size = window.inner_size(); + + let width = window_size.width + .clamp(capabilities.min_image_extent.width, + capabilities.max_image_extent.width); + let height = window_size.height + .clamp(capabilities.min_image_extent.height, + capabilities.max_image_extent.height); + + Ok(vk::Extent2D::builder().width(width).height(height).build()) + } + } + fn render(&mut self, window_id: WindowId) -> Result<()> { if let Some(window) = self.window.get() && window_id == window.id() @@ -614,6 +874,12 @@ impl Surreality { impl Drop for Surreality { #[allow(unsafe_code)] fn drop(&mut self) { + if let Some(swapchain) = self.swapchain.get() + && let Some(device) = self.device.get() + { + unsafe { device.destroy_swapchain_khr(*swapchain, None) }; + } + if let Some(surface) = self.surface.get() && let Some(instance) = self.instance.get() { |