185 lines
8.5 KiB
Rust
185 lines
8.5 KiB
Rust
//! Large-image benchmark (issue #49): a 14000×14000 (or `--size`) 8-bit RGB photo through open,
|
||
//! canvas refresh, panel thumbnails, a filter, a brush stroke, save and export, with the GPU
|
||
//! device created the way the app creates it (so its texture limits are the app's).
|
||
//!
|
||
//! One operation per run, so the process's peak RSS is that operation's (plus opening):
|
||
//!
|
||
//! ```sh
|
||
//! cargo build --release -p photocraft-ui-egui --example large_image_bench
|
||
//! for op in open refresh thumbs filter brush psd png jpeg pcraft; do
|
||
//! /usr/bin/time -l target/release/examples/large_image_bench --size 14000x14000 --op $op 2>&1 | grep -E 'ms|maximum resident'
|
||
//! done
|
||
//! ```
|
||
//!
|
||
//! The source JPEG is generated once and cached (`--cache <dir>`, default the system temp dir).
|
||
//! `--op open` is the baseline every other operation includes. `--cpu` skips the GPU.
|
||
//! `--depth 16` or `--depth 32` converts the document to that bit depth after opening (the
|
||
//! conversion is part of "open"), e.g. a 36 MP 16-bit refresh: `--size 7360x4912 --depth 16 --op refresh`.
|
||
|
||
use std::time::Instant;
|
||
|
||
use eframe::egui_wgpu::RenderState;
|
||
use photocraft_engine::Session;
|
||
use photocraft_ui_egui::gpu_canvas::GpuCanvas;
|
||
use serde_json::{Value, json};
|
||
|
||
fn arg(args: &[String], name: &str) -> Option<String> {
|
||
args.iter().position(|a| a == name).and_then(|i| args.get(i + 1).cloned())
|
||
}
|
||
|
||
fn ms(t: Instant) -> f64 {
|
||
t.elapsed().as_secs_f64() * 1000.0
|
||
}
|
||
|
||
/// A photo-like RGB image: smooth gradients, soft blobs, fine noise (so JPEG has real work).
|
||
fn photo(w: u32, h: u32) -> photocraft_codecs::Image {
|
||
use rayon::prelude::*;
|
||
let mut px = vec![0u8; w as usize * h as usize * 3];
|
||
px.par_chunks_mut(w as usize * 3).enumerate().for_each(|(y, row)| {
|
||
for x in 0..w as usize {
|
||
let (fx, fy) = (x as f32 / w as f32, y as f32 / h as f32);
|
||
let n = ((x.wrapping_mul(73_856_093) ^ y.wrapping_mul(19_349_663)) % 997) as f32 / 997.0 - 0.5;
|
||
let blob = (-(((fx - 0.4) * 3.0).powi(2) + ((fy - 0.55) * 4.0).powi(2))).exp();
|
||
let r = 0.55 + 0.35 * (fx * 6.0).sin() * 0.5 + 0.3 * blob + n * 0.06;
|
||
let g = 0.45 + 0.25 * (fy * 9.0 + fx * 2.0).cos() * 0.5 + 0.2 * blob + n * 0.06;
|
||
let b = 0.35 + 0.4 * fy - 0.2 * blob + n * 0.06;
|
||
for (c, v) in [r, g, b].into_iter().enumerate() {
|
||
row[x * 3 + c] = (v.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
|
||
}
|
||
}
|
||
});
|
||
photocraft_codecs::Image::from_raw(w, h, photocraft_codecs::ChannelLayout::Rgb, photocraft_codecs::SampleType::U8, px).expect("image")
|
||
}
|
||
|
||
/// A headless GPU canvas on a device created like the app's.
|
||
fn canvas() -> Option<(GpuCanvas, RenderState)> {
|
||
let setup = photocraft_ui_egui::gpu_canvas::wgpu_setup();
|
||
let rs = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| egui_kittest::wgpu::create_render_state(setup, Default::default()))).ok()?;
|
||
let l = rs.device.limits();
|
||
eprintln!("adapter: {} (max texture {}, max buffer {} MB)", rs.adapter.get_info().name, l.max_texture_dimension_2d, l.max_buffer_size >> 20);
|
||
Some((GpuCanvas::new(&rs), rs))
|
||
}
|
||
|
||
fn exec(s: &mut Session, id: &str, p: Value) {
|
||
if let Err(e) = s.execute(id, p) {
|
||
panic!("{id}: {e}");
|
||
}
|
||
}
|
||
|
||
/// The canvas refresh the app does after an edit (`canvas::ensure_gpu`): the damage rect, or
|
||
/// everything, then waits for the GPU (as presenting the next frame would, which also releases
|
||
/// the frame's transient GPU memory). Returns the path taken.
|
||
fn refresh(gpu: Option<&(GpuCanvas, RenderState)>, s: &Session, full: bool) -> &'static str {
|
||
let st = s.active().expect("document");
|
||
let damage = if full { None } else { st.last_damage };
|
||
match gpu {
|
||
Some((g, rs)) => {
|
||
let kind = g.refresh(st.doc.id.0, &st.doc, damage, None).kind;
|
||
let _ = rs.device.poll(eframe::wgpu::PollType::Wait { submission_index: None, timeout: None });
|
||
kind
|
||
}
|
||
None => {
|
||
let r = damage.unwrap_or(st.doc.bounds()).intersect(&st.doc.bounds());
|
||
std::hint::black_box(photocraft_compose::render(&st.doc, r));
|
||
"cpu"
|
||
}
|
||
}
|
||
}
|
||
|
||
fn main() {
|
||
let args: Vec<String> = std::env::args().collect();
|
||
let (w, h) =
|
||
arg(&args, "--size").and_then(|s| s.split_once('x').map(|(a, b)| (a.parse().unwrap_or(14000), b.parse().unwrap_or(14000)))).unwrap_or((14000, 14000));
|
||
let op = arg(&args, "--op").unwrap_or_else(|| "open".into());
|
||
let dir = arg(&args, "--cache").map_or_else(std::env::temp_dir, std::path::PathBuf::from);
|
||
let src = dir.join(format!("photocraft-bench-{w}x{h}.jpg"));
|
||
if !src.exists() {
|
||
let t = Instant::now();
|
||
let img = photo(w, h);
|
||
let jpeg = photocraft_codecs::encode(&img, photocraft_codecs::Format::Jpeg, &Default::default()).expect("jpeg");
|
||
std::fs::write(&src, jpeg).expect("write cache");
|
||
eprintln!("generated {} in {:.0} ms (run again to measure)", src.display(), ms(t));
|
||
return;
|
||
}
|
||
let gpu = if args.iter().any(|a| a == "--cpu") { None } else { canvas() };
|
||
let gpu = gpu.as_ref();
|
||
println!("document {w}×{h} ({:.0} MP), op {op}", (w as f64 * h as f64) / 1e6);
|
||
|
||
let t = Instant::now();
|
||
let bytes = std::fs::read(&src).expect("read");
|
||
let doc = photocraft_io::import("photo.jpg", &bytes).expect("import").document;
|
||
drop(bytes);
|
||
let mut s = Session::new();
|
||
s.open_document(doc, Some("photo.jpg".into()));
|
||
match arg(&args, "--depth").as_deref() {
|
||
Some("16") => exec(&mut s, "image.mode.bits16", json!({})),
|
||
Some("32") => exec(&mut s, "image.mode.bits32", json!({})),
|
||
_ => {}
|
||
}
|
||
let t_open = ms(t);
|
||
let t = Instant::now();
|
||
let path = refresh(gpu, &s, true);
|
||
println!("open (decode + document) {t_open:>9.0} ms");
|
||
println!("first refresh ({path:<8}) {:>9.0} ms", ms(t));
|
||
if let Some((g, _)) = gpu
|
||
&& let Some((format, bytes)) = g.texture_info(s.active().expect("doc").doc.id.0)
|
||
{
|
||
println!("canvas texture {format:?}, {} MB with mips", bytes >> 20);
|
||
}
|
||
|
||
let doc = || s.active().expect("doc").doc.clone();
|
||
match op.as_str() {
|
||
"open" => {}
|
||
"refresh" => {
|
||
for _ in 0..3 {
|
||
let t = Instant::now();
|
||
let path = refresh(gpu, &s, true);
|
||
println!("full refresh ({path:<8}) {:>9.0} ms", ms(t));
|
||
}
|
||
}
|
||
"thumbs" => {
|
||
let d = doc();
|
||
let t = Instant::now();
|
||
std::hint::black_box(photocraft_compose::thumbnail(&d, 512));
|
||
println!("navigator thumbnail (512) {:>9.0} ms", ms(t));
|
||
let t = Instant::now();
|
||
std::hint::black_box(photocraft_compose::thumbnail(&d, 56));
|
||
println!("channel thumbnails (56) {:>9.0} ms", ms(t));
|
||
let t = Instant::now();
|
||
std::hint::black_box(photocraft_compose::thumbnail(&d, 384));
|
||
println!("histogram source (384) {:>9.0} ms", ms(t));
|
||
}
|
||
"filter" => {
|
||
let t = Instant::now();
|
||
exec(&mut s, "filter.blur.gaussianBlur", json!({"radius": 10.0}));
|
||
let t1 = ms(t);
|
||
let t = Instant::now();
|
||
let path = refresh(gpu, &s, false);
|
||
println!("Gaussian Blur r 10 {t1:>9.0} ms");
|
||
println!(" refresh ({path:<8}) {:>9.0} ms", ms(t));
|
||
}
|
||
"brush" => {
|
||
exec(&mut s, "layer.new.layer", json!({"name": "paint"}));
|
||
refresh(gpu, &s, true);
|
||
for k in 0..3 {
|
||
let (x, y) = (300 + k * 2500, 300 + k * 1500);
|
||
let pts: Vec<Value> = (0..40).map(|i| json!([x + i * 30, y + ((i as f32 * 0.4).sin() * 80.0) as i32])).collect();
|
||
let t = Instant::now();
|
||
exec(&mut s, "paint.stroke", json!({"points": pts, "size": 120, "hardness": 0.5, "color": "#2040c0", "spacing": 0.25}));
|
||
let t1 = ms(t);
|
||
let t = Instant::now();
|
||
let path = refresh(gpu, &s, false);
|
||
println!("brush stroke 40 dabs {t1:>9.0} ms refresh ({path}) {:>6.0} ms", ms(t));
|
||
}
|
||
}
|
||
"psd" | "png" | "jpeg" | "pcraft" => {
|
||
let ext = if op == "jpeg" { "jpg" } else { op.as_str() };
|
||
let d = doc();
|
||
let t = Instant::now();
|
||
let out = photocraft_io::export(&d, &format!("x.{ext}"), &Default::default()).expect("export");
|
||
println!("save {ext:<6} ({:>5} MB) {:>9.0} ms", out.bytes.len() >> 20, ms(t));
|
||
}
|
||
other => eprintln!("unknown --op {other}"),
|
||
}
|
||
}
|