feat: add a headless mode, driven by clap CLI args
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
// Headless PNG rendering: parse the CLI, build the exact same view/state the
|
||||
// windowed app would from it, then render straight to a file. No window, no
|
||||
// event loop, no worker-thread debounce (nothing to debounce for a one-shot
|
||||
// render); it just creates its own wgpu device, computes the reference orbit
|
||||
// once, and renders through the same `ExportRender` path the "Export PNG"
|
||||
// button uses.
|
||||
|
||||
use eframe::egui_wgpu::wgpu;
|
||||
|
||||
use crate::app::{FractalApp, unix_timestamp};
|
||||
use crate::cli::Cli;
|
||||
use crate::fractal::{ExportRender, FractalRenderer, export_to_png_blocking};
|
||||
|
||||
/// Cap on the output image dimension (px), to stay within GPU texture limits.
|
||||
const MAX_DIM: u32 = 8192 * 16;
|
||||
|
||||
pub fn run(cli: Cli) -> Result<(), String> {
|
||||
if cli.buddhabrot {
|
||||
return Err("headless mode doesn't support --buddhabrot yet".into());
|
||||
}
|
||||
|
||||
let width = cli.width.clamp(16, MAX_DIM);
|
||||
let height = cli.height.clamp(16, MAX_DIM);
|
||||
let export_path = cli
|
||||
.export_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("fractal-{}.png", unix_timestamp()));
|
||||
|
||||
let mut app = FractalApp::default_state();
|
||||
app.apply_cli(cli);
|
||||
|
||||
eprintln!("computing reference orbit…");
|
||||
app.compute_reference_blocking();
|
||||
|
||||
let (device, queue) = pollster::block_on(request_device())?;
|
||||
let format = wgpu::TextureFormat::Rgba8UnormSrgb;
|
||||
let renderer = FractalRenderer::new(&device, format);
|
||||
let (pipeline, bind_group_layout, format) = renderer.export_handles();
|
||||
|
||||
let uniforms = app.make_uniforms(width as f64 / height as f64);
|
||||
let er = ExportRender::new(
|
||||
&device,
|
||||
&queue,
|
||||
pipeline,
|
||||
&bind_group_layout,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
uniforms,
|
||||
app.reference_points(),
|
||||
);
|
||||
|
||||
eprintln!("rendering {width}×{height}…");
|
||||
let png = export_to_png_blocking(&device, &queue, &er, |phase, fraction| {
|
||||
eprint!("\r{phase} {:>3.0}%", fraction * 100.0);
|
||||
});
|
||||
eprintln!();
|
||||
|
||||
std::fs::write(&export_path, &png).map_err(|e| format!("save failed: {e}"))?;
|
||||
println!("saved {export_path} ({width}×{height})");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set up a wgpu device with no surface/window attached, matching the limits
|
||||
/// `main::wgpu_options` requests for the windowed app (the fractal fragment
|
||||
/// shader needs storage buffers, which downlevel/WebGL-style limits disallow).
|
||||
async fn request_device() -> Result<(wgpu::Device, wgpu::Queue), String> {
|
||||
let instance = wgpu::Instance::default();
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions::default())
|
||||
.await
|
||||
.map_err(|e| format!("no compatible GPU adapter: {e}"))?;
|
||||
adapter
|
||||
.request_device(&wgpu::DeviceDescriptor {
|
||||
label: Some("headless fractal device"),
|
||||
required_features: wgpu::Features::empty(),
|
||||
required_limits: adapter.limits(),
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("failed to create device: {e}"))
|
||||
}
|
||||
Reference in New Issue
Block a user