mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-28 04:06:54 -06:00
M9: parallel composition scales (Copy FaceRef instead of per-glyph Arc clones; BMP coverage bitset; read-locked word cache)
Cold compose of 1M chars on 8+ threads: ~255 ms -> ~38 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
12e502c203
commit
8e4403af38
@@ -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<FontFace>,
|
||||
pub face: designcraft_fonts::FaceRef,
|
||||
pub gid: u32,
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
|
||||
+48
-32
@@ -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<FontFace>,
|
||||
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<Box<str>, Arc<[ShapedGlyph]>>;
|
||||
type WordKey = (u32, bool, String);
|
||||
|
||||
/// Shaped words (font units) by (face, caps, features), then text.
|
||||
static WORD_CACHE: Mutex<Vec<((u32, bool, String), WordMap)>> = 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<Vec<(WordKey, Arc<RwLock<WordMap>>)>> = RwLock::new(Vec::new());
|
||||
const WORD_CACHE_MAX: usize = 50_000;
|
||||
|
||||
fn word_map(key: WordKey) -> Arc<RwLock<WordMap>> {
|
||||
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<Option<Arc<[ShapedGlyph]>>> = {
|
||||
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<str>, 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<FontFace>, 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<ShapedGlyph> = 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 },
|
||||
|
||||
@@ -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<Box<[u64]>>,
|
||||
}
|
||||
|
||||
/// 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<FontFace>) -> FaceRef {
|
||||
static LEAKED: RwLock<Vec<Option<&'static FontFace>>> = 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<FontFace> = 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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user