diff options
author | 2023-02-02 01:24:27 +0100 | |
---|---|---|
committer | 2023-02-24 13:22:57 +0100 | |
commit | 1d0c44fb255f5fbf5a03e7c737e40ab66d39de9e (patch) | |
tree | 8cc41f70f935ab332864d3c280f864f0dfc4f505 | |
parent | 032e860f13a562719faf128238abe7ffb7f2a610 (diff) | |
download | iced-1d0c44fb255f5fbf5a03e7c737e40ab66d39de9e.tar.gz iced-1d0c44fb255f5fbf5a03e7c737e40ab66d39de9e.tar.bz2 iced-1d0c44fb255f5fbf5a03e7c737e40ab66d39de9e.zip |
Implement basic text caching in `iced_wgpu`
-rw-r--r-- | core/src/font.rs | 16 | ||||
-rw-r--r-- | wgpu/Cargo.toml | 2 | ||||
-rw-r--r-- | wgpu/src/text.rs | 201 |
3 files changed, 159 insertions, 60 deletions
diff --git a/core/src/font.rs b/core/src/font.rs index 3f9ad2b5..130e378e 100644 --- a/core/src/font.rs +++ b/core/src/font.rs @@ -1,3 +1,5 @@ +use std::hash::{Hash, Hasher}; + /// A font. #[derive(Debug, Clone, Copy)] pub enum Font { @@ -22,3 +24,17 @@ impl Default for Font { Font::Default } } + +impl Hash for Font { + fn hash<H: Hasher>(&self, hasher: &mut H) { + match self { + Self::Default => { + 0.hash(hasher); + } + Self::External { name, .. } => { + 1.hash(hasher); + name.hash(hasher); + } + } + } +} diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index d29c1129..1a94c6a3 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -34,6 +34,8 @@ guillotiere = "0.6" futures = "0.3" bitflags = "1.2" once_cell = "1.0" +rustc-hash = "1.1" +twox-hash = "1.6" [dependencies.bytemuck] version = "1.9" diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 3f828da1..a1d621d4 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -2,16 +2,107 @@ pub use iced_native::text::Hit; use iced_graphics::layer::Text; use iced_native::alignment; -use iced_native::{Font, Rectangle, Size}; +use iced_native::{Color, Font, Rectangle, Size}; + +use rustc_hash::{FxHashMap, FxHashSet}; +use std::cell::RefCell; +use std::hash::{BuildHasher, Hash, Hasher}; +use twox_hash::RandomXxHashBuilder64; #[allow(missing_debug_implementations)] pub struct Pipeline { renderers: Vec<glyphon::TextRenderer>, atlas: glyphon::TextAtlas, cache: glyphon::SwashCache<'static>, + measurement_cache: RefCell<Cache>, + render_cache: Cache, layer: usize, } +struct Cache { + entries: FxHashMap<KeyHash, glyphon::Buffer<'static>>, + recently_used: FxHashSet<KeyHash>, + hasher: RandomXxHashBuilder64, +} + +impl Cache { + fn new() -> Self { + Self { + entries: FxHashMap::default(), + recently_used: FxHashSet::default(), + hasher: RandomXxHashBuilder64::default(), + } + } + + fn get(&self, key: &KeyHash) -> Option<&glyphon::Buffer<'static>> { + self.entries.get(key) + } + + fn allocate( + &mut self, + key: Key<'_>, + ) -> (KeyHash, &mut glyphon::Buffer<'static>) { + let hash = { + let mut hasher = self.hasher.build_hasher(); + + key.content.hash(&mut hasher); + (key.size as i32).hash(&mut hasher); + key.font.hash(&mut hasher); + (key.bounds.width as i32).hash(&mut hasher); + (key.bounds.height as i32).hash(&mut hasher); + key.color.into_rgba8().hash(&mut hasher); + + hasher.finish() + }; + + if !self.entries.contains_key(&hash) { + let metrics = + glyphon::Metrics::new(key.size as i32, (key.size * 1.2) as i32); + + let mut buffer = glyphon::Buffer::new(&FONT_SYSTEM, metrics); + + buffer.set_size(key.bounds.width as i32, key.bounds.height as i32); + buffer.set_text( + key.content, + glyphon::Attrs::new().family(to_family(key.font)).color({ + let [r, g, b, a] = key.color.into_linear(); + + glyphon::Color::rgba( + (r * 255.0) as u8, + (g * 255.0) as u8, + (b * 255.0) as u8, + (a * 255.0) as u8, + ) + }), + ); + + let _ = self.entries.insert(hash, buffer); + } + + let _ = self.recently_used.insert(hash); + + (hash, self.entries.get_mut(&hash).unwrap()) + } + + fn trim(&mut self) { + self.entries + .retain(|key, _| self.recently_used.contains(key)); + + self.recently_used.clear(); + } +} + +#[derive(Debug, Clone, Copy)] +struct Key<'a> { + content: &'a str, + size: f32, + font: Font, + bounds: Size, + color: Color, +} + +type KeyHash = u64; + // TODO: Share with `iced_graphics` static FONT_SYSTEM: once_cell::sync::Lazy<glyphon::FontSystem> = once_cell::sync::Lazy::new(glyphon::FontSystem::new); @@ -28,6 +119,8 @@ impl Pipeline { renderers: Vec::new(), atlas: glyphon::TextAtlas::new(device, queue, format), cache: glyphon::SwashCache::new(&FONT_SYSTEM), + measurement_cache: RefCell::new(Cache::new()), + render_cache: Cache::new(), layer: 0, } } @@ -48,48 +141,29 @@ impl Pipeline { let renderer = &mut self.renderers[self.layer]; - let buffers: Vec<_> = sections + let keys: Vec<_> = sections .iter() .map(|section| { - let metrics = glyphon::Metrics::new( - (section.size * scale_factor) as i32, - (section.size * 1.2 * scale_factor) as i32, - ); - - let mut buffer = glyphon::Buffer::new(&FONT_SYSTEM, metrics); - - buffer.set_size( - (section.bounds.width * scale_factor).ceil() as i32, - (section.bounds.height * scale_factor).ceil() as i32, - ); - - buffer.set_text( - section.content, - glyphon::Attrs::new() - .color({ - let [r, g, b, a] = section.color.into_linear(); - - glyphon::Color::rgba( - (r * 255.0) as u8, - (g * 255.0) as u8, - (b * 255.0) as u8, - (a * 255.0) as u8, - ) - }) - .family(match section.font { - Font::Default => glyphon::Family::SansSerif, - Font::External { name, .. } => { - glyphon::Family::Name(name) - } - }), - ); - - buffer.shape_until_scroll(); - - buffer + let (key, _) = self.render_cache.allocate(Key { + content: section.content, + size: section.size * scale_factor, + font: section.font, + bounds: Size { + width: section.bounds.width * scale_factor, + height: section.bounds.height * scale_factor, + }, + color: section.color, + }); + + key }) .collect(); + let buffers: Vec<_> = keys + .iter() + .map(|key| self.render_cache.get(key).expect("Get cached buffer")) + .collect(); + let bounds = glyphon::TextBounds { left: (bounds.x * scale_factor) as i32, top: (bounds.y * scale_factor) as i32, @@ -190,29 +264,27 @@ impl Pipeline { font: Font, bounds: Size, ) -> (f32, f32) { - let attrs = match font { - Font::Default => glyphon::Attrs::new(), - Font::External { name, .. } => glyphon::Attrs { - family: glyphon::Family::Name(name), - ..glyphon::Attrs::new() + let mut measurement_cache = self.measurement_cache.borrow_mut(); + + let (_, paragraph) = measurement_cache.allocate(Key { + content, + size: size, + font, + bounds: Size { + width: bounds.width, + height: f32::INFINITY, }, - }; + color: Color::BLACK, + }); + + let (total_lines, max_width) = paragraph + .layout_runs() + .enumerate() + .fold((0, 0.0), |(_, max), (i, buffer)| { + (i + 1, buffer.line_w.max(max)) + }); - let mut paragraph = - glyphon::BufferLine::new(content, glyphon::AttrsList::new(attrs)); - - // TODO: Cache layout - let layout = paragraph.layout( - &FONT_SYSTEM, - size as i32, - bounds.width as i32, - glyphon::Wrap::Word, - ); - - ( - layout.iter().fold(0.0, |max, line| line.w.max(max)), - size * 1.2 * layout.len() as f32, - ) + (max_width, size * 1.2 * total_lines as f32) } pub fn hit_test( @@ -227,5 +299,14 @@ impl Pipeline { None } - pub fn trim_measurement_cache(&mut self) {} + pub fn trim_measurement_cache(&mut self) { + self.measurement_cache.borrow_mut().trim(); + } +} + +fn to_family(font: Font) -> glyphon::Family<'static> { + match font { + Font::Default => glyphon::Family::SansSerif, + Font::External { name, .. } => glyphon::Family::Name(name), + } } |