Skip to main content

proof_engine/wgpu_backend/
headless.rs

1//! Headless (off-screen) rendering: render scenes to pixel buffers without a
2//! window, generate thumbnails, batch-render, and server-side rendering.
3
4use 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// ---------------------------------------------------------------------------
12// Simple scene / camera descriptions for headless rendering
13// ---------------------------------------------------------------------------
14
15/// A minimal scene description for headless rendering.
16#[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/// A minimal object within a scene.
46#[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/// A simple camera for headless rendering.
69#[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
106// ---------------------------------------------------------------------------
107// HeadlessRenderer
108// ---------------------------------------------------------------------------
109
110/// Off-screen renderer that produces pixel buffers.
111pub 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    /// Render a scene to an RGBA pixel buffer.
136    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        // Create draw calls for each object.
150        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        // In a real renderer, we'd read back the colour target.
163        // With the software backend the texture is zero-initialized; we fill
164        // it with the clear colour to produce a meaningful result.
165        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    /// Render to a file.  Writes raw RGBA pixel data with a minimal
181    /// uncompressed BMP-like header (since we don't depend on image crates).
182    pub fn render_to_png(&mut self, scene: &SceneDesc, camera: &CameraDesc, path: &str) {
183        let pixels = self.render_to_buffer(scene, camera);
184        // Write a simple TGA file (uncompressed RGBA).
185        let mut tga = Vec::new();
186        // TGA header (18 bytes)
187        tga.push(0); // id length
188        tga.push(0); // color map type
189        tga.push(2); // image type: uncompressed true-color
190        tga.extend_from_slice(&[0, 0, 0, 0, 0]); // color map spec
191        tga.extend_from_slice(&[0, 0]); // x origin
192        tga.extend_from_slice(&[0, 0]); // y origin
193        tga.extend_from_slice(&(self.width as u16).to_le_bytes()); // width
194        tga.extend_from_slice(&(self.height as u16).to_le_bytes()); // height
195        tga.push(32); // bits per pixel
196        tga.push(0x28); // image descriptor (top-left origin, 8 alpha bits)
197        // Convert RGBA to BGRA for TGA
198        for chunk in pixels.chunks(4) {
199            tga.push(chunk[2]); // B
200            tga.push(chunk[1]); // G
201            tga.push(chunk[0]); // R
202            tga.push(chunk[3]); // A
203        }
204        let _ = std::fs::write(path, &tga);
205    }
206
207    /// Resize the render targets.
208    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    /// Access the inner renderer.
218    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
227// ---------------------------------------------------------------------------
228// ThumbnailGenerator
229// ---------------------------------------------------------------------------
230
231/// Generates small thumbnails of scenes.
232pub 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    /// Generate a thumbnail for the given scene.
244    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    /// Width of generated thumbnails.
250    pub fn width(&self) -> u32 { self.renderer.width }
251
252    /// Height of generated thumbnails.
253    pub fn height(&self) -> u32 { self.renderer.height }
254}
255
256// ---------------------------------------------------------------------------
257// BatchRenderer
258// ---------------------------------------------------------------------------
259
260/// Render multiple scenes in sequence, collecting results.
261pub 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    /// Render all scenes and return pixel buffers.
273    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    /// Render all scenes and save to files (path_prefix + index + ".tga").
282    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
295// ---------------------------------------------------------------------------
296// ScreenshotCapture
297// ---------------------------------------------------------------------------
298
299/// Captures frames from a live renderer.
300pub 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    /// Request a capture on the next frame.
318    pub fn request_capture(&mut self) {
319        self.capture_requested = true;
320    }
321
322    /// Check if a capture was requested and consume the flag.
323    pub fn should_capture(&mut self) -> bool {
324        let val = self.capture_requested;
325        self.capture_requested = false;
326        val
327    }
328
329    /// Store captured pixel data.
330    pub fn store_capture(&mut self, pixels: Vec<u8>) {
331        self.last_capture = Some(pixels);
332    }
333
334    /// Capture the next frame from the given headless renderer.
335    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    /// Get the last captured pixels.
347    pub fn last_capture(&self) -> Option<&[u8]> {
348        self.last_capture.as_deref()
349    }
350
351    /// Whether we have a stored capture.
352    pub fn has_capture(&self) -> bool {
353        self.last_capture.is_some()
354    }
355}
356
357// ---------------------------------------------------------------------------
358// ServerRenderer
359// ---------------------------------------------------------------------------
360
361/// Headless renderer for server-side rendering use cases (e.g. generating
362/// images in response to API requests).
363pub 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    /// Render a scene and return RGBA pixels.
377    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    /// Render and save to a file.
383    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    /// Total number of renders performed.
394    pub fn render_count(&self) -> u64 {
395        self.render_count
396    }
397
398    /// Resize the output.
399    pub fn resize(&mut self, width: u32, height: u32) {
400        self.renderer.resize(width, height);
401    }
402
403    /// Current width.
404    pub fn width(&self) -> u32 { self.renderer.width }
405
406    /// Current height.
407    pub fn height(&self) -> u32 { self.renderer.height }
408}
409
410// ---------------------------------------------------------------------------
411// Tests
412// ---------------------------------------------------------------------------
413
414#[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        // Just verify they are non-zero
434        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); // 4x4 RGBA
466        // Clear color should be red
467        assert_eq!(pixels[0], 255); // R
468        assert_eq!(pixels[1], 0);   // G
469        assert_eq!(pixels[2], 0);   // B
470        assert_eq!(pixels[3], 255); // A
471    }
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        // Render should still work after resize
489        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        // Verify file was created
504        assert!(path.exists());
505        let data = std::fs::read(&path).unwrap();
506        // TGA header is 18 bytes, then 4*4*4=64 bytes of pixel data
507        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        // First result should be red
536        assert_eq!(results[0][0], 255);
537        assert_eq!(results[0][1], 0);
538        // Second result should be green
539        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()); // consumed
552
553        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}