use std::borrow::Cow;
use std::cell::{Cell, RefCell, RefMut};
use std::collections::BTreeMap;
use std::mem;
use std::ops::{ControlFlow, Range};
use std::slice::ChunksExactMut;
use rayon::prelude::*;
use crate::layout::{Flusher, Layout, Slice, TileFill};
use crate::painter::layer_workbench::{OptimizerTileWriteOp, TileWriteOp};
use crate::rasterizer::{search_last_by_key, PixelSegment};
use crate::simd::{f32x4, f32x8, i16x16, i32x8, i8x16, u32x4, u32x8, u8x32, Simd};
use crate::{PIXEL_DOUBLE_WIDTH, PIXEL_WIDTH, TILE_HEIGHT, TILE_WIDTH};
mod layer_workbench;
#[macro_use]
mod style;
use layer_workbench::{Context, LayerPainter, LayerWorkbench};
pub use style::{
BlendMode, Fill, FillRule, Gradient, GradientBuilder, GradientType, Image, ImageId, Style,
Texture,
};
pub use self::style::{Channel, Color, BGR0, BGR1, BGRA, RGB0, RGB1, RGBA};
const PIXEL_AREA: usize = PIXEL_WIDTH * PIXEL_WIDTH;
const PIXEL_DOUBLE_AREA: usize = 2 * PIXEL_AREA;
const C23: u32 = 0x4B00_0000;
macro_rules! cols {
( & $array:expr, $x0:expr, $x1:expr ) => {{
fn size_of_el<T: Simd>(_: impl AsRef<[T]>) -> usize {
T::LANES
}
let from = $x0 * crate::TILE_HEIGHT / size_of_el(&$array);
let to = $x1 * crate::TILE_HEIGHT / size_of_el(&$array);
&$array[from..to]
}};
( & mut $array:expr, $x0:expr, $x1:expr ) => {{
fn size_of_el<T: Simd>(_: impl AsRef<[T]>) -> usize {
T::LANES
}
let from = $x0 * crate::TILE_HEIGHT / size_of_el(&$array);
let to = $x1 * crate::TILE_HEIGHT / size_of_el(&$array);
&mut $array[from..to]
}};
}
#[inline]
fn doubled_area_to_coverage(doubled_area: i32x8, fill_rule: FillRule) -> f32x8 {
match fill_rule {
FillRule::NonZero => {
let doubled_area: f32x8 = doubled_area.into();
(doubled_area * f32x8::splat((PIXEL_DOUBLE_AREA as f32).recip()))
.abs()
.clamp(f32x8::splat(0.0), f32x8::splat(1.0))
}
FillRule::EvenOdd => {
let doubled_area: f32x8 = (i32x8::splat(PIXEL_DOUBLE_AREA as i32)
- ((doubled_area & i32x8::splat(2 * PIXEL_DOUBLE_AREA as i32 - 1))
- i32x8::splat(PIXEL_DOUBLE_AREA as i32))
.abs())
.into();
doubled_area * f32x8::splat((PIXEL_DOUBLE_AREA as f32).recip())
}
}
}
#[allow(clippy::many_single_char_names)]
#[inline]
fn linear_to_srgb_approx_simdx8(l: f32x8) -> f32x8 {
let a = f32x8::splat(0.201_017_72f32);
let b = f32x8::splat(-0.512_801_47f32);
let c = f32x8::splat(1.344_401f32);
let d = f32x8::splat(-0.030_656_587f32);
let s = l.sqrt();
let s2 = l;
let s3 = s2 * s;
let m = l * f32x8::splat(12.92);
let n = a.mul_add(s3, b.mul_add(s2, c.mul_add(s, d)));
m.select(n, l.le(f32x8::splat(0.003_130_8)))
}
#[allow(clippy::many_single_char_names)]
#[inline]
fn linear_to_srgb_approx_simdx4(l: f32x4) -> f32x4 {
let a = f32x4::splat(0.201_017_72f32);
let b = f32x4::splat(-0.512_801_47f32);
let c = f32x4::splat(1.344_401f32);
let d = f32x4::splat(-0.030_656_587f32);
let s = l.sqrt();
let s2 = l;
let s3 = s2 * s;
let m = l * f32x4::splat(12.92);
let n = a.mul_add(s3, b.mul_add(s2, c.mul_add(s, d)));
m.select(n, l.le(f32x4::splat(0.003_130_8)))
}
#[inline]
fn to_u32x8(val: f32x8) -> u32x8 {
let max = f32x8::splat(f32::from(u8::MAX));
let c23 = u32x8::splat(C23);
let scaled = (val * max).clamp(f32x8::splat(0.0), max);
let val = scaled + f32x8::from_bits(c23);
val.to_bits()
}
#[inline]
fn to_u32x4(val: f32x4) -> u32x4 {
let max = f32x4::splat(f32::from(u8::MAX));
let c23 = u32x4::splat(C23);
let scaled = (val * max).clamp(f32x4::splat(0.0), max);
let val = scaled + f32x4::from_bits(c23);
val.to_bits()
}
#[inline]
fn to_srgb_bytes(color: [f32; 4]) -> [u8; 4] {
let linear = f32x4::new([color[0], color[1], color[2], 0.0]);
let srgb = to_u32x4(linear_to_srgb_approx_simdx4(linear).set::<3>(color[3]));
srgb.into()
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Rect {
pub(crate) hor: Range<usize>,
pub(crate) vert: Range<usize>,
}
impl Rect {
pub fn new(horizontal: Range<usize>, vertical: Range<usize>) -> Self {
Self {
hor: horizontal.start / TILE_WIDTH..(horizontal.end + TILE_WIDTH - 1) / TILE_WIDTH,
vert: vertical.start / TILE_HEIGHT..(vertical.end + TILE_HEIGHT - 1) / TILE_HEIGHT,
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Func {
Draw(Style),
Clip(usize),
}
impl Default for Func {
fn default() -> Self {
Self::Draw(Style::default())
}
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct Props {
pub fill_rule: FillRule,
pub func: Func,
}
pub trait LayerProps: Send + Sync {
fn get(&self, layer_id: u32) -> Cow<'_, Props>;
fn is_unchanged(&self, layer_id: u32) -> bool;
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct Cover {
covers: [i8x16; TILE_HEIGHT / i8x16::LANES],
}
impl Cover {
pub fn as_slice_mut(&mut self) -> &mut [i8; TILE_HEIGHT] {
unsafe { mem::transmute(&mut self.covers) }
}
pub fn add_cover_to(&self, covers: &mut [i8x16]) {
for (i, &cover) in self.covers.iter().enumerate() {
covers[i] += cover;
}
}
pub fn is_empty(&self, fill_rule: FillRule) -> bool {
match fill_rule {
FillRule::NonZero => self.covers.iter().all(|&cover| cover.eq(i8x16::splat(0)).all()),
FillRule::EvenOdd => self
.covers
.iter()
.all(|&cover| (cover.abs() & i8x16::splat(31)).eq(i8x16::splat(0)).all()),
}
}
pub fn is_full(&self, fill_rule: FillRule) -> bool {
match fill_rule {
FillRule::NonZero => self
.covers
.iter()
.all(|&cover| cover.abs().eq(i8x16::splat(PIXEL_WIDTH as i8)).all()),
FillRule::EvenOdd => self.covers.iter().any(|&cover| {
(cover.abs() & i8x16::splat(0b1_1111)).eq(i8x16::splat(0b1_0000)).all()
}),
}
}
}
impl PartialEq for Cover {
fn eq(&self, other: &Self) -> bool {
self.covers.iter().zip(other.covers.iter()).all(|(t, o)| t.eq(*o).all())
}
}
#[derive(Clone, Copy, Debug)]
pub struct CoverCarry {
cover: Cover,
layer_id: u32,
}
#[derive(Debug)]
pub(crate) struct Painter {
doubled_areas: [i16x16; TILE_WIDTH * TILE_HEIGHT / i16x16::LANES],
covers: [i8x16; (TILE_WIDTH + 1) * TILE_HEIGHT / i8x16::LANES],
clip: Option<([f32x8; TILE_WIDTH * TILE_HEIGHT / f32x8::LANES], u32)>,
red: [f32x8; TILE_WIDTH * TILE_HEIGHT / f32x8::LANES],
green: [f32x8; TILE_WIDTH * TILE_HEIGHT / f32x8::LANES],
blue: [f32x8; TILE_WIDTH * TILE_HEIGHT / f32x8::LANES],
alpha: [f32x8; TILE_WIDTH * TILE_HEIGHT / f32x8::LANES],
srgb: [u8x32; TILE_WIDTH * TILE_HEIGHT * 4 / u8x32::LANES],
}
impl LayerPainter for Painter {
fn clear_cells(&mut self) {
self.doubled_areas.iter_mut().for_each(|doubled_area| *doubled_area = i16x16::splat(0));
self.covers.iter_mut().for_each(|cover| *cover = i8x16::splat(0));
}
fn acc_segment(&mut self, segment: PixelSegment<TILE_WIDTH, TILE_HEIGHT>) {
let x = segment.local_x() as usize;
let y = segment.local_y() as usize;
let doubled_areas: &mut [i16; TILE_WIDTH * TILE_HEIGHT] =
unsafe { mem::transmute(&mut self.doubled_areas) };
let covers: &mut [i8; (TILE_WIDTH + 1) * TILE_HEIGHT] =
unsafe { mem::transmute(&mut self.covers) };
doubled_areas[x * TILE_HEIGHT + y] += segment.double_area();
covers[(x + 1) * TILE_HEIGHT + y] += segment.cover();
}
fn acc_cover(&mut self, cover: Cover) {
cover.add_cover_to(&mut self.covers);
}
fn clear(&mut self, color: Color) {
self.red.iter_mut().for_each(|r| *r = f32x8::splat(color.r));
self.green.iter_mut().for_each(|g| *g = f32x8::splat(color.g));
self.blue.iter_mut().for_each(|b| *b = f32x8::splat(color.b));
self.alpha.iter_mut().for_each(|alpha| *alpha = f32x8::splat(color.a));
}
fn paint_layer(
&mut self,
tile_x: usize,
tile_y: usize,
layer_id: u32,
props: &Props,
apply_clip: bool,
) -> Cover {
let mut doubled_areas = [i32x8::splat(0); TILE_HEIGHT / i32x8::LANES];
let mut covers = [i8x16::splat(0); TILE_HEIGHT / i8x16::LANES];
let mut coverages = [f32x8::splat(0.0); TILE_HEIGHT / f32x8::LANES];
if let Some((_, last_layer)) = self.clip {
if last_layer < layer_id {
self.clip = None;
}
}
for x in 0..=TILE_WIDTH {
if x != 0 {
self.compute_doubled_areas(x - 1, &covers, &mut doubled_areas);
for y in 0..coverages.len() {
coverages[y] = doubled_area_to_coverage(doubled_areas[y], props.fill_rule);
match &props.func {
Func::Draw(style) => {
if coverages[y].eq(f32x8::splat(0.0)).all() {
continue;
}
if apply_clip && self.clip.is_none() {
continue;
}
let fill = Self::fill_at(
x - 1 + tile_x * TILE_WIDTH,
y * f32x8::LANES + tile_y * TILE_HEIGHT,
style,
);
self.blend_at(x - 1, y, coverages, apply_clip, fill, style.blend_mode);
}
Func::Clip(layers) => {
self.clip_at(x - 1, y, coverages, layer_id + *layers as u32)
}
}
}
}
let column = cols!(&self.covers, x, x + 1);
for y in 0..column.len() {
covers[y] += column[y];
}
}
Cover { covers }
}
}
impl Painter {
pub fn new() -> Self {
Self {
doubled_areas: [i16x16::splat(0); TILE_WIDTH * TILE_HEIGHT / i16x16::LANES],
covers: [i8x16::splat(0); (TILE_WIDTH + 1) * TILE_HEIGHT / i8x16::LANES],
clip: None,
red: [f32x8::splat(0.0); TILE_WIDTH * TILE_HEIGHT / f32x8::LANES],
green: [f32x8::splat(0.0); TILE_WIDTH * TILE_HEIGHT / f32x8::LANES],
blue: [f32x8::splat(0.0); TILE_WIDTH * TILE_HEIGHT / f32x8::LANES],
alpha: [f32x8::splat(1.0); TILE_WIDTH * TILE_HEIGHT / f32x8::LANES],
srgb: [u8x32::splat(0); TILE_WIDTH * TILE_HEIGHT * 4 / u8x32::LANES],
}
}
#[inline]
fn fill_at(x: usize, y: usize, style: &Style) -> [f32x8; 4] {
match &style.fill {
Fill::Solid(color) => {
let Color { r, g, b, a } = *color;
[f32x8::splat(r), f32x8::splat(g), f32x8::splat(b), f32x8::splat(a)]
}
Fill::Gradient(gradient) => gradient.color_at(x as f32, y as f32),
Fill::Texture(texture) => texture.color_at(x as f32, y as f32),
}
}
fn compute_doubled_areas(
&self,
x: usize,
covers: &[i8x16; TILE_HEIGHT / i8x16::LANES],
doubled_areas: &mut [i32x8; TILE_HEIGHT / i32x8::LANES],
) {
let column = cols!(&self.doubled_areas, x, x + 1);
for y in 0..covers.len() {
let covers: [i32x8; 2] = covers[y].into();
let column: [i32x8; 2] = column[y].into();
for yy in 0..2 {
doubled_areas[2 * y + yy] =
i32x8::splat(PIXEL_DOUBLE_WIDTH as i32) * covers[yy] + column[yy];
}
}
}
fn blend_at(
&mut self,
x: usize,
y: usize,
coverages: [f32x8; TILE_HEIGHT / f32x8::LANES],
is_clipped: bool,
fill: [f32x8; 4],
blend_mode: BlendMode,
) {
let dst_r = &mut cols!(&mut self.red, x, x + 1)[y];
let dst_g = &mut cols!(&mut self.green, x, x + 1)[y];
let dst_b = &mut cols!(&mut self.blue, x, x + 1)[y];
let dst_a = &mut cols!(&mut self.alpha, x, x + 1)[y];
let src_r = fill[0];
let src_g = fill[1];
let src_b = fill[2];
let mut src_a = fill[3] * coverages[y];
if is_clipped {
if let Some((mask, _)) = self.clip {
src_a *= cols!(&mask, x, x + 1)[y];
}
}
let [blended_r, blended_g, blended_b] =
blend_function!(blend_mode, *dst_r, *dst_g, *dst_b, src_r, src_g, src_b);
let inv_dst_a = f32x8::splat(1.0) - *dst_a;
let inv_dst_a_src_a = inv_dst_a * src_a;
let inv_src_a = f32x8::splat(1.0) - src_a;
let dst_a_src_a = *dst_a * src_a;
let current_r = src_r.mul_add(inv_dst_a_src_a, blended_r * dst_a_src_a);
let current_g = src_g.mul_add(inv_dst_a_src_a, blended_g * dst_a_src_a);
let current_b = src_b.mul_add(inv_dst_a_src_a, blended_b * dst_a_src_a);
*dst_r = dst_r.mul_add(inv_src_a, current_r);
*dst_g = dst_g.mul_add(inv_src_a, current_g);
*dst_b = dst_b.mul_add(inv_src_a, current_b);
*dst_a = dst_a.mul_add(inv_src_a, src_a);
}
fn clip_at(
&mut self,
x: usize,
y: usize,
coverages: [f32x8; TILE_HEIGHT / f32x8::LANES],
last_layer_id: u32,
) {
let clip = self.clip.get_or_insert_with(|| {
([f32x8::splat(0.0); TILE_WIDTH * TILE_HEIGHT / f32x8::LANES], last_layer_id)
});
cols!(&mut clip.0, x, x + 1)[y] = coverages[y];
}
fn compute_srgb(&mut self, channels: [Channel; 4]) {
for ((((&red, &green), &blue), &alpha), srgb) in self
.red
.iter()
.zip(self.green.iter())
.zip(self.blue.iter())
.zip(self.alpha.iter())
.zip(self.srgb.iter_mut())
{
let red = linear_to_srgb_approx_simdx8(red);
let green = linear_to_srgb_approx_simdx8(green);
let blue = linear_to_srgb_approx_simdx8(blue);
let unpacked = channels.map(|c| to_u32x8(c.select(red, green, blue, alpha)));
*srgb = u8x32::from_u32_interleaved(unpacked);
}
}
#[allow(clippy::too_many_arguments)]
pub fn paint_tile_row<S: LayerProps, L: Layout>(
&mut self,
workbench: &mut LayerWorkbench,
tile_y: usize,
mut segments: &[PixelSegment<TILE_WIDTH, TILE_HEIGHT>],
props: &S,
channels: [Channel; 4],
clear_color: Color,
previous_clear_color: Option<Color>,
cached_tiles: Option<&[CachedTile]>,
row: ChunksExactMut<'_, Slice<'_, u8>>,
crop: &Option<Rect>,
flusher: Option<&dyn Flusher>,
) {
let mut covers_left_of_row: BTreeMap<u32, Cover> = BTreeMap::new();
let tile_x_start = crop.as_ref().map(|rect| rect.hor.start as i16).unwrap_or_default();
if let Ok(last_clipped_index) =
search_last_by_key(segments, false, |segment| segment.tile_x() >= tile_x_start)
{
for segment in &segments[..=last_clipped_index] {
let cover = covers_left_of_row.entry(segment.layer_id()).or_default();
cover.as_slice_mut()[segment.local_y() as usize] += segment.cover();
}
segments = &segments[last_clipped_index + 1..];
}
workbench.init(
covers_left_of_row.into_iter().map(|(layer_id, cover)| CoverCarry { cover, layer_id }),
);
for (tile_x, slices) in row.enumerate() {
if let Some(rect) = &crop {
if !rect.hor.contains(&tile_x) {
continue;
}
}
let current_segments =
search_last_by_key(segments, tile_x as i16, |segment| segment.tile_x())
.map(|last_index| {
let current_segments = &segments[..=last_index];
segments = &segments[last_index + 1..];
current_segments
})
.unwrap_or(&[]);
let context = Context {
tile_x,
tile_y,
segments: current_segments,
props,
cached_clear_color: previous_clear_color,
cached_tile: cached_tiles.map(|cached_tiles| &cached_tiles[tile_x]),
channels,
clear_color,
};
self.clip = None;
match workbench.drive_tile_painting(self, &context) {
TileWriteOp::None => (),
TileWriteOp::Solid(color) => L::write(slices, flusher, TileFill::Solid(color)),
TileWriteOp::ColorBuffer => {
self.compute_srgb(channels);
let colors: &[[u8; 4]] = unsafe {
std::slice::from_raw_parts(
self.srgb.as_ptr().cast(),
self.srgb.len() * mem::size_of::<u8x32>() / mem::size_of::<[u8; 4]>(),
)
};
L::write(slices, flusher, TileFill::Full(colors));
}
}
}
}
}
thread_local!(static PAINTER_WORKBENCH: RefCell<(Painter, LayerWorkbench)> = RefCell::new((
Painter::new(),
LayerWorkbench::new(),
)));
#[allow(clippy::too_many_arguments)]
fn print_row<S: LayerProps, L: Layout>(
segments: &[PixelSegment<TILE_WIDTH, TILE_HEIGHT>],
channels: [Channel; 4],
clear_color: Color,
crop: &Option<Rect>,
styles: &S,
j: usize,
row: ChunksExactMut<'_, Slice<'_, u8>>,
previous_clear_color: Option<Color>,
cached_tiles: Option<&[CachedTile]>,
flusher: Option<&dyn Flusher>,
) {
if let Some(rect) = crop {
if !rect.vert.contains(&j) {
return;
}
}
let segments = search_last_by_key(segments, j as i16, |segment| segment.tile_y())
.map(|end| {
let result =
search_last_by_key(&segments[..end], j as i16 - 1, |segment| segment.tile_y());
let start = match result {
Ok(i) => i + 1,
Err(i) => i,
};
&segments[start..=end]
})
.unwrap_or(&[]);
PAINTER_WORKBENCH.with(|pair| {
let (mut painter, mut workbench) =
RefMut::map_split(pair.borrow_mut(), |pair| (&mut pair.0, &mut pair.1));
painter.paint_tile_row::<S, L>(
&mut workbench,
j,
segments,
styles,
channels,
clear_color,
previous_clear_color,
cached_tiles,
row,
crop,
flusher,
);
});
}
#[derive(Clone, Debug, Default)]
pub struct CachedTile {
tags: Cell<u8>,
layer_count: Cell<[u8; 3]>,
solid_color: Cell<[u8; 4]>,
}
impl CachedTile {
pub fn layer_count(&self) -> Option<u32> {
let layer_count = self.layer_count.get();
let layer_count = u32::from_le_bytes([layer_count[0], layer_count[1], layer_count[2], 0]);
match self.tags.get() {
0b10 | 0b11 => Some(layer_count),
_ => None,
}
}
pub fn solid_color(&self) -> Option<[u8; 4]> {
match self.tags.get() {
0b01 | 0b11 => Some(self.solid_color.get()),
_ => None,
}
}
pub fn update_layer_count(&self, layer_count: Option<u32>) -> Option<u32> {
let previous_layer_count = self.layer_count();
match layer_count {
None => {
self.tags.set(self.tags.get() & 0b01);
}
Some(layer_count) => {
self.tags.set(self.tags.get() | 0b10);
self.layer_count.set(layer_count.to_le_bytes()[..3].try_into().unwrap());
}
};
previous_layer_count
}
pub fn update_solid_color(&self, solid_color: Option<[u8; 4]>) -> Option<[u8; 4]> {
let previous_solid_color = self.solid_color();
match solid_color {
None => {
self.tags.set(self.tags.get() & 0b10);
}
Some(color) => {
self.tags.set(self.tags.get() | 0b01);
self.solid_color.set(color);
}
};
previous_solid_color
}
pub fn convert_optimizer_op<P: LayerProps>(
tile_op: ControlFlow<OptimizerTileWriteOp>,
context: &Context<'_, P>,
) -> ControlFlow<TileWriteOp> {
match tile_op {
ControlFlow::Break(OptimizerTileWriteOp::Solid(color)) => {
let color = to_srgb_bytes(context.channels.map(|c| color.channel(c)));
let color_is_unchanged = context
.cached_tile
.as_ref()
.map(|cached_tile| cached_tile.update_solid_color(Some(color)) == Some(color))
.unwrap_or_default();
if color_is_unchanged {
ControlFlow::Break(TileWriteOp::None)
} else {
ControlFlow::Break(TileWriteOp::Solid(color))
}
}
ControlFlow::Break(OptimizerTileWriteOp::None) => ControlFlow::Break(TileWriteOp::None),
_ => {
if let Some(cached_tile) = context.cached_tile {
cached_tile.update_solid_color(None);
}
ControlFlow::Continue(())
}
}
}
}
#[allow(clippy::too_many_arguments)]
#[inline]
pub fn for_each_row<L: Layout, S: LayerProps>(
layout: &mut L,
buffer: &mut [u8],
channels: [Channel; 4],
flusher: Option<&dyn Flusher>,
previous_clear_color: Option<Color>,
cached_tiles: Option<RefMut<'_, Vec<CachedTile>>>,
mut segments: &[PixelSegment<TILE_WIDTH, TILE_HEIGHT>],
clear_color: Color,
crop: &Option<Rect>,
styles: &S,
) {
if let Ok(start) = search_last_by_key(segments, false, |segment| segment.tile_y() >= 0) {
segments = &segments[start + 1..];
}
let width_in_tiles = layout.width_in_tiles();
let row_of_tiles_len = width_in_tiles * layout.slices_per_tile();
let mut slices = layout.slices(buffer);
if let Some(mut cached_tiles) = cached_tiles {
slices
.par_chunks_mut(row_of_tiles_len)
.zip_eq(cached_tiles.par_chunks_mut(width_in_tiles))
.enumerate()
.for_each(|(j, (row_of_tiles, cached_tiles))| {
print_row::<S, L>(
segments,
channels,
clear_color,
crop,
styles,
j,
row_of_tiles.chunks_exact_mut(row_of_tiles.len() / width_in_tiles),
previous_clear_color,
Some(cached_tiles),
flusher,
);
});
} else {
slices.par_chunks_mut(row_of_tiles_len).enumerate().for_each(|(j, row_of_tiles)| {
print_row::<S, L>(
segments,
channels,
clear_color,
crop,
styles,
j,
row_of_tiles.chunks_exact_mut(row_of_tiles.len() / width_in_tiles),
previous_clear_color,
None,
flusher,
);
});
}
}
#[cfg(feature = "bench")]
pub fn painter_fill_at_bench(width: usize, height: usize, style: &Style) -> f32x8 {
let mut sum = f32x8::indexed();
for y in 0..width {
for x in 0..height {
for c in Painter::fill_at(x, y, style) {
sum += c;
}
}
}
sum
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::iter;
use crate::layout::LinearLayout;
use crate::point::Point;
use crate::rasterizer::Rasterizer;
use crate::{GeomId, Layer, LinesBuilder, Order};
const RED: Color = Color { r: 1.0, g: 0.0, b: 0.0, a: 1.0 };
const RED_GREEN_50: Color = Color { r: 1.0, g: 0.5, b: 0.0, a: 1.0 };
const RED_50: Color = Color { r: 0.5, g: 0.0, b: 0.0, a: 1.0 };
const RED_50_GREEN_50: Color = Color { r: 0.5, g: 0.5, b: 0.0, a: 1.0 };
const GREEN: Color = Color { r: 0.0, g: 1.0, b: 0.0, a: 1.0 };
const GREEN_50: Color = Color { r: 0.0, g: 0.5, b: 0.0, a: 1.0 };
const BLUE: Color = Color { r: 0.0, g: 0.0, b: 1.0, a: 1.0 };
const WHITE: Color = Color { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
const BLACK: Color = Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0 };
const BLACK_ALPHA_50: Color = Color { r: 0.0, g: 0.0, b: 0.0, a: 0.5 };
const BLACK_ALPHA_0: Color = Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
const BLACK_RGBA: [u8; 4] = [0, 0, 0, 255];
const RED_RGBA: [u8; 4] = [255, 0, 0, 255];
const GREEN_RGBA: [u8; 4] = [0, 255, 0, 255];
const BLUE_RGBA: [u8; 4] = [0, 0, 255, 255];
impl LayerProps for HashMap<u32, Style> {
fn get(&self, layer_id: u32) -> Cow<'_, Props> {
let style = self.get(&layer_id).unwrap().clone();
Cow::Owned(Props { fill_rule: FillRule::NonZero, func: Func::Draw(style) })
}
fn is_unchanged(&self, _: u32) -> bool {
false
}
}
impl LayerProps for HashMap<u32, Props> {
fn get(&self, layer_id: u32) -> Cow<'_, Props> {
Cow::Owned(self.get(&layer_id).unwrap().clone())
}
fn is_unchanged(&self, _: u32) -> bool {
false
}
}
impl<F> LayerProps for F
where
F: Fn(u32) -> Style + Send + Sync,
{
fn get(&self, layer_id: u32) -> Cow<'_, Props> {
let style = self(layer_id);
Cow::Owned(Props { fill_rule: FillRule::NonZero, func: Func::Draw(style) })
}
fn is_unchanged(&self, _: u32) -> bool {
false
}
}
impl Painter {
fn colors(&self) -> [[f32; 4]; TILE_WIDTH * TILE_HEIGHT] {
let mut colors = [[0.0, 0.0, 0.0, 1.0]; TILE_WIDTH * TILE_HEIGHT];
for (i, (((c0, c1), c2), alpha)) in self
.red
.iter()
.copied()
.flat_map(f32x8::to_array)
.zip(self.green.iter().copied().flat_map(f32x8::to_array))
.zip(self.blue.iter().copied().flat_map(f32x8::to_array))
.zip(self.alpha.iter().copied().flat_map(f32x8::to_array))
.enumerate()
{
colors[i] = [c0, c1, c2, alpha];
}
colors
}
}
fn line_segments(
points: &[(Point, Point)],
same_layer: bool,
) -> Vec<PixelSegment<TILE_WIDTH, TILE_HEIGHT>> {
let mut builder = LinesBuilder::new();
let ids = iter::successors(Some(GeomId::default()), |id| Some(id.next()));
for (&(p0, p1), id) in points.iter().zip(ids) {
let id = if same_layer { GeomId::default() } else { id };
builder.push(id, [p0, p1]);
}
let lines = builder.build(|id| {
Some(Layer { order: Some(Order::new(id.get() as u32).unwrap()), ..Default::default() })
});
let mut rasterizer = Rasterizer::new();
rasterizer.rasterize(&lines);
let mut segments: Vec<_> = rasterizer.segments().to_vec();
segments.sort_unstable();
segments
}
fn paint_tile(
cover_carries: impl IntoIterator<Item = CoverCarry>,
segments: &[PixelSegment<TILE_WIDTH, TILE_HEIGHT>],
props: &impl LayerProps,
clear_color: Color,
) -> [[f32; 4]; TILE_WIDTH * TILE_HEIGHT] {
let mut painter = Painter::new();
let mut workbench = LayerWorkbench::new();
let context = Context {
tile_x: 0,
tile_y: 0,
segments,
props,
cached_clear_color: None,
cached_tile: None,
channels: RGBA,
clear_color,
};
workbench.init(cover_carries);
workbench.drive_tile_painting(&mut painter, &context);
painter.colors()
}
fn coverage(double_area: i32, fill_rules: FillRule) -> f32 {
let array = doubled_area_to_coverage(i32x8::splat(double_area), fill_rules).to_array();
for val in array {
assert_eq!(val, array[0]);
}
array[0]
}
#[test]
fn double_area_non_zero() {
let area = PIXEL_DOUBLE_AREA as i32;
assert_eq!(coverage(-area * 2, FillRule::NonZero), 1.0);
assert_eq!(coverage(-area * 3 / 2, FillRule::NonZero), 1.0);
assert_eq!(coverage(-area, FillRule::NonZero), 1.0);
assert_eq!(coverage(-area / 2, FillRule::NonZero), 0.5);
assert_eq!(coverage(0, FillRule::NonZero), 0.0);
assert_eq!(coverage(area / 2, FillRule::NonZero), 0.5);
assert_eq!(coverage(area, FillRule::NonZero), 1.0);
assert_eq!(coverage(area * 3 / 2, FillRule::NonZero), 1.0);
assert_eq!(coverage(area * 2, FillRule::NonZero), 1.0);
}
#[test]
fn double_area_even_odd() {
let area = PIXEL_DOUBLE_AREA as i32;
assert_eq!(coverage(-area * 2, FillRule::NonZero), 1.0);
assert_eq!(coverage(-area * 3 / 2, FillRule::EvenOdd), 0.5);
assert_eq!(coverage(-area, FillRule::EvenOdd), 1.0);
assert_eq!(coverage(-area / 2, FillRule::EvenOdd), 0.5);
assert_eq!(coverage(0, FillRule::EvenOdd), 0.0);
assert_eq!(coverage(area / 2, FillRule::EvenOdd), 0.5);
assert_eq!(coverage(area, FillRule::EvenOdd), 1.0);
assert_eq!(coverage(area * 3 / 2, FillRule::EvenOdd), 0.5);
assert_eq!(coverage(area * 2, FillRule::NonZero), 1.0);
}
#[test]
fn carry_cover() {
let mut cover_carry = CoverCarry { cover: Cover::default(), layer_id: 0 };
cover_carry.cover.covers[0].as_mut_array()[1] = 16;
cover_carry.layer_id = 1;
let segments =
line_segments(&[(Point::new(0.0, 0.0), Point::new(0.0, TILE_HEIGHT as f32))], false);
let mut styles = HashMap::new();
styles.insert(0, Style { fill: Fill::Solid(GREEN), ..Default::default() });
styles.insert(1, Style { fill: Fill::Solid(RED), ..Default::default() });
assert_eq!(
paint_tile([cover_carry], &segments, &styles, BLACK)[0..2],
[GREEN, RED].map(Color::to_array),
);
}
#[test]
fn overlapping_triangles() {
let segments = line_segments(
&[
(Point::new(0.0, 0.0), Point::new(4.0, 4.0)),
(Point::new(4.0, 0.0), Point::new(0.0, 4.0)),
],
false,
);
let mut styles = HashMap::new();
styles.insert(0, Style { fill: Fill::Solid(GREEN), ..Default::default() });
styles.insert(1, Style { fill: Fill::Solid(RED), ..Default::default() });
let colors = paint_tile([], &segments, &styles, BLACK);
let row_start = 0;
let row_end = 4;
let mut column = 0;
assert_eq!(
colors[column + row_start..column + row_end],
[GREEN_50, BLACK, BLACK, RED_50].map(Color::to_array),
);
column += TILE_HEIGHT;
assert_eq!(
colors[column + row_start..column + row_end],
[GREEN, GREEN_50, RED_50, RED].map(Color::to_array),
);
column += TILE_HEIGHT;
assert_eq!(
colors[column + row_start..column + row_end],
[GREEN, RED_50_GREEN_50, RED, RED].map(Color::to_array),
);
column += TILE_HEIGHT;
assert_eq!(
colors[column + row_start..column + row_end],
[RED_50_GREEN_50, RED, RED, RED].map(Color::to_array),
);
}
#[test]
fn transparent_overlay() {
let segments = line_segments(
&[
(Point::new(0.0, 0.0), Point::new(0.0, TILE_HEIGHT as f32)),
(Point::new(0.0, 0.0), Point::new(0.0, TILE_HEIGHT as f32)),
],
false,
);
let mut styles = HashMap::new();
styles.insert(0, Style { fill: Fill::Solid(RED), ..Default::default() });
styles.insert(1, Style { fill: Fill::Solid(BLACK_ALPHA_50), ..Default::default() });
assert_eq!(paint_tile([], &segments, &styles, BLACK)[0], RED_50.to_array());
}
#[test]
fn linear_blend_over() {
let segments = line_segments(
&[
(Point::new(0.0, 0.0), Point::new(0.0, TILE_HEIGHT as f32)),
(Point::new(0.0, 0.0), Point::new(0.0, TILE_HEIGHT as f32)),
],
false,
);
let mut styles = HashMap::new();
styles.insert(0, Style { fill: Fill::Solid(RED), ..Default::default() });
styles.insert(
1,
Style { fill: Fill::Solid(Color { a: 0.5, ..GREEN }), ..Default::default() },
);
assert_eq!(paint_tile([], &segments, &styles, BLACK)[0], RED_50_GREEN_50.to_array());
}
#[test]
fn linear_blend_difference() {
let segments = line_segments(
&[
(Point::new(0.0, 0.0), Point::new(0.0, TILE_HEIGHT as f32)),
(Point::new(0.0, 0.0), Point::new(0.0, TILE_HEIGHT as f32)),
],
false,
);
let mut styles = HashMap::new();
styles.insert(0, Style { fill: Fill::Solid(RED), ..Default::default() });
styles.insert(
1,
Style {
fill: Fill::Solid(Color { a: 0.5, ..GREEN }),
blend_mode: BlendMode::Difference,
..Default::default()
},
);
assert_eq!(paint_tile([], &segments, &styles, BLACK)[0], RED_GREEN_50.to_array());
}
#[test]
fn linear_blend_hue_white_opaque_brackground() {
let segments =
line_segments(&[(Point::new(0.0, 0.0), Point::new(0.0, TILE_HEIGHT as f32))], false);
let mut styles = HashMap::new();
styles.insert(
0,
Style {
fill: Fill::Solid(Color { a: 0.5, ..GREEN }),
blend_mode: BlendMode::Hue,
..Default::default()
},
);
assert_eq!(paint_tile([], &segments, &styles, WHITE)[0], WHITE.to_array());
}
#[test]
fn linear_blend_hue_white_transparent_brackground() {
let segments =
line_segments(&[(Point::new(0.0, 0.0), Point::new(0.0, TILE_HEIGHT as f32))], false);
let mut styles = HashMap::new();
styles.insert(
0,
Style {
fill: Fill::Solid(Color { a: 0.5, ..GREEN }),
blend_mode: BlendMode::Hue,
..Default::default()
},
);
assert_eq!(
paint_tile([], &segments, &styles, Color { a: 0.0, ..WHITE })[0],
[0.5, 1.0, 0.5, 0.5],
);
}
#[test]
fn cover_carry_is_empty() {
assert!(Cover { covers: [i8x16::splat(0); TILE_HEIGHT / 16] }.is_empty(FillRule::NonZero));
assert!(!Cover { covers: [i8x16::splat(1); TILE_HEIGHT / 16] }.is_empty(FillRule::NonZero));
assert!(!Cover { covers: [i8x16::splat(-1); TILE_HEIGHT / 16] }.is_empty(FillRule::NonZero));
assert!(!Cover { covers: [i8x16::splat(16); TILE_HEIGHT / 16] }.is_empty(FillRule::NonZero));
assert!(
!Cover { covers: [i8x16::splat(-16); TILE_HEIGHT / 16] }.is_empty(FillRule::NonZero)
);
assert!(Cover { covers: [i8x16::splat(0); TILE_HEIGHT / 16] }.is_empty(FillRule::EvenOdd));
assert!(!Cover { covers: [i8x16::splat(1); TILE_HEIGHT / 16] }.is_empty(FillRule::EvenOdd));
assert!(!Cover { covers: [i8x16::splat(-1); TILE_HEIGHT / 16] }.is_empty(FillRule::EvenOdd));
assert!(!Cover { covers: [i8x16::splat(16); TILE_HEIGHT / 16] }.is_empty(FillRule::EvenOdd));
assert!(
!Cover { covers: [i8x16::splat(-16); TILE_HEIGHT / 16] }.is_empty(FillRule::EvenOdd)
);
assert!(Cover { covers: [i8x16::splat(32); TILE_HEIGHT / 16] }.is_empty(FillRule::EvenOdd));
assert!(Cover { covers: [i8x16::splat(-32); TILE_HEIGHT / 16] }.is_empty(FillRule::EvenOdd));
assert!(!Cover { covers: [i8x16::splat(48); TILE_HEIGHT / 16] }.is_empty(FillRule::EvenOdd));
assert!(
!Cover { covers: [i8x16::splat(-48); TILE_HEIGHT / 16] }.is_empty(FillRule::EvenOdd)
);
}
#[test]
fn cover_carry_is_full() {
assert!(!Cover { covers: [i8x16::splat(0); TILE_HEIGHT / 16] }.is_full(FillRule::NonZero));
assert!(!Cover { covers: [i8x16::splat(1); TILE_HEIGHT / 16] }.is_full(FillRule::NonZero));
assert!(!Cover { covers: [i8x16::splat(-1); TILE_HEIGHT / 16] }.is_full(FillRule::NonZero));
assert!(Cover { covers: [i8x16::splat(16); TILE_HEIGHT / 16] }.is_full(FillRule::NonZero));
assert!(Cover { covers: [i8x16::splat(-16); TILE_HEIGHT / 16] }.is_full(FillRule::NonZero));
assert!(!Cover { covers: [i8x16::splat(0); TILE_HEIGHT / 16] }.is_full(FillRule::EvenOdd));
assert!(!Cover { covers: [i8x16::splat(1); TILE_HEIGHT / 16] }.is_full(FillRule::EvenOdd));
assert!(!Cover { covers: [i8x16::splat(-1); TILE_HEIGHT / 16] }.is_full(FillRule::EvenOdd));
assert!(Cover { covers: [i8x16::splat(16); TILE_HEIGHT / 16] }.is_full(FillRule::EvenOdd));
assert!(Cover { covers: [i8x16::splat(-16); TILE_HEIGHT / 16] }.is_full(FillRule::EvenOdd));
assert!(!Cover { covers: [i8x16::splat(32); TILE_HEIGHT / 16] }.is_full(FillRule::EvenOdd));
assert!(!Cover { covers: [i8x16::splat(-32); TILE_HEIGHT / 16] }.is_full(FillRule::EvenOdd));
assert!(Cover { covers: [i8x16::splat(48); TILE_HEIGHT / 16] }.is_full(FillRule::EvenOdd));
assert!(Cover { covers: [i8x16::splat(-48); TILE_HEIGHT / 16] }.is_full(FillRule::EvenOdd));
}
#[test]
fn clip() {
let segments = line_segments(
&[
(Point::new(0.0, 0.0), Point::new(4.0, 4.0)),
(Point::new(0.0, 0.0), Point::new(0.0, 4.0)),
(Point::new(0.0, 0.0), Point::new(0.0, 4.0)),
],
false,
);
let mut props = HashMap::new();
props.insert(0, Props { fill_rule: FillRule::NonZero, func: Func::Clip(2) });
props.insert(
1,
Props {
fill_rule: FillRule::NonZero,
func: Func::Draw(Style {
fill: Fill::Solid(GREEN),
is_clipped: true,
..Default::default()
}),
},
);
props.insert(
2,
Props {
fill_rule: FillRule::NonZero,
func: Func::Draw(Style {
fill: Fill::Solid(RED),
is_clipped: true,
..Default::default()
}),
},
);
props.insert(
3,
Props {
fill_rule: FillRule::NonZero,
func: Func::Draw(Style { fill: Fill::Solid(GREEN), ..Default::default() }),
},
);
let mut painter = Painter::new();
let mut workbench = LayerWorkbench::new();
let mut context = Context {
tile_x: 0,
tile_y: 0,
segments: &segments,
props: &props,
cached_clear_color: None,
cached_tile: None,
channels: RGBA,
clear_color: BLACK,
};
workbench.drive_tile_painting(&mut painter, &context);
let colors = painter.colors();
let mut col = [BLACK.to_array(); 4];
for i in 0..4 {
col[i] = [0.5, 0.25, 0.0, 1.0];
if i >= 1 {
col[i - 1] = RED.to_array();
}
assert_eq!(colors[i * TILE_HEIGHT..i * TILE_HEIGHT + 4], col);
}
let segments = line_segments(&[(Point::new(4.0, 0.0), Point::new(4.0, 4.0))], false);
context.tile_x = 1;
context.segments = &segments;
workbench.drive_tile_painting(&mut painter, &context);
for i in 0..4 {
assert_eq!(painter.colors()[i * TILE_HEIGHT..i * TILE_HEIGHT + 4], [RED.to_array(); 4]);
}
}
#[test]
fn f32_to_u8_scaled() {
fn convert(val: f32) -> u8 {
let vals: [u8; 4] = to_u32x4(f32x4::splat(val)).into();
vals[0]
}
assert_eq!(convert(-0.001), 0);
assert_eq!(convert(1.001), 255);
for i in 0..255 {
assert_eq!(convert(f32::from(i) * 255.0f32.recip()), i);
}
}
#[test]
fn srgb() {
let premultiplied = [
0.001 * 0.5,
0.2 * 0.5,
0.5 * 0.5,
0.5,
];
assert_eq!(to_srgb_bytes(premultiplied), [2, 89, 137, 128]);
}
#[test]
fn flusher() {
macro_rules! seg {
( $j:expr, $i:expr ) => {
PixelSegment::new($j, $i, 0, 0, 0, 0, 0)
};
}
#[derive(Debug)]
struct WhiteFlusher;
impl Flusher for WhiteFlusher {
fn flush(&self, slice: &mut [u8]) {
for color in slice {
*color = 255u8;
}
}
}
let width = TILE_WIDTH + TILE_WIDTH / 2;
let mut buffer = vec![0u8; width * TILE_HEIGHT * 4];
let mut buffer_layout = LinearLayout::new(width, width * 4, TILE_HEIGHT);
let segments = &[seg!(0, 0), seg!(0, 1), seg!(1, 0), seg!(1, 1)];
for_each_row(
&mut buffer_layout,
&mut buffer,
RGBA,
Some(&WhiteFlusher),
None,
None,
segments,
BLACK_ALPHA_0,
&None,
&|_| Style::default(),
);
assert!(buffer.iter().all(|&color| color == 255u8));
}
#[test]
fn flush_background() {
#[derive(Debug)]
struct WhiteFlusher;
impl Flusher for WhiteFlusher {
fn flush(&self, slice: &mut [u8]) {
for color in slice {
*color = 255u8;
}
}
}
let mut buffer = vec![0u8; TILE_WIDTH * TILE_HEIGHT * 4];
let mut buffer_layout = LinearLayout::new(TILE_WIDTH, TILE_WIDTH * 4, TILE_HEIGHT);
for_each_row(
&mut buffer_layout,
&mut buffer,
RGBA,
Some(&WhiteFlusher),
None,
None,
&[],
BLACK_ALPHA_0,
&None,
&|_| Style::default(),
);
assert!(buffer.iter().all(|&color| color == 255u8));
}
#[test]
fn skip_opaque_tiles() {
let mut buffer = vec![0u8; TILE_WIDTH * TILE_HEIGHT * 3 * 4];
let mut buffer_layout = LinearLayout::new(TILE_WIDTH * 3, TILE_WIDTH * 3 * 4, TILE_HEIGHT);
let mut segments = vec![];
for y in 0..TILE_HEIGHT {
segments.push(PixelSegment::new(
2,
-1,
0,
TILE_WIDTH as u8 - 1,
y as u8,
0,
PIXEL_WIDTH as i8,
));
}
segments.push(PixelSegment::new(0, -1, 0, TILE_WIDTH as u8 - 1, 0, 0, PIXEL_WIDTH as i8));
segments.push(PixelSegment::new(1, 0, 0, 0, 1, 0, PIXEL_WIDTH as i8));
for y in 0..TILE_HEIGHT {
segments.push(PixelSegment::new(
2,
1,
0,
TILE_WIDTH as u8 - 1,
y as u8,
0,
-(PIXEL_WIDTH as i8),
));
}
segments.sort();
let mut styles = HashMap::new();
styles.insert(0, Style { fill: Fill::Solid(BLUE), ..Default::default() });
styles.insert(1, Style { fill: Fill::Solid(GREEN), ..Default::default() });
styles.insert(2, Style { fill: Fill::Solid(RED), ..Default::default() });
for_each_row(
&mut buffer_layout,
&mut buffer,
RGBA,
None,
None,
None,
&segments,
BLACK,
&None,
&|layer| styles[&layer].clone(),
);
let tiles = buffer_layout.slices(&mut buffer);
assert_eq!(
tiles.iter().map(|slice| slice.to_vec()).collect::<Vec<_>>(),
iter::repeat(vec![RED_RGBA; TILE_WIDTH].concat())
.take(TILE_HEIGHT)
.chain(iter::repeat(vec![RED_RGBA; TILE_WIDTH].concat()).take(TILE_HEIGHT))
.chain(
iter::once(vec![BLUE_RGBA; TILE_WIDTH].concat())
.chain(iter::once(vec![GREEN_RGBA; TILE_WIDTH].concat()))
.chain(
iter::repeat(vec![BLACK_RGBA; TILE_WIDTH].concat())
.take(TILE_HEIGHT - 2)
)
)
.collect::<Vec<_>>()
);
}
#[test]
fn crop() {
let mut buffer = vec![0u8; TILE_WIDTH * TILE_HEIGHT * 9 * 4];
let mut buffer_layout =
LinearLayout::new(TILE_WIDTH * 3, TILE_WIDTH * 3 * 4, TILE_HEIGHT * 3);
let mut segments = vec![];
for j in 0..3 {
for y in 0..TILE_HEIGHT {
segments.push(PixelSegment::new(
0,
0,
j,
TILE_WIDTH as u8 - 1,
y as u8,
0,
PIXEL_WIDTH as i8,
));
}
}
segments.sort();
let mut styles = HashMap::new();
styles.insert(0, Style { fill: Fill::Solid(BLUE), ..Default::default() });
for_each_row(
&mut buffer_layout,
&mut buffer,
RGBA,
None,
None,
None,
&segments,
RED,
&Some(Rect::new(
TILE_WIDTH..TILE_WIDTH * 2 + TILE_WIDTH / 2,
TILE_HEIGHT..TILE_HEIGHT * 2,
)),
&|layer| styles[&layer].clone(),
);
let tiles = buffer_layout.slices(&mut buffer);
assert_eq!(
tiles.iter().map(|slice| slice.to_vec()).collect::<Vec<_>>(),
iter::repeat(vec![0u8; TILE_WIDTH * 4])
.take(TILE_HEIGHT * 3)
.chain(iter::repeat(vec![0u8; TILE_WIDTH * 4]).take(TILE_HEIGHT))
.chain(iter::repeat(vec![BLUE_RGBA; TILE_WIDTH].concat()).take(TILE_HEIGHT * 2))
.chain(iter::repeat(vec![0u8; TILE_WIDTH * 4]).take(TILE_HEIGHT * 3))
.collect::<Vec<_>>()
);
}
#[test]
fn tiles_len() {
let width = TILE_WIDTH * 4;
let width_stride = TILE_WIDTH * 5 * 4;
let height = TILE_HEIGHT * 8;
let buffer_layout = LinearLayout::new(width, width_stride, height);
assert_eq!(buffer_layout.width_in_tiles() * buffer_layout.height_in_tiles(), 32);
}
#[test]
fn cached_tiles() {
const RED: [u8; 4] = [255, 0, 0, 255];
let cached_tile = CachedTile::default();
assert_eq!(cached_tile.solid_color(), None);
assert_eq!(cached_tile.update_solid_color(Some(RED)), None);
assert_eq!(cached_tile.solid_color(), Some(RED));
assert_eq!(cached_tile.layer_count(), None);
assert_eq!(cached_tile.update_layer_count(Some(2)), None);
assert_eq!(cached_tile.layer_count(), Some(2));
}
}