Files
mandelbrot/src/headless.rs
T
2026-09-20 13:20:29 +02:00

84 lines
3.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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::Bgra8Unorm;
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(),
app.lights(),
);
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}"))
}