slint/internal/core/software_renderer/draw_functions.rs
Olivier Goffart 424f046416 swrenderer: refactor image drawing algorithm
For tiling, we will need to know the actual source size in addition to
the scaling factor that can be different. So store the scaling factors
in the scene command, as well as an offset where to start.

This is more accurate in case of clipping and rotation.
For rotation that doesn't matter (appart from the fact that the testing
can now be more strict)
But for clipping this prevent glitches with partial rendering where it
would seem like the image are moving a bit by a pixel when it is redrawn
with a different clip
2024-02-15 18:25:47 +01:00

692 lines
25 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
#![allow(clippy::identity_op)] // We use x + 0 a lot here for symmetry
//! This is the module for the functions that are drawing the pixels
//! on the line buffer
use super::{PhysicalLength, PhysicalRect};
use crate::graphics::{PixelFormat, Rgb8Pixel};
use crate::lengths::{PointLengths, RectLengths, SizeLengths};
use crate::software_renderer::fixed::Fixed;
use crate::Color;
use derive_more::{Add, Mul, Sub};
use integer_sqrt::IntegerSquareRoot;
/// Draw one line of the texture in the line buffer
pub(super) fn draw_texture_line(
span: &PhysicalRect,
line: PhysicalLength,
texture: &super::SceneTexture,
line_buffer: &mut [impl TargetPixel],
) {
let super::SceneTexture {
data,
format,
pixel_stride,
extra: super::SceneTextureExtra { colorize, alpha, rotation, dx, dy, off_x, off_y },
} = *texture;
let span_size = span.size.cast::<usize>();
let y = (line - span.origin.y_length()).cast::<usize>();
let line_buffer = &mut line_buffer[span.origin.x as usize..][..span.size.width as usize];
let y = if rotation.mirror_width() { span_size.height - y.get() - 1 } else { y.get() } as i32;
if !rotation.is_transpose() {
let mut delta = Fixed::<i32, 8>::from_fixed(dx);
let mut pos = Fixed::from_integer(
(Fixed::<i32, 8>::from_fixed(off_y) + Fixed::<i32, 8>::from_fixed(dy) * y).truncate(),
) * pixel_stride as i32
+ Fixed::<i32, 8>::from_fixed(off_x);
if rotation.mirror_height() {
pos += delta * (span_size.width as i32 - 1);
delta = -delta;
};
fetch_blend_pixel(
line_buffer,
format,
data,
alpha,
colorize,
#[inline(always)]
|bpp| {
let p = pos.truncate() as usize * bpp;
pos += delta;
p
},
);
} else {
let bpp = format.bpp();
let col = Fixed::<i32, 8>::from_fixed(off_x) + Fixed::<i32, 8>::from_fixed(dx) * y;
let col = col.truncate() as usize * bpp;
let stride = pixel_stride as usize * bpp;
let row_delta = Fixed::<i32, 8>::from_fixed(dy);
let (mut row, row_delta) = if rotation.mirror_height() {
(Fixed::from_fixed(off_y) + row_delta * (span_size.width as i32 - 1), -row_delta)
} else {
(Fixed::from_fixed(off_y), row_delta)
};
fetch_blend_pixel(
line_buffer,
format,
data,
alpha,
colorize,
#[inline(always)]
|_| {
let pos = row.truncate() as usize * stride + col;
row += row_delta;
pos
},
);
};
fn fetch_blend_pixel(
line_buffer: &mut [impl TargetPixel],
format: PixelFormat,
data: &[u8],
alpha: u8,
color: Color,
mut pos: impl FnMut(usize) -> usize,
) {
match format {
PixelFormat::Rgb => {
for pix in line_buffer {
let pos = pos(3);
let p = &data[pos..pos + 3];
if alpha == 0xff {
*pix = TargetPixel::from_rgb(p[0], p[1], p[2]);
} else {
pix.blend(PremultipliedRgbaColor::premultiply(Color::from_argb_u8(
alpha, p[0], p[1], p[2],
)))
}
}
}
PixelFormat::Rgba => {
if color.alpha() == 0 {
for pix in line_buffer {
let pos = pos(4);
let alpha = ((data[pos + 3] as u16 * alpha as u16) / 255) as u8;
let c = PremultipliedRgbaColor::premultiply(Color::from_argb_u8(
alpha,
data[pos + 0],
data[pos + 1],
data[pos + 2],
));
pix.blend(c);
}
} else {
for pix in line_buffer {
let pos = pos(4);
let alpha = ((data[pos + 3] as u16 * alpha as u16) / 255) as u8;
let c = PremultipliedRgbaColor::premultiply(Color::from_argb_u8(
alpha,
color.red(),
color.green(),
color.blue(),
));
pix.blend(c);
}
}
}
PixelFormat::RgbaPremultiplied => {
if color.alpha() > 0 {
for pix in line_buffer {
let pos = pos(4);
let c = PremultipliedRgbaColor::premultiply(Color::from_argb_u8(
((data[pos + 3] as u16 * alpha as u16) / 255) as u8,
color.red(),
color.green(),
color.blue(),
));
pix.blend(c);
}
} else if alpha == 0xff {
for pix in line_buffer {
let pos = pos(4);
let c = PremultipliedRgbaColor {
alpha: data[pos + 3],
red: data[pos + 0],
green: data[pos + 1],
blue: data[pos + 2],
};
pix.blend(c);
}
} else {
for pix in line_buffer {
let pos = pos(4);
let c = PremultipliedRgbaColor {
alpha: (data[pos + 3] as u16 * alpha as u16 / 255) as u8,
red: (data[pos + 0] as u16 * alpha as u16 / 255) as u8,
green: (data[pos + 1] as u16 * alpha as u16 / 255) as u8,
blue: (data[pos + 2] as u16 * alpha as u16 / 255) as u8,
};
pix.blend(c);
}
}
}
PixelFormat::AlphaMap => {
for pix in line_buffer {
let pos = pos(1);
let c = PremultipliedRgbaColor::premultiply(Color::from_argb_u8(
((data[pos] as u16 * alpha as u16) / 255) as u8,
color.red(),
color.green(),
color.blue(),
));
pix.blend(c);
}
}
};
}
}
/// draw one line of the rounded rectangle in the line buffer
#[allow(clippy::unnecessary_cast)] // Coord
pub(super) fn draw_rounded_rectangle_line(
span: &PhysicalRect,
line: PhysicalLength,
rr: &super::RoundedRectangle,
line_buffer: &mut [impl TargetPixel],
) {
/// This is an integer shifted by 4 bits.
/// Note: this is not a "fixed point" because multiplication and sqrt operation operate to
/// the shifted integer
#[derive(Clone, Copy, PartialEq, Ord, PartialOrd, Eq, Add, Sub, Mul)]
struct Shifted(u32);
impl Shifted {
const ONE: Self = Shifted(1 << 4);
pub fn new(value: impl TryInto<u32>) -> Self {
Self(value.try_into().map_err(|_| ()).unwrap() << 4)
}
pub fn floor(self) -> u32 {
self.0 >> 4
}
pub fn ceil(self) -> u32 {
(self.0 + Self::ONE.0 - 1) >> 4
}
pub fn saturating_sub(self, other: Self) -> Self {
Self(self.0.saturating_sub(other.0))
}
pub fn sqrt(self) -> Self {
Self(self.0.integer_sqrt())
}
}
impl core::ops::Mul for Shifted {
type Output = Shifted;
fn mul(self, rhs: Self) -> Self::Output {
Self(self.0 * rhs.0)
}
}
let pos_x = span.origin.x as usize;
let y1 = (line - span.origin.y_length()) + rr.top_clip;
let y2 = (span.origin.y_length() + span.size.height_length() - line) + rr.bottom_clip
- PhysicalLength::new(1);
let y = y1.min(y2);
debug_assert!(y.get() >= 0,);
let border = Shifted::new(rr.width.get());
const ONE: Shifted = Shifted::ONE;
const ZERO: Shifted = Shifted(0);
let anti_alias = |x1: Shifted, x2: Shifted, process_pixel: &mut dyn FnMut(usize, u32)| {
// x1 and x2 are the coordinate on the top and bottom of the intersection of the pixel
// line and the curve.
// `process_pixel` be called for the coordinate in the array and a coverage between 0..255
// This algorithm just go linearly which is not perfect, but good enough.
for x in x1.floor()..x2.ceil() {
// the coverage is basically how much of the pixel should be used
let cov = ((ONE + Shifted::new(x) - x1).0 << 8) / (ONE + x2 - x1).0;
process_pixel(x as usize, cov);
}
};
let rev = |x: Shifted| {
(Shifted::new(span.size.width) + Shifted::new(rr.right_clip.get())).saturating_sub(x)
};
let calculate_xxxx = |r: i16, y: i16| {
let r = Shifted::new(r);
// `y` is how far away from the center of the circle the current line is.
let y = r - Shifted::new(y);
// Circle equation: x = √(r² - y²)
// Coordinate from the left edge: x' = r - x
let x2 = r - (r * r).saturating_sub(y * y).sqrt();
let x1 = r - (r * r).saturating_sub((y - ONE) * (y - ONE)).sqrt();
let r2 = r.saturating_sub(border);
let x4 = r - (r2 * r2).saturating_sub(y * y).sqrt();
let x3 = r - (r2 * r2).saturating_sub((y - ONE) * (y - ONE)).sqrt();
(x1, x2, x3, x4)
};
let (x1, x2, x3, x4, x5, x6, x7, x8) = if let Some(r) = rr.radius.as_uniform() {
let (x1, x2, x3, x4) =
if y.get() < r { calculate_xxxx(r, y.get()) } else { (ZERO, ZERO, border, border) };
(x1, x2, x3, x4, rev(x4), rev(x3), rev(x2), rev(x1))
} else {
let (x1, x2, x3, x4) = if y1 < PhysicalLength::new(rr.radius.top_left) {
calculate_xxxx(rr.radius.top_left, y.get())
} else if y2 < PhysicalLength::new(rr.radius.bottom_left) {
calculate_xxxx(rr.radius.bottom_left, y.get())
} else {
(ZERO, ZERO, border, border)
};
let (x5, x6, x7, x8) = if y1 < PhysicalLength::new(rr.radius.top_right) {
let x = calculate_xxxx(rr.radius.top_right, y.get());
(x.3, x.2, x.1, x.0)
} else if y2 < PhysicalLength::new(rr.radius.bottom_right) {
let x = calculate_xxxx(rr.radius.bottom_right, y.get());
(x.3, x.2, x.1, x.0)
} else {
(border, border, ZERO, ZERO)
};
(x1, x2, x3, x4, rev(x5), rev(x6), rev(x7), rev(x8))
};
anti_alias(
x1.saturating_sub(Shifted::new(rr.left_clip.get())),
x2.saturating_sub(Shifted::new(rr.left_clip.get())),
&mut |x, cov| {
if x >= span.size.width as usize {
return;
}
let c = if border == ZERO { rr.inner_color } else { rr.border_color };
let col = PremultipliedRgbaColor {
alpha: (((c.alpha as u32) * cov as u32) / 255) as u8,
red: (((c.red as u32) * cov as u32) / 255) as u8,
green: (((c.green as u32) * cov as u32) / 255) as u8,
blue: (((c.blue as u32) * cov as u32) / 255) as u8,
};
line_buffer[pos_x + x].blend(col);
},
);
if y < rr.width {
// up or down border (x2 .. x7)
let l = x2.ceil().saturating_sub(rr.left_clip.get() as u32).min(span.size.width as u32)
as usize;
let r = x7.floor().min(span.size.width as u32) as usize;
if l < r {
TargetPixel::blend_slice(&mut line_buffer[pos_x + l..pos_x + r], rr.border_color)
}
} else {
if border > ZERO {
// 3. draw the border (between x2 and x3)
if ONE + x2 <= x3 {
TargetPixel::blend_slice(
&mut line_buffer[pos_x
+ x2.ceil()
.saturating_sub(rr.left_clip.get() as u32)
.min(span.size.width as u32) as usize
..pos_x
+ x3.floor()
.saturating_sub(rr.left_clip.get() as u32)
.min(span.size.width as u32)
as usize],
rr.border_color,
)
}
// 4. anti-aliasing for the contents (x3 .. x4)
anti_alias(
x3.saturating_sub(Shifted::new(rr.left_clip.get())),
x4.saturating_sub(Shifted::new(rr.left_clip.get())),
&mut |x, cov| {
if x >= span.size.width as usize {
return;
}
let col = interpolate_color(cov, rr.border_color, rr.inner_color);
line_buffer[pos_x + x].blend(col);
},
);
}
if rr.inner_color.alpha > 0 {
// 5. inside (x4 .. x5)
let begin =
x4.ceil().saturating_sub(rr.left_clip.get() as u32).min(span.size.width as u32);
let end = x5.floor().min(span.size.width as u32);
if begin < end {
TargetPixel::blend_slice(
&mut line_buffer[pos_x + begin as usize..pos_x + end as usize],
rr.inner_color,
)
}
}
if border > ZERO {
// 6. border anti-aliasing: x5..x6
anti_alias(x5, x6, &mut |x, cov| {
if x >= span.size.width as usize {
return;
}
let col = interpolate_color(cov, rr.inner_color, rr.border_color);
line_buffer[pos_x + x].blend(col)
});
// 7. border x6 .. x7
if ONE + x6 <= x7 {
TargetPixel::blend_slice(
&mut line_buffer[pos_x + x6.ceil().min(span.size.width as u32) as usize
..pos_x + x7.floor().min(span.size.width as u32) as usize],
rr.border_color,
)
}
}
}
anti_alias(x7, x8, &mut |x, cov| {
if x >= span.size.width as usize {
return;
}
let c = if border == ZERO { rr.inner_color } else { rr.border_color };
let col = PremultipliedRgbaColor {
alpha: (((c.alpha as u32) * (255 - cov) as u32) / 255) as u8,
red: (((c.red as u32) * (255 - cov) as u32) / 255) as u8,
green: (((c.green as u32) * (255 - cov) as u32) / 255) as u8,
blue: (((c.blue as u32) * (255 - cov) as u32) / 255) as u8,
};
line_buffer[pos_x + x].blend(col);
});
}
// a is between 0 and 255. When 0, we get color1, when 255 we get color2
fn interpolate_color(
a: u32,
color1: PremultipliedRgbaColor,
color2: PremultipliedRgbaColor,
) -> PremultipliedRgbaColor {
let b = 255 - a;
let al1 = color1.alpha as u32;
let al2 = color2.alpha as u32;
let a_ = a * al2;
let b_ = b * al1;
let m = a_ + b_;
if m == 0 {
return PremultipliedRgbaColor::default();
}
PremultipliedRgbaColor {
alpha: (m / 255) as u8,
red: ((b * color1.red as u32 + a * color2.red as u32) / 255) as u8,
green: ((b * color1.green as u32 + a * color2.green as u32) / 255) as u8,
blue: ((b * color1.blue as u32 + a * color2.blue as u32) / 255) as u8,
}
}
pub(super) fn draw_gradient_line(
rect: &PhysicalRect,
line: PhysicalLength,
g: &super::GradientCommand,
line_buffer: &mut [impl TargetPixel],
) {
let mut buffer = &mut line_buffer
[rect.origin.x as usize..(rect.origin.x_length() + rect.width_length()).get() as usize];
let fill_col1 = g.flags & 0b010 != 0;
let fill_col2 = g.flags & 0b100 != 0;
let invert_slope = g.flags & 0b1 != 0;
let y = (line.get() - rect.min_y() + g.top_clip.get()) as i32;
let size_y = (rect.height() + g.top_clip.get() + g.bottom_clip.get()) as i32;
let start = g.start as i32;
let (mut color1, mut color2) = (g.color1, g.color2);
if g.start == 0 {
let p = if invert_slope {
(255 - start) * y / size_y
} else {
start + (255 - start) * y / size_y
};
if (fill_col1 || p >= 0) && (fill_col2 || p < 255) {
let col = interpolate_color(p.clamp(0, 255) as u32, color1, color2);
TargetPixel::blend_slice(buffer, col);
}
return;
}
let size_x = (rect.width() + g.left_clip.get() + g.right_clip.get()) as i32;
let mut x = if invert_slope {
(y * size_x * (255 - start)) / (size_y * start)
} else {
(size_y - y) * size_x * (255 - start) / (size_y * start)
} + g.left_clip.get() as i32;
let len = ((255 * size_x) / start) as usize;
if x < 0 {
let l = (-x as usize).min(buffer.len());
if invert_slope {
if fill_col1 {
TargetPixel::blend_slice(&mut buffer[..l], g.color1);
}
} else if fill_col2 {
TargetPixel::blend_slice(&mut buffer[..l], g.color2);
}
buffer = &mut buffer[l..];
x = 0;
}
if buffer.len() + x as usize > len {
let l = len.saturating_sub(x as usize);
if invert_slope {
if fill_col2 {
TargetPixel::blend_slice(&mut buffer[l..], g.color2);
}
} else if fill_col1 {
TargetPixel::blend_slice(&mut buffer[l..], g.color1);
}
buffer = &mut buffer[..l];
}
if buffer.is_empty() {
return;
}
if !invert_slope {
core::mem::swap(&mut color1, &mut color2);
}
let dr = (((color2.red as i32 - color1.red as i32) * start) << 15) / (255 * size_x);
let dg = (((color2.green as i32 - color1.green as i32) * start) << 15) / (255 * size_x);
let db = (((color2.blue as i32 - color1.blue as i32) * start) << 15) / (255 * size_x);
let da = (((color2.alpha as i32 - color1.alpha as i32) * start) << 15) / (255 * size_x);
let mut r = ((color1.red as u32) << 15).wrapping_add((x * dr) as _);
let mut g = ((color1.green as u32) << 15).wrapping_add((x * dg) as _);
let mut b = ((color1.blue as u32) << 15).wrapping_add((x * db) as _);
let mut a = ((color1.alpha as u32) << 15).wrapping_add((x * da) as _);
if color1.alpha == 255 && color2.alpha == 255 {
buffer.fill_with(|| {
let pix = TargetPixel::from_rgb((r >> 15) as u8, (g >> 15) as u8, (b >> 15) as u8);
r = r.wrapping_add(dr as _);
g = g.wrapping_add(dg as _);
b = b.wrapping_add(db as _);
pix
})
} else {
for pix in buffer {
pix.blend(PremultipliedRgbaColor {
red: (r >> 15) as u8,
green: (g >> 15) as u8,
blue: (b >> 15) as u8,
alpha: (a >> 15) as u8,
});
r = r.wrapping_add(dr as _);
g = g.wrapping_add(dg as _);
b = b.wrapping_add(db as _);
a = a.wrapping_add(da as _);
}
}
}
/// A color whose component have been pre-multiplied by alpha
///
/// The renderer operates faster on pre-multiplied color since it
/// caches the multiplication of its component
///
/// PremultipliedRgbaColor can be constructed from a [`Color`] with
/// the [`From`] trait. This conversion will pre-multiply the color
/// components
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct PremultipliedRgbaColor {
pub red: u8,
pub green: u8,
pub blue: u8,
pub alpha: u8,
}
/// Convert a non-premultiplied color to a premultiplied one
impl From<Color> for PremultipliedRgbaColor {
fn from(col: Color) -> Self {
Self::premultiply(col)
}
}
impl PremultipliedRgbaColor {
/// Convert a non premultiplied color to a premultiplied one
fn premultiply(col: Color) -> Self {
let a = col.alpha() as u16;
Self {
alpha: col.alpha(),
red: (col.red() as u16 * a / 255) as u8,
green: (col.green() as u16 * a / 255) as u8,
blue: (col.blue() as u16 * a / 255) as u8,
}
}
}
/// Trait for the pixels in the buffer
pub trait TargetPixel: Sized + Copy {
/// Blend a single pixel with a color
fn blend(&mut self, color: PremultipliedRgbaColor);
/// Blend a color to all the pixel in the slice.
fn blend_slice(slice: &mut [Self], color: PremultipliedRgbaColor) {
if color.alpha == u8::MAX {
slice.fill(Self::from_rgb(color.red, color.green, color.blue))
} else {
for x in slice {
Self::blend(x, color);
}
}
}
/// Create a pixel from the red, gree, blue component in the range 0..=255
fn from_rgb(red: u8, green: u8, blue: u8) -> Self;
/// Pixel which will be filled as the background in case the slint view has transparency
fn background() -> Self {
Self::from_rgb(0, 0, 0)
}
}
impl TargetPixel for crate::graphics::image::Rgb8Pixel {
fn blend(&mut self, color: PremultipliedRgbaColor) {
let a = (u8::MAX - color.alpha) as u16;
self.r = (self.r as u16 * a / 255) as u8 + color.red;
self.g = (self.g as u16 * a / 255) as u8 + color.green;
self.b = (self.b as u16 * a / 255) as u8 + color.blue;
}
fn from_rgb(r: u8, g: u8, b: u8) -> Self {
Self::new(r, g, b)
}
}
impl TargetPixel for PremultipliedRgbaColor {
fn blend(&mut self, color: PremultipliedRgbaColor) {
let a = (u8::MAX - color.alpha) as u16;
self.red = (self.red as u16 * a / 255) as u8 + color.red;
self.green = (self.green as u16 * a / 255) as u8 + color.green;
self.blue = (self.blue as u16 * a / 255) as u8 + color.blue;
self.alpha = (self.alpha as u16 + color.alpha as u16
- (self.alpha as u16 * color.alpha as u16) / 255) as u8;
}
fn from_rgb(r: u8, g: u8, b: u8) -> Self {
Self { red: r, green: g, blue: b, alpha: 255 }
}
fn background() -> Self {
Self { red: 0, green: 0, blue: 0, alpha: 0 }
}
}
/// A 16bit pixel that has 5 red bits, 6 green bits and 5 blue bits
#[repr(transparent)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Rgb565Pixel(pub u16);
impl Rgb565Pixel {
const R_MASK: u16 = 0b1111_1000_0000_0000;
const G_MASK: u16 = 0b0000_0111_1110_0000;
const B_MASK: u16 = 0b0000_0000_0001_1111;
/// Return the red component as a u8.
///
/// The bits are shifted so that the result is between 0 and 255
fn red(self) -> u8 {
((self.0 & Self::R_MASK) >> 8) as u8
}
/// Return the green component as a u8.
///
/// The bits are shifted so that the result is between 0 and 255
fn green(self) -> u8 {
((self.0 & Self::G_MASK) >> 3) as u8
}
/// Return the blue component as a u8.
///
/// The bits are shifted so that the result is between 0 and 255
fn blue(self) -> u8 {
((self.0 & Self::B_MASK) << 3) as u8
}
}
impl TargetPixel for Rgb565Pixel {
fn blend(&mut self, color: PremultipliedRgbaColor) {
let a = (u8::MAX - color.alpha) as u32;
// convert to 5 bits
let a = (a + 4) >> 3;
// 00000ggg_ggg00000_rrrrr000_000bbbbb
let expanded = (self.0 & (Self::R_MASK | Self::B_MASK)) as u32
| (((self.0 & Self::G_MASK) as u32) << 16);
// gggggggg_000rrrrr_rrr000bb_bbbbbb00
let c =
((color.red as u32) << 13) | ((color.green as u32) << 24) | ((color.blue as u32) << 2);
// gggggg00_000rrrrr_000000bb_bbb00000
let c = c & 0b11111100_00011111_00000011_11100000;
let res = expanded * a + c;
self.0 = ((res >> 21) as u16 & Self::G_MASK)
| ((res >> 5) as u16 & (Self::R_MASK | Self::B_MASK));
}
fn from_rgb(r: u8, g: u8, b: u8) -> Self {
Self(((r as u16 & 0b11111000) << 8) | ((g as u16 & 0b11111100) << 3) | (b as u16 >> 3))
}
}
impl From<Rgb8Pixel> for Rgb565Pixel {
fn from(p: Rgb8Pixel) -> Self {
Self::from_rgb(p.r, p.g, p.b)
}
}
impl From<Rgb565Pixel> for Rgb8Pixel {
fn from(p: Rgb565Pixel) -> Self {
Rgb8Pixel { r: p.red(), g: p.green(), b: p.blue() }
}
}
#[test]
fn rgb565() {
let pix565 = Rgb565Pixel::from_rgb(0xff, 0x25, 0);
let pix888: Rgb8Pixel = pix565.into();
assert_eq!(pix565, pix888.into());
let pix565 = Rgb565Pixel::from_rgb(0x56, 0x42, 0xe3);
let pix888: Rgb8Pixel = pix565.into();
assert_eq!(pix565, pix888.into());
}