Cache loaded fonts and glyph outlines.

Approximately a 70% CPU time improvement on the NanoVG demo.
This commit is contained in:
Patrick Walton 2020-04-16 16:27:28 -07:00
parent 582f025c91
commit 0b43f629cd
6 changed files with 216 additions and 139 deletions

1
Cargo.lock generated
View File

@ -285,6 +285,7 @@ dependencies = [
"gl 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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_canvas 0.1.0",
"pathfinder_color 0.1.0", "pathfinder_color 0.1.0",
"pathfinder_content 0.1.0", "pathfinder_content 0.1.0",

View File

@ -94,16 +94,18 @@ impl Canvas {
self.scene 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")] #[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"))] #[cfg(not(feature = "pf-text"))]
let default_font_collection = Arc::new(FontCollection); let default_font_collection = Arc::new(FontCollection);
CanvasRenderingContext2D { CanvasRenderingContext2D {
canvas: self, canvas: self,
current_state: State::default(default_font_collection), current_state: State::default(default_font_collection),
saved_states: vec![], saved_states: vec![],
font_context, canvas_font_context,
} }
} }
@ -118,7 +120,7 @@ pub struct CanvasRenderingContext2D {
current_state: State, current_state: State,
saved_states: Vec<State>, saved_states: Vec<State>,
#[allow(dead_code)] #[allow(dead_code)]
font_context: CanvasFontContext, canvas_font_context: CanvasFontContext,
} }
impl CanvasRenderingContext2D { impl CanvasRenderingContext2D {
@ -134,11 +136,6 @@ impl CanvasRenderingContext2D {
self.canvas self.canvas
} }
#[inline]
pub fn font_context(&self) -> CanvasFontContext {
self.font_context.clone()
}
// Drawing rectangles // Drawing rectangles
#[inline] #[inline]

View File

@ -21,8 +21,10 @@ use pathfinder_geometry::transform2d::Transform2F;
use pathfinder_geometry::util; use pathfinder_geometry::util;
use pathfinder_geometry::vector::{Vector2F, vec2f}; use pathfinder_geometry::vector::{Vector2F, vec2f};
use pathfinder_renderer::paint::PaintId; use pathfinder_renderer::paint::PaintId;
use pathfinder_text::{SceneExt, TextRenderMode}; use pathfinder_text::{FontContext, FontRenderOptions, TextRenderMode};
use skribo::{FontCollection, FontFamily, FontRef, Layout, TextStyle}; use skribo::{FontCollection, FontFamily, FontRef, Layout, TextStyle};
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
impl CanvasRenderingContext2D { impl CanvasRenderingContext2D {
@ -51,14 +53,22 @@ impl CanvasRenderingContext2D {
let clip_path = self.current_state.clip_path; let clip_path = self.current_state.clip_path;
let blend_mode = self.current_state.global_composite_operation.to_blend_mode(); let blend_mode = self.current_state.global_composite_operation.to_blend_mode();
drop(self.canvas.scene.push_layout(&layout, // TODO(pcwalton): Report errors.
&TextStyle { size: self.current_state.font_size }, drop(self.canvas_font_context
&(transform * self.current_state.transform), .0
TextRenderMode::Fill, .borrow_mut()
HintingOptions::None, .font_context
clip_path, .push_layout(&mut self.canvas.scene,
blend_mode, &layout,
paint_id)); &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, fn fill_or_stroke_text(&mut self,
@ -75,14 +85,21 @@ impl CanvasRenderingContext2D {
let transform = self.current_state.transform * Transform2F::from_translation(position); let transform = self.current_state.transform * Transform2F::from_translation(position);
// TODO(pcwalton): Report errors. // TODO(pcwalton): Report errors.
drop(self.canvas.scene.push_layout(&layout, drop(self.canvas_font_context
&TextStyle { size: self.current_state.font_size }, .0
&transform, .borrow_mut()
render_mode, .font_context
HintingOptions::None, .push_layout(&mut self.canvas.scene,
clip_path, &layout,
blend_mode, &TextStyle { size: self.current_state.font_size },
paint_id)); &FontRenderOptions {
transform,
render_mode,
hinting_options: HintingOptions::None,
clip_path,
blend_mode,
paint_id,
}));
} }
fn layout_text(&self, string: &str) -> Layout { fn layout_text(&self, string: &str) -> Layout {
@ -100,7 +117,7 @@ impl CanvasRenderingContext2D {
#[inline] #[inline]
pub fn set_font<FC>(&mut self, font_collection: FC) where FC: IntoFontCollection { pub fn set_font<FC>(&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; self.current_state.font_collection = font_collection;
} }
@ -179,7 +196,10 @@ pub struct TextMetrics {
#[cfg(feature = "pf-text")] #[cfg(feature = "pf-text")]
#[derive(Clone)] #[derive(Clone)]
pub struct CanvasFontContext { pub struct CanvasFontContext(pub(crate) Rc<RefCell<CanvasFontContextData>>);
pub(super) struct CanvasFontContextData {
pub(super) font_context: FontContext<Font>,
#[allow(dead_code)] #[allow(dead_code)]
pub(super) font_source: Arc<dyn Source>, pub(super) font_source: Arc<dyn Source>,
#[allow(dead_code)] #[allow(dead_code)]
@ -196,10 +216,11 @@ impl CanvasFontContext {
} }
} }
CanvasFontContext { CanvasFontContext(Rc::new(RefCell::new(CanvasFontContextData {
font_source, font_source,
default_font_collection: Arc::new(default_font_collection), default_font_collection: Arc::new(default_font_collection),
} font_context: FontContext::new(),
})))
} }
/// A convenience method to create a font context with the system source. /// A convenience method to create a font context with the system source.
@ -212,6 +233,18 @@ impl CanvasFontContext {
pub fn from_fonts<I>(fonts: I) -> CanvasFontContext where I: Iterator<Item = Handle> { pub fn from_fonts<I>(fonts: I) -> CanvasFontContext where I: Iterator<Item = Handle> {
CanvasFontContext::new(Arc::new(MemSource::from_fonts(fonts).unwrap())) 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 // Text layout utilities
@ -413,6 +446,7 @@ impl IntoFontCollection for Vec<FontFamily> {
} }
} }
/*
impl IntoFontCollection for Handle { impl IntoFontCollection for Handle {
#[inline] #[inline]
fn into_font_collection(self, context: &CanvasFontContext) -> Arc<FontCollection> { fn into_font_collection(self, context: &CanvasFontContext) -> Arc<FontCollection> {
@ -422,15 +456,18 @@ impl IntoFontCollection for Handle {
impl<'a> IntoFontCollection for &'a [Handle] { impl<'a> IntoFontCollection for &'a [Handle] {
#[inline] #[inline]
fn into_font_collection(self, _: &CanvasFontContext) -> Arc<FontCollection> { fn into_font_collection(self, context: &CanvasFontContext) -> Arc<FontCollection> {
let mut font_collection = FontCollection::new(); let mut font_collection = FontCollection::new();
for handle in self { for handle in self {
let postscript_name = handle.postscript_name();
let font = handle.load().expect("Failed to load the font!"); let font = handle.load().expect("Failed to load the font!");
font_collection.add_family(FontFamily::new_from_font(font)); font_collection.add_family(FontFamily::new_from_font(font));
} }
Arc::new(font_collection) Arc::new(font_collection)
} }
} }
*/
impl IntoFontCollection for Font { impl IntoFontCollection for Font {
#[inline] #[inline]
@ -453,10 +490,7 @@ impl<'a> IntoFontCollection for &'a [Font] {
impl<'a> IntoFontCollection for &'a str { impl<'a> IntoFontCollection for &'a str {
#[inline] #[inline]
fn into_font_collection(self, context: &CanvasFontContext) -> Arc<FontCollection> { fn into_font_collection(self, context: &CanvasFontContext) -> Arc<FontCollection> {
context.font_source context.get_font_by_postscript_name(self).into_font_collection(context)
.select_by_postscript_name(self)
.expect("Couldn't find a font with that PostScript name!")
.into_font_collection(context)
} }
} }
@ -465,11 +499,7 @@ impl<'a, 'b> IntoFontCollection for &'a [&'b str] {
fn into_font_collection(self, context: &CanvasFontContext) -> Arc<FontCollection> { fn into_font_collection(self, context: &CanvasFontContext) -> Arc<FontCollection> {
let mut font_collection = FontCollection::new(); let mut font_collection = FontCollection::new();
for postscript_name in self { for postscript_name in self {
let font = context.font_source let font = context.get_font_by_postscript_name(postscript_name);
.select_by_postscript_name(postscript_name)
.expect("Failed to find a font with that PostScript name!")
.load()
.expect("Failed to load the font!");
font_collection.add_family(FontFamily::new_from_font(font)); font_collection.add_family(FontFamily::new_from_font(font));
} }
Arc::new(font_collection) Arc::new(font_collection)

View File

@ -16,6 +16,10 @@ version = "0.23"
default-features = false default-features = false
features = ["png"] features = ["png"]
[dependencies.log]
version = "0.4"
features = ["release_max_level_info"]
[dependencies.pathfinder_canvas] [dependencies.pathfinder_canvas]
path = "../../canvas" path = "../../canvas"
features = ["pf-text"] features = ["pf-text"]

View File

@ -1544,7 +1544,8 @@ fn main() {
gpu_graph.render(&mut context, vec2f(415.0, 5.0)); gpu_graph.render(&mut context, vec2f(415.0, 5.0));
// Render the canvas to screen. // 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()); scene.build_and_render(&mut renderer, BuildOptions::default());
window.gl_swap_window(); window.gl_swap_window();

View File

@ -11,138 +11,182 @@
use font_kit::error::GlyphLoadingError; use font_kit::error::GlyphLoadingError;
use font_kit::hinting::HintingOptions; use font_kit::hinting::HintingOptions;
use font_kit::loader::Loader; use font_kit::loader::Loader;
use font_kit::loaders::default::Font as DefaultLoader;
use font_kit::outline::OutlineSink; use font_kit::outline::OutlineSink;
use pathfinder_content::effects::BlendMode; use pathfinder_content::effects::BlendMode;
use pathfinder_content::outline::{Contour, Outline}; use pathfinder_content::outline::{Contour, Outline};
use pathfinder_content::stroke::{OutlineStrokeToFill, StrokeStyle}; use pathfinder_content::stroke::{OutlineStrokeToFill, StrokeStyle};
use pathfinder_geometry::line_segment::LineSegment2F; use pathfinder_geometry::line_segment::LineSegment2F;
use pathfinder_geometry::transform2d::Transform2F; use pathfinder_geometry::transform2d::Transform2F;
use pathfinder_geometry::vector::Vector2F; use pathfinder_geometry::vector::{Vector2F, vec2f};
use pathfinder_renderer::paint::PaintId; use pathfinder_renderer::paint::PaintId;
use pathfinder_renderer::scene::{ClipPathId, DrawPath, Scene}; use pathfinder_renderer::scene::{ClipPathId, DrawPath, Scene};
use skribo::{FontCollection, Layout, TextStyle}; use skribo::{FontCollection, Layout, TextStyle};
use std::collections::HashMap;
use std::mem; use std::mem;
// FIXME(pcwalton): Too many parameters! #[derive(Clone)]
pub trait SceneExt { pub struct FontContext<F> where F: Loader {
// TODO(pcwalton): Support stroked glyphs. font_info: HashMap<String, FontInfo<F>>,
fn push_glyph<F>(&mut self,
font: &F,
glyph_id: u32,
transform: &Transform2F,
render_mode: TextRenderMode,
hinting_options: HintingOptions,
clip_path: Option<ClipPathId>,
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<ClipPathId>,
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<ClipPathId>,
blend_mode: BlendMode,
paint_id: PaintId)
-> Result<(), GlyphLoadingError>;
} }
impl SceneExt for Scene { #[derive(Clone)]
#[inline] struct FontInfo<F> where F: Loader {
fn push_glyph<F>(&mut self, font: F,
font: &F, outline_cache: HashMap<GlyphId, Outline>,
glyph_id: u32, }
transform: &Transform2F,
render_mode: TextRenderMode,
hinting_options: HintingOptions,
clip_path: Option<ClipPathId>,
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();
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<ClipPathId>,
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<F> FontContext<F> where F: Loader {
#[inline]
pub fn new() -> FontContext<F> {
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); let mut stroke_to_fill = OutlineStrokeToFill::new(&outline, stroke_style);
stroke_to_fill.offset(); stroke_to_fill.offset();
outline = stroke_to_fill.into_outline(); outline = stroke_to_fill.into_outline();
} }
let mut path = DrawPath::new(outline, paint_id); let mut path = DrawPath::new(outline, render_options.paint_id);
path.set_clip_path(clip_path); path.set_clip_path(render_options.clip_path);
path.set_blend_mode(blend_mode); path.set_blend_mode(render_options.blend_mode);
self.push_path(path); scene.push_path(path);
Ok(()) Ok(())
} }
fn push_layout(&mut self, /// Attempts to look up a font in the font cache.
layout: &Layout, #[inline]
style: &TextStyle, pub fn get_cached_font(&self, postscript_name: &str) -> Option<&F> {
transform: &Transform2F, self.font_info.get(postscript_name).map(|font_info| &font_info.font)
render_mode: TextRenderMode, }
hinting_options: HintingOptions, }
clip_path: Option<ClipPathId>,
blend_mode: BlendMode, impl FontContext<DefaultLoader> {
paint_id: PaintId) pub fn push_layout(&mut self,
-> Result<(), GlyphLoadingError> { scene: &mut Scene,
layout: &Layout,
style: &TextStyle,
render_options: &FontRenderOptions)
-> Result<(), GlyphLoadingError> {
for glyph in &layout.glyphs { for glyph in &layout.glyphs {
let offset = glyph.offset; let offset = glyph.offset;
let font = &*glyph.font.font; let font = &*glyph.font.font;
// FIXME(pcwalton): Cache this! // FIXME(pcwalton): Cache this!
let scale = style.size / (font.metrics().units_per_em as f32); let scale = style.size / (font.metrics().units_per_em as f32);
let scale = Vector2F::new(scale, -scale); let scale = vec2f(scale, -scale);
let transform = *transform * Transform2F::from_scale(scale).translate(offset); let render_options = FontRenderOptions {
self.push_glyph(font, transform: render_options.transform *
glyph.glyph_id, Transform2F::from_scale(scale).translate(offset),
&transform, ..*render_options
render_mode, };
hinting_options, self.push_glyph(scene, font, GlyphId(glyph.glyph_id), &render_options)?;
clip_path,
blend_mode,
paint_id)?;
} }
Ok(()) Ok(())
} }
#[inline] #[inline]
fn push_text(&mut self, pub fn push_text(&mut self,
text: &str, scene: &mut Scene,
style: &TextStyle, text: &str,
collection: &FontCollection, style: &TextStyle,
transform: &Transform2F, collection: &FontCollection,
render_mode: TextRenderMode, render_options: &FontRenderOptions)
hinting_options: HintingOptions, -> Result<(), GlyphLoadingError> {
clip_path: Option<ClipPathId>,
blend_mode: BlendMode,
paint_id: PaintId)
-> Result<(), GlyphLoadingError> {
let layout = skribo::layout(style, collection, text); let layout = skribo::layout(style, collection, text);
self.push_layout(&layout, self.push_layout(scene, &layout, style, render_options)
style, }
&transform, }
render_mode,
hinting_options, impl<F> FontInfo<F> where F: Loader {
clip_path, fn new(font: F) -> FontInfo<F> {
blend_mode, FontInfo { font, outline_cache: HashMap::new() }
paint_id)
} }
} }