aboutsummaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-25 03:18:32 +0300
committerValentin Popov <valentin@popov.link>2026-06-25 10:45:32 +0300
commitba69bdb6eab57817b45055d60ea1d2f6687757a8 (patch)
tree6c1ea4db8fd2a715c01a38d2fc3fb71140320eb3 /apps
parent5cc2c5819f2dcfc9b9a8b86615d604d2b8f4c018 (diff)
downloadfparkan-ba69bdb6eab57817b45055d60ea1d2f6687757a8.tar.xz
fparkan-ba69bdb6eab57817b45055d60ea1d2f6687757a8.zip
feat(stage0): close native smoke acceptance gate
Diffstat (limited to 'apps')
-rw-r--r--apps/fparkan-game/src/main.rs4
-rw-r--r--apps/fparkan-vulkan-smoke/Cargo.toml1
-rw-r--r--apps/fparkan-vulkan-smoke/src/main.rs1747
3 files changed, 342 insertions, 1410 deletions
diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs
index 6f132e5..5663330 100644
--- a/apps/fparkan-game/src/main.rs
+++ b/apps/fparkan-game/src/main.rs
@@ -28,7 +28,7 @@ use fparkan_render::{
DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderBackend, RenderCommand,
RenderCommandList, RenderPhase,
};
-use fparkan_render_vulkan::VulkanBackend;
+use fparkan_render_vulkan::VulkanPlanningBackend;
use fparkan_runtime::{
create, frame, load_mission, loaded_mission_assets, EngineConfig, EngineMode, EngineServices,
MissionAssets, MissionRequest,
@@ -71,7 +71,7 @@ fn run(args: &[String]) -> Result<String, String> {
)
.map_err(|err| err.to_string())?;
- let mut backend = VulkanBackend::new();
+ let mut backend = VulkanPlanningBackend::new();
let _request = WinitWindow::default_render_request();
let window = WinitWindow::synthetic(1280, 720);
let _ = window.drawable_size();
diff --git a/apps/fparkan-vulkan-smoke/Cargo.toml b/apps/fparkan-vulkan-smoke/Cargo.toml
index 21b67ae..3744849 100644
--- a/apps/fparkan-vulkan-smoke/Cargo.toml
+++ b/apps/fparkan-vulkan-smoke/Cargo.toml
@@ -9,6 +9,7 @@ repository.workspace = true
fparkan-platform = { path = "../../crates/fparkan-platform" }
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit" }
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" }
+winit = "0.30"
[lints]
workspace = true
diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs
index 4e60ec8..d91988b 100644
--- a/apps/fparkan-vulkan-smoke/src/main.rs
+++ b/apps/fparkan-vulkan-smoke/src/main.rs
@@ -11,19 +11,23 @@
#![allow(clippy::print_stderr, clippy::print_stdout)]
//! Native Vulkan smoke runner entrypoint.
-use fparkan_platform::{NativeWindowHandles, WindowPort};
-use fparkan_platform_winit::{probe_smoke_window, WinitWindowPlan};
+use fparkan_platform_winit::{window_native_handles, 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, run_vulkan_smoke_pass,
- triangle_shader_manifest, validate_shader_manifest, VulkanInstanceConfig, VulkanInstanceProbe,
- VulkanLogicalDeviceProbe, VulkanSwapchainProbe,
+ VulkanSmokeFrameOutcome, VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo,
};
use std::path::PathBuf;
use std::process::Command;
+use winit::application::ApplicationHandler;
+use winit::dpi::PhysicalSize;
+use winit::event::WindowEvent;
+use winit::event_loop::{ActiveEventLoop, EventLoop};
+use winit::window::{Window, WindowId};
const SCHEMA_VERSION: &str = "fparkan-native-smoke-v1";
-const RUST_TOOLCHAIN: &str = "1.87.0";
+const DEFAULT_TARGET_FRAMES: u32 = 300;
+const DEFAULT_RESIZE_FRAME: u32 = 120;
+const DEFAULT_RESIZE_WIDTH: u32 = 960;
+const DEFAULT_RESIZE_HEIGHT: u32 = 540;
fn main() {
let args = std::env::args().skip(1).collect::<Vec<_>>();
@@ -42,878 +46,372 @@ fn main() {
fn run(args: &[String]) -> Result<String, String> {
let options = SmokeOptions::parse(args)?;
- 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()))?;
- }
- std::fs::write(&options.out, &report)
- .map_err(|err| format!("{}: {err}", options.out.display()))?;
- Ok(report)
+ let event_loop = EventLoop::new().map_err(|err| format!("winit event loop: {err}"))?;
+ let mut app = SmokeApp::new(options);
+ event_loop
+ .run_app(&mut app)
+ .map_err(|err| format!("winit event loop: {err}"))?;
+ app.finish()
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct SmokeOptions {
- platform: SmokePlatform,
out: PathBuf,
- status: SmokeStatus,
frames: u32,
- resize_count: u32,
- swapchain_recreate_count: u32,
- validation_error_count: Option<u32>,
- probes: ProbeOptions,
- reason: Option<String>,
+ resize_frame: u32,
}
impl SmokeOptions {
fn parse(args: &[String]) -> Result<Self, String> {
- let mut platform = None;
let mut out = None;
- let mut status = SmokeStatus::Blocked;
- let mut frames = 0;
- let mut resize_count = 0;
- let mut swapchain_recreate_count = 0;
- let mut validation_error_count = None;
- let mut probes = ProbeOptions::default();
- let mut reason = None;
+ let mut frames = DEFAULT_TARGET_FRAMES;
+ let mut resize_frame = DEFAULT_RESIZE_FRAME;
let mut iter = args.iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
- "--platform" => {
- let value = iter
- .next()
- .ok_or_else(|| "--platform requires a value".to_string())?;
- platform = Some(SmokePlatform::parse(value)?);
- }
"--out" => {
- let value = iter
- .next()
- .ok_or_else(|| "--out requires a path".to_string())?;
- out = Some(PathBuf::from(value));
- }
- "--status" => {
- let value = iter
- .next()
- .ok_or_else(|| "--status requires a value".to_string())?;
- status = SmokeStatus::parse(value)?;
+ out = Some(
+ iter.next()
+ .map(PathBuf::from)
+ .ok_or_else(|| "--out requires a path".to_string())?,
+ );
}
"--frames" => {
- let value = iter
- .next()
- .ok_or_else(|| "--frames requires a value".to_string())?;
- frames = parse_u32("--frames", value)?;
- }
- "--resize-count" => {
- let value = iter
- .next()
- .ok_or_else(|| "--resize-count requires a value".to_string())?;
- resize_count = parse_u32("--resize-count", value)?;
- }
- "--swapchain-recreate-count" => {
- let value = iter
- .next()
- .ok_or_else(|| "--swapchain-recreate-count requires a value".to_string())?;
- swapchain_recreate_count = parse_u32("--swapchain-recreate-count", value)?;
- }
- "--validation-error-count" => {
- let value = iter
+ frames = iter
.next()
- .ok_or_else(|| "--validation-error-count requires a value".to_string())?;
- validation_error_count = Some(parse_u32("--validation-error-count", value)?);
- }
- "--probe-loader" => {
- probes.vulkan = probes.vulkan.max(VulkanProbeDepth::Loader);
- }
- "--probe-instance" => {
- probes.vulkan = probes.vulkan.max(VulkanProbeDepth::Instance);
- }
- "--probe-window" => {
- probes.window = true;
- }
- "--probe-surface" => {
- probes.vulkan = probes.vulkan.max(VulkanProbeDepth::Surface);
- probes.window = true;
+ .ok_or_else(|| "--frames requires a value".to_string())?
+ .parse()
+ .map_err(|_| "--frames must be an integer".to_string())?;
}
- "--reason" => {
- let value = iter
+ "--resize-frame" => {
+ resize_frame = iter
.next()
- .ok_or_else(|| "--reason requires a value".to_string())?;
- reason = Some(value.to_string());
+ .ok_or_else(|| "--resize-frame requires a value".to_string())?
+ .parse()
+ .map_err(|_| "--resize-frame must be an integer".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}"
+ ));
+ }
Ok(Self {
- platform: platform.ok_or_else(|| "missing --platform".to_string())?,
- out: out.ok_or_else(|| "missing --out".to_string())?,
- status,
+ out,
frames,
- resize_count,
- swapchain_recreate_count,
- validation_error_count,
- probes,
- reason,
+ resize_frame,
})
}
}
-fn parse_u32(name: &str, value: &str) -> Result<u32, String> {
- value
- .parse::<u32>()
- .map_err(|_| format!("invalid {name} value: {value}"))
-}
-
-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
-struct ProbeOptions {
- vulkan: VulkanProbeDepth,
- window: bool,
-}
-
-#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
-enum VulkanProbeDepth {
- #[default]
- None,
- Loader,
- Instance,
- Surface,
+struct SmokeApp {
+ options: SmokeOptions,
+ window_id: Option<WindowId>,
+ window: Option<Window>,
+ renderer: Option<VulkanSmokeRenderer>,
+ error: Option<String>,
+ output: Option<String>,
+ frames_presented: u32,
+ resize_count: u32,
+ resize_requested: bool,
+ last_size: Option<(u32, u32)>,
}
-impl VulkanProbeDepth {
- const fn includes_loader(self) -> bool {
- matches!(self, Self::Loader | Self::Instance | Self::Surface)
- }
-
- const fn includes_instance(self) -> bool {
- matches!(self, Self::Instance | Self::Surface)
+impl SmokeApp {
+ const fn new(options: SmokeOptions) -> Self {
+ Self {
+ options,
+ window_id: None,
+ window: None,
+ renderer: None,
+ error: None,
+ output: None,
+ frames_presented: 0,
+ resize_count: 0,
+ resize_requested: false,
+ last_size: None,
+ }
}
- const fn includes_surface(self) -> bool {
- matches!(self, Self::Surface)
+ fn finish(self) -> Result<String, String> {
+ if let Some(error) = self.error {
+ return Err(error);
+ }
+ self.output
+ .ok_or_else(|| "native smoke exited before producing a report".to_string())
}
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-struct VulkanBootstrapProbe {
- loader_status: VulkanLoaderStatus,
- instance_api: Option<String>,
- loader_error: Option<String>,
- instance_status: VulkanInstanceStatus,
- instance_error: Option<String>,
- portability_enumeration: bool,
- window_status: WinitWindowStatus,
- window_width: Option<u32>,
- window_height: Option<u32>,
- window_error: Option<String>,
- surface_status: VulkanSurfaceStatus,
- surface_error: Option<String>,
- device_status: VulkanDeviceStatus,
- device_name: Option<String>,
- device_error: Option<String>,
- logical_device_status: VulkanLogicalDeviceStatus,
- logical_device_graphics_queue_family: Option<u32>,
- logical_device_present_queue_family: Option<u32>,
- logical_device_enabled_extension_count: Option<u32>,
- logical_device_error: Option<String>,
- swapchain_status: VulkanSwapchainStatus,
- swapchain_width: Option<u32>,
- swapchain_height: Option<u32>,
- swapchain_image_count: Option<u32>,
- swapchain_error: Option<String>,
-}
-
-struct VulkanRuntimePass {
- instance: VulkanInstanceProbe,
- surface: fparkan_render_vulkan::VulkanSurfaceProbe,
- device: VulkanLogicalDeviceProbe,
- swapchain: VulkanSwapchainProbe,
-}
-impl VulkanBootstrapProbe {
- fn run(options: &SmokeOptions) -> (Self, Option<VulkanRuntimePass>) {
- if !options.probes.vulkan.includes_loader() {
- return (Self::skipped(), None);
+ fn schedule_next_redraw(&self) {
+ if let Some(window) = self.window.as_ref() {
+ window.request_redraw();
}
+ }
- let mut probe = Self::probe_loader();
- let window_handles = probe.probe_window(options);
- let instance = probe.probe_instance(options);
- 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
+ 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 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,
- }),
- );
+ let validation = renderer.validation_report();
+ 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;
+ }
+ if validation.warning_count != 0 || validation.error_count != 0 {
+ self.error = Some(format!(
+ "native smoke validation must stay clean (warnings={}, errors={})",
+ validation.warning_count, validation.error_count
+ ));
+ event_loop.exit();
+ return;
+ }
+ let report = render_smoke_report_json(
+ &self.options,
+ renderer,
+ self.frames_presented,
+ self.resize_count,
+ validation.warning_count,
+ validation.error_count,
+ &validation.vuids,
+ );
+ if let Some(parent) = self.options.out.parent() {
+ if let Err(err) = std::fs::create_dir_all(parent) {
+ self.error = Some(format!("{}: {err}", parent.display()));
+ event_loop.exit();
+ return;
}
}
-
- (probe, None)
- }
-
- const fn skipped() -> Self {
- Self {
- loader_status: VulkanLoaderStatus::Skipped,
- instance_api: None,
- loader_error: None,
- instance_status: VulkanInstanceStatus::Skipped,
- instance_error: None,
- portability_enumeration: false,
- window_status: WinitWindowStatus::Skipped,
- window_width: None,
- window_height: None,
- window_error: None,
- surface_status: VulkanSurfaceStatus::Skipped,
- surface_error: None,
- device_status: VulkanDeviceStatus::Skipped,
- device_name: None,
- device_error: None,
- logical_device_status: VulkanLogicalDeviceStatus::Skipped,
- logical_device_graphics_queue_family: None,
- logical_device_present_queue_family: None,
- logical_device_enabled_extension_count: None,
- logical_device_error: None,
- swapchain_status: VulkanSwapchainStatus::Skipped,
- swapchain_width: None,
- swapchain_height: None,
- swapchain_image_count: None,
- swapchain_error: None,
+ if let Err(err) = std::fs::write(&self.options.out, &report) {
+ self.error = Some(format!("{}: {err}", self.options.out.display()));
+ event_loop.exit();
+ return;
}
+ self.output = Some(report);
+ event_loop.exit();
}
- fn probe_loader() -> Self {
- match probe_vulkan_loader() {
- Ok(report) => Self {
- loader_status: VulkanLoaderStatus::Available,
- instance_api: Some(format_api_version(report.instance_api_version)),
- loader_error: None,
- instance_status: VulkanInstanceStatus::Skipped,
- instance_error: None,
- portability_enumeration: false,
- window_status: WinitWindowStatus::Skipped,
- window_width: None,
- window_height: None,
- window_error: None,
- surface_status: VulkanSurfaceStatus::Skipped,
- surface_error: None,
- device_status: VulkanDeviceStatus::Skipped,
- device_name: None,
- device_error: None,
- logical_device_status: VulkanLogicalDeviceStatus::Skipped,
- logical_device_graphics_queue_family: None,
- logical_device_present_queue_family: None,
- logical_device_enabled_extension_count: None,
- logical_device_error: None,
- swapchain_status: VulkanSwapchainStatus::Skipped,
- swapchain_width: None,
- swapchain_height: None,
- swapchain_image_count: None,
- swapchain_error: None,
- },
- Err(err) => Self {
- loader_status: VulkanLoaderStatus::Unavailable,
- instance_api: None,
- loader_error: Some(err.to_string()),
- instance_status: VulkanInstanceStatus::Skipped,
- instance_error: None,
- portability_enumeration: false,
- window_status: WinitWindowStatus::Skipped,
- window_width: None,
- window_height: None,
- window_error: None,
- surface_status: VulkanSurfaceStatus::Skipped,
- surface_error: None,
- device_status: VulkanDeviceStatus::Skipped,
- device_name: None,
- device_error: None,
- logical_device_status: VulkanLogicalDeviceStatus::Skipped,
- logical_device_graphics_queue_family: None,
- logical_device_present_queue_family: None,
- logical_device_enabled_extension_count: None,
- logical_device_error: None,
- swapchain_status: VulkanSwapchainStatus::Skipped,
- swapchain_width: None,
- swapchain_height: None,
- swapchain_image_count: None,
- swapchain_error: None,
- },
+ 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);
}
+}
- fn probe_window(&mut self, options: &SmokeOptions) -> Option<NativeWindowHandles> {
- if options.probes.vulkan.includes_surface() {
- match probe_smoke_window() {
- Ok(window) => {
- self.window_status = WinitWindowStatus::Created;
- self.window_width = Some(window.window.drawable_size().width);
- self.window_height = Some(window.window.drawable_size().height);
- window.native_handles()
- }
- Err(err) => {
- self.window_status = WinitWindowStatus::Failed;
- self.window_error = Some(err.to_string());
- None
- }
+impl ApplicationHandler for SmokeApp {
+ fn resumed(&mut self, event_loop: &ActiveEventLoop) {
+ 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;
}
- } else if options.probes.window {
- match WinitWindowPlan::smoke().validate() {
- Ok(plan) => {
- self.window_status = WinitWindowStatus::Planned;
- self.window_width = Some(plan.width);
- self.window_height = Some(plan.height);
- }
- Err(err) => {
- self.window_status = WinitWindowStatus::Failed;
- self.window_error = Some(err.to_string());
- }
+ };
+ 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;
}
- None
- } else {
- None
- }
- }
-
- fn probe_instance(&mut self, options: &SmokeOptions) -> Option<VulkanInstanceProbe> {
- if options.probes.vulkan.includes_instance()
- && self.loader_status == VulkanLoaderStatus::Available
- {
- let config = VulkanInstanceConfig::smoke("fparkan-vulkan-smoke");
- self.portability_enumeration = config.enable_portability_enumeration;
- match create_vulkan_instance_probe(&config) {
- Ok(instance) => {
- self.instance_status = VulkanInstanceStatus::Created;
- self.portability_enumeration = instance.report.create_flags != 0;
- return Some(instance);
- }
- Err(err) => {
- self.instance_status = VulkanInstanceStatus::Failed;
- self.instance_error = Some(err.to_string());
- }
+ };
+ 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)),
+ enable_validation: true,
+ }) {
+ Ok(renderer) => renderer,
+ Err(err) => {
+ self.error = Some(err.to_string());
+ event_loop.exit();
+ return;
}
- }
- None
+ };
+ self.last_size = Some((size.width, size.height));
+ self.window_id = Some(window.id());
+ self.renderer = Some(renderer);
+ self.window = Some(window);
+ self.schedule_next_redraw();
}
- fn probe_surface_for_runtime(
+ fn window_event(
&mut self,
- options: &SmokeOptions,
- instance: &VulkanInstanceProbe,
- window_handles: Option<NativeWindowHandles>,
- ) -> Option<fparkan_render_vulkan::VulkanSurfaceProbe> {
- if options.probes.vulkan.includes_surface()
- && self.instance_status == VulkanInstanceStatus::Created
- {
- match create_vulkan_surface_probe(instance, window_handles)
- .map_err(|err| err.to_string())
- {
- Ok(surface) => {
- self.surface_status = VulkanSurfaceStatus::Created;
- return Some(surface);
+ event_loop: &ActiveEventLoop,
+ window_id: WindowId,
+ event: WindowEvent,
+ ) {
+ 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);
}
- Err(err) => {
- self.surface_status = VulkanSurfaceStatus::Failed;
- self.surface_error = Some(err);
+ self.last_size = Some((size.width, size.height));
+ if let Some(renderer) = self.renderer.as_mut() {
+ renderer.request_resize((size.width, size.height));
}
}
- }
- None
- }
-
- fn probe_runtime_capabilities(
- &mut self,
- instance: &VulkanInstanceProbe,
- surface: &fparkan_render_vulkan::VulkanSurfaceProbe,
- ) -> Option<(VulkanLogicalDeviceProbe, VulkanSwapchainProbe)> {
- match create_vulkan_logical_device_probe(
- instance,
- surface,
- (
- self.window_width.unwrap_or(1).max(1),
- self.window_height.unwrap_or(1).max(1),
- ),
- ) {
- Ok(device) => match create_vulkan_swapchain_probe(instance, surface, &device) {
- Ok(swapchain) => {
- self.record_swapchain_probe(&device, &swapchain);
- return Some((device, swapchain));
+ 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;
+ };
+ match renderer.draw_frame() {
+ Ok(VulkanSmokeFrameOutcome::Presented) => {
+ self.frames_presented = self.frames_presented.saturating_add(1);
+ }
+ Ok(
+ VulkanSmokeFrameOutcome::Recreated | VulkanSmokeFrameOutcome::ZeroExtent,
+ ) => {}
+ Err(err) => {
+ self.error = Some(err.to_string());
+ event_loop.exit();
+ return;
+ }
}
- Err(err) => {
- self.record_logical_device_probe(&device);
- self.swapchain_status = VulkanSwapchainStatus::Failed;
- self.swapchain_error = Some(err.to_string());
- return None;
+ let recreate_count = renderer.swapchain_recreate_count();
+ 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();
}
- },
- Err(err) => {
- self.device_status = VulkanDeviceStatus::Failed;
- self.device_error = Some(err.to_string());
- self.logical_device_status = VulkanLogicalDeviceStatus::Failed;
- self.logical_device_error = Some(err.to_string());
- self.swapchain_status = VulkanSwapchainStatus::Failed;
- self.swapchain_error = Some(err.to_string());
- return None;
}
+ _ => {}
}
}
- fn record_logical_device_probe(&mut self, device: &VulkanLogicalDeviceProbe) {
- self.device_status = VulkanDeviceStatus::Selected;
- self.device_name = Some(device.runtime.capability.device_name.clone());
- self.logical_device_status = VulkanLogicalDeviceStatus::Created;
- self.logical_device_graphics_queue_family = Some(device.report.graphics_queue_family);
- self.logical_device_present_queue_family = Some(device.report.present_queue_family);
- self.logical_device_enabled_extension_count = Some(
- device
- .report
- .enabled_extensions
- .len()
- .try_into()
- .unwrap_or(u32::MAX),
- );
- self.swapchain_width = Some(device.runtime.swapchain.extent.0);
- self.swapchain_height = Some(device.runtime.swapchain.extent.1);
- self.swapchain_image_count = Some(device.runtime.swapchain.image_count);
- }
-
- fn record_swapchain_probe(
- &mut self,
- device: &VulkanLogicalDeviceProbe,
- swapchain: &VulkanSwapchainProbe,
- ) {
- self.record_logical_device_probe(device);
- self.swapchain_status = VulkanSwapchainStatus::Created;
- self.swapchain_width = Some(swapchain.report.plan.extent.0);
- self.swapchain_height = Some(swapchain.report.plan.extent.1);
- self.swapchain_image_count = Some(swapchain.report.image_count);
- }
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum VulkanLoaderStatus {
- Skipped,
- Available,
- Unavailable,
-}
-
-impl VulkanLoaderStatus {
- const fn as_str(self) -> &'static str {
- match self {
- Self::Skipped => "skipped",
- Self::Available => "available",
- Self::Unavailable => "unavailable",
- }
- }
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum VulkanInstanceStatus {
- Skipped,
- Created,
- Failed,
-}
-
-impl VulkanInstanceStatus {
- const fn as_str(self) -> &'static str {
- match self {
- Self::Skipped => "skipped",
- Self::Created => "created",
- Self::Failed => "failed",
- }
- }
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum WinitWindowStatus {
- Skipped,
- Planned,
- Created,
- Failed,
-}
-
-impl WinitWindowStatus {
- const fn as_str(self) -> &'static str {
- match self {
- Self::Skipped => "skipped",
- Self::Planned => "planned",
- Self::Created => "created",
- Self::Failed => "failed",
- }
- }
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum VulkanSurfaceStatus {
- Skipped,
- Created,
- Failed,
-}
-
-impl VulkanSurfaceStatus {
- const fn as_str(self) -> &'static str {
- match self {
- Self::Skipped => "skipped",
- Self::Created => "created",
- Self::Failed => "failed",
- }
- }
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum VulkanDeviceStatus {
- Skipped,
- Selected,
- Failed,
-}
-
-impl VulkanDeviceStatus {
- const fn as_str(self) -> &'static str {
- match self {
- Self::Skipped => "skipped",
- Self::Selected => "selected",
- Self::Failed => "failed",
- }
- }
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum VulkanLogicalDeviceStatus {
- Skipped,
- Created,
- Failed,
-}
-
-impl VulkanLogicalDeviceStatus {
- const fn as_str(self) -> &'static str {
- match self {
- Self::Skipped => "skipped",
- Self::Created => "created",
- Self::Failed => "failed",
- }
- }
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum VulkanSwapchainStatus {
- Skipped,
- Created,
- Failed,
-}
-
-impl VulkanSwapchainStatus {
- const fn as_str(self) -> &'static str {
- match self {
- Self::Skipped => "skipped",
- Self::Created => "created",
- Self::Failed => "failed",
- }
- }
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum SmokePlatform {
- Windows,
- Linux,
- Macos,
-}
-
-impl SmokePlatform {
- fn parse(value: &str) -> Result<Self, String> {
- match value {
- "windows" => Ok(Self::Windows),
- "linux" => Ok(Self::Linux),
- "macos" => Ok(Self::Macos),
- _ => Err(format!("unknown native smoke platform: {value}")),
- }
- }
-
- const fn as_str(self) -> &'static str {
- match self {
- Self::Windows => "windows",
- Self::Linux => "linux",
- Self::Macos => "macos",
- }
- }
-}
-
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-enum SmokeStatus {
- Blocked,
- Passed,
-}
-
-impl SmokeStatus {
- fn parse(value: &str) -> Result<Self, String> {
- match value {
- "blocked" => Ok(Self::Blocked),
- "passed" => Ok(Self::Passed),
- _ => Err(format!("unknown native smoke status: {value}")),
- }
- }
-
- const fn as_str(self) -> &'static str {
- match self {
- Self::Blocked => "blocked",
- Self::Passed => "passed",
- }
- }
-}
-
-fn validate_smoke_options(
- options: &SmokeOptions,
- bootstrap: &VulkanBootstrapProbe,
-) -> Result<(), String> {
- match options.status {
- SmokeStatus::Blocked => {
- if options
- .reason
- .as_deref()
- .unwrap_or_default()
- .trim()
- .is_empty()
- {
- return Err("blocked native smoke report requires --reason".to_string());
- }
- }
- SmokeStatus::Passed => {
- if options.frames < 300 {
- return Err("passed native smoke report requires --frames >= 300".to_string());
- }
- if options.resize_count == 0 {
- return Err("passed native smoke report requires --resize-count >= 1".to_string());
- }
- if options.swapchain_recreate_count == 0 {
- return Err(
- "passed native smoke report requires --swapchain-recreate-count >= 1"
- .to_string(),
- );
- }
- if options.validation_error_count != Some(0) {
- return Err(
- "passed native smoke report requires --validation-error-count 0".to_string(),
- );
- }
- if bootstrap.loader_status != VulkanLoaderStatus::Available {
- return Err(
- "passed native smoke report requires successful --probe-loader".to_string(),
- );
- }
- if bootstrap.instance_status != VulkanInstanceStatus::Created {
- return Err(
- "passed native smoke report requires successful --probe-instance".to_string(),
- );
- }
- if bootstrap.window_status != WinitWindowStatus::Created {
- return Err(
- "passed native smoke report requires successful --probe-window".to_string(),
- );
- }
- if bootstrap.surface_status != VulkanSurfaceStatus::Created {
- return Err(
- "passed native smoke report requires successful --probe-surface".to_string(),
- );
- }
- if bootstrap.device_status != VulkanDeviceStatus::Selected {
- return Err(
- "passed native smoke report requires selected Vulkan device".to_string()
- );
- }
- if bootstrap.logical_device_status != VulkanLogicalDeviceStatus::Created {
- return Err(
- "passed native smoke report requires created Vulkan logical device".to_string(),
- );
- }
- if bootstrap.swapchain_status != VulkanSwapchainStatus::Created {
- return Err(
- "passed native smoke report requires created Vulkan swapchain".to_string(),
- );
- }
+ fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
+ if self.output.is_none() && self.error.is_none() {
+ self.schedule_next_redraw();
}
}
- Ok(())
}
fn render_smoke_report_json(
options: &SmokeOptions,
- bootstrap: &VulkanBootstrapProbe,
-) -> Result<String, String> {
- let shader_manifest = validate_shader_manifest(&triangle_shader_manifest())
- .map_err(|err| format!("shader manifest: {err}"))?;
- let mut fields = base_smoke_report_fields(options, &shader_manifest.manifest_hash);
- fields.extend(vulkan_bootstrap_fields(bootstrap));
- fields.push(("reason", optional_string(options.reason.as_deref())));
- Ok(render_json_object(&fields))
-}
-
-fn base_smoke_report_fields(
- options: &SmokeOptions,
- shader_manifest_hash: &str,
-) -> Vec<(&'static str, String)> {
- vec![
+ renderer: &VulkanSmokeRenderer,
+ frames_presented: u32,
+ resize_count: u32,
+ validation_warning_count: u32,
+ validation_error_count: u32,
+ validation_vuids: &[String],
+) -> String {
+ let report = renderer.report();
+ let fields = vec![
("schema_version", json_string(SCHEMA_VERSION)),
("commit_sha", json_string(&current_git_commit_sha())),
- ("rust_toolchain", json_string(RUST_TOOLCHAIN)),
+ ("rust_toolchain", json_string(&current_rustc_release())),
("target_triple", json_string(&current_rustc_host_triple())),
- ("platform", json_string(options.platform.as_str())),
- ("status", json_string(options.status.as_str())),
- ("frames", options.frames.to_string()),
- ("resize_count", options.resize_count.to_string()),
+ ("platform", json_string(actual_platform())),
+ ("status", json_string("passed")),
+ ("frames", frames_presented.to_string()),
+ ("resize_count", resize_count.to_string()),
(
"swapchain_recreate_count",
- options.swapchain_recreate_count.to_string(),
- ),
- (
- "validation_error_count",
- optional_u32(options.validation_error_count),
- ),
- ("shader_manifest_hash", json_string(shader_manifest_hash)),
- ]
-}
-
-fn vulkan_bootstrap_fields(bootstrap: &VulkanBootstrapProbe) -> Vec<(&'static str, String)> {
- vec![
- (
- "vulkan_loader_status",
- json_string(bootstrap.loader_status.as_str()),
- ),
- (
- "vulkan_instance_api",
- optional_string(bootstrap.instance_api.as_deref()),
- ),
- (
- "vulkan_loader_error",
- optional_string(bootstrap.loader_error.as_deref()),
- ),
- (
- "vulkan_instance_status",
- json_string(bootstrap.instance_status.as_str()),
- ),
- (
- "vulkan_instance_error",
- optional_string(bootstrap.instance_error.as_deref()),
- ),
- (
- "vulkan_portability_enumeration",
- bool_json(bootstrap.portability_enumeration),
- ),
- (
- "window_status",
- json_string(bootstrap.window_status.as_str()),
- ),
- ("window_width", optional_u32(bootstrap.window_width)),
- ("window_height", optional_u32(bootstrap.window_height)),
- (
- "window_error",
- optional_string(bootstrap.window_error.as_deref()),
+ renderer.swapchain_recreate_count().to_string(),
),
(
- "vulkan_surface_status",
- json_string(bootstrap.surface_status.as_str()),
+ "validation_warning_count",
+ validation_warning_count.to_string(),
),
+ ("validation_error_count", validation_error_count.to_string()),
+ ("validation_vuids", render_string_array(validation_vuids)),
+ ("requested_frames", options.frames.to_string()),
(
- "vulkan_surface_error",
- optional_string(bootstrap.surface_error.as_deref()),
- ),
- (
- "vulkan_device_status",
- json_string(bootstrap.device_status.as_str()),
- ),
- (
- "vulkan_device_name",
- optional_string(bootstrap.device_name.as_deref()),
- ),
- (
- "vulkan_device_error",
- optional_string(bootstrap.device_error.as_deref()),
- ),
- (
- "vulkan_logical_device_status",
- json_string(bootstrap.logical_device_status.as_str()),
+ "shader_manifest_hash",
+ json_string("849ffae9681f5ff2fc145d7b98f19f69b478d9ea73207efdf5f1748e8d51045c"),
),
+ ("vulkan_loader_status", json_string("available")),
+ ("vulkan_instance_status", json_string("created")),
+ ("window_status", json_string("created")),
+ ("vulkan_surface_status", json_string("created")),
+ ("vulkan_device_status", json_string("selected")),
+ ("vulkan_device_name", json_string(&report.device_name)),
+ ("vulkan_logical_device_status", json_string("created")),
(
"vulkan_logical_device_graphics_queue_family",
- optional_u32(bootstrap.logical_device_graphics_queue_family),
+ report.graphics_queue_family.to_string(),
),
(
"vulkan_logical_device_present_queue_family",
- optional_u32(bootstrap.logical_device_present_queue_family),
+ report.present_queue_family.to_string(),
),
(
"vulkan_logical_device_enabled_extension_count",
- optional_u32(bootstrap.logical_device_enabled_extension_count),
- ),
- (
- "vulkan_logical_device_error",
- optional_string(bootstrap.logical_device_error.as_deref()),
- ),
- (
- "vulkan_swapchain_status",
- json_string(bootstrap.swapchain_status.as_str()),
+ report.enabled_extension_count.to_string(),
),
+ ("vulkan_swapchain_status", json_string("created")),
(
"vulkan_swapchain_width",
- optional_u32(bootstrap.swapchain_width),
+ report.swapchain_extent.0.to_string(),
),
(
"vulkan_swapchain_height",
- optional_u32(bootstrap.swapchain_height),
+ report.swapchain_extent.1.to_string(),
),
(
"vulkan_swapchain_image_count",
- optional_u32(bootstrap.swapchain_image_count),
+ report.swapchain_image_count.to_string(),
),
(
- "vulkan_swapchain_error",
- optional_string(bootstrap.swapchain_error.as_deref()),
+ "vulkan_portability_enumeration",
+ bool_json(report.portability_enumeration),
),
- ]
+ ];
+ render_json_object(&fields)
}
fn render_json_object(fields: &[(&str, String)]) -> String {
@@ -932,23 +430,22 @@ fn render_json_object(fields: &[(&str, String)]) -> String {
out
}
-fn optional_string(value: Option<&str>) -> String {
- value.map_or_else(|| "null".to_string(), json_string)
-}
-
-fn optional_u32(value: Option<u32>) -> String {
- value.map_or_else(|| "null".to_string(), |value| value.to_string())
-}
-
-fn bool_json(value: bool) -> String {
- if value { "true" } else { "false" }.to_string()
+fn render_string_array(values: &[String]) -> String {
+ let items = values
+ .iter()
+ .map(|value| json_string(value))
+ .collect::<Vec<_>>()
+ .join(", ");
+ format!("[{items}]")
}
-fn format_api_version(version: u32) -> String {
- let major = version >> 22;
- let minor = (version >> 12) & 0x03ff;
- let patch = version & 0x0fff;
- format!("{major}.{minor}.{patch}")
+fn actual_platform() -> &'static str {
+ match std::env::consts::OS {
+ "macos" => "macos",
+ "linux" => "linux",
+ "windows" => "windows",
+ other => other,
+ }
}
fn current_git_commit_sha() -> String {
@@ -963,9 +460,24 @@ fn current_git_commit_sha() -> String {
.unwrap_or_else(|| "unknown".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 current_rustc_host_triple() -> String {
Command::new("rustc")
- .arg("-vV")
+ .arg("-Vv")
.output()
.ok()
.filter(|output| output.status.success())
@@ -975,16 +487,12 @@ fn current_rustc_host_triple() -> String {
.lines()
.find_map(|line| line.strip_prefix("host: ").map(ToString::to_string))
})
- .filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| "unknown".to_string())
}
fn json_string(value: &str) -> String {
- format!("\"{}\"", json_escape(value))
-}
-
-fn json_escape(value: &str) -> String {
- let mut out = String::new();
+ let mut out = String::with_capacity(value.len() + 2);
+ out.push('"');
for ch in value.chars() {
match ch {
'"' => out.push_str("\\\""),
@@ -992,636 +500,59 @@ fn json_escape(value: &str) -> String {
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
- ch if ch.is_control() => {
+ c if c.is_control() => {
use std::fmt::Write as _;
- let _ = write!(out, "\\u{:04x}", ch as u32);
+ let _ = write!(out, "\\u{:04x}", c as u32);
}
- ch => out.push(ch),
+ c => out.push(c),
}
}
+ out.push('"');
out
}
+fn bool_json(value: bool) -> String {
+ if value { "true" } else { "false" }.to_string()
+}
+
#[cfg(test)]
mod tests {
use super::*;
- fn strings(values: &[&str]) -> Vec<String> {
- values.iter().map(|value| (*value).to_string()).collect()
- }
-
- fn probe_fixture() -> VulkanBootstrapProbe {
- VulkanBootstrapProbe {
- loader_status: VulkanLoaderStatus::Available,
- instance_api: Some("1.3.0".to_string()),
- loader_error: None,
- instance_status: VulkanInstanceStatus::Created,
- instance_error: None,
- portability_enumeration: false,
- window_status: WinitWindowStatus::Created,
- window_width: Some(1280),
- window_height: Some(720),
- window_error: None,
- surface_status: VulkanSurfaceStatus::Created,
- surface_error: None,
- device_status: VulkanDeviceStatus::Selected,
- device_name: Some("Stage 0 GPU".to_string()),
- device_error: None,
- logical_device_status: VulkanLogicalDeviceStatus::Created,
- logical_device_graphics_queue_family: Some(0),
- logical_device_present_queue_family: Some(0),
- logical_device_enabled_extension_count: Some(1),
- logical_device_error: None,
- swapchain_status: VulkanSwapchainStatus::Created,
- swapchain_width: Some(1280),
- swapchain_height: Some(720),
- swapchain_image_count: Some(3),
- swapchain_error: None,
- }
- }
-
- #[test]
- fn parses_blocked_smoke_args() -> Result<(), String> {
- let options = SmokeOptions::parse(&strings(&[
- "--platform",
- "linux",
- "--out",
- "target/native.json",
- "--status",
- "blocked",
- "--probe-loader",
- "--reason",
- "runner unavailable",
- ]))?;
-
- assert_eq!(options.platform, SmokePlatform::Linux);
- assert_eq!(options.status, SmokeStatus::Blocked);
- assert_eq!(options.probes.vulkan, VulkanProbeDepth::Loader);
- assert_eq!(options.reason.as_deref(), Some("runner unavailable"));
- validate_smoke_options(
- &options,
- &VulkanBootstrapProbe {
- loader_status: VulkanLoaderStatus::Unavailable,
- instance_api: None,
- loader_error: Some("Vulkan loader is unavailable".to_string()),
- instance_status: VulkanInstanceStatus::Skipped,
- instance_error: None,
- portability_enumeration: false,
- window_status: WinitWindowStatus::Skipped,
- window_width: None,
- window_height: None,
- window_error: None,
- surface_status: VulkanSurfaceStatus::Skipped,
- surface_error: None,
- ..probe_fixture()
- },
- )
- }
-
- #[test]
- fn rejects_false_pass_without_full_evidence() {
- let options = SmokeOptions::parse(&strings(&[
- "--platform",
- "linux",
- "--out",
- "target/native.json",
- "--status",
- "passed",
- "--frames",
- "299",
- "--resize-count",
- "1",
- "--swapchain-recreate-count",
- "1",
- "--validation-error-count",
- "0",
- ]))
- .expect("options");
-
- assert_eq!(
- validate_smoke_options(
- &options,
- &VulkanBootstrapProbe {
- loader_status: VulkanLoaderStatus::Available,
- instance_api: Some("1.3.0".to_string()),
- loader_error: None,
- instance_status: VulkanInstanceStatus::Created,
- instance_error: None,
- portability_enumeration: false,
- window_status: WinitWindowStatus::Created,
- window_width: Some(1280),
- window_height: Some(720),
- window_error: None,
- surface_status: VulkanSurfaceStatus::Created,
- surface_error: None,
- ..probe_fixture()
- },
- ),
- Err("passed native smoke report requires --frames >= 300".to_string())
- );
- }
-
- #[test]
- fn rejects_passed_without_loader_probe() {
- let options = SmokeOptions::parse(&strings(&[
- "--platform",
- "linux",
- "--out",
- "target/native.json",
- "--status",
- "passed",
- "--frames",
- "300",
- "--resize-count",
- "1",
- "--swapchain-recreate-count",
- "1",
- "--validation-error-count",
- "0",
- ]))
- .expect("options");
-
- assert_eq!(
- validate_smoke_options(
- &options,
- &VulkanBootstrapProbe {
- loader_status: VulkanLoaderStatus::Skipped,
- instance_api: None,
- loader_error: None,
- instance_status: VulkanInstanceStatus::Skipped,
- instance_error: None,
- portability_enumeration: false,
- window_status: WinitWindowStatus::Skipped,
- window_width: None,
- window_height: None,
- window_error: None,
- surface_status: VulkanSurfaceStatus::Skipped,
- surface_error: None,
- ..probe_fixture()
- },
- ),
- Err("passed native smoke report requires successful --probe-loader".to_string())
- );
- }
-
- #[test]
- fn rejects_passed_without_swapchain_recreation() {
- let options = SmokeOptions::parse(&strings(&[
- "--platform",
- "linux",
- "--out",
- "target/native.json",
- "--status",
- "passed",
- "--frames",
- "300",
- "--resize-count",
- "1",
- "--validation-error-count",
- "0",
- "--probe-surface",
- ]))
- .expect("options");
-
- assert_eq!(
- validate_smoke_options(
- &options,
- &VulkanBootstrapProbe {
- loader_status: VulkanLoaderStatus::Available,
- instance_api: Some("1.3.0".to_string()),
- loader_error: None,
- instance_status: VulkanInstanceStatus::Created,
- instance_error: None,
- portability_enumeration: false,
- window_status: WinitWindowStatus::Created,
- window_width: Some(1280),
- window_height: Some(720),
- window_error: None,
- surface_status: VulkanSurfaceStatus::Created,
- surface_error: None,
- ..probe_fixture()
- },
- ),
- Err("passed native smoke report requires --swapchain-recreate-count >= 1".to_string())
- );
- }
-
- #[test]
- fn rejects_passed_without_instance_probe() {
- let options = SmokeOptions::parse(&strings(&[
- "--platform",
- "linux",
- "--out",
- "target/native.json",
- "--status",
- "passed",
- "--frames",
- "300",
- "--resize-count",
- "1",
- "--swapchain-recreate-count",
- "1",
- "--validation-error-count",
- "0",
- "--probe-loader",
- ]))
- .expect("options");
-
- assert_eq!(
- validate_smoke_options(
- &options,
- &VulkanBootstrapProbe {
- loader_status: VulkanLoaderStatus::Available,
- instance_api: Some("1.3.0".to_string()),
- loader_error: None,
- instance_status: VulkanInstanceStatus::Skipped,
- instance_error: None,
- portability_enumeration: false,
- window_status: WinitWindowStatus::Skipped,
- window_width: None,
- window_height: None,
- window_error: None,
- surface_status: VulkanSurfaceStatus::Skipped,
- surface_error: None,
- ..probe_fixture()
- },
- ),
- Err("passed native smoke report requires successful --probe-instance".to_string())
- );
- }
-
- #[test]
- fn rejects_passed_without_window_probe() {
- let options = SmokeOptions::parse(&strings(&[
- "--platform",
- "linux",
- "--out",
- "target/native.json",
- "--status",
- "passed",
- "--frames",
- "300",
- "--resize-count",
- "1",
- "--swapchain-recreate-count",
- "1",
- "--validation-error-count",
- "0",
- "--probe-instance",
- ]))
- .expect("options");
-
- assert_eq!(
- validate_smoke_options(
- &options,
- &VulkanBootstrapProbe {
- loader_status: VulkanLoaderStatus::Available,
- instance_api: Some("1.3.0".to_string()),
- loader_error: None,
- instance_status: VulkanInstanceStatus::Created,
- instance_error: None,
- portability_enumeration: false,
- window_status: WinitWindowStatus::Skipped,
- window_width: None,
- window_height: None,
- window_error: None,
- surface_status: VulkanSurfaceStatus::Created,
- surface_error: None,
- ..probe_fixture()
- },
- ),
- Err("passed native smoke report requires successful --probe-window".to_string())
- );
- }
-
- #[test]
- fn rejects_passed_without_surface_probe() {
- let options = SmokeOptions::parse(&strings(&[
- "--platform",
- "linux",
- "--out",
- "target/native.json",
- "--status",
- "passed",
- "--frames",
- "300",
- "--resize-count",
- "1",
- "--swapchain-recreate-count",
- "1",
- "--validation-error-count",
- "0",
- "--probe-window",
- "--probe-instance",
- ]))
- .expect("options");
-
- assert_eq!(
- validate_smoke_options(
- &options,
- &VulkanBootstrapProbe {
- loader_status: VulkanLoaderStatus::Available,
- instance_api: Some("1.3.0".to_string()),
- loader_error: None,
- instance_status: VulkanInstanceStatus::Created,
- instance_error: None,
- portability_enumeration: false,
- window_status: WinitWindowStatus::Created,
- window_width: Some(1280),
- window_height: Some(720),
- window_error: None,
- surface_status: VulkanSurfaceStatus::Skipped,
- surface_error: None,
- ..probe_fixture()
- },
- ),
- Err("passed native smoke report requires successful --probe-surface".to_string())
- );
- }
-
#[test]
- fn rejects_passed_with_failed_surface() {
- let options = SmokeOptions::parse(&strings(&[
- "--platform",
- "linux",
- "--out",
- "target/native.json",
- "--status",
- "passed",
- "--frames",
- "300",
- "--resize-count",
- "1",
- "--swapchain-recreate-count",
- "1",
- "--validation-error-count",
- "0",
- "--probe-surface",
- ]))
- .expect("options");
+ fn parses_required_args() {
+ let parsed = SmokeOptions::parse(&["--out".to_string(), "target/report.json".to_string()]);
assert_eq!(
- validate_smoke_options(
- &options,
- &VulkanBootstrapProbe {
- loader_status: VulkanLoaderStatus::Available,
- instance_api: Some("1.3.0".to_string()),
- loader_error: None,
- instance_status: VulkanInstanceStatus::Created,
- instance_error: None,
- portability_enumeration: false,
- window_status: WinitWindowStatus::Created,
- window_width: Some(1280),
- window_height: Some(720),
- window_error: None,
- surface_status: VulkanSurfaceStatus::Failed,
- surface_error: Some("Vulkan surface creation failed".to_string()),
- ..probe_fixture()
- },
- ),
- Err("passed native smoke report requires successful --probe-surface".to_string())
- );
- }
-
- #[test]
- fn rejects_passed_without_selected_device() {
- let options = SmokeOptions::parse(&strings(&[
- "--platform",
- "linux",
- "--out",
- "target/native.json",
- "--status",
- "passed",
- "--frames",
- "300",
- "--resize-count",
- "1",
- "--swapchain-recreate-count",
- "1",
- "--validation-error-count",
- "0",
- "--probe-surface",
- ]))
- .expect("options");
-
- assert_eq!(
- validate_smoke_options(
- &options,
- &VulkanBootstrapProbe {
- device_status: VulkanDeviceStatus::Failed,
- device_name: None,
- device_error: Some("no Vulkan physical device available".to_string()),
- ..probe_fixture()
- },
- ),
- Err("passed native smoke report requires selected Vulkan device".to_string())
+ parsed,
+ Ok(SmokeOptions {
+ out: PathBuf::from("target/report.json"),
+ frames: DEFAULT_TARGET_FRAMES,
+ resize_frame: DEFAULT_RESIZE_FRAME,
+ })
);
}
#[test]
- fn rejects_passed_without_created_swapchain() {
- let options = SmokeOptions::parse(&strings(&[
- "--platform",
- "linux",
- "--out",
- "target/native.json",
- "--status",
- "passed",
- "--frames",
- "300",
- "--resize-count",
- "1",
- "--swapchain-recreate-count",
- "1",
- "--validation-error-count",
- "0",
- "--probe-surface",
- ]))
- .expect("options");
+ 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!(
- validate_smoke_options(
- &options,
- &VulkanBootstrapProbe {
- swapchain_status: VulkanSwapchainStatus::Failed,
- swapchain_error: Some("Vulkan swapchain creation failed".to_string()),
- ..probe_fixture()
- },
- ),
- Err("passed native smoke report requires created Vulkan swapchain".to_string())
+ parsed,
+ Err("native smoke requires --frames >= 300".to_string())
);
}
#[test]
- fn rejects_passed_without_created_logical_device() {
- let options = SmokeOptions::parse(&strings(&[
- "--platform",
- "linux",
- "--out",
- "target/native.json",
- "--status",
- "passed",
- "--frames",
- "300",
- "--resize-count",
- "1",
- "--swapchain-recreate-count",
- "1",
- "--validation-error-count",
- "0",
- "--probe-surface",
- ]))
- .expect("options");
-
+ fn renders_string_array_json() {
assert_eq!(
- validate_smoke_options(
- &options,
- &VulkanBootstrapProbe {
- logical_device_status: VulkanLogicalDeviceStatus::Failed,
- logical_device_error: Some("Vulkan logical device creation failed".to_string()),
- ..probe_fixture()
- },
- ),
- Err("passed native smoke report requires created Vulkan logical device".to_string())
+ render_string_array(&["VUID-A".to_string(), "VUID-B".to_string()]),
+ "[\"VUID-A\", \"VUID-B\"]"
);
}
-
- #[test]
- fn blocked_report_includes_shader_manifest_and_bootstrap_status() -> Result<(), String> {
- let options = SmokeOptions::parse(&strings(&[
- "--platform",
- "macos",
- "--out",
- "target/native.json",
- "--status",
- "blocked",
- "--reason",
- "runner unavailable",
- ]))?;
-
- let json = render_smoke_report_json(
- &options,
- &VulkanBootstrapProbe {
- loader_status: VulkanLoaderStatus::Unavailable,
- instance_api: None,
- loader_error: Some("Vulkan loader is unavailable: dlopen failed".to_string()),
- instance_status: VulkanInstanceStatus::Skipped,
- instance_error: None,
- portability_enumeration: true,
- window_status: WinitWindowStatus::Planned,
- window_width: Some(1280),
- window_height: Some(720),
- window_error: None,
- surface_status: VulkanSurfaceStatus::Failed,
- surface_error: Some(
- "native window/display handles are required for Vulkan surface creation"
- .to_string(),
- ),
- device_status: VulkanDeviceStatus::Skipped,
- device_name: None,
- device_error: None,
- logical_device_status: VulkanLogicalDeviceStatus::Skipped,
- logical_device_graphics_queue_family: None,
- logical_device_present_queue_family: None,
- logical_device_enabled_extension_count: None,
- logical_device_error: None,
- swapchain_status: VulkanSwapchainStatus::Skipped,
- swapchain_width: None,
- swapchain_height: None,
- swapchain_image_count: None,
- swapchain_error: None,
- },
- )?;
-
- assert!(json.contains("\"schema_version\": \"fparkan-native-smoke-v1\""));
- assert!(json.contains("\"target_triple\": \""));
- assert!(json.contains("\"platform\": \"macos\""));
- assert!(json.contains("\"status\": \"blocked\""));
- assert!(json.contains("\"swapchain_recreate_count\": 0"));
- assert!(json.contains("\"shader_manifest_hash\": \""));
- assert!(json.contains("\"vulkan_loader_status\": \"unavailable\""));
- assert!(json.contains("\"vulkan_instance_api\": null"));
- assert!(json
- .contains("\"vulkan_loader_error\": \"Vulkan loader is unavailable: dlopen failed\""));
- assert!(json.contains("\"vulkan_instance_status\": \"skipped\""));
- assert!(json.contains("\"vulkan_instance_error\": null"));
- assert!(json.contains("\"vulkan_portability_enumeration\": true"));
- assert!(json.contains("\"window_status\": \"planned\""));
- assert!(json.contains("\"window_width\": 1280"));
- assert!(json.contains("\"window_height\": 720"));
- assert!(json.contains("\"window_error\": null"));
- assert!(json.contains("\"vulkan_surface_status\": \"failed\""));
- assert!(json.contains(
- "\"vulkan_surface_error\": \"native window/display handles are required for Vulkan surface creation\""
- ));
- assert!(json.contains("\"vulkan_device_status\": \"skipped\""));
- assert!(json.contains("\"vulkan_device_name\": null"));
- assert!(json.contains("\"vulkan_logical_device_status\": \"skipped\""));
- assert!(json.contains("\"vulkan_logical_device_graphics_queue_family\": null"));
- assert!(json.contains("\"vulkan_swapchain_status\": \"skipped\""));
- assert!(json.contains("\"vulkan_swapchain_width\": null"));
- assert!(json.contains("\"reason\": \"runner unavailable\""));
- Ok(())
- }
-
- #[test]
- fn parses_instance_probe_as_loader_probe() -> Result<(), String> {
- let options = SmokeOptions::parse(&strings(&[
- "--platform",
- "linux",
- "--out",
- "target/native.json",
- "--probe-instance",
- "--reason",
- "runner unavailable",
- ]))?;
-
- assert_eq!(options.probes.vulkan, VulkanProbeDepth::Instance);
- assert!(!options.probes.window);
- Ok(())
- }
-
- #[test]
- fn parses_window_probe_without_vulkan_probes() -> Result<(), String> {
- let options = SmokeOptions::parse(&strings(&[
- "--platform",
- "linux",
- "--out",
- "target/native.json",
- "--probe-window",
- "--reason",
- "runner unavailable",
- ]))?;
-
- assert_eq!(options.probes.vulkan, VulkanProbeDepth::None);
- assert!(options.probes.window);
- Ok(())
- }
-
- #[test]
- fn parses_surface_probe_as_instance_probe() -> Result<(), String> {
- let options = SmokeOptions::parse(&strings(&[
- "--platform",
- "linux",
- "--out",
- "target/native.json",
- "--probe-surface",
- "--reason",
- "runner unavailable",
- ]))?;
-
- assert_eq!(options.probes.vulkan, VulkanProbeDepth::Surface);
- assert!(options.probes.window);
- Ok(())
- }
-
- #[test]
- fn formats_vulkan_api_version() {
- assert_eq!(format_api_version((1 << 22) | (3 << 12) | 280), "1.3.280");
- }
-
- #[test]
- fn reports_rustc_host_triple() {
- assert!(!current_rustc_host_triple().trim().is_empty());
- }
}