132 lines
4.5 KiB
Rust
132 lines
4.5 KiB
Rust
//! Headless rendering experiment; creates no desktop window and sends no input.
|
|
use iced::advanced::{
|
|
graphics::geometry::Renderer as _,
|
|
renderer::{Headless, Renderer as _},
|
|
};
|
|
use iced::{
|
|
Color, Point, Rectangle, Renderer, Size, Vector,
|
|
widget::canvas::{Cache, Frame, Geometry, Path},
|
|
};
|
|
use std::{cell::Cell, hint::black_box, time::Instant};
|
|
|
|
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 screenshot(renderer: &mut Renderer, size: Size, geometry: Geometry) -> Vec<u8> {
|
|
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<T>(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::<usize>().unwrap())
|
|
.unwrap_or(20)
|
|
.max(1);
|
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap();
|
|
let mut renderer = runtime
|
|
.block_on(<Renderer as Headless>::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 = 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));
|
|
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)
|
|
});
|
|
}
|
|
}
|