aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-23 22:56:40 +0300
committerValentin Popov <valentin@popov.link>2026-06-23 22:56:40 +0300
commit4d19728c392c6566358ad78f30bba769f8e723cf (patch)
tree04b67eb48f476d7189231dfdbb1f38e8edbc0c56
parent54f07ee3be4c9aad41181ed46b50d273c10767bd (diff)
downloadfparkan-4d19728c392c6566358ad78f30bba769f8e723cf.tar.xz
fparkan-4d19728c392c6566358ad78f30bba769f8e723cf.zip
feat: create native smoke window handles
-rw-r--r--Cargo.lock1
-rw-r--r--adapters/fparkan-platform-winit/src/lib.rs144
-rw-r--r--apps/fparkan-vulkan-smoke/Cargo.toml1
-rw-r--r--apps/fparkan-vulkan-smoke/src/main.rs121
-rw-r--r--fixtures/acceptance/coverage.tsv1
-rw-r--r--fixtures/acceptance/stage_0_2_roadmap.md1
-rw-r--r--xtask/src/main.rs4
7 files changed, 230 insertions, 43 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 01e4f71..c69efa3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -767,6 +767,7 @@ dependencies = [
name = "fparkan-vulkan-smoke"
version = "0.1.0"
dependencies = [
+ "fparkan-platform",
"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 d8efa3e..30c5497 100644
--- a/adapters/fparkan-platform-winit/src/lib.rs
+++ b/adapters/fparkan-platform-winit/src/lib.rs
@@ -28,9 +28,12 @@ use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use std::collections::VecDeque;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
+use winit::application::ApplicationHandler;
+use winit::dpi::PhysicalSize as WinitPhysicalSize;
use winit::event::{Event, MouseButton, WindowEvent};
+use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::platform::scancode::PhysicalKeyExtScancode;
-use winit::window::Window;
+use winit::window::{Window, WindowId};
static NEXT_WINDOW_HANDLE_ID: AtomicU64 = AtomicU64::new(1);
const DEFAULT_SMOKE_WIDTH: u32 = 1280;
@@ -184,8 +187,115 @@ impl WinitWindowPlan {
}
}
+/// Native smoke window creation result.
+#[derive(Clone, Copy, Debug)]
+pub struct WinitSmokeWindowProbe {
+ /// Validated creation plan.
+ pub plan: WinitWindowPlan,
+ /// Captured window descriptor.
+ pub window: WinitWindow,
+}
+
+impl WinitSmokeWindowProbe {
+ /// Returns raw native handles captured from the native window.
+ #[must_use]
+ pub fn native_handles(&self) -> Option<NativeWindowHandles> {
+ self.window.native_handles()
+ }
+}
+
+/// Creates a native smoke window, captures raw handles, then exits the event loop.
+///
+/// # Errors
+///
+/// Returns [`PlatformError`] when the plan is invalid, the event loop/window
+/// cannot be created, or raw native handles are unavailable.
+pub fn probe_smoke_window() -> Result<WinitSmokeWindowProbe, PlatformError> {
+ let plan = WinitWindowPlan::smoke().validate()?;
+ let event_loop = EventLoop::new().map_err(|err| PlatformError::Backend {
+ context: "winit event loop",
+ message: err.to_string(),
+ })?;
+ let mut app = SmokeWindowApp::new(plan);
+ event_loop
+ .run_app(&mut app)
+ .map_err(|err| PlatformError::Backend {
+ context: "winit event loop",
+ message: err.to_string(),
+ })?;
+ app.into_probe()
+}
+
+struct SmokeWindowApp {
+ plan: WinitWindowPlan,
+ window: Option<WinitWindow>,
+ error: Option<String>,
+}
+
+impl SmokeWindowApp {
+ const fn new(plan: WinitWindowPlan) -> Self {
+ Self {
+ plan,
+ window: None,
+ error: None,
+ }
+ }
+
+ fn into_probe(self) -> Result<WinitSmokeWindowProbe, PlatformError> {
+ if let Some(message) = self.error {
+ return Err(PlatformError::Backend {
+ context: "winit smoke window",
+ message,
+ });
+ }
+ let window = self.window.ok_or_else(|| PlatformError::Backend {
+ context: "winit smoke window",
+ message: "event loop exited before creating a window".to_string(),
+ })?;
+ if self.plan.requires_native_handles && window.native_handles().is_none() {
+ return Err(PlatformError::Backend {
+ context: "winit smoke window",
+ message: "native window/display handles are unavailable".to_string(),
+ });
+ }
+ Ok(WinitSmokeWindowProbe {
+ plan: self.plan,
+ window,
+ })
+ }
+}
+
+impl ApplicationHandler for SmokeWindowApp {
+ fn resumed(&mut self, event_loop: &ActiveEventLoop) {
+ if self.window.is_some() || self.error.is_some() {
+ event_loop.exit();
+ return;
+ }
+ let attributes = Window::default_attributes()
+ .with_title("FParkan Vulkan smoke")
+ .with_inner_size(WinitPhysicalSize::new(self.plan.width, self.plan.height));
+ match event_loop.create_window(attributes) {
+ Ok(window) => {
+ self.window = Some(WinitWindow::from_window(&window));
+ }
+ Err(err) => {
+ self.error = Some(err.to_string());
+ }
+ }
+ event_loop.exit();
+ }
+
+ fn window_event(
+ &mut self,
+ _event_loop: &ActiveEventLoop,
+ _window_id: WindowId,
+ _event: WindowEvent,
+ ) {
+ }
+}
+
/// Minimal window view over a `winit` window.
-#[derive(Clone, Debug)]
+#[derive(Clone, Copy, Debug)]
pub struct WinitWindow {
handle: WindowHandle,
width: u32,
@@ -344,6 +454,36 @@ mod tests {
}
#[test]
+ fn smoke_window_app_requires_created_native_window() {
+ let app = SmokeWindowApp::new(WinitWindowPlan::smoke());
+
+ assert!(matches!(
+ app.into_probe(),
+ Err(PlatformError::Backend {
+ context: "winit smoke window",
+ ..
+ })
+ ));
+ }
+
+ #[test]
+ fn smoke_window_app_rejects_synthetic_window_without_native_handles() {
+ let mut app = SmokeWindowApp::new(WinitWindowPlan::smoke());
+ app.window = Some(WinitWindow::synthetic(
+ DEFAULT_SMOKE_WIDTH,
+ DEFAULT_SMOKE_HEIGHT,
+ ));
+
+ assert!(matches!(
+ app.into_probe(),
+ Err(PlatformError::Backend {
+ context: "winit smoke window",
+ ..
+ })
+ ));
+ }
+
+ #[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 f872920..21b67ae 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 = { path = "../../crates/fparkan-platform" }
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit" }
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan" }
diff --git a/apps/fparkan-vulkan-smoke/src/main.rs b/apps/fparkan-vulkan-smoke/src/main.rs
index 088054c..045985f 100644
--- a/apps/fparkan-vulkan-smoke/src/main.rs
+++ b/apps/fparkan-vulkan-smoke/src/main.rs
@@ -11,7 +11,8 @@
#![allow(clippy::print_stderr, clippy::print_stdout)]
//! Native Vulkan smoke runner entrypoint.
-use fparkan_platform_winit::WinitWindowPlan;
+use fparkan_platform::{NativeWindowHandles, WindowPort};
+use fparkan_platform_winit::{probe_smoke_window, WinitWindowPlan};
use fparkan_render_vulkan::{
create_vulkan_instance_probe, plan_vulkan_surface, probe_vulkan_loader,
triangle_shader_manifest, validate_shader_manifest, VulkanInstanceConfig,
@@ -209,23 +210,35 @@ struct VulkanBootstrapProbe {
impl VulkanBootstrapProbe {
fn run(options: &SmokeOptions) -> Self {
if !options.probes.vulkan.includes_loader() {
- return 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,
- };
+ return Self::skipped();
}
- let mut probe = match probe_vulkan_loader() {
+ let mut probe = Self::probe_loader();
+ let window_handles = probe.probe_window(options);
+ probe.probe_instance(options);
+ probe.probe_surface(options, window_handles);
+ probe
+ }
+
+ 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,
+ }
+ }
+
+ 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)),
@@ -254,51 +267,79 @@ impl VulkanBootstrapProbe {
surface_status: VulkanSurfaceStatus::Skipped,
surface_error: None,
},
- };
+ }
+ }
- if options.probes.window {
+ 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
+ }
+ }
+ } else 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);
+ self.window_status = WinitWindowStatus::Planned;
+ self.window_width = Some(plan.width);
+ self.window_height = Some(plan.height);
}
Err(err) => {
- probe.window_status = WinitWindowStatus::Failed;
- probe.window_error = Some(err.to_string());
+ self.window_status = WinitWindowStatus::Failed;
+ self.window_error = Some(err.to_string());
}
}
+ None
+ } else {
+ None
}
+ }
+
+ fn probe_instance(&mut self, options: &SmokeOptions) {
if options.probes.vulkan.includes_instance()
- && probe.loader_status == VulkanLoaderStatus::Available
+ && self.loader_status == VulkanLoaderStatus::Available
{
let config = VulkanInstanceConfig::smoke("fparkan-vulkan-smoke");
- probe.portability_enumeration = config.enable_portability_enumeration;
+ self.portability_enumeration = config.enable_portability_enumeration;
match create_vulkan_instance_probe(&config) {
Ok(instance) => {
- probe.instance_status = VulkanInstanceStatus::Created;
- probe.portability_enumeration = instance.report.create_flags != 0;
+ self.instance_status = VulkanInstanceStatus::Created;
+ self.portability_enumeration = instance.report.create_flags != 0;
}
Err(err) => {
- probe.instance_status = VulkanInstanceStatus::Failed;
- probe.instance_error = Some(err.to_string());
+ self.instance_status = VulkanInstanceStatus::Failed;
+ self.instance_error = Some(err.to_string());
}
}
}
+ }
+
+ fn probe_surface(
+ &mut self,
+ options: &SmokeOptions,
+ window_handles: Option<NativeWindowHandles>,
+ ) {
if options.probes.vulkan.includes_surface()
- && probe.instance_status == VulkanInstanceStatus::Created
+ && self.instance_status == VulkanInstanceStatus::Created
{
- match plan_vulkan_surface(None) {
+ match plan_vulkan_surface(window_handles) {
Ok(_) => {
- probe.surface_status = VulkanSurfaceStatus::Planned;
+ self.surface_status = VulkanSurfaceStatus::Planned;
}
Err(err) => {
- probe.surface_status = VulkanSurfaceStatus::MissingWindowHandles;
- probe.surface_error = Some(err.to_string());
+ self.surface_status = VulkanSurfaceStatus::MissingWindowHandles;
+ self.surface_error = Some(err.to_string());
}
}
}
- probe
}
}
@@ -340,6 +381,7 @@ impl VulkanInstanceStatus {
enum WinitWindowStatus {
Skipped,
Planned,
+ Created,
Failed,
}
@@ -348,6 +390,7 @@ impl WinitWindowStatus {
match self {
Self::Skipped => "skipped",
Self::Planned => "planned",
+ Self::Created => "created",
Self::Failed => "failed",
}
}
@@ -463,7 +506,7 @@ fn validate_smoke_options(
"passed native smoke report requires successful --probe-instance".to_string(),
);
}
- if bootstrap.window_status != WinitWindowStatus::Planned {
+ if bootstrap.window_status != WinitWindowStatus::Created {
return Err(
"passed native smoke report requires successful --probe-window".to_string(),
);
@@ -710,7 +753,7 @@ mod tests {
instance_status: VulkanInstanceStatus::Created,
instance_error: None,
portability_enumeration: false,
- window_status: WinitWindowStatus::Planned,
+ window_status: WinitWindowStatus::Created,
window_width: Some(1280),
window_height: Some(720),
window_error: None,
@@ -793,7 +836,7 @@ mod tests {
instance_status: VulkanInstanceStatus::Created,
instance_error: None,
portability_enumeration: false,
- window_status: WinitWindowStatus::Planned,
+ window_status: WinitWindowStatus::Created,
window_width: Some(1280),
window_height: Some(720),
window_error: None,
@@ -923,7 +966,7 @@ mod tests {
instance_status: VulkanInstanceStatus::Created,
instance_error: None,
portability_enumeration: false,
- window_status: WinitWindowStatus::Planned,
+ window_status: WinitWindowStatus::Created,
window_width: Some(1280),
window_height: Some(720),
window_error: None,
diff --git a/fixtures/acceptance/coverage.tsv b/fixtures/acceptance/coverage.tsv
index 11325e2..d050fab 100644
--- a/fixtures/acceptance/coverage.tsv
+++ b/fixtures/acceptance/coverage.tsv
@@ -29,6 +29,7 @@ S0-CLI-002 covered cargo test -p fparkan-cli --offline accepts_json_format_optio
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-PLAT-004 covered cargo test -p fparkan-platform-winit --offline smoke_window_app_requires_created_native_window smoke_window_app_rejects_synthetic_window_without_native_handles
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
diff --git a/fixtures/acceptance/stage_0_2_roadmap.md b/fixtures/acceptance/stage_0_2_roadmap.md
index 37e1daa..60974da 100644
--- a/fixtures/acceptance/stage_0_2_roadmap.md
+++ b/fixtures/acceptance/stage_0_2_roadmap.md
@@ -29,6 +29,7 @@
`S0-PLAT-001`
`S0-PLAT-002`
`S0-PLAT-003`
+`S0-PLAT-004`
`S0-VK-001`
`S0-VK-002`
`S0-VK-003`
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 3d94424..5990022 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -1527,7 +1527,7 @@ fn validate_native_smoke_report(
"created",
failures,
);
- expect_string_field(platform, report, "window_status", "planned", failures);
+ expect_string_field(platform, report, "window_status", "created", failures);
expect_string_field(
platform,
report,
@@ -2232,7 +2232,7 @@ mod tests {
"shader_manifest_hash": "dd293e4ff08ffca1c037900d08b0ffd415db39f238b4fcdde46468fa049b679c",
"vulkan_loader_status": "available",
"vulkan_instance_status": "created",
- "window_status": "planned",
+ "window_status": "created",
"vulkan_surface_status": "planned"
}),
)