aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-24 00:05:31 +0300
committerValentin Popov <valentin@popov.link>2026-06-24 00:05:31 +0300
commitd41add32c48f28dd498271b1552daceba8c85600 (patch)
tree9e962c184da561a241e3ce45154360f046b58b6f
parent159731664fae9ea3f08ec594985b82248988732d (diff)
downloadfparkan-d41add32c48f28dd498271b1552daceba8c85600.tar.xz
fparkan-d41add32c48f28dd498271b1552daceba8c85600.zip
feat: create Vulkan swapchain probe
-rw-r--r--adapters/fparkan-render-vulkan/src/lib.rs160
-rw-r--r--apps/fparkan-vulkan-smoke/src/main.rs43
-rw-r--r--fixtures/acceptance/coverage.tsv3
-rw-r--r--fixtures/acceptance/stage_0_2_roadmap.md1
-rw-r--r--xtask/src/main.rs4
5 files changed, 195 insertions, 16 deletions
diff --git a/adapters/fparkan-render-vulkan/src/lib.rs b/adapters/fparkan-render-vulkan/src/lib.rs
index d2345e1..68009b2 100644
--- a/adapters/fparkan-render-vulkan/src/lib.rs
+++ b/adapters/fparkan-render-vulkan/src/lib.rs
@@ -27,7 +27,10 @@
//!
//! This crate is the declared low-level Vulkan boundary.
-use ash::{khr::surface, vk};
+use ash::{
+ khr::{surface, swapchain},
+ vk,
+};
use fparkan_binary::{sha256, sha256_hex};
use fparkan_platform::{NativeWindowHandles, RenderRequest};
use fparkan_render::{
@@ -323,6 +326,7 @@ pub struct VulkanRuntimeCapabilityProbe {
/// Created Vulkan logical device probe.
pub struct VulkanLogicalDeviceProbe {
device: ash::Device,
+ physical_device: vk::PhysicalDevice,
/// Runtime capability report used for device selection.
pub runtime: VulkanRuntimeCapabilityProbe,
/// Deterministic logical device creation report.
@@ -351,6 +355,32 @@ pub struct VulkanLogicalDeviceReport {
pub enabled_extensions: Vec<String>,
}
+/// Created Vulkan swapchain probe.
+pub struct VulkanSwapchainProbe {
+ loader: swapchain::Device,
+ swapchain: vk::SwapchainKHR,
+ /// Deterministic swapchain creation report.
+ pub report: VulkanSwapchainReport,
+}
+
+impl Drop for VulkanSwapchainProbe {
+ fn drop(&mut self) {
+ // SAFETY: The swapchain was created by this probe and is destroyed once during drop.
+ unsafe { self.loader.destroy_swapchain(self.swapchain, None) };
+ }
+}
+
+/// Runtime swapchain creation report.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct VulkanSwapchainReport {
+ /// Report schema version.
+ pub schema: u32,
+ /// Deterministic swapchain policy used for creation.
+ pub plan: VulkanSwapchainPlan,
+ /// Number of images returned by `vkGetSwapchainImagesKHR`.
+ pub image_count: u32,
+}
+
/// Live Vulkan device/surface capability probe error.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum VulkanRuntimeCapabilityError {
@@ -479,6 +509,44 @@ impl std::fmt::Display for VulkanLogicalDeviceError {
impl std::error::Error for VulkanLogicalDeviceError {}
+/// Vulkan swapchain creation error.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum VulkanSwapchainProbeError {
+ /// Surface capability query failed.
+ SurfaceCapabilitiesFailed {
+ /// Vulkan result.
+ result: String,
+ },
+ /// Swapchain creation failed.
+ CreateFailed {
+ /// Vulkan result.
+ result: String,
+ },
+ /// Swapchain image query failed.
+ ImagesFailed {
+ /// Vulkan result.
+ result: String,
+ },
+}
+
+impl std::fmt::Display for VulkanSwapchainProbeError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::SurfaceCapabilitiesFailed { result } => {
+ write!(f, "Vulkan surface capabilities query failed: {result}")
+ }
+ Self::CreateFailed { result } => {
+ write!(f, "Vulkan swapchain creation failed: {result}")
+ }
+ Self::ImagesFailed { result } => {
+ write!(f, "Vulkan swapchain image query failed: {result}")
+ }
+ }
+ }
+}
+
+impl std::error::Error for VulkanSwapchainProbeError {}
+
/// Builds a deterministic Vulkan surface plan from native window handles.
///
/// # Errors
@@ -606,6 +674,7 @@ pub fn create_vulkan_logical_device_probe(
let _present_queue = unsafe { device.get_device_queue(capability.present_queue_family, 0) };
Ok(VulkanLogicalDeviceProbe {
device,
+ physical_device: selected.physical_device,
report: VulkanLogicalDeviceReport {
schema: 1,
device_name: capability.device_name.clone(),
@@ -617,6 +686,83 @@ pub fn create_vulkan_logical_device_probe(
})
}
+/// Creates a Vulkan swapchain for the live logical device and surface.
+///
+/// # Errors
+///
+/// Returns [`VulkanSwapchainProbeError`] when live surface capability queries,
+/// swapchain creation, or swapchain image enumeration fails.
+pub fn create_vulkan_swapchain_probe(
+ instance: &VulkanInstanceProbe,
+ surface: &VulkanSurfaceProbe,
+ device: &VulkanLogicalDeviceProbe,
+) -> Result<VulkanSwapchainProbe, VulkanSwapchainProbeError> {
+ let raw_capabilities = {
+ // SAFETY: The physical device and surface are live query inputs and no handles are retained.
+ unsafe {
+ surface
+ .loader
+ .get_physical_device_surface_capabilities(device.physical_device, surface.surface)
+ }
+ }
+ .map_err(
+ |error| VulkanSwapchainProbeError::SurfaceCapabilitiesFailed {
+ result: format!("{error:?}"),
+ },
+ )?;
+ let plan = &device.runtime.swapchain;
+ let queue_family_indices = unique_queue_families(
+ device.runtime.capability.graphics_queue_family,
+ device.runtime.capability.present_queue_family,
+ );
+ let sharing_mode = if queue_family_indices.len() > 1 {
+ vk::SharingMode::CONCURRENT
+ } else {
+ vk::SharingMode::EXCLUSIVE
+ };
+ let create_info = vk::SwapchainCreateInfoKHR::default()
+ .surface(surface.surface)
+ .min_image_count(plan.image_count)
+ .image_format(vk::Format::from_raw(plan.format.format))
+ .image_color_space(vk::ColorSpaceKHR::from_raw(plan.format.color_space))
+ .image_extent(vk::Extent2D {
+ width: plan.extent.0,
+ height: plan.extent.1,
+ })
+ .image_array_layers(1)
+ .image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT)
+ .image_sharing_mode(sharing_mode)
+ .queue_family_indices(&queue_family_indices)
+ .pre_transform(raw_capabilities.current_transform)
+ .composite_alpha(select_composite_alpha(
+ raw_capabilities.supported_composite_alpha,
+ ))
+ .present_mode(vk::PresentModeKHR::from_raw(plan.present_mode))
+ .clipped(true);
+ let loader = swapchain::Device::new(&instance.instance, &device.device);
+ // SAFETY: The create info references live instance/device/surface handles for this call.
+ let swapchain = unsafe { loader.create_swapchain(&create_info, None) }.map_err(|error| {
+ VulkanSwapchainProbeError::CreateFailed {
+ result: format!("{error:?}"),
+ }
+ })?;
+ // SAFETY: The swapchain was created above and the returned image handles are owned by it.
+ let images = unsafe { loader.get_swapchain_images(swapchain) }.map_err(|error| {
+ VulkanSwapchainProbeError::ImagesFailed {
+ result: format!("{error:?}"),
+ }
+ })?;
+ Ok(VulkanSwapchainProbe {
+ loader,
+ swapchain,
+ report: VulkanSwapchainReport {
+ schema: 1,
+ plan: plan.clone(),
+ image_count: images.len().try_into().unwrap_or(u32::MAX),
+ },
+ })
+}
+
fn select_live_device_candidate(
instance: &VulkanInstanceProbe,
surface: &VulkanSurfaceProbe,
@@ -1665,6 +1811,18 @@ fn select_image_count(capabilities: VulkanSwapchainSurfaceCapabilities) -> u32 {
}
}
+fn select_composite_alpha(supported: vk::CompositeAlphaFlagsKHR) -> vk::CompositeAlphaFlagsKHR {
+ if supported.contains(vk::CompositeAlphaFlagsKHR::OPAQUE) {
+ vk::CompositeAlphaFlagsKHR::OPAQUE
+ } else if supported.contains(vk::CompositeAlphaFlagsKHR::PRE_MULTIPLIED) {
+ vk::CompositeAlphaFlagsKHR::PRE_MULTIPLIED
+ } else if supported.contains(vk::CompositeAlphaFlagsKHR::POST_MULTIPLIED) {
+ vk::CompositeAlphaFlagsKHR::POST_MULTIPLIED
+ } else {
+ vk::CompositeAlphaFlagsKHR::INHERIT
+ }
+}
+
/// Builds a deterministic swapchain recreation report.
#[must_use]
pub const fn swapchain_recreation_report(
diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs
index 9425c31..f742502 100644
--- a/apps/fparkan-vulkan-smoke/src/main.rs
+++ b/apps/fparkan-vulkan-smoke/src/main.rs
@@ -15,8 +15,9 @@ use fparkan_platform::{NativeWindowHandles, WindowPort};
use fparkan_platform_winit::{probe_smoke_window, WinitWindowPlan};
use fparkan_render_vulkan::{
create_vulkan_instance_probe, create_vulkan_logical_device_probe, create_vulkan_surface_probe,
- probe_vulkan_loader, triangle_shader_manifest, validate_shader_manifest, VulkanInstanceConfig,
- VulkanInstanceProbe, VulkanLogicalDeviceProbe,
+ create_vulkan_swapchain_probe, probe_vulkan_loader, triangle_shader_manifest,
+ validate_shader_manifest, VulkanInstanceConfig, VulkanInstanceProbe, VulkanLogicalDeviceProbe,
+ VulkanSwapchainProbe,
};
use std::path::PathBuf;
use std::process::Command;
@@ -424,7 +425,14 @@ impl VulkanBootstrapProbe {
self.window_height.unwrap_or(1).max(1),
),
) {
- Ok(device) => self.record_logical_device_probe(&device),
+ Ok(device) => match create_vulkan_swapchain_probe(instance, surface, &device) {
+ Ok(swapchain) => self.record_swapchain_probe(&device, &swapchain),
+ Err(err) => {
+ self.record_logical_device_probe(&device);
+ self.swapchain_status = VulkanSwapchainStatus::Failed;
+ self.swapchain_error = Some(err.to_string());
+ }
+ },
Err(err) => {
self.device_status = VulkanDeviceStatus::Failed;
self.device_error = Some(err.to_string());
@@ -450,11 +458,22 @@ impl VulkanBootstrapProbe {
.try_into()
.unwrap_or(u32::MAX),
);
- self.swapchain_status = VulkanSwapchainStatus::Planned;
self.swapchain_width = Some(device.runtime.swapchain.extent.0);
self.swapchain_height = Some(device.runtime.swapchain.extent.1);
self.swapchain_image_count = Some(device.runtime.swapchain.image_count);
}
+
+ fn record_swapchain_probe(
+ &mut self,
+ device: &VulkanLogicalDeviceProbe,
+ swapchain: &VulkanSwapchainProbe,
+ ) {
+ self.record_logical_device_probe(device);
+ self.swapchain_status = VulkanSwapchainStatus::Created;
+ self.swapchain_width = Some(swapchain.report.plan.extent.0);
+ self.swapchain_height = Some(swapchain.report.plan.extent.1);
+ self.swapchain_image_count = Some(swapchain.report.image_count);
+ }
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -564,7 +583,7 @@ impl VulkanLogicalDeviceStatus {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum VulkanSwapchainStatus {
Skipped,
- Planned,
+ Created,
Failed,
}
@@ -572,7 +591,7 @@ impl VulkanSwapchainStatus {
const fn as_str(self) -> &'static str {
match self {
Self::Skipped => "skipped",
- Self::Planned => "planned",
+ Self::Created => "created",
Self::Failed => "failed",
}
}
@@ -691,9 +710,9 @@ fn validate_smoke_options(
"passed native smoke report requires created Vulkan logical device".to_string(),
);
}
- if bootstrap.swapchain_status != VulkanSwapchainStatus::Planned {
+ if bootstrap.swapchain_status != VulkanSwapchainStatus::Created {
return Err(
- "passed native smoke report requires planned Vulkan swapchain".to_string(),
+ "passed native smoke report requires created Vulkan swapchain".to_string(),
);
}
}
@@ -953,7 +972,7 @@ mod tests {
logical_device_present_queue_family: Some(0),
logical_device_enabled_extension_count: Some(1),
logical_device_error: None,
- swapchain_status: VulkanSwapchainStatus::Planned,
+ swapchain_status: VulkanSwapchainStatus::Created,
swapchain_width: Some(1280),
swapchain_height: Some(720),
swapchain_image_count: Some(3),
@@ -1340,7 +1359,7 @@ mod tests {
}
#[test]
- fn rejects_passed_without_planned_swapchain() {
+ fn rejects_passed_without_created_swapchain() {
let options = SmokeOptions::parse(&strings(&[
"--platform",
"linux",
@@ -1365,11 +1384,11 @@ mod tests {
&options,
&VulkanBootstrapProbe {
swapchain_status: VulkanSwapchainStatus::Failed,
- swapchain_error: Some("Vulkan swapchain has no surface format".to_string()),
+ swapchain_error: Some("Vulkan swapchain creation failed".to_string()),
..probe_fixture()
},
),
- Err("passed native smoke report requires planned Vulkan swapchain".to_string())
+ Err("passed native smoke report requires created Vulkan swapchain".to_string())
);
}
diff --git a/fixtures/acceptance/coverage.tsv b/fixtures/acceptance/coverage.tsv
index ed9a090..0f01121 100644
--- a/fixtures/acceptance/coverage.tsv
+++ b/fixtures/acceptance/coverage.tsv
@@ -61,8 +61,9 @@ S0-VK-028 covered cargo test -p fparkan-vulkan-smoke --offline reports_rustc_hos
S0-VK-029 covered cargo test -p xtask --offline native_smoke_audit_accepts_complete_three_platform_pass native_smoke_audit_rejects_blocked_or_incomplete_reports
S0-VK-030 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_with_failed_surface
S0-VK-031 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_selected_device
-S0-VK-032 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_planned_swapchain
+S0-VK-032 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_created_swapchain
S0-VK-033 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_created_logical_device
+S0-VK-034 covered cargo test -p xtask --offline native_smoke_audit_accepts_complete_three_platform_pass native_smoke_audit_rejects_blocked_or_incomplete_reports
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 6fc3239..68d65bd 100644
--- a/fixtures/acceptance/stage_0_2_roadmap.md
+++ b/fixtures/acceptance/stage_0_2_roadmap.md
@@ -63,6 +63,7 @@
`S0-VK-031`
`S0-VK-032`
`S0-VK-033`
+`S0-VK-034`
`S0-LIMIT-001`
`S0-LIMIT-002`
`L1-P1-NRES-001`
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 4babad2..ed40de8 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -1553,7 +1553,7 @@ fn validate_native_smoke_report(
platform,
report,
"vulkan_swapchain_status",
- "planned",
+ "created",
failures,
);
expect_u64_at_least(platform, report, "frames", 300, failures);
@@ -2292,7 +2292,7 @@ mod tests {
"vulkan_logical_device_graphics_queue_family": 0,
"vulkan_logical_device_present_queue_family": 0,
"vulkan_logical_device_enabled_extension_count": 1,
- "vulkan_swapchain_status": "planned",
+ "vulkan_swapchain_status": "created",
"vulkan_swapchain_width": 1280,
"vulkan_swapchain_height": 720,
"vulkan_swapchain_image_count": 3