refactor(vulkan-ffi): extract surface bootstrap module

This commit is contained in:
2026-06-25 06:12:20 +04:00
parent 1eead8d597
commit b6b47ae6f6
3 changed files with 168 additions and 155 deletions
+8 -155
View File
@@ -28,6 +28,7 @@
//! This crate is the declared low-level Vulkan boundary. //! This crate is the declared low-level Vulkan boundary.
mod instance; mod instance;
mod surface;
pub use self::instance::{ pub use self::instance::{
create_vulkan_instance_probe, plan_vulkan_instance, probe_vulkan_loader, create_vulkan_instance_probe, plan_vulkan_instance, probe_vulkan_loader,
@@ -37,19 +38,20 @@ pub use self::instance::{
}; };
#[cfg(test)] #[cfg(test)]
use self::instance::{cstring_vec, ensure_instance_extensions_available}; use self::instance::{cstring_vec, ensure_instance_extensions_available};
#[cfg(test)]
use self::surface::extension_name;
pub use self::surface::{
create_vulkan_surface_probe, plan_vulkan_surface, render_surface_plan_json, VulkanSurfaceError,
VulkanSurfacePlan, VulkanSurfaceProbe,
};
use crate::policy::*; use crate::policy::*;
use crate::shader_manifest::{ use crate::shader_manifest::{
triangle_shader_manifest, validate_shader_manifest, VulkanShaderManifestError, triangle_shader_manifest, validate_shader_manifest, VulkanShaderManifestError,
}; };
use ash::{ use ash::{khr::swapchain, vk};
khr::{surface, swapchain},
vk,
};
use fparkan_platform::NativeWindowHandles; use fparkan_platform::NativeWindowHandles;
use serde::Serialize;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::ffi::{CStr, CString}; use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Mutex; use std::sync::Mutex;
/// Minimum Vulkan API version accepted by the Stage 0 backend. /// Minimum Vulkan API version accepted by the Stage 0 backend.
@@ -442,74 +444,6 @@ pub(crate) const TRIANGLE_FRAGMENT_SHADER_WORDS: &[u32] = &[
0x0001_0038, 0x0001_0038,
]; ];
/// Deterministic Vulkan surface creation plan.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VulkanSurfacePlan {
/// Report schema version.
pub schema: u32,
/// Instance extensions required by the native display backend.
pub required_instance_extensions: Vec<String>,
}
/// Vulkan surface bootstrap error.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum VulkanSurfaceError {
/// No native raw window/display handles were available.
MissingNativeHandles,
/// Required platform surface extensions could not be enumerated.
RequiredExtensionsFailed {
/// Vulkan result.
result: vk::Result,
},
/// A required extension pointer was not valid UTF-8.
InvalidExtensionName,
/// Surface creation failed.
CreateFailed {
/// Vulkan result.
result: vk::Result,
},
}
impl std::fmt::Display for VulkanSurfaceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingNativeHandles => {
write!(
f,
"native window/display handles are required for Vulkan surface creation"
)
}
Self::RequiredExtensionsFailed { result } => write!(
f,
"failed to enumerate required Vulkan surface extensions: {result:?}"
),
Self::InvalidExtensionName => {
write!(f, "Vulkan surface extension name is not valid UTF-8")
}
Self::CreateFailed { result } => {
write!(f, "Vulkan surface creation failed: {result:?}")
}
}
}
}
impl std::error::Error for VulkanSurfaceError {}
/// Created Vulkan surface probe.
pub struct VulkanSurfaceProbe {
loader: surface::Instance,
surface: vk::SurfaceKHR,
/// Deterministic surface creation report.
pub report: VulkanSurfacePlan,
}
impl Drop for VulkanSurfaceProbe {
fn drop(&mut self) {
// SAFETY: The `SurfaceKHR` was created by this probe and is destroyed once during drop.
unsafe { self.loader.destroy_surface(self.surface, None) };
}
}
/// Live Vulkan device/surface capability probe. /// Live Vulkan device/surface capability probe.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct VulkanRuntimeCapabilityProbe { pub struct VulkanRuntimeCapabilityProbe {
@@ -2308,61 +2242,6 @@ impl std::fmt::Display for VulkanSwapchainProbeError {
impl std::error::Error for VulkanSwapchainProbeError {} impl std::error::Error for VulkanSwapchainProbeError {}
/// Builds a deterministic Vulkan surface plan from native window handles.
///
/// # Errors
///
/// Returns [`VulkanSurfaceError`] when no native handles exist or the platform
/// display backend has no Vulkan surface extension mapping.
pub fn plan_vulkan_surface(
handles: Option<NativeWindowHandles>,
) -> Result<VulkanSurfacePlan, VulkanSurfaceError> {
let handles = handles.ok_or(VulkanSurfaceError::MissingNativeHandles)?;
let required = ash_window::enumerate_required_extensions(handles.display)
.map_err(|error| VulkanSurfaceError::RequiredExtensionsFailed { result: error })?;
let mut required_instance_extensions = Vec::with_capacity(required.len());
for extension in required {
let name = extension_name(*extension)?;
required_instance_extensions.push(name);
}
required_instance_extensions.sort();
required_instance_extensions.dedup();
Ok(VulkanSurfacePlan {
schema: 1,
required_instance_extensions,
})
}
/// Creates a Vulkan surface probe from native window handles.
///
/// # Errors
///
/// Returns [`VulkanSurfaceError`] when handles are missing, required extensions
/// cannot be planned, or `vkCreate*SurfaceKHR` fails.
pub fn create_vulkan_surface_probe(
instance: &VulkanInstanceProbe,
handles: Option<NativeWindowHandles>,
) -> Result<VulkanSurfaceProbe, VulkanSurfaceError> {
let handles = handles.ok_or(VulkanSurfaceError::MissingNativeHandles)?;
let report = plan_vulkan_surface(Some(handles))?;
// SAFETY: The platform handles are only used to create a child surface owned by this probe.
let surface = unsafe {
ash_window::create_surface(
&instance.entry,
&instance.instance,
handles.display,
handles.window,
None,
)
}
.map_err(|error| VulkanSurfaceError::CreateFailed { result: error })?;
Ok(VulkanSurfaceProbe {
loader: surface::Instance::new(&instance.entry, &instance.instance),
surface,
report,
})
}
/// Probes live Vulkan device, queue, surface and swapchain capabilities. /// Probes live Vulkan device, queue, surface and swapchain capabilities.
/// ///
/// # Errors /// # Errors
@@ -2841,32 +2720,6 @@ fn live_surface_capabilities(
}) })
} }
/// Renders a deterministic JSON Vulkan surface plan.
#[must_use]
pub fn render_surface_plan_json(plan: &VulkanSurfacePlan) -> String {
#[derive(Serialize)]
struct SurfacePlanJson<'a> {
schema: u32,
required_instance_extensions: &'a [String],
}
serialize_json_or_fallback(
&SurfacePlanJson {
schema: plan.schema,
required_instance_extensions: &plan.required_instance_extensions,
},
"{\"schema\":0,\"required_instance_extensions\":[]}",
)
}
fn extension_name(extension: *const c_char) -> Result<String, VulkanSurfaceError> {
// SAFETY: `ash-window` returns extension pointers to static NUL-terminated Vulkan names.
let name = unsafe { CStr::from_ptr(extension) };
name.to_str()
.map(str::to_string)
.map_err(|_| VulkanSurfaceError::InvalidExtensionName)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -0,0 +1,159 @@
#![allow(unsafe_code)]
use ash::{khr::surface, vk};
use fparkan_platform::NativeWindowHandles;
use serde::Serialize;
use std::ffi::CStr;
use std::os::raw::c_char;
use super::VulkanInstanceProbe;
use crate::policy::serialize_json_or_fallback;
/// Deterministic Vulkan surface creation plan.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VulkanSurfacePlan {
/// Report schema version.
pub schema: u32,
/// Instance extensions required by the native display backend.
pub required_instance_extensions: Vec<String>,
}
/// Vulkan surface bootstrap error.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum VulkanSurfaceError {
/// No native raw window/display handles were available.
MissingNativeHandles,
/// Required platform surface extensions could not be enumerated.
RequiredExtensionsFailed {
/// Vulkan result.
result: vk::Result,
},
/// A required extension pointer was not valid UTF-8.
InvalidExtensionName,
/// Surface creation failed.
CreateFailed {
/// Vulkan result.
result: vk::Result,
},
}
impl std::fmt::Display for VulkanSurfaceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingNativeHandles => {
write!(
f,
"native window/display handles are required for Vulkan surface creation"
)
}
Self::RequiredExtensionsFailed { result } => write!(
f,
"failed to enumerate required Vulkan surface extensions: {result:?}"
),
Self::InvalidExtensionName => {
write!(f, "Vulkan surface extension name is not valid UTF-8")
}
Self::CreateFailed { result } => {
write!(f, "Vulkan surface creation failed: {result:?}")
}
}
}
}
impl std::error::Error for VulkanSurfaceError {}
/// Created Vulkan surface probe.
pub struct VulkanSurfaceProbe {
pub(super) loader: surface::Instance,
pub(super) surface: vk::SurfaceKHR,
/// Deterministic surface creation report.
pub report: VulkanSurfacePlan,
}
impl Drop for VulkanSurfaceProbe {
fn drop(&mut self) {
// SAFETY: The `SurfaceKHR` was created by this probe and is destroyed once during drop.
unsafe { self.loader.destroy_surface(self.surface, None) };
}
}
/// Builds a deterministic Vulkan surface plan from native window handles.
///
/// # Errors
///
/// Returns [`VulkanSurfaceError`] when no native handles exist or the platform
/// display backend has no Vulkan surface extension mapping.
pub fn plan_vulkan_surface(
handles: Option<NativeWindowHandles>,
) -> Result<VulkanSurfacePlan, VulkanSurfaceError> {
let handles = handles.ok_or(VulkanSurfaceError::MissingNativeHandles)?;
let required = ash_window::enumerate_required_extensions(handles.display)
.map_err(|error| VulkanSurfaceError::RequiredExtensionsFailed { result: error })?;
let mut required_instance_extensions = Vec::with_capacity(required.len());
for extension in required {
let name = extension_name(*extension)?;
required_instance_extensions.push(name);
}
required_instance_extensions.sort();
required_instance_extensions.dedup();
Ok(VulkanSurfacePlan {
schema: 1,
required_instance_extensions,
})
}
/// Creates a Vulkan surface probe from native window handles.
///
/// # Errors
///
/// Returns [`VulkanSurfaceError`] when handles are missing, required extensions
/// cannot be planned, or `vkCreate*SurfaceKHR` fails.
pub fn create_vulkan_surface_probe(
instance: &VulkanInstanceProbe,
handles: Option<NativeWindowHandles>,
) -> Result<VulkanSurfaceProbe, VulkanSurfaceError> {
let handles = handles.ok_or(VulkanSurfaceError::MissingNativeHandles)?;
let report = plan_vulkan_surface(Some(handles))?;
// SAFETY: The platform handles are only used to create a child surface owned by this probe.
let surface = unsafe {
ash_window::create_surface(
&instance.entry,
&instance.instance,
handles.display,
handles.window,
None,
)
}
.map_err(|error| VulkanSurfaceError::CreateFailed { result: error })?;
Ok(VulkanSurfaceProbe {
loader: surface::Instance::new(&instance.entry, &instance.instance),
surface,
report,
})
}
/// Renders a deterministic JSON Vulkan surface plan.
#[must_use]
pub fn render_surface_plan_json(plan: &VulkanSurfacePlan) -> String {
#[derive(Serialize)]
struct SurfacePlanJson<'a> {
schema: u32,
required_instance_extensions: &'a [String],
}
serialize_json_or_fallback(
&SurfacePlanJson {
schema: plan.schema,
required_instance_extensions: &plan.required_instance_extensions,
},
"{\"schema\":0,\"required_instance_extensions\":[]}",
)
}
pub(super) fn extension_name(extension: *const c_char) -> Result<String, VulkanSurfaceError> {
// SAFETY: `ash-window` returns extension pointers to static NUL-terminated Vulkan names.
let name = unsafe { CStr::from_ptr(extension) };
name.to_str()
.map(str::to_string)
.map_err(|_| VulkanSurfaceError::InvalidExtensionName)
}
+1
View File
@@ -1242,6 +1242,7 @@ fn has_safety_comment(line: &str) -> bool {
const AUDITED_UNSAFE_SOURCE_FILES: &[&str] = &[ const AUDITED_UNSAFE_SOURCE_FILES: &[&str] = &[
"adapters/fparkan-render-vulkan/src/ffi.rs", "adapters/fparkan-render-vulkan/src/ffi.rs",
"adapters/fparkan-render-vulkan/src/ffi/instance.rs", "adapters/fparkan-render-vulkan/src/ffi/instance.rs",
"adapters/fparkan-render-vulkan/src/ffi/surface.rs",
]; ];
fn is_audited_unsafe_source(path: &Path) -> bool { fn is_audited_unsafe_source(path: &Path) -> bool {