mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-28 23:06:53 +01:00
perf(liquify): reuse interpolation taps and bound render buffers (#8855)
* perf(liquify): reuse cubic field taps across tile axes * perf(liquify): bound completed render tiles
This commit is contained in:
1 parent
0c124bf388
commit
08a606c7d1
1 file changed
+67
-22
+67
-22
@@ -191,6 +191,9 @@ struct StrokeCursor {
|
||||
/// 125,000); smaller ones are not worth the hand-off.
|
||||
const PARALLEL_DAB_NODES: usize = 16_384;
|
||||
|
||||
/// Bound completed output buffers independently of the document's size.
|
||||
const RENDER_BATCH_TILES: usize = 64;
|
||||
|
||||
/// Distance between dabs along a stroke for brush radius `r`: a twentieth of the radius, so a
|
||||
/// drag is many small pushes and its edge stays smooth.
|
||||
fn dab_spacing(r: f64) -> f64 {
|
||||
@@ -593,14 +596,11 @@ impl LiquifyField {
|
||||
}
|
||||
|
||||
/// Catmull-Rom (bicubic) displacement at grid coordinates.
|
||||
fn sample_cubic(&self, gx: f64, gy: f64, wx: &[f64; 4], wy: &[f64; 4]) -> [f64; 2] {
|
||||
let (ix, iy) = (gx.floor() as isize, gy.floor() as isize);
|
||||
fn sample_cubic(&self, xs: &[(usize, f64); 4], ys: &[(usize, f64); 4]) -> [f64; 2] {
|
||||
let mut acc = [0.0f64; 2];
|
||||
for (jj, wyj) in wy.iter().enumerate() {
|
||||
let y = (iy - 1 + jj as isize).clamp(0, self.h as isize - 1) as usize;
|
||||
for &(y, wyj) in ys {
|
||||
let row = &self.d[y * self.w..(y + 1) * self.w];
|
||||
for (ii, wxi) in wx.iter().enumerate() {
|
||||
let x = (ix - 1 + ii as isize).clamp(0, self.w as isize - 1) as usize;
|
||||
for &(x, wxi) in xs {
|
||||
let k = wxi * wyj;
|
||||
acc[0] += f64::from(row[x][0]) * k;
|
||||
acc[1] += f64::from(row[x][1]) * k;
|
||||
@@ -660,6 +660,13 @@ fn catmull_rom(t: f64) -> [f64; 4] {
|
||||
[-0.5 * t3 + t2 - 0.5 * t, 1.5 * t3 - 2.5 * t2 + 1.0, -1.5 * t3 + 2.0 * t2 + 0.5 * t, 0.5 * t3 - 0.5 * t2]
|
||||
}
|
||||
|
||||
/// A field axis's clamped nodes and weights, reusable across a tile's rows or columns.
|
||||
fn cubic_taps(g: f64, len: usize) -> [(usize, f64); 4] {
|
||||
let i = g.floor() as isize;
|
||||
let weights = catmull_rom(g - g.floor());
|
||||
std::array::from_fn(|k| ((i - 1 + k as isize).clamp(0, len as isize - 1) as usize, weights[k]))
|
||||
}
|
||||
|
||||
/// Resamples `src` through the field: inside `field.bounds` each pixel shows the source at
|
||||
/// `p + d(p)` (bicubic field, bicubic premultiplied source, edge pixels repeat past the field
|
||||
/// bounds); everything outside the bounds is kept. Same format as `src`.
|
||||
@@ -680,19 +687,24 @@ pub fn apply_liquify(src: &Surface, field: &LiquifyField) -> Surface {
|
||||
let work = |t: &Rect| -> Option<(Rect, Vec<f32>)> {
|
||||
let w = t.width() as usize;
|
||||
let h = t.height() as usize;
|
||||
// Field per pixel (bicubic), rows share their y weights.
|
||||
// Field per pixel (bicubic), with axis taps shared across rows and columns.
|
||||
let mut disp = vec![[0.0f64; 2]; w * h];
|
||||
let (mut fx0, mut fy0, mut fx1, mut fy1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
|
||||
let mut moved = false;
|
||||
let columns: Vec<_> = (0..w)
|
||||
.map(|x| {
|
||||
let px = f64::from(t.x0) + x as f64 + 0.5;
|
||||
let gx = (px - f64::from(b.x0) - 0.5) / field.cell;
|
||||
(px, cubic_taps(gx, field.w))
|
||||
})
|
||||
.collect();
|
||||
for y in 0..h {
|
||||
let py = f64::from(t.y0) + y as f64 + 0.5;
|
||||
let gy = (py - f64::from(b.y0) - 0.5) / field.cell;
|
||||
let wy = catmull_rom(gy - gy.floor());
|
||||
for x in 0..w {
|
||||
let px = f64::from(t.x0) + x as f64 + 0.5;
|
||||
let gx = (px - f64::from(b.x0) - 0.5) / field.cell;
|
||||
let wx = catmull_rom(gx - gx.floor());
|
||||
let mut d = field.sample_cubic(gx, gy, &wx, &wy);
|
||||
let ys = cubic_taps(gy, field.h);
|
||||
for (x, (px, xs)) in columns.iter().enumerate() {
|
||||
let px = *px;
|
||||
let mut d = field.sample_cubic(xs, &ys);
|
||||
if has_freeze {
|
||||
let mask = f64::from(field.freeze_at(px, py)).clamp(0.0, 1.0);
|
||||
if mask > 0.0 {
|
||||
@@ -774,15 +786,10 @@ pub fn apply_liquify(src: &Surface, field: &LiquifyField) -> Surface {
|
||||
}
|
||||
Some((*t, outp))
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let done: Vec<(Rect, Vec<f32>)> = {
|
||||
use rayon::prelude::*;
|
||||
tiles.par_iter().filter_map(work).collect()
|
||||
};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let done: Vec<(Rect, Vec<f32>)> = tiles.iter().filter_map(work).collect();
|
||||
for (r, v) in done {
|
||||
out.write_region(r, &v);
|
||||
for batch in tiles.chunks(RENDER_BATCH_TILES) {
|
||||
for (r, v) in par::map(batch.len(), true, |i| batch.get(i).and_then(work)).into_iter().flatten() {
|
||||
out.write_region(r, &v);
|
||||
}
|
||||
}
|
||||
if alpha {
|
||||
out.prune();
|
||||
@@ -1318,6 +1325,44 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_wide_integer_translation_keeps_channels_and_tile_edges_at_every_format() {
|
||||
let b = Rect::new(13, -1, 13 + 70 * 256, 1);
|
||||
let w = b.width() as usize;
|
||||
for mode in [ColorMode::Grayscale, ColorMode::Rgb, ColorMode::Cmyk] {
|
||||
for depth in [SampleType::U8, SampleType::U16, SampleType::F32] {
|
||||
let format = PixelFormat::new(mode, depth, true);
|
||||
let n = format.channels();
|
||||
let mut data = Vec::new();
|
||||
for i in 0..w * 2 {
|
||||
let alpha = if i % 13 == 0 { 0.0 } else { 0.5 };
|
||||
for c in 0..mode.color_channels() {
|
||||
data.push(if alpha == 0.0 { 0.0 } else { ((i * 19 + c * 43) % 251 + 2) as f32 / 255.0 });
|
||||
}
|
||||
data.push(alpha);
|
||||
}
|
||||
let mut source = Surface::new(format);
|
||||
source.write_region(b, &data);
|
||||
source.write_pixel(b.x0 - 1, b.y0 - 1, &vec![0.75; n]);
|
||||
let before = source.read_region(b);
|
||||
let mut field = LiquifyField::new(b, 2.5);
|
||||
field.d.fill([-1.0, 0.0]);
|
||||
let output = apply_liquify(&source, &field);
|
||||
let after = output.read_region(b);
|
||||
let tolerance = if depth == SampleType::F32 { 4.0 * f32::EPSILON } else { 0.0 };
|
||||
for (original, moved) in before.chunks_exact(w * n).zip(after.chunks_exact(w * n)) {
|
||||
for (x, pixel) in moved.chunks_exact(n).enumerate() {
|
||||
let offset = x.saturating_sub(1) * n;
|
||||
for (actual, expected) in pixel.iter().zip(&original[offset..offset + n]) {
|
||||
assert!((actual - expected).abs() <= tolerance, "{mode:?}/{depth:?} at column {x}");
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(output.pixel(b.x0 - 1, b.y0 - 1), source.pixel(b.x0 - 1, b.y0 - 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_strokes_is_identity_at_every_depth() {
|
||||
for st in [SampleType::U8, SampleType::U16, SampleType::F32] {
|
||||
|
||||
Reference in new issue
Block a user