mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-28 10:11:58 +01:00
Releasing a Dodge, Burn, Sponge, Blur, Sharpen or Smudge stroke ran every
dab of the stroke again on the UI thread (70-350 ms for a 960 px stroke on
24 MP, growing with stroke length). The Brush, Clone Stamp and History
Brush already hand their live pixels to the commit; LiveDab didn't, and
its preview wasn't the commit's pixels anyway: its working copy was a
native-depth Surface rounded after every dab (at 8-bit a live Blur
differed on 21% of pixels by up to 3 levels), the finishing dabs weren't
run, and canvas points carried tilt/times the commands don't take.
- LiveDab keeps its working copy in f32 tiles (WorkTiles), drops tilt and
times like LiveRetouch, and into_commit runs the finishing dabs.
- Session::offer_live_dab; dab_cmd takes a matching offer ("fromLive").
Refused when the brush, tool options or layer changed mid-stroke, and
for Color Replacement.
- mix_back leaves pixels the dabs didn't change exactly as they were.
- The canvas offers the live stroke on release.
Release: Blur 20/70/140/303 ms -> 0.2-0.4 ms at 50/150/300/600 px;
Smudge and Dodge likewise. Live frame cost unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
88 lines
3.7 KiB
Rust
88 lines
3.7 KiB
Rust
//! Release cost of a live sequential-dab stroke on a 24 MP layer, in release mode: the commit
|
|
//! rendering the stroke again (no offer) against taking the live pixels (`offer_live_dab`, which
|
|
//! runs the finishing dabs, plus the commit). Also the live stroke's per-frame cost while drawn.
|
|
use std::time::Instant;
|
|
|
|
use photocraft_engine::{EngineError, Session, paint::StrokePoint, retouch_cmds::LiveDab};
|
|
use photocraft_geom::Rect;
|
|
use serde_json::json;
|
|
|
|
const FRAMES: usize = 60;
|
|
|
|
fn ms(t: Instant) -> f64 {
|
|
t.elapsed().as_secs_f64() * 1e3
|
|
}
|
|
|
|
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut s = Session::new();
|
|
s.execute("file.new", json!({"width":6000,"height":4000,"depth":8,"mode":"rgb"}))?;
|
|
s.execute("layer.new.layer", json!({}))?;
|
|
s.edit("stripes", |doc, active| {
|
|
let id = active.ok_or_else(|| EngineError::Other("no layer".into()))?;
|
|
let surface = doc.layer_mut(id).and_then(|l| l.surface_mut()).ok_or_else(|| EngineError::Other("no surface".into()))?;
|
|
for x in (0..6000).step_by(64) {
|
|
let c = if x % 128 == 0 { [0.9, 0.3, 0.1, 1.0] } else { [0.1, 0.4, 0.8, 1.0] };
|
|
surface.fill_rect(Rect::new(x, 0, (x + 64).min(6000), 4000), &c);
|
|
}
|
|
Ok(())
|
|
})?;
|
|
// ~1000 px/s at 60 fps: 16 px of drag per frame, in four samples.
|
|
let pts: Vec<[f64; 2]> = std::iter::once([1000.0, 2000.0])
|
|
.chain((1..=FRAMES).flat_map(|f| (1..=4).map(move |k| [1000.0 + (f as f64 - 1.0) * 16.0 + k as f64 * 4.0, 2000.0])))
|
|
.collect();
|
|
for cmd in ["paint.blur", "paint.smudge", "paint.dodge"] {
|
|
for size in [50, 150, 300, 600] {
|
|
let p = |v: &[[f64; 2]]| json!({"points": v, "size": size, "hardness": 0, "strength": 50, "spacing": 25});
|
|
let draw = |s: &Session| -> Result<(LiveDab, Vec<f64>), Box<dyn std::error::Error>> {
|
|
let mut live = LiveDab::begin(s, cmd, &p(&pts[..1]))?;
|
|
let mut frames = Vec::new();
|
|
for batch in pts[1..].chunks(4) {
|
|
let b: Vec<_> = batch.iter().map(|q| StrokePoint::new(q[0], q[1], 1.0)).collect();
|
|
let t = Instant::now();
|
|
live.push(&b)?;
|
|
frames.push(ms(t));
|
|
}
|
|
Ok((live, frames))
|
|
};
|
|
let (mut before, mut after) = (Vec::new(), Vec::new());
|
|
let mut frames = Vec::new();
|
|
for _ in 0..3 {
|
|
let (_, f) = draw(&s)?;
|
|
frames.extend(f);
|
|
let t = Instant::now();
|
|
s.execute(cmd, p(&pts))?;
|
|
before.push(ms(t));
|
|
s.execute("edit.undo", json!({}))?;
|
|
let (live, _) = draw(&s)?;
|
|
let t = Instant::now();
|
|
s.offer_live_dab(live)?;
|
|
let r = s.execute(cmd, p(&pts))?;
|
|
after.push(ms(t));
|
|
if r.get("fromLive").is_none() {
|
|
return Err("the commit didn't take the live pixels".into());
|
|
}
|
|
s.execute("edit.undo", json!({}))?;
|
|
}
|
|
before.sort_by(f64::total_cmp);
|
|
after.sort_by(f64::total_cmp);
|
|
frames.sort_by(f64::total_cmp);
|
|
let total: f64 = frames.iter().sum::<f64>() / 3.0;
|
|
println!(
|
|
"{cmd:12} size={size:4} release: render again={:7.1} ms take live={:6.1} ms | live frames: max={:6.2} ms total={:7.1} ms",
|
|
before[1],
|
|
after[1],
|
|
frames.last().copied().unwrap_or(0.0),
|
|
total
|
|
);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn main() {
|
|
if let Err(e) = run() {
|
|
eprintln!("{e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|