diff options
| author | Valentin Popov <valentin@popov.link> | 2026-06-23 22:56:40 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-06-23 22:56:40 +0300 |
| commit | 4d19728c392c6566358ad78f30bba769f8e723cf (patch) | |
| tree | 04b67eb48f476d7189231dfdbb1f38e8edbc0c56 | |
| parent | 54f07ee3be4c9aad41181ed46b50d273c10767bd (diff) | |
| download | fparkan-4d19728c392c6566358ad78f30bba769f8e723cf.tar.xz fparkan-4d19728c392c6566358ad78f30bba769f8e723cf.zip | |
feat: create native smoke window handles
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | adapters/fparkan-platform-winit/src/lib.rs | 144 | ||||
| -rw-r--r-- | apps/fparkan-vulkan-smoke/Cargo.toml | 1 | ||||
| -rw-r--r-- | apps/fparkan-vulkan-smoke/src/main.rs | 121 | ||||
| -rw-r--r-- | fixtures/acceptance/coverage.tsv | 1 | ||||
| -rw-r--r-- | fixtures/acceptance/stage_0_2_roadmap.md | 1 | ||||
| -rw-r--r-- | xtask/src/main.rs | 4 |
7 files changed, 230 insertions, 43 deletions
@@ -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" }), ) |
