mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-02 06:17:22 -06:00
The first version slowed small brushes by about a third (reviewer's measurement: a 20 px soft zigzag 856 → 1736 ms over 6000 dabs), for three reasons, all gone: - `CoverageMap::accumulate` looked the dab's tiles up by walking the whole tile map (`iter_mut().filter_map`) on every dab, so a long stroke's thousands of tiles taxed every small dab. The touched tiles now leave the map (`remove`), accumulate, and go back: one to four lookups for a small tip. - The composite allocated a fresh 64 KB buffer per tile per frame (6872 `brk` calls per 3000 dabs against main's 542): the heap grew and trimmed every frame. The buffers live in a pool on the renderer (`RegionPool`, emptied on clone) and are reused from composite to composite. - The row / tile bodies passed as closures to the helpers cost a 20 px tip's `rasterize` half as much again (7.5 → 11.6 µs). They are `#[inline(always)]` steps on `Copy` structs (`RoundTip::row`, `DabValues::tile`), called from plain loops below the threshold and from rayon above it; `rasterize` is back to main's time. `examples/stroke_bench.rs` times a long zigzag stroke of one tip, push + composite per point (best of four, loaded machine; main → now): 20 px 667 → 706 ms, 50 px 1805 → 1734 ms, 150 px 11 017 → 5163 ms. Comments: the per-tile copy of `src` in the composite is the intended reading (a tile without per-dab colour only occurs in strokes without per-dab colour, where `src` is never rewritten), and `set_parallel_min_pixels` changes a process-wide static harmlessly. `parallel()` and the helpers' `pixels` are `cfg`'d so the wasm build has no unused warnings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
65 lines
3.0 KiB
Rust
65 lines
3.0 KiB
Rust
//! A long stroke of one tip, timed per dab: a zigzag over a 4928×3280 RGBA8 document that sweeps
|
||
//! the whole page, `push` then `composite` after every point, as painting does per frame. Small
|
||
//! tips are the everyday case; a long stroke grows the stroke's tile map to thousands of tiles,
|
||
//! and the time per dab must not grow with it.
|
||
//! `cargo run --release -p photocraft-paint --example stroke_bench -- [size] [hardness] [dabs]`
|
||
use photocraft_color::PixelFormat;
|
||
use photocraft_geom::Rect;
|
||
use photocraft_paint::{BrushSettings, StrokePoint, StrokeRenderer};
|
||
use photocraft_raster::Surface;
|
||
use std::time::Instant;
|
||
|
||
fn main() {
|
||
let args: Vec<String> = std::env::args().collect();
|
||
let size: f32 = args.get(1).and_then(|s| s.parse().ok()).unwrap_or(20.0);
|
||
let hardness: f32 = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(0.0);
|
||
let dabs: usize = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(6000);
|
||
let (w, h) = (4928.0f64, 3280.0f64);
|
||
let brush = BrushSettings { size, hardness, spacing: 0.25, color: [1.0, 0.0, 0.0, 1.0], ..Default::default() };
|
||
let fmt = PixelFormat::RGBA8;
|
||
let mut pre = Surface::new(fmt);
|
||
pre.fill_rect(Rect::new(0, 0, w as i32, h as i32), &[0.5, 0.5, 0.5, 1.0]);
|
||
let mut target = pre.clone();
|
||
// Segments from the top margin to the bottom one and back, advancing across the width so that
|
||
// `dabs` dabs at the brush's spacing sweep the document once.
|
||
let step = f64::from(size) * 0.25;
|
||
let margin = f64::from(size);
|
||
let span = h - 2.0 * margin;
|
||
let segments = ((dabs as f64 * step) / span).ceil().max(1.0) as usize;
|
||
let dx = (w - 2.0 * margin) / segments as f64;
|
||
let mut r = StrokeRenderer::new(&brush, Some(fmt), 1.0);
|
||
let (mut t_push, mut t_comp) = (0.0f64, 0.0f64);
|
||
let t_all = Instant::now();
|
||
let mut pos = (margin, margin);
|
||
r.push(&[StrokePoint::new(pos.0, pos.1, 1.0)]);
|
||
let mut pushes = 0usize;
|
||
'stroke: for seg in 0..segments {
|
||
let (x0, y0) = pos;
|
||
let (x1, y1) = (margin + (seg + 1) as f64 * dx, if seg % 2 == 0 { h - margin } else { margin });
|
||
let n = (((x1 - x0).powi(2) + (y1 - y0).powi(2)).sqrt() / step).ceil().max(1.0) as usize;
|
||
for i in 1..=n {
|
||
let f = i as f64 / n as f64;
|
||
let t = Instant::now();
|
||
r.push(&[StrokePoint::new(x0 + (x1 - x0) * f, y0 + (y1 - y0) * f, 1.0)]);
|
||
let tp = t.elapsed().as_secs_f64();
|
||
r.composite(&pre, &mut target, None, false, false);
|
||
t_comp += t.elapsed().as_secs_f64() - tp;
|
||
t_push += tp;
|
||
pushes += 1;
|
||
if r.dab_count() >= dabs {
|
||
break 'stroke;
|
||
}
|
||
}
|
||
pos = (x1, y1);
|
||
}
|
||
let total = t_all.elapsed().as_secs_f64();
|
||
let placed = r.dab_count();
|
||
println!(
|
||
"tip {size} px, hardness {hardness}: {placed} dabs in {pushes} pushes over {segments} segments: {:.0} ms (push {:.0}, composite {:.0}), {:.1} µs per dab",
|
||
total * 1e3,
|
||
t_push * 1e3,
|
||
t_comp * 1e3,
|
||
total * 1e6 / placed.max(1) as f64
|
||
);
|
||
}
|