Skip to main content

vello_cpu/fine/highp/
blend.rs

1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use crate::peniko::{BlendMode, Mix};
5use crate::util::Premultiply;
6use vello_common::fearless_simd::*;
7
8#[derive(Copy, Clone)]
9struct Channels<S: Simd> {
10    r: f32x4<S>,
11    g: f32x4<S>,
12    b: f32x4<S>,
13}
14
15impl<S: Simd> Channels<S> {
16    #[inline(always)]
17    fn unpremultiply(mut self, a: f32x4<S>) -> Self {
18        self.r = self.r.unpremultiply(a);
19        self.g = self.g.unpremultiply(a);
20        self.b = self.b.unpremultiply(a);
21
22        self
23    }
24}
25
26// TODO: blending is still extremely slow, investigate whether there is something obvious we are
27// missing that other renderers do.
28pub(crate) fn mix<S: Simd>(src_c: f32x16<S>, bg: f32x16<S>, blend_mode: BlendMode) -> f32x16<S> {
29    if matches!(blend_mode.mix, Mix::Normal) {
30        return src_c;
31    }
32    // See https://www.w3.org/TR/compositing-1/#blending
33    let simd = src_c.simd;
34
35    let split = |input: f32x16<S>| {
36        let mut storage = [0.0; 16];
37        simd.store_interleaved_128_f32x16(input, &mut storage);
38        let input_v = f32x16::from_slice(simd, &storage);
39
40        let p1 = simd.split_f32x16(input_v);
41        let (r, g) = simd.split_f32x8(p1.0);
42        let (b, a) = simd.split_f32x8(p1.1);
43
44        (Channels { r, g, b }, a)
45    };
46
47    let (bg_channels, bg_a) = split(bg);
48    let (src_channels, src_a) = split(src_c);
49
50    let unpremultiplied_bg = bg_channels.unpremultiply(bg_a);
51    let unpremultiplied_src = src_channels.unpremultiply(src_a);
52
53    let mut res_bg = unpremultiplied_bg;
54    let mix_src = blend_mode.mix(unpremultiplied_src, unpremultiplied_bg);
55
56    let apply_alpha = |unpremultiplied_src_channel: f32x4<S>,
57                       mix_src_channel: f32x4<S>,
58                       dest_channel: &mut f32x4<S>| {
59        let p1 = (1.0 - bg_a) * unpremultiplied_src_channel;
60        let p2 = bg_a * mix_src_channel;
61
62        *dest_channel = (p1 + p2).premultiply(src_a);
63    };
64
65    apply_alpha(unpremultiplied_src.r, mix_src.r, &mut res_bg.r);
66    apply_alpha(unpremultiplied_src.g, mix_src.g, &mut res_bg.g);
67    apply_alpha(unpremultiplied_src.b, mix_src.b, &mut res_bg.b);
68
69    let combined = simd.combine_f32x8(
70        simd.combine_f32x4(res_bg.r, res_bg.g),
71        simd.combine_f32x4(res_bg.b, src_a),
72    );
73
74    let mut storage = [0.0; 16];
75    simd.store_interleaved_128_f32x16(combined, &mut storage);
76    f32x16::from_slice(simd, &storage)
77}
78
79trait MixExt {
80    fn mix<S: Simd>(&self, src: Channels<S>, bg: Channels<S>) -> Channels<S>;
81}
82
83impl MixExt for BlendMode {
84    fn mix<S: Simd>(&self, src: Channels<S>, bg: Channels<S>) -> Channels<S> {
85        match self.mix {
86            Mix::Normal => src,
87            Mix::Multiply => Multiply::mix(src, bg),
88            Mix::Screen => Screen::mix(src, bg),
89            Mix::Overlay => Overlay::mix(src, bg),
90            Mix::Darken => Darken::mix(src, bg),
91            Mix::Lighten => Lighten::mix(src, bg),
92            Mix::ColorDodge => ColorDodge::mix(src, bg),
93            Mix::ColorBurn => ColorBurn::mix(src, bg),
94            Mix::HardLight => HardLight::mix(src, bg),
95            Mix::SoftLight => SoftLight::mix(src, bg),
96            Mix::Difference => Difference::mix(src, bg),
97            Mix::Exclusion => Exclusion::mix(src, bg),
98            Mix::Luminosity => Luminosity::mix(src, bg),
99            Mix::Color => Color::mix(src, bg),
100            Mix::Hue => Hue::mix(src, bg),
101            Mix::Saturation => Saturation::mix(src, bg),
102        }
103    }
104}
105
106impl Multiply {
107    #[inline(always)]
108    fn single<S: Simd>(src: f32x4<S>, bg: f32x4<S>) -> f32x4<S> {
109        src * bg
110    }
111}
112
113impl Screen {
114    #[inline(always)]
115    fn single<S: Simd>(src: f32x4<S>, bg: f32x4<S>) -> f32x4<S> {
116        bg + src - src * bg
117    }
118}
119
120impl HardLight {
121    fn single<S: Simd>(src: f32x4<S>, bg: f32x4<S>) -> f32x4<S> {
122        let two = f32x4::splat(src.simd, 2.0);
123
124        let mask = src.simd.simd_le_f32x4(src, f32x4::splat(src.simd, 0.5));
125        let opt1 = Multiply::single(bg, src * two);
126        let opt2 = Screen::single(bg, two * src - 1.0);
127
128        src.simd.select_f32x4(mask, opt1, opt2)
129    }
130}
131
132macro_rules! separable_mix {
133    ($name:ident, $calc:expr) => {
134        pub(crate) struct $name;
135
136        impl $name {
137            #[inline(always)]
138            fn mix<S: Simd>(mut src: Channels<S>, bg: Channels<S>) -> Channels<S> {
139                src.r = $calc(src.r, bg.r);
140                src.g = $calc(src.g, bg.g);
141                src.b = $calc(src.b, bg.b);
142
143                src
144            }
145        }
146    };
147}
148
149separable_mix!(Multiply, |cs: f32x4<S>, cb: f32x4<S>| Multiply::single(
150    cs, cb
151));
152separable_mix!(Screen, |cs: f32x4<S>, cb: f32x4<S>| Screen::single(cs, cb));
153separable_mix!(Overlay, |cs: f32x4<S>, cb: f32x4<S>| HardLight::single(
154    cb, cs
155));
156separable_mix!(Darken, |cs: f32x4<S>, cb: f32x4<S>| cs.min(cb));
157separable_mix!(Lighten, |cs: f32x4<S>, cb: f32x4<S>| cs.max(cb));
158separable_mix!(Difference, |cs: f32x4<S>, cb: f32x4<S>| {
159    cs.simd
160        .select_f32x4(cs.simd.simd_le_f32x4(cs, cb), cb - cs, cs - cb)
161});
162separable_mix!(HardLight, |cs: f32x4<S>, cb: f32x4<S>| HardLight::single(
163    cs, cb
164));
165separable_mix!(Exclusion, |cs: f32x4<S>, cb: f32x4<S>| {
166    (cs + cb) - 2.0 * (cs * cb)
167});
168separable_mix!(SoftLight, |cs: f32x4<S>, cb: f32x4<S>| {
169    let mask_1 = cs.simd.simd_le_f32x4(cb, f32x4::splat(cs.simd, 0.25));
170
171    let d = cs
172        .simd
173        .select_f32x4(mask_1, ((16.0 * cb - 12.0) * cb + 4.0) * cb, cb.sqrt());
174
175    let mask_2 = cs.simd.simd_le_f32x4(cs, f32x4::splat(cs.simd, 0.5));
176
177    cs.simd.select_f32x4(
178        mask_2,
179        cb - (1.0 - 2.0 * cs) * cb * (1.0 - cb),
180        cb + (2.0 * cs - 1.0) * (d - cb),
181    )
182});
183separable_mix!(ColorDodge, |cs: f32x4<S>, cb: f32x4<S>| {
184    let mask_1 = cb.simd.simd_eq_f32x4(cb, f32x4::splat(cb.simd, 0.0));
185    let mask_2 = cs.simd.simd_eq_f32x4(cs, f32x4::splat(cs.simd, 1.0));
186
187    cs.simd.select_f32x4(
188        // if cb == 0
189        mask_1,
190        f32x4::splat(cs.simd, 0.0),
191        // else if cs == 1
192        cs.simd.select_f32x4(
193            mask_2,
194            f32x4::splat(cs.simd, 1.0),
195            // else
196            f32x4::splat(cs.simd, 1.0).min(cb / (1.0 - cs)),
197        ),
198    )
199});
200separable_mix!(ColorBurn, |cs: f32x4<S>, cb: f32x4<S>| {
201    let mask_1 = cb.simd.simd_eq_f32x4(cb, f32x4::splat(cb.simd, 1.0));
202    let mask_2 = cs.simd.simd_eq_f32x4(cs, f32x4::splat(cs.simd, 0.0));
203
204    cs.simd.select_f32x4(
205        // if cb == 1
206        mask_1,
207        f32x4::splat(cs.simd, 1.0),
208        // else if cs == 0
209        cs.simd.select_f32x4(
210            mask_2,
211            f32x4::splat(cs.simd, 0.0),
212            // else
213            1.0 - f32x4::splat(cs.simd, 1.0).min((1.0 - cb) / cs),
214        ),
215    )
216});
217
218macro_rules! non_separable_mix {
219    ($name:ident, $calc:expr) => {
220        pub(crate) struct $name;
221
222        impl $name {
223            #[inline(always)]
224            fn mix<S: Simd>(mut src: Channels<S>, mut bg: Channels<S>) -> Channels<S> {
225                $calc(&mut src, &mut bg)
226            }
227        }
228    };
229}
230
231non_separable_mix!(Hue, |cs: &mut Channels<S>, cb: &mut Channels<S>| {
232    set_sat(&mut cs.r, &mut cs.g, &mut cs.b, sat(cb.r, cb.g, cb.b));
233    set_lum(&mut cs.r, &mut cs.g, &mut cs.b, lum(cb.r, cb.g, cb.b));
234
235    *cs
236});
237
238non_separable_mix!(Saturation, |cs: &mut Channels<S>, cb: &mut Channels<S>| {
239    let lum = lum(cb.r, cb.g, cb.b);
240    set_sat(&mut cb.r, &mut cb.g, &mut cb.b, sat(cs.r, cs.g, cs.b));
241    set_lum(&mut cb.r, &mut cb.g, &mut cb.b, lum);
242
243    *cb
244});
245
246non_separable_mix!(Color, |cs: &mut Channels<S>, cb: &mut Channels<S>| {
247    set_lum(&mut cs.r, &mut cs.g, &mut cs.b, lum(cb.r, cb.g, cb.b));
248
249    *cs
250});
251non_separable_mix!(Luminosity, |cs: &mut Channels<S>, cb: &mut Channels<S>| {
252    set_lum(&mut cb.r, &mut cb.g, &mut cb.b, lum(cs.r, cs.g, cs.b));
253
254    *cb
255});
256
257fn lum<S: Simd>(r: f32x4<S>, g: f32x4<S>, b: f32x4<S>) -> f32x4<S> {
258    0.3 * r + 0.59 * g + 0.11 * b
259}
260
261fn sat<S: Simd>(r: f32x4<S>, g: f32x4<S>, b: f32x4<S>) -> f32x4<S> {
262    r.max(g).max(b) - r.min(g).min(b)
263}
264
265fn clip_color<S: Simd>(r: &mut f32x4<S>, g: &mut f32x4<S>, b: &mut f32x4<S>) {
266    let simd = r.simd;
267
268    let l = lum(*r, *g, *b);
269    let n = r.min(g.min(*b));
270    let x = r.max(g.max(*b));
271
272    for c in [r, g, b] {
273        *c = simd.select_f32x4(
274            simd.simd_lt_f32x4(n, f32x4::splat(simd, 0.0)),
275            l + (((*c - l) * l) / (l - n)),
276            *c,
277        );
278
279        *c = simd.select_f32x4(
280            simd.simd_gt_f32x4(x, f32x4::splat(simd, 1.0)),
281            l + (((*c - l) * (1.0 - l)) / (x - l)),
282            *c,
283        );
284    }
285}
286
287fn set_lum<S: Simd>(r: &mut f32x4<S>, g: &mut f32x4<S>, b: &mut f32x4<S>, l: f32x4<S>) {
288    let d = l - lum(*r, *g, *b);
289    *r += d;
290    *g += d;
291    *b += d;
292
293    clip_color(r, g, b);
294}
295
296// Adapted from tiny-skia
297fn set_sat<S: Simd>(r: &mut f32x4<S>, g: &mut f32x4<S>, b: &mut f32x4<S>, s: f32x4<S>) {
298    let simd = r.simd;
299    let zero = f32x4::splat(simd, 0.0);
300    let mn = r.min(g.min(*b));
301    let mx = r.max(g.max(*b));
302    let sat = mx - mn;
303
304    // Map min channel to 0, max channel to s, and scale the middle proportionally.
305    let scale = |c| simd.select_f32x4(simd.simd_eq_f32x4(sat, zero), zero, (c - mn) * s / sat);
306
307    *r = scale(*r);
308    *g = scale(*g);
309    *b = scale(*b);
310}