docs: publish grid cache measurements and Windows verification
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
//! 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 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<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 = 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));
|
||||
measure("batched_geometry", n, || {
|
||||
batched(&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)
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user