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) {
185 if let Err(e) = self.try_render_to_file(scene, camera, path) {
186 log::warn!("headless render to {path} failed: {e}");
187 }
188 }
189
190 pub fn try_render_to_file(&mut self, scene: &SceneDesc, camera: &CameraDesc, path: &str) -> std::io::Result<()> {
192 let pixels = self.render_to_buffer(scene, camera);
193 crate::export::save_rgba(path, self.width, self.height, &pixels)
194 }
195
196 pub fn resize(&mut self, width: u32, height: u32) {
198 self.renderer.destroy_texture(self.color_target);
199 self.renderer.destroy_texture(self.depth_target);
200 self.width = width;
201 self.height = height;
202 self.color_target = self.renderer.create_color_texture(width, height);
203 self.depth_target = self.renderer.create_depth_texture(width, height);
204 }
205
206 pub fn renderer(&self) -> &MultiBackendRenderer {
208 &self.renderer
209 }
210
211 pub fn renderer_mut(&mut self) -> &mut MultiBackendRenderer {
212 &mut self.renderer
213 }
214}
215
216pub struct ThumbnailGenerator {
222 renderer: HeadlessRenderer,
223}
224
225impl ThumbnailGenerator {
226 pub fn new(width: u32, height: u32) -> Self {
227 Self {
228 renderer: HeadlessRenderer::new(width, height),
229 }
230 }
231
232 pub fn generate_thumbnail(&mut self, scene: &SceneDesc) -> Vec<u8> {
234 let camera = CameraDesc::default();
235 self.renderer.render_to_buffer(scene, &camera)
236 }
237
238 pub fn width(&self) -> u32 { self.renderer.width }
240
241 pub fn height(&self) -> u32 { self.renderer.height }
243}
244
245pub struct BatchRenderer {
251 renderer: HeadlessRenderer,
252}
253
254impl BatchRenderer {
255 pub fn new(width: u32, height: u32) -> Self {
256 Self {
257 renderer: HeadlessRenderer::new(width, height),
258 }
259 }
260
261 pub fn render_all(
263 &mut self,
264 scenes: &[SceneDesc],
265 camera: &CameraDesc,
266 ) -> Vec<Vec<u8>> {
267 scenes.iter().map(|s| self.renderer.render_to_buffer(s, camera)).collect()
268 }
269
270 pub fn render_all_to_files(
272 &mut self,
273 scenes: &[SceneDesc],
274 camera: &CameraDesc,
275 path_prefix: &str,
276 ) {
277 for (i, scene) in scenes.iter().enumerate() {
278 let path = format!("{}{}.tga", path_prefix, i);
279 self.renderer.render_to_png(scene, camera, &path);
280 }
281 }
282}
283
284pub struct ScreenshotCapture {
290 width: u32,
291 height: u32,
292 capture_requested: bool,
293 last_capture: Option<Vec<u8>>,
294}
295
296impl ScreenshotCapture {
297 pub fn new(width: u32, height: u32) -> Self {
298 Self {
299 width,
300 height,
301 capture_requested: false,
302 last_capture: None,
303 }
304 }
305
306 pub fn request_capture(&mut self) {
308 self.capture_requested = true;
309 }
310
311 pub fn should_capture(&mut self) -> bool {
313 let val = self.capture_requested;
314 self.capture_requested = false;
315 val
316 }
317
318 pub fn store_capture(&mut self, pixels: Vec<u8>) {
320 self.last_capture = Some(pixels);
321 }
322
323 pub fn capture_next_frame(
325 &mut self,
326 renderer: &mut HeadlessRenderer,
327 scene: &SceneDesc,
328 camera: &CameraDesc,
329 ) -> Vec<u8> {
330 let pixels = renderer.render_to_buffer(scene, camera);
331 self.last_capture = Some(pixels.clone());
332 pixels
333 }
334
335 pub fn last_capture(&self) -> Option<&[u8]> {
337 self.last_capture.as_deref()
338 }
339
340 pub fn has_capture(&self) -> bool {
342 self.last_capture.is_some()
343 }
344}
345
346pub struct ServerRenderer {
353 renderer: HeadlessRenderer,
354 render_count: u64,
355}
356
357impl ServerRenderer {
358 pub fn new(width: u32, height: u32) -> Self {
359 Self {
360 renderer: HeadlessRenderer::new(width, height),
361 render_count: 0,
362 }
363 }
364
365 pub fn render(&mut self, scene: &SceneDesc, camera: &CameraDesc) -> Vec<u8> {
367 self.render_count += 1;
368 self.renderer.render_to_buffer(scene, camera)
369 }
370
371 pub fn render_to_file(
373 &mut self,
374 scene: &SceneDesc,
375 camera: &CameraDesc,
376 path: &str,
377 ) {
378 self.render_count += 1;
379 self.renderer.render_to_png(scene, camera, path);
380 }
381
382 pub fn render_count(&self) -> u64 {
384 self.render_count
385 }
386
387 pub fn resize(&mut self, width: u32, height: u32) {
389 self.renderer.resize(width, height);
390 }
391
392 pub fn width(&self) -> u32 { self.renderer.width }
394
395 pub fn height(&self) -> u32 { self.renderer.height }
397}
398
399#[cfg(test)]
404mod tests {
405 use super::*;
406
407 fn test_scene() -> SceneDesc {
408 SceneDesc::new()
409 .with_clear_color(0.2, 0.3, 0.4, 1.0)
410 .with_object(ObjectDesc::new(vec![0u8; 36], 3).with_color(1.0, 0.0, 0.0, 1.0))
411 }
412
413 fn test_camera() -> CameraDesc {
414 CameraDesc::new(Vec3::new(0.0, 0.0, 5.0), Vec3::ZERO)
415 }
416
417 #[test]
418 fn camera_desc_matrices() {
419 let cam = test_camera();
420 let view = cam.view_matrix();
421 let proj = cam.projection_matrix(16.0 / 9.0);
422 assert_ne!(view, Mat4::ZERO);
424 assert_ne!(proj, Mat4::ZERO);
425 }
426
427 #[test]
428 fn camera_desc_default() {
429 let cam = CameraDesc::default();
430 assert_eq!(cam.eye, Vec3::new(0.0, 0.0, 5.0));
431 assert_eq!(cam.target, Vec3::ZERO);
432 }
433
434 #[test]
435 fn scene_desc_builder() {
436 let scene = test_scene();
437 assert_eq!(scene.objects.len(), 1);
438 assert_eq!(scene.clear_color[0], 0.2);
439 }
440
441 #[test]
442 fn headless_renderer_new() {
443 let renderer = HeadlessRenderer::new(320, 240);
444 assert_eq!(renderer.width, 320);
445 assert_eq!(renderer.height, 240);
446 }
447
448 #[test]
449 fn headless_render_to_buffer() {
450 let mut renderer = HeadlessRenderer::new(4, 4);
451 let scene = SceneDesc::new().with_clear_color(1.0, 0.0, 0.0, 1.0);
452 let camera = test_camera();
453 let pixels = renderer.render_to_buffer(&scene, &camera);
454 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); }
461
462 #[test]
463 fn headless_render_with_objects() {
464 let mut renderer = HeadlessRenderer::new(8, 8);
465 let scene = test_scene();
466 let camera = test_camera();
467 let pixels = renderer.render_to_buffer(&scene, &camera);
468 assert_eq!(pixels.len(), 8 * 8 * 4);
469 }
470
471 #[test]
472 fn headless_resize() {
473 let mut renderer = HeadlessRenderer::new(100, 100);
474 renderer.resize(200, 150);
475 assert_eq!(renderer.width, 200);
476 assert_eq!(renderer.height, 150);
477 let scene = SceneDesc::new();
479 let camera = test_camera();
480 let pixels = renderer.render_to_buffer(&scene, &camera);
481 assert_eq!(pixels.len(), 200 * 150 * 4);
482 }
483
484 #[test]
485 fn headless_render_to_file() {
486 let mut renderer = HeadlessRenderer::new(4, 4);
487 let scene = SceneDesc::new().with_clear_color(0.0, 1.0, 0.0, 1.0);
488 let camera = test_camera();
489 let path = std::env::temp_dir().join("proof_engine_test_headless.tga");
490 let path_str = path.to_string_lossy().to_string();
491 renderer.render_to_png(&scene, &camera, &path_str);
492 assert!(path.exists());
494 let data = std::fs::read(&path).unwrap();
495 let img = image::load_from_memory_with_format(&data, image::ImageFormat::Tga).unwrap().to_rgba8();
497 assert_eq!(img.dimensions(), (4, 4));
498 assert_eq!(img.get_pixel(2, 2).0, [0, 255, 0, 255]);
499 let _ = std::fs::remove_file(&path);
500 }
501
502 #[test]
503 fn thumbnail_generator() {
504 let mut gen = ThumbnailGenerator::new(32, 32);
505 assert_eq!(gen.width(), 32);
506 assert_eq!(gen.height(), 32);
507 let scene = test_scene();
508 let thumb = gen.generate_thumbnail(&scene);
509 assert_eq!(thumb.len(), 32 * 32 * 4);
510 }
511
512 #[test]
513 fn batch_renderer_render_all() {
514 let mut batch = BatchRenderer::new(4, 4);
515 let scenes = vec![
516 SceneDesc::new().with_clear_color(1.0, 0.0, 0.0, 1.0),
517 SceneDesc::new().with_clear_color(0.0, 1.0, 0.0, 1.0),
518 SceneDesc::new().with_clear_color(0.0, 0.0, 1.0, 1.0),
519 ];
520 let camera = test_camera();
521 let results = batch.render_all(&scenes, &camera);
522 assert_eq!(results.len(), 3);
523 for r in &results {
524 assert_eq!(r.len(), 4 * 4 * 4);
525 }
526 assert_eq!(results[0][0], 255);
528 assert_eq!(results[0][1], 0);
529 assert_eq!(results[1][0], 0);
531 assert_eq!(results[1][1], 255);
532 }
533
534 #[test]
535 fn screenshot_capture_workflow() {
536 let mut cap = ScreenshotCapture::new(8, 8);
537 assert!(!cap.has_capture());
538 assert!(!cap.should_capture());
539
540 cap.request_capture();
541 assert!(cap.should_capture());
542 assert!(!cap.should_capture()); cap.store_capture(vec![42u8; 256]);
545 assert!(cap.has_capture());
546 assert_eq!(cap.last_capture().unwrap().len(), 256);
547 }
548
549 #[test]
550 fn screenshot_capture_from_renderer() {
551 let mut cap = ScreenshotCapture::new(4, 4);
552 let mut renderer = HeadlessRenderer::new(4, 4);
553 let scene = SceneDesc::new().with_clear_color(0.5, 0.5, 0.5, 1.0);
554 let camera = test_camera();
555 let pixels = cap.capture_next_frame(&mut renderer, &scene, &camera);
556 assert_eq!(pixels.len(), 4 * 4 * 4);
557 assert!(cap.has_capture());
558 assert_eq!(cap.last_capture().unwrap(), &pixels[..]);
559 }
560
561 #[test]
562 fn server_renderer_basic() {
563 let mut srv = ServerRenderer::new(16, 16);
564 assert_eq!(srv.width(), 16);
565 assert_eq!(srv.height(), 16);
566 assert_eq!(srv.render_count(), 0);
567
568 let scene = test_scene();
569 let camera = test_camera();
570 let pixels = srv.render(&scene, &camera);
571 assert_eq!(pixels.len(), 16 * 16 * 4);
572 assert_eq!(srv.render_count(), 1);
573
574 srv.resize(32, 32);
575 let pixels2 = srv.render(&scene, &camera);
576 assert_eq!(pixels2.len(), 32 * 32 * 4);
577 assert_eq!(srv.render_count(), 2);
578 }
579
580 #[test]
581 fn server_renderer_to_file() {
582 let mut srv = ServerRenderer::new(4, 4);
583 let scene = SceneDesc::new();
584 let camera = test_camera();
585 let path = std::env::temp_dir().join("proof_engine_test_server.tga");
586 let path_str = path.to_string_lossy().to_string();
587 srv.render_to_file(&scene, &camera, &path_str);
588 assert_eq!(srv.render_count(), 1);
589 assert!(path.exists());
590 let _ = std::fs::remove_file(&path);
591 }
592
593 #[test]
594 fn object_desc_builder() {
595 let obj = ObjectDesc::new(vec![0u8; 12], 1)
596 .with_color(0.5, 0.6, 0.7, 0.8);
597 assert_eq!(obj.color[0], 0.5);
598 assert_eq!(obj.vertex_count, 1);
599 }
600
601 #[test]
602 fn headless_with_custom_backend() {
603 let backend = Box::new(SoftwareContext::new());
604 let renderer = HeadlessRenderer::with_backend(10, 10, backend);
605 assert_eq!(renderer.width, 10);
606 }
607
608 #[test]
609 fn headless_renderer_access() {
610 let renderer = HeadlessRenderer::new(4, 4);
611 assert_eq!(renderer.renderer().backend_name(), "Software");
612 }
613}