From f8e447ffee746cfe6580cc0e78a8a225aa39b546 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Tue, 23 Jun 2026 22:05:16 +0400 Subject: feat: close stage 0-2 audit groundwork Remove legacy SDL/OpenGL adapters from the workspace and introduce winit/Vulkan adapter boundaries for the rendered composition root. Add reproducible toolchain and xtask CI coverage for formatting, tests, clippy, docs, policy, deny, acceptance auditing, and hosted OS matrix evidence. Strengthen Stage 1 data contracts with byte-first paths, VFS hardening, structured diagnostics, RsLi writer/edit scaffolding, corpus reporting, and resource error classification. Advance Stage 2 asset preparation by moving mission loading through assets/runtime boundaries, materializing prototype graph data, preserving provenance, and adding inspection/viewer integration. Record the Stage 0-2 audit input, acceptance roadmap, coverage updates, and documentation notes for follow-up evidence. --- adapters/fparkan-platform-winit/src/lib.rs | 257 +++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 adapters/fparkan-platform-winit/src/lib.rs (limited to 'adapters/fparkan-platform-winit/src/lib.rs') diff --git a/adapters/fparkan-platform-winit/src/lib.rs b/adapters/fparkan-platform-winit/src/lib.rs new file mode 100644 index 0000000..ec30908 --- /dev/null +++ b/adapters/fparkan-platform-winit/src/lib.rs @@ -0,0 +1,257 @@ +#![forbid(unsafe_code)] +//! Minimal `winit`-backed platform adapter shim. + +use fparkan_platform::{ + EventSource, MonotonicClock, MonotonicInstant, PlatformEvent, PlatformError, PhysicalSize, + RenderRequest, WindowHandle, WindowPort, +}; +use winit::event::{MouseButton, WindowEvent}; +use winit::event_loop::Event; +use std::collections::VecDeque; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; +use winit::window::Window; + +static NEXT_WINDOW_HANDLE_ID: AtomicU64 = AtomicU64::new(1); + +fn next_window_id() -> u64 { + NEXT_WINDOW_HANDLE_ID.fetch_add(1, Ordering::Relaxed) +} + +/// Simple monotonic clock for windowing abstractions. +#[derive(Clone, Copy, Debug)] +pub struct WinitClock; + +impl MonotonicClock for WinitClock { + fn now(&self) -> MonotonicInstant { + let duration = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); + MonotonicInstant(duration.as_millis().try_into().unwrap_or(u64::MAX)) + } +} + +/// Event source backed by pre-buffered platform events. +#[derive(Clone, Debug, Default)] +pub struct WinitEventSource { + queue: VecDeque, +} + +impl WinitEventSource { + /// Creates an empty source. + #[must_use] + pub const fn new() -> Self { + Self { + queue: VecDeque::new(), + } + } + + /// Pushes a synthetic event (used by tests and smoke stubs). + pub fn push(&mut self, event: PlatformEvent) { + self.queue.push_back(event); + } + + /// Pushes a mapped native window event. + pub fn push_window_event(&mut self, event: &WindowEvent<'_>) { + match event { + WindowEvent::KeyboardInput { event, .. } => { + self.queue.push_back(PlatformEvent::KeyboardInput { + scancode: event.physical_key.to_scancode().unwrap_or(0), + pressed: event.state.is_pressed(), + }); + } + WindowEvent::MouseInput { state, button, .. } => { + self.queue.push_back(PlatformEvent::MouseInput { + button: mouse_button_code(*button), + pressed: state.is_pressed(), + x: 0.0, + y: 0.0, + }); + } + WindowEvent::CursorMoved { position, .. } => { + self.queue.push_back(PlatformEvent::CursorMoved { + x: position.x, + y: position.y, + }); + } + WindowEvent::Resized(size) => { + self.queue.push_back(PlatformEvent::Resize { + width: size.width, + height: size.height, + }); + } + WindowEvent::Focused(focused) => { + self.queue.push_back(PlatformEvent::FocusChanged { focused: *focused }); + } + WindowEvent::ScaleFactorChanged { + scale_factor, + .. + } => { + self.queue + .push_back(PlatformEvent::DpiChanged { scale: *scale_factor }); + } + WindowEvent::CloseRequested => { + self.queue.push_back(PlatformEvent::QuitRequested); + } + _ => {} + } + } + + /// Pushes events from an event loop event. + pub fn push_event(&mut self, event: &Event<'_, T>) { + if let Event::WindowEvent { event, .. } = event { + self.push_window_event(event); + } + } +} + +fn mouse_button_code(button: MouseButton) -> u16 { + match button { + MouseButton::Left => 0, + MouseButton::Right => 1, + MouseButton::Middle => 2, + MouseButton::Back => 3, + MouseButton::Forward => 4, + MouseButton::Other(index) => 100 + u16::try_from(index).unwrap_or(0), + } +} + +impl EventSource for WinitEventSource { + fn poll(&mut self, out: &mut Vec) -> Result<(), PlatformError> { + while let Some(event) = self.queue.pop_front() { + out.push(event); + } + Ok(()) + } +} + +/// Minimal window view over a `winit` window. +#[derive(Clone, Debug)] +pub struct WinitWindow { + handle: WindowHandle, + width: u32, + height: u32, + scale: f64, + focused: bool, + minimized: bool, + occluded: bool, +} + +impl WinitWindow { + /// Builds a stable descriptor from a `winit` window. + #[must_use] + pub fn from_window(window: &Window) -> Self { + let scale = window.scale_factor(); + let size = window.inner_size(); + Self { + handle: WindowHandle { + id: next_window_id(), + }, + width: size.width, + height: size.height, + scale, + focused: true, + minimized: false, + occluded: false, + } + } + + /// Returns conservative defaults if a native window is not available yet. + #[must_use] + pub fn synthetic(width: u32, height: u32) -> Self { + Self { + handle: WindowHandle { + id: next_window_id(), + }, + width, + height, + scale: 1.0, + focused: true, + minimized: false, + occluded: false, + } + } + + /// Returns requested default render profile for integration points. + #[must_use] + pub const fn default_render_request() -> RenderRequest { + RenderRequest::conservative() + } +} + +impl WindowPort for WinitWindow { + fn drawable_size(&self) -> PhysicalSize { + PhysicalSize { + width: self.width, + height: self.height, + } + } + + fn dpi_scale(&self) -> f64 { + self.scale + } + + fn has_focus(&self) -> bool { + self.focused + } + + fn is_minimized(&self) -> bool { + self.minimized + } + + fn is_occluded(&self) -> bool { + self.occluded + } + + fn handle(&self) -> WindowHandle { + self.handle + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn event_source_buffers_synthetic_events() -> Result<(), PlatformError> { + let mut source = WinitEventSource::new(); + source.push(PlatformEvent::Resumed); + source.push(PlatformEvent::QuitRequested); + let mut events = Vec::new(); + source.poll(&mut events)?; + assert_eq!(events, vec![PlatformEvent::Resumed, PlatformEvent::QuitRequested]); + Ok(()) + } + + #[test] + fn window_port_reports_default_request_profile() { + let window = WinitWindow::synthetic(640, 360); + let request = WinitWindow::default_render_request(); + assert_eq!(request.presentation, fparkan_platform::PresentationMode::Fifo); + assert_eq!(window.drawable_size(), PhysicalSize { width: 640, height: 360 }); + } + + #[test] + fn window_events_push_expected_platform_events() { + let mut source = WinitEventSource::new(); + let size = winit::dpi::PhysicalSize::new(1024u32, 768u32); + + source.push_window_event(&WindowEvent::Resized(size)); + source.push_window_event(&WindowEvent::Focused(false)); + source.push_window_event(&WindowEvent::CloseRequested); + + let mut events = Vec::new(); + source + .poll(&mut events) + .expect("platform event pump should never fail"); + + assert!(events.contains(&PlatformEvent::Resize { + width: 1024, + height: 768, + })); + assert!(events.contains(&PlatformEvent::FocusChanged { focused: false })); + assert!(events.contains(&PlatformEvent::QuitRequested)); + } +} + +// SAFETY: no unsafe usage in this crate. -- cgit v1.2.3