Skip to main content

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