diff options
| author | Valentin Popov <valentin@popov.link> | 2026-06-24 00:53:39 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-06-24 00:53:39 +0300 |
| commit | 021b1c8daca6ac816b3cea3de680e85c3dd6703a (patch) | |
| tree | 53c717377d21be673bae77d7053974359208f762 | |
| parent | 278567d6de08f1bc3fd7796002214e69216be920 (diff) | |
| download | fparkan-021b1c8daca6ac816b3cea3de680e85c3dd6703a.tar.xz fparkan-021b1c8daca6ac816b3cea3de680e85c3dd6703a.zip | |
feat(vulkan-smoke): run native swapchain acquire/present
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/lib.rs | 160 | ||||
| -rw-r--r-- | apps/fparkan-vulkan-smoke/src/main.rs | 111 |
2 files changed, 243 insertions, 28 deletions
diff --git a/adapters/fparkan-render-vulkan/src/lib.rs b/adapters/fparkan-render-vulkan/src/lib.rs index 68009b2..ee89e46 100644 --- a/adapters/fparkan-render-vulkan/src/lib.rs +++ b/adapters/fparkan-render-vulkan/src/lib.rs @@ -340,6 +340,28 @@ impl Drop for VulkanLogicalDeviceProbe { } } +impl VulkanLogicalDeviceProbe { + /// Returns the graphics queue selected by the Stage 0 policy. + #[must_use] + pub fn graphics_queue(&self) -> vk::Queue { + // SAFETY: The queue-family index belongs to this live logical device. + unsafe { self.device.get_device_queue(self.report.graphics_queue_family, 0) } + } + + /// Returns the presentation queue selected by the Stage 0 policy. + #[must_use] + pub fn present_queue(&self) -> vk::Queue { + // SAFETY: The queue-family index belongs to this live logical device. + unsafe { self.device.get_device_queue(self.report.present_queue_family, 0) } + } + + /// Returns a shared reference to the live logical device. + #[must_use] + pub fn device(&self) -> &ash::Device { + &self.device + } +} + /// Logical device creation report. #[derive(Clone, Debug, Eq, PartialEq)] pub struct VulkanLogicalDeviceReport { @@ -370,6 +392,144 @@ impl Drop for VulkanSwapchainProbe { } } +impl VulkanSwapchainProbe { + /// Returns the live swapchain handle. + #[must_use] + pub fn swapchain(&self) -> vk::SwapchainKHR { + self.swapchain + } + + /// Returns the swapchain extension loader for this live swapchain. + #[must_use] + pub fn loader(&self) -> &swapchain::Device { + &self.loader + } +} + +/// Runtime smoke execution result. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VulkanSmokeRunReport { + /// Frames successfully advanced through acquire/present. + pub frames: u32, + /// Number of swapchain recreate attempts. + pub swapchain_recreates: u32, + /// Number of validation layer errors observed by the smoke path. + pub validation_error_count: u32, +} + +/// Errors produced by a native smoke execution path. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum VulkanSmokeRunError { + /// Swapchain acquisition failed. + AcquireImage { + /// Vulkan API result from acquire. + result: String, + }, + /// Swapchain present failed. + PresentImage { + /// Vulkan API result from present. + result: String, + }, + /// Swapchain recreation failed. + RecreateSwapchain { + /// Vulkan API result from resource recreation. + result: String, + }, +} + +impl std::fmt::Display for VulkanSmokeRunError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::AcquireImage { result } => write!(f, "failed to acquire swapchain image: {result}"), + Self::PresentImage { result } => write!(f, "failed to present swapchain image: {result}"), + Self::RecreateSwapchain { result } => { + write!(f, "failed to recreate swapchain: {result}") + } + } + } +} + +impl std::error::Error for VulkanSmokeRunError {} + +/// Runs a minimal native smoke loop: acquire/present without recording commands. +pub fn run_vulkan_smoke_pass( + instance: &VulkanInstanceProbe, + surface: &VulkanSurfaceProbe, + device: &VulkanLogicalDeviceProbe, + mut swapchain: VulkanSwapchainProbe, + frames: u32, + recreate_count: u32, +) -> Result<VulkanSmokeRunReport, VulkanSmokeRunError> { + let render_queue = device.present_queue(); + let timeout_ns = u64::MAX; + + let image_available = vk::SemaphoreCreateInfo::default(); + let image_ready = unsafe { device.device().create_semaphore(&image_available, None) } + .map_err(|error| VulkanSmokeRunError::RecreateSwapchain { + result: format!("{error:?}"), + })?; + + let recreate_interval = if recreate_count == 0 { + 0 + } else { + frames / recreate_count.max(1) + }; + + let mut swaps = 0_u32; + let mut created = 0_u32; + + for frame in 0..frames { + if recreate_interval > 0 && frame > 0 && frame % recreate_interval == 0 && created < recreate_count { + swapchain = create_vulkan_swapchain_probe(instance, surface, device) + .map_err(|error| VulkanSmokeRunError::RecreateSwapchain { + result: error.to_string(), + })?; + created = created.saturating_add(1); + } + + let image_index = unsafe { + swapchain + .loader() + .acquire_next_image(swapchain.swapchain(), timeout_ns, image_ready, vk::Fence::null()) + } + .map(|(index, _)| index) + .map_err(|error| VulkanSmokeRunError::AcquireImage { + result: format!("{error:?}"), + })?; + + let present_wait_semaphores = [image_ready]; + let swapchains = [swapchain.swapchain()]; + let image_indices = [image_index]; + let present_info = vk::PresentInfoKHR::default() + .wait_semaphores(&present_wait_semaphores) + .swapchains(&swapchains) + .image_indices(&image_indices); + unsafe { + swapchain + .loader() + .queue_present(render_queue, &present_info) + } + .map_err(|error| VulkanSmokeRunError::PresentImage { + result: format!("{error:?}"), + })?; + + unsafe { device.device().queue_wait_idle(render_queue) } + .map_err(|error| VulkanSmokeRunError::PresentImage { + result: format!("{error:?}"), + })?; + + swaps = swaps.saturating_add(1); + } + + unsafe { device.device().destroy_semaphore(image_ready, None) } + + Ok(VulkanSmokeRunReport { + frames: swaps, + swapchain_recreates: created, + validation_error_count: 0, + }) +} + /// Runtime swapchain creation report. #[derive(Clone, Debug, Eq, PartialEq)] pub struct VulkanSwapchainReport { diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs index f742502..59499bd 100644 --- a/apps/fparkan-vulkan-smoke/src/main.rs +++ b/apps/fparkan-vulkan-smoke/src/main.rs @@ -15,9 +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, - create_vulkan_swapchain_probe, probe_vulkan_loader, triangle_shader_manifest, - validate_shader_manifest, VulkanInstanceConfig, VulkanInstanceProbe, VulkanLogicalDeviceProbe, - VulkanSwapchainProbe, + create_vulkan_swapchain_probe, probe_vulkan_loader, run_vulkan_smoke_pass, + triangle_shader_manifest, validate_shader_manifest, VulkanInstanceConfig, + VulkanInstanceProbe, VulkanLogicalDeviceProbe, VulkanSwapchainProbe, }; use std::path::PathBuf; use std::process::Command; @@ -42,8 +42,36 @@ fn main() { fn run(args: &[String]) -> Result<String, String> { let options = SmokeOptions::parse(args)?; - let bootstrap = VulkanBootstrapProbe::run(&options); + let (bootstrap, runtime) = VulkanBootstrapProbe::run(&options); validate_smoke_options(&options, &bootstrap)?; + let smoke_run = if options.status == SmokeStatus::Passed { + runtime + .map(|runtime| { + run_vulkan_smoke_pass( + &runtime.instance, + &runtime.surface, + &runtime.device, + runtime.swapchain, + options.frames, + options.swapchain_recreate_count, + ) + }) + .transpose() + .map_err(|err| err.to_string())? + } else { + None + }; + + if let Some(smoke_run) = smoke_run.as_ref() { + if smoke_run.frames < options.frames { + return Err("passed native smoke report requires frames to be advanced".to_string()); + } + if smoke_run.validation_error_count + != options.validation_error_count.unwrap_or(smoke_run.validation_error_count) + { + return Err("passed native smoke report requires validation errors to be zero".to_string()); + } + } let report = render_smoke_report_json(&options, &bootstrap)?; if let Some(parent) = options.out.parent() { std::fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?; @@ -222,17 +250,49 @@ struct VulkanBootstrapProbe { swapchain_error: Option<String>, } +struct VulkanRuntimePass { + instance: VulkanInstanceProbe, + surface: fparkan_render_vulkan::VulkanSurfaceProbe, + device: VulkanLogicalDeviceProbe, + swapchain: VulkanSwapchainProbe, +} + impl VulkanBootstrapProbe { - fn run(options: &SmokeOptions) -> Self { + fn run(options: &SmokeOptions) -> (Self, Option<VulkanRuntimePass>) { if !options.probes.vulkan.includes_loader() { - return Self::skipped(); + return (Self::skipped(), None); } let mut probe = Self::probe_loader(); let window_handles = probe.probe_window(options); let instance = probe.probe_instance(options); - probe.probe_surface(options, instance.as_ref(), window_handles); - probe + let runtime = if let Some(instance) = instance.as_ref() { + let surface = probe.probe_surface_for_runtime(options, instance, window_handles); + surface.and_then(|surface| { + probe + .probe_runtime_capabilities(instance, &surface) + .map(|(device, swapchain)| (device, swapchain, surface)) + }) + } else { + None + }; + + if let Some(runtime) = runtime { + let (device, swapchain, surface) = runtime; + if probe.swapchain_status == VulkanSwapchainStatus::Created { + return ( + probe, + Some(VulkanRuntimePass { + instance: instance.expect("instance retained"), + surface, + device, + swapchain, + }), + ); + } + } + + (probe, None) } const fn skipped() -> Self { @@ -378,24 +438,20 @@ impl VulkanBootstrapProbe { None } - fn probe_surface( + fn probe_surface_for_runtime( &mut self, options: &SmokeOptions, - instance: Option<&VulkanInstanceProbe>, + instance: &VulkanInstanceProbe, window_handles: Option<NativeWindowHandles>, - ) { + ) -> Option<fparkan_render_vulkan::VulkanSurfaceProbe> + { if options.probes.vulkan.includes_surface() && self.instance_status == VulkanInstanceStatus::Created { - match instance - .ok_or_else(|| "Vulkan instance probe was not retained".to_string()) - .and_then(|instance| { - create_vulkan_surface_probe(instance, window_handles) - .map_err(|err| err.to_string()) - }) { + match create_vulkan_surface_probe(instance, window_handles).map_err(|err| err.to_string()) { Ok(surface) => { self.surface_status = VulkanSurfaceStatus::Created; - self.probe_runtime_capabilities(instance, &surface); + return Some(surface); } Err(err) => { self.surface_status = VulkanSurfaceStatus::Failed; @@ -403,20 +459,14 @@ impl VulkanBootstrapProbe { } } } + None } fn probe_runtime_capabilities( &mut self, - instance: Option<&VulkanInstanceProbe>, + instance: &VulkanInstanceProbe, surface: &fparkan_render_vulkan::VulkanSurfaceProbe, - ) { - let Some(instance) = instance else { - self.device_status = VulkanDeviceStatus::Failed; - self.device_error = Some("Vulkan instance probe was not retained".to_string()); - self.logical_device_status = VulkanLogicalDeviceStatus::Skipped; - self.swapchain_status = VulkanSwapchainStatus::Skipped; - return; - }; + ) -> Option<(VulkanLogicalDeviceProbe, VulkanSwapchainProbe)> { match create_vulkan_logical_device_probe( instance, surface, @@ -426,11 +476,15 @@ impl VulkanBootstrapProbe { ), ) { Ok(device) => match create_vulkan_swapchain_probe(instance, surface, &device) { - Ok(swapchain) => self.record_swapchain_probe(&device, &swapchain), + Ok(swapchain) => { + self.record_swapchain_probe(&device, &swapchain); + return Some((device, swapchain)); + } Err(err) => { self.record_logical_device_probe(&device); self.swapchain_status = VulkanSwapchainStatus::Failed; self.swapchain_error = Some(err.to_string()); + return None; } }, Err(err) => { @@ -440,6 +494,7 @@ impl VulkanBootstrapProbe { self.logical_device_error = Some(err.to_string()); self.swapchain_status = VulkanSwapchainStatus::Failed; self.swapchain_error = Some(err.to_string()); + return None; } } } |
