fix(vulkan-smoke): track bootstrap timeout evidence

This commit is contained in:
2026-06-25 08:10:31 +04:00
parent b617e2958d
commit d146953bcc
4 changed files with 191 additions and 20 deletions
+3 -2
View File
@@ -58,8 +58,9 @@ pub use self::runtime::{
VulkanLogicalDeviceReport, VulkanLogicalDeviceReport,
}; };
pub use self::smoke_types::{ pub use self::smoke_types::{
VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeBootstrapProgress, VulkanSmokeBootstrapSnapshot, VulkanSmokeFrameOutcome,
VulkanSmokeRendererError, VulkanSmokeRendererReport, VulkanValidationReport, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeRendererError,
VulkanSmokeRendererReport, VulkanValidationReport,
}; };
#[cfg(test)] #[cfg(test)]
use self::surface::extension_name; use self::surface::extension_name;
@@ -76,9 +76,11 @@ impl VulkanSmokeRenderer {
/// ///
/// Returns [`VulkanSmokeRendererError`] when Vulkan bootstrap, pipeline creation, /// Returns [`VulkanSmokeRendererError`] when Vulkan bootstrap, pipeline creation,
/// memory allocation, or synchronization resource creation fails. /// memory allocation, or synchronization resource creation fails.
#[allow(clippy::too_many_lines)]
pub fn new( pub fn new(
create_info: &VulkanSmokeRendererCreateInfo, create_info: &VulkanSmokeRendererCreateInfo,
) -> Result<Self, VulkanSmokeRendererError> { ) -> Result<Self, VulkanSmokeRendererError> {
let bootstrap_progress = create_info.bootstrap_progress.as_ref();
let shader_manifest = validate_shader_manifest(&triangle_shader_manifest()) let shader_manifest = validate_shader_manifest(&triangle_shader_manifest())
.map_err(VulkanSmokeRendererError::ShaderManifest)?; .map_err(VulkanSmokeRendererError::ShaderManifest)?;
let surface_plan = plan_vulkan_surface(Some(create_info.native_handles)) 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; instance_config.enable_validation = create_info.enable_validation;
let instance = create_vulkan_instance_probe(&instance_config) let instance = create_vulkan_instance_probe(&instance_config)
.map_err(VulkanSmokeRendererError::Instance)?; .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 { let validation = if create_info.enable_validation {
Some(create_validation_messenger(&instance)?) Some(create_validation_messenger(&instance)?)
} else { } else {
@@ -97,9 +103,15 @@ impl VulkanSmokeRenderer {
}; };
let surface = create_vulkan_surface_probe(&instance, Some(create_info.native_handles)) let surface = create_vulkan_surface_probe(&instance, Some(create_info.native_handles))
.map_err(VulkanSmokeRendererError::Surface)?; .map_err(VulkanSmokeRendererError::Surface)?;
if let Some(progress) = bootstrap_progress {
progress.mark_surface_created();
}
let device = let device =
create_vulkan_logical_device_probe(&instance, &surface, create_info.drawable_extent) create_vulkan_logical_device_probe(&instance, &surface, create_info.drawable_extent)
.map_err(VulkanSmokeRendererError::LogicalDevice)?; .map_err(VulkanSmokeRendererError::LogicalDevice)?;
if let Some(progress) = bootstrap_progress {
progress.mark_logical_device_created();
}
let swapchain = create_vulkan_swapchain_probe_for_extent( let swapchain = create_vulkan_swapchain_probe_for_extent(
&instance, &instance,
&surface, &surface,
@@ -108,6 +120,9 @@ impl VulkanSmokeRenderer {
vk::SwapchainKHR::null(), vk::SwapchainKHR::null(),
) )
.map_err(VulkanSmokeRendererError::Swapchain)?; .map_err(VulkanSmokeRendererError::Swapchain)?;
if let Some(progress) = bootstrap_progress {
progress.mark_swapchain_created();
}
let command_pool = create_command_pool(&device)?; let command_pool = create_command_pool(&device)?;
let vertex_buffer = match create_triangle_vertex_buffer(&instance, &device) { let vertex_buffer = match create_triangle_vertex_buffer(&instance, &device) {
Ok(buffer) => buffer, Ok(buffer) => buffer,
@@ -1,5 +1,7 @@
use ash::vk; use ash::vk;
use fparkan_platform::NativeWindowHandles; use fparkan_platform::NativeWindowHandles;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
use super::{ use super::{
VulkanAllocatedBuffer, VulkanFrameSync, VulkanInstanceError, VulkanInstanceProbe, VulkanAllocatedBuffer, VulkanFrameSync, VulkanInstanceError, VulkanInstanceProbe,
@@ -20,8 +22,86 @@ pub struct VulkanSmokeRendererCreateInfo {
pub drawable_extent: (u32, u32), pub drawable_extent: (u32, u32),
/// Whether validation layers must be enabled. /// Whether validation layers must be enabled.
pub enable_validation: bool, 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. /// Stable smoke renderer bootstrap report.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct VulkanSmokeRendererReport { pub struct VulkanSmokeRendererReport {
+93 -18
View File
@@ -13,12 +13,13 @@
use fparkan_platform_winit::{window_native_handles, WinitWindowPlan}; use fparkan_platform_winit::{window_native_handles, WinitWindowPlan};
use fparkan_render_vulkan::{ use fparkan_render_vulkan::{
VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanSmokeBootstrapProgress, VulkanSmokeFrameOutcome, VulkanSmokeRenderer,
VulkanSmokeRendererCreateInfo,
}; };
use serde::Serialize; use serde::Serialize;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use winit::application::ApplicationHandler; use winit::application::ApplicationHandler;
@@ -52,28 +53,38 @@ fn main() {
fn run(args: &[String]) -> Result<String, String> { fn run(args: &[String]) -> Result<String, String> {
let options = SmokeOptions::parse(args)?; let options = SmokeOptions::parse(args)?;
remove_stale_output(&options)?;
let event_loop = EventLoop::new().map_err(|err| format!("winit event loop: {err}"))?; let event_loop = EventLoop::new().map_err(|err| format!("winit event loop: {err}"))?;
event_loop.set_control_flow(ControlFlow::Poll); event_loop.set_control_flow(ControlFlow::Poll);
let completed = Arc::new(AtomicBool::new(false)); let completed = Arc::new(AtomicBool::new(false));
spawn_timeout_watchdog(options.clone(), Arc::clone(&completed)); let progress = Arc::new(SharedSmokeProgress::default());
let mut app = SmokeApp::new(options, completed); 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) { if let Err(err) = event_loop.run_app(&mut app) {
app.error = Some(format!("winit event loop: {err}")); app.error = Some(format!("winit event loop: {err}"));
} }
app.finish() 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::spawn(move || {
std::thread::sleep(Duration::from_secs(options.timeout_seconds)); std::thread::sleep(Duration::from_secs(options.timeout_seconds));
if completed.load(Ordering::SeqCst) || options.out.exists() { if completed.load(Ordering::SeqCst) {
return; return;
} }
let failure_reason = format!( let failure_reason = format!(
"native smoke timed out after {} seconds", "native smoke timed out after {} seconds",
options.timeout_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() { if let Some(parent) = options.out.parent() {
let _ = std::fs::create_dir_all(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)] #[derive(Clone, Debug, Eq, PartialEq)]
struct SmokeOptions { struct SmokeOptions {
out: PathBuf, out: PathBuf,
@@ -153,6 +171,7 @@ impl SmokeOptions {
struct SmokeApp { struct SmokeApp {
options: SmokeOptions, options: SmokeOptions,
completed: Arc<AtomicBool>, completed: Arc<AtomicBool>,
progress: Arc<SharedSmokeProgress>,
window_id: Option<WindowId>, window_id: Option<WindowId>,
window: Option<Window>, window: Option<Window>,
renderer: Option<VulkanSmokeRenderer>, renderer: Option<VulkanSmokeRenderer>,
@@ -166,10 +185,15 @@ struct SmokeApp {
} }
impl SmokeApp { impl SmokeApp {
fn new(options: SmokeOptions, completed: Arc<AtomicBool>) -> Self { fn new(
options: SmokeOptions,
completed: Arc<AtomicBool>,
progress: Arc<SharedSmokeProgress>,
) -> Self {
Self { Self {
options, options,
completed, completed,
progress,
window_id: None, window_id: None,
window: None, window: None,
renderer: None, renderer: None,
@@ -386,7 +410,9 @@ impl SmokeApp {
fn render_timeout_failure_report( fn render_timeout_failure_report(
options: &SmokeOptions, options: &SmokeOptions,
failure_reason: &str, failure_reason: &str,
progress: &SharedSmokeProgress,
) -> Result<String, String> { ) -> Result<String, String> {
let bootstrap = progress.bootstrap.snapshot();
let smoke_report = SmokeReport { let smoke_report = SmokeReport {
schema_version: SCHEMA_VERSION, schema_version: SCHEMA_VERSION,
commit_sha: compiled_commit_sha(), commit_sha: compiled_commit_sha(),
@@ -398,26 +424,54 @@ fn render_timeout_failure_report(
platform: actual_platform(), platform: actual_platform(),
status: "failed", status: "failed",
failure_reason: Some(failure_reason), failure_reason: Some(failure_reason),
frames: 0, frames: progress.frames_presented.load(Ordering::SeqCst),
resize_count: 0, resize_count: progress.resize_count.load(Ordering::SeqCst),
swapchain_recreate_count: 0, swapchain_recreate_count: progress.swapchain_recreate_count.load(Ordering::SeqCst),
validation_warning_count: 0, validation_warning_count: 0,
validation_error_count: 0, validation_error_count: 0,
validation_vuids: &[], validation_vuids: &[],
requested_frames: options.frames, requested_frames: options.frames,
timeout_seconds: options.timeout_seconds, timeout_seconds: options.timeout_seconds,
shader_manifest_hash: "", shader_manifest_hash: "",
vulkan_loader_status: "failed", vulkan_loader_status: if bootstrap.loader_available {
vulkan_instance_status: "failed", "available"
window_status: "failed", } else {
vulkan_surface_status: "failed", "failed"
vulkan_device_status: "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_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_graphics_queue_family: 0,
vulkan_logical_device_present_queue_family: 0, vulkan_logical_device_present_queue_family: 0,
vulkan_logical_device_enabled_extension_count: 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_width: 0,
vulkan_swapchain_height: 0, vulkan_swapchain_height: 0,
vulkan_swapchain_image_count: 0, vulkan_swapchain_image_count: 0,
@@ -471,6 +525,7 @@ impl ApplicationHandler for SmokeApp {
native_handles, native_handles,
drawable_extent: (size.width.max(1), size.height.max(1)), drawable_extent: (size.width.max(1), size.height.max(1)),
enable_validation: true, enable_validation: true,
bootstrap_progress: Some(Arc::clone(&self.progress.bootstrap)),
}) { }) {
Ok(renderer) => renderer, Ok(renderer) => renderer,
Err(err) => { Err(err) => {
@@ -481,6 +536,7 @@ impl ApplicationHandler for SmokeApp {
}; };
self.last_size = Some((size.width, size.height)); self.last_size = Some((size.width, size.height));
self.window_id = Some(window.id()); self.window_id = Some(window.id());
self.progress.window_created.store(true, Ordering::SeqCst);
self.renderer = Some(renderer); self.renderer = Some(renderer);
self.window = Some(window); self.window = Some(window);
self.schedule_next_redraw(); self.schedule_next_redraw();
@@ -511,6 +567,9 @@ impl ApplicationHandler for SmokeApp {
.is_some_and(|last| last != (size.width, size.height)) .is_some_and(|last| last != (size.width, size.height))
{ {
self.resize_count = self.resize_count.saturating_add(1); 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)); self.last_size = Some((size.width, size.height));
if let Some(renderer) = self.renderer.as_mut() { if let Some(renderer) = self.renderer.as_mut() {
@@ -526,6 +585,9 @@ impl ApplicationHandler for SmokeApp {
match renderer.draw_frame() { match renderer.draw_frame() {
Ok(VulkanSmokeFrameOutcome::Presented) => { Ok(VulkanSmokeFrameOutcome::Presented) => {
self.frames_presented = self.frames_presented.saturating_add(1); self.frames_presented = self.frames_presented.saturating_add(1);
self.progress
.frames_presented
.store(self.frames_presented, Ordering::SeqCst);
} }
Ok( Ok(
VulkanSmokeFrameOutcome::Recreated | VulkanSmokeFrameOutcome::ZeroExtent, VulkanSmokeFrameOutcome::Recreated | VulkanSmokeFrameOutcome::ZeroExtent,
@@ -537,6 +599,9 @@ impl ApplicationHandler for SmokeApp {
} }
} }
let recreate_count = renderer.swapchain_recreate_count(); let recreate_count = renderer.swapchain_recreate_count();
self.progress
.swapchain_recreate_count
.store(recreate_count, Ordering::SeqCst);
let should_request_resize = let should_request_resize =
!self.resize_requested && self.frames_presented >= self.options.resize_frame; !self.resize_requested && self.frames_presented >= self.options.resize_frame;
let should_complete = self.frames_presented >= self.options.frames let should_complete = self.frames_presented >= self.options.frames
@@ -606,6 +671,15 @@ struct SmokeReport<'a> {
vulkan_portability_subset_enabled: bool, 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 { fn actual_platform() -> &'static str {
match std::env::consts::OS { match std::env::consts::OS {
"macos" => "macos", "macos" => "macos",
@@ -900,6 +974,7 @@ mod tests {
timeout_seconds: 7, timeout_seconds: 7,
}, },
completed: Arc::new(AtomicBool::new(false)), completed: Arc::new(AtomicBool::new(false)),
progress: Arc::new(SharedSmokeProgress::default()),
window_id: None, window_id: None,
window: None, window: None,
renderer: None, renderer: None,