use std::{any::Any, cmp::Ordering, hash::Hash, ops::Range};
use guillotiere::euclid::SideOffsets2D;
use rayon::{prelude::ParallelIterator, slice::ParallelSlice};
use ribir_algo::Resource;
use ribir_painter::{
ColorFormat, PaintPath, PaintingStyle, Path, PixelImage, StrokeOptions, Vertex, VertexBuffers,
};
use ribir_types::{DeviceRect, DeviceSize, Size, Transform, transform_to_device_rect};
use super::{
Texture,
atlas::{Atlas, AtlasConfig, AtlasDist},
};
use crate::GPUBackendImpl;
const TOLERANCE: f32 = 0.1_f32;
const PAR_CHUNKS_SIZE: usize = 64;
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Copy)]
pub(super) enum TextureID {
Alpha(usize),
Rgba(usize),
Bundle(usize),
}
#[derive(PartialEq, Clone)]
enum PathKey {
Fill(Resource<dyn Any>),
Stroke { resource: Resource<dyn Any>, options: StrokeOptions },
}
pub(super) struct TexturesMgr<T: Texture> {
alpha_atlas: Atlas<PathKey, T>,
rgba_atlas: Atlas<Resource<dyn Any>, T>,
bundle_atlas: Atlas<Resource<dyn Any>, T>,
tess_task: Vec<TessTask>,
tess_task_buffer: VertexBuffers<()>,
}
struct TessTask {
slice: TextureSlice,
path: PaintPath,
style: PaintingStyle,
transform: Transform,
clip_rect: Option<DeviceRect>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct TextureSlice {
pub(super) tex_id: TextureID,
pub(super) rect: DeviceRect,
}
macro_rules! id_to_texture_mut {
($mgr:ident, $id:expr) => {
match $id {
TextureID::Alpha(id) => $mgr.alpha_atlas.get_texture_mut(id),
TextureID::Rgba(id) => $mgr.rgba_atlas.get_texture_mut(id),
TextureID::Bundle(id) => $mgr.bundle_atlas.get_texture_mut(id),
}
};
}
macro_rules! id_to_texture {
($mgr:ident, $id:expr) => {
match $id {
TextureID::Alpha(id) => $mgr.alpha_atlas.get_texture(id),
TextureID::Rgba(id) => $mgr.rgba_atlas.get_texture(id),
TextureID::Bundle(id) => $mgr.bundle_atlas.get_texture(id),
}
};
}
impl<T: Texture> TexturesMgr<T>
where
T::Host: GPUBackendImpl<Texture = T>,
{
pub(super) fn new(gpu_impl: &mut T::Host) -> Self {
let limits = gpu_impl.limits();
let max_size = limits.texture_size;
Self {
alpha_atlas: Atlas::new(
AtlasConfig::new("Alpha atlas", max_size),
ColorFormat::Alpha8,
gpu_impl,
),
rgba_atlas: Atlas::new(
AtlasConfig::new("Rgba atlas", max_size),
ColorFormat::Rgba8,
gpu_impl,
),
bundle_atlas: Atlas::new(
AtlasConfig::new("Bundle atlas", max_size),
ColorFormat::Rgba8,
gpu_impl,
),
tess_task: <_>::default(),
tess_task_buffer: <_>::default(),
}
}
pub(super) fn store_alpha_path(
&mut self, path: &PaintPath, style: &PaintingStyle, matrix: &Transform, viewport: &DeviceRect,
gpu: &mut T::Host,
) -> (TextureSlice, Transform) {
let path_bounds = path.bounds(style.line_width());
match path {
PaintPath::Share(p) => {
let resource = p.clone().into_any();
let cache_scale: f32 = self.cache_scale(&path_bounds.size, matrix);
let key = match style {
PaintingStyle::Fill => PathKey::Fill(resource),
PaintingStyle::Stroke(options) => PathKey::Stroke { resource, options: options.clone() },
};
let (slice, scale) = if let Some(h) = self.alpha_atlas.get(&key, cache_scale).copied() {
let mask_slice = self.alpha_atlas_dist_to_tex_slice(&h.dist);
(mask_slice, h.scale)
} else {
let scale_bounds = path_bounds.scale(cache_scale, cache_scale);
let (dist, slice) =
self.alpha_allocate(scale_bounds.round_out().size.to_i32().cast_unit(), gpu);
let _ = self.alpha_atlas.cache(key, cache_scale, dist);
let offset = slice.rect.origin.to_f32().cast_unit() - scale_bounds.origin;
let transform = Transform::scale(cache_scale, cache_scale).then_translate(offset);
self.tess_task.push(TessTask {
slice,
path: path.clone(),
transform,
clip_rect: None,
style: style.clone(),
});
(slice, cache_scale)
};
let path_origin = path_bounds.origin * scale;
let slice_origin = slice.rect.origin.to_vector().to_f32();
let matrix = Transform::translation(-slice_origin.x, -slice_origin.y)
.then_translate(path_origin.to_vector().cast_unit())
.then_scale(1. / scale, 1. / scale)
.then(matrix);
(slice.expand_for_paste(), matrix)
}
PaintPath::Own(_) => {
let paint_bounds = transform_to_device_rect(&path_bounds, matrix);
let alloc_size = size_expand_blank(paint_bounds.size);
let (visual_rect, clip_rect) = if self.alpha_atlas.is_good_size_to_alloc(alloc_size) {
(paint_bounds, None)
} else {
let visual_rect = paint_bounds.intersection(viewport).unwrap();
(visual_rect, Some(visual_rect))
};
let (_, slice) = self.alpha_allocate(visual_rect.size, gpu);
let offset = (slice.rect.origin - visual_rect.origin)
.to_f32()
.cast_unit();
let ts = matrix.then_translate(offset);
let task =
TessTask { slice, transform: ts, path: path.clone(), style: style.clone(), clip_rect };
self.tess_task.push(task);
let offset = (visual_rect.origin - slice.rect.origin).to_f32();
(slice.expand_for_paste(), Transform::translation(offset.x, offset.y))
}
PaintPath::PixelImage(img) => {
let key = PathKey::Fill(img.clone().into_any());
let (slice, _scale) = if let Some(h) = self.alpha_atlas.get(&key, 1.).copied() {
let mask_slice = self.alpha_atlas_dist_to_tex_slice(&h.dist);
(mask_slice, h.scale)
} else {
let (dist, slice) = self.alpha_allocate(img.size(), gpu);
let _ = self.alpha_atlas.cache(key, 1., dist);
let texture = self.alpha_atlas.get_texture_mut(dist.tex_id());
texture.write_data(&slice.rect, img.pixel_bytes(), gpu);
(slice, 1.0)
};
let slice_origin = slice.rect.origin.to_vector().to_f32();
let ts = Transform::translation(-slice_origin.x, -slice_origin.y).then(matrix);
(slice.expand_for_paste(), ts)
}
}
}
pub(super) fn store_image(
&mut self, img: &Resource<PixelImage>, gpu: &mut T::Host,
) -> TextureSlice {
match img.color_format() {
ColorFormat::Rgba8 => {
let blank_side = SideOffsets2D::new_all_same(RGBA_BLEED_EDGE);
let atlas = &mut self.rgba_atlas;
let h = atlas.get_or_cache(
img.clone().into_any(),
1.,
size_expand_edge(img.size(), RGBA_BLEED_EDGE),
gpu,
|rect, texture, gpu| texture.write_data(rect, &rgba_pixels_with_bleed(img), gpu),
);
TextureSlice {
tex_id: TextureID::Rgba(h.tex_id()),
rect: h.tex_rect(atlas).inner_rect(blank_side),
}
}
ColorFormat::Alpha8 => {
let key = PathKey::Fill(img.clone().into_any());
let atlas = &mut self.alpha_atlas;
let h = atlas.get_or_cache(key, 1., img.size(), gpu, |rect, texture, gpu| {
texture.write_data(rect, img.pixel_bytes(), gpu)
});
TextureSlice { tex_id: TextureID::Alpha(h.tex_id()), rect: h.tex_rect(atlas) }
}
}
}
pub(super) fn store_commands(
&mut self, size: DeviceSize, target: Resource<dyn Any>, scale: f32, gpu: &mut T::Host,
init: impl FnOnce(&DeviceRect, &mut T, &mut T::Host),
) -> (f32, TextureSlice) {
if let Some(h) = self.bundle_atlas.get(&target, scale).copied() {
return (
h.scale,
TextureSlice {
tex_id: TextureID::Bundle(h.tex_id()),
rect: h.tex_rect(&self.bundle_atlas),
},
);
}
let bundle_dist = self.bundle_atlas.allocate(size, gpu);
let bundle_rect = bundle_dist.tex_rect(&self.bundle_atlas);
let mut temp_tex = gpu.new_texture(size, ColorFormat::Rgba8);
let temp_rect = DeviceRect::from_size(size);
init(&temp_rect, &mut temp_tex, gpu);
let bundle_tex = self
.bundle_atlas
.get_texture_mut(bundle_dist.tex_id());
gpu.copy_texture_from_texture(bundle_tex, bundle_rect.min(), &temp_tex, &temp_rect);
let h = self
.bundle_atlas
.cache(target, scale, bundle_dist);
(
h.scale,
TextureSlice { tex_id: TextureID::Bundle(h.tex_id()), rect: h.tex_rect(&self.bundle_atlas) },
)
}
pub(super) fn texture(&self, tex_id: TextureID) -> &T { id_to_texture!(self, tex_id) }
fn alpha_allocate(
&mut self, mut size: DeviceSize, gpu: &mut T::Host,
) -> (AtlasDist, TextureSlice) {
size = size_expand_blank(size);
let dist = self.alpha_atlas.allocate(size, gpu);
(dist, self.alpha_atlas_dist_to_tex_slice(&dist))
}
fn alpha_atlas_dist_to_tex_slice(&self, dist: &AtlasDist) -> TextureSlice {
let blank_side = SideOffsets2D::new_all_same(ALPHA_BLANK_EDGE);
let rect = dist.tex_rect(&self.alpha_atlas);
TextureSlice { tex_id: TextureID::Alpha(dist.tex_id()), rect: rect.inner_rect(blank_side) }
}
pub(super) fn cache_scale(&self, size: &Size, matrix: &Transform) -> f32 {
let Transform { m11, m12, m21, m22, .. } = matrix;
let scale = (m11.abs() + m12.abs()).max(m21.abs() + m22.abs());
let dis = size.width.max(size.height);
if dis * scale < 32. {
32. / dis
} else {
let max_size = size_shrink_blank(self.alpha_atlas.max_size()).to_f32();
let max_scale = (max_size.width / size.width).min(max_size.width / size.height);
scale.min(max_scale)
}
}
fn tessellate(
path: &Path, style: &PaintingStyle, ts: &Transform, slice_size: &DeviceSize,
buffer: &mut VertexBuffers<()>,
) -> Range<u32> {
let start = buffer.indices.len() as u32;
let path_size = path.bounds(style.line_width()).size;
let slice_size = slice_size.to_f32();
let scale = (slice_size.width / path_size.width).max(slice_size.height / path_size.height);
let tolerance = TOLERANCE / scale;
let vertex_ctor = |pos| {
let pos = ts.transform_point(pos);
Vertex::new([pos.x, pos.y], ())
};
match style {
PaintingStyle::Fill => path.fill_tessellate(tolerance, buffer, vertex_ctor),
PaintingStyle::Stroke(options) => {
path.stroke_tessellate(tolerance, options.clone(), buffer, vertex_ctor)
}
}
start..buffer.indices.len() as u32
}
pub(crate) fn draw_alpha_textures<G: GPUBackendImpl<Texture = T>>(&mut self, gpu_impl: &mut G)
where
T: Texture<Host = G>,
{
if self.tess_task.is_empty() {
return;
}
self.tess_task.sort_by(|a, b| {
let a_clip = a.clip_rect.is_some();
let b_clip = b.clip_rect.is_some();
if a_clip == b_clip {
a.slice.tex_id.cmp(&b.slice.tex_id)
} else if a_clip {
Ordering::Less
} else {
Ordering::Greater
}
});
let mut draw_indices = Vec::with_capacity(self.tess_task.len());
if self.tess_task.len() < PAR_CHUNKS_SIZE {
for f in self.tess_task.iter() {
let TessTask { slice, path, clip_rect, transform, style } = f;
let rg =
Self::tessellate(path, style, transform, &slice.rect.size, &mut self.tess_task_buffer);
draw_indices.push((slice.tex_id, rg, clip_rect));
}
} else {
let mut tasks = Vec::with_capacity(self.tess_task.len());
for f in self.tess_task.iter() {
let TessTask { slice, path, clip_rect, transform, style } = f;
tasks.push((slice, style, transform, path, clip_rect));
}
let par_tess_res = tasks
.par_chunks(PAR_CHUNKS_SIZE)
.map(|tasks| {
let mut buffer = VertexBuffers::default();
let mut indices = Vec::with_capacity(tasks.len());
for (slice, style, ts, path, clip_rect) in tasks.iter() {
let rg = Self::tessellate(path, style, ts, &slice.rect.size, &mut buffer);
indices.push((slice.tex_id, rg, *clip_rect));
}
(indices, buffer)
})
.collect::<Vec<_>>();
par_tess_res
.into_iter()
.for_each(|(indices, buffer)| {
let offset = self.tess_task_buffer.indices.len() as u32;
draw_indices.extend(indices.into_iter().map(|(id, mut rg, clip)| {
rg.start += offset;
rg.end += offset;
(id, rg, clip)
}));
extend_buffer(&mut self.tess_task_buffer, buffer);
})
};
gpu_impl.load_alpha_vertices(&self.tess_task_buffer);
let mut idx = 0;
loop {
if idx >= draw_indices.len() {
break;
}
let (tex_id, rg, Some(clip_rect)) = &draw_indices[idx] else {
break;
};
let texture = id_to_texture_mut!(self, *tex_id);
let size_offset = gpu_impl.load_alpha_size(texture.size());
gpu_impl.draw_alpha_triangles_with_scissor(rg, texture, *clip_rect, size_offset);
idx += 1;
}
loop {
if idx >= draw_indices.len() {
break;
}
let (tex_id, rg, None) = &draw_indices[idx] else {
unreachable!();
};
let next = draw_indices[idx..]
.iter()
.position(|(next, _, _)| tex_id != next);
let indices = if let Some(mut next) = next {
next += idx;
idx = next;
let (_, end, _) = &draw_indices[next];
rg.start..end.start
} else {
idx = draw_indices.len();
rg.start..self.tess_task_buffer.indices.len() as u32
};
let texture = id_to_texture_mut!(self, *tex_id);
let size_offset = gpu_impl.load_alpha_size(texture.size());
gpu_impl.draw_alpha_triangles(&indices, texture, size_offset);
}
self.tess_task.clear();
self.tess_task_buffer.vertices.clear();
self.tess_task_buffer.indices.clear();
}
pub(crate) fn end_frame(&mut self, gpu_impl: &mut T::Host) -> bool {
let mut clear_areas = vec![];
self.alpha_atlas.end_frame_with(|rect| {
clear_areas.push(rect);
});
self.rgba_atlas.end_frame();
self.bundle_atlas.end_frame();
if clear_areas.is_empty() {
false
} else {
let texture = self.alpha_atlas.get_texture_mut(0);
texture.clear_areas(&clear_areas, gpu_impl);
true
}
}
}
fn extend_buffer<V>(dist: &mut VertexBuffers<V>, from: VertexBuffers<V>) {
if dist.vertices.is_empty() {
dist.vertices.extend(from.vertices);
dist.indices.extend(from.indices);
} else {
let offset = dist.vertices.len() as u32;
dist
.indices
.extend(from.indices.into_iter().map(|i| offset + i));
dist.vertices.extend(from.vertices);
}
}
const ALPHA_BLANK_EDGE: i32 = 2;
const RGBA_BLEED_EDGE: i32 = 1;
fn size_expand_edge(mut size: DeviceSize, edge: i32) -> DeviceSize {
size.width += edge * 2;
size.height += edge * 2;
size
}
fn size_expand_blank(size: DeviceSize) -> DeviceSize { size_expand_edge(size, ALPHA_BLANK_EDGE) }
fn size_shrink_blank(mut size: DeviceSize) -> DeviceSize {
size.width -= ALPHA_BLANK_EDGE * 2;
size.height -= ALPHA_BLANK_EDGE * 2;
size
}
fn rgba_pixels_with_bleed(img: &PixelImage) -> Vec<u8> {
let width = img.width() as usize;
let height = img.height() as usize;
if width == 0 || height == 0 {
return Vec::new();
}
let src = img.pixel_bytes();
let edge = RGBA_BLEED_EDGE as usize;
let padded_width = width + edge * 2;
let padded_height = height + edge * 2;
let stride = width * 4;
let padded_stride = padded_width * 4;
let mut padded = vec![0; padded_stride * padded_height];
for y in 0..height {
let src_row = &src[y * stride..(y + 1) * stride];
let dst_offset = (y + edge) * padded_stride;
let dst_row = &mut padded[dst_offset..dst_offset + padded_stride];
dst_row[edge * 4..edge * 4 + stride].copy_from_slice(src_row);
let first = &src_row[..4];
for x in 0..edge {
let offset = x * 4;
dst_row[offset..offset + 4].copy_from_slice(first);
}
let last = &src_row[stride - 4..stride];
for x in 0..edge {
let offset = (edge + width + x) * 4;
dst_row[offset..offset + 4].copy_from_slice(last);
}
}
let first_row = padded[edge * padded_stride..(edge + 1) * padded_stride].to_vec();
for y in 0..edge {
let start = y * padded_stride;
padded[start..start + padded_stride].copy_from_slice(&first_row);
}
let last_row_start = (edge + height - 1) * padded_stride;
let last_row = padded[last_row_start..last_row_start + padded_stride].to_vec();
for y in 0..edge {
let start = (edge + height + y) * padded_stride;
padded[start..start + padded_stride].copy_from_slice(&last_row);
}
padded
}
impl TextureSlice {
pub fn expand_for_paste(mut self) -> TextureSlice {
const EXPANDED_EDGE: i32 = 1;
let blank_side = SideOffsets2D::new_all_same(EXPANDED_EDGE);
self.rect = self.rect.outer_rect(blank_side);
self
}
}
impl Hash for PathKey {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
PathKey::Fill(path) => path.hash(state),
PathKey::Stroke { resource: path, options } => {
path.hash(state);
let StrokeOptions { width, miter_limit, line_cap, line_join } = options;
width.to_bits().hash(state);
miter_limit.to_bits().hash(state);
line_cap.hash(state);
line_join.hash(state);
}
}
}
}
impl Eq for PathKey {}
#[cfg(feature = "wgpu")]
#[cfg(test)]
pub mod tests {
use std::borrow::Cow;
use futures::executor::block_on;
use ribir_painter::Color;
use ribir_types::*;
use super::*;
use crate::{WgpuImpl, WgpuTexture};
pub fn color_image(color: Color, width: u32, height: u32) -> Resource<PixelImage> {
let data = std::iter::repeat_n(color.into_components(), width as usize * height as usize)
.flatten()
.collect::<Vec<_>>();
let img = PixelImage::new(Cow::Owned(data), width, height, ColorFormat::Rgba8);
Resource::new(img)
}
#[test]
fn smoke_store_image() {
let mut wgpu = block_on(WgpuImpl::headless());
let mut mgr = TexturesMgr::new(&mut wgpu);
let red_img = color_image(Color::RED, 32, 32);
let red_rect = mgr.store_image(&red_img, &mut wgpu);
assert_eq!(red_rect.rect.min().to_array(), [RGBA_BLEED_EDGE, RGBA_BLEED_EDGE]);
assert_eq!(red_rect, mgr.store_image(&red_img, &mut wgpu));
color_img_check(&mgr, &red_rect, &mut wgpu, Color::RED);
let yellow_img = color_image(Color::YELLOW, 64, 64);
let yellow_rect = mgr.store_image(&yellow_img, &mut wgpu);
color_img_check(&mgr, &red_rect, &mut wgpu, Color::RED);
color_img_check(&mgr, &yellow_rect, &mut wgpu, Color::YELLOW);
let extra_blue_img = color_image(Color::BLUE, 1024, 1024);
let blue_rect = mgr.store_image(&extra_blue_img, &mut wgpu);
color_img_check(&mgr, &blue_rect, &mut wgpu, Color::BLUE);
color_img_check(&mgr, &red_rect, &mut wgpu, Color::RED);
color_img_check(&mgr, &yellow_rect, &mut wgpu, Color::YELLOW);
}
fn color_img_check(
mgr: &TexturesMgr<WgpuTexture>, rect: &TextureSlice, wgpu: &mut WgpuImpl, color: Color,
) {
wgpu.begin_frame();
let texture = mgr.texture(rect.tex_id);
let img = texture.copy_as_image(&rect.rect, wgpu);
wgpu.end_frame();
let img = block_on(img).unwrap();
assert!(
img
.pixel_bytes()
.chunks(4)
.all(|c| c == color.into_components())
);
}
#[test]
fn transform_path_share_cache() {
let mut wgpu = block_on(WgpuImpl::headless());
let mut mgr = TexturesMgr::<WgpuTexture>::new(&mut wgpu);
let p = Resource::new(Path::rect(&rect(0., 0., 300., 300.)));
let p = PaintPath::Share(p.clone());
let viewport = rect(0, 0, 1024, 1024);
let (slice1, ts1) = mgr.store_alpha_path(
&p,
&PaintingStyle::Fill,
&Transform::scale(2., 2.),
&viewport,
&mut wgpu,
);
let (slice2, ts2) = mgr.store_alpha_path(
&p,
&PaintingStyle::Fill,
&Transform::translation(100., 100.),
&viewport,
&mut wgpu,
);
assert_eq!(slice1, slice2);
assert_eq!(ts1, Transform::new(1., 0., 0., 1., -2., -2.));
assert_eq!(ts2, Transform::new(0.5, 0., 0., 0.5, 99., 99.));
}
#[test]
fn fix_resource_address_conflict() {
let mut wgpu = block_on(WgpuImpl::headless());
let mut mgr = TexturesMgr::<WgpuTexture>::new(&mut wgpu);
{
let red_img = color_image(Color::RED, 32, 32);
mgr.store_image(&red_img, &mut wgpu);
}
for _ in 0..10 {
mgr.end_frame(&mut wgpu);
let red_img = color_image(Color::RED, 32, 32).into_any();
assert!(mgr.rgba_atlas.get(&red_img, 1.).is_none());
}
}
#[test]
fn rgba_images_store_edge_bleed_pixels() {
let mut wgpu = block_on(WgpuImpl::headless());
let mut mgr = TexturesMgr::<WgpuTexture>::new(&mut wgpu);
let pixels = vec![1, 2, 3, 4, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120];
let img = Resource::new(PixelImage::new(Cow::Owned(pixels), 2, 2, ColorFormat::Rgba8));
let slice = mgr.store_image(&img, &mut wgpu);
let bleed = guillotiere::euclid::SideOffsets2D::<_, ribir_types::PhysicUnit>::new_all_same(
RGBA_BLEED_EDGE,
);
let atlas_rect = slice.rect.outer_rect(bleed);
wgpu.begin_frame();
let texture = mgr.texture(slice.tex_id);
let atlas_img = texture.copy_as_image(&atlas_rect, &mut wgpu);
wgpu.end_frame();
let atlas_img = block_on(atlas_img).unwrap();
assert_eq!(atlas_img.pixel_bytes(), rgba_pixels_with_bleed(&img));
}
}