aboutsummaryrefslogtreecommitdiff
path: root/apps/fparkan-vulkan-smoke/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'apps/fparkan-vulkan-smoke/src/main.rs')
-rw-r--r--apps/fparkan-vulkan-smoke/src/main.rs1973
1 files changed, 0 insertions, 1973 deletions
diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs
deleted file mode 100644
index ec2d19c..0000000
--- a/apps/fparkan-vulkan-smoke/src/main.rs
+++ /dev/null
@@ -1,1973 +0,0 @@
-#![forbid(unsafe_code)]
-#![cfg_attr(
- test,
- allow(
- clippy::expect_used,
- clippy::needless_raw_string_hashes,
- clippy::panic,
- clippy::unwrap_used
- )
-)]
-#![allow(clippy::print_stderr, clippy::print_stdout)]
-//! Native Vulkan smoke runner entrypoint.
-
-use fparkan_platform::RenderRequest;
-use fparkan_platform_winit::{window_native_handles, WinitWindowPlan};
-use fparkan_render_vulkan::{
- project_land_msh_to_static_mesh, project_msh_to_static_mesh, VulkanReadbackArtifact,
- VulkanSmokeBootstrapProgress, VulkanSmokeFrameOutcome, VulkanSmokeRenderer,
- VulkanSmokeRendererCreateInfo, VulkanSmokeRendererReport, VulkanSmokeShutdownReport,
- VulkanStaticCamera, VulkanStaticMaterial, VulkanStaticMesh, VulkanStaticTexture,
- VulkanValidationReport,
-};
-use serde::Serialize;
-use std::path::PathBuf;
-use std::process::Command;
-use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
-use std::sync::Arc;
-use std::time::{Duration, Instant};
-use winit::application::ApplicationHandler;
-use winit::dpi::PhysicalSize;
-use winit::event::StartCause;
-use winit::event::WindowEvent;
-use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
-use winit::window::{Window, WindowId};
-
-const SCHEMA_VERSION: &str = "fparkan-native-smoke-v1";
-const DEFAULT_TARGET_FRAMES: u32 = 300;
-const DEFAULT_RESIZE_FRAME: u32 = 120;
-const DEFAULT_RESIZE_WIDTH: u32 = 960;
-const DEFAULT_RESIZE_HEIGHT: u32 = 540;
-const DEFAULT_TIMEOUT_SECONDS: u64 = 120;
-
-fn main() {
- let args = std::env::args().skip(1).collect::<Vec<_>>();
- let code = match run(&args) {
- Ok(output) => {
- println!("{output}");
- 0
- }
- Err(err) => {
- eprintln!("{err}");
- 2
- }
- };
- std::process::exit(code);
-}
-
-fn run(args: &[String]) -> Result<String, String> {
- let options = SmokeOptions::parse(args)?;
- let mesh = options.load_mesh()?;
- let materials = options.load_materials(&mesh)?;
- 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));
- let progress = Arc::new(SharedSmokeProgress::default());
- spawn_timeout_watchdog(
- options.clone(),
- Arc::clone(&completed),
- Arc::clone(&progress),
- );
- let mut app = SmokeApp::new(options, mesh, materials, 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>,
- progress: Arc<SharedSmokeProgress>,
-) {
- std::thread::spawn(move || {
- std::thread::sleep(Duration::from_secs(options.timeout_seconds));
- 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, &progress) {
- if let Some(parent) = options.out.parent() {
- let _ = std::fs::create_dir_all(parent);
- }
- let _ = std::fs::write(&options.out, report);
- }
- eprintln!("{failure_reason}");
- std::process::exit(2);
- });
-}
-
-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()))
-}
-
-fn compare_readback_files(
- expected: &std::path::Path,
- actual: &std::path::Path,
-) -> Result<(), String> {
- let expected_format = readback_format_from_path(expected)?;
- let actual_format = readback_format_from_path(actual)?;
- if expected_format != actual_format {
- return Err(format!(
- "pixel readback format mismatch: expected={expected_format} actual={actual_format}"
- ));
- }
- let expected_bytes =
- std::fs::read(expected).map_err(|err| format!("{}: {err}", expected.display()))?;
- let actual_bytes =
- std::fs::read(actual).map_err(|err| format!("{}: {err}", actual.display()))?;
- if expected_bytes.len() != actual_bytes.len() {
- return Err(format!(
- "pixel readback length mismatch: expected={} actual={}",
- expected_bytes.len(),
- actual_bytes.len()
- ));
- }
- if let Some((offset, (expected_byte, actual_byte))) = expected_bytes
- .iter()
- .zip(&actual_bytes)
- .enumerate()
- .find(|(_, (expected_byte, actual_byte))| expected_byte != actual_byte)
- {
- return Err(format!(
- "pixel readback mismatch at byte {offset}: expected={expected_byte} actual={actual_byte}"
- ));
- }
- Ok(())
-}
-
-fn readback_format_from_path(path: &std::path::Path) -> Result<i32, String> {
- let name = path
- .file_name()
- .and_then(std::ffi::OsStr::to_str)
- .ok_or_else(|| {
- format!(
- "pixel readback artifact has no UTF-8 file name: {}",
- path.display()
- )
- })?;
- let prefix = ".readback-vkformat-";
- let raw = name
- .strip_suffix(".raw")
- .and_then(|stem| stem.rsplit_once(prefix).map(|(_, raw)| raw))
- .ok_or_else(|| {
- format!(
- "pixel readback artifact lacks format identity: {}",
- path.display()
- )
- })?;
- raw.parse::<i32>().map_err(|_| {
- format!(
- "pixel readback artifact has invalid format identity: {}",
- path.display()
- )
- })
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct SmokeOptions {
- out: PathBuf,
- frames: u32,
- resize_frame: u32,
- timeout_seconds: u64,
- expected_readback: Option<PathBuf>,
- mesh_input: MeshInput,
- texture_input: Option<TextureInput>,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct ResourceInput {
- root: PathBuf,
- archive: String,
- name: String,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct WearMaterialInput {
- root: PathBuf,
- archive: String,
- name: String,
- material_index: u16,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct WearInput {
- root: PathBuf,
- archive: String,
- name: String,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-enum TextureInput {
- Direct(ResourceInput),
- WearMaterial(WearMaterialInput),
- WearBatchMaterials(WearInput),
-}
-
-impl TextureInput {
- fn source(&self) -> &'static str {
- match self {
- Self::Direct(_) => "original-texm",
- Self::WearMaterial(_) => "original-wear-mat0-texm",
- Self::WearBatchMaterials(_) => "original-wear-mat0-texm-batch",
- }
- }
-
- fn archive(&self) -> &str {
- match self {
- Self::Direct(input) => &input.archive,
- Self::WearMaterial(_) | Self::WearBatchMaterials(_) => "Textures.lib",
- }
- }
-
- fn requested_name(&self) -> &str {
- match self {
- Self::Direct(input) => &input.name,
- // The selected name comes from MAT0 at load time and is not a CLI request.
- Self::WearMaterial(_) | Self::WearBatchMaterials(_) => "",
- }
- }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-enum MeshInput {
- SmokeTriangle,
- Msh {
- root: PathBuf,
- archive: String,
- name: String,
- },
- LandMsh {
- path: PathBuf,
- },
-}
-
-impl MeshInput {
- fn kind(&self) -> &'static str {
- match self {
- Self::SmokeTriangle => "synthetic-smoke-triangle",
- Self::Msh { .. } => "original-msh",
- Self::LandMsh { .. } => "original-land-msh",
- }
- }
-
- fn archive(&self) -> &str {
- match self {
- Self::Msh { archive, .. } => archive,
- Self::SmokeTriangle | Self::LandMsh { .. } => "",
- }
- }
-
- fn name(&self) -> &str {
- match self {
- Self::SmokeTriangle => "",
- Self::Msh { name, .. } => name,
- Self::LandMsh { .. } => "Land.msh",
- }
- }
-}
-
-impl SmokeOptions {
- #[allow(clippy::too_many_lines)]
- fn parse(args: &[String]) -> Result<Self, String> {
- let mut out = None;
- let mut frames = DEFAULT_TARGET_FRAMES;
- let mut resize_frame = DEFAULT_RESIZE_FRAME;
- let mut timeout_seconds = DEFAULT_TIMEOUT_SECONDS;
- let mut expected_readback = None;
- let mut model_root = None;
- let mut model_archive = None;
- let mut model_name = None;
- let mut terrain_path = None;
- let mut texture_root = None;
- let mut texture_archive = None;
- let mut texture_name = None;
- let mut wear_root = None;
- let mut wear_archive = None;
- let mut wear_name = None;
- let mut material_index = None;
- let mut iter = args.iter();
- while let Some(arg) = iter.next() {
- match arg.as_str() {
- "--out" => {
- out = Some(
- iter.next()
- .map(PathBuf::from)
- .ok_or_else(|| "--out requires a path".to_string())?,
- );
- }
- "--frames" => {
- frames = iter
- .next()
- .ok_or_else(|| "--frames requires a value".to_string())?
- .parse()
- .map_err(|_| "--frames must be an integer".to_string())?;
- }
- "--resize-frame" => {
- resize_frame = iter
- .next()
- .ok_or_else(|| "--resize-frame requires a value".to_string())?
- .parse()
- .map_err(|_| "--resize-frame must be an integer".to_string())?;
- }
- "--timeout-seconds" => {
- timeout_seconds = iter
- .next()
- .ok_or_else(|| "--timeout-seconds requires a value".to_string())?
- .parse()
- .map_err(|_| "--timeout-seconds must be an integer".to_string())?;
- }
- "--expected-readback" => {
- expected_readback = Some(
- iter.next()
- .map(PathBuf::from)
- .ok_or_else(|| "--expected-readback requires a path".to_string())?,
- );
- }
- "--model-root" => {
- model_root = Some(
- iter.next()
- .map(PathBuf::from)
- .ok_or_else(|| "--model-root requires a path".to_string())?,
- );
- }
- "--model-archive" => {
- model_archive = Some(
- iter.next()
- .cloned()
- .ok_or_else(|| "--model-archive requires a value".to_string())?,
- );
- }
- "--model-name" => {
- model_name = Some(
- iter.next()
- .cloned()
- .ok_or_else(|| "--model-name requires a value".to_string())?,
- );
- }
- "--terrain-path" => {
- terrain_path = Some(
- iter.next()
- .map(PathBuf::from)
- .ok_or_else(|| "--terrain-path requires a path".to_string())?,
- );
- }
- "--texture-root" => {
- texture_root = Some(
- iter.next()
- .map(PathBuf::from)
- .ok_or_else(|| "--texture-root requires a path".to_string())?,
- );
- }
- "--texture-archive" => {
- texture_archive = Some(
- iter.next()
- .cloned()
- .ok_or_else(|| "--texture-archive requires a value".to_string())?,
- );
- }
- "--texture-name" => {
- texture_name = Some(
- iter.next()
- .cloned()
- .ok_or_else(|| "--texture-name requires a value".to_string())?,
- );
- }
- "--wear-root" => {
- wear_root = Some(
- iter.next()
- .map(PathBuf::from)
- .ok_or_else(|| "--wear-root requires a path".to_string())?,
- );
- }
- "--wear-archive" => {
- wear_archive = Some(
- iter.next()
- .cloned()
- .ok_or_else(|| "--wear-archive requires a value".to_string())?,
- );
- }
- "--wear-name" => {
- wear_name = Some(
- iter.next()
- .cloned()
- .ok_or_else(|| "--wear-name requires a value".to_string())?,
- );
- }
- "--material-index" => {
- material_index = Some(
- iter.next()
- .ok_or_else(|| "--material-index requires a value".to_string())?
- .parse()
- .map_err(|_| "--material-index must be a u16".to_string())?,
- );
- }
- _ => return Err(format!("unknown native smoke option: {arg}")),
- }
- }
- let out = out.ok_or_else(|| "missing --out".to_string())?;
- if frames < DEFAULT_TARGET_FRAMES {
- return Err(format!(
- "native smoke requires --frames >= {DEFAULT_TARGET_FRAMES}"
- ));
- }
- if timeout_seconds == 0 {
- return Err("native smoke requires --timeout-seconds >= 1".to_string());
- }
- let mesh_input = match (terrain_path, model_root, model_archive, model_name) {
- (Some(path), None, None, None) => MeshInput::LandMsh { path },
- (None, None, None, None) => MeshInput::SmokeTriangle,
- (None, Some(root), Some(archive), Some(name)) => MeshInput::Msh {
- root,
- archive,
- name,
- },
- (None, _, _, _) => {
- return Err(
- "--model-root, --model-archive and --model-name must be supplied together"
- .to_string(),
- );
- }
- _ => return Err("--terrain-path cannot be combined with --model-*".to_string()),
- };
- let texture_input = match (texture_root, texture_archive, texture_name) {
- (None, None, None) => None,
- (Some(root), Some(archive), Some(name)) => Some(TextureInput::Direct(ResourceInput {
- root,
- archive,
- name,
- })),
- _ => {
- return Err(
- "--texture-root, --texture-archive and --texture-name must be supplied together"
- .to_string(),
- );
- }
- };
- let wear_material_input = match (wear_root, wear_archive, wear_name, material_index) {
- (None, None, None, None) => None,
- (Some(root), Some(archive), Some(name), Some(material_index)) => {
- Some(TextureInput::WearMaterial(WearMaterialInput {
- root,
- archive,
- name,
- material_index,
- }))
- }
- (Some(root), Some(archive), Some(name), None) => {
- Some(TextureInput::WearBatchMaterials(WearInput {
- root,
- archive,
- name,
- }))
- }
- _ => {
- return Err(
- "--wear-root, --wear-archive and --wear-name must be supplied together; --material-index is optional"
- .to_string(),
- );
- }
- };
- if texture_input.is_some() && wear_material_input.is_some() {
- return Err(
- "manual --texture-* input cannot be combined with --wear-* material input"
- .to_string(),
- );
- }
- Ok(Self {
- out,
- frames,
- resize_frame,
- timeout_seconds,
- expected_readback,
- mesh_input,
- texture_input: texture_input.or(wear_material_input),
- })
- }
-
- fn load_mesh(&self) -> Result<VulkanStaticMesh, String> {
- match &self.mesh_input {
- MeshInput::SmokeTriangle => Ok(VulkanStaticMesh::smoke_triangle()),
- MeshInput::Msh {
- root,
- archive,
- name,
- } => {
- let model = fparkan_inspection::load_model_from_root(root, archive, name)?;
- project_msh_to_static_mesh(&model).map_err(|err| err.to_string())
- }
- MeshInput::LandMsh { path } => {
- let terrain = fparkan_inspection::load_land_msh_from_path(path)?;
- project_land_msh_to_static_mesh(&terrain).map_err(|err| err.to_string())
- }
- }
- }
-
- fn load_materials(&self, mesh: &VulkanStaticMesh) -> Result<Vec<LoadedTexture>, String> {
- let Some(input) = self.texture_input.as_ref() else {
- return Ok(Vec::new());
- };
- match input {
- TextureInput::Direct(input) => Ok(vec![Self::load_material(
- 0,
- {
- let image = fparkan_inspection::load_texture_mip0_rgba8_from_root(
- &input.root,
- &input.archive,
- &input.name,
- )?;
- (image.width, image.height, image.rgba8)
- },
- input.name.clone(),
- )]),
- TextureInput::WearMaterial(input) => {
- let selected = fparkan_inspection::load_wear_material_texture_mip0_rgba8_from_root(
- &input.root,
- &input.archive,
- &input.name,
- input.material_index,
- )?;
- Ok(vec![Self::load_material(
- input.material_index,
- (
- selected.image.width,
- selected.image.height,
- selected.image.rgba8,
- ),
- selected.texture_name,
- )])
- }
- TextureInput::WearBatchMaterials(input) => {
- let mut selectors = mesh
- .draw_ranges
- .iter()
- .map(|range| range.material_index)
- .collect::<Vec<_>>();
- selectors.sort_unstable();
- selectors.dedup();
- selectors
- .into_iter()
- .map(|material_index| {
- let selected =
- fparkan_inspection::load_wear_material_texture_mip0_rgba8_from_root(
- &input.root,
- &input.archive,
- &input.name,
- material_index,
- )?;
- Ok(Self::load_material(
- material_index,
- (
- selected.image.width,
- selected.image.height,
- selected.image.rgba8,
- ),
- selected.texture_name,
- ))
- })
- .collect()
- }
- }
- }
-
- fn load_material(
- material_index: u16,
- image: (u32, u32, Vec<u8>),
- selected_name: String,
- ) -> LoadedTexture {
- LoadedTexture {
- material_index,
- texture: VulkanStaticTexture {
- width: image.0,
- height: image.1,
- rgba8: image.2,
- },
- selected_name,
- }
- }
-}
-
-#[derive(Clone, Debug)]
-struct LoadedTexture {
- material_index: u16,
- texture: VulkanStaticTexture,
- selected_name: String,
-}
-
-struct SmokeApp {
- options: SmokeOptions,
- mesh: VulkanStaticMesh,
- materials: Vec<LoadedTexture>,
- completed: Arc<AtomicBool>,
- progress: Arc<SharedSmokeProgress>,
- window_id: Option<WindowId>,
- window: Option<Window>,
- renderer: Option<VulkanSmokeRenderer>,
- final_renderer: Option<RendererSnapshot>,
- error: Option<String>,
- output: Option<String>,
- frames_presented: u32,
- resize_count: u32,
- resize_requested: bool,
- last_size: Option<(u32, u32)>,
- started_at: Instant,
-}
-
-fn drop_renderer_before_window<Renderer, WindowLike>(
- renderer: &mut Option<Renderer>,
- window: &mut Option<WindowLike>,
-) {
- drop(renderer.take());
- drop(window.take());
-}
-
-#[derive(Clone, Debug)]
-struct RendererSnapshot {
- report: VulkanSmokeRendererReport,
- readback_artifact: Option<VulkanReadbackArtifact>,
- swapchain_recreate_count: u32,
- validation: VulkanValidationReport,
-}
-
-impl From<VulkanSmokeShutdownReport> for RendererSnapshot {
- fn from(report: VulkanSmokeShutdownReport) -> Self {
- Self {
- report: report.renderer_report,
- readback_artifact: report.readback_artifact,
- swapchain_recreate_count: report.swapchain_recreate_count,
- validation: report.validation,
- }
- }
-}
-
-impl SmokeApp {
- fn new(
- options: SmokeOptions,
- mesh: VulkanStaticMesh,
- materials: Vec<LoadedTexture>,
- completed: Arc<AtomicBool>,
- progress: Arc<SharedSmokeProgress>,
- ) -> Self {
- Self {
- options,
- mesh,
- materials,
- completed,
- progress,
- window_id: None,
- window: None,
- renderer: None,
- final_renderer: None,
- error: None,
- output: None,
- frames_presented: 0,
- resize_count: 0,
- resize_requested: false,
- last_size: None,
- started_at: Instant::now(),
- }
- }
-
- fn finish(mut self) -> Result<String, String> {
- if let Some(output) = self.output.take() {
- return Ok(output);
- }
- self.completed.store(true, Ordering::SeqCst);
- let error = self
- .error
- .clone()
- .unwrap_or_else(|| "native smoke exited before producing a report".to_string());
- self.write_failure_report(&error)?;
- Err(error)
- }
-
- fn schedule_next_redraw(&self) {
- if let Some(window) = self.window.as_ref() {
- window.request_redraw();
- }
- }
-
- fn write_report(&self, report: &str) -> Result<(), String> {
- if let Some(parent) = self.options.out.parent() {
- std::fs::create_dir_all(parent)
- .map_err(|err| format!("{}: {err}", parent.display()))?;
- }
- std::fs::write(&self.options.out, report)
- .map_err(|err| format!("{}: {err}", self.options.out.display()))
- }
-
- fn readback_artifact_path(&self, artifact: &VulkanReadbackArtifact) -> PathBuf {
- let stem = self
- .options
- .out
- .file_stem()
- .and_then(std::ffi::OsStr::to_str)
- .unwrap_or("smoke");
- self.options
- .out
- .with_file_name(format!("{stem}.readback-vkformat-{}.raw", artifact.format))
- }
-
- fn write_readback_artifact(
- &self,
- artifact: &VulkanReadbackArtifact,
- ) -> Result<PathBuf, String> {
- let path = self.readback_artifact_path(artifact);
- std::fs::write(&path, &artifact.bytes)
- .map_err(|err| format!("{}: {err}", path.display()))
- .map(|()| path)
- }
-
- fn live_renderer_snapshot(&self) -> Option<RendererSnapshot> {
- self.renderer.as_ref().map(|renderer| RendererSnapshot {
- report: renderer.report().clone(),
- readback_artifact: None,
- swapchain_recreate_count: renderer.swapchain_recreate_count(),
- validation: renderer.validation_report(),
- })
- }
-
- fn renderer_snapshot(&self) -> Option<RendererSnapshot> {
- self.final_renderer
- .clone()
- .or_else(|| self.live_renderer_snapshot())
- }
-
- #[allow(clippy::too_many_lines)]
- fn render_report(
- &self,
- status: &'static str,
- failure_reason: Option<&str>,
- ) -> Result<String, String> {
- let renderer = self.renderer_snapshot();
- let validation = renderer
- .as_ref()
- .map_or_else(VulkanValidationReport::default, |snapshot| {
- snapshot.validation.clone()
- });
- let smoke_report = SmokeReport {
- schema_version: SCHEMA_VERSION,
- commit_sha: compiled_commit_sha(),
- git_dirty: compiled_git_dirty(),
- runner_identity: measured_runner_identity(),
- runner_architecture: actual_architecture(),
- rust_toolchain: compiled_rust_toolchain(),
- target_triple: compiled_target_triple(),
- platform: actual_platform(),
- status,
- failure_reason,
- frames: self.frames_presented,
- resize_count: self.resize_count,
- swapchain_recreate_count: renderer
- .as_ref()
- .map_or(0, |snapshot| snapshot.swapchain_recreate_count),
- validation_warning_count: validation.warning_count,
- validation_error_count: validation.error_count,
- validation_vuids: &validation.vuids,
- requested_frames: self.options.frames,
- timeout_seconds: self.options.timeout_seconds,
- mesh_source: self.options.mesh_input.kind(),
- mesh_archive: self.options.mesh_input.archive(),
- mesh_name: self.options.mesh_input.name(),
- mesh_vertex_count: self.mesh.vertices.len(),
- mesh_index_count: self.mesh.indices.len(),
- mesh_draw_count: self.mesh.draw_ranges.len(),
- texture_source: self
- .options
- .texture_input
- .as_ref()
- .map_or("none", TextureInput::source),
- texture_archive: self
- .options
- .texture_input
- .as_ref()
- .map_or("", TextureInput::archive),
- texture_name: match self.materials.as_slice() {
- [] => self
- .options
- .texture_input
- .as_ref()
- .map_or("", TextureInput::requested_name),
- [material] => material.selected_name.as_str(),
- _ => "multiple",
- },
- texture_width: self
- .materials
- .first()
- .map_or(0, |material| material.texture.width),
- texture_height: self
- .materials
- .first()
- .map_or(0, |material| material.texture.height),
- material_binding_count: self.materials.len(),
- mesh_material_indices: self
- .mesh
- .draw_ranges
- .iter()
- .map(|range| range.material_index)
- .collect(),
- material_texture_names: self
- .materials
- .iter()
- .map(|material| material.selected_name.clone())
- .collect(),
- shader_manifest_hash: renderer
- .as_ref()
- .map_or("", |snapshot| snapshot.report.shader_manifest_hash.as_str()),
- vulkan_loader_status: if renderer.is_some() {
- "available"
- } else {
- "failed"
- },
- vulkan_instance_status: if renderer.is_some() {
- "created"
- } else {
- "failed"
- },
- window_status: if self.window.is_some() {
- "created"
- } else {
- "failed"
- },
- vulkan_surface_status: if renderer.is_some() {
- "created"
- } else {
- "failed"
- },
- vulkan_device_status: if renderer.is_some() {
- "selected"
- } else {
- "failed"
- },
- vulkan_device_name: renderer
- .as_ref()
- .map_or("", |snapshot| snapshot.report.device_name.as_str()),
- vulkan_logical_device_status: if renderer.is_some() {
- "created"
- } else {
- "failed"
- },
- vulkan_logical_device_graphics_queue_family: renderer
- .as_ref()
- .map_or(0, |snapshot| snapshot.report.graphics_queue_family),
- vulkan_logical_device_present_queue_family: renderer
- .as_ref()
- .map_or(0, |snapshot| snapshot.report.present_queue_family),
- vulkan_logical_device_enabled_extension_count: renderer
- .as_ref()
- .map_or(0, |snapshot| snapshot.report.enabled_extension_count),
- vulkan_swapchain_status: if renderer.is_some() {
- "created"
- } else {
- "failed"
- },
- vulkan_swapchain_width: renderer
- .as_ref()
- .map_or(0, |snapshot| snapshot.report.swapchain_extent.0),
- vulkan_swapchain_height: renderer
- .as_ref()
- .map_or(0, |snapshot| snapshot.report.swapchain_extent.1),
- vulkan_swapchain_image_count: renderer
- .as_ref()
- .map_or(0, |snapshot| snapshot.report.swapchain_image_count),
- vulkan_swapchain_image_format: renderer
- .as_ref()
- .map_or(0, |snapshot| snapshot.report.swapchain_image_format),
- vulkan_swapchain_image_usage: renderer
- .as_ref()
- .map_or(0, |snapshot| snapshot.report.swapchain_image_usage),
- vulkan_readback_copy_count: renderer
- .as_ref()
- .map_or(0, |snapshot| snapshot.report.readback_copy_count),
- vulkan_readback_byte_count: renderer
- .as_ref()
- .map_or(0, |snapshot| snapshot.report.readback_byte_count),
- vulkan_readback_fnv1a64: renderer
- .as_ref()
- .map_or(0, |snapshot| snapshot.report.readback_fnv1a64),
- vulkan_portability_enumeration: renderer
- .as_ref()
- .is_some_and(|snapshot| snapshot.report.portability_enumeration),
- vulkan_portability_subset_enabled: renderer
- .as_ref()
- .is_some_and(|snapshot| snapshot.report.portability_subset_enabled),
- };
- serde_json::to_string_pretty(&smoke_report)
- .map(|json| format!("{json}\n"))
- .map_err(|err| format!("native smoke report serialization failed: {err}"))
- }
-
- fn write_failure_report(&self, failure_reason: &str) -> Result<(), String> {
- let report = self.render_report("failed", Some(failure_reason))?;
- self.write_report(&report)
- }
-
- fn abort_if_timed_out(&mut self, event_loop: &ActiveEventLoop) -> bool {
- if self.output.is_some() || self.error.is_some() {
- return false;
- }
- if self.started_at.elapsed() <= Duration::from_secs(self.options.timeout_seconds) {
- return false;
- }
- self.error = Some(format!(
- "native smoke timed out after {} seconds",
- self.options.timeout_seconds
- ));
- event_loop.exit();
- true
- }
-
- fn complete(&mut self, event_loop: &ActiveEventLoop) {
- let Some(renderer) = self.renderer.as_ref() else {
- self.error = Some("native smoke renderer was not initialized".to_string());
- event_loop.exit();
- return;
- };
- if self.frames_presented < self.options.frames {
- self.error = Some("native smoke did not reach the required frame count".to_string());
- event_loop.exit();
- return;
- }
- if self.resize_count == 0 || renderer.swapchain_recreate_count() == 0 {
- self.error = Some(
- "native smoke requires at least one measured resize and swapchain recreation"
- .to_string(),
- );
- event_loop.exit();
- return;
- }
- let Some(renderer) = self.renderer.take() else {
- self.error = Some("native smoke renderer was not initialized".to_string());
- event_loop.exit();
- return;
- };
- let final_renderer = match renderer.shutdown() {
- Ok(report) => RendererSnapshot::from(report),
- Err(err) => {
- self.error = Some(err.to_string());
- event_loop.exit();
- return;
- }
- };
- if final_renderer.validation.warning_count != 0
- || final_renderer.validation.error_count != 0
- {
- self.final_renderer = Some(final_renderer.clone());
- self.error = Some(format!(
- "native smoke validation must stay clean (warnings={}, errors={})",
- final_renderer.validation.warning_count, final_renderer.validation.error_count
- ));
- event_loop.exit();
- return;
- }
- self.final_renderer = Some(final_renderer);
- if let Some(artifact) = self
- .final_renderer
- .as_ref()
- .and_then(|snapshot| snapshot.readback_artifact.as_ref())
- {
- let path = match self.write_readback_artifact(artifact) {
- Ok(path) => path,
- Err(err) => {
- self.error = Some(err);
- event_loop.exit();
- return;
- }
- };
- if let Some(expected) = self.options.expected_readback.as_ref() {
- if let Err(err) = compare_readback_files(expected, &path) {
- self.error = Some(err);
- event_loop.exit();
- return;
- }
- }
- }
- let report = match self.render_report("passed", None) {
- Ok(report) => report,
- Err(err) => {
- self.error = Some(err);
- event_loop.exit();
- return;
- }
- };
- self.completed.store(true, Ordering::SeqCst);
- if let Err(err) = self.write_report(&report) {
- self.error = Some(err);
- event_loop.exit();
- return;
- }
- self.output = Some(report);
- event_loop.exit();
- }
-
- fn request_controlled_resize(&mut self) {
- if self.resize_requested {
- return;
- }
- let Some(window) = self.window.as_ref() else {
- return;
- };
- self.resize_requested = true;
- let requested = PhysicalSize::new(DEFAULT_RESIZE_WIDTH, DEFAULT_RESIZE_HEIGHT);
- let _ = window.request_inner_size(requested);
- }
-}
-
-impl Drop for SmokeApp {
- fn drop(&mut self) {
- // Keep the native window alive until the Vulkan renderer finishes
- // destroying swapchain and surface state that still references it.
- drop_renderer_before_window(&mut self.renderer, &mut self.window);
- }
-}
-
-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(),
- git_dirty: compiled_git_dirty(),
- runner_identity: measured_runner_identity(),
- runner_architecture: actual_architecture(),
- rust_toolchain: compiled_rust_toolchain(),
- target_triple: compiled_target_triple(),
- platform: actual_platform(),
- status: "failed",
- failure_reason: Some(failure_reason),
- 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,
- mesh_source: options.mesh_input.kind(),
- mesh_archive: options.mesh_input.archive(),
- mesh_name: options.mesh_input.name(),
- mesh_vertex_count: 0,
- mesh_index_count: 0,
- mesh_draw_count: 0,
- texture_source: options
- .texture_input
- .as_ref()
- .map_or("none", TextureInput::source),
- texture_archive: options
- .texture_input
- .as_ref()
- .map_or("", TextureInput::archive),
- texture_name: options
- .texture_input
- .as_ref()
- .map_or("", TextureInput::requested_name),
- texture_width: 0,
- texture_height: 0,
- material_binding_count: 0,
- mesh_material_indices: Vec::new(),
- material_texture_names: Vec::new(),
- shader_manifest_hash: "",
- vulkan_loader_status: if bootstrap.loader_available {
- "available"
- } else {
- "failed"
- },
- vulkan_instance_status: if bootstrap.instance_created {
- "created"
- } else {
- "failed"
- },
- window_status: progress.window_phase.status(),
- 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: 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: if bootstrap.swapchain_created {
- "created"
- } else {
- "failed"
- },
- vulkan_swapchain_width: 0,
- vulkan_swapchain_height: 0,
- vulkan_swapchain_image_count: 0,
- vulkan_swapchain_image_format: 0,
- vulkan_swapchain_image_usage: 0,
- vulkan_readback_copy_count: 0,
- vulkan_readback_byte_count: 0,
- vulkan_readback_fnv1a64: 0,
- vulkan_portability_enumeration: false,
- vulkan_portability_subset_enabled: false,
- };
- serde_json::to_string_pretty(&smoke_report)
- .map(|json| format!("{json}\n"))
- .map_err(|err| format!("native smoke report serialization failed: {err}"))
-}
-
-impl ApplicationHandler for SmokeApp {
- fn new_events(&mut self, event_loop: &ActiveEventLoop, _cause: StartCause) {
- let _ = self.abort_if_timed_out(event_loop);
- }
-
- fn resumed(&mut self, event_loop: &ActiveEventLoop) {
- if self.abort_if_timed_out(event_loop) {
- return;
- }
- self.progress.window_phase.store(WindowPhase::Resumed);
- if self.window.is_some() {
- return;
- }
- let plan = match WinitWindowPlan::smoke().validate() {
- Ok(plan) => plan,
- Err(err) => {
- self.error = Some(err.to_string());
- event_loop.exit();
- return;
- }
- };
- self.progress
- .window_phase
- .store(WindowPhase::CreatingWindow);
- let attributes = Window::default_attributes()
- .with_title("FParkan Vulkan smoke")
- .with_inner_size(PhysicalSize::new(plan.width, plan.height));
- let window = match event_loop.create_window(attributes) {
- Ok(window) => window,
- Err(err) => {
- self.error = Some(format!("winit window: {err}"));
- event_loop.exit();
- return;
- }
- };
- let Some(native_handles) = window_native_handles(&window) else {
- self.error = Some("winit window does not expose native handles".to_string());
- event_loop.exit();
- return;
- };
- let size = window.inner_size();
- let renderer = match VulkanSmokeRenderer::new(&VulkanSmokeRendererCreateInfo {
- application_name: "fparkan-vulkan-smoke".to_string(),
- native_handles,
- drawable_extent: (size.width.max(1), size.height.max(1)),
- render_request: RenderRequest::conservative(),
- enable_validation: true,
- mesh: self.mesh.clone(),
- camera: VulkanStaticCamera::default(),
- materials: self
- .materials
- .iter()
- .map(|material| VulkanStaticMaterial {
- material_index: material.material_index,
- texture: material.texture.clone(),
- })
- .collect(),
- bootstrap_progress: Some(Arc::clone(&self.progress.bootstrap)),
- }) {
- Ok(renderer) => renderer,
- Err(err) => {
- self.error = Some(err.to_string());
- event_loop.exit();
- return;
- }
- };
- self.last_size = Some((size.width, size.height));
- self.window_id = Some(window.id());
- self.progress.window_phase.store(WindowPhase::Created);
- self.renderer = Some(renderer);
- self.window = Some(window);
- self.schedule_next_redraw();
- }
-
- fn window_event(
- &mut self,
- event_loop: &ActiveEventLoop,
- window_id: WindowId,
- event: WindowEvent,
- ) {
- if self.abort_if_timed_out(event_loop) {
- return;
- }
- if Some(window_id) != self.window_id {
- return;
- }
- match event {
- WindowEvent::CloseRequested => {
- if self.output.is_none() {
- self.error = Some("native smoke window closed before completion".to_string());
- }
- event_loop.exit();
- }
- WindowEvent::Resized(size) => {
- if self
- .last_size
- .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() {
- renderer.request_resize((size.width, size.height));
- }
- }
- WindowEvent::RedrawRequested => {
- let Some(renderer) = self.renderer.as_mut() else {
- self.error = Some("native smoke renderer was not initialized".to_string());
- event_loop.exit();
- return;
- };
- if let Err(error) = renderer.set_camera(VulkanStaticCamera::default()) {
- self.error = Some(error.to_string());
- event_loop.exit();
- return;
- }
- 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,
- ) => {}
- Err(err) => {
- self.error = Some(err.to_string());
- event_loop.exit();
- return;
- }
- }
- 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
- && self.resize_count > 0
- && recreate_count > 0;
- let _ = renderer;
- if should_request_resize {
- self.request_controlled_resize();
- }
- if should_complete {
- self.complete(event_loop);
- } else {
- self.schedule_next_redraw();
- }
- }
- _ => {}
- }
- }
-
- fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
- if self.abort_if_timed_out(event_loop) {
- return;
- }
- if self.output.is_none() && self.error.is_none() {
- self.schedule_next_redraw();
- }
- }
-}
-
-#[derive(Serialize)]
-struct SmokeReport<'a> {
- schema_version: &'static str,
- commit_sha: String,
- git_dirty: bool,
- runner_identity: String,
- runner_architecture: &'static str,
- rust_toolchain: String,
- target_triple: String,
- platform: &'static str,
- status: &'a str,
- #[serde(skip_serializing_if = "Option::is_none")]
- failure_reason: Option<&'a str>,
- frames: u32,
- resize_count: u32,
- swapchain_recreate_count: u32,
- validation_warning_count: u32,
- validation_error_count: u32,
- validation_vuids: &'a [String],
- requested_frames: u32,
- timeout_seconds: u64,
- mesh_source: &'a str,
- mesh_archive: &'a str,
- mesh_name: &'a str,
- mesh_vertex_count: usize,
- mesh_index_count: usize,
- mesh_draw_count: usize,
- texture_source: &'a str,
- texture_archive: &'a str,
- texture_name: &'a str,
- texture_width: u32,
- texture_height: u32,
- material_binding_count: usize,
- mesh_material_indices: Vec<u16>,
- material_texture_names: Vec<String>,
- shader_manifest_hash: &'a str,
- vulkan_loader_status: &'a str,
- vulkan_instance_status: &'a str,
- window_status: &'a str,
- vulkan_surface_status: &'a str,
- vulkan_device_status: &'a str,
- vulkan_device_name: &'a str,
- vulkan_logical_device_status: &'a str,
- vulkan_logical_device_graphics_queue_family: u32,
- vulkan_logical_device_present_queue_family: u32,
- vulkan_logical_device_enabled_extension_count: u32,
- vulkan_swapchain_status: &'a str,
- vulkan_swapchain_width: u32,
- vulkan_swapchain_height: u32,
- vulkan_swapchain_image_count: u32,
- vulkan_swapchain_image_format: i32,
- vulkan_swapchain_image_usage: u32,
- vulkan_readback_copy_count: u64,
- vulkan_readback_byte_count: u64,
- vulkan_readback_fnv1a64: u64,
- vulkan_portability_enumeration: bool,
- vulkan_portability_subset_enabled: bool,
-}
-
-#[derive(Debug, Default)]
-struct SharedSmokeProgress {
- bootstrap: Arc<VulkanSmokeBootstrapProgress>,
- window_phase: AtomicWindowPhase,
- frames_presented: AtomicU32,
- resize_count: AtomicU32,
- swapchain_recreate_count: AtomicU32,
-}
-
-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
-enum WindowPhase {
- #[default]
- NotStarted,
- Resumed,
- CreatingWindow,
- Created,
-}
-
-impl WindowPhase {
- const fn as_u8(self) -> u8 {
- match self {
- Self::NotStarted => 0,
- Self::Resumed => 1,
- Self::CreatingWindow => 2,
- Self::Created => 3,
- }
- }
-
- const fn from_u8(value: u8) -> Self {
- match value {
- 1 => Self::Resumed,
- 2 => Self::CreatingWindow,
- 3 => Self::Created,
- _ => Self::NotStarted,
- }
- }
-
- const fn status(self) -> &'static str {
- match self {
- Self::NotStarted => "failed",
- Self::Resumed => "resumed",
- Self::CreatingWindow => "creating",
- Self::Created => "created",
- }
- }
-}
-
-#[derive(Debug, Default)]
-struct AtomicWindowPhase(AtomicU32);
-
-impl AtomicWindowPhase {
- fn store(&self, phase: WindowPhase) {
- self.0.store(u32::from(phase.as_u8()), Ordering::SeqCst);
- }
-
- fn status(&self) -> &'static str {
- match u8::try_from(self.0.load(Ordering::SeqCst)) {
- Ok(value) => WindowPhase::from_u8(value),
- Err(_) => WindowPhase::NotStarted,
- }
- .status()
- }
-}
-
-fn actual_platform() -> &'static str {
- match std::env::consts::OS {
- "windows" => "windows",
- other => other,
- }
-}
-
-fn actual_architecture() -> &'static str {
- match std::env::consts::ARCH {
- "arm64" => "aarch64",
- other => other,
- }
-}
-
-fn compiled_commit_sha() -> String {
- option_env!("FPARKAN_BUILD_COMMIT_SHA")
- .filter(|value| is_commit_sha(value))
- .map_or_else(runtime_git_commit_sha, ToString::to_string)
-}
-
-fn runtime_git_commit_sha() -> String {
- Command::new("git")
- .args(["rev-parse", "HEAD"])
- .output()
- .ok()
- .filter(|output| output.status.success())
- .and_then(|output| String::from_utf8(output.stdout).ok())
- .map(|value| value.trim().to_string())
- .filter(|value| is_commit_sha(value))
- .unwrap_or_else(|| "unknown".to_string())
-}
-
-fn compiled_git_dirty() -> bool {
- option_env!("FPARKAN_BUILD_GIT_DIRTY")
- .and_then(parse_bool_env)
- .unwrap_or_else(runtime_git_dirty)
-}
-
-fn runtime_git_dirty() -> bool {
- Command::new("git")
- .args(["status", "--short"])
- .output()
- .ok()
- .filter(|output| output.status.success())
- .and_then(|output| String::from_utf8(output.stdout).ok())
- .is_some_and(|output| !output.trim().is_empty())
-}
-
-fn compiled_rust_toolchain() -> String {
- option_env!("FPARKAN_BUILD_RUST_TOOLCHAIN")
- .filter(|value| !value.trim().is_empty())
- .map_or_else(current_rustc_release, ToString::to_string)
-}
-
-fn current_rustc_release() -> String {
- Command::new("rustc")
- .arg("-Vv")
- .output()
- .ok()
- .filter(|output| output.status.success())
- .and_then(|output| String::from_utf8(output.stdout).ok())
- .and_then(|output| {
- output
- .lines()
- .find_map(|line| line.strip_prefix("release: ").map(ToString::to_string))
- })
- .unwrap_or_else(|| "unknown".to_string())
-}
-
-fn compiled_target_triple() -> String {
- option_env!("FPARKAN_BUILD_TARGET_TRIPLE")
- .filter(|value| !value.trim().is_empty())
- .map_or_else(current_rustc_host_triple, ToString::to_string)
-}
-
-fn measured_runner_identity() -> String {
- if std::env::var_os("GITHUB_ACTIONS").is_some() {
- let run_id = std::env::var("GITHUB_RUN_ID").unwrap_or_else(|_| "unknown-run".to_string());
- let job = std::env::var("GITHUB_JOB").unwrap_or_else(|_| "unknown-job".to_string());
- format!("github-actions/{run_id}/{job}")
- } else if std::env::var_os("CI").is_some() {
- let job = std::env::var("CI_JOB_NAME")
- .or_else(|_| std::env::var("BUILD_ID"))
- .unwrap_or_else(|_| "generic-ci".to_string());
- format!("ci/{job}")
- } else {
- format!("local/{}", std::env::consts::OS)
- }
-}
-
-fn current_rustc_host_triple() -> String {
- Command::new("rustc")
- .arg("-Vv")
- .output()
- .ok()
- .filter(|output| output.status.success())
- .and_then(|output| String::from_utf8(output.stdout).ok())
- .and_then(|output| {
- output
- .lines()
- .find_map(|line| line.strip_prefix("host: ").map(ToString::to_string))
- })
- .unwrap_or_else(|| "unknown".to_string())
-}
-
-fn is_commit_sha(value: &str) -> bool {
- value.len() == 40 && value.chars().all(|ch| ch.is_ascii_hexdigit())
-}
-
-fn parse_bool_env(value: &str) -> Option<bool> {
- match value {
- "true" => Some(true),
- "false" => Some(false),
- _ => None,
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use std::cell::RefCell;
- use std::rc::Rc;
-
- #[derive(Clone, Copy, Debug, Eq, PartialEq)]
- enum DropStep {
- Renderer,
- Window,
- }
-
- struct DropTracker {
- step: DropStep,
- log: Rc<RefCell<Vec<DropStep>>>,
- }
-
- impl Drop for DropTracker {
- fn drop(&mut self) {
- self.log.borrow_mut().push(self.step);
- }
- }
-
- fn tracker(step: DropStep, log: &Rc<RefCell<Vec<DropStep>>>) -> DropTracker {
- DropTracker {
- step,
- log: Rc::clone(log),
- }
- }
-
- #[test]
- fn parses_required_args() {
- let parsed = SmokeOptions::parse(&["--out".to_string(), "target/report.json".to_string()]);
-
- assert_eq!(
- parsed,
- Ok(SmokeOptions {
- out: PathBuf::from("target/report.json"),
- frames: DEFAULT_TARGET_FRAMES,
- resize_frame: DEFAULT_RESIZE_FRAME,
- timeout_seconds: DEFAULT_TIMEOUT_SECONDS,
- expected_readback: None,
- mesh_input: MeshInput::SmokeTriangle,
- texture_input: None,
- })
- );
- }
-
- #[test]
- fn rejects_too_few_frames() {
- let parsed = SmokeOptions::parse(&[
- "--out".to_string(),
- "target/report.json".to_string(),
- "--frames".to_string(),
- "299".to_string(),
- ]);
-
- assert_eq!(
- parsed,
- Err("native smoke requires --frames >= 300".to_string())
- );
- }
-
- #[test]
- fn parses_timeout_seconds() {
- let parsed = SmokeOptions::parse(&[
- "--out".to_string(),
- "target/report.json".to_string(),
- "--timeout-seconds".to_string(),
- "45".to_string(),
- ]);
-
- assert_eq!(
- parsed,
- Ok(SmokeOptions {
- out: PathBuf::from("target/report.json"),
- frames: DEFAULT_TARGET_FRAMES,
- resize_frame: DEFAULT_RESIZE_FRAME,
- timeout_seconds: 45,
- expected_readback: None,
- mesh_input: MeshInput::SmokeTriangle,
- texture_input: None,
- })
- );
- }
-
- #[test]
- fn rejects_zero_timeout_seconds() {
- let parsed = SmokeOptions::parse(&[
- "--out".to_string(),
- "target/report.json".to_string(),
- "--timeout-seconds".to_string(),
- "0".to_string(),
- ]);
-
- assert_eq!(
- parsed,
- Err("native smoke requires --timeout-seconds >= 1".to_string())
- );
- }
-
- #[test]
- fn parses_original_msh_input_as_one_atomic_request() {
- let parsed = SmokeOptions::parse(&[
- "--out".to_string(),
- "target/report.json".to_string(),
- "--model-root".to_string(),
- "C:/GOG".to_string(),
- "--model-archive".to_string(),
- "system.rlb".to_string(),
- "--model-name".to_string(),
- "MTCHECK.MSH".to_string(),
- ]);
-
- assert!(matches!(
- parsed,
- Ok(SmokeOptions {
- mesh_input: MeshInput::Msh { archive, name, .. },
- ..
- }) if archive == "system.rlb" && name == "MTCHECK.MSH"
- ));
- }
-
- #[test]
- fn parses_original_land_msh_as_direct_terrain_input() {
- let parsed = SmokeOptions::parse(&[
- "--out".to_string(),
- "target/report.json".to_string(),
- "--terrain-path".to_string(),
- "C:/GOG/DATA/MAPS/11/Land.msh".to_string(),
- ]);
-
- assert!(matches!(
- parsed,
- Ok(SmokeOptions {
- mesh_input: MeshInput::LandMsh { path },
- ..
- }) if path == std::path::Path::new("C:/GOG/DATA/MAPS/11/Land.msh")
- ));
- }
-
- #[test]
- fn rejects_partial_original_msh_input() {
- let parsed = SmokeOptions::parse(&[
- "--out".to_string(),
- "target/report.json".to_string(),
- "--model-root".to_string(),
- "C:/GOG".to_string(),
- ]);
-
- assert_eq!(
- parsed,
- Err(
- "--model-root, --model-archive and --model-name must be supplied together"
- .to_string()
- )
- );
- }
-
- #[test]
- fn parses_original_texm_input_as_one_atomic_request() {
- let parsed = SmokeOptions::parse(&[
- "--out".to_string(),
- "target/report.json".to_string(),
- "--texture-root".to_string(),
- "C:/GOG".to_string(),
- "--texture-archive".to_string(),
- "Textures.lib".to_string(),
- "--texture-name".to_string(),
- "DEFAULT.0".to_string(),
- ]);
-
- assert!(matches!(
- parsed,
- Ok(SmokeOptions {
- texture_input: Some(TextureInput::Direct(ResourceInput { archive, name, .. })),
- ..
- }) if archive == "Textures.lib" && name == "DEFAULT.0"
- ));
- }
-
- #[test]
- fn parses_original_wear_material_input_as_one_atomic_request() {
- let parsed = SmokeOptions::parse(&[
- "--out".to_string(),
- "target/report.json".to_string(),
- "--wear-root".to_string(),
- "C:/GOG".to_string(),
- "--wear-archive".to_string(),
- "system.rlb".to_string(),
- "--wear-name".to_string(),
- "MODEL.WEA".to_string(),
- "--material-index".to_string(),
- "7".to_string(),
- ]);
-
- assert!(matches!(
- parsed,
- Ok(SmokeOptions {
- texture_input: Some(TextureInput::WearMaterial(WearMaterialInput {
- archive,
- name,
- material_index,
- ..
- })),
- ..
- }) if archive == "system.rlb" && name == "MODEL.WEA" && material_index == 7
- ));
- }
-
- #[test]
- fn parses_original_wear_batch_material_input_without_override() {
- let parsed = SmokeOptions::parse(&[
- "--out".to_string(),
- "target/report.json".to_string(),
- "--wear-root".to_string(),
- "C:/GOG".to_string(),
- "--wear-archive".to_string(),
- "system.rlb".to_string(),
- "--wear-name".to_string(),
- "MODEL.WEA".to_string(),
- ]);
-
- assert!(matches!(
- parsed,
- Ok(SmokeOptions {
- texture_input: Some(TextureInput::WearBatchMaterials(WearInput { archive, name, .. })),
- ..
- }) if archive == "system.rlb" && name == "MODEL.WEA"
- ));
- }
-
- #[test]
- fn rejects_deprecated_self_assertion_flags() {
- for flag in [
- "--status",
- "--platform",
- "--validation-error-count",
- "--resize-count",
- "--swapchain-recreate-count",
- ] {
- let parsed = SmokeOptions::parse(&[
- "--out".to_string(),
- "target/report.json".to_string(),
- flag.to_string(),
- "value".to_string(),
- ]);
-
- assert_eq!(parsed, Err(format!("unknown native smoke option: {flag}")));
- }
- }
-
- #[test]
- fn commit_sha_validation_accepts_hex_head() {
- assert!(is_commit_sha("0123456789abcdef0123456789abcdef01234567"));
- }
-
- #[test]
- fn commit_sha_validation_rejects_non_hex_or_wrong_length() {
- assert!(!is_commit_sha("0123456789abcdef0123456789abcdef0123456"));
- assert!(!is_commit_sha("zz23456789abcdef0123456789abcdef01234567"));
- }
-
- #[test]
- fn parses_bool_env_values() {
- assert_eq!(parse_bool_env("true"), Some(true));
- assert_eq!(parse_bool_env("false"), Some(false));
- assert_eq!(parse_bool_env("1"), None);
- }
-
- #[test]
- fn smoke_report_json_contains_expected_fields() {
- let json = serde_json::to_string_pretty(&SmokeReport {
- schema_version: SCHEMA_VERSION,
- commit_sha: "0123456789abcdef0123456789abcdef01234567".to_string(),
- git_dirty: false,
- runner_identity: "windows-local/stage0".to_string(),
- runner_architecture: "x86_64",
- rust_toolchain: "1.97.1".to_string(),
- target_triple: "x86_64-pc-windows-msvc".to_string(),
- platform: "windows",
- status: "passed",
- failure_reason: None,
- frames: 300,
- resize_count: 1,
- swapchain_recreate_count: 1,
- validation_warning_count: 0,
- validation_error_count: 0,
- validation_vuids: &["VUID-A".to_string(), "VUID-B".to_string()],
- requested_frames: 300,
- timeout_seconds: DEFAULT_TIMEOUT_SECONDS,
- mesh_source: "original-msh",
- mesh_archive: "system.rlb",
- mesh_name: "MTCHECK.MSH",
- mesh_vertex_count: 128,
- mesh_index_count: 252,
- mesh_draw_count: 1,
- texture_source: "original-texm",
- texture_archive: "Textures.lib",
- texture_name: "DEFAULT.0",
- texture_width: 16,
- texture_height: 16,
- material_binding_count: 1,
- mesh_material_indices: vec![0],
- material_texture_names: vec!["DEFAULT.0".to_string()],
- shader_manifest_hash: "deadbeef",
- vulkan_loader_status: "available",
- vulkan_instance_status: "created",
- window_status: "created",
- vulkan_surface_status: "created",
- vulkan_device_status: "selected",
- vulkan_device_name: "Windows test GPU",
- vulkan_logical_device_status: "created",
- vulkan_logical_device_graphics_queue_family: 0,
- vulkan_logical_device_present_queue_family: 0,
- vulkan_logical_device_enabled_extension_count: 2,
- vulkan_swapchain_status: "created",
- vulkan_swapchain_width: 960,
- vulkan_swapchain_height: 540,
- vulkan_swapchain_image_count: 3,
- vulkan_swapchain_image_format: 50,
- vulkan_swapchain_image_usage: 17,
- vulkan_readback_copy_count: 300,
- vulkan_readback_byte_count: 4_147_200,
- vulkan_readback_fnv1a64: 0x1234_5678_90ab_cdef,
- vulkan_portability_enumeration: false,
- vulkan_portability_subset_enabled: false,
- })
- .expect("smoke report should serialize");
-
- assert!(json.contains("\"schema_version\": \"fparkan-native-smoke-v1\""));
- assert!(json.contains("\"validation_vuids\": ["));
- assert!(json.contains("\"vulkan_device_name\": \"Windows test GPU\""));
- assert!(json.contains("\"runner_architecture\": \"x86_64\""));
- assert!(json.contains("\"mesh_source\": \"original-msh\""));
- assert!(json.contains("\"mesh_draw_count\": 1"));
- assert!(json.contains("\"texture_source\": \"original-texm\""));
- assert!(json.contains("\"vulkan_readback_copy_count\": 300"));
- assert!(json.contains("\"vulkan_readback_byte_count\": 4147200"));
- }
-
- #[test]
- fn finish_writes_failure_artifact() {
- let root = std::env::temp_dir().join(format!(
- "fparkan-native-smoke-{}-{}",
- std::process::id(),
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .expect("time")
- .as_nanos()
- ));
- std::fs::create_dir_all(&root).expect("temp dir");
- let out = root.join("report.json");
-
- let result = SmokeApp {
- options: SmokeOptions {
- out: out.clone(),
- frames: DEFAULT_TARGET_FRAMES,
- resize_frame: DEFAULT_RESIZE_FRAME,
- timeout_seconds: 7,
- expected_readback: None,
- mesh_input: MeshInput::SmokeTriangle,
- texture_input: None,
- },
- mesh: VulkanStaticMesh::smoke_triangle(),
- materials: Vec::new(),
- completed: Arc::new(AtomicBool::new(false)),
- progress: Arc::new(SharedSmokeProgress::default()),
- window_id: None,
- window: None,
- renderer: None,
- final_renderer: None,
- error: Some("native smoke timed out after 7 seconds".to_string()),
- output: None,
- frames_presented: 42,
- resize_count: 0,
- resize_requested: false,
- last_size: None,
- started_at: Instant::now(),
- }
- .finish();
-
- assert_eq!(
- result,
- Err("native smoke timed out after 7 seconds".to_string())
- );
-
- let json = std::fs::read_to_string(&out).expect("failure report");
- assert!(json.contains("\"status\": \"failed\""));
- assert!(json.contains("\"failure_reason\": \"native smoke timed out after 7 seconds\""));
- assert!(json.contains("\"timeout_seconds\": 7"));
-
- std::fs::remove_file(out).expect("cleanup report");
- std::fs::remove_dir(root).expect("cleanup dir");
- }
-
- #[test]
- fn readback_comparator_accepts_exact_bytes_and_reports_first_difference() {
- let root = std::env::temp_dir().join(format!("fparkan-readback-{}", std::process::id()));
- std::fs::create_dir_all(&root).expect("temp dir");
- let expected = root.join("expected.readback-vkformat-50.raw");
- let actual = root.join("actual.readback-vkformat-50.raw");
- std::fs::write(&expected, [1_u8, 2, 3]).expect("expected");
- std::fs::write(&actual, [1_u8, 2, 3]).expect("actual");
- assert_eq!(compare_readback_files(&expected, &actual), Ok(()));
- std::fs::write(&actual, [1_u8, 9, 3]).expect("different actual");
- assert_eq!(
- compare_readback_files(&expected, &actual),
- Err("pixel readback mismatch at byte 1: expected=2 actual=9".to_string())
- );
- let incompatible = root.join("incompatible.readback-vkformat-37.raw");
- std::fs::write(&incompatible, [1_u8, 2, 3]).expect("incompatible actual");
- assert_eq!(
- compare_readback_files(&expected, &incompatible),
- Err("pixel readback format mismatch: expected=50 actual=37".to_string())
- );
- std::fs::remove_dir_all(root).expect("cleanup");
- }
-
- #[test]
- fn renderer_is_dropped_before_window() {
- let log = Rc::new(RefCell::new(Vec::new()));
- let mut renderer = Some(tracker(DropStep::Renderer, &log));
- let mut window = Some(tracker(DropStep::Window, &log));
-
- drop_renderer_before_window(&mut renderer, &mut window);
-
- assert!(renderer.is_none());
- assert!(window.is_none());
- assert_eq!(
- Rc::into_inner(log).expect("trackers released").into_inner(),
- vec![DropStep::Renderer, DropStep::Window]
- );
- }
-}