polyscope_rs/headless.rs
1//! Headless rendering API for polyscope-rs.
2//!
3//! Provides functions to render the current scene to an image buffer or file
4//! without opening a window. Useful for integration tests, batch processing,
5//! and automated screenshot generation.
6
7use crate::Result;
8use crate::app::App;
9use pollster::FutureExt;
10use polyscope_core::state::with_context_mut;
11use polyscope_render::RenderEngine;
12
13/// Renders the current scene to a file.
14///
15/// Creates a headless GPU context, renders one frame of the current scene
16/// (all registered structures and quantities), and saves the result as
17/// a PNG or JPEG image.
18///
19/// The camera is automatically fitted to the scene bounding box.
20///
21/// # Example
22/// ```no_run
23/// use polyscope_rs::*;
24///
25/// init().unwrap();
26/// register_point_cloud("pts", vec![Vec3::ZERO, Vec3::X, Vec3::Y]);
27/// render_to_file("output.png", 800, 600).unwrap();
28/// ```
29pub fn render_to_file(filename: &str, width: u32, height: u32) -> Result<()> {
30 let data = render_to_image(width, height)?;
31 polyscope_render::save_image(filename, &data, width, height)
32 .map_err(|e| crate::PolyscopeError::RenderError(format!("Failed to save image: {e}")))
33}
34
35/// Renders the current scene to a raw RGBA pixel buffer.
36///
37/// Creates a headless GPU context, renders one frame of the current scene,
38/// and returns the pixel data as `Vec<u8>` in RGBA format (4 bytes per pixel).
39///
40/// The returned buffer has dimensions `width * height * 4` bytes.
41/// Pixels are ordered row-by-row from top-left to bottom-right.
42///
43/// # Example
44/// ```no_run
45/// use polyscope_rs::*;
46///
47/// init().unwrap();
48/// register_point_cloud("pts", vec![Vec3::ZERO, Vec3::X, Vec3::Y]);
49/// let pixels = render_to_image(800, 600).unwrap();
50/// assert_eq!(pixels.len(), 800 * 600 * 4);
51/// ```
52pub fn render_to_image(width: u32, height: u32) -> Result<Vec<u8>> {
53 let mut app = App::new();
54
55 // Create headless render engine
56 let engine = RenderEngine::new_headless(width, height)
57 .block_on()
58 .map_err(|e| {
59 crate::PolyscopeError::RenderError(format!("Failed to create headless engine: {e}"))
60 })?;
61 app.engine = Some(engine);
62
63 // Clear stale GPU resources from all structures so they get re-initialized
64 // with the new wgpu device. This is necessary because each headless render
65 // call creates a fresh device, but structures in the global registry may
66 // retain buffers from a previous device.
67 with_context_mut(|ctx| {
68 for structure in ctx.registry.iter_mut() {
69 structure.clear_gpu_resources();
70 }
71 });
72
73 // Render one frame and capture
74 app.render_frame_headless();
75 app.capture_to_buffer()
76}