aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi.rs387
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi/instance.rs390
-rw-r--r--xtask/src/main.rs5
3 files changed, 404 insertions, 378 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs
index b897a03..070c2ad 100644
--- a/adapters/fparkan-render-vulkan/src/ffi.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi.rs
@@ -27,6 +27,16 @@
//!
//! This crate is the declared low-level Vulkan boundary.
+mod instance;
+
+pub use self::instance::{
+ create_vulkan_instance_probe, plan_vulkan_instance, probe_vulkan_loader,
+ render_instance_plan_json, render_loader_probe_report_json, vulkan_entry_symbol_name,
+ VulkanInstanceConfig, VulkanInstanceError, VulkanInstancePlan, VulkanInstanceProbe,
+ VulkanLoaderError, VulkanLoaderProbeReport,
+};
+#[cfg(test)]
+use self::instance::{cstring_vec, ensure_instance_extensions_available};
use crate::policy::*;
use crate::shader_manifest::{
triangle_shader_manifest, validate_shader_manifest, VulkanShaderManifestError,
@@ -432,60 +442,6 @@ pub(crate) const TRIANGLE_FRAGMENT_SHADER_WORDS: &[u32] = &[
0x0001_0038,
];
-/// 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 {
- entry: ash::Entry,
- 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) };
- }
-}
-
/// Deterministic Vulkan surface creation plan.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VulkanSurfacePlan {
@@ -2911,329 +2867,6 @@ fn extension_name(extension: *const c_char) -> Result<String, VulkanSurfaceError
.map_err(|_| VulkanSurfaceError::InvalidExtensionName)
}
-/// 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 {}
-
-/// 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,
- })
-}
-
-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()
-}
-
-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
- })?])
-}
-
-/// 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\":[]}",
- )
-}
-
-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()
-}
-
-/// 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 {}
-
-/// 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\"}",
- )
-}
-
#[cfg(test)]
mod tests {
use super::*;
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()
+}
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 13aa656..95866b4 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -1239,7 +1239,10 @@ fn has_safety_comment(line: &str) -> bool {
line.contains("SAFETY:")
}
-const AUDITED_UNSAFE_SOURCE_FILES: &[&str] = &["adapters/fparkan-render-vulkan/src/ffi.rs"];
+const AUDITED_UNSAFE_SOURCE_FILES: &[&str] = &[
+ "adapters/fparkan-render-vulkan/src/ffi.rs",
+ "adapters/fparkan-render-vulkan/src/ffi/instance.rs",
+];
fn is_audited_unsafe_source(path: &Path) -> bool {
let as_path = path.as_os_str().to_string_lossy();