tiny_skia/pipeline/
mod.rs

1// Copyright 2016 Google Inc.
2// Copyright 2020 Yevhenii Reizner
3//
4// Use of this source code is governed by a BSD-style license that can be
5// found in the LICENSE file.
6
7/*!
8A raster pipeline implementation.
9
10Despite having a lot of changes compared to `SkRasterPipeline`,
11the core principles are the same:
12
131. A pipeline consists of stages.
141. A pipeline has a global context shared by all stages.
15   Unlike Skia, were each stage has it's own, possibly shared, context.
161. Each stage has a high precision implementation. See `highp.rs`.
171. Some stages have a low precision implementation. See `lowp.rs`.
181. Each stage calls the "next" stage after its done.
191. During pipeline "compilation", if **all** stages have a lowp implementation,
20   the lowp pipeline will be used. Otherwise, the highp variant will be used.
211. The pipeline "compilation" produces a list of function pointer.
22   The last pointer is a pointer to the "return" function,
23   which simply stops the execution of the pipeline.
24
25This implementation is a bit tricky, but it gives the maximum performance.
26A simple and straightforward implementation using traits and loops, like:
27
28```ignore
29trait StageTrait {
30    fn apply(&mut self, pixels: &mut [Pixel]);
31}
32
33let stages: Vec<&mut dyn StageTrait>;
34for stage in stages {
35    stage.apply(pixels);
36}
37```
38
39will be at least 20-30% slower. Not really sure why.
40
41Also, since this module is all about performance, any kind of branching is
42strictly forbidden. All stage functions must not use `if`, `match` or loops.
43There are still some exceptions, which are basically an imperfect implementations
44and should be optimized out in the future.
45*/
46
47use alloc::vec::Vec;
48
49use arrayvec::ArrayVec;
50
51use tiny_skia_path::NormalizedF32;
52
53use crate::{Color, PremultipliedColor, PremultipliedColorU8, SpreadMode};
54use crate::{PixmapRef, Transform};
55
56pub use blitter::RasterPipelineBlitter;
57
58use crate::geom::ScreenIntRect;
59use crate::pixmap::SubPixmapMut;
60use crate::wide::u32x8;
61
62mod blitter;
63#[rustfmt::skip] mod highp;
64#[rustfmt::skip] mod lowp;
65
66const MAX_STAGES: usize = 32; // More than enough.
67
68#[allow(dead_code)]
69#[derive(Copy, Clone, Debug)]
70pub enum Stage {
71    MoveSourceToDestination = 0,
72    MoveDestinationToSource,
73    Clamp0,
74    ClampA,
75    Premultiply,
76    UniformColor,
77    SeedShader,
78    LoadDestination,
79    Store,
80    LoadDestinationU8,
81    StoreU8,
82    Gather,
83    LoadMaskU8,
84    MaskU8,
85    ScaleU8,
86    LerpU8,
87    Scale1Float,
88    Lerp1Float,
89    DestinationAtop,
90    DestinationIn,
91    DestinationOut,
92    DestinationOver,
93    SourceAtop,
94    SourceIn,
95    SourceOut,
96    SourceOver,
97    Clear,
98    Modulate,
99    Multiply,
100    Plus,
101    Screen,
102    Xor,
103    ColorBurn,
104    ColorDodge,
105    Darken,
106    Difference,
107    Exclusion,
108    HardLight,
109    Lighten,
110    Overlay,
111    SoftLight,
112    Hue,
113    Saturation,
114    Color,
115    Luminosity,
116    SourceOverRgba,
117    Transform,
118    Reflect,
119    Repeat,
120    Bilinear,
121    Bicubic,
122    PadX1,
123    ReflectX1,
124    RepeatX1,
125    Gradient,
126    EvenlySpaced2StopGradient,
127    XYToRadius,
128    XYTo2PtConicalFocalOnCircle,
129    XYTo2PtConicalWellBehaved,
130    XYTo2PtConicalGreater,
131    Mask2PtConicalDegenerates,
132    ApplyVectorMask,
133}
134
135pub const STAGES_COUNT: usize = Stage::ApplyVectorMask as usize + 1;
136
137impl<'a> PixmapRef<'a> {
138    #[inline(always)]
139    pub(crate) fn gather(&self, index: u32x8) -> [PremultipliedColorU8; highp::STAGE_WIDTH] {
140        let index: [u32; 8] = bytemuck::cast(index);
141        let pixels = self.pixels();
142        [
143            pixels[index[0] as usize],
144            pixels[index[1] as usize],
145            pixels[index[2] as usize],
146            pixels[index[3] as usize],
147            pixels[index[4] as usize],
148            pixels[index[5] as usize],
149            pixels[index[6] as usize],
150            pixels[index[7] as usize],
151        ]
152    }
153}
154
155impl<'a> SubPixmapMut<'a> {
156    #[inline(always)]
157    pub(crate) fn offset(&self, dx: usize, dy: usize) -> usize {
158        self.real_width * dy + dx
159    }
160
161    #[inline(always)]
162    pub(crate) fn slice_at_xy(&mut self, dx: usize, dy: usize) -> &mut [PremultipliedColorU8] {
163        let offset = self.offset(dx, dy);
164        &mut self.pixels_mut()[offset..]
165    }
166
167    #[inline(always)]
168    pub(crate) fn slice_mask_at_xy(&mut self, dx: usize, dy: usize) -> &mut [u8] {
169        let offset = self.offset(dx, dy);
170        &mut self.data[offset..]
171    }
172
173    #[inline(always)]
174    pub(crate) fn slice4_at_xy(
175        &mut self,
176        dx: usize,
177        dy: usize,
178    ) -> &mut [PremultipliedColorU8; highp::STAGE_WIDTH] {
179        arrayref::array_mut_ref!(self.pixels_mut(), self.offset(dx, dy), highp::STAGE_WIDTH)
180    }
181
182    #[inline(always)]
183    pub(crate) fn slice16_at_xy(
184        &mut self,
185        dx: usize,
186        dy: usize,
187    ) -> &mut [PremultipliedColorU8; lowp::STAGE_WIDTH] {
188        arrayref::array_mut_ref!(self.pixels_mut(), self.offset(dx, dy), lowp::STAGE_WIDTH)
189    }
190
191    #[inline(always)]
192    pub(crate) fn slice16_mask_at_xy(
193        &mut self,
194        dx: usize,
195        dy: usize,
196    ) -> &mut [u8; lowp::STAGE_WIDTH] {
197        arrayref::array_mut_ref!(self.data, self.offset(dx, dy), lowp::STAGE_WIDTH)
198    }
199}
200
201#[derive(Default, Debug)]
202pub struct AAMaskCtx {
203    pub pixels: [u8; 2],
204    pub stride: u32,  // can be zero
205    pub shift: usize, // mask offset/position in pixmap coordinates
206}
207
208impl AAMaskCtx {
209    #[inline(always)]
210    pub fn copy_at_xy(&self, dx: usize, dy: usize, tail: usize) -> [u8; 2] {
211        let offset = (self.stride as usize * dy + dx) - self.shift;
212        // We have only 3 variants, so unroll them.
213        match (offset, tail) {
214            (0, 1) => [self.pixels[0], 0],
215            (0, 2) => [self.pixels[0], self.pixels[1]],
216            (1, 1) => [self.pixels[1], 0],
217            _ => [0, 0], // unreachable
218        }
219    }
220}
221
222#[derive(Copy, Clone, Debug, Default)]
223pub struct MaskCtx<'a> {
224    pub data: &'a [u8],
225    pub real_width: u32,
226}
227
228impl MaskCtx<'_> {
229    #[inline(always)]
230    fn offset(&self, dx: usize, dy: usize) -> usize {
231        self.real_width as usize * dy + dx
232    }
233}
234
235#[derive(Default)]
236pub struct Context {
237    pub current_coverage: f32,
238    pub sampler: SamplerCtx,
239    pub uniform_color: UniformColorCtx,
240    pub evenly_spaced_2_stop_gradient: EvenlySpaced2StopGradientCtx,
241    pub gradient: GradientCtx,
242    pub two_point_conical_gradient: TwoPointConicalGradientCtx,
243    pub limit_x: TileCtx,
244    pub limit_y: TileCtx,
245    pub transform: Transform,
246}
247
248#[derive(Copy, Clone, Default, Debug)]
249pub struct SamplerCtx {
250    pub spread_mode: SpreadMode,
251    pub inv_width: f32,
252    pub inv_height: f32,
253}
254
255#[derive(Copy, Clone, Default, Debug)]
256pub struct UniformColorCtx {
257    pub r: f32,
258    pub g: f32,
259    pub b: f32,
260    pub a: f32,
261    pub rgba: [u16; 4], // [0,255] in a 16-bit lane.
262}
263
264// A gradient color is an unpremultiplied RGBA not in a 0..1 range.
265// It basically can have any float value.
266#[derive(Copy, Clone, Default, Debug)]
267pub struct GradientColor {
268    pub r: f32,
269    pub g: f32,
270    pub b: f32,
271    pub a: f32,
272}
273
274impl GradientColor {
275    pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
276        GradientColor { r, g, b, a }
277    }
278}
279
280impl From<Color> for GradientColor {
281    fn from(c: Color) -> Self {
282        GradientColor {
283            r: c.red(),
284            g: c.green(),
285            b: c.blue(),
286            a: c.alpha(),
287        }
288    }
289}
290
291#[derive(Copy, Clone, Default, Debug)]
292pub struct EvenlySpaced2StopGradientCtx {
293    pub factor: GradientColor,
294    pub bias: GradientColor,
295}
296
297#[derive(Clone, Default, Debug)]
298pub struct GradientCtx {
299    /// This value stores the actual colors count.
300    /// `factors` and `biases` must store at least 16 values,
301    /// since this is the length of a lowp pipeline stage.
302    /// So any any value past `len` is just zeros.
303    pub len: usize,
304    pub factors: Vec<GradientColor>,
305    pub biases: Vec<GradientColor>,
306    pub t_values: Vec<NormalizedF32>,
307}
308
309impl GradientCtx {
310    pub fn push_const_color(&mut self, color: GradientColor) {
311        self.factors.push(GradientColor::new(0.0, 0.0, 0.0, 0.0));
312        self.biases.push(color);
313    }
314}
315
316#[derive(Copy, Clone, Default, Debug)]
317pub struct TwoPointConicalGradientCtx {
318    // This context is used only in highp, where we use Tx4.
319    pub mask: u32x8,
320    pub p0: f32,
321}
322
323#[derive(Copy, Clone, Default, Debug)]
324pub struct TileCtx {
325    pub scale: f32,
326    pub inv_scale: f32, // cache of 1/scale
327}
328
329pub struct RasterPipelineBuilder {
330    stages: ArrayVec<Stage, MAX_STAGES>,
331    force_hq_pipeline: bool,
332    pub ctx: Context,
333}
334
335impl RasterPipelineBuilder {
336    pub fn new() -> Self {
337        RasterPipelineBuilder {
338            stages: ArrayVec::new(),
339            force_hq_pipeline: false,
340            ctx: Context::default(),
341        }
342    }
343
344    pub fn set_force_hq_pipeline(&mut self, hq: bool) {
345        self.force_hq_pipeline = hq;
346    }
347
348    pub fn push(&mut self, stage: Stage) {
349        self.stages.push(stage);
350    }
351
352    pub fn push_transform(&mut self, ts: Transform) {
353        if ts.is_finite() && !ts.is_identity() {
354            self.stages.push(Stage::Transform);
355            self.ctx.transform = ts;
356        }
357    }
358
359    pub fn push_uniform_color(&mut self, c: PremultipliedColor) {
360        let r = c.red();
361        let g = c.green();
362        let b = c.blue();
363        let a = c.alpha();
364        let rgba = [
365            (r * 255.0 + 0.5) as u16,
366            (g * 255.0 + 0.5) as u16,
367            (b * 255.0 + 0.5) as u16,
368            (a * 255.0 + 0.5) as u16,
369        ];
370
371        let ctx = UniformColorCtx { r, g, b, a, rgba };
372
373        self.stages.push(Stage::UniformColor);
374        self.ctx.uniform_color = ctx;
375    }
376
377    pub fn compile(self) -> RasterPipeline {
378        if self.stages.is_empty() {
379            return RasterPipeline {
380                kind: RasterPipelineKind::High {
381                    functions: ArrayVec::new(),
382                    tail_functions: ArrayVec::new(),
383                },
384                ctx: Context::default(),
385            };
386        }
387
388        let is_lowp_compatible = self
389            .stages
390            .iter()
391            .all(|stage| !lowp::fn_ptr_eq(lowp::STAGES[*stage as usize], lowp::null_fn));
392
393        if self.force_hq_pipeline || !is_lowp_compatible {
394            let mut functions: ArrayVec<_, MAX_STAGES> = self
395                .stages
396                .iter()
397                .map(|stage| highp::STAGES[*stage as usize] as highp::StageFn)
398                .collect();
399            functions.push(highp::just_return as highp::StageFn);
400
401            // I wasn't able to reproduce Skia's load_8888_/store_8888_ performance.
402            // Skia uses fallthrough switch, which is probably the reason.
403            // In Rust, any branching in load/store code drastically affects the performance.
404            // So instead, we're using two "programs": one for "full stages" and one for "tail stages".
405            // While the only difference is the load/store methods.
406            let mut tail_functions = functions.clone();
407            for fun in &mut tail_functions {
408                if highp::fn_ptr(*fun) == highp::fn_ptr(highp::load_dst) {
409                    *fun = highp::load_dst_tail as highp::StageFn;
410                } else if highp::fn_ptr(*fun) == highp::fn_ptr(highp::store) {
411                    *fun = highp::store_tail as highp::StageFn;
412                } else if highp::fn_ptr(*fun) == highp::fn_ptr(highp::load_dst_u8) {
413                    *fun = highp::load_dst_u8_tail as highp::StageFn;
414                } else if highp::fn_ptr(*fun) == highp::fn_ptr(highp::store_u8) {
415                    *fun = highp::store_u8_tail as highp::StageFn;
416                } else if highp::fn_ptr(*fun) == highp::fn_ptr(highp::source_over_rgba) {
417                    // SourceOverRgba calls load/store manually, without the pipeline,
418                    // therefore we have to switch it too.
419                    *fun = highp::source_over_rgba_tail as highp::StageFn;
420                }
421            }
422
423            RasterPipeline {
424                kind: RasterPipelineKind::High {
425                    functions,
426                    tail_functions,
427                },
428                ctx: self.ctx,
429            }
430        } else {
431            let mut functions: ArrayVec<_, MAX_STAGES> = self
432                .stages
433                .iter()
434                .map(|stage| lowp::STAGES[*stage as usize] as lowp::StageFn)
435                .collect();
436            functions.push(lowp::just_return as lowp::StageFn);
437
438            // See above.
439            let mut tail_functions = functions.clone();
440            for fun in &mut tail_functions {
441                if lowp::fn_ptr(*fun) == lowp::fn_ptr(lowp::load_dst) {
442                    *fun = lowp::load_dst_tail as lowp::StageFn;
443                } else if lowp::fn_ptr(*fun) == lowp::fn_ptr(lowp::store) {
444                    *fun = lowp::store_tail as lowp::StageFn;
445                } else if lowp::fn_ptr(*fun) == lowp::fn_ptr(lowp::load_dst_u8) {
446                    *fun = lowp::load_dst_u8_tail as lowp::StageFn;
447                } else if lowp::fn_ptr(*fun) == lowp::fn_ptr(lowp::store_u8) {
448                    *fun = lowp::store_u8_tail as lowp::StageFn;
449                } else if lowp::fn_ptr(*fun) == lowp::fn_ptr(lowp::source_over_rgba) {
450                    // SourceOverRgba calls load/store manually, without the pipeline,
451                    // therefore we have to switch it too.
452                    *fun = lowp::source_over_rgba_tail as lowp::StageFn;
453                }
454            }
455
456            RasterPipeline {
457                kind: RasterPipelineKind::Low {
458                    functions,
459                    tail_functions,
460                },
461                ctx: self.ctx,
462            }
463        }
464    }
465}
466
467pub enum RasterPipelineKind {
468    High {
469        functions: ArrayVec<highp::StageFn, MAX_STAGES>,
470        tail_functions: ArrayVec<highp::StageFn, MAX_STAGES>,
471    },
472    Low {
473        functions: ArrayVec<lowp::StageFn, MAX_STAGES>,
474        tail_functions: ArrayVec<lowp::StageFn, MAX_STAGES>,
475    },
476}
477
478pub struct RasterPipeline {
479    kind: RasterPipelineKind,
480    pub ctx: Context,
481}
482
483impl RasterPipeline {
484    pub fn run(
485        &mut self,
486        rect: &ScreenIntRect,
487        aa_mask_ctx: AAMaskCtx,
488        mask_ctx: MaskCtx,
489        pixmap_src: PixmapRef,
490        pixmap_dst: &mut SubPixmapMut,
491    ) {
492        match self.kind {
493            RasterPipelineKind::High {
494                ref functions,
495                ref tail_functions,
496            } => {
497                highp::start(
498                    functions.as_slice(),
499                    tail_functions.as_slice(),
500                    rect,
501                    aa_mask_ctx,
502                    mask_ctx,
503                    &mut self.ctx,
504                    pixmap_src,
505                    pixmap_dst,
506                );
507            }
508            RasterPipelineKind::Low {
509                ref functions,
510                ref tail_functions,
511            } => {
512                lowp::start(
513                    functions.as_slice(),
514                    tail_functions.as_slice(),
515                    rect,
516                    aa_mask_ctx,
517                    mask_ctx,
518                    &mut self.ctx,
519                    // lowp doesn't support pattern, so no `pixmap_src` for it.
520                    pixmap_dst,
521                );
522            }
523        }
524    }
525}
526
527#[rustfmt::skip]
528#[cfg(test)]
529mod blend_tests {
530    // Test blending modes.
531    //
532    // Skia has two kinds of a raster pipeline: high and low precision.
533    // "High" uses f32 and "low" uses u16.
534    // And for basic operations we don't need f32 and u16 simply faster.
535    // But those modes are not identical. They can produce slightly different results
536    // due rounding.
537
538    use super::*;
539    use crate::{BlendMode, Color, Pixmap, PremultipliedColorU8};
540    use crate::geom::IntSizeExt;
541
542    macro_rules! test_blend {
543        ($name:ident, $mode:expr, $is_highp:expr, $r:expr, $g:expr, $b:expr, $a:expr) => {
544            #[test]
545            fn $name() {
546                let mut pixmap = Pixmap::new(1, 1).unwrap();
547                pixmap.fill(Color::from_rgba8(50, 127, 150, 200));
548
549                let pixmap_src = PixmapRef::from_bytes(&[0, 0, 0, 0], 1, 1).unwrap();
550
551                let mut p = RasterPipelineBuilder::new();
552                p.set_force_hq_pipeline($is_highp);
553                p.push_uniform_color(Color::from_rgba8(220, 140, 75, 180).premultiply());
554                p.push(Stage::LoadDestination);
555                p.push($mode.to_stage().unwrap());
556                p.push(Stage::Store);
557                let mut p = p.compile();
558                let rect = pixmap.size().to_screen_int_rect(0, 0);
559                p.run(&rect, AAMaskCtx::default(), MaskCtx::default(), pixmap_src,
560                      &mut pixmap.as_mut().as_subpixmap());
561
562                assert_eq!(
563                    pixmap.as_ref().pixel(0, 0).unwrap(),
564                    PremultipliedColorU8::from_rgba($r, $g, $b, $a).unwrap()
565                );
566            }
567        };
568    }
569
570    macro_rules! test_blend_lowp {
571        ($name:ident, $mode:expr, $r:expr, $g:expr, $b:expr, $a:expr) => (
572            test_blend!{$name, $mode, false, $r, $g, $b, $a}
573        )
574    }
575
576    macro_rules! test_blend_highp {
577        ($name:ident, $mode:expr, $r:expr, $g:expr, $b:expr, $a:expr) => (
578            test_blend!{$name, $mode, true, $r, $g, $b, $a}
579        )
580    }
581
582    test_blend_lowp!(clear_lowp,              BlendMode::Clear,                 0,   0,   0,   0);
583    // Source is a no-op
584    test_blend_lowp!(destination_lowp,        BlendMode::Destination,          39, 100, 118, 200);
585    test_blend_lowp!(source_over_lowp,        BlendMode::SourceOver,          167, 129,  88, 239);
586    test_blend_lowp!(destination_over_lowp,   BlendMode::DestinationOver,      73, 122, 130, 239);
587    test_blend_lowp!(source_in_lowp,          BlendMode::SourceIn,            122,  78,  42, 141);
588    test_blend_lowp!(destination_in_lowp,     BlendMode::DestinationIn,        28,  71,  83, 141);
589    test_blend_lowp!(source_out_lowp,         BlendMode::SourceOut,            34,  22,  12,  39);
590    test_blend_lowp!(destination_out_lowp,    BlendMode::DestinationOut,       12,  30,  35,  59);
591    test_blend_lowp!(source_atop_lowp,        BlendMode::SourceAtop,          133, 107,  76, 200);
592    test_blend_lowp!(destination_atop_lowp,   BlendMode::DestinationAtop,      61,  92,  95, 180);
593    test_blend_lowp!(xor_lowp,                BlendMode::Xor,                  45,  51,  46,  98);
594    test_blend_lowp!(plus_lowp,               BlendMode::Plus,                194, 199, 171, 255);
595    test_blend_lowp!(modulate_lowp,           BlendMode::Modulate,             24,  39,  25, 141);
596    test_blend_lowp!(screen_lowp,             BlendMode::Screen,              170, 160, 146, 239);
597    test_blend_lowp!(overlay_lowp,            BlendMode::Overlay,              92, 128, 106, 239);
598    test_blend_lowp!(darken_lowp,             BlendMode::Darken,               72, 121,  88, 239);
599    test_blend_lowp!(lighten_lowp,            BlendMode::Lighten,             166, 128, 129, 239);
600    // ColorDodge in not available for lowp.
601    // ColorBurn in not available for lowp.
602    test_blend_lowp!(hard_light_lowp,         BlendMode::HardLight,           154, 128,  95, 239);
603    // SoftLight in not available for lowp.
604    test_blend_lowp!(difference_lowp,         BlendMode::Difference,          138,  57,  87, 239);
605    test_blend_lowp!(exclusion_lowp,          BlendMode::Exclusion,           146, 121, 121, 239);
606    test_blend_lowp!(multiply_lowp,           BlendMode::Multiply,             69,  90,  71, 238);
607    // Hue in not available for lowp.
608    // Saturation in not available for lowp.
609    // Color in not available for lowp.
610    // Luminosity in not available for lowp.
611
612    test_blend_highp!(clear_highp,            BlendMode::Clear,                 0,   0,   0,   0);
613    // Source is a no-op
614    test_blend_highp!(destination_highp,      BlendMode::Destination,          39, 100, 118, 200);
615    test_blend_highp!(source_over_highp,      BlendMode::SourceOver,          167, 128,  88, 239);
616    test_blend_highp!(destination_over_highp, BlendMode::DestinationOver,      72, 121, 129, 239);
617    test_blend_highp!(source_in_highp,        BlendMode::SourceIn,            122,  78,  42, 141);
618    test_blend_highp!(destination_in_highp,   BlendMode::DestinationIn,        28,  71,  83, 141);
619    test_blend_highp!(source_out_highp,       BlendMode::SourceOut,            33,  21,  11,  39);
620    test_blend_highp!(destination_out_highp,  BlendMode::DestinationOut,       11,  29,  35,  59);
621    test_blend_highp!(source_atop_highp,      BlendMode::SourceAtop,          133, 107,  76, 200);
622    test_blend_highp!(destination_atop_highp, BlendMode::DestinationAtop,      61,  92,  95, 180);
623    test_blend_highp!(xor_highp,              BlendMode::Xor,                  45,  51,  46,  98);
624    test_blend_highp!(plus_highp,             BlendMode::Plus,                194, 199, 171, 255);
625    test_blend_highp!(modulate_highp,         BlendMode::Modulate,             24,  39,  24, 141);
626    test_blend_highp!(screen_highp,           BlendMode::Screen,              171, 160, 146, 239);
627    test_blend_highp!(overlay_highp,          BlendMode::Overlay,              92, 128, 106, 239);
628    test_blend_highp!(darken_highp,           BlendMode::Darken,               72, 121,  88, 239);
629    test_blend_highp!(lighten_highp,          BlendMode::Lighten,             167, 128, 129, 239);
630    test_blend_highp!(color_dodge_highp,      BlendMode::ColorDodge,          186, 192, 164, 239);
631    test_blend_highp!(color_burn_highp,       BlendMode::ColorBurn,            54,  63,  46, 239);
632    test_blend_highp!(hard_light_highp,       BlendMode::HardLight,           155, 128,  95, 239);
633    test_blend_highp!(soft_light_highp,       BlendMode::SoftLight,            98, 124, 115, 239);
634    test_blend_highp!(difference_highp,       BlendMode::Difference,          139,  58,  88, 239);
635    test_blend_highp!(exclusion_highp,        BlendMode::Exclusion,           147, 121, 122, 239);
636    test_blend_highp!(multiply_highp,         BlendMode::Multiply,             69,  89,  71, 239);
637    test_blend_highp!(hue_highp,              BlendMode::Hue,                 128, 103,  74, 239);
638    test_blend_highp!(saturation_highp,       BlendMode::Saturation,           59, 126, 140, 239);
639    test_blend_highp!(color_highp,            BlendMode::Color,               139, 100,  60, 239);
640    test_blend_highp!(luminosity_highp,       BlendMode::Luminosity,          100, 149, 157, 239);
641}