From 0b43f629cdbe20d5f995af0307152c9b39a967c3 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Thu, 16 Apr 2020 16:27:28 -0700 Subject: [PATCH] Cache loaded fonts and glyph outlines. Approximately a 70% CPU time improvement on the NanoVG demo. --- Cargo.lock | 1 + canvas/src/lib.rs | 15 +- canvas/src/text.rs | 92 +++++++---- examples/canvas_nanovg/Cargo.toml | 4 + examples/canvas_nanovg/src/main.rs | 3 +- text/src/lib.rs | 240 +++++++++++++++++------------ 6 files changed, 216 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8804df4f..90601443 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -285,6 +285,7 @@ dependencies = [ "gl 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)", "image 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)", "jemallocator 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "pathfinder_canvas 0.1.0", "pathfinder_color 0.1.0", "pathfinder_content 0.1.0", diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index dbe02544..4de6a028 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -94,16 +94,18 @@ impl Canvas { self.scene } - pub fn get_context_2d(self, font_context: CanvasFontContext) -> CanvasRenderingContext2D { + pub fn get_context_2d(self, canvas_font_context: CanvasFontContext) + -> CanvasRenderingContext2D { #[cfg(feature = "pf-text")] - let default_font_collection = font_context.default_font_collection.clone(); + let default_font_collection = + canvas_font_context.0.borrow().default_font_collection.clone(); #[cfg(not(feature = "pf-text"))] let default_font_collection = Arc::new(FontCollection); CanvasRenderingContext2D { canvas: self, current_state: State::default(default_font_collection), saved_states: vec![], - font_context, + canvas_font_context, } } @@ -118,7 +120,7 @@ pub struct CanvasRenderingContext2D { current_state: State, saved_states: Vec, #[allow(dead_code)] - font_context: CanvasFontContext, + canvas_font_context: CanvasFontContext, } impl CanvasRenderingContext2D { @@ -134,11 +136,6 @@ impl CanvasRenderingContext2D { self.canvas } - #[inline] - pub fn font_context(&self) -> CanvasFontContext { - self.font_context.clone() - } - // Drawing rectangles #[inline] diff --git a/canvas/src/text.rs b/canvas/src/text.rs index be718893..44af8a19 100644 --- a/canvas/src/text.rs +++ b/canvas/src/text.rs @@ -21,8 +21,10 @@ use pathfinder_geometry::transform2d::Transform2F; use pathfinder_geometry::util; use pathfinder_geometry::vector::{Vector2F, vec2f}; use pathfinder_renderer::paint::PaintId; -use pathfinder_text::{SceneExt, TextRenderMode}; +use pathfinder_text::{FontContext, FontRenderOptions, TextRenderMode}; use skribo::{FontCollection, FontFamily, FontRef, Layout, TextStyle}; +use std::cell::RefCell; +use std::rc::Rc; use std::sync::Arc; impl CanvasRenderingContext2D { @@ -51,14 +53,22 @@ impl CanvasRenderingContext2D { let clip_path = self.current_state.clip_path; let blend_mode = self.current_state.global_composite_operation.to_blend_mode(); - drop(self.canvas.scene.push_layout(&layout, - &TextStyle { size: self.current_state.font_size }, - &(transform * self.current_state.transform), - TextRenderMode::Fill, - HintingOptions::None, - clip_path, - blend_mode, - paint_id)); + // TODO(pcwalton): Report errors. + drop(self.canvas_font_context + .0 + .borrow_mut() + .font_context + .push_layout(&mut self.canvas.scene, + &layout, + &TextStyle { size: self.current_state.font_size }, + &FontRenderOptions { + transform: transform * self.current_state.transform, + render_mode: TextRenderMode::Fill, + hinting_options: HintingOptions::None, + clip_path, + blend_mode, + paint_id, + })); } fn fill_or_stroke_text(&mut self, @@ -75,14 +85,21 @@ impl CanvasRenderingContext2D { let transform = self.current_state.transform * Transform2F::from_translation(position); // TODO(pcwalton): Report errors. - drop(self.canvas.scene.push_layout(&layout, - &TextStyle { size: self.current_state.font_size }, - &transform, - render_mode, - HintingOptions::None, - clip_path, - blend_mode, - paint_id)); + drop(self.canvas_font_context + .0 + .borrow_mut() + .font_context + .push_layout(&mut self.canvas.scene, + &layout, + &TextStyle { size: self.current_state.font_size }, + &FontRenderOptions { + transform, + render_mode, + hinting_options: HintingOptions::None, + clip_path, + blend_mode, + paint_id, + })); } fn layout_text(&self, string: &str) -> Layout { @@ -100,7 +117,7 @@ impl CanvasRenderingContext2D { #[inline] pub fn set_font(&mut self, font_collection: FC) where FC: IntoFontCollection { - let font_collection = font_collection.into_font_collection(&self.font_context); + let font_collection = font_collection.into_font_collection(&self.canvas_font_context); self.current_state.font_collection = font_collection; } @@ -179,7 +196,10 @@ pub struct TextMetrics { #[cfg(feature = "pf-text")] #[derive(Clone)] -pub struct CanvasFontContext { +pub struct CanvasFontContext(pub(crate) Rc>); + +pub(super) struct CanvasFontContextData { + pub(super) font_context: FontContext, #[allow(dead_code)] pub(super) font_source: Arc, #[allow(dead_code)] @@ -196,10 +216,11 @@ impl CanvasFontContext { } } - CanvasFontContext { + CanvasFontContext(Rc::new(RefCell::new(CanvasFontContextData { font_source, default_font_collection: Arc::new(default_font_collection), - } + font_context: FontContext::new(), + }))) } /// A convenience method to create a font context with the system source. @@ -212,6 +233,18 @@ impl CanvasFontContext { pub fn from_fonts(fonts: I) -> CanvasFontContext where I: Iterator { CanvasFontContext::new(Arc::new(MemSource::from_fonts(fonts).unwrap())) } + + fn get_font_by_postscript_name(&self, postscript_name: &str) -> Font { + let this = self.0.borrow(); + if let Some(cached_font) = this.font_context.get_cached_font(postscript_name) { + return (*cached_font).clone(); + } + this.font_source + .select_by_postscript_name(postscript_name) + .expect("Couldn't find a font with that PostScript name!") + .load() + .expect("Failed to load the font!") + } } // Text layout utilities @@ -413,6 +446,7 @@ impl IntoFontCollection for Vec { } } +/* impl IntoFontCollection for Handle { #[inline] fn into_font_collection(self, context: &CanvasFontContext) -> Arc { @@ -422,15 +456,18 @@ impl IntoFontCollection for Handle { impl<'a> IntoFontCollection for &'a [Handle] { #[inline] - fn into_font_collection(self, _: &CanvasFontContext) -> Arc { + fn into_font_collection(self, context: &CanvasFontContext) -> Arc { let mut font_collection = FontCollection::new(); for handle in self { + let postscript_name = handle.postscript_name(); + let font = handle.load().expect("Failed to load the font!"); font_collection.add_family(FontFamily::new_from_font(font)); } Arc::new(font_collection) } } +*/ impl IntoFontCollection for Font { #[inline] @@ -453,10 +490,7 @@ impl<'a> IntoFontCollection for &'a [Font] { impl<'a> IntoFontCollection for &'a str { #[inline] fn into_font_collection(self, context: &CanvasFontContext) -> Arc { - context.font_source - .select_by_postscript_name(self) - .expect("Couldn't find a font with that PostScript name!") - .into_font_collection(context) + context.get_font_by_postscript_name(self).into_font_collection(context) } } @@ -465,11 +499,7 @@ impl<'a, 'b> IntoFontCollection for &'a [&'b str] { fn into_font_collection(self, context: &CanvasFontContext) -> Arc { let mut font_collection = FontCollection::new(); for postscript_name in self { - let font = context.font_source - .select_by_postscript_name(postscript_name) - .expect("Failed to find a font with that PostScript name!") - .load() - .expect("Failed to load the font!"); + let font = context.get_font_by_postscript_name(postscript_name); font_collection.add_family(FontFamily::new_from_font(font)); } Arc::new(font_collection) diff --git a/examples/canvas_nanovg/Cargo.toml b/examples/canvas_nanovg/Cargo.toml index 4f07895e..fdafb3fc 100644 --- a/examples/canvas_nanovg/Cargo.toml +++ b/examples/canvas_nanovg/Cargo.toml @@ -16,6 +16,10 @@ version = "0.23" default-features = false features = ["png"] +[dependencies.log] +version = "0.4" +features = ["release_max_level_info"] + [dependencies.pathfinder_canvas] path = "../../canvas" features = ["pf-text"] diff --git a/examples/canvas_nanovg/src/main.rs b/examples/canvas_nanovg/src/main.rs index 7373bca3..f7c49a20 100644 --- a/examples/canvas_nanovg/src/main.rs +++ b/examples/canvas_nanovg/src/main.rs @@ -1544,7 +1544,8 @@ fn main() { gpu_graph.render(&mut context, vec2f(415.0, 5.0)); // Render the canvas to screen. - let scene = SceneProxy::from_scene(context.into_canvas().into_scene(), RayonExecutor); + let canvas = context.into_canvas(); + let scene = SceneProxy::from_scene(canvas.into_scene(), RayonExecutor); scene.build_and_render(&mut renderer, BuildOptions::default()); window.gl_swap_window(); diff --git a/text/src/lib.rs b/text/src/lib.rs index f8dfef4b..575e7f4c 100644 --- a/text/src/lib.rs +++ b/text/src/lib.rs @@ -11,138 +11,182 @@ use font_kit::error::GlyphLoadingError; use font_kit::hinting::HintingOptions; use font_kit::loader::Loader; +use font_kit::loaders::default::Font as DefaultLoader; use font_kit::outline::OutlineSink; use pathfinder_content::effects::BlendMode; use pathfinder_content::outline::{Contour, Outline}; use pathfinder_content::stroke::{OutlineStrokeToFill, StrokeStyle}; use pathfinder_geometry::line_segment::LineSegment2F; use pathfinder_geometry::transform2d::Transform2F; -use pathfinder_geometry::vector::Vector2F; +use pathfinder_geometry::vector::{Vector2F, vec2f}; use pathfinder_renderer::paint::PaintId; use pathfinder_renderer::scene::{ClipPathId, DrawPath, Scene}; use skribo::{FontCollection, Layout, TextStyle}; +use std::collections::HashMap; use std::mem; -// FIXME(pcwalton): Too many parameters! -pub trait SceneExt { - // TODO(pcwalton): Support stroked glyphs. - fn push_glyph(&mut self, - font: &F, - glyph_id: u32, - transform: &Transform2F, - render_mode: TextRenderMode, - hinting_options: HintingOptions, - clip_path: Option, - blend_mode: BlendMode, - paint_id: PaintId) - -> Result<(), GlyphLoadingError> - where F: Loader; - - fn push_layout(&mut self, - layout: &Layout, - style: &TextStyle, - transform: &Transform2F, - render_mode: TextRenderMode, - hinting_options: HintingOptions, - clip_path: Option, - blend_mode: BlendMode, - paint_id: PaintId) - -> Result<(), GlyphLoadingError>; - - fn push_text(&mut self, - text: &str, - style: &TextStyle, - collection: &FontCollection, - transform: &Transform2F, - render_mode: TextRenderMode, - hinting_options: HintingOptions, - clip_path: Option, - blend_mode: BlendMode, - paint_id: PaintId) - -> Result<(), GlyphLoadingError>; +#[derive(Clone)] +pub struct FontContext where F: Loader { + font_info: HashMap>, } -impl SceneExt for Scene { - #[inline] - fn push_glyph(&mut self, - font: &F, - glyph_id: u32, - transform: &Transform2F, - render_mode: TextRenderMode, - hinting_options: HintingOptions, - clip_path: Option, - blend_mode: BlendMode, - paint_id: PaintId) - -> Result<(), GlyphLoadingError> - where F: Loader { - let mut outline_builder = OutlinePathBuilder::new(transform); - font.outline(glyph_id, hinting_options, &mut outline_builder)?; - let mut outline = outline_builder.build(); +#[derive(Clone)] +struct FontInfo where F: Loader { + font: F, + outline_cache: HashMap, +} - if let TextRenderMode::Stroke(stroke_style) = render_mode { +#[derive(Clone, Copy)] +pub struct FontRenderOptions { + pub transform: Transform2F, + pub render_mode: TextRenderMode, + pub hinting_options: HintingOptions, + pub clip_path: Option, + pub blend_mode: BlendMode, + pub paint_id: PaintId, +} + +impl Default for FontRenderOptions { + #[inline] + fn default() -> FontRenderOptions { + FontRenderOptions { + transform: Transform2F::default(), + render_mode: TextRenderMode::Fill, + hinting_options: HintingOptions::None, + clip_path: None, + blend_mode: BlendMode::SrcOver, + paint_id: PaintId(0), + } + } +} + +#[derive(Clone, Copy, PartialEq, Debug, Eq, Hash)] +pub struct GlyphId(pub u32); + +impl FontContext where F: Loader { + #[inline] + pub fn new() -> FontContext { + FontContext { font_info: HashMap::new() } + } + + pub fn push_glyph(&mut self, + scene: &mut Scene, + font: &F, + glyph_id: GlyphId, + render_options: &FontRenderOptions) + -> Result<(), GlyphLoadingError> { + let font_key = font.postscript_name(); + let metrics = font.metrics(); + + // Insert the font into the cache if needed. + if let Some(ref font_key) = font_key { + if !self.font_info.contains_key(&*font_key) { + self.font_info.insert((*font_key).clone(), FontInfo::new((*font).clone())); + } + } + + // See if we have a cached outline. + // + // TODO(pcwalton): Cache hinted outlines too. + let mut cached_outline = None; + let can_cache_outline = font_key.is_some() && + render_options.hinting_options == HintingOptions::None; + if can_cache_outline { + if let Some(ref font_info) = self.font_info.get(&*font_key.as_ref().unwrap()) { + if let Some(ref outline) = font_info.outline_cache.get(&glyph_id) { + cached_outline = Some((*outline).clone()); + } + } + } + + let mut outline = match cached_outline { + Some(mut cached_outline) => { + let scale = 1.0 / metrics.units_per_em as f32; + cached_outline.transform(&(render_options.transform * + Transform2F::from_scale(scale))); + cached_outline + } + None => { + let transform = if can_cache_outline { + Transform2F::from_scale(metrics.units_per_em as f32) + } else { + render_options.transform + }; + let mut outline_builder = OutlinePathBuilder::new(&transform); + font.outline(glyph_id.0, render_options.hinting_options, &mut outline_builder)?; + let mut outline = outline_builder.build(); + if can_cache_outline { + let font_key = font_key.as_ref().unwrap(); + let font_info = self.font_info.get_mut(&*font_key).unwrap(); + font_info.outline_cache.insert(glyph_id, outline.clone()); + let scale = 1.0 / metrics.units_per_em as f32; + outline.transform(&(render_options.transform * + Transform2F::from_scale(scale))); + } + outline + } + }; + + if let TextRenderMode::Stroke(stroke_style) = render_options.render_mode { let mut stroke_to_fill = OutlineStrokeToFill::new(&outline, stroke_style); stroke_to_fill.offset(); outline = stroke_to_fill.into_outline(); } - let mut path = DrawPath::new(outline, paint_id); - path.set_clip_path(clip_path); - path.set_blend_mode(blend_mode); + let mut path = DrawPath::new(outline, render_options.paint_id); + path.set_clip_path(render_options.clip_path); + path.set_blend_mode(render_options.blend_mode); - self.push_path(path); + scene.push_path(path); Ok(()) } - fn push_layout(&mut self, - layout: &Layout, - style: &TextStyle, - transform: &Transform2F, - render_mode: TextRenderMode, - hinting_options: HintingOptions, - clip_path: Option, - blend_mode: BlendMode, - paint_id: PaintId) - -> Result<(), GlyphLoadingError> { + /// Attempts to look up a font in the font cache. + #[inline] + pub fn get_cached_font(&self, postscript_name: &str) -> Option<&F> { + self.font_info.get(postscript_name).map(|font_info| &font_info.font) + } +} + +impl FontContext { + pub fn push_layout(&mut self, + scene: &mut Scene, + layout: &Layout, + style: &TextStyle, + render_options: &FontRenderOptions) + -> Result<(), GlyphLoadingError> { for glyph in &layout.glyphs { let offset = glyph.offset; let font = &*glyph.font.font; // FIXME(pcwalton): Cache this! let scale = style.size / (font.metrics().units_per_em as f32); - let scale = Vector2F::new(scale, -scale); - let transform = *transform * Transform2F::from_scale(scale).translate(offset); - self.push_glyph(font, - glyph.glyph_id, - &transform, - render_mode, - hinting_options, - clip_path, - blend_mode, - paint_id)?; + let scale = vec2f(scale, -scale); + let render_options = FontRenderOptions { + transform: render_options.transform * + Transform2F::from_scale(scale).translate(offset), + ..*render_options + }; + self.push_glyph(scene, font, GlyphId(glyph.glyph_id), &render_options)?; } Ok(()) } #[inline] - fn push_text(&mut self, - text: &str, - style: &TextStyle, - collection: &FontCollection, - transform: &Transform2F, - render_mode: TextRenderMode, - hinting_options: HintingOptions, - clip_path: Option, - blend_mode: BlendMode, - paint_id: PaintId) - -> Result<(), GlyphLoadingError> { + pub fn push_text(&mut self, + scene: &mut Scene, + text: &str, + style: &TextStyle, + collection: &FontCollection, + render_options: &FontRenderOptions) + -> Result<(), GlyphLoadingError> { let layout = skribo::layout(style, collection, text); - self.push_layout(&layout, - style, - &transform, - render_mode, - hinting_options, - clip_path, - blend_mode, - paint_id) + self.push_layout(scene, &layout, style, render_options) + } +} + +impl FontInfo where F: Loader { + fn new(font: F) -> FontInfo { + FontInfo { font, outline_cache: HashMap::new() } } }