From 1eb3233373a805fe080141f09385708942b0d1bb Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 8 Feb 2024 11:12:41 -0700 Subject: [PATCH] Add shape-run-cache feature, that can significantly improve shaping performance --- CHANGELOG.md | 4 +++ Cargo.toml | 3 +- src/attrs.rs | 2 +- src/font/system.rs | 6 ++++ src/lib.rs | 3 ++ src/shape.rs | 69 ++++++++++++++++++++++++++++++++++++++++++ src/shape_run_cache.rs | 49 ++++++++++++++++++++++++++++++ 7 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 src/shape_run_cache.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 07bf1db..90a9d1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Add `shape-run-cache` feature, that can significantly improve shaping performance + ### Removed - Remove editor-libcosmic, see cosmic-edit instead diff --git a/Cargo.toml b/Cargo.toml index 1edd1ae..ece49b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,9 @@ features = ["hardcoded-data"] [features] default = ["std", "swash", "fontconfig"] +fontconfig = ["fontdb/fontconfig", "std"] no_std = ["rustybuzz/libm", "hashbrown"] +shape-run-cache = [] std = [ "fontdb/memmap", "fontdb/std", @@ -47,7 +49,6 @@ std = [ vi = ["modit", "syntect", "cosmic_undo_2"] wasm-web = ["sys-locale?/js"] warn_on_missing_glyphs = [] -fontconfig = ["fontdb/fontconfig", "std"] [[bench]] name = "layout" diff --git a/src/attrs.rs b/src/attrs.rs index a7886d0..439e94b 100644 --- a/src/attrs.rs +++ b/src/attrs.rs @@ -252,7 +252,7 @@ impl AttrsOwned { #[derive(Debug, Clone, Eq, PartialEq)] pub struct AttrsList { defaults: AttrsOwned, - spans: RangeMap, + pub(crate) spans: RangeMap, } impl AttrsList { diff --git a/src/font/system.rs b/src/font/system.rs index be801a6..c238584 100644 --- a/src/font/system.rs +++ b/src/font/system.rs @@ -32,6 +32,10 @@ pub struct FontSystem { /// Cache for rustybuzz shape plans. shape_plan_cache: ShapePlanCache, + + /// Cache for shaped runs + #[cfg(feature = "shape-run-cache")] + pub shape_run_cache: crate::ShapeRunCache, } impl fmt::Debug for FontSystem { @@ -81,6 +85,8 @@ impl FontSystem { font_cache: Default::default(), font_matches_cache: Default::default(), shape_plan_cache: ShapePlanCache::default(), + #[cfg(feature = "shape-run-cache")] + shape_run_cache: crate::ShapeRunCache::default(), } } diff --git a/src/lib.rs b/src/lib.rs index b15eb1c..9b1bae9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -129,6 +129,9 @@ mod shape; use self::shape_plan_cache::*; mod shape_plan_cache; +pub use self::shape_run_cache::*; +mod shape_run_cache; + #[cfg(feature = "swash")] pub use self::swash::*; #[cfg(feature = "swash")] diff --git a/src/shape.rs b/src/shape.rs index 4bed30a..4db2d63 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -52,6 +52,7 @@ impl Shaping { match self { #[cfg(feature = "swash")] Self::Basic => shape_skip(font_system, glyphs, line, attrs_list, start_run, end_run), + #[cfg(not(feature = "shape-run-cache"))] Self::Advanced => shape_run( scratch, glyphs, @@ -62,6 +63,17 @@ impl Shaping { end_run, span_rtl, ), + #[cfg(feature = "shape-run-cache")] + Self::Advanced => shape_run_cached( + scratch, + glyphs, + font_system, + line, + attrs_list, + start_run, + end_run, + span_rtl, + ), } } } @@ -334,6 +346,63 @@ fn shape_run( scratch.scripts = scripts; } +#[cfg(feature = "shape-run-cache")] +fn shape_run_cached( + scratch: &mut ShapeBuffer, + glyphs: &mut Vec, + font_system: &mut FontSystem, + line: &str, + attrs_list: &AttrsList, + start_run: usize, + end_run: usize, + span_rtl: bool, +) { + use crate::{AttrsOwned, ShapeRunKey}; + + let run_range = start_run..end_run; + let mut key = ShapeRunKey { + text: line[run_range.clone()].to_string(), + default_attrs: AttrsOwned::new(attrs_list.defaults()), + attrs_spans: Vec::new(), + }; + for (attrs_range, attrs) in attrs_list.spans.overlapping(&run_range) { + if attrs == &key.default_attrs { + // Skip if attrs matches default attrs + continue; + } + let start = max(attrs_range.start, start_run) + .checked_sub(start_run) + .unwrap_or(0); + let end = min(attrs_range.end, end_run) + .checked_sub(start_run) + .unwrap_or(0); + if end > start { + let range = start..end; + key.attrs_spans.push((range, attrs.clone())); + } + } + if let Some(cache_glyphs) = font_system.shape_run_cache.get(&key) { + // Use cached glyphs + glyphs.extend_from_slice(&cache_glyphs); + return; + } + + // Fill in cache if not already set + let mut cache_glyphs = Vec::new(); + shape_run( + scratch, + &mut cache_glyphs, + font_system, + line, + attrs_list, + start_run, + end_run, + span_rtl, + ); + glyphs.extend_from_slice(&cache_glyphs); + font_system.shape_run_cache.insert(key, cache_glyphs); +} + #[cfg(feature = "swash")] fn shape_skip( font_system: &mut FontSystem, diff --git a/src/shape_run_cache.rs b/src/shape_run_cache.rs new file mode 100644 index 0000000..226deee --- /dev/null +++ b/src/shape_run_cache.rs @@ -0,0 +1,49 @@ +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; +use core::ops::Range; + +use crate::{AttrsOwned, HashMap, ShapeGlyph}; + +/// Key for caching shape runs. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct ShapeRunKey { + pub text: String, + pub default_attrs: AttrsOwned, + pub attrs_spans: Vec<(Range, AttrsOwned)>, +} + +/// A helper structure for caching shape runs. +#[derive(Clone, Default)] +pub struct ShapeRunCache { + age: u64, + cache: HashMap)>, +} + +impl ShapeRunCache { + /// Get cache item, updating age if found + pub fn get(&mut self, key: &ShapeRunKey) -> Option<&Vec> { + self.cache.get_mut(key).map(|(age, glyphs)| { + *age = self.age; + &*glyphs + }) + } + + /// Insert cache item with current age + pub fn insert(&mut self, key: ShapeRunKey, glyphs: Vec) { + self.cache.insert(key, (self.age, glyphs)); + } + + /// Remove anything in the cache with an age older than keep_ages + pub fn trim(&mut self, keep_ages: u64) { + self.cache + .retain(|_key, (age, _glyphs)| *age + keep_ages >= self.age); + // Increase age + self.age += 1; + } +} + +impl core::fmt::Debug for ShapeRunCache { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("ShapeRunCache").finish() + } +}