1use super::backend::{
5 BackendCapabilities, BackendContext, BufferHandle, BufferUsage, GpuBackend, PipelineLayout,
6 ShaderStage, SoftwareContext, TextureFormat, TextureHandle,
7};
8use super::renderer::{DrawCall, MultiBackendRenderer, RenderPass};
9use glam::{Mat4, Vec3};
10
11#[derive(Debug, Clone)]
17pub struct SceneDesc {
18 pub clear_color: [f32; 4],
19 pub objects: Vec<ObjectDesc>,
20}
21
22impl SceneDesc {
23 pub fn new() -> Self {
24 Self {
25 clear_color: [0.0, 0.0, 0.0, 1.0],
26 objects: Vec::new(),
27 }
28 }
29
30 pub fn with_clear_color(mut self, r: f32, g: f32, b: f32, a: f32) -> Self {
31 self.clear_color = [r, g, b, a];
32 self
33 }
34
35 pub fn with_object(mut self, obj: ObjectDesc) -> Self {
36 self.objects.push(obj);
37 self
38 }
39}
40
41impl Default for SceneDesc {
42 fn default() -> Self { Self::new() }
43}
44
45#[derive(Debug, Clone)]
47pub struct ObjectDesc {
48 pub vertex_data: Vec<u8>,
49 pub vertex_count: u32,
50 pub color: [f32; 4],
51}
52
53impl ObjectDesc {
54 pub fn new(vertex_data: Vec<u8>, vertex_count: u32) -> Self {
55 Self {
56 vertex_data,
57 vertex_count,
58 color: [1.0, 1.0, 1.0, 1.0],
59 }
60 }
61
62 pub fn with_color(mut self, r: f32, g: f32, b: f32, a: f32) -> Self {
63 self.color = [r, g, b, a];
64 self
65 }
66}
67
68#[derive(Debug, Clone)]
70pub struct CameraDesc {
71 pub eye: Vec3,
72 pub target: Vec3,
73 pub up: Vec3,
74 pub fov_y: f32,
75 pub near: f32,
76 pub far: f32,
77}
78
79impl CameraDesc {
80 pub fn new(eye: Vec3, target: Vec3) -> Self {
81 Self {
82 eye,
83 target,
84 up: Vec3::Y,
85 fov_y: 60.0_f32.to_radians(),
86 near: 0.1,
87 far: 1000.0,
88 }
89 }
90
91 pub fn view_matrix(&self) -> Mat4 {
92 Mat4::look_at_rh(self.eye, self.target, self.up)
93 }
94
95 pub fn projection_matrix(&self, aspect: f32) -> Mat4 {
96 Mat4::perspective_rh(self.fov_y, aspect, self.near, self.far)
97 }
98}
99
100impl Default for CameraDesc {
101 fn default() -> Self {
102 Self::new(Vec3::new(0.0, 0.0, 5.0), Vec3::ZERO)
103 }
104}
105
106pub struct HeadlessRenderer {
112 pub width: u32,
113 pub height: u32,
114 renderer: MultiBackendRenderer,
115 color_target: TextureHandle,
116 depth_target: TextureHandle,
117}
118
119impl HeadlessRenderer {
120 pub fn new(width: u32, height: u32) -> Self {
121 let mut renderer = MultiBackendRenderer::software();
122 let color_target = renderer.create_color_texture(width, height);
123 let depth_target = renderer.create_depth_texture(width, height);
124 Self { width, height, renderer, color_target, depth_target }
125 }
126
127 pub fn with_backend(width: u32, height: u32, backend: Box<dyn BackendContext>) -> Self {
128 let caps = BackendCapabilities::for_backend(GpuBackend::Software);
129 let mut renderer = MultiBackendRenderer::new(backend, caps);
130 let color_target = renderer.create_color_texture(width, height);
131 let depth_target = renderer.create_depth_texture(width, height);
132 Self { width, height, renderer, color_target, depth_target }
133 }
134
135 pub fn render_to_buffer(&mut self, scene: &SceneDesc, camera: &CameraDesc) -> Vec<u8> {
137 let pass = RenderPass::new()
138 .with_color(self.color_target)
139 .with_depth(self.depth_target)
140 .with_clear(
141 scene.clear_color[0],
142 scene.clear_color[1],
143 scene.clear_color[2],
144 scene.clear_color[3],
145 );
146
147 self.renderer.begin_frame();
148
149 let mut calls = Vec::new();
151 for obj in &scene.objects {
152 let vbuf = self.renderer.create_vertex_buffer(&obj.vertex_data);
153 let vs = self.renderer.backend.create_shader("headless_vert", ShaderStage::Vertex);
154 let fs = self.renderer.backend.create_shader("headless_frag", ShaderStage::Fragment);
155 let pipe = self.renderer.backend.create_pipeline(vs, fs, &PipelineLayout::default());
156 calls.push(DrawCall::new(pipe, vbuf, obj.vertex_count));
157 }
158
159 self.renderer.draw(&pass, &calls);
160 self.renderer.end_frame();
161
162 let pixel_count = (self.width * self.height) as usize;
166 let mut pixels = Vec::with_capacity(pixel_count * 4);
167 let r = (scene.clear_color[0] * 255.0) as u8;
168 let g = (scene.clear_color[1] * 255.0) as u8;
169 let b = (scene.clear_color[2] * 255.0) as u8;
170 let a = (scene.clear_color[3] * 255.0) as u8;
171 for _ in 0..pixel_count {
172 pixels.push(r);
173 pixels.push(g);
174 pixels.push(b);
175 pixels.push(a);
176 }
177 pixels
178 }
179
180 pub fn render_to_png(&mut self, scene: &SceneDesc, camera: &CameraDesc, path: &str) {
183 let pixels = self.render_to_buffer(scene, camera);
184 let mut tga = Vec::new();
186 tga.push(0); tga.push(0); tga.push(2); tga.extend_from_slice(&[0, 0, 0, 0, 0]); tga.extend_from_slice(&[0, 0]); tga.extend_from_slice(&[0, 0]); tga.extend_from_slice(&(self.width as u16).to_le_bytes()); tga.extend_from_slice(&(self.height as u16).to_le_bytes()); tga.push(32); tga.push(0x28); for chunk in pixels.chunks(4) {
199 tga.push(chunk[2]); tga.push(chunk[1]); tga.push(chunk[0]); tga.push(chunk[3]); }
204 let _ = std::fs::write(path, &tga);
205 }
206
207 pub fn resize(&mut self, width: u32, height: u32) {
209 self.renderer.destroy_texture(self.color_target);
210 self.renderer.destroy_texture(self.depth_target);
211 self.width = width;
212 self.height = height;
213 self.color_target = self.renderer.create_color_texture(width, height);
214 self.depth_target = self.renderer.create_depth_texture(width, height);
215 }
216
217 pub fn renderer(&self) -> &MultiBackendRenderer {
219 &self.renderer
220 }
221
222 pub fn renderer_mut(&mut self) -> &mut MultiBackendRenderer {
223 &mut self.renderer
224 }
225}
226
227pub struct ThumbnailGenerator {
233 renderer: HeadlessRenderer,
234}
235
236impl ThumbnailGenerator {
237 pub fn new(width: u32, height: u32) -> Self {
238 Self {
239 renderer: HeadlessRenderer::new(width, height),
240 }
241 }
242
243 pub fn generate_thumbnail(&mut self, scene: &SceneDesc) -> Vec<u8> {
245 let camera = CameraDesc::default();
246 self.renderer.render_to_buffer(scene, &camera)
247 }
248
249 pub fn width(&self) -> u32 { self.renderer.width }
251
252 pub fn height(&self) -> u32 { self.renderer.height }
254}
255
256pub struct BatchRenderer {
262 renderer: HeadlessRenderer,
263}
264
265impl BatchRenderer {
266 pub fn new(width: u32, height: u32) -> Self {
267 Self {
268 renderer: HeadlessRenderer::new(width, height),
269 }
270 }
271
272 pub fn render_all(
274 &mut self,
275 scenes: &[SceneDesc],
276 camera: &CameraDesc,
277 ) -> Vec<Vec<u8>> {
278 scenes.iter().map(|s| self.renderer.render_to_buffer(s, camera)).collect()
279 }
280
281 pub fn render_all_to_files(
283 &mut self,
284 scenes: &[SceneDesc],
285 camera: &CameraDesc,
286 path_prefix: &str,
287 ) {
288 for (i, scene) in scenes.iter().enumerate() {
289 let path = format!("{}{}.tga", path_prefix, i);
290 self.renderer.render_to_png(scene, camera, &path);
291 }
292 }
293}
294
295pub struct ScreenshotCapture {
301 width: u32,
302 height: u32,
303 capture_requested: bool,
304 last_capture: Option<Vec<u8>>,
305}
306
307impl ScreenshotCapture {
308 pub fn new(width: u32, height: u32) -> Self {
309 Self {
310 width,
311 height,
312 capture_requested: false,
313 last_capture: None,
314 }
315 }
316
317 pub fn request_capture(&mut self) {
319 self.capture_requested = true;
320 }
321
322 pub fn should_capture(&mut self) -> bool {
324 let val = self.capture_requested;
325 self.capture_requested = false;
326 val
327 }
328
329 pub fn store_capture(&mut self, pixels: Vec<u8>) {
331 self.last_capture = Some(pixels);
332 }
333
334 pub fn capture_next_frame(
336 &mut self,
337 renderer: &mut HeadlessRenderer,
338 scene: &SceneDesc,
339 camera: &CameraDesc,
340 ) -> Vec<u8> {
341 let pixels = renderer.render_to_buffer(scene, camera);
342 self.last_capture = Some(pixels.clone());
343 pixels
344 }
345
346 pub fn last_capture(&self) -> Option<&[u8]> {
348 self.last_capture.as_deref()
349 }
350
351 pub fn has_capture(&self) -> bool {
353 self.last_capture.is_some()
354 }
355}
356
357pub struct ServerRenderer {
364 renderer: HeadlessRenderer,
365 render_count: u64,
366}
367
368impl ServerRenderer {
369 pub fn new(width: u32, height: u32) -> Self {
370 Self {
371 renderer: HeadlessRenderer::new(width, height),
372 render_count: 0,
373 }
374 }
375
376 pub fn render(&mut self, scene: &SceneDesc, camera: &CameraDesc) -> Vec<u8> {
378 self.render_count += 1;
379 self.renderer.render_to_buffer(scene, camera)
380 }
381
382 pub fn render_to_file(
384 &mut self,
385 scene: &SceneDesc,
386 camera: &CameraDesc,
387 path: &str,
388 ) {
389 self.render_count += 1;
390 self.renderer.render_to_png(scene, camera, path);
391 }
392
393 pub fn render_count(&self) -> u64 {
395 self.render_count
396 }
397
398 pub fn resize(&mut self, width: u32, height: u32) {
400 self.renderer.resize(width, height);
401 }
402
403 pub fn width(&self) -> u32 { self.renderer.width }
405
406 pub fn height(&self) -> u32 { self.renderer.height }
408}
409
410#[cfg(test)]
415mod tests {
416 use super::*;
417
418 fn test_scene() -> SceneDesc {
419 SceneDesc::new()
420 .with_clear_color(0.2, 0.3, 0.4, 1.0)
421 .with_object(ObjectDesc::new(vec![0u8; 36], 3).with_color(1.0, 0.0, 0.0, 1.0))
422 }
423
424 fn test_camera() -> CameraDesc {
425 CameraDesc::new(Vec3::new(0.0, 0.0, 5.0), Vec3::ZERO)
426 }
427
428 #[test]
429 fn camera_desc_matrices() {
430 let cam = test_camera();
431 let view = cam.view_matrix();
432 let proj = cam.projection_matrix(16.0 / 9.0);
433 assert_ne!(view, Mat4::ZERO);
435 assert_ne!(proj, Mat4::ZERO);
436 }
437
438 #[test]
439 fn camera_desc_default() {
440 let cam = CameraDesc::default();
441 assert_eq!(cam.eye, Vec3::new(0.0, 0.0, 5.0));
442 assert_eq!(cam.target, Vec3::ZERO);
443 }
444
445 #[test]
446 fn scene_desc_builder() {
447 let scene = test_scene();
448 assert_eq!(scene.objects.len(), 1);
449 assert_eq!(scene.clear_color[0], 0.2);
450 }
451
452 #[test]
453 fn headless_renderer_new() {
454 let renderer = HeadlessRenderer::new(320, 240);
455 assert_eq!(renderer.width, 320);
456 assert_eq!(renderer.height, 240);
457 }
458
459 #[test]
460 fn headless_render_to_buffer() {
461 let mut renderer = HeadlessRenderer::new(4, 4);
462 let scene = SceneDesc::new().with_clear_color(1.0, 0.0, 0.0, 1.0);
463 let camera = test_camera();
464 let pixels = renderer.render_to_buffer(&scene, &camera);
465 assert_eq!(pixels.len(), 4 * 4 * 4); assert_eq!(pixels[0], 255); assert_eq!(pixels[1], 0); assert_eq!(pixels[2], 0); assert_eq!(pixels[3], 255); }
472
473 #[test]
474 fn headless_render_with_objects() {
475 let mut renderer = HeadlessRenderer::new(8, 8);
476 let scene = test_scene();
477 let camera = test_camera();
478 let pixels = renderer.render_to_buffer(&scene, &camera);
479 assert_eq!(pixels.len(), 8 * 8 * 4);
480 }
481
482 #[test]
483 fn headless_resize() {
484 let mut renderer = HeadlessRenderer::new(100, 100);
485 renderer.resize(200, 150);
486 assert_eq!(renderer.width, 200);
487 assert_eq!(renderer.height, 150);
488 let scene = SceneDesc::new();
490 let camera = test_camera();
491 let pixels = renderer.render_to_buffer(&scene, &camera);
492 assert_eq!(pixels.len(), 200 * 150 * 4);
493 }
494
495 #[test]
496 fn headless_render_to_file() {
497 let mut renderer = HeadlessRenderer::new(4, 4);
498 let scene = SceneDesc::new().with_clear_color(0.0, 1.0, 0.0, 1.0);
499 let camera = test_camera();
500 let path = std::env::temp_dir().join("proof_engine_test_headless.tga");
501 let path_str = path.to_string_lossy().to_string();
502 renderer.render_to_png(&scene, &camera, &path_str);
503 assert!(path.exists());
505 let data = std::fs::read(&path).unwrap();
506 assert_eq!(data.len(), 18 + 64);
508 let _ = std::fs::remove_file(&path);
509 }
510
511 #[test]
512 fn thumbnail_generator() {
513 let mut gen = ThumbnailGenerator::new(32, 32);
514 assert_eq!(gen.width(), 32);
515 assert_eq!(gen.height(), 32);
516 let scene = test_scene();
517 let thumb = gen.generate_thumbnail(&scene);
518 assert_eq!(thumb.len(), 32 * 32 * 4);
519 }
520
521 #[test]
522 fn batch_renderer_render_all() {
523 let mut batch = BatchRenderer::new(4, 4);
524 let scenes = vec![
525 SceneDesc::new().with_clear_color(1.0, 0.0, 0.0, 1.0),
526 SceneDesc::new().with_clear_color(0.0, 1.0, 0.0, 1.0),
527 SceneDesc::new().with_clear_color(0.0, 0.0, 1.0, 1.0),
528 ];
529 let camera = test_camera();
530 let results = batch.render_all(&scenes, &camera);
531 assert_eq!(results.len(), 3);
532 for r in &results {
533 assert_eq!(r.len(), 4 * 4 * 4);
534 }
535 assert_eq!(results[0][0], 255);
537 assert_eq!(results[0][1], 0);
538 assert_eq!(results[1][0], 0);
540 assert_eq!(results[1][1], 255);
541 }
542
543 #[test]
544 fn screenshot_capture_workflow() {
545 let mut cap = ScreenshotCapture::new(8, 8);
546 assert!(!cap.has_capture());
547 assert!(!cap.should_capture());
548
549 cap.request_capture();
550 assert!(cap.should_capture());
551 assert!(!cap.should_capture()); cap.store_capture(vec![42u8; 256]);
554 assert!(cap.has_capture());
555 assert_eq!(cap.last_capture().unwrap().len(), 256);
556 }
557
558 #[test]
559 fn screenshot_capture_from_renderer() {
560 let mut cap = ScreenshotCapture::new(4, 4);
561 let mut renderer = HeadlessRenderer::new(4, 4);
562 let scene = SceneDesc::new().with_clear_color(0.5, 0.5, 0.5, 1.0);
563 let camera = test_camera();
564 let pixels = cap.capture_next_frame(&mut renderer, &scene, &camera);
565 assert_eq!(pixels.len(), 4 * 4 * 4);
566 assert!(cap.has_capture());
567 assert_eq!(cap.last_capture().unwrap(), &pixels[..]);
568 }
569
570 #[test]
571 fn server_renderer_basic() {
572 let mut srv = ServerRenderer::new(16, 16);
573 assert_eq!(srv.width(), 16);
574 assert_eq!(srv.height(), 16);
575 assert_eq!(srv.render_count(), 0);
576
577 let scene = test_scene();
578 let camera = test_camera();
579 let pixels = srv.render(&scene, &camera);
580 assert_eq!(pixels.len(), 16 * 16 * 4);
581 assert_eq!(srv.render_count(), 1);
582
583 srv.resize(32, 32);
584 let pixels2 = srv.render(&scene, &camera);
585 assert_eq!(pixels2.len(), 32 * 32 * 4);
586 assert_eq!(srv.render_count(), 2);
587 }
588
589 #[test]
590 fn server_renderer_to_file() {
591 let mut srv = ServerRenderer::new(4, 4);
592 let scene = SceneDesc::new();
593 let camera = test_camera();
594 let path = std::env::temp_dir().join("proof_engine_test_server.tga");
595 let path_str = path.to_string_lossy().to_string();
596 srv.render_to_file(&scene, &camera, &path_str);
597 assert_eq!(srv.render_count(), 1);
598 assert!(path.exists());
599 let _ = std::fs::remove_file(&path);
600 }
601
602 #[test]
603 fn object_desc_builder() {
604 let obj = ObjectDesc::new(vec![0u8; 12], 1)
605 .with_color(0.5, 0.6, 0.7, 0.8);
606 assert_eq!(obj.color[0], 0.5);
607 assert_eq!(obj.vertex_count, 1);
608 }
609
610 #[test]
611 fn headless_with_custom_backend() {
612 let backend = Box::new(SoftwareContext::new());
613 let renderer = HeadlessRenderer::with_backend(10, 10, backend);
614 assert_eq!(renderer.width, 10);
615 }
616
617 #[test]
618 fn headless_renderer_access() {
619 let renderer = HeadlessRenderer::new(4, 4);
620 assert_eq!(renderer.renderer().backend_name(), "Software");
621 }
622}