mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-19 02:10:49 -05:00
A layer with Blending Options › Blend If made the planner refuse the whole document, so every refresh of it ran the CPU compositor: on the #209 layered document an opacity edit took 140 ms instead of 6 ms. Blend If is compose::apply_blend_if: the composite with the layer mixed back towards the backdrop by a per-pixel weight from the layer's own content with its masks ("This Layer"; an adjustment's result) and the backdrop ("Underlying Layer"). The planner now keeps the backdrop, composites the layer as before (channel restrictions included, as on the CPU), renders its own content and emits one Lerp pass with the new F_BLEND_IF flag and the ranges packed exactly in its parameters. Base and clipped layers both go through it; Advanced Blending still falls back to the CPU. Hard points: compositing can leave an 8-bit level a few millionths below itself (a level-20 backdrop at partial alpha reads 19.999998), which put it on the wrong side of a hard point at that level on the CPU, and the GPU's arithmetic differs there. Both now treat a value within 1e-4 of a level as that level; 8/16-bit values off a level are at least 1/257 away. On 16/32-bit documents the GPU reads layers as RGBA16F, so a value within about a tenth of a level of a hard point can still land on the other side, as with the GPU Threshold adjustment. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
6 files changed
+252
-33
No files matched your search
@@ -1121,7 +1121,7 @@ fn blend_if_weight(layer: &Layer, mode: photocraft_color::ColorMode, this: Optio
|
||||
let mut k = 1.0;
|
||||
for (side, px) in [this, under].into_iter().enumerate() {
|
||||
let Some(p) = px else { continue };
|
||||
let v = |c: usize| p[c].clamp(0.0, 1.0) * 255.0;
|
||||
let v = |c: usize| blend_if_level(p[c]);
|
||||
match mode {
|
||||
// Gray is the colour channels' luma (Rec. 601 weights), then R, G, B.
|
||||
M::Rgb => {
|
||||
@@ -1142,6 +1142,17 @@ fn blend_if_weight(layer: &Layer, mode: photocraft_color::ColorMode, this: Optio
|
||||
k
|
||||
}
|
||||
|
||||
/// A channel value on Blend If's 0..=255 scale. Compositing can leave an 8- or 16-bit level a few
|
||||
/// millionths off (a level-20 backdrop composited at partial alpha reads 19.999998), which would
|
||||
/// put it on the wrong side of a hard point at that level: a value that close to a level is that
|
||||
/// level. Values of those depths that aren't on a level are at least 1/257 away from one. The GPU
|
||||
/// compositor (`compose.wgsl` `blend_if_weight`) snaps alike.
|
||||
fn blend_if_level(x: f32) -> f32 {
|
||||
let v = x.clamp(0.0, 1.0) * 255.0;
|
||||
let r = v.round();
|
||||
if (v - r).abs() < 1e-4 { r } else { v }
|
||||
}
|
||||
|
||||
/// Apply `layer`'s Blend If to a finished composite: `out` (the backdrop with the layer drawn)
|
||||
/// is mixed back towards `before` (the backdrop without it) where the ranges hide the layer.
|
||||
/// Mixing premultiplied colour by `k` equals compositing the layer at `k` × its alpha, since
|
||||
|
||||
@@ -1513,6 +1513,22 @@ fn blend_if_underlying_layer_hides_by_the_backdrop_value() {
|
||||
assert!(close4(px(&d, 6, 1), [1.0, 1.0, 1.0, 1.0]));
|
||||
}
|
||||
|
||||
/// An 8-bit backdrop at level `k` counts as `k` for a hard Underlying black point at `k`, also
|
||||
/// over a semi-transparent backdrop, where compositing leaves the value a few millionths off the
|
||||
/// level (19.999998 for 20 before #4395); the GPU compositor agrees.
|
||||
#[test]
|
||||
fn blend_if_hard_point_includes_its_own_level() {
|
||||
for k in 0..=255u8 {
|
||||
let v = f32::from(k) / 255.0;
|
||||
let mut d = Document::new("t", Size::new(2, 1), ColorMode::Rgb, SampleType::U8);
|
||||
d.layers.push(solid_layer("under", Rect::new(0, 0, 2, 1), [v, v, v, 184.0 / 255.0]));
|
||||
let mut l = solid_layer("top", Rect::new(0, 0, 2, 1), [1.0, 0.0, 0.0, 1.0]);
|
||||
l.blend_if.set(1, [FULL, range([k, k], [255, 255])]);
|
||||
d.layers.push(l);
|
||||
assert!(close4(px(&d, 0, 0), [1.0, 0.0, 0.0, 1.0]), "level {k} is hidden by a black point at {k}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blend_if_split_point_fades_like_opacity() {
|
||||
// A split black point (Alt-drag) at 50/150 shows a value-100 layer at half strength, which
|
||||
|
||||
@@ -51,6 +51,7 @@ const F_LAB: u32 = 65536u; // Lab document: Normal mixes in CIELAB
|
||||
const F_HDR: u32 = 262144u; // 32-bit float document: HDR blends and Exposure retain values above 1
|
||||
const F_QUANT: u32 = 32768u; // lerp: A rounded to p0.x steps (adjustment results, integer docs)
|
||||
const F_ADD_DIFF: u32 = 16384u; // lerp: A + (B - C) premultiplied (clips on pass-through groups)
|
||||
const F_BLEND_IF: u32 = 524288u; // lerp: B back towards A by the Blend If ranges in p0..p3
|
||||
const F_TEXT_GAMMA: u32 = 8192u; // blend / atop / fx merge: type layer, mix coverage at gamma p4.w
|
||||
|
||||
@group(0) @binding(0) var<uniform> chunk: Chunk;
|
||||
@@ -874,6 +875,21 @@ fn fs_lerp(in: VOut) -> @location(0) vec4<f32> {
|
||||
if (al <= 0.0) { return vec4(0.0); }
|
||||
return vec4(clamp(pm.rgb / al, vec3(0.0), vec3(1.0)), al);
|
||||
}
|
||||
if ((op.flags & F_BLEND_IF) != 0u) {
|
||||
// compose::apply_blend_if: "This Layer" is C (the layer's own content with its masks),
|
||||
// or B itself for an adjustment layer; "Underlying Layer" is A.
|
||||
var this_px = b;
|
||||
if (op.p4.y < 0.5) {
|
||||
this_px = textureLoad(tex_c, p, 0);
|
||||
}
|
||||
let k = blend_if_weight(this_px, a);
|
||||
if (k >= 1.0) {
|
||||
return b;
|
||||
}
|
||||
let alpha = a.a + (b.a - a.a) * k;
|
||||
if (alpha <= 0.0) { return vec4(0.0); }
|
||||
return vec4(clamp((a.rgb * a.a + (b.rgb * b.a - a.rgb * a.a) * k) / alpha, vec3(0.0), vec3(1.0)), alpha);
|
||||
}
|
||||
let k = op.opacity * mask_value(doc_px(p));
|
||||
let wa = a.a * (1.0 - k);
|
||||
let wb = b.a * k;
|
||||
@@ -882,6 +898,57 @@ fn fs_lerp(in: VOut) -> @location(0) vec4<f32> {
|
||||
return vec4((a.rgb * wa + b.rgb * wb) / alpha, alpha);
|
||||
}
|
||||
|
||||
// ---- Blend If (photocraft_doc::BlendRange, compose::blend_if_weight) ---------------------------
|
||||
|
||||
// 0 at or below `lo`, 1 at or above `hi`; a hard step at `lo` when unsplit.
|
||||
fn bi_ramp(lo: f32, hi: f32, x: f32) -> f32 {
|
||||
if (hi <= lo) {
|
||||
return select(0.0, 1.0, x >= lo);
|
||||
}
|
||||
return clamp((x - lo) / (hi - lo), 0.0, 1.0);
|
||||
}
|
||||
|
||||
// How much of value `v` (0..255) shows through range `e` (0 = Gray, then the channels) on side
|
||||
// `s` (0 = this layer, 1 = underlying). The ranges are packed by `plan::blend_if_params`.
|
||||
fn bi_weight(e: u32, s: u32, v: f32) -> f32 {
|
||||
var f = array<vec4<f32>, 4>(op.p0, op.p1, op.p2, op.p3);
|
||||
let i = (e * 2u + s) * 2u;
|
||||
let black = f[i / 4u][i % 4u];
|
||||
let white = f[(i + 1u) / 4u][(i + 1u) % 4u];
|
||||
let b_lo = floor(black / 256.0);
|
||||
let b_hi = black - b_lo * 256.0;
|
||||
let w_lo = floor(white / 256.0);
|
||||
let w_hi = white - w_lo * 256.0;
|
||||
return bi_ramp(b_lo, b_hi, v) * bi_ramp(255.0 - w_hi, 255.0 - w_lo, 255.0 - v);
|
||||
}
|
||||
|
||||
// The product of every range over the layer's own colour and the colour beneath it (a side
|
||||
// without content, alpha 0, doesn't count). `p4.x` marks a one-channel document.
|
||||
fn blend_if_weight(this_px: vec4<f32>, under: vec4<f32>) -> f32 {
|
||||
var k = 1.0;
|
||||
for (var s = 0u; s < 2u; s = s + 1u) {
|
||||
let px = select(this_px, under, s == 1u);
|
||||
if (px.a <= 0.0) {
|
||||
continue;
|
||||
}
|
||||
// The CPU gets 8- and 16-bit levels exactly (k / 255 × 255 = k), but GPU arithmetic can
|
||||
// land a few millionths below, which flips a hard threshold at that level: values that
|
||||
// close to a level are that level. Real 8/16-bit values off a level are ≥ 1/257 away.
|
||||
let raw = clamp(px.rgb, vec3(0.0), vec3(1.0)) * 255.0;
|
||||
let v = select(raw, round(raw), abs(raw - round(raw)) < vec3(1e-4));
|
||||
if (op.p4.x > 0.5) {
|
||||
k = k * bi_weight(0u, s, v.r) * bi_weight(1u, s, v.r);
|
||||
} else {
|
||||
let gray = 0.299 * v.r + 0.587 * v.g + 0.114 * v.b;
|
||||
k = k * bi_weight(0u, s, gray) * bi_weight(1u, s, v.r) * bi_weight(2u, s, v.g) * bi_weight(3u, s, v.b);
|
||||
}
|
||||
if (k <= 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
return k;
|
||||
}
|
||||
|
||||
// ---- layer effects (compose::effects::composite_with_effects) -------------------------------
|
||||
|
||||
const INSIDE_EPS: f32 = 0.5 / 255.0;
|
||||
|
||||
+78
-27
@@ -388,9 +388,30 @@ pub const F_HDR: u32 = 262144;
|
||||
pub const F_QUANT: u32 = 32768;
|
||||
/// `Lerp` as A + (B − C) premultiplied (layers clipped to pass-through groups).
|
||||
pub const F_ADD_DIFF: u32 = 16384;
|
||||
/// `Lerp`: B mixed back towards A (the backdrop) by the layer's Blend If weight
|
||||
/// (`compose::apply_blend_if`), from C ("This Layer", or B itself for an adjustment layer, `p4.y`)
|
||||
/// and A ("Underlying Layer"). The ranges are packed in `p0..p3` (`blend_if_params`); `p4.x` marks
|
||||
/// a one-channel document.
|
||||
pub const F_BLEND_IF: u32 = 524288;
|
||||
/// Blend / Atop / FxMerge of a type layer: coverage mixed at `psblend::TEXT_GAMMA`.
|
||||
pub const F_TEXT_GAMMA: u32 = 8192;
|
||||
|
||||
/// A layer's Blend If ranges for `F_BLEND_IF`: Gray, then the colour channels, each as
|
||||
/// [this layer, underlying layer]; every range as two floats, `black low × 256 + black high` and
|
||||
/// `white low × 256 + white high` (exact in f32), so entry `e`, side `s` starts at float
|
||||
/// `(e × 2 + s) × 2`. Entries past the fourth never apply (RGB has four, one-channel modes two).
|
||||
fn blend_if_params(bi: &photocraft_doc::BlendIf) -> [[f32; 4]; 4] {
|
||||
let mut f = [0.0f32; 16];
|
||||
for e in 0..4 {
|
||||
for (s, r) in bi.get(e).iter().enumerate() {
|
||||
let i = (e * 2 + s) * 2;
|
||||
f[i] = f32::from(r.black[0]) * 256.0 + f32::from(r.black[1]);
|
||||
f[i + 1] = f32::from(r.white[0]) * 256.0 + f32::from(r.white[1]);
|
||||
}
|
||||
}
|
||||
[[f[0], f[1], f[2], f[3]], [f[4], f[5], f[6], f[7]], [f[8], f[9], f[10], f[11]], [f[12], f[13], f[14], f[15]]]
|
||||
}
|
||||
|
||||
/// `F_TEXT_GAMMA` for type layers while text gamma blending is on; the gamma goes in `p4.w`.
|
||||
fn gamma_flag(layer: &Layer) -> u32 {
|
||||
if photocraft_compose::text_gamma(layer) != 1.0 { F_TEXT_GAMMA } else { 0 }
|
||||
@@ -515,12 +536,44 @@ impl<'a> Planner<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Blending Options › Blend If has no GPU pass yet: such documents use the CPU compositor.
|
||||
fn check_blend_if(&self, layer: &Layer) -> Result<(), Unsupported> {
|
||||
if photocraft_compose::blend_if_active(layer, self.cx.mode) {
|
||||
return Err(Unsupported(format!("Blend If on `{}` (composited on the CPU)", layer.name)));
|
||||
/// `compose::apply_blend_if`: `after` (the backdrop with `layer` drawn) mixed back towards
|
||||
/// `before` (the backdrop without it) where the layer's Blend If ranges hide it. "This Layer"
|
||||
/// is the layer's own content with its masks; an adjustment layer is judged by its result.
|
||||
/// Consumes both slots.
|
||||
fn blend_if(&mut self, layer: &'a Layer, before: Slot, after: Slot) -> Result<Slot, Unsupported> {
|
||||
let own = match layer.content {
|
||||
LayerContent::Adjustment(_) => None,
|
||||
_ => Some(self.content(layer)?),
|
||||
};
|
||||
let mut p = Pass::new(Kernel::Lerp, 0);
|
||||
p.a = Some(before);
|
||||
p.b = Some(after);
|
||||
p.c = own;
|
||||
p.flags = F_BLEND_IF;
|
||||
p.params = blend_if_params(&layer.blend_if);
|
||||
let one_channel = self.cx.mode != photocraft_color::ColorMode::Rgb;
|
||||
p.extra = [f32::from(u8::from(one_channel)), f32::from(u8::from(own.is_none())), 0.0, 0.0];
|
||||
Ok(self.emit(p))
|
||||
}
|
||||
|
||||
/// Channel restrictions and Blend If around `draw`, which composites `layer` onto `backdrop`
|
||||
/// (`compose::composite_layer` / `composite_atop`: both are relative to the same backdrop).
|
||||
fn restricted(&mut self, layer: &'a Layer, backdrop: Slot, draw: impl FnOnce(&mut Self, Slot) -> Result<Slot, Unsupported>) -> Result<Slot, Unsupported> {
|
||||
let w = photocraft_compose::channel_weights(layer, self.cx.mode);
|
||||
let blend_if = photocraft_compose::blend_if_active(layer, self.cx.mode);
|
||||
if w.is_none() && !blend_if {
|
||||
return draw(self, backdrop);
|
||||
}
|
||||
Ok(())
|
||||
let before = self.retain(backdrop);
|
||||
let mut after = draw(self, backdrop)?;
|
||||
if let Some(w) = w {
|
||||
let b = if blend_if { self.retain(before) } else { before };
|
||||
after = self.restore_channels(b, after, w);
|
||||
}
|
||||
if blend_if {
|
||||
after = self.blend_if(layer, before, after)?;
|
||||
}
|
||||
Ok(after)
|
||||
}
|
||||
|
||||
/// Blending Options › Advanced Blending (knockout, clipped layers blended individually, blend
|
||||
@@ -536,16 +589,8 @@ impl<'a> Planner<'a> {
|
||||
|
||||
/// composite_layer, honouring the layer's channel restrictions.
|
||||
fn layer(&mut self, layer: &'a Layer, clipped: &'a [Layer], backdrop: Slot) -> Result<Slot, Unsupported> {
|
||||
self.check_blend_if(layer)?;
|
||||
self.check_advanced(layer, clipped)?;
|
||||
match photocraft_compose::channel_weights(layer, self.cx.mode) {
|
||||
Some(w) => {
|
||||
let before = self.retain(backdrop);
|
||||
let after = self.layer_any(layer, clipped, backdrop)?;
|
||||
Ok(self.restore_channels(before, after, w))
|
||||
}
|
||||
None => self.layer_any(layer, clipped, backdrop),
|
||||
}
|
||||
self.restricted(layer, backdrop, |s, b| s.layer_any(layer, clipped, b))
|
||||
}
|
||||
|
||||
/// `compose::restore_channels`: `after` with the channels a layer leaves out taken from
|
||||
@@ -711,7 +756,6 @@ impl<'a> Planner<'a> {
|
||||
if clip.is_some_and(|c| c.is_empty()) {
|
||||
// Nothing to draw (an empty layer, or a group of them).
|
||||
for c in &visible_clipped {
|
||||
self.check_blend_if(c)?;
|
||||
self.check_advanced(c, &[])?;
|
||||
}
|
||||
return Ok(backdrop);
|
||||
@@ -894,16 +938,8 @@ impl<'a> Planner<'a> {
|
||||
/// composite_atop: `layer` onto `base`, restricted to the base's alpha, honouring the
|
||||
/// layer's channel restrictions.
|
||||
fn atop(&mut self, layer: &'a Layer, base: Slot) -> Result<Slot, Unsupported> {
|
||||
self.check_blend_if(layer)?;
|
||||
self.check_advanced(layer, &[])?;
|
||||
match photocraft_compose::channel_weights(layer, self.cx.mode) {
|
||||
Some(w) => {
|
||||
let before = self.retain(base);
|
||||
let after = self.atop_any(layer, base)?;
|
||||
Ok(self.restore_channels(before, after, w))
|
||||
}
|
||||
None => self.atop_any(layer, base),
|
||||
}
|
||||
self.restricted(layer, base, |s, b| s.atop_any(layer, b))
|
||||
}
|
||||
|
||||
fn atop_any(&mut self, layer: &'a Layer, base: Slot) -> Result<Slot, Unsupported> {
|
||||
@@ -1973,17 +2009,32 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blend_if_falls_back_to_the_cpu() {
|
||||
fn blend_if_plans_on_the_gpu() {
|
||||
let mut d = Document::with_background("t", Size::new(8, 8), ColorMode::Rgb, SampleType::U8, Color::WHITE);
|
||||
let mut l = Layer::raster("bi", d.pixel_format());
|
||||
l.surface_mut().unwrap().fill_rect(photocraft_geom::Rect::new(0, 0, 4, 4), &[1.0, 0.0, 0.0, 1.0]);
|
||||
l.blend_if.set(0, [photocraft_doc::BlendRange { black: [40, 40], white: [255, 255] }, photocraft_doc::BlendRange::FULL]);
|
||||
d.layers.push(l.clone());
|
||||
assert!(plan(&d).unwrap_err().0.contains("Blend If"));
|
||||
let blend_if = |d: &Document| plan(d).unwrap().passes.iter().filter(|p| p.flags & F_BLEND_IF != 0).count();
|
||||
assert_eq!(blend_if(&d), 1);
|
||||
// Clipped layers too.
|
||||
l.clipped = true;
|
||||
d.layers[1].blend_if = Default::default();
|
||||
d.layers.push(l);
|
||||
assert!(plan(&d).is_err());
|
||||
assert_eq!(blend_if(&d), 1);
|
||||
// Outside RGB, Grayscale and Duotone the setting round-trips without applying.
|
||||
d.mode = ColorMode::Lab;
|
||||
assert_eq!(blend_if(&d), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blend_if_params_pack_every_range_exactly() {
|
||||
let mut bi = photocraft_doc::BlendIf::default();
|
||||
bi.set(3, [photocraft_doc::BlendRange { black: [255, 255], white: [0, 1] }, photocraft_doc::BlendRange { black: [12, 200], white: [201, 254] }]);
|
||||
let f: Vec<f32> = blend_if_params(&bi).iter().flatten().copied().collect();
|
||||
// Entry 3 (B), this layer then underlying; unset entries are full ranges.
|
||||
assert_eq!(&f[12..16], &[255.0 * 256.0 + 255.0, 1.0, 12.0 * 256.0 + 200.0, 201.0 * 256.0 + 254.0]);
|
||||
assert_eq!(&f[0..2], &[0.0, 255.0 * 256.0 + 255.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -370,7 +370,7 @@ fn prefix_partial_damage_context_occlusion_and_budget_are_conservative() {
|
||||
check(&mut g, &doc, "bounded admission");
|
||||
assert!(g.comp.composite_cache_bytes() <= 1 << 20);
|
||||
assert!(diff_rect(&mut g, &doc, doc.bounds(), "bounded cache keeps its hits").unwrap().prefix_hits > 0);
|
||||
doc.layers[0].blend_if.set(0, [photocraft_doc::BlendRange { black: [40, 40], white: [255, 255] }, photocraft_doc::BlendRange::FULL]);
|
||||
doc.layers[0].advanced.knockout = photocraft_doc::Knockout::Deep;
|
||||
assert!(render_to_vec(&mut g.comp, &g.device, &g.queue, &doc, doc.bounds()).is_err());
|
||||
assert_eq!(g.comp.composite_cache_bytes(), 0);
|
||||
}
|
||||
@@ -1699,6 +1699,78 @@ fn stroke_effects_on_filled_and_stroked_shapes() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Blend If on the GPU matches `compose::apply_blend_if` (#4395): this-layer and underlying
|
||||
/// ranges, hard and split points, per channel, on raster, masked, adjustment and clipped layers,
|
||||
/// with channel restrictions, and in grayscale. Hard points are compared on 8-bit documents only:
|
||||
/// 16/32-bit layers reach the GPU as RGBA16F, so a value within about a tenth of a level of a
|
||||
/// hard point can land on its other side (as with the GPU Threshold adjustment, also compared at
|
||||
/// 8 bits); split points are compared at every depth.
|
||||
#[test]
|
||||
fn blend_if() {
|
||||
use photocraft_doc::BlendRange;
|
||||
let Some(mut g) = gpu() else { return };
|
||||
let r = |b0: u8, b1: u8, w0: u8, w1: u8| BlendRange { black: [b0, b1], white: [w0, w1] };
|
||||
// (name, entry, [this layer, underlying], has a hard point)
|
||||
let cases: [(&str, usize, [BlendRange; 2], bool); 7] = [
|
||||
("gray this hard", 0, [r(90, 90, 255, 255), BlendRange::FULL], true),
|
||||
("gray under split", 0, [BlendRange::FULL, r(30, 140, 150, 230)], false),
|
||||
("red both", 1, [r(0, 60, 200, 200), r(20, 20, 180, 250)], true),
|
||||
("red both split", 1, [r(0, 60, 190, 230), r(10, 50, 180, 250)], false),
|
||||
("green white split", 2, [r(0, 0, 100, 220), BlendRange::FULL], false),
|
||||
("blue under hard", 3, [BlendRange::FULL, r(128, 128, 255, 255)], true),
|
||||
("everything hidden", 0, [r(255, 255, 0, 0), BlendRange::FULL], true),
|
||||
];
|
||||
for depth in [SampleType::U8, SampleType::U16, SampleType::F32] {
|
||||
let fmt = PixelFormat { sample: depth, ..PixelFormat::RGBA8 };
|
||||
for (name, e, pair, hard) in cases {
|
||||
if hard && depth != SampleType::U8 {
|
||||
continue;
|
||||
}
|
||||
let mut d = Document::new("bi", Size::new(64, 48), ColorMode::Rgb, depth);
|
||||
d.layers.push(noise_layer("bg", fmt, Rect::new(0, 0, 64, 48), 1, 0.3));
|
||||
let mut l = noise_layer("top", fmt, Rect::new(4, 3, 60, 45), 2, 0.0);
|
||||
l.blend = BlendMode::Multiply;
|
||||
l.opacity = 0.8;
|
||||
l.blend_if.set(e, pair);
|
||||
d.layers.push(l.clone());
|
||||
check(&mut g, &d, &format!("{depth:?} {name}"));
|
||||
// Edits above it reuse the lower stack, Blend If included (prefix checkpoints).
|
||||
let mut above = d.clone();
|
||||
above.layers.push(noise_layer("above", fmt, Rect::new(20, 0, 40, 48), 4, 0.5));
|
||||
check(&mut g, &above, &format!("{depth:?} {name} under another layer"));
|
||||
above.layers[2].opacity = 0.4;
|
||||
let hits = diff_rect(&mut g, &above, above.bounds(), &format!("{depth:?} {name} edit above")).unwrap_or_else(|e| panic!("{e}"));
|
||||
assert!(hits.prefix_hits > 0, "{depth:?} {name}: the edit above reuses the lower stack");
|
||||
|
||||
// Masked, with channel restrictions.
|
||||
let mut m = l.clone();
|
||||
m.mask = Some(mask(Rect::new(0, 0, 64, 48), 5, 1.0));
|
||||
m.excluded_channels = 0b010;
|
||||
d.layers[1] = m;
|
||||
check(&mut g, &d, &format!("{depth:?} {name} masked, channels"));
|
||||
|
||||
// An adjustment judged by its result, and a clipped layer.
|
||||
let mut adj = Layer::new("inv", LayerContent::Adjustment(Adjustment::Invert));
|
||||
adj.blend_if.set(e, pair);
|
||||
d.layers[1] = l.clone();
|
||||
d.layers[1].blend_if = Default::default();
|
||||
d.layers.push(adj);
|
||||
let mut c = noise_layer("clip", fmt, Rect::new(10, 10, 50, 40), 3, 0.2);
|
||||
c.clipped = true;
|
||||
c.blend_if.set(e, pair);
|
||||
d.layers.push(c);
|
||||
check(&mut g, &d, &format!("{depth:?} {name} adjustment, clipped"));
|
||||
}
|
||||
}
|
||||
// Grayscale: the one channel's entries.
|
||||
let mut d = Document::new("g", Size::new(32, 32), ColorMode::Grayscale, SampleType::U8);
|
||||
d.layers.push(noise_layer("bg", PixelFormat::GRAYA8, Rect::new(0, 0, 32, 32), 9, 1.0));
|
||||
let mut l = noise_layer("top", PixelFormat::GRAYA8, Rect::new(0, 0, 32, 32), 10, 1.0);
|
||||
l.blend_if.set(1, [r(60, 120, 255, 255), r(0, 0, 140, 200)]);
|
||||
d.layers.push(l);
|
||||
check(&mut g, &d, "gray blend if");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_restrictions() {
|
||||
let Some(mut g) = gpu() else { return };
|
||||
|
||||
@@ -13,10 +13,12 @@ existing planner before this boundary. Only the accumulated backdrop is live the
|
||||
it into the checkpoint's slot is sufficient to execute the remaining passes. No checkpoint is
|
||||
placed inside a group, clipping unit or adjustment program.
|
||||
|
||||
Blend-If currently returns `Unsupported` from the GPU planner and uses the existing CPU path.
|
||||
The same holds for Advanced Blending (on main since b0680fd): it must keep falling back to the
|
||||
CPU, or keep the checkpoint boundary rule — a later GPU implementation of advanced blending
|
||||
preserves the boundary invariant either way.
|
||||
Blend If runs on the GPU (#4395) within this rule: like channel restrictions, it reads only the
|
||||
backdrop from before its own unit (retained across the unit, released at its end), so nothing
|
||||
else is live at the boundary. Advanced Blending (on main since b0680fd) returns `Unsupported`
|
||||
from the GPU planner and uses the CPU path: it must keep falling back to the CPU, or keep the
|
||||
checkpoint boundary rule — a later GPU implementation of advanced blending preserves the
|
||||
boundary invariant either way.
|
||||
|
||||
## Invalidation
|
||||
|
||||
|
||||
Reference in new issue
Block a user