Skip to main content

impeller_hal/
blend.rs

1//! How a draw combines with what a target already holds.
2//!
3//! Two families live here, and they differ in kind rather than degree.
4//!
5//! The Porter-Duff modes decide *where* each side survives, and are expressible
6//! with fixed-function blend factors: they work on every device with no
7//! extension and no capability gate, which is why they came first. A renderer
8//! unable to composite without an extension would be unusable on the hardware
9//! least likely to have one.
10//!
11//! The separable modes — multiply, screen, overlay and the rest — mix the two
12//! sides *arithmetically*, channel by channel. No combination of blend factors
13//! expresses that, so they need an advanced-blend extension and are
14//! capability-gated. Their formulas are fixed by the specification, which is
15//! what makes them testable against a computed expectation rather than against
16//! a recorded picture.
17//!
18//! The non-separable modes — hue, saturation, color and luminosity — need the
19//! same extension and differ again in kind: each output channel depends on all
20//! three input channels, because they are stated in terms of a color's hue,
21//! saturation and luminosity rather than of its components. They are checked
22//! against what they are named for rather than against values: whether hue kept
23//! the backdrop's luminosity, whether saturation took the source's.
24//!
25//! Every mode assumes **premultiplied** color, which is what the render target
26//! holds. The factors differ from the straight-alpha forms: source-over is
27//! `ONE` rather than `SRC_ALPHA`, because the source has already been scaled.
28
29/// A factor a blend equation multiplies one side by.
30///
31/// Portable rather than each backend naming its own, so the Porter-Duff table
32/// below exists once instead of once per backend, where the two copies would
33/// drift.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum BlendFactor {
36    Zero,
37    One,
38    SrcAlpha,
39    OneMinusSrcAlpha,
40    DstAlpha,
41    OneMinusDstAlpha,
42    DstColor,
43}
44
45/// The factors for one blend mode.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct BlendFactors {
48    pub src: BlendFactor,
49    pub dst: BlendFactor,
50}
51
52impl BlendFactors {
53    const fn new(src: BlendFactor, dst: BlendFactor) -> Self {
54        Self { src, dst }
55    }
56}
57
58/// How a draw combines with what a target already holds.
59///
60/// Three families: the Porter-Duff set, which decides which of the source and
61/// destination survive and where; the separable modes, which mix the two
62/// arithmetically channel by channel; and the non-separable modes, which
63/// exchange whole attributes of a color. Only the first works everywhere.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
65pub enum BlendMode {
66    /// Leave nothing behind. Neither source nor destination survives.
67    Clear,
68    /// Replace the destination outright, alpha included.
69    ///
70    /// Not the same as [`Self::SrcOver`] with an opaque source: this overwrites
71    /// destination alpha too, which decides whether a layer composites
72    /// correctly when it is itself drawn onto something else.
73    Src,
74    /// Keep the destination and discard the source.
75    Dst,
76    /// Source over destination: the default for drawing one thing on another.
77    #[default]
78    SrcOver,
79    /// Destination over source, as though the source were drawn underneath.
80    DstOver,
81    /// Source, clipped to where the destination is.
82    SrcIn,
83    /// Destination, clipped to where the source is.
84    DstIn,
85    /// Source, clipped to where the destination is not.
86    SrcOut,
87    /// Destination, clipped to where the source is not.
88    DstOut,
89    /// Source drawn on top, but only within the destination's shape.
90    SrcATop,
91    /// Destination drawn on top, but only within the source's shape.
92    DstATop,
93    /// Whichever of the two is not overlapped by the other.
94    Xor,
95    /// The two added together, saturating.
96    ///
97    /// Used for light accumulation, where overlapping contributions should
98    /// brighten rather than replace.
99    Plus,
100    /// The two multiplied together.
101    Modulate,
102
103    // Everything below mixes the two sides arithmetically and requires
104    // advanced blending. The formulas are the separable ones from the
105    // compositing specification, applied per channel to unpremultiplied color.
106    /// Multiply the two, which always darkens.
107    Multiply,
108    /// The inverse of multiplying the inverses, which always lightens.
109    Screen,
110    /// Multiply or screen depending on the destination, so the destination
111    /// decides the contrast.
112    Overlay,
113    /// The darker of the two, per channel.
114    Darken,
115    /// The lighter of the two, per channel.
116    Lighten,
117    /// Brighten the destination in proportion to the source.
118    ColorDodge,
119    /// Darken the destination in proportion to the inverse of the source.
120    ColorBurn,
121    /// Overlay with the roles reversed, so the source decides the contrast.
122    HardLight,
123    /// A gentler hard-light, without the hard transition at the midpoint.
124    SoftLight,
125    /// The absolute difference, which inverts where the two agree.
126    Difference,
127    /// Like difference, but with a softer response near the midpoint.
128    Exclusion,
129
130    // Everything below is non-separable: each output channel depends on all
131    // three input channels, because these are stated in terms of a color's
132    // hue, saturation and luminosity rather than of its components.
133    /// The source's hue, with the backdrop's saturation and luminosity.
134    Hue,
135    /// The source's saturation, with the backdrop's hue and luminosity.
136    Saturation,
137    /// The source's hue and saturation, with the backdrop's luminosity.
138    Color,
139    /// The source's luminosity, with the backdrop's hue and saturation.
140    Luminosity,
141}
142
143impl BlendMode {
144    /// Whether this mode changes the destination where the source is absent.
145    ///
146    /// The modes for which "the source drew nothing here" is not the same as
147    /// "leave this pixel alone": `DstIn` zeroes what the source did not cover,
148    /// `Clear` zeroes everything, `Src` replaces it. A layer composited with
149    /// one of these has to cover everything it might affect rather than only
150    /// what it drew, so it is the test for whether a layer's target may be
151    /// narrowed to its content.
152    ///
153    /// The same nine upstream lists in `Entity::IsBlendModeDestructive`, and
154    /// the reason to match it exactly is that this decides a target's size:
155    /// a mode missing here is a layer that silently stops masking what it
156    /// used to.
157    pub fn is_destructive(self) -> bool {
158        matches!(
159            self,
160            Self::Clear
161                | Self::Src
162                | Self::SrcIn
163                | Self::DstIn
164                | Self::SrcOut
165                | Self::DstOut
166                | Self::DstATop
167                | Self::Xor
168                | Self::Modulate
169        )
170    }
171
172    /// The modes every device can do, with no extension.
173    pub const PORTER_DUFF: &'static [Self] = &[
174        Self::Clear,
175        Self::Src,
176        Self::Dst,
177        Self::SrcOver,
178        Self::DstOver,
179        Self::SrcIn,
180        Self::DstIn,
181        Self::SrcOut,
182        Self::DstOut,
183        Self::SrcATop,
184        Self::DstATop,
185        Self::Xor,
186        Self::Plus,
187        Self::Modulate,
188    ];
189
190    /// The modes that need advanced blending.
191    pub const ADVANCED: &'static [Self] = &[
192        Self::Multiply,
193        Self::Screen,
194        Self::Overlay,
195        Self::Darken,
196        Self::Lighten,
197        Self::ColorDodge,
198        Self::ColorBurn,
199        Self::HardLight,
200        Self::SoftLight,
201        Self::Difference,
202        Self::Exclusion,
203        Self::Hue,
204        Self::Saturation,
205        Self::Color,
206        Self::Luminosity,
207    ];
208
209    /// Every mode, for exhaustive tests and reporting.
210    ///
211    /// Deliberately *not* what a backend iterates to decide what it supports —
212    /// that is what [`Self::is_advanced`] and the capability flag are for.
213    pub const ALL: &'static [Self] = &[
214        Self::Clear,
215        Self::Src,
216        Self::Dst,
217        Self::SrcOver,
218        Self::DstOver,
219        Self::SrcIn,
220        Self::DstIn,
221        Self::SrcOut,
222        Self::DstOut,
223        Self::SrcATop,
224        Self::DstATop,
225        Self::Xor,
226        Self::Plus,
227        Self::Modulate,
228        Self::Multiply,
229        Self::Screen,
230        Self::Overlay,
231        Self::Darken,
232        Self::Lighten,
233        Self::ColorDodge,
234        Self::ColorBurn,
235        Self::HardLight,
236        Self::SoftLight,
237        Self::Difference,
238        Self::Exclusion,
239        Self::Hue,
240        Self::Saturation,
241        Self::Color,
242        Self::Luminosity,
243    ];
244
245    /// Whether this mode needs advanced blending.
246    ///
247    /// Callers check [`crate::Capabilities::advanced_blend`] before using one, and a
248    /// backend without it refuses rather than substituting something that looks
249    /// close: a silently wrong blend mode is a picture nobody can debug from.
250    /// The number a shader reads to identify this mode.
251    ///
252    /// Spelled out rather than cast from the discriminant, because the
253    /// discriminant is an implementation detail a reordering would change and
254    /// this number is written into a uniform buffer that a shader decodes. The
255    /// two must agree, and only one of them is visible from the shader.
256    pub const fn code(self) -> f32 {
257        match self {
258            Self::Clear => 0.0,
259            Self::Src => 1.0,
260            Self::Dst => 2.0,
261            Self::SrcOver => 3.0,
262            Self::DstOver => 4.0,
263            Self::SrcIn => 5.0,
264            Self::DstIn => 6.0,
265            Self::SrcOut => 7.0,
266            Self::DstOut => 8.0,
267            Self::SrcATop => 9.0,
268            Self::DstATop => 10.0,
269            Self::Xor => 11.0,
270            Self::Plus => 12.0,
271            Self::Modulate => 13.0,
272            Self::Multiply => 14.0,
273            Self::Screen => 15.0,
274            Self::Overlay => 16.0,
275            Self::Darken => 17.0,
276            Self::Lighten => 18.0,
277            Self::ColorDodge => 19.0,
278            Self::ColorBurn => 20.0,
279            Self::HardLight => 21.0,
280            Self::SoftLight => 22.0,
281            Self::Difference => 23.0,
282            Self::Exclusion => 24.0,
283            Self::Hue => 25.0,
284            Self::Saturation => 26.0,
285            Self::Color => 27.0,
286            Self::Luminosity => 28.0,
287        }
288    }
289
290    pub const fn is_advanced(self) -> bool {
291        !matches!(
292            self,
293            Self::Clear
294                | Self::Src
295                | Self::Dst
296                | Self::SrcOver
297                | Self::DstOver
298                | Self::SrcIn
299                | Self::DstIn
300                | Self::SrcOut
301                | Self::DstOut
302                | Self::SrcATop
303                | Self::DstATop
304                | Self::Xor
305                | Self::Plus
306                | Self::Modulate
307        )
308    }
309
310    /// The factors this mode blends with, assuming premultiplied color.
311    ///
312    /// `None` for an advanced mode: those are not expressible as factors at
313    /// all, which is exactly why they need an extension. Returning an
314    /// option rather than a plausible pair keeps a backend from silently
315    /// rendering the wrong thing.
316    pub const fn factors(self) -> Option<BlendFactors> {
317        use BlendFactor::*;
318        Some(match self {
319            Self::Clear => BlendFactors::new(Zero, Zero),
320            Self::Src => BlendFactors::new(One, Zero),
321            Self::Dst => BlendFactors::new(Zero, One),
322            Self::SrcOver => BlendFactors::new(One, OneMinusSrcAlpha),
323            Self::DstOver => BlendFactors::new(OneMinusDstAlpha, One),
324            Self::SrcIn => BlendFactors::new(DstAlpha, Zero),
325            Self::DstIn => BlendFactors::new(Zero, SrcAlpha),
326            Self::SrcOut => BlendFactors::new(OneMinusDstAlpha, Zero),
327            Self::DstOut => BlendFactors::new(Zero, OneMinusSrcAlpha),
328            Self::SrcATop => BlendFactors::new(DstAlpha, OneMinusSrcAlpha),
329            Self::DstATop => BlendFactors::new(OneMinusDstAlpha, SrcAlpha),
330            Self::Xor => BlendFactors::new(OneMinusDstAlpha, OneMinusSrcAlpha),
331            Self::Plus => BlendFactors::new(One, One),
332            Self::Modulate => BlendFactors::new(DstColor, Zero),
333            _ => return None,
334        })
335    }
336
337    /// Whether the destination contributes to the result.
338    ///
339    /// A mode that ignores it can skip loading the target on a tiler, which is
340    /// a bandwidth saving rather than a correctness one.
341    pub const fn reads_destination(self) -> bool {
342        match self.factors() {
343            Some(factors) => {
344                !matches!(factors.dst, BlendFactor::Zero)
345                    || matches!(
346                        factors.src,
347                        BlendFactor::DstAlpha
348                            | BlendFactor::OneMinusDstAlpha
349                            | BlendFactor::DstColor
350                    )
351            }
352            // Every advanced mode is a function of both sides by definition.
353            None => true,
354        }
355    }
356
357    /// Whether a source scaled by coverage composites as though the shape were
358    /// partly there.
359    ///
360    /// This is what decides whether a shape may be antialiased by computing
361    /// coverage in the fragment stage rather than by multisampling. Such a
362    /// shape is drawn on a quad larger than itself and emits a fragment
363    /// everywhere on it, including where coverage is nothing — so the mode has
364    /// to leave the destination alone for a fully transparent source, or the
365    /// quad erases what is behind it in the gap between the two.
366    ///
367    /// Which is exactly the destination factor being `One` or
368    /// `OneMinusSrcAlpha`: the source term vanishes with the alpha whatever its
369    /// own factor is, so only the destination's own weight is left. Every
370    /// advanced mode qualifies as well — their equations carry a
371    /// `(1 - source alpha)` on the destination by construction, and reduce to
372    /// it when the source contributes nothing.
373    ///
374    /// The same condition happens to be what makes the partly covered edge
375    /// correct rather than merely harmless. Weighting a premultiplied source by
376    /// coverage and compositing gives the same answer as mixing the unweighted
377    /// result into the destination by coverage, which is what multisampling
378    /// would have produced.
379    pub const fn respects_coverage(self) -> bool {
380        match self.factors() {
381            Some(factors) => matches!(
382                factors.dst,
383                BlendFactor::One | BlendFactor::OneMinusSrcAlpha
384            ),
385            None => true,
386        }
387    }
388
389    /// Whether this simply writes the source, so blending can be switched off.
390    pub const fn is_plain_write(self) -> bool {
391        matches!(self, Self::Src)
392    }
393
394    pub const fn name(self) -> &'static str {
395        match self {
396            Self::Clear => "clear",
397            Self::Src => "src",
398            Self::Dst => "dst",
399            Self::SrcOver => "src-over",
400            Self::DstOver => "dst-over",
401            Self::SrcIn => "src-in",
402            Self::DstIn => "dst-in",
403            Self::SrcOut => "src-out",
404            Self::DstOut => "dst-out",
405            Self::SrcATop => "src-atop",
406            Self::DstATop => "dst-atop",
407            Self::Xor => "xor",
408            Self::Plus => "plus",
409            Self::Modulate => "modulate",
410            Self::Multiply => "multiply",
411            Self::Screen => "screen",
412            Self::Overlay => "overlay",
413            Self::Darken => "darken",
414            Self::Lighten => "lighten",
415            Self::ColorDodge => "color-dodge",
416            Self::ColorBurn => "color-burn",
417            Self::HardLight => "hard-light",
418            Self::SoftLight => "soft-light",
419            Self::Difference => "difference",
420            Self::Exclusion => "exclusion",
421            Self::Hue => "hue",
422            Self::Saturation => "saturation",
423            Self::Color => "color",
424            Self::Luminosity => "luminosity",
425        }
426    }
427}
428
429impl std::fmt::Display for BlendMode {
430    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
431        f.write_str(self.name())
432    }
433}
434
435/// The separable blend function `B(Cb, Cs)`, on one unpremultiplied channel.
436///
437/// This is the compositing specification's definition transcribed directly.
438/// It exists so there is exactly one statement of what each mode means: the
439/// conformance tests check the hardware against it, and any future software
440/// path evaluates it. Nothing here is a second implementation of the backend
441/// mapping — that maps an enum to an extension's blend op and shares no code
442/// with this, which is what makes checking one against the other meaningful.
443///
444/// Returns `None` for a mode that is not separable-advanced.
445pub fn separable_blend(mode: BlendMode, backdrop: f32, source: f32) -> Option<f32> {
446    let (cb, cs) = (backdrop, source);
447    let multiply = |a: f32, b: f32| a * b;
448    let screen = |a: f32, b: f32| a + b - a * b;
449    // Hard-light is its own function *and* the body of overlay with the two
450    // sides exchanged, so it is written once and called twice.
451    let hard_light = |cb: f32, cs: f32| {
452        if cs <= 0.5 {
453            multiply(cb, 2.0 * cs)
454        } else {
455            screen(cb, 2.0 * cs - 1.0)
456        }
457    };
458    Some(match mode {
459        BlendMode::Multiply => multiply(cb, cs),
460        BlendMode::Screen => screen(cb, cs),
461        BlendMode::Overlay => hard_light(cs, cb),
462        BlendMode::Darken => cb.min(cs),
463        BlendMode::Lighten => cb.max(cs),
464        BlendMode::ColorDodge => {
465            // The order of these three cases is load-bearing: a black backdrop
466            // stays black even under a full-strength source, and only then does
467            // a full-strength source saturate. Swapping them makes the corner
468            // where both hold produce white instead of black.
469            if cb <= 0.0 {
470                0.0
471            } else if cs >= 1.0 {
472                1.0
473            } else {
474                (cb / (1.0 - cs)).min(1.0)
475            }
476        }
477        BlendMode::ColorBurn => {
478            if cb >= 1.0 {
479                1.0
480            } else if cs <= 0.0 {
481                0.0
482            } else {
483                1.0 - ((1.0 - cb) / cs).min(1.0)
484            }
485        }
486        BlendMode::HardLight => hard_light(cb, cs),
487        BlendMode::SoftLight => {
488            if cs <= 0.5 {
489                cb - (1.0 - 2.0 * cs) * cb * (1.0 - cb)
490            } else {
491                // The specification's D(Cb): a cubic below a quarter, a square
492                // root above it, chosen so the two meet with equal slope.
493                let d = if cb <= 0.25 {
494                    ((16.0 * cb - 12.0) * cb + 4.0) * cb
495                } else {
496                    cb.sqrt()
497                };
498                cb + (2.0 * cs - 1.0) * (d - cb)
499            }
500        }
501        BlendMode::Difference => (cs - cb).abs(),
502        BlendMode::Exclusion => cs + cb - 2.0 * cs * cb,
503        _ => return None,
504    })
505}
506
507/// The non-separable blend function, on unpremultiplied color.
508///
509/// These are stated in terms of a color's luminosity and saturation rather than
510/// of its components, so each output channel depends on all three inputs — the
511/// reason they cannot go through [`separable_blend`] and the reason they are a
512/// separate family rather than four more entries in the same table.
513///
514/// The helpers below are the specification's, transcribed directly. The one
515/// that repays reading is the clip: setting a luminosity can push a channel
516/// outside zero to one, and clipping each channel independently would change
517/// the hue that the whole operation exists to preserve. Scaling the color
518/// toward its own luminosity instead brings it back into range along a line
519/// that holds hue fixed.
520///
521/// Returns `None` for a mode that is not non-separable-advanced.
522pub fn nonseparable_blend(
523    mode: BlendMode,
524    backdrop: [f32; 3],
525    source: [f32; 3],
526) -> Option<[f32; 3]> {
527    // The specification's coefficients, which weight green far above blue
528    // because the eye does. Not the same as any of the standard luma matrices,
529    // and deliberately so: this is what the compositing specification says.
530    fn lum(c: [f32; 3]) -> f32 {
531        0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
532    }
533
534    fn clip_color(mut c: [f32; 3]) -> [f32; 3] {
535        let l = lum(c);
536        let n = c[0].min(c[1]).min(c[2]);
537        let x = c[0].max(c[1]).max(c[2]);
538        if n < 0.0 {
539            // Guarded because a color whose luminosity equals its darkest
540            // channel is already flat, and the scale would divide by zero.
541            let span = l - n;
542            if span > 0.0 {
543                for channel in &mut c {
544                    *channel = l + (*channel - l) * l / span;
545                }
546            }
547        }
548        if x > 1.0 {
549            let span = x - l;
550            if span > 0.0 {
551                for channel in &mut c {
552                    *channel = l + (*channel - l) * (1.0 - l) / span;
553                }
554            }
555        }
556        c
557    }
558
559    fn set_lum(mut c: [f32; 3], l: f32) -> [f32; 3] {
560        let d = l - lum(c);
561        for channel in &mut c {
562            *channel += d;
563        }
564        clip_color(c)
565    }
566
567    fn sat(c: [f32; 3]) -> f32 {
568        c[0].max(c[1]).max(c[2]) - c[0].min(c[1]).min(c[2])
569    }
570
571    /// Rescale a color to a given saturation, keeping which channel is which.
572    ///
573    /// Written by index rather than by sorting the components, because the
574    /// result has to go back where it came from: the middle channel of the
575    /// input stays the middle channel of the output.
576    fn set_sat(c: [f32; 3], s: f32) -> [f32; 3] {
577        let mut order = [0usize, 1, 2];
578        order.sort_by(|a, b| {
579            c[*a]
580                .partial_cmp(&c[*b])
581                .unwrap_or(std::cmp::Ordering::Equal)
582        });
583        let (low, mid, high) = (order[0], order[1], order[2]);
584        let mut out = [0.0f32; 3];
585        if c[high] > c[low] {
586            out[mid] = (c[mid] - c[low]) * s / (c[high] - c[low]);
587            out[high] = s;
588        }
589        // A flat color has no saturation to scale, so it stays flat at zero
590        // rather than being given one arbitrarily.
591        out[low] = 0.0;
592        out
593    }
594
595    let (cb, cs) = (backdrop, source);
596    Some(match mode {
597        BlendMode::Hue => set_lum(set_sat(cs, sat(cb)), lum(cb)),
598        BlendMode::Saturation => set_lum(set_sat(cb, sat(cs)), lum(cb)),
599        BlendMode::Color => set_lum(cs, lum(cb)),
600        BlendMode::Luminosity => set_lum(cb, lum(cs)),
601        _ => return None,
602    })
603}
604
605/// An advanced mode applied to premultiplied colors, giving premultiplied color.
606///
607/// The blend function itself is defined on unpremultiplied channels, so this
608/// un-premultiplies, blends, and recombines using the specification's
609/// composite: the blended color applies only where the two sides overlap, and
610/// each side survives alone where the other is absent.
611pub fn blend_advanced(mode: BlendMode, source: [f32; 4], backdrop: [f32; 4]) -> Option<[f32; 4]> {
612    let (a_s, a_b) = (source[3], backdrop[3]);
613    let mut out = [0.0f32; 4];
614    out[3] = a_s + a_b - a_s * a_b;
615
616    // Dividing by a zero alpha would give a NaN that then propagates through a
617    // term the same alpha multiplies away, so the color under a fully
618    // transparent side is taken as zero rather than computed.
619    //
620    // Clamped for a second reason, stated separately because it is a different
621    // one: the formulas below are defined on channels between zero and one and
622    // not outside, so a component describing a color the sRGB primaries cannot
623    // hold has to be brought to the triangle's edge before a mode is evaluated.
624    // The shader states the same domain at the same place, because these two
625    // exist to be checked against each other and a domain named in one and not
626    // the other is how they begin to disagree.
627    let straight = |c: [f32; 4], a: f32| {
628        if a > 0.0 {
629            [
630                (c[0] / a).clamp(0.0, 1.0),
631                (c[1] / a).clamp(0.0, 1.0),
632                (c[2] / a).clamp(0.0, 1.0),
633            ]
634        } else {
635            [0.0; 3]
636        }
637    };
638    let cs = straight(source, a_s);
639    let cb = straight(backdrop, a_b);
640
641    // The whole-color family first. A non-separable mode has no per-channel
642    // form, and trying the separable table first would have it fall through to
643    // a `None` that reads as "not an advanced mode at all".
644    let blended = match nonseparable_blend(mode, cb, cs) {
645        Some(blended) => blended,
646        None => {
647            let mut per_channel = [0.0f32; 3];
648            for (channel, slot) in per_channel.iter_mut().enumerate() {
649                *slot = separable_blend(mode, cb[channel], cs[channel])?;
650            }
651            per_channel
652        }
653    };
654
655    for channel in 0..3 {
656        out[channel] = a_s * (1.0 - a_b) * cs[channel]
657            + a_s * a_b * blended[channel]
658            + (1.0 - a_s) * a_b * cb[channel];
659    }
660    Some(out)
661}
662
663#[cfg(test)]
664mod coverage_tests {
665    use super::*;
666
667    #[test]
668    fn a_mode_respects_coverage_when_a_transparent_source_changes_nothing() {
669        // Worked out from the factors rather than listed, so a mode added later
670        // is classified rather than forgotten. These are the answers that
671        // classification has to produce.
672        for mode in [
673            BlendMode::SrcOver,
674            BlendMode::DstOver,
675            BlendMode::Dst,
676            BlendMode::Plus,
677            BlendMode::Xor,
678            BlendMode::SrcATop,
679            BlendMode::DstOut,
680        ] {
681            assert!(mode.respects_coverage(), "{} should", mode.name());
682        }
683        // These discard the destination where the source is transparent, so a
684        // quad larger than its shape would erase what is behind it.
685        for mode in [
686            BlendMode::Clear,
687            BlendMode::Src,
688            BlendMode::SrcIn,
689            BlendMode::DstIn,
690            BlendMode::SrcOut,
691            BlendMode::DstATop,
692            BlendMode::Modulate,
693        ] {
694            assert!(!mode.respects_coverage(), "{} should not", mode.name());
695        }
696    }
697
698    #[test]
699    fn every_advanced_mode_respects_coverage() {
700        // Their equations carry a (1 - source alpha) on the destination by
701        // construction, so a transparent source leaves it whole.
702        for mode in BlendMode::ALL {
703            if mode.is_advanced() {
704                assert!(mode.respects_coverage(), "{} should", mode.name());
705            }
706        }
707    }
708}
709
710#[cfg(test)]
711mod tests {
712    use super::*;
713
714    /// Evaluate a factor against premultiplied source and destination.
715    fn value(factor: BlendFactor, src: [f32; 4], dst: [f32; 4], channel: usize) -> f32 {
716        match factor {
717            BlendFactor::Zero => 0.0,
718            BlendFactor::One => 1.0,
719            BlendFactor::SrcAlpha => src[3],
720            BlendFactor::OneMinusSrcAlpha => 1.0 - src[3],
721            BlendFactor::DstAlpha => dst[3],
722            BlendFactor::OneMinusDstAlpha => 1.0 - dst[3],
723            BlendFactor::DstColor => dst[channel],
724        }
725    }
726
727    /// The blend equation, as the hardware applies it.
728    fn blend(mode: BlendMode, src: [f32; 4], dst: [f32; 4]) -> [f32; 4] {
729        let f = mode.factors().expect("Porter-Duff mode");
730        let mut out = [0.0f32; 4];
731        for channel in 0..4 {
732            out[channel] = (src[channel] * value(f.src, src, dst, channel)
733                + dst[channel] * value(f.dst, src, dst, channel))
734            .clamp(0.0, 1.0);
735        }
736        out
737    }
738
739    fn close(a: [f32; 4], b: [f32; 4]) -> bool {
740        a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-5)
741    }
742
743    // Premultiplied half-opaque red and opaque blue.
744    const SRC: [f32; 4] = [0.5, 0.0, 0.0, 0.5];
745    const DST: [f32; 4] = [0.0, 0.0, 1.0, 1.0];
746
747    #[test]
748    fn clear_leaves_nothing() {
749        assert!(close(blend(BlendMode::Clear, SRC, DST), [0.0; 4]));
750    }
751
752    #[test]
753    fn src_and_dst_each_keep_one_side_entirely() {
754        assert!(close(blend(BlendMode::Src, SRC, DST), SRC));
755        assert!(close(blend(BlendMode::Dst, SRC, DST), DST));
756    }
757
758    #[test]
759    fn source_over_and_destination_over_are_mirror_images() {
760        // Half-opaque red over opaque blue: half the blue survives.
761        assert!(close(
762            blend(BlendMode::SrcOver, SRC, DST),
763            [0.5, 0.0, 0.5, 1.0]
764        ));
765        // The other way round, the destination is opaque so nothing shows
766        // through and the source is entirely hidden.
767        assert!(close(blend(BlendMode::DstOver, SRC, DST), DST));
768    }
769
770    #[test]
771    fn the_in_modes_clip_one_side_to_the_other() {
772        // Source clipped to an opaque destination is the source unchanged.
773        assert!(close(blend(BlendMode::SrcIn, SRC, DST), SRC));
774        // Destination clipped to a half-opaque source keeps half of it.
775        assert!(close(
776            blend(BlendMode::DstIn, SRC, DST),
777            [0.0, 0.0, 0.5, 0.5]
778        ));
779    }
780
781    #[test]
782    fn the_out_modes_are_the_complement_of_the_in_modes() {
783        // The destination is opaque, so there is nowhere the source is outside
784        // it: source-out leaves nothing.
785        assert!(close(blend(BlendMode::SrcOut, SRC, DST), [0.0; 4]));
786        // Half the destination lies outside a half-opaque source.
787        assert!(close(
788            blend(BlendMode::DstOut, SRC, DST),
789            [0.0, 0.0, 0.5, 0.5]
790        ));
791    }
792
793    #[test]
794    fn atop_keeps_the_shape_of_the_side_it_is_named_for() {
795        // Source atop destination has the destination's shape: alpha stays at
796        // the destination's, which is what distinguishes it from source-over.
797        let result = blend(BlendMode::SrcATop, SRC, DST);
798        assert!((result[3] - DST[3]).abs() < 1e-5, "alpha should follow dst");
799        assert!(close(result, [0.5, 0.0, 0.5, 1.0]));
800    }
801
802    #[test]
803    fn xor_keeps_only_what_the_other_side_does_not_cover() {
804        // An opaque destination covers everything, so only the part of the
805        // destination outside the half-opaque source survives.
806        assert!(close(blend(BlendMode::Xor, SRC, DST), [0.0, 0.0, 0.5, 0.5]));
807    }
808
809    #[test]
810    fn plus_accumulates_and_saturates() {
811        let sum = blend(BlendMode::Plus, SRC, DST);
812        assert!(close(sum, [0.5, 0.0, 1.0, 1.0]));
813        // Saturating rather than wrapping: two bright sources must not go dark.
814        let bright = blend(BlendMode::Plus, [0.8, 0.8, 0.8, 1.0], [0.8, 0.8, 0.8, 1.0]);
815        assert!(close(bright, [1.0, 1.0, 1.0, 1.0]));
816    }
817
818    #[test]
819    fn modulate_multiplies_the_two() {
820        let result = blend(
821            BlendMode::Modulate,
822            [0.5, 1.0, 0.5, 1.0],
823            [0.5, 0.5, 1.0, 1.0],
824        );
825        assert!(close(result, [0.25, 0.5, 0.5, 1.0]));
826    }
827
828    #[test]
829    fn only_src_can_skip_blending_entirely() {
830        // Everything else needs the hardware to combine two sides, so treating
831        // another mode as a plain write would silently drop the destination.
832        for mode in BlendMode::PORTER_DUFF {
833            assert_eq!(
834                mode.is_plain_write(),
835                *mode == BlendMode::Src,
836                "{mode} misreported whether it is a plain write"
837            );
838        }
839    }
840
841    #[test]
842    fn modes_that_ignore_the_destination_are_identified() {
843        // A mode that never reads the target can skip loading it on a tiler.
844        for mode in [BlendMode::Clear, BlendMode::Src] {
845            assert!(!mode.reads_destination(), "{mode}");
846        }
847        for mode in [
848            BlendMode::SrcOver,
849            BlendMode::DstOver,
850            BlendMode::SrcIn,
851            BlendMode::Modulate,
852            BlendMode::Xor,
853        ] {
854            assert!(mode.reads_destination(), "{mode}");
855        }
856    }
857
858    #[test]
859    fn the_separable_modes_hit_their_defining_fixed_points() {
860        // Each identity below is what the mode is *for*, so getting one wrong
861        // means the transcription is wrong regardless of what the hardware does.
862        let cases: &[(BlendMode, f32, f32, f32)] = &[
863            (BlendMode::Multiply, 0.5, 0.5, 0.25),
864            (BlendMode::Screen, 0.5, 0.5, 0.75),
865            (BlendMode::Darken, 0.2, 0.8, 0.2),
866            (BlendMode::Lighten, 0.2, 0.8, 0.8),
867            (BlendMode::Difference, 0.2, 0.8, 0.6),
868            (BlendMode::Exclusion, 0.5, 0.5, 0.5),
869            // Half-strength hard-light and overlay are both the identity on the
870            // side that decides the contrast.
871            (BlendMode::HardLight, 0.3, 0.5, 0.3),
872            (BlendMode::Overlay, 0.5, 0.3, 0.3),
873            // Half-strength soft-light leaves the backdrop untouched.
874            (BlendMode::SoftLight, 0.3, 0.5, 0.3),
875            // The dodge and burn corners, where the ordering of the guards shows.
876            (BlendMode::ColorDodge, 0.0, 1.0, 0.0),
877            (BlendMode::ColorDodge, 0.25, 0.5, 0.5),
878            (BlendMode::ColorBurn, 1.0, 0.0, 1.0),
879            (BlendMode::ColorBurn, 0.5, 0.5, 0.0),
880        ];
881        for &(mode, cb, cs, want) in cases {
882            let got = separable_blend(mode, cb, cs).expect("separable");
883            assert!(
884                (got - want).abs() < 1e-6,
885                "{mode}(backdrop {cb}, source {cs}) gave {got}, want {want}"
886            );
887        }
888    }
889
890    #[test]
891    fn soft_light_is_continuous_where_its_two_branches_meet() {
892        // The branches join at a source of one half and at a backdrop of one
893        // quarter; a transcription error in either shows as a step.
894        for &(cb, cs) in &[(0.25, 0.5), (0.2499, 0.75), (0.5, 0.4999)] {
895            let here = separable_blend(BlendMode::SoftLight, cb, cs).unwrap();
896            let there = separable_blend(BlendMode::SoftLight, cb + 2e-4, cs + 2e-4).unwrap();
897            assert!((here - there).abs() < 1e-3, "step at ({cb}, {cs})");
898        }
899    }
900
901    #[test]
902    fn an_advanced_mode_over_nothing_is_just_the_source() {
903        // Where the backdrop is absent the blend function has nothing to mix
904        // with, so every mode must reduce to the source unchanged.
905        let src = [0.3, 0.0, 0.15, 0.6];
906        for mode in BlendMode::ADVANCED {
907            let out = blend_advanced(*mode, src, [0.0; 4]).expect("advanced");
908            assert!(close(out, src), "{mode} over nothing gave {out:?}");
909        }
910    }
911
912    #[test]
913    fn the_non_separable_modes_take_what_they_are_named_for() {
914        // Each of these is defined as taking some attributes from one side and
915        // the rest from the other, so the check is which side each attribute
916        // came from rather than a value someone recorded.
917        fn lum(c: [f32; 3]) -> f32 {
918            0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
919        }
920        fn sat(c: [f32; 3]) -> f32 {
921            c[0].max(c[1]).max(c[2]) - c[0].min(c[1]).min(c[2])
922        }
923
924        // Two requirements on these inputs, and both were found by getting them
925        // wrong. They must differ in luminosity and in saturation, or two of
926        // these four modes look identical — the first pair tried here agreed on
927        // luminosity exactly, by coincidence. And the results must stay inside
928        // zero to one, because the clip that brings an out-of-range color back
929        // reduces its saturation; that is correct behavior and would make the
930        // exact assertions below wrong, so the clip has a test of its own.
931        let cb = [0.15, 0.55, 0.35];
932        let cs = [0.75, 0.5, 0.45];
933        assert!((lum(cb) - lum(cs)).abs() > 0.05);
934        assert!((sat(cb) - sat(cs)).abs() > 0.05);
935
936        let close = |a: f32, b: f32| (a - b).abs() < 1e-4;
937
938        let hue = nonseparable_blend(BlendMode::Hue, cb, cs).unwrap();
939        assert!(close(lum(hue), lum(cb)), "hue kept the wrong luminosity");
940        assert!(close(sat(hue), sat(cb)), "hue kept the wrong saturation");
941
942        let saturation = nonseparable_blend(BlendMode::Saturation, cb, cs).unwrap();
943        assert!(
944            close(lum(saturation), lum(cb)),
945            "saturation moved luminosity"
946        );
947        assert!(
948            close(sat(saturation), sat(cs)),
949            "saturation did not take the source's"
950        );
951
952        let color = nonseparable_blend(BlendMode::Color, cb, cs).unwrap();
953        assert!(close(lum(color), lum(cb)), "color moved luminosity");
954
955        let luminosity = nonseparable_blend(BlendMode::Luminosity, cb, cs).unwrap();
956        assert!(
957            close(lum(luminosity), lum(cs)),
958            "luminosity did not take the source's"
959        );
960        assert!(
961            close(sat(luminosity), sat(cb)),
962            "luminosity moved saturation"
963        );
964    }
965
966    #[test]
967    fn setting_a_luminosity_keeps_every_channel_in_range() {
968        // Shifting a color's luminosity pushes channels outside zero to one,
969        // and clipping each independently would change the hue the operation
970        // exists to preserve. Scaling toward the luminosity is what brings them
971        // back along a line that holds hue fixed -- so this checks the range
972        // and, separately, that the result is still a scaling of the original.
973        for (cb, cs) in [
974            ([0.02, 0.02, 0.9], [0.99, 0.99, 0.99]),
975            ([0.99, 0.5, 0.02], [0.01, 0.01, 0.01]),
976            ([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]),
977            ([1.0, 1.0, 1.0], [0.0, 0.0, 0.0]),
978        ] {
979            for mode in [BlendMode::Color, BlendMode::Luminosity, BlendMode::Hue] {
980                let out = nonseparable_blend(mode, cb, cs).unwrap();
981                for channel in out {
982                    assert!(
983                        (-1e-5..=1.0 + 1e-5).contains(&channel),
984                        "{mode} on {cb:?} and {cs:?} gave {out:?}"
985                    );
986                    assert!(channel.is_finite(), "{mode} produced {out:?}");
987                }
988            }
989        }
990    }
991
992    #[test]
993    fn a_flat_backdrop_has_no_saturation_to_take() {
994        // A gray has no hue and no saturation, so a mode asking for either gets
995        // a gray back. The guard that makes this work is also the one that
996        // stops a division by zero, which is why it is worth pinning.
997        let gray = [0.4, 0.4, 0.4];
998        let colorful = [0.9, 0.2, 0.5];
999        let hue = nonseparable_blend(BlendMode::Hue, gray, colorful).unwrap();
1000        assert!(
1001            hue.iter().all(|c| (c - hue[0]).abs() < 1e-5),
1002            "hue from a gray backdrop came back colored: {hue:?}"
1003        );
1004        let saturation = nonseparable_blend(BlendMode::Saturation, gray, colorful).unwrap();
1005        assert!(
1006            saturation.iter().all(|c| (c - saturation[0]).abs() < 1e-5),
1007            "saturating a gray produced color from nothing: {saturation:?}"
1008        );
1009    }
1010
1011    #[test]
1012    fn the_two_families_do_not_overlap() {
1013        // Every mode belongs to exactly one family, and the family it claims
1014        // agrees with whether it has factors.
1015        assert_eq!(
1016            BlendMode::PORTER_DUFF.len() + BlendMode::ADVANCED.len(),
1017            BlendMode::ALL.len()
1018        );
1019        for mode in BlendMode::ALL {
1020            assert_eq!(
1021                mode.is_advanced(),
1022                mode.factors().is_none(),
1023                "{mode} disagrees with itself about which family it is in"
1024            );
1025            let has_formula = separable_blend(*mode, 0.5, 0.5).is_some()
1026                || nonseparable_blend(*mode, [0.5; 3], [0.5; 3]).is_some();
1027            assert_eq!(
1028                mode.is_advanced(),
1029                has_formula,
1030                "{mode} has no advanced formula but claims to be advanced"
1031            );
1032            // And never both, since the two families are evaluated differently
1033            // and a mode in both would take whichever path was tried first.
1034            assert!(
1035                !(separable_blend(*mode, 0.5, 0.5).is_some()
1036                    && nonseparable_blend(*mode, [0.5; 3], [0.5; 3]).is_some()),
1037                "{mode} has a formula in both families"
1038            );
1039        }
1040    }
1041
1042    #[test]
1043    fn every_mode_has_a_distinct_name() {
1044        let mut names: Vec<&str> = BlendMode::ALL.iter().map(|m| m.name()).collect();
1045        names.sort_unstable();
1046        let count = names.len();
1047        names.dedup();
1048        assert_eq!(names.len(), count, "duplicate blend mode name");
1049    }
1050}