aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-23 21:53:54 +0300
committerValentin Popov <valentin@popov.link>2026-06-23 21:53:54 +0300
commitf5fae8e84a346d4322bad06647315265eebfa21f (patch)
tree2c375fc848cf3be7db6358459900ccb0ce070589
parenta0a4089e4b75296e43a89f9a9ca2592f7fc2f68f (diff)
downloadfparkan-f5fae8e84a346d4322bad06647315265eebfa21f.tar.xz
fparkan-f5fae8e84a346d4322bad06647315265eebfa21f.zip
feat: add Vulkan surface bootstrap boundary
-rw-r--r--adapters/fparkan-render-vulkan/src/lib.rs192
-rw-r--r--fixtures/acceptance/coverage.tsv3
-rw-r--r--fixtures/acceptance/stage_0_2_roadmap.md3
3 files changed, 194 insertions, 4 deletions
diff --git a/adapters/fparkan-render-vulkan/src/lib.rs b/adapters/fparkan-render-vulkan/src/lib.rs
index 1db4f68..60e4f54 100644
--- a/adapters/fparkan-render-vulkan/src/lib.rs
+++ b/adapters/fparkan-render-vulkan/src/lib.rs
@@ -27,8 +27,8 @@
//!
//! This crate is the declared low-level Vulkan boundary.
-use ash::vk;
-use fparkan_platform::RenderRequest;
+use ash::{khr::surface, vk};
+use fparkan_platform::{NativeWindowHandles, RenderRequest};
use fparkan_render::{
canonical_capture, FrameOutput, RenderBackend, RenderCommandList, RenderError,
};
@@ -83,7 +83,7 @@ pub struct VulkanInstancePlan {
/// Created Vulkan instance probe.
pub struct VulkanInstanceProbe {
- _entry: ash::Entry,
+ entry: ash::Entry,
instance: ash::Instance,
/// Deterministic instance creation report.
pub report: VulkanInstancePlan,
@@ -96,6 +96,157 @@ impl Drop for VulkanInstanceProbe {
}
}
+/// 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: String,
+ },
+ /// A required extension pointer was not valid UTF-8.
+ InvalidExtensionName,
+ /// Surface creation failed.
+ CreateFailed {
+ /// Vulkan result.
+ result: String,
+ },
+}
+
+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) };
+ }
+}
+
+/// 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: format!("{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: format!("{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 {
+ let mut out = String::new();
+ out.push_str("{\"schema\":");
+ out.push_str(&plan.schema.to_string());
+ out.push_str(",\"required_instance_extensions\":[");
+ for (index, extension) in plan.required_instance_extensions.iter().enumerate() {
+ if index > 0 {
+ out.push(',');
+ }
+ push_json_string(&mut out, extension);
+ }
+ out.push_str("]}");
+ out
+}
+
+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)
+}
+
/// Vulkan instance bootstrap error.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum VulkanInstanceError {
@@ -198,7 +349,7 @@ pub fn create_vulkan_instance_probe(
}
})?;
Ok(VulkanInstanceProbe {
- _entry: entry,
+ entry,
instance,
report: plan,
})
@@ -1004,6 +1155,39 @@ mod tests {
);
}
+ #[test]
+ fn surface_plan_requires_native_handles() {
+ assert_eq!(
+ plan_vulkan_surface(None),
+ Err(VulkanSurfaceError::MissingNativeHandles)
+ );
+ assert_eq!(
+ VulkanSurfaceError::MissingNativeHandles.to_string(),
+ "native window/display handles are required for Vulkan surface creation"
+ );
+ }
+
+ #[test]
+ fn surface_plan_json_is_stable() {
+ assert_eq!(
+ render_surface_plan_json(&VulkanSurfacePlan {
+ schema: 1,
+ required_instance_extensions: vec![
+ "VK_KHR_surface".to_string(),
+ "VK_EXT_metal_surface".to_string(),
+ ],
+ }),
+ "{\"schema\":1,\"required_instance_extensions\":[\"VK_KHR_surface\",\"VK_EXT_metal_surface\"]}"
+ );
+ }
+
+ #[test]
+ fn static_surface_extension_name_is_decoded() {
+ let name = extension_name(ash::khr::surface::NAME.as_ptr()).expect("extension name");
+
+ assert_eq!(name, "VK_KHR_surface");
+ }
+
fn device(
name: &str,
device_type: VulkanDeviceType,
diff --git a/fixtures/acceptance/coverage.tsv b/fixtures/acceptance/coverage.tsv
index 048a654..7b24c69 100644
--- a/fixtures/acceptance/coverage.tsv
+++ b/fixtures/acceptance/coverage.tsv
@@ -33,6 +33,9 @@ S0-VK-007 covered cargo xtask policy
S0-VK-008 covered cargo test -p fparkan-render-vulkan --offline instance_plan_is_sorted_deduplicated_and_portability_aware
S0-VK-009 covered cargo test -p fparkan-render-vulkan --offline instance_plan_adds_portability_extension_when_requested
S0-VK-010 covered cargo test -p fparkan-render-vulkan --offline invalid_instance_extension_name_is_reported_before_loader_use
+S0-VK-011 covered cargo test -p fparkan-render-vulkan --offline surface_plan_requires_native_handles
+S0-VK-012 covered cargo test -p fparkan-render-vulkan --offline surface_plan_json_is_stable
+S0-VK-013 covered cargo test -p fparkan-render-vulkan --offline static_surface_extension_name_is_decoded
S0-LIMIT-001 covered cargo test -p fparkan-binary --offline rejects_count_stride_overflow
S0-LIMIT-002 covered cargo test -p fparkan-binary --offline rejects_oversized_declared_allocation_before_read
L1-P1-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
diff --git a/fixtures/acceptance/stage_0_2_roadmap.md b/fixtures/acceptance/stage_0_2_roadmap.md
index 66d8cdd..e203bad 100644
--- a/fixtures/acceptance/stage_0_2_roadmap.md
+++ b/fixtures/acceptance/stage_0_2_roadmap.md
@@ -33,6 +33,9 @@
`S0-VK-008`
`S0-VK-009`
`S0-VK-010`
+`S0-VK-011`
+`S0-VK-012`
+`S0-VK-013`
`S0-LIMIT-001`
`S0-LIMIT-002`
`L1-P1-NRES-001`