Skip to main content

canvas_renderer/
lib.rs

1//! # Saorsa Canvas Renderer
2//!
3//! Custom minimal renderer built on wgpu for maximum control and smallest footprint.
4//!
5//! ## Rendering Backends
6//!
7//! ```text
8//! ┌─────────────────────────────────────────────┐
9//! │            Renderer Trait                   │
10//! ├─────────────┬─────────────┬─────────────────┤
11//! │ WebGPU/wgpu │ WebGL2      │ 2D Fallback     │
12//! │ (GPU)       │ (older GPU) │ (no GPU)        │
13//! └─────────────┴─────────────┴─────────────────┘
14//! ```
15
16#![forbid(unsafe_code)]
17#![deny(missing_docs)]
18#![deny(clippy::all)]
19#![deny(clippy::pedantic)]
20#![allow(clippy::module_name_repetitions)]
21
22pub mod backend;
23#[cfg(feature = "charts")]
24pub mod chart;
25pub mod error;
26#[cfg(feature = "export")]
27pub mod export;
28pub mod holographic;
29#[cfg(feature = "images")]
30pub mod image;
31pub mod quilt;
32pub mod spatial;
33#[cfg(feature = "images")]
34pub mod texture_cache;
35#[cfg(feature = "gpu")]
36pub mod video;
37
38pub use backend::RenderBackend;
39pub use error::{RenderError, RenderResult};
40#[cfg(feature = "export")]
41pub use export::{ExportConfig, ExportFormat, SceneExporter};
42pub use holographic::{
43    HoloPlayInfo, HolographicRenderResult, HolographicRenderer, HolographicStats,
44};
45pub use quilt::{LookingGlassPreset, Quilt, QuiltRenderSettings, QuiltRenderTarget, QuiltView};
46pub use spatial::{Camera, HolographicConfig, Mat4, QuiltRenderInfo, Vec3};
47#[cfg(feature = "gpu")]
48pub use video::{
49    VideoFrameData, VideoTextureEntry, VideoTextureError, VideoTextureManager, VideoTextureResult,
50};
51
52use canvas_core::Scene;
53
54/// Configuration for the renderer.
55#[derive(Debug, Clone)]
56pub struct RendererConfig {
57    /// Preferred backend (will fall back if unavailable).
58    pub preferred_backend: BackendType,
59    /// Target frames per second.
60    pub target_fps: u32,
61    /// Enable anti-aliasing.
62    pub anti_aliasing: bool,
63    /// Background color (RGBA).
64    pub background_color: [f32; 4],
65}
66
67impl Default for RendererConfig {
68    fn default() -> Self {
69        Self {
70            preferred_backend: BackendType::WebGpu,
71            target_fps: 60,
72            anti_aliasing: true,
73            background_color: [1.0, 1.0, 1.0, 1.0], // White
74        }
75    }
76}
77
78/// Available rendering backends.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum BackendType {
81    /// WebGPU via wgpu (best quality, requires modern GPU).
82    WebGpu,
83    /// WebGL2 fallback (older GPU support).
84    WebGl2,
85    /// Pure 2D canvas fallback (no GPU required).
86    Canvas2D,
87}
88
89/// The main renderer interface.
90pub struct Renderer {
91    config: RendererConfig,
92    backend: Box<dyn RenderBackend>,
93    frame_count: u64,
94}
95
96impl Renderer {
97    /// Create a new renderer with the given configuration.
98    ///
99    /// # Errors
100    ///
101    /// Returns an error if no suitable backend is available.
102    pub fn new(config: RendererConfig) -> RenderResult<Self> {
103        let backend = Self::create_backend(&config)?;
104        Ok(Self::with_backend(backend, config))
105    }
106
107    /// Create a renderer from an explicit backend implementation.
108    #[must_use]
109    pub fn with_backend(backend: Box<dyn RenderBackend>, config: RendererConfig) -> Self {
110        Self {
111            config,
112            backend,
113            frame_count: 0,
114        }
115    }
116
117    /// Create the appropriate backend based on config and availability.
118    fn create_backend(config: &RendererConfig) -> RenderResult<Box<dyn RenderBackend>> {
119        match config.preferred_backend {
120            BackendType::WebGpu => {
121                #[cfg(feature = "gpu")]
122                {
123                    match backend::wgpu::WgpuBackend::new() {
124                        Ok(b) => return Ok(Box::new(b)),
125                        Err(e) => {
126                            tracing::warn!("WebGPU unavailable, falling back: {}", e);
127                        }
128                    }
129                }
130                // Fall through to next backend
131                Self::create_backend(&RendererConfig {
132                    preferred_backend: BackendType::WebGl2,
133                    ..config.clone()
134                })
135            }
136            BackendType::WebGl2 => {
137                // TODO: Implement WebGL2 backend
138                tracing::warn!("WebGL2 not yet implemented, falling back to 2D");
139                Self::create_backend(&RendererConfig {
140                    preferred_backend: BackendType::Canvas2D,
141                    ..config.clone()
142                })
143            }
144            BackendType::Canvas2D => Ok(Box::new(backend::canvas2d::Canvas2DBackend::new())),
145        }
146    }
147
148    /// Render a frame.
149    ///
150    /// # Errors
151    ///
152    /// Returns an error if rendering fails.
153    pub fn render(&mut self, scene: &Scene) -> RenderResult<()> {
154        self.backend.render(scene)?;
155        self.frame_count += 1;
156        Ok(())
157    }
158
159    /// Get the current frame count.
160    #[must_use]
161    pub fn frame_count(&self) -> u64 {
162        self.frame_count
163    }
164
165    /// Get the active backend type.
166    #[must_use]
167    pub fn active_backend(&self) -> BackendType {
168        self.backend.backend_type()
169    }
170
171    /// Get the renderer configuration.
172    #[must_use]
173    pub fn config(&self) -> &RendererConfig {
174        &self.config
175    }
176
177    /// Resize the rendering surface.
178    ///
179    /// # Errors
180    ///
181    /// Returns an error if resize fails.
182    pub fn resize(&mut self, width: u32, height: u32) -> RenderResult<()> {
183        self.backend.resize(width, height)
184    }
185}