//! Headless rendering experiment; creates no desktop window and sends no input. use iced::advanced::{ graphics::geometry::Renderer as _, renderer::{Headless, Renderer as _}, }; use iced::widget::canvas; use iced::{ Color, Point, Rectangle, Renderer, Size, Vector, widget::canvas::{Cache, Frame, Geometry, Path}, }; use std::{cell::Cell, hint::black_box, time::Instant}; // Compile the actual candidate cache, not a second implementation of its key. #[path = "../../../crates/oa-desktop/src/diagram/grid.rs"] mod grid; mod style { pub const LINE: iced::Color = iced::Color::from_rgb(0.86, 0.89, 0.91); } const BG: Color = Color::from_rgb8(246, 248, 251); const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91); fn paint(frame: &mut Frame, size: Size, zoom: f32, pan: Vector) { frame.fill_rectangle(Point::ORIGIN, size, BG); let step = (24. * zoom).max(14.); let ox = pan.x.rem_euclid(step); let oy = pan.y.rem_euclid(step); for x in 0..(size.width / step) as i32 + 1 { for y in 0..(size.height / step) as i32 + 1 { frame.fill( &Path::circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65), DOT, ); } } } fn fresh(renderer: &Renderer, size: Size, zoom: f32, pan: Vector) -> Geometry { let mut frame = Frame::new(renderer, size); paint(&mut frame, size, zoom, pan); frame.into_geometry() } fn batched(renderer: &Renderer, size: Size, zoom: f32, pan: Vector) -> Geometry { let mut frame = Frame::new(renderer, size); frame.fill_rectangle(Point::ORIGIN, size, BG); let step = (24. * zoom).max(14.); let ox = pan.x.rem_euclid(step); let oy = pan.y.rem_euclid(step); let path = Path::new(|p| { for x in 0..(size.width / step) as i32 + 1 { for y in 0..(size.height / step) as i32 + 1 { p.circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65); p.close(); } } }); frame.fill(&path, DOT); frame.into_geometry() } fn screenshot(renderer: &mut Renderer, size: Size, geometry: Geometry) -> Vec { renderer.reset(Rectangle::with_size(size)); renderer.draw_geometry(geometry); Headless::screenshot( renderer, Size::new(size.width as u32, size.height as u32), 1., BG, ) } fn measure(label: &str, n: usize, mut f: impl FnMut() -> T) { for _ in 0..3 { black_box(f()); } let mut values = Vec::new(); for _ in 0..n { let now = Instant::now(); let value = black_box(f()); values.push(now.elapsed().as_secs_f64() * 1000.); drop(value); } values.sort_by(f64::total_cmp); let at = |p: f64| values[((n - 1) as f64 * p).ceil() as usize]; println!( "{label}: n={n} p50_ms={:.4} p95_ms={:.4} max_ms={:.4}", at(0.5), at(0.95), values[n - 1] ); } fn main() { let n = std::env::args() .nth(1) .map(|s| s.parse::().unwrap()) .unwrap_or(20) .max(1); let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap(); let mut renderer = runtime .block_on(::new( iced::Font::DEFAULT, 16.into(), None, )) .expect("headless production renderer"); println!( "renderer={} samples={n}; screenshot includes offscreen rendering + GPU readback, NOT native frame presentation", renderer.name() ); for (size, zoom) in [ (Size::new(1900., 660.), 1.), (Size::new(1900., 660.), 0.5), (Size::new(1440., 900.), 1.), (Size::new(1440., 900.), 0.5), ] { let pan = Vector::new(-13.25, 9.75); let cache = Cache::new(); let calls = Cell::new(0); let cached = |r: &Renderer| { cache.draw(r, size, |f| { calls.set(calls.get() + 1); paint(f, size, zoom, pan); }) }; let step = (24f32 * zoom).max(14.); println!( "\nsize={}x{} zoom={zoom} dots={}", size.width, size.height, ((size.width / step) as i32 + 1) * ((size.height / step) as i32 + 1) ); let baseline = fresh(&renderer, size, zoom, pan); let baseline_pixels = screenshot(&mut renderer, size, baseline); let geometry = batched(&renderer, size, zoom, pan); let pixels = screenshot(&mut renderer, size, geometry); let differences = baseline_pixels .iter() .zip(&pixels) .filter(|(a, b)| a != b) .count(); println!("batched_rgba_byte_differences={differences}"); assert_eq!( differences, 0, "batched path must preserve exact grid pixels" ); let geometry = cached(&renderer); assert_eq!(baseline_pixels, screenshot(&mut renderer, size, geometry)); let geometry = cached(&renderer); assert_eq!(baseline_pixels, screenshot(&mut renderer, size, geometry)); assert_eq!(calls.get(), 1); println!("pixel_parity=cold_and_warm_exact"); measure("fresh_geometry", n, || fresh(&renderer, size, zoom, pan)); let production = grid::Grid::default(); let g = production.draw(&renderer, size, zoom, pan); assert_eq!(baseline_pixels, screenshot(&mut renderer, size, g)); measure("production_cached_geometry", n, || { production.draw(&renderer, size, zoom, pan) }); let mut tick = 0; measure("production_changing_pan_geometry", n, || { tick += 1; production.draw( &renderer, size, zoom, pan + Vector::new(tick as f32 * 0.25, 0.), ) }); measure("cached_geometry", n, || cached(&renderer)); assert_eq!(calls.get(), 1, "identical camera must reuse geometry"); measure("fresh_geometry_and_screenshot", n, || { let g = fresh(&renderer, size, zoom, pan); screenshot(&mut renderer, size, g) }); measure("cached_geometry_and_screenshot", n, || { let g = cached(&renderer); screenshot(&mut renderer, size, g) }); measure("cache_forced_miss_geometry", n, || { cache.clear(); cached(&renderer) }); } }