1use super::{perspective::PerspectiveCorrection, LedPanel, LedWall};
11use crate::math::{Matrix4, Point3, Vector3};
12use crate::{tracking::CameraPose, Result, VirtualProductionError};
13use rayon::prelude::*;
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct LedRendererConfig {
19 pub target_fps: f64,
21 pub perspective_correction: bool,
23 pub color_correction: bool,
25 pub quality: f32,
27 pub motion_blur: bool,
29}
30
31impl Default for LedRendererConfig {
32 fn default() -> Self {
33 Self {
34 target_fps: 60.0,
35 perspective_correction: true,
36 color_correction: true,
37 quality: 1.0,
38 motion_blur: false,
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
45pub struct RenderOutput {
46 pub pixels: Vec<u8>,
48 pub width: usize,
50 pub height: usize,
52 pub frame_number: u64,
54 pub timestamp_ns: u64,
56}
57
58impl RenderOutput {
59 #[must_use]
61 pub fn new(width: usize, height: usize, frame_number: u64, timestamp_ns: u64) -> Self {
62 Self {
63 pixels: vec![0; width * height * 3],
64 width,
65 height,
66 frame_number,
67 timestamp_ns,
68 }
69 }
70
71 #[must_use]
73 pub fn get_pixel(&self, x: usize, y: usize) -> Option<[u8; 3]> {
74 if x >= self.width || y >= self.height {
75 return None;
76 }
77
78 let idx = (y * self.width + x) * 3;
79 Some([self.pixels[idx], self.pixels[idx + 1], self.pixels[idx + 2]])
80 }
81
82 pub fn set_pixel(&mut self, x: usize, y: usize, rgb: [u8; 3]) {
84 if x >= self.width || y >= self.height {
85 return;
86 }
87
88 let idx = (y * self.width + x) * 3;
89 self.pixels[idx] = rgb[0];
90 self.pixels[idx + 1] = rgb[1];
91 self.pixels[idx + 2] = rgb[2];
92 }
93}
94
95struct PanelBuffer {
98 pixels: Vec<u8>,
99 width: usize,
100 height: usize,
101 x_offset: usize,
103}
104
105pub struct LedRenderer {
107 config: LedRendererConfig,
108 led_wall: Option<LedWall>,
109 perspective: PerspectiveCorrection,
110 frame_number: u64,
111}
112
113impl LedRenderer {
114 pub fn new(config: LedRendererConfig) -> Result<Self> {
116 let perspective = PerspectiveCorrection::new()?;
117
118 Ok(Self {
119 config,
120 led_wall: None,
121 perspective,
122 frame_number: 0,
123 })
124 }
125
126 pub fn set_led_wall(&mut self, wall: LedWall) {
128 self.led_wall = Some(wall);
129 }
130
131 pub fn render(
136 &mut self,
137 camera_pose: &CameraPose,
138 source_frame: &[u8],
139 source_width: usize,
140 source_height: usize,
141 timestamp_ns: u64,
142 ) -> Result<RenderOutput> {
143 let led_wall = self
144 .led_wall
145 .as_ref()
146 .ok_or_else(|| VirtualProductionError::LedWall("No LED wall configured".to_string()))?;
147
148 let (output_width, output_height) = led_wall.total_resolution();
149
150 let panels: Vec<LedPanel> = led_wall.panels.clone();
152
153 let perspective_enabled = self.config.perspective_correction;
157 let perspective_config = self.perspective.config().clone();
158
159 let x_offsets: Vec<usize> = panels
162 .iter()
163 .scan(0usize, |acc, p| {
164 let off = *acc;
165 *acc += p.resolution.0;
166 Some(off)
167 })
168 .collect();
169
170 let panel_buffers: Result<Vec<PanelBuffer>> = panels
172 .par_iter()
173 .zip(x_offsets.par_iter())
174 .map(|(panel, &x_offset)| {
175 render_panel_pure(
176 panel,
177 camera_pose,
178 source_frame,
179 source_width,
180 source_height,
181 perspective_enabled,
182 &perspective_config,
183 )
184 .map(|(pixels, w, h)| PanelBuffer {
185 pixels,
186 width: w,
187 height: h,
188 x_offset,
189 })
190 })
191 .collect();
192
193 let panel_buffers = panel_buffers?;
194
195 let mut output =
197 RenderOutput::new(output_width, output_height, self.frame_number, timestamp_ns);
198
199 for pb in &panel_buffers {
200 for y in 0..pb.height {
201 for x in 0..pb.width {
202 let src = (y * pb.width + x) * 3;
203 if src + 2 < pb.pixels.len() {
204 let dst_x = pb.x_offset + x;
205 output.set_pixel(
206 dst_x,
207 y,
208 [pb.pixels[src], pb.pixels[src + 1], pb.pixels[src + 2]],
209 );
210 }
211 }
212 }
213 }
214
215 self.frame_number += 1;
216
217 Ok(output)
218 }
219
220 #[must_use]
222 pub fn frame_number(&self) -> u64 {
223 self.frame_number
224 }
225
226 pub fn reset_frame_counter(&mut self) {
228 self.frame_number = 0;
229 }
230
231 #[must_use]
233 pub fn config(&self) -> &LedRendererConfig {
234 &self.config
235 }
236
237 #[must_use]
239 pub fn led_wall(&self) -> Option<&LedWall> {
240 self.led_wall.as_ref()
241 }
242}
243
244fn render_panel_pure(
248 panel: &LedPanel,
249 camera_pose: &CameraPose,
250 source_frame: &[u8],
251 source_width: usize,
252 source_height: usize,
253 perspective_enabled: bool,
254 perspective_config: &super::perspective::PerspectiveCorrectionConfig,
255) -> Result<(Vec<u8>, usize, usize)> {
256 use super::perspective::PerspectiveCorrection;
257
258 let (panel_width, panel_height) = panel.resolution;
259
260 let transform = if perspective_enabled {
263 let pc = PerspectiveCorrection::with_config(perspective_config.clone())?;
264 pc.compute_transform(camera_pose, panel)?
265 } else {
266 Matrix4::identity()
267 };
268
269 let mut pixels = vec![0u8; panel_width * panel_height * 3];
270
271 for y in 0..panel_height {
272 for x in 0..panel_width {
273 let pixel_x = (x as f64 / panel_width as f64) * panel.width;
275 let pixel_y = (y as f64 / panel_height as f64) * panel.height;
276
277 let world_pos = panel.position + Vector3::new(pixel_x, pixel_y, 0.0);
278
279 let transformed = transform * world_pos.to_homogeneous();
281 let screen_pos = Point3::from_homogeneous(transformed).unwrap_or(world_pos);
282
283 let src_x = ((screen_pos.x / panel.width) * source_width as f64) as usize;
285 let src_y = ((screen_pos.y / panel.height) * source_height as f64) as usize;
286
287 if src_x < source_width && src_y < source_height {
288 let src_idx = (src_y * source_width + src_x) * 3;
289 if src_idx + 2 < source_frame.len() {
290 let dst = (y * panel_width + x) * 3;
291 pixels[dst] = source_frame[src_idx];
292 pixels[dst + 1] = source_frame[src_idx + 1];
293 pixels[dst + 2] = source_frame[src_idx + 2];
294 }
295 }
296 }
297 }
298
299 Ok((pixels, panel_width, panel_height))
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_render_output() {
308 let output = RenderOutput::new(1920, 1080, 0, 0);
309 assert_eq!(output.width, 1920);
310 assert_eq!(output.height, 1080);
311 assert_eq!(output.pixels.len(), 1920 * 1080 * 3);
312 }
313
314 #[test]
315 fn test_render_output_pixel() {
316 let mut output = RenderOutput::new(100, 100, 0, 0);
317 output.set_pixel(50, 50, [255, 128, 64]);
318
319 let pixel = output.get_pixel(50, 50);
320 assert_eq!(pixel, Some([255, 128, 64]));
321 }
322
323 #[test]
324 fn test_led_renderer_creation() {
325 let config = LedRendererConfig::default();
326 let renderer = LedRenderer::new(config);
327 assert!(renderer.is_ok());
328 }
329
330 #[test]
331 fn test_led_renderer_frame_counter() {
332 let config = LedRendererConfig::default();
333 let mut renderer = LedRenderer::new(config).expect("should succeed in test");
334
335 assert_eq!(renderer.frame_number(), 0);
336 renderer.reset_frame_counter();
337 assert_eq!(renderer.frame_number(), 0);
338 }
339
340 #[test]
341 fn test_led_renderer_set_wall() {
342 let config = LedRendererConfig::default();
343 let mut renderer = LedRenderer::new(config).expect("should succeed in test");
344
345 let wall = LedWall::new("Test Wall".to_string());
346 renderer.set_led_wall(wall);
347
348 assert!(renderer.led_wall().is_some());
349 }
350
351 #[test]
352 fn test_render_parallel_vs_serial_determinism() {
353 use crate::led::{LedPanel, LedWall};
354 use crate::math::Point3;
355 use crate::tracking::CameraPose;
356
357 let mut config = LedRendererConfig::default();
358 config.perspective_correction = false; let mut renderer1 = LedRenderer::new(config.clone()).expect("ok");
361 let mut renderer2 = LedRenderer::new(config).expect("ok");
362
363 let mut wall1 = LedWall::new("W".to_string());
364 let mut wall2 = LedWall::new("W".to_string());
365
366 for i in 0..3 {
367 let panel = LedPanel::new(
368 Point3::new(i as f64 * 2.0, 0.0, 0.0),
369 2.0,
370 1.5,
371 (16, 12),
372 2.5,
373 );
374 wall1.add_panel(panel.clone());
375 wall2.add_panel(panel);
376 }
377
378 renderer1.set_led_wall(wall1);
379 renderer2.set_led_wall(wall2);
380
381 let pose = CameraPose::default();
382 let source = vec![128u8; 64 * 48 * 3];
383
384 let out1 = renderer1.render(&pose, &source, 64, 48, 0).expect("ok");
385 let out2 = renderer2.render(&pose, &source, 64, 48, 0).expect("ok");
386
387 assert_eq!(
388 out1.pixels, out2.pixels,
389 "parallel and serial must produce identical output"
390 );
391 }
392}