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