aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-23 22:42:20 +0300
committerValentin Popov <valentin@popov.link>2026-06-23 22:42:20 +0300
commitc71e706d6969f516152142bbeebf5f836d38db9b (patch)
tree0ba128f72c476959205666fc3ab9c8a4e263ec10
parentaa2133d82b2a9d92fdbdce2b60eec103536fe484 (diff)
downloadfparkan-c71e706d6969f516152142bbeebf5f836d38db9b.tar.xz
fparkan-c71e706d6969f516152142bbeebf5f836d38db9b.zip
feat: add native smoke window preflight
-rw-r--r--Cargo.lock1
-rw-r--r--adapters/fparkan-platform-winit/src/lib.rs67
-rw-r--r--apps/fparkan-vulkan-smoke/Cargo.toml1
-rw-r--r--apps/fparkan-vulkan-smoke/src/main.rs233
-rw-r--r--fixtures/acceptance/coverage.tsv3
-rw-r--r--fixtures/acceptance/stage_0_2_roadmap.md1
6 files changed, 280 insertions, 26 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2c2a00e..028a63d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -767,6 +767,7 @@ dependencies = [
name = "fparkan-vulkan-smoke"
version = "0.1.0"
dependencies = [
+ "fparkan-platform-winit",
"fparkan-render-vulkan",
]
diff --git a/adapters/fparkan-platform-winit/src/lib.rs b/adapters/fparkan-platform-winit/src/lib.rs
index 9a9941c..d8efa3e 100644
--- a/adapters/fparkan-platform-winit/src/lib.rs
+++ b/adapters/fparkan-platform-winit/src/lib.rs
@@ -33,6 +33,8 @@ use winit::platform::scancode::PhysicalKeyExtScancode;
use winit::window::Window;
static NEXT_WINDOW_HANDLE_ID: AtomicU64 = AtomicU64::new(1);
+const DEFAULT_SMOKE_WIDTH: u32 = 1280;
+const DEFAULT_SMOKE_HEIGHT: u32 = 720;
fn next_window_id() -> u64 {
NEXT_WINDOW_HANDLE_ID.fetch_add(1, Ordering::Relaxed)
@@ -144,6 +146,44 @@ impl EventSource for WinitEventSource {
}
}
+/// Window creation plan for native smoke entrypoints.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct WinitWindowPlan {
+ /// Requested drawable width in physical pixels.
+ pub width: u32,
+ /// Requested drawable height in physical pixels.
+ pub height: u32,
+ /// Whether native window/display handles are required by the caller.
+ pub requires_native_handles: bool,
+}
+
+impl WinitWindowPlan {
+ /// Returns the Stage 0 native smoke window plan.
+ #[must_use]
+ pub const fn smoke() -> Self {
+ Self {
+ width: DEFAULT_SMOKE_WIDTH,
+ height: DEFAULT_SMOKE_HEIGHT,
+ requires_native_handles: true,
+ }
+ }
+
+ /// Validates the window plan before a native event loop is entered.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`PlatformError`] when the drawable extent is zero.
+ pub fn validate(self) -> Result<Self, PlatformError> {
+ if self.width == 0 || self.height == 0 {
+ return Err(PlatformError::Backend {
+ context: "winit window plan",
+ message: "drawable extent must be non-zero".to_string(),
+ });
+ }
+ Ok(self)
+ }
+}
+
/// Minimal window view over a `winit` window.
#[derive(Clone, Debug)]
pub struct WinitWindow {
@@ -277,6 +317,33 @@ mod tests {
}
#[test]
+ fn smoke_window_plan_requires_native_handles_and_nonzero_extent() -> Result<(), PlatformError> {
+ let plan = WinitWindowPlan::smoke().validate()?;
+
+ assert_eq!(plan.width, DEFAULT_SMOKE_WIDTH);
+ assert_eq!(plan.height, DEFAULT_SMOKE_HEIGHT);
+ assert!(plan.requires_native_handles);
+ Ok(())
+ }
+
+ #[test]
+ fn smoke_window_plan_rejects_zero_extent() {
+ let plan = WinitWindowPlan {
+ width: 0,
+ height: DEFAULT_SMOKE_HEIGHT,
+ requires_native_handles: true,
+ };
+
+ assert!(matches!(
+ plan.validate(),
+ Err(PlatformError::Backend {
+ context: "winit window plan",
+ ..
+ })
+ ));
+ }
+
+ #[test]
fn window_events_push_expected_platform_events() {
let mut source = WinitEventSource::new();
let size = winit::dpi::PhysicalSize::new(1024u32, 768u32);
diff --git a/apps/fparkan-vulkan-smoke/Cargo.toml b/apps/fparkan-vulkan-smoke/Cargo.toml
index 793a4c4..f872920 100644
--- a/apps/fparkan-vulkan-smoke/Cargo.toml
+++ b/apps/fparkan-vulkan-smoke/Cargo.toml
@@ -6,6 +6,7 @@ license.workspace = true
repository.workspace = true
[dependencies]
+fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit" }
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" }
[lints]
diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs
index e6fc800..a05a510 100644
--- a/apps/fparkan-vulkan-smoke/src/main.rs
+++ b/apps/fparkan-vulkan-smoke/src/main.rs
@@ -11,6 +11,7 @@
#![allow(clippy::print_stderr, clippy::print_stdout)]
//! Native Vulkan smoke runner entrypoint.
+use fparkan_platform_winit::WinitWindowPlan;
use fparkan_render_vulkan::{
create_vulkan_instance_probe, plan_vulkan_surface, probe_vulkan_loader,
triangle_shader_manifest, validate_shader_manifest, VulkanInstanceConfig,
@@ -57,9 +58,7 @@ struct SmokeOptions {
frames: u32,
resize_count: u32,
validation_error_count: Option<u32>,
- probe_loader: bool,
- probe_instance: bool,
- probe_surface: bool,
+ probes: ProbeOptions,
reason: Option<String>,
}
@@ -71,9 +70,7 @@ impl SmokeOptions {
let mut frames = 0;
let mut resize_count = 0;
let mut validation_error_count = None;
- let mut probe_loader = false;
- let mut probe_instance = false;
- let mut probe_surface = false;
+ let mut probes = ProbeOptions::default();
let mut reason = None;
let mut iter = args.iter();
while let Some(arg) = iter.next() {
@@ -115,16 +112,17 @@ impl SmokeOptions {
validation_error_count = Some(parse_u32("--validation-error-count", value)?);
}
"--probe-loader" => {
- probe_loader = true;
+ probes.vulkan = probes.vulkan.max(VulkanProbeDepth::Loader);
}
"--probe-instance" => {
- probe_loader = true;
- probe_instance = true;
+ probes.vulkan = probes.vulkan.max(VulkanProbeDepth::Instance);
+ }
+ "--probe-window" => {
+ probes.window = true;
}
"--probe-surface" => {
- probe_loader = true;
- probe_instance = true;
- probe_surface = true;
+ probes.vulkan = probes.vulkan.max(VulkanProbeDepth::Surface);
+ probes.window = true;
}
"--reason" => {
let value = iter
@@ -142,9 +140,7 @@ impl SmokeOptions {
frames,
resize_count,
validation_error_count,
- probe_loader,
- probe_instance,
- probe_surface,
+ probes,
reason,
})
}
@@ -156,6 +152,35 @@ fn parse_u32(name: &str, value: &str) -> Result<u32, String> {
.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,
+}
+
+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)
+ }
+
+ const fn includes_surface(self) -> bool {
+ matches!(self, Self::Surface)
+ }
+}
+
#[derive(Clone, Debug, Eq, PartialEq)]
struct VulkanBootstrapProbe {
loader_status: VulkanLoaderStatus,
@@ -164,13 +189,17 @@ struct VulkanBootstrapProbe {
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>,
}
impl VulkanBootstrapProbe {
fn run(options: &SmokeOptions) -> Self {
- if !options.probe_loader {
+ if !options.probes.vulkan.includes_loader() {
return Self {
loader_status: VulkanLoaderStatus::Skipped,
instance_api: None,
@@ -178,6 +207,10 @@ impl VulkanBootstrapProbe {
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,
};
@@ -191,6 +224,10 @@ impl VulkanBootstrapProbe {
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,
},
@@ -201,12 +238,31 @@ impl VulkanBootstrapProbe {
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,
},
};
- if options.probe_instance && probe.loader_status == VulkanLoaderStatus::Available {
+ if options.probes.window {
+ match WinitWindowPlan::smoke().validate() {
+ Ok(plan) => {
+ probe.window_status = WinitWindowStatus::Planned;
+ probe.window_width = Some(plan.width);
+ probe.window_height = Some(plan.height);
+ }
+ Err(err) => {
+ probe.window_status = WinitWindowStatus::Failed;
+ probe.window_error = Some(err.to_string());
+ }
+ }
+ }
+ if options.probes.vulkan.includes_instance()
+ && probe.loader_status == VulkanLoaderStatus::Available
+ {
let config = VulkanInstanceConfig::smoke("fparkan-vulkan-smoke");
probe.portability_enumeration = config.enable_portability_enumeration;
match create_vulkan_instance_probe(&config) {
@@ -220,7 +276,9 @@ impl VulkanBootstrapProbe {
}
}
}
- if options.probe_surface && probe.instance_status == VulkanInstanceStatus::Created {
+ if options.probes.vulkan.includes_surface()
+ && probe.instance_status == VulkanInstanceStatus::Created
+ {
match plan_vulkan_surface(None) {
Ok(_) => {
probe.surface_status = VulkanSurfaceStatus::Planned;
@@ -270,6 +328,23 @@ impl VulkanInstanceStatus {
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum WinitWindowStatus {
+ Skipped,
+ Planned,
+ Failed,
+}
+
+impl WinitWindowStatus {
+ const fn as_str(self) -> &'static str {
+ match self {
+ Self::Skipped => "skipped",
+ Self::Planned => "planned",
+ Self::Failed => "failed",
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum VulkanSurfaceStatus {
Skipped,
Planned,
@@ -373,6 +448,11 @@ fn validate_smoke_options(
"passed native smoke report requires successful --probe-instance".to_string(),
);
}
+ if bootstrap.window_status != WinitWindowStatus::Planned {
+ return Err(
+ "passed native smoke report requires successful --probe-window".to_string(),
+ );
+ }
if bootstrap.surface_status != VulkanSurfaceStatus::Planned {
return Err(
"passed native smoke report requires successful --probe-surface".to_string(),
@@ -408,6 +488,16 @@ fn render_smoke_report_json(
.instance_error
.as_ref()
.map_or_else(|| "null".to_string(), |value| json_string(value));
+ let window_width = bootstrap
+ .window_width
+ .map_or_else(|| "null".to_string(), |value| value.to_string());
+ let window_height = bootstrap
+ .window_height
+ .map_or_else(|| "null".to_string(), |value| value.to_string());
+ let window_error = bootstrap
+ .window_error
+ .as_ref()
+ .map_or_else(|| "null".to_string(), |value| json_string(value));
let surface_error = bootstrap
.surface_error
.as_ref()
@@ -430,6 +520,10 @@ fn render_smoke_report_json(
" \"vulkan_instance_status\": \"{}\",\n",
" \"vulkan_instance_error\": {},\n",
" \"vulkan_portability_enumeration\": {},\n",
+ " \"window_status\": \"{}\",\n",
+ " \"window_width\": {},\n",
+ " \"window_height\": {},\n",
+ " \"window_error\": {},\n",
" \"vulkan_surface_status\": \"{}\",\n",
" \"vulkan_surface_error\": {},\n",
" \"reason\": {}\n",
@@ -454,6 +548,10 @@ fn render_smoke_report_json(
} else {
"false"
},
+ bootstrap.window_status.as_str(),
+ window_width,
+ window_height,
+ window_error,
bootstrap.surface_status.as_str(),
surface_error,
reason
@@ -526,7 +624,7 @@ mod tests {
assert_eq!(options.platform, SmokePlatform::Linux);
assert_eq!(options.status, SmokeStatus::Blocked);
- assert!(options.probe_loader);
+ assert_eq!(options.probes.vulkan, VulkanProbeDepth::Loader);
assert_eq!(options.reason.as_deref(), Some("runner unavailable"));
validate_smoke_options(
&options,
@@ -537,6 +635,10 @@ mod tests {
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,
},
@@ -571,6 +673,10 @@ mod tests {
instance_status: VulkanInstanceStatus::Created,
instance_error: None,
portability_enumeration: false,
+ window_status: WinitWindowStatus::Planned,
+ window_width: Some(1280),
+ window_height: Some(720),
+ window_error: None,
surface_status: VulkanSurfaceStatus::Planned,
surface_error: None,
},
@@ -607,6 +713,10 @@ mod tests {
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,
},
@@ -644,6 +754,10 @@ mod tests {
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,
},
@@ -653,6 +767,47 @@ mod tests {
}
#[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",
+ "--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::Planned,
+ surface_error: None,
+ },
+ ),
+ Err("passed native smoke report requires successful --probe-window".to_string())
+ );
+ }
+
+ #[test]
fn rejects_passed_without_surface_probe() {
let options = SmokeOptions::parse(&strings(&[
"--platform",
@@ -667,6 +822,7 @@ mod tests {
"1",
"--validation-error-count",
"0",
+ "--probe-window",
"--probe-instance",
]))
.expect("options");
@@ -681,6 +837,10 @@ mod tests {
instance_status: VulkanInstanceStatus::Created,
instance_error: None,
portability_enumeration: false,
+ window_status: WinitWindowStatus::Planned,
+ window_width: Some(1280),
+ window_height: Some(720),
+ window_error: None,
surface_status: VulkanSurfaceStatus::Skipped,
surface_error: None,
},
@@ -711,6 +871,10 @@ mod tests {
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::MissingWindowHandles,
surface_error: Some(
"native window/display handles are required for Vulkan surface creation"
@@ -730,6 +894,10 @@ mod tests {
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\": \"missing_window_handles\""));
assert!(json.contains(
"\"vulkan_surface_error\": \"native window/display handles are required for Vulkan surface creation\""
@@ -750,9 +918,25 @@ mod tests {
"runner unavailable",
]))?;
- assert!(options.probe_loader);
- assert!(options.probe_instance);
- assert!(!options.probe_surface);
+ 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(())
}
@@ -768,9 +952,8 @@ mod tests {
"runner unavailable",
]))?;
- assert!(options.probe_loader);
- assert!(options.probe_instance);
- assert!(options.probe_surface);
+ assert_eq!(options.probes.vulkan, VulkanProbeDepth::Surface);
+ assert!(options.probes.window);
Ok(())
}
diff --git a/fixtures/acceptance/coverage.tsv b/fixtures/acceptance/coverage.tsv
index 7bbcb3d..c886430 100644
--- a/fixtures/acceptance/coverage.tsv
+++ b/fixtures/acceptance/coverage.tsv
@@ -28,6 +28,7 @@ S0-CLI-001 covered cargo test -p fparkan-cli --offline stable_exit_codes_are_map
S0-CLI-002 covered cargo test -p fparkan-cli --offline accepts_json_format_option archive_json_has_schema_version
S0-PLAT-001 covered cargo test -p fparkan-platform-winit --offline window_port_reports_default_request_profile
S0-PLAT-002 covered cargo clippy -p fparkan-platform -p fparkan-platform-winit --all-targets --all-features --locked -- -D warnings
+S0-PLAT-003 covered cargo test -p fparkan-platform-winit --offline smoke_window_plan_requires_native_handles_and_nonzero_extent smoke_window_plan_rejects_zero_extent
S0-VK-001 covered cargo test -p fparkan-render-vulkan --offline backend_tracks_render_request_and_presents
S0-VK-002 covered cargo test -p fparkan-render-vulkan --offline device_scoring_is_deterministic_and_prefers_discrete_unified_queue
S0-VK-003 covered cargo test -p fparkan-render-vulkan --offline portability_subset_is_reported_and_enabled_when_exposed
@@ -53,7 +54,7 @@ S0-VK-022 covered cargo test -p fparkan-render-vulkan --offline backend_tracks_r
S0-VK-023 covered cargo test -p fparkan-vulkan-smoke --offline rejects_false_pass_without_full_evidence blocked_report_includes_shader_manifest_and_bootstrap_status
S0-VK-024 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_loader_probe formats_vulkan_api_version
S0-VK-025 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_instance_probe parses_instance_probe_as_loader_probe
-S0-VK-026 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_surface_probe parses_surface_probe_as_instance_probe
+S0-VK-026 covered cargo test -p fparkan-vulkan-smoke --offline rejects_passed_without_window_probe rejects_passed_without_surface_probe parses_surface_probe_as_instance_probe
S0-LIMIT-001 covered cargo test -p fparkan-binary --offline rejects_count_stride_overflow
S0-LIMIT-002 covered cargo test -p fparkan-binary --offline rejects_oversized_declared_allocation_before_read
L1-P1-NRES-001 covered cargo test -p fparkan-nres --offline licensed_corpora_nres_roundtrip_gates
diff --git a/fixtures/acceptance/stage_0_2_roadmap.md b/fixtures/acceptance/stage_0_2_roadmap.md
index 0977b2c..5fc1f4b 100644
--- a/fixtures/acceptance/stage_0_2_roadmap.md
+++ b/fixtures/acceptance/stage_0_2_roadmap.md
@@ -28,6 +28,7 @@
`S0-CLI-002`
`S0-PLAT-001`
`S0-PLAT-002`
+`S0-PLAT-003`
`S0-VK-001`
`S0-VK-002`
`S0-VK-003`