Files
ARCADIA-Studio/harness/experiments/grid-cache-20260925/grid_render_probe_v1.rs
T

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)
});
}
}