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}