diff options
| author | Valentin Popov <valentin@popov.link> | 2026-06-24 00:05:31 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-06-24 00:05:31 +0300 |
| commit | d41add32c48f28dd498271b1552daceba8c85600 (patch) | |
| tree | 9e962c184da561a241e3ce45154360f046b58b6f | |
| parent | 159731664fae9ea3f08ec594985b82248988732d (diff) | |
| download | fparkan-d41add32c48f28dd498271b1552daceba8c85600.tar.xz fparkan-d41add32c48f28dd498271b1552daceba8c85600.zip | |
feat: create Vulkan swapchain probe
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/lib.rs | 160 | ||||
| -rw-r--r-- | apps/fparkan-vulkan-smoke/src/main.rs | 43 | ||||
| -rw-r--r-- | fixtures/acceptance/coverage.tsv | 3 | ||||
| -rw-r--r-- | fixtures/acceptance/stage_0_2_roadmap.md | 1 | ||||
| -rw-r--r-- | xtask/src/main.rs | 4 |
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 |
