aboutsummaryrefslogtreecommitdiff
path: root/adapters
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-22 12:12:27 +0300
committerValentin Popov <valentin@popov.link>2026-06-22 12:13:32 +0300
commitd0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448 (patch)
treea0bd35c3940be62a5b5de1acc2366af377ffd181 /adapters
parent7416fdc7e9a48837fff5056e6dc8d0774e90964b (diff)
downloadfparkan-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.toml12
-rw-r--r--adapters/fparkan-platform-sdl/src/lib.rs123
-rw-r--r--adapters/fparkan-render-gl/Cargo.toml12
-rw-r--r--adapters/fparkan-render-gl/src/lib.rs242
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,
+ ],
+ }
+ }
+}