diff options
Diffstat (limited to 'adapters')
| -rw-r--r-- | adapters/fparkan-platform-sdl/src/lib.rs | 123 | ||||
| -rw-r--r-- | adapters/fparkan-platform-winit/Cargo.toml (renamed from adapters/fparkan-platform-sdl/Cargo.toml) | 3 | ||||
| -rw-r--r-- | adapters/fparkan-platform-winit/src/lib.rs | 257 | ||||
| -rw-r--r-- | adapters/fparkan-render-gl/src/lib.rs | 242 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/Cargo.toml (renamed from adapters/fparkan-render-gl/Cargo.toml) | 3 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/lib.rs | 175 |
6 files changed, 436 insertions, 367 deletions
diff --git a/adapters/fparkan-platform-sdl/src/lib.rs b/adapters/fparkan-platform-sdl/src/lib.rs deleted file mode 100644 index f573885..0000000 --- a/adapters/fparkan-platform-sdl/src/lib.rs +++ /dev/null @@ -1,123 +0,0 @@ -#![forbid(unsafe_code)] -//! SDL platform adapter boundary stubs behind safe `FParkan` ports. - -use fparkan_platform::{ - EventSource, GraphicsContextRequest, GraphicsProfile, PhysicalSize, PlatformError, - PlatformEvent, Version, WindowPort, -}; - -/// Adapter capabilities compiled into this package. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct SdlAdapterCapabilities { - /// Supported graphics context requests in preference order. - pub graphics: Vec<GraphicsContextRequest>, - /// Whether adapter-owned code is free of `unsafe`. - pub project_owned_unsafe_free: bool, -} - -impl Default for SdlAdapterCapabilities { - fn default() -> Self { - Self { - graphics: vec![ - GraphicsContextRequest { - profile: GraphicsProfile::DesktopCore, - version: Version { major: 3, minor: 3 }, - }, - GraphicsContextRequest { - profile: GraphicsProfile::Embedded, - version: Version { major: 2, minor: 0 }, - }, - ], - project_owned_unsafe_free: true, - } - } -} - -/// Returns whether the project-owned adapter boundary avoids `unsafe`. -#[must_use] -pub fn project_owned_layer_unsafe_free() -> bool { - SdlAdapterCapabilities::default().project_owned_unsafe_free -} - -/// In-memory event source used by adapter smoke tests before a concrete SDL -/// runtime is selected. -#[derive(Clone, Debug, Default)] -pub struct SdlEventSourceStub { - pending: Vec<PlatformEvent>, -} - -impl SdlEventSourceStub { - /// Creates an event source with deterministic pending events. - #[must_use] - pub fn new(pending: Vec<PlatformEvent>) -> Self { - Self { pending } - } -} - -impl EventSource for SdlEventSourceStub { - fn poll(&mut self, out: &mut Vec<PlatformEvent>) -> Result<(), PlatformError> { - out.append(&mut self.pending); - Ok(()) - } -} - -/// Safe window-port stub with SDL-compatible drawable-size semantics. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct SdlWindowStub { - size: PhysicalSize, - presents: u64, -} - -impl SdlWindowStub { - /// Creates a stub window with a fixed drawable size. - #[must_use] - pub fn new(size: PhysicalSize) -> Self { - Self { size, presents: 0 } - } - - /// Number of successful present calls. - #[must_use] - pub fn presents(&self) -> u64 { - self.presents - } -} - -impl WindowPort for SdlWindowStub { - fn drawable_size(&self) -> PhysicalSize { - self.size - } - - fn present(&mut self) -> Result<(), PlatformError> { - self.presents = self.presents.saturating_add(1); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn adapter_boundary_is_project_owned_unsafe_free() { - assert!(project_owned_layer_unsafe_free()); - assert_eq!(SdlAdapterCapabilities::default().graphics.len(), 2); - } - - #[test] - fn event_source_and_window_ports_are_deterministic() -> Result<(), PlatformError> { - let mut source = SdlEventSourceStub::new(vec![PlatformEvent::Quit]); - let mut events = Vec::new(); - source.poll(&mut events)?; - source.poll(&mut events)?; - assert_eq!(events, vec![PlatformEvent::Quit]); - - let mut window = SdlWindowStub::new(PhysicalSize { - width: 320, - height: 240, - }); - assert_eq!(window.drawable_size().width, 320); - window.present()?; - assert_eq!(window.presents(), 1); - Ok(()) - } -} diff --git a/adapters/fparkan-platform-sdl/Cargo.toml b/adapters/fparkan-platform-winit/Cargo.toml index fd9b040..e0ec438 100644 --- a/adapters/fparkan-platform-sdl/Cargo.toml +++ b/adapters/fparkan-platform-winit/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "fparkan-platform-sdl" +name = "fparkan-platform-winit" version.workspace = true edition.workspace = true license.workspace = true @@ -7,6 +7,7 @@ repository.workspace = true [dependencies] fparkan-platform = { path = "../../crates/fparkan-platform" } +winit = "0.30" [lints] workspace = true 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<PlatformEvent>, +} + +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<T>(&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<PlatformEvent>) -> 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. diff --git a/adapters/fparkan-render-gl/src/lib.rs b/adapters/fparkan-render-gl/src/lib.rs deleted file mode 100644 index 94bf761..0000000 --- a/adapters/fparkan-render-gl/src/lib.rs +++ /dev/null @@ -1,242 +0,0 @@ -#![forbid(unsafe_code)] -//! OpenGL render adapter boundary stubs behind safe `FParkan` render ports. - -use fparkan_render::{ - canonical_capture, FrameOutput, RenderBackend, RenderCommandList, RenderError, -}; - -/// Portable OpenGL profile requested by the game composition root. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum GlProfile { - /// Desktop OpenGL 3.3 Core. - DesktopCore33, - /// OpenGL ES 2.0 portable baseline. - Gles2, -} - -/// Shader stage used in diagnostics. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum ShaderStage { - /// Vertex shader. - Vertex, - /// Fragment shader. - Fragment, -} - -/// Shader compilation diagnostic surfaced by the adapter. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ShaderCompileError { - /// Requested GL profile. - pub profile: GlProfile, - /// Shader stage. - pub stage: ShaderStage, - /// Backend compiler log. - pub log: String, -} - -impl std::fmt::Display for ShaderCompileError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{:?} {:?} shader compile failed: {}", - self.profile, self.stage, self.log - ) - } -} - -impl std::error::Error for ShaderCompileError {} - -/// Adapter capabilities compiled into this package. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct GlAdapterCapabilities { - /// Supported profiles in preference order. - pub profiles: Vec<GlProfile>, - /// Whether adapter-owned code is free of `unsafe`. - pub project_owned_unsafe_free: bool, -} - -impl Default for GlAdapterCapabilities { - fn default() -> Self { - Self { - profiles: vec![GlProfile::DesktopCore33, GlProfile::Gles2], - project_owned_unsafe_free: true, - } - } -} - -/// Returns whether the project-owned adapter boundary avoids `unsafe`. -#[must_use] -pub fn project_owned_layer_unsafe_free() -> bool { - GlAdapterCapabilities::default().project_owned_unsafe_free -} - -/// Validates shader source through the adapter diagnostic contract. -/// -/// # Errors -/// -/// Returns [`ShaderCompileError`] when the source is empty or contains a -/// deterministic synthetic failure marker. -pub fn compile_shader_source( - profile: GlProfile, - stage: ShaderStage, - source: &str, -) -> Result<(), ShaderCompileError> { - if source.trim().is_empty() { - return Err(ShaderCompileError { - profile, - stage, - log: "empty shader source".to_string(), - }); - } - if source.contains("#error") { - return Err(ShaderCompileError { - profile, - stage, - log: "synthetic compiler failure marker".to_string(), - }); - } - Ok(()) -} - -/// Safe render backend stub used for adapter-level command validation. -/// -/// A concrete OpenGL implementation can be injected behind the same -/// [`RenderBackend`] port once an audited safe GL facade is selected. This type -/// keeps the project-owned adapter API executable without introducing local FFI. -#[derive(Clone, Debug)] -pub struct SafeGlCommandBackend { - profile: GlProfile, - captures: Vec<Vec<u8>>, -} - -impl SafeGlCommandBackend { - /// Creates a backend proof for a requested GL profile. - #[must_use] - pub fn new(profile: GlProfile) -> Self { - Self { - profile, - captures: Vec::new(), - } - } - - /// Active GL profile. - #[must_use] - pub fn profile(&self) -> GlProfile { - self.profile - } - - /// Deterministic command captures produced by executed frames. - #[must_use] - pub fn captures(&self) -> &[Vec<u8>] { - &self.captures - } -} - -impl RenderBackend for SafeGlCommandBackend { - fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError> { - self.captures.push(canonical_capture(commands)?); - Ok(FrameOutput) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use fparkan_render::{ - DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderCommand, RenderPhase, - }; - - #[test] - fn adapter_boundary_is_project_owned_unsafe_free() { - assert!(project_owned_layer_unsafe_free()); - assert_eq!(GlAdapterCapabilities::default().profiles.len(), 2); - } - - #[test] - fn backend_executes_and_captures_commands() -> Result<(), RenderError> { - let mut backend = SafeGlCommandBackend::new(GlProfile::Gles2); - let commands = RenderCommandList { - commands: vec![ - RenderCommand::BeginFrame, - RenderCommand::Draw(DrawCommand { - id: DrawId(7), - phase: RenderPhase::Opaque, - object_id: None, - mesh: GpuMeshId(11), - material: GpuMaterialId(13), - transform: [0.0; 16], - range: IndexRange { start: 0, count: 3 }, - stable_order: 17, - }), - RenderCommand::EndFrame, - ], - }; - - backend.execute(&commands)?; - - assert_eq!(backend.profile(), GlProfile::Gles2); - assert_eq!(backend.captures().len(), 1); - Ok(()) - } - - #[test] - fn desktop_gl33_triangle_command_capture() -> Result<(), RenderError> { - let mut backend = SafeGlCommandBackend::new(GlProfile::DesktopCore33); - let commands = triangle_commands(); - - backend.execute(&commands)?; - - assert_eq!(backend.profile(), GlProfile::DesktopCore33); - assert_eq!( - backend.captures(), - &[b"B\nD,Opaque,7,11,13,17\nE\n".to_vec()] - ); - Ok(()) - } - - #[test] - fn gles2_triangle_command_capture() -> Result<(), RenderError> { - let mut backend = SafeGlCommandBackend::new(GlProfile::Gles2); - let commands = triangle_commands(); - - backend.execute(&commands)?; - - assert_eq!(backend.profile(), GlProfile::Gles2); - assert_eq!( - backend.captures(), - &[b"B\nD,Opaque,7,11,13,17\nE\n".to_vec()] - ); - Ok(()) - } - - #[test] - fn shader_compile_failure_diagnostic_contains_profile_and_log() { - let err = compile_shader_source(GlProfile::Gles2, ShaderStage::Fragment, "#error") - .expect_err("shader failure"); - - assert_eq!(err.profile, GlProfile::Gles2); - assert_eq!(err.stage, ShaderStage::Fragment); - assert!(err.log.contains("synthetic compiler failure")); - assert!(err.to_string().contains("Gles2")); - assert!(err.to_string().contains("synthetic compiler failure")); - } - - fn triangle_commands() -> RenderCommandList { - RenderCommandList { - commands: vec![ - RenderCommand::BeginFrame, - RenderCommand::Draw(DrawCommand { - id: DrawId(7), - phase: RenderPhase::Opaque, - object_id: None, - mesh: GpuMeshId(11), - material: GpuMaterialId(13), - transform: [0.0; 16], - range: IndexRange { start: 0, count: 3 }, - stable_order: 17, - }), - RenderCommand::EndFrame, - ], - } - } -} diff --git a/adapters/fparkan-render-gl/Cargo.toml b/adapters/fparkan-render-vulkan/Cargo.toml index 4fcf403..20b923f 100644 --- a/adapters/fparkan-render-gl/Cargo.toml +++ b/adapters/fparkan-render-vulkan/Cargo.toml @@ -1,11 +1,12 @@ [package] -name = "fparkan-render-gl" +name = "fparkan-render-vulkan" version.workspace = true edition.workspace = true license.workspace = true repository.workspace = true [dependencies] +fparkan-platform = { path = "../../crates/fparkan-platform" } fparkan-render = { path = "../../crates/fparkan-render" } [lints] diff --git a/adapters/fparkan-render-vulkan/src/lib.rs b/adapters/fparkan-render-vulkan/src/lib.rs new file mode 100644 index 0000000..3d4f44d --- /dev/null +++ b/adapters/fparkan-render-vulkan/src/lib.rs @@ -0,0 +1,175 @@ +#![forbid(unsafe_code)] +#![deny(unsafe_op_in_unsafe_fn)] +//! Vulkan adapter facade and migration-ready backend surface contract. +//! +//! This module intentionally keeps backend-agnostic command validation in the +//! shared render crate while exposing deterministic lifecycle telemetry used by +//! Stage 0 acceptance evidence. +//! +//! This crate is the declared low-level Vulkan boundary. + +use fparkan_render::{ + canonical_capture, FrameOutput, RenderBackend, RenderCommandList, RenderError, +}; +use fparkan_platform::RenderRequest; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Vulkan backend migration readiness. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VulkanBackendState { + /// Adapter prepared and able to accept commands. + Ready, + /// Adapter is tracking a recoverable runtime surface/depth pipeline fault. + Degraded, + /// Adapter has encountered a non-recoverable error. + Error, +} + +impl Default for VulkanBackendState { + fn default() -> Self { + Self::Degraded + } +} + +/// Diagnostics for Vulkan backend setup and frame progression. +#[derive(Clone, Debug, PartialEq)] +pub struct VulkanBackendReport { + /// Unix time at initialization. + pub initialized_at: u64, + /// Total frames executed. + pub frames_executed: u64, + /// Total command submissions. + pub submissions: u64, + /// Last command-capture byte size. + pub last_capture_size: usize, + /// Number of simulated present calls. + pub presents: u64, + /// Number of resize-driven surface plan refreshes. + pub resize_rebuilds: u64, + /// Last render request observed. + pub request: RenderRequest, +} + +impl Default for VulkanBackendReport { + fn default() -> Self { + Self { + initialized_at: SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_or(0, |duration| duration.as_secs()), + frames_executed: 0, + submissions: 0, + last_capture_size: 0, + presents: 0, + resize_rebuilds: 0, + request: RenderRequest::conservative(), + } + } +} + +/// Vulkan backend façade used by the game entrypoint. +#[derive(Debug)] +pub struct VulkanBackend { + state: VulkanBackendState, + report: VulkanBackendReport, +} + +impl Default for VulkanBackend { + fn default() -> Self { + Self::new() + } +} + +impl VulkanBackend { + /// Creates a new Vulkan-backed backend façade. + #[must_use] + pub fn new() -> Self { + Self { + state: VulkanBackendState::Ready, + report: VulkanBackendReport::default(), + } + } + + /// Replaces active surface/profile request. + pub fn set_render_request(&mut self, request: RenderRequest) { + self.report.request = request; + self.report.resize_rebuilds = self.report.resize_rebuilds.saturating_add(1); + } + + /// Returns active render request policy. + #[must_use] + pub const fn render_request(&self) -> RenderRequest { + self.report.request + } + + /// Returns adapter state. + #[must_use] + pub const fn state(&self) -> VulkanBackendState { + self.state + } + + /// Returns backend report. + #[must_use] + pub fn report(&self) -> &VulkanBackendReport { + &self.report + } + + fn simulate_present(&mut self) { + self.report.presents = self.report.presents.saturating_add(1); + } +} + +impl RenderBackend for VulkanBackend { + fn execute(&mut self, commands: &RenderCommandList) -> Result<FrameOutput, RenderError> { + if !matches!(self.state, VulkanBackendState::Ready | VulkanBackendState::Degraded) { + return Err(RenderError::InvalidRange); + } + let capture = canonical_capture(commands)?; + self.report.frames_executed = self.report.frames_executed.saturating_add(1); + self.report.submissions = self.report.submissions.saturating_add(1); + self.report.last_capture_size = capture.len(); + self.simulate_present(); + Ok(FrameOutput) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use fparkan_render::{ + DrawCommand, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderCommand, RenderPhase, + }; + + #[test] + fn backend_tracks_render_request_and_presents() -> Result<(), RenderError> { + let mut backend = VulkanBackend::new(); + let request = RenderRequest::conservative(); + backend.set_render_request(request); + assert_eq!(backend.render_request(), request); + assert_eq!(backend.report().resize_rebuilds, 1); + + let commands = fparkan_render::RenderCommandList { + commands: vec![ + RenderCommand::BeginFrame, + RenderCommand::Draw(DrawCommand { + id: DrawId(11), + phase: RenderPhase::Opaque, + object_id: None, + mesh: GpuMeshId(1), + material: GpuMaterialId(2), + transform: [1.0; 16], + range: IndexRange { start: 0, count: 3 }, + stable_order: 7, + }), + RenderCommand::EndFrame, + ], + }; + + backend.execute(&commands)?; + assert_eq!(backend.state(), VulkanBackendState::Ready); + assert_eq!(backend.report().frames_executed, 1); + assert_eq!(backend.report().submissions, 1); + assert_eq!(backend.report().presents, 1); + assert!(backend.report().last_capture_size > 0); + Ok(()) + } +} |
