summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.rs202
1 files changed, 181 insertions, 21 deletions
diff --git a/src/main.rs b/src/main.rs
index 5bdce3b..05c8cd5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,6 @@
 #![deny(unsafe_code)]
 use std::cell::OnceCell;
-use std::collections::HashSet;
+use std::collections::{ BTreeMap, HashSet };
 use std::ffi::{ c_void, CStr };
 use vulkanalia::{ Entry, Instance, Version };
 use vulkanalia::loader::{ LibloadingLoader, LIBRARY };
@@ -100,14 +100,36 @@ fn ignore_errors(mut body: impl FnMut() -> Result<()>) -> () {
 const VULKAN_FIRST_PORTABILITY_VERSION: Version = Version::new(1, 3, 216);
 
 
+enum Acceptable<T> {
+  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<Window>,
+
+  //   The Vulkan "entry" is the part of the Vulkan library ecosystem that's
+  // responsible for finding and loading the other parts.
   entry: OnceCell<Entry>,
+
+  //   The Vulkan "instance" is the bulk of the Vulkan library, with most of
+  // the high-level responsibilities around lifecycle management.
   instance: OnceCell<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.
   debug_messager: OnceCell<vk::DebugUtilsMessengerEXT>,
+
+  // The Vulkan "device" is the abstraction for a GPU.
+  device: OnceCell<vk::PhysicalDevice>,
 }
 
 impl Surreality {
@@ -117,6 +139,7 @@ impl Surreality {
       entry: OnceCell::new(),
       instance: OnceCell::new(),
       debug_messager: OnceCell::new(),
+      device: OnceCell::new(),
     }
   }
 
@@ -133,6 +156,10 @@ impl Surreality {
       self.init_vulkan_instance()?;
     }
 
+    if self.device.get().is_none() {
+      self.init_vulkan_device()?;
+    }
+
     Ok(())
   }
 
@@ -151,6 +178,7 @@ impl Surreality {
     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
@@ -161,9 +189,7 @@ impl Surreality {
     //   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.
-    #[allow(unsafe_code)]
     let loader = unsafe { LibloadingLoader::new(LIBRARY) }?;
-    #[allow(unsafe_code)]
     let entry = unsafe { Entry::new(loader) }?;
 
     let _ = self.entry.set(entry);
@@ -171,6 +197,7 @@ impl Surreality {
     Ok(())
   }
 
+  #[allow(unsafe_code)]
   fn init_vulkan_instance(&mut self) -> Result<()> {
     let entry = self.entry.get().unwrap();
 
@@ -186,7 +213,6 @@ impl Surreality {
     //   Before we go any further, use Vulkan's introspection to list off
     // what's available.
     let mut available_extensions = HashSet::new();
-    #[allow(unsafe_code)]
     for extension in
             unsafe { entry.enumerate_instance_extension_properties(None) }?
     {
@@ -195,7 +221,6 @@ impl Surreality {
     let available_extensions = available_extensions;
 
     let mut available_layers = HashSet::new();
-    #[allow(unsafe_code)]
     for layer in unsafe { entry.enumerate_instance_layer_properties() }? {
       available_layers.insert(layer.layer_name);
     }
@@ -287,7 +312,6 @@ impl Surreality {
       Some(debug_info)
     } else { None };
 
-    #[allow(unsafe_code)]
     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
@@ -314,6 +338,120 @@ impl Surreality {
     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<vk::PhysicalDevice> {
+    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<Acceptable<u64>>
+  {
+    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()
@@ -328,18 +466,17 @@ impl Surreality {
 }
 
 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()
     {
-      #[allow(unsafe_code)]
       unsafe {
         instance.destroy_debug_utils_messenger_ext(*debug_messager, None);
       }
     }
 
     if let Some(instance) = self.instance.get() {
-      #[allow(unsafe_code)]
       unsafe { instance.destroy_instance(None) };
     }
   }
@@ -394,28 +531,51 @@ fn main() -> std::process::ExitCode {
 }
 
 
+#[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
 {
-  #[allow(unsafe_code)]
-  let data = unsafe { *data };
-  #[allow(unsafe_code)]
-  let text = unsafe { CStr::from_ptr(data.message) }.to_string_lossy();
-
-  let severity = if severity >= vk::DebugUtilsMessageSeverityFlagsEXT::ERROR {
-    "error"
-  } else if severity >= vk::DebugUtilsMessageSeverityFlagsEXT::WARNING {
-    "warning"
-  } else if severity >= vk::DebugUtilsMessageSeverityFlagsEXT::INFO {
-    "informational message"
+  //   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 {
-    "message of unknown, very minor severity"
+    vk::DebugUtilsMessageSeverityFlagsEXT::WARNING
   };
 
-  eprintln!("Vulkan {}: {} (flags {:?})", severity, text, flags);
+  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