699 lines
30 KiB
Rust
699 lines
30 KiB
Rust
//! Effect maps on the GPU (`photocraft_compose::effects::build_maps`).
|
||
//!
|
||
//! Every enabled effect of a layer becomes a small *map program*: a chain of single-channel
|
||
//! fragment passes over the layer's effect region (shift, dilate, separable Gaussian blur, glow
|
||
//! ramp, bevel height and shading, contour, stroke band), each mirroring one step of the CPU
|
||
//! builders. Programs are pure functions of the effect settings and the global light.
|
||
//!
|
||
//! Inputs come from the CPU, computed by compose itself so they match it bit for bit: the
|
||
//! layer's **shape** (`compose::layer_shape`) and the **distance fields** of it that the effects
|
||
//! use (`compose::effects::distance_field`, an inherently sequential transform). Both are
|
||
//! computed per layer state and uploaded once, and only where the layer changed:
|
||
//!
|
||
//! - damage is the union of the layer's tiles whose copy-on-write `Arc` changed, so a brush dab
|
||
//! recomputes one 256² tile of shape;
|
||
//! - a distance field is exact up to the distance its effects can use (beyond it every consumer
|
||
//! saturates), so it is recomputed over the damage grown by that reach, from a window grown by
|
||
//! the reach again; large windows split into bands computed in parallel;
|
||
//! - each program re-runs only over the damage grown by its own reach, and only for effects
|
||
//! whose settings changed; an unrelated edit (another layer, an adjustment, the effect's colour
|
||
//! or opacity) reuses everything.
|
||
|
||
use std::collections::HashMap;
|
||
use std::hash::{Hash, Hasher};
|
||
use std::sync::Arc;
|
||
|
||
use photocraft_compose::effects::BevelPaint;
|
||
use photocraft_compose::effects::FieldKind;
|
||
use photocraft_doc::{BevelTechnique, Contour, Effect, GlobalLight, GlowSource, GlowTechnique, Layer, LayerContent, Pattern};
|
||
use photocraft_geom::{Rect, TileCoord};
|
||
use photocraft_raster::{Surface, Tile};
|
||
|
||
use crate::plan::{Kernel, stroke_widths};
|
||
|
||
/// Input of a map stage.
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||
pub(crate) enum In {
|
||
/// The layer's alpha over the region.
|
||
Shape,
|
||
/// A distance field of the shape.
|
||
Field(FieldKind),
|
||
/// Output of stage `n`.
|
||
Val(usize),
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
pub(crate) struct Stage {
|
||
pub kernel: Kernel,
|
||
pub a: Option<In>,
|
||
pub b: Option<In>,
|
||
/// Bound as `layer_tex` (the shape for `× shape` steps).
|
||
pub s: Option<In>,
|
||
/// LUT row 0 (blur weights, contour, sine profile).
|
||
pub lut: Option<Arc<Vec<f32>>>,
|
||
pub p0: [f32; 4],
|
||
pub p1: [f32; 4],
|
||
/// Pattern placement (`p3`: origin, rotation) and `p4` (inverse scale, tile size) for stages
|
||
/// that sample `pattern` (bevel texture).
|
||
pub p3: [f32; 4],
|
||
pub p4: [f32; 4],
|
||
pub pattern: Option<Pattern>,
|
||
/// How far (px) the stage reads from its inputs around the output pixel.
|
||
pub radius: i32,
|
||
/// Final map index this stage writes (else a temporary).
|
||
pub out: Option<usize>,
|
||
}
|
||
|
||
/// The passes building one effect's maps.
|
||
#[derive(Clone, Debug)]
|
||
pub(crate) struct MapProgram {
|
||
/// Identity of the program (settings that change the maps; not colours or opacities).
|
||
pub key: u64,
|
||
pub maps: usize,
|
||
pub stages: Vec<Stage>,
|
||
/// Distance fields read, with the distance up to which each must be exact.
|
||
pub fields: Vec<(FieldKind, i32)>,
|
||
}
|
||
|
||
/// How far a field value depends on the shape, for a field exact up to `reach`.
|
||
pub(crate) fn field_radius(_kind: FieldKind, reach: i32) -> i32 {
|
||
// The nearest seed lies within the reach (seeds may start up to 1 px in, at their sub-pixel
|
||
// edge); stroke fields classify seeds by their 3×3 local coverage, one more pixel.
|
||
reach + 3
|
||
}
|
||
|
||
impl MapProgram {
|
||
fn empty() -> Self {
|
||
MapProgram { key: 0, maps: 0, stages: Vec::new(), fields: Vec::new() }
|
||
}
|
||
|
||
/// Per-stage output window for a shape damage rect `d` (all inside `region`): what each stage
|
||
/// must recompute so every final map is exact over `d` grown by its reach. `field_reach`:
|
||
/// the reach each field was computed with.
|
||
pub fn windows(&self, d: Rect, region: Rect, field_reach: &dyn Fn(FieldKind) -> i32) -> Vec<Rect> {
|
||
let n = self.stages.len();
|
||
let reach_of = |reach: &[i32], i: Option<In>| match i {
|
||
Some(In::Val(k)) => reach[k],
|
||
Some(In::Field(f)) => field_radius(f, field_reach(f)),
|
||
_ => 0,
|
||
};
|
||
let mut reach = vec![0i32; n];
|
||
for (i, s) in self.stages.iter().enumerate() {
|
||
reach[i] = reach_of(&reach, s.a).max(reach_of(&reach, s.b)).max(reach_of(&reach, s.s)) + s.radius;
|
||
}
|
||
let mut need = vec![Rect::EMPTY; n];
|
||
for (i, s) in self.stages.iter().enumerate() {
|
||
if s.out.is_some() {
|
||
need[i] = d.inflate(reach[i]).intersect(®ion);
|
||
}
|
||
}
|
||
for i in (0..n).rev() {
|
||
let w = need[i];
|
||
if w.is_empty() {
|
||
continue;
|
||
}
|
||
let s = &self.stages[i];
|
||
for inp in [s.a, s.b, s.s].into_iter().flatten() {
|
||
if let In::Val(k) = inp {
|
||
let grown = w.inflate(s.radius).intersect(®ion);
|
||
need[k] = if need[k].is_empty() { grown } else { need[k].union(&grown) };
|
||
}
|
||
}
|
||
}
|
||
need
|
||
}
|
||
|
||
/// Physical temporary index per stage (None for final maps), reusing temporaries after
|
||
/// their last read; returns (assignment, temporaries needed).
|
||
pub fn temps(&self) -> (Vec<Option<usize>>, usize) {
|
||
let n = self.stages.len();
|
||
let mut last_use = vec![0usize; n];
|
||
for (i, s) in self.stages.iter().enumerate() {
|
||
for inp in [s.a, s.b, s.s].into_iter().flatten() {
|
||
if let In::Val(k) = inp {
|
||
last_use[k] = last_use[k].max(i);
|
||
}
|
||
}
|
||
}
|
||
let mut free: Vec<usize> = Vec::new();
|
||
let mut count = 0;
|
||
let mut out = vec![None; n];
|
||
let mut held: Vec<(usize, usize)> = Vec::new();
|
||
for (i, (stage, slot)) in self.stages.iter().zip(out.iter_mut()).enumerate() {
|
||
// Release temporaries whose last reader was before this stage.
|
||
held.retain(|&(st, t)| {
|
||
if last_use[st] < i {
|
||
free.push(t);
|
||
false
|
||
} else {
|
||
true
|
||
}
|
||
});
|
||
if stage.out.is_some() {
|
||
continue;
|
||
}
|
||
let t = free.pop().unwrap_or_else(|| {
|
||
count += 1;
|
||
count - 1
|
||
});
|
||
*slot = Some(t);
|
||
held.push((i, t));
|
||
}
|
||
(out, count)
|
||
}
|
||
}
|
||
|
||
struct B {
|
||
stages: Vec<Stage>,
|
||
fields: Vec<(FieldKind, i32)>,
|
||
}
|
||
|
||
fn stage(kernel: Kernel, a: Option<In>, b: Option<In>, p0: [f32; 4], radius: i32) -> Stage {
|
||
Stage { kernel, a, b, s: None, lut: None, p0, p1: [0.0; 4], p3: [0.0; 4], p4: [0.0; 4], pattern: None, radius, out: None }
|
||
}
|
||
|
||
impl B {
|
||
fn push(&mut self, s: Stage) -> In {
|
||
self.stages.push(s);
|
||
In::Val(self.stages.len() - 1)
|
||
}
|
||
fn field(&mut self, kind: FieldKind, width: f32) -> In {
|
||
self.fields.push((kind, reach_for(width)));
|
||
In::Field(kind)
|
||
}
|
||
/// `Map::shifted` by whole pixels (`invert`: of `1 - a`); identity shifts of the plain map
|
||
/// are skipped.
|
||
fn shift(&mut self, src: In, dx: f32, dy: f32, outside: f32, invert: bool) -> In {
|
||
if dx == 0.0 && dy == 0.0 && !invert {
|
||
return src;
|
||
}
|
||
let r = dx.abs().max(dy.abs()) as i32;
|
||
self.push(stage(Kernel::MShift, Some(src), None, [dx, dy, outside, f32::from(u8::from(invert))], r))
|
||
}
|
||
/// `dilate(src, r)` given `dist_outside(src)`.
|
||
fn dilate(&mut self, src: In, dist: In, r: f32) -> In {
|
||
self.push(stage(Kernel::MDilate, Some(src), Some(dist), [r, 0.0, 0.0, 0.0], 0))
|
||
}
|
||
fn blur(&mut self, src: In, size: f32) -> In {
|
||
let Some(k) = blur_kernel(size) else { return src };
|
||
self.conv(src, k)
|
||
}
|
||
/// Separable convolution with a symmetric kernel `(radius, weights)` (two `MBlur` passes).
|
||
fn conv(&mut self, src: In, (r, w): (i32, Vec<f32>)) -> In {
|
||
if r <= 0 {
|
||
return src;
|
||
}
|
||
let w = Arc::new(w);
|
||
let mut h = stage(Kernel::MBlur, Some(src), None, [0.0, r as f32, 0.0, 0.0], r);
|
||
h.lut = Some(w.clone());
|
||
let h = self.push(h);
|
||
let mut v = stage(Kernel::MBlur, Some(h), None, [1.0, r as f32, 0.0, 0.0], r);
|
||
v.lut = Some(w);
|
||
self.push(v)
|
||
}
|
||
fn finish(&mut self, a: In, b: Option<In>, invert: bool, contour: &Contour, times_shape: bool, out: usize) {
|
||
self.finish_lut(a, b, invert, contour_lut(contour), times_shape, out);
|
||
}
|
||
fn finish_lut(&mut self, a: In, b: Option<In>, invert: bool, lut: Option<Vec<f32>>, times_shape: bool, out: usize) {
|
||
let flag = |v: bool| f32::from(u8::from(v));
|
||
let mut s = stage(Kernel::MFinish, Some(a), b, [flag(b.is_some()), flag(invert), flag(lut.is_some()), flag(times_shape)], 0);
|
||
s.lut = lut.map(Arc::new);
|
||
if times_shape {
|
||
s.s = Some(In::Shape);
|
||
}
|
||
s.out = Some(out);
|
||
self.stages.push(s);
|
||
}
|
||
}
|
||
|
||
/// Distance up to which a field must be exact for a band / dilation of width `w`
|
||
/// (`clamp(w + 0.5 - d)`, with `d` up to 1.5 px beyond the pixel distance).
|
||
fn reach_for(w: f32) -> i32 {
|
||
w.clamp(0.0, photocraft_compose::effects::MAX_REACH).ceil() as i32 + 2
|
||
}
|
||
|
||
/// `compose::effects::blur`'s kernel (a tent of the effect size): (radius, normalised weights),
|
||
/// or None when it is the identity.
|
||
pub(crate) fn blur_kernel(size: f32) -> Option<(i32, Vec<f32>)> {
|
||
if !size.is_finite() {
|
||
return None;
|
||
}
|
||
let k = photocraft_compose::effects::tent_kernel(size);
|
||
(k.0 > 0).then_some(k)
|
||
}
|
||
|
||
fn contour_lut(c: &Contour) -> Option<Vec<f32>> {
|
||
match c {
|
||
Contour::Linear => None,
|
||
Contour::Custom { points, .. } => Some(photocraft_compose::adjust::curve_lut(points)),
|
||
}
|
||
}
|
||
|
||
/// `compose::effects::offset`: shadow offset away from the light, whole pixels.
|
||
fn offset(angle: f32, distance: f32) -> (f32, f32) {
|
||
let a = angle.to_radians();
|
||
((-a.cos() * distance).round(), (a.sin() * distance).round())
|
||
}
|
||
|
||
/// The map program of one enabled effect (`build_maps`), without pattern sources.
|
||
#[cfg(test)]
|
||
pub(crate) fn program(e: &Effect, light: &GlobalLight, vector_shape: bool) -> MapProgram {
|
||
program_with(e, light, vector_shape, &[], (0.0, 0.0))
|
||
}
|
||
|
||
/// The map program of one enabled effect (`build_maps`); bevel textures tile `patterns` from
|
||
/// `anchor` (`effects::TextureCtx`).
|
||
pub(crate) fn program_with(e: &Effect, light: &GlobalLight, vector_shape: bool, patterns: &[Pattern], anchor: (f64, f64)) -> MapProgram {
|
||
let mut b = B { stages: Vec::new(), fields: Vec::new() };
|
||
let maps = match e {
|
||
Effect::DropShadow(s) | Effect::InnerShadow(s) => {
|
||
// shadow_map
|
||
let inner = matches!(e, Effect::InnerShadow(_));
|
||
let angle = if s.use_global_light { light.angle } else { s.angle };
|
||
let (dx, dy) = offset(angle, s.distance);
|
||
let src = b.shift(In::Shape, dx, dy, if inner { 1.0 } else { 0.0 }, inner);
|
||
let (r, bw) = photocraft_compose::effects::spread_split(s.size, s.spread);
|
||
let mut m = src;
|
||
if r > 0.0 {
|
||
// dist_outside of the shifted map is the shifted field (integer offsets; shifted-in
|
||
// pixels are far away for a drop shadow, inside for an inner shadow).
|
||
let f = b.field(if inner { FieldKind::OutsideInverse } else { FieldKind::Outside }, r);
|
||
let d = if dx == 0.0 && dy == 0.0 {
|
||
f
|
||
} else {
|
||
b.push(stage(Kernel::MShift, Some(f), None, [dx, dy, if inner { -0.5 } else { 1e10 }, 0.0], dx.abs().max(dy.abs()) as i32))
|
||
};
|
||
m = b.dilate(src, d, r);
|
||
}
|
||
let m = b.blur(m, bw);
|
||
b.finish(m, None, false, &s.contour, inner, 0);
|
||
1
|
||
}
|
||
Effect::OuterGlow(g) | Effect::InnerGlow(g) => {
|
||
// glow_map
|
||
let inner = matches!(e, Effect::InnerGlow(_));
|
||
let edge = inner && g.source == GlowSource::Edge;
|
||
let center = inner && g.source == GlowSource::Center;
|
||
match g.technique {
|
||
GlowTechnique::Precise => {
|
||
let d = b.field(if edge { FieldKind::Inside } else { FieldKind::Outside }, g.size);
|
||
let solid = g.size * g.spread;
|
||
let soft = (g.size - solid).max(1e-3);
|
||
let m = b.push(stage(Kernel::MGlow, Some(d), None, [solid, soft, f32::from(u8::from(center)), 0.0], 0));
|
||
b.finish_lut(m, None, false, photocraft_compose::effects::glow_lut(g), inner, 0);
|
||
}
|
||
GlowTechnique::Softer => {
|
||
// Inner glows (edge, and centre as 1 - the edge result) spread 1 - alpha.
|
||
let src = if inner { b.shift(In::Shape, 0.0, 0.0, 0.0, true) } else { In::Shape };
|
||
let (r, bw) = photocraft_compose::effects::spread_split(g.size, g.spread);
|
||
let mut m = src;
|
||
if r > 0.0 {
|
||
let d = b.field(if inner { FieldKind::OutsideInverse } else { FieldKind::Outside }, r);
|
||
m = b.dilate(src, d, r);
|
||
}
|
||
let m = b.blur(m, bw);
|
||
b.finish_lut(m, None, center, photocraft_compose::effects::glow_lut(g), inner, 0);
|
||
}
|
||
}
|
||
1
|
||
}
|
||
Effect::Satin(s) => {
|
||
// satin_map
|
||
let (dx, dy) = offset(s.angle, s.distance);
|
||
let a = b.shift(In::Shape, dx, dy, 0.0, false);
|
||
let a = b.conv(a, photocraft_compose::effects::tent_kernel(s.size));
|
||
let c = b.shift(In::Shape, -dx, -dy, 0.0, false);
|
||
let c = b.conv(c, photocraft_compose::effects::tent_kernel(s.size));
|
||
b.finish(a, Some(c), s.invert, &s.contour, true, 0);
|
||
1
|
||
}
|
||
Effect::BevelEmboss(bv) => {
|
||
// bevel_maps: height map (tent blur of the shape, or chiselled distance ramps), then
|
||
// highlight / shadow shading inside and / or outside the shape.
|
||
let g = photocraft_compose::effects::bevel_geom(bv);
|
||
let size = bv.size.max(1.0);
|
||
let paint = |p: BevelPaint| match p {
|
||
BevelPaint::Outer => 0.0,
|
||
BevelPaint::Both => 1.0,
|
||
BevelPaint::Inner => 2.0,
|
||
};
|
||
let mut h = if bv.technique == BevelTechnique::Smooth {
|
||
b.conv(In::Shape, photocraft_compose::effects::tent_kernel(g.width))
|
||
} else {
|
||
let din = b.field(FieldKind::Inside, size);
|
||
let dout = b.field(FieldKind::Outside, size);
|
||
let h = b.push(stage(Kernel::MBevelH, Some(din), Some(dout), [paint(g.paint), size, 0.0, 0.0], 0));
|
||
if g.chisel_soft > 0.0 { b.conv(h, photocraft_compose::effects::tent_kernel(g.chisel_soft)) } else { h }
|
||
};
|
||
if let Some(c) = &bv.contour {
|
||
// Contour element: the height through the contour over its range.
|
||
let lut = photocraft_compose::effects::ranged_lut(&c.contour, c.range).unwrap_or_else(|| (0..4096).map(|k| k as f32 / 4095.0).collect());
|
||
let mut st = stage(Kernel::MFinish, Some(h), None, [0.0, 0.0, 1.0, 0.0], 0);
|
||
st.lut = Some(Arc::new(lut));
|
||
h = b.push(st);
|
||
}
|
||
if let Some(t) = &bv.texture
|
||
&& let Some(pat) = photocraft_doc::pattern::find(patterns, &t.id, &t.name).filter(|p| !p.is_empty())
|
||
{
|
||
// Texture element (`effects::bevel_height`): luminance × depth / unit added.
|
||
let unit = if bv.depth.abs() > 1e-6 { (g.depth / bv.depth).abs().max(1e-3) } else { g.width.max(1.0) };
|
||
let pl = photocraft_compose::pattern::Placement::anchored(anchor, t.link, t.phase, t.scale, 0.0);
|
||
let (origin, cs, inv) = pl.parts();
|
||
let mut st = stage(Kernel::MBevelTex, Some(h), None, [t.depth / unit, f32::from(u8::from(t.invert)), 0.0, 0.0], 0);
|
||
st.p3 = [origin.0 as f32, origin.1 as f32, cs.0 as f32, cs.1 as f32];
|
||
st.p4 = [inv as f32, pat.width as f32, pat.height as f32, 0.0];
|
||
st.pattern = Some(pat.clone());
|
||
h = b.push(st);
|
||
}
|
||
if bv.soften >= 1.0 {
|
||
h = b.conv(h, photocraft_compose::effects::tent_kernel(bv.soften));
|
||
}
|
||
let (angle, altitude) = if bv.use_global_light { (light.angle, light.altitude) } else { (bv.angle, bv.altitude) };
|
||
let (sa, ca) = angle.to_radians().sin_cos();
|
||
let (se, ce) = altitude.to_radians().sin_cos();
|
||
let lut = contour_lut(&bv.gloss_contour).map(Arc::new);
|
||
let passes: Vec<(f32, bool)> = match g.paint {
|
||
BevelPaint::Inner => vec![(g.depth, false)],
|
||
BevelPaint::Outer => vec![(g.depth, true)],
|
||
BevelPaint::Both => vec![(g.depth, false), (if g.pillow { -g.depth } else { g.depth }, true)],
|
||
};
|
||
let mut out = 0;
|
||
for (depth, outer) in &passes {
|
||
for which in [0.0, 1.0] {
|
||
let mut s = stage(Kernel::MBevelShade, Some(h), None, [ca * ce, -sa * ce, se, se], 1);
|
||
s.p1 = [*depth, f32::from(u8::from(*outer)), which, f32::from(u8::from(lut.is_some()))];
|
||
s.lut = lut.clone();
|
||
s.s = Some(In::Shape);
|
||
s.out = Some(out);
|
||
out += 1;
|
||
b.stages.push(s);
|
||
}
|
||
}
|
||
out
|
||
}
|
||
Effect::Stroke(st) => {
|
||
// composite_with_effects' stroke bands (maps 0 outside, 1 inside)
|
||
let (in_w, out_w) = stroke_widths(st);
|
||
if out_w > 0.0 {
|
||
let d = b.field(if vector_shape { FieldKind::StrokeOutsideVector } else { FieldKind::StrokeOutside }, out_w);
|
||
let mut s = stage(Kernel::MStroke, Some(d), None, [out_w, 0.0, 0.0, 0.0], 0);
|
||
s.out = Some(0);
|
||
b.stages.push(s);
|
||
}
|
||
if in_w > 0.0 {
|
||
let d = b.field(FieldKind::StrokeInside, in_w);
|
||
let mut s = stage(Kernel::MStroke, Some(d), None, [in_w, 0.0, 0.0, 0.0], 0);
|
||
s.out = Some(1);
|
||
b.stages.push(s);
|
||
}
|
||
2
|
||
}
|
||
Effect::ColorOverlay { .. } | Effect::GradientOverlay { .. } | Effect::PatternOverlay { .. } => return MapProgram::empty(),
|
||
};
|
||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||
for s in &b.stages {
|
||
(format!("{:?}", s.kernel), s.a, s.b, s.s, s.radius, s.out).hash(&mut h);
|
||
for v in s.p0.iter().chain(&s.p1).chain(&s.p3).chain(&s.p4) {
|
||
v.to_bits().hash(&mut h);
|
||
}
|
||
if let Some(p) = &s.pattern {
|
||
(&p.id, &p.name, p.width, p.height).hash(&mut h);
|
||
for (c, t) in p.surface.tiles() {
|
||
(c.tx, c.ty, Arc::as_ptr(t) as usize).hash(&mut h);
|
||
}
|
||
}
|
||
if let Some(l) = &s.lut {
|
||
for v in l.iter() {
|
||
v.to_bits().hash(&mut h);
|
||
}
|
||
}
|
||
}
|
||
(maps, &b.fields).hash(&mut h);
|
||
MapProgram { key: h.finish() | 1, maps, stages: b.stages, fields: b.fields }
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------
|
||
// CPU inputs: shape and distance fields
|
||
|
||
/// Row-major crop of the region-sized `shape` to `r` (inside `region`).
|
||
fn crop(shape: &[f32], region: Rect, r: Rect) -> Vec<f32> {
|
||
let sw = region.width() as usize;
|
||
let mut out = Vec::with_capacity(r.width() as usize * r.height() as usize);
|
||
for y in r.y0..r.y1 {
|
||
let o = (y - region.y0) as usize * sw + (r.x0 - region.x0) as usize;
|
||
out.extend_from_slice(&shape[o..o + r.width() as usize]);
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Rows `[y0, y1)` of `r` split into bands of about `rows`.
|
||
fn bands(r: Rect, rows: i32) -> Vec<Rect> {
|
||
let mut out = Vec::new();
|
||
let mut y = r.y0;
|
||
while y < r.y1 {
|
||
out.push(Rect::new(r.x0, y, r.x1, (y + rows).min(r.y1)));
|
||
y += rows;
|
||
}
|
||
out
|
||
}
|
||
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
fn par_map<T: Send, R: Send>(items: Vec<T>, f: impl Fn(T) -> R + Sync + Send) -> Vec<R> {
|
||
use rayon::prelude::*;
|
||
items.into_par_iter().map(f).collect()
|
||
}
|
||
|
||
#[cfg(target_arch = "wasm32")]
|
||
fn par_map<T, R>(items: Vec<T>, f: impl Fn(T) -> R) -> Vec<R> {
|
||
items.into_iter().map(f).collect()
|
||
}
|
||
|
||
/// Band height for parallel field / shape computation.
|
||
const BAND: i32 = 128;
|
||
|
||
/// Field `kind` (exact up to `reach`) over `out` (inside `region`), computed from the
|
||
/// region-sized `shape`. Values within the reach depend only on the shape within
|
||
/// [`field_radius`], so each band is computed from a window grown by it.
|
||
pub(crate) fn field(kind: FieldKind, reach: i32, shape: &[f32], region: Rect, out: Rect) -> Vec<f32> {
|
||
let halo = field_radius(kind, reach) + 1;
|
||
let parts = par_map(bands(out, BAND.max(halo)), |band| {
|
||
let win = band.inflate(halo).intersect(®ion);
|
||
let f = photocraft_compose::effects::distance_field(kind, crop(shape, region, win), win.width() as usize, win.height() as usize);
|
||
crop(&f, win, band)
|
||
});
|
||
parts.concat()
|
||
}
|
||
|
||
/// The layer's shape (`compose::layer_shape`) over `r`, in parallel bands.
|
||
pub(crate) fn shape(doc: &photocraft_doc::Document, layer: &Layer, r: Rect) -> Vec<f32> {
|
||
if r.is_empty() {
|
||
return Vec::new();
|
||
}
|
||
par_map(bands(r, BAND), |band| photocraft_compose::layer_shape(doc, layer, band)).concat()
|
||
}
|
||
|
||
/// Write `v` (row-major over `r`) into the region-sized `shape`.
|
||
pub(crate) fn paste(shape: &mut [f32], region: Rect, r: Rect, v: &[f32]) {
|
||
let sw = region.width() as usize;
|
||
let w = r.width() as usize;
|
||
for (row, src) in v.chunks_exact(w).enumerate() {
|
||
let o = (r.y0 - region.y0) as usize * sw + row * sw + (r.x0 - region.x0) as usize;
|
||
shape[o..o + w].copy_from_slice(src);
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------------------------
|
||
// Shape identity and damage
|
||
|
||
pub(crate) type Tiles = HashMap<TileCoord, Arc<Tile>>;
|
||
|
||
pub(crate) fn snapshot(s: &Surface) -> Tiles {
|
||
s.tiles().map(|(c, t)| (*c, t.clone())).collect()
|
||
}
|
||
|
||
/// Union of the tiles that differ between `old` and `s`.
|
||
pub(crate) fn damage(old: &Tiles, s: &Surface) -> Rect {
|
||
let mut d = Rect::EMPTY;
|
||
let mut add = |r: Rect| d = if d.is_empty() { r } else { d.union(&r) };
|
||
for (c, t) in s.tiles() {
|
||
if old.get(c).is_none_or(|o| !Arc::ptr_eq(o, t)) {
|
||
add(c.rect());
|
||
}
|
||
}
|
||
for c in old.keys() {
|
||
if s.tile(*c).is_none() {
|
||
add(c.rect());
|
||
}
|
||
}
|
||
d
|
||
}
|
||
|
||
/// The surfaces a non-group layer's shape is read from: (content, mask).
|
||
pub(crate) fn shape_sources(layer: &Layer) -> (Option<&Surface>, Option<&Surface>) {
|
||
let content = match &layer.content {
|
||
LayerContent::Fill(f) => layer.fill_cache.as_ref().filter(|c| c.fill == *f).map(|c| &c.surface),
|
||
_ => layer.surface(),
|
||
};
|
||
let mask = layer.mask.as_ref().filter(|m| m.enabled).map(|m| &m.surface);
|
||
(content, mask)
|
||
}
|
||
|
||
/// Everything a non-group layer's shape depends on except tile contents and position.
|
||
pub(crate) fn shape_key(layer: &Layer, canvas: Rect) -> u64 {
|
||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||
layer.id.0.hash(&mut h);
|
||
let (content, mask) = shape_sources(layer);
|
||
match &layer.content {
|
||
LayerContent::Fill(f) => {
|
||
format!("{f:?}").hash(&mut h);
|
||
let fr = photocraft_compose::fill_frame(layer, canvas);
|
||
(fr.x0, fr.y0, fr.x1, fr.y1, content.is_some()).hash(&mut h);
|
||
}
|
||
other => std::mem::discriminant(other).hash(&mut h),
|
||
}
|
||
for s in [content, mask].into_iter().flatten() {
|
||
format!("{:?}{:?}", s.format(), s.default_pixel()).hash(&mut h);
|
||
}
|
||
if let Some(m) = &layer.mask {
|
||
(m.enabled, m.density.to_bits(), m.feather.to_bits()).hash(&mut h);
|
||
}
|
||
format!("{:?}", layer.vector_mask).hash(&mut h);
|
||
h.finish()
|
||
}
|
||
|
||
/// Deep identity of a group (children, their pixels by tile pointer, settings).
|
||
pub(crate) fn group_key(layer: &Layer, light: &GlobalLight) -> u64 {
|
||
fn surface_fp(s: &Surface, h: &mut std::collections::hash_map::DefaultHasher) {
|
||
format!("{:?}{:?}", s.format(), s.default_pixel()).hash(h);
|
||
for (c, t) in s.tiles() {
|
||
(c.tx, c.ty, Arc::as_ptr(t) as usize).hash(h);
|
||
}
|
||
}
|
||
fn walk(l: &Layer, h: &mut std::collections::hash_map::DefaultHasher) {
|
||
l.id.0.hash(h);
|
||
(l.visible, l.clipped, l.opacity.to_bits(), l.fill_opacity.to_bits(), format!("{:?}", l.blend)).hash(h);
|
||
match &l.content {
|
||
LayerContent::Group(g) => {
|
||
for c in &g.children {
|
||
walk(c, h);
|
||
}
|
||
}
|
||
LayerContent::Fill(f) => {
|
||
format!("{f:?}").hash(h);
|
||
if let Some(c) = &l.fill_cache {
|
||
surface_fp(&c.surface, h);
|
||
}
|
||
}
|
||
LayerContent::Adjustment(a) => format!("{a:?}").hash(h),
|
||
_ => {
|
||
if let Some(s) = l.surface() {
|
||
surface_fp(s, h);
|
||
}
|
||
}
|
||
}
|
||
if let Some(m) = &l.mask {
|
||
(m.enabled, m.density.to_bits(), m.feather.to_bits()).hash(h);
|
||
surface_fp(&m.surface, h);
|
||
}
|
||
format!("{:?}{:?}", l.vector_mask, l.effects).hash(h);
|
||
h.write_u8(0xfe);
|
||
}
|
||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||
(light.angle.to_bits(), light.altitude.to_bits()).hash(&mut h);
|
||
match &layer.content {
|
||
LayerContent::Group(g) => {
|
||
for c in &g.children {
|
||
walk(c, &mut h);
|
||
}
|
||
if let Some(m) = &layer.mask {
|
||
(m.enabled, m.density.to_bits(), m.feather.to_bits()).hash(&mut h);
|
||
surface_fp(&m.surface, &mut h);
|
||
}
|
||
}
|
||
_ => walk(layer, &mut h),
|
||
}
|
||
h.finish()
|
||
}
|
||
|
||
#[cfg(test)]
|
||
#[allow(clippy::unreachable)] // clippy.toml exempts unwrap/expect/panic in tests, not unreachable!
|
||
mod tests {
|
||
use super::*;
|
||
use photocraft_color::BlendMode;
|
||
use photocraft_doc::FxCommon;
|
||
|
||
#[test]
|
||
fn programs_key_on_geometry_not_colour() {
|
||
let light = GlobalLight::default();
|
||
let Effect::DropShadow(mut s) = Effect::default_drop_shadow() else { unreachable!() };
|
||
let a = program(&Effect::DropShadow(s.clone()), &light, false);
|
||
s.color = photocraft_color::Color::rgb(1.0, 0.0, 0.0);
|
||
s.common = FxCommon::new(BlendMode::Screen, 0.3);
|
||
let b = program(&Effect::DropShadow(s.clone()), &light, false);
|
||
assert_eq!(a.key, b.key);
|
||
s.size += 1.0;
|
||
assert_ne!(a.key, program(&Effect::DropShadow(s), &light, false).key);
|
||
}
|
||
|
||
#[test]
|
||
fn windows_grow_by_reach_and_temps_are_reused() {
|
||
let light = GlobalLight::default();
|
||
let Effect::DropShadow(mut s) = Effect::default_drop_shadow() else { unreachable!() };
|
||
s.spread = 0.5;
|
||
let p = program(&Effect::DropShadow(s), &light, false);
|
||
let region = Rect::new(-100, -100, 1000, 1000);
|
||
let d = Rect::new(0, 0, 10, 10);
|
||
let fr = |_| 5;
|
||
let w = p.windows(d, region, &fr);
|
||
let last = *w.last().unwrap();
|
||
assert!(last.contains_rect(&d.inflate(7)), "{last:?}");
|
||
// Earlier stages need at least as much as later ones read.
|
||
assert!(w[0].width() >= last.width());
|
||
let (t, n) = p.temps();
|
||
assert!(n <= 3, "{n} temporaries for {} stages", p.stages.len());
|
||
assert!(t.last().unwrap().is_none());
|
||
}
|
||
|
||
/// A distance field computed in bands from grown windows equals the whole-region field
|
||
/// wherever it is within its reach (the property incremental updates rely on).
|
||
#[test]
|
||
fn banded_fields_match_whole_region_within_reach() {
|
||
let region = Rect::new(-20, -10, 300, 290);
|
||
let (w, h) = (region.width() as usize, region.height() as usize);
|
||
let mut shape = vec![0.0f32; w * h];
|
||
for y in 0..h {
|
||
for x in 0..w {
|
||
let (fx, fy) = (x as f32 - 150.0, y as f32 - 140.0);
|
||
let r = (fx * fx + fy * fy).sqrt() + 8.0 * (fy * 0.07).sin() * (fx * 0.05).cos();
|
||
shape[y * w + x] = (90.0 - r).clamp(0.0, 1.0) * if (x / 13 + y / 17) % 5 == 0 { 0.6 } else { 1.0 };
|
||
}
|
||
}
|
||
for kind in [
|
||
FieldKind::Outside,
|
||
FieldKind::Inside,
|
||
FieldKind::OutsideInverse,
|
||
FieldKind::StrokeOutside,
|
||
FieldKind::StrokeInside,
|
||
FieldKind::StrokeOutsideVector,
|
||
] {
|
||
let reach = 9;
|
||
let whole = photocraft_compose::effects::distance_field(kind, shape.clone(), w, h);
|
||
let banded = field(kind, reach, &shape, region, region);
|
||
let part = Rect::new(40, 30, 170, 120);
|
||
let partial = field(kind, reach, &shape, region, part);
|
||
for (i, (a, b)) in whole.iter().zip(&banded).enumerate() {
|
||
if a.min(*b) < reach as f32 {
|
||
assert!((a - b).abs() < 1e-5, "{kind:?} at {i}: {a} vs {b}");
|
||
}
|
||
}
|
||
for (i, b) in partial.iter().enumerate() {
|
||
let (x, y) = (part.x0 + (i % part.width() as usize) as i32, part.y0 + (i / part.width() as usize) as i32);
|
||
let a = whole[(y - region.y0) as usize * w + (x - region.x0) as usize];
|
||
if a.min(*b) < reach as f32 {
|
||
assert!((a - b).abs() < 1e-5, "{kind:?} at ({x},{y}): {a} vs {b}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|