From 0119578550fccec843de6069f3d31c2331e4212b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20K=C3=A5re=20Alsaker?= Date: Tue, 10 Oct 2023 11:44:02 +0200 Subject: [PATCH] Add a `tiny_skia` rendering backend --- Cargo.toml | 7 +- src/renderer.rs | 53 ++++- tiny_skia/Cargo.toml | 17 ++ tiny_skia/src/lib.rs | 547 +++++++++++++++++++++++++++++++++++++++++++ vger/src/lib.rs | 6 +- 5 files changed, 626 insertions(+), 4 deletions(-) create mode 100644 tiny_skia/Cargo.toml create mode 100644 tiny_skia/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 5b7d2da2..ac7c4df2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,9 @@ clipboard = "0.5.0" smallvec = "1.10.0" educe = "0.4.20" taffy = "0.3.13" -rfd = { version = "0.11.4", default-features = false, features = ["xdg-portal"] } +rfd = { version = "0.11.4", default-features = false, features = [ + "xdg-portal", +] } raw-window-handle = "0.5.1" kurbo = { version = "0.9.5", features = ["serde"] } unicode-segmentation = "1.10.0" @@ -24,6 +26,7 @@ im = "15.1.0" parking_lot = { version = "0.12.1" } floem_renderer = { path = "renderer" } floem_vger = { path = "vger" } +floem_tiny_skia = { path = "tiny_skia" } floem_reactive = { path = "reactive" } winit = { git = "https://github.com/lapce/winit", rev = "25edc72fa4869d0fa83c61c26f0e38d7d7be9b0d" } # winit = { path = "../winit" } @@ -33,7 +36,7 @@ image = { version = "0.24", features = ["jpeg", "png"] } serde = ["winit/serde"] [workspace] -members = ["renderer", "vger", "reactive", "examples/*"] +members = ["renderer", "vger", "tiny_skia", "reactive", "examples/*"] [workspace.package] license = "MIT" diff --git a/src/renderer.rs b/src/renderer.rs index ac24ef3f..0e1ded49 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -49,12 +49,15 @@ //! use crate::cosmic_text::TextLayout; use floem_renderer::Img; +use floem_tiny_skia::TinySkiaRenderer; use floem_vger::VgerRenderer; use kurbo::{Affine, Rect, Shape, Size}; use peniko::BrushRef; +#[allow(clippy::large_enum_variant)] pub enum Renderer { Vger(VgerRenderer), + TinySkia(TinySkiaRenderer), } impl Renderer { @@ -63,19 +66,34 @@ impl Renderer { W: raw_window_handle::HasRawDisplayHandle + raw_window_handle::HasRawWindowHandle, { let size = Size::new(size.width.max(1.0), size.height.max(1.0)); - Self::Vger(VgerRenderer::new(window, size.width as u32, size.height as u32, scale).unwrap()) + + let vger_err = match VgerRenderer::new(window, size.width as u32, size.height as u32, scale) + { + Ok(vger) => return Self::Vger(vger), + Err(vger_err) => vger_err, + }; + + let tiny_skia_err = + match TinySkiaRenderer::new(window, size.width as u32, size.height as u32, scale) { + Ok(tiny_skia) => return Self::TinySkia(tiny_skia), + Err(vger_err) => vger_err, + }; + + panic!("Failed to create VgerRenderer: {vger_err}\nFailed to create TinySkiaRenderer: {tiny_skia_err}") } pub fn resize(&mut self, scale: f64, size: Size) { let size = Size::new(size.width.max(1.0), size.height.max(1.0)); match self { Renderer::Vger(r) => r.resize(size.width as u32, size.height as u32, scale), + Renderer::TinySkia(r) => r.resize(size.width as u32, size.height as u32, scale), } } pub fn set_scale(&mut self, scale: f64) { match self { Renderer::Vger(r) => r.set_scale(scale), + Renderer::TinySkia(r) => r.set_scale(scale), } } } @@ -86,6 +104,9 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vger(r) => { r.begin(); } + Renderer::TinySkia(r) => { + r.begin(); + } } } @@ -94,6 +115,9 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vger(v) => { v.clip(shape); } + Renderer::TinySkia(v) => { + v.clip(shape); + } } } @@ -102,6 +126,9 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vger(v) => { v.clear_clip(); } + Renderer::TinySkia(v) => { + v.clear_clip(); + } } } @@ -110,6 +137,9 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vger(v) => { v.stroke(shape, brush, width); } + Renderer::TinySkia(v) => { + v.stroke(shape, brush, width); + } } } @@ -123,6 +153,9 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vger(v) => { v.fill(path, brush, blur_radius); } + Renderer::TinySkia(v) => { + v.fill(path, brush, blur_radius); + } } } @@ -131,6 +164,9 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vger(v) => { v.draw_text(layout, pos); } + Renderer::TinySkia(v) => { + v.draw_text(layout, pos); + } } } @@ -139,6 +175,9 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vger(v) => { v.draw_img(img, width, height, rect); } + Renderer::TinySkia(v) => { + v.draw_img(img, width, height, rect); + } } } @@ -152,6 +191,9 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vger(v) => { v.draw_svg(svg, rect, brush); } + Renderer::TinySkia(v) => { + v.draw_svg(svg, rect, brush); + } } } @@ -160,6 +202,9 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vger(v) => { v.transform(transform); } + Renderer::TinySkia(v) => { + v.transform(transform); + } } } @@ -168,6 +213,9 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vger(v) => { v.set_z_index(z_index); } + Renderer::TinySkia(v) => { + v.set_z_index(z_index); + } } } @@ -176,6 +224,9 @@ impl floem_renderer::Renderer for Renderer { Renderer::Vger(r) => { r.finish(); } + Renderer::TinySkia(r) => { + r.finish(); + } } } } diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml new file mode 100644 index 00000000..1d15e53a --- /dev/null +++ b/tiny_skia/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "floem_tiny_skia" +version = "0.1.0" +edition = "2021" +license.workspace = true + +[dependencies] +resvg = "0.33.0" +raw-window-handle = "0.5.1" +futures = "0.3.26" +anyhow = "1.0.69" +peniko = { git = "https://github.com/linebender/peniko", rev = "cafdac9a211a0fb2fec5656bd663d1ac770bcc81" } +swash = "0.1.8" +floem_renderer = { path = "../renderer" } +softbuffer = "0.3.1" +bytemuck = "1.12" +image = { version = "0.24", features = ["jpeg", "png"] } diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs new file mode 100644 index 00000000..421abf49 --- /dev/null +++ b/tiny_skia/src/lib.rs @@ -0,0 +1,547 @@ +use anyhow::{anyhow, Result}; +use floem_renderer::cosmic_text::{CacheKey, SubpixelBin, SwashCache, SwashContent, TextLayout}; +use floem_renderer::tiny_skia::{ + self, FillRule, FilterQuality, GradientStop, LinearGradient, Mask, MaskType, Paint, Path, + PathBuilder, Pattern, Pixmap, RadialGradient, Shader, SpreadMode, Stroke, Transform, +}; +use floem_renderer::Img; +use floem_renderer::Renderer; +use peniko::kurbo::PathEl; +use peniko::{ + kurbo::{Affine, Point, Rect, Shape}, + BrushRef, Color, GradientKind, +}; +use softbuffer::{Context, Surface}; +use std::collections::HashMap; +use std::num::NonZeroU32; +use std::rc::Rc; + +macro_rules! try_ret { + ($e:expr) => { + if let Some(e) = $e { + e + } else { + return; + } + }; +} + +struct Glyph { + pixmap: Pixmap, + left: f32, + top: f32, +} + +#[derive(PartialEq, Clone, Copy)] +struct CacheColor(bool); + +pub struct TinySkiaRenderer { + #[allow(unused)] + context: Context, + surface: Surface, + pixmap: Pixmap, + mask: Mask, + scale: f64, + transform: Affine, + clip: Option, + + /// The cache color value set for cache entries accessed this frame. + cache_color: CacheColor, + + image_cache: HashMap, (CacheColor, Rc)>, + #[allow(clippy::type_complexity)] + glyph_cache: HashMap<(CacheKey, Color), (CacheColor, Option>)>, +} + +impl TinySkiaRenderer { + pub fn new< + W: raw_window_handle::HasRawDisplayHandle + raw_window_handle::HasRawWindowHandle, + >( + window: &W, + width: u32, + height: u32, + scale: f64, + ) -> Result { + let context = unsafe { + Context::new(&window).map_err(|err| anyhow!("unable to create context: {}", err))? + }; + let surface = unsafe { + Surface::new(&context, &window) + .map_err(|err| anyhow!("unable to create surface: {}", err))? + }; + + let pixmap = + Pixmap::new(width, height).ok_or_else(|| anyhow!("unable to create pixmap"))?; + + let mask = Mask::new(width, height).ok_or_else(|| anyhow!("unable to create mask"))?; + + Ok(Self { + context, + surface, + pixmap, + mask, + scale, + transform: Affine::IDENTITY, + clip: None, + cache_color: CacheColor(false), + image_cache: Default::default(), + glyph_cache: Default::default(), + }) + } + + pub fn resize(&mut self, width: u32, height: u32, scale: f64) { + if width != self.pixmap.width() || height != self.pixmap.width() { + self.surface + .resize( + NonZeroU32::new(width).unwrap_or(NonZeroU32::new(1).unwrap()), + NonZeroU32::new(height).unwrap_or(NonZeroU32::new(1).unwrap()), + ) + .expect("failed to resize surface"); + self.pixmap = Pixmap::new(width, height).expect("unable to create pixmap"); + self.mask = Mask::new(width, height).expect("unable to create mask"); + } + self.scale = scale; + } + + pub fn set_scale(&mut self, scale: f64) { + self.scale = scale; + } +} + +fn to_color(color: Color) -> tiny_skia::Color { + tiny_skia::Color::from_rgba8(color.r, color.g, color.b, color.a) +} + +fn to_point(point: Point) -> tiny_skia::Point { + tiny_skia::Point::from_xy(point.x as f32, point.y as f32) +} + +impl TinySkiaRenderer { + fn shape_to_path(&self, shape: &impl Shape) -> Option { + let mut builder = PathBuilder::new(); + for element in shape.path_elements(0.1) { + match element { + PathEl::ClosePath => builder.close(), + PathEl::MoveTo(p) => builder.move_to(p.x as f32, p.y as f32), + PathEl::LineTo(p) => builder.line_to(p.x as f32, p.y as f32), + PathEl::QuadTo(p1, p2) => { + builder.quad_to(p1.x as f32, p1.y as f32, p2.x as f32, p2.y as f32) + } + PathEl::CurveTo(p1, p2, p3) => builder.cubic_to( + p1.x as f32, + p1.y as f32, + p2.x as f32, + p2.y as f32, + p3.x as f32, + p3.y as f32, + ), + } + } + builder.finish() + } + + fn brush_to_paint<'b>(&self, brush: impl Into>) -> Option> { + let shader = match brush.into() { + BrushRef::Solid(c) => Shader::SolidColor(to_color(c)), + BrushRef::Gradient(g) => { + let stops = g + .stops + .iter() + .map(|s| GradientStop::new(s.offset, to_color(s.color))) + .collect(); + match g.kind { + GradientKind::Linear { start, end } => LinearGradient::new( + to_point(start), + to_point(end), + stops, + SpreadMode::Pad, + Transform::identity(), + )?, + GradientKind::Radial { + start_center, + start_radius: _, + end_center, + end_radius, + } => { + // FIXME: Doesn't use `start_radius` + RadialGradient::new( + to_point(start_center), + to_point(end_center), + end_radius, + stops, + SpreadMode::Pad, + Transform::identity(), + )? + } + GradientKind::Sweep { .. } => return None, + } + } + BrushRef::Image(_) => return None, + }; + Some(Paint { + shader, + ..Default::default() + }) + } + + /// Transform a `Rect`, applying `self.transform`, into a `tiny_skia::Rect` and + /// residual transform. + fn rect(&self, rect: Rect) -> Option { + tiny_skia::Rect::from_ltrb( + rect.x0 as f32, + rect.y0 as f32, + rect.x1 as f32, + rect.y1 as f32, + ) + } + + fn clip_rect(&self, rect: tiny_skia::Rect) -> Option { + let clip = if let Some(clip) = self.clip { + clip + } else { + return Some(rect); + }; + let clip = self.rect(clip.scale_from_origin(self.scale))?; + clip.intersect(&rect) + } + + /// Renders the pixmap at the position without transforming it. + fn render_pixmap_direct(&mut self, pixmap: &Pixmap, x: f32, y: f32) { + let rect = try_ret!(tiny_skia::Rect::from_xywh( + x, + y, + pixmap.width() as f32, + pixmap.height() as f32, + )); + let paint = Paint { + shader: Pattern::new( + pixmap.as_ref(), + SpreadMode::Pad, + FilterQuality::Nearest, + 1.0, + Transform::from_translate(x, y), + ), + ..Default::default() + }; + + if let Some(rect) = self.clip_rect(rect) { + self.pixmap + .fill_rect(rect, &paint, Transform::identity(), None); + } + } + + fn render_pixmap_rect(&mut self, pixmap: &Pixmap, rect: tiny_skia::Rect) { + let paint = Paint { + shader: Pattern::new( + pixmap.as_ref(), + SpreadMode::Pad, + FilterQuality::Bilinear, + 1.0, + Transform::from_scale( + rect.width() / pixmap.width() as f32, + rect.height() / pixmap.height() as f32, + ), + ), + ..Default::default() + }; + + self.pixmap.fill_rect( + rect, + &paint, + self.current_transform(), + self.clip.is_some().then_some(&self.mask), + ); + } + + fn render_pixmap_paint( + &mut self, + pixmap: &Pixmap, + rect: tiny_skia::Rect, + paint: Option>, + ) { + let paint = if let Some(paint) = paint { + paint + } else { + return self.render_pixmap_rect(pixmap, rect); + }; + + let mut fill = try_ret!(Pixmap::new(pixmap.width(), pixmap.height())); + fill.fill_rect( + try_ret!(tiny_skia::Rect::from_xywh( + 0.0, + 0.0, + pixmap.width() as f32, + pixmap.height() as f32 + )), + &paint, + Transform::identity(), + None, + ); + + let mask = Mask::from_pixmap(pixmap.as_ref(), MaskType::Alpha); + fill.apply_mask(&mask); + + self.render_pixmap_rect(&fill, rect); + } + + fn current_transform(&self) -> Transform { + let transfrom = self.transform.as_coeffs(); + let scale = self.scale as f32; + Transform::from_row( + transfrom[0] as f32, + transfrom[1] as f32, + transfrom[2] as f32, + transfrom[3] as f32, + transfrom[4] as f32, + transfrom[5] as f32, + ) + .post_scale(scale, scale) + } + + fn cache_glyph(&mut self, cache_key: CacheKey, color: Color) -> Option> { + if let Some((color, glyph)) = self.glyph_cache.get_mut(&(cache_key, color)) { + *color = self.cache_color; + return glyph.clone(); + } + + let mut swash_cache = SwashCache::new(); + let image = swash_cache.get_image_uncached(cache_key)?; + + let result = if image.placement.width == 0 || image.placement.height == 0 { + // We can't create an empty `Pixmap` + None + } else { + let mut pixmap = Pixmap::new(image.placement.width, image.placement.height)?; + + if image.content == SwashContent::Mask { + for (a, &alpha) in pixmap.pixels_mut().iter_mut().zip(image.data.iter()) { + *a = tiny_skia::Color::from_rgba8(color.r, color.g, color.b, alpha) + .premultiply() + .to_color_u8(); + } + } else { + panic!("unexpected image content: {:?}", image.content); + } + + Some(Rc::new(Glyph { + pixmap, + left: image.placement.left as f32, + top: image.placement.top as f32, + })) + }; + + self.glyph_cache + .insert((cache_key, color), (self.cache_color, result.clone())); + + result + } +} + +impl Renderer for TinySkiaRenderer { + fn begin(&mut self) { + self.transform = Affine::IDENTITY; + self.pixmap.fill(tiny_skia::Color::WHITE); + self.clip = None; + } + + fn stroke<'b>(&mut self, shape: &impl Shape, brush: impl Into>, width: f64) { + let paint = try_ret!(self.brush_to_paint(brush)); + let path = try_ret!(self.shape_to_path(shape)); + self.pixmap.stroke_path( + &path, + &paint, + &Stroke { + width: width as f32, + ..Default::default() + }, + self.current_transform(), + self.clip.is_some().then_some(&self.mask), + ); + } + + fn fill<'b>(&mut self, shape: &impl Shape, brush: impl Into>, _blur_radius: f64) { + // FIXME: Handle _blur_radius + + let paint = try_ret!(self.brush_to_paint(brush)); + if let Some(rect) = shape.as_rect() { + let rect = try_ret!(self.rect(rect)); + self.pixmap + .fill_rect(rect, &paint, self.current_transform(), None); + } else { + let path = try_ret!(self.shape_to_path(shape)); + self.pixmap.fill_path( + &path, + &paint, + FillRule::Winding, + self.current_transform(), + self.clip.is_some().then_some(&self.mask), + ); + } + } + + fn draw_text(&mut self, layout: &TextLayout, pos: impl Into) { + let offset = self.transform.translation(); + let pos: Point = pos.into(); + let clip = self.clip; + for line in layout.layout_runs() { + if let Some(rect) = clip { + let y = pos.y + offset.y + line.line_y as f64; + if y + (line.line_height as f64) < rect.y0 { + continue; + } + if y - (line.line_height as f64) > rect.y1 { + break; + } + } + + 'line_loop: for glyph_run in line.glyphs { + let x = glyph_run.x + pos.x as f32 + offset.x as f32; + let y = line.line_y + pos.y as f32 + offset.y as f32; + + if let Some(rect) = clip { + if ((x + glyph_run.w) as f64) < rect.x0 { + continue; + } else if x as f64 > rect.x1 { + break 'line_loop; + } + } + + let glyph_x = x * self.scale as f32; + let (new_x, subpx_x) = SubpixelBin::new(glyph_x); + let glyph_x = new_x as f32; + + let glyph_y = (y * self.scale as f32).round(); + let (new_y, subpx_y) = SubpixelBin::new(glyph_y); + let glyph_y = new_y as f32; + + let font_size = (glyph_run.font_size * self.scale as f32).round() as u32; + + let mut cache_key = glyph_run.cache_key; + cache_key.font_size = font_size; + cache_key.x_bin = subpx_x; + cache_key.y_bin = subpx_y; + + let pixmap = self.cache_glyph(cache_key, glyph_run.color); + + if let Some(glyph) = pixmap { + self.render_pixmap_direct( + &glyph.pixmap, + glyph_x + glyph.left, + glyph_y - glyph.top, + ); + } + } + } + } + + fn draw_img(&mut self, img: Img<'_>, _img_width: u32, _img_height: u32, rect: Rect) { + let rect = try_ret!(self.rect(rect)); + if let Some((color, pixmap)) = self.image_cache.get_mut(img.hash) { + *color = self.cache_color; + let pixmap = pixmap.clone(); + self.render_pixmap_rect(&pixmap, rect); + return; + } + + let rgba_image = try_ret!(image::load_from_memory(img.data).ok()).into_rgba8(); + let mut pixmap = try_ret!(Pixmap::new(rgba_image.width(), rgba_image.height())); + for (a, &b) in pixmap.pixels_mut().iter_mut().zip(rgba_image.pixels()) { + *a = tiny_skia::Color::from_rgba8(b.0[0], b.0[1], b.0[2], b.0[3]) + .premultiply() + .to_color_u8(); + } + + self.render_pixmap_rect(&pixmap, rect); + + self.image_cache + .insert(img.hash.to_owned(), (self.cache_color, Rc::new(pixmap))); + } + + fn draw_svg<'b>( + &mut self, + svg: floem_renderer::Svg<'b>, + rect: Rect, + brush: Option>>, + ) { + let width = (rect.width() * self.scale).round() as u32; + let height = (rect.height() * self.scale).round() as u32; + + let rect = try_ret!(self.rect(rect)); + + let paint = brush.and_then(|brush| self.brush_to_paint(brush)); + + if let Some((color, pixmap)) = self.image_cache.get_mut(svg.hash) { + *color = self.cache_color; + let pixmap = pixmap.clone(); + self.render_pixmap_paint(&pixmap, rect, paint); + return; + } + + let mut pixmap = try_ret!(tiny_skia::Pixmap::new(width, height)); + let rtree = resvg::Tree::from_usvg(svg.tree); + let svg_transform = tiny_skia::Transform::from_scale( + (width as f64 / rtree.size.width()) as f32, + (height as f64 / rtree.size.height()) as f32, + ); + rtree.render(svg_transform, &mut pixmap.as_mut()); + + self.render_pixmap_paint(&pixmap, rect, paint); + + self.image_cache + .insert(svg.hash.to_owned(), (self.cache_color, Rc::new(pixmap))); + } + + fn transform(&mut self, transform: Affine) { + self.transform = transform; + } + + fn set_z_index(&mut self, _z_index: i32) { + // FIXME: Remove this method? + } + + fn clip(&mut self, shape: &impl Shape) { + let rect = if let Some(rect) = shape.as_rect() { + rect + } else if let Some(rect) = shape.as_rounded_rect() { + rect.rect() + } else { + shape.bounding_box() + }; + + let offset = self.transform.translation(); + self.clip = Some(rect + offset); + + self.mask.clear(); + let path = try_ret!(self.shape_to_path(shape)); + self.mask + .fill_path(&path, FillRule::Winding, false, self.current_transform()); + } + + fn clear_clip(&mut self) { + self.clip = None; + } + + fn finish(&mut self) { + // Remove cache entries which were not accessed. + self.image_cache.retain(|_, (c, _)| *c == self.cache_color); + self.glyph_cache.retain(|_, (c, _)| *c == self.cache_color); + + // Swap the cache color. + self.cache_color = CacheColor(!self.cache_color.0); + + let mut buffer = self + .surface + .buffer_mut() + .expect("failed to get the surface buffer"); + + // Copy from `tiny_skia::Pixmap` to the format specified by `softbuffer::Buffer`. + for (out_pixel, pixel) in (buffer.iter_mut()).zip(self.pixmap.pixels().iter()) { + *out_pixel = + (pixel.red() as u32) << 16 | (pixel.green() as u32) << 8 | (pixel.blue() as u32); + } + + buffer + .present() + .expect("failed to present the surface buffer"); + } +} diff --git a/vger/src/lib.rs b/vger/src/lib.rs index d0c7ec2d..c116f77a 100644 --- a/vger/src/lib.rs +++ b/vger/src/lib.rs @@ -9,7 +9,7 @@ use peniko::{ BrushRef, Color, GradientKind, }; use vger::{Image, PaintIndex, PixelFormat, Vger}; -use wgpu::{Device, Queue, Surface, SurfaceConfiguration, TextureFormat}; +use wgpu::{Device, DeviceType, Queue, Surface, SurfaceConfiguration, TextureFormat}; pub struct VgerRenderer { device: Arc, @@ -44,6 +44,10 @@ impl VgerRenderer { })) .ok_or_else(|| anyhow::anyhow!("can't get adaptor"))?; + if adapter.get_info().device_type == DeviceType::Cpu { + return Err(anyhow::anyhow!("only cpu adaptor found")); + } + let (device, queue) = futures::executor::block_on(adapter.request_device( &wgpu::DeviceDescriptor { features: wgpu::Features::empty(),