diff options
| author | Valentin Popov <valentin@popov.link> | 2026-06-22 12:12:27 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-06-22 12:13:32 +0300 |
| commit | d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448 (patch) | |
| tree | a0bd35c3940be62a5b5de1acc2366af377ffd181 /adapters | |
| parent | 7416fdc7e9a48837fff5056e6dc8d0774e90964b (diff) | |
| download | fparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.tar.xz fparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.zip | |
feat: implement FParkan architecture foundation
Add the modular fparkan workspace, domain crates, adapters, apps, xtask policy/CI, acceptance evidence, and licensed corpus gates for the macOS-focused roadmap foundation.
Diffstat (limited to 'adapters')
| -rw-r--r-- | adapters/fparkan-platform-sdl/Cargo.toml | 12 | ||||
| -rw-r--r-- | adapters/fparkan-platform-sdl/src/lib.rs | 123 | ||||
| -rw-r--r-- | adapters/fparkan-render-gl/Cargo.toml | 12 | ||||
| -rw-r--r-- | adapters/fparkan-render-gl/src/lib.rs | 242 |
4 files changed, 389 insertions, 0 deletions
diff --git a/adapters/fparkan-platform-sdl/Cargo.toml b/adapters/fparkan-platform-sdl/Cargo.toml new file mode 100644 index 0000000..fd9b040 --- /dev/null +++ b/adapters/fparkan-platform-sdl/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "fparkan-platform-sdl" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +fparkan-platform = { path = "../../crates/fparkan-platform" } + +[lints] +workspace = true diff --git a/adapters/fparkan-platform-sdl/src/lib.rs b/adapters/fparkan-platform-sdl/src/lib.rs new file mode 100644 index 0000000..73aea1f --- /dev/null +++ b/adapters/fparkan-platform-sdl/src/lib.rs @@ -0,0 +1,123 @@ +#![forbid(unsafe_code)] +//! SDL platform adapter proof 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 adapter readiness status for the safe project-owned layer. +#[must_use] +pub fn safe_adapter_ready() -> bool { + SdlAdapterCapabilities::default().project_owned_unsafe_free +} + +/// In-memory event source used by adapter smoke tests and composition roots +/// before a concrete SDL runtime is injected. +#[derive(Clone, Debug, Default)] +pub struct SdlEventSourceProof { + pending: Vec<PlatformEvent>, +} + +impl SdlEventSourceProof { + /// Creates an event source with deterministic pending events. + #[must_use] + pub fn new(pending: Vec<PlatformEvent>) -> Self { + Self { pending } + } +} + +impl EventSource for SdlEventSourceProof { + fn poll(&mut self, out: &mut Vec<PlatformEvent>) -> Result<(), PlatformError> { + out.append(&mut self.pending); + Ok(()) + } +} + +/// Safe window-port proof with SDL-compatible drawable-size semantics. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SdlWindowProof { + size: PhysicalSize, + presents: u64, +} + +impl SdlWindowProof { + /// Creates a proof 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 SdlWindowProof { + 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_reports_safe_project_layer_ready() { + assert!(safe_adapter_ready()); + assert_eq!(SdlAdapterCapabilities::default().graphics.len(), 2); + } + + #[test] + fn event_source_and_window_ports_are_deterministic() -> Result<(), PlatformError> { + let mut source = SdlEventSourceProof::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 = SdlWindowProof::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-render-gl/Cargo.toml b/adapters/fparkan-render-gl/Cargo.toml new file mode 100644 index 0000000..4fcf403 --- /dev/null +++ b/adapters/fparkan-render-gl/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "fparkan-render-gl" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +fparkan-render = { path = "../../crates/fparkan-render" } + +[lints] +workspace = true diff --git a/adapters/fparkan-render-gl/src/lib.rs b/adapters/fparkan-render-gl/src/lib.rs new file mode 100644 index 0000000..094b1ad --- /dev/null +++ b/adapters/fparkan-render-gl/src/lib.rs @@ -0,0 +1,242 @@ +#![forbid(unsafe_code)] +//! OpenGL render adapter proof 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 adapter readiness status for the safe project-owned layer. +#[must_use] +pub fn safe_adapter_ready() -> 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 facade 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_reports_safe_project_layer_ready() { + assert!(safe_adapter_ready()); + 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, + ], + } + } +} |
