From 89ccd2904ff50346bb5cda7f27ccdbc7e80202c4 Mon Sep 17 00:00:00 2001 From: kamiduki Date: Fri, 10 Jul 2026 15:02:07 +0800 Subject: [PATCH 1/2] Support `usvg::Tree` as svg data --- Cargo.lock | 1 + core/Cargo.toml | 1 + core/src/svg.rs | 127 +++++++++++++++++++----- tiny_skia/src/vector.rs | 59 +++++------ wgpu/src/image/vector.rs | 206 +++++++++++++++++++-------------------- 5 files changed, 232 insertions(+), 162 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d40d44e29..91349a2857 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2304,6 +2304,7 @@ dependencies = [ "log", "num-traits", "raw-window-handle", + "resvg", "rustc-hash 2.1.3", "serde", "smol_str 0.2.2", diff --git a/core/Cargo.toml b/core/Cargo.toml index 7176b7917b..cdbc628953 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -28,6 +28,7 @@ lilt.workspace = true log.workspace = true num-traits.workspace = true raw-window-handle.workspace = true +resvg.workspace = true rustc-hash.workspace = true smol_str.workspace = true thiserror.workspace = true diff --git a/core/src/svg.rs b/core/src/svg.rs index 4a25b7e780..4a50022762 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -1,12 +1,56 @@ //! Load and draw vector graphics. -use crate::{Color, Radians, Rectangle, Size}; +use crate::{Bytes, Color, Radians, Rectangle, Size}; +use resvg::usvg; use rustc_hash::FxHasher; -use std::borrow::Cow; -use std::hash::{Hash, Hasher as _}; +use std::fmt::{self, Debug}; +use std::fs; +use std::hash::{Hash, Hasher}; use std::path::PathBuf; use std::sync::Arc; +/// The unique identifier of some [`Handle`] data. +#[derive(Clone)] +pub enum Id { + /// Hash value of [`Data`] + Hash(u64), + /// Address of allocated [`usvg::Tree`] + Addr(Arc), +} + +impl Debug for Id { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + &Self::Hash(hash) => write!(f, "{}", hash), + Self::Addr(addr) => write!(f, "{:?}", addr.as_ref() as *const usvg::Tree), + } + } +} + +impl PartialEq for Id { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (&Self::Hash(x), &Self::Hash(y)) => x == y, + (Self::Addr(x), Self::Addr(y)) => { + (x.as_ref() as *const usvg::Tree) == (y.as_ref() as *const usvg::Tree) + } + _ => false, + } + } +} + +impl Eq for Id {} + +impl Hash for Id { + fn hash(&self, state: &mut H) { + core::mem::discriminant(self).hash(state); + match self { + &Id::Hash(x) => state.write_u64(x), + Id::Addr(tree) => (tree.as_ref() as *const usvg::Tree).hash(state), + } + } +} + /// A raster image that can be drawn. #[derive(Debug, Clone, PartialEq)] pub struct Svg { @@ -68,10 +112,18 @@ impl From<&Handle> for Svg { } /// A handle of Svg data. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Handle { - id: u64, - data: Arc, +#[derive(Debug, Clone)] +pub enum Handle { + /// Unloaded svg [`Data`] + Unloaded { + /// Hash value of [`Handle::Unloaded::data`] + hash: u64, + /// Data storage of a [`Handle`] + data: Data, + }, + + /// Parsed [`usvg::Tree`] + Loaded(Arc), } impl Handle { @@ -86,7 +138,7 @@ impl Handle { /// /// This is useful if you already have your SVG data in-memory, maybe /// because you downloaded or generated it procedurally. - pub fn from_memory(bytes: impl Into>) -> Handle { + pub fn from_memory(bytes: impl Into) -> Handle { Self::from_data(Data::Bytes(bytes.into())) } @@ -94,20 +146,23 @@ impl Handle { let mut hasher = FxHasher::default(); data.hash(&mut hasher); - Handle { - id: hasher.finish(), - data: Arc::new(data), + Handle::Unloaded { + hash: hasher.finish(), + data, } } - /// Returns the unique identifier of the [`Handle`]. - pub fn id(&self) -> u64 { - self.id + /// Creates an SVG [`Handle`] from a parsed `usvg::Tree` + pub fn from_tree(tree: Arc) -> Handle { + Self::Loaded(tree) } - /// Returns a reference to the SVG [`Data`]. - pub fn data(&self) -> &Data { - &self.data + /// Returns the unique identifier of the [`Handle`]. + pub fn id(&self) -> Id { + match self { + &Handle::Unloaded { hash, .. } => Id::Hash(hash), + Handle::Loaded(tree) => Id::Addr(tree.clone()), + } } } @@ -121,8 +176,20 @@ where } impl Hash for Handle { - fn hash(&self, state: &mut H) { - self.id.hash(state); + fn hash(&self, state: &mut H) { + match self { + &Handle::Unloaded { hash, .. } => state.write_u64(hash), + _ => {} + } + } +} + +impl PartialEq for Handle { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (&Handle::Unloaded { hash: x, .. }, &Handle::Unloaded { hash: y, .. }) => x == y, + _ => false, + } } } @@ -135,14 +202,26 @@ pub enum Data { /// In-memory data /// /// Can contain an SVG string or a gzip compressed data. - Bytes(Cow<'static, [u8]>), + Bytes(Bytes), +} + +impl Data { + /// Try to load and parse `Data` to `usvg::Tree` + pub fn load(&self, options: &usvg::Options<'_>) -> Option { + match self { + Self::Path(path) => fs::read_to_string(&path) + .ok() + .and_then(|text| usvg::Tree::from_str(&text, &options).ok()), + Data::Bytes(bytes) => usvg::Tree::from_data(&bytes, options).ok(), + } + } } -impl std::fmt::Debug for Data { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Debug for Data { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Data::Path(path) => write!(f, "Path({path:?})"), - Data::Bytes(_) => write!(f, "Bytes(...)"), + Self::Path(path) => f.debug_tuple("Path").field(path).finish(), + Self::Bytes(_) => f.write_str("Bytes(...)"), } } } diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs index 2dad44c38a..42c874f8fc 100644 --- a/tiny_skia/src/vector.rs +++ b/tiny_skia/src/vector.rs @@ -1,4 +1,4 @@ -use crate::core::svg::{Data, Handle}; +use crate::core::svg::{Handle, Id}; use crate::core::{Color, Rectangle, Size}; use resvg::usvg; @@ -6,10 +6,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use tiny_skia::Transform; use std::cell::RefCell; -use std::collections::hash_map; -use std::fs; use std::panic; -#[cfg(feature = "svg-text")] use std::sync::Arc; #[derive(Debug)] @@ -70,7 +67,7 @@ impl Pipeline { #[derive(Default)] struct Cache { - trees: FxHashMap>, + trees: FxHashMap>>, tree_hits: FxHashSet, rasters: FxHashMap, raster_hits: FxHashSet, @@ -78,17 +75,15 @@ struct Cache { fontdb: Option>, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] struct RasterKey { - id: u64, + id: Id, color: Option<[u8; 4]>, size: Size, } impl Cache { - fn load(&mut self, handle: &Handle) -> Option<&usvg::Tree> { - let id = handle.id(); - + fn load(&mut self, handle: &Handle) -> Option> { // TODO: Reuse `cosmic-text` font database #[cfg(feature = "svg-text")] if self.fontdb.is_none() { @@ -98,29 +93,25 @@ impl Cache { self.fontdb = Some(Arc::new(fontdb)); } - let options = usvg::Options { - #[cfg(feature = "svg-text")] - fontdb: self - .fontdb - .as_ref() - .expect("fontdb must be initialized") + match handle { + &Handle::Unloaded { hash, ref data } => self + .trees + .entry(hash) + .or_insert_with(|| { + data.load(&usvg::Options { + #[cfg(feature = "svg-text")] + fontdb: self + .fontdb + .as_ref() + .expect("fontdb must be initialized") + .clone(), + ..usvg::Options::default() + }) + .map(Arc::new) + }) .clone(), - ..usvg::Options::default() - }; - - if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) { - let svg = match handle.data() { - Data::Path(path) => fs::read_to_string(path) - .ok() - .and_then(|contents| usvg::Tree::from_str(&contents, &options).ok()), - Data::Bytes(bytes) => usvg::Tree::from_data(bytes, &options).ok(), - }; - - let _ = entry.insert(svg); + Handle::Loaded(tree) => Some(tree.clone()), } - - let _ = self.tree_hits.insert(id); - self.trees.get(&id).unwrap().as_ref() } fn viewport_dimensions(&mut self, handle: &Handle) -> Option> { @@ -175,7 +166,7 @@ impl Cache { // SVG rendering can panic on malformed or complex vectors. // We catch panics to prevent crashes and continue gracefully. let render = panic::catch_unwind(panic::AssertUnwindSafe(|| { - resvg::render(tree, transform, &mut image.as_mut()); + resvg::render(&tree, transform, &mut image.as_mut()); })); if let Err(error) = render { @@ -198,10 +189,10 @@ impl Cache { } } - let _ = self.rasters.insert(key, image); + let _ = self.rasters.insert(key.clone(), image); } - let _ = self.raster_hits.insert(key); + let _ = self.raster_hits.insert(key.clone()); self.rasters.get(&key).map(tiny_skia::Pixmap::as_ref) } diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index b90cc60eaa..036ddd44b2 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -5,15 +5,14 @@ use crate::image::atlas::{self, Atlas}; use resvg::tiny_skia; use resvg::usvg; use rustc_hash::{FxHashMap, FxHashSet}; -use std::fs; use std::panic; -#[cfg(feature = "svg-text")] use std::sync::Arc; /// Entry in cache corresponding to an svg handle +#[derive(Clone)] pub enum Svg { /// Parsed svg - Loaded(usvg::Tree), + Loaded(Arc), /// Svg not found or failed to parse NotFound, } @@ -30,15 +29,31 @@ impl Svg { Svg::NotFound => Size::new(1, 1), } } + + fn loaded(&self) -> Option<&usvg::Tree> { + match self { + Svg::Loaded(tree) => Some(tree), + Svg::NotFound => None, + } + } +} + +impl From>> for Svg { + fn from(value: Option>) -> Self { + match value { + Some(tree) => Svg::Loaded(tree), + None => Svg::NotFound, + } + } } /// Caches svg vector and raster data #[derive(Debug, Default)] pub struct Cache { svgs: FxHashMap, - rasterized: FxHashMap<(u64, u32, u32, ColorFilter), atlas::Entry>, + rasterized: FxHashMap<(svg::Id, u32, u32, ColorFilter), atlas::Entry>, svg_hits: FxHashSet, - rasterized_hits: FxHashSet<(u64, u32, u32, ColorFilter)>, + rasterized_hits: FxHashSet<(svg::Id, u32, u32, ColorFilter)>, should_trim: bool, #[cfg(feature = "svg-text")] fontdb: Option>, @@ -48,46 +63,28 @@ type ColorFilter = Option<[u8; 4]>; impl Cache { /// Load svg - pub fn load(&mut self, handle: &svg::Handle) -> &Svg { - if self.svgs.contains_key(&handle.id()) { - return self.svgs.get(&handle.id()).unwrap(); - } - - // TODO: Reuse `cosmic-text` font database - #[cfg(feature = "svg-text")] - if self.fontdb.is_none() { - let mut fontdb = usvg::fontdb::Database::new(); - fontdb.load_system_fonts(); - - self.fontdb = Some(Arc::new(fontdb)); - } - - let options = usvg::Options { - #[cfg(feature = "svg-text")] - fontdb: self - .fontdb - .as_ref() - .expect("fontdb must be initialized") + pub fn load<'a>(&'a mut self, handle: &'a svg::Handle) -> Svg { + match handle { + &svg::Handle::Unloaded { hash, ref data } => self + .svgs + .entry(hash) + .or_insert_with(|| { + self.should_trim = true; + data.load(&usvg::Options { + #[cfg(feature = "svg-text")] + fontdb: self + .fontdb + .as_ref() + .expect("fontdb must be initialized") + .clone(), + ..usvg::Options::default() + }) + .map(Arc::new) + .into() + }) .clone(), - ..usvg::Options::default() - }; - - let svg = match handle.data() { - svg::Data::Path(path) => fs::read_to_string(path) - .ok() - .and_then(|contents| usvg::Tree::from_str(&contents, &options).ok()) - .map(Svg::Loaded) - .unwrap_or(Svg::NotFound), - svg::Data::Bytes(bytes) => match usvg::Tree::from_data(bytes, &options) { - Ok(tree) => Svg::Loaded(tree), - Err(_) => Svg::NotFound, - }, - }; - - self.should_trim = true; - - let _ = self.svgs.insert(handle.id(), svg); - self.svgs.get(&handle.id()).unwrap() + svg::Handle::Loaded(tree) => Some(tree.clone()).into(), + } } /// Load svg and upload raster data @@ -104,83 +101,84 @@ impl Cache { let id = handle.id(); let color = color.map(Color::into_rgba8); - let key = (id, size.width, size.height, color); + let key = (id.clone(), size.width, size.height, color); // TODO: Optimize! // We currently rerasterize the SVG when its size changes. This is slow // as heck. A GPU rasterizer like `pathfinder` may perform better. // It would be cool to be able to smooth resize the `svg` example. if self.rasterized.contains_key(&key) { - let _ = self.svg_hits.insert(id); - let _ = self.rasterized_hits.insert(key); + if let svg::Id::Hash(hash) = id { + _ = self.svg_hits.insert(hash) + } + let _ = self.rasterized_hits.insert(key.clone()); return self.rasterized.get(&key); } - match self.load(handle) { - Svg::Loaded(tree) => { - // TODO: Optimize! - // We currently rerasterize the SVG when its size changes. This is slow - // as heck. A GPU rasterizer like `pathfinder` may perform better. - // It would be cool to be able to smooth resize the `svg` example. - let mut img = tiny_skia::Pixmap::new(size.width, size.height)?; - - let tree_size = tree.size().to_int_size(); - - let target_size = if size.width > size.height { - tree_size.scale_to_height(size.height) - } else { - tree_size.scale_to_width(size.width) - }; - - let transform = if let Some(target_size) = target_size { - let tree_size = tree_size.to_size(); - let target_size = target_size.to_size(); - - tiny_skia::Transform::from_scale( - target_size.width() / tree_size.width(), - target_size.height() / tree_size.height(), - ) - } else { - tiny_skia::Transform::default() - }; - - // SVG rendering can panic on malformed or complex vectors. - // We catch panics to prevent crashes and continue gracefully. - let render = panic::catch_unwind(panic::AssertUnwindSafe(|| { - resvg::render(tree, transform, &mut img.as_mut()); - })); - - if let Err(error) = render { - log::warn!("SVG rendering for {handle:?} panicked: {error:?}"); - } + let tree = self.load(handle); + let tree = tree.loaded()?; - let mut rgba = img.take(); + // TODO: Optimize! + // We currently rerasterize the SVG when its size changes. This is slow + // as heck. A GPU rasterizer like `pathfinder` may perform better. + // It would be cool to be able to smooth resize the `svg` example. + let mut img = tiny_skia::Pixmap::new(size.width, size.height)?; - if let Some(color) = color { - rgba.chunks_exact_mut(4).for_each(|rgba| { - if rgba[3] > 0 { - rgba[0] = color[0]; - rgba[1] = color[1]; - rgba[2] = color[2]; - } - }); - } + let tree_size = tree.size().to_int_size(); - let allocation = - atlas.upload(device, encoder, belt, size.width, size.height, &rgba)?; + let target_size = if size.width > size.height { + tree_size.scale_to_height(size.height) + } else { + tree_size.scale_to_width(size.width) + }; - log::debug!("allocating {id} {}x{}", size.width, size.height); + let transform = if let Some(target_size) = target_size { + let tree_size = tree_size.to_size(); + let target_size = target_size.to_size(); - let _ = self.svg_hits.insert(id); - let _ = self.rasterized_hits.insert(key); - let _ = self.rasterized.insert(key, allocation); - self.should_trim = true; + tiny_skia::Transform::from_scale( + target_size.width() / tree_size.width(), + target_size.height() / tree_size.height(), + ) + } else { + tiny_skia::Transform::default() + }; - self.rasterized.get(&key) - } - Svg::NotFound => None, + // SVG rendering can panic on malformed or complex vectors. + // We catch panics to prevent crashes and continue gracefully. + let render = panic::catch_unwind(panic::AssertUnwindSafe(|| { + resvg::render(&tree, transform, &mut img.as_mut()); + })); + + if let Err(error) = render { + log::warn!("SVG rendering for {handle:?} panicked: {error:?}"); + } + + let mut rgba = img.take(); + + if let Some(color) = color { + rgba.chunks_exact_mut(4).for_each(|rgba| { + if rgba[3] > 0 { + rgba[0] = color[0]; + rgba[1] = color[1]; + rgba[2] = color[2]; + } + }); } + + let allocation = atlas.upload(device, encoder, belt, size.width, size.height, &rgba)?; + + log::debug!("allocating {id:?} {}x{}", size.width, size.height); + + if let svg::Id::Hash(hash) = id { + _ = self.svg_hits.insert(hash) + } + let _ = self.rasterized_hits.insert(key.clone()); + let _ = self.rasterized.insert(key.clone(), allocation); + self.should_trim = true; + + self.rasterized.get(&key) } /// Load svg and upload raster data From 41f01786277815373c0436987063613d60c9b45a Mon Sep 17 00:00:00 2001 From: kamiduki Date: Fri, 10 Jul 2026 16:52:55 +0800 Subject: [PATCH 2/2] Fix lints --- core/src/svg.rs | 20 +++++--------------- wgpu/src/image/vector.rs | 8 ++++---- wgpu/src/layer.rs | 4 ++-- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/core/src/svg.rs b/core/src/svg.rs index 4a50022762..460b70b95e 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -7,6 +7,7 @@ use std::fmt::{self, Debug}; use std::fs; use std::hash::{Hash, Hasher}; use std::path::PathBuf; +use std::ptr; use std::sync::Arc; /// The unique identifier of some [`Handle`] data. @@ -31,9 +32,7 @@ impl PartialEq for Id { fn eq(&self, other: &Self) -> bool { match (self, other) { (&Self::Hash(x), &Self::Hash(y)) => x == y, - (Self::Addr(x), Self::Addr(y)) => { - (x.as_ref() as *const usvg::Tree) == (y.as_ref() as *const usvg::Tree) - } + (Self::Addr(x), Self::Addr(y)) => ptr::eq(x.as_ref(), y.as_ref()), _ => false, } } @@ -175,15 +174,6 @@ where } } -impl Hash for Handle { - fn hash(&self, state: &mut H) { - match self { - &Handle::Unloaded { hash, .. } => state.write_u64(hash), - _ => {} - } - } -} - impl PartialEq for Handle { fn eq(&self, other: &Self) -> bool { match (self, other) { @@ -209,10 +199,10 @@ impl Data { /// Try to load and parse `Data` to `usvg::Tree` pub fn load(&self, options: &usvg::Options<'_>) -> Option { match self { - Self::Path(path) => fs::read_to_string(&path) + Self::Path(path) => fs::read_to_string(path) .ok() - .and_then(|text| usvg::Tree::from_str(&text, &options).ok()), - Data::Bytes(bytes) => usvg::Tree::from_data(&bytes, options).ok(), + .and_then(|text| usvg::Tree::from_str(&text, options).ok()), + Data::Bytes(bytes) => usvg::Tree::from_data(bytes, options).ok(), } } } diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index 036ddd44b2..7d45bb81b5 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -109,7 +109,7 @@ impl Cache { // It would be cool to be able to smooth resize the `svg` example. if self.rasterized.contains_key(&key) { if let svg::Id::Hash(hash) = id { - _ = self.svg_hits.insert(hash) + _ = self.svg_hits.insert(hash); } let _ = self.rasterized_hits.insert(key.clone()); @@ -148,7 +148,7 @@ impl Cache { // SVG rendering can panic on malformed or complex vectors. // We catch panics to prevent crashes and continue gracefully. let render = panic::catch_unwind(panic::AssertUnwindSafe(|| { - resvg::render(&tree, transform, &mut img.as_mut()); + resvg::render(tree, transform, &mut img.as_mut()); })); if let Err(error) = render { @@ -158,7 +158,7 @@ impl Cache { let mut rgba = img.take(); if let Some(color) = color { - rgba.chunks_exact_mut(4).for_each(|rgba| { + rgba.as_chunks_mut::<4>().0.iter_mut().for_each(|rgba| { if rgba[3] > 0 { rgba[0] = color[0]; rgba[1] = color[1]; @@ -172,7 +172,7 @@ impl Cache { log::debug!("allocating {id:?} {}x{}", size.width, size.height); if let svg::Id::Hash(hash) = id { - _ = self.svg_hits.insert(hash) + _ = self.svg_hits.insert(hash); } let _ = self.rasterized_hits.insert(key.clone()); let _ = self.rasterized.insert(key.clone(), allocation); diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index f3b243b6a9..3f166fbd28 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -255,7 +255,7 @@ impl Layer { if !self.pending_meshes.is_empty() { self.triangles.push(triangle::Item::Group { transformation: Transformation::IDENTITY, - meshes: self.pending_meshes.drain(..).collect(), + meshes: std::mem::take(&mut self.pending_meshes), }); } } @@ -264,7 +264,7 @@ impl Layer { if !self.pending_text.is_empty() { self.text.push(text::Item::Group { transformation: Transformation::IDENTITY, - text: self.pending_text.drain(..).collect(), + text: std::mem::take(&mut self.pending_text), }); } }