aboutsummaryrefslogtreecommitdiff
path: root/adapters/fparkan-render-vulkan/src/ffi/instance.rs
diff options
context:
space:
mode:
Diffstat (limited to 'adapters/fparkan-render-vulkan/src/ffi/instance.rs')
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/instance.rs390
1 files changed, 390 insertions, 0 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi/instance.rs b/adapters/fparkan-render-vulkan/src/ffi/instance.rs
new file mode 100644
index 0000000..50a056e
--- /dev/null
+++ b/adapters/fparkan-render-vulkan/src/ffi/instance.rs
@@ -0,0 +1,390 @@
+#![allow(unsafe_code)]
+
+use ash::vk;
+use serde::Serialize;
+use std::collections::BTreeSet;
+use std::ffi::{CStr, CString};
+use std::os::raw::c_char;
+
+use super::{
+ EXT_DEBUG_UTILS_EXTENSION, KHR_PORTABILITY_ENUMERATION_EXTENSION, MIN_VULKAN_API_VERSION,
+ VALIDATION_LAYER_NAME,
+};
+use crate::policy::{format_api_version, serialize_json_or_fallback};
+
+/// Vulkan instance bootstrap configuration.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanInstanceConfig {
+ /// Application name reported to the loader.
+ pub application_name: String,
+ /// Required instance extensions, usually including surface extensions.
+ pub required_extensions: Vec<String>,
+ /// Whether `VK_KHR_portability_enumeration` and its create flag are enabled.
+ pub enable_portability_enumeration: bool,
+ /// Whether validation layers are requested.
+ pub enable_validation: bool,
+}
+
+impl VulkanInstanceConfig {
+ /// Returns a conservative instance configuration for smoke probes.
+ #[must_use]
+ pub fn smoke(application_name: impl Into<String>) -> Self {
+ Self {
+ application_name: application_name.into(),
+ required_extensions: Vec::new(),
+ enable_portability_enumeration: cfg!(target_os = "macos"),
+ enable_validation: false,
+ }
+ }
+}
+
+/// Deterministic Vulkan instance creation plan.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanInstancePlan {
+ /// Report schema version.
+ pub schema: u32,
+ /// Instance extensions requested at creation time.
+ pub enabled_extensions: Vec<String>,
+ /// Raw Vulkan instance creation flags.
+ pub create_flags: u32,
+ /// Whether validation was requested.
+ pub validation_requested: bool,
+}
+
+/// Created Vulkan instance probe.
+pub struct VulkanInstanceProbe {
+ pub(super) entry: ash::Entry,
+ pub(super) instance: ash::Instance,
+ /// Deterministic instance creation report.
+ pub report: VulkanInstancePlan,
+}
+
+impl Drop for VulkanInstanceProbe {
+ fn drop(&mut self) {
+ // SAFETY: The `Instance` was created by this probe and is destroyed once during drop.
+ unsafe { self.instance.destroy_instance(None) };
+ }
+}
+
+/// Vulkan instance bootstrap error.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum VulkanInstanceError {
+ /// The Vulkan loader could not be opened.
+ Loader(VulkanLoaderError),
+ /// Application name contained an interior NUL byte.
+ InvalidApplicationName,
+ /// An extension name contained an interior NUL byte.
+ InvalidExtensionName {
+ /// Invalid extension name.
+ extension: String,
+ },
+ /// A required instance extension is unavailable from the loader.
+ MissingInstanceExtension {
+ /// Required extension name.
+ extension: String,
+ },
+ /// Validation layers were requested but unavailable.
+ MissingValidationLayer,
+ /// Instance creation failed.
+ CreateFailed {
+ /// Vulkan result.
+ result: vk::Result,
+ },
+}
+
+impl std::fmt::Display for VulkanInstanceError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Loader(error) => write!(f, "{error}"),
+ Self::InvalidApplicationName => {
+ write!(f, "Vulkan application name contains an interior NUL byte")
+ }
+ Self::InvalidExtensionName { extension } => {
+ write!(
+ f,
+ "Vulkan instance extension name contains an interior NUL byte: {extension:?}"
+ )
+ }
+ Self::MissingInstanceExtension { extension } => {
+ write!(f, "Vulkan instance extension {extension} is unavailable")
+ }
+ Self::MissingValidationLayer => {
+ write!(
+ f,
+ "Vulkan validation layer VK_LAYER_KHRONOS_validation is unavailable"
+ )
+ }
+ Self::CreateFailed { result } => {
+ write!(f, "Vulkan instance creation failed: {result:?}")
+ }
+ }
+ }
+}
+
+impl std::error::Error for VulkanInstanceError {}
+
+/// Deterministic Vulkan loader probe report.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanLoaderProbeReport {
+ /// Report schema version.
+ pub schema: u32,
+ /// Whether the Vulkan loader was opened successfully.
+ pub loader_available: bool,
+ /// Reported loader instance API version.
+ pub instance_api_version: u32,
+}
+
+/// Vulkan loader bootstrap error.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum VulkanLoaderError {
+ /// The Vulkan loader library could not be opened.
+ Unavailable {
+ /// Loader error text.
+ message: String,
+ },
+}
+
+impl std::fmt::Display for VulkanLoaderError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Unavailable { message } => {
+ write!(f, "Vulkan loader is unavailable: {message}")
+ }
+ }
+ }
+}
+
+impl std::error::Error for VulkanLoaderError {}
+
+/// Builds the deterministic instance creation plan without touching the loader.
+#[must_use]
+pub fn plan_vulkan_instance(config: &VulkanInstanceConfig) -> VulkanInstancePlan {
+ let mut enabled_extensions = config.required_extensions.clone();
+ if config.enable_validation
+ && !enabled_extensions
+ .iter()
+ .any(|extension| extension == EXT_DEBUG_UTILS_EXTENSION)
+ {
+ enabled_extensions.push(EXT_DEBUG_UTILS_EXTENSION.to_string());
+ }
+ if config.enable_portability_enumeration
+ && !enabled_extensions
+ .iter()
+ .any(|extension| extension == KHR_PORTABILITY_ENUMERATION_EXTENSION)
+ {
+ enabled_extensions.push(KHR_PORTABILITY_ENUMERATION_EXTENSION.to_string());
+ }
+ enabled_extensions.sort();
+ enabled_extensions.dedup();
+ VulkanInstancePlan {
+ schema: 1,
+ enabled_extensions,
+ create_flags: if config.enable_portability_enumeration {
+ vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR.as_raw()
+ } else {
+ 0
+ },
+ validation_requested: config.enable_validation,
+ }
+}
+
+/// Creates a Vulkan instance probe from the supplied configuration.
+///
+/// # Errors
+///
+/// Returns [`VulkanInstanceError`] when the loader is unavailable, names are not
+/// valid C strings, or `vkCreateInstance` fails.
+pub fn create_vulkan_instance_probe(
+ config: &VulkanInstanceConfig,
+) -> Result<VulkanInstanceProbe, VulkanInstanceError> {
+ // SAFETY: Loading the entry only resolves loader symbols; no raw Vulkan handles escape.
+ let entry = unsafe { ash::Entry::load() }.map_err(|error| {
+ VulkanInstanceError::Loader(VulkanLoaderError::Unavailable {
+ message: error.to_string(),
+ })
+ })?;
+ let app_name = CString::new(config.application_name.clone())
+ .map_err(|_| VulkanInstanceError::InvalidApplicationName)?;
+ let engine_name = c"fparkan";
+ let plan = plan_vulkan_instance(config);
+ let available_extensions = available_instance_extensions(&entry)?;
+ ensure_instance_extensions_available(&plan.enabled_extensions, &available_extensions)?;
+ let extension_names = cstring_vec(&plan.enabled_extensions)?;
+ let extension_ptrs = cstring_ptrs(&extension_names);
+ let layer_names = validation_layer_cstrings(&entry, config.enable_validation)?;
+ let layer_ptrs = cstring_ptrs(&layer_names);
+ let app_info = vk::ApplicationInfo::default()
+ .application_name(&app_name)
+ .application_version(0)
+ .engine_name(engine_name)
+ .engine_version(0)
+ .api_version(MIN_VULKAN_API_VERSION);
+ let create_info = vk::InstanceCreateInfo::default()
+ .application_info(&app_info)
+ .enabled_extension_names(&extension_ptrs)
+ .enabled_layer_names(&layer_ptrs)
+ .flags(vk::InstanceCreateFlags::from_raw(plan.create_flags));
+ // SAFETY: `create_info` points to stack-owned Vulkan create data that lives for the call.
+ let instance = unsafe { entry.create_instance(&create_info, None) }
+ .map_err(|error| VulkanInstanceError::CreateFailed { result: error })?;
+ Ok(VulkanInstanceProbe {
+ entry,
+ instance,
+ report: plan,
+ })
+}
+
+/// Renders a deterministic JSON Vulkan instance plan.
+#[must_use]
+pub fn render_instance_plan_json(plan: &VulkanInstancePlan) -> String {
+ #[derive(Serialize)]
+ struct InstancePlanJson<'a> {
+ schema: u32,
+ create_flags: u32,
+ validation_requested: bool,
+ enabled_extensions: &'a [String],
+ }
+
+ serialize_json_or_fallback(
+ &InstancePlanJson {
+ schema: plan.schema,
+ create_flags: plan.create_flags,
+ validation_requested: plan.validation_requested,
+ enabled_extensions: &plan.enabled_extensions,
+ },
+ "{\"schema\":0,\"create_flags\":0,\"validation_requested\":false,\"enabled_extensions\":[]}",
+ )
+}
+
+/// Opens the Vulkan loader and reports the supported instance API version.
+///
+/// # Errors
+///
+/// Returns [`VulkanLoaderError`] when no Vulkan loader library can be opened on
+/// the host.
+pub fn probe_vulkan_loader() -> Result<VulkanLoaderProbeReport, VulkanLoaderError> {
+ // SAFETY: Loading the entry only resolves loader symbols; no raw Vulkan handles escape.
+ let entry = unsafe { ash::Entry::load() }.map_err(|error| VulkanLoaderError::Unavailable {
+ message: error.to_string(),
+ })?;
+ // SAFETY: The resolved entry only queries the loader-supported instance API version.
+ let version = unsafe { entry.try_enumerate_instance_version() }
+ .map_err(|error| VulkanLoaderError::Unavailable {
+ message: error.to_string(),
+ })?
+ .unwrap_or(vk::API_VERSION_1_0);
+ Ok(VulkanLoaderProbeReport {
+ schema: 1,
+ loader_available: true,
+ instance_api_version: version,
+ })
+}
+
+/// Returns the static Vulkan entry name used by loader probes.
+#[must_use]
+pub fn vulkan_entry_symbol_name() -> &'static CStr {
+ c"vkGetInstanceProcAddr"
+}
+
+/// Renders a deterministic JSON Vulkan loader report.
+#[must_use]
+pub fn render_loader_probe_report_json(report: &VulkanLoaderProbeReport) -> String {
+ #[derive(Serialize)]
+ struct LoaderProbeReportJson {
+ schema: u32,
+ loader_available: bool,
+ instance_api: String,
+ }
+
+ serialize_json_or_fallback(
+ &LoaderProbeReportJson {
+ schema: report.schema,
+ loader_available: report.loader_available,
+ instance_api: format_api_version(report.instance_api_version),
+ },
+ "{\"schema\":0,\"loader_available\":false,\"instance_api\":\"0.0.0\"}",
+ )
+}
+
+fn available_instance_extensions(entry: &ash::Entry) -> Result<Vec<String>, VulkanInstanceError> {
+ let available_extensions =
+ // SAFETY: Enumerating instance extensions reads loader-owned immutable metadata.
+ unsafe { entry.enumerate_instance_extension_properties(None) }.map_err(|error| {
+ VulkanInstanceError::CreateFailed {
+ result: error,
+ }
+ })?;
+ available_extensions
+ .into_iter()
+ .map(|extension| {
+ // SAFETY: Vulkan extension names are fixed-size NUL-terminated strings from the loader.
+ Ok(unsafe { CStr::from_ptr(extension.extension_name.as_ptr()) }
+ .to_string_lossy()
+ .into_owned())
+ })
+ .collect()
+}
+
+pub(super) fn ensure_instance_extensions_available(
+ required_extensions: &[String],
+ available_extensions: &[String],
+) -> Result<(), VulkanInstanceError> {
+ let available = available_extensions
+ .iter()
+ .map(String::as_str)
+ .collect::<BTreeSet<_>>();
+ for extension in required_extensions {
+ if !available.contains(extension.as_str()) {
+ return Err(VulkanInstanceError::MissingInstanceExtension {
+ extension: extension.clone(),
+ });
+ }
+ }
+ Ok(())
+}
+
+fn validation_layer_cstrings(
+ entry: &ash::Entry,
+ enable_validation: bool,
+) -> Result<Vec<CString>, VulkanInstanceError> {
+ if !enable_validation {
+ return Ok(Vec::new());
+ }
+ let available_layers =
+ // SAFETY: Enumerating instance layers reads loader-owned immutable metadata.
+ unsafe { entry.enumerate_instance_layer_properties() }.map_err(|error| {
+ VulkanInstanceError::CreateFailed {
+ result: error,
+ }
+ })?;
+ let validation_available = available_layers.iter().any(|layer| {
+ // SAFETY: Vulkan layer names are fixed-size NUL-terminated strings from the loader.
+ unsafe { CStr::from_ptr(layer.layer_name.as_ptr()) }
+ .to_string_lossy()
+ .as_ref()
+ == VALIDATION_LAYER_NAME
+ });
+ if !validation_available {
+ return Err(VulkanInstanceError::MissingValidationLayer);
+ }
+ Ok(vec![CString::new(VALIDATION_LAYER_NAME).map_err(|_| {
+ VulkanInstanceError::InvalidApplicationName
+ })?])
+}
+
+pub(super) fn cstring_vec(values: &[String]) -> Result<Vec<CString>, VulkanInstanceError> {
+ values
+ .iter()
+ .map(|extension| {
+ CString::new(extension.as_str()).map_err(|_| {
+ VulkanInstanceError::InvalidExtensionName {
+ extension: extension.clone(),
+ }
+ })
+ })
+ .collect()
+}
+
+fn cstring_ptrs(values: &[CString]) -> Vec<*const c_char> {
+ values.iter().map(|value| value.as_ptr()).collect()
+}