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: &[PremultipliedColorU8] = 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: usize = (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(r:0.0, g:0.0, b:0.0, a: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}
642