FFFF photocraft/crates/gpu/src/fx.rs at main · hmmhmmhm/photocraft · GitHub
[go: up one dir, main page]

Skip to content

Latest commit

 

History

History
711 lines (670 loc) · 30.1 KB

fx.rs

File metadata and controls

711 lines (670 loc) · 30.1 KB
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(&region);
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}");
}
}
}
}
}
0