Files
photocraft/crates/paint/examples/stroke_bench.rs
T
UnemarsandClaude Fable 5.1 4b06b0cc5a Brush: small tips keep their speed next to the parallel large ones
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>
2026-10-18 15:53:07 +02:00

65 lines
3.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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
);
}