summary refs log tree commit diff
diff options
context:
space:
mode:
authorIrene Knapp <ireneista@irenes.space>2026-07-04 03:07:45 -0700
committerIrene Knapp <ireneista@irenes.space>2026-07-04 13:16:39 -0700
commitb3a7ce316f51314a4851eba34d42ed612a6da163 (patch)
treedaceea55860f0c93a736021e9ffb61084e169430
parentcfa6a8e42affa661728dcd7d69534555d3c20605 (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.rs310
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()
     {