diff options
| author | Valentin Popov <valentin@popov.link> | 2026-06-23 21:05:16 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-06-23 21:05:16 +0300 |
| commit | f8e447ffee746cfe6580cc0e78a8a225aa39b546 (patch) | |
| tree | e37ebc6c5edd908fd9f44cd3aaf7bffed8de8a88 /adapters | |
| parent | 83d763dd70ef20b7d30a905c15cad3d5531ebc6a (diff) | |
| download | fparkan-f8e447ffee746cfe6580cc0e78a8a225aa39b546.tar.xz fparkan-f8e447ffee746cfe6580cc0e78a8a225aa39b546.zip | |
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.
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(()) + } +} |
