diff options
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi.rs | 5 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke.rs | 15 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs | 80 | ||||
| -rw-r--r-- | apps/fparkan-vulkan-smoke/src/main.rs | 111 |
4 files changed, 191 insertions, 20 deletions
diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs index 5977f43..ece45e0 100644 --- a/adapters/fparkan-render-vulkan/src/ffi.rs +++ b/adapters/fparkan-render-vulkan/src/ffi.rs @@ -58,8 +58,9 @@ pub use self::runtime::{ VulkanLogicalDeviceReport, }; pub use self::smoke_types::{ - VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, - VulkanSmokeRendererError, VulkanSmokeRendererReport, VulkanValidationReport, + VulkanSmokeBootstrapProgress, VulkanSmokeBootstrapSnapshot, VulkanSmokeFrameOutcome, + VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError, + VulkanSmokeRendererReport, VulkanValidationReport, }; #[cfg(test)] use self::surface::extension_name; diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index e11706f..02ea0d6 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -76,9 +76,11 @@ impl VulkanSmokeRenderer { /// /// Returns [`VulkanSmokeRendererError`] when Vulkan bootstrap, pipeline creation, /// memory allocation, or synchronization resource creation fails. + #[allow(clippy::too_many_lines)] pub fn new( create_info: &VulkanSmokeRendererCreateInfo, ) -> Result<Self, VulkanSmokeRendererError> { + let bootstrap_progress = create_info.bootstrap_progress.as_ref(); let shader_manifest = validate_shader_manifest(&triangle_shader_manifest()) .map_err(VulkanSmokeRendererError::ShaderManifest)?; let surface_plan = plan_vulkan_surface(Some(create_info.native_handles)) @@ -90,6 +92,10 @@ impl VulkanSmokeRenderer { instance_config.enable_validation = create_info.enable_validation; let instance = create_vulkan_instance_probe(&instance_config) .map_err(VulkanSmokeRendererError::Instance)?; + if let Some(progress) = bootstrap_progress { + progress.mark_loader_available(); + progress.mark_instance_created(); + } let validation = if create_info.enable_validation { Some(create_validation_messenger(&instance)?) } else { @@ -97,9 +103,15 @@ impl VulkanSmokeRenderer { }; let surface = create_vulkan_surface_probe(&instance, Some(create_info.native_handles)) .map_err(VulkanSmokeRendererError::Surface)?; + if let Some(progress) = bootstrap_progress { + progress.mark_surface_created(); + } let device = create_vulkan_logical_device_probe(&instance, &surface, create_info.drawable_extent) .map_err(VulkanSmokeRendererError::LogicalDevice)?; + if let Some(progress) = bootstrap_progress { + progress.mark_logical_device_created(); + } let swapchain = create_vulkan_swapchain_probe_for_extent( &instance, &surface, @@ -108,6 +120,9 @@ impl VulkanSmokeRenderer { vk::SwapchainKHR::null(), ) .map_err(VulkanSmokeRendererError::Swapchain)?; + if let Some(progress) = bootstrap_progress { + progress.mark_swapchain_created(); + } let command_pool = create_command_pool(&device)?; let vertex_buffer = match create_triangle_vertex_buffer(&instance, &device) { Ok(buffer) => buffer, diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index 87d7992..836f82d 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -1,5 +1,7 @@ use ash::vk; use fparkan_platform::NativeWindowHandles; +use std::sync::atomic::{AtomicU8, Ordering}; +use std::sync::Arc; use super::{ VulkanAllocatedBuffer, VulkanFrameSync, VulkanInstanceError, VulkanInstanceProbe, @@ -20,8 +22,86 @@ pub struct VulkanSmokeRendererCreateInfo { pub drawable_extent: (u32, u32), /// Whether validation layers must be enabled. pub enable_validation: bool, + /// Optional shared bootstrap progress tracker for failure evidence. + pub bootstrap_progress: Option<Arc<VulkanSmokeBootstrapProgress>>, } +/// Shared bootstrap progress used to report partial renderer startup evidence. +#[derive(Debug, Default)] +pub struct VulkanSmokeBootstrapProgress { + flags: AtomicU8, +} + +impl VulkanSmokeBootstrapProgress { + /// Marks the Vulkan loader as available. + pub fn mark_loader_available(&self) { + self.set_flag(BOOTSTRAP_LOADER_AVAILABLE); + } + + /// Marks the Vulkan instance as created. + pub fn mark_instance_created(&self) { + self.set_flag(BOOTSTRAP_INSTANCE_CREATED); + } + + /// Marks the Vulkan surface as created. + pub fn mark_surface_created(&self) { + self.set_flag(BOOTSTRAP_SURFACE_CREATED); + } + + /// Marks a suitable Vulkan device as selected and the logical device as created. + pub fn mark_logical_device_created(&self) { + self.set_flag(BOOTSTRAP_DEVICE_SELECTED | BOOTSTRAP_LOGICAL_DEVICE_CREATED); + } + + /// Marks the Vulkan swapchain as created. + pub fn mark_swapchain_created(&self) { + self.set_flag(BOOTSTRAP_SWAPCHAIN_CREATED); + } + + /// Returns a stable snapshot of the measured bootstrap state. + #[must_use] + pub fn snapshot(&self) -> VulkanSmokeBootstrapSnapshot { + let flags = self.flags.load(Ordering::SeqCst); + VulkanSmokeBootstrapSnapshot { + loader_available: flags & BOOTSTRAP_LOADER_AVAILABLE != 0, + instance_created: flags & BOOTSTRAP_INSTANCE_CREATED != 0, + surface_created: flags & BOOTSTRAP_SURFACE_CREATED != 0, + device_selected: flags & BOOTSTRAP_DEVICE_SELECTED != 0, + logical_device_created: flags & BOOTSTRAP_LOGICAL_DEVICE_CREATED != 0, + swapchain_created: flags & BOOTSTRAP_SWAPCHAIN_CREATED != 0, + } + } + + fn set_flag(&self, flag: u8) { + self.flags.fetch_or(flag, Ordering::SeqCst); + } +} + +/// Stable snapshot of measured bootstrap progress. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[allow(clippy::struct_excessive_bools)] +pub struct VulkanSmokeBootstrapSnapshot { + /// Whether the Vulkan loader was resolved. + pub loader_available: bool, + /// Whether the Vulkan instance was created. + pub instance_created: bool, + /// Whether the Vulkan surface was created. + pub surface_created: bool, + /// Whether a suitable Vulkan device was selected. + pub device_selected: bool, + /// Whether the logical device was created. + pub logical_device_created: bool, + /// Whether the swapchain was created. + pub swapchain_created: bool, +} + +const BOOTSTRAP_LOADER_AVAILABLE: u8 = 1 << 0; +const BOOTSTRAP_INSTANCE_CREATED: u8 = 1 << 1; +const BOOTSTRAP_SURFACE_CREATED: u8 = 1 << 2; +const BOOTSTRAP_DEVICE_SELECTED: u8 = 1 << 3; +const BOOTSTRAP_LOGICAL_DEVICE_CREATED: u8 = 1 << 4; +const BOOTSTRAP_SWAPCHAIN_CREATED: u8 = 1 << 5; + /// Stable smoke renderer bootstrap report. #[derive(Clone, Debug, Eq, PartialEq)] pub struct VulkanSmokeRendererReport { diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs index ddd8a22..34fd63b 100644 --- a/apps/fparkan-vulkan-smoke/src/main.rs +++ b/apps/fparkan-vulkan-smoke/src/main.rs @@ -13,12 +13,13 @@ use fparkan_platform_winit::{window_native_handles, WinitWindowPlan}; use fparkan_render_vulkan::{ - VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, + VulkanSmokeBootstrapProgress, VulkanSmokeFrameOutcome, VulkanSmokeRenderer, + VulkanSmokeRendererCreateInfo, }; use serde::Serialize; use std::path::PathBuf; use std::process::Command; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; use winit::application::ApplicationHandler; @@ -52,28 +53,38 @@ fn main() { fn run(args: &[String]) -> Result<String, String> { let options = SmokeOptions::parse(args)?; + remove_stale_output(&options)?; let event_loop = EventLoop::new().map_err(|err| format!("winit event loop: {err}"))?; event_loop.set_control_flow(ControlFlow::Poll); let completed = Arc::new(AtomicBool::new(false)); - spawn_timeout_watchdog(options.clone(), Arc::clone(&completed)); - let mut app = SmokeApp::new(options, completed); + let progress = Arc::new(SharedSmokeProgress::default()); + spawn_timeout_watchdog( + options.clone(), + Arc::clone(&completed), + Arc::clone(&progress), + ); + let mut app = SmokeApp::new(options, completed, progress); if let Err(err) = event_loop.run_app(&mut app) { app.error = Some(format!("winit event loop: {err}")); } app.finish() } -fn spawn_timeout_watchdog(options: SmokeOptions, completed: Arc<AtomicBool>) { +fn spawn_timeout_watchdog( + options: SmokeOptions, + completed: Arc<AtomicBool>, + progress: Arc<SharedSmokeProgress>, +) { std::thread::spawn(move || { std::thread::sleep(Duration::from_secs(options.timeout_seconds)); - if completed.load(Ordering::SeqCst) || options.out.exists() { + if completed.load(Ordering::SeqCst) { return; } let failure_reason = format!( "native smoke timed out after {} seconds", options.timeout_seconds ); - if let Ok(report) = render_timeout_failure_report(&options, &failure_reason) { + if let Ok(report) = render_timeout_failure_report(&options, &failure_reason, &progress) { if let Some(parent) = options.out.parent() { let _ = std::fs::create_dir_all(parent); } @@ -84,6 +95,13 @@ fn spawn_timeout_watchdog(options: SmokeOptions, completed: Arc<AtomicBool>) { }); } +fn remove_stale_output(options: &SmokeOptions) -> Result<(), String> { + if !options.out.exists() { + return Ok(()); + } + std::fs::remove_file(&options.out).map_err(|err| format!("{}: {err}", options.out.display())) +} + #[derive(Clone, Debug, Eq, PartialEq)] struct SmokeOptions { out: PathBuf, @@ -153,6 +171,7 @@ impl SmokeOptions { struct SmokeApp { options: SmokeOptions, completed: Arc<AtomicBool>, + progress: Arc<SharedSmokeProgress>, window_id: Option<WindowId>, window: Option<Window>, renderer: Option<VulkanSmokeRenderer>, @@ -166,10 +185,15 @@ struct SmokeApp { } impl SmokeApp { - fn new(options: SmokeOptions, completed: Arc<AtomicBool>) -> Self { + fn new( + options: SmokeOptions, + completed: Arc<AtomicBool>, + progress: Arc<SharedSmokeProgress>, + ) -> Self { Self { options, completed, + progress, window_id: None, window: None, renderer: None, @@ -386,7 +410,9 @@ impl SmokeApp { fn render_timeout_failure_report( options: &SmokeOptions, failure_reason: &str, + progress: &SharedSmokeProgress, ) -> Result<String, String> { + let bootstrap = progress.bootstrap.snapshot(); let smoke_report = SmokeReport { schema_version: SCHEMA_VERSION, commit_sha: compiled_commit_sha(), @@ -398,26 +424,54 @@ fn render_timeout_failure_report( platform: actual_platform(), status: "failed", failure_reason: Some(failure_reason), - frames: 0, - resize_count: 0, - swapchain_recreate_count: 0, + frames: progress.frames_presented.load(Ordering::SeqCst), + resize_count: progress.resize_count.load(Ordering::SeqCst), + swapchain_recreate_count: progress.swapchain_recreate_count.load(Ordering::SeqCst), validation_warning_count: 0, validation_error_count: 0, validation_vuids: &[], requested_frames: options.frames, timeout_seconds: options.timeout_seconds, shader_manifest_hash: "", - vulkan_loader_status: "failed", - vulkan_instance_status: "failed", - window_status: "failed", - vulkan_surface_status: "failed", - vulkan_device_status: "failed", + vulkan_loader_status: if bootstrap.loader_available { + "available" + } else { + "failed" + }, + vulkan_instance_status: if bootstrap.instance_created { + "created" + } else { + "failed" + }, + window_status: if progress.window_created.load(Ordering::SeqCst) { + "created" + } else { + "failed" + }, + vulkan_surface_status: if bootstrap.surface_created { + "created" + } else { + "failed" + }, + vulkan_device_status: if bootstrap.device_selected { + "selected" + } else { + "failed" + }, vulkan_device_name: "", - vulkan_logical_device_status: "failed", + vulkan_logical_device_status: if bootstrap.logical_device_created { + "created" + } else { + "failed" + }, vulkan_logical_device_graphics_queue_family: 0, vulkan_logical_device_present_queue_family: 0, vulkan_logical_device_enabled_extension_count: 0, - vulkan_swapchain_status: "failed", + vulkan_swapchain_status: if bootstrap.swapchain_created { + "created" + } else { + "failed" + }, vulkan_swapchain_width: 0, vulkan_swapchain_height: 0, vulkan_swapchain_image_count: 0, @@ -471,6 +525,7 @@ impl ApplicationHandler for SmokeApp { native_handles, drawable_extent: (size.width.max(1), size.height.max(1)), enable_validation: true, + bootstrap_progress: Some(Arc::clone(&self.progress.bootstrap)), }) { Ok(renderer) => renderer, Err(err) => { @@ -481,6 +536,7 @@ impl ApplicationHandler for SmokeApp { }; self.last_size = Some((size.width, size.height)); self.window_id = Some(window.id()); + self.progress.window_created.store(true, Ordering::SeqCst); self.renderer = Some(renderer); self.window = Some(window); self.schedule_next_redraw(); @@ -511,6 +567,9 @@ impl ApplicationHandler for SmokeApp { .is_some_and(|last| last != (size.width, size.height)) { self.resize_count = self.resize_count.saturating_add(1); + self.progress + .resize_count + .store(self.resize_count, Ordering::SeqCst); } self.last_size = Some((size.width, size.height)); if let Some(renderer) = self.renderer.as_mut() { @@ -526,6 +585,9 @@ impl ApplicationHandler for SmokeApp { match renderer.draw_frame() { Ok(VulkanSmokeFrameOutcome::Presented) => { self.frames_presented = self.frames_presented.saturating_add(1); + self.progress + .frames_presented + .store(self.frames_presented, Ordering::SeqCst); } Ok( VulkanSmokeFrameOutcome::Recreated | VulkanSmokeFrameOutcome::ZeroExtent, @@ -537,6 +599,9 @@ impl ApplicationHandler for SmokeApp { } } let recreate_count = renderer.swapchain_recreate_count(); + self.progress + .swapchain_recreate_count + .store(recreate_count, Ordering::SeqCst); let should_request_resize = !self.resize_requested && self.frames_presented >= self.options.resize_frame; let should_complete = self.frames_presented >= self.options.frames @@ -606,6 +671,15 @@ struct SmokeReport<'a> { vulkan_portability_subset_enabled: bool, } +#[derive(Debug, Default)] +struct SharedSmokeProgress { + bootstrap: Arc<VulkanSmokeBootstrapProgress>, + window_created: AtomicBool, + frames_presented: AtomicU32, + resize_count: AtomicU32, + swapchain_recreate_count: AtomicU32, +} + fn actual_platform() -> &'static str { match std::env::consts::OS { "macos" => "macos", @@ -900,6 +974,7 @@ mod tests { timeout_seconds: 7, }, completed: Arc::new(AtomicBool::new(false)), + progress: Arc::new(SharedSmokeProgress::default()), window_id: None, window: None, renderer: None, |
