diff options
Diffstat (limited to 'adapters/fparkan-render-gl/src/lib.rs')
| -rw-r--r-- | adapters/fparkan-render-gl/src/lib.rs | 242 |
1 files changed, 242 insertions, 0 deletions
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, + ], + } + } +} |
