#[cfg(feature = "gpu")]
use crate::backend::wgpu::{Viewport, WgpuBackend};
#[cfg(feature = "gpu")]
use crate::error::RenderResult;
#[cfg(feature = "gpu")]
use crate::quilt::QuiltView;
use crate::quilt::{Quilt, QuiltRenderSettings, QuiltRenderTarget};
use crate::spatial::{Camera, HolographicConfig};
use canvas_core::Scene;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct HolographicRenderResult {
pub target: QuiltRenderTarget,
pub view_count: u32,
pub render_time_ms: f64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HolographicStats {
pub frames_rendered: u64,
pub avg_render_time_ms: f64,
pub peak_render_time_ms: f64,
pub total_views_rendered: u64,
}
impl HolographicStats {
pub fn update(&mut self, result: &HolographicRenderResult) {
self.frames_rendered += 1;
self.total_views_rendered += u64::from(result.view_count);
let alpha = 0.1;
self.avg_render_time_ms =
alpha * result.render_time_ms + (1.0 - alpha) * self.avg_render_time_ms;
if result.render_time_ms > self.peak_render_time_ms {
self.peak_render_time_ms = result.render_time_ms;
}
}
pub fn reset(&mut self) {
*self = Self::default();
}
}
#[derive(Debug)]
pub struct HolographicRenderer {
config: HolographicConfig,
settings: QuiltRenderSettings,
stats: HolographicStats,
}
impl HolographicRenderer {
#[must_use]
pub fn new(config: HolographicConfig) -> Self {
Self {
config,
settings: QuiltRenderSettings::default(),
stats: HolographicStats::default(),
}
}
#[must_use]
pub fn with_settings(config: HolographicConfig, settings: QuiltRenderSettings) -> Self {
Self {
config,
settings,
stats: HolographicStats::default(),
}
}
#[must_use]
pub const fn config(&self) -> &HolographicConfig {
&self.config
}
#[must_use]
pub const fn settings(&self) -> &QuiltRenderSettings {
&self.settings
}
#[must_use]
pub const fn stats(&self) -> &HolographicStats {
&self.stats
}
pub fn set_config(&mut self, config: HolographicConfig) {
self.config = config;
}
pub fn set_settings(&mut self, settings: QuiltRenderSettings) {
self.settings = settings;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn render_quilt(&mut self, scene: &Scene, camera: &Camera) -> HolographicRenderResult {
let start = std::time::Instant::now();
let quilt = Quilt::new(self.config.clone(), camera);
let mut target = QuiltRenderTarget::from_quilt(&quilt);
let clear_color = Self::float_color_to_bytes(&self.settings.clear_color);
target.clear(clear_color);
for view in &quilt.views {
self.render_view_placeholder(&mut target, view, scene);
}
let elapsed = start.elapsed();
let render_time_ms = elapsed.as_secs_f64() * 1000.0;
let view_count = self.config.num_views;
let result = HolographicRenderResult {
target,
view_count,
render_time_ms,
};
self.stats.update(&result);
result
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn float_color_to_bytes(color: &[f32; 4]) -> [u8; 4] {
[
(color[0].clamp(0.0, 1.0) * 255.0) as u8,
(color[1].clamp(0.0, 1.0) * 255.0) as u8,
(color[2].clamp(0.0, 1.0) * 255.0) as u8,
(color[3].clamp(0.0, 1.0) * 255.0) as u8,
]
}
#[cfg(feature = "gpu")]
pub fn render_view(
&self,
backend: &mut WgpuBackend,
scene: &Scene,
view: &QuiltView,
) -> RenderResult<()> {
let viewport = Viewport::new(view.x_offset, view.y_offset, view.width, view.height);
backend.render_with_camera(scene, Some(&view.camera), Some(viewport))
}
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
fn render_view_placeholder(
&self,
target: &mut QuiltRenderTarget,
view: &crate::quilt::QuiltView,
_scene: &Scene,
) {
let progress = view.index as f32 / (self.config.num_views - 1).max(1) as f32;
let red = (progress * 255.0) as u8;
let green = 100_u8;
let blue = ((1.0 - progress) * 255.0) as u8;
target.fill_rect(
view.x_offset,
view.y_offset,
view.width,
view.height,
[red, green, blue, 255],
);
Self::draw_border(
target,
view.x_offset,
view.y_offset,
view.width,
view.height,
);
}
fn draw_border(target: &mut QuiltRenderTarget, x: u32, y: u32, width: u32, height: u32) {
const BORDER_COLOR: [u8; 4] = [255, 255, 255, 128];
const BORDER_WIDTH: u32 = 2;
let borders = [
(x, y, width, BORDER_WIDTH), (x, y + height - BORDER_WIDTH, width, BORDER_WIDTH), (x, y, BORDER_WIDTH, height), (x + width - BORDER_WIDTH, y, BORDER_WIDTH, height), ];
for (bx, by, bw, bh) in borders {
target.fill_rect(bx, by, bw, bh, BORDER_COLOR);
}
}
#[must_use]
pub fn quilt_dimensions(&self) -> (u32, u32) {
(self.config.quilt_width(), self.config.quilt_height())
}
pub fn reset_stats(&mut self) {
self.stats.reset();
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HoloPlayInfo {
pub display_connected: bool,
pub display_name: Option<String>,
pub display_resolution: Option<(u32, u32)>,
pub recommended_views: Option<u32>,
pub view_cone_degrees: Option<f32>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_holographic_renderer_creation() {
let config = HolographicConfig::looking_glass_portrait();
let renderer = HolographicRenderer::new(config);
assert_eq!(renderer.config().num_views, 45);
}
#[test]
fn test_holographic_renderer_with_settings() {
let config = HolographicConfig::looking_glass_portrait();
let settings = QuiltRenderSettings {
clear_color: [0.0, 0.0, 0.0, 1.0],
wireframe: true,
depth_test: false,
backface_cull: false,
};
let renderer = HolographicRenderer::with_settings(config, settings);
assert!(renderer.settings().wireframe);
assert!(!renderer.settings().depth_test);
}
#[test]
fn test_holographic_render_quilt() {
let config = HolographicConfig {
num_views: 4,
quilt_columns: 2,
quilt_rows: 2,
view_width: 100,
view_height: 100,
view_cone: 40.0_f32.to_radians(),
focal_distance: 2.0,
};
let mut renderer = HolographicRenderer::new(config);
let scene = Scene::new(800.0, 600.0);
let camera = Camera::default();
let result = renderer.render_quilt(&scene, &camera);
assert_eq!(result.view_count, 4);
assert_eq!(result.target.width, 200); assert_eq!(result.target.height, 200); }
#[test]
fn test_holographic_stats_update() {
let mut stats = HolographicStats::default();
let result = HolographicRenderResult {
target: QuiltRenderTarget::new(100, 100),
view_count: 45,
render_time_ms: 16.6,
};
stats.update(&result);
assert_eq!(stats.frames_rendered, 1);
assert_eq!(stats.total_views_rendered, 45);
assert!(stats.avg_render_time_ms > 0.0);
}
#[test]
fn test_holographic_stats_peak() {
let mut stats = HolographicStats::default();
stats.update(&HolographicRenderResult {
target: QuiltRenderTarget::new(10, 10),
view_count: 4,
render_time_ms: 10.0,
});
stats.update(&HolographicRenderResult {
target: QuiltRenderTarget::new(10, 10),
view_count: 4,
render_time_ms: 20.0,
});
assert!((stats.peak_render_time_ms - 20.0).abs() < f64::EPSILON);
}
#[test]
fn test_holographic_quilt_dimensions() {
let config = HolographicConfig::looking_glass_portrait();
let renderer = HolographicRenderer::new(config);
let (width, height) = renderer.quilt_dimensions();
assert_eq!(width, 2100);
assert_eq!(height, 5040);
}
#[test]
fn test_holographic_set_config() {
let config1 = HolographicConfig::looking_glass_portrait();
let config2 = HolographicConfig::looking_glass_4k();
let mut renderer = HolographicRenderer::new(config1);
assert_eq!(renderer.config().view_width, 420);
renderer.set_config(config2);
assert_eq!(renderer.config().view_width, 819);
}
#[test]
fn test_holoplay_info_default() {
let info = HoloPlayInfo::default();
assert!(!info.display_connected);
assert!(info.display_name.is_none());
}
#[test]
fn test_holographic_stats_reset() {
let mut stats = HolographicStats {
frames_rendered: 100,
avg_render_time_ms: 16.6,
peak_render_time_ms: 33.3,
total_views_rendered: 4500,
};
stats.reset();
assert_eq!(stats.frames_rendered, 0);
assert!(stats.avg_render_time_ms.abs() < f64::EPSILON);
}
#[test]
fn test_render_produces_colored_views() {
let config = HolographicConfig {
num_views: 2,
quilt_columns: 2,
quilt_rows: 1,
view_width: 10,
view_height: 10,
view_cone: 40.0_f32.to_radians(),
focal_distance: 2.0,
};
let mut renderer = HolographicRenderer::new(config);
let scene = Scene::new(100.0, 100.0);
let camera = Camera::default();
let result = renderer.render_quilt(&scene, &camera);
let left_pixel = result.target.get_pixel(5, 5).expect("should get pixel");
let right_pixel = result.target.get_pixel(15, 5).expect("should get pixel");
assert!(left_pixel[2] > left_pixel[0]);
assert!(right_pixel[0] > right_pixel[2]);
}
}