Remove planning and acceptance scaffolding while retaining the native Vulkan mission preview, format readers, runtime algorithms, and ordinary Rust tests. Keep the book aligned with the runnable project and validate checked-in shaders without generated tool metadata.
558 lines
17 KiB
Rust
558 lines
17 KiB
Rust
#![forbid(unsafe_code)]
|
|
//! Minimal `winit`-backed platform adapter shim.
|
|
|
|
use fparkan_platform::{
|
|
EventSource, MonotonicClock, MonotonicInstant, NativeWindowHandles, PhysicalSize,
|
|
PlatformError, PlatformEvent, RenderRequest, WindowHandle, WindowPort,
|
|
};
|
|
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
|
|
use std::collections::VecDeque;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
use std::sync::OnceLock;
|
|
use std::time::Instant;
|
|
use winit::event::{Event, MouseButton, WindowEvent};
|
|
use winit::platform::scancode::PhysicalKeyExtScancode;
|
|
use winit::window::Window;
|
|
|
|
static NEXT_WINDOW_HANDLE_ID: AtomicU64 = AtomicU64::new(1);
|
|
static CLOCK_START: OnceLock<Instant> = OnceLock::new();
|
|
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)
|
|
}
|
|
|
|
/// Simple monotonic clock for windowing abstractions.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct WinitClock;
|
|
|
|
impl MonotonicClock for WinitClock {
|
|
fn now(&self) -> MonotonicInstant {
|
|
let elapsed = CLOCK_START.get_or_init(Instant::now).elapsed();
|
|
MonotonicInstant(elapsed.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<PlatformEvent>,
|
|
cursor_position: Option<(f64, f64)>,
|
|
minimized: Option<bool>,
|
|
occluded: Option<bool>,
|
|
}
|
|
|
|
impl WinitEventSource {
|
|
/// Creates an empty source.
|
|
#[must_use]
|
|
pub const fn new() -> Self {
|
|
Self {
|
|
queue: VecDeque::new(),
|
|
cursor_position: None,
|
|
minimized: None,
|
|
occluded: None,
|
|
}
|
|
}
|
|
|
|
/// 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, .. } => {
|
|
if let Some(scancode) = event.physical_key.to_scancode() {
|
|
self.queue.push_back(PlatformEvent::KeyboardInput {
|
|
scancode,
|
|
pressed: event.state.is_pressed(),
|
|
});
|
|
}
|
|
}
|
|
WindowEvent::MouseInput { state, button, .. } => {
|
|
let (x, y) = self.cursor_position.unwrap_or((0.0, 0.0));
|
|
self.queue.push_back(PlatformEvent::MouseInput {
|
|
button: mouse_button_code(*button),
|
|
pressed: state.is_pressed(),
|
|
x,
|
|
y,
|
|
});
|
|
}
|
|
WindowEvent::CursorMoved { position, .. } => {
|
|
self.cursor_position = Some((position.x, position.y));
|
|
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,
|
|
});
|
|
let minimized = size.width == 0 || size.height == 0;
|
|
if self.minimized != Some(minimized) {
|
|
self.minimized = Some(minimized);
|
|
self.queue.push_back(PlatformEvent::Minimized { minimized });
|
|
}
|
|
}
|
|
WindowEvent::Focused(focused) => {
|
|
self.queue
|
|
.push_back(PlatformEvent::FocusChanged { focused: *focused });
|
|
}
|
|
WindowEvent::Occluded(occluded) => {
|
|
if self.occluded != Some(*occluded) {
|
|
self.occluded = Some(*occluded);
|
|
self.queue.push_back(PlatformEvent::Occluded {
|
|
occluded: *occluded,
|
|
});
|
|
}
|
|
}
|
|
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<T>(&mut self, event: &Event<T>) {
|
|
match event {
|
|
Event::Resumed => self.queue.push_back(PlatformEvent::Resumed),
|
|
Event::Suspended => self.queue.push_back(PlatformEvent::Suspended),
|
|
Event::WindowEvent { 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.saturating_add(index),
|
|
}
|
|
}
|
|
|
|
impl EventSource for WinitEventSource {
|
|
fn poll(&mut self, out: &mut Vec<PlatformEvent>) -> Result<(), PlatformError> {
|
|
while let Some(event) = self.queue.pop_front() {
|
|
out.push(event);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// 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, Copy, Debug)]
|
|
pub struct WinitWindow {
|
|
handle: WindowHandle,
|
|
width: u32,
|
|
height: u32,
|
|
scale: f64,
|
|
focused: bool,
|
|
minimized: bool,
|
|
occluded: bool,
|
|
native_handles: Option<NativeWindowHandles>,
|
|
}
|
|
|
|
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,
|
|
native_handles: window_native_handles(window),
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
native_handles: None,
|
|
}
|
|
}
|
|
|
|
/// Returns requested default render profile for integration points.
|
|
#[must_use]
|
|
pub const fn default_render_request() -> RenderRequest {
|
|
RenderRequest::conservative()
|
|
}
|
|
|
|
/// Applies one platform event to the cached window descriptor state.
|
|
pub fn apply_event(&mut self, event: &PlatformEvent) {
|
|
match event {
|
|
PlatformEvent::Resize { width, height } => {
|
|
self.width = *width;
|
|
self.height = *height;
|
|
}
|
|
PlatformEvent::DpiChanged { scale } => {
|
|
self.scale = *scale;
|
|
}
|
|
PlatformEvent::FocusChanged { focused } => {
|
|
self.focused = *focused;
|
|
}
|
|
PlatformEvent::Minimized { minimized } => {
|
|
self.minimized = *minimized;
|
|
}
|
|
PlatformEvent::Occluded { occluded } => {
|
|
self.occluded = *occluded;
|
|
}
|
|
PlatformEvent::Suspended
|
|
| PlatformEvent::Resumed
|
|
| PlatformEvent::QuitRequested
|
|
| PlatformEvent::KeyboardInput { .. }
|
|
| PlatformEvent::MouseInput { .. }
|
|
| PlatformEvent::CursorMoved { .. } => {}
|
|
}
|
|
}
|
|
|
|
/// Applies a sequence of platform events to the cached window descriptor state.
|
|
pub fn apply_events<'a>(&mut self, events: impl IntoIterator<Item = &'a PlatformEvent>) {
|
|
for event in events {
|
|
self.apply_event(event);
|
|
}
|
|
}
|
|
|
|
/// Applies one native `winit` window event to the cached window descriptor state.
|
|
pub fn apply_window_event(&mut self, event: &WindowEvent) {
|
|
match event {
|
|
WindowEvent::Resized(size) => {
|
|
self.width = size.width;
|
|
self.height = size.height;
|
|
self.minimized = size.width == 0 || size.height == 0;
|
|
}
|
|
WindowEvent::Focused(focused) => {
|
|
self.focused = *focused;
|
|
}
|
|
WindowEvent::Occluded(occluded) => {
|
|
self.occluded = *occluded;
|
|
}
|
|
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
|
|
self.scale = *scale_factor;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
fn native_handles(&self) -> Option<NativeWindowHandles> {
|
|
self.native_handles
|
|
}
|
|
}
|
|
|
|
/// Extracts raw handles from a live `winit::Window`.
|
|
#[must_use]
|
|
pub fn window_native_handles(window: &Window) -> Option<NativeWindowHandles> {
|
|
let display = window.display_handle().ok()?.as_raw();
|
|
let window = window.window_handle().ok()?.as_raw();
|
|
Some(NativeWindowHandles { display, window })
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use winit::event::{DeviceId, ElementState};
|
|
|
|
#[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
|
|
}
|
|
);
|
|
assert!(window.native_handles().is_none());
|
|
}
|
|
|
|
#[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 monotonic_clock_uses_process_local_epoch() {
|
|
let clock = WinitClock;
|
|
let first = clock.now();
|
|
let second = clock.now();
|
|
|
|
assert!(second >= first);
|
|
}
|
|
|
|
#[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));
|
|
}
|
|
|
|
#[test]
|
|
fn push_event_maps_lifecycle_resumed_and_suspended() -> Result<(), PlatformError> {
|
|
let mut source = WinitEventSource::new();
|
|
source.push_event(&Event::<()>::Resumed);
|
|
source.push_event(&Event::<()>::Suspended);
|
|
|
|
let mut events = Vec::new();
|
|
source.poll(&mut events)?;
|
|
|
|
assert_eq!(
|
|
events,
|
|
vec![PlatformEvent::Resumed, PlatformEvent::Suspended]
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn cursor_position_and_occlusion_are_preserved_for_mouse_input() -> Result<(), PlatformError> {
|
|
let mut source = WinitEventSource::new();
|
|
source.push_window_event(&WindowEvent::CursorMoved {
|
|
device_id: DeviceId::dummy(),
|
|
position: (320.0, 240.0).into(),
|
|
});
|
|
source.push_window_event(&WindowEvent::MouseInput {
|
|
device_id: DeviceId::dummy(),
|
|
state: ElementState::Pressed,
|
|
button: MouseButton::Other(u16::MAX),
|
|
});
|
|
source.push_window_event(&WindowEvent::Occluded(true));
|
|
|
|
let mut events = Vec::new();
|
|
source.poll(&mut events)?;
|
|
|
|
assert!(events.contains(&PlatformEvent::CursorMoved { x: 320.0, y: 240.0 }));
|
|
assert!(events.contains(&PlatformEvent::MouseInput {
|
|
button: u16::MAX,
|
|
pressed: true,
|
|
x: 320.0,
|
|
y: 240.0,
|
|
}));
|
|
assert!(events.contains(&PlatformEvent::Occluded { occluded: true }));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn zero_extent_resize_updates_minimized_state() -> Result<(), PlatformError> {
|
|
let mut source = WinitEventSource::new();
|
|
source.push_window_event(&WindowEvent::Resized(winit::dpi::PhysicalSize::new(
|
|
0u32, 720u32,
|
|
)));
|
|
source.push_window_event(&WindowEvent::Resized(winit::dpi::PhysicalSize::new(
|
|
1280u32, 720u32,
|
|
)));
|
|
|
|
let mut events = Vec::new();
|
|
source.poll(&mut events)?;
|
|
|
|
assert!(events.contains(&PlatformEvent::Minimized { minimized: true }));
|
|
assert!(events.contains(&PlatformEvent::Minimized { minimized: false }));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn window_descriptor_applies_lifecycle_and_resize_events() {
|
|
let mut window = WinitWindow::synthetic(640, 360);
|
|
let events = [
|
|
PlatformEvent::Resize {
|
|
width: 1280,
|
|
height: 720,
|
|
},
|
|
PlatformEvent::DpiChanged { scale: 2.0 },
|
|
PlatformEvent::FocusChanged { focused: false },
|
|
PlatformEvent::Minimized { minimized: true },
|
|
PlatformEvent::Occluded { occluded: true },
|
|
];
|
|
|
|
window.apply_events(events.iter());
|
|
|
|
assert_eq!(
|
|
window.drawable_size(),
|
|
PhysicalSize {
|
|
width: 1280,
|
|
height: 720,
|
|
}
|
|
);
|
|
assert_eq!(window.dpi_scale(), 2.0);
|
|
assert!(!window.has_focus());
|
|
assert!(window.is_minimized());
|
|
assert!(window.is_occluded());
|
|
}
|
|
|
|
#[test]
|
|
fn window_descriptor_applies_native_window_events() {
|
|
let mut window = WinitWindow::synthetic(640, 360);
|
|
|
|
window.apply_window_event(&WindowEvent::Resized(winit::dpi::PhysicalSize::new(
|
|
0u32, 720u32,
|
|
)));
|
|
window.apply_window_event(&WindowEvent::Focused(false));
|
|
window.apply_window_event(&WindowEvent::Occluded(true));
|
|
|
|
assert_eq!(
|
|
window.drawable_size(),
|
|
PhysicalSize {
|
|
width: 0,
|
|
height: 720,
|
|
}
|
|
);
|
|
assert!(!window.has_focus());
|
|
assert!(window.is_minimized());
|
|
assert!(window.is_occluded());
|
|
}
|
|
}
|
|
|
|
// SAFETY: no unsafe usage in this crate.
|