diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index f61972434..a58179866 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -16,13 +16,13 @@ pub mod shape; use std::collections::HashMap; use std::ops::Range; -use std::sync::{Arc, Mutex}; +use std::sync::Mutex; use designcraft_doc::{ Align, Composer, Document, FirstBaseline, GridAlign, ItemId, ParaProps, SpanColumns, StartParagraph, Story, StoryId, TabAlign, TextFrameOptions, VerticalJustification, WrapMode, story, }; -use designcraft_fonts::{FontDb, FontFace}; +use designcraft_fonts::FontDb; use designcraft_geom::{Point, Rect}; use crate::breaker::{Break, Spacing}; @@ -46,7 +46,7 @@ pub struct RunStyle { /// A positioned glyph. `x` is absolute in frame inner space; `y` is relative to the line baseline. #[derive(Clone, Debug)] pub struct PlacedGlyph { - pub face: Arc, + pub face: designcraft_fonts::FaceRef, pub gid: u32, pub x: f64, pub y: f64, diff --git a/crates/compose/src/shape.rs b/crates/compose/src/shape.rs index a935f52c5..0c9c8257b 100644 --- a/crates/compose/src/shape.rs +++ b/crates/compose/src/shape.rs @@ -1,17 +1,17 @@ //! Paragraph → styled, positioned glyphs (before line breaking). use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, RwLock}; use designcraft_doc::{Capitalization, CharProps, Kerning, Leading, Position, Story, Styles, story}; -use designcraft_fonts::{Feature, FontDb, FontFace, ShapedGlyph, feature}; +use designcraft_fonts::{FaceRef, Feature, FontDb, FontFace, ShapedGlyph, feature}; use crate::RunStyle; /// One glyph with every character attribute applied, in points. #[derive(Clone, Debug)] pub struct Glyph { - pub face: Arc, + pub face: FaceRef, pub gid: u32, /// Story byte offset of the cluster and its byte length (0 for generated glyphs). pub byte: usize, @@ -229,11 +229,29 @@ fn shape_run( } type WordMap = HashMap, Arc<[ShapedGlyph]>>; +type WordKey = (u32, bool, String); -/// Shaped words (font units) by (face, caps, features), then text. -static WORD_CACHE: Mutex> = Mutex::new(Vec::new()); +/// Shaped words (font units) by (face, caps, features), then text. Lookups take read locks and +/// misses are shaped outside any lock, so parallel composition doesn't serialize here. +static WORD_CACHE: RwLock>)>> = RwLock::new(Vec::new()); const WORD_CACHE_MAX: usize = 50_000; +fn word_map(key: WordKey) -> Arc> { + if let Some((_, m)) = WORD_CACHE.read().unwrap_or_else(|e| e.into_inner()).iter().find(|e| e.0 == key) { + return m.clone(); + } + let mut w = WORD_CACHE.write().unwrap_or_else(|e| e.into_inner()); + if let Some((_, m)) = w.iter().find(|e| e.0 == key) { + return m.clone(); + } + if w.len() > 64 { + w.clear(); + } + let m = Arc::new(RwLock::new(HashMap::new())); + w.push((key, m.clone())); + m +} + /// Shape `src` word by word through a cache (like a browser's word cache): the text is split /// after each U+0020 so repeated words are shaped once. Shaping does not cross word spaces in the /// scripts we lay out, and clusters stay byte offsets into `src`. @@ -242,34 +260,31 @@ fn shape_cached(face: &FontFace, src: &str, feats: &[Feature], caps: bool) -> Ve if src.len() < 2 || !src.contains(' ') { return designcraft_fonts::shape(face, src, feats, map); } - let fkey = format!("{feats:?}"); - let mut out = Vec::with_capacity(src.len()); - let mut guard = WORD_CACHE.lock().unwrap_or_else(|e| e.into_inner()); - let key = (face.id(), caps, fkey); - let idx = match guard.iter().position(|e| e.0 == key) { - Some(i) => i, - None => { - if guard.len() > 64 { - guard.clear(); - } - guard.push((key, HashMap::new())); - guard.len() - 1 - } + let words = word_map((face.id(), caps, format!("{feats:?}"))); + let pieces: Vec<&str> = src.split_inclusive(' ').collect(); + let mut found: Vec>> = { + let r = words.read().unwrap_or_else(|e| e.into_inner()); + pieces.iter().map(|p| r.get(*p).cloned()).collect() }; - let cache = &mut guard[idx].1; - if cache.len() > WORD_CACHE_MAX { - cache.clear(); + let mut fresh: Vec<(Box, Arc<[ShapedGlyph]>)> = Vec::new(); + for (k, f) in found.iter_mut().enumerate() { + if f.is_none() { + let g: Arc<[ShapedGlyph]> = designcraft_fonts::shape(face, pieces[k], feats, map).into(); + fresh.push((pieces[k].into(), g.clone())); + *f = Some(g); + } } + if !fresh.is_empty() { + let mut w = words.write().unwrap_or_else(|e| e.into_inner()); + if w.len() + fresh.len() > WORD_CACHE_MAX { + w.clear(); + } + w.extend(fresh); + } + let mut out = Vec::with_capacity(src.len()); let mut start = 0; - for piece in src.split_inclusive(' ') { - let glyphs = match cache.get(piece) { - Some(g) => g.clone(), - None => { - let g: Arc<[ShapedGlyph]> = designcraft_fonts::shape(face, piece, feats, map).into(); - cache.insert(piece.into(), g.clone()); - g - } - }; + for (piece, glyphs) in pieces.iter().zip(found) { + let glyphs = glyphs.unwrap_or_else(|| Arc::from(Vec::new())); out.extend(glyphs.iter().map(|g| ShapedGlyph { cluster: g.cluster + start, ..*g })); start += piece.len(); } @@ -299,7 +314,7 @@ fn effective_size(p: &CharProps) -> (f64, f64) { fn control_glyph(face: &Arc, p: &CharProps, auto_leading: f64, style: u32, byte: usize, ch: char) -> Glyph { let (size, k, ascent, descent, leading, cap, xh, shift) = metrics(face, p, auto_leading); Glyph { - face: face.clone(), + face: FaceRef::of(face), gid: face.glyph_for(' '), byte, len: ch.len_utf8(), @@ -345,6 +360,7 @@ fn shape_segment( let space = face.advance(face.glyph_for(' ')) * k * hs; let shaped: Vec = shape_cached(face, src, &feats, caps); let n = shaped.len(); + let fref = FaceRef::of(face); for (gi, sg) in shaped.iter().enumerate() { let (byte, len, ch) = if replacement.is_some() { (range.start, if gi == 0 { range.len() } else { 0 }, text[range.start..].chars().next().unwrap_or(' ')) @@ -363,7 +379,7 @@ fn shape_segment( // Only the first glyph of a cluster owns the bytes (so ranges partition the text). let first_in_cluster = gi == 0 || shaped[gi - 1].cluster != sg.cluster; out.push(Glyph { - face: face.clone(), + face: fref, gid: sg.gid, byte, len: if first_in_cluster || replacement.is_some() { len } else { 0 }, diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index 5cade71bb..951a1519d 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -57,6 +57,57 @@ pub struct FontFace { pub cap_height: f64, pub x_height: f64, pub shaper: harfrust::ShaperData, + /// Basic Multilingual Plane coverage bitset, built on first use. + bmp: std::sync::OnceLock>, +} + +/// A cheap `Copy` handle to a face. Faces are never unloaded, so the handle lives for the rest of +/// the process; unlike cloning an `Arc`, copying it touches no shared reference count (which made +/// parallel composition scale negatively). +#[derive(Clone, Copy)] +pub struct FaceRef(&'static FontFace); + +impl FaceRef { + /// The handle for a loaded face (memoised per face). + pub fn of(face: &Arc) -> FaceRef { + static LEAKED: RwLock>> = RwLock::new(Vec::new()); + let id = face.id as usize; + if let Some(Some(f)) = LEAKED.read().unwrap_or_else(|e| e.into_inner()).get(id) { + return FaceRef(f); + } + let mut w = LEAKED.write().unwrap_or_else(|e| e.into_inner()); + if w.len() <= id { + w.resize(id + 1, None); + } + let f: &'static FontFace = w[id].unwrap_or_else(|| { + let keep: &'static Arc = Box::leak(Box::new(face.clone())); + keep + }); + w[id] = Some(f); + FaceRef(f) + } + pub fn get(self) -> &'static FontFace { + self.0 + } +} + +impl std::ops::Deref for FaceRef { + type Target = FontFace; + fn deref(&self) -> &FontFace { + self.0 + } +} + +impl std::fmt::Debug for FaceRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl PartialEq for FaceRef { + fn eq(&self, o: &FaceRef) -> bool { + self.0.id == o.0.id + } } impl std::fmt::Debug for FontFace { @@ -88,6 +139,21 @@ impl FontFace { } /// Does the face map `c` to a glyph? pub fn covers(&self, c: char) -> bool { + let cp = c as u32; + if cp < 0x1_0000 { + let bits = self.bmp.get_or_init(|| { + let mut b = vec![0u64; 1024].into_boxed_slice(); + if let Some(f) = self.skrifa() { + for (cp, _) in f.charmap().mappings() { + if cp < 0x1_0000 { + b[(cp / 64) as usize] |= 1 << (cp % 64); + } + } + } + b + }); + return bits[(cp / 64) as usize] & (1 << (cp % 64)) != 0; + } self.skrifa().is_some_and(|f| f.charmap().map(c).is_some()) } /// Units per em. @@ -223,6 +289,7 @@ fn make_face(bytes: FontBytes, index: u32, family: String, style: String) -> Opt shaper, bytes, index, + bmp: std::sync::OnceLock::new(), }) } diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs index ee29ed215..70432dfeb 100644 --- a/crates/fonts/src/lib.rs +++ b/crates/fonts/src/lib.rs @@ -8,7 +8,7 @@ mod fontdb; -pub use fontdb::{FALLBACK_FAMILY, FontDb, FontFace}; +pub use fontdb::{FALLBACK_FAMILY, FaceRef, FontDb, FontFace}; pub use harfrust::Feature; use harfrust::{Direction, ShapeOptions, Tag, UnicodeBuffer}; pub use kurbo::BezPath;